Compare commits

..

1 Commits

Author SHA1 Message Date
charlesh88
db2c92edce Add new icon for aggregate telemetry 2021-08-30 11:03:09 -07:00
253 changed files with 4963 additions and 9130 deletions

View File

@@ -13,27 +13,24 @@ jobs:
type: string
browser:
type: string
always-pass:
type: boolean
executor: linux
steps:
- checkout
- restore_cache:
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}
- node/install:
install-npm: false
node-version: << parameters.node-version >>
- run: npm install
- when:
- node/install-packages:
override-ci-command: npm install
- when: # Just to save time until caching saves the browser bin
condition:
equal: [ "FirefoxESR", <<parameters.browser>> ]
steps:
- browser-tools/install-firefox:
version: "91.2.0esr" #https://archive.mozilla.org/pub/firefox/releases/
- when:
condition:
equal: [ "FirefoxHeadless", <<parameters.browser>> ]
steps:
- browser-tools/install-firefox
- when:
version: "78.11.0esr" #https://archive.mozilla.org/pub/firefox/releases/
- when: # Just to save time until caching saves the browser bin
condition:
equal: [ "ChromeHeadless", <<parameters.browser>> ]
steps:
@@ -45,53 +42,46 @@ jobs:
- ~/.npm
- ~/.cache
- node_modules
- when:
condition:
equal: [ "", <<parameters.browser>> ] #Only run linting when browsers are not running to save time
steps:
- run: npm run lint
- when:
condition: << parameters.browser >> #Truthy evaluation to only run when browser is specified
steps:
- run: npm run test:coverage -- --browsers=<<parameters.browser>>
- store_test_results:
path: dist/reports/tests/
- store_artifacts:
path: dist/reports/
- run: npm run test:coverage -- --browsers=<<parameters.browser>> || <<parameters.always-pass>>
- store_test_results:
path: dist/reports/tests/
- store_artifacts:
path: dist/reports/
workflows:
matrix-tests:
jobs:
- test:
post-steps:
- run:
command:
curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov
name: node10-chrome
node-version: lts/dubnium
browser: ChromeHeadless
always-pass: false
- test:
name: node12-build-lint
name: node12-firefoxESR-build-only
node-version: lts/erbium
browser: "" #Skip unit tests
- test:
name: node14-build-lint
browser: FirefoxESR
always-pass: true
- test:
name: node14-chrome-build-only
node-version: lts/fermium
browser: "" #Skip unit tests
browser: ChromeHeadless
always-pass: true
nightly:
jobs:
- test:
name: node10-chrome-nightly
node-version: lts/dubnium
browser: ChromeHeadless
always-pass: false
- test:
name: node12-firefoxESR-nightly
node-version: lts/erbium
browser: FirefoxESR
always-pass: false
- test:
name: node14-firefox-nightly
name: node14-chrome-nightly
node-version: lts/fermium
browser: FirefoxHeadless
browser: ChromeHeadless
always-pass: false
triggers:
- schedule:
cron: "0 0 * * *"

View File

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

View File

@@ -1,23 +0,0 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 2
labels:
- "type:maintenance"
- "dependencies"
allow:
- dependency-name: "eslint*"
- dependency-name: "karma*"
- dependency-name: "jasmine*"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
labels:
- "type:maintenance"
- "dependencies"

View File

@@ -1,41 +0,0 @@
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
paths-ignore:
- '**/*Spec.js'
- '**/*.md'
- '**/*.txt'
- '**/*.yml'
- '**/*.yaml'
schedule:
- cron: '28 21 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: javascript
- name: Autobuild
uses: github/codeql-action/autobuild@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

3
.gitignore vendored
View File

@@ -44,6 +44,3 @@ report.*.json
.lighthouseci
package-lock.json
#codecov artifacts
codecov

View File

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

View File

@@ -1,4 +1,4 @@
# Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/nasa/openmct.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/nasa/openmct/context:javascript) [![codecov](https://codecov.io/gh/nasa/openmct/branch/master/graph/badge.svg?token=7DQIipp3ej)](https://codecov.io/gh/nasa/openmct)
# Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/nasa/openmct.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/nasa/openmct/context:javascript)
Open MCT (Open Mission Control Technologies) is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data.
@@ -85,8 +85,6 @@ naming convention is otherwise the same.)
When `npm test` is run, test results will be written as HTML to
`dist/reports/tests/`. Code coverage information is written to `dist/reports/coverage`.
Code Coverage Reports are available from [codecov.io](https://app.codecov.io/gh/nasa/openmct/)
# Glossary
Certain terms are used throughout Open MCT with consistent meanings

View File

@@ -1,27 +0,0 @@
codecov:
require_ci_to_pass: false #This setting will update the bot regardless of whether or not tests pass
coverage:
status:
project:
default:
informational: true
patch:
default:
informational: true
precision: 2
round: down
range: "66...100"
parsers:
gcov:
branch_detection:
conditional: true
loop: true
method: false
macro: false
comment:
layout: "reach,diff,flags,files,footer"
behavior: default
require_changes: false

View File

@@ -28,15 +28,6 @@ define([
domain: 2
}
},
{
key: "cos",
name: "Cosine",
unit: "deg",
formatString: '%0.2f',
hints: {
domain: 3
}
},
// Need to enable "LocalTimeSystem" plugin to make use of this
// {
// key: "local",

View File

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

View File

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

View File

@@ -25,7 +25,7 @@
const devMode = process.env.NODE_ENV !== 'production';
const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeHeadless'];
const coverageEnabled = process.env.COVERAGE === 'true';
const reporters = ['spec', 'junit'];
const reporters = ['progress', 'html', 'junit'];
if (coverageEnabled) {
reporters.push('coverage-istanbul');
@@ -60,7 +60,7 @@ module.exports = (config) => {
client: {
jasmine: {
random: false,
timeoutInterval: 5000
timeoutInterval: 30000
}
},
customLaunchers: {
@@ -78,37 +78,33 @@ module.exports = (config) => {
logLevel: config.LOG_INFO,
autoWatch: true,
// HTML test reporting.
// htmlReporter: {
// outputDir: "dist/reports/tests",
// preserveDescribeNesting: true,
// foldAll: false
// },
htmlReporter: {
outputDir: "dist/reports/tests",
preserveDescribeNesting: true,
foldAll: false
},
junitReporter: {
outputDir: "dist/reports/tests",
outputFile: "test-results.xml",
useBrowserName: false
},
browserConsoleLogOptions: {
level: "error",
format: "%b %T: %m",
terminal: true
},
coverageIstanbulReporter: {
fixWebpackSourcePaths: true,
dir: process.env.CIRCLE_ARTIFACTS
? process.env.CIRCLE_ARTIFACTS + '/coverage'
: "dist/reports/coverage",
reports: ['lcovonly', 'text-summary'],
reports: ['html', 'lcovonly', 'text-summary'],
thresholds: {
global: {
lines: 66
}
}
},
specReporter: {
maxLogLines: 5,
suppressErrorSummary: true,
suppressFailed: false,
suppressPassed: false,
suppressSkipped: true,
showSpecTiming: true,
failFast: false
},
preprocessors: {
'indexTest.js': ['webpack', 'sourcemap']
},

View File

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

View File

@@ -34,6 +34,9 @@ define([
"./src/policies/EditPersistableObjectsPolicy",
"./src/representers/EditRepresenter",
"./src/capabilities/EditorCapability",
"./src/capabilities/TransactionCapabilityDecorator",
"./src/services/TransactionManager",
"./src/services/TransactionService",
"./src/creation/CreateMenuController",
"./src/creation/LocatorController",
"./src/creation/CreationPolicy",
@@ -60,6 +63,9 @@ define([
EditPersistableObjectsPolicy,
EditRepresenter,
EditorCapability,
TransactionCapabilityDecorator,
TransactionManager,
TransactionService,
CreateMenuController,
LocatorController,
CreationPolicy,
@@ -257,6 +263,26 @@ define([
}
],
"components": [
{
"type": "decorator",
"provides": "capabilityService",
"implementation": TransactionCapabilityDecorator,
"depends": [
"$q",
"transactionManager"
],
"priority": "fallback"
},
{
"type": "provider",
"provides": "transactionService",
"implementation": TransactionService,
"depends": [
"$q",
"$log",
"cacheService"
]
},
{
"key": "CreateActionProvider",
"provides": "actionService",
@@ -294,6 +320,7 @@ define([
"description": "Provides transactional editing capabilities",
"implementation": EditorCapability,
"depends": [
"transactionService",
"openmct"
]
}
@@ -304,6 +331,15 @@ define([
"template": locatorTemplate
}
],
"services": [
{
"key": "transactionManager",
"implementation": TransactionManager,
"depends": [
"transactionService"
]
}
],
"runs": [
{
depends: [

View File

@@ -96,7 +96,8 @@ function (
SaveAsAction.prototype.save = function () {
var self = this,
domainObject = this.domainObject,
dialog = new SaveInProgressDialog(this.dialogService);
dialog = new SaveInProgressDialog(this.dialogService),
toUndirty = [];
function doWizardSave(parent) {
var wizard = self.createWizard(parent);
@@ -131,14 +132,11 @@ function (
return fetchObject(object.getModel().location);
}
function saveObject(object) {
//persist the object, which adds it to the transaction and then call editor.save
return object.getCapability("persistence").persist()
.then(() => {
return self.openmct.editor.save().then(() => {
return object;
});
});
function saveObject(parent) {
return self.openmct.editor.save().then(() => {
// Force mutation for search indexing
return parent;
});
}
function addSavedObjectToParent(parent) {
@@ -152,6 +150,17 @@ function (
});
}
function undirty(object) {
return object.getCapability('persistence').refresh();
}
function undirtyOriginals(object) {
return Promise.all(toUndirty.map(undirty))
.then(() => {
return object;
});
}
function indexForSearch(addedObject) {
addedObject.useCapability('mutation', (model) => {
return model;
@@ -178,9 +187,10 @@ function (
return getParent(domainObject)
.then(doWizardSave)
.then(showBlockingDialog)
.then(saveObject)
.then(getParent)
.then(saveObject)
.then(addSavedObjectToParent)
.then(undirtyOriginals)
.then((addedObject) => {
return fetchObject(addedObject.getId());
})

View File

@@ -30,17 +30,34 @@ define(
* Once initiated, any persist operations will be queued pending a
* subsequent call to [.save()](@link #save) or [.finish()](@link
* #finish).
* @param transactionService
* @param domainObject
* @constructor
*/
function EditorCapability(
transactionService,
openmct,
domainObject
) {
this.transactionService = transactionService;
this.openmct = openmct;
this.domainObject = domainObject;
}
/**
* Initiate an editing session. This will start a transaction during
* which any persist operations will be deferred until either save()
* or finish() are called.
*/
EditorCapability.prototype.edit = function () {
console.warn('DEPRECATED: cannot edit via edit capability, use openmct.editor instead.');
if (!this.openmct.editor.isEditing()) {
this.openmct.editor.edit();
this.domainObject.getCapability('status').set('editing', true);
}
};
/**
* Determines whether this object, or any of its ancestors are
* currently being edited.
@@ -59,6 +76,38 @@ define(
return this.openmct.editor.isEditing();
};
/**
* Save any unsaved changes from this editing session. This will
* end the current transaction and continue with a new one.
* @returns {*}
*/
EditorCapability.prototype.save = function () {
console.warn('DEPRECATED: cannot save via edit capability, use openmct.editor instead.');
return Promise.resolve();
};
EditorCapability.prototype.invoke = EditorCapability.prototype.edit;
/**
* Finish the current editing session. This will discard any pending
* persist operations
* @returns {*}
*/
EditorCapability.prototype.finish = function () {
console.warn('DEPRECATED: cannot finish via edit capability, use openmct.editor instead.');
return Promise.resolve();
};
/**
* @returns {boolean} true if there have been any domain model
* modifications since the last persist, false otherwise.
*/
EditorCapability.prototype.dirty = function () {
return this.transactionService.size() > 0;
};
return EditorCapability;
}
);

View File

@@ -0,0 +1,75 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
['./TransactionalPersistenceCapability'],
function (TransactionalPersistenceCapability) {
/**
* Wraps the [PersistenceCapability]{@link PersistenceCapability} with
* transactional capabilities.
* @param $q
* @param transactionService
* @param capabilityService
* @see TransactionalPersistenceCapability
* @constructor
*/
function TransactionCapabilityDecorator(
$q,
transactionService,
capabilityService
) {
this.capabilityService = capabilityService;
this.transactionService = transactionService;
this.$q = $q;
}
/**
* Decorate PersistenceCapability to queue persistence calls when a
* transaction is in progress.
*/
TransactionCapabilityDecorator.prototype.getCapabilities = function () {
var self = this,
capabilities = this.capabilityService.getCapabilities
.apply(this.capabilityService, arguments),
persistenceCapability = capabilities.persistence;
capabilities.persistence = function (domainObject) {
var original =
(typeof persistenceCapability === 'function')
? persistenceCapability(domainObject)
: persistenceCapability;
return new TransactionalPersistenceCapability(
self.$q,
self.transactionService,
original,
domainObject
);
};
return capabilities;
};
return TransactionCapabilityDecorator;
}
);

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(
[],
function () {
/**
* Wraps persistence capability to enable transactions. Transactions
* will cause persist calls not to be invoked immediately, but
* rather queued until [EditorCapability.save()]{@link EditorCapability#save}
* or [EditorCapability.cancel()]{@link EditorCapability#cancel} are
* called.
* @memberof platform/commonUI/edit/capabilities
* @param $q
* @param transactionManager
* @param persistenceCapability
* @param domainObject
* @constructor
*/
function TransactionalPersistenceCapability(
$q,
transactionManager,
persistenceCapability,
domainObject
) {
this.transactionManager = transactionManager;
this.persistenceCapability = persistenceCapability;
this.domainObject = domainObject;
this.$q = $q;
}
/**
* The wrapped persist function. If a transaction is active, persist
* will be queued until the transaction is committed or cancelled.
* @returns {*}
*/
TransactionalPersistenceCapability.prototype.persist = function () {
var wrappedPersistence = this.persistenceCapability;
if (this.transactionManager.isActive()) {
this.transactionManager.addToTransaction(
this.domainObject.getId(),
wrappedPersistence.persist.bind(wrappedPersistence),
wrappedPersistence.refresh.bind(wrappedPersistence)
);
//Need to return a promise from this function
return this.$q.when(true);
} else {
return this.persistenceCapability.persist();
}
};
TransactionalPersistenceCapability.prototype.refresh = function () {
this.transactionManager
.clearTransactionsFor(this.domainObject.getId());
return this.persistenceCapability.refresh();
};
TransactionalPersistenceCapability.prototype.getSpace = function () {
return this.persistenceCapability.getSpace();
};
TransactionalPersistenceCapability.prototype.persisted = function () {
return this.persistenceCapability.persisted();
};
return TransactionalPersistenceCapability;
}
);

View File

@@ -19,35 +19,31 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { BAR_GRAPH_KEY } from './BarGraphConstants';
export default function BarGraphCompositionPolicy(openmct) {
function hasRange(metadata) {
const rangeValues = metadata.valuesForHints(['range']);
return rangeValues.length > 0;
define(['./Transaction'], function (Transaction) {
/**
* A nested transaction is a transaction which takes place in the context
* of a larger parent transaction. It becomes part of the parent
* transaction when (and only when) committed.
* @param parent
* @constructor
* @extends {platform/commonUI/edit/services.Transaction}
* @memberof platform/commonUI/edit/services
*/
function NestedTransaction(parent) {
this.parent = parent;
Transaction.call(this, parent.$log);
}
function hasBarGraphTelemetry(domainObject) {
if (!openmct.telemetry.isTelemetryObject(domainObject)) {
return false;
}
NestedTransaction.prototype = Object.create(Transaction.prototype);
let metadata = openmct.telemetry.getMetadata(domainObject);
NestedTransaction.prototype.commit = function () {
this.parent.add(
Transaction.prototype.commit.bind(this),
Transaction.prototype.cancel.bind(this)
);
return metadata.values().length > 0 && hasRange(metadata);
}
return {
allow: function (parent, child) {
if ((parent.type === BAR_GRAPH_KEY)
&& (hasBarGraphTelemetry(child) === false)
) {
return false;
}
return true;
}
return Promise.resolve(true);
};
}
return NestedTransaction;
});

View File

@@ -0,0 +1,99 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([], function () {
/**
* A Transaction represents a set of changes that are intended to
* be kept or discarded as a unit.
* @param $log Angular's `$log` service, for logging messages
* @constructor
* @memberof platform/commonUI/edit/services
*/
function Transaction($log) {
this.$log = $log;
this.callbacks = [];
}
/**
* Add a change to the current transaction, as expressed by functions
* to either keep or discard the change.
* @param {Function} commit called when the transaction is committed
* @param {Function} cancel called when the transaction is cancelled
* @returns {Function) a function which may be called to remove this
* pair of callbacks from the transaction
*/
Transaction.prototype.add = function (commit, cancel) {
var callback = {
commit: commit,
cancel: cancel
};
this.callbacks.push(callback);
return function () {
this.callbacks = this.callbacks.filter(function (c) {
return c !== callback;
});
}.bind(this);
};
/**
* Get the number of changes in the current transaction.
* @returns {number} the size of the current transaction
*/
Transaction.prototype.size = function () {
return this.callbacks.length;
};
/**
* Keep all changes associated with this transaction.
* @method {platform/commonUI/edit/services.Transaction#commit}
* @returns {Promise} a promise which will resolve when all callbacks
* have been handled.
*/
/**
* Discard all changes associated with this transaction.
* @method {platform/commonUI/edit/services.Transaction#cancel}
* @returns {Promise} a promise which will resolve when all callbacks
* have been handled.
*/
['commit', 'cancel'].forEach(function (method) {
Transaction.prototype[method] = function () {
var promises = [];
var callback;
while (this.callbacks.length > 0) {
callback = this.callbacks.shift();
try {
promises.push(callback[method]());
} catch (e) {
this.$log
.error("Error trying to " + method + " transaction.");
}
}
return Promise.all(promises);
};
});
return Transaction;
});

View File

@@ -0,0 +1,119 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([], function () {
/**
* Manages transactions to support the TransactionalPersistenceCapability.
* This assumes that all commit/cancel callbacks for a given domain
* object are equivalent, and only need to be added once to any active
* transaction. Violating this assumption may cause unexpected behavior.
* @constructor
* @memberof platform/commonUI/edit
*/
function TransactionManager(transactionService) {
this.transactionService = transactionService;
this.clearTransactionFns = {};
}
/**
* Check if a transaction is currently active.
* @returns {boolean} true if there is a transaction active
*/
TransactionManager.prototype.isActive = function () {
return this.transactionService.isActive();
};
/**
* Check if callbacks associated with this domain object have already
* been added to the active transaction.
* @private
* @param {string} id the identifier of the domain object to check
* @returns {boolean} true if callbacks have been added
*/
TransactionManager.prototype.isScheduled = function (id) {
return Boolean(this.clearTransactionFns[id]);
};
/**
* Add callbacks associated with this domain object to the active
* transaction. Both callbacks are expected to return promises that
* resolve when their associated behavior is complete.
*
* If callbacks associated with this domain object have already been
* added to the active transaction, this call will be ignored.
*
* @param {string} id the identifier of the associated domain object
* @param {Function} onCommit behavior to invoke when committing transaction
* @param {Function} onCancel behavior to invoke when cancelling transaction
*/
TransactionManager.prototype.addToTransaction = function (
id,
onCommit,
onCancel
) {
var release = this.releaseClearFn.bind(this, id);
function chain(promiseFn, nextFn) {
return function () {
return promiseFn().then(nextFn);
};
}
/**
* Clear any existing persistence calls for object with given ID. This ensures only the most recent persistence
* call is executed. This should prevent stale objects being persisted and overwriting fresh ones.
*/
if (this.isScheduled(id)) {
this.clearTransactionsFor(id);
}
this.clearTransactionFns[id] =
this.transactionService.addToTransaction(
chain(onCommit, release),
chain(onCancel, release)
);
};
/**
* Remove any callbacks associated with this domain object from the
* active transaction.
* @param {string} id the identifier for the domain object
*/
TransactionManager.prototype.clearTransactionsFor = function (id) {
if (this.isScheduled(id)) {
this.clearTransactionFns[id]();
this.releaseClearFn(id);
}
};
/**
* Release the cached "remove from transaction" function that has been
* stored in association with this domain object.
* @param {string} id the identifier for the domain object
* @private
*/
TransactionManager.prototype.releaseClearFn = function (id) {
delete this.clearTransactionFns[id];
};
return TransactionManager;
});

View File

@@ -0,0 +1,138 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
['./Transaction', './NestedTransaction'],
function (Transaction, NestedTransaction) {
/**
* Implements an application-wide transaction state. Once a
* transaction is started, calls to
* [PersistenceCapability.persist()]{@link PersistenceCapability#persist}
* will be deferred until a subsequent call to
* [TransactionService.commit]{@link TransactionService#commit} is made.
*
* @memberof platform/commonUI/edit/services
* @param $q
* @constructor
*/
function TransactionService($q, $log, cacheService) {
this.$q = $q;
this.$log = $log;
this.cacheService = cacheService;
this.transactions = [];
}
/**
* Starts a transaction. While a transaction is active all calls to
* [PersistenceCapability.persist](@link PersistenceCapability#persist)
* will be queued until [commit]{@link #commit} or [cancel]{@link
* #cancel} are called
*/
TransactionService.prototype.startTransaction = function () {
var transaction = this.isActive()
? new NestedTransaction(this.transactions[0])
: new Transaction(this.$log);
this.transactions.push(transaction);
};
/**
* @returns {boolean} If true, indicates that a transaction is in progress
*/
TransactionService.prototype.isActive = function () {
return this.transactions.length > 0;
};
/**
* Adds provided functions to a queue to be called on
* [.commit()]{@link #commit} or
* [.cancel()]{@link #commit}
* @param onCommit A function to call on commit
* @param onCancel A function to call on cancel
*/
TransactionService.prototype.addToTransaction = function (onCommit, onCancel) {
if (this.isActive()) {
return this.activeTransaction().add(onCommit, onCancel);
} else {
//Log error because this is a programming error if it occurs.
this.$log.error("No transaction in progress");
}
};
/**
* Get the transaction at the top of the stack.
* @private
*/
TransactionService.prototype.activeTransaction = function () {
return this.transactions[this.transactions.length - 1];
};
/**
* All persist calls deferred since the beginning of the transaction
* will be committed. If this is the last transaction, clears the
* cache.
*
* @returns {Promise} resolved when all persist operations have
* completed. Will reject if any commit operations fail
*/
TransactionService.prototype.commit = function () {
var transaction = this.transactions.pop();
if (!transaction) {
return Promise.reject();
}
if (!this.isActive()) {
return transaction.commit()
.then(function (r) {
this.cacheService.flush();
return r;
}.bind(this));
}
return transaction.commit();
};
/**
* Cancel the current transaction, replacing any dirty objects from
* persistence. Not a true rollback, as it cannot be used to undo any
* persist calls that were successful in the event one of a batch of
* persists failing.
*
* @returns {*}
*/
TransactionService.prototype.cancel = function () {
var transaction = this.transactions.pop();
return transaction ? transaction.cancel() : Promise.reject();
};
/**
* Get the size (the number of commit/cancel callbacks) of
* the active transaction.
* @returns {number} size of the active transaction
*/
TransactionService.prototype.size = function () {
return this.isActive() ? this.activeTransaction().size() : 0;
};
return TransactionService;
});

View File

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

View File

@@ -20,52 +20,35 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default class Transaction {
constructor(objectAPI) {
this.dirtyObjects = new Set();
this.objectAPI = objectAPI;
}
define(
[
"../../src/capabilities/TransactionalPersistenceCapability",
"../../src/capabilities/TransactionCapabilityDecorator"
],
function (TransactionalPersistenceCapability, TransactionCapabilityDecorator) {
add(object) {
this.dirtyObjects.add(object);
}
describe("The transaction capability decorator", function () {
var mockQ,
mockTransactionService,
mockCapabilityService,
provider;
cancel() {
return this._clear();
}
commit() {
const promiseArray = [];
const save = this.objectAPI.save.bind(this.objectAPI);
this.dirtyObjects.forEach(object => {
promiseArray.push(this.createDirtyObjectPromise(object, save));
});
return Promise.all(promiseArray);
}
createDirtyObjectPromise(object, action) {
return new Promise((resolve, reject) => {
action(object)
.then(resolve)
.catch(reject)
.finally(() => {
this.dirtyObjects.delete(object);
beforeEach(function () {
mockQ = {};
mockTransactionService = {};
mockCapabilityService = jasmine.createSpyObj("capabilityService", ["getCapabilities"]);
mockCapabilityService.getCapabilities.and.returnValue({
persistence: function () {}
});
provider = new TransactionCapabilityDecorator(mockQ, mockTransactionService, mockCapabilityService);
});
it("decorates the persistence capability", function () {
var capabilities = provider.getCapabilities();
expect(capabilities.persistence({}) instanceof TransactionalPersistenceCapability).toBe(true);
});
});
}
start() {
this.dirtyObjects = new Set();
}
_clear() {
const promiseArray = [];
const refresh = this.objectAPI.refresh.bind(this.objectAPI);
this.dirtyObjects.forEach(object => {
promiseArray.push(this.createDirtyObjectPromise(object, refresh));
});
return Promise.all(promiseArray);
}
}
);

View File

@@ -0,0 +1,111 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
"../../src/capabilities/TransactionalPersistenceCapability"
],
function (TransactionalPersistenceCapability) {
function fastPromise(val) {
return {
then: function (callback) {
return callback(val);
}
};
}
describe("The transactional persistence decorator", function () {
var mockQ,
mockTransactionManager,
mockPersistence,
mockDomainObject,
testId,
capability;
beforeEach(function () {
testId = "test-id";
mockQ = jasmine.createSpyObj("$q", ["when"]);
mockQ.when.and.callFake(function (val) {
return fastPromise(val);
});
mockTransactionManager = jasmine.createSpyObj(
"transactionService",
["isActive", "addToTransaction", "clearTransactionsFor"]
);
mockPersistence = jasmine.createSpyObj(
"persistenceCapability",
["persist", "refresh", "getSpace"]
);
mockPersistence.persist.and.returnValue(fastPromise());
mockPersistence.refresh.and.returnValue(fastPromise());
mockDomainObject = jasmine.createSpyObj(
"domainObject",
["getModel", "getId"]
);
mockDomainObject.getModel.and.returnValue({persisted: 1});
mockDomainObject.getId.and.returnValue(testId);
capability = new TransactionalPersistenceCapability(
mockQ,
mockTransactionManager,
mockPersistence,
mockDomainObject
);
});
it("if no transaction is active, passes through to persistence"
+ " provider", function () {
mockTransactionManager.isActive.and.returnValue(false);
capability.persist();
expect(mockPersistence.persist).toHaveBeenCalled();
});
it("if transaction is active, persist and cancel calls are"
+ " queued", function () {
mockTransactionManager.isActive.and.returnValue(true);
capability.persist();
expect(mockTransactionManager.addToTransaction).toHaveBeenCalled();
mockTransactionManager.addToTransaction.calls.mostRecent().args[1]();
expect(mockPersistence.persist).toHaveBeenCalled();
mockTransactionManager.addToTransaction.calls.mostRecent().args[2]();
expect(mockPersistence.refresh).toHaveBeenCalled();
});
it("wraps getSpace", function () {
mockPersistence.getSpace.and.returnValue('foo');
expect(capability.getSpace()).toEqual('foo');
});
it("clears transactions and delegates refresh calls", function () {
capability.refresh();
expect(mockTransactionManager.clearTransactionsFor)
.toHaveBeenCalledWith(testId);
expect(mockPersistence.refresh)
.toHaveBeenCalled();
});
});
}
);

View File

@@ -0,0 +1,75 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(["../../src/services/NestedTransaction"], function (NestedTransaction) {
var TRANSACTION_METHODS = ['add', 'commit', 'cancel', 'size'];
describe("A NestedTransaction", function () {
var mockTransaction,
nestedTransaction;
beforeEach(function () {
mockTransaction =
jasmine.createSpyObj('transaction', TRANSACTION_METHODS);
nestedTransaction = new NestedTransaction(mockTransaction);
});
it("exposes a Transaction's interface", function () {
TRANSACTION_METHODS.forEach(function (method) {
expect(nestedTransaction[method])
.toEqual(jasmine.any(Function));
});
});
describe("when callbacks are added", function () {
var mockCommit,
mockCancel;
beforeEach(function () {
mockCommit = jasmine.createSpy('commit');
mockCancel = jasmine.createSpy('cancel');
nestedTransaction.add(mockCommit, mockCancel);
});
it("does not interact with its parent transaction", function () {
TRANSACTION_METHODS.forEach(function (method) {
expect(mockTransaction[method])
.not.toHaveBeenCalled();
});
});
describe("and the transaction is committed", function () {
beforeEach(function () {
nestedTransaction.commit();
});
it("adds to its parent transaction", function () {
expect(mockTransaction.add).toHaveBeenCalledWith(
jasmine.any(Function),
jasmine.any(Function)
);
});
});
});
});
});

View File

@@ -0,0 +1,141 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../../src/services/TransactionManager"],
function (TransactionManager) {
describe("TransactionManager", function () {
var mockTransactionService,
testId,
mockOnCommit,
mockOnCancel,
mockRemoves,
mockPromise,
manager;
beforeEach(function () {
mockRemoves = [];
mockTransactionService = jasmine.createSpyObj(
"transactionService",
["addToTransaction", "isActive"]
);
mockOnCommit = jasmine.createSpy('commit');
mockOnCancel = jasmine.createSpy('cancel');
testId = 'test-id';
mockPromise = jasmine.createSpyObj('promise', ['then']);
mockOnCommit.and.returnValue(mockPromise);
mockOnCancel.and.returnValue(mockPromise);
mockTransactionService.addToTransaction.and.callFake(function () {
var mockRemove =
jasmine.createSpy('remove-' + mockRemoves.length);
mockRemoves.push(mockRemove);
return mockRemove;
});
manager = new TransactionManager(mockTransactionService);
});
it("delegates isActive calls", function () {
[false, true].forEach(function (state) {
mockTransactionService.isActive.and.returnValue(state);
expect(manager.isActive()).toBe(state);
});
});
describe("when addToTransaction is called", function () {
beforeEach(function () {
manager.addToTransaction(
testId,
mockOnCommit,
mockOnCancel
);
});
it("adds callbacks to the active transaction", function () {
expect(mockTransactionService.addToTransaction)
.toHaveBeenCalledWith(
jasmine.any(Function),
jasmine.any(Function)
);
});
it("invokes passed-in callbacks from its own callbacks", function () {
expect(mockOnCommit).not.toHaveBeenCalled();
mockTransactionService.addToTransaction
.calls.mostRecent().args[0]();
expect(mockOnCommit).toHaveBeenCalled();
expect(mockOnCancel).not.toHaveBeenCalled();
mockTransactionService.addToTransaction
.calls.mostRecent().args[1]();
expect(mockOnCancel).toHaveBeenCalled();
});
describe("Adds callbacks to transaction", function () {
beforeEach(function () {
spyOn(manager, 'clearTransactionsFor');
manager.clearTransactionsFor.and.callThrough();
});
it("and clears pending calls if same object", function () {
manager.addToTransaction(
testId,
jasmine.createSpy(),
jasmine.createSpy()
);
expect(manager.clearTransactionsFor).toHaveBeenCalledWith(testId);
});
it("and does not clear pending calls if different object", function () {
manager.addToTransaction(
'other-id',
jasmine.createSpy(),
jasmine.createSpy()
);
expect(manager.clearTransactionsFor).not.toHaveBeenCalled();
});
afterEach(function () {
expect(mockTransactionService.addToTransaction.calls.count()).toEqual(2);
});
});
it("does not remove callbacks from the transaction", function () {
expect(mockRemoves[0]).not.toHaveBeenCalled();
});
describe("and clearTransactionsFor is subsequently called", function () {
beforeEach(function () {
manager.clearTransactionsFor(testId);
});
it("removes callbacks from the transaction", function () {
expect(mockRemoves[0]).toHaveBeenCalled();
});
});
});
});
}
);

View File

@@ -0,0 +1,139 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../../src/services/TransactionService"],
function (TransactionService) {
describe("The Transaction Service", function () {
var mockQ,
mockLog,
mockCacheService,
transactionService;
function fastPromise(val) {
return {
then: function (callback) {
return fastPromise(callback(val));
}
};
}
beforeEach(function () {
mockQ = jasmine.createSpyObj("$q", ["all"]);
mockCacheService = jasmine.createSpyObj("cacheService", ["flush"]);
mockQ.all.and.returnValue(fastPromise());
mockLog = jasmine.createSpyObj("$log", ["error"]);
transactionService = new TransactionService(mockQ, mockLog, mockCacheService);
});
it("isActive returns true if a transaction is in progress", function () {
expect(transactionService.isActive()).toBe(false);
transactionService.startTransaction();
expect(transactionService.isActive()).toBe(true);
});
it("addToTransaction queues onCommit and onCancel functions", function () {
var onCommit = jasmine.createSpy('onCommit'),
onCancel = jasmine.createSpy('onCancel');
transactionService.startTransaction();
transactionService.addToTransaction(onCommit, onCancel);
expect(transactionService.size()).toBe(1);
});
it("size function returns size of commit and cancel queues", function () {
var onCommit = jasmine.createSpy('onCommit'),
onCancel = jasmine.createSpy('onCancel');
transactionService.startTransaction();
transactionService.addToTransaction(onCommit, onCancel);
transactionService.addToTransaction(onCommit, onCancel);
transactionService.addToTransaction(onCommit, onCancel);
expect(transactionService.size()).toBe(3);
});
describe("commit", function () {
var onCommits;
beforeEach(function () {
onCommits = [0, 1, 2].map(function (val) {
return jasmine.createSpy("onCommit" + val);
});
transactionService.startTransaction();
onCommits.forEach(transactionService.addToTransaction.bind(transactionService));
});
it("commit calls all queued commit functions", function () {
expect(transactionService.size()).toBe(3);
return transactionService.commit().then(() => {
onCommits.forEach(function (spy) {
expect(spy).toHaveBeenCalled();
});
});
});
it("commit resets active state and clears queues", function () {
return transactionService.commit().then(() => {
expect(transactionService.isActive()).toBe(false);
expect(transactionService.size()).toBe(0);
expect(transactionService.size()).toBe(0);
});
});
});
describe("cancel", function () {
var onCancels;
beforeEach(function () {
onCancels = [0, 1, 2].map(function (val) {
return jasmine.createSpy("onCancel" + val);
});
transactionService.startTransaction();
onCancels.forEach(function (onCancel) {
transactionService.addToTransaction(undefined, onCancel);
});
});
it("cancel calls all queued cancel functions", function () {
expect(transactionService.size()).toBe(3);
transactionService.cancel();
onCancels.forEach(function (spy) {
expect(spy).toHaveBeenCalled();
});
});
it("cancel resets active state and clears queues", function () {
transactionService.cancel();
expect(transactionService.isActive()).toBe(false);
expect(transactionService.size()).toBe(0);
});
});
});
}
);

View File

@@ -0,0 +1,109 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../../src/services/Transaction"],
function (Transaction) {
describe("A Transaction", function () {
var mockLog,
transaction;
beforeEach(function () {
mockLog = jasmine.createSpyObj(
'$log',
['warn', 'info', 'error', 'debug']
);
transaction = new Transaction(mockLog);
});
it("initially has a size of zero", function () {
expect(transaction.size()).toEqual(0);
});
describe("when callbacks are added", function () {
var mockCommit,
mockCancel,
remove;
beforeEach(function () {
mockCommit = jasmine.createSpy('commit');
mockCancel = jasmine.createSpy('cancel');
remove = transaction.add(mockCommit, mockCancel);
});
it("reports a new size", function () {
expect(transaction.size()).toEqual(1);
});
it("returns a function to remove those callbacks", function () {
expect(remove).toEqual(jasmine.any(Function));
remove();
expect(transaction.size()).toEqual(0);
});
describe("and the transaction is committed", function () {
beforeEach(function () {
transaction.commit();
});
it("triggers the commit callback", function () {
expect(mockCommit).toHaveBeenCalled();
});
it("does not trigger the cancel callback", function () {
expect(mockCancel).not.toHaveBeenCalled();
});
});
describe("and the transaction is cancelled", function () {
beforeEach(function () {
transaction.cancel();
});
it("triggers the cancel callback", function () {
expect(mockCancel).toHaveBeenCalled();
});
it("does not trigger the commit callback", function () {
expect(mockCommit).not.toHaveBeenCalled();
});
});
describe("and an exception is encountered during commit", function () {
beforeEach(function () {
mockCommit.and.callFake(function () {
throw new Error("test error");
});
transaction.commit();
});
it("logs an error", function () {
expect(mockLog.error).toHaveBeenCalled();
});
});
});
});
}
);

View File

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

View File

@@ -45,6 +45,7 @@ define([
"./src/capabilities/MutationCapability",
"./src/capabilities/DelegationCapability",
"./src/capabilities/InstantiationCapability",
"./src/runs/TransactingMutationListener",
"./src/services/Now",
"./src/services/Throttle",
"./src/services/Topic",
@@ -74,6 +75,7 @@ define([
MutationCapability,
DelegationCapability,
InstantiationCapability,
TransactingMutationListener,
Now,
Throttle,
Topic,
@@ -361,6 +363,12 @@ define([
]
}
],
"runs": [
{
"implementation": TransactingMutationListener,
"depends": ["topic", "transactionService", "cacheService"]
}
],
"constants": [
{
"key": "PERSISTENCE_SPACE",

View File

@@ -0,0 +1,55 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([], function () {
/**
* Listens for mutation on domain objects and triggers persistence when
* it occurs.
* @param {Topic} topic the `topic` service; used to listen for mutation
* @memberof platform/core
*/
function TransactingMutationListener(
topic,
transactionService,
cacheService
) {
function hasChanged(domainObject) {
var model = domainObject.getModel();
return model.persisted === undefined || model.modified > model.persisted;
}
var mutationTopic = topic('mutation');
mutationTopic.listen(function (domainObject) {
var persistence = domainObject.getCapability('persistence');
cacheService.put(domainObject.getId(), domainObject.getModel());
if (hasChanged(domainObject)) {
persistence.persist();
}
});
}
return TransactingMutationListener;
});

View File

@@ -0,0 +1,112 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../../src/runs/TransactingMutationListener"],
function (TransactingMutationListener) {
describe("TransactingMutationListener", function () {
var mockTopic,
mockMutationTopic,
mockCacheService,
mockTransactionService,
mockDomainObject,
mockModel,
mockPersistence;
beforeEach(function () {
mockTopic = jasmine.createSpy('topic');
mockMutationTopic =
jasmine.createSpyObj('mutation', ['listen']);
mockCacheService =
jasmine.createSpyObj('cacheService', [
'put'
]);
mockTransactionService =
jasmine.createSpyObj('transactionService', [
'isActive',
'startTransaction',
'commit'
]);
mockDomainObject = jasmine.createSpyObj(
'domainObject',
['getId', 'getCapability', 'getModel']
);
mockPersistence = jasmine.createSpyObj(
'persistence',
['persist', 'refresh', 'persisted']
);
mockTopic.and.callFake(function (t) {
expect(t).toBe('mutation');
return mockMutationTopic;
});
mockDomainObject.getId.and.returnValue('mockId');
mockDomainObject.getCapability.and.callFake(function (c) {
expect(c).toBe('persistence');
return mockPersistence;
});
mockModel = {};
mockDomainObject.getModel.and.returnValue(mockModel);
mockPersistence.persisted.and.returnValue(true);
return new TransactingMutationListener(
mockTopic,
mockTransactionService,
mockCacheService
);
});
it("listens for mutation", function () {
expect(mockMutationTopic.listen)
.toHaveBeenCalledWith(jasmine.any(Function));
});
it("calls persist if the model has changed", function () {
mockModel.persisted = Date.now();
//Mark the model dirty by setting the mutated date later than the last persisted date.
mockModel.modified = mockModel.persisted + 1;
mockMutationTopic.listen.calls.mostRecent()
.args[0](mockDomainObject);
expect(mockPersistence.persist).toHaveBeenCalled();
});
it("does not call persist if the model has not changed", function () {
mockModel.persisted = Date.now();
mockModel.modified = mockModel.persisted;
mockMutationTopic.listen.calls.mostRecent()
.args[0](mockDomainObject);
expect(mockPersistence.persist).not.toHaveBeenCalled();
});
});
}
);

View File

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

View File

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

View File

@@ -0,0 +1,110 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'moment',
'moment-timezone'
],
function (
moment,
momentTimezone
) {
/**
* Controller for views of a Clock domain object.
*
* @constructor
* @memberof platform/features/clock
* @param {angular.Scope} $scope the Angular scope
* @param {platform/features/clock.TickerService} tickerService
* a service used to align behavior with clock ticks
*/
function ClockController($scope, tickerService) {
var lastTimestamp,
unlisten,
timeFormat,
zoneName,
self = this;
function update() {
var m = zoneName
? moment.utc(lastTimestamp).tz(zoneName) : moment.utc(lastTimestamp);
self.zoneAbbr = m.zoneAbbr();
self.textValue = timeFormat && m.format(timeFormat);
self.ampmValue = m.format("A"); // Just the AM or PM part
}
function tick(timestamp) {
lastTimestamp = timestamp;
update();
}
function updateModel(model) {
var baseFormat;
if (model !== undefined) {
baseFormat = model.clockFormat[0];
self.use24 = model.clockFormat[1] === 'clock24';
timeFormat = self.use24
? baseFormat.replace('hh', "HH") : baseFormat;
// If wrong timezone is provided, the UTC will be used
zoneName = momentTimezone.tz.names().includes(model.timezone)
? model.timezone : "UTC";
update();
}
}
// Pull in the model (clockFormat and timezone) from the domain object model
$scope.$watch('model', updateModel);
// Listen for clock ticks ... and stop listening on destroy
unlisten = tickerService.listen(tick);
$scope.$on('$destroy', unlisten);
}
/**
* Get the clock's time zone, as displayable text.
* @returns {string}
*/
ClockController.prototype.zone = function () {
return this.zoneAbbr;
};
/**
* Get the current time, as displayable text.
* @returns {string}
*/
ClockController.prototype.text = function () {
return this.textValue;
};
/**
* Get the text to display to qualify a time as AM or PM.
* @returns {string}
*/
ClockController.prototype.ampm = function () {
return this.use24 ? '' : this.ampmValue;
};
return ClockController;
}
);

View File

@@ -0,0 +1,65 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
['moment'],
function (moment) {
/**
* Indicator that displays the current UTC time in the status area.
* @implements {Indicator}
* @memberof platform/features/clock
* @param {platform/features/clock.TickerService} tickerService
* a service used to align behavior with clock ticks
* @param {string} indicatorFormat format string for timestamps
* shown in this indicator
*/
function ClockIndicator(tickerService, indicatorFormat) {
var self = this;
this.text = "";
tickerService.listen(function (timestamp) {
self.text = moment.utc(timestamp)
.format(indicatorFormat) + " UTC";
});
}
ClockIndicator.prototype.getGlyphClass = function () {
return "";
};
ClockIndicator.prototype.getCssClass = function () {
return "t-indicator-clock icon-clock no-minify c-indicator--not-clickable";
};
ClockIndicator.prototype.getText = function () {
return this.text;
};
ClockIndicator.prototype.getDescription = function () {
return "";
};
return ClockIndicator;
}
);

View File

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

View File

@@ -0,0 +1,58 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../../src/indicators/ClockIndicator"],
function (ClockIndicator) {
// Wed, 03 Jun 2015 17:56:14 GMT
var TEST_TIMESTAMP = 1433354174000,
TEST_FORMAT = "YYYY-DDD HH:mm:ss";
describe("The clock indicator", function () {
var mockTicker,
mockUnticker,
indicator;
beforeEach(function () {
mockTicker = jasmine.createSpyObj('ticker', ['listen']);
mockUnticker = jasmine.createSpy('unticker');
mockTicker.listen.and.returnValue(mockUnticker);
indicator = new ClockIndicator(mockTicker, TEST_FORMAT);
});
it("displays the current time", function () {
mockTicker.listen.calls.mostRecent().args[0](TEST_TIMESTAMP);
expect(indicator.getText()).toEqual("2015-154 17:56:14 UTC");
});
it("implements the Indicator interface", function () {
expect(indicator.getCssClass()).toEqual(jasmine.any(String));
expect(indicator.getText()).toEqual(jasmine.any(String));
expect(indicator.getDescription()).toEqual(jasmine.any(String));
});
});
}
);

View File

@@ -44,11 +44,9 @@ define(
setText(result.name);
scope.ngModel[scope.field] = result;
control.$setValidity("file-input", true);
scope.$digest();
}, function () {
setText('Select File');
control.$setValidity("file-input", false);
scope.$digest();
});
}

View File

@@ -30,8 +30,8 @@ define([
return function ImportExportPlugin() {
return function (openmct) {
ExportAsJSONAction.prototype.appliesTo = function (context) {
return this.openmct.$injector.get('policyService')
ExportAsJSONAction.appliesTo = function (context) {
return openmct.$injector.get('policyService')
.allow("creation", context.domainObject.getCapability("type")
);
};

View File

@@ -29,7 +29,7 @@ define(
],
function (ExportAsJSONAction, domainObjectFactory, MCT, AdapterCapability) {
describe("The export JSON action", function () {
xdescribe("The export JSON action", function () {
var context,
action,
@@ -102,7 +102,7 @@ define(
expect(action).toBeDefined();
});
xit("doesn't export non-creatable objects in tree", function () {
it("doesn't export non-creatable objects in tree", function () {
var nonCreatableType = {
hasFeature:
function (feature) {
@@ -149,7 +149,7 @@ define(
});
});
xit("can export self-containing objects", function () {
it("can export self-containing objects", function () {
var parent = domainObjectFactory({
name: 'parent',
model: {
@@ -191,7 +191,7 @@ define(
});
});
xit("exports links to external objects as new objects", function () {
it("exports links to external objects as new objects", function () {
var parent = domainObjectFactory({
name: 'parent',
model: {

View File

@@ -27,7 +27,7 @@ define(
],
function (ImportAsJSONAction, domainObjectFactory) {
describe("The import JSON action", function () {
xdescribe("The import JSON action", function () {
var context = {};
var action,
@@ -146,7 +146,7 @@ define(
});
});
xit("can import self-containing objects", function () {
it("can import self-containing objects", function () {
var compDomainObject = domainObjectFactory({
name: 'compObject',
model: { name: 'compObject'},
@@ -198,7 +198,7 @@ define(
});
});
xit("assigns new ids to each imported object", function () {
it("assigns new ids to each imported object", function () {
dialogService.getUserInput.and.returnValue(Promise.resolve(
{
selectFile: {

View File

@@ -136,7 +136,7 @@ define([
* @memberof module:openmct.MCT#
* @name conductor
*/
this.time = new api.TimeAPI(this);
this.time = new api.TimeAPI();
/**
* An interface for interacting with the composition of domain objects.
@@ -253,8 +253,6 @@ define([
this.status = new api.StatusAPI(this);
this.priority = api.PriorityAPI;
this.router = new ApplicationRouter(this);
this.branding = BrandingAPI.default;
@@ -265,7 +263,6 @@ define([
// Plugins that are installed by default
this.install(this.plugins.Plot());
this.install(this.plugins.Chart());
this.install(this.plugins.TelemetryTable.default());
this.install(PreviewPlugin.default());
this.install(LegacyIndicatorsPlugin());

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* Open openmct, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* Open openmct is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
@@ -14,27 +14,33 @@
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* Open openmct includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default function timelineInterceptor(openmct) {
define([
openmct.objects.addGetInterceptor({
appliesTo: (identifier, domainObject) => {
return domainObject && domainObject.type === 'time-strip';
},
invoke: (identifier, object) => {
], function (
if (object && object.configuration === undefined) {
object.configuration = {
useIndependentTime: true
};
}
) {
return object;
function checkForDeprecatedFunctionality(typeDef) {
if (Object.prototype.hasOwnProperty.call(typeDef, 'telemetry')) {
console.warn(
'DEPRECATION WARNING: Telemetry data on type '
+ 'registrations will be deprecated in a future version, '
+ 'please convert to a custom telemetry metadata provider '
+ 'for type: ' + typeDef.key
);
}
});
}
}
function TypeDeprecationChecker(types) {
types.forEach(checkForDeprecatedFunctionality);
}
return TypeDeprecationChecker;
});

View File

@@ -81,8 +81,14 @@ define([
return models;
}
//Temporary fix for missing models - don't retry using this.apiFetch
return models;
return this.apiFetch(missingIds)
.then(function (apiResults) {
Object.keys(apiResults).forEach(function (k) {
models[k] = apiResults[k];
});
return models;
});
}.bind(this));
};

View File

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

View File

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

View File

@@ -34,6 +34,7 @@ export default class Editor extends EventEmitter {
* Initiate an editing session. This will start a transaction during
* which any persist operations will be deferred until either save()
* or finish() are called.
* @private
*/
edit() {
if (this.editing === true) {
@@ -41,8 +42,8 @@ export default class Editor extends EventEmitter {
}
this.editing = true;
this.getTransactionService().startTransaction();
this.emit('isEditing', true);
this.openmct.objects.startTransaction();
}
/**
@@ -55,36 +56,41 @@ export default class Editor extends EventEmitter {
/**
* Save any unsaved changes from this editing session. This will
* end the current transaction.
*
* @private
*/
save() {
const transaction = this.openmct.objects.getActiveTransaction();
return this.getTransactionService().commit().then((result) => {
this.editing = false;
this.emit('isEditing', false);
return transaction.commit()
.then(() => {
this.editing = false;
this.emit('isEditing', false);
}).catch(error => {
throw error;
}).finally(() => {
this.openmct.objects.endTransaction();
});
return result;
}).catch((error) => {
throw error;
});
}
/**
* End the currently active transaction and discard unsaved changes.
*
* @private
*/
cancel() {
let cancelPromise = this.getTransactionService().cancel();
this.editing = false;
this.emit('isEditing', false);
return new Promise((resolve, reject) => {
const transaction = this.openmct.objects.getActiveTransaction();
transaction.cancel()
.then(resolve)
.catch(reject)
.finally(() => {
this.openmct.objects.endTransaction();
});
});
return cancelPromise;
}
/**
* @private
*/
getTransactionService() {
if (!this.transactionService) {
this.transactionService = this.openmct.$injector.get('transactionService');
}
return this.transactionService;
}
}

View File

@@ -110,7 +110,7 @@ class ActionsAPI extends EventEmitter {
return actionsObject;
}
_groupAndSortActions(actionsArray = []) {
_groupAndSortActions(actionsArray) {
if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') {
actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]);
}

View File

@@ -31,8 +31,7 @@ define([
'./Editor',
'./menu/MenuAPI',
'./actions/ActionsAPI',
'./status/StatusAPI',
'./priority/PriorityAPI'
'./status/StatusAPI'
], function (
TimeAPI,
ObjectAPI,
@@ -44,11 +43,10 @@ define([
EditorAPI,
MenuAPI,
ActionsAPI,
StatusAPI,
PriorityAPI
StatusAPI
) {
return {
TimeAPI: TimeAPI.default,
TimeAPI: TimeAPI,
ObjectAPI: ObjectAPI,
CompositionAPI: CompositionAPI,
TypeRegistry: TypeRegistry,
@@ -58,7 +56,6 @@ define([
EditorAPI: EditorAPI,
MenuAPI: MenuAPI.default,
ActionsAPI: ActionsAPI.default,
StatusAPI: StatusAPI.default,
PriorityAPI: PriorityAPI.default
StatusAPI: StatusAPI.default
};
});

View File

@@ -31,22 +31,14 @@ define([
this.indicatorObjects = [];
}
IndicatorAPI.prototype.getIndicatorObjectsByPriority = function () {
const sortedIndicators = this.indicatorObjects.sort((a, b) => b.priority - a.priority);
return sortedIndicators;
};
IndicatorAPI.prototype.simpleIndicator = function () {
return new SimpleIndicator(this.openmct);
};
/**
* Accepts an indicator object, which is a simple object
* with a two attributes: 'element' which has an HTMLElement
* as its value, and 'priority' with an integer that specifies its order in the layout.
* The lower the priority, the further to the right the element is placed.
* If undefined, the priority will be assigned -1.
* with a single attribute, 'element' which has an HTMLElement
* as its value.
*
* We provide .simpleIndicator() as a convenience function
* which will create a default Open MCT indicator that can
@@ -55,7 +47,7 @@ define([
* and dynamic behavior.
*
* Eg.
* const myIndicator = openmct.indicators.simpleIndicator();
* var myIndicator = openmct.indicators.simpleIndicator();
* openmct.indicators.add(myIndicator);
*
* myIndicator.text("Hello World!");
@@ -63,10 +55,6 @@ define([
*
*/
IndicatorAPI.prototype.add = function (indicator) {
if (!indicator.priority) {
indicator.priority = this.openmct.priority.DEFAULT;
}
this.indicatorObjects.push(indicator);
};

View File

@@ -19,64 +19,97 @@
* 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 SimpleIndicator from './SimpleIndicator';
describe("The Indicator API", () => {
let openmct;
define(
[
"../../MCT",
"../../../platform/commonUI/general/src/directives/MCTIndicators"
],
function (
MCT,
MCTIndicators
) {
xdescribe("The Indicator API", function () {
let openmct;
let directive;
let holderElement;
beforeEach(() => {
openmct = createOpenMct();
beforeEach(function () {
openmct = new MCT();
directive = new MCTIndicators(openmct);
holderElement = document.createElement('div');
});
describe("The simple indicator", function () {
let simpleIndicator;
beforeEach(function () {
simpleIndicator = openmct.indicators.simpleIndicator();
openmct.indicators.add(simpleIndicator);
renderIndicators();
});
it("applies the set icon class", function () {
simpleIndicator.iconClass('testIconClass');
expect(getIconElement().classList.contains('testIconClass')).toBe(true);
simpleIndicator.iconClass('anotherIconClass');
expect(getIconElement().classList.contains('testIconClass')).toBe(false);
expect(getIconElement().classList.contains('anotherIconClass')).toBe(true);
});
it("applies the set status class", function () {
simpleIndicator.statusClass('testStatusClass');
expect(getIconElement().classList.contains('testStatusClass')).toBe(true);
simpleIndicator.statusClass('anotherStatusClass');
expect(getIconElement().classList.contains('testStatusClass')).toBe(false);
expect(getIconElement().classList.contains('anotherStatusClass')).toBe(true);
});
it("displays the set text", function () {
simpleIndicator.text('some test text');
expect(getTextElement().textContent.trim()).toEqual('some test text');
});
it("sets the indicator's title", function () {
simpleIndicator.description('a test description');
expect(getIndicatorElement().getAttribute('title')).toEqual('a test description');
});
it("Hides indicator icon if no text is set", function () {
simpleIndicator.text('');
expect(getIndicatorElement().classList.contains('hidden')).toBe(true);
});
function getIconElement() {
return holderElement.querySelector('.ls-indicator');
}
function getIndicatorElement() {
return holderElement.querySelector('.ls-indicator');
}
function getTextElement() {
return holderElement.querySelector('.indicator-text');
}
});
it("Supports registration of a completely custom indicator", function () {
const customIndicator = document.createElement('div');
customIndicator.classList.add('customIndicator');
customIndicator.textContent = 'A custom indicator';
openmct.indicators.add({element: customIndicator});
renderIndicators();
expect(holderElement.querySelector('.customIndicator').textContent.trim()).toEqual('A custom indicator');
});
function renderIndicators() {
directive.link({}, holderElement);
}
});
});
afterEach(() => {
return resetApplicationState(openmct);
});
function generateIndicator(className, label, priority) {
const element = document.createElement('div');
element.classList.add(className);
const textNode = document.createTextNode(label);
element.appendChild(textNode);
const testIndicator = {
element,
priority
};
return testIndicator;
}
it("can register an indicator", () => {
const testIndicator = generateIndicator('test-indicator', 'This is a test indicator', 2);
openmct.indicators.add(testIndicator);
expect(openmct.indicators.indicatorObjects).toBeDefined();
// notifier indicator is installed by default
expect(openmct.indicators.indicatorObjects.length).toBe(2);
});
it("can order indicators based on priority", () => {
const testIndicator1 = generateIndicator('test-indicator-1', 'This is a test indicator', openmct.priority.LOW);
openmct.indicators.add(testIndicator1);
const testIndicator2 = generateIndicator('test-indicator-2', 'This is another test indicator', openmct.priority.DEFAULT);
openmct.indicators.add(testIndicator2);
const testIndicator3 = generateIndicator('test-indicator-3', 'This is yet another test indicator', openmct.priority.LOW);
openmct.indicators.add(testIndicator3);
const testIndicator4 = generateIndicator('test-indicator-4', 'This is yet another test indicator', openmct.priority.HIGH);
openmct.indicators.add(testIndicator4);
expect(openmct.indicators.indicatorObjects.length).toBe(5);
const indicatorObjectsByPriority = openmct.indicators.getIndicatorObjectsByPriority();
expect(indicatorObjectsByPriority.length).toBe(5);
expect(indicatorObjectsByPriority[2].priority).toBe(openmct.priority.DEFAULT);
});
it("the simple indicator can be added", () => {
const simpleIndicator = new SimpleIndicator(openmct);
openmct.indicators.add(simpleIndicator);
expect(openmct.indicators.indicatorObjects.length).toBe(2);
});
});

View File

@@ -27,7 +27,6 @@ define(['zepto', './res/indicator-template.html'],
function SimpleIndicator(openmct) {
this.openmct = openmct;
this.element = $(indicatorTemplate)[0];
this.priority = openmct.priority.DEFAULT;
this.textElement = this.element.querySelector('.js-indicator-text');

View File

@@ -1,2 +0,0 @@
export default class ConflictError extends Error {
}

View File

@@ -129,7 +129,9 @@ class MutableDomainObject {
mutable.$observe('$_synchronize_model', (updatedObject) => {
let clone = JSON.parse(JSON.stringify(updatedObject));
utils.refresh(mutable, clone);
let deleted = _.difference(Object.keys(mutable), Object.keys(updatedObject));
deleted.forEach((propertyName) => delete mutable[propertyName]);
Object.assign(mutable, clone);
});
return mutable;

View File

@@ -26,8 +26,6 @@ import RootRegistry from './RootRegistry';
import RootObjectProvider from './RootObjectProvider';
import EventEmitter from 'EventEmitter';
import InterceptorRegistry from './InterceptorRegistry';
import Transaction from './Transaction';
import ConflictError from './ConflictError';
/**
* Utilities for loading, saving, and manipulating domain objects.
@@ -36,13 +34,12 @@ import ConflictError from './ConflictError';
*/
function ObjectAPI(typeRegistry, openmct) {
this.openmct = openmct;
this.typeRegistry = typeRegistry;
this.eventEmitter = new EventEmitter();
this.providers = {};
this.rootRegistry = new RootRegistry();
this.injectIdentifierService = function () {
this.identifierService = this.openmct.$injector.get("identifierService");
this.identifierService = openmct.$injector.get("identifierService");
};
this.rootProvider = new RootObjectProvider(this.rootRegistry);
@@ -50,10 +47,6 @@ function ObjectAPI(typeRegistry, openmct) {
this.interceptorRegistry = new InterceptorRegistry();
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan'];
this.errors = {
Conflict: ConflictError
};
}
/**
@@ -93,14 +86,6 @@ ObjectAPI.prototype.getProvider = function (identifier) {
return this.providers[namespace] || this.fallbackProvider;
};
/**
* Get an active transaction instance
* @returns {Transaction} a transaction object
*/
ObjectAPI.prototype.getActiveTransaction = function () {
return this.transaction;
};
/**
* Get the root-level object.
* @returns {Promise.<DomainObject>} a promise for the root object
@@ -196,22 +181,7 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) {
let objectPromise = provider.get(identifier, abortSignal).then(result => {
delete this.cache[keystring];
result = this.applyGetInterceptors(identifier, result);
if (result.isMutable) {
result.$refresh(result);
} else {
let mutableDomainObject = this._toMutable(result);
mutableDomainObject.$refresh(result);
}
return result;
}).catch((result) => {
console.warn(`Failed to retrieve ${keystring}:`, result);
delete this.cache[keystring];
result = this.applyGetInterceptors(identifier);
return result;
});
@@ -315,7 +285,6 @@ ObjectAPI.prototype.isPersistable = function (idOrKeyString) {
ObjectAPI.prototype.save = function (domainObject) {
let provider = this.getProvider(domainObject.identifier);
let savedResolve;
let savedReject;
let result;
if (!this.isPersistable(domainObject.identifier)) {
@@ -325,22 +294,14 @@ ObjectAPI.prototype.save = function (domainObject) {
} else {
const persistedTime = Date.now();
if (domainObject.persisted === undefined) {
result = new Promise((resolve, reject) => {
result = new Promise((resolve) => {
savedResolve = resolve;
savedReject = reject;
});
domainObject.persisted = persistedTime;
const newObjectPromise = provider.create(domainObject);
if (newObjectPromise) {
newObjectPromise.then(response => {
this.mutate(domainObject, 'persisted', persistedTime);
savedResolve(response);
}).catch((error) => {
savedReject(error);
});
} else {
result = Promise.reject(`[ObjectAPI][save] Object provider returned ${newObjectPromise} when creating new object.`);
}
provider.create(domainObject).then((response) => {
this.mutate(domainObject, 'persisted', persistedTime);
savedResolve(response);
});
} else {
domainObject.persisted = persistedTime;
this.mutate(domainObject, 'persisted', persistedTime);
@@ -351,24 +312,6 @@ ObjectAPI.prototype.save = function (domainObject) {
return result;
};
/**
* After entering into edit mode, creates a new instance of Transaction to keep track of changes in Objects
*/
ObjectAPI.prototype.startTransaction = function () {
if (this.isTransactionActive()) {
throw new Error("Unable to start new Transaction: Previous Transaction is active");
}
this.transaction = new Transaction(this);
};
/**
* Clear instance of Transaction
*/
ObjectAPI.prototype.endTransaction = function () {
this.transaction = null;
};
/**
* Add a root-level object.
* @param {module:openmct.ObjectAPI~Identifier|function} an array of
@@ -415,20 +358,6 @@ ObjectAPI.prototype.applyGetInterceptors = function (identifier, domainObject) {
return domainObject;
};
/**
* Return relative url path from a given object path
* eg: #/browse/mine/cb56f6bf-c900-43b7-b923-2e3b64b412db/6e89e858-77ce-46e4-a1ad-749240286497/....
* @param {Array} objectPath
* @returns {string} relative url for object
*/
ObjectAPI.prototype.getRelativePath = function (objectPath) {
return objectPath
.map(p => this.makeKeyString(p.identifier))
.reverse()
.join('/')
;
};
/**
* Modify a domain object.
* @param {module:openmct.DomainObject} object the object to mutate
@@ -458,12 +387,6 @@ ObjectAPI.prototype.mutate = function (domainObject, path, value) {
//Destroy temporary mutable object
this.destroyMutable(mutableDomainObject);
}
if (this.isTransactionActive()) {
this.transaction.add(domainObject);
} else {
this.save(domainObject);
}
};
/**
@@ -500,23 +423,6 @@ ObjectAPI.prototype._toMutable = function (object) {
return mutableObject;
};
/**
* Updates a domain object based on its latest persisted state. Note that this will mutate the provided object.
* @param {module:openmct.DomainObject} domainObject an object to refresh from its persistence store
* @returns {Promise} the provided object, updated to reflect the latest persisted state of the object.
*/
ObjectAPI.prototype.refresh = async function (domainObject) {
const refreshedObject = await this.get(domainObject.identifier);
if (domainObject.isMutable) {
domainObject.$refresh(refreshedObject);
} else {
utils.refresh(domainObject, refreshedObject);
}
return domainObject;
};
/**
* @param module:openmct.ObjectAPI~Identifier identifier An object identifier
* @returns {boolean} true if the object can be mutated, otherwise returns false
@@ -593,10 +499,6 @@ ObjectAPI.prototype.isObjectPathToALink = function (domainObject, objectPath) {
&& domainObject.location !== this.makeKeyString(objectPath[1].identifier);
};
ObjectAPI.prototype.isTransactionActive = function () {
return Boolean(this.transaction && this.openmct.editor.isEditing());
};
/**
* Uniquely identifies a domain object.
*

View File

@@ -26,10 +26,6 @@ describe("The Object API", () => {
openmct.$injector.get.and.returnValue(mockIdentifierService);
objectAPI = new ObjectAPI(typeRegistry, openmct);
openmct.editor = {};
openmct.editor.isEditing = () => false;
mockDomainObject = {
identifier: {
namespace: TEST_NAMESPACE,
@@ -227,28 +223,6 @@ describe("The Object API", () => {
expect(testObject.name).toBe(MUTATED_NAME);
});
it('Provides a way of refreshing an object from the persistence store', () => {
const modifiedTestObject = JSON.parse(JSON.stringify(testObject));
const OTHER_ATTRIBUTE_VALUE = 'Modified value';
const NEW_ATTRIBUTE_VALUE = 'A new attribute';
modifiedTestObject.otherAttribute = OTHER_ATTRIBUTE_VALUE;
modifiedTestObject.newAttribute = NEW_ATTRIBUTE_VALUE;
delete modifiedTestObject.objectAttribute;
spyOn(objectAPI, 'get');
objectAPI.get.and.returnValue(Promise.resolve(modifiedTestObject));
expect(objectAPI.get).not.toHaveBeenCalled();
return objectAPI.refresh(testObject).then(() => {
expect(objectAPI.get).toHaveBeenCalledWith(testObject.identifier);
expect(testObject.otherAttribute).toEqual(OTHER_ATTRIBUTE_VALUE);
expect(testObject.newAttribute).toEqual(NEW_ATTRIBUTE_VALUE);
expect(testObject.objectAttribute).not.toBeDefined();
});
});
describe ('uses a MutableDomainObject', () => {
it('and retains properties of original object ', function () {
expect(hasOwnProperty(mutable, 'identifier')).toBe(true);

View File

@@ -165,19 +165,12 @@ define([
return identifierEquals(a.identifier, b.identifier);
}
function refresh(oldObject, newObject) {
let deleted = _.difference(Object.keys(oldObject), Object.keys(newObject));
deleted.forEach((propertyName) => delete oldObject[propertyName]);
Object.assign(oldObject, newObject);
}
return {
toOldFormat: toOldFormat,
toNewFormat: toNewFormat,
makeKeyString: makeKeyString,
parseKeyString: parseKeyString,
equals: objectEquals,
identifierEquals: identifierEquals,
refresh: refresh
identifierEquals: identifierEquals
};
});

View File

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

View File

@@ -30,10 +30,7 @@ class OverlayAPI {
*/
showOverlay(overlay) {
if (this.activeOverlays.length) {
const previousOverlay = this.activeOverlays[this.activeOverlays.length - 1];
if (previousOverlay.autoHide) {
previousOverlay.container.classList.add('invisible');
}
this.activeOverlays[this.activeOverlays.length - 1].container.classList.add('invisible');
}
this.activeOverlays.push(overlay);

View File

@@ -1,28 +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.
*****************************************************************************/
const PRIORITIES = Object.freeze({
HIGH: 1000,
DEFAULT: 0,
LOW: -1000
});
export default PRIORITIES;

View File

@@ -180,6 +180,12 @@ define([
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/
TelemetryAPI.prototype.canProvideTelemetry = function (domainObject) {
console.warn(
'DEPRECATION WARNING: openmct.telemetry.canProvideTelemetry '
+ 'will not be supported in future versions of Open MCT. Please '
+ 'use openmct.telemetry.isTelemetryObject instead.'
);
return Boolean(this.findSubscriptionProvider(domainObject))
|| Boolean(this.findRequestProvider(domainObject));
};
@@ -477,10 +483,6 @@ define([
* @returns {Object<String, {TelemetryValueFormatter}>}
*/
TelemetryAPI.prototype.getFormatMap = function (metadata) {
if (!metadata) {
return {};
}
if (!this.formatMapCache.has(metadata)) {
const formatMap = metadata.values().reduce(function (map, valueMetadata) {
map[valueMetadata.key] = this.getValueFormatter(valueMetadata);

View File

@@ -49,6 +49,7 @@ export class TelemetryCollection extends EventEmitter {
this.parseTime = undefined;
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
this.unsubscribe = undefined;
this.historicalProvider = undefined;
this.options = options;
this.pageState = undefined;
this.lastBounds = undefined;
@@ -64,13 +65,13 @@ export class TelemetryCollection extends EventEmitter {
this._error(ERRORS.LOADED);
}
this._setTimeSystem(this.openmct.time.timeSystem());
this._timeSystem(this.openmct.time.timeSystem());
this.lastBounds = this.openmct.time.bounds();
this._watchBounds();
this._watchTimeSystem();
this._requestHistoricalTelemetry();
this._initiateHistoricalRequests();
this._initiateSubscriptionTelemetry();
this.loaded = true;
@@ -102,49 +103,43 @@ export class TelemetryCollection extends EventEmitter {
return this.boundedTelemetry;
}
/**
* Sets up the telemetry collection for historical requests,
* this uses the "standardizeRequestOptions" from Telemetry API
* @private
*/
_initiateHistoricalRequests() {
this.openmct.telemetry.standardizeRequestOptions(this.options);
this.historicalProvider = this.openmct.telemetry.
findRequestProvider(this.domainObject, this.options);
this._requestHistoricalTelemetry();
}
/**
* If a historical provider exists, then historical requests will be made
* @private
*/
async _requestHistoricalTelemetry() {
let options = { ...this.options };
let historicalProvider;
this.openmct.telemetry.standardizeRequestOptions(options);
historicalProvider = this.openmct.telemetry.
findRequestProvider(this.domainObject, options);
if (!historicalProvider) {
if (!this.historicalProvider) {
return;
}
let historicalData;
options.onPartialResponse = this._processNewTelemetry.bind(this);
try {
if (this.requestAbort) {
this.requestAbort.abort();
}
this.requestAbort = new AbortController();
options.signal = this.requestAbort.signal;
this.emit('requestStarted');
historicalData = await historicalProvider.request(this.domainObject, options);
this.options.signal = this.requestAbort.signal;
historicalData = await this.historicalProvider.request(this.domainObject, this.options);
this.requestAbort = undefined;
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Error requesting telemetry data...');
this._error(error);
}
console.error('Error requesting telemetry data...');
this.requestAbort = undefined;
this._error(error);
}
this.emit('requestEnded');
this.requestAbort = undefined;
this._processNewTelemetry(historicalData);
}
/**
* This uses the built in subscription function from Telemetry API
* @private
@@ -172,10 +167,6 @@ export class TelemetryCollection extends EventEmitter {
* @private
*/
_processNewTelemetry(telemetryData) {
if (telemetryData === undefined) {
return;
}
let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData];
let parsedValue;
let beforeStartOfBounds;
@@ -202,10 +193,9 @@ export class TelemetryCollection extends EventEmitter {
if (endIndex > startIndex) {
let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex);
isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, datum));
}
} else if (startIndex === this.boundedTelemetry.length) {
isDuplicate = _.isEqual(datum, this.boundedTelemetry[this.boundedTelemetry.length - 1]);
}
if (!isDuplicate) {
@@ -321,7 +311,7 @@ export class TelemetryCollection extends EventEmitter {
* Time System
* @private
*/
_setTimeSystem(timeSystem) {
_timeSystem(timeSystem) {
let domains = this.metadata.valuesForHints(['domain']);
let domain = domains.find((d) => d.key === timeSystem.key);
@@ -337,10 +327,7 @@ export class TelemetryCollection extends EventEmitter {
this.parseTime = (datum) => {
return valueFormatter.parse(datum);
};
}
_setTimeSystemAndFetchData(timeSystem) {
this._setTimeSystem(timeSystem);
this._reset();
}
@@ -355,8 +342,6 @@ export class TelemetryCollection extends EventEmitter {
this.boundedTelemetry = [];
this.futureBuffer = [];
this.emit('clear');
this._requestHistoricalTelemetry();
}
@@ -377,19 +362,19 @@ export class TelemetryCollection extends EventEmitter {
}
/**
* adds the _setTimeSystemAndFetchData callback to the 'timeSystem' timeAPI listener
* adds the _timeSystem callback to the 'timeSystem' timeAPI listener
* @private
*/
_watchTimeSystem() {
this.openmct.time.on('timeSystem', this._setTimeSystemAndFetchData, this);
this.openmct.time.on('timeSystem', this._timeSystem, this);
}
/**
* removes the _setTimeSystemAndFetchData callback from the 'timeSystem' timeAPI listener
* removes the _timeSystem callback from the 'timeSystem' timeAPI listener
* @private
*/
_unwatchTimeSystem() {
this.openmct.time.off('timeSystem', this._setTimeSystemAndFetchData, this);
this.openmct.time.off('timeSystem', this._timeSystem, this);
}
/**

View File

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

View File

@@ -1,106 +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 TimeContext from "./TimeContext";
/**
* The GlobalContext handles getting and setting time of the openmct application in general.
* Views will use this context unless they specify an alternate/independent time context
*/
class GlobalTimeContext extends TimeContext {
constructor() {
super();
//The Time Of Interest
this.toi = undefined;
}
/**
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
bounds(newBounds) {
if (arguments.length > 0) {
super.bounds.call(this, ...arguments);
// If a bounds change results in a TOI outside of the current
// bounds, unset it
if (this.toi < newBounds.start || this.toi > newBounds.end) {
this.timeOfInterest(undefined);
}
}
//Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal));
}
/**
* Update bounds based on provided time and current offsets
* @private
* @param {number} timestamp A time from which bounds will be calculated
* using current offsets.
*/
tick(timestamp) {
super.tick.call(this, ...arguments);
// If a bounds change results in a TOI outside of the current
// bounds, unset it
if (this.toi < this.boundsVal.start || this.toi > this.boundsVal.end) {
this.timeOfInterest(undefined);
}
}
/**
* Get or set the Time of Interest. The Time of Interest is a single point
* in time, and constitutes the temporal focus of application views. It can
* be manipulated by the user from the time conductor or from other views.
* The time of interest can effectively be unset by assigning a value of
* 'undefined'.
* @fires module:openmct.TimeAPI~timeOfInterest
* @param newTOI
* @returns {number} the current time of interest
* @memberof module:openmct.TimeAPI#
* @method timeOfInterest
*/
timeOfInterest(newTOI) {
if (arguments.length > 0) {
this.toi = newTOI;
/**
* The Time of Interest has moved.
* @event timeOfInterest
* @memberof module:openmct.TimeAPI~
* @property {number} Current time of interest
*/
this.emit('timeOfInterest', this.toi);
}
return this.toi;
}
}
export default GlobalTimeContext;

View File

@@ -1,94 +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 TimeContext from "./TimeContext";
/**
* The IndependentTimeContext handles getting and setting time of the openmct application in general.
* Views will use the GlobalTimeContext unless they specify an alternate/independent time context here.
*/
class IndependentTimeContext extends TimeContext {
constructor(globalTimeContext, key) {
super();
this.key = key;
this.globalTimeContext = globalTimeContext;
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock
* can be unset by calling {@link stopClock}.
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds. This maintains a sliding time window of a fixed
* width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
clock(keyOrClock, offsets) {
if (arguments.length === 2) {
let clock;
if (typeof keyOrClock === 'string') {
clock = this.globalTimeContext.clocks.get(keyOrClock);
if (clock === undefined) {
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.globalTimeContext.clocks.has(clock.key)) {
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
}
}
const previousClock = this.activeClock;
if (previousClock !== undefined) {
previousClock.off("tick", this.tick);
}
this.activeClock = clock;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit("clock", this.activeClock);
if (this.activeClock !== undefined) {
this.clockOffsets(offsets);
this.activeClock.on("tick", this.tick);
}
} else if (arguments.length === 1) {
throw "When setting the clock, clock offsets must also be provided";
}
return this.activeClock;
}
}
export default IndependentTimeContext;

View File

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

View File

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

View File

@@ -1,360 +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 EventEmitter from 'EventEmitter';
class TimeContext extends EventEmitter {
constructor() {
super();
//The Time System
this.timeSystems = new Map();
this.system = undefined;
this.clocks = new Map();
this.boundsVal = {
start: undefined,
end: undefined
};
this.activeClock = undefined;
this.offsets = undefined;
this.tick = this.tick.bind(this);
}
/**
* Get or set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystem
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method timeSystem
*/
timeSystem(timeSystemOrKey, bounds) {
if (arguments.length >= 1) {
if (arguments.length === 1 && !this.activeClock) {
throw new Error(
"Must specify bounds when changing time system without "
+ "an active clock."
);
}
let timeSystem;
if (timeSystemOrKey === undefined) {
throw "Please provide a time system";
}
if (typeof timeSystemOrKey === 'string') {
timeSystem = this.timeSystems.get(timeSystemOrKey);
if (timeSystem === undefined) {
throw "Unknown time system " + timeSystemOrKey + ". Has it been registered with 'addTimeSystem'?";
}
} else if (typeof timeSystemOrKey === 'object') {
timeSystem = timeSystemOrKey;
if (!this.timeSystems.has(timeSystem.key)) {
throw "Unknown time system " + timeSystem.key + ". Has it been registered with 'addTimeSystem'?";
}
} else {
throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key";
}
this.system = timeSystem;
/**
* The time system used by the time
* conductor has changed. A change in Time System will always be
* followed by a bounds event specifying new query bounds.
*
* @event module:openmct.TimeAPI~timeSystem
* @property {TimeSystem} The value of the currently applied
* Time System
* */
this.emit('timeSystem', this.system);
if (bounds) {
this.bounds(bounds);
}
}
return this.system;
}
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {object} ValidationResult
* @property {boolean} valid Result of the validation - true or false.
* @property {string} message An error message if valid is false.
*/
/**
* Validate the given bounds. This can be used for pre-validation of bounds,
* for example by views validating user inputs.
* @param {TimeBounds} bounds The start and end time of the conductor.
* @returns {ValidationResult} A validation error, or true if valid
* @memberof module:openmct.TimeAPI#
* @method validateBounds
*/
validateBounds(bounds) {
if ((bounds.start === undefined)
|| (bounds.end === undefined)
|| isNaN(bounds.start)
|| isNaN(bounds.end)
) {
return {
valid: false,
message: "Start and end must be specified as integer values"
};
} else if (bounds.start > bounds.end) {
return {
valid: false,
message: "Specified start date exceeds end bound"
};
}
return {
valid: true,
message: ''
};
}
/**
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
bounds(newBounds) {
if (arguments.length > 0) {
const validationResult = this.validateBounds(newBounds);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
//Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
/**
* The start time, end time, or both have been updated.
* @event bounds
* @memberof module:openmct.TimeAPI~
* @property {TimeConductorBounds} bounds The newly updated bounds
* @property {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (ie. was an automatic update), false otherwise.
*/
this.emit('bounds', this.boundsVal, false);
}
//Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal));
}
/**
* Validate the given offsets. This can be used for pre-validation of
* offsets, for example by views validating user inputs.
* @param {ClockOffsets} offsets The start and end offsets from a 'now' value.
* @returns { ValidationResult } A validation error, and true/false if valid or not
* @memberof module:openmct.TimeAPI#
* @method validateOffsets
*/
validateOffsets(offsets) {
if ((offsets.start === undefined)
|| (offsets.end === undefined)
|| isNaN(offsets.start)
|| isNaN(offsets.end)
) {
return {
valid: false,
message: "Start and end offsets must be specified as integer values"
};
} else if (offsets.start >= offsets.end) {
return {
valid: false,
message: "Specified start offset must be < end offset"
};
}
return {
valid: true,
message: ''
};
}
/**
* @typedef {Object} TimeBounds
* @property {number} start The start time displayed by the time conductor
* in ms since epoch. Epoch determined by currently active time system
* @property {number} end The end time displayed by the time conductor in ms
* since epoch.
* @memberof module:openmct.TimeAPI~
*/
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {object} ClockOffsets
* @property {number} start A time span relative to the current value of the
* ticking clock, from which start bounds will be calculated. This value must
* be < 0. When a clock is active, bounds will be calculated automatically
* based on the value provided by the clock, and the defined clock offsets.
* @property {number} end A time span relative to the current value of the
* ticking clock, from which end bounds will be calculated. This value must
* be >= 0.
*/
/**
* Get or set the currently applied clock offsets. If no parameter is provided,
* the current value will be returned. If provided, the new value will be
* used as the new clock offsets.
* @param {ClockOffsets} offsets
* @returns {ClockOffsets}
*/
clockOffsets(offsets) {
if (arguments.length > 0) {
const validationResult = this.validateOffsets(offsets);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
this.offsets = offsets;
const currentValue = this.activeClock.currentValue();
const newBounds = {
start: currentValue + offsets.start,
end: currentValue + offsets.end
};
this.bounds(newBounds);
/**
* Event that is triggered when clock offsets change.
* @event clockOffsets
* @memberof module:openmct.TimeAPI~
* @property {ClockOffsets} clockOffsets The newly activated clock
* offsets.
*/
this.emit("clockOffsets", offsets);
}
return this.offsets;
}
/**
* Stop the currently active clock from ticking, and unset it. This will
* revert all views to showing a static time frame defined by the current
* bounds.
*/
stopClock() {
if (this.activeClock) {
this.clock(undefined, undefined);
}
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock
* can be unset by calling {@link stopClock}.
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds. This maintains a sliding time window of a fixed
* width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
clock(keyOrClock, offsets) {
if (arguments.length === 2) {
let clock;
if (typeof keyOrClock === 'string') {
clock = this.clocks.get(keyOrClock);
if (clock === undefined) {
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.clocks.has(clock.key)) {
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
}
}
const previousClock = this.activeClock;
if (previousClock !== undefined) {
previousClock.off("tick", this.tick);
}
this.activeClock = clock;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit("clock", this.activeClock);
if (this.activeClock !== undefined) {
this.clockOffsets(offsets);
this.activeClock.on("tick", this.tick);
}
} else if (arguments.length === 1) {
throw "When setting the clock, clock offsets must also be provided";
}
return this.activeClock;
}
/**
* Update bounds based on provided time and current offsets
* @param {number} timestamp A time from which bounds will be calculated
* using current offsets.
*/
tick(timestamp) {
if (!this.activeClock) {
return;
}
const newBounds = {
start: timestamp + this.offsets.start,
end: timestamp + this.offsets.end
};
this.boundsVal = newBounds;
this.emit('bounds', this.boundsVal, true);
}
}
export default TimeContext;

View File

@@ -1,155 +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 TimeAPI from "./TimeAPI";
import {createOpenMct} from "utils/testing";
describe("The Independent Time API", function () {
let api;
let domainObjectKey;
let clockKey;
let clock;
let bounds;
let independentBounds;
let eventListener;
let openmct;
beforeEach(function () {
openmct = createOpenMct();
api = new TimeAPI(openmct);
clockKey = "someClockKey";
clock = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
clock.currentValue.and.returnValue(100);
clock.key = clockKey;
api.addClock(clock);
domainObjectKey = 'test-key';
bounds = {
start: 0,
end: 1
};
api.bounds(bounds);
independentBounds = {
start: 10,
end: 11
};
eventListener = jasmine.createSpy("eventListener");
});
it("Creates an independent time context", () => {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getIndependentContext(domainObjectKey);
expect(timeContext.bounds()).toEqual(independentBounds);
destroyTimeContext();
});
it("Gets an independent time context given the objectPath", () => {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{
identifier: {
namespace: '',
key: 'blah'
}
}, { identifier: domainObjectKey }]);
expect(timeContext.bounds()).toEqual(independentBounds);
destroyTimeContext();
});
it("defaults to the global time context given the objectPath", () => {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{
identifier: {
namespace: '',
key: 'blah'
}
}]);
expect(timeContext.bounds()).toEqual(bounds);
destroyTimeContext();
});
it("Allows setting of valid bounds", function () {
bounds = {
start: 0,
end: 1
};
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(timeContext.bounds()).not.toEqual(bounds);
timeContext.bounds(bounds);
expect(timeContext.bounds()).toEqual(bounds);
destroyTimeContext();
});
it("Disallows setting of invalid bounds", function () {
bounds = {
start: 1,
end: 0
};
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(timeContext.bounds()).not.toBe(bounds);
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
expect(timeContext.bounds()).not.toEqual(bounds);
bounds = {start: 1};
expect(timeContext.bounds()).not.toEqual(bounds);
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
expect(timeContext.bounds()).not.toEqual(bounds);
destroyTimeContext();
});
it("Emits an event when bounds change", function () {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(eventListener).not.toHaveBeenCalled();
timeContext.on('bounds', eventListener);
timeContext.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
destroyTimeContext();
});
describe(" when using real time clock", function () {
const mockOffsets = {
start: 10,
end: 11
};
it("Emits an event when bounds change based on current value", function () {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(eventListener).not.toHaveBeenCalled();
timeContext.clock('someClockKey', mockOffsets);
timeContext.on('bounds', eventListener);
timeContext.tick(10);
expect(eventListener).toHaveBeenCalledWith({
start: 20,
end: 21
}, true);
destroyTimeContext();
});
});
});

View File

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

View File

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

View File

@@ -41,6 +41,7 @@ const DEFAULTS = [
'platform/forms',
'platform/identity',
'platform/persistence/aggregator',
'platform/persistence/queue',
'platform/policy',
'platform/entanglement',
'platform/search',

View File

@@ -32,7 +32,7 @@ describe('the plugin', function () {
let openmct;
let composition;
beforeEach(() => {
beforeEach((done) => {
openmct = createOpenMct();
@@ -47,6 +47,11 @@ describe('the plugin', function () {
}
}));
openmct.on('start', done);
openmct.startHeadless();
composition = openmct.composition.get({identifier});
spyOn(couchPlugin.couchProvider, 'getObjectsByFilter').and.returnValue(Promise.resolve([
{
identifier: {
@@ -61,19 +66,6 @@ describe('the plugin', function () {
}
}
]));
spyOn(couchPlugin.couchProvider, "get").and.callFake((id) => {
return Promise.resolve({
identifier: id
});
});
return new Promise((resolve) => {
openmct.once('start', resolve);
openmct.startHeadless();
}).then(() => {
composition = openmct.composition.get({identifier});
});
});
afterEach(() => {

View File

@@ -96,11 +96,11 @@ export default {
this.timestampKey = this.openmct.time.timeSystem().key;
this.valueMetadata = this.metadata ? this
this.valueMetadata = this
.metadata
.valuesForHints(['range'])[0] : undefined;
.valuesForHints(['range'])[0];
this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;
this.valueKey = this.valueMetadata.key;
this.unsubscribe = this.openmct
.telemetry
@@ -151,10 +151,7 @@ export default {
size: 1,
strategy: 'latest'
})
.then((array) => this.updateValues(array[array.length - 1]))
.catch((error) => {
console.warn('Error fetching data', error);
});
.then((array) => this.updateValues(array[array.length - 1]));
},
updateBounds(bounds, isTick) {
this.bounds = bounds;

View File

@@ -73,9 +73,8 @@ export default {
hasUnits() {
let itemsWithUnits = this.items.filter((item) => {
let metadata = this.openmct.telemetry.getMetadata(item.domainObject);
const valueMetadatas = metadata ? metadata.valueMetadatas : [];
return this.metadataHasUnits(valueMetadatas);
return this.metadataHasUnits(metadata.valueMetadatas);
});

View File

@@ -1,3 +0,0 @@
export const BAR_GRAPH_VIEW = 'bar-graph.view';
export const BAR_GRAPH_KEY = 'telemetry.plot.bar-graph';
export const BAR_GRAPH_INSPECTOR_KEY = 'telemetry.plot.bar-graph.inspector';

View File

@@ -1,289 +0,0 @@
<template>
<div ref="plotWrapper"
class="has-local-controls"
:class="{ 's-unsynced' : isZoomed }"
>
<div v-if="isZoomed"
class="l-state-indicators"
>
<span class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle"
title="This plot is not currently displaying the latest data. Reset pan/zoom to view latest data."
></span>
</div>
<div ref="plot"
class="c-bar-chart"
@plotly_relayout="zoom"
></div>
<div v-if="false"
ref="localControl"
class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover"
>
<button v-if="data.length"
class="c-button icon-reset"
:disabled="!isZoomed"
title="Reset pan/zoom"
@click="reset()"
>
</button>
</div>
</div>
</template>
<script>
import Plotly from 'plotly-basic';
const MULTI_AXES_X_PADDING_PERCENT = {
LEFT: 8,
RIGHT: 94
};
export default {
inject: ['openmct', 'domainObject'],
props: {
data: {
type: Array,
default() {
return [];
}
},
plotAxisTitle: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
isZoomed: false,
primaryYAxisRange: {
min: '',
max: ''
},
xAxisRange: {
min: '',
max: ''
}
};
},
watch: {
data: {
immediate: false,
handler: 'updateData'
}
},
mounted() {
Plotly.newPlot(this.$refs.plot, Array.from(this.data), this.getLayout(), {
responsive: true,
displayModeBar: false
});
this.registerListeners();
},
beforeDestroy() {
if (this.plotResizeObserver) {
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
clearTimeout(this.resizeTimer);
}
if (this.removeBarColorListener) {
this.removeBarColorListener();
}
},
methods: {
getAxisMinMax(axis) {
const min = axis.autoSize
? ''
: axis.min;
const max = axis.autoSize
? ''
: axis.max;
return {
min,
max
};
},
getLayout() {
const yAxesMeta = this.getYAxisMeta();
const primaryYaxis = this.getYaxisLayout(yAxesMeta['1']);
const xAxisDomain = this.getXAxisDomain(yAxesMeta);
return {
autosize: true,
showlegend: false,
textposition: 'auto',
font: {
family: 'Helvetica Neue, Helvetica, Arial, sans-serif',
size: '12px',
color: '#666'
},
xaxis: {
domain: xAxisDomain,
range: [this.xAxisRange.min, this.xAxisRange.max],
title: this.plotAxisTitle.xAxisTitle,
automargin: true,
fixedrange: true
},
yaxis: primaryYaxis,
margin: {
l: 5,
r: 5,
t: 5,
b: 0
},
paper_bgcolor: 'transparent',
plot_bgcolor: 'transparent'
};
},
getYAxisMeta() {
const yAxisMeta = {};
this.data.forEach(datum => {
const yAxisMetadata = datum.yAxisMetadata;
const range = '1';
const side = 'left';
const name = '';
const unit = yAxisMetadata.units;
yAxisMeta[range] = {
range,
side,
name,
unit
};
});
return yAxisMeta;
},
getXAxisDomain(yAxisMeta) {
let leftPaddingPerc = 0;
let rightPaddingPerc = 100;
let rightSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'right'));
let leftSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'left'));
if (yAxisMeta && rightSide.length > 1) {
rightPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.RIGHT;
}
if (yAxisMeta && leftSide.length > 1) {
leftPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.LEFT;
}
return [leftPaddingPerc / 100, rightPaddingPerc / 100];
},
getYaxisLayout(yAxisMeta) {
if (!yAxisMeta) {
return {};
}
const { name, range, side = 'left', unit } = yAxisMeta;
const title = `${name} ${unit ? '(' + unit + ')' : ''}`;
const yaxis = {
automargin: true,
fixedrange: true,
title
};
let yAxistype = this.primaryYAxisRange;
if (range === '1') {
yaxis.range = [yAxistype.min, yAxistype.max];
return yaxis;
}
yaxis.range = [yAxistype.min, yAxistype.max];
yaxis.anchor = side.toLowerCase() === 'left'
? 'free'
: 'x';
yaxis.showline = side.toLowerCase() === 'left';
yaxis.side = side.toLowerCase();
yaxis.overlaying = 'y';
yaxis.position = 0.01;
return yaxis;
},
registerListeners() {
this.removeBarColorListener = this.openmct.objects.observe(
this.domainObject,
'configuration.barStyles',
this.barColorChanged
);
this.resizeTimer = false;
if (window.ResizeObserver) {
this.plotResizeObserver = new ResizeObserver(() => {
// debounce and trigger window resize so that plotly can resize the plot
clearTimeout(this.resizeTimer);
this.resizeTimer = setTimeout(() => {
window.dispatchEvent(new Event('resize'));
}, 250);
});
this.plotResizeObserver.observe(this.$refs.plotWrapper);
}
},
reset() {
this.updatePlot();
this.isZoomed = false;
this.$emit('subscribe');
},
barColorChanged() {
const colors = [];
const indices = [];
this.data.forEach((item, index) => {
const key = item.key;
const colorExists = this.domainObject.configuration.barStyles.series[key] && this.domainObject.configuration.barStyles.series[key].color;
indices.push(index);
if (colorExists) {
colors.push(this.domainObject.configuration.barStyles.series[key].color);
} else {
colors.push(item.marker.color);
}
});
const plotUpdate = {
'marker.color': colors
};
Plotly.restyle(this.$refs.plot, plotUpdate, indices);
},
updateData() {
this.updatePlot();
},
updateLocalControlPosition() {
const localControl = this.$refs.localControl;
localControl.style.display = 'none';
const plot = this.$refs.plot;
const bgLayer = this.$el.querySelector('.bglayer');
const plotBoundingRect = plot.getBoundingClientRect();
const bgLayerBoundingRect = bgLayer.getBoundingClientRect();
const top = bgLayerBoundingRect.top - plotBoundingRect.top + 5;
const left = bgLayerBoundingRect.left - plotBoundingRect.left + 5;
localControl.style.top = `${top}px`;
localControl.style.left = `${left}px`;
localControl.style.display = 'block';
},
updatePlot() {
if (!this.$refs || !this.$refs.plot) {
return;
}
Plotly.react(this.$refs.plot, Array.from(this.data), this.getLayout());
},
zoom(eventData) {
const autorange = eventData['xaxis.autorange'];
const { autosize } = eventData;
if (autosize || autorange) {
this.isZoomed = false;
this.reset();
return;
}
this.isZoomed = true;
this.$emit('unsubscribe');
}
}
};
</script>

View File

@@ -1,304 +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.
-->
<template>
<BarGraph ref="barGraph"
class="c-plot c-bar-chart-view"
:data="trace"
:plot-axis-title="plotAxisTitle"
@subscribe="subscribeToAll"
@unsubscribe="removeAllSubscriptions"
/>
</template>
<script>
import BarGraph from './BarGraphPlot.vue';
import _ from 'lodash';
export default {
components: {
BarGraph
},
inject: ['openmct', 'domainObject', 'path'],
data() {
this.telemetryObjects = {};
this.telemetryObjectFormats = {};
this.subscriptions = [];
this.composition = {};
return {
trace: []
};
},
computed: {
plotAxisTitle() {
const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {};
const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : '';
const yAxisUnit = yAxisMetadata.units ? `(${yAxisMetadata.units})` : '';
return {
xAxisTitle: `${xAxisMetadata.name || ''} ${xAxisUnit}`,
yAxisTitle: `${yAxisMetadata.name || ''} ${yAxisUnit}`
};
}
},
mounted() {
this.loadComposition();
this.openmct.time.on('bounds', this.refreshData);
},
beforeDestroy() {
this.openmct.time.off('bounds', this.refreshData);
this.removeAllSubscriptions();
if (!this.composition) {
return;
}
this.composition.off('add', this.addTelemetryObject);
this.composition.off('remove', this.removeTelemetryObject);
},
methods: {
addTelemetryObject(telemetryObject) {
// grab information we need from the added telmetry object
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
this.telemetryObjects[key] = telemetryObject;
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
this.telemetryObjectFormats[key] = this.openmct.telemetry.getFormatMap(metadata);
const telemetryObjectPath = [telemetryObject, ...this.path];
const telemetryIsAlias = this.openmct.objects.isObjectPathToALink(telemetryObject, telemetryObjectPath);
// make an update object that's a clone of the existing styles object so we preserve existing choices
let stylesUpdate = {};
if (this.domainObject.configuration.barStyles.series[key]) {
stylesUpdate = _.clone(this.domainObject.configuration.barStyles.series[key]);
}
stylesUpdate.name = telemetryObject.name;
stylesUpdate.type = telemetryObject.type;
stylesUpdate.isAlias = telemetryIsAlias;
// if something has changed, mutate and notify listeners
if (!_.isEqual(stylesUpdate, this.domainObject.configuration.barStyles.series[key])) {
this.openmct.objects.mutate(
this.domainObject,
`configuration.barStyles.series["${key}"]`,
stylesUpdate
);
}
// ask for the current telemetry data, then subcribe for changes
this.requestDataFor(telemetryObject);
this.subscribeToObject(telemetryObject);
},
addTrace(trace, key) {
if (!this.trace.length) {
this.trace = this.trace.concat([trace]);
return;
}
let isInTrace = false;
const newTrace = this.trace.map((currentTrace, index) => {
if (currentTrace.key !== key) {
return currentTrace;
}
isInTrace = true;
return trace;
});
this.trace = isInTrace ? newTrace : newTrace.concat([trace]);
},
getAxisMetadata(telemetryObject) {
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
if (!metadata) {
return {};
}
const yAxisMetadata = metadata.valuesForHints(['range'])[0];
//Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only
const xAxisMetadata = metadata.valuesForHints(['range']);
return {
xAxisMetadata,
yAxisMetadata
};
},
getOptions() {
const { start, end } = this.openmct.time.bounds();
return {
end,
start
};
},
loadComposition() {
this.composition = this.openmct.composition.get(this.domainObject);
if (!this.composition) {
this.addTelemetryObject(this.domainObject);
return;
}
this.composition.on('add', this.addTelemetryObject);
this.composition.on('remove', this.removeTelemetryObject);
this.composition.load();
},
refreshData(bounds, isTick) {
if (!isTick) {
const telemetryObjects = Object.values(this.telemetryObjects);
telemetryObjects.forEach(this.requestDataFor);
}
},
removeAllSubscriptions() {
this.subscriptions.forEach(subscription => subscription.unsubscribe());
this.subscriptions = [];
},
removeSubscription(key) {
const found = this.subscriptions.findIndex(subscription => subscription.key === key);
if (found > -1) {
this.subscriptions[found].unsubscribe();
this.subscriptions.splice(found, 1);
}
},
removeTelemetryObject(identifier) {
const key = this.openmct.objects.makeKeyString(identifier);
delete this.telemetryObjects[key];
if (this.telemetryObjectFormats && this.telemetryObjectFormats[key]) {
delete this.telemetryObjectFormats[key];
}
if (this.domainObject.configuration.barStyles.series[key]) {
delete this.domainObject.configuration.barStyles.series[key];
this.openmct.objects.mutate(
this.domainObject,
`configuration.barStyles.series["${key}"]`,
undefined
);
}
this.removeSubscription(key);
this.trace = this.trace.filter(t => t.key !== key);
},
addDataToGraph(telemetryObject, data, axisMetadata) {
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (data.message) {
this.openmct.notifications.alert(data.message);
}
if (!this.isDataInTimeRange(data, key)) {
return;
}
let xValues = [];
let yValues = [];
//populate X and Y values for plotly
axisMetadata.xAxisMetadata.forEach((metadata) => {
xValues.push(metadata.name);
if (data[metadata.key]) {
const formattedValue = this.format(key, metadata.key, data);
yValues.push(formattedValue);
} else {
yValues.push(null);
}
});
const trace = {
key,
name: telemetryObject.name,
x: xValues,
y: yValues,
text: yValues.map(String),
xAxisMetadata: axisMetadata.xAxisMetadata,
yAxisMetadata: axisMetadata.yAxisMetadata,
type: 'bar',
marker: {
color: this.domainObject.configuration.barStyles.series[key].color
},
hoverinfo: 'skip'
};
this.addTrace(trace, key);
},
isDataInTimeRange(datum, key) {
const timeSystemKey = this.openmct.time.timeSystem().key;
let currentTimestamp = this.parse(key, timeSystemKey, datum);
return currentTimestamp && this.openmct.time.bounds().end >= currentTimestamp;
},
format(telemetryObjectKey, metadataKey, data) {
const formats = this.telemetryObjectFormats[telemetryObjectKey];
return formats[metadataKey].format(data);
},
parse(telemetryObjectKey, metadataKey, datum) {
if (!datum) {
return;
}
const formats = this.telemetryObjectFormats[telemetryObjectKey];
return formats[metadataKey].parse(datum);
},
requestDataFor(telemetryObject) {
const axisMetadata = this.getAxisMetadata(telemetryObject);
this.openmct.telemetry.request(telemetryObject)
.then(data => {
data.forEach((datum) => {
this.addDataToGraph(telemetryObject, datum, axisMetadata);
});
})
.catch((error) => {
console.warn(`Error fetching data`, error);
});
},
subscribeToObject(telemetryObject) {
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
this.removeSubscription(key);
const options = this.getOptions();
const axisMetadata = this.getAxisMetadata(telemetryObject);
const unsubscribe = this.openmct.telemetry.subscribe(telemetryObject,
data => this.addDataToGraph(telemetryObject, data, axisMetadata)
, options);
this.subscriptions.push({
key,
unsubscribe
});
},
subscribeToAll() {
const telemetryObjects = Object.values(this.telemetryObjects);
telemetryObjects.forEach(this.subscribeToObject);
}
}
};
</script>

View File

@@ -1,79 +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 BarGraphView from './BarGraphView.vue';
import { BAR_GRAPH_KEY, BAR_GRAPH_VIEW } from './BarGraphConstants';
import Vue from 'vue';
export default function BarGraphViewProvider(openmct) {
function isCompactView(objectPath) {
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
}
return {
key: BAR_GRAPH_VIEW,
name: 'Bar Graph',
cssClass: 'icon-telemetry',
canView(domainObject, objectPath) {
return domainObject && domainObject.type === BAR_GRAPH_KEY;
},
canEdit(domainObject, objectPath) {
return domainObject && domainObject.type === BAR_GRAPH_KEY;
},
view: function (domainObject, objectPath) {
let component;
return {
show: function (element) {
let isCompact = isCompactView(objectPath);
component = new Vue({
el: element,
components: {
BarGraphView
},
provide: {
openmct,
domainObject,
path: objectPath
},
data() {
return {
options: {
compact: isCompact
}
};
},
template: '<bar-graph-view :options="options"></bar-graph-view>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
}
};
}

View File

@@ -1,48 +0,0 @@
import { BAR_GRAPH_INSPECTOR_KEY, BAR_GRAPH_KEY } from '../BarGraphConstants';
import Vue from 'vue';
import BarGraphOptions from "./BarGraphOptions.vue";
export default function BarGraphInspectorViewProvider(openmct) {
return {
key: BAR_GRAPH_INSPECTOR_KEY,
name: 'Bar Graph Inspector View',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 0) {
return false;
}
let object = selection[0][0].context.item;
return object
&& object.type === BAR_GRAPH_KEY;
},
view: function (selection) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
BarGraphOptions
},
provide: {
openmct,
domainObject: selection[0][0].context.item
},
template: '<bar-graph-options></bar-graph-options>'
});
},
destroy: function () {
if (component) {
component.$destroy();
component = undefined;
}
}
};
},
priority: function () {
return 1;
}
};
}

View File

@@ -1,70 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2020, 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>
<ul class="c-tree c-bar-graph-options">
<h2 title="Display properties for this object">Bar Graph Series</h2>
<li v-for="series in domainObject.composition"
:key="series.key"
>
<series-options :item="series"
:color-palette="colorPalette"
/>
</li>
</ul>
</template>
<script>
import SeriesOptions from "./SeriesOptions.vue";
import ColorPalette from '@/ui/color/ColorPalette';
export default {
components: {
SeriesOptions
},
inject: ['openmct', 'domainObject'],
data() {
return {
isEditing: this.openmct.editor.isEditing(),
colorPalette: this.colorPalette
};
},
computed: {
canEdit() {
return this.isEditing && !this.domainObject.locked;
}
},
beforeMount() {
this.colorPalette = new ColorPalette();
},
mounted() {
this.openmct.editor.on('isEditing', this.setEditState);
},
beforeDestroy() {
this.openmct.editor.off('isEditing', this.setEditState);
},
methods: {
setEditState(isEditing) {
this.isEditing = isEditing;
}
}
};
</script>

View File

@@ -1,153 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2020, 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>
<ul>
<li class="c-tree__item menus-to-left"
:class="aliasCss"
>
<span class="c-disclosure-triangle is-enabled flex-elem"
:class="expandedCssClass"
@click="expanded = !expanded"
>
</span>
<div class="c-object-label">
<div :class="[seriesCss]">
</div>
<div class="c-object-label__name">{{ name }}</div>
</div>
</li>
<ColorSwatch v-if="expanded"
:current-color="currentColor"
title="Manually set the color for this bar graph series."
edit-title="Manually set the color for this bar graph series"
view-title="The color for this bar graph series."
short-label="Color"
class="grid-properties"
@colorSet="setColor"
/>
</ul>
</template>
<script>
import ColorSwatch from '@/ui/color/ColorSwatch.vue';
import Color from "@/ui/color/Color";
export default {
components: {
ColorSwatch
},
inject: ['openmct', 'domainObject'],
props: {
item: {
type: Object,
required: true
},
colorPalette: {
type: Object,
required: true
}
},
data() {
return {
currentColor: undefined,
name: '',
type: '',
isAlias: false,
expanded: false
};
},
computed: {
expandedCssClass() {
return this.expanded ? 'c-disclosure-triangle--expanded' : '';
},
seriesCss() {
const type = this.openmct.types.get(this.type);
if (type && type.definition && type.definition.cssClass) {
return `c-object-label__type-icon ${type.definition.cssClass}`;
}
return 'c-object-label__type-icon';
},
aliasCss() {
let cssClass = '';
if (this.isAlias) {
cssClass = 'is-alias';
}
return cssClass;
}
},
watch: {
item: {
handler() {
this.initColorAndName();
},
deep: true
}
},
mounted() {
this.key = this.openmct.objects.makeKeyString(this.item);
this.initColorAndName();
this.removeBarStylesListener = this.openmct.objects.observe(this.domainObject, `configuration.barStyles.series["${this.key}"]`, this.initColorAndName);
},
beforeDestroy() {
if (this.removeBarStylesListener) {
this.removeBarStylesListener();
}
},
methods: {
initColorAndName() {
// this is called before the plot is initialized
if (!this.domainObject.configuration.barStyles.series[this.key]) {
const color = this.colorPalette.getNextColor().asHexString();
this.domainObject.configuration.barStyles.series[this.key] = {
color,
type: '',
name: '',
isAlias: false
};
} else if (!this.domainObject.configuration.barStyles.series[this.key].color) {
this.domainObject.configuration.barStyles.series[this.key].color = this.colorPalette.getNextColor().asHexString();
}
this.currentColor = this.domainObject.configuration.barStyles.series[this.key].color;
this.name = this.domainObject.configuration.barStyles.series[this.key].name;
this.type = this.domainObject.configuration.barStyles.series[this.key].type;
this.isAlias = this.domainObject.configuration.barStyles.series[this.key].isAlias;
let colorHexString = this.currentColor;
const colorObject = Color.fromHexString(colorHexString);
this.colorPalette.remove(colorObject);
},
setColor(chosenColor) {
this.currentColor = chosenColor.asHexString();
this.openmct.objects.mutate(
this.domainObject,
`configuration.barStyles.series["${this.key}"].color`,
this.currentColor
);
}
}
};
</script>

View File

@@ -1,51 +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 { BAR_GRAPH_KEY } from './BarGraphConstants';
import BarGraphViewProvider from './BarGraphViewProvider';
import BarGraphInspectorViewProvider from './inspector/BarGraphInspectorViewProvider';
import BarGraphCompositionPolicy from './BarGraphCompositionPolicy';
export default function () {
return function install(openmct) {
openmct.types.addType(BAR_GRAPH_KEY, {
key: BAR_GRAPH_KEY,
name: "Bar Graph",
cssClass: "icon-bar-chart",
description: "View data as a bar graph. Can be added to Display Layouts.",
creatable: true,
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {
barStyles: { series: {} }
};
},
priority: 891
});
openmct.objectViews.addProvider(new BarGraphViewProvider(openmct));
openmct.inspectorViews.addProvider(new BarGraphInspectorViewProvider(openmct));
openmct.composition.addPolicy(new BarGraphCompositionPolicy(openmct).allow);
};
}

View File

@@ -1,485 +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 Vue from "vue";
import BarGraphPlugin from "./plugin";
import BarGraph from './BarGraphPlot.vue';
import EventEmitter from "EventEmitter";
import { BAR_GRAPH_VIEW, BAR_GRAPH_KEY } from './BarGraphConstants';
describe("the plugin", function () {
let element;
let child;
let openmct;
let telemetryPromise;
let telemetryPromiseResolve;
let mockObjectPath;
beforeEach((done) => {
mockObjectPath = [
{
name: 'mock folder',
type: 'fake-folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
},
{
name: 'mock parent folder',
type: 'time-strip',
identifier: {
key: 'mock-parent-folder',
namespace: ''
}
}
];
const testTelemetry = [
{
'utc': 1,
'some-key': 'some-value 1',
'some-other-key': 'some-other-value 1'
},
{
'utc': 2,
'some-key': 'some-value 2',
'some-other-key': 'some-other-value 2'
},
{
'utc': 3,
'some-key': 'some-value 3',
'some-other-key': 'some-other-value 3'
}
];
openmct = createOpenMct();
telemetryPromise = new Promise((resolve) => {
telemetryPromiseResolve = resolve;
});
spyOn(openmct.telemetry, 'request').and.callFake(() => {
telemetryPromiseResolve(testTelemetry);
return telemetryPromise;
});
openmct.install(new BarGraphPlugin());
element = document.createElement("div");
element.style.width = "640px";
element.style.height = "480px";
child = document.createElement("div");
child.style.width = "640px";
child.style.height = "480px";
element.appendChild(child);
document.body.appendChild(element);
spyOn(window, 'ResizeObserver').and.returnValue({
observe() {},
unobserve() {},
disconnect() {}
});
openmct.time.timeSystem("utc", {
start: 0,
end: 4
});
openmct.types.addType("test-object", {
creatable: true
});
openmct.on("start", done);
openmct.startHeadless();
});
afterEach((done) => {
openmct.time.timeSystem('utc', {
start: 0,
end: 1
});
resetApplicationState(openmct).then(done).catch(done);
});
describe("The bar graph view", () => {
let testDomainObject;
let barGraphObject;
// eslint-disable-next-line no-unused-vars
let component;
let mockComposition;
beforeEach(async () => {
const getFunc = openmct.$injector.get;
spyOn(openmct.$injector, "get")
.withArgs("exportImageService").and.returnValue({
exportPNG: () => {},
exportJPG: () => {}
})
.and.callFake(getFunc);
barGraphObject = {
identifier: {
namespace: "",
key: "test-plot"
},
type: "telemetry.plot.bar-graph",
name: "Test Bar Graph"
};
testDomainObject = {
identifier: {
namespace: "",
key: "test-object"
},
configuration: {
barStyles: {
series: {}
}
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testDomainObject);
return [testDomainObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
let viewContainer = document.createElement("div");
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
BarGraph
},
provide: {
openmct: openmct,
domainObject: barGraphObject,
composition: openmct.composition.get(barGraphObject)
},
template: "<BarGraph></BarGraph>"
});
await Vue.nextTick();
});
it("provides a bar graph view", () => {
const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);
const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW);
expect(plotViewProvider).toBeDefined();
});
it("Renders plotly bar graph", () => {
let barChartElement = element.querySelectorAll(".plotly");
expect(barChartElement.length).toBe(1);
});
it("Handles dots in telemetry id", () => {
const dotFullTelemetryObject = {
identifier: {
namespace: "someNamespace",
key: "~OpenMCT~outer.test-object.foo.bar"
},
type: "test-dotful-object",
name: "A Dotful Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key.foo.name.45",
name: "Some dotful attribute",
hints: {
range: 1
}
}, {
key: "some-other-key.bar.344.rad",
name: "Another dotful attribute",
hints: {
range: 2
}
}]
}
};
const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);
const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW);
const barGraphView = plotViewProvider.view(testDomainObject, [testDomainObject]);
barGraphView.show(child, true);
expect(testDomainObject.configuration.barStyles.series["test-object"].name).toEqual("Test Object");
mockComposition.emit('add', dotFullTelemetryObject);
expect(testDomainObject.configuration.barStyles.series["someNamespace:~OpenMCT~outer.test-object.foo.bar"].name).toEqual("A Dotful Object");
barGraphView.destroy();
});
});
describe("the bar graph objects", () => {
const mockObject = {
name: 'A very nice bar graph',
key: BAR_GRAPH_KEY,
creatable: true
};
it('defines a bar graph object type with the correct key', () => {
const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition;
expect(objectDef.key).toEqual(mockObject.key);
});
it('is creatable', () => {
const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition;
expect(objectDef.creatable).toEqual(mockObject.creatable);
});
});
describe("The bar graph composition policy", () => {
it("allows composition for telemetry that contain at least one range", () => {
const parent = {
"composition": [],
"configuration": {},
"name": "Some Bar Graph",
"type": "telemetry.plot.bar-graph",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "some-key",
name: "Some attribute",
hints: {
domain: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 1
}
}]
}
};
const composition = openmct.composition.get(parent);
expect(() => {
composition.add(testTelemetryObject);
}).not.toThrow();
expect(parent.composition.length).toBe(1);
});
it("disallows composition for telemetry that don't contain any range hints", () => {
const parent = {
"composition": [],
"configuration": {},
"name": "Some Bar Graph",
"type": "telemetry.plot.bar-graph",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "some-key",
name: "Some attribute"
}, {
key: "some-other-key",
name: "Another attribute"
}]
}
};
const composition = openmct.composition.get(parent);
expect(() => {
composition.add(testTelemetryObject);
}).toThrow();
expect(parent.composition.length).toBe(0);
});
});
describe('the inspector view', () => {
let mockComposition;
let testDomainObject;
let selection;
let plotInspectorView;
let viewContainer;
let optionsElement;
beforeEach(async () => {
testDomainObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
selection = [
[
{
context: {
item: {
id: "test-object",
identifier: {
key: "test-object",
namespace: ''
},
type: "telemetry.plot.bar-graph",
configuration: {
barStyles: {
series: {
'~Some~foo.bar': {
name: 'A telemetry object',
type: 'some-type',
isAlias: true
}
}
}
},
composition: [
{
key: '~Some~foo.bar'
}
]
}
}
},
{
context: {
item: {
type: 'time-strip',
identifier: {
key: 'some-other-key',
namespace: ''
}
}
}
}
]
];
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testDomainObject);
return [testDomainObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
viewContainer = document.createElement('div');
child.append(viewContainer);
const applicableViews = openmct.inspectorViews.get(selection);
plotInspectorView = applicableViews[0];
plotInspectorView.show(viewContainer);
await Vue.nextTick();
optionsElement = element.querySelector('.c-bar-graph-options');
});
afterEach(() => {
plotInspectorView.destroy();
});
it('it renders the options', () => {
expect(optionsElement).toBeDefined();
});
it('shows the name', () => {
const seriesEl = optionsElement.querySelector('.c-object-label__name');
expect(seriesEl.innerHTML).toEqual('A telemetry object');
});
});
});

View File

@@ -1,78 +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.
*****************************************************************************/
function inSelectionPath(openmct, domainObject) {
const domainObjectIdentifier = domainObject.identifier;
return openmct.selection.get().some(selectionPath => {
return selectionPath.some(objectInPath => {
const objectInPathIdentifier = objectInPath.context.item.identifier;
return openmct.objects.areIdsEqual(objectInPathIdentifier, domainObjectIdentifier);
});
});
}
export default class ClearDataAction {
constructor(openmct, appliesToObjects) {
this.name = 'Clear Data for Object';
this.key = 'clear-data-action';
this.description = 'Clears current data for object, unsubscribes and resubscribes to data';
this.cssClass = 'icon-clear-data';
this._openmct = openmct;
this._appliesToObjects = appliesToObjects;
}
invoke(objectPath) {
let domainObject = null;
if (objectPath) {
domainObject = objectPath[0];
}
this._openmct.objectViews.emit('clearData', domainObject);
}
appliesTo(objectPath) {
if (!objectPath) {
return false;
}
const contextualDomainObject = objectPath[0];
// first check to see if this action applies to this sort of object at all
const appliesToThisObject = this._appliesToObjects.some(type => {
return contextualDomainObject.type === type;
});
if (!appliesToThisObject) {
// we've selected something not applicable
return false;
}
const objectInSelectionPath = inSelectionPath(this._openmct, contextualDomainObject);
if (objectInSelectionPath) {
return true;
} else {
// if this it doesn't match up, check to see if we're in a composition (i.e., layout)
const routerPath = this._openmct.router.path[0];
return routerPath.type === 'layout';
}
}
}

View File

@@ -20,24 +20,22 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<li class="c-inspect-properties__row">
<div class="c-inspect-properties__label">
{{ detail.name }}
</div>
<div class="c-inspect-properties__value">
{{ detail.value }}
</div>
</li>
</template>
export default class ClearDataAction {
constructor(openmct, appliesToObjects) {
this.name = 'Clear Data for Object';
this.key = 'clear-data-action';
this.description = 'Clears current data for object, unsubscribes and resubscribes to data';
this.cssClass = 'icon-clear-data';
<script>
export default {
props: {
detail: {
type: Object,
required: true
}
this._openmct = openmct;
this._appliesToObjects = appliesToObjects;
}
};
</script>
invoke(objectPath) {
this._openmct.objectViews.emit('clearData', objectPath[0]);
}
appliesTo(objectPath) {
let contextualDomainObject = objectPath[0];
return this._appliesToObjects.filter(type => contextualDomainObject.type === type).length;
}
}

View File

@@ -22,7 +22,7 @@
define([
'./components/globalClearIndicator.vue',
'./ClearDataAction',
'./clearDataAction',
'vue'
], function (
GlobaClearIndicator,
@@ -47,9 +47,7 @@ define([
});
let indicator = {
element: component.$mount().$el,
key: 'clear-data-indicator',
priority: openmct.priority.DEFAULT
element: component.$mount().$el
};
openmct.indicators.add(indicator);

View File

@@ -1,143 +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 ClearDataActionPlugin from '../plugin.js';
import ClearDataAction from '../ClearDataAction.js';
describe('When the Clear Data Plugin is installed,', () => {
const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']);
const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']);
const mockActionsProvider = jasmine.createSpyObj('actions', ['register']);
const goodMockSelectionPath = [[{
context: {
item: {
identifier: {
key: 'apple',
namespace: ''
}
}
}
}]];
const openmct = {
objectViews: mockObjectViews,
indicators: mockIndicatorProvider,
priority: {
DEFAULT: 0
},
actions: mockActionsProvider,
install: function (plugin) {
plugin(this);
},
selection: {
get: function () {
return goodMockSelectionPath;
}
},
objects: {
areIdsEqual: function (obj1, obj2) {
return true;
}
}
};
const mockObjectPath = [
{
name: 'mockObject1',
type: 'apple'
},
{
name: 'mockObject2',
type: 'banana'
}
];
it('Global Clear Indicator is installed', () => {
openmct.install(ClearDataActionPlugin(openmct, {indicator: true}));
expect(mockIndicatorProvider.add).toHaveBeenCalled();
});
it('Clear Data context menu action is installed', () => {
openmct.install(ClearDataActionPlugin(openmct, []));
expect(mockActionsProvider.register).toHaveBeenCalled();
});
it('clear data action emits a clearData event when invoked', () => {
const action = new ClearDataAction(openmct);
action.invoke(mockObjectPath);
expect(mockObjectViews.emit).toHaveBeenCalledWith('clearData', mockObjectPath[0]);
});
it('clears data on applicable objects', () => {
let action = new ClearDataAction(openmct, ['apple']);
const actionApplies = action.appliesTo(mockObjectPath);
expect(actionApplies).toBe(true);
});
it('does not clear data on inapplicable objects', () => {
let action = new ClearDataAction(openmct, ['pineapple']);
const actionApplies = action.appliesTo(mockObjectPath);
expect(actionApplies).toBe(false);
});
it('does not clear data if not in the selection path and not a layout', () => {
openmct.objects = {
areIdsEqual: function (obj1, obj2) {
return false;
}
};
openmct.router = {
path: [{type: 'not-a-layout'}]
};
let action = new ClearDataAction(openmct, ['apple']);
const actionApplies = action.appliesTo(mockObjectPath);
expect(actionApplies).toBe(false);
});
it('does clear data if not in the selection path and is a layout', () => {
openmct.objects = {
areIdsEqual: function (obj1, obj2) {
return false;
}
};
openmct.router = {
path: [{type: 'layout'}]
};
let action = new ClearDataAction(openmct, ['apple']);
const actionApplies = action.appliesTo(mockObjectPath);
expect(actionApplies).toBe(true);
});
});

View File

@@ -0,0 +1,64 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ClearDataActionPlugin from '../plugin.js';
import ClearDataAction from '../clearDataAction.js';
describe('When the Clear Data Plugin is installed,', function () {
const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']);
const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']);
const mockActionsProvider = jasmine.createSpyObj('actions', ['register']);
const openmct = {
objectViews: mockObjectViews,
indicators: mockIndicatorProvider,
actions: mockActionsProvider,
install: function (plugin) {
plugin(this);
}
};
const mockObjectPath = [
{name: 'mockObject1'},
{name: 'mockObject2'}
];
it('Global Clear Indicator is installed', function () {
openmct.install(ClearDataActionPlugin([]));
expect(mockIndicatorProvider.add).toHaveBeenCalled();
});
it('Clear Data context menu action is installed', function () {
openmct.install(ClearDataActionPlugin([]));
expect(mockActionsProvider.register).toHaveBeenCalled();
});
it('clear data action emits a clearData event when invoked', function () {
let action = new ClearDataAction(openmct);
action.invoke(mockObjectPath);
expect(mockObjectViews.emit).toHaveBeenCalledWith('clearData', mockObjectPath[0]);
});
});

View File

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

View File

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

View File

@@ -1,64 +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.
-->
<template>
<div class="c-indicator t-indicator-clock icon-clock no-minify c-indicator--not-clickable">
<span class="label c-indicator__label">
{{ timeTextValue }}
</span>
</div>
</template>
<script>
import moment from 'moment';
export default {
inject: ['openmct'],
props: {
indicatorFormat: {
type: String,
required: true
}
},
data() {
return {
timeTextValue: null
};
},
mounted() {
this.openmct.on('start', () => {
const TickerService = this.openmct.$injector.get('tickerService');
this.unlisten = TickerService.listen(this.tick);
});
},
beforeDestroy() {
if (this.unlisten) {
this.unlisten();
}
},
methods: {
tick(timestamp) {
this.timeTextValue = `${moment.utc(timestamp).format(this.indicatorFormat)} UTC`;
}
}
};
</script>

View File

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

View File

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

View File

@@ -65,7 +65,7 @@ export default class Condition extends EventEmitter {
}
this.trigger = conditionConfiguration.configuration.trigger;
this.summary = '';
this.description = '';
}
updateResult(datum) {
@@ -134,6 +134,7 @@ export default class Condition extends EventEmitter {
criterionConfigurations.forEach((criterionConfiguration) => {
this.addCriterion(criterionConfiguration);
});
this.updateDescription();
}
updateCriteria(criterionConfigurations) {
@@ -145,6 +146,7 @@ export default class Condition extends EventEmitter {
this.criteria.forEach((criterion) => {
criterion.updateTelemetryObjects(this.conditionManager.telemetryObjects);
});
this.updateDescription();
}
/**
@@ -198,6 +200,7 @@ export default class Condition extends EventEmitter {
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.off('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
this.criteria.splice(found.index, 1, newCriterion);
this.updateDescription();
}
}
@@ -213,6 +216,7 @@ export default class Condition extends EventEmitter {
});
criterion.destroy();
this.criteria.splice(found.index, 1);
this.updateDescription();
return true;
}
@@ -224,6 +228,7 @@ export default class Condition extends EventEmitter {
let found = this.findCriterion(criterion.id);
if (found) {
this.criteria[found.index] = criterion.data;
this.updateDescription();
}
}
@@ -249,7 +254,8 @@ export default class Condition extends EventEmitter {
description = `${description} ${criterion.getDescription()} ${(index < this.criteria.length - 1) ? triggerDescription.conjunction : ''}`;
});
this.summary = description;
this.description = description;
this.conditionManager.updateConditionDescription(this);
}
getTriggerDescription() {

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