Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
370fdc6466 | ||
|
|
44cbd3fc50 | ||
|
|
858de567ae | ||
|
|
ad22661029 | ||
|
|
26aa49d9a8 | ||
|
|
cb55500cba | ||
|
|
340246ab26 | ||
|
|
9cb743fb48 | ||
|
|
9c52ec7233 | ||
|
|
933ce7aa3c | ||
|
|
8ef3869325 | ||
|
|
53f5fdabe5 | ||
|
|
d10561fc7f | ||
|
|
566469e691 | ||
|
|
6d4a324fca | ||
|
|
b8fabb7e73 | ||
|
|
1680c3cc1b | ||
|
|
61dd85c704 | ||
|
|
980777691a |
@@ -7,6 +7,15 @@ orbs:
|
||||
node: circleci/node@4.5.1
|
||||
browser-tools: circleci/browser-tools@1.1.3
|
||||
jobs:
|
||||
npm-audit:
|
||||
executor: linux
|
||||
steps:
|
||||
- checkout
|
||||
- node/install:
|
||||
install-npm: true
|
||||
node-version: lts/fermium
|
||||
- run: npm install
|
||||
- run: npm audit --audit-level=low
|
||||
test:
|
||||
parameters:
|
||||
node-version:
|
||||
@@ -92,6 +101,7 @@ workflows:
|
||||
name: node14-firefox-nightly
|
||||
node-version: lts/fermium
|
||||
browser: FirefoxHeadless
|
||||
- npm-audit
|
||||
triggers:
|
||||
- schedule:
|
||||
cron: "0 0 * * *"
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -2,4 +2,4 @@ blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Discussions
|
||||
url: https://github.com/nasa/openmct/discussions
|
||||
about: Got a question?
|
||||
about: Have a question about the project?
|
||||
|
||||
23
.github/ISSUE_TEMPLATE/feature_request.md
vendored
23
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,23 +0,0 @@
|
||||
<!--- This is for filing enhancements or features. If you have a general -->
|
||||
<!--- question, please visit https://github.com/nasa/openmct/discussions -->
|
||||
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Thank you for suggesting an idea to make Open MCT better.
|
||||
|
||||
Please fill in as much of the template below as you're able.
|
||||
-->
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
<!-- Please describe the problem you are trying to solve. -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
<!--- Please describe the desired behavior. -->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
<!--- Please describe alternative solutions or features you have considered. -->
|
||||
11
.github/ISSUE_TEMPLATE/maintenance-type.md
vendored
Normal file
11
.github/ISSUE_TEMPLATE/maintenance-type.md
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: Maintenance
|
||||
about: Add, update or remove documentation, tests, or dependencies.
|
||||
title: ''
|
||||
labels: type:maintenance
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
#### Summary
|
||||
<!--- Generally describe the purpose of the change. -->
|
||||
31
SECURITY.md
Normal file
31
SECURITY.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Security Policy
|
||||
|
||||
The Open MCT team secures our code base using a combination of code review, dependency review, and periodic security reviews. Static analysis performed during automated verification additionally safeguards against common coding errors which may result in vulnerabilities.
|
||||
|
||||
### Reporting a Vulnerability
|
||||
|
||||
For general defects, please for a [Bug Report](https://github.com/nasa/openmct/issues/new/choose)
|
||||
|
||||
To report a vulnerability for Open MCT please send a detailed report to [arc-dl-openmct](mailto:arc-dl-openmct@mail.nasa.gov).
|
||||
|
||||
See our [top-level security policy](https://github.com/nasa/openmct/security/policy) for additional information.
|
||||
|
||||
### CodeQL and LGTM
|
||||
|
||||
The [CodeQL GitHub Actions workflow](https://github.com/nasa/openmct/blob/master/.github/workflows/codeql-analysis.yml) is available to the public. To review the results, fork the repository and run the CodeQL workflow.
|
||||
|
||||
CodeQL is run for every pull-request in GitHub Actions.
|
||||
|
||||
The project is also monitored by [LGTM](https://lgtm.com/projects/g/nasa/openmct/) and is available to public.
|
||||
|
||||
### ESLint
|
||||
|
||||
Static analysis is run for every push on the master branch and every pull request on all branches in Github Actions.
|
||||
|
||||
For more information about ESLint, visit https://eslint.org/.
|
||||
|
||||
### General Support
|
||||
|
||||
For additional support, please open a [Github Discussion](https://github.com/nasa/openmct/discussions).
|
||||
|
||||
If you wish to report a cybersecurity incident or concern, please contact the NASA Security Operations Center either by phone at 1-877-627-2732 or via email address soc@nasa.gov.
|
||||
@@ -20,156 +20,219 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
const DEFAULT_IMAGE_SAMPLES = [
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18731.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18733.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18734.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18735.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18736.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18737.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18738.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18739.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18740.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18741.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18742.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18743.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18744.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18745.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18746.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg"
|
||||
];
|
||||
const DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS = 20000;
|
||||
const MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS = 5000;
|
||||
|
||||
], function (
|
||||
let openmctInstance;
|
||||
|
||||
) {
|
||||
function ImageryPlugin() {
|
||||
|
||||
const IMAGE_SAMPLES = [
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18731.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18733.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18734.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18735.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18736.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18737.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18738.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18739.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18740.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18741.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18742.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18743.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18744.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18745.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18746.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg"
|
||||
];
|
||||
const IMAGE_DELAY = 20000;
|
||||
|
||||
function getCompassValues(min, max) {
|
||||
return min + Math.random() * (max - min);
|
||||
}
|
||||
|
||||
function pointForTimestamp(timestamp, name) {
|
||||
const url = IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length];
|
||||
const urlItems = url.split('/');
|
||||
const imageDownloadName = `example.imagery.${urlItems[urlItems.length - 1]}`;
|
||||
|
||||
return {
|
||||
name,
|
||||
utc: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
|
||||
local: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
|
||||
url,
|
||||
sunOrientation: getCompassValues(0, 360),
|
||||
cameraPan: getCompassValues(0, 360),
|
||||
heading: getCompassValues(0, 360),
|
||||
imageDownloadName
|
||||
};
|
||||
}
|
||||
|
||||
var realtimeProvider = {
|
||||
supportsSubscribe: function (domainObject) {
|
||||
return domainObject.type === 'example.imagery';
|
||||
},
|
||||
subscribe: function (domainObject, callback) {
|
||||
var interval = setInterval(function () {
|
||||
callback(pointForTimestamp(Date.now(), domainObject.name));
|
||||
}, IMAGE_DELAY);
|
||||
|
||||
return function () {
|
||||
clearInterval(interval);
|
||||
export default function () {
|
||||
return function install(openmct) {
|
||||
openmctInstance = openmct;
|
||||
openmct.types.addType('example.imagery', {
|
||||
key: 'example.imagery',
|
||||
name: 'Example Imagery',
|
||||
cssClass: 'icon-image',
|
||||
description: 'For development use. Creates example imagery '
|
||||
+ 'data that mimics a live imagery stream.',
|
||||
creatable: true,
|
||||
initialize: (object) => {
|
||||
object.configuration = {
|
||||
imageLocation: '',
|
||||
imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS,
|
||||
imageSamples: []
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var historicalProvider = {
|
||||
supportsRequest: function (domainObject, options) {
|
||||
return domainObject.type === 'example.imagery'
|
||||
&& options.strategy !== 'latest';
|
||||
},
|
||||
request: function (domainObject, options) {
|
||||
var start = options.start;
|
||||
var end = Math.min(options.end, Date.now());
|
||||
var data = [];
|
||||
while (start <= end && data.length < IMAGE_DELAY) {
|
||||
data.push(pointForTimestamp(start, domainObject.name));
|
||||
start += IMAGE_DELAY;
|
||||
}
|
||||
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
};
|
||||
|
||||
var ladProvider = {
|
||||
supportsRequest: function (domainObject, options) {
|
||||
return domainObject.type === 'example.imagery'
|
||||
&& options.strategy === 'latest';
|
||||
},
|
||||
request: function (domainObject, options) {
|
||||
return Promise.resolve([pointForTimestamp(Date.now(), domainObject.name)]);
|
||||
}
|
||||
};
|
||||
|
||||
return function install(openmct) {
|
||||
openmct.types.addType('example.imagery', {
|
||||
key: 'example.imagery',
|
||||
name: 'Example Imagery',
|
||||
cssClass: 'icon-image',
|
||||
description: 'For development use. Creates example imagery '
|
||||
+ 'data that mimics a live imagery stream.',
|
||||
creatable: true,
|
||||
initialize: function (object) {
|
||||
object.telemetry = {
|
||||
values: [
|
||||
{
|
||||
name: 'Name',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
name: 'Time',
|
||||
key: 'utc',
|
||||
format: 'utc',
|
||||
hints: {
|
||||
domain: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Local Time',
|
||||
key: 'local',
|
||||
format: 'local-format',
|
||||
hints: {
|
||||
domain: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Image',
|
||||
key: 'url',
|
||||
format: 'image',
|
||||
hints: {
|
||||
image: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Image Download Name',
|
||||
key: 'imageDownloadName',
|
||||
format: 'imageDownloadName',
|
||||
hints: {
|
||||
imageDownloadName: 1
|
||||
}
|
||||
object.telemetry = {
|
||||
values: [
|
||||
{
|
||||
name: 'Name',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
name: 'Time',
|
||||
key: 'utc',
|
||||
format: 'utc',
|
||||
hints: {
|
||||
domain: 2
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
{
|
||||
name: 'Local Time',
|
||||
key: 'local',
|
||||
format: 'local-format',
|
||||
hints: {
|
||||
domain: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Image',
|
||||
key: 'url',
|
||||
format: 'image',
|
||||
hints: {
|
||||
image: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Image Download Name',
|
||||
key: 'imageDownloadName',
|
||||
format: 'imageDownloadName',
|
||||
hints: {
|
||||
imageDownloadName: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
form: [
|
||||
{
|
||||
key: 'imageLocation',
|
||||
name: 'Images url list (comma separated)',
|
||||
control: 'textarea',
|
||||
cssClass: 'l-inline',
|
||||
property: [
|
||||
"configuration",
|
||||
"imageLocation"
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'imageLoadDelayInMilliSeconds',
|
||||
name: 'Image load delay (milliseconds)',
|
||||
control: 'numberfield',
|
||||
required: true,
|
||||
cssClass: 'l-inline',
|
||||
property: [
|
||||
"configuration",
|
||||
"imageLoadDelayInMilliSeconds"
|
||||
]
|
||||
}
|
||||
});
|
||||
]
|
||||
});
|
||||
|
||||
openmct.telemetry.addProvider(realtimeProvider);
|
||||
openmct.telemetry.addProvider(historicalProvider);
|
||||
openmct.telemetry.addProvider(ladProvider);
|
||||
};
|
||||
openmct.telemetry.addProvider(getRealtimeProvider());
|
||||
openmct.telemetry.addProvider(getHistoricalProvider());
|
||||
openmct.telemetry.addProvider(getLadProvider());
|
||||
};
|
||||
}
|
||||
|
||||
function getCompassValues(min, max) {
|
||||
return min + Math.random() * (max - min);
|
||||
}
|
||||
|
||||
function getImageSamples(configuration) {
|
||||
let imageSamples = DEFAULT_IMAGE_SAMPLES;
|
||||
|
||||
if (configuration.imageLocation && configuration.imageLocation.length) {
|
||||
imageSamples = getImageUrlListFromConfig(configuration);
|
||||
}
|
||||
|
||||
return ImageryPlugin;
|
||||
});
|
||||
return imageSamples;
|
||||
}
|
||||
|
||||
function getImageUrlListFromConfig(configuration) {
|
||||
return configuration.imageLocation.split(',');
|
||||
}
|
||||
|
||||
function getImageLoadDelay(domainObject) {
|
||||
const imageLoadDelay = domainObject.configuration.imageLoadDelayInMilliSeconds;
|
||||
if (!imageLoadDelay) {
|
||||
openmctInstance.objects.mutate(domainObject, 'configuration.imageLoadDelayInMilliSeconds', DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS);
|
||||
|
||||
return DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS;
|
||||
}
|
||||
|
||||
if (imageLoadDelay < MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS) {
|
||||
openmctInstance.objects.mutate(domainObject, 'configuration.imageLoadDelayInMilliSeconds', MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS);
|
||||
|
||||
return MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS;
|
||||
}
|
||||
|
||||
return imageLoadDelay;
|
||||
}
|
||||
|
||||
function getRealtimeProvider() {
|
||||
return {
|
||||
supportsSubscribe: domainObject => domainObject.type === 'example.imagery',
|
||||
subscribe: (domainObject, callback) => {
|
||||
const delay = getImageLoadDelay(domainObject);
|
||||
const interval = setInterval(() => {
|
||||
callback(pointForTimestamp(Date.now(), domainObject.name, getImageSamples(domainObject.configuration), delay));
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getHistoricalProvider() {
|
||||
return {
|
||||
supportsRequest: (domainObject, options) => {
|
||||
return domainObject.type === 'example.imagery'
|
||||
&& options.strategy !== 'latest';
|
||||
},
|
||||
request: (domainObject, options) => {
|
||||
const delay = getImageLoadDelay(domainObject);
|
||||
let start = options.start;
|
||||
const end = Math.min(options.end, Date.now());
|
||||
const data = [];
|
||||
while (start <= end && data.length < delay) {
|
||||
data.push(pointForTimestamp(start, domainObject.name, getImageSamples(domainObject.configuration), delay));
|
||||
start += delay;
|
||||
}
|
||||
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getLadProvider() {
|
||||
return {
|
||||
supportsRequest: (domainObject, options) => {
|
||||
return domainObject.type === 'example.imagery'
|
||||
&& options.strategy === 'latest';
|
||||
},
|
||||
request: (domainObject, options) => {
|
||||
const delay = getImageLoadDelay(domainObject);
|
||||
|
||||
return Promise.resolve([pointForTimestamp(Date.now(), domainObject.name, delay)]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function pointForTimestamp(timestamp, name, imageSamples, delay) {
|
||||
const url = imageSamples[Math.floor(timestamp / delay) % imageSamples.length];
|
||||
const urlItems = url.split('/');
|
||||
const imageDownloadName = `example.imagery.${urlItems[urlItems.length - 1]}`;
|
||||
|
||||
return {
|
||||
name,
|
||||
utc: Math.floor(timestamp / delay) * delay,
|
||||
local: Math.floor(timestamp / delay) * delay,
|
||||
url,
|
||||
sunOrientation: getCompassValues(0, 360),
|
||||
cameraPan: getCompassValues(0, 360),
|
||||
heading: getCompassValues(0, 360),
|
||||
imageDownloadName
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
);
|
||||
|
||||
openmct.install(openmct.plugins.LocalStorage());
|
||||
|
||||
openmct.install(openmct.plugins.Espresso());
|
||||
openmct.install(openmct.plugins.MyItems());
|
||||
openmct.install(openmct.plugins.Generator());
|
||||
@@ -196,6 +197,7 @@
|
||||
{indicator: true}
|
||||
));
|
||||
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
|
||||
openmct.install(openmct.plugins.Timer());
|
||||
openmct.start();
|
||||
</script>
|
||||
</html>
|
||||
|
||||
@@ -86,11 +86,20 @@ define(
|
||||
})
|
||||
.join('/');
|
||||
|
||||
openmct.router.navigate(url);
|
||||
function editObject() {
|
||||
const path = objectPath.slice(-1).map(obj => {
|
||||
const objNew = obj.getCapability('adapter').invoke();
|
||||
|
||||
if (isFirstViewEditable(object.useCapability('adapter'), objectPath)) {
|
||||
openmct.editor.edit();
|
||||
return objNew;
|
||||
});
|
||||
if (isFirstViewEditable(object.useCapability('adapter'), path)) {
|
||||
openmct.editor.edit();
|
||||
}
|
||||
}
|
||||
|
||||
openmct.router.once('afterNavigation', editObject);
|
||||
|
||||
openmct.router.navigate(url);
|
||||
}
|
||||
|
||||
newModel.type = this.type.getKey();
|
||||
|
||||
@@ -28,7 +28,6 @@ define([
|
||||
"./src/models/ModelCacheService",
|
||||
"./src/models/PersistedModelProvider",
|
||||
"./src/models/CachingModelDecorator",
|
||||
"./src/models/MissingModelDecorator",
|
||||
"./src/types/TypeProvider",
|
||||
"./src/actions/ActionProvider",
|
||||
"./src/actions/ActionAggregator",
|
||||
@@ -57,7 +56,6 @@ define([
|
||||
ModelCacheService,
|
||||
PersistedModelProvider,
|
||||
CachingModelDecorator,
|
||||
MissingModelDecorator,
|
||||
TypeProvider,
|
||||
ActionProvider,
|
||||
ActionAggregator,
|
||||
@@ -148,12 +146,6 @@ define([
|
||||
"cacheService"
|
||||
]
|
||||
},
|
||||
{
|
||||
"provides": "modelService",
|
||||
"type": "decorator",
|
||||
"priority": "fallback",
|
||||
"implementation": MissingModelDecorator
|
||||
},
|
||||
{
|
||||
"provides": "typeService",
|
||||
"type": "provider",
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
["../../src/models/MissingModelDecorator"],
|
||||
function (MissingModelDecorator) {
|
||||
|
||||
describe("The missing model decorator", function () {
|
||||
var mockModelService,
|
||||
testModels,
|
||||
decorator;
|
||||
|
||||
function asPromise(value) {
|
||||
return (value || {}).then ? value : {
|
||||
then: function (callback) {
|
||||
return asPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockModelService = jasmine.createSpyObj(
|
||||
"modelService",
|
||||
["getModels"]
|
||||
);
|
||||
|
||||
testModels = {
|
||||
testId: { someKey: "some value" }
|
||||
};
|
||||
|
||||
mockModelService.getModels.and.returnValue(asPromise(testModels));
|
||||
|
||||
decorator = new MissingModelDecorator(mockModelService);
|
||||
});
|
||||
|
||||
it("delegates to the wrapped model service", function () {
|
||||
decorator.getModels(['a', 'b', 'c']);
|
||||
expect(mockModelService.getModels)
|
||||
.toHaveBeenCalledWith(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it("provides models for any IDs which are missing", function () {
|
||||
var models;
|
||||
decorator.getModels(['testId', 'otherId'])
|
||||
.then(function (m) {
|
||||
models = m;
|
||||
});
|
||||
expect(models.otherId).toBeDefined();
|
||||
});
|
||||
|
||||
it("does not overwrite existing models", function () {
|
||||
var models;
|
||||
decorator.getModels(['testId', 'otherId'])
|
||||
.then(function (m) {
|
||||
models = m;
|
||||
});
|
||||
expect(models.testId).toEqual({ someKey: "some value" });
|
||||
});
|
||||
|
||||
it("does not modify the wrapped service's response", function () {
|
||||
decorator.getModels(['testId', 'otherId']);
|
||||
expect(testModels.otherId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
);
|
||||
@@ -1,209 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
"./src/services/TickerService",
|
||||
"./src/services/TimerService",
|
||||
"./src/controllers/TimerController",
|
||||
"./src/controllers/RefreshingController",
|
||||
"./src/actions/StartTimerAction",
|
||||
"./src/actions/RestartTimerAction",
|
||||
"./src/actions/StopTimerAction",
|
||||
"./src/actions/PauseTimerAction",
|
||||
"./res/templates/timer.html"
|
||||
], function (
|
||||
TickerService,
|
||||
TimerService,
|
||||
TimerController,
|
||||
RefreshingController,
|
||||
StartTimerAction,
|
||||
RestartTimerAction,
|
||||
StopTimerAction,
|
||||
PauseTimerAction,
|
||||
timerTemplate
|
||||
) {
|
||||
return {
|
||||
name: "platform/features/clock",
|
||||
definition: {
|
||||
"name": "Clocks/Timers",
|
||||
"descriptions": "Domain objects for displaying current & relative times.",
|
||||
"configuration": {
|
||||
"paths": {
|
||||
"moment-duration-format": "moment-duration-format"
|
||||
},
|
||||
"shim": {
|
||||
"moment-duration-format": {
|
||||
"deps": [
|
||||
"moment"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"constants": [
|
||||
{
|
||||
"key": "CLOCK_INDICATOR_FORMAT",
|
||||
"value": "YYYY/MM/DD HH:mm:ss"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"key": "tickerService",
|
||||
"implementation": TickerService,
|
||||
"depends": [
|
||||
"$timeout",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "timerService",
|
||||
"implementation": TimerService,
|
||||
"depends": ["openmct"]
|
||||
}
|
||||
],
|
||||
"controllers": [
|
||||
{
|
||||
"key": "TimerController",
|
||||
"implementation": TimerController,
|
||||
"depends": [
|
||||
"$scope",
|
||||
"$window",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "RefreshingController",
|
||||
"implementation": RefreshingController,
|
||||
"depends": [
|
||||
"$scope",
|
||||
"tickerService"
|
||||
]
|
||||
}
|
||||
],
|
||||
"views": [
|
||||
{
|
||||
"key": "timer",
|
||||
"type": "timer",
|
||||
"editable": false,
|
||||
"template": timerTemplate
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"key": "timer.start",
|
||||
"implementation": StartTimerAction,
|
||||
"depends": [
|
||||
"now"
|
||||
],
|
||||
"category": "contextual",
|
||||
"name": "Start",
|
||||
"cssClass": "icon-play",
|
||||
"priority": "preferred"
|
||||
},
|
||||
{
|
||||
"key": "timer.pause",
|
||||
"implementation": PauseTimerAction,
|
||||
"depends": [
|
||||
"now"
|
||||
],
|
||||
"category": "contextual",
|
||||
"name": "Pause",
|
||||
"cssClass": "icon-pause",
|
||||
"priority": "preferred"
|
||||
},
|
||||
{
|
||||
"key": "timer.restart",
|
||||
"implementation": RestartTimerAction,
|
||||
"depends": [
|
||||
"now"
|
||||
],
|
||||
"category": "contextual",
|
||||
"name": "Restart at 0",
|
||||
"cssClass": "icon-refresh",
|
||||
"priority": "preferred"
|
||||
},
|
||||
{
|
||||
"key": "timer.stop",
|
||||
"implementation": StopTimerAction,
|
||||
"depends": [
|
||||
"now"
|
||||
],
|
||||
"category": "contextual",
|
||||
"name": "Stop",
|
||||
"cssClass": "icon-box-round-corners",
|
||||
"priority": "preferred"
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
{
|
||||
"key": "timer",
|
||||
"name": "Timer",
|
||||
"cssClass": "icon-timer",
|
||||
"description": "A timer that counts up or down to a datetime. Timers can be started, stopped and reset whenever needed, and support a variety of display formats. Each Timer displays the same value to all users. Timers can be added to Display Layouts.",
|
||||
"priority": 100,
|
||||
"features": [
|
||||
"creation"
|
||||
],
|
||||
"properties": [
|
||||
{
|
||||
"key": "timestamp",
|
||||
"control": "datetime",
|
||||
"name": "Target"
|
||||
},
|
||||
{
|
||||
"key": "timerFormat",
|
||||
"control": "select",
|
||||
"name": "Display Format",
|
||||
"options": [
|
||||
{
|
||||
"value": "long",
|
||||
"name": "DDD hh:mm:ss"
|
||||
},
|
||||
{
|
||||
"value": "short",
|
||||
"name": "hh:mm:ss"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"model": {
|
||||
"timerFormat": "DDD hh:mm:ss"
|
||||
}
|
||||
}
|
||||
],
|
||||
"runs": [],
|
||||
"licenses": [
|
||||
{
|
||||
"name": "moment-duration-format",
|
||||
"version": "1.3.0",
|
||||
"author": "John Madhavan-Reese",
|
||||
"description": "Duration parsing/formatting",
|
||||
"website": "https://github.com/jsmreese/moment-duration-format",
|
||||
"copyright": "Copyright 2014 John Madhavan-Reese",
|
||||
"license": "license-mit",
|
||||
"link": "https://github.com/jsmreese/moment-duration-format/blob/master/LICENSE"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -1,37 +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-timer u-style-receiver js-style-receiver is-{{timer.timerState}}" ng-controller="TimerController as timer">
|
||||
<div class="c-timer__controls">
|
||||
<button ng-click="timer.clickStopButton()"
|
||||
ng-hide="timer.timerState == 'stopped'"
|
||||
title="Reset"
|
||||
class="c-timer__ctrl-reset c-icon-button c-icon-button--major icon-reset"></button>
|
||||
<button ng-click="timer.clickButton()"
|
||||
title="{{timer.buttonText()}}"
|
||||
class="c-timer__ctrl-pause-play c-icon-button c-icon-button--major {{timer.buttonCssClass()}}"></button>
|
||||
</div>
|
||||
<div class="c-timer__direction {{timer.signClass()}}"
|
||||
ng-hide="!timer.signClass()"></div>
|
||||
<div class="c-timer__value">{{timer.text() || "--:--:--"}}
|
||||
</div>
|
||||
<span class="c-timer__ng-controller u-contents" ng-controller="RefreshingController"></span>
|
||||
</div>
|
||||
@@ -1,70 +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(
|
||||
[],
|
||||
function () {
|
||||
|
||||
/**
|
||||
* Implements the "Pause" action for timers.
|
||||
*
|
||||
* Sets the reference pausedTime in a timer to the current
|
||||
* time, such that it stops counting up.
|
||||
*
|
||||
* @implements {Action}
|
||||
* @memberof platform/features/clock
|
||||
* @constructor
|
||||
* @param {Function} now a function which returns the current
|
||||
* time (typically wrapping `Date.now`)
|
||||
* @param {ActionContext} context the context for this action
|
||||
*/
|
||||
function PauseTimerAction(now, context) {
|
||||
this.domainObject = context.domainObject;
|
||||
this.now = now;
|
||||
}
|
||||
|
||||
PauseTimerAction.appliesTo = function (context) {
|
||||
var model =
|
||||
(context.domainObject && context.domainObject.getModel())
|
||||
|| {};
|
||||
|
||||
// We show this variant for timers which have
|
||||
// a target time, or is in a playing state.
|
||||
return model.type === 'timer'
|
||||
&& model.timerState === 'started';
|
||||
};
|
||||
|
||||
PauseTimerAction.prototype.perform = function () {
|
||||
var domainObject = this.domainObject,
|
||||
now = this.now;
|
||||
|
||||
function updateModel(model) {
|
||||
model.timerState = 'paused';
|
||||
model.pausedTime = now();
|
||||
}
|
||||
|
||||
return domainObject.useCapability('mutation', updateModel);
|
||||
};
|
||||
|
||||
return PauseTimerAction;
|
||||
}
|
||||
);
|
||||
@@ -1,70 +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(
|
||||
[],
|
||||
function () {
|
||||
|
||||
/**
|
||||
* Implements the "Restart at 0" action.
|
||||
*
|
||||
* Behaves the same as (and delegates functionality to)
|
||||
* the "Start" action.
|
||||
*
|
||||
* @implements {Action}
|
||||
* @memberof platform/features/clock
|
||||
* @constructor
|
||||
* @param {Function} now a function which returns the current
|
||||
* time (typically wrapping `Date.now`)
|
||||
* @param {ActionContext} context the context for this action
|
||||
*/
|
||||
function RestartTimerAction(now, context) {
|
||||
this.domainObject = context.domainObject;
|
||||
this.now = now;
|
||||
}
|
||||
|
||||
RestartTimerAction.appliesTo = function (context) {
|
||||
var model =
|
||||
(context.domainObject && context.domainObject.getModel())
|
||||
|| {};
|
||||
|
||||
// We show this variant for timers which already have a target time.
|
||||
return model.type === 'timer'
|
||||
&& model.timerState !== 'stopped';
|
||||
};
|
||||
|
||||
RestartTimerAction.prototype.perform = function () {
|
||||
var domainObject = this.domainObject,
|
||||
now = this.now;
|
||||
|
||||
function updateModel(model) {
|
||||
model.timestamp = now();
|
||||
model.timerState = 'started';
|
||||
model.pausedTime = undefined;
|
||||
}
|
||||
|
||||
return domainObject.useCapability('mutation', updateModel);
|
||||
};
|
||||
|
||||
return RestartTimerAction;
|
||||
}
|
||||
);
|
||||
@@ -1,78 +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(
|
||||
[],
|
||||
function () {
|
||||
|
||||
/**
|
||||
* Implements the "Start" action for timers.
|
||||
*
|
||||
* Sets the reference timestamp in a timer to the current
|
||||
* time, such that it begins counting up.
|
||||
*
|
||||
* @implements {Action}
|
||||
* @memberof platform/features/clock
|
||||
* @constructor
|
||||
* @param {Function} now a function which returns the current
|
||||
* time (typically wrapping `Date.now`)
|
||||
* @param {ActionContext} context the context for this action
|
||||
*/
|
||||
function StartTimerAction(now, context) {
|
||||
this.domainObject = context.domainObject;
|
||||
this.now = now;
|
||||
}
|
||||
|
||||
StartTimerAction.appliesTo = function (context) {
|
||||
var model =
|
||||
(context.domainObject && context.domainObject.getModel())
|
||||
|| {};
|
||||
|
||||
// We show this variant for timers which do not yet have
|
||||
// a target time.
|
||||
return model.type === 'timer'
|
||||
&& model.timerState !== 'started';
|
||||
};
|
||||
|
||||
StartTimerAction.prototype.perform = function () {
|
||||
var domainObject = this.domainObject,
|
||||
now = this.now;
|
||||
|
||||
function updateModel(model) {
|
||||
//if we are resuming
|
||||
if (model.pausedTime) {
|
||||
var timeShift = now() - model.pausedTime;
|
||||
model.timestamp = model.timestamp + timeShift;
|
||||
} else {
|
||||
model.timestamp = now();
|
||||
}
|
||||
|
||||
model.timerState = 'started';
|
||||
model.pausedTime = undefined;
|
||||
}
|
||||
|
||||
return domainObject.useCapability('mutation', updateModel);
|
||||
};
|
||||
|
||||
return StartTimerAction;
|
||||
}
|
||||
);
|
||||
@@ -1,70 +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(
|
||||
[],
|
||||
function () {
|
||||
|
||||
/**
|
||||
* Implements the "Stop" action for timers.
|
||||
*
|
||||
* Sets the reference timestamp in a timer undefined,
|
||||
* such that it is reset and makes no movements.
|
||||
*
|
||||
* @implements {Action}
|
||||
* @memberof platform/features/clock
|
||||
* @constructor
|
||||
* @param {Function} now a function which returns the current
|
||||
* time (typically wrapping `Date.now`)
|
||||
* @param {ActionContext} context the context for this action
|
||||
*/
|
||||
function StopTimerAction(now, context) {
|
||||
this.domainObject = context.domainObject;
|
||||
this.now = now;
|
||||
}
|
||||
|
||||
StopTimerAction.appliesTo = function (context) {
|
||||
var model =
|
||||
(context.domainObject && context.domainObject.getModel())
|
||||
|| {};
|
||||
|
||||
// We show this variant for timers which do not yet have
|
||||
// a target time.
|
||||
return model.type === 'timer'
|
||||
&& model.timerState !== 'stopped';
|
||||
};
|
||||
|
||||
StopTimerAction.prototype.perform = function () {
|
||||
var domainObject = this.domainObject;
|
||||
|
||||
function updateModel(model) {
|
||||
model.timestamp = undefined;
|
||||
model.timerState = 'stopped';
|
||||
model.pausedTime = undefined;
|
||||
}
|
||||
|
||||
return domainObject.useCapability('mutation', updateModel);
|
||||
};
|
||||
|
||||
return StopTimerAction;
|
||||
}
|
||||
);
|
||||
@@ -1,55 +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(
|
||||
[],
|
||||
function () {
|
||||
|
||||
/**
|
||||
* Continually refreshes the represented domain object.
|
||||
*
|
||||
* This is a short-term workaround to assure Timer views stay
|
||||
* up-to-date; should be replaced by a global auto-refresh.
|
||||
*
|
||||
* @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 RefreshingController($scope, tickerService) {
|
||||
var unlisten;
|
||||
|
||||
function triggerRefresh() {
|
||||
var persistence = $scope.domainObject
|
||||
&& $scope.domainObject.getCapability('persistence');
|
||||
|
||||
return persistence && persistence.refresh();
|
||||
}
|
||||
|
||||
unlisten = tickerService.listen(triggerRefresh);
|
||||
$scope.$on('$destroy', unlisten);
|
||||
}
|
||||
|
||||
return RefreshingController;
|
||||
}
|
||||
);
|
||||
@@ -1,239 +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(
|
||||
['./TimerFormatter'],
|
||||
function (TimerFormatter) {
|
||||
|
||||
var FORMATTER = new TimerFormatter();
|
||||
|
||||
/**
|
||||
* Controller for views of a Timer domain object.
|
||||
*
|
||||
* @constructor
|
||||
* @memberof platform/features/clock
|
||||
* @param {angular.Scope} $scope the Angular scope
|
||||
* @param $window Angular-provided window object
|
||||
* @param {Function} now a function which returns the current
|
||||
* time (typically wrapping `Date.now`)
|
||||
*/
|
||||
function TimerController($scope, $window, now) {
|
||||
var formatter,
|
||||
active = true,
|
||||
relativeTimestamp,
|
||||
lastTimestamp,
|
||||
relativeTimerState,
|
||||
self = this;
|
||||
|
||||
function update() {
|
||||
var timeDelta = lastTimestamp - relativeTimestamp;
|
||||
|
||||
if (formatter && !isNaN(timeDelta)) {
|
||||
self.textValue = formatter(timeDelta);
|
||||
self.signValue = timeDelta < 0 ? "-"
|
||||
: timeDelta >= 1000 ? "+" : "";
|
||||
self.signCssClass = timeDelta < 0 ? "icon-minus"
|
||||
: timeDelta >= 1000 ? "icon-plus" : "";
|
||||
} else {
|
||||
self.textValue = "";
|
||||
self.signValue = "";
|
||||
self.signCssClass = "";
|
||||
}
|
||||
}
|
||||
|
||||
function updateFormat(key) {
|
||||
formatter = FORMATTER[key] || FORMATTER.long;
|
||||
}
|
||||
|
||||
function updateTimestamp(timestamp) {
|
||||
relativeTimestamp = timestamp;
|
||||
}
|
||||
|
||||
function updateTimerState(timerState) {
|
||||
self.timerState = relativeTimerState = timerState;
|
||||
}
|
||||
|
||||
function updateActions(actionCapability, actionKey) {
|
||||
self.relevantAction = actionCapability
|
||||
&& actionCapability.getActions(actionKey)[0];
|
||||
|
||||
self.stopAction = relativeTimerState !== 'stopped'
|
||||
? actionCapability && actionCapability.getActions('timer.stop')[0] : undefined;
|
||||
|
||||
}
|
||||
|
||||
function isPaused() {
|
||||
return relativeTimerState === 'paused';
|
||||
}
|
||||
|
||||
function handleLegacyTimer(model) {
|
||||
if (model.timerState === undefined) {
|
||||
model.timerState = model.timestamp === undefined
|
||||
? 'stopped' : 'started';
|
||||
}
|
||||
}
|
||||
|
||||
function updateObject(domainObject) {
|
||||
var model = domainObject.getModel();
|
||||
handleLegacyTimer(model);
|
||||
|
||||
var timestamp = model.timestamp,
|
||||
formatKey = model.timerFormat,
|
||||
timerState = model.timerState,
|
||||
actionCapability = domainObject.getCapability('action'),
|
||||
actionKey = (timerState !== 'started')
|
||||
? 'timer.start' : 'timer.pause';
|
||||
|
||||
updateFormat(formatKey);
|
||||
updateTimestamp(timestamp);
|
||||
updateTimerState(timerState);
|
||||
updateActions(actionCapability, actionKey);
|
||||
|
||||
//if paused on startup show last known position
|
||||
if (isPaused() && !lastTimestamp) {
|
||||
lastTimestamp = model.pausedTime;
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
function handleObjectChange(domainObject) {
|
||||
if (domainObject) {
|
||||
updateObject(domainObject);
|
||||
}
|
||||
}
|
||||
|
||||
function handleModification() {
|
||||
handleObjectChange($scope.domainObject);
|
||||
}
|
||||
|
||||
function tick() {
|
||||
var lastSign = self.signValue,
|
||||
lastText = self.textValue;
|
||||
|
||||
if (!isPaused()) {
|
||||
lastTimestamp = now();
|
||||
update();
|
||||
}
|
||||
|
||||
if (relativeTimerState === undefined) {
|
||||
handleModification();
|
||||
}
|
||||
|
||||
// We're running in an animation frame, not in a digest cycle.
|
||||
// We need to trigger a digest cycle if our displayable data
|
||||
// changes.
|
||||
if (lastSign !== self.signValue || lastText !== self.textValue) {
|
||||
$scope.$apply();
|
||||
}
|
||||
|
||||
if (active) {
|
||||
$window.requestAnimationFrame(tick);
|
||||
}
|
||||
}
|
||||
|
||||
$window.requestAnimationFrame(tick);
|
||||
|
||||
// Pull in the timer format from the domain object model
|
||||
$scope.$watch('domainObject', handleObjectChange);
|
||||
$scope.$watch('model.modified', handleModification);
|
||||
|
||||
// When the scope is destroyed, stop requesting anim. frames
|
||||
$scope.$on('$destroy', function () {
|
||||
active = false;
|
||||
});
|
||||
|
||||
this.$scope = $scope;
|
||||
this.signValue = '';
|
||||
this.textValue = '';
|
||||
this.updateObject = updateObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the CSS class to display the right icon
|
||||
* for the start/pause button.
|
||||
* @returns {string} cssclass to display
|
||||
*/
|
||||
TimerController.prototype.buttonCssClass = function () {
|
||||
return this.relevantAction
|
||||
? this.relevantAction.getMetadata().cssClass : "";
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the text to show for the start/pause button
|
||||
* (e.g. in a tooltip)
|
||||
* @returns {string} name of the action
|
||||
*/
|
||||
TimerController.prototype.buttonText = function () {
|
||||
return this.relevantAction
|
||||
? this.relevantAction.getMetadata().name : "";
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform the action associated with the start/pause button.
|
||||
*/
|
||||
TimerController.prototype.clickButton = function () {
|
||||
if (this.relevantAction) {
|
||||
this.relevantAction.perform();
|
||||
this.updateObject(this.$scope.domainObject);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform the action associated with the stop button.
|
||||
*/
|
||||
TimerController.prototype.clickStopButton = function () {
|
||||
if (this.stopAction) {
|
||||
this.stopAction.perform();
|
||||
this.updateObject(this.$scope.domainObject);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the sign (+ or -) of the current timer value, as
|
||||
* displayable text.
|
||||
* @returns {string} sign of the current timer value
|
||||
*/
|
||||
TimerController.prototype.sign = function () {
|
||||
return this.signValue;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the sign (+ or -) of the current timer value, as
|
||||
* a CSS class.
|
||||
* @returns {string} sign of the current timer value
|
||||
*/
|
||||
TimerController.prototype.signClass = function () {
|
||||
return this.signCssClass;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the text to display for the current timer value.
|
||||
* @returns {string} current timer value
|
||||
*/
|
||||
TimerController.prototype.text = function () {
|
||||
return this.textValue;
|
||||
};
|
||||
|
||||
return TimerController;
|
||||
}
|
||||
);
|
||||
@@ -1,73 +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-duration-format'],
|
||||
function (moment) {
|
||||
|
||||
var SHORT_FORMAT = "HH:mm:ss",
|
||||
LONG_FORMAT = "d[D] HH:mm:ss";
|
||||
|
||||
/**
|
||||
* Provides formatting functions for Timers.
|
||||
*
|
||||
* Display formats for timers are a little different from what
|
||||
* moment.js provides, so we have custom logic here. This specifically
|
||||
* supports `TimerController`.
|
||||
*
|
||||
* @constructor
|
||||
* @memberof platform/features/clock
|
||||
*/
|
||||
function TimerFormatter() {
|
||||
}
|
||||
|
||||
// Round this timestamp down to the second boundary
|
||||
// (e.g. 1124ms goes down to 1000ms, -2400ms goes down to -3000ms)
|
||||
function toWholeSeconds(duration) {
|
||||
return Math.abs(Math.floor(duration / 1000) * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a duration for display, using the short form.
|
||||
* (e.g. 03:33:11)
|
||||
* @param {number} duration the duration, in milliseconds
|
||||
* @param {boolean} sign true if positive
|
||||
*/
|
||||
TimerFormatter.prototype.short = function (duration) {
|
||||
return moment.duration(toWholeSeconds(duration), 'ms')
|
||||
.format(SHORT_FORMAT, { trim: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a duration for display, using the long form.
|
||||
* (e.g. 0d 03:33:11)
|
||||
* @param {number} duration the duration, in milliseconds
|
||||
* @param {boolean} sign true if positive
|
||||
*/
|
||||
TimerFormatter.prototype.long = function (duration) {
|
||||
return moment.duration(toWholeSeconds(duration), 'ms')
|
||||
.format(LONG_FORMAT, { trim: false });
|
||||
};
|
||||
|
||||
return TimerFormatter;
|
||||
}
|
||||
);
|
||||
@@ -1,87 +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(
|
||||
[],
|
||||
function () {
|
||||
|
||||
/**
|
||||
* Calls functions every second, as close to the actual second
|
||||
* tick as is feasible.
|
||||
* @constructor
|
||||
* @memberof platform/features/clock
|
||||
* @param $timeout Angular's $timeout
|
||||
* @param {Function} now function to provide the current time in ms
|
||||
*/
|
||||
function TickerService($timeout, now) {
|
||||
var self = this;
|
||||
|
||||
function tick() {
|
||||
var timestamp = now(),
|
||||
millis = timestamp % 1000;
|
||||
|
||||
// Only update callbacks if a second has actually passed.
|
||||
if (timestamp >= self.last + 1000) {
|
||||
self.callbacks.forEach(function (callback) {
|
||||
callback(timestamp);
|
||||
});
|
||||
self.last = timestamp - millis;
|
||||
}
|
||||
|
||||
// Try to update at exactly the next second
|
||||
$timeout(tick, 1000 - millis, true);
|
||||
}
|
||||
|
||||
tick();
|
||||
|
||||
this.callbacks = [];
|
||||
this.last = now() - 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for clock ticks. The provided callback will
|
||||
* be invoked with the current timestamp (in milliseconds
|
||||
* since Jan 1 1970) at regular intervals, as near to the
|
||||
* second boundary as possible.
|
||||
*
|
||||
* @param {Function} callback callback to invoke
|
||||
* @returns {Function} a function to unregister this listener
|
||||
*/
|
||||
TickerService.prototype.listen = function (callback) {
|
||||
var self = this;
|
||||
|
||||
self.callbacks.push(callback);
|
||||
|
||||
// Provide immediate feedback
|
||||
callback(this.last);
|
||||
|
||||
// Provide a deregistration function
|
||||
return function () {
|
||||
self.callbacks = self.callbacks.filter(function (cb) {
|
||||
return cb !== callback;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
return TickerService;
|
||||
}
|
||||
);
|
||||
@@ -1,113 +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(['EventEmitter'], function (EventEmitter) {
|
||||
|
||||
/**
|
||||
* Tracks the currently-followed Timer object. Used by
|
||||
* timelines et al to synchronize to a particular timer.
|
||||
*
|
||||
* The TimerService emits `change` events when the active timer
|
||||
* is changed.
|
||||
*/
|
||||
function TimerService(openmct) {
|
||||
EventEmitter.apply(this);
|
||||
this.time = openmct.time;
|
||||
this.objects = openmct.objects;
|
||||
}
|
||||
|
||||
TimerService.prototype = Object.create(EventEmitter.prototype);
|
||||
|
||||
/**
|
||||
* Set (or clear, if `timer` is undefined) the currently active timer.
|
||||
* @param {DomainObject} timer the new active timer
|
||||
* @emits change
|
||||
*/
|
||||
TimerService.prototype.setTimer = function (timer) {
|
||||
this.timer = timer;
|
||||
this.emit('change', timer);
|
||||
|
||||
if (this.stopObserving) {
|
||||
this.stopObserving();
|
||||
delete this.stopObserving;
|
||||
}
|
||||
|
||||
if (timer) {
|
||||
this.stopObserving =
|
||||
this.objects.observe(timer, '*', this.setTimer.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the currently active timer.
|
||||
* @return {DomainObject} the active timer
|
||||
* @emits change
|
||||
*/
|
||||
TimerService.prototype.getTimer = function () {
|
||||
return this.timer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if there is a currently active timer.
|
||||
* @return {boolean} true if there is a timer
|
||||
*/
|
||||
TimerService.prototype.hasTimer = function () {
|
||||
return Boolean(this.timer);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert the provided timestamp to milliseconds relative to
|
||||
* the active timer.
|
||||
* @return {number} milliseconds since timer start
|
||||
*/
|
||||
TimerService.prototype.convert = function (timestamp) {
|
||||
var clock = this.time.clock();
|
||||
var canConvert = this.hasTimer()
|
||||
&& Boolean(clock)
|
||||
&& this.timer.timerState !== 'stopped';
|
||||
|
||||
if (!canConvert) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
var now = clock.currentValue();
|
||||
var delta = this.timer.timerState === 'paused'
|
||||
? now - this.timer.pausedTime : 0;
|
||||
var epoch = this.timer.timestamp;
|
||||
|
||||
return timestamp - epoch - delta;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the value of the active clock, adjusted to be relative to the active
|
||||
* timer. If there is no clock or no active timer, this will return
|
||||
* `undefined`.
|
||||
* @return {number} milliseconds since the start of the active timer
|
||||
*/
|
||||
TimerService.prototype.now = function () {
|
||||
var clock = this.time.clock();
|
||||
|
||||
return clock && this.convert(clock.currentValue());
|
||||
};
|
||||
|
||||
return TimerService;
|
||||
});
|
||||
@@ -1,106 +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/actions/PauseTimerAction"],
|
||||
function (PauseTimerAction) {
|
||||
|
||||
describe("A timer's Pause action", function () {
|
||||
var mockNow,
|
||||
mockDomainObject,
|
||||
testModel,
|
||||
testContext,
|
||||
action;
|
||||
|
||||
function asPromise(value) {
|
||||
return (value || {}).then ? value : {
|
||||
then: function (callback) {
|
||||
return asPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function testState(type, timerState, timestamp, expected) {
|
||||
testModel.type = type;
|
||||
testModel.timerState = timerState;
|
||||
testModel.timestamp = timestamp;
|
||||
|
||||
if (expected) {
|
||||
expect(PauseTimerAction.appliesTo(testContext)).toBeTruthy();
|
||||
} else {
|
||||
expect(PauseTimerAction.appliesTo(testContext)).toBeFalsy();
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockNow = jasmine.createSpy('now');
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
'domainObject',
|
||||
['getCapability', 'useCapability', 'getModel']
|
||||
);
|
||||
|
||||
mockDomainObject.useCapability.and.callFake(function (c, v) {
|
||||
if (c === 'mutation') {
|
||||
testModel = v(testModel) || testModel;
|
||||
|
||||
return asPromise(true);
|
||||
}
|
||||
});
|
||||
mockDomainObject.getModel.and.callFake(function () {
|
||||
return testModel;
|
||||
});
|
||||
|
||||
testModel = {};
|
||||
testContext = {domainObject: mockDomainObject};
|
||||
|
||||
action = new PauseTimerAction(mockNow, testContext);
|
||||
});
|
||||
|
||||
it("updates the model with a timerState", function () {
|
||||
testModel.timerState = 'started';
|
||||
action.perform();
|
||||
expect(testModel.timerState).toEqual('paused');
|
||||
});
|
||||
|
||||
it("updates the model with a pausedTime", function () {
|
||||
testModel.pausedTime = undefined;
|
||||
mockNow.and.returnValue(12000);
|
||||
action.perform();
|
||||
expect(testModel.pausedTime).toEqual(12000);
|
||||
});
|
||||
|
||||
it("applies only to timers in a playing state", function () {
|
||||
//in a stopped state
|
||||
testState('timer', 'stopped', undefined, false);
|
||||
|
||||
//in a paused state
|
||||
testState('timer', 'paused', 12000, false);
|
||||
|
||||
//in a playing state
|
||||
testState('timer', 'started', 12000, true);
|
||||
|
||||
//not a timer
|
||||
testState('clock', 'started', 12000, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,112 +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/actions/RestartTimerAction"],
|
||||
function (RestartTimerAction) {
|
||||
|
||||
describe("A timer's restart action", function () {
|
||||
var mockNow,
|
||||
mockDomainObject,
|
||||
testModel,
|
||||
testContext,
|
||||
action;
|
||||
|
||||
function asPromise(value) {
|
||||
return (value || {}).then ? value : {
|
||||
then: function (callback) {
|
||||
return asPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function testState(type, timerState, timestamp, expected) {
|
||||
testModel.type = type;
|
||||
testModel.timerState = timerState;
|
||||
testModel.timestamp = timestamp;
|
||||
|
||||
if (expected) {
|
||||
expect(RestartTimerAction.appliesTo(testContext)).toBeTruthy();
|
||||
} else {
|
||||
expect(RestartTimerAction.appliesTo(testContext)).toBeFalsy();
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockNow = jasmine.createSpy('now');
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
'domainObject',
|
||||
['getCapability', 'useCapability', 'getModel']
|
||||
);
|
||||
|
||||
mockDomainObject.useCapability.and.callFake(function (c, v) {
|
||||
if (c === 'mutation') {
|
||||
testModel = v(testModel) || testModel;
|
||||
|
||||
return asPromise(true);
|
||||
}
|
||||
});
|
||||
mockDomainObject.getModel.and.callFake(function () {
|
||||
return testModel;
|
||||
});
|
||||
|
||||
testModel = {};
|
||||
testContext = { domainObject: mockDomainObject };
|
||||
|
||||
action = new RestartTimerAction(mockNow, testContext);
|
||||
});
|
||||
|
||||
it("updates the model with a timestamp", function () {
|
||||
testModel.pausedTime = 12000;
|
||||
mockNow.and.returnValue(12000);
|
||||
action.perform();
|
||||
expect(testModel.timestamp).toEqual(12000);
|
||||
});
|
||||
|
||||
it("updates the model with a pausedTime", function () {
|
||||
testModel.pausedTime = 12000;
|
||||
action.perform();
|
||||
expect(testModel.pausedTime).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("updates the model with a timerState", function () {
|
||||
testModel.timerState = 'stopped';
|
||||
action.perform();
|
||||
expect(testModel.timerState).toEqual('started');
|
||||
});
|
||||
|
||||
it("applies only to timers in a non-stopped state", function () {
|
||||
//in a stopped state
|
||||
testState('timer', 'stopped', undefined, false);
|
||||
|
||||
//in a paused state
|
||||
testState('timer', 'paused', 12000, true);
|
||||
|
||||
//in a playing state
|
||||
testState('timer', 'started', 12000, true);
|
||||
|
||||
//not a timer
|
||||
testState('clock', 'paused', 12000, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,111 +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/actions/StartTimerAction"],
|
||||
function (StartTimerAction) {
|
||||
|
||||
describe("A timer's start action", function () {
|
||||
var mockNow,
|
||||
mockDomainObject,
|
||||
testModel,
|
||||
testContext,
|
||||
action;
|
||||
|
||||
function asPromise(value) {
|
||||
return (value || {}).then ? value : {
|
||||
then: function (callback) {
|
||||
return asPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function testState(type, timerState, timestamp, expected) {
|
||||
testModel.type = type;
|
||||
testModel.timerState = timerState;
|
||||
testModel.timestamp = timestamp;
|
||||
|
||||
if (expected) {
|
||||
expect(StartTimerAction.appliesTo(testContext)).toBeTruthy();
|
||||
} else {
|
||||
expect(StartTimerAction.appliesTo(testContext)).toBeFalsy();
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockNow = jasmine.createSpy('now');
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
'domainObject',
|
||||
['getCapability', 'useCapability', 'getModel']
|
||||
);
|
||||
|
||||
mockDomainObject.useCapability.and.callFake(function (c, v) {
|
||||
if (c === 'mutation') {
|
||||
testModel = v(testModel) || testModel;
|
||||
|
||||
return asPromise(true);
|
||||
}
|
||||
});
|
||||
mockDomainObject.getModel.and.callFake(function () {
|
||||
return testModel;
|
||||
});
|
||||
|
||||
testModel = {};
|
||||
testContext = {domainObject: mockDomainObject};
|
||||
|
||||
action = new StartTimerAction(mockNow, testContext);
|
||||
});
|
||||
|
||||
it("updates the model with a timestamp", function () {
|
||||
mockNow.and.returnValue(12000);
|
||||
action.perform();
|
||||
expect(testModel.timestamp).toEqual(12000);
|
||||
});
|
||||
|
||||
it("updates the model with a pausedTime", function () {
|
||||
testModel.pausedTime = 12000;
|
||||
action.perform();
|
||||
expect(testModel.pausedTime).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("updates the model with a timerState", function () {
|
||||
testModel.timerState = undefined;
|
||||
action.perform();
|
||||
expect(testModel.timerState).toEqual('started');
|
||||
});
|
||||
|
||||
it("applies only to timers not in a playing state", function () {
|
||||
//in a stopped state
|
||||
testState('timer', 'stopped', undefined, true);
|
||||
|
||||
//in a paused state
|
||||
testState('timer', 'paused', 12000, true);
|
||||
|
||||
//in a playing state
|
||||
testState('timer', 'started', 12000, false);
|
||||
|
||||
//not a timer
|
||||
testState('clock', 'paused', 12000, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,111 +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/actions/StopTimerAction"],
|
||||
function (StopTimerAction) {
|
||||
|
||||
describe("A timer's stop action", function () {
|
||||
var mockNow,
|
||||
mockDomainObject,
|
||||
testModel,
|
||||
testContext,
|
||||
action;
|
||||
|
||||
function asPromise(value) {
|
||||
return (value || {}).then ? value : {
|
||||
then: function (callback) {
|
||||
return asPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function testState(type, timerState, timestamp, expected) {
|
||||
testModel.type = type;
|
||||
testModel.timerState = timerState;
|
||||
testModel.timestamp = timestamp;
|
||||
|
||||
if (expected) {
|
||||
expect(StopTimerAction.appliesTo(testContext)).toBeTruthy();
|
||||
} else {
|
||||
expect(StopTimerAction.appliesTo(testContext)).toBeFalsy();
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockNow = jasmine.createSpy('now');
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
'domainObject',
|
||||
['getCapability', 'useCapability', 'getModel']
|
||||
);
|
||||
|
||||
mockDomainObject.useCapability.and.callFake(function (c, v) {
|
||||
if (c === 'mutation') {
|
||||
testModel = v(testModel) || testModel;
|
||||
|
||||
return asPromise(true);
|
||||
}
|
||||
});
|
||||
mockDomainObject.getModel.and.callFake(function () {
|
||||
return testModel;
|
||||
});
|
||||
|
||||
testModel = {};
|
||||
testContext = {domainObject: mockDomainObject};
|
||||
|
||||
action = new StopTimerAction(mockNow, testContext);
|
||||
});
|
||||
|
||||
it("updates the model with a timestamp", function () {
|
||||
mockNow.and.returnValue(12000);
|
||||
action.perform();
|
||||
expect(testModel.timestamp).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("updates the model with a pausedTime", function () {
|
||||
testModel.pausedTime = 12000;
|
||||
action.perform();
|
||||
expect(testModel.pausedTime).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("updates the model with a timerState", function () {
|
||||
testModel.timerState = 'started';
|
||||
action.perform();
|
||||
expect(testModel.timerState).toEqual('stopped');
|
||||
});
|
||||
|
||||
it("applies only to timers in a non-stopped state", function () {
|
||||
//in a stopped state
|
||||
testState('timer', 'stopped', undefined, false);
|
||||
|
||||
//in a paused state
|
||||
testState('timer', 'paused', 12000, true);
|
||||
|
||||
//in a playing state
|
||||
testState('timer', 'started', 12000, true);
|
||||
|
||||
//not a timer
|
||||
testState('clock', 'paused', 12000, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,80 +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/controllers/RefreshingController"],
|
||||
function (RefreshingController) {
|
||||
|
||||
describe("The refreshing controller", function () {
|
||||
var mockScope,
|
||||
mockTicker,
|
||||
mockUnticker,
|
||||
controller;
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj('$scope', ['$on']);
|
||||
mockTicker = jasmine.createSpyObj('ticker', ['listen']);
|
||||
mockUnticker = jasmine.createSpy('unticker');
|
||||
|
||||
mockTicker.listen.and.returnValue(mockUnticker);
|
||||
|
||||
controller = new RefreshingController(mockScope, mockTicker);
|
||||
});
|
||||
|
||||
it("refreshes the represented object on every tick", function () {
|
||||
var mockDomainObject = jasmine.createSpyObj(
|
||||
'domainObject',
|
||||
['getCapability']
|
||||
),
|
||||
mockPersistence = jasmine.createSpyObj(
|
||||
'persistence',
|
||||
['persist', 'refresh']
|
||||
);
|
||||
|
||||
mockDomainObject.getCapability.and.callFake(function (c) {
|
||||
return (c === 'persistence') && mockPersistence;
|
||||
});
|
||||
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
|
||||
mockTicker.listen.calls.mostRecent().args[0](12321);
|
||||
expect(mockPersistence.refresh).toHaveBeenCalled();
|
||||
expect(mockPersistence.persist).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,227 +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/controllers/TimerController"],
|
||||
function (TimerController) {
|
||||
|
||||
// Wed, 03 Jun 2015 17:56:14 GMT
|
||||
var TEST_TIMESTAMP = 1433354174000;
|
||||
|
||||
describe("A timer view's controller", function () {
|
||||
var mockScope,
|
||||
mockWindow,
|
||||
mockNow,
|
||||
mockDomainObject,
|
||||
mockActionCapability,
|
||||
mockStart,
|
||||
mockPause,
|
||||
mockStop,
|
||||
testModel,
|
||||
controller;
|
||||
|
||||
function invokeWatch(expr, value) {
|
||||
mockScope.$watch.calls.all().forEach(function (call) {
|
||||
if (call.args[0] === expr) {
|
||||
call.args[1](value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj(
|
||||
'$scope',
|
||||
['$watch', '$on', '$apply']
|
||||
);
|
||||
mockWindow = jasmine.createSpyObj(
|
||||
'$window',
|
||||
['requestAnimationFrame']
|
||||
);
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
'domainObject',
|
||||
['getCapability', 'useCapability', 'getModel']
|
||||
);
|
||||
mockActionCapability = jasmine.createSpyObj(
|
||||
'action',
|
||||
['getActions']
|
||||
);
|
||||
mockStart = jasmine.createSpyObj(
|
||||
'start',
|
||||
['getMetadata', 'perform']
|
||||
);
|
||||
mockPause = jasmine.createSpyObj(
|
||||
'paused',
|
||||
['getMetadata', 'perform']
|
||||
);
|
||||
mockStop = jasmine.createSpyObj(
|
||||
'stopped',
|
||||
['getMetadata', 'perform']
|
||||
);
|
||||
mockNow = jasmine.createSpy('now');
|
||||
|
||||
mockDomainObject.getCapability.and.callFake(function (c) {
|
||||
return (c === 'action') && mockActionCapability;
|
||||
});
|
||||
mockDomainObject.getModel.and.callFake(function () {
|
||||
return testModel;
|
||||
});
|
||||
mockActionCapability.getActions.and.callFake(function (k) {
|
||||
return [{
|
||||
'timer.start': mockStart,
|
||||
'timer.pause': mockPause,
|
||||
'timer.stop': mockStop
|
||||
}[k]];
|
||||
});
|
||||
|
||||
mockStart.getMetadata.and.returnValue({
|
||||
cssClass: "icon-play",
|
||||
name: "Start"
|
||||
});
|
||||
mockPause.getMetadata.and.returnValue({
|
||||
cssClass: "icon-pause",
|
||||
name: "Pause"
|
||||
});
|
||||
mockStop.getMetadata.and.returnValue({
|
||||
cssClass: "icon-box-round-corners",
|
||||
name: "Stop"
|
||||
});
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
|
||||
testModel = {};
|
||||
|
||||
controller = new TimerController(mockScope, mockWindow, mockNow);
|
||||
});
|
||||
|
||||
it("watches for the domain object in view", function () {
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||
"domainObject",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("watches for domain object modifications", function () {
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||
"model.modified",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("updates on a timer", function () {
|
||||
expect(mockWindow.requestAnimationFrame)
|
||||
.toHaveBeenCalledWith(jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("displays nothing when there is no target", function () {
|
||||
// Notify that domain object is available via scope
|
||||
invokeWatch('domainObject', mockDomainObject);
|
||||
mockNow.and.returnValue(TEST_TIMESTAMP);
|
||||
mockWindow.requestAnimationFrame.calls.mostRecent().args[0]();
|
||||
expect(controller.sign()).toEqual("");
|
||||
expect(controller.signClass()).toEqual("");
|
||||
expect(controller.text()).toEqual("");
|
||||
});
|
||||
|
||||
it("formats time to display relative to target", function () {
|
||||
testModel.timestamp = TEST_TIMESTAMP;
|
||||
testModel.timerFormat = 'long';
|
||||
// Notify that domain object is available via scope
|
||||
invokeWatch('domainObject', mockDomainObject);
|
||||
|
||||
mockNow.and.returnValue(TEST_TIMESTAMP + 121000);
|
||||
mockWindow.requestAnimationFrame.calls.mostRecent().args[0]();
|
||||
expect(controller.sign()).toEqual("+");
|
||||
expect(controller.signClass()).toEqual("icon-plus");
|
||||
expect(controller.text()).toEqual("0D 00:02:01");
|
||||
|
||||
mockNow.and.returnValue(TEST_TIMESTAMP - 121000);
|
||||
mockWindow.requestAnimationFrame.calls.mostRecent().args[0]();
|
||||
expect(controller.sign()).toEqual("-");
|
||||
expect(controller.signClass()).toEqual("icon-minus");
|
||||
expect(controller.text()).toEqual("0D 00:02:01");
|
||||
|
||||
mockNow.and.returnValue(TEST_TIMESTAMP);
|
||||
mockWindow.requestAnimationFrame.calls.mostRecent().args[0]();
|
||||
expect(controller.sign()).toEqual("");
|
||||
expect(controller.signClass()).toEqual("");
|
||||
expect(controller.text()).toEqual("0D 00:00:00");
|
||||
});
|
||||
|
||||
it("shows cssClass & name for the applicable start/pause action", function () {
|
||||
invokeWatch('domainObject', mockDomainObject);
|
||||
expect(controller.buttonCssClass()).toEqual("icon-play");
|
||||
expect(controller.buttonText()).toEqual("Start");
|
||||
|
||||
testModel.timestamp = 12321;
|
||||
testModel.timerState = 'started';
|
||||
invokeWatch('model.modified', 1);
|
||||
expect(controller.buttonCssClass()).toEqual("icon-pause");
|
||||
expect(controller.buttonText()).toEqual("Pause");
|
||||
});
|
||||
|
||||
it("performs correct start/pause/stop action on click", function () {
|
||||
//test start
|
||||
invokeWatch('domainObject', mockDomainObject);
|
||||
expect(mockStart.perform).not.toHaveBeenCalled();
|
||||
controller.clickButton();
|
||||
expect(mockStart.perform).toHaveBeenCalled();
|
||||
|
||||
//test pause
|
||||
testModel.timestamp = 12321;
|
||||
testModel.timerState = 'started';
|
||||
invokeWatch('model.modified', 1);
|
||||
expect(mockPause.perform).not.toHaveBeenCalled();
|
||||
controller.clickButton();
|
||||
expect(mockPause.perform).toHaveBeenCalled();
|
||||
|
||||
//test stop
|
||||
expect(mockStop.perform).not.toHaveBeenCalled();
|
||||
controller.clickStopButton();
|
||||
expect(mockStop.perform).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("stops requesting animation frames when destroyed", function () {
|
||||
var initialCount = mockWindow.requestAnimationFrame.calls.count();
|
||||
|
||||
// First, check that normally new frames keep getting requested
|
||||
mockWindow.requestAnimationFrame.calls.mostRecent().args[0]();
|
||||
expect(mockWindow.requestAnimationFrame.calls.count())
|
||||
.toEqual(initialCount + 1);
|
||||
mockWindow.requestAnimationFrame.calls.mostRecent().args[0]();
|
||||
expect(mockWindow.requestAnimationFrame.calls.count())
|
||||
.toEqual(initialCount + 2);
|
||||
|
||||
// Now, verify that it stops after $destroy
|
||||
expect(mockScope.$on.calls.mostRecent().args[0])
|
||||
.toEqual('$destroy');
|
||||
mockScope.$on.calls.mostRecent().args[1]();
|
||||
|
||||
// Frames should no longer get requested
|
||||
mockWindow.requestAnimationFrame.calls.mostRecent().args[0]();
|
||||
expect(mockWindow.requestAnimationFrame.calls.count())
|
||||
.toEqual(initialCount + 2);
|
||||
mockWindow.requestAnimationFrame.calls.mostRecent().args[0]();
|
||||
expect(mockWindow.requestAnimationFrame.calls.count())
|
||||
.toEqual(initialCount + 2);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,111 +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/controllers/TimerFormatter"],
|
||||
function (TimerFormatter) {
|
||||
|
||||
var MS_IN_SEC = 1000,
|
||||
MS_IN_MIN = MS_IN_SEC * 60,
|
||||
MS_IN_HR = MS_IN_MIN * 60,
|
||||
MS_IN_DAY = MS_IN_HR * 24;
|
||||
|
||||
describe("The timer value formatter", function () {
|
||||
var formatter = new TimerFormatter();
|
||||
|
||||
function sum(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
function toDuration(days, hours, mins, secs) {
|
||||
return [
|
||||
days * MS_IN_DAY,
|
||||
hours * MS_IN_HR,
|
||||
mins * MS_IN_MIN,
|
||||
secs * MS_IN_SEC
|
||||
].reduce(sum, 0);
|
||||
}
|
||||
|
||||
it("formats short-form values (no days)", function () {
|
||||
expect(formatter.short(toDuration(0, 123, 2, 3) + 123))
|
||||
.toEqual("123:02:03");
|
||||
});
|
||||
|
||||
it("formats negative short-form values (no days)", function () {
|
||||
expect(formatter.short(-toDuration(0, 123, 2, 3) + 123))
|
||||
.toEqual("123:02:03");
|
||||
});
|
||||
|
||||
it("formats long-form values (with days)", function () {
|
||||
expect(formatter.long(toDuration(0, 123, 2, 3) + 123))
|
||||
.toEqual("5D 03:02:03");
|
||||
});
|
||||
|
||||
it("formats negative long-form values (no days)", function () {
|
||||
expect(formatter.long(-toDuration(0, 123, 2, 3) + 123))
|
||||
.toEqual("5D 03:02:03");
|
||||
});
|
||||
|
||||
it("rounds seconds down for positive durations", function () {
|
||||
expect(formatter.short(MS_IN_SEC + 600))
|
||||
.toEqual("00:00:01");
|
||||
});
|
||||
|
||||
it("rounds seconds up for negative durations", function () {
|
||||
expect(formatter.short(-MS_IN_SEC - 600))
|
||||
.toEqual("00:00:02");
|
||||
});
|
||||
|
||||
it("short-formats correctly around negative time borders", function () {
|
||||
expect(formatter.short(-1)).toEqual("00:00:01");
|
||||
expect(formatter.short(-1000)).toEqual("00:00:01");
|
||||
expect(formatter.short(-1001)).toEqual("00:00:02");
|
||||
expect(formatter.short(-2000)).toEqual("00:00:02");
|
||||
expect(formatter.short(-59001)).toEqual("00:01:00");
|
||||
expect(formatter.short(-60000)).toEqual("00:01:00");
|
||||
|
||||
expect(formatter.short(-MS_IN_HR + 999)).toEqual("01:00:00");
|
||||
expect(formatter.short(-MS_IN_HR)).toEqual("01:00:00");
|
||||
});
|
||||
|
||||
it("differentiates between values around zero", function () {
|
||||
// These are more than 1000 ms apart so should not appear
|
||||
// as the same second
|
||||
expect(formatter.short(-999))
|
||||
.not.toEqual(formatter.short(999));
|
||||
});
|
||||
|
||||
it("handles negative days", function () {
|
||||
expect(formatter.long(-10 * MS_IN_DAY))
|
||||
.toEqual("10D 00:00:00");
|
||||
expect(formatter.long(-10 * MS_IN_DAY + 100))
|
||||
.toEqual("10D 00:00:00");
|
||||
expect(formatter.long(-10 * MS_IN_DAY + 999))
|
||||
.toEqual("10D 00:00:00");
|
||||
|
||||
expect(formatter.short(-10 * MS_IN_DAY + 100))
|
||||
.toEqual("240:00:00");
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,62 +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/services/TickerService"],
|
||||
function (TickerService) {
|
||||
|
||||
var TEST_TIMESTAMP = 1433354174000;
|
||||
|
||||
describe("The ticker service", function () {
|
||||
var mockTimeout,
|
||||
mockNow,
|
||||
mockCallback,
|
||||
tickerService;
|
||||
|
||||
beforeEach(function () {
|
||||
mockTimeout = jasmine.createSpy('$timeout');
|
||||
mockNow = jasmine.createSpy('now');
|
||||
mockCallback = jasmine.createSpy('callback');
|
||||
|
||||
mockNow.and.returnValue(TEST_TIMESTAMP);
|
||||
|
||||
tickerService = new TickerService(mockTimeout, mockNow);
|
||||
});
|
||||
|
||||
it("notifies listeners of clock ticks", function () {
|
||||
tickerService.listen(mockCallback);
|
||||
mockNow.and.returnValue(TEST_TIMESTAMP + 12321);
|
||||
mockTimeout.calls.mostRecent().args[0]();
|
||||
expect(mockCallback)
|
||||
.toHaveBeenCalledWith(TEST_TIMESTAMP + 12321);
|
||||
});
|
||||
|
||||
it("allows listeners to unregister", function () {
|
||||
tickerService.listen(mockCallback)(); // Unregister immediately
|
||||
mockNow.and.returnValue(TEST_TIMESTAMP + 12321);
|
||||
mockTimeout.calls.mostRecent().args[0]();
|
||||
expect(mockCallback).not
|
||||
.toHaveBeenCalledWith(TEST_TIMESTAMP + 12321);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,77 +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/services/TimerService'
|
||||
], function (TimerService) {
|
||||
describe("TimerService", function () {
|
||||
var callback;
|
||||
var mockmct;
|
||||
var timerService;
|
||||
|
||||
beforeEach(function () {
|
||||
callback = jasmine.createSpy('callback');
|
||||
mockmct = {
|
||||
time: { clock: jasmine.createSpy('clock') },
|
||||
objects: { observe: jasmine.createSpy('observe') }
|
||||
};
|
||||
timerService = new TimerService(mockmct);
|
||||
timerService.on('change', callback);
|
||||
});
|
||||
|
||||
it("initially emits no change events", function () {
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports no current timer", function () {
|
||||
expect(timerService.getTimer()).toBeUndefined();
|
||||
});
|
||||
|
||||
describe("setTimer", function () {
|
||||
var testTimer;
|
||||
|
||||
beforeEach(function () {
|
||||
testTimer = { name: "I am some timer; you are nobody." };
|
||||
timerService.setTimer(testTimer);
|
||||
});
|
||||
|
||||
it("emits a change event", function () {
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports the current timer", function () {
|
||||
expect(timerService.getTimer()).toBe(testTimer);
|
||||
});
|
||||
|
||||
it("observes changes to an object", function () {
|
||||
var newTimer = { name: "I am another timer." };
|
||||
expect(mockmct.objects.observe).toHaveBeenCalledWith(
|
||||
testTimer,
|
||||
'*',
|
||||
jasmine.any(Function)
|
||||
);
|
||||
mockmct.objects.observe.calls.mostRecent().args[2](newTimer);
|
||||
expect(timerService.getTimer()).toBe(newTimer);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -25,7 +25,6 @@ define([
|
||||
'./capabilities/AdapterCapability',
|
||||
'./directives/MCTView',
|
||||
'./services/Instantiate',
|
||||
'./services/MissingModelCompatibilityDecorator',
|
||||
'./capabilities/APICapabilityDecorator',
|
||||
'./policies/AdaptedViewPolicy',
|
||||
'./runs/AlternateCompositionInitializer',
|
||||
@@ -41,7 +40,6 @@ define([
|
||||
AdapterCapability,
|
||||
MCTView,
|
||||
Instantiate,
|
||||
MissingModelCompatibilityDecorator,
|
||||
APICapabilityDecorator,
|
||||
AdaptedViewPolicy,
|
||||
AlternateCompositionInitializer,
|
||||
@@ -97,12 +95,6 @@ define([
|
||||
implementation: ActionDialogDecorator,
|
||||
depends: ["openmct"]
|
||||
},
|
||||
{
|
||||
type: "decorator",
|
||||
provides: "modelService",
|
||||
implementation: MissingModelCompatibilityDecorator,
|
||||
depends: ["openmct"]
|
||||
},
|
||||
{
|
||||
provides: "objectService",
|
||||
type: "decorator",
|
||||
|
||||
@@ -133,9 +133,13 @@ define([
|
||||
|
||||
return this.objectService.getObjects([keyString])
|
||||
.then(function (results) {
|
||||
let model = results[keyString].getModel();
|
||||
if (results[keyString]) {
|
||||
let model = results[keyString].getModel();
|
||||
|
||||
return utils.toNewFormat(model, key);
|
||||
return utils.toNewFormat(model, key);
|
||||
}
|
||||
|
||||
return;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'objectUtils'
|
||||
], function (
|
||||
objectUtils
|
||||
) {
|
||||
|
||||
/**
|
||||
* Compatibility decorator for New API.
|
||||
*
|
||||
* When the model service returns no results, this attempts to load
|
||||
* the model from the new Object API and returns that instead. In order
|
||||
* to prevent infinite recursion, this only tries to fetch from the API
|
||||
* a single time.
|
||||
*
|
||||
*/
|
||||
function MissingModelCompatibilityDecorator(api, modelService) {
|
||||
this.api = api;
|
||||
this.modelService = modelService;
|
||||
this.apiFetching = {}; // to prevent loops, if we have already
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a set of ids from the public api and return a promise for their
|
||||
* models. If a model is requested twice, respond with a missing result.
|
||||
*/
|
||||
MissingModelCompatibilityDecorator.prototype.apiFetch = function (ids) {
|
||||
const results = {};
|
||||
|
||||
const promises = ids.map(function (id) {
|
||||
if (this.apiFetching[id]) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.apiFetching[id] = true;
|
||||
|
||||
return this.api.objects.get(objectUtils.parseKeyString(id))
|
||||
.then(function (newDO) {
|
||||
results[id] = objectUtils.toOldFormat(newDO);
|
||||
});
|
||||
}, this);
|
||||
|
||||
return Promise.all(promises).then(function () {
|
||||
return results;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a promise for model results based on provided ids. Will attempt
|
||||
* to fetch any missing results from the object api.
|
||||
*/
|
||||
MissingModelCompatibilityDecorator.prototype.getModels = function (ids) {
|
||||
return this.modelService.getModels(ids)
|
||||
.then(function (models) {
|
||||
const missingIds = ids.filter(function (id) {
|
||||
return !models[id];
|
||||
});
|
||||
|
||||
if (!missingIds.length) {
|
||||
return models;
|
||||
}
|
||||
|
||||
//Temporary fix for missing models - don't retry using this.apiFetch
|
||||
return models;
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
return MissingModelCompatibilityDecorator;
|
||||
});
|
||||
|
||||
@@ -184,6 +184,15 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) {
|
||||
}
|
||||
|
||||
identifier = utils.parseKeyString(identifier);
|
||||
let dirtyObject;
|
||||
if (this.isTransactionActive()) {
|
||||
dirtyObject = this.transaction.getDirtyObject(keystring);
|
||||
}
|
||||
|
||||
if (dirtyObject) {
|
||||
return Promise.resolve(dirtyObject);
|
||||
}
|
||||
|
||||
const provider = this.getProvider(identifier);
|
||||
|
||||
if (!provider) {
|
||||
@@ -302,6 +311,13 @@ ObjectAPI.prototype.isPersistable = function (idOrKeyString) {
|
||||
&& provider.update !== undefined;
|
||||
};
|
||||
|
||||
ObjectAPI.prototype.isMissing = function (domainObject) {
|
||||
let identifier = utils.makeKeyString(domainObject.identifier);
|
||||
let missingName = 'Missing: ' + identifier;
|
||||
|
||||
return domainObject.name === missingName;
|
||||
};
|
||||
|
||||
/**
|
||||
* Save this domain object in its current state. EXPERIMENTAL
|
||||
*
|
||||
@@ -466,6 +482,23 @@ ObjectAPI.prototype.mutate = function (domainObject, path, value) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates a domain object based on its latest persisted state. Note that this will mutate the provided object.
|
||||
* @param {module:openmct.DomainObject} domainObject an object to refresh from its persistence store
|
||||
* @returns {Promise} the provided object, updated to reflect the latest persisted state of the object.
|
||||
*/
|
||||
ObjectAPI.prototype.refresh = async function (domainObject) {
|
||||
const refreshedObject = await this.get(domainObject.identifier);
|
||||
|
||||
if (domainObject.isMutable) {
|
||||
domainObject.$refresh(refreshedObject);
|
||||
} else {
|
||||
utils.refresh(domainObject, refreshedObject);
|
||||
}
|
||||
|
||||
return domainObject;
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
|
||||
@@ -55,6 +55,17 @@ export default class Transaction {
|
||||
});
|
||||
}
|
||||
|
||||
getDirtyObject(keystring) {
|
||||
let dirtyObject;
|
||||
this.dirtyObjects.forEach(object => {
|
||||
if (this.objectAPI.makeKeyString(object.identifier) === keystring) {
|
||||
dirtyObject = object;
|
||||
}
|
||||
});
|
||||
|
||||
return dirtyObject;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.dirtyObjects = new Set();
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ const DEFAULTS = [
|
||||
'platform/execution',
|
||||
'platform/exporters',
|
||||
'platform/telemetry',
|
||||
'platform/features/clock',
|
||||
'platform/forms',
|
||||
'platform/identity',
|
||||
'platform/persistence/aggregator',
|
||||
@@ -77,8 +76,6 @@ define([
|
||||
'../platform/entanglement/bundle',
|
||||
'../platform/execution/bundle',
|
||||
'../platform/exporters/bundle',
|
||||
'../platform/features/clock/bundle',
|
||||
'../platform/features/my-items/bundle',
|
||||
'../platform/features/static-markup/bundle',
|
||||
'../platform/forms/bundle',
|
||||
'../platform/framework/bundle',
|
||||
|
||||
@@ -48,6 +48,7 @@ const CONTEXT_MENU_ACTIONS = [
|
||||
'viewHistoricalData',
|
||||
'remove'
|
||||
];
|
||||
const BLANK_VALUE = '---';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'currentView'],
|
||||
@@ -67,15 +68,43 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
datum: undefined,
|
||||
timestamp: undefined,
|
||||
value: '---',
|
||||
valueClass: '',
|
||||
timestampKey: undefined,
|
||||
unit: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
value() {
|
||||
if (!this.datum) {
|
||||
return BLANK_VALUE;
|
||||
}
|
||||
|
||||
return this.formats[this.valueKey].format(this.datum);
|
||||
},
|
||||
valueClass() {
|
||||
if (!this.datum) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const limit = this.limitEvaluator.evaluate(this.datum, this.valueMetadata);
|
||||
|
||||
return limit ? limit.cssClass : '';
|
||||
|
||||
},
|
||||
formattedTimestamp() {
|
||||
return this.timestamp !== undefined ? this.getFormattedTimestamp(this.timestamp) : '---';
|
||||
if (!this.timestamp) {
|
||||
return BLANK_VALUE;
|
||||
}
|
||||
|
||||
return this.timeSystemFormat.format(this.timestamp);
|
||||
},
|
||||
timeSystemFormat() {
|
||||
if (!this.formats[this.timestampKey]) {
|
||||
console.warn(`No formatter for ${this.timestampKey} time system for ${this.domainObject.name}.`);
|
||||
}
|
||||
|
||||
return this.formats[this.timestampKey];
|
||||
},
|
||||
objectPath() {
|
||||
return [this.domainObject, ...this.pathToTable];
|
||||
@@ -96,15 +125,19 @@ export default {
|
||||
|
||||
this.timestampKey = this.openmct.time.timeSystem().key;
|
||||
|
||||
this.valueMetadata = this.metadata ? this
|
||||
.metadata
|
||||
.valuesForHints(['range'])[0] : undefined;
|
||||
this.valueMetadata = undefined;
|
||||
|
||||
if (this.metadata) {
|
||||
this.valueMetadata = this
|
||||
.metadata
|
||||
.valuesForHints(['range'])[0] || this.firstNonDomainAttribute(this.metadata);
|
||||
}
|
||||
|
||||
this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;
|
||||
|
||||
this.unsubscribe = this.openmct
|
||||
.telemetry
|
||||
.subscribe(this.domainObject, this.updateValues);
|
||||
.subscribe(this.domainObject, this.setLatestValues);
|
||||
|
||||
this.requestHistory();
|
||||
|
||||
@@ -118,29 +151,29 @@ export default {
|
||||
this.openmct.time.off('bounds', this.updateBounds);
|
||||
},
|
||||
methods: {
|
||||
updateValues(datum) {
|
||||
let newTimestamp = this.getParsedTimestamp(datum);
|
||||
let limit;
|
||||
updateView() {
|
||||
if (!this.updatingView) {
|
||||
this.updatingView = true;
|
||||
requestAnimationFrame(() => {
|
||||
let newTimestamp = this.getParsedTimestamp(this.latestDatum);
|
||||
|
||||
if (this.shouldUpdate(newTimestamp)) {
|
||||
this.datum = datum;
|
||||
this.timestamp = newTimestamp;
|
||||
this.value = this.formats[this.valueKey].format(datum);
|
||||
limit = this.limitEvaluator.evaluate(datum, this.valueMetadata);
|
||||
if (limit) {
|
||||
this.valueClass = limit.cssClass;
|
||||
} else {
|
||||
this.valueClass = '';
|
||||
}
|
||||
if (this.shouldUpdate(newTimestamp)) {
|
||||
this.timestamp = newTimestamp;
|
||||
this.datum = this.latestDatum;
|
||||
}
|
||||
|
||||
this.updatingView = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
shouldUpdate(newTimestamp) {
|
||||
let newTimestampInBounds = this.inBounds(newTimestamp);
|
||||
let noExistingTimestamp = this.timestamp === undefined;
|
||||
let newTimestampIsLatest = newTimestamp > this.timestamp;
|
||||
setLatestValues(datum) {
|
||||
this.latestDatum = datum;
|
||||
|
||||
return newTimestampInBounds
|
||||
&& (noExistingTimestamp || newTimestampIsLatest);
|
||||
this.updateView();
|
||||
},
|
||||
shouldUpdate(newTimestamp) {
|
||||
return this.inBounds(newTimestamp)
|
||||
&& (this.timestamp === undefined || newTimestamp > this.timestamp);
|
||||
},
|
||||
requestHistory() {
|
||||
this.openmct
|
||||
@@ -151,7 +184,7 @@ export default {
|
||||
size: 1,
|
||||
strategy: 'latest'
|
||||
})
|
||||
.then((array) => this.updateValues(array[array.length - 1]))
|
||||
.then((array) => this.setLatestValues(array[array.length - 1]))
|
||||
.catch((error) => {
|
||||
console.warn('Error fetching data', error);
|
||||
});
|
||||
@@ -189,31 +222,21 @@ export default {
|
||||
}
|
||||
},
|
||||
resetValues() {
|
||||
this.value = '---';
|
||||
this.timestamp = undefined;
|
||||
this.valueClass = '';
|
||||
this.datum = undefined;
|
||||
},
|
||||
getParsedTimestamp(timestamp) {
|
||||
if (this.timeSystemFormat()) {
|
||||
return this.formats[this.timestampKey].parse(timestamp);
|
||||
}
|
||||
},
|
||||
getFormattedTimestamp(timestamp) {
|
||||
if (this.timeSystemFormat()) {
|
||||
return this.formats[this.timestampKey].format(timestamp);
|
||||
}
|
||||
},
|
||||
timeSystemFormat() {
|
||||
if (this.formats[this.timestampKey]) {
|
||||
return true;
|
||||
} else {
|
||||
console.warn(`No formatter for ${this.timestampKey} time system for ${this.domainObject.name}.`);
|
||||
|
||||
return false;
|
||||
if (this.timeSystemFormat) {
|
||||
return this.timeSystemFormat.parse(timestamp);
|
||||
}
|
||||
},
|
||||
setUnit() {
|
||||
this.unit = this.valueMetadata.unit || '';
|
||||
},
|
||||
firstNonDomainAttribute(metadata) {
|
||||
return metadata
|
||||
.values()
|
||||
.find(metadatum => metadatum.hints.domain === undefined && metadatum.key !== 'name');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
getMockObjects,
|
||||
getMockTelemetry,
|
||||
getLatestTelemetry,
|
||||
spyOnBuiltins,
|
||||
resetApplicationState
|
||||
} from 'utils/testing';
|
||||
|
||||
@@ -160,6 +161,11 @@ describe("The LAD Table", () => {
|
||||
anotherTelemetryObjectResolve = resolve;
|
||||
});
|
||||
|
||||
spyOnBuiltins(['requestAnimationFrame']);
|
||||
window.requestAnimationFrame.and.callFake((callBack) => {
|
||||
callBack();
|
||||
});
|
||||
|
||||
openmct.telemetry.request.and.callFake(() => {
|
||||
telemetryRequestResolve(mockTelemetry);
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import momentTimezone from 'moment-timezone';
|
||||
import ticker from 'utils/clock/Ticker';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
@@ -82,8 +83,7 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const TickerService = this.openmct.$injector.get('tickerService');
|
||||
this.unlisten = TickerService.listen(this.tick);
|
||||
this.unlisten = ticker.listen(this.tick);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.unlisten) {
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import ticker from 'utils/clock/Ticker';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
@@ -45,10 +46,7 @@ export default {
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.openmct.on('start', () => {
|
||||
const TickerService = this.openmct.$injector.get('tickerService');
|
||||
this.unlisten = TickerService.listen(this.tick);
|
||||
});
|
||||
this.unlisten = ticker.listen(this.tick);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.unlisten) {
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
v-if="isEditing"
|
||||
:grid-size="gridSize"
|
||||
:show-grid="showGrid"
|
||||
:grid-dimensions="gridDimensions"
|
||||
/>
|
||||
<div
|
||||
v-if="shouldDisplayLayoutDimensions"
|
||||
@@ -159,7 +160,8 @@ export default {
|
||||
initSelectIndex: undefined,
|
||||
selection: [],
|
||||
showGrid: true,
|
||||
viewContext: {}
|
||||
viewContext: {},
|
||||
gridDimensions: [0, 0]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -204,6 +206,23 @@ export default {
|
||||
if (value) {
|
||||
this.showGrid = value;
|
||||
}
|
||||
},
|
||||
layoutItems: {
|
||||
handler(value) {
|
||||
let wMax = this.$el.clientWidth / this.gridSize[0];
|
||||
let hMax = this.$el.clientHeight / this.gridSize[1];
|
||||
value.forEach(item => {
|
||||
if (item.x + item.width > wMax) {
|
||||
wMax = item.x + item.width + 2;
|
||||
}
|
||||
|
||||
if (item.y + item.height > hMax) {
|
||||
hMax = item.y + item.height + 2;
|
||||
}
|
||||
});
|
||||
this.gridDimensions = [wMax * this.gridSize[0], hMax * this.gridSize[1]];
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -213,6 +232,7 @@ export default {
|
||||
this.composition.on('add', this.addChild);
|
||||
this.composition.on('remove', this.removeChild);
|
||||
this.composition.load();
|
||||
this.gridDimensions = [this.$el.offsetWidth, this.$el.scrollHeight];
|
||||
},
|
||||
destroyed: function () {
|
||||
this.openmct.selection.off('change', this.setSelection);
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
<div
|
||||
v-if="gridSize[0] >= 3"
|
||||
class="c-grid__x l-grid l-grid-x"
|
||||
:style="[{ backgroundSize: gridSize[0] + 'px 100%' }]"
|
||||
:style="[{ backgroundSize: gridSize[0] + 'px 100%', width: gridDimensions[0] +'px', height: gridDimensions[1] +'px' }]"
|
||||
></div>
|
||||
<div
|
||||
v-if="gridSize[1] >= 3"
|
||||
class="c-grid__y l-grid l-grid-y"
|
||||
:style="[{ backgroundSize: '100%' + gridSize[1] + 'px' }]"
|
||||
:style="[{ backgroundSize: '100%' + gridSize[1] + 'px', width: gridDimensions[0] +'px', height: gridDimensions[1] +'px' }]"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -28,6 +28,12 @@ export default {
|
||||
showGrid: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
gridDimensions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
validator: (arr) => arr && arr.length === 2
|
||||
&& arr.every(el => typeof el === 'number')
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -263,7 +263,8 @@ export default {
|
||||
this.openmct.telemetry.request(this.domainObject, options)
|
||||
.then(data => {
|
||||
if (data.length > 0) {
|
||||
this.updateView(data[data.length - 1]);
|
||||
this.latestDatum = data[data.length - 1];
|
||||
this.updateView();
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -275,12 +276,19 @@ export default {
|
||||
|| (datumTimeStamp
|
||||
&& (this.openmct.time.bounds().end >= datumTimeStamp))
|
||||
) {
|
||||
this.updateView(datum);
|
||||
this.latestDatum = datum;
|
||||
this.updateView();
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
updateView(datum) {
|
||||
this.datum = datum;
|
||||
updateView() {
|
||||
if (!this.updatingView) {
|
||||
this.updatingView = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.datum = this.latestDatum;
|
||||
this.updatingView = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
removeSubscription() {
|
||||
if (this.subscription) {
|
||||
@@ -290,7 +298,8 @@ export default {
|
||||
},
|
||||
refreshData(bounds, isTick) {
|
||||
if (!isTick) {
|
||||
this.datum = undefined;
|
||||
this.latestDatum = undefined;
|
||||
this.updateView();
|
||||
this.requestHistoricalData(this.domainObject);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -392,7 +392,7 @@ export default {
|
||||
} else {
|
||||
// container is taller than image
|
||||
sizedImageDimensions.width = this.imageContainerWidth;
|
||||
sizedImageDimensions.height = this.imageContainerWidth * this.focusedImageNaturalAspectRatio;
|
||||
sizedImageDimensions.height = this.imageContainerWidth / this.focusedImageNaturalAspectRatio;
|
||||
}
|
||||
|
||||
return sizedImageDimensions;
|
||||
@@ -414,9 +414,11 @@ export default {
|
||||
if (this.indexForFocusedImage !== undefined) {
|
||||
imageIndex = this.initFocusedImageIndex;
|
||||
} else {
|
||||
imageIndex = newSize - 1;
|
||||
imageIndex = newSize > 0 ? newSize - 1 : undefined;
|
||||
}
|
||||
|
||||
// handle subscription update
|
||||
this.previousFocusedImage = this.focusedImage ? JSON.parse(JSON.stringify(this.focusedImage)) : undefined;
|
||||
this.setFocusedImage(imageIndex, false);
|
||||
this.scrollToRight();
|
||||
},
|
||||
@@ -670,24 +672,49 @@ export default {
|
||||
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
|
||||
});
|
||||
},
|
||||
matchIndexOfPreviousImage(previous, imageHistory) {
|
||||
// match logic uses a composite of url and time to account
|
||||
// for example imagery not having fully unique urls
|
||||
return imageHistory.findIndex((x) => (
|
||||
x.url === previous.url
|
||||
&& x.time === previous.time
|
||||
));
|
||||
},
|
||||
setFocusedImage(index, thumbnailClick = false) {
|
||||
let focusedIndex = index;
|
||||
if (!(Number.isInteger(index) && index > -1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.previousFocusedImage) {
|
||||
console.log('previousImageUrl', this.previousFocusedImage.url)
|
||||
// determine if the previous image exists in the new bounds of imageHistory
|
||||
const matchIndex = this.matchIndexOfPreviousImage(
|
||||
this.previousFocusedImage,
|
||||
this.imageHistory
|
||||
);
|
||||
focusedIndex = matchIndex > -1 ? matchIndex : this.imageHistory.length - 1;
|
||||
console.log('new focused image', focusedIndex, this.imageHistory[focusedIndex].url)
|
||||
}
|
||||
|
||||
if (thumbnailClick) {
|
||||
//We use the props till the user changes what they want to see
|
||||
this.initFocusedImageIndex = undefined;
|
||||
}
|
||||
|
||||
this.focusedImageIndex = focusedIndex;
|
||||
delete this.previousFocusedImage;
|
||||
|
||||
if (this.isPaused && !thumbnailClick && this.initFocusedImageIndex === undefined) {
|
||||
this.nextImageIndex = index;
|
||||
this.nextImageIndex = focusedIndex;
|
||||
//this could happen if bounds changes
|
||||
if (this.focusedImageIndex > this.imageHistory.length - 1) {
|
||||
this.focusedImageIndex = index;
|
||||
this.focusedImageIndex = focusedIndex;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.focusedImageIndex = index;
|
||||
|
||||
if (thumbnailClick && !this.isPaused) {
|
||||
this.paused(true);
|
||||
}
|
||||
|
||||
@@ -122,14 +122,46 @@ export default {
|
||||
return this.timeFormatter.parse(datum);
|
||||
},
|
||||
boundsChange(bounds, isTick) {
|
||||
if (!isTick) {
|
||||
this.requestHistory();
|
||||
if (isTick) {
|
||||
return;
|
||||
}
|
||||
|
||||
// the focusedImageIndex is calculated twice; once before and after request history arrives
|
||||
const focusedImage = this.imageHistory[this.focusedImageIndex];
|
||||
const previousFocusedImage = focusedImage ? JSON.parse(JSON.stringify(focusedImage)) : undefined;
|
||||
this.updateFocusedImageIndex(previousFocusedImage, this.imageHistory);
|
||||
|
||||
return this.requestHistory();
|
||||
},
|
||||
matchIndexOfPreviousImage(previous, imageHistory) {
|
||||
// match logic uses a composite of url and time to account
|
||||
// for example imagery not having fully unique urls
|
||||
return imageHistory.findIndex((x) => (
|
||||
x.url === previous.url
|
||||
&& x.time === previous.time
|
||||
&& x.utc === previous.utc
|
||||
));
|
||||
},
|
||||
updateFocusedImageIndex(previous, imageHistory) {
|
||||
if (!previous) {
|
||||
return;
|
||||
}
|
||||
|
||||
const matchIndex = this.matchIndexOfPreviousImage(
|
||||
previous,
|
||||
imageHistory
|
||||
);
|
||||
this.focusedImageIndex = matchIndex > -1 ? matchIndex : this.imageHistory.length - 1;
|
||||
|
||||
},
|
||||
async requestHistory() {
|
||||
let bounds = this.timeContext.bounds();
|
||||
this.requestCount++;
|
||||
const requestId = this.requestCount;
|
||||
|
||||
// maintain previous focused image value to discern new index
|
||||
const focusedImage = this.imageHistory[this.focusedImageIndex];
|
||||
const previousFocusedImage = focusedImage ? JSON.parse(JSON.stringify(focusedImage)) : undefined;
|
||||
this.imageHistory = [];
|
||||
|
||||
let data = await this.openmct.telemetry
|
||||
@@ -143,7 +175,8 @@ export default {
|
||||
imagery.push(image);
|
||||
}
|
||||
});
|
||||
//this is to optimize anything that reacts to imageHistory length
|
||||
this.updateFocusedImageIndex(previousFocusedImage, imagery);
|
||||
|
||||
this.imageHistory = imagery;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
export default function MissingObjectInterceptor(openmct) {
|
||||
openmct.objects.addGetInterceptor({
|
||||
appliesTo: (identifier, domainObject) => {
|
||||
return identifier.key !== 'mine';
|
||||
return true;
|
||||
},
|
||||
invoke: (identifier, object) => {
|
||||
if (object === undefined) {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import missingObjectInterceptor from "./missingObjectInterceptor";
|
||||
import myItemsInterceptor from "./myItemsInterceptor";
|
||||
|
||||
export default function plugin() {
|
||||
return function install(openmct) {
|
||||
myItemsInterceptor(openmct);
|
||||
missingObjectInterceptor(openmct);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,22 +75,5 @@ describe('the plugin', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the My items object if not found', () => {
|
||||
const identifier = {
|
||||
namespace: TEST_NAMESPACE,
|
||||
key: 'mine'
|
||||
};
|
||||
|
||||
return openmct.objects.get(identifier).then((testObject) => {
|
||||
expect(testObject).toEqual({
|
||||
identifier,
|
||||
"name": "My Items",
|
||||
"type": "folder",
|
||||
"composition": [],
|
||||
"location": "ROOT"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
8
src/plugins/myItems/createMyItemsIdentifier.js
Normal file
8
src/plugins/myItems/createMyItemsIdentifier.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export const MY_ITEMS_KEY = 'mine';
|
||||
|
||||
export function createMyItemsIdentifier(namespace = '') {
|
||||
return {
|
||||
key: MY_ITEMS_KEY,
|
||||
namespace
|
||||
};
|
||||
}
|
||||
@@ -20,24 +20,30 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
export default function MyItemsInterceptor(openmct) {
|
||||
import { MY_ITEMS_KEY } from "./createMyItemsIdentifier";
|
||||
|
||||
openmct.objects.addGetInterceptor({
|
||||
appliesTo: (identifier, domainObject) => {
|
||||
return identifier.key === 'mine';
|
||||
function myItemsInterceptor(identifierObject, openmct) {
|
||||
|
||||
const myItemsModel = {
|
||||
identifier: identifierObject,
|
||||
"name": "My Items",
|
||||
"type": "folder",
|
||||
"composition": [],
|
||||
"location": "ROOT"
|
||||
};
|
||||
|
||||
return {
|
||||
appliesTo: (identifier) => {
|
||||
return identifier.key === MY_ITEMS_KEY;
|
||||
},
|
||||
invoke: (identifier, object) => {
|
||||
if (object === undefined) {
|
||||
return {
|
||||
identifier,
|
||||
"name": "My Items",
|
||||
"type": "folder",
|
||||
"composition": [],
|
||||
"location": "ROOT"
|
||||
};
|
||||
if (openmct.objects.isMissing(object)) {
|
||||
return myItemsModel;
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default myItemsInterceptor;
|
||||
@@ -20,30 +20,14 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([], function () {
|
||||
return {
|
||||
name: "platform/features/my-items",
|
||||
definition: {
|
||||
"name": "My Items",
|
||||
"description": "Defines a root named My Items",
|
||||
"extensions": {
|
||||
"roots": [
|
||||
{
|
||||
"id": "mine"
|
||||
}
|
||||
],
|
||||
"models": [
|
||||
{
|
||||
"id": "mine",
|
||||
"model": {
|
||||
"name": "My Items",
|
||||
"type": "folder",
|
||||
"composition": [],
|
||||
"location": "ROOT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
import { createMyItemsIdentifier } from "./createMyItemsIdentifier";
|
||||
import myItemsInterceptor from "./myItemsInterceptor";
|
||||
|
||||
export default function MyItemsPlugin(namespace = '') {
|
||||
return function install(openmct) {
|
||||
const identifier = createMyItemsIdentifier(namespace);
|
||||
|
||||
openmct.objects.addGetInterceptor(myItemsInterceptor(identifier, openmct));
|
||||
openmct.objects.addRoot(identifier);
|
||||
};
|
||||
});
|
||||
}
|
||||
90
src/plugins/myItems/pluginSpec.js
Normal file
90
src/plugins/myItems/pluginSpec.js
Normal file
@@ -0,0 +1,90 @@
|
||||
/*****************************************************************************
|
||||
* 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 {
|
||||
createMyItemsIdentifier,
|
||||
MY_ITEMS_KEY
|
||||
} from './createMyItemsIdentifier';
|
||||
|
||||
const MISSING_NAME = `Missing: ${MY_ITEMS_KEY}`;
|
||||
const myItemsIdentifier = createMyItemsIdentifier();
|
||||
|
||||
describe("the plugin", () => {
|
||||
let openmct;
|
||||
let missingObj = {
|
||||
identifier: myItemsIdentifier,
|
||||
type: 'unknown',
|
||||
name: MISSING_NAME
|
||||
};
|
||||
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
|
||||
openmct.install(openmct.plugins.MyItems());
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('when installed, adds "My Items" to the root', async () => {
|
||||
const root = await openmct.objects.get('ROOT');
|
||||
const rootCompostionCollection = openmct.composition.get(root);
|
||||
const rootCompostion = await rootCompostionCollection.load();
|
||||
let myItems = rootCompostion.filter((domainObject) => {
|
||||
return openmct.objects.areIdsEqual(domainObject.identifier, myItemsIdentifier);
|
||||
})[0];
|
||||
|
||||
expect(myItems).toBeDefined();
|
||||
});
|
||||
|
||||
describe('adds an interceptor that returns a "My Items" model for', () => {
|
||||
let myItemsMissing;
|
||||
let mockMissingProvider;
|
||||
let activeProvider;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockMissingProvider = {
|
||||
get: () => Promise.resolve(missingObj)
|
||||
};
|
||||
|
||||
activeProvider = mockMissingProvider;
|
||||
spyOn(openmct.objects, 'getProvider').and.returnValue(activeProvider);
|
||||
myItemsMissing = await openmct.objects.get(myItemsIdentifier);
|
||||
});
|
||||
|
||||
it('missing objects', () => {
|
||||
let idsMatchMissing = openmct.objects.areIdsEqual(myItemsMissing.identifier, myItemsIdentifier);
|
||||
|
||||
expect(myItemsMissing).toBeDefined();
|
||||
expect(idsMatchMissing).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vu
|
||||
import SnapshotContainer from './snapshot-container';
|
||||
import monkeyPatchObjectAPIForNotebooks from './monkeyPatchObjectAPIForNotebooks.js';
|
||||
|
||||
import { notebookImageMigration } from '../notebook/utils/notebook-migration';
|
||||
import { notebookImageMigration, IMAGE_MIGRATION_VER } from '../notebook/utils/notebook-migration';
|
||||
import { NOTEBOOK_TYPE } from './notebook-constants';
|
||||
|
||||
import Vue from 'vue';
|
||||
@@ -28,6 +28,7 @@ export default function NotebookPlugin() {
|
||||
domainObject.configuration = {
|
||||
defaultSort: 'oldest',
|
||||
entries: {},
|
||||
imageMigrationVer: IMAGE_MIGRATION_VER,
|
||||
pageTitle: 'Page',
|
||||
sections: [],
|
||||
sectionTitle: 'Section',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createNotebookImageDomainObject, getThumbnailURLFromimageUrl, saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from './notebook-image';
|
||||
import { mutateObject } from './notebook-entries';
|
||||
|
||||
const IMAGE_MIGRATION_VER = "v1";
|
||||
export const IMAGE_MIGRATION_VER = "v1";
|
||||
|
||||
export function notebookImageMigration(openmct, domainObject) {
|
||||
const configuration = domainObject.configuration;
|
||||
|
||||
@@ -370,11 +370,13 @@ class CouchObjectProvider {
|
||||
}
|
||||
|
||||
return () => {
|
||||
this.observers[keyString] = this.observers[keyString].filter(observer => observer !== callback);
|
||||
if (this.observers[keyString].length === 0) {
|
||||
delete this.observers[keyString];
|
||||
if (Object.keys(this.observers).length === 0 && this.isObservingObjectChanges()) {
|
||||
this.stopObservingObjectChanges();
|
||||
if (this.observers[keyString]) {
|
||||
this.observers[keyString] = this.observers[keyString].filter(observer => observer !== callback);
|
||||
if (this.observers[keyString].length === 0) {
|
||||
delete this.observers[keyString];
|
||||
if (Object.keys(this.observers).length === 0 && this.isObservingObjectChanges()) {
|
||||
this.stopObservingObjectChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -41,13 +41,12 @@ describe('the plugin', () => {
|
||||
namespace: '',
|
||||
key: 'some-value'
|
||||
},
|
||||
type: 'mock-type',
|
||||
type: 'notebook',
|
||||
modified: 0
|
||||
};
|
||||
options = {
|
||||
url: testPath,
|
||||
filter: {},
|
||||
disableObserve: true
|
||||
filter: {}
|
||||
};
|
||||
openmct = createOpenMct();
|
||||
|
||||
@@ -66,7 +65,7 @@ describe('the plugin', () => {
|
||||
|
||||
openmct.install(new CouchPlugin(options));
|
||||
|
||||
openmct.types.addType('mock-type', {creatable: true});
|
||||
openmct.types.addType('notebook', {creatable: true});
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
@@ -98,33 +97,26 @@ describe('the plugin', () => {
|
||||
fetch.and.returnValue(mockPromise);
|
||||
});
|
||||
|
||||
it('gets an object', () => {
|
||||
return openmct.objects.get(mockDomainObject.identifier).then((result) => {
|
||||
expect(result.identifier.key).toEqual(mockDomainObject.identifier.key);
|
||||
});
|
||||
it('gets an object', async () => {
|
||||
const result = await openmct.objects.get(mockDomainObject.identifier);
|
||||
expect(result.identifier.key).toEqual(mockDomainObject.identifier.key);
|
||||
});
|
||||
|
||||
it('creates an object', () => {
|
||||
return openmct.objects.save(mockDomainObject).then((result) => {
|
||||
expect(provider.create).toHaveBeenCalled();
|
||||
expect(result).toBeTrue();
|
||||
});
|
||||
it('creates an object', async () => {
|
||||
const result = await openmct.objects.save(mockDomainObject);
|
||||
expect(provider.create).toHaveBeenCalled();
|
||||
expect(result).toBeTrue();
|
||||
});
|
||||
|
||||
it('updates an object', (done) => {
|
||||
return openmct.objects.save(mockDomainObject).then((result) => {
|
||||
expect(result).toBeTrue();
|
||||
expect(provider.create).toHaveBeenCalled();
|
||||
|
||||
//Set modified timestamp it detects a change and persists the updated model.
|
||||
mockDomainObject.modified = Date.now();
|
||||
|
||||
return openmct.objects.save(mockDomainObject).then((updatedResult) => {
|
||||
expect(updatedResult).toBeTrue();
|
||||
expect(provider.update).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
xit('updates an object', async () => {
|
||||
const result = await openmct.objects.save(mockDomainObject);
|
||||
expect(result).toBeTrue();
|
||||
expect(provider.create).toHaveBeenCalled();
|
||||
//Set modified timestamp it detects a change and persists the updated model.
|
||||
mockDomainObject.modified = Date.now();
|
||||
const updatedResult = await openmct.objects.save(mockDomainObject);
|
||||
expect(updatedResult).toBeTrue();
|
||||
expect(provider.update).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('batches requests', () => {
|
||||
@@ -140,7 +132,7 @@ describe('the plugin', () => {
|
||||
});
|
||||
fetch.and.returnValue(mockPromise);
|
||||
});
|
||||
it('for multiple simultaneous gets', () => {
|
||||
it('for multiple simultaneous gets', async () => {
|
||||
const objectIds = [
|
||||
{
|
||||
namespace: '',
|
||||
@@ -154,37 +146,32 @@ describe('the plugin', () => {
|
||||
}
|
||||
];
|
||||
|
||||
const getAllObjects = Promise.all(
|
||||
await Promise.all(
|
||||
objectIds.map((identifier) =>
|
||||
openmct.objects.get(identifier)
|
||||
));
|
||||
)
|
||||
);
|
||||
|
||||
return getAllObjects.then(() => {
|
||||
const requestUrl = fetch.calls.mostRecent().args[0];
|
||||
const requestMethod = fetch.calls.mostRecent().args[1].method;
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(requestUrl.includes('_all_docs')).toBeTrue();
|
||||
expect(requestMethod).toEqual('POST');
|
||||
});
|
||||
const requestUrl = fetch.calls.mostRecent().args[0];
|
||||
const requestMethod = fetch.calls.mostRecent().args[1].method;
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(requestUrl.includes('_all_docs')).toBeTrue();
|
||||
expect(requestMethod).toEqual('POST');
|
||||
});
|
||||
|
||||
it('but not for single gets', () => {
|
||||
it('but not for single gets', async () => {
|
||||
const objectId = {
|
||||
namespace: '',
|
||||
key: 'object-1'
|
||||
};
|
||||
|
||||
const getObject = openmct.objects.get(objectId);
|
||||
await openmct.objects.get(objectId);
|
||||
const requestUrl = fetch.calls.mostRecent().args[0];
|
||||
const requestMethod = fetch.calls.mostRecent().args[1].method;
|
||||
|
||||
return getObject.then(() => {
|
||||
const requestUrl = fetch.calls.mostRecent().args[0];
|
||||
const requestMethod = fetch.calls.mostRecent().args[1].method;
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(requestUrl.endsWith(`${objectId.key}`)).toBeTrue();
|
||||
expect(requestMethod).toEqual('GET');
|
||||
});
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(requestUrl.endsWith(`${objectId.key}`)).toBeTrue();
|
||||
expect(requestMethod).toEqual('GET');
|
||||
});
|
||||
});
|
||||
describe('implements server-side search', () => {
|
||||
@@ -207,22 +194,20 @@ describe('the plugin', () => {
|
||||
fetch.and.returnValue(mockPromise);
|
||||
});
|
||||
|
||||
it("using Couch's 'find' endpoint", () => {
|
||||
return Promise.all(openmct.objects.search('test')).then(() => {
|
||||
const requestUrl = fetch.calls.mostRecent().args[0];
|
||||
it("using Couch's 'find' endpoint", async () => {
|
||||
await Promise.all(openmct.objects.search('test'));
|
||||
const requestUrl = fetch.calls.mostRecent().args[0];
|
||||
|
||||
expect(fetch).toHaveBeenCalled();
|
||||
expect(requestUrl.endsWith('_find')).toBeTrue();
|
||||
});
|
||||
expect(fetch).toHaveBeenCalled();
|
||||
expect(requestUrl.endsWith('_find')).toBeTrue();
|
||||
});
|
||||
|
||||
it("and supports search by object name", () => {
|
||||
return Promise.all(openmct.objects.search('test')).then(() => {
|
||||
const requestPayload = JSON.parse(fetch.calls.mostRecent().args[1].body);
|
||||
it("and supports search by object name", async () => {
|
||||
await Promise.all(openmct.objects.search('test'));
|
||||
const requestPayload = JSON.parse(fetch.calls.mostRecent().args[1].body);
|
||||
|
||||
expect(requestPayload).toBeDefined();
|
||||
expect(requestPayload.selector.model.name.$regex).toEqual('(?i)test');
|
||||
});
|
||||
expect(requestPayload).toBeDefined();
|
||||
expect(requestPayload.selector.model.name.$regex).toEqual('(?i)test');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ define([
|
||||
'./remoteClock/plugin',
|
||||
'./localTimeSystem/plugin',
|
||||
'./ISOTimeFormat/plugin',
|
||||
'./myItems/plugin',
|
||||
'../../example/generator/plugin',
|
||||
'./autoflow/AutoflowTabularPlugin',
|
||||
'./timeConductor/plugin',
|
||||
@@ -71,6 +72,7 @@ define([
|
||||
'./timeline/plugin',
|
||||
'./hyperlink/plugin',
|
||||
'./clock/plugin',
|
||||
'./timer/plugin',
|
||||
'./DeviceClassifier/plugin'
|
||||
], function (
|
||||
_,
|
||||
@@ -78,6 +80,7 @@ define([
|
||||
RemoteClock,
|
||||
LocalTimeSystem,
|
||||
ISOTimeFormat,
|
||||
MyItems,
|
||||
GeneratorPlugin,
|
||||
AutoflowPlugin,
|
||||
TimeConductorPlugin,
|
||||
@@ -123,11 +126,11 @@ define([
|
||||
Timeline,
|
||||
Hyperlink,
|
||||
Clock,
|
||||
Timer,
|
||||
DeviceClassifier
|
||||
) {
|
||||
const bundleMap = {
|
||||
LocalStorage: 'platform/persistence/local',
|
||||
MyItems: 'platform/features/my-items',
|
||||
Elasticsearch: 'platform/persistence/elastic'
|
||||
};
|
||||
|
||||
@@ -143,6 +146,8 @@ define([
|
||||
plugins.LocalTimeSystem = LocalTimeSystem;
|
||||
plugins.RemoteClock = RemoteClock.default;
|
||||
|
||||
plugins.MyItems = MyItems.default;
|
||||
|
||||
plugins.ImportExport = ImportExport;
|
||||
|
||||
plugins.StaticRootPlugin = StaticRootPlugin;
|
||||
@@ -188,7 +193,7 @@ define([
|
||||
return GeneratorPlugin;
|
||||
};
|
||||
|
||||
plugins.ExampleImagery = ExampleImagery;
|
||||
plugins.ExampleImagery = ExampleImagery.default;
|
||||
plugins.ImageryPlugin = ImageryPlugin;
|
||||
plugins.Plot = PlotPlugin.default;
|
||||
plugins.Chart = ChartPlugin.default;
|
||||
@@ -229,6 +234,7 @@ define([
|
||||
plugins.Timeline = Timeline.default;
|
||||
plugins.Hyperlink = Hyperlink.default;
|
||||
plugins.Clock = Clock.default;
|
||||
plugins.Timer = Timer.default;
|
||||
plugins.DeviceClassifier = DeviceClassifier.default;
|
||||
|
||||
return plugins;
|
||||
|
||||
@@ -31,8 +31,6 @@
|
||||
flex-direction: column;
|
||||
|
||||
&--hidden {
|
||||
height: 1000px;
|
||||
width: 1000px;
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
top: -9999px;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<template>
|
||||
<div class="c-tabs-view">
|
||||
<div
|
||||
ref="tabs"
|
||||
class="c-tabs-view"
|
||||
>
|
||||
<div
|
||||
ref="tabsHolder"
|
||||
class="c-tabs-view__tabs-holder c-tabs"
|
||||
:class="{
|
||||
'is-dragging': isDragging && allowEditing,
|
||||
@@ -28,8 +32,10 @@
|
||||
}"
|
||||
@click="showTab(tab, index)"
|
||||
>
|
||||
<div class="c-tabs-view__tab__label c-object-label"
|
||||
:class="[tab.status ? `is-status--${tab.status}` : '']"
|
||||
<div
|
||||
ref="tabsLabel"
|
||||
class="c-tabs-view__tab__label c-object-label"
|
||||
:class="[tab.status ? `is-status--${tab.status}` : '']"
|
||||
>
|
||||
<div class="c-object-label__type-icon"
|
||||
:class="tab.type.definition.cssClass"
|
||||
@@ -49,11 +55,12 @@
|
||||
<div
|
||||
v-for="tab in tabsList"
|
||||
:key="tab.keyString"
|
||||
:style="getTabStyles(tab)"
|
||||
class="c-tabs-view__object-holder"
|
||||
:class="{'c-tabs-view__object-holder--hidden': !isCurrent(tab)}"
|
||||
>
|
||||
<object-view
|
||||
v-if="isTabLoaded(tab)"
|
||||
v-if="shouldLoadTab(tab)"
|
||||
class="c-tabs-view__object"
|
||||
:default-object="tab.domainObject"
|
||||
:object-path="tab.objectPath"
|
||||
@@ -65,6 +72,7 @@
|
||||
<script>
|
||||
import ObjectView from '../../../ui/components/ObjectView.vue';
|
||||
import RemoveAction from '../../remove/RemoveAction.js';
|
||||
import _ from 'lodash';
|
||||
|
||||
const unknownObjectType = {
|
||||
definition: {
|
||||
@@ -88,6 +96,8 @@ export default {
|
||||
let keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
|
||||
return {
|
||||
tabWidth: undefined,
|
||||
tabHeight: undefined,
|
||||
internalDomainObject: this.domainObject,
|
||||
currentTab: {},
|
||||
currentTabIndex: undefined,
|
||||
@@ -122,6 +132,10 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
this.handleWindowResize = _.debounce(this.handleWindowResize, 500);
|
||||
this.tabsViewResizeObserver = new ResizeObserver(this.handleWindowResize);
|
||||
this.tabsViewResizeObserver.observe(this.$refs.tabs);
|
||||
|
||||
this.unsubscribe = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
|
||||
|
||||
this.openmct.router.on('change:params', this.updateCurrentTab.bind(this));
|
||||
@@ -138,6 +152,8 @@ export default {
|
||||
this.composition.off('remove', this.removeItem);
|
||||
this.composition.off('reorder', this.onReorder);
|
||||
|
||||
this.tabsViewResizeObserver.disconnect();
|
||||
|
||||
this.tabsList.forEach(tab => {
|
||||
tab.statusUnsubscribe();
|
||||
});
|
||||
@@ -158,12 +174,28 @@ export default {
|
||||
|
||||
this.loadedTabs[tab.keyString] = true;
|
||||
},
|
||||
getTabStyles(tab) {
|
||||
let styles = {};
|
||||
|
||||
if (!this.isCurrent(tab)) {
|
||||
styles = {
|
||||
height: this.tabHeight,
|
||||
width: this.tabWidth
|
||||
};
|
||||
}
|
||||
|
||||
return styles;
|
||||
},
|
||||
setCurrentTabByIndex(index) {
|
||||
if (this.tabsList[index]) {
|
||||
this.showTab(this.tabsList[index]);
|
||||
}
|
||||
},
|
||||
showTab(tab, index) {
|
||||
if (!tab) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (index !== undefined) {
|
||||
this.storeCurrentTabIndexInURL(index);
|
||||
}
|
||||
@@ -171,6 +203,13 @@ export default {
|
||||
this.currentTab = tab;
|
||||
this.addTabToLoaded(tab);
|
||||
},
|
||||
shouldLoadTab(tab) {
|
||||
const isLoaded = this.isTabLoaded(tab);
|
||||
const isCurrent = this.isCurrent(tab);
|
||||
const tabElLoaded = this.tabWidth !== undefined && this.tabHeight !== undefined;
|
||||
|
||||
return (isLoaded && isCurrent) || ((isLoaded && !isCurrent) && tabElLoaded);
|
||||
},
|
||||
showRemoveDialog(index) {
|
||||
if (!this.tabsList[index]) {
|
||||
return;
|
||||
@@ -325,6 +364,14 @@ export default {
|
||||
|
||||
this.currentTabIndex = tabIndex;
|
||||
this.currentTab = this.tabsList[tabIndex];
|
||||
},
|
||||
handleWindowResize() {
|
||||
if (!this.$refs.tabs || !this.$refs.tabsHolder) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tabWidth = this.$refs.tabs.offsetWidth + 'px';
|
||||
this.tabHeight = this.$refs.tabsHolder.offsetHeight - this.$refs.tabs.offsetHeight + 'px';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -20,7 +20,11 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { createOpenMct, resetApplicationState } from 'utils/testing';
|
||||
import {
|
||||
createOpenMct,
|
||||
resetApplicationState,
|
||||
spyOnBuiltins
|
||||
} from 'utils/testing';
|
||||
import TabsLayout from './plugin';
|
||||
import Vue from "vue";
|
||||
import EventEmitter from "EventEmitter";
|
||||
@@ -63,13 +67,13 @@ describe('the plugin', function () {
|
||||
'phase': 5,
|
||||
'randomness': 0
|
||||
},
|
||||
'name': 'Sine Wave Generator',
|
||||
'type': 'generator',
|
||||
'modified': 1592851063871,
|
||||
'location': 'mine',
|
||||
'persisted': 1592851063871
|
||||
};
|
||||
let telemetryItem1 = Object.assign({}, telemetryItemTemplate, {
|
||||
'name': 'Sine Wave Generator 1',
|
||||
'id': '55122607-e65e-44d5-9c9d-9c31a914ca89',
|
||||
'identifier': {
|
||||
'namespace': '',
|
||||
@@ -77,6 +81,7 @@ describe('the plugin', function () {
|
||||
}
|
||||
});
|
||||
let telemetryItem2 = Object.assign({}, telemetryItemTemplate, {
|
||||
'name': 'Sine Wave Generator 2',
|
||||
'id': '55122607-e65e-44d5-9c9d-9c31a914ca90',
|
||||
'identifier': {
|
||||
'namespace': '',
|
||||
@@ -91,6 +96,9 @@ describe('the plugin', function () {
|
||||
|
||||
element = document.createElement('div');
|
||||
child = document.createElement('div');
|
||||
child.style.display = 'block';
|
||||
child.style.width = '1920px';
|
||||
child.style.height = '1080px';
|
||||
element.appendChild(child);
|
||||
|
||||
openmct.on('start', done);
|
||||
@@ -150,8 +158,17 @@ describe('the plugin', function () {
|
||||
let tabsLayoutViewProvider;
|
||||
let mockComposition;
|
||||
let count = 0;
|
||||
let resizeCallback;
|
||||
|
||||
beforeEach(() => {
|
||||
class mockResizeObserver {
|
||||
constructor(cb) {
|
||||
resizeCallback = cb;
|
||||
}
|
||||
observe() { }
|
||||
disconnect() { }
|
||||
}
|
||||
|
||||
mockComposition = new EventEmitter();
|
||||
mockComposition.load = () => {
|
||||
if (count === 0) {
|
||||
@@ -165,6 +182,9 @@ describe('the plugin', function () {
|
||||
|
||||
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
|
||||
|
||||
spyOnBuiltins(['ResizeObserver']);
|
||||
window.ResizeObserver.and.callFake(mockResizeObserver);
|
||||
|
||||
const applicableViews = openmct.objectViews.get(testViewObject, []);
|
||||
tabsLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'tabs');
|
||||
let view = tabsLayoutViewProvider.view(testViewObject, []);
|
||||
@@ -175,6 +195,7 @@ describe('the plugin', function () {
|
||||
|
||||
afterEach(() => {
|
||||
count = 0;
|
||||
testViewObject.keep_alive = true;
|
||||
});
|
||||
|
||||
it ('renders a tab for each item', () => {
|
||||
@@ -185,10 +206,22 @@ describe('the plugin', function () {
|
||||
|
||||
describe('with domainObject.keep_alive set to', () => {
|
||||
|
||||
it ('true, will keep all views loaded, regardless of current tab view', () => {
|
||||
let tabViewEls = element.querySelectorAll('.c-tabs-view__object');
|
||||
it ('true, will keep all views loaded, regardless of current tab view', (done) => {
|
||||
resizeCallback();
|
||||
|
||||
expect(tabViewEls.length).toEqual(2);
|
||||
// the function called by the resize observer is debounced 500ms,
|
||||
// this is to account for that
|
||||
let promise = new Promise((resolve, reject) => {
|
||||
setTimeout(resolve, 501);
|
||||
});
|
||||
|
||||
Promise.all([Vue.nextTick(), promise]).then(() => {
|
||||
let tabViewEls = element.querySelectorAll('.c-tabs-view__object');
|
||||
|
||||
expect(tabViewEls.length).toEqual(2);
|
||||
}).finally(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it ('false, will only keep the current tab view loaded', async () => {
|
||||
|
||||
65
src/plugins/timer/TimerViewProvider.js
Normal file
65
src/plugins/timer/TimerViewProvider.js
Normal file
@@ -0,0 +1,65 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2009-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 Timer from './components/Timer.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default function TimerViewProvider(openmct) {
|
||||
return {
|
||||
key: 'timer.view',
|
||||
name: 'Timer',
|
||||
cssClass: 'icon-timer',
|
||||
canView(domainObject) {
|
||||
return domainObject.type === 'timer';
|
||||
},
|
||||
|
||||
view: function (domainObject, objectPath) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
Timer
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
objectPath,
|
||||
currentView: this
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
domainObject
|
||||
};
|
||||
},
|
||||
template: '<timer :domain-object="domainObject" />'
|
||||
});
|
||||
},
|
||||
destroy: function () {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* Open MCT, Copyright (c) 2009-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
@@ -20,43 +20,37 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
export default class PauseTimerAction {
|
||||
constructor(openmct) {
|
||||
this.name = 'Pause';
|
||||
this.key = 'timer.pause';
|
||||
this.description = 'Pause the currently displayed timer';
|
||||
this.group = 'view';
|
||||
this.cssClass = 'icon-pause';
|
||||
this.priority = 3;
|
||||
|
||||
/**
|
||||
* Adds placeholder domain object models for any models which
|
||||
* fail to load from the underlying model service.
|
||||
* @constructor
|
||||
* @memberof platform/core
|
||||
* @param {ModelService} modelService this service to decorate
|
||||
* @implements {ModelService}
|
||||
*/
|
||||
function MissingModelDecorator(modelService) {
|
||||
this.modelService = modelService;
|
||||
}
|
||||
|
||||
function missingModel(id) {
|
||||
return {
|
||||
type: "unknown",
|
||||
name: "Missing: " + id
|
||||
};
|
||||
}
|
||||
|
||||
MissingModelDecorator.prototype.getModels = function (ids) {
|
||||
function addMissingModels(models) {
|
||||
var result = {};
|
||||
ids.forEach(function (id) {
|
||||
result[id] = models[id] || missingModel(id);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return this.modelService.getModels(ids).then(addMissingModels);
|
||||
};
|
||||
|
||||
return MissingModelDecorator;
|
||||
this.openmct = openmct;
|
||||
}
|
||||
);
|
||||
invoke(objectPath) {
|
||||
const domainObject = objectPath[0];
|
||||
if (!domainObject || !domainObject.configuration) {
|
||||
return new Error('Unable to run pause timer action. No domainObject provided.');
|
||||
}
|
||||
|
||||
const newConfiguration = { ...domainObject.configuration };
|
||||
newConfiguration.timerState = 'paused';
|
||||
newConfiguration.pausedTime = new Date();
|
||||
|
||||
this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration);
|
||||
}
|
||||
appliesTo(objectPath) {
|
||||
const domainObject = objectPath[0];
|
||||
if (!domainObject || !domainObject.configuration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { timerState } = domainObject.configuration;
|
||||
|
||||
return domainObject.type === 'timer' && timerState === 'started';
|
||||
}
|
||||
}
|
||||
57
src/plugins/timer/actions/RestartTimerAction.js
Normal file
57
src/plugins/timer/actions/RestartTimerAction.js
Normal file
@@ -0,0 +1,57 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2009-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.
|
||||
*****************************************************************************/
|
||||
|
||||
export default class RestartTimerAction {
|
||||
constructor(openmct) {
|
||||
this.name = 'Restart at 0';
|
||||
this.key = 'timer.restart';
|
||||
this.description = 'Restart the currently displayed timer';
|
||||
this.group = 'view';
|
||||
this.cssClass = 'icon-refresh';
|
||||
this.priority = 2;
|
||||
|
||||
this.openmct = openmct;
|
||||
}
|
||||
invoke(objectPath) {
|
||||
const domainObject = objectPath[0];
|
||||
if (!domainObject || !domainObject.configuration) {
|
||||
return new Error('Unable to run restart timer action. No domainObject provided.');
|
||||
}
|
||||
|
||||
const newConfiguration = { ...domainObject.configuration };
|
||||
newConfiguration.timerState = 'started';
|
||||
newConfiguration.timestamp = new Date();
|
||||
newConfiguration.pausedTime = undefined;
|
||||
|
||||
this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration);
|
||||
}
|
||||
appliesTo(objectPath) {
|
||||
const domainObject = objectPath[0];
|
||||
if (!domainObject || !domainObject.configuration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { timerState } = domainObject.configuration;
|
||||
|
||||
return domainObject.type === 'timer' && timerState !== 'stopped';
|
||||
}
|
||||
}
|
||||
76
src/plugins/timer/actions/StartTimerAction.js
Normal file
76
src/plugins/timer/actions/StartTimerAction.js
Normal file
@@ -0,0 +1,76 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2009-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 moment from 'moment';
|
||||
|
||||
export default class StartTimerAction {
|
||||
constructor(openmct) {
|
||||
this.name = 'Start';
|
||||
this.key = 'timer.start';
|
||||
this.description = 'Start the currently displayed timer';
|
||||
this.group = 'view';
|
||||
this.cssClass = 'icon-play';
|
||||
this.priority = 3;
|
||||
|
||||
this.openmct = openmct;
|
||||
}
|
||||
invoke(objectPath) {
|
||||
const domainObject = objectPath[0];
|
||||
if (!domainObject || !domainObject.configuration) {
|
||||
return new Error('Unable to run start timer action. No domainObject provided.');
|
||||
}
|
||||
|
||||
let { pausedTime, timestamp } = domainObject.configuration;
|
||||
const newConfiguration = { ...domainObject.configuration };
|
||||
|
||||
if (pausedTime) {
|
||||
pausedTime = moment(pausedTime);
|
||||
}
|
||||
|
||||
if (timestamp) {
|
||||
timestamp = moment(timestamp);
|
||||
}
|
||||
|
||||
const now = moment(new Date());
|
||||
if (pausedTime) {
|
||||
const timeShift = moment.duration(now.diff(pausedTime));
|
||||
const shiftedTime = timestamp.add(timeShift);
|
||||
newConfiguration.timestamp = shiftedTime.toDate();
|
||||
} else if (!timestamp) {
|
||||
newConfiguration.timestamp = now.toDate();
|
||||
}
|
||||
|
||||
newConfiguration.timerState = 'started';
|
||||
newConfiguration.pausedTime = undefined;
|
||||
this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration);
|
||||
}
|
||||
appliesTo(objectPath) {
|
||||
const domainObject = objectPath[0];
|
||||
if (!domainObject || !domainObject.configuration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { timerState } = domainObject.configuration;
|
||||
|
||||
return domainObject.type === 'timer' && timerState !== 'started';
|
||||
}
|
||||
}
|
||||
57
src/plugins/timer/actions/StopTimerAction.js
Normal file
57
src/plugins/timer/actions/StopTimerAction.js
Normal file
@@ -0,0 +1,57 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2009-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.
|
||||
*****************************************************************************/
|
||||
|
||||
export default class StopTimerAction {
|
||||
constructor(openmct) {
|
||||
this.name = 'Stop';
|
||||
this.key = 'timer.stop';
|
||||
this.description = 'Stop the currently displayed timer';
|
||||
this.group = 'view';
|
||||
this.cssClass = 'icon-box-round-corners';
|
||||
this.priority = 1;
|
||||
|
||||
this.openmct = openmct;
|
||||
}
|
||||
invoke(objectPath) {
|
||||
const domainObject = objectPath[0];
|
||||
if (!domainObject || !domainObject.configuration) {
|
||||
return new Error('Unable to run stop timer action. No domainObject provided.');
|
||||
}
|
||||
|
||||
const newConfiguration = { ...domainObject.configuration };
|
||||
newConfiguration.timerState = 'stopped';
|
||||
newConfiguration.timestamp = undefined;
|
||||
newConfiguration.pausedTime = undefined;
|
||||
|
||||
this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration);
|
||||
}
|
||||
appliesTo(objectPath) {
|
||||
const domainObject = objectPath[0];
|
||||
if (!domainObject || !domainObject.configuration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { timerState } = domainObject.configuration;
|
||||
|
||||
return domainObject.type === 'timer' && timerState !== 'stopped';
|
||||
}
|
||||
}
|
||||
233
src/plugins/timer/components/Timer.vue
Normal file
233
src/plugins/timer/components/Timer.vue
Normal file
@@ -0,0 +1,233 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2009-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-timer u-style-receiver js-style-receiver"
|
||||
:class="[`is-${timerState}`]"
|
||||
>
|
||||
<div class="c-timer__controls">
|
||||
<button
|
||||
title="Reset"
|
||||
class="c-timer__ctrl-reset c-icon-button c-icon-button--major icon-reset"
|
||||
:class="[{'hide': timerState === 'stopped' }]"
|
||||
@click="restartTimer"
|
||||
></button>
|
||||
<button :title="timerStateButtonText"
|
||||
class="c-timer__ctrl-pause-play c-icon-button c-icon-button--major"
|
||||
:class="[timerStateButtonIcon]"
|
||||
@click="toggleStateButton"
|
||||
></button>
|
||||
</div>
|
||||
<div
|
||||
class="c-timer__direction"
|
||||
:class="[{'hide': !timerSign }, `icon-${timerSign}`]"
|
||||
></div>
|
||||
<div class="c-timer__value">{{ timeTextValue || "--:--:--" }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ticker from 'utils/clock/Ticker';
|
||||
|
||||
const moment = require("moment-timezone");
|
||||
const momentDurationFormatSetup = require("moment-duration-format");
|
||||
|
||||
momentDurationFormatSetup(moment);
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'currentView', 'objectPath'],
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
lastTimestamp: undefined,
|
||||
active: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
configuration() {
|
||||
let configuration;
|
||||
if (this.domainObject && this.domainObject.configuration) {
|
||||
configuration = this.domainObject.configuration;
|
||||
}
|
||||
|
||||
return configuration;
|
||||
},
|
||||
relativeTimestamp() {
|
||||
let relativeTimestamp;
|
||||
if (this.configuration && this.configuration.timestamp) {
|
||||
relativeTimestamp = moment(this.configuration.timestamp).toDate();
|
||||
} else if (this.configuration && this.configuration.timestamp === undefined) {
|
||||
relativeTimestamp = undefined;
|
||||
}
|
||||
|
||||
return relativeTimestamp;
|
||||
},
|
||||
timeDelta() {
|
||||
return this.lastTimestamp - this.relativeTimestamp;
|
||||
},
|
||||
timeTextValue() {
|
||||
if (isNaN(this.timeDelta)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const toWholeSeconds = Math.abs(Math.floor(this.timeDelta / 1000) * 1000);
|
||||
|
||||
return moment.duration(toWholeSeconds, 'ms').format(this.format, { trim: false });
|
||||
},
|
||||
pausedTime() {
|
||||
let pausedTime;
|
||||
if (this.configuration && this.configuration.pausedTime) {
|
||||
pausedTime = moment(this.configuration.pausedTime).toDate();
|
||||
} else if (this.configuration && this.configuration.pausedTime === undefined) {
|
||||
pausedTime = undefined;
|
||||
}
|
||||
|
||||
return pausedTime;
|
||||
},
|
||||
timerState() {
|
||||
let timerState = 'started';
|
||||
if (this.configuration && this.configuration.timerState) {
|
||||
timerState = this.configuration.timerState;
|
||||
}
|
||||
|
||||
return timerState;
|
||||
},
|
||||
timerStateButtonText() {
|
||||
let buttonText = 'Pause';
|
||||
if (['paused', 'stopped'].includes(this.timerState)) {
|
||||
buttonText = 'Start';
|
||||
}
|
||||
|
||||
return buttonText;
|
||||
},
|
||||
timerStateButtonIcon() {
|
||||
let buttonIcon = 'icon-pause';
|
||||
if (['paused', 'stopped'].includes(this.timerState)) {
|
||||
buttonIcon = 'icon-play';
|
||||
}
|
||||
|
||||
return buttonIcon;
|
||||
},
|
||||
timerFormat() {
|
||||
let timerFormat = 'long';
|
||||
if (this.configuration && this.configuration.timerFormat) {
|
||||
timerFormat = this.configuration.timerFormat;
|
||||
}
|
||||
|
||||
return timerFormat;
|
||||
},
|
||||
format() {
|
||||
let format;
|
||||
if (this.timerFormat === 'long') {
|
||||
format = 'd[D] HH:mm:ss';
|
||||
}
|
||||
|
||||
if (this.timerFormat === 'short') {
|
||||
format = 'HH:mm:ss';
|
||||
}
|
||||
|
||||
return format;
|
||||
},
|
||||
timerType() {
|
||||
let timerType = null;
|
||||
if (isNaN(this.timeDelta)) {
|
||||
return timerType;
|
||||
}
|
||||
|
||||
if (this.timeDelta < 0) {
|
||||
timerType = 'countDown';
|
||||
} else if (this.timeDelta >= 1000) {
|
||||
timerType = 'countUp';
|
||||
}
|
||||
|
||||
return timerType;
|
||||
},
|
||||
timerSign() {
|
||||
let timerSign = null;
|
||||
if (this.timerType === 'countUp') {
|
||||
timerSign = 'plus';
|
||||
} else if (this.timerType === 'countDown') {
|
||||
timerSign = 'minus';
|
||||
}
|
||||
|
||||
return timerSign;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
if (this.configuration && this.configuration.timerState === undefined) {
|
||||
const timerAction = !this.relativeTimestamp ? 'stop' : 'start';
|
||||
this.triggerAction(`timer.${timerAction}`);
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(this.tick);
|
||||
this.unlisten = ticker.listen(() => {
|
||||
this.openmct.objects.refresh(this.domainObject);
|
||||
});
|
||||
});
|
||||
},
|
||||
destroyed() {
|
||||
this.active = false;
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
tick() {
|
||||
const isTimerRunning = !['paused', 'stopped'].includes(this.timerState);
|
||||
if (isTimerRunning) {
|
||||
this.lastTimestamp = new Date();
|
||||
}
|
||||
|
||||
if (this.timerState === 'paused' && !this.lastTimestamp) {
|
||||
this.lastTimestamp = this.pausedTime;
|
||||
}
|
||||
|
||||
if (this.active) {
|
||||
window.requestAnimationFrame(this.tick);
|
||||
}
|
||||
},
|
||||
restartTimer() {
|
||||
this.triggerAction('timer.restart');
|
||||
},
|
||||
toggleStateButton() {
|
||||
if (this.timerState === 'started') {
|
||||
this.triggerAction('timer.pause');
|
||||
} else if (['paused', 'stopped'].includes(this.timerState)) {
|
||||
this.triggerAction('timer.start');
|
||||
}
|
||||
},
|
||||
triggerAction(actionKey) {
|
||||
const action = this.openmct.actions.getAction(actionKey);
|
||||
if (action) {
|
||||
action.invoke(this.objectPath, this.currentView);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
119
src/plugins/timer/plugin.js
Normal file
119
src/plugins/timer/plugin.js
Normal file
@@ -0,0 +1,119 @@
|
||||
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2009-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 TimerViewProvider from './TimerViewProvider';
|
||||
|
||||
import PauseTimerAction from './actions/PauseTimerAction';
|
||||
import RestartTimerAction from './actions/RestartTimerAction';
|
||||
import StartTimerAction from './actions/StartTimerAction';
|
||||
import StopTimerAction from './actions/StopTimerAction';
|
||||
|
||||
export default function TimerPlugin() {
|
||||
return function install(openmct) {
|
||||
openmct.types.addType('timer', {
|
||||
name: 'Timer',
|
||||
description: 'A timer that counts up or down to a datetime. Timers can be started, stopped and reset whenever needed, and support a variety of display formats. Each Timer displays the same value to all users. Timers can be added to Display Layouts.',
|
||||
creatable: true,
|
||||
cssClass: 'icon-timer',
|
||||
initialize: function (domainObject) {
|
||||
domainObject.configuration = {
|
||||
timerFormat: 'long',
|
||||
timestamp: undefined,
|
||||
timezone: 'UTC',
|
||||
timerState: undefined,
|
||||
pausedTime: undefined
|
||||
};
|
||||
},
|
||||
"form": [
|
||||
{
|
||||
"key": "timestamp",
|
||||
"control": "datetime",
|
||||
"name": "Target",
|
||||
property: [
|
||||
'configuration',
|
||||
'timestamp'
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "timerFormat",
|
||||
"name": "Display Format",
|
||||
"control": "select",
|
||||
"options": [
|
||||
{
|
||||
"value": "long",
|
||||
"name": "DDD hh:mm:ss"
|
||||
},
|
||||
{
|
||||
"value": "short",
|
||||
"name": "hh:mm:ss"
|
||||
}
|
||||
],
|
||||
property: [
|
||||
'configuration',
|
||||
'timerFormat'
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
openmct.objectViews.addProvider(new TimerViewProvider(openmct));
|
||||
|
||||
openmct.actions.register(new PauseTimerAction(openmct));
|
||||
openmct.actions.register(new RestartTimerAction(openmct));
|
||||
openmct.actions.register(new StartTimerAction(openmct));
|
||||
openmct.actions.register(new StopTimerAction(openmct));
|
||||
|
||||
openmct.objects.addGetInterceptor({
|
||||
appliesTo: (identifier, domainObject) => {
|
||||
return domainObject && domainObject.type === 'timer';
|
||||
},
|
||||
invoke: (identifier, domainObject) => {
|
||||
if (domainObject.configuration) {
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
const configuration = {};
|
||||
|
||||
if (domainObject.timerFormat) {
|
||||
configuration.timerFormat = domainObject.timerFormat;
|
||||
}
|
||||
|
||||
if (domainObject.timestamp) {
|
||||
configuration.timestamp = domainObject.timestamp;
|
||||
}
|
||||
|
||||
if (domainObject.timerState) {
|
||||
configuration.timerState = domainObject.timerState;
|
||||
}
|
||||
|
||||
if (domainObject.pausedTime) {
|
||||
configuration.pausedTime = domainObject.pausedTime;
|
||||
}
|
||||
|
||||
openmct.objects.mutate(domainObject, 'configuration', configuration);
|
||||
|
||||
return domainObject;
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
}
|
||||
354
src/plugins/timer/pluginSpec.js
Normal file
354
src/plugins/timer/pluginSpec.js
Normal file
@@ -0,0 +1,354 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2009-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, spyOnBuiltins, resetApplicationState } from 'utils/testing';
|
||||
import timerPlugin from './plugin';
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
describe("Timer plugin:", () => {
|
||||
let openmct;
|
||||
let timerDefinition;
|
||||
let element;
|
||||
let child;
|
||||
let appHolder;
|
||||
|
||||
let timerDomainObject;
|
||||
|
||||
function setupTimer() {
|
||||
return new Promise((resolve, reject) => {
|
||||
timerDomainObject = {
|
||||
identifier: {
|
||||
key: 'timer',
|
||||
namespace: 'test-namespace'
|
||||
},
|
||||
type: 'timer'
|
||||
};
|
||||
|
||||
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(timerPlugin());
|
||||
|
||||
timerDefinition = openmct.types.get('timer').definition;
|
||||
timerDefinition.initialize(timerDomainObject);
|
||||
|
||||
openmct.on('start', resolve);
|
||||
openmct.start(appHolder);
|
||||
});
|
||||
}
|
||||
|
||||
describe("should still work if it's in the old format", () => {
|
||||
let timerViewProvider;
|
||||
let timerView;
|
||||
let timerViewObject;
|
||||
let mutableTimerObject;
|
||||
let timerObjectPath;
|
||||
const relativeTimestamp = 1634774400000; // Oct 21 2021, 12:00 AM
|
||||
|
||||
beforeEach(async () => {
|
||||
await setupTimer();
|
||||
|
||||
timerViewObject = {
|
||||
identifier: {
|
||||
key: 'timer',
|
||||
namespace: 'test-namespace'
|
||||
},
|
||||
type: 'timer',
|
||||
id: "test-object",
|
||||
name: 'Timer',
|
||||
timerFormat: 'short',
|
||||
timestamp: relativeTimestamp,
|
||||
timerState: 'paused',
|
||||
pausedTime: relativeTimestamp
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(timerViewObject, [timerViewObject]);
|
||||
timerViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'timer.view');
|
||||
|
||||
mutableTimerObject = await openmct.objects.getMutable(timerViewObject.identifier);
|
||||
|
||||
timerObjectPath = [mutableTimerObject];
|
||||
timerView = timerViewProvider.view(mutableTimerObject, timerObjectPath);
|
||||
timerView.show(child);
|
||||
|
||||
await Vue.nextTick();
|
||||
});
|
||||
|
||||
it("should migrate old object properties to the configuration section", () => {
|
||||
openmct.objects.applyGetInterceptors(timerViewObject.identifier, timerViewObject);
|
||||
expect(timerViewObject.configuration.timerFormat).toBe('short');
|
||||
expect(timerViewObject.configuration.timestamp).toBe(relativeTimestamp);
|
||||
expect(timerViewObject.configuration.timerState).toBe('paused');
|
||||
expect(timerViewObject.configuration.pausedTime).toBe(relativeTimestamp);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Timer view:", () => {
|
||||
let timerViewProvider;
|
||||
let timerView;
|
||||
let timerViewObject;
|
||||
let mutableTimerObject;
|
||||
let timerObjectPath;
|
||||
|
||||
beforeEach(async () => {
|
||||
await setupTimer();
|
||||
|
||||
spyOnBuiltins(['requestAnimationFrame']);
|
||||
window.requestAnimationFrame.and.callFake((cb) => setTimeout(cb, 500));
|
||||
const baseTimestamp = 1634688000000; // Oct 20, 2021, 12:00 AM
|
||||
const relativeTimestamp = 1634774400000; // Oct 21 2021, 12:00 AM
|
||||
|
||||
jasmine.clock().install();
|
||||
const baseTime = new Date(baseTimestamp);
|
||||
jasmine.clock().mockDate(baseTime);
|
||||
|
||||
timerViewObject = {
|
||||
...timerDomainObject,
|
||||
id: "test-object",
|
||||
name: 'Timer',
|
||||
configuration: {
|
||||
timerFormat: 'long',
|
||||
timestamp: relativeTimestamp,
|
||||
timezone: 'UTC',
|
||||
timerState: undefined,
|
||||
pausedTime: undefined
|
||||
}
|
||||
};
|
||||
|
||||
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(timerViewObject));
|
||||
spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true));
|
||||
|
||||
const applicableViews = openmct.objectViews.get(timerViewObject, [timerViewObject]);
|
||||
timerViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'timer.view');
|
||||
|
||||
mutableTimerObject = await openmct.objects.getMutable(timerViewObject.identifier);
|
||||
|
||||
timerObjectPath = [mutableTimerObject];
|
||||
timerView = timerViewProvider.view(mutableTimerObject, timerObjectPath);
|
||||
timerView.show(child);
|
||||
|
||||
await Vue.nextTick();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jasmine.clock().uninstall();
|
||||
timerView.destroy();
|
||||
openmct.objects.destroyMutable(mutableTimerObject);
|
||||
if (appHolder) {
|
||||
appHolder.remove();
|
||||
}
|
||||
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it("has name as Timer", () => {
|
||||
expect(timerDefinition.name).toEqual('Timer');
|
||||
});
|
||||
|
||||
it("is creatable", () => {
|
||||
expect(timerDefinition.creatable).toEqual(true);
|
||||
});
|
||||
|
||||
it("provides timer view", () => {
|
||||
expect(timerViewProvider).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders timer element", () => {
|
||||
const timerElement = element.querySelectorAll('.c-timer');
|
||||
expect(timerElement.length).toBe(1);
|
||||
});
|
||||
|
||||
it("renders major elements", () => {
|
||||
const timerElement = element.querySelector('.c-timer');
|
||||
const resetButton = timerElement.querySelector('.c-timer__ctrl-reset');
|
||||
const pausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play');
|
||||
const timerDirectionIcon = timerElement.querySelector('.c-timer__direction');
|
||||
const timerValue = timerElement.querySelector('.c-timer__value');
|
||||
const hasMajorElements = Boolean(resetButton && pausePlayButton && timerDirectionIcon && timerValue);
|
||||
|
||||
expect(hasMajorElements).toBe(true);
|
||||
});
|
||||
|
||||
it("gets errors from actions if configuration is not passed", async () => {
|
||||
await Vue.nextTick();
|
||||
const objectPath = _.cloneDeep(timerObjectPath);
|
||||
delete objectPath[0].configuration;
|
||||
|
||||
let action = openmct.actions.getAction('timer.start');
|
||||
let actionResults = action.invoke(objectPath);
|
||||
let actionFilterWithoutConfig = action.appliesTo(objectPath);
|
||||
await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'started' });
|
||||
let actionFilterWithConfig = action.appliesTo(timerObjectPath);
|
||||
|
||||
let actionError = new Error('Unable to run start timer action. No domainObject provided.');
|
||||
expect(actionResults).toEqual(actionError);
|
||||
expect(actionFilterWithoutConfig).toBe(undefined);
|
||||
expect(actionFilterWithConfig).toBe(false);
|
||||
|
||||
action = openmct.actions.getAction('timer.stop');
|
||||
actionResults = action.invoke(objectPath);
|
||||
actionFilterWithoutConfig = action.appliesTo(objectPath);
|
||||
await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'stopped' });
|
||||
actionFilterWithConfig = action.appliesTo(timerObjectPath);
|
||||
|
||||
actionError = new Error('Unable to run stop timer action. No domainObject provided.');
|
||||
expect(actionResults).toEqual(actionError);
|
||||
expect(actionFilterWithoutConfig).toBe(undefined);
|
||||
expect(actionFilterWithConfig).toBe(false);
|
||||
|
||||
action = openmct.actions.getAction('timer.pause');
|
||||
actionResults = action.invoke(objectPath);
|
||||
actionFilterWithoutConfig = action.appliesTo(objectPath);
|
||||
await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'paused' });
|
||||
actionFilterWithConfig = action.appliesTo(timerObjectPath);
|
||||
|
||||
actionError = new Error('Unable to run pause timer action. No domainObject provided.');
|
||||
expect(actionResults).toEqual(actionError);
|
||||
expect(actionFilterWithoutConfig).toBe(undefined);
|
||||
expect(actionFilterWithConfig).toBe(false);
|
||||
|
||||
action = openmct.actions.getAction('timer.restart');
|
||||
actionResults = action.invoke(objectPath);
|
||||
actionFilterWithoutConfig = action.appliesTo(objectPath);
|
||||
await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'stopped' });
|
||||
actionFilterWithConfig = action.appliesTo(timerObjectPath);
|
||||
|
||||
actionError = new Error('Unable to run restart timer action. No domainObject provided.');
|
||||
expect(actionResults).toEqual(actionError);
|
||||
expect(actionFilterWithoutConfig).toBe(undefined);
|
||||
expect(actionFilterWithConfig).toBe(false);
|
||||
});
|
||||
|
||||
it("displays a started timer ticking down to a future date", async () => {
|
||||
const newBaseTime = 1634774400000; // Oct 21 2021, 12:00 AM
|
||||
openmct.objects.mutate(timerViewObject, 'configuration.timestamp', newBaseTime);
|
||||
|
||||
jasmine.clock().tick(5000);
|
||||
await Vue.nextTick();
|
||||
|
||||
const timerElement = element.querySelector('.c-timer');
|
||||
const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play');
|
||||
const timerDirectionIcon = timerElement.querySelector('.c-timer__direction');
|
||||
const timerValue = timerElement.querySelector('.c-timer__value').innerText;
|
||||
|
||||
expect(timerPausePlayButton.classList.contains('icon-pause')).toBe(true);
|
||||
expect(timerDirectionIcon.classList.contains('icon-minus')).toBe(true);
|
||||
expect(timerValue).toBe('0D 23:59:55');
|
||||
});
|
||||
|
||||
it("displays a started timer ticking up from a past date", async () => {
|
||||
const newBaseTime = 1634601600000; // Oct 19, 2021, 12:00 AM
|
||||
openmct.objects.mutate(timerViewObject, 'configuration.timestamp', newBaseTime);
|
||||
|
||||
jasmine.clock().tick(5000);
|
||||
await Vue.nextTick();
|
||||
|
||||
const timerElement = element.querySelector('.c-timer');
|
||||
const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play');
|
||||
const timerDirectionIcon = timerElement.querySelector('.c-timer__direction');
|
||||
const timerValue = timerElement.querySelector('.c-timer__value').innerText;
|
||||
|
||||
expect(timerPausePlayButton.classList.contains('icon-pause')).toBe(true);
|
||||
expect(timerDirectionIcon.classList.contains('icon-plus')).toBe(true);
|
||||
expect(timerValue).toBe('1D 00:00:05');
|
||||
});
|
||||
|
||||
it("displays a paused timer correctly in the DOM", async () => {
|
||||
jasmine.clock().tick(5000);
|
||||
await Vue.nextTick();
|
||||
|
||||
let action = openmct.actions.getAction('timer.pause');
|
||||
if (action) {
|
||||
action.invoke(timerObjectPath, timerView);
|
||||
}
|
||||
|
||||
await Vue.nextTick();
|
||||
const timerElement = element.querySelector('.c-timer');
|
||||
const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play');
|
||||
let timerValue = timerElement.querySelector('.c-timer__value').innerText;
|
||||
|
||||
expect(timerPausePlayButton.classList.contains('icon-play')).toBe(true);
|
||||
expect(timerValue).toBe('0D 23:59:55');
|
||||
|
||||
jasmine.clock().tick(5000);
|
||||
await Vue.nextTick();
|
||||
expect(timerValue).toBe('0D 23:59:55');
|
||||
|
||||
action = openmct.actions.getAction('timer.start');
|
||||
if (action) {
|
||||
action.invoke(timerObjectPath, timerView);
|
||||
}
|
||||
|
||||
await Vue.nextTick();
|
||||
action = openmct.actions.getAction('timer.pause');
|
||||
if (action) {
|
||||
action.invoke(timerObjectPath, timerView);
|
||||
}
|
||||
|
||||
await Vue.nextTick();
|
||||
timerValue = timerElement.querySelector('.c-timer__value').innerText;
|
||||
expect(timerValue).toBe('1D 00:00:00');
|
||||
});
|
||||
|
||||
it("displays a stopped timer correctly in the DOM", async () => {
|
||||
const action = openmct.actions.getAction('timer.stop');
|
||||
if (action) {
|
||||
action.invoke(timerObjectPath, timerView);
|
||||
}
|
||||
|
||||
await Vue.nextTick();
|
||||
const timerElement = element.querySelector('.c-timer');
|
||||
const timerValue = timerElement.querySelector('.c-timer__value').innerText;
|
||||
const timerResetButton = timerElement.querySelector('.c-timer__ctrl-reset');
|
||||
const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play');
|
||||
|
||||
expect(timerResetButton.classList.contains('hide')).toBe(true);
|
||||
expect(timerPausePlayButton.classList.contains('icon-play')).toBe(true);
|
||||
expect(timerValue).toBe('--:--:--');
|
||||
});
|
||||
|
||||
it("displays a restarted timer correctly in the DOM", async () => {
|
||||
const action = openmct.actions.getAction('timer.restart');
|
||||
if (action) {
|
||||
action.invoke(timerObjectPath, timerView);
|
||||
}
|
||||
|
||||
jasmine.clock().tick(5000);
|
||||
await Vue.nextTick();
|
||||
const timerElement = element.querySelector('.c-timer');
|
||||
const timerValue = timerElement.querySelector('.c-timer__value').innerText;
|
||||
const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play');
|
||||
|
||||
expect(timerPausePlayButton.classList.contains('icon-pause')).toBe(true);
|
||||
expect(timerValue).toBe('0D 00:00:05');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -382,6 +382,7 @@ $colorItemTreeEditingFg: $editUIColor;
|
||||
$colorItemTreeEditingIcon: $editUIColor;
|
||||
$colorItemTreeVC: $colorDisclosureCtrl;
|
||||
$colorItemTreeVCHover: $colorDisclosureCtrlHov;
|
||||
$colorItemTreeNewNode: rgba($colorBodyFg, 0.7);
|
||||
$shdwItemTreeIcon: none;
|
||||
|
||||
// Layout frame controls
|
||||
|
||||
@@ -386,6 +386,7 @@ $colorItemTreeEditingFg: $editUIColor;
|
||||
$colorItemTreeEditingIcon: $editUIColor;
|
||||
$colorItemTreeVC: $colorDisclosureCtrl;
|
||||
$colorItemTreeVCHover: $colorDisclosureCtrlHov;
|
||||
$colorItemTreeNewNode: rgba($colorBodyFg, 0.7);
|
||||
$shdwItemTreeIcon: none;
|
||||
|
||||
// Layout frame controls
|
||||
|
||||
@@ -382,6 +382,7 @@ $colorItemTreeEditingFg: $editUIColor;
|
||||
$colorItemTreeEditingIcon: $editUIColor;
|
||||
$colorItemTreeVC: $colorDisclosureCtrl;
|
||||
$colorItemTreeVCHover: $colorDisclosureCtrlHov;
|
||||
$colorItemTreeNewNode: rgba($colorBodyFg, 0.5);
|
||||
$shdwItemTreeIcon: none;
|
||||
|
||||
// Layout frame controls
|
||||
|
||||
@@ -105,7 +105,12 @@
|
||||
color: $colorItemTreeSelectedFg;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-new {
|
||||
animation-name: animTemporaryHighlight;
|
||||
animation-timing-function: ease-out;
|
||||
animation-duration: 3s;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
&.is-context-clicked {
|
||||
box-shadow: inset $colorItemTreeSelectedBg 0 0 0 1px;
|
||||
}
|
||||
@@ -289,13 +294,19 @@
|
||||
}
|
||||
|
||||
@keyframes animSlideLeft {
|
||||
0% {opactiy: 0; transform: translateX(100%);}
|
||||
0% {opacity: 0; transform: translateX(100%);}
|
||||
10% {opacity: 1;}
|
||||
100% {transform: translateX(0);}
|
||||
}
|
||||
|
||||
@keyframes animSlideRight {
|
||||
0% {opactiy: 0; transform: translateX(-100%);}
|
||||
0% {opacity: 0; transform: translateX(-100%);}
|
||||
10% {opacity: 1;}
|
||||
100% {transform: translateX(0);}
|
||||
}
|
||||
|
||||
@keyframes animTemporaryHighlight {
|
||||
from { background: transparent; }
|
||||
30% { background: $colorItemTreeNewNode; }
|
||||
100% { background: transparent; }
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
:node="treeItem"
|
||||
:active-search="activeSearch"
|
||||
:left-offset="!activeSearch ? treeItem.leftOffset : '0px'"
|
||||
:is-new="treeItem.isNew"
|
||||
:item-offset="itemOffset"
|
||||
:item-index="index"
|
||||
:item-height="itemHeight"
|
||||
@@ -112,7 +113,7 @@ import search from '../components/search.vue';
|
||||
|
||||
const ITEM_BUFFER = 25;
|
||||
const LOCAL_STORAGE_KEY__TREE_EXPANDED = 'mct-tree-expanded';
|
||||
const RETURN_ALL_DESCDNDANTS = true;
|
||||
const SORT_MY_ITEMS_ALPH_ASC = true;
|
||||
const TREE_ITEM_INDENT_PX = 18;
|
||||
|
||||
export default {
|
||||
@@ -430,10 +431,42 @@ export default {
|
||||
|
||||
return scrollTopAmount >= treeStart && scrollTopAmount < treeEnd;
|
||||
},
|
||||
sortNameAscending(a, b) {
|
||||
// sorting tree children items
|
||||
if (!(a.name && b.name)) {
|
||||
if (a.object.name > b.object.name) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (b.object.name > a.object.name) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// sorting compositon items
|
||||
if (a.name > b.name) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (b.name > a.name) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
isSortable(parentObjectPath) {
|
||||
// determine if any part of the parent's path includes a key value of mine; aka My Items
|
||||
return Boolean(parentObjectPath.find(path => path.identifier.key === 'mine'));
|
||||
},
|
||||
async loadAndBuildTreeItemsFor(domainObject, parentObjectPath, abortSignal) {
|
||||
let collection = this.openmct.composition.get(domainObject);
|
||||
let composition = await collection.load(abortSignal);
|
||||
|
||||
if (SORT_MY_ITEMS_ALPH_ASC && this.isSortable(parentObjectPath)) {
|
||||
const sortedComposition = composition.slice().sort(this.sortNameAscending);
|
||||
composition = sortedComposition;
|
||||
}
|
||||
|
||||
if (parentObjectPath.length) {
|
||||
let navigationPath = this.buildNavigationPath(parentObjectPath);
|
||||
|
||||
@@ -456,7 +489,7 @@ export default {
|
||||
return this.buildTreeItem(object, parentObjectPath);
|
||||
});
|
||||
},
|
||||
buildTreeItem(domainObject, parentObjectPath) {
|
||||
buildTreeItem(domainObject, parentObjectPath, isNew = false) {
|
||||
let objectPath = [domainObject].concat(parentObjectPath);
|
||||
let navigationPath = this.buildNavigationPath(objectPath);
|
||||
|
||||
@@ -464,6 +497,7 @@ export default {
|
||||
id: this.openmct.objects.makeKeyString(domainObject.identifier),
|
||||
object: domainObject,
|
||||
leftOffset: ((objectPath.length - 1) * TREE_ITEM_INDENT_PX) + 'px',
|
||||
isNew,
|
||||
objectPath,
|
||||
navigationPath
|
||||
};
|
||||
@@ -475,12 +509,35 @@ export default {
|
||||
},
|
||||
compositionAddHandler(navigationPath) {
|
||||
return (domainObject) => {
|
||||
let parentItem = this.getTreeItemByPath(navigationPath);
|
||||
let newItem = this.buildTreeItem(domainObject, parentItem.objectPath);
|
||||
let allDescendants = this.getChildrenInTreeFor(parentItem, RETURN_ALL_DESCDNDANTS);
|
||||
let afterItem = allDescendants.length ? allDescendants.pop() : parentItem;
|
||||
const parentItem = this.getTreeItemByPath(navigationPath);
|
||||
const newItem = this.buildTreeItem(domainObject, parentItem.objectPath, true);
|
||||
const descendants = this.getChildrenInTreeFor(parentItem, true);
|
||||
const directDescendants = this.getChildrenInTreeFor(parentItem);
|
||||
|
||||
this.addItemToTreeAfter(newItem, afterItem);
|
||||
if (directDescendants.length === 0) {
|
||||
this.addItemToTreeAfter(newItem, parentItem);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (SORT_MY_ITEMS_ALPH_ASC && this.isSortable(parentItem.objectPath)) {
|
||||
const newItemIndex = directDescendants
|
||||
.findIndex(descendant => this.sortNameAscending(descendant, newItem) > 0);
|
||||
const shouldInsertFirst = newItemIndex === 0;
|
||||
const shouldInsertLast = newItemIndex === -1;
|
||||
|
||||
if (shouldInsertFirst) {
|
||||
this.addItemToTreeAfter(newItem, parentItem);
|
||||
} else if (shouldInsertLast) {
|
||||
this.addItemToTreeAfter(newItem, descendants.pop());
|
||||
} else {
|
||||
this.addItemToTreeBefore(newItem, directDescendants[newItemIndex]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.addItemToTreeAfter(newItem, descendants.pop());
|
||||
};
|
||||
},
|
||||
compositionRemoveHandler(navigationPath) {
|
||||
@@ -512,9 +569,18 @@ export default {
|
||||
const removeIndex = this.getTreeItemIndex(item.navigationPath);
|
||||
this.treeItems.splice(removeIndex, 1);
|
||||
},
|
||||
addItemToTreeBefore(addItem, beforeItem) {
|
||||
const addIndex = this.getTreeItemIndex(beforeItem.navigationPath);
|
||||
|
||||
this.addItemToTree(addItem, addIndex);
|
||||
},
|
||||
addItemToTreeAfter(addItem, afterItem) {
|
||||
const addIndex = this.getTreeItemIndex(afterItem.navigationPath);
|
||||
this.treeItems.splice(addIndex + 1, 0, addItem);
|
||||
|
||||
this.addItemToTree(addItem, addIndex + 1);
|
||||
},
|
||||
addItemToTree(addItem, index) {
|
||||
this.treeItems.splice(index, 0, addItem);
|
||||
|
||||
if (this.isTreeItemOpen(addItem)) {
|
||||
this.openTreeItem(addItem);
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
:class="{
|
||||
'is-alias': isAlias,
|
||||
'is-navigated-object': navigated,
|
||||
'is-context-clicked': contextClickActive
|
||||
'is-context-clicked': contextClickActive,
|
||||
'is-new': isNewItem
|
||||
}"
|
||||
@click.capture="handleClick"
|
||||
@contextmenu.capture="handleContextMenu"
|
||||
@@ -59,6 +60,10 @@ export default {
|
||||
type: String,
|
||||
default: '0px'
|
||||
},
|
||||
isNew: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
itemIndex: {
|
||||
type: Number,
|
||||
required: false,
|
||||
@@ -104,6 +109,9 @@ export default {
|
||||
|
||||
return parentKeyString !== this.node.object.location;
|
||||
},
|
||||
isNewItem() {
|
||||
return this.isNew;
|
||||
},
|
||||
isLoading() {
|
||||
return Boolean(this.loadingItems[this.navigationPath]);
|
||||
},
|
||||
|
||||
@@ -147,6 +147,10 @@ class ApplicationRouter extends EventEmitter {
|
||||
let targetObject = objectPath[0];
|
||||
let navigatedObject = this.path[0];
|
||||
|
||||
if (!targetObject.identifier) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.openmct.objects.areIdsEqual(targetObject.identifier, navigatedObject.identifier);
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ define([
|
||||
objects = objects.reverse();
|
||||
|
||||
openmct.router.path = objects;
|
||||
openmct.router.emit('afterNavigation');
|
||||
browseObject = objects[0];
|
||||
|
||||
openmct.layout.$refs.browseBar.domainObject = browseObject;
|
||||
|
||||
81
src/utils/clock/Ticker.js
Normal file
81
src/utils/clock/Ticker.js
Normal file
@@ -0,0 +1,81 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2009-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.
|
||||
*****************************************************************************/
|
||||
|
||||
class Ticker {
|
||||
constructor() {
|
||||
this.callbacks = [];
|
||||
this.last = new Date() - 1000;
|
||||
|
||||
this.tick();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls functions every second, as close to the actual second
|
||||
* tick as is feasible.
|
||||
* @constructor
|
||||
* @memberof utils/clock
|
||||
*/
|
||||
tick() {
|
||||
const timestamp = new Date();
|
||||
const millis = timestamp % 1000;
|
||||
|
||||
// Only update callbacks if a second has actually passed.
|
||||
if (timestamp >= this.last + 1000) {
|
||||
this.callbacks.forEach(function (callback) {
|
||||
callback(timestamp);
|
||||
});
|
||||
this.last = timestamp - millis;
|
||||
}
|
||||
|
||||
// Try to update at exactly the next second
|
||||
setTimeout(() => {
|
||||
this.tick();
|
||||
}, 1000 - millis, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for clock ticks. The provided callback will
|
||||
* be invoked with the current timestamp (in milliseconds
|
||||
* since Jan 1 1970) at regular intervals, as near to the
|
||||
* second boundary as possible.
|
||||
*
|
||||
* @param {Function} callback callback to invoke
|
||||
* @returns {Function} a function to unregister this listener
|
||||
*/
|
||||
listen(callback) {
|
||||
this.callbacks.push(callback);
|
||||
|
||||
// Provide immediate feedback
|
||||
callback(this.last);
|
||||
|
||||
// Provide a deregistration function
|
||||
return () => {
|
||||
this.callbacks = this.callbacks.filter(function (cb) {
|
||||
return cb !== callback;
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let ticker = new Ticker();
|
||||
|
||||
export default ticker;
|
||||
Reference in New Issue
Block a user