Compare commits

...

19 Commits

Author SHA1 Message Date
Michael Rogers
370fdc6466 Preserve the previousFocusedImage for subscription updates, bound change 2021-12-14 16:30:43 -06:00
Michael Rogers
44cbd3fc50 4328 - Maintain reference to a focusedImage if the bounds change (#4545)
* WIP: adding assertions to catch negative index state

* just testing the flow

* SUpdate the image history index to previous selected image

* Cleaning up spacing and log statements

* Converted focusedImageIndex assignment to ternary and general cleanup

Co-authored-by: Jamie Vigliotta <jamie.j.vigliotta@nasa.gov>
2021-12-07 14:54:31 -08:00
Andrew Henry
858de567ae Fixes LAD rows for string telemetry (#4508)
* Fixes LAD rows for string telemetry
* saving the object if it was missing (#4471)
2021-11-29 09:29:47 -08:00
Jamie V
ad22661029 [LAD Tables] Use RAF for updating template (#4500) 2021-11-24 15:23:47 -08:00
Jamie V
26aa49d9a8 [Tabs] Sizing of offscreen tabs causing issues (#4444)
Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-11-24 11:22:54 -08:00
Jamie V
cb55500cba adding RAF to display layout alphanumerics (#4486) 2021-11-24 10:38:47 -08:00
Michael Rogers
340246ab26 Changed descending to ascending in sort order method (#4480) 2021-11-18 13:18:37 -08:00
David Tsay
9cb743fb48 Bugfix/create tree node (#4472)
* Transaction fix (#4421)

* When transaction is active, objects.get should search in dirty object first.

Co-authored-by: Andrew Henry <akhenry@gmail.com>

* find insert location prior to adding item to tree

* no need to resort

add item should only add to direct descendants

* remove unused function

* copy composition before sorting

* remove unused var

* remove master pollution

* Revert "remove master pollution"

This reverts commit 93bee13915.

* add item to correct location

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-11-18 12:30:42 -08:00
Shefali Joshi
9c52ec7233 Transaction fix (#4421) (#4461)
* When transaction is active, objects.get should search in dirty object first.
2021-11-15 14:27:23 -08:00
Shefali Joshi
933ce7aa3c [#4378] Ensure navigated object check works correctly when path is modified (#4433)
* [#4378] Emit event when router path is set. Handle the event when creating an object

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-11-15 15:35:59 -06:00
Scott Bell
8ef3869325 Notebook Objects cannot be created with CouchDB enabled (#4425)
* added null check
* added test coverage for exception that caused bug

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-11-15 14:55:04 -06:00
David 'Epper' Marshall
53f5fdabe5 Mct4207 (#4366)
* MCT 4207
* fix lint

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2021-11-15 11:25:57 -08:00
Andrew Henry
d10561fc7f Fix security policy typo, and duplicate security issue template entries (#4447)
* Tiny typo
* Remove duped security issue entries
2021-11-10 15:38:13 -08:00
John Hill
566469e691 [Task] Update our top level security policy (#4429)
* Create SECURITY.md

* Remove unused template

* Add security template to our issue chooser

* Add new issue type for maintenance

* eslint changes

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-11-10 13:33:22 -08:00
Khalid Adil
6d4a324fca [Timer] Re-implement Timer object in Vue.js (#4311)
* Re-implemented timer and clock and installed as a plugin
* Clock indicator is installed as configuration to clock plugin
* Uses moment

Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
2021-11-09 14:41:42 -08:00
Jamie V
b8fabb7e73 [MyItems Plugin] Migrate/remove legacy MyItems code into new plugin (#4107)
* removed legacy my items, created my items plugin, moved relevant code to new plugin
* added object api method for checking if a domainObject is a missing object

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-11-05 11:17:49 -07:00
Nikhil
1680c3cc1b example imagery refactor to have configurable image urls and IMAGE_DELAY (#4024)
*Imagery refactor to have configurable image urls and IMAGE_DELAY.
2021-11-05 10:54:12 -07:00
Michael Rogers
61dd85c704 Sort MyItems alphabetically when building the tree #1717 (#4376)
* Sort MyItems alphabetically when building the tree

* Fixed brace indentation

* Modified tree-item to accept is-new prop for initial highlighting

* Adjusted sort function and properly sort children before splicing treetItems

* Mods for #1717
- Refined color and timing, color is now theme-compliant;
- Added new theme color constants;
- Smoke tested in Espresso and Snow themes;

Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
2021-11-04 13:35:10 -07:00
John Hill
980777691a [CI] Add npm audit to the nightly (#4431) 2021-11-04 06:53:55 -07:00
78 changed files with 1973 additions and 2724 deletions

View File

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

View File

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

View File

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

View 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
View 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.

View File

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

View File

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

View File

@@ -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();

View File

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

View File

@@ -1,86 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../../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();
});
});
}
);

View File

@@ -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"
}
]
}
}
};
});

View File

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

View File

@@ -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;
}
);

View File

@@ -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;
}
);

View File

@@ -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;
}
);

View File

@@ -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;
}
);

View File

@@ -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;
}
);

View File

@@ -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;
}
);

View File

@@ -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;
}
);

View File

@@ -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;
}
);

View File

@@ -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;
});

View File

@@ -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);
});
});
}
);

View File

@@ -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);
});
});
}
);

View File

@@ -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);
});
});
}
);

View File

@@ -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);
});
});
}
);

View File

@@ -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();
});
});
}
);

View File

@@ -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);
});
});
}
);

View File

@@ -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");
});
});
}
);

View File

@@ -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);
});
});
}
);

View File

@@ -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);
});
});
});
});

View File

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

View File

@@ -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;
});
};

View File

@@ -1,91 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'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;
});

View File

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

View File

@@ -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();
}

View File

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

View File

@@ -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');
}
}
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -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')
}
}
};

View File

@@ -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);
}
},

View File

@@ -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);
}

View File

@@ -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;
}
},

View File

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

View File

@@ -1,9 +1,7 @@
import missingObjectInterceptor from "./missingObjectInterceptor";
import myItemsInterceptor from "./myItemsInterceptor";
export default function plugin() {
return function install(openmct) {
myItemsInterceptor(openmct);
missingObjectInterceptor(openmct);
};
}

View File

@@ -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"
});
});
});
});
});

View File

@@ -0,0 +1,8 @@
export const MY_ITEMS_KEY = 'mine';
export function createMyItemsIdentifier(namespace = '') {
return {
key: MY_ITEMS_KEY,
namespace
};
}

View File

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

View File

@@ -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);
};
});
}

View 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();
});
});
});

View File

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

View File

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

View File

@@ -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();
}
}
}
};

View File

@@ -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');
});
});

View File

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

View File

@@ -31,8 +31,6 @@
flex-direction: column;
&--hidden {
height: 1000px;
width: 1000px;
position: absolute;
left: -9999px;
top: -9999px;

View File

@@ -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';
}
}
};

View File

@@ -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 () => {

View 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;
}
};
}
};
}

View File

@@ -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';
}
}

View 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';
}
}

View 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';
}
}

View 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';
}
}

View 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
View 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;
}
});
};
}

View 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');
});
});
});

View File

@@ -382,6 +382,7 @@ $colorItemTreeEditingFg: $editUIColor;
$colorItemTreeEditingIcon: $editUIColor;
$colorItemTreeVC: $colorDisclosureCtrl;
$colorItemTreeVCHover: $colorDisclosureCtrlHov;
$colorItemTreeNewNode: rgba($colorBodyFg, 0.7);
$shdwItemTreeIcon: none;
// Layout frame controls

View File

@@ -386,6 +386,7 @@ $colorItemTreeEditingFg: $editUIColor;
$colorItemTreeEditingIcon: $editUIColor;
$colorItemTreeVC: $colorDisclosureCtrl;
$colorItemTreeVCHover: $colorDisclosureCtrlHov;
$colorItemTreeNewNode: rgba($colorBodyFg, 0.7);
$shdwItemTreeIcon: none;
// Layout frame controls

View File

@@ -382,6 +382,7 @@ $colorItemTreeEditingFg: $editUIColor;
$colorItemTreeEditingIcon: $editUIColor;
$colorItemTreeVC: $colorDisclosureCtrl;
$colorItemTreeVCHover: $colorDisclosureCtrlHov;
$colorItemTreeNewNode: rgba($colorBodyFg, 0.5);
$shdwItemTreeIcon: none;
// Layout frame controls

View File

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

View File

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

View File

@@ -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]);
},

View File

@@ -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);
}

View File

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