Compare commits

..

2 Commits

Author SHA1 Message Date
Jamie Vigliotta
d4c2085a10 just testing the flow 2021-11-23 09:45:42 -08:00
Michael Rogers
c1d6eeff94 WIP: adding assertions to catch negative index state 2021-11-12 11:25:50 -06:00
81 changed files with 2790 additions and 1980 deletions

View File

@@ -7,15 +7,6 @@ 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:
@@ -101,7 +92,6 @@ 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: Have a question about the project?
about: Got a question?

View File

@@ -0,0 +1,23 @@
<!--- 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

@@ -1,11 +0,0 @@
---
name: Maintenance
about: Add, update or remove documentation, tests, or dependencies.
title: ''
labels: type:maintenance
assignees: ''
---
#### Summary
<!--- Generally describe the purpose of the change. -->

View File

@@ -1,31 +0,0 @@
# 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,219 +20,156 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
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;
define([
let openmctInstance;
], function (
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: []
};
) {
function ImageryPlugin() {
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"
]
}
]
});
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;
openmct.telemetry.addProvider(getRealtimeProvider());
openmct.telemetry.addProvider(getHistoricalProvider());
openmct.telemetry.addProvider(getLadProvider());
};
}
function getCompassValues(min, max) {
return min + Math.random() * (max - min);
}
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]}`;
function getImageSamples(configuration) {
let imageSamples = DEFAULT_IMAGE_SAMPLES;
if (configuration.imageLocation && configuration.imageLocation.length) {
imageSamples = getImageUrlListFromConfig(configuration);
}
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);
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
};
}
};
}
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;
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);
};
}
};
return Promise.resolve(data);
}
};
}
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;
}
function getLadProvider() {
return {
supportsRequest: (domainObject, options) => {
return domainObject.type === 'example.imagery'
&& options.strategy === 'latest';
},
request: (domainObject, options) => {
const delay = getImageLoadDelay(domainObject);
return Promise.resolve(data);
}
};
return Promise.resolve([pointForTimestamp(Date.now(), domainObject.name, delay)]);
}
};
}
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)]);
}
};
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 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
}
}
]
};
}
});
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
};
}
openmct.telemetry.addProvider(realtimeProvider);
openmct.telemetry.addProvider(historicalProvider);
openmct.telemetry.addProvider(ladProvider);
};
}
return ImageryPlugin;
});

View File

@@ -82,7 +82,6 @@
);
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.Espresso());
openmct.install(openmct.plugins.MyItems());
openmct.install(openmct.plugins.Generator());
@@ -197,7 +196,6 @@
{indicator: true}
));
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
openmct.install(openmct.plugins.Timer());
openmct.start();
</script>
</html>

View File

@@ -102,7 +102,7 @@ module.exports = (config) => {
},
specReporter: {
maxLogLines: 5,
suppressErrorSummary: false,
suppressErrorSummary: true,
suppressFailed: false,
suppressPassed: false,
suppressSkipped: true,

View File

@@ -86,20 +86,11 @@ define(
})
.join('/');
function editObject() {
const path = objectPath.slice(-1).map(obj => {
const objNew = obj.getCapability('adapter').invoke();
return objNew;
});
if (isFirstViewEditable(object.useCapability('adapter'), path)) {
openmct.editor.edit();
}
}
openmct.router.once('afterNavigation', editObject);
openmct.router.navigate(url);
if (isFirstViewEditable(object.useCapability('adapter'), objectPath)) {
openmct.editor.edit();
}
}
newModel.type = this.type.getKey();

View File

@@ -28,6 +28,7 @@ define([
"./src/models/ModelCacheService",
"./src/models/PersistedModelProvider",
"./src/models/CachingModelDecorator",
"./src/models/MissingModelDecorator",
"./src/types/TypeProvider",
"./src/actions/ActionProvider",
"./src/actions/ActionAggregator",
@@ -56,6 +57,7 @@ define([
ModelCacheService,
PersistedModelProvider,
CachingModelDecorator,
MissingModelDecorator,
TypeProvider,
ActionProvider,
ActionAggregator,
@@ -146,6 +148,12 @@ define([
"cacheService"
]
},
{
"provides": "modelService",
"type": "decorator",
"priority": "fallback",
"implementation": MissingModelDecorator
},
{
"provides": "typeService",
"type": "provider",

View File

@@ -1,5 +1,5 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2021, United States Government
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
@@ -20,37 +20,43 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
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;
define(
[],
function () {
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.');
/**
* 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;
}
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;
function missingModel(id) {
return {
type: "unknown",
name: "Missing: " + id
};
}
const { timerState } = domainObject.configuration;
MissingModelDecorator.prototype.getModels = function (ids) {
function addMissingModels(models) {
var result = {};
ids.forEach(function (id) {
result[id] = models[id] || missingModel(id);
});
return domainObject.type === 'timer' && timerState === 'started';
return result;
}
return this.modelService.getModels(ids).then(addMissingModels);
};
return MissingModelDecorator;
}
}
);

View File

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

@@ -0,0 +1,209 @@
/*****************************************************************************
* 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

@@ -0,0 +1,37 @@
<!--
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

@@ -0,0 +1,70 @@
/*****************************************************************************
* 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

@@ -0,0 +1,70 @@
/*****************************************************************************
* 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

@@ -0,0 +1,78 @@
/*****************************************************************************
* 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

@@ -0,0 +1,70 @@
/*****************************************************************************
* 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

@@ -0,0 +1,55 @@
/*****************************************************************************
* 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

@@ -0,0 +1,239 @@
/*****************************************************************************
* 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

@@ -0,0 +1,73 @@
/*****************************************************************************
* 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

@@ -0,0 +1,87 @@
/*****************************************************************************
* 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

@@ -0,0 +1,113 @@
/*****************************************************************************
* 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

@@ -0,0 +1,106 @@
/*****************************************************************************
* 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

@@ -0,0 +1,112 @@
/*****************************************************************************
* 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

@@ -0,0 +1,111 @@
/*****************************************************************************
* 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

@@ -0,0 +1,111 @@
/*****************************************************************************
* 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

@@ -0,0 +1,80 @@
/*****************************************************************************
* 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

@@ -0,0 +1,227 @@
/*****************************************************************************
* 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

@@ -0,0 +1,111 @@
/*****************************************************************************
* 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

@@ -0,0 +1,62 @@
/*****************************************************************************
* 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

@@ -0,0 +1,77 @@
/*****************************************************************************
* 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

@@ -20,14 +20,30 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
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);
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"
}
}
]
}
}
};
}
});

View File

@@ -71,15 +71,13 @@ define(['zepto', 'objectUtils'], function ($, objectUtils) {
var rootObj = this.instantiate(rootModel, rootId);
var newStyleParent = parent.useCapability('adapter');
var newStyleRootObj = rootObj.useCapability('adapter');
newStyleRootObj.location = parent.getId();
if (this.openmct.composition.checkPolicy(newStyleParent, newStyleRootObj)) {
// Instantiate all objects in tree with their newly generated ids,
// adding each to its rightful parent's composition
rootObj.getCapability("location").setPrimaryLocation(parent.getId());
this.deepInstantiate(rootObj, tree.openmct, []);
this.openmct.objects.save(newStyleRootObj);
const compositionCollection = this.openmct.composition.get(newStyleParent);
compositionCollection.add(newStyleRootObj);
parent.getCapability("composition").add(rootObj);
} else {
var dialog = this.openmct.overlays.dialog({
iconClass: 'alert',
@@ -105,6 +103,7 @@ define(['zepto', 'objectUtils'], function ($, objectUtils) {
var newObj;
seen.push(parent.getId());
parentModel.composition.forEach(function (childId) {
let keystring = this.openmct.objects.makeKeyString(childId);
@@ -116,7 +115,8 @@ define(['zepto', 'objectUtils'], function ($, objectUtils) {
delete newModel.persisted;
newObj = this.instantiate(newModel, keystring);
this.openmct.objects.save(newModel);
newObj.getCapability("location")
.setPrimaryLocation(tree[keystring].location);
this.deepInstantiate(newObj, tree, seen);
}, this);
}

View File

@@ -42,21 +42,15 @@ define(
newObjects;
beforeEach(function () {
uniqueId = 0;
newObjects = [];
openmct = {
$injector: jasmine.createSpyObj('$injector', ['get']),
objects: {
makeKeyString: identifier => identifier.key,
save: o => true
},
composition: {
get: (o) => {
return {
add: v => {}
};
},
checkPolicy: (a, b) => true
makeKeyString: function (identifier) {
return identifier.key;
}
}
};
mockInstantiate = jasmine.createSpy('instantiate').and.callFake(
@@ -66,6 +60,14 @@ define(
"id": id,
"capabilities": {}
};
var locationCapability = {
setPrimaryLocation: jasmine.createSpy('setPrimaryLocation')
.and
.callFake(function (newLocation) {
config.model.location = newLocation;
})
};
config.capabilities.location = locationCapability;
if (model.composition) {
var compCapability =
jasmine.createSpy('compCapability')
@@ -77,10 +79,6 @@ define(
config.capabilities.composition = compCapability;
}
config.capabilities.adapter = {
invoke: () => model
};
newObjects.push(domainObjectFactory(config));
return domainObjectFactory(config);
@@ -148,33 +146,14 @@ define(
});
});
it("can import self-containing objects", function () {
xit("can import self-containing objects", function () {
var compDomainObject = domainObjectFactory({
name: 'compObject',
model: { name: 'compObject'},
capabilities: {
"composition": compositionCapability,
'adapter': {
invoke: () => {
return {
name: 'parent',
composition: [],
id: "mine",
identifier: {
namespace: '',
key: 'mine'
},
location: "ROOT",
modified: 1637287323760,
persisted: 1637287323760,
type: "folder"
};
}
}
}
capabilities: {"composition": compositionCapability}
});
context.domainObject = compDomainObject;
dialogService.getUserInput.and.returnValue(Promise.resolve(
{
selectFile: {
@@ -182,14 +161,10 @@ define(
"openmct": {
"infiniteParent": {
"composition": [{
"key": "infinteChild",
"namespace": ""
key: "infinteChild",
namespace: ""
}],
"identifier": {
"key": "infiniteParent",
"namespace": ""
},
"name": "parent",
"name": "1",
"type": "folder",
"modified": 1503598129176,
"location": "mine",
@@ -197,14 +172,10 @@ define(
},
"infinteChild": {
"composition": [{
"key": "infiniteParent",
"namespace": ""
key: "infinteParent",
namespace: ""
}],
"identifier": {
"key": "infinteChild",
"namespace": ""
},
"name": "child",
"name": "2",
"type": "folder",
"modified": 1503598132428,
"location": "infiniteParent",
@@ -227,7 +198,7 @@ define(
});
});
it("assigns new ids to each imported object", function () {
xit("assigns new ids to each imported object", function () {
dialogService.getUserInput.and.returnValue(Promise.resolve(
{
selectFile: {
@@ -258,6 +229,7 @@ define(
expect(newObjects[0].getId()).toBe('1');
});
});
});
}
);

View File

@@ -25,6 +25,7 @@ define([
'./capabilities/AdapterCapability',
'./directives/MCTView',
'./services/Instantiate',
'./services/MissingModelCompatibilityDecorator',
'./capabilities/APICapabilityDecorator',
'./policies/AdaptedViewPolicy',
'./runs/AlternateCompositionInitializer',
@@ -40,6 +41,7 @@ define([
AdapterCapability,
MCTView,
Instantiate,
MissingModelCompatibilityDecorator,
APICapabilityDecorator,
AdaptedViewPolicy,
AlternateCompositionInitializer,
@@ -95,6 +97,12 @@ define([
implementation: ActionDialogDecorator,
depends: ["openmct"]
},
{
type: "decorator",
provides: "modelService",
implementation: MissingModelCompatibilityDecorator,
depends: ["openmct"]
},
{
provides: "objectService",
type: "decorator",

View File

@@ -133,13 +133,9 @@ define([
return this.objectService.getObjects([keyString])
.then(function (results) {
if (results[keyString]) {
let model = results[keyString].getModel();
let model = results[keyString].getModel();
return utils.toNewFormat(model, key);
}
return;
return utils.toNewFormat(model, key);
});
};

View File

@@ -0,0 +1,91 @@
/*****************************************************************************
* 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,15 +184,6 @@ 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) {
@@ -311,13 +302,6 @@ 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
*
@@ -482,23 +466,6 @@ 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,17 +55,6 @@ 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,6 +37,7 @@ const DEFAULTS = [
'platform/execution',
'platform/exporters',
'platform/telemetry',
'platform/features/clock',
'platform/forms',
'platform/identity',
'platform/persistence/aggregator',
@@ -76,6 +77,8 @@ 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,7 +48,6 @@ const CONTEXT_MENU_ACTIONS = [
'viewHistoricalData',
'remove'
];
const BLANK_VALUE = '---';
export default {
inject: ['openmct', 'currentView'],
@@ -68,43 +67,15 @@ export default {
},
data() {
return {
datum: undefined,
timestamp: undefined,
timestampKey: undefined,
value: '---',
valueClass: '',
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() {
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];
return this.timestamp !== undefined ? this.getFormattedTimestamp(this.timestamp) : '---';
},
objectPath() {
return [this.domainObject, ...this.pathToTable];
@@ -125,19 +96,15 @@ export default {
this.timestampKey = this.openmct.time.timeSystem().key;
this.valueMetadata = undefined;
if (this.metadata) {
this.valueMetadata = this
.metadata
.valuesForHints(['range'])[0] || this.firstNonDomainAttribute(this.metadata);
}
this.valueMetadata = this.metadata ? this
.metadata
.valuesForHints(['range'])[0] : undefined;
this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;
this.unsubscribe = this.openmct
.telemetry
.subscribe(this.domainObject, this.setLatestValues);
.subscribe(this.domainObject, this.updateValues);
this.requestHistory();
@@ -151,29 +118,29 @@ export default {
this.openmct.time.off('bounds', this.updateBounds);
},
methods: {
updateView() {
if (!this.updatingView) {
this.updatingView = true;
requestAnimationFrame(() => {
let newTimestamp = this.getParsedTimestamp(this.latestDatum);
updateValues(datum) {
let newTimestamp = this.getParsedTimestamp(datum);
let limit;
if (this.shouldUpdate(newTimestamp)) {
this.timestamp = newTimestamp;
this.datum = this.latestDatum;
}
this.updatingView = false;
});
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 = '';
}
}
},
setLatestValues(datum) {
this.latestDatum = datum;
this.updateView();
},
shouldUpdate(newTimestamp) {
return this.inBounds(newTimestamp)
&& (this.timestamp === undefined || newTimestamp > this.timestamp);
let newTimestampInBounds = this.inBounds(newTimestamp);
let noExistingTimestamp = this.timestamp === undefined;
let newTimestampIsLatest = newTimestamp > this.timestamp;
return newTimestampInBounds
&& (noExistingTimestamp || newTimestampIsLatest);
},
requestHistory() {
this.openmct
@@ -184,7 +151,7 @@ export default {
size: 1,
strategy: 'latest'
})
.then((array) => this.setLatestValues(array[array.length - 1]))
.then((array) => this.updateValues(array[array.length - 1]))
.catch((error) => {
console.warn('Error fetching data', error);
});
@@ -222,21 +189,31 @@ export default {
}
},
resetValues() {
this.value = '---';
this.timestamp = undefined;
this.datum = undefined;
this.valueClass = '';
},
getParsedTimestamp(timestamp) {
if (this.timeSystemFormat) {
return this.timeSystemFormat.parse(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;
}
},
setUnit() {
this.unit = this.valueMetadata.unit || '';
},
firstNonDomainAttribute(metadata) {
return metadata
.values()
.find(metadatum => metadatum.hints.domain === undefined && metadatum.key !== 'name');
}
}
};

View File

@@ -26,7 +26,6 @@ import {
getMockObjects,
getMockTelemetry,
getLatestTelemetry,
spyOnBuiltins,
resetApplicationState
} from 'utils/testing';
@@ -161,11 +160,6 @@ describe("The LAD Table", () => {
anotherTelemetryObjectResolve = resolve;
});
spyOnBuiltins(['requestAnimationFrame']);
window.requestAnimationFrame.and.callFake((callBack) => {
callBack();
});
openmct.telemetry.request.and.callFake(() => {
telemetryRequestResolve(mockTelemetry);

View File

@@ -41,7 +41,6 @@
<script>
import moment from 'moment';
import momentTimezone from 'moment-timezone';
import ticker from 'utils/clock/Ticker';
export default {
inject: ['openmct', 'domainObject'],
@@ -83,7 +82,8 @@ export default {
}
},
mounted() {
this.unlisten = ticker.listen(this.tick);
const TickerService = this.openmct.$injector.get('tickerService');
this.unlisten = TickerService.listen(this.tick);
},
beforeDestroy() {
if (this.unlisten) {

View File

@@ -30,7 +30,6 @@
<script>
import moment from 'moment';
import ticker from 'utils/clock/Ticker';
export default {
inject: ['openmct'],
@@ -46,7 +45,10 @@ export default {
};
},
mounted() {
this.unlisten = ticker.listen(this.tick);
this.openmct.on('start', () => {
const TickerService = this.openmct.$injector.get('tickerService');
this.unlisten = TickerService.listen(this.tick);
});
},
beforeDestroy() {
if (this.unlisten) {

View File

@@ -35,7 +35,6 @@
v-if="isEditing"
:grid-size="gridSize"
:show-grid="showGrid"
:grid-dimensions="gridDimensions"
/>
<div
v-if="shouldDisplayLayoutDimensions"
@@ -160,8 +159,7 @@ export default {
initSelectIndex: undefined,
selection: [],
showGrid: true,
viewContext: {},
gridDimensions: [0, 0]
viewContext: {}
};
},
computed: {
@@ -206,23 +204,6 @@ 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() {
@@ -232,7 +213,6 @@ 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%', width: gridDimensions[0] +'px', height: gridDimensions[1] +'px' }]"
:style="[{ backgroundSize: gridSize[0] + 'px 100%' }]"
></div>
<div
v-if="gridSize[1] >= 3"
class="c-grid__y l-grid l-grid-y"
:style="[{ backgroundSize: '100%' + gridSize[1] + 'px', width: gridDimensions[0] +'px', height: gridDimensions[1] +'px' }]"
:style="[{ backgroundSize: '100%' + gridSize[1] + 'px' }]"
></div>
</div>
</template>
@@ -28,12 +28,6 @@ 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,8 +263,7 @@ export default {
this.openmct.telemetry.request(this.domainObject, options)
.then(data => {
if (data.length > 0) {
this.latestDatum = data[data.length - 1];
this.updateView();
this.updateView(data[data.length - 1]);
}
});
},
@@ -276,19 +275,12 @@ export default {
|| (datumTimeStamp
&& (this.openmct.time.bounds().end >= datumTimeStamp))
) {
this.latestDatum = datum;
this.updateView();
this.updateView(datum);
}
}.bind(this));
},
updateView() {
if (!this.updatingView) {
this.updatingView = true;
requestAnimationFrame(() => {
this.datum = this.latestDatum;
this.updatingView = false;
});
}
updateView(datum) {
this.datum = datum;
},
removeSubscription() {
if (this.subscription) {
@@ -298,8 +290,7 @@ export default {
},
refreshData(bounds, isTick) {
if (!isTick) {
this.latestDatum = undefined;
this.updateView();
this.datum = undefined;
this.requestHistoricalData(this.domainObject);
}
},

View File

@@ -296,6 +296,11 @@ export default {
return disabled;
},
focusedImage() {
// console.assert(this.imageHistory.length > this.focusedImageIndex, {
// imageHistoryLength: this.imageHistory.length,
// focusedImageIndex: this.focusedImageIndex
// });
return this.imageHistory[this.focusedImageIndex];
},
parsedSelectedTime() {
@@ -392,7 +397,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;
@@ -412,11 +417,20 @@ export default {
imageHistorySize(newSize, oldSize) {
let imageIndex;
if (this.indexForFocusedImage !== undefined) {
console.log('setting to initFocusedImageIndex', this.initFocusedImageIndex)
imageIndex = this.initFocusedImageIndex;
} else {
imageIndex = newSize > 0 ? newSize - 1 : undefined;
imageIndex = newSize > 0 ? newSize -1 : undefined;
}
// console.table({
// newSize,
// oldSize,
// imageIndex,
// imageHistoryLength: this.imageHistory.length,
// indexForFocusedImage: this.indexForFocusedImage,
// initFocusedImageIndex: this.initFocusedImageIndex
// });
// console.assert(imageIndex > -1, "The imageIndex value of %s fails", imageIndex);
this.setFocusedImage(imageIndex, false);
this.scrollToRight();
},
@@ -502,6 +516,7 @@ export default {
this.timeContext.on('timeSystem', this.trackDuration);
this.timeContext.on('clock', this.trackDuration);
this.timeContext.on("timeContext", this.setTimeContext);
// this.timeContext.on('bounds', this.handleNewBounds);
},
stopFollowingTimeContext() {
if (this.timeContext) {
@@ -513,9 +528,35 @@ export default {
boundsChange(bounds, isTick) {
if (!isTick) {
this.previousFocusedImage = this.focusedImage ? JSON.parse(JSON.stringify(this.focusedImage)) : undefined;
this.requestHistory();
this.requestHistory().then(() => { console.log('done in imageryview', this.imageHistory)});
}
},
// handleNewBounds(bounds) {
// // console.trace();
// console.group('handleBounds');
// console.log('handleBounds: image history length', this.imageHistory.length, Array.isArray(this.imageHistory))
// console.log('handleBounds: focusedImage', this.focusedImage, this.focusedImageIndex);
// // if the focused image is no longer in bounds;
// // if (this.focusedImage && (bounds.start > this.focusedImage.time || bounds.end < this.focusedImage.time)
// // // || (!this.focusedImage && this.focusedImageIndex > -1)
// // ) {
// // console.log('handleBounds: not in bounds');
// // this.isPaused = false;
// // } else {
// // console.log('handleBounds: is in bounds');
// // }
// console.groupEnd();
// // setTimeout(() => {
// // console.log('imagery history length after timeout', this.imageHistory.length, typeof this.imageHistory, this.imageHistory[this.imageHistory.length - 1], Array.isArray(this.imageHistory));
// // // this.setFocusedImage(this.imageHistory.length - 2, false);
// // }, 2000)
// // console.log('ImageryVue end')
// },
expand() {
const actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView);
const visibleActions = actionCollection.getVisibleActions();
@@ -676,47 +717,31 @@ 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)) {
if (!index) {
return;
}
if (this.previousFocusedImage) {
// 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;
delete this.previousFocusedImage;
}
console.log('previous?', this.previousFocusedImage);
// console.assert(index > -1, {index})
console.log('setFocusedImageIndex', 'from', this.focusedImageIndex, "to", index)
if (thumbnailClick) {
//We use the props till the user changes what they want to see
this.initFocusedImageIndex = undefined;
}
if (this.isPaused && !thumbnailClick && this.initFocusedImageIndex === undefined) {
this.nextImageIndex = focusedIndex;
console.log('setFocusedImage is paused and not thumnail click', index);
this.nextImageIndex = index;
//this could happen if bounds changes
if (this.focusedImageIndex > this.imageHistory.length - 1) {
this.focusedImageIndex = focusedIndex;
this.focusedImageIndex = index;
}
return;
}
this.focusedImageIndex = focusedIndex;
this.focusedImageIndex = index;
if (thumbnailClick && !this.isPaused) {
this.paused(true);

View File

@@ -144,8 +144,10 @@ export default {
}
});
//this is to optimize anything that reacts to imageHistory length
console.log(imagery, 'set to imageHistory');
this.imageHistory = imagery;
}
console.log('end of requestHistory');
},
timeSystemChange() {
this.timeSystem = this.timeContext.timeSystem();

View File

@@ -23,7 +23,7 @@
export default function MissingObjectInterceptor(openmct) {
openmct.objects.addGetInterceptor({
appliesTo: (identifier, domainObject) => {
return true;
return identifier.key !== 'mine';
},
invoke: (identifier, object) => {
if (object === undefined) {

View File

@@ -20,30 +20,24 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { MY_ITEMS_KEY } from "./createMyItemsIdentifier";
export default function MyItemsInterceptor(openmct) {
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;
openmct.objects.addGetInterceptor({
appliesTo: (identifier, domainObject) => {
return identifier.key === 'mine';
},
invoke: (identifier, object) => {
if (openmct.objects.isMissing(object)) {
return myItemsModel;
if (object === undefined) {
return {
identifier,
"name": "My Items",
"type": "folder",
"composition": [],
"location": "ROOT"
};
}
return object;
}
};
});
}
export default myItemsInterceptor;

View File

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

View File

@@ -75,5 +75,22 @@ 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

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

View File

@@ -1,90 +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.
*****************************************************************************/
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, IMAGE_MIGRATION_VER } from '../notebook/utils/notebook-migration';
import { notebookImageMigration } from '../notebook/utils/notebook-migration';
import { NOTEBOOK_TYPE } from './notebook-constants';
import Vue from 'vue';
@@ -28,7 +28,6 @@ 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';
export const IMAGE_MIGRATION_VER = "v1";
const IMAGE_MIGRATION_VER = "v1";
export function notebookImageMigration(openmct, domainObject) {
const configuration = domainObject.configuration;

View File

@@ -370,13 +370,11 @@ class CouchObjectProvider {
}
return () => {
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();
}
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,12 +41,13 @@ describe('the plugin', () => {
namespace: '',
key: 'some-value'
},
type: 'notebook',
type: 'mock-type',
modified: 0
};
options = {
url: testPath,
filter: {}
filter: {},
disableObserve: true
};
openmct = createOpenMct();
@@ -65,7 +66,7 @@ describe('the plugin', () => {
openmct.install(new CouchPlugin(options));
openmct.types.addType('notebook', {creatable: true});
openmct.types.addType('mock-type', {creatable: true});
openmct.on('start', done);
openmct.startHeadless();
@@ -97,26 +98,33 @@ describe('the plugin', () => {
fetch.and.returnValue(mockPromise);
});
it('gets an object', async () => {
const result = await openmct.objects.get(mockDomainObject.identifier);
expect(result.identifier.key).toEqual(mockDomainObject.identifier.key);
it('gets an object', () => {
return openmct.objects.get(mockDomainObject.identifier).then((result) => {
expect(result.identifier.key).toEqual(mockDomainObject.identifier.key);
});
});
it('creates an object', async () => {
const result = await openmct.objects.save(mockDomainObject);
expect(provider.create).toHaveBeenCalled();
expect(result).toBeTrue();
it('creates an object', () => {
return openmct.objects.save(mockDomainObject).then((result) => {
expect(provider.create).toHaveBeenCalled();
expect(result).toBeTrue();
});
});
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();
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();
});
});
});
});
describe('batches requests', () => {
@@ -132,7 +140,7 @@ describe('the plugin', () => {
});
fetch.and.returnValue(mockPromise);
});
it('for multiple simultaneous gets', async () => {
it('for multiple simultaneous gets', () => {
const objectIds = [
{
namespace: '',
@@ -146,32 +154,37 @@ describe('the plugin', () => {
}
];
await Promise.all(
const getAllObjects = Promise.all(
objectIds.map((identifier) =>
openmct.objects.get(identifier)
)
);
));
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');
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');
});
});
it('but not for single gets', async () => {
it('but not for single gets', () => {
const objectId = {
namespace: '',
key: 'object-1'
};
await openmct.objects.get(objectId);
const requestUrl = fetch.calls.mostRecent().args[0];
const requestMethod = fetch.calls.mostRecent().args[1].method;
const getObject = openmct.objects.get(objectId);
expect(fetch).toHaveBeenCalledTimes(1);
expect(requestUrl.endsWith(`${objectId.key}`)).toBeTrue();
expect(requestMethod).toEqual('GET');
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');
});
});
});
describe('implements server-side search', () => {
@@ -194,20 +207,22 @@ describe('the plugin', () => {
fetch.and.returnValue(mockPromise);
});
it("using Couch's 'find' endpoint", async () => {
await Promise.all(openmct.objects.search('test'));
const requestUrl = fetch.calls.mostRecent().args[0];
it("using Couch's 'find' endpoint", () => {
return Promise.all(openmct.objects.search('test')).then(() => {
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", async () => {
await Promise.all(openmct.objects.search('test'));
const requestPayload = JSON.parse(fetch.calls.mostRecent().args[1].body);
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);
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,7 +26,6 @@ define([
'./remoteClock/plugin',
'./localTimeSystem/plugin',
'./ISOTimeFormat/plugin',
'./myItems/plugin',
'../../example/generator/plugin',
'./autoflow/AutoflowTabularPlugin',
'./timeConductor/plugin',
@@ -72,7 +71,6 @@ define([
'./timeline/plugin',
'./hyperlink/plugin',
'./clock/plugin',
'./timer/plugin',
'./DeviceClassifier/plugin'
], function (
_,
@@ -80,7 +78,6 @@ define([
RemoteClock,
LocalTimeSystem,
ISOTimeFormat,
MyItems,
GeneratorPlugin,
AutoflowPlugin,
TimeConductorPlugin,
@@ -126,11 +123,11 @@ define([
Timeline,
Hyperlink,
Clock,
Timer,
DeviceClassifier
) {
const bundleMap = {
LocalStorage: 'platform/persistence/local',
MyItems: 'platform/features/my-items',
Elasticsearch: 'platform/persistence/elastic'
};
@@ -146,8 +143,6 @@ define([
plugins.LocalTimeSystem = LocalTimeSystem;
plugins.RemoteClock = RemoteClock.default;
plugins.MyItems = MyItems.default;
plugins.ImportExport = ImportExport;
plugins.StaticRootPlugin = StaticRootPlugin;
@@ -193,7 +188,7 @@ define([
return GeneratorPlugin;
};
plugins.ExampleImagery = ExampleImagery.default;
plugins.ExampleImagery = ExampleImagery;
plugins.ImageryPlugin = ImageryPlugin;
plugins.Plot = PlotPlugin.default;
plugins.Chart = ChartPlugin.default;
@@ -234,7 +229,6 @@ 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,6 +31,8 @@
flex-direction: column;
&--hidden {
height: 1000px;
width: 1000px;
position: absolute;
left: -9999px;
top: -9999px;

View File

@@ -1,10 +1,6 @@
<template>
<div
ref="tabs"
class="c-tabs-view"
>
<div class="c-tabs-view">
<div
ref="tabsHolder"
class="c-tabs-view__tabs-holder c-tabs"
:class="{
'is-dragging': isDragging && allowEditing,
@@ -32,10 +28,8 @@
}"
@click="showTab(tab, index)"
>
<div
ref="tabsLabel"
class="c-tabs-view__tab__label c-object-label"
:class="[tab.status ? `is-status--${tab.status}` : '']"
<div 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"
@@ -55,12 +49,11 @@
<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="shouldLoadTab(tab)"
v-if="isTabLoaded(tab)"
class="c-tabs-view__object"
:default-object="tab.domainObject"
:object-path="tab.objectPath"
@@ -72,7 +65,6 @@
<script>
import ObjectView from '../../../ui/components/ObjectView.vue';
import RemoveAction from '../../remove/RemoveAction.js';
import _ from 'lodash';
const unknownObjectType = {
definition: {
@@ -96,8 +88,6 @@ export default {
let keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
return {
tabWidth: undefined,
tabHeight: undefined,
internalDomainObject: this.domainObject,
currentTab: {},
currentTabIndex: undefined,
@@ -132,10 +122,6 @@ 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));
@@ -152,8 +138,6 @@ export default {
this.composition.off('remove', this.removeItem);
this.composition.off('reorder', this.onReorder);
this.tabsViewResizeObserver.disconnect();
this.tabsList.forEach(tab => {
tab.statusUnsubscribe();
});
@@ -174,28 +158,12 @@ 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);
}
@@ -203,13 +171,6 @@ 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;
@@ -364,14 +325,6 @@ 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,11 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState,
spyOnBuiltins
} from 'utils/testing';
import { createOpenMct, resetApplicationState } from 'utils/testing';
import TabsLayout from './plugin';
import Vue from "vue";
import EventEmitter from "EventEmitter";
@@ -67,13 +63,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': '',
@@ -81,7 +77,6 @@ describe('the plugin', function () {
}
});
let telemetryItem2 = Object.assign({}, telemetryItemTemplate, {
'name': 'Sine Wave Generator 2',
'id': '55122607-e65e-44d5-9c9d-9c31a914ca90',
'identifier': {
'namespace': '',
@@ -96,9 +91,6 @@ 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);
@@ -158,17 +150,8 @@ 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) {
@@ -182,9 +165,6 @@ 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, []);
@@ -195,7 +175,6 @@ describe('the plugin', function () {
afterEach(() => {
count = 0;
testViewObject.keep_alive = true;
});
it ('renders a tab for each item', () => {
@@ -206,22 +185,10 @@ describe('the plugin', function () {
describe('with domainObject.keep_alive set to', () => {
it ('true, will keep all views loaded, regardless of current tab view', (done) => {
resizeCallback();
it ('true, will keep all views loaded, regardless of current tab view', () => {
let tabViewEls = element.querySelectorAll('.c-tabs-view__object');
// 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();
});
expect(tabViewEls.length).toEqual(2);
});
it ('false, will only keep the current tab view loaded', async () => {

View File

@@ -1,65 +0,0 @@
/*****************************************************************************
* 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,57 +0,0 @@
/*****************************************************************************
* 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

@@ -1,76 +0,0 @@
/*****************************************************************************
* 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

@@ -1,57 +0,0 @@
/*****************************************************************************
* 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

@@ -1,233 +0,0 @@
<!--
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>

View File

@@ -1,119 +0,0 @@
/*****************************************************************************
* 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

@@ -1,354 +0,0 @@
/*****************************************************************************
* 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,7 +382,6 @@ $colorItemTreeEditingFg: $editUIColor;
$colorItemTreeEditingIcon: $editUIColor;
$colorItemTreeVC: $colorDisclosureCtrl;
$colorItemTreeVCHover: $colorDisclosureCtrlHov;
$colorItemTreeNewNode: rgba($colorBodyFg, 0.7);
$shdwItemTreeIcon: none;
// Layout frame controls

View File

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

View File

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

View File

@@ -105,12 +105,7 @@
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;
}
@@ -294,19 +289,13 @@
}
@keyframes animSlideLeft {
0% {opacity: 0; transform: translateX(100%);}
0% {opactiy: 0; transform: translateX(100%);}
10% {opacity: 1;}
100% {transform: translateX(0);}
}
@keyframes animSlideRight {
0% {opacity: 0; transform: translateX(-100%);}
0% {opactiy: 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,7 +74,6 @@
:node="treeItem"
:active-search="activeSearch"
:left-offset="!activeSearch ? treeItem.leftOffset : '0px'"
:is-new="treeItem.isNew"
:item-offset="itemOffset"
:item-index="index"
:item-height="itemHeight"
@@ -113,7 +112,7 @@ import search from '../components/search.vue';
const ITEM_BUFFER = 25;
const LOCAL_STORAGE_KEY__TREE_EXPANDED = 'mct-tree-expanded';
const SORT_MY_ITEMS_ALPH_ASC = true;
const RETURN_ALL_DESCDNDANTS = true;
const TREE_ITEM_INDENT_PX = 18;
export default {
@@ -431,42 +430,10 @@ 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);
@@ -489,7 +456,7 @@ export default {
return this.buildTreeItem(object, parentObjectPath);
});
},
buildTreeItem(domainObject, parentObjectPath, isNew = false) {
buildTreeItem(domainObject, parentObjectPath) {
let objectPath = [domainObject].concat(parentObjectPath);
let navigationPath = this.buildNavigationPath(objectPath);
@@ -497,7 +464,6 @@ export default {
id: this.openmct.objects.makeKeyString(domainObject.identifier),
object: domainObject,
leftOffset: ((objectPath.length - 1) * TREE_ITEM_INDENT_PX) + 'px',
isNew,
objectPath,
navigationPath
};
@@ -509,35 +475,12 @@ export default {
},
compositionAddHandler(navigationPath) {
return (domainObject) => {
const parentItem = this.getTreeItemByPath(navigationPath);
const newItem = this.buildTreeItem(domainObject, parentItem.objectPath, true);
const descendants = this.getChildrenInTreeFor(parentItem, true);
const directDescendants = this.getChildrenInTreeFor(parentItem);
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;
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());
this.addItemToTreeAfter(newItem, afterItem);
};
},
compositionRemoveHandler(navigationPath) {
@@ -569,18 +512,9 @@ 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.addItemToTree(addItem, addIndex + 1);
},
addItemToTree(addItem, index) {
this.treeItems.splice(index, 0, addItem);
this.treeItems.splice(addIndex + 1, 0, addItem);
if (this.isTreeItemOpen(addItem)) {
this.openTreeItem(addItem);

View File

@@ -8,8 +8,7 @@
:class="{
'is-alias': isAlias,
'is-navigated-object': navigated,
'is-context-clicked': contextClickActive,
'is-new': isNewItem
'is-context-clicked': contextClickActive
}"
@click.capture="handleClick"
@contextmenu.capture="handleContextMenu"
@@ -60,10 +59,6 @@ export default {
type: String,
default: '0px'
},
isNew: {
type: Boolean,
default: false
},
itemIndex: {
type: Number,
required: false,
@@ -109,9 +104,6 @@ export default {
return parentKeyString !== this.node.object.location;
},
isNewItem() {
return this.isNew;
},
isLoading() {
return Boolean(this.loadingItems[this.navigationPath]);
},

View File

@@ -147,10 +147,6 @@ 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,7 +68,6 @@ define([
objects = objects.reverse();
openmct.router.path = objects;
openmct.router.emit('afterNavigation');
browseObject = objects[0];
openmct.layout.$refs.browseBar.domainObject = browseObject;

View File

@@ -1,81 +0,0 @@
/*****************************************************************************
* 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;