Compare commits

..

33 Commits

Author SHA1 Message Date
Jamie V
fe1d8c0866 Merge branch 'master' into stackplots-destroy 2021-10-21 17:03:31 -07:00
Jamie Vigliotta
93f76ac0d7 removed unused eventHelper from stacked plots, now that we destroy stacked plot children, some test code could be cleaned up 2021-10-21 17:02:23 -07:00
Nikhil
4f8cba160d Router test fix (#3973)
* removed DEPRECATION warnings
* test fixes
* added spec reporter to debug failing tests  and fixed couple specs
* disabled failFast to see all failing tests
* disabled fail test and  change timeoutInterval to 5000
* removed unused debounce import

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-10-21 16:55:34 -07:00
Jamie Vigliotta
c74439a013 Merge branch 'stackplots-destroy' of https://github.com/nasa/openmct into stackplots-destroy
Merge'n master
2021-10-21 13:00:12 -07:00
Jamie Vigliotta
0aeab0361a added eventHelper.js back to stacked plot 2021-10-21 12:10:48 -07:00
Jamie V
21f447dd3d Merge branch 'master' into stackplots-destroy 2021-10-21 11:52:51 -07:00
Jamie Vigliotta
0ca3bde106 reverting tabs changes to be done in separate PR 2021-10-21 11:49:21 -07:00
Jamie Vigliotta
a8f8415dc7 removed eventHelper.js from stacked plots as it was not being used
added beforeDestroy hook to StackedPlotItem.vue to programmatically destroy programmatically created MCTPlot component instance
2021-10-21 11:46:23 -07:00
Charles Hacskaylo
c269e089da Condition Widgets multiple fixes (#4335) 2021-10-21 10:16:41 -07:00
John Hill
4873f40614 Add clarity to PR Template (#4336)
* Add clarity to PR Template

* one more time

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

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

Lint resolved

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

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

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

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

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

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

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

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

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

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

* Changed progress-bar v-if test
- Per PR change suggestion, replace `v-if=progressLoad.progressPerc !== 100`
to `v-if=loading`;
2021-09-17 15:53:00 -07:00
Charles Hacskaylo
b27317631b Add new glyphs for Bar Chart and Map views (#4219)
- New glyph and background classes for `icon-bar-chart` and
`bg-icon-bar-chart`;
- New glyph and background classes for `icon-map` and
`bg-icon-map`;
2021-09-17 13:11:09 -07:00
Shefali Joshi
953a9daafb Fixes resizing handler error - adds null check (#4218) 2021-09-16 16:10:48 -07:00
Khalid Adil
63f9cd449f Refactor Angular Clock and Clock Indicator as plugin (#4106)
* clock and clock indicator installed as a plugin
* { enableIndicator: true } to install indicator
* Vue for views
2021-09-15 11:01:40 -07:00
Jamie V
54220f547b WIP (#4213)
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-09-14 20:18:33 -04:00
Nikhil
93d967c2b3 Snapshot notice link not navigating as expected #4194 (#4199)
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2021-09-14 15:17:31 -07:00
Nikhil
1226459c6f View Large overlay doesn't allow taking Snapshots (#4091)
* Added NotebookMenuSwitcher in preview header
* Use preview component inside viewLargeAction
* Added autoHide flag to overlay API that allows developers to specify whether an overlay should remain visible if other overlays are subsequently triggered (eg. the snapshot overlay)
* When in edit mode, disable navigation link to notebook entry.

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-09-13 19:36:14 -05:00
Nikhil
d7c9c9cb98 Snapshot images should use the namespace of the notebook they are being saved to or LocalStorage (#4020)
* Snapshot images should use the namespace of the notebook they are being saved to, or LocalStorage #4007

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2021-09-13 13:44:38 -07:00
205 changed files with 9127 additions and 2906 deletions

View File

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

View File

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

View File

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

View File

@@ -41,6 +41,11 @@ define([
"$scope"
]
}
],
"routes": [
{
"templateUrl": "templates/exampleForm.html"
}
]
}
}

View File

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

View File

@@ -96,9 +96,5 @@ define([
return this.workerInterface.subscribe(workerRequest, callback);
};
GeneratorProvider.prototype.destroy = function () {
this.workerInterface.destroy();
};
return GeneratorProvider;
});

View File

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

View File

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

View File

@@ -40,11 +40,6 @@ define([
this.callbacks = {};
}
WorkerInterface.prototype.destroy = function () {
delete this.worker.onmessage;
this.worker.terminate();
};
WorkerInterface.prototype.onMessage = function (message) {
message = message.data;
var callback = this.callbacks[message.id];

View File

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

View File

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

View File

@@ -195,6 +195,7 @@
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'],
{indicator: true}
));
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
openmct.start();
</script>
</html>

View File

@@ -1,36 +1,3 @@
const jasmineIt = window.it;
const specIdsToExpectCount = new Map();
const failures = [];
window.it = function (name, specFunction) {
const expectRE = /expect\(/g;
const expectCount = (specFunction.toString().match(expectRE) || []).length;
const spec = jasmineIt(name, specFunction);
specIdsToExpectCount.set(spec.id, expectCount);
return spec;
};
const testsContext = require.context('.', true, /\/(src|platform)\/.*Spec.js$/);
jasmine.getEnv().addReporter({
specDone(spec) {
const totalExpectsRun = (spec.failedExpectations || []).length + (spec.passedExpectations || []).length;
const totalExpectsDefined = specIdsToExpectCount.get(spec.id);
if (totalExpectsRun < totalExpectsDefined) {
failures.push(`Executed ${totalExpectsRun} but ${totalExpectsDefined} were defined for spec ${spec.fullName}`);
}
},
jasmineDone() {
window.it = jasmineIt;
failures.forEach(failure => {
console.error(failure);
});
}
});
testsContext.keys().forEach(testsContext);

View File

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

View File

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

View File

@@ -164,6 +164,16 @@ define([
"license": "license-apache",
"link": "http://logging.apache.org/log4net/license.html"
}
],
"routes": [
{
"when": "/licenses",
"template": licensesTemplate
},
{
"when": "/licenses-md",
"template": licensesExportMdTemplate
}
]
}
}

View File

@@ -50,6 +50,8 @@ define([
name: "platform/commonUI/browse",
definition: {
"extensions": {
"routes": [
],
"constants": [
{
"key": "DEFAULT_PATH",

View File

@@ -39,6 +39,9 @@ define(
this.callbacks = [];
this.checks = [];
this.$window = $window;
this.oldUnload = $window.onbeforeunload;
$window.onbeforeunload = this.onBeforeUnload.bind(this);
}
/**

View File

@@ -50,8 +50,6 @@ define(
* 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);
@@ -82,8 +80,6 @@ define(
* @returns {*}
*/
EditorCapability.prototype.save = function () {
console.warn('DEPRECATED: cannot save via edit capability, use openmct.editor instead.');
return Promise.resolve();
};
@@ -95,8 +91,6 @@ define(
* @returns {*}
*/
EditorCapability.prototype.finish = function () {
console.warn('DEPRECATED: cannot finish via edit capability, use openmct.editor instead.');
return Promise.resolve();
};

View File

@@ -0,0 +1,249 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../../src/actions/SaveAsAction"],
function (SaveAsAction) {
xdescribe("The Save As action", function () {
var mockDomainObject,
mockClonedObject,
mockEditorCapability,
mockActionCapability,
mockObjectService,
mockDialogService,
mockCopyService,
mockNotificationService,
mockParent,
actionContext,
capabilities = {},
action;
function noop() {}
function mockPromise(value) {
return (value || {}).then ? value
: {
then: function (callback) {
return mockPromise(callback(value));
},
catch: function (callback) {
return mockPromise(callback(value));
}
};
}
beforeEach(function () {
mockDomainObject = jasmine.createSpyObj(
"domainObject",
[
"getCapability",
"hasCapability",
"getModel",
"getId"
]
);
mockDomainObject.hasCapability.and.returnValue(true);
mockDomainObject.getCapability.and.callFake(function (capability) {
return capabilities[capability];
});
mockDomainObject.getModel.and.returnValue({
location: 'a',
persisted: undefined
});
mockDomainObject.getId.and.returnValue(0);
mockClonedObject = jasmine.createSpyObj(
"clonedObject",
[
"getId"
]
);
mockClonedObject.getId.and.returnValue(1);
mockParent = jasmine.createSpyObj(
"parentObject",
[
"getCapability",
"hasCapability",
"getModel"
]
);
mockEditorCapability = jasmine.createSpyObj(
"editor",
["save", "finish", "isEditContextRoot"]
);
mockEditorCapability.save.and.returnValue(mockPromise(true));
mockEditorCapability.finish.and.returnValue(mockPromise(true));
mockEditorCapability.isEditContextRoot.and.returnValue(true);
capabilities.editor = mockEditorCapability;
mockActionCapability = jasmine.createSpyObj(
"action",
["perform"]
);
capabilities.action = mockActionCapability;
mockObjectService = jasmine.createSpyObj(
"objectService",
["getObjects"]
);
mockObjectService.getObjects.and.returnValue(mockPromise({'a': mockParent}));
mockDialogService = jasmine.createSpyObj(
"dialogService",
[
"getUserInput",
"showBlockingMessage"
]
);
mockDialogService.getUserInput.and.returnValue(mockPromise(undefined));
mockCopyService = jasmine.createSpyObj(
"copyService",
[
"perform"
]
);
mockCopyService.perform.and.returnValue(mockPromise(mockClonedObject));
mockNotificationService = jasmine.createSpyObj(
"notificationService",
[
"info",
"error"
]
);
actionContext = {
domainObject: mockDomainObject
};
action = new SaveAsAction(
undefined,
undefined,
mockDialogService,
mockCopyService,
mockNotificationService,
actionContext);
spyOn(action, "getObjectService");
action.getObjectService.and.returnValue(mockObjectService);
spyOn(action, "createWizard");
action.createWizard.and.returnValue({
getFormStructure: noop,
getInitialFormValue: noop,
populateObjectFromInput: function () {
return mockDomainObject;
}
});
});
it("only applies to domain object with an editor capability", function () {
expect(SaveAsAction.appliesTo(actionContext)).toBe(true);
expect(mockDomainObject.hasCapability).toHaveBeenCalledWith("editor");
mockDomainObject.hasCapability.and.returnValue(false);
mockDomainObject.getCapability.and.returnValue(undefined);
expect(SaveAsAction.appliesTo(actionContext)).toBe(false);
});
it("only applies to domain object that has not already been"
+ " persisted", function () {
expect(SaveAsAction.appliesTo(actionContext)).toBe(true);
expect(mockDomainObject.hasCapability).toHaveBeenCalledWith("editor");
mockDomainObject.getModel.and.returnValue({persisted: 0});
expect(SaveAsAction.appliesTo(actionContext)).toBe(false);
});
it("uses the editor capability to save the object", function () {
mockEditorCapability.save.and.returnValue(Promise.resolve());
return action.perform().then(function () {
expect(mockEditorCapability.save).toHaveBeenCalled();
});
});
it("uses the editor capability to finish editing the object", function () {
return action.perform().then(function () {
expect(mockEditorCapability.finish.calls.count()).toBeGreaterThan(0);
});
});
it("returns to browse after save", function () {
spyOn(action, "save");
action.save.and.returnValue(mockPromise(mockDomainObject));
action.perform();
expect(mockActionCapability.perform).toHaveBeenCalledWith(
"navigate"
);
});
it("prompts the user for object details", function () {
action.perform();
expect(mockDialogService.getUserInput).toHaveBeenCalled();
});
describe("in order to keep the user in the loop", function () {
var mockDialogHandle;
beforeEach(function () {
mockDialogHandle = jasmine.createSpyObj("dialogHandle", ["dismiss"]);
mockDialogService.showBlockingMessage.and.returnValue(mockDialogHandle);
});
it("shows a blocking dialog indicating that saving is in progress", function () {
mockEditorCapability.save.and.returnValue(new Promise(function () {}));
action.perform();
expect(mockDialogService.showBlockingMessage).toHaveBeenCalled();
expect(mockDialogHandle.dismiss).not.toHaveBeenCalled();
});
it("hides the blocking dialog after saving finishes", function () {
return action.perform().then(function () {
expect(mockDialogService.showBlockingMessage).toHaveBeenCalled();
expect(mockDialogHandle.dismiss).toHaveBeenCalled();
});
});
it("notifies if saving succeeded", function () {
return action.perform().then(function () {
expect(mockNotificationService.info).toHaveBeenCalled();
expect(mockNotificationService.error).not.toHaveBeenCalled();
});
});
it("notifies if saving failed", function () {
mockCopyService.perform.and.returnValue(Promise.reject("some failure reason"));
action.perform().then(function () {
expect(mockNotificationService.error).toHaveBeenCalled();
expect(mockNotificationService.info).not.toHaveBeenCalled();
});
});
});
});
}
);

View File

@@ -0,0 +1,192 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../../src/capabilities/EditorCapability"],
function (EditorCapability) {
xdescribe("The editor capability", function () {
var mockDomainObject,
capabilities,
mockParentObject,
mockTransactionService,
mockStatusCapability,
mockParentStatus,
mockContextCapability,
capability;
function fastPromise(val) {
return {
then: function (callback) {
return callback(val);
}
};
}
beforeEach(function () {
mockDomainObject = jasmine.createSpyObj(
"domainObject",
["getId", "getModel", "hasCapability", "getCapability", "useCapability"]
);
mockParentObject = jasmine.createSpyObj(
"domainObject",
["getId", "getModel", "hasCapability", "getCapability", "useCapability"]
);
mockTransactionService = jasmine.createSpyObj(
"transactionService",
[
"startTransaction",
"size",
"commit",
"cancel"
]
);
mockTransactionService.commit.and.returnValue(fastPromise());
mockTransactionService.cancel.and.returnValue(fastPromise());
mockTransactionService.isActive = jasmine.createSpy('isActive');
mockStatusCapability = jasmine.createSpyObj(
"statusCapability",
["get", "set"]
);
mockParentStatus = jasmine.createSpyObj(
"statusCapability",
["get", "set"]
);
mockContextCapability = jasmine.createSpyObj(
"contextCapability",
["getParent"]
);
mockContextCapability.getParent.and.returnValue(mockParentObject);
capabilities = {
context: mockContextCapability,
status: mockStatusCapability
};
mockDomainObject.hasCapability.and.callFake(function (name) {
return capabilities[name] !== undefined;
});
mockDomainObject.getCapability.and.callFake(function (name) {
return capabilities[name];
});
mockParentObject.getCapability.and.returnValue(mockParentStatus);
mockParentObject.hasCapability.and.returnValue(false);
capability = new EditorCapability(
mockTransactionService,
mockDomainObject
);
});
it("starts a transaction when edit is invoked", function () {
capability.edit();
expect(mockTransactionService.startTransaction).toHaveBeenCalled();
});
it("sets editing status on object", function () {
capability.edit();
expect(mockStatusCapability.set).toHaveBeenCalledWith("editing", true);
});
it("uses editing status to determine editing context root", function () {
capability.edit();
mockStatusCapability.get.and.returnValue(false);
expect(capability.isEditContextRoot()).toBe(false);
mockStatusCapability.get.and.returnValue(true);
expect(capability.isEditContextRoot()).toBe(true);
});
it("inEditingContext returns true if parent object is being"
+ " edited", function () {
mockStatusCapability.get.and.returnValue(false);
mockParentStatus.get.and.returnValue(false);
expect(capability.inEditContext()).toBe(false);
mockParentStatus.get.and.returnValue(true);
expect(capability.inEditContext()).toBe(true);
});
describe("save", function () {
beforeEach(function () {
capability.edit();
capability.save();
});
it("commits the transaction", function () {
expect(mockTransactionService.commit).toHaveBeenCalled();
});
it("begins a new transaction", function () {
expect(mockTransactionService.startTransaction).toHaveBeenCalled();
});
});
describe("finish", function () {
beforeEach(function () {
mockTransactionService.isActive.and.returnValue(true);
capability.edit();
capability.finish();
});
it("cancels the transaction", function () {
expect(mockTransactionService.cancel).toHaveBeenCalled();
});
it("resets the edit state", function () {
expect(mockStatusCapability.set).toHaveBeenCalledWith('editing', false);
});
});
describe("finish", function () {
beforeEach(function () {
mockTransactionService.isActive.and.returnValue(false);
capability.edit();
});
it("does not cancel transaction when transaction is not active", function () {
capability.finish();
expect(mockTransactionService.cancel).not.toHaveBeenCalled();
});
it("returns a promise", function () {
expect(capability.finish() instanceof Promise).toBe(true);
});
});
describe("dirty", function () {
var model = {};
beforeEach(function () {
mockDomainObject.getModel.and.returnValue(model);
capability.edit();
capability.finish();
});
it("returns true if the object has been modified since it"
+ " was last persisted", function () {
mockTransactionService.size.and.returnValue(0);
expect(capability.dirty()).toBe(false);
mockTransactionService.size.and.returnValue(1);
expect(capability.dirty()).toBe(true);
});
});
});
}
);

View File

@@ -0,0 +1,186 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/**
* MCTRepresentationSpec. Created by vwoeltje on 11/6/14.
*/
define(
["../../src/creation/CreateAction"],
function (CreateAction) {
xdescribe("The create action", function () {
var mockType,
mockParent,
mockContext,
mockDomainObject,
capabilities = {},
mockEditAction,
action;
function mockPromise(value) {
return {
then: function (callback) {
return mockPromise(callback(value));
}
};
}
beforeEach(function () {
mockType = jasmine.createSpyObj(
"type",
[
"getKey",
"getGlyph",
"getCssClass",
"getName",
"getDescription",
"getProperties",
"getInitialModel"
]
);
mockParent = jasmine.createSpyObj(
"domainObject",
[
"getId",
"getModel",
"getCapability",
"useCapability"
]
);
mockDomainObject = jasmine.createSpyObj(
"domainObject",
[
"getId",
"getModel",
"getCapability",
"hasCapability",
"useCapability"
]
);
mockDomainObject.hasCapability.and.callFake(function (name) {
return Boolean(capabilities[name]);
});
mockDomainObject.getCapability.and.callFake(function (name) {
return capabilities[name];
});
capabilities.action = jasmine.createSpyObj(
"actionCapability",
[
"getActions",
"perform"
]
);
capabilities.editor = jasmine.createSpyObj(
"editorCapability",
[
"edit",
"save",
"finish"
]
);
mockEditAction = jasmine.createSpyObj(
"editAction",
[
"perform"
]
);
mockContext = {
domainObject: mockParent
};
mockParent.useCapability.and.returnValue(mockDomainObject);
mockType.getKey.and.returnValue("test");
mockType.getCssClass.and.returnValue("icon-telemetry");
mockType.getDescription.and.returnValue("a test type");
mockType.getName.and.returnValue("Test");
mockType.getProperties.and.returnValue([]);
mockType.getInitialModel.and.returnValue({});
action = new CreateAction(
mockType,
mockParent,
mockContext
);
});
it("exposes type-appropriate metadata", function () {
var metadata = action.getMetadata();
expect(metadata.name).toEqual("Test");
expect(metadata.description).toEqual("a test type");
expect(metadata.cssClass).toEqual("icon-telemetry");
});
describe("the perform function", function () {
var promise = jasmine.createSpyObj("promise", ["then"]);
beforeEach(function () {
capabilities.action.getActions.and.returnValue([mockEditAction]);
});
it("uses the instantiation capability when performed", function () {
action.perform();
expect(mockParent.useCapability).toHaveBeenCalledWith("instantiation", jasmine.any(Object));
});
it("uses the edit action if available", function () {
action.perform();
expect(mockEditAction.perform).toHaveBeenCalled();
});
it("uses the save-as action if object does not have an edit action"
+ " available", function () {
capabilities.action.getActions.and.returnValue([]);
capabilities.action.perform.and.returnValue(mockPromise(undefined));
capabilities.editor.save.and.returnValue(promise);
action.perform();
expect(capabilities.action.perform).toHaveBeenCalledWith("save-as");
});
describe("uses to editor capability", function () {
beforeEach(function () {
capabilities.action.getActions.and.returnValue([]);
capabilities.action.perform.and.returnValue(promise);
capabilities.editor.save.and.returnValue(promise);
});
it("to save the edit if user saves dialog", function () {
action.perform();
expect(promise.then).toHaveBeenCalled();
promise.then.calls.mostRecent().args[0]();
expect(capabilities.editor.save).toHaveBeenCalled();
});
it("to finish the edit if user cancels dialog", function () {
action.perform();
promise.then.calls.mostRecent().args[1]();
expect(capabilities.editor.finish).toHaveBeenCalled();
});
});
});
});
}
);

View File

@@ -0,0 +1,197 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/**
* MCTRepresentationSpec. Created by vwoeltje on 11/6/14.
*/
define(
["../../src/creation/CreateWizard"],
function (CreateWizard) {
xdescribe("The create wizard", function () {
var mockType,
mockParent,
mockProperties,
mockPolicyService,
testModel,
mockDomainObject,
wizard;
function createMockProperty(name) {
var mockProperty = jasmine.createSpyObj(
"property" + name,
["getDefinition", "getValue", "setValue"]
);
mockProperty.getDefinition.and.returnValue({
control: "textfield"
});
mockProperty.getValue.and.returnValue(name);
return mockProperty;
}
beforeEach(function () {
mockType = jasmine.createSpyObj(
"type",
[
"getKey",
"getGlyph",
"getCssClass",
"getName",
"getDescription",
"getProperties",
"getInitialModel"
]
);
mockParent = jasmine.createSpyObj(
"domainObject",
[
"getId",
"getModel",
"getCapability"
]
);
mockProperties = ["A", "B", "C"].map(createMockProperty);
mockPolicyService = jasmine.createSpyObj('policyService', ['allow']);
testModel = { someKey: "some value" };
mockType.getKey.and.returnValue("test");
mockType.getCssClass.and.returnValue("icon-telemetry");
mockType.getDescription.and.returnValue("a test type");
mockType.getName.and.returnValue("Test");
mockType.getInitialModel.and.returnValue(testModel);
mockType.getProperties.and.returnValue(mockProperties);
mockDomainObject = jasmine.createSpyObj(
'domainObject',
['getCapability', 'useCapability', 'getModel']
);
//Mocking the getCapability('type') call
mockDomainObject.getCapability.and.returnValue(mockType);
mockDomainObject.useCapability.and.returnValue();
mockDomainObject.getModel.and.returnValue(testModel);
wizard = new CreateWizard(
mockDomainObject,
mockParent,
mockPolicyService
);
});
it("creates a form model with a Properties section", function () {
expect(wizard.getFormStructure().sections[0].name)
.toEqual("Properties");
});
it("adds one row per defined type property", function () {
// Three properties were defined in the mock type
expect(wizard.getFormStructure().sections[0].rows.length)
.toEqual(3);
});
it("interprets form data using type-defined properties", function () {
// Use key names from mock properties
wizard.createModel([
"field 0",
"field 1",
"field 2"
]);
// Should have gotten a setValue call
mockProperties.forEach(function (mockProperty, i) {
expect(mockProperty.setValue).toHaveBeenCalledWith(
{
someKey: "some value",
type: 'test'
},
"field " + i
);
});
});
it("looks up initial values from properties", function () {
var initialValue = wizard.getInitialFormValue();
expect(initialValue[0]).toEqual("A");
expect(initialValue[1]).toEqual("B");
expect(initialValue[2]).toEqual("C");
// Verify that expected argument was passed
mockProperties.forEach(function (mockProperty) {
expect(mockProperty.getValue)
.toHaveBeenCalledWith(testModel);
});
});
it("populates the model on the associated object", function () {
var formValue = {
"A": "ValueA",
"B": "ValueB",
"C": "ValueC"
},
compareModel = wizard.createModel(formValue);
//populateObjectFromInput adds a .location attribute that is not added by createModel.
compareModel.location = undefined;
wizard.populateObjectFromInput(formValue);
expect(mockDomainObject.useCapability).toHaveBeenCalledWith('mutation', jasmine.any(Function));
expect(mockDomainObject.useCapability.calls.mostRecent().args[1]()).toEqual(compareModel);
});
it("validates selection types using policy", function () {
var mockDomainObj = jasmine.createSpyObj(
'domainObject',
['getCapability']
),
mockOtherType = jasmine.createSpyObj(
'otherType',
['getKey']
),
//Create a form structure with location
structure = wizard.getFormStructure(true),
sections = structure.sections,
rows = structure.sections[sections.length - 1].rows,
locationRow = rows[rows.length - 1];
mockDomainObj.getCapability.and.returnValue(mockOtherType);
locationRow.validate(mockDomainObj);
// Should check policy to see if the user-selected location
// can actually contain objects of this type
expect(mockPolicyService.allow).toHaveBeenCalledWith(
'composition',
mockDomainObj,
mockDomainObject
);
});
it("creates a form model without a location if not requested", function () {
expect(wizard.getFormStructure(false).sections.some(function (section) {
return section.name === 'Location';
})).toEqual(false);
});
});
}
);

View File

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

View File

@@ -0,0 +1,290 @@
/*****************************************************************************
* 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/ui/TreeView',
'zepto'
], function (TreeView, $) {
xdescribe("TreeView", function () {
var mockGestureService,
mockGestureHandle,
mockDomainObject,
mockMutation,
mockUnlisten,
testCapabilities,
treeView;
function makeMockDomainObject(id, model, capabilities) {
var mockDomainObj = jasmine.createSpyObj(
'domainObject-' + id,
[
'getId',
'getModel',
'getCapability',
'hasCapability',
'useCapability'
]
);
mockDomainObj.getId.and.returnValue(id);
mockDomainObj.getModel.and.returnValue(model);
mockDomainObj.hasCapability.and.callFake(function (c) {
return Boolean(capabilities[c]);
});
mockDomainObj.getCapability.and.callFake(function (c) {
return capabilities[c];
});
mockDomainObj.useCapability.and.callFake(function (c) {
return capabilities[c] && capabilities[c].invoke();
});
return mockDomainObj;
}
beforeEach(function () {
mockGestureService = jasmine.createSpyObj(
'gestureService',
['attachGestures']
);
mockGestureHandle = jasmine.createSpyObj('gestures', ['destroy']);
mockGestureService.attachGestures.and.returnValue(mockGestureHandle);
mockMutation = jasmine.createSpyObj('mutation', ['listen']);
mockUnlisten = jasmine.createSpy('unlisten');
mockMutation.listen.and.returnValue(mockUnlisten);
testCapabilities = { mutation: mockMutation };
mockDomainObject =
makeMockDomainObject('parent', {}, testCapabilities);
treeView = new TreeView(mockGestureService);
});
describe("elements", function () {
var elements;
beforeEach(function () {
elements = treeView.elements();
});
it("is an unordered list", function () {
expect(elements[0].tagName.toLowerCase())
.toEqual('ul');
});
});
describe("model", function () {
var mockComposition;
function makeGenericCapabilities() {
var mockStatus =
jasmine.createSpyObj('status', ['listen', 'list']);
mockStatus.list.and.returnValue([]);
return {
context: jasmine.createSpyObj('context', ['getPath']),
type: jasmine.createSpyObj('type', ['getCssClass']),
location: jasmine.createSpyObj('location', ['isLink']),
mutation: jasmine.createSpyObj('mutation', ['listen']),
status: mockStatus
};
}
beforeEach(function () {
mockComposition = ['a', 'b', 'c'].map(function (id) {
var testCaps = makeGenericCapabilities(),
mockChild =
makeMockDomainObject(id, {}, testCaps);
testCaps.context.getPath
.and.returnValue([mockDomainObject, mockChild]);
return mockChild;
});
testCapabilities.composition =
jasmine.createSpyObj('composition', ['invoke']);
testCapabilities.composition.invoke
.and.returnValue(Promise.resolve(mockComposition));
treeView.model(mockDomainObject);
return testCapabilities.composition.invoke();
});
it("adds one node per composition element", function () {
expect(treeView.elements()[0].childElementCount)
.toEqual(mockComposition.length);
});
it("listens for mutation", function () {
expect(testCapabilities.mutation.listen)
.toHaveBeenCalledWith(jasmine.any(Function));
});
describe("when mutation occurs", function () {
beforeEach(function () {
mockComposition.pop();
testCapabilities.mutation.listen
.calls.mostRecent().args[0](mockDomainObject.getModel());
return testCapabilities.composition.invoke();
});
it("continues to show one node per composition element", function () {
expect(treeView.elements()[0].childElementCount)
.toEqual(mockComposition.length);
});
});
describe("when replaced with a non-compositional domain object", function () {
beforeEach(function () {
delete testCapabilities.composition;
treeView.model(mockDomainObject);
});
it("stops listening for mutation", function () {
expect(mockUnlisten).toHaveBeenCalled();
});
it("removes all tree nodes", function () {
expect(treeView.elements()[0].childElementCount)
.toEqual(0);
});
});
describe("when selection state changes", function () {
var selectionIndex = 1;
beforeEach(function () {
treeView.value(mockComposition[selectionIndex]);
});
it("communicates selection state to an appropriate node", function () {
var selected = $(treeView.elements()[0]).find('.selected');
expect(selected.length).toEqual(1);
});
});
describe("when a context-less object is selected", function () {
beforeEach(function () {
var testCaps = makeGenericCapabilities(),
mockDomainObj =
makeMockDomainObject('xyz', {}, testCaps);
delete testCaps.context;
treeView.value(mockDomainObj);
});
it("clears all selection state", function () {
var selected = $(treeView.elements()[0]).find('.selected');
expect(selected.length).toEqual(0);
});
});
describe("when children contain children", function () {
beforeEach(function () {
var newCapabilities = makeGenericCapabilities(),
gcCapabilities = makeGenericCapabilities(),
mockNewChild =
makeMockDomainObject('d', {}, newCapabilities),
mockGrandchild =
makeMockDomainObject('gc', {}, gcCapabilities);
newCapabilities.composition =
jasmine.createSpyObj('composition', ['invoke']);
newCapabilities.composition.invoke
.and.returnValue(Promise.resolve([mockGrandchild]));
mockComposition.push(mockNewChild);
newCapabilities.context.getPath.and.returnValue([
mockDomainObject,
mockNewChild
]);
gcCapabilities.context.getPath.and.returnValue([
mockDomainObject,
mockNewChild,
mockGrandchild
]);
testCapabilities.mutation.listen
.calls.mostRecent().args[0](mockDomainObject);
return testCapabilities.composition.invoke().then(function () {
treeView.value(mockGrandchild);
return newCapabilities.composition.invoke();
});
});
it("creates inner trees", function () {
expect($(treeView.elements()[0]).find('ul').length)
.toEqual(1);
});
});
describe("when status changes", function () {
var testStatuses;
beforeEach(function () {
var mockStatus = mockComposition[1].getCapability('status');
testStatuses = ['foo'];
mockStatus.list.and.returnValue(testStatuses);
mockStatus.listen.calls.mostRecent().args[0](testStatuses);
});
it("reflects the status change in the tree", function () {
expect($(treeView.elements()).find('.s-status-foo').length)
.toEqual(1);
});
});
});
describe("observe", function () {
var mockCallback,
unobserve;
beforeEach(function () {
mockCallback = jasmine.createSpy('callback');
unobserve = treeView.observe(mockCallback);
});
it("notifies listeners when value is changed", function () {
treeView.value(mockDomainObject, {some: event});
expect(mockCallback)
.toHaveBeenCalledWith(mockDomainObject, {some: event});
});
it("does not notify listeners when deactivated", function () {
unobserve();
treeView.value(mockDomainObject);
expect(mockCallback).not.toHaveBeenCalled();
});
});
});
});

View File

@@ -0,0 +1,95 @@
/*****************************************************************************
* 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/ComposeActionPolicy"],
function (ComposeActionPolicy) {
xdescribe("The compose action policy", function () {
var mockInjector,
mockPolicyService,
mockTypes,
mockDomainObjects,
mockAction,
testContext,
policy;
beforeEach(function () {
mockInjector = jasmine.createSpyObj('$injector', ['get']);
mockPolicyService = jasmine.createSpyObj(
'policyService',
['allow']
);
mockTypes = ['a', 'b'].map(function (type) {
var mockType = jasmine.createSpyObj('type-' + type, ['getKey']);
mockType.getKey.and.returnValue(type);
return mockType;
});
mockDomainObjects = ['a', 'b'].map(function (id, index) {
var mockDomainObject = jasmine.createSpyObj(
'domainObject-' + id,
['getId', 'getCapability']
);
mockDomainObject.getId.and.returnValue(id);
mockDomainObject.getCapability.and.callFake(function (c) {
return c === 'type' && mockTypes[index];
});
return mockDomainObject;
});
mockAction = jasmine.createSpyObj('action', ['getMetadata']);
testContext = {
key: 'compose',
domainObject: mockDomainObjects[0],
selectedObject: mockDomainObjects[1]
};
mockAction.getMetadata.and.returnValue(testContext);
mockInjector.get.and.callFake(function (service) {
return service === 'policyService' && mockPolicyService;
});
policy = new ComposeActionPolicy(mockInjector);
});
it("defers to composition policy", function () {
mockPolicyService.allow.and.returnValue(false);
expect(policy.allow(mockAction, testContext)).toBeFalsy();
mockPolicyService.allow.and.returnValue(true);
expect(policy.allow(mockAction, testContext)).toBeTruthy();
expect(mockPolicyService.allow).toHaveBeenCalledWith(
'composition',
mockDomainObjects[0],
mockDomainObjects[1]
);
});
it("allows actions other than compose", function () {
testContext.key = 'somethingElse';
mockPolicyService.allow.and.returnValue(false);
expect(policy.allow(mockAction, testContext)).toBeTruthy();
});
});
}
);

View File

@@ -21,32 +21,24 @@
*****************************************************************************/
define([
"moment-timezone",
"./src/indicators/ClockIndicator",
"./src/services/TickerService",
"./src/services/TimerService",
"./src/controllers/ClockController",
"./src/controllers/TimerController",
"./src/controllers/RefreshingController",
"./src/actions/StartTimerAction",
"./src/actions/RestartTimerAction",
"./src/actions/StopTimerAction",
"./src/actions/PauseTimerAction",
"./res/templates/clock.html",
"./res/templates/timer.html"
], function (
MomentTimezone,
ClockIndicator,
TickerService,
TimerService,
ClockController,
TimerController,
RefreshingController,
StartTimerAction,
RestartTimerAction,
StopTimerAction,
PauseTimerAction,
clockTemplate,
timerTemplate
) {
return {
@@ -73,24 +65,13 @@ define([
"value": "YYYY/MM/DD HH:mm:ss"
}
],
"indicators": [
{
"implementation": ClockIndicator,
"depends": [
"tickerService",
"CLOCK_INDICATOR_FORMAT"
],
"priority": "preferred"
}
],
"services": [
{
"key": "tickerService",
"implementation": TickerService,
"depends": [
"$timeout",
"now",
"$rootScope"
"now"
]
},
{
@@ -100,14 +81,6 @@ define([
}
],
"controllers": [
{
"key": "ClockController",
"implementation": ClockController,
"depends": [
"$scope",
"tickerService"
]
},
{
"key": "TimerController",
"implementation": TimerController,
@@ -127,12 +100,6 @@ define([
}
],
"views": [
{
"key": "clock",
"type": "clock",
"editable": false,
"template": clockTemplate
},
{
"key": "timer",
"type": "timer",
@@ -187,70 +154,6 @@ define([
}
],
"types": [
{
"key": "clock",
"name": "Clock",
"cssClass": "icon-clock",
"description": "A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts.",
"priority": 101,
"features": [
"creation"
],
"properties": [
{
"key": "clockFormat",
"name": "Display Format",
"control": "composite",
"items": [
{
"control": "select",
"options": [
{
"value": "YYYY/MM/DD hh:mm:ss",
"name": "YYYY/MM/DD hh:mm:ss"
},
{
"value": "YYYY/DDD hh:mm:ss",
"name": "YYYY/DDD hh:mm:ss"
},
{
"value": "hh:mm:ss",
"name": "hh:mm:ss"
}
],
"cssClass": "l-inline"
},
{
"control": "select",
"options": [
{
"value": "clock12",
"name": "12hr"
},
{
"value": "clock24",
"name": "24hr"
}
],
"cssClass": "l-inline"
}
]
},
{
"key": "timezone",
"name": "Timezone",
"control": "autocomplete",
"options": MomentTimezone.tz.names()
}
],
"model": {
"clockFormat": [
"YYYY/MM/DD hh:mm:ss",
"clock12"
],
"timezone": "UTC"
}
},
{
"key": "timer",
"name": "Timer",

View File

@@ -1,32 +0,0 @@
<!--
Open MCT, Copyright (c) 2009-2016, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div class="c-clock l-time-display u-style-receiver js-style-receiver" ng-controller="ClockController as clock">
<div class="c-clock__timezone">
{{clock.zone()}}
</div>
<div class="c-clock__value">
{{clock.text()}}
</div>
<div class="c-clock__ampm">
{{clock.ampm()}}
</div>
</div>

View File

@@ -1,110 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, 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([
'moment',
'moment-timezone'
],
function (
moment,
momentTimezone
) {
/**
* Controller for views of a Clock domain object.
*
* @constructor
* @memberof platform/features/clock
* @param {angular.Scope} $scope the Angular scope
* @param {platform/features/clock.TickerService} tickerService
* a service used to align behavior with clock ticks
*/
function ClockController($scope, tickerService) {
var lastTimestamp,
unlisten,
timeFormat,
zoneName,
self = this;
function update() {
var m = zoneName
? moment.utc(lastTimestamp).tz(zoneName) : moment.utc(lastTimestamp);
self.zoneAbbr = m.zoneAbbr();
self.textValue = timeFormat && m.format(timeFormat);
self.ampmValue = m.format("A"); // Just the AM or PM part
}
function tick(timestamp) {
lastTimestamp = timestamp;
update();
}
function updateModel(model) {
var baseFormat;
if (model !== undefined) {
baseFormat = model.clockFormat[0];
self.use24 = model.clockFormat[1] === 'clock24';
timeFormat = self.use24
? baseFormat.replace('hh', "HH") : baseFormat;
// If wrong timezone is provided, the UTC will be used
zoneName = momentTimezone.tz.names().includes(model.timezone)
? model.timezone : "UTC";
update();
}
}
// Pull in the model (clockFormat and timezone) from the domain object model
$scope.$watch('model', updateModel);
// Listen for clock ticks ... and stop listening on destroy
unlisten = tickerService.listen(tick);
$scope.$on('$destroy', unlisten);
}
/**
* Get the clock's time zone, as displayable text.
* @returns {string}
*/
ClockController.prototype.zone = function () {
return this.zoneAbbr;
};
/**
* Get the current time, as displayable text.
* @returns {string}
*/
ClockController.prototype.text = function () {
return this.textValue;
};
/**
* Get the text to display to qualify a time as AM or PM.
* @returns {string}
*/
ClockController.prototype.ampm = function () {
return this.use24 ? '' : this.ampmValue;
};
return ClockController;
}
);

View File

@@ -1,65 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, 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(
['moment'],
function (moment) {
/**
* Indicator that displays the current UTC time in the status area.
* @implements {Indicator}
* @memberof platform/features/clock
* @param {platform/features/clock.TickerService} tickerService
* a service used to align behavior with clock ticks
* @param {string} indicatorFormat format string for timestamps
* shown in this indicator
*/
function ClockIndicator(tickerService, indicatorFormat) {
var self = this;
this.text = "";
tickerService.listen(function (timestamp) {
self.text = moment.utc(timestamp)
.format(indicatorFormat) + " UTC";
});
}
ClockIndicator.prototype.getGlyphClass = function () {
return "";
};
ClockIndicator.prototype.getCssClass = function () {
return "t-indicator-clock icon-clock no-minify c-indicator--not-clickable";
};
ClockIndicator.prototype.getText = function () {
return this.text;
};
ClockIndicator.prototype.getDescription = function () {
return "";
};
return ClockIndicator;
}
);

View File

@@ -32,13 +32,8 @@ define(
* @param $timeout Angular's $timeout
* @param {Function} now function to provide the current time in ms
*/
function TickerService($timeout, now, $rootScope) {
function TickerService($timeout, now) {
var self = this;
var timeoutId;
$rootScope.$on('$destroy', function () {
$timeout.cancel(timeoutId);
});
function tick() {
var timestamp = now(),
@@ -53,7 +48,7 @@ define(
}
// Try to update at exactly the next second
timeoutId = $timeout(tick, 1000 - millis, true);
$timeout(tick, 1000 - millis, true);
}
tick();

View File

@@ -1,107 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2017, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../../src/controllers/ClockController"],
function (ClockController) {
// Wed, 03 Jun 2015 17:56:14 GMT
var TEST_TIMESTAMP = 1433354174000;
describe("A clock view's controller", function () {
var mockScope,
mockTicker,
mockUnticker,
controller;
beforeEach(function () {
mockScope = jasmine.createSpyObj('$scope', ['$watch', '$on']);
mockTicker = jasmine.createSpyObj('ticker', ['listen']);
mockUnticker = jasmine.createSpy('unticker');
mockTicker.listen.and.returnValue(mockUnticker);
controller = new ClockController(mockScope, mockTicker);
});
it("watches for model (clockFormat and timezone) from the domain object model", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
"model",
jasmine.any(Function)
);
});
it("subscribes to clock ticks", function () {
expect(mockTicker.listen)
.toHaveBeenCalledWith(jasmine.any(Function));
});
it("unsubscribes to ticks when destroyed", function () {
// Make sure $destroy is being listened for...
expect(mockScope.$on.calls.mostRecent().args[0]).toEqual('$destroy');
expect(mockUnticker).not.toHaveBeenCalled();
// ...and makes sure that its listener unsubscribes from ticker
mockScope.$on.calls.mostRecent().args[1]();
expect(mockUnticker).toHaveBeenCalled();
});
it("formats using the format string from the model", function () {
mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP);
mockScope.$watch.calls.mostRecent().args[1]({
"clockFormat": [
"YYYY-DDD hh:mm:ss",
"clock24"
],
"timezone": "Canada/Eastern"
});
expect(controller.zone()).toEqual("EDT");
expect(controller.text()).toEqual("2015-154 13:56:14");
expect(controller.ampm()).toEqual("");
});
it("formats 12-hour time", function () {
mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP);
mockScope.$watch.calls.mostRecent().args[1]({
"clockFormat": [
"YYYY-DDD hh:mm:ss",
"clock12"
],
"timezone": ""
});
expect(controller.zone()).toEqual("UTC");
expect(controller.text()).toEqual("2015-154 05:56:14");
expect(controller.ampm()).toEqual("PM");
});
it("does not throw exceptions when model is undefined", function () {
mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP);
expect(function () {
mockScope.$watch.calls.mostRecent().args[1](undefined);
}).not.toThrow();
});
});
}
);

View File

@@ -1,58 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, 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/indicators/ClockIndicator"],
function (ClockIndicator) {
// Wed, 03 Jun 2015 17:56:14 GMT
var TEST_TIMESTAMP = 1433354174000,
TEST_FORMAT = "YYYY-DDD HH:mm:ss";
describe("The clock indicator", function () {
var mockTicker,
mockUnticker,
indicator;
beforeEach(function () {
mockTicker = jasmine.createSpyObj('ticker', ['listen']);
mockUnticker = jasmine.createSpy('unticker');
mockTicker.listen.and.returnValue(mockUnticker);
indicator = new ClockIndicator(mockTicker, TEST_FORMAT);
});
it("displays the current time", function () {
mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP);
expect(indicator.getText()).toEqual("2015-154 17:56:14 UTC");
});
it("implements the Indicator interface", function () {
expect(indicator.getCssClass()).toEqual(jasmine.any(String));
expect(indicator.getText()).toEqual(jasmine.any(String));
expect(indicator.getDescription()).toEqual(jasmine.any(String));
});
});
}
);

View File

@@ -30,18 +30,16 @@ define(
var mockTimeout,
mockNow,
mockCallback,
tickerService,
mockRootScope;
tickerService;
beforeEach(function () {
mockTimeout = jasmine.createSpy('$timeout');
mockNow = jasmine.createSpy('now');
mockCallback = jasmine.createSpy('callback');
mockRootScope = jasmine.createSpyObj('rootScope', ['$on']);
mockNow.and.returnValue(TEST_TIMESTAMP);
tickerService = new TickerService(mockTimeout, mockNow, mockRootScope);
tickerService = new TickerService(mockTimeout, mockNow);
});
it("notifies listeners of clock ticks", function () {

View File

@@ -58,7 +58,7 @@ define([
) {
var $http = this.$http,
$log = this.$log,
app = angular.module(Constants.MODULE_NAME, []),
app = angular.module(Constants.MODULE_NAME, ["ngRoute"]),
loader = new BundleLoader($http, $log, openmct.legacyRegistry),
resolver = new BundleResolver(
new ExtensionResolver(

View File

@@ -28,7 +28,8 @@
define(
[
'./FrameworkLayer',
'angular'
'angular',
'angular-route'
],
function (
FrameworkLayer,

View File

@@ -138,6 +138,34 @@ define(
}
}
// Custom registration function for extensions of category "route"
function registerRoute(extension) {
var app = this.app,
$log = this.$log,
route = Object.create(extension);
// Adjust path for bundle
if (route.templateUrl) {
route.templateUrl = [
route.bundle.path,
route.bundle.resources,
route.templateUrl
].join(Constants.SEPARATOR);
}
// Log the registration
$log.info("Registering route: " + (route.key || route.when));
// Register the route with Angular
app.config(['$routeProvider', function ($routeProvider) {
if (route.when) {
$routeProvider.when(route.when, route);
} else {
$routeProvider.otherwise(route);
}
}]);
}
// Handle service compositing
function registerComponents(components) {
var app = this.app,
@@ -166,6 +194,13 @@ define(
CustomRegistrars.prototype.constants =
mapUpon(registerConstant);
/**
* Register Angular routes.
* @param {Array} extensions the resolved extensions
*/
CustomRegistrars.prototype.routes =
mapUpon(registerRoute);
/**
* Register Angular directives.
* @param {Array} extensions the resolved extensions

View File

@@ -57,6 +57,7 @@ define(
expect(customRegistrars.directives).toBeTruthy();
expect(customRegistrars.controllers).toBeTruthy();
expect(customRegistrars.services).toBeTruthy();
expect(customRegistrars.routes).toBeTruthy();
expect(customRegistrars.constants).toBeTruthy();
expect(customRegistrars.runs).toBeTruthy();
});
@@ -138,6 +139,47 @@ define(
expect(mockLog.warn.calls.count()).toEqual(0);
});
it("allows routes to be registered", function () {
var mockRouteProvider = jasmine.createSpyObj(
"$routeProvider",
["when", "otherwise"]
),
bundle = {
path: "test/bundle",
resources: "res"
},
routes = [
{
when: "foo",
templateUrl: "templates/test.html",
bundle: bundle
},
{
templateUrl: "templates/default.html",
bundle: bundle
}
];
customRegistrars.routes(routes);
// Give it the route provider based on its config call
mockApp.config.calls.all().forEach(function (call) {
// Invoke the provided callback
call.args[0][1](mockRouteProvider);
});
// The "when" clause should have been mapped to the when method...
expect(mockRouteProvider.when).toHaveBeenCalled();
expect(mockRouteProvider.when.calls.mostRecent().args[0]).toEqual("foo");
expect(mockRouteProvider.when.calls.mostRecent().args[1].templateUrl)
.toEqual("test/bundle/res/templates/test.html");
// ...while the other should have been treated as a default route
expect(mockRouteProvider.otherwise).toHaveBeenCalled();
expect(mockRouteProvider.otherwise.calls.mostRecent().args[0].templateUrl)
.toEqual("test/bundle/res/templates/default.html");
});
it("accepts components for service compositing", function () {
// Most relevant code will be exercised in service compositor spec
expect(customRegistrars.components).toBeTruthy();

View File

@@ -110,15 +110,8 @@ define([
worker = workerService.run('bareBonesSearchWorker');
}
function handleWorkerMessage(messageEvent) {
worker.addEventListener('message', function (messageEvent) {
provider.onWorkerMessage(messageEvent);
}
worker.addEventListener('message', handleWorkerMessage);
this.openmct.once('destroy', () => {
worker.removeEventListener('message', handleWorkerMessage);
worker.terminate();
});
return worker;

View File

@@ -31,6 +31,7 @@ define([
'objectUtils',
'./plugins/plugins',
'./adapter/indicators/legacy-indicators-plugin',
'./plugins/buildInfo/plugin',
'./ui/registries/ViewRegistry',
'./plugins/imagery/plugin',
'./ui/registries/InspectorViewRegistry',
@@ -39,7 +40,6 @@ define([
'./ui/router/Browse',
'../platform/framework/src/Main',
'./ui/layout/Layout.vue',
'./ui/inspector/styles/StylesManager',
'../platform/core/src/objects/DomainObjectImpl',
'../platform/core/src/capabilities/ContextualDomainObject',
'./ui/preview/plugin',
@@ -60,6 +60,7 @@ define([
objectUtils,
plugins,
LegacyIndicatorsPlugin,
buildInfoPlugin,
ViewRegistry,
ImageryPlugin,
InspectorViewRegistry,
@@ -68,7 +69,6 @@ define([
Browse,
Main,
Layout,
stylesManager,
DomainObjectImpl,
ContextualDomainObject,
PreviewPlugin,
@@ -123,7 +123,6 @@ define([
};
this.destroy = this.destroy.bind(this);
/**
* Tracks current selection state of the application.
* @private
@@ -137,7 +136,7 @@ define([
* @memberof module:openmct.MCT#
* @name conductor
*/
this.time = new api.TimeAPI();
this.time = new api.TimeAPI(this);
/**
* An interface for interacting with the composition of domain objects.
@@ -377,7 +376,6 @@ define([
* MCT; if undefined, MCT will be run in the body of the document
*/
MCT.prototype.start = function (domElement = document.body, isHeadlessMode = false) {
if (this.types.get('layout') === undefined) {
this.install(this.plugins.DisplayLayout({
showAsView: ['summary-widget']
@@ -436,9 +434,6 @@ define([
domElement.appendChild(appLayout.$mount().$el);
this.layout = appLayout.$refs.layout;
this.once('destroy', () => {
appLayout.$destroy();
});
Browse(this);
}
@@ -467,40 +462,9 @@ define([
};
MCT.prototype.destroy = function () {
if (this._destroyed === true) {
return;
}
window.removeEventListener('beforeunload', this.destroy);
this.emit('destroy');
this.removeAllListeners();
if (this.$injector) {
this.$injector.get('$rootScope').$destroy();
this.$injector = null;
}
if (this.$angular) {
this.$angular.element(this.element).off().removeData();
this.$angular.element(this.element).empty();
this.$angular = null;
}
this.overlays.destroy();
if (this.element) {
this.element.remove();
}
stylesManager.default.removeAllListeners();
window.angular = null;
window.openmct = null;
Object.keys(require.cache).forEach(key => delete require.cache[key]);
this._destroyed = true;
this.router.destroy();
};
MCT.prototype.plugins = plugins;

View File

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

View File

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

View File

@@ -32,10 +32,6 @@ define([
// cannot be injected.
function AlternateCompositionInitializer(openmct) {
AlternateCompositionCapability.appliesTo = function (model, id) {
openmct.once('destroy', () => {
delete AlternateCompositionCapability.appliesTo;
});
model = objectUtils.toNewFormat(model, id || '');
return Boolean(openmct.composition.get(model));

View File

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

View File

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

View File

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

View File

@@ -28,10 +28,6 @@ export default class Editor extends EventEmitter {
super();
this.editing = false;
this.openmct = openmct;
openmct.once('destroy', () => {
this.removeAllListeners();
});
}
/**

View File

@@ -44,15 +44,7 @@ describe('The ActionCollection', () => {
}
});
openmct.$injector.get.and.callFake((key) => {
return {
'identifierService': mockIdentifierService,
'$rootScope': {
'$destroy': () => {}
}
}[key];
});
openmct.$injector.get.and.returnValue(mockIdentifierService);
mockObjectPath = [
{
name: 'mock folder',

View File

@@ -46,7 +46,7 @@ define([
StatusAPI
) {
return {
TimeAPI: TimeAPI,
TimeAPI: TimeAPI.default,
ObjectAPI: ObjectAPI,
CompositionAPI: CompositionAPI,
TypeRegistry: TypeRegistry,

View File

@@ -182,6 +182,12 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) {
let objectPromise = provider.get(identifier, abortSignal).then(result => {
delete this.cache[keystring];
result = this.applyGetInterceptors(identifier, result);
if (result.isMutable) {
result.$refresh(result);
} else {
let mutableDomainObject = this._toMutable(result);
mutableDomainObject.$refresh(result);
}
return result;
});
@@ -298,10 +304,15 @@ ObjectAPI.prototype.save = function (domainObject) {
savedResolve = resolve;
});
domainObject.persisted = persistedTime;
provider.create(domainObject).then((response) => {
this.mutate(domainObject, 'persisted', persistedTime);
savedResolve(response);
});
const newObjectPromise = provider.create(domainObject);
if (newObjectPromise) {
newObjectPromise.then(response => {
this.mutate(domainObject, 'persisted', persistedTime);
savedResolve(response);
});
} else {
result = Promise.reject(`[ObjectAPI][save] Object provider returned ${newObjectPromise} when creating new object.`);
}
} else {
domainObject.persisted = persistedTime;
this.mutate(domainObject, 'persisted', persistedTime);
@@ -358,6 +369,20 @@ ObjectAPI.prototype.applyGetInterceptors = function (identifier, domainObject) {
return domainObject;
};
/**
* Return relative url path from a given object path
* eg: #/browse/mine/cb56f6bf-c900-43b7-b923-2e3b64b412db/6e89e858-77ce-46e4-a1ad-749240286497/....
* @param {Array} objectPath
* @returns {string} relative url for object
*/
ObjectAPI.prototype.getRelativePath = function (objectPath) {
return objectPath
.map(p => this.makeKeyString(p.identifier))
.reverse()
.join('/')
;
};
/**
* Modify a domain object.
* @param {module:openmct.DomainObject} object the object to mutate

View File

@@ -10,28 +10,37 @@ const cssClasses = {
};
class Overlay extends EventEmitter {
constructor(options) {
constructor({
buttons,
autoHide = true,
dismissable = true,
element,
onDestroy,
size
} = {}) {
super();
this.dismissable = options.dismissable !== false;
this.container = document.createElement('div');
this.container.classList.add('l-overlay-wrapper', cssClasses[options.size]);
this.container.classList.add('l-overlay-wrapper', cssClasses[size]);
this.autoHide = autoHide;
this.dismissable = dismissable !== false;
this.component = new Vue({
provide: {
dismiss: this.dismiss.bind(this),
element: options.element,
buttons: options.buttons,
dismissable: this.dismissable
},
components: {
OverlayComponent: OverlayComponent
},
provide: {
dismiss: this.dismiss.bind(this),
element,
buttons,
dismissable: this.dismissable
},
template: '<overlay-component></overlay-component>'
});
if (options.onDestroy) {
this.once('destroy', options.onDestroy);
if (onDestroy) {
this.once('destroy', onDestroy);
}
}

View File

@@ -17,7 +17,11 @@ class OverlayAPI {
this.dismissLastOverlay = this.dismissLastOverlay.bind(this);
document.addEventListener('keyup', this.dismissLastOverlay);
document.addEventListener('keyup', (event) => {
if (event.key === 'Escape') {
this.dismissLastOverlay();
}
});
}
@@ -26,7 +30,10 @@ class OverlayAPI {
*/
showOverlay(overlay) {
if (this.activeOverlays.length) {
this.activeOverlays[this.activeOverlays.length - 1].container.classList.add('invisible');
const previousOverlay = this.activeOverlays[this.activeOverlays.length - 1];
if (previousOverlay.autoHide) {
previousOverlay.container.classList.add('invisible');
}
}
this.activeOverlays.push(overlay);
@@ -45,12 +52,10 @@ class OverlayAPI {
/**
* private
*/
dismissLastOverlay(event) {
if (event.key === 'Escape') {
let lastOverlay = this.activeOverlays[this.activeOverlays.length - 1];
if (lastOverlay && lastOverlay.dismissable) {
lastOverlay.dismiss();
}
dismissLastOverlay() {
let lastOverlay = this.activeOverlays[this.activeOverlays.length - 1];
if (lastOverlay && lastOverlay.dismissable) {
lastOverlay.dismiss();
}
}
@@ -58,7 +63,7 @@ class OverlayAPI {
* A description of option properties that can be passed into the overlay
* @typedef options
* @property {object} element DOMElement that is to be inserted/shown on the overlay
* @property {string} size prefered size of the overlay (large, small, fit)
* @property {string} size preferred size of the overlay (large, small, fit)
* @property {array} buttons optional button objects with label and callback properties
* @property {function} onDestroy callback to be called when overlay is destroyed
* @property {boolean} dismissable allow user to dismiss overlay by using esc, and clicking away
@@ -127,10 +132,6 @@ class OverlayAPI {
return progressDialog;
}
destroy() {
document.removeEventListener('keyup', this.dismissLastOverlay);
}
}
export default OverlayAPI;

View File

@@ -32,10 +32,6 @@ export default class StatusAPI extends EventEmitter {
this.get = this.get.bind(this);
this.set = this.set.bind(this);
this.observe = this.observe.bind(this);
openmct.once('destroy', () => {
this.removeAllListeners();
});
}
get(identifier) {

View File

@@ -180,12 +180,6 @@ define([
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/
TelemetryAPI.prototype.canProvideTelemetry = function (domainObject) {
console.warn(
'DEPRECATION WARNING: openmct.telemetry.canProvideTelemetry '
+ 'will not be supported in future versions of Open MCT. Please '
+ 'use openmct.telemetry.isTelemetryObject instead.'
);
return Boolean(this.findSubscriptionProvider(domainObject))
|| Boolean(this.findRequestProvider(domainObject));
};

View File

@@ -97,7 +97,7 @@ describe('Telemetry API', function () {
expect(telemetryProvider.subscribe).not.toHaveBeenCalled();
expect(unsubscribe).toEqual(jasmine.any(Function));
return telemetryAPI.request(domainObject).then((response) => {
telemetryAPI.request(domainObject).then((response) => {
expect(telemetryProvider.supportsRequest)
.toHaveBeenCalledWith(domainObject, jasmine.any(Object));
expect(telemetryProvider.request).not.toHaveBeenCalled();

View File

@@ -115,6 +115,7 @@ export class TelemetryCollection extends EventEmitter {
this._requestHistoricalTelemetry();
}
/**
* If a historical provider exists, then historical requests will be made
* @private
@@ -126,20 +127,25 @@ export class TelemetryCollection extends EventEmitter {
let historicalData;
this.options.onPartialResponse = this._processNewTelemetry.bind(this);
try {
this.requestAbort = new AbortController();
this.options.signal = this.requestAbort.signal;
historicalData = await this.historicalProvider.request(this.domainObject, this.options);
this.requestAbort = undefined;
} catch (error) {
console.error('Error requesting telemetry data...');
this.requestAbort = undefined;
this._error(error);
if (error.name !== 'AbortError') {
console.error('Error requesting telemetry data...');
this._error(error);
}
}
this.requestAbort = undefined;
this._processNewTelemetry(historicalData);
}
/**
* This uses the built in subscription function from Telemetry API
* @private
@@ -342,6 +348,8 @@ export class TelemetryCollection extends EventEmitter {
this.boundedTelemetry = [];
this.futureBuffer = [];
this.emit('clear');
this._requestHistoricalTelemetry();
}

View File

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

View File

@@ -0,0 +1,106 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TimeContext from "./TimeContext";
/**
* The GlobalContext handles getting and setting time of the openmct application in general.
* Views will use this context unless they specify an alternate/independent time context
*/
class GlobalTimeContext extends TimeContext {
constructor() {
super();
//The Time Of Interest
this.toi = undefined;
}
/**
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
bounds(newBounds) {
if (arguments.length > 0) {
super.bounds.call(this, ...arguments);
// If a bounds change results in a TOI outside of the current
// bounds, unset it
if (this.toi < newBounds.start || this.toi > newBounds.end) {
this.timeOfInterest(undefined);
}
}
//Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal));
}
/**
* Update bounds based on provided time and current offsets
* @private
* @param {number} timestamp A time from which bounds will be calculated
* using current offsets.
*/
tick(timestamp) {
super.tick.call(this, ...arguments);
// If a bounds change results in a TOI outside of the current
// bounds, unset it
if (this.toi < this.boundsVal.start || this.toi > this.boundsVal.end) {
this.timeOfInterest(undefined);
}
}
/**
* Get or set the Time of Interest. The Time of Interest is a single point
* in time, and constitutes the temporal focus of application views. It can
* be manipulated by the user from the time conductor or from other views.
* The time of interest can effectively be unset by assigning a value of
* 'undefined'.
* @fires module:openmct.TimeAPI~timeOfInterest
* @param newTOI
* @returns {number} the current time of interest
* @memberof module:openmct.TimeAPI#
* @method timeOfInterest
*/
timeOfInterest(newTOI) {
if (arguments.length > 0) {
this.toi = newTOI;
/**
* The Time of Interest has moved.
* @event timeOfInterest
* @memberof module:openmct.TimeAPI~
* @property {number} Current time of interest
*/
this.emit('timeOfInterest', this.toi);
}
return this.toi;
}
}
export default GlobalTimeContext;

View File

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

View File

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

View File

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

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

@@ -0,0 +1,360 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
class TimeContext extends EventEmitter {
constructor() {
super();
//The Time System
this.timeSystems = new Map();
this.system = undefined;
this.clocks = new Map();
this.boundsVal = {
start: undefined,
end: undefined
};
this.activeClock = undefined;
this.offsets = undefined;
this.tick = this.tick.bind(this);
}
/**
* Get or set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystem
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method timeSystem
*/
timeSystem(timeSystemOrKey, bounds) {
if (arguments.length >= 1) {
if (arguments.length === 1 && !this.activeClock) {
throw new Error(
"Must specify bounds when changing time system without "
+ "an active clock."
);
}
let timeSystem;
if (timeSystemOrKey === undefined) {
throw "Please provide a time system";
}
if (typeof timeSystemOrKey === 'string') {
timeSystem = this.timeSystems.get(timeSystemOrKey);
if (timeSystem === undefined) {
throw "Unknown time system " + timeSystemOrKey + ". Has it been registered with 'addTimeSystem'?";
}
} else if (typeof timeSystemOrKey === 'object') {
timeSystem = timeSystemOrKey;
if (!this.timeSystems.has(timeSystem.key)) {
throw "Unknown time system " + timeSystem.key + ". Has it been registered with 'addTimeSystem'?";
}
} else {
throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key";
}
this.system = timeSystem;
/**
* The time system used by the time
* conductor has changed. A change in Time System will always be
* followed by a bounds event specifying new query bounds.
*
* @event module:openmct.TimeAPI~timeSystem
* @property {TimeSystem} The value of the currently applied
* Time System
* */
this.emit('timeSystem', this.system);
if (bounds) {
this.bounds(bounds);
}
}
return this.system;
}
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {object} ValidationResult
* @property {boolean} valid Result of the validation - true or false.
* @property {string} message An error message if valid is false.
*/
/**
* Validate the given bounds. This can be used for pre-validation of bounds,
* for example by views validating user inputs.
* @param {TimeBounds} bounds The start and end time of the conductor.
* @returns {ValidationResult} A validation error, or true if valid
* @memberof module:openmct.TimeAPI#
* @method validateBounds
*/
validateBounds(bounds) {
if ((bounds.start === undefined)
|| (bounds.end === undefined)
|| isNaN(bounds.start)
|| isNaN(bounds.end)
) {
return {
valid: false,
message: "Start and end must be specified as integer values"
};
} else if (bounds.start > bounds.end) {
return {
valid: false,
message: "Specified start date exceeds end bound"
};
}
return {
valid: true,
message: ''
};
}
/**
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
bounds(newBounds) {
if (arguments.length > 0) {
const validationResult = this.validateBounds(newBounds);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
//Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
/**
* The start time, end time, or both have been updated.
* @event bounds
* @memberof module:openmct.TimeAPI~
* @property {TimeConductorBounds} bounds The newly updated bounds
* @property {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (ie. was an automatic update), false otherwise.
*/
this.emit('bounds', this.boundsVal, false);
}
//Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal));
}
/**
* Validate the given offsets. This can be used for pre-validation of
* offsets, for example by views validating user inputs.
* @param {ClockOffsets} offsets The start and end offsets from a 'now' value.
* @returns { ValidationResult } A validation error, and true/false if valid or not
* @memberof module:openmct.TimeAPI#
* @method validateOffsets
*/
validateOffsets(offsets) {
if ((offsets.start === undefined)
|| (offsets.end === undefined)
|| isNaN(offsets.start)
|| isNaN(offsets.end)
) {
return {
valid: false,
message: "Start and end offsets must be specified as integer values"
};
} else if (offsets.start >= offsets.end) {
return {
valid: false,
message: "Specified start offset must be < end offset"
};
}
return {
valid: true,
message: ''
};
}
/**
* @typedef {Object} TimeBounds
* @property {number} start The start time displayed by the time conductor
* in ms since epoch. Epoch determined by currently active time system
* @property {number} end The end time displayed by the time conductor in ms
* since epoch.
* @memberof module:openmct.TimeAPI~
*/
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {object} ClockOffsets
* @property {number} start A time span relative to the current value of the
* ticking clock, from which start bounds will be calculated. This value must
* be < 0. When a clock is active, bounds will be calculated automatically
* based on the value provided by the clock, and the defined clock offsets.
* @property {number} end A time span relative to the current value of the
* ticking clock, from which end bounds will be calculated. This value must
* be >= 0.
*/
/**
* Get or set the currently applied clock offsets. If no parameter is provided,
* the current value will be returned. If provided, the new value will be
* used as the new clock offsets.
* @param {ClockOffsets} offsets
* @returns {ClockOffsets}
*/
clockOffsets(offsets) {
if (arguments.length > 0) {
const validationResult = this.validateOffsets(offsets);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
this.offsets = offsets;
const currentValue = this.activeClock.currentValue();
const newBounds = {
start: currentValue + offsets.start,
end: currentValue + offsets.end
};
this.bounds(newBounds);
/**
* Event that is triggered when clock offsets change.
* @event clockOffsets
* @memberof module:openmct.TimeAPI~
* @property {ClockOffsets} clockOffsets The newly activated clock
* offsets.
*/
this.emit("clockOffsets", offsets);
}
return this.offsets;
}
/**
* Stop the currently active clock from ticking, and unset it. This will
* revert all views to showing a static time frame defined by the current
* bounds.
*/
stopClock() {
if (this.activeClock) {
this.clock(undefined, undefined);
}
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock
* can be unset by calling {@link stopClock}.
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds. This maintains a sliding time window of a fixed
* width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
clock(keyOrClock, offsets) {
if (arguments.length === 2) {
let clock;
if (typeof keyOrClock === 'string') {
clock = this.clocks.get(keyOrClock);
if (clock === undefined) {
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.clocks.has(clock.key)) {
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
}
}
const previousClock = this.activeClock;
if (previousClock !== undefined) {
previousClock.off("tick", this.tick);
}
this.activeClock = clock;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit("clock", this.activeClock);
if (this.activeClock !== undefined) {
this.clockOffsets(offsets);
this.activeClock.on("tick", this.tick);
}
} else if (arguments.length === 1) {
throw "When setting the clock, clock offsets must also be provided";
}
return this.activeClock;
}
/**
* Update bounds based on provided time and current offsets
* @param {number} timestamp A time from which bounds will be calculated
* using current offsets.
*/
tick(timestamp) {
if (!this.activeClock) {
return;
}
const newBounds = {
start: timestamp + this.offsets.start,
end: timestamp + this.offsets.end
};
this.boundsVal = newBounds;
this.emit('bounds', this.boundsVal, true);
}
}
export default TimeContext;

View File

@@ -0,0 +1,155 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TimeAPI from "./TimeAPI";
import {createOpenMct} from "utils/testing";
describe("The Independent Time API", function () {
let api;
let domainObjectKey;
let clockKey;
let clock;
let bounds;
let independentBounds;
let eventListener;
let openmct;
beforeEach(function () {
openmct = createOpenMct();
api = new TimeAPI(openmct);
clockKey = "someClockKey";
clock = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
clock.currentValue.and.returnValue(100);
clock.key = clockKey;
api.addClock(clock);
domainObjectKey = 'test-key';
bounds = {
start: 0,
end: 1
};
api.bounds(bounds);
independentBounds = {
start: 10,
end: 11
};
eventListener = jasmine.createSpy("eventListener");
});
it("Creates an independent time context", () => {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getIndependentContext(domainObjectKey);
expect(timeContext.bounds()).toEqual(independentBounds);
destroyTimeContext();
});
it("Gets an independent time context given the objectPath", () => {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{
identifier: {
namespace: '',
key: 'blah'
}
}, { identifier: domainObjectKey }]);
expect(timeContext.bounds()).toEqual(independentBounds);
destroyTimeContext();
});
it("defaults to the global time context given the objectPath", () => {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{
identifier: {
namespace: '',
key: 'blah'
}
}]);
expect(timeContext.bounds()).toEqual(bounds);
destroyTimeContext();
});
it("Allows setting of valid bounds", function () {
bounds = {
start: 0,
end: 1
};
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(timeContext.bounds()).not.toEqual(bounds);
timeContext.bounds(bounds);
expect(timeContext.bounds()).toEqual(bounds);
destroyTimeContext();
});
it("Disallows setting of invalid bounds", function () {
bounds = {
start: 1,
end: 0
};
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(timeContext.bounds()).not.toBe(bounds);
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
expect(timeContext.bounds()).not.toEqual(bounds);
bounds = {start: 1};
expect(timeContext.bounds()).not.toEqual(bounds);
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
expect(timeContext.bounds()).not.toEqual(bounds);
destroyTimeContext();
});
it("Emits an event when bounds change", function () {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(eventListener).not.toHaveBeenCalled();
timeContext.on('bounds', eventListener);
timeContext.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
destroyTimeContext();
});
describe(" when using real time clock", function () {
const mockOffsets = {
start: 10,
end: 11
};
it("Emits an event when bounds change based on current value", function () {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(eventListener).not.toHaveBeenCalled();
timeContext.clock('someClockKey', mockOffsets);
timeContext.on('bounds', eventListener);
timeContext.tick(10);
expect(eventListener).toHaveBeenCalledWith({
start: 20,
end: 21
}, true);
destroyTimeContext();
});
});
});

View File

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

View File

@@ -90,7 +90,7 @@ class ImageExporter {
element.id = oldId;
},
removeContainer: true // Set to false to debug what html2canvas renders
}).then(function (canvas) {
}).then(canvas => {
dialog.dismiss();
return new Promise(function (resolve, reject) {
@@ -105,9 +105,10 @@ class ImageExporter {
return canvas.toBlob(blob => resolve({ blob }), mimeType);
});
}, function (error) {
console.log('error capturing image', error);
}).catch(error => {
dialog.dismiss();
console.error('error capturing image', error);
const errorDialog = overlays.dialog({
iconClass: 'error',
message: 'Image was not captured successfully!',

View File

@@ -0,0 +1,59 @@
/*****************************************************************************
* 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 Clock from './components/Clock.vue';
import Vue from 'vue';
export default function ClockViewProvider(openmct) {
return {
key: 'clock.view',
name: 'Clock',
cssClass: 'icon-clock',
canView(domainObject) {
return domainObject.type === 'clock';
},
view: function (domainObject) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
Clock
},
provide: {
openmct,
domainObject
},
template: '<clock />'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
}
};
}

View File

@@ -0,0 +1,99 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div class="l-angular-ov-wrapper">
<div class="u-contents">
<div class="c-clock l-time-display u-style-receiver js-style-receiver">
<div class="c-clock__timezone">
{{ timeZoneAbbr }}
</div>
<div class="c-clock__value">
{{ timeTextValue }}
</div>
<div class="c-clock__ampm">
{{ timeAmPm }}
</div>
</div>
</div>
</div>
</template>
<script>
import moment from 'moment';
import momentTimezone from 'moment-timezone';
export default {
inject: ['openmct', 'domainObject'],
data() {
return {
lastTimestamp: null
};
},
computed: {
configuration() {
return this.domainObject.configuration;
},
baseFormat() {
return this.configuration.baseFormat;
},
use24() {
return this.configuration.use24 === 'clock24';
},
timezone() {
return this.configuration.timezone;
},
timeFormat() {
return this.use24 ? this.baseFormat.replace('hh', "HH") : this.baseFormat;
},
zoneName() {
return momentTimezone.tz.names().includes(this.timezone) ? this.timezone : "UTC";
},
momentTime() {
return this.zoneName ? moment.utc(this.lastTimestamp).tz(this.zoneName) : moment.utc(this.lastTimestamp);
},
timeZoneAbbr() {
return this.momentTime.zoneAbbr();
},
timeTextValue() {
return this.timeFormat && this.momentTime.format(this.timeFormat);
},
timeAmPm() {
return this.use24 ? '' : this.momentTime.format("A");
}
},
mounted() {
const TickerService = this.openmct.$injector.get('tickerService');
this.unlisten = TickerService.listen(this.tick);
},
beforeDestroy() {
if (this.unlisten) {
this.unlisten();
}
},
methods: {
tick(timestamp) {
this.lastTimestamp = timestamp;
}
}
};
</script>

View File

@@ -0,0 +1,64 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div class="c-indicator t-indicator-clock icon-clock no-minify c-indicator--not-clickable">
<span class="label c-indicator__label">
{{ timeTextValue }}
</span>
</div>
</template>
<script>
import moment from 'moment';
export default {
inject: ['openmct'],
props: {
indicatorFormat: {
type: String,
required: true
}
},
data() {
return {
timeTextValue: null
};
},
mounted() {
this.openmct.on('start', () => {
const TickerService = this.openmct.$injector.get('tickerService');
this.unlisten = TickerService.listen(this.tick);
});
},
beforeDestroy() {
if (this.unlisten) {
this.unlisten();
}
},
methods: {
tick(timestamp) {
this.timeTextValue = `${moment.utc(timestamp).format(this.indicatorFormat)} UTC`;
}
}
};
</script>

154
src/plugins/clock/plugin.js Normal file
View File

@@ -0,0 +1,154 @@
/*****************************************************************************
* 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 ClockViewProvider from './ClockViewProvider';
import ClockIndicator from './components/ClockIndicator.vue';
import momentTimezone from 'moment-timezone';
import Vue from 'vue';
export default function ClockPlugin(options) {
return function install(openmct) {
const CLOCK_INDICATOR_FORMAT = 'YYYY/MM/DD HH:mm:ss';
openmct.types.addType('clock', {
name: 'Clock',
description: 'A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts.',
creatable: true,
cssClass: 'icon-clock',
initialize: function (domainObject) {
domainObject.configuration = {
baseFormat: 'YYYY/MM/DD hh:mm:ss',
use24: 'clock12',
timezone: 'UTC'
};
},
"form": [
{
"key": "displayFormat",
"name": "Display Format",
control: 'select',
options: [
{
value: 'YYYY/MM/DD hh:mm:ss',
name: 'YYYY/MM/DD hh:mm:ss'
},
{
value: 'YYYY/DDD hh:mm:ss',
name: 'YYYY/DDD hh:mm:ss'
},
{
value: 'hh:mm:ss',
name: 'hh:mm:ss'
}
],
cssClass: 'l-inline',
property: [
'configuration',
'baseFormat'
]
},
{
control: 'select',
options: [
{
value: 'clock12',
name: '12hr'
},
{
value: 'clock24',
name: '24hr'
}
],
cssClass: 'l-inline',
property: [
'configuration',
'use24'
]
},
{
"key": "timezone",
"name": "Timezone",
"control": "autocomplete",
"options": momentTimezone.tz.names(),
property: [
'configuration',
'timezone'
]
}
]
});
openmct.objectViews.addProvider(new ClockViewProvider(openmct));
if (options && options.enableClockIndicator === true) {
const clockIndicator = new Vue ({
components: {
ClockIndicator
},
provide: {
openmct
},
data() {
return {
indicatorFormat: CLOCK_INDICATOR_FORMAT
};
},
template: '<ClockIndicator :indicator-format="indicatorFormat" />'
});
const indicator = {
element: clockIndicator.$mount().$el,
key: 'clock-indicator'
};
openmct.indicators.add(indicator);
}
openmct.objects.addGetInterceptor({
appliesTo: (identifier, domainObject) => {
return domainObject && domainObject.type === 'clock';
},
invoke: (identifier, domainObject) => {
if (domainObject.configuration) {
return domainObject;
}
if (domainObject.clockFormat
&& domainObject.timezone) {
const baseFormat = domainObject.clockFormat[0];
const use24 = domainObject.clockFormat[1];
const timezone = domainObject.timezone;
domainObject.configuration = {
baseFormat,
use24,
timezone
};
openmct.objects.mutate(domainObject, 'configuration', domainObject.configuration);
}
return domainObject;
}
});
};
}

View File

@@ -0,0 +1,231 @@
/*****************************************************************************
* 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 clockPlugin from './plugin';
import Vue from 'vue';
describe("Clock plugin:", () => {
let openmct;
let clockDefinition;
let element;
let child;
let appHolder;
let clockDomainObject;
function setupClock(enableClockIndicator) {
return new Promise((resolve, reject) => {
clockDomainObject = {
identifier: {
key: 'clock',
namespace: 'test-namespace'
},
type: 'clock'
};
appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
document.body.appendChild(appHolder);
openmct = createOpenMct();
element = document.createElement('div');
child = document.createElement('div');
element.appendChild(child);
openmct.install(clockPlugin({ enableClockIndicator }));
clockDefinition = openmct.types.get('clock').definition;
clockDefinition.initialize(clockDomainObject);
openmct.on('start', resolve);
openmct.start(appHolder);
});
}
describe("Clock view:", () => {
let clockViewProvider;
let clockView;
let clockViewObject;
let mutableClockObject;
beforeEach(async () => {
await setupClock(true);
clockViewObject = {
...clockDomainObject,
id: "test-object",
name: 'Clock',
configuration: {
baseFormat: 'YYYY/MM/DD hh:mm:ss',
use24: 'clock12',
timezone: 'UTC'
}
};
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(clockViewObject));
spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true));
const applicableViews = openmct.objectViews.get(clockViewObject, [clockViewObject]);
clockViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'clock.view');
mutableClockObject = await openmct.objects.getMutable(clockViewObject.identifier);
clockView = clockViewProvider.view(mutableClockObject);
clockView.show(child);
await Vue.nextTick();
});
afterEach(() => {
clockView.destroy();
openmct.objects.destroyMutable(mutableClockObject);
if (appHolder) {
appHolder.remove();
}
return resetApplicationState(openmct);
});
it("has name as Clock", () => {
expect(clockDefinition.name).toEqual('Clock');
});
it("is creatable", () => {
expect(clockDefinition.creatable).toEqual(true);
});
it("provides clock view", () => {
expect(clockViewProvider).toBeDefined();
});
it("renders clock element", () => {
const clockElement = element.querySelectorAll('.c-clock');
expect(clockElement.length).toBe(1);
});
it("renders major elements", () => {
const clockElement = element.querySelector('.c-clock');
const timezone = clockElement.querySelector('.c-clock__timezone');
const time = clockElement.querySelector('.c-clock__value');
const amPm = clockElement.querySelector('.c-clock__ampm');
const hasMajorElements = Boolean(timezone && time && amPm);
expect(hasMajorElements).toBe(true);
});
it("renders time in UTC", () => {
const clockElement = element.querySelector('.c-clock');
const timezone = clockElement.querySelector('.c-clock__timezone').textContent.trim();
expect(timezone).toBe('UTC');
});
it("updates the 24 hour option in the configuration", (done) => {
expect(clockDomainObject.configuration.use24).toBe('clock12');
const new24Option = 'clock24';
openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => {
expect(changedDomainObject.use24).toBe(new24Option);
done();
});
openmct.objects.mutate(clockViewObject, 'configuration.use24', new24Option);
});
it("updates the timezone option in the configuration", (done) => {
expect(clockDomainObject.configuration.timezone).toBe('UTC');
const newZone = 'CST6CDT';
openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => {
expect(changedDomainObject.timezone).toBe(newZone);
done();
});
openmct.objects.mutate(clockViewObject, 'configuration.timezone', newZone);
});
it("updates the time format option in the configuration", (done) => {
expect(clockDomainObject.configuration.baseFormat).toBe('YYYY/MM/DD hh:mm:ss');
const newFormat = 'hh:mm:ss';
openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => {
expect(changedDomainObject.baseFormat).toBe(newFormat);
done();
});
openmct.objects.mutate(clockViewObject, 'configuration.baseFormat', newFormat);
});
});
describe("Clock Indicator view:", () => {
let clockIndicator;
afterEach(() => {
if (clockIndicator) {
clockIndicator.remove();
}
clockIndicator = undefined;
if (appHolder) {
appHolder.remove();
}
return resetApplicationState(openmct);
});
it("doesn't exist", async () => {
await setupClock(false);
clockIndicator = openmct.indicators.indicatorObjects
.find(indicator => indicator.key === 'clock-indicator');
const clockIndicatorMissing = clockIndicator === null || clockIndicator === undefined;
expect(clockIndicatorMissing).toBe(true);
});
it("exists", async () => {
await setupClock(true);
clockIndicator = openmct.indicators.indicatorObjects
.find(indicator => indicator.key === 'clock-indicator').element;
const hasClockIndicator = clockIndicator !== null && clockIndicator !== undefined;
expect(hasClockIndicator).toBe(true);
});
it("contains text", async () => {
await setupClock(true);
clockIndicator = openmct.indicators.indicatorObjects
.find(indicator => indicator.key === 'clock-indicator').element;
const clockIndicatorText = clockIndicator.textContent.trim();
const textIncludesUTC = clockIndicatorText.includes('UTC');
expect(textIncludesUTC).toBe(true);
});
});
});

View File

@@ -30,10 +30,10 @@
<div v-if="staticStyle"
class="c-inspect-styles__style"
>
<style-editor class="c-inspect-styles__editor"
:style-item="staticStyle"
:is-editing="isEditing"
@persist="updateStaticStyle"
<StyleEditor class="c-inspect-styles__editor"
:style-item="staticStyle"
:is-editing="isEditing"
@persist="updateStaticStyle"
/>
</div>
<button
@@ -87,10 +87,10 @@
<condition-description :show-label="true"
:condition="getCondition(conditionStyle.conditionId)"
/>
<style-editor class="c-inspect-styles__editor"
:style-item="conditionStyle"
:is-editing="isEditing"
@persist="updateConditionalStyle"
<StyleEditor class="c-inspect-styles__editor"
:style-item="conditionStyle"
:is-editing="isEditing"
@persist="updateConditionalStyle"
/>
</div>
</div>
@@ -240,10 +240,10 @@ export default {
}
let vm = new Vue({
components: {ConditionSetSelectorDialog},
provide: {
openmct: this.openmct
},
components: {ConditionSetSelectorDialog},
data() {
return {
handleItemSelection
@@ -273,10 +273,7 @@ export default {
this.openmct.objects.getOriginalPath(this.conditionSetDomainObject.identifier).then(
(objectPath) => {
this.objectPath = objectPath;
this.navigateToPath = '#/browse/' + this.objectPath
.map(o => o && this.openmct.objects.makeKeyString(o.identifier))
.reverse()
.join('/');
this.navigateToPath = '#/browse/' + this.openmct.objects.getRelativePath(this.objectPath);
}
);
},

View File

@@ -40,13 +40,13 @@
<div v-if="staticStyle"
class="c-inspect-styles__style"
>
<style-editor class="c-inspect-styles__editor"
:style-item="staticStyle"
:is-editing="allowEditing"
:mixed-styles="mixedStyles"
:non-specific-font-properties="nonSpecificFontProperties"
@persist="updateStaticStyle"
@save-style="saveStyle"
<StyleEditor class="c-inspect-styles__editor"
:style-item="staticStyle"
:is-editing="allowEditing"
:mixed-styles="mixedStyles"
:non-specific-font-properties="nonSpecificFontProperties"
@persist="updateStaticStyle"
@save-style="saveStyle"
/>
</div>
<button
@@ -108,12 +108,12 @@
<condition-description :show-label="true"
:condition="getCondition(conditionStyle.conditionId)"
/>
<style-editor class="c-inspect-styles__editor"
:style-item="conditionStyle"
:non-specific-font-properties="nonSpecificFontProperties"
:is-editing="allowEditing"
@persist="updateConditionalStyle"
@save-style="saveStyle"
<StyleEditor class="c-inspect-styles__editor"
:style-item="conditionStyle"
:non-specific-font-properties="nonSpecificFontProperties"
:is-editing="allowEditing"
@persist="updateConditionalStyle"
@save-style="saveStyle"
/>
</div>
</div>
@@ -297,10 +297,7 @@ export default {
this.openmct.objects.getOriginalPath(this.conditionSetDomainObject.identifier).then(
(objectPath) => {
this.objectPath = objectPath;
this.navigateToPath = '#/browse/' + this.objectPath
.map(o => o && this.openmct.objects.makeKeyString(o.identifier))
.reverse()
.join('/');
this.navigateToPath = '#/browse/' + this.openmct.objects.getRelativePath(this.objectPath);
}
);
},
@@ -559,10 +556,10 @@ export default {
}
let vm = new Vue({
components: {ConditionSetSelectorDialog},
provide: {
openmct: this.openmct
},
components: {ConditionSetSelectorDialog},
data() {
return {
handleItemSelection

View File

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

View File

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

View File

@@ -0,0 +1,73 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ImageryTimeView from './components/ImageryTimeView.vue';
import Vue from "vue";
export default function ImageryTimestripViewProvider(openmct) {
const type = 'example.imagery.time-strip.view';
function hasImageTelemetry(domainObject) {
const metadata = openmct.telemetry.getMetadata(domainObject);
if (!metadata) {
return false;
}
return metadata.valuesForHints(['image']).length > 0;
}
return {
key: type,
name: 'Imagery Timestrip View',
cssClass: 'icon-image',
canView: function (domainObject, objectPath) {
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
return hasImageTelemetry(domainObject) && isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
},
view: function (domainObject, objectPath) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
ImageryTimeView
},
provide: {
openmct: openmct,
domainObject: domainObject,
objectPath: objectPath
},
template: '<imagery-time-view></imagery-time-view>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
}
};
}

View File

@@ -1,4 +1,4 @@
import ImageryViewLayout from './components/ImageryViewLayout.vue';
import ImageryViewComponent from './components/ImageryView.vue';
import Vue from 'vue';
@@ -14,7 +14,7 @@ export default class ImageryView {
this.component = new Vue({
el: element,
components: {
ImageryViewLayout
'imagery-view': ImageryViewComponent
},
provide: {
openmct: this.openmct,
@@ -22,7 +22,8 @@ export default class ImageryView {
objectPath: this.objectPath,
currentView: this
},
template: '<imagery-view-layout ref="ImageryLayout"></imagery-view-layout>'
template: '<imagery-view ref="ImageryContainer"></imagery-view>'
});
}

View File

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

View File

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

View File

@@ -0,0 +1,475 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div ref="imagery"
class="c-imagery-tsv c-timeline-holder"
>
<div ref="imageryHolder"
class="c-imagery-tsv__contents u-contents"
>
</div>
</div>
</template>
<script>
import * as d3Scale from 'd3-scale';
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
import Vue from "vue";
import imageryData from "../../imagery/mixins/imageryData";
import PreviewAction from "@/ui/preview/PreviewAction";
import _ from "lodash";
const PADDING = 1;
const ROW_HEIGHT = 100;
const IMAGE_WIDTH_THRESHOLD = 40;
export default {
mixins: [imageryData],
inject: ['openmct', 'domainObject', 'objectPath'],
data() {
let timeSystem = this.openmct.time.timeSystem();
this.metadata = {};
this.requestCount = 0;
return {
viewBounds: undefined,
height: 0,
durationFormatter: undefined,
imageHistory: [],
timeSystem: timeSystem,
keyString: undefined
};
},
computed: {
imageHistorySize() {
return this.imageHistory.length;
}
},
watch: {
imageHistorySize(newSize, oldSize) {
this.updatePlotImagery();
}
},
mounted() {
this.previewAction = new PreviewAction(this.openmct);
this.canvas = this.$refs.imagery.appendChild(document.createElement('canvas'));
this.canvas.height = 0;
this.canvasContext = this.canvas.getContext('2d');
this.setDimensions();
this.updateViewBounds();
this.openmct.time.on("timeSystem", this.setScaleAndPlotImagery);
this.openmct.time.on("bounds", this.updateViewBounds);
this.resize = _.debounce(this.resize, 400);
this.imageryStripResizeObserver = new ResizeObserver(this.resize);
this.imageryStripResizeObserver.observe(this.$refs.imagery);
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
},
beforeDestroy() {
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
}
if (this.imageryStripResizeObserver) {
this.imageryStripResizeObserver.disconnect();
}
this.openmct.time.off("timeSystem", this.setScaleAndPlotImagery);
this.openmct.time.off("bounds", this.updateViewBounds);
if (this.unlisten) {
this.unlisten();
}
},
methods: {
expand(index) {
const path = this.objectPath[0];
this.previewAction.invoke([path]);
},
observeForChanges(mutatedObject) {
this.updateViewBounds();
},
resize() {
let clientWidth = this.getClientWidth();
if (clientWidth !== this.width) {
this.setDimensions();
this.updateViewBounds();
}
},
getClientWidth() {
let clientWidth = this.$refs.imagery.clientWidth;
if (!clientWidth) {
//this is a hack - need a better way to find the parent of this component
let parent = this.openmct.layout.$refs.browseObject.$el;
if (parent) {
clientWidth = parent.getBoundingClientRect().width;
}
}
return clientWidth;
},
updateViewBounds(bounds, isTick) {
this.viewBounds = this.openmct.time.bounds();
//Add a 50% padding to the end bounds to look ahead
let timespan = (this.viewBounds.end - this.viewBounds.start);
let padding = timespan / 2;
this.viewBounds.end = this.viewBounds.end + padding;
if (this.timeSystem === undefined) {
this.timeSystem = this.openmct.time.timeSystem();
}
this.setScaleAndPlotImagery(this.timeSystem, !isTick);
},
setScaleAndPlotImagery(timeSystem, clearAllImagery) {
if (timeSystem !== undefined) {
this.timeSystem = timeSystem;
this.timeFormatter = this.getFormatter(this.timeSystem.key);
}
this.setScale(this.timeSystem);
this.updatePlotImagery(clearAllImagery);
},
getFormatter(key) {
const metadata = this.openmct.telemetry.getMetadata(this.domainObject);
let metadataValue = metadata.value(key) || { format: key };
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
return valueFormatter;
},
updatePlotImagery(clearAllImagery) {
this.clearPreviousImagery(clearAllImagery);
if (this.xScale) {
this.drawImagery();
}
},
clearPreviousImagery(clearAllImagery) {
//TODO: Only clear items that are out of bounds
let noItemsEl = this.$el.querySelectorAll(".c-imagery-tsv__no-items");
noItemsEl.forEach(item => {
item.remove();
});
let imagery = this.$el.querySelectorAll(".c-imagery-tsv__image-wrapper");
imagery.forEach(item => {
if (clearAllImagery) {
item.remove();
} else {
const id = this.getNSAttributesForElement(item, 'id');
if (id) {
const timestamp = id.replace('id-', '');
if (!this.isImageryInBounds({
time: timestamp
})) {
item.remove();
}
}
}
});
},
setDimensions() {
const imageryHolder = this.$refs.imagery;
this.width = this.getClientWidth();
this.height = Math.round(imageryHolder.getBoundingClientRect().height);
},
setScale(timeSystem) {
if (!this.width) {
return;
}
if (timeSystem === undefined) {
timeSystem = this.openmct.time.timeSystem();
}
if (timeSystem.isUTCBased) {
this.xScale = d3Scale.scaleUtc();
this.xScale.domain(
[new Date(this.viewBounds.start), new Date(this.viewBounds.end)]
);
} else {
this.xScale = d3Scale.scaleLinear();
this.xScale.domain(
[this.viewBounds.start, this.viewBounds.end]
);
}
this.xScale.range([PADDING, this.width - PADDING * 2]);
},
isImageryInBounds(imageObj) {
return (imageObj.time < this.viewBounds.end) && (imageObj.time > this.viewBounds.start);
},
getImageryContainer() {
let svgHeight = 100;
let svgWidth = this.imageHistory.length ? this.width : 200;
let groupSVG;
let existingSVG = this.$el.querySelector(".c-imagery-tsv__contents svg");
if (existingSVG) {
groupSVG = existingSVG;
this.setNSAttributesForElement(groupSVG, {
width: svgWidth
});
} else {
let component = new Vue({
components: {
SwimLane
},
provide: {
openmct: this.openmct
},
data() {
return {
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>`
});
this.$refs.imageryHolder.appendChild(component.$mount().$el);
groupSVG = component.$el.querySelector('svg');
groupSVG.addEventListener('mouseout', (event) => {
if (event.target.nodeName === 'svg' || event.target.nodeName === 'use') {
this.removeFromForeground();
}
});
}
return groupSVG;
},
isImageryWidthAcceptable() {
// We're calculating if there is enough space between images to show the thumbnails.
// This algorithm could probably be enhanced to check the x co-ordinate distance between 2 consecutive images, but
// we will go with this for now assuming imagery is not sorted by asc time so it's difficult to calculate.
// TODO: Use telemetry.requestCollection to get sorted telemetry
const currentStart = this.viewBounds.start;
const currentEnd = this.viewBounds.end;
const rectX = this.xScale(currentStart);
const rectY = this.xScale(currentEnd);
const imageContainerWidth = this.imageHistory.length ? (rectY - rectX) / this.imageHistory.length : 0;
return imageContainerWidth < IMAGE_WIDTH_THRESHOLD;
},
drawImagery() {
let groupSVG = this.getImageryContainer();
const showImagePlaceholders = this.isImageryWidthAcceptable();
if (this.imageHistory.length) {
this.imageHistory.forEach((currentImageObject, index) => {
if (this.isImageryInBounds(currentImageObject)) {
this.plotImagery(currentImageObject, showImagePlaceholders, groupSVG, index);
}
});
} else {
this.plotNoItems(groupSVG);
}
},
plotNoItems(svgElement) {
let textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
this.setNSAttributesForElement(textElement, {
x: "10",
y: "20",
class: "c-imagery-tsv__no-items"
});
textElement.innerHTML = 'No images within timeframe';
svgElement.appendChild(textElement);
},
setNSAttributesForElement(element, attributes) {
Object.keys(attributes).forEach((key) => {
if (key === 'url') {
element.setAttributeNS('http://www.w3.org/1999/xlink', 'href', attributes[key]);
} else {
element.setAttributeNS(null, key, attributes[key]);
}
});
},
getNSAttributesForElement(element, attribute) {
return element.getAttributeNS(null, attribute);
},
getImageWrapper(item) {
const id = `id-${item.time}`;
return this.$el.querySelector(`.c-imagery-tsv__contents g[id=${id}]`);
},
plotImagery(item, showImagePlaceholders, svgElement, index) {
//TODO: Placeholder image
let existingImageWrapper = this.getImageWrapper(item);
//imageWrapper wraps the vertical tick rect and the image
if (existingImageWrapper) {
this.updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders);
} else {
let imageWrapper = this.createImageWrapper(index, item, showImagePlaceholders, svgElement);
svgElement.appendChild(imageWrapper);
}
},
updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) {
//Update the x co-ordinates of the handle and image elements and the url of image
//this is to avoid tearing down all elements completely and re-drawing them
this.setNSAttributesForElement(existingImageWrapper, {
showImagePlaceholders
});
let imageTickElement = existingImageWrapper.querySelector('rect.c-imagery-tsv__image-handle');
this.setNSAttributesForElement(imageTickElement, {
x: this.xScale(item.time)
});
let imageRect = existingImageWrapper.querySelector('rect.c-imagery-tsv__image-placeholder');
this.setNSAttributesForElement(imageRect, {
x: this.xScale(item.time) + 2
});
let imageElement = existingImageWrapper.querySelector('image');
const selector = `href*=${existingImageWrapper.id}`;
let hoverEl = this.$el.querySelector(`.c-imagery-tsv__contents use[${selector}]`);
const hideImageUrl = (showImagePlaceholders && !hoverEl);
this.setNSAttributesForElement(imageElement, {
x: this.xScale(item.time) + 2,
url: hideImageUrl ? '' : item.url
});
},
createImageWrapper(index, item, showImagePlaceholders, svgElement) {
const id = `id-${item.time}`;
const imgSize = String(ROW_HEIGHT - 15);
let imageWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
this.setNSAttributesForElement(imageWrapper, {
id,
class: 'c-imagery-tsv__image-wrapper',
showImagePlaceholders
});
//create image tick indicator
let imageTickElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
this.setNSAttributesForElement(imageTickElement, {
class: 'c-imagery-tsv__image-handle',
x: this.xScale(item.time),
y: 5,
rx: 0,
width: 2,
height: String(ROW_HEIGHT - 10)
});
imageWrapper.appendChild(imageTickElement);
let imageRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
this.setNSAttributesForElement(imageRect, {
class: 'c-imagery-tsv__image-placeholder',
x: this.xScale(item.time) + 2,
y: 10,
rx: 0,
width: imgSize,
height: imgSize,
mask: `#image-${item.time}`
});
imageWrapper.appendChild(imageRect);
let imageElement = document.createElementNS('http://www.w3.org/2000/svg', 'image');
this.setNSAttributesForElement(imageElement, {
id: `image-${item.time}`,
x: this.xScale(item.time) + 2,
y: 10,
rx: 0,
width: imgSize,
height: imgSize,
url: showImagePlaceholders ? '' : item.url
});
imageWrapper.appendChild(imageElement);
//TODO: Don't add the hover listener if the width is too small
imageWrapper.addEventListener('mouseover', this.bringToForeground.bind(this, svgElement, imageWrapper, index, item.url));
return imageWrapper;
},
bringToForeground(svgElement, imageWrapper, index, url, event) {
const selector = `href*=${imageWrapper.id}`;
let hoverEls = this.$el.querySelectorAll(`.c-imagery-tsv__contents use:not([${selector}])`);
if (hoverEls.length > 0) {
this.removeFromForeground(hoverEls);
}
hoverEls = this.$el.querySelectorAll(`.c-imagery-tsv__contents use[${selector}]`);
if (hoverEls.length) {
return;
}
let imageElement = imageWrapper.querySelector('image');
this.setNSAttributesForElement(imageElement, {
url: url,
fill: 'none'
});
let hoverElement = document.createElementNS('http://www.w3.org/2000/svg', 'use');
this.setNSAttributesForElement(hoverElement, {
class: 'image-highlight',
x: 0,
href: `#${imageWrapper.id}`
});
this.setNSAttributesForElement(imageWrapper, {
class: 'c-imagery-tsv__image-wrapper is-hovered'
});
// We're using mousedown here and not 'click' because 'click' doesn't seem to be triggered reliably
hoverElement.addEventListener('mousedown', (e) => {
if (e.button === 0) {
this.expand(index);
}
});
svgElement.appendChild(hoverElement);
},
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();
});
}
}
};
</script>

View File

@@ -84,18 +84,18 @@
/>
</div>
</div>
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
<button class="c-nav c-nav--prev"
title="Previous image"
:disabled="isPrevDisabled"
@click="prevImage()"
></button>
<button class="c-nav c-nav--next"
title="Next image"
:disabled="isNextDisabled"
@click="nextImage()"
></button>
</div>
<button class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-button c-nav c-nav--prev"
title="Previous image"
:disabled="isPrevDisabled"
@click="prevImage()"
></button>
<button class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-button c-nav c-nav--next"
title="Next image"
:disabled="isNextDisabled"
@click="nextImage()"
></button>
<div class="c-imagery__control-bar">
<div class="c-imagery__time">
@@ -129,12 +129,11 @@
</div>
</div>
</div>
<div
class="c-imagery__thumbs-wrapper"
:class="[
{ 'is-paused': isPaused },
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
]"
<div class="c-imagery__thumbs-wrapper"
:class="[
{ 'is-paused': isPaused },
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
]"
>
<div
ref="thumbsWrapper"
@@ -175,7 +174,8 @@ import moment from 'moment';
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
import Compass from './Compass/Compass.vue';
const DEFAULT_DURATION_FORMATTER = 'duration';
import imageryData from "../../imagery/mixins/imageryData";
const REFRESH_CSS_MS = 500;
const DURATION_TRACK_MS = 1000;
const ARROW_DOWN_DELAY_CHECK_MS = 400;
@@ -197,30 +197,29 @@ export default {
components: {
Compass
},
mixins: [imageryData],
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
data() {
let timeSystem = this.openmct.time.timeSystem();
this.metadata = {};
this.requestCount = 0;
return {
autoScroll: true,
durationFormatter: undefined,
imageHistory: [],
timeSystem: timeSystem,
keyString: undefined,
autoScroll: true,
filters: {
brightness: 100,
contrast: 100
},
imageHistory: [],
thumbnailClick: THUMBNAIL_CLICKED,
isPaused: false,
metadata: {},
requestCount: 0,
timeSystem: timeSystem,
timeFormatter: undefined,
refreshCSS: false,
keyString: undefined,
focusedImageIndex: undefined,
focusedImageRelatedTelemetry: {},
numericDuration: undefined,
metadataEndpoints: {},
relatedTelemetry: {},
latestRelatedTelemetry: {},
focusedImageNaturalAspectRatio: undefined,
@@ -231,6 +230,9 @@ export default {
};
},
computed: {
imageHistorySize() {
return this.imageHistory.length;
},
compassRoseSizingClasses() {
let compassRoseSizingClasses = '';
if (this.sizedImageDimensions.width < 300) {
@@ -258,9 +260,6 @@ export default {
canTrackDuration() {
return this.openmct.time.clock() && this.timeSystem.isUTCBased;
},
focusedImageDownloadName() {
return this.getImageDownloadName(this.focusedImage);
},
isNextDisabled() {
let disabled = false;
@@ -383,6 +382,10 @@ export default {
}
},
watch: {
imageHistorySize(newSize, oldSize) {
this.setFocusedImage(newSize - 1, false);
this.scrollToRight();
},
focusedImageIndex() {
this.trackDuration();
this.resetAgeCSS();
@@ -391,18 +394,9 @@ export default {
}
},
async mounted() {
// listen
this.openmct.time.on('bounds', this.boundsChange);
this.openmct.time.on('timeSystem', this.timeSystemChange);
this.openmct.time.on('clock', this.clockChange);
// set
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] };
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]};
//listen
this.openmct.time.on('timeSystem', this.trackDuration);
this.openmct.time.on('clock', this.trackDuration);
// related telemetry keys
this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ'];
@@ -410,56 +404,49 @@ export default {
this.cameraKeys = ['cameraPan', 'cameraTilt'];
this.sunKeys = ['sunOrientation'];
// initialize
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
// kickoff
this.subscribe();
this.requestHistory();
// related telemetry
await this.initializeRelatedTelemetry();
this.updateRelatedTelemetryForFocusedImage();
await this.updateRelatedTelemetryForFocusedImage();
this.trackLatestRelatedTelemetry();
// for scrolling through images quickly and resizing the object view
_.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
_.debounce(this.resizeImageContainer, 400);
this.updateRelatedTelemetryForFocusedImage = _.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
this.imageContainerResizeObserver.observe(this.$refs.imageBG);
// for resizing the object view
this.resizeImageContainer = _.debounce(this.resizeImageContainer, 400);
if (this.$refs.imageBG) {
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
this.imageContainerResizeObserver.observe(this.$refs.imageBG);
}
// For adjusting scroll bar size and position when resizing thumbs wrapper
this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY);
this.handleThumbWindowResizeEnded = _.debounce(this.handleThumbWindowResizeEnded, SCROLL_LATENCY);
this.handleThumbWindowResizeStart = _.debounce(this.handleThumbWindowResizeStart, SCROLL_LATENCY);
if (this.$refs.thumbsWrapper) {
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper);
}
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper);
},
beforeDestroy() {
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
this.openmct.time.off('timeSystem', this.trackDuration);
this.openmct.time.off('clock', this.trackDuration);
if (this.thumbWrapperResizeObserver) {
this.thumbWrapperResizeObserver.disconnect();
}
if (this.imageContainerResizeObserver) {
this.imageContainerResizeObserver.disconnect();
}
if (this.thumbWrapperResizeObserver) {
this.thumbWrapperResizeObserver.disconnect();
}
if (this.relatedTelemetry.hasRelatedTelemetry) {
this.relatedTelemetry.destroy();
}
this.stopDurationTracking();
this.openmct.time.off('bounds', this.boundsChange);
this.openmct.time.off('timeSystem', this.timeSystemChange);
this.openmct.time.off('clock', this.clockChange);
// unsubscribe from related telemetry
if (this.relatedTelemetry.hasRelatedTelemetry) {
for (let key of this.relatedTelemetry.keys) {
@@ -576,56 +563,6 @@ export default {
focusElement() {
this.$el.focus();
},
datumIsNotValid(datum) {
if (this.imageHistory.length === 0) {
return false;
}
const datumURL = this.formatImageUrl(datum);
const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]);
// datum is not valid if it matches the last datum in history,
// or it is before the last datum in the history
const datumTimeCheck = this.parseTime(datum);
const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]);
const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL);
const isStale = datumTimeCheck < historyTimeCheck;
return matchesLast || isStale;
},
formatImageUrl(datum) {
if (!datum) {
return;
}
return this.imageFormatter.format(datum);
},
formatTime(datum) {
if (!datum) {
return;
}
let dateTimeStr = this.timeFormatter.format(datum);
// Replace ISO "T" with a space to allow wrapping
return dateTimeStr.replace("T", " ");
},
getImageDownloadName(datum) {
let imageDownloadName = '';
if (datum) {
const key = this.imageDownloadNameHints.key;
imageDownloadName = datum[key];
}
return imageDownloadName;
},
parseTime(datum) {
if (!datum) {
return;
}
return this.timeFormatter.parse(datum);
},
handleScroll() {
const thumbsWrapper = this.$refs.thumbsWrapper;
if (!thumbsWrapper || this.resizingWindow) {
@@ -683,6 +620,10 @@ export default {
setFocusedImage(index, thumbnailClick = false) {
if (this.isPaused && !thumbnailClick) {
this.nextImageIndex = index;
//this could happen if bounds changes
if (this.focusedImageIndex > this.imageHistory.length - 1) {
this.focusedImageIndex = index;
}
return;
}
@@ -693,70 +634,6 @@ export default {
this.paused(true);
}
},
boundsChange(bounds, isTick) {
if (!isTick) {
this.requestHistory();
}
},
async requestHistory() {
let bounds = this.openmct.time.bounds();
this.requestCount++;
const requestId = this.requestCount;
this.imageHistory = [];
let data = await this.openmct.telemetry
.request(this.domainObject, bounds) || [];
if (this.requestCount === requestId) {
data.forEach((datum, index) => {
this.updateHistory(datum, index === data.length - 1);
});
}
},
timeSystemChange(system) {
this.timeSystem = this.openmct.time.timeSystem();
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.trackDuration();
},
clockChange(clock) {
this.trackDuration();
},
subscribe() {
this.unsubscribe = this.openmct.telemetry
.subscribe(this.domainObject, (datum) => {
let parsedTimestamp = this.parseTime(datum);
let bounds = this.openmct.time.bounds();
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
this.updateHistory(datum);
}
});
},
updateHistory(datum, setFocused = true) {
if (this.datumIsNotValid(datum)) {
return;
}
let image = { ...datum };
image.formattedTime = this.formatTime(datum);
image.url = this.formatImageUrl(datum);
image.time = datum[this.timeKey];
image.imageDownloadName = this.getImageDownloadName(datum);
this.imageHistory.push(image);
if (setFocused) {
this.setFocusedImage(this.imageHistory.length - 1);
this.scrollToRight();
}
},
getFormatter(key) {
let metadataValue = this.metadata.value(key) || { format: key };
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
return valueFormatter;
},
trackDuration() {
if (this.canTrackDuration) {
this.stopDurationTracking();
@@ -876,6 +753,10 @@ export default {
}, { once: true });
},
resizeImageContainer() {
if (!this.$refs.imageBG) {
return;
}
if (this.$refs.imageBG.clientWidth !== this.imageContainerWidth) {
this.imageContainerWidth = this.$refs.imageBG.clientWidth;
}

View File

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

View File

@@ -0,0 +1,174 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
const DEFAULT_DURATION_FORMATTER = 'duration';
export default {
inject: ['openmct', 'domainObject', 'objectPath'],
mounted() {
// listen
this.openmct.time.on('bounds', this.boundsChange);
this.openmct.time.on('timeSystem', this.timeSystemChange);
// set
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] };
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]};
// initialize
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
// kickoff
this.subscribe();
this.requestHistory();
},
beforeDestroy() {
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
}
this.openmct.time.off('bounds', this.boundsChange);
this.openmct.time.off('timeSystem', this.timeSystemChange);
},
methods: {
datumIsNotValid(datum) {
if (this.imageHistory.length === 0) {
return false;
}
const datumURL = this.formatImageUrl(datum);
const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]);
// datum is not valid if it matches the last datum in history,
// or it is before the last datum in the history
const datumTimeCheck = this.parseTime(datum);
const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]);
const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL);
const isStale = datumTimeCheck < historyTimeCheck;
return matchesLast || isStale;
},
formatImageUrl(datum) {
if (!datum) {
return;
}
return this.imageFormatter.format(datum);
},
formatTime(datum) {
if (!datum) {
return;
}
let dateTimeStr = this.timeFormatter.format(datum);
// Replace ISO "T" with a space to allow wrapping
return dateTimeStr.replace("T", " ");
},
getImageDownloadName(datum) {
let imageDownloadName = '';
if (datum) {
const key = this.imageDownloadNameHints.key;
imageDownloadName = datum[key];
}
return imageDownloadName;
},
parseTime(datum) {
if (!datum) {
return;
}
return this.timeFormatter.parse(datum);
},
boundsChange(bounds, isTick) {
if (!isTick) {
this.requestHistory();
}
},
async requestHistory() {
let bounds = this.openmct.time.bounds();
this.requestCount++;
const requestId = this.requestCount;
this.imageHistory = [];
let data = await this.openmct.telemetry
.request(this.domainObject, bounds) || [];
if (this.requestCount === requestId) {
let imagery = [];
data.forEach((datum) => {
let image = this.normalizeDatum(datum);
if (image) {
imagery.push(image);
}
});
//this is to optimize anything that reacts to imageHistory length
this.imageHistory = imagery;
}
},
timeSystemChange() {
this.timeSystem = this.openmct.time.timeSystem();
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
},
subscribe() {
this.unsubscribe = this.openmct.telemetry
.subscribe(this.domainObject, (datum) => {
let parsedTimestamp = this.parseTime(datum);
let bounds = this.openmct.time.bounds();
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
let image = this.normalizeDatum(datum);
if (image) {
this.imageHistory.push(image);
}
}
});
},
normalizeDatum(datum) {
if (this.datumIsNotValid(datum)) {
return;
}
let image = { ...datum };
image.formattedTime = this.formatTime(datum);
image.url = this.formatImageUrl(datum);
image.time = datum[this.timeKey];
image.imageDownloadName = this.getImageDownloadName(datum);
return image;
},
getFormatter(key) {
let metadataValue = this.metadata.value(key) || { format: key };
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
return valueFormatter;
}
}
};

View File

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

View File

@@ -32,19 +32,19 @@ const TEN_MINUTES = ONE_MINUTE * 10;
const MAIN_IMAGE_CLASS = '.js-imageryView-image';
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
const REFRESH_CSS_MS = 500;
const TOLERANCE = 0.50;
// 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 comparisonFunction(valueOne, valueTwo) {
// let larger = valueOne;
// let smaller = valueTwo;
//
// if (larger < smaller) {
// larger = valueTwo;
// smaller = valueOne;
// }
//
// return (larger - smaller) < TOLERANCE;
// }
function getImageInfo(doc) {
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
@@ -84,12 +84,14 @@ function generateTelemetry(start, count) {
return telemetry;
}
describe("The Imagery View Layout", () => {
describe("The Imagery View Layouts", () => {
const imageryKey = 'example.imagery';
const imageryForTimeStripKey = 'example.imagery.time-strip.view';
const START = Date.now();
const COUNT = 10;
let resolveFunction;
let originalRouterPath;
let openmct;
let appHolder;
@@ -116,51 +118,51 @@ describe("The Imagery View Layout", () => {
"image": 1,
"priority": 3
},
"source": "url",
"relatedTelemetry": {
"heading": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "heading",
"valueKey": "value"
}
},
"roll": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "roll",
"valueKey": "value"
}
},
"pitch": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "pitch",
"valueKey": "value"
}
},
"cameraPan": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "cameraPan",
"valueKey": "value"
}
},
"cameraTilt": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "cameraTilt",
"valueKey": "value"
}
},
"sunOrientation": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "sunOrientation",
"valueKey": "value"
}
}
}
"source": "url"
// "relatedTelemetry": {
// "heading": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "heading",
// "valueKey": "value"
// }
// },
// "roll": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "roll",
// "valueKey": "value"
// }
// },
// "pitch": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "pitch",
// "valueKey": "value"
// }
// },
// "cameraPan": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "cameraPan",
// "valueKey": "value"
// }
// },
// "cameraTilt": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "cameraTilt",
// "valueKey": "value"
// }
// },
// "sunOrientation": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "sunOrientation",
// "valueKey": "value"
// }
// }
// }
},
{
"name": "Name",
@@ -218,7 +220,9 @@ describe("The Imagery View Layout", () => {
});
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;
openmct.on('start', done);
openmct.start(appHolder);
@@ -229,10 +233,34 @@ describe("The Imagery View Layout", () => {
start: 0,
end: 1
});
openmct.router.path = originalRouterPath;
return resetApplicationState(openmct);
});
it("should provide an imagery time strip view when in a time strip", () => {
openmct.router.path = [{
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}];
let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, {
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}]);
let imageryView = applicableViews.find(
viewProvider => viewProvider.key === imageryForTimeStripKey
);
expect(imageryView).toBeDefined();
});
it("should provide an imagery view only for imagery producing objects", () => {
let applicableViews = openmct.objectViews.get(imageryObject, []);
let imageryView = applicableViews.find(
@@ -242,6 +270,46 @@ describe("The Imagery View Layout", () => {
expect(imageryView).toBeDefined();
});
it("should not provide an imagery view when in a time strip", () => {
openmct.router.path = [{
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}];
let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, {
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}]);
let imageryView = applicableViews.find(
viewProvider => viewProvider.key === imageryKey
);
expect(imageryView).toBeUndefined();
});
it("should provide an imagery view when navigated to in the composition of a time strip", () => {
openmct.router.path = [imageryObject];
let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, {
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}]);
let imageryView = applicableViews.find(
viewProvider => viewProvider.key === imageryKey
);
expect(imageryView).toBeDefined();
});
describe("imagery view", () => {
let applicableViews;
let imageryViewProvider;
@@ -302,18 +370,15 @@ describe("The Imagery View Layout", () => {
});
});
xit("should show that an image is not new", (done) => {
it("should show that an image is not new", (done) => {
const target = imageTelemetry[2].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
Vue.nextTick(() => {
// used in code, need to wait to the 500ms here too
setTimeout(() => {
const imageIsNew = isNew(parent);
const imageIsNew = isNew(parent);
expect(imageIsNew).toBeFalse();
done();
}, REFRESH_CSS_MS);
expect(imageIsNew).toBeFalse();
done();
});
});
@@ -367,18 +432,18 @@ describe("The Imagery View Layout", () => {
});
it ('shows an auto scroll button when scroll to left', async () => {
// to mock what a scroll would do
imageryView._getInstance().$refs.ImageryLayout.autoScroll = false;
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
await Vue.nextTick();
let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button');
expect(autoScrollButton).toBeTruthy();
});
it ('scrollToRight is called when clicking on auto scroll button', async () => {
// use spyon to spy the scroll function
spyOn(imageryView._getInstance().$refs.ImageryLayout, 'scrollToRight');
imageryView._getInstance().$refs.ImageryLayout.autoScroll = false;
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight');
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
await Vue.nextTick();
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryLayout.scrollToRight).toHaveBeenCalledWith('reset');
expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset');
});
});

View File

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

View File

@@ -75,16 +75,7 @@ describe("the plugin", () => {
mockDialogService.getUserInput.and.returnValue(mockPromise);
spyOn(openmct.$injector, 'get');
openmct.$injector.get.and.callFake((key) => {
return {
'dialogService': mockDialogService,
'$rootScope': {
'$destroy': () => {}
}
}[key];
});
spyOn(openmct.$injector, 'get').and.returnValue(mockDialogService);
spyOn(compositionAPI, 'get').and.returnValue(mockComposition);
spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true));

View File

@@ -140,6 +140,7 @@ import SearchResults from './SearchResults.vue';
import Sidebar from './Sidebar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSectionId, setDefaultNotebookPageId } from '../utils/notebook-storage';
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
import { NOTEBOOK_VIEW_TYPE } from '../notebook-constants';
import objectUtils from 'objectUtils';
@@ -385,9 +386,13 @@ export default {
const snapshotId = event.dataTransfer.getData('openmct/snapshot/id');
if (snapshotId.length) {
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
this.newEntry(snapshot);
this.newEntry(snapshot.embedObject);
this.snapshotContainer.removeSnapshot(snapshotId);
const namespace = this.domainObject.identifier.namespace;
const notebookImageDomainObject = updateNamespaceOfDomainObject(snapshot.notebookImageDomainObject, namespace);
saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject);
return;
}
@@ -451,12 +456,9 @@ export default {
: undefined;
},
getDefaultNotebookObject() {
const oldNotebookStorage = getDefaultNotebook();
if (!oldNotebookStorage) {
return null;
}
const defaultNotebook = getDefaultNotebook();
return this.openmct.objects.get(oldNotebookStorage.identifier);
return defaultNotebook && this.openmct.objects.get(defaultNotebook.identifier);
},
getLinktoNotebook() {
const objectPath = this.openmct.router.path;

View File

@@ -40,7 +40,7 @@ export default {
components: {
PopupMenu
},
inject: ['openmct'],
inject: ['openmct', 'snapshotContainer'],
props: {
embed: {
type: Object,
@@ -48,6 +48,12 @@ export default {
return {};
}
},
isSnapshotContainer: {
type: Boolean,
default() {
return false;
}
},
removeActionString: {
type: String,
default() {
@@ -135,6 +141,14 @@ export default {
return;
}
if (this.isSnapshotContainer) {
const snapshot = this.snapshotContainer.getSnapshot(this.embed.id);
const fullSizeImageURL = snapshot.notebookImageDomainObject.configuration.fullSizeImageURL;
painterroInstance.show(fullSizeImageURL);
return;
}
this.openmct.objects.get(fullSizeImageObjectIdentifier)
.then(object => {
painterroInstance.show(object.configuration.fullSizeImageURL);
@@ -190,6 +204,14 @@ export default {
return;
}
if (this.isSnapshotContainer) {
const snapshot = this.snapshotContainer.getSnapshot(this.embed.id);
const fullSizeImageURL = snapshot.notebookImageDomainObject.configuration.fullSizeImageURL;
this.openSnapshotOverlay(fullSizeImageURL);
return;
}
this.openmct.objects.get(fullSizeImageObjectIdentifier)
.then(object => {
this.openSnapshotOverlay(object.configuration.fullSizeImageURL);
@@ -259,8 +281,20 @@ export default {
updateSnapshot(snapshotObject) {
this.embed.snapshot.thumbnailImage = snapshotObject.thumbnailImage;
updateNotebookImageDomainObject(this.openmct, this.embed.snapshot.fullSizeImageObjectIdentifier, snapshotObject.fullSizeImage);
this.updateNotebookImageDomainObjectSnapshot(snapshotObject);
this.updateEmbed(this.embed);
},
updateNotebookImageDomainObjectSnapshot(snapshotObject) {
if (this.isSnapshotContainer) {
const snapshot = this.snapshotContainer.getSnapshot(this.embed.id);
snapshot.embedObject.snapshot.thumbnailImage = snapshotObject.thumbnailImage;
snapshot.notebookImageDomainObject.configuration.fullSizeImageURL = snapshotObject.fullSizeImage.src;
this.snapshotContainer.updateSnapshot(snapshot);
} else {
updateNotebookImageDomainObject(this.openmct, this.embed.snapshot.fullSizeImageObjectIdentifier, snapshotObject.fullSizeImage);
}
}
}
};

View File

@@ -102,9 +102,11 @@
<script>
import NotebookEmbed from './NotebookEmbed.vue';
import { createNewEmbed } from '../utils/notebook-entries';
import Moment from 'moment';
import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue';
import { createNewEmbed } from '../utils/notebook-entries';
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
import Moment from 'moment';
export default {
components: {
@@ -210,8 +212,12 @@ export default {
const snapshotId = $event.dataTransfer.getData('openmct/snapshot/id');
if (snapshotId.length) {
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
this.entry.embeds.push(snapshot.embedObject);
this.snapshotContainer.removeSnapshot(snapshotId);
this.entry.embeds.push(snapshot);
const namespace = this.domainObject.identifier.namespace;
const notebookImageDomainObject = updateNamespaceOfDomainObject(snapshot.notebookImageDomainObject, namespace);
saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject);
} else {
const data = $event.dataTransfer.getData('openmct/domain-object-path');
const objectPath = JSON.parse(data);

View File

@@ -17,19 +17,26 @@
<script>
import Snapshot from '../snapshot';
import { getDefaultNotebook, getNotebookSectionAndPage, validateNotebookStorageObject } from '../utils/notebook-storage';
import { getDefaultNotebook, validateNotebookStorageObject } from '../utils/notebook-storage';
import { NOTEBOOK_DEFAULT, NOTEBOOK_SNAPSHOT } from '../notebook-constants';
import { getMenuItems } from '../utils/notebook-snapshot-menu';
export default {
inject: ['openmct'],
props: {
currentView: {
type: Object,
default() {
return {};
}
},
domainObject: {
type: Object,
default() {
return {};
}
},
ignoreLink: {
isPreview: {
type: Boolean,
default() {
return false;
@@ -50,51 +57,40 @@ export default {
},
mounted() {
validateNotebookStorageObject();
this.getDefaultNotebookObject();
this.notebookSnapshot = new Snapshot(this.openmct);
this.setDefaultNotebookStatus();
},
methods: {
getDefaultNotebookObject() {
const defaultNotebook = getDefaultNotebook();
getPreviewObjectLink() {
const relativePath = this.openmct.objects.getRelativePath(this.objectPath);
const urlParams = this.openmct.router.getParams();
urlParams.view = this.currentView.key;
return defaultNotebook && this.openmct.objects.get(defaultNotebook.identifier);
const urlParamsString = Object.entries(urlParams)
.map(([key, value]) => `${key}=${value}`)
.join('&');
return `#/browse/${relativePath}?${urlParamsString}`;
},
async showMenu(event) {
const notebookTypes = [];
const menuItemOptions = {
default: {
cssClass: 'icon-notebook',
name: `Save to Notebook`,
onItemClicked: () => this.snapshot(NOTEBOOK_DEFAULT, event.target)
},
snapshot: {
cssClass: 'icon-camera',
name: 'Save to Notebook Snapshots',
onItemClicked: () => this.snapshot(NOTEBOOK_SNAPSHOT, event.target)
}
};
const notebookTypes = await getMenuItems(this.openmct, menuItemOptions);
const elementBoundingClientRect = this.$el.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y + elementBoundingClientRect.height;
const defaultNotebookObject = await this.getDefaultNotebookObject();
if (defaultNotebookObject) {
const defaultNotebook = getDefaultNotebook();
const { section, page } = getNotebookSectionAndPage(defaultNotebookObject, defaultNotebook.defaultSectionId, defaultNotebook.defaultPageId);
if (section && page) {
const name = defaultNotebookObject.name;
const sectionName = section.name;
const pageName = page.name;
const defaultPath = `${name} - ${sectionName} - ${pageName}`;
notebookTypes.push({
cssClass: 'icon-notebook',
name: `Save to Notebook ${defaultPath}`,
onItemClicked: () => {
return this.snapshot(NOTEBOOK_DEFAULT, event.target);
}
});
}
}
notebookTypes.push({
cssClass: 'icon-camera',
name: 'Save to Notebook Snapshots',
onItemClicked: () => {
return this.snapshot(NOTEBOOK_SNAPSHOT, event.target);
}
});
this.openmct.menus.showMenu(x, y, notebookTypes);
},
snapshot(notebookType, target) {
@@ -102,15 +98,12 @@ export default {
const wrapper = target && target.closest('.js-notebook-snapshot-item-wrapper')
|| document;
const element = wrapper.querySelector('.js-notebook-snapshot-item');
const bounds = this.openmct.time.bounds();
const link = !this.ignoreLink
? window.location.hash
: null;
const objectPath = this.objectPath || this.openmct.router.path;
const link = this.isPreview
? this.getPreviewObjectLink()
: window.location.hash;
const snapshotMeta = {
bounds,
bounds: this.openmct.time.bounds(),
link,
objectPath,
openmct: this.openmct

View File

@@ -27,15 +27,15 @@
</div><!-- closes l-browse-bar -->
<div class="c-snapshots">
<span v-for="snapshot in snapshots"
:key="snapshot.id"
:key="snapshot.embedObject.id"
draggable="true"
@dragstart="startEmbedDrag(snapshot, $event)"
>
<NotebookEmbed ref="notebookEmbed"
:key="snapshot.id"
:embed="snapshot"
:key="snapshot.embedObject.id"
:embed="snapshot.embedObject"
:is-snapshot-container="true"
:remove-action-string="'Delete Snapshot'"
@updateEmbed="updateSnapshot"
@removeEmbed="removeSnapshot"
/>
</span>
@@ -119,11 +119,8 @@ export default {
this.snapshots = this.snapshotContainer.getSnapshots();
},
startEmbedDrag(snapshot, event) {
event.dataTransfer.setData('text/plain', snapshot.id);
event.dataTransfer.setData('openmct/snapshot/id', snapshot.id);
},
updateSnapshot(snapshot) {
this.snapshotContainer.updateSnapshot(snapshot);
event.dataTransfer.setData('text/plain', snapshot.embedObject.id);
event.dataTransfer.setData('openmct/snapshot/id', snapshot.embedObject.id);
}
}
};

View File

@@ -117,10 +117,6 @@ export default function NotebookPlugin() {
key: 'notebook-snapshot-indicator'
};
openmct.once('destroy', () => {
snapshotContainer.destroy();
});
openmct.indicators.add(indicator);
openmct.objectViews.addProvider({

View File

@@ -18,13 +18,18 @@ export default class SnapshotContainer extends EventEmitter {
return SnapshotContainer.instance;
}
addSnapshot(embedObject) {
addSnapshot(notebookImageDomainObject, embedObject) {
const snapshots = this.getSnapshots();
if (snapshots.length >= NOTEBOOK_SNAPSHOT_MAX_COUNT) {
snapshots.pop();
}
snapshots.unshift(embedObject);
const snapshotObject = {
notebookImageDomainObject,
embedObject
};
snapshots.unshift(snapshotObject);
return this.saveSnapshots(snapshots);
}
@@ -32,7 +37,7 @@ export default class SnapshotContainer extends EventEmitter {
getSnapshot(id) {
const snapshots = this.getSnapshots();
return snapshots.find(s => s.id === id);
return snapshots.find(s => s.embedObject.id === id);
}
getSnapshots() {
@@ -47,7 +52,7 @@ export default class SnapshotContainer extends EventEmitter {
}
const snapshots = this.getSnapshots();
const filteredsnapshots = snapshots.filter(snapshot => snapshot.id !== id);
const filteredsnapshots = snapshots.filter(snapshot => snapshot.embedObject.id !== id);
return this.saveSnapshots(filteredsnapshots);
}
@@ -73,15 +78,11 @@ export default class SnapshotContainer extends EventEmitter {
updateSnapshot(snapshot) {
const snapshots = this.getSnapshots();
const updatedSnapshots = snapshots.map(s => {
return s.id === snapshot.id
return s.embedObject.id === snapshot.embedObject.id
? snapshot
: s;
});
return this.saveSnapshots(updatedSnapshots);
}
destroy() {
delete SnapshotContainer.instance;
}
}

View File

@@ -1,7 +1,7 @@
import { addNotebookEntry, createNewEmbed } from './utils/notebook-entries';
import { getDefaultNotebook, getNotebookSectionAndPage, getDefaultNotebookLink, setDefaultNotebook } from './utils/notebook-storage';
import { NOTEBOOK_DEFAULT } from '@/plugins/notebook/notebook-constants';
import { createNotebookImageDomainObject, DEFAULT_SIZE } from './utils/notebook-image';
import { createNotebookImageDomainObject, saveNotebookImageDomainObject, updateNamespaceOfDomainObject, DEFAULT_SIZE } from './utils/notebook-image';
import SnapshotContainer from './snapshot-container';
import ImageExporter from '../../exporters/ImageExporter';
@@ -35,29 +35,28 @@ export default class Snapshot {
* @private
*/
_saveSnapShot(notebookType, fullSizeImageURL, thumbnailImageURL, snapshotMeta) {
createNotebookImageDomainObject(this.openmct, fullSizeImageURL)
.then(object => {
const thumbnailImage = { src: thumbnailImageURL || '' };
const snapshot = {
fullSizeImageObjectIdentifier: object.identifier,
thumbnailImage
};
const embed = createNewEmbed(snapshotMeta, snapshot);
if (notebookType === NOTEBOOK_DEFAULT) {
this._saveToDefaultNoteBook(embed);
const object = createNotebookImageDomainObject(fullSizeImageURL);
const thumbnailImage = { src: thumbnailImageURL || '' };
const snapshot = {
fullSizeImageObjectIdentifier: object.identifier,
thumbnailImage
};
const embed = createNewEmbed(snapshotMeta, snapshot);
if (notebookType === NOTEBOOK_DEFAULT) {
const notebookStorage = getDefaultNotebook();
return;
}
this._saveToNotebookSnapshots(embed);
});
this._saveToDefaultNoteBook(notebookStorage, embed);
const notebookImageDomainObject = updateNamespaceOfDomainObject(object, notebookStorage.identifier.namespace);
saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject);
} else {
this._saveToNotebookSnapshots(object, embed);
}
}
/**
* @private
*/
_saveToDefaultNoteBook(embed) {
const notebookStorage = getDefaultNotebook();
_saveToDefaultNoteBook(notebookStorage, embed) {
this.openmct.objects.get(notebookStorage.identifier)
.then(async (domainObject) => {
addNotebookEntry(this.openmct, domainObject, notebookStorage, embed);
@@ -85,19 +84,22 @@ export default class Snapshot {
/**
* @private
*/
_saveToNotebookSnapshots(embed) {
this.snapshotContainer.addSnapshot(embed);
_saveToNotebookSnapshots(notebookImageDomainObject, embed) {
this.snapshotContainer.addSnapshot(notebookImageDomainObject, embed);
}
_showNotification(msg, url) {
const options = {
autoDismissTimeout: 30000,
link: {
autoDismissTimeout: 30000
};
if (!this.openmct.editor.isEditing()) {
options.link = {
cssClass: '',
text: 'click to view',
onClick: this._navigateToNotebook(url)
}
};
};
}
this.openmct.notifications.info(msg, options);
}
@@ -108,7 +110,8 @@ export default class Snapshot {
}
return () => {
window.location.href = window.location.origin + url;
const path = window.location.href.split('#');
window.location.href = path[0] + url;
};
}
}

View File

@@ -22,109 +22,95 @@
import * as NotebookEntries from './notebook-entries';
import { createOpenMct, resetApplicationState } from 'utils/testing';
let notebookStorage;
let notebookEntries;
let notebookDomainObject;
let selectedSection;
let selectedPage;
const notebookStorage = {
name: 'notebook',
identifier: {
namespace: '',
key: 'test-notebook'
},
defaultSectionId: '03a79b6a-971c-4e56-9892-ec536332c3f0',
defaultPageId: '8b548fd9-2b8a-4b02-93a9-4138e22eba00'
};
const notebookEntries = {
'03a79b6a-971c-4e56-9892-ec536332c3f0': {
'8b548fd9-2b8a-4b02-93a9-4138e22eba00': []
}
};
const notebookDomainObject = {
identifier: {
key: 'notebook',
namespace: ''
},
type: 'notebook',
configuration: {
defaultSort: 'oldest',
entries: notebookEntries,
pageTitle: 'Page',
sections: [],
sectionTitle: 'Section',
type: 'General'
}
};
const selectedSection = {
id: '03a79b6a-971c-4e56-9892-ec536332c3f0',
isDefault: false,
isSelected: true,
name: 'Day 1',
pages: [
{
id: '54deb3d5-8267-4be4-95e9-3579ed8c082d',
isDefault: false,
isSelected: false,
name: 'Shift 1',
pageTitle: 'Page'
},
{
id: '2ea41c78-8e60-4657-a350-53f1a1fa3021',
isDefault: false,
isSelected: false,
name: 'Shift 2',
pageTitle: 'Page'
},
{
id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00',
isDefault: false,
isSelected: true,
name: 'Unnamed Page',
pageTitle: 'Page'
}
],
sectionTitle: 'Section'
};
const selectedPage = {
id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00',
isDefault: false,
isSelected: true,
name: 'Unnamed Page',
pageTitle: 'Page'
};
let openmct;
let mockIdentifierService;
describe('Notebook Entries:', () => {
beforeEach(() => {
notebookStorage = {
name: 'notebook',
identifier: {
namespace: '',
key: 'test-notebook'
},
defaultSectionId: '03a79b6a-971c-4e56-9892-ec536332c3f0',
defaultPageId: '8b548fd9-2b8a-4b02-93a9-4138e22eba00'
};
notebookEntries = {
'03a79b6a-971c-4e56-9892-ec536332c3f0': {
'8b548fd9-2b8a-4b02-93a9-4138e22eba00': []
}
};
notebookDomainObject = {
identifier: {
key: 'notebook',
namespace: ''
},
type: 'notebook',
configuration: {
defaultSort: 'oldest',
entries: notebookEntries,
pageTitle: 'Page',
sections: [],
sectionTitle: 'Section',
type: 'General'
}
};
selectedSection = {
id: '03a79b6a-971c-4e56-9892-ec536332c3f0',
isDefault: false,
isSelected: true,
name: 'Day 1',
pages: [
{
id: '54deb3d5-8267-4be4-95e9-3579ed8c082d',
isDefault: false,
isSelected: false,
name: 'Shift 1',
pageTitle: 'Page'
},
{
id: '2ea41c78-8e60-4657-a350-53f1a1fa3021',
isDefault: false,
isSelected: false,
name: 'Shift 2',
pageTitle: 'Page'
},
{
id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00',
isDefault: false,
isSelected: true,
name: 'Unnamed Page',
pageTitle: 'Page'
}
],
sectionTitle: 'Section'
};
selectedPage = {
id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00',
isDefault: false,
isSelected: true,
name: 'Unnamed Page',
pageTitle: 'Page'
};
openmct = createOpenMct();
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
mockIdentifierService = jasmine.createSpyObj(
'identifierService',
['parse']
);
openmct.$injector.get.and.callFake((key) => {
return {
'identifierService': mockIdentifierService,
'$rootScope': {
'$destroy': () => {}
}
}[key];
});
mockIdentifierService.parse.and.returnValue({
getSpace: () => {
return '';
}
});
openmct.$injector.get.and.returnValue(mockIdentifierService);
openmct.types.addType('notebook', {
creatable: true
});

View File

@@ -5,14 +5,14 @@ export const DEFAULT_SIZE = {
height: 30
};
export function createNotebookImageDomainObject(openmct, fullSizeImageURL) {
export function createNotebookImageDomainObject(fullSizeImageURL) {
const identifier = {
key: uuid(),
namespace: ''
};
const viewType = 'notebookSnapshotImage';
const object = {
return {
name: 'Notebook Snapshot Image',
type: viewType,
identifier,
@@ -20,21 +20,6 @@ export function createNotebookImageDomainObject(openmct, fullSizeImageURL) {
fullSizeImageURL
}
};
return new Promise((resolve, reject) => {
openmct.objects.save(object)
.then(result => {
if (result) {
resolve(object);
}
reject();
})
.catch(e => {
console.error(e);
reject();
});
});
}
export function getThumbnailURLFromCanvas(canvas, size = DEFAULT_SIZE) {
@@ -67,6 +52,23 @@ export function getThumbnailURLFromimageUrl(imageUrl, size = DEFAULT_SIZE) {
});
}
export function saveNotebookImageDomainObject(openmct, object) {
return new Promise((resolve, reject) => {
openmct.objects.save(object)
.then(result => {
if (result) {
resolve(object);
} else {
reject();
}
})
.catch(e => {
console.error(e);
reject();
});
});
}
export function updateNotebookImageDomainObject(openmct, identifier, fullSizeImage) {
openmct.objects.get(identifier)
.then(domainObject => {
@@ -76,3 +78,9 @@ export function updateNotebookImageDomainObject(openmct, identifier, fullSizeIma
openmct.objects.mutate(domainObject, 'configuration', configuration);
});
}
export function updateNamespaceOfDomainObject(object, namespace) {
object.identifier.namespace = namespace;
return object;
}

View File

@@ -1,16 +1,18 @@
import { createNotebookImageDomainObject, getThumbnailURLFromimageUrl } from './notebook-image';
import { createNotebookImageDomainObject, getThumbnailURLFromimageUrl, saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from './notebook-image';
import { mutateObject } from './notebook-entries';
const IMAGE_MIGRATION_VER = "v1";
export function notebookImageMigration(openmct, domainObject) {
const configuration = domainObject.configuration;
const notebookEntries = configuration.entries;
const imageMigrationVer = configuration.imageMigrationVer;
if (imageMigrationVer && imageMigrationVer === 'v1') {
if (imageMigrationVer && imageMigrationVer === IMAGE_MIGRATION_VER) {
return;
}
configuration.imageMigrationVer = 'v1';
configuration.imageMigrationVer = IMAGE_MIGRATION_VER;
// to avoid muliple notebookImageMigration calls updating images.
mutateObject(openmct, domainObject, 'configuration', configuration);
@@ -27,14 +29,16 @@ export function notebookImageMigration(openmct, domainObject) {
const fullSizeImageURL = snapshot.src;
if (fullSizeImageURL) {
const thumbnailImageURL = await getThumbnailURLFromimageUrl(fullSizeImageURL);
const notebookImageDomainObject = await createNotebookImageDomainObject(openmct, fullSizeImageURL);
const object = createNotebookImageDomainObject(fullSizeImageURL);
const notebookImageDomainObject = updateNamespaceOfDomainObject(object, domainObject.identifier.namespace);
embed.snapshot = {
fullSizeImageObjectIdentifier: notebookImageDomainObject.identifier,
thumbnailImage: { src: thumbnailImageURL || '' }
};
mutateObject(openmct, domainObject, 'configuration.entries', notebookEntries);
saveNotebookImageDomainObject(openmct, notebookImageDomainObject);
}
});
});

View File

@@ -0,0 +1,31 @@
import { getDefaultNotebook, getNotebookSectionAndPage } from './notebook-storage';
export async function getMenuItems(openmct, menuItemOptions) {
const notebookTypes = [];
const defaultNotebook = getDefaultNotebook();
const defaultNotebookObject = defaultNotebook && await openmct.objects.get(defaultNotebook.identifier);
if (defaultNotebookObject) {
const { section, page } = getNotebookSectionAndPage(defaultNotebookObject, defaultNotebook.defaultSectionId, defaultNotebook.defaultPageId);
if (section && page) {
const name = defaultNotebookObject.name;
const sectionName = section.name;
const pageName = page.name;
const defaultPath = `${name} - ${sectionName} - ${pageName}`;
notebookTypes.push({
cssClass: menuItemOptions.default.cssClass,
name: `${menuItemOptions.default.name} ${defaultPath}`,
onItemClicked: menuItemOptions.default.onItemClicked
});
}
}
notebookTypes.push({
cssClass: menuItemOptions.snapshot.cssClass,
name: menuItemOptions.snapshot.name,
onItemClicked: menuItemOptions.snapshot.onItemClicked
});
return notebookTypes;
}

View File

@@ -71,11 +71,7 @@ export async function getDefaultNotebookLink(openmct, domainObject = null) {
}
const path = await openmct.objects.getOriginalPath(domainObject.identifier)
.then(objectPath => objectPath
.map(o => o && openmct.objects.makeKeyString(o.identifier))
.reverse()
.join('/')
);
.then(openmct.objects.getRelativePath);
const { defaultPageId, defaultSectionId } = getDefaultNotebook();
return `#/browse/${path}?sectionId=${defaultSectionId}&pageId=${defaultPageId}`;
@@ -100,6 +96,9 @@ export function setDefaultNotebookPageId(pageId) {
export function validateNotebookStorageObject() {
const notebookStorage = getDefaultNotebook();
if (!notebookStorage) {
return true;
}
let valid = false;
if (notebookStorage) {

View File

@@ -23,55 +23,51 @@
import * as NotebookStorage from './notebook-storage';
import { createOpenMct, resetApplicationState } from 'utils/testing';
let notebookSection;
let domainObject;
let notebookStorage;
const notebookSection = {
id: 'temp-section',
isDefault: false,
isSelected: true,
name: 'section',
pages: [
{
id: 'temp-page',
isDefault: false,
isSelected: true,
name: 'page',
pageTitle: 'Page'
}
],
sectionTitle: 'Section'
};
const domainObject = {
name: 'notebook',
identifier: {
namespace: '',
key: 'test-notebook'
},
configuration: {
sections: [
notebookSection
]
}
};
const notebookStorage = {
name: 'notebook',
identifier: {
namespace: '',
key: 'test-notebook'
},
defaultSectionId: 'temp-section',
defaultPageId: 'temp-page'
};
let openmct;
let mockIdentifierService;
describe('Notebook Storage:', () => {
beforeEach(() => {
notebookSection = {
id: 'temp-section',
isDefault: false,
isSelected: true,
name: 'section',
pages: [
{
id: 'temp-page',
isDefault: false,
isSelected: true,
name: 'page',
pageTitle: 'Page'
}
],
sectionTitle: 'Section'
};
domainObject = {
name: 'notebook',
identifier: {
namespace: '',
key: 'test-notebook'
},
configuration: {
sections: [
notebookSection
]
}
};
notebookStorage = {
name: 'notebook',
identifier: {
namespace: '',
key: 'test-notebook'
},
defaultSectionId: 'temp-section',
defaultPageId: 'temp-page'
};
openmct = createOpenMct();
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
mockIdentifierService = jasmine.createSpyObj(
@@ -84,15 +80,7 @@ describe('Notebook Storage:', () => {
}
});
openmct.$injector.get.and.callFake((key) => {
return {
'identifierService': mockIdentifierService,
'$rootScope': {
'$destroy': () => {}
}
}[key];
});
openmct.$injector.get.and.returnValue(mockIdentifierService);
window.localStorage.setItem('notebook-storage', null);
openmct.objects.addProvider('', jasmine.createSpyObj('mockNotebookProvider', [
'create',

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