Compare commits

..

1 Commits

Author SHA1 Message Date
c46f10de74 add timer plugin 2021-01-11 11:40:11 -08:00
161 changed files with 1111 additions and 9003 deletions

View File

@ -54,7 +54,7 @@ module.exports = {
{
"anonymous": "always",
"asyncArrow": "always",
"named": "never"
"named": "never",
}
],
"array-bracket-spacing": "error",
@ -178,10 +178,7 @@ module.exports = {
//https://eslint.org/docs/rules/no-whitespace-before-property
"no-whitespace-before-property": "error",
// https://eslint.org/docs/rules/object-curly-newline
"object-curly-newline": ["error", {
"consistent": true,
"multiline": true
}],
"object-curly-newline": ["error", {"consistent": true, "multiline": true}],
// https://eslint.org/docs/rules/object-property-newline
"object-property-newline": "error",
// https://eslint.org/docs/rules/brace-style
@ -191,7 +188,7 @@ module.exports = {
// https://eslint.org/docs/rules/operator-linebreak
"operator-linebreak": ["error", "before", {"overrides": {"=": "after"}}],
// https://eslint.org/docs/rules/padding-line-between-statements
"padding-line-between-statements": ["error", {
"padding-line-between-statements":["error", {
"blankLine": "always",
"prev": "multiline-block-like",
"next": "*"
@ -203,17 +200,11 @@ module.exports = {
// https://eslint.org/docs/rules/space-infix-ops
"space-infix-ops": "error",
// https://eslint.org/docs/rules/space-unary-ops
"space-unary-ops": ["error", {
"words": true,
"nonwords": false
}],
"space-unary-ops": ["error", {"words": true, "nonwords": false}],
// https://eslint.org/docs/rules/arrow-spacing
"arrow-spacing": "error",
// https://eslint.org/docs/rules/semi-spacing
"semi-spacing": ["error", {
"before": false,
"after": true
}],
"semi-spacing": ["error", {"before": false, "after": true}],
"vue/html-indent": [
"error",
@ -246,7 +237,6 @@ module.exports = {
}],
"vue/multiline-html-element-content-newline": "off",
"vue/singleline-html-element-content-newline": "off",
"vue/no-mutating-props": "off"
},
"overrides": [

View File

@ -138,7 +138,7 @@ define([
"id": "styleguide:home",
"priority": "preferred",
"model": {
"type": "noneditable.folder",
"type": "folder",
"name": "Style Guide Home",
"location": "ROOT",
"composition": [
@ -155,7 +155,7 @@ define([
"id": "styleguide:ui-elements",
"priority": "preferred",
"model": {
"type": "noneditable.folder",
"type": "folder",
"name": "UI Elements",
"location": "styleguide:home",
"composition": [

View File

@ -1,6 +1,6 @@
{
"name": "openmct",
"version": "1.6.2-SNAPSHOT",
"version": "1.5.0-SNAPSHOT",
"description": "The Open MCT core platform",
"dependencies": {},
"devDependencies": {
@ -23,7 +23,7 @@
"d3-time": "1.0.x",
"d3-time-format": "2.1.x",
"eslint": "7.0.0",
"eslint-plugin-vue": "^7.5.0",
"eslint-plugin-vue": "^6.0.0",
"eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0",
"eventemitter3": "^1.2.0",
"exports-loader": "^0.7.0",

View File

@ -44,9 +44,9 @@ define(
// is also invoked during the create process which should be allowed,
// because it may be saved elsewhere
if ((key === 'edit' && category === 'view-control') || key === 'properties') {
let identifier = this.openmct.objects.parseKeyString(domainObject.getId());
let newStyleObject = objectUtils.toNewFormat(domainObject, domainObject.getId());
return this.openmct.objects.isPersistable(identifier);
return this.openmct.objects.isPersistable(newStyleObject);
}
return true;

View File

@ -43,8 +43,7 @@ define(
);
mockObjectAPI = jasmine.createSpyObj('objectAPI', [
'isPersistable',
'parseKeyString'
'isPersistable'
]);
mockAPI = {

View File

@ -48,9 +48,9 @@ define(
// prevents editing of objects that cannot be persisted, so we can assume that this
// is a new object.
if (!(parent.hasCapability('editor') && parent.getCapability('editor').isEditContextRoot())) {
let identifier = this.openmct.objects.parseKeyString(parent.getId());
let newStyleObject = objectUtils.toNewFormat(parent, parent.getId());
return this.openmct.objects.isPersistable(identifier);
return this.openmct.objects.isPersistable(newStyleObject);
}
return true;

View File

@ -33,8 +33,7 @@ define(
beforeEach(function () {
objectAPI = jasmine.createSpyObj('objectsAPI', [
'isPersistable',
'parseKeyString'
'isPersistable'
]);
mockOpenMCT = {

View File

@ -146,16 +146,11 @@ define([
* @param {String} id to be indexed.
*/
GenericSearchProvider.prototype.scheduleForIndexing = function (id) {
const identifier = objectUtils.parseKeyString(id);
const objectProvider = this.openmct.objects.getProvider(identifier);
if (objectProvider === undefined || objectProvider.search === undefined) {
if (!this.indexedIds[id] && !this.pendingIndex[id]) {
this.indexedIds[id] = true;
this.pendingIndex[id] = true;
this.idsToIndex.push(id);
}
}
this.keepIndexing();
};

View File

@ -219,7 +219,7 @@ define([
* @memberof module:openmct.MCT#
* @name objects
*/
this.objects = new api.ObjectAPI.default(this.types, this);
this.objects = new api.ObjectAPI();
/**
* An interface for retrieving and interpreting telemetry data associated
@ -283,7 +283,6 @@ define([
this.install(this.plugins.NewFolderAction());
this.install(this.plugins.ViewDatumAction());
this.install(this.plugins.ObjectInterceptors());
this.install(this.plugins.NonEditableFolder());
}
MCT.prototype = Object.create(EventEmitter.prototype);
@ -372,7 +371,7 @@ define([
* MCT; if undefined, MCT will be run in the body of the document
*/
MCT.prototype.start = function (domElement = document.body, isHeadlessMode = false) {
if (this.types.get('layout') === undefined) {
if (!this.plugins.DisplayLayout._installed) {
this.install(this.plugins.DisplayLayout({
showAsView: ['summary-widget']
}));

View File

@ -61,7 +61,6 @@ define([
const newStyleObject = utils.toNewFormat(legacyObject.getModel(), legacyObject.getId());
const keystring = utils.makeKeyString(newStyleObject.identifier);
this.eventEmitter.emit(keystring + ':$_synchronize_model', newStyleObject);
this.eventEmitter.emit(keystring + ":*", newStyleObject);
this.eventEmitter.emit('mutation', newStyleObject);
}.bind(this);
@ -139,12 +138,6 @@ define([
});
};
ObjectServiceProvider.prototype.superSecretFallbackSearch = function (query, options) {
const searchService = this.$injector.get('searchService');
return searchService.query(query);
};
// Injects new object API as a decorator so that it hijacks all requests.
// Object providers implemented on new API should just work, old API should just work, many things may break.
function LegacyObjectAPIInterceptor(openmct, ROOTS, instantiate, topic, objectService) {

View File

@ -50,10 +50,6 @@ describe('The ActionCollection', () => {
}
}
];
openmct.objects.addProvider('', jasmine.createSpyObj('mockMutableObjectProvider', [
'create',
'update'
]));
mockView = {
getViewContext: () => {
return {

View File

@ -60,17 +60,6 @@ define([
};
this.onProviderAdd = this.onProviderAdd.bind(this);
this.onProviderRemove = this.onProviderRemove.bind(this);
this.mutables = {};
if (this.domainObject.isMutable) {
this.returnMutables = true;
let unobserve = this.domainObject.$on('$_destroy', () => {
Object.values(this.mutables).forEach(mutable => {
this.publicAPI.objects.destroyMutable(mutable);
});
unobserve();
});
}
}
/**
@ -86,6 +75,10 @@ define([
throw new Error('Event not supported by composition: ' + event);
}
if (!this.mutationListener) {
this._synchronize();
}
if (this.provider.on && this.provider.off) {
if (event === 'add') {
this.provider.on(
@ -196,13 +189,6 @@ define([
this.provider.add(this.domainObject, child.identifier);
} else {
if (this.returnMutables && this.publicAPI.objects.supportsMutation(child.identifier)) {
let keyString = this.publicAPI.objects.makeKeyString(child.identifier);
child = this.publicAPI.objects._toMutable(child);
this.mutables[keyString] = child;
}
this.emit('add', child);
}
};
@ -216,8 +202,6 @@ define([
* @name load
*/
CompositionCollection.prototype.load = function () {
this.cleanUpMutables();
return this.provider.load(this.domainObject)
.then(function (children) {
return Promise.all(children.map((c) => this.publicAPI.objects.get(c)));
@ -250,14 +234,6 @@ define([
if (!skipMutate) {
this.provider.remove(this.domainObject, child.identifier);
} else {
if (this.returnMutables) {
let keyString = this.publicAPI.objects.makeKeyString(child);
if (this.mutables[keyString] !== undefined && this.mutables[keyString].isMutable) {
this.publicAPI.objects.destroyMutable(this.mutables[keyString]);
delete this.mutables[keyString];
}
}
this.emit('remove', child);
}
};
@ -305,6 +281,12 @@ define([
this.remove(child, true);
};
CompositionCollection.prototype._synchronize = function () {
this.mutationListener = this.publicAPI.objects.observe(this.domainObject, '*', (newDomainObject) => {
this.domainObject = JSON.parse(JSON.stringify(newDomainObject));
});
};
CompositionCollection.prototype._destroy = function () {
if (this.mutationListener) {
this.mutationListener();
@ -326,11 +308,5 @@ define([
});
};
CompositionCollection.prototype.cleanUpMutables = function () {
Object.values(this.mutables).forEach(mutable => {
this.publicAPI.objects.destroyMutable(mutable);
});
};
return CompositionCollection;
});

View File

@ -30,12 +30,12 @@ class Menu extends EventEmitter {
this.options = options;
this.component = new Vue({
components: {
MenuComponent
},
provide: {
actions: options.actions
},
components: {
MenuComponent
},
template: '<menu-component />'
});

View File

@ -75,20 +75,13 @@ export default class NotificationAPI extends EventEmitter {
* Info notifications are low priority informational messages for the user. They will be auto-destroy after a brief
* period of time.
* @param {string} message The message to display to the user
* @param {Object} [options] object with following properties
* autoDismissTimeout: {number} in miliseconds to automatically dismisses notification
* link: {Object} Add a link to notifications for navigation
* onClick: callback function
* cssClass: css class name to add style on link
* text: text to display for link
* @returns {InfoNotification}
*/
info(message, options = {}) {
info(message) {
let notificationModel = {
message: message,
autoDismiss: true,
severity: "info",
options
severity: "info"
};
return this._notify(notificationModel);
@ -97,19 +90,12 @@ export default class NotificationAPI extends EventEmitter {
/**
* Present an alert to the user.
* @param {string} message The message to display to the user.
* @param {Object} [options] object with following properties
* autoDismissTimeout: {number} in miliseconds to automatically dismisses notification
* link: {Object} Add a link to notifications for navigation
* onClick: callback function
* cssClass: css class name to add style on link
* text: text to display for link
* @returns {Notification}
*/
alert(message, options = {}) {
alert(message) {
let notificationModel = {
message: message,
severity: "alert",
options
severity: "alert"
};
return this._notify(notificationModel);
@ -118,19 +104,12 @@ export default class NotificationAPI extends EventEmitter {
/**
* Present an error message to the user
* @param {string} message
* @param {Object} [options] object with following properties
* autoDismissTimeout: {number} in miliseconds to automatically dismisses notification
* link: {Object} Add a link to notifications for navigation
* onClick: callback function
* cssClass: css class name to add style on link
* text: text to display for link
* @returns {Notification}
*/
error(message, options = {}) {
error(message) {
let notificationModel = {
message: message,
severity: "error",
options
severity: "error"
};
return this._notify(notificationModel);
@ -346,11 +325,9 @@ export default class NotificationAPI extends EventEmitter {
this.emit('notification', notification);
if (notification.model.autoDismiss || this._selectNextNotification()) {
const autoDismissTimeout = notification.model.options.autoDismissTimeout
|| DEFAULT_AUTO_DISMISS_TIMEOUT;
this.activeTimeout = setTimeout(() => {
this._dismissOrMinimize(notification);
}, autoDismissTimeout);
}, DEFAULT_AUTO_DISMISS_TIMEOUT);
} else {
delete this.activeTimeout;
}

View File

@ -1,154 +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.
*****************************************************************************/
import NotificationAPI from './NotificationAPI';
describe('The Notifiation API', () => {
let notificationAPIInstance;
let defaultTimeout = 4000;
beforeAll(() => {
notificationAPIInstance = new NotificationAPI();
});
describe('the info method', () => {
let message = 'Example Notification Message';
let severity = 'info';
let notificationModel;
beforeAll(() => {
notificationModel = notificationAPIInstance.info(message).model;
});
afterAll(() => {
notificationAPIInstance.dismissAllNotifications();
});
it('shows a string message with info severity', () => {
expect(notificationModel.message).toEqual(message);
expect(notificationModel.severity).toEqual(severity);
});
it('auto dismisses the notification after a brief timeout', (done) => {
window.setTimeout(() => {
expect(notificationAPIInstance.notifications.length).toEqual(0);
done();
}, defaultTimeout);
});
});
describe('the alert method', () => {
let message = 'Example alert message';
let severity = 'alert';
let notificationModel;
beforeAll(() => {
notificationModel = notificationAPIInstance.alert(message).model;
});
afterAll(() => {
notificationAPIInstance.dismissAllNotifications();
});
it('shows a string message, with alert severity', () => {
expect(notificationModel.message).toEqual(message);
expect(notificationModel.severity).toEqual(severity);
});
it('does not auto dismiss the notification', (done) => {
window.setTimeout(() => {
expect(notificationAPIInstance.notifications.length).toEqual(1);
done();
}, defaultTimeout);
});
});
describe('the error method', () => {
let message = 'Example error message';
let severity = 'error';
let notificationModel;
beforeAll(() => {
notificationModel = notificationAPIInstance.error(message).model;
});
afterAll(() => {
notificationAPIInstance.dismissAllNotifications();
});
it('shows a string message, with severity error', () => {
expect(notificationModel.message).toEqual(message);
expect(notificationModel.severity).toEqual(severity);
});
it('does not auto dismiss the notification', (done) => {
window.setTimeout(() => {
expect(notificationAPIInstance.notifications.length).toEqual(1);
done();
}, defaultTimeout);
});
});
describe('the progress method', () => {
let title = 'This is a progress notification';
let message1 = 'Example progress message 1';
let message2 = 'Example progress message 2';
let percentage1 = 50;
let percentage2 = 99.9;
let severity = 'info';
let notification;
let updatedPercentage;
let updatedMessage;
beforeAll(() => {
notification = notificationAPIInstance.progress(title, percentage1, message1);
notification.on('progress', (percentage, text) => {
updatedPercentage = percentage;
updatedMessage = text;
});
});
afterAll(() => {
notificationAPIInstance.dismissAllNotifications();
});
it ('shows a notification with a message, progress message, percentage and info severity', () => {
expect(notification.model.message).toEqual(title);
expect(notification.model.severity).toEqual(severity);
expect(notification.model.progressText).toEqual(message1);
expect(notification.model.progressPerc).toEqual(percentage1);
});
it ('allows dynamically updating the progress attributes', () => {
notification.progress(percentage2, message2);
expect(updatedPercentage).toEqual(percentage2);
expect(updatedMessage).toEqual(message2);
});
it ('allows dynamically dismissing of progress notification', () => {
notification.dismiss();
expect(notificationAPIInstance.notifications.length).toEqual(0);
});
});
});

View File

@ -1,147 +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.
*****************************************************************************/
import _ from 'lodash';
import utils from './object-utils.js';
import EventEmitter from 'EventEmitter';
const ANY_OBJECT_EVENT = 'mutation';
/**
* Wraps a domain object to keep its model synchronized with other instances of the same object.
*
* Creating a MutableDomainObject will automatically register listeners to keep its model in sync. As such, developers
* should be careful to destroy MutableDomainObject in order to avoid memory leaks.
*
* All Open MCT API functions that provide objects will provide MutableDomainObjects where possible, except
* `openmct.objects.get()`, and will manage that object's lifecycle for you. Calling `openmct.objects.getMutable()`
* will result in the creation of a new MutableDomainObject and you will be responsible for destroying it
* (via openmct.objects.destroy) when you're done with it.
*
* @typedef MutableDomainObject
* @memberof module:openmct
*/
class MutableDomainObject {
constructor(eventEmitter) {
Object.defineProperties(this, {
_globalEventEmitter: {
value: eventEmitter,
// Property should not be serialized
enumerable: false
},
_instanceEventEmitter: {
value: new EventEmitter(),
// Property should not be serialized
enumerable: false
},
_observers: {
value: [],
// Property should not be serialized
enumerable: false
},
isMutable: {
value: true,
// Property should not be serialized
enumerable: false
}
});
}
$observe(path, callback) {
let fullPath = qualifiedEventName(this, path);
let eventOff =
this._globalEventEmitter.off.bind(this._globalEventEmitter, fullPath, callback);
this._globalEventEmitter.on(fullPath, callback);
this._observers.push(eventOff);
return eventOff;
}
$set(path, value) {
_.set(this, path, value);
_.set(this, 'modified', Date.now());
//Emit secret synchronization event first, so that all objects are in sync before subsequent events fired.
this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), this);
//Emit a general "any object" event
this._globalEventEmitter.emit(ANY_OBJECT_EVENT, this);
//Emit wildcard event, with path so that callback knows what changed
this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this, path, value);
//Emit events specific to properties affected
let parentPropertiesList = path.split('.');
for (let index = parentPropertiesList.length; index > 0; index--) {
let parentPropertyPath = parentPropertiesList.slice(0, index).join('.');
this._globalEventEmitter.emit(qualifiedEventName(this, parentPropertyPath), _.get(this, parentPropertyPath));
}
//TODO: Emit events for listeners of child properties when parent changes.
// Do it at observer time - also register observers for parent attribute path.
}
$refresh(model) {
//TODO: Currently we are updating the entire object.
// In the future we could update a specific property of the object using the 'path' parameter.
this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), model);
//Emit wildcard event, with path so that callback knows what changed
this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this);
}
$on(event, callback) {
this._instanceEventEmitter.on(event, callback);
return () => this._instanceEventEmitter.off(event, callback);
}
$destroy() {
this._observers.forEach(observer => observer());
delete this._globalEventEmitter;
delete this._observers;
this._instanceEventEmitter.emit('$_destroy');
}
static createMutable(object, mutationTopic) {
let mutable = Object.create(new MutableDomainObject(mutationTopic));
Object.assign(mutable, object);
mutable.$observe('$_synchronize_model', (updatedObject) => {
let clone = JSON.parse(JSON.stringify(updatedObject));
let deleted = _.difference(Object.keys(mutable), Object.keys(updatedObject));
deleted.forEach((propertyName) => delete mutable[propertyName]);
Object.assign(mutable, clone);
});
return mutable;
}
static mutateObject(object, path, value) {
_.set(object, path, value);
_.set(object, 'modified', Date.now());
}
}
function qualifiedEventName(object, eventName) {
let keystring = utils.makeKeyString(object.identifier);
return [keystring, eventName].join(':');
}
export default MutableDomainObject;

View File

@ -0,0 +1,102 @@
/*****************************************************************************
* 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.
*****************************************************************************/
define([
'objectUtils',
'lodash'
], function (
utils,
_
) {
const ANY_OBJECT_EVENT = "mutation";
/**
* The MutableObject wraps a DomainObject and provides getters and
* setters for
* @param eventEmitter
* @param object
* @interface MutableObject
*/
function MutableObject(eventEmitter, object) {
this.eventEmitter = eventEmitter;
this.object = object;
this.unlisteners = [];
}
function qualifiedEventName(object, eventName) {
const keystring = utils.makeKeyString(object.identifier);
return [keystring, eventName].join(':');
}
MutableObject.prototype.stopListening = function () {
this.unlisteners.forEach(function (unlisten) {
unlisten();
});
this.unlisteners = [];
};
/**
* Observe changes to this domain object.
* @param {string} path the property to observe
* @param {Function} callback a callback to invoke when new values for
* this property are observed
* @method on
* @memberof module:openmct.MutableObject#
*/
MutableObject.prototype.on = function (path, callback) {
const fullPath = qualifiedEventName(this.object, path);
const eventOff =
this.eventEmitter.off.bind(this.eventEmitter, fullPath, callback);
this.eventEmitter.on(fullPath, callback);
this.unlisteners.push(eventOff);
};
/**
* Modify this domain object.
* @param {string} path the property to modify
* @param {*} value the new value for this property
* @method set
* @memberof module:openmct.MutableObject#
*/
MutableObject.prototype.set = function (path, value) {
_.set(this.object, path, value);
_.set(this.object, 'modified', Date.now());
const handleRecursiveMutation = function (newObject) {
this.object = newObject;
}.bind(this);
//Emit wildcard event
this.eventEmitter.emit(qualifiedEventName(this.object, '*'), this.object);
//Emit a general "any object" event
this.eventEmitter.emit(ANY_OBJECT_EVENT, this.object);
this.eventEmitter.on(qualifiedEventName(this.object, '*'), handleRecursiveMutation);
//Emit event specific to property
this.eventEmitter.emit(qualifiedEventName(this.object, path), value);
this.eventEmitter.off(qualifiedEventName(this.object, '*'), handleRecursiveMutation);
};
return MutableObject;
});

View File

@ -20,79 +20,68 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import utils from 'objectUtils';
import MutableDomainObject from './MutableDomainObject';
import RootRegistry from './RootRegistry';
import RootObjectProvider from './RootObjectProvider';
import EventEmitter from 'EventEmitter';
import InterceptorRegistry from './InterceptorRegistry';
define([
'lodash',
'objectUtils',
'./MutableObject',
'./RootRegistry',
'./RootObjectProvider',
'./InterceptorRegistry',
'EventEmitter'
], function (
_,
utils,
MutableObject,
RootRegistry,
RootObjectProvider,
InterceptorRegistry,
EventEmitter
) {
/**
/**
* Utilities for loading, saving, and manipulating domain objects.
* @interface ObjectAPI
* @memberof module:openmct
*/
function ObjectAPI(typeRegistry, openmct) {
this.typeRegistry = typeRegistry;
function ObjectAPI() {
this.eventEmitter = new EventEmitter();
this.providers = {};
this.rootRegistry = new RootRegistry();
this.injectIdentifierService = function () {
this.identifierService = openmct.$injector.get("identifierService");
};
this.rootProvider = new RootObjectProvider(this.rootRegistry);
this.rootProvider = new RootObjectProvider.default(this.rootRegistry);
this.cache = {};
this.interceptorRegistry = new InterceptorRegistry();
}
this.interceptorRegistry = new InterceptorRegistry.default();
}
/**
/**
* Set fallback provider, this is an internal API for legacy reasons.
* @private
*/
ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) {
ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) {
this.fallbackProvider = p;
};
};
/**
* @private
*/
ObjectAPI.prototype.getIdentifierService = function () {
// Lazily acquire identifier service
if (!this.identifierService) {
this.injectIdentifierService();
}
return this.identifierService;
};
/**
/**
* Retrieve the provider for a given identifier.
* @private
*/
ObjectAPI.prototype.getProvider = function (identifier) {
//handles the '' vs 'mct' namespace issue
const keyString = utils.makeKeyString(identifier);
const identifierService = this.getIdentifierService();
const namespace = identifierService.parse(keyString).getSpace();
ObjectAPI.prototype.getProvider = function (identifier) {
if (identifier.key === 'ROOT') {
return this.rootProvider;
}
return this.providers[namespace] || this.fallbackProvider;
};
return this.providers[identifier.namespace] || this.fallbackProvider;
};
/**
/**
* Get the root-level object.
* @returns {Promise.<DomainObject>} a promise for the root object
*/
ObjectAPI.prototype.getRoot = function () {
ObjectAPI.prototype.getRoot = function () {
return this.rootProvider.get();
};
};
/**
/**
* Register a new object provider for a particular namespace.
*
* @param {string} namespace the namespace for which to provide objects
@ -101,11 +90,11 @@ ObjectAPI.prototype.getRoot = function () {
* @memberof {module:openmct.ObjectAPI#}
* @name addProvider
*/
ObjectAPI.prototype.addProvider = function (namespace, provider) {
ObjectAPI.prototype.addProvider = function (namespace, provider) {
this.providers[namespace] = provider;
};
};
/**
/**
* Provides the ability to read, write, and delete domain objects.
*
* When registering a new object provider, all methods on this interface
@ -115,7 +104,7 @@ ObjectAPI.prototype.addProvider = function (namespace, provider) {
* @memberof module:openmct
*/
/**
/**
* Create the given domain object in the corresponding persistence store
*
* @method create
@ -126,7 +115,7 @@ ObjectAPI.prototype.addProvider = function (namespace, provider) {
* has been created, or be rejected if it cannot be saved
*/
/**
/**
* Update this domain object in its persistence store
*
* @method update
@ -137,7 +126,7 @@ ObjectAPI.prototype.addProvider = function (namespace, provider) {
* has been updated, or be rejected if it cannot be saved
*/
/**
/**
* Delete this domain object.
*
* @method delete
@ -148,7 +137,7 @@ ObjectAPI.prototype.addProvider = function (namespace, provider) {
* has been deleted, or be rejected if it cannot be deleted
*/
/**
/**
* Get a domain object.
*
* @method get
@ -158,7 +147,17 @@ ObjectAPI.prototype.addProvider = function (namespace, provider) {
* has been saved, or be rejected if it cannot be saved
*/
ObjectAPI.prototype.get = function (identifier) {
/**
* Get a domain object.
*
* @method get
* @memberof module:openmct.ObjectAPI#
* @param {module:openmct.ObjectAPI~Identifier} identifier
* the identifier for the domain object to load
* @returns {Promise} a promise which will resolve when the domain object
* has been saved, or be rejected if it cannot be saved
*/
ObjectAPI.prototype.get = function (identifier) {
let keystring = this.makeKeyString(identifier);
if (this.cache[keystring] !== undefined) {
return this.cache[keystring];
@ -176,6 +175,7 @@ ObjectAPI.prototype.get = function (identifier) {
}
let objectPromise = provider.get(identifier);
this.cache[keystring] = objectPromise;
return objectPromise.then(result => {
@ -187,101 +187,21 @@ ObjectAPI.prototype.get = function (identifier) {
return result;
});
};
};
/**
* Search for domain objects.
*
* Object providersSearches and combines results of each object provider search.
* Objects without search provided will have been indexed
* and will be searched using the fallback indexed search.
* Search results are asynchronous and resolve in parallel.
*
* @method search
* @memberof module:openmct.ObjectAPI#
* @param {string} query the term to search for
* @param {Object} options search options
* @returns {Array.<Promise.<module:openmct.DomainObject>>}
* an array of promises returned from each object provider's search function
* each resolving to domain objects matching provided search query and options.
*/
ObjectAPI.prototype.search = function (query, options) {
const searchPromises = Object.values(this.providers)
.filter(provider => provider.search !== undefined)
.map(provider => provider.search(query, options));
searchPromises.push(this.fallbackProvider.superSecretFallbackSearch(query, options)
.then(results => results.hits
.map(hit => utils.toNewFormat(hit.object.getModel(), hit.object.getId()))));
return searchPromises;
};
/**
* Will fetch object for the given identifier, returning a version of the object that will automatically keep
* itself updated as it is mutated. Before using this function, you should ask yourself whether you really need it.
* The platform will provide mutable objects to views automatically if the underlying object can be mutated. The
* platform will manage the lifecycle of any mutable objects that it provides. If you use `getMutable` you are
* committing to managing that lifecycle yourself. `.destroy` should be called when the object is no longer needed.
*
* @memberof {module:openmct.ObjectAPI#}
* @returns {Promise.<MutableDomainObject>} a promise that will resolve with a MutableDomainObject if
* the object can be mutated.
*/
ObjectAPI.prototype.getMutable = function (idOrKeyString) {
if (!this.supportsMutation(idOrKeyString)) {
throw new Error(`Object "${this.makeKeyString(idOrKeyString)}" does not support mutation.`);
}
return this.get(idOrKeyString).then((object) => {
const mutableDomainObject = this._toMutable(object);
// Check if provider supports realtime updates
let identifier = utils.parseKeyString(idOrKeyString);
let provider = this.getProvider(identifier);
if (provider !== undefined
&& provider.observe !== undefined) {
let unobserve = provider.observe(identifier, (updatedModel) => {
mutableDomainObject.$refresh(updatedModel);
});
mutableDomainObject.$on('$destroy', () => {
unobserve();
});
}
return mutableDomainObject;
});
};
/**
* This function is for cleaning up a mutable domain object when you're done with it.
* You only need to use this if you retrieved the object using `getMutable()`. If the object was provided by the
* platform (eg. passed into a `view()` function) then the platform is responsible for its lifecycle.
* @param {MutableDomainObject} domainObject
*/
ObjectAPI.prototype.destroyMutable = function (domainObject) {
if (domainObject.isMutable) {
return domainObject.$destroy();
} else {
throw new Error("Attempted to destroy non-mutable domain object");
}
};
ObjectAPI.prototype.delete = function () {
ObjectAPI.prototype.delete = function () {
throw new Error('Delete not implemented');
};
};
ObjectAPI.prototype.isPersistable = function (idOrKeyString) {
let identifier = utils.parseKeyString(idOrKeyString);
let provider = this.getProvider(identifier);
ObjectAPI.prototype.isPersistable = function (domainObject) {
let provider = this.getProvider(domainObject.identifier);
return provider !== undefined
&& provider.create !== undefined
&& provider.update !== undefined;
};
};
/**
/**
* Save this domain object in its current state. EXPERIMENTAL
*
* @private
@ -291,12 +211,12 @@ ObjectAPI.prototype.isPersistable = function (idOrKeyString) {
* @returns {Promise} a promise which will resolve when the domain object
* has been saved, or be rejected if it cannot be saved
*/
ObjectAPI.prototype.save = function (domainObject) {
ObjectAPI.prototype.save = function (domainObject) {
let provider = this.getProvider(domainObject.identifier);
let savedResolve;
let result;
if (!this.isPersistable(domainObject.identifier)) {
if (!this.isPersistable(domainObject)) {
result = Promise.reject('Object provider does not support saving');
} else if (hasAlreadyBeenPersisted(domainObject)) {
result = Promise.resolve(true);
@ -319,9 +239,9 @@ ObjectAPI.prototype.save = function (domainObject) {
}
return result;
};
};
/**
/**
* Add a root-level object.
* @param {module:openmct.ObjectAPI~Identifier|function} an array of
* identifiers for root level objects, or a function that returns a
@ -329,32 +249,11 @@ ObjectAPI.prototype.save = function (domainObject) {
* @method addRoot
* @memberof module:openmct.ObjectAPI#
*/
ObjectAPI.prototype.addRoot = function (key) {
ObjectAPI.prototype.addRoot = function (key) {
this.rootRegistry.addRoot(key);
};
};
/**
* Register an object interceptor that transforms a domain object requested via module:openmct.ObjectAPI.get
* The domain object will be transformed after it is retrieved from the persistence store
* The domain object will be transformed only if the interceptor is applicable to that domain object as defined by the InterceptorDef
*
* @param {module:openmct.InterceptorDef} interceptorDef the interceptor definition to add
* @method addGetInterceptor
* @memberof module:openmct.InterceptorRegistry#
*/
ObjectAPI.prototype.addGetInterceptor = function (interceptorDef) {
this.interceptorRegistry.addInterceptor(interceptorDef);
};
/**
* Retrieve the interceptors for a given domain object.
* @private
*/
ObjectAPI.prototype.listGetInterceptors = function (identifier, object) {
return this.interceptorRegistry.getInterceptors(identifier, object);
};
/**
/**
* Modify a domain object.
* @param {module:openmct.DomainObject} object the object to mutate
* @param {string} path the property to modify
@ -362,49 +261,14 @@ ObjectAPI.prototype.listGetInterceptors = function (identifier, object) {
* @method mutate
* @memberof module:openmct.ObjectAPI#
*/
ObjectAPI.prototype.mutate = function (domainObject, path, value) {
if (!this.supportsMutation(domainObject.identifier)) {
throw `Error: Attempted to mutate immutable object ${domainObject.name}`;
}
ObjectAPI.prototype.mutate = function (domainObject, path, value) {
const mutableObject =
new MutableObject(this.eventEmitter, domainObject);
if (domainObject.isMutable) {
domainObject.$set(path, value);
} else {
//Creating a temporary mutable domain object allows other mutable instances of the
//object to be kept in sync.
let mutableDomainObject = this._toMutable(domainObject);
return mutableObject.set(path, value);
};
//Mutate original object
MutableDomainObject.mutateObject(domainObject, path, value);
//Mutate temporary mutable object, in the process informing any other mutable instances
mutableDomainObject.$set(path, value);
//Destroy temporary mutable object
this.destroyMutable(mutableDomainObject);
}
};
/**
* @private
*/
ObjectAPI.prototype._toMutable = function (object) {
if (object.isMutable) {
return object;
} else {
return MutableDomainObject.createMutable(object, this.eventEmitter);
}
};
/**
* @param module:openmct.ObjectAPI~Identifier identifier An object identifier
* @returns {boolean} true if the object can be mutated, otherwise returns false
*/
ObjectAPI.prototype.supportsMutation = function (identifier) {
return this.isPersistable(identifier);
};
/**
/**
* Observe changes to a domain object.
* @param {module:openmct.DomainObject} object the object to observe
* @param {string} path the property to observe
@ -413,47 +277,36 @@ ObjectAPI.prototype.supportsMutation = function (identifier) {
* @method observe
* @memberof module:openmct.ObjectAPI#
*/
ObjectAPI.prototype.observe = function (domainObject, path, callback) {
if (domainObject.isMutable) {
return domainObject.$observe(path, callback);
} else {
let mutable = this._toMutable(domainObject);
mutable.$observe(path, callback);
ObjectAPI.prototype.observe = function (domainObject, path, callback) {
const mutableObject =
new MutableObject(this.eventEmitter, domainObject);
mutableObject.on(path, callback);
return () => mutable.$destroy();
}
};
return mutableObject.stopListening.bind(mutableObject);
};
/**
/**
* @param {module:openmct.ObjectAPI~Identifier} identifier
* @returns {string} A string representation of the given identifier, including namespace and key
*/
ObjectAPI.prototype.makeKeyString = function (identifier) {
ObjectAPI.prototype.makeKeyString = function (identifier) {
return utils.makeKeyString(identifier);
};
};
/**
* @param {string} keyString A string representation of the given identifier, that is, a namespace and key separated by a colon.
* @returns {module:openmct.ObjectAPI~Identifier} An identifier object
*/
ObjectAPI.prototype.parseKeyString = function (keyString) {
return utils.parseKeyString(keyString);
};
/**
/**
* Given any number of identifiers, will return true if they are all equal, otherwise false.
* @param {module:openmct.ObjectAPI~Identifier[]} identifiers
*/
ObjectAPI.prototype.areIdsEqual = function (...identifiers) {
ObjectAPI.prototype.areIdsEqual = function (...identifiers) {
return identifiers.map(utils.parseKeyString)
.every(identifier => {
return identifier === identifiers[0]
|| (identifier.namespace === identifiers[0].namespace
&& identifier.key === identifiers[0].key);
});
};
};
ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) {
ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) {
return this.get(identifier).then((domainObject) => {
path.push(domainObject);
let location = domainObject.location;
@ -464,9 +317,30 @@ ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) {
return path;
}
});
};
};
/**
/**
* Register an object interceptor that transforms a domain object requested via module:openmct.ObjectAPI.get
* The domain object will be transformed after it is retrieved from the persistence store
* The domain object will be transformed only if the interceptor is applicable to that domain object as defined by the InterceptorDef
*
* @param {module:openmct.InterceptorDef} interceptorDef the interceptor definition to add
* @method addGetInterceptor
* @memberof module:openmct.InterceptorRegistry#
*/
ObjectAPI.prototype.addGetInterceptor = function (interceptorDef) {
this.interceptorRegistry.addInterceptor(interceptorDef);
};
/**
* Retrieve the interceptors for a given domain object.
* @private
*/
ObjectAPI.prototype.listGetInterceptors = function (identifier, object) {
return this.interceptorRegistry.getInterceptors(identifier, object);
};
/**
* Uniquely identifies a domain object.
*
* @typedef Identifier
@ -477,7 +351,7 @@ ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) {
* within that namespace
*/
/**
/**
* A domain object is an entity of relevance to a user's workflow, that
* should appear as a distinct and meaningful object within the user
* interface. Examples of domain objects are folders, telemetry sensors,
@ -501,9 +375,10 @@ ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) {
* @memberof module:openmct
*/
function hasAlreadyBeenPersisted(domainObject) {
function hasAlreadyBeenPersisted(domainObject) {
return domainObject.persisted !== undefined
&& domainObject.persisted === domainObject.modified;
}
}
export default ObjectAPI;
return ObjectAPI;
});

View File

@ -1,119 +0,0 @@
import ObjectAPI from './ObjectAPI.js';
describe("The Object API Search Function", () => {
const MOCK_PROVIDER_KEY = 'mockProvider';
const ANOTHER_MOCK_PROVIDER_KEY = 'anotherMockProvider';
const MOCK_PROVIDER_SEARCH_DELAY = 15000;
const ANOTHER_MOCK_PROVIDER_SEARCH_DELAY = 20000;
const TOTAL_TIME_ELAPSED = 21000;
const BASE_TIME = new Date(2021, 0, 1);
let objectAPI;
let mockObjectProvider;
let anotherMockObjectProvider;
let mockFallbackProvider;
let fallbackProviderSearchResults;
let resultsPromises;
beforeEach(() => {
jasmine.clock().install();
jasmine.clock().mockDate(BASE_TIME);
resultsPromises = [];
fallbackProviderSearchResults = {
hits: []
};
objectAPI = new ObjectAPI();
mockObjectProvider = jasmine.createSpyObj("mock object provider", [
"search"
]);
anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [
"search"
]);
mockFallbackProvider = jasmine.createSpyObj("super secret fallback provider", [
"superSecretFallbackSearch"
]);
objectAPI.addProvider('objects', mockObjectProvider);
objectAPI.addProvider('other-objects', anotherMockObjectProvider);
objectAPI.supersecretSetFallbackProvider(mockFallbackProvider);
mockObjectProvider.search.and.callFake(() => {
return new Promise(resolve => {
const mockProviderSearch = {
name: MOCK_PROVIDER_KEY,
start: new Date()
};
setTimeout(() => {
mockProviderSearch.end = new Date();
return resolve(mockProviderSearch);
}, MOCK_PROVIDER_SEARCH_DELAY);
});
});
anotherMockObjectProvider.search.and.callFake(() => {
return new Promise(resolve => {
const anotherMockProviderSearch = {
name: ANOTHER_MOCK_PROVIDER_KEY,
start: new Date()
};
setTimeout(() => {
anotherMockProviderSearch.end = new Date();
return resolve(anotherMockProviderSearch);
}, ANOTHER_MOCK_PROVIDER_SEARCH_DELAY);
});
});
mockFallbackProvider.superSecretFallbackSearch.and.callFake(
() => new Promise(
resolve => setTimeout(
() => resolve(fallbackProviderSearchResults),
50
)
)
);
resultsPromises = objectAPI.search('foo');
jasmine.clock().tick(TOTAL_TIME_ELAPSED);
});
afterEach(() => {
jasmine.clock().uninstall();
});
it("uses each objects given provider's search function", () => {
expect(mockObjectProvider.search).toHaveBeenCalled();
expect(anotherMockObjectProvider.search).toHaveBeenCalled();
});
it("uses the fallback indexed search for objects without a search function provided", () => {
expect(mockFallbackProvider.superSecretFallbackSearch).toHaveBeenCalled();
});
it("provides each providers results as promises that resolve in parallel", async () => {
const results = await Promise.all(resultsPromises);
const mockProviderResults = results.find(
result => result.name === MOCK_PROVIDER_KEY
);
const anotherMockProviderResults = results.find(
result => result.name === ANOTHER_MOCK_PROVIDER_KEY
);
const mockProviderStart = mockProviderResults.start.getTime();
const mockProviderEnd = mockProviderResults.end.getTime();
const anotherMockProviderStart = anotherMockProviderResults.start.getTime();
const anotherMockProviderEnd = anotherMockProviderResults.end.getTime();
const searchElapsedTime = Math.max(mockProviderEnd, anotherMockProviderEnd)
- Math.min(mockProviderEnd, anotherMockProviderEnd);
expect(mockProviderStart).toBeLessThan(anotherMockProviderEnd);
expect(anotherMockProviderStart).toBeLessThan(mockProviderEnd);
expect(searchElapsedTime).toBeLessThan(
MOCK_PROVIDER_SEARCH_DELAY
+ ANOTHER_MOCK_PROVIDER_SEARCH_DELAY
);
});
});

View File

@ -2,30 +2,12 @@ import ObjectAPI from './ObjectAPI.js';
describe("The Object API", () => {
let objectAPI;
let typeRegistry;
let openmct = {};
let mockIdentifierService;
let mockDomainObject;
const TEST_NAMESPACE = "test-namespace";
const FIFTEEN_MINUTES = 15 * 60 * 1000;
beforeEach(() => {
typeRegistry = jasmine.createSpyObj('typeRegistry', [
'get'
]);
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
mockIdentifierService = jasmine.createSpyObj(
'identifierService',
['parse']
);
mockIdentifierService.parse.and.returnValue({
getSpace: () => {
return TEST_NAMESPACE;
}
});
openmct.$injector.get.and.returnValue(mockIdentifierService);
objectAPI = new ObjectAPI(typeRegistry, openmct);
objectAPI = new ObjectAPI();
mockDomainObject = {
identifier: {
namespace: TEST_NAMESPACE,
@ -51,7 +33,6 @@ describe("The Object API", () => {
"update"
]);
mockProvider.create.and.returnValue(Promise.resolve(true));
mockProvider.update.and.returnValue(Promise.resolve(true));
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
});
it("Calls 'create' on provider if object is new", () => {
@ -147,155 +128,4 @@ describe("The Object API", () => {
});
});
});
describe("the mutation API", () => {
let testObject;
let updatedTestObject;
let mutable;
let mockProvider;
let callbacks = [];
beforeEach(function () {
objectAPI = new ObjectAPI(typeRegistry, openmct);
testObject = {
identifier: {
namespace: TEST_NAMESPACE,
key: 'test-key'
},
name: 'test object',
otherAttribute: 'other-attribute-value',
objectAttribute: {
embeddedObject: {
embeddedKey: 'embedded-value'
}
}
};
updatedTestObject = Object.assign({otherAttribute: 'changed-attribute-value'}, testObject);
mockProvider = jasmine.createSpyObj("mock provider", [
"get",
"create",
"update",
"observe",
"observeObjectChanges"
]);
mockProvider.get.and.returnValue(Promise.resolve(testObject));
mockProvider.observeObjectChanges.and.callFake(() => {
callbacks[0](updatedTestObject);
callbacks.splice(0, 1);
});
mockProvider.observe.and.callFake((id, callback) => {
if (callbacks.length === 0) {
callbacks.push(callback);
} else {
callbacks[0] = callback;
}
});
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
return objectAPI.getMutable(testObject.identifier)
.then(object => {
mutable = object;
return mutable;
});
});
afterEach(() => {
mutable.$destroy();
});
it('mutates the original object', () => {
const MUTATED_NAME = 'mutated name';
objectAPI.mutate(testObject, 'name', MUTATED_NAME);
expect(testObject.name).toBe(MUTATED_NAME);
});
describe ('uses a MutableDomainObject', () => {
it('and retains properties of original object ', function () {
expect(hasOwnProperty(mutable, 'identifier')).toBe(true);
expect(hasOwnProperty(mutable, 'otherAttribute')).toBe(true);
expect(mutable.identifier).toEqual(testObject.identifier);
expect(mutable.otherAttribute).toEqual(testObject.otherAttribute);
});
it('that is identical to original object when serialized', function () {
expect(JSON.stringify(mutable)).toEqual(JSON.stringify(testObject));
});
it('that observes for object changes', function () {
let mockListener = jasmine.createSpy('mockListener');
objectAPI.observe(testObject, '*', mockListener);
mockProvider.observeObjectChanges();
expect(mockListener).toHaveBeenCalled();
});
});
describe('uses events', function () {
let testObjectDuplicate;
let mutableSecondInstance;
beforeEach(function () {
// Duplicate object to guarantee we are not sharing object instance, which would invalidate test
testObjectDuplicate = JSON.parse(JSON.stringify(testObject));
mutableSecondInstance = objectAPI._toMutable(testObjectDuplicate);
});
afterEach(() => {
mutableSecondInstance.$destroy();
});
it('to stay synchronized when mutated', function () {
objectAPI.mutate(mutable, 'otherAttribute', 'new-attribute-value');
expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value');
});
it('to indicate when a property changes', function () {
let mutationCallback = jasmine.createSpy('mutation-callback');
let unlisten;
return new Promise(function (resolve) {
mutationCallback.and.callFake(resolve);
unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback);
objectAPI.mutate(mutable, 'otherAttribute', 'some-new-value');
}).then(function () {
expect(mutationCallback).toHaveBeenCalledWith('some-new-value');
unlisten();
});
});
it('to indicate when a child property has changed', function () {
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
let listeners = [];
return new Promise(function (resolve) {
objectAttributeCallback.and.callFake(resolve);
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
objectAPI.mutate(mutable, 'objectAttribute.embeddedObject.embeddedKey', 'updated-embedded-value');
}).then(function () {
expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value');
expect(embeddedObjectCallback).toHaveBeenCalledWith({
embeddedKey: 'updated-embedded-value'
});
expect(objectAttributeCallback).toHaveBeenCalledWith({
embeddedObject: {
embeddedKey: 'updated-embedded-value'
}
});
listeners.forEach(listener => listener());
});
});
});
});
});
function hasOwnProperty(object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
}

View File

@ -48,7 +48,7 @@ define([
this.providers.push(function () {
return key;
});
} else if (typeof key === "function") {
} else if (_.isFunction(key)) {
this.providers.push(key);
}
};

View File

@ -20,12 +20,13 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import MCTChartSeriesElement from './MCTChartSeriesElement';
export default class MCTChartLineLinear extends MCTChartSeriesElement {
addPoint(point, start, count) {
this.buffer[start] = point.x;
this.buffer[start + 1] = point.y;
}
}
define([
"EventEmitter"
], function (
EventEmitter
) {
/**
* Provides a singleton event bus for sharing between objects.
*/
return new EventEmitter();
});

View File

@ -6,9 +6,6 @@ class Dialog extends Overlay {
constructor({iconClass, message, title, hint, timestamp, ...options}) {
let component = new Vue({
components: {
DialogComponent: DialogComponent
},
provide: {
iconClass,
message,
@ -16,6 +13,9 @@ class Dialog extends Overlay {
hint,
timestamp
},
components: {
DialogComponent: DialogComponent
},
template: '<dialog-component></dialog-component>'
}).$mount();

View File

@ -7,9 +7,6 @@ let component;
class ProgressDialog extends Overlay {
constructor({progressPerc, progressText, iconClass, message, title, hint, timestamp, ...options}) {
component = new Vue({
components: {
ProgressDialogComponent: ProgressDialogComponent
},
provide: {
iconClass,
message,
@ -17,6 +14,9 @@ class ProgressDialog extends Overlay {
hint,
timestamp
},
components: {
ProgressDialogComponent: ProgressDialogComponent
},
data() {
return {
model: {

View File

@ -38,12 +38,12 @@
<script>
export default {
inject: ['dismiss', 'element', 'buttons', 'dismissable'],
data: function () {
return {
focusIndex: -1
};
},
inject: ['dismiss', 'element', 'buttons', 'dismissable'],
mounted() {
const element = this.$refs.element;
element.appendChild(this.element);

View File

@ -1,37 +0,0 @@
export default function (couchPlugin, searchFilter) {
return function install(openmct) {
const couchProvider = couchPlugin.couchProvider;
openmct.objects.addRoot({
namespace: 'couch-search',
key: 'couch-search'
});
openmct.objects.addProvider('couch-search', {
get(identifier) {
if (identifier.key !== 'couch-search') {
return undefined;
} else {
return Promise.resolve({
identifier,
type: 'folder',
name: "CouchDB Documents"
});
}
}
});
openmct.composition.addProvider({
appliesTo(domainObject) {
return domainObject.identifier.namespace === 'couch-search'
&& domainObject.identifier.key === 'couch-search';
},
load() {
return couchProvider.getObjectsByFilter(searchFilter).then(objects => {
return objects.map(object => object.identifier);
});
}
});
};
}

View File

@ -1,91 +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.
*****************************************************************************/
import {createOpenMct, resetApplicationState} from "utils/testing";
import CouchDBSearchFolderPlugin from './plugin';
describe('the plugin', function () {
let identifier = {
namespace: 'couch-search',
key: "couch-search"
};
let testPath = '/test/db';
let openmct;
let composition;
beforeEach((done) => {
openmct = createOpenMct();
let couchPlugin = openmct.plugins.CouchDB(testPath);
openmct.install(couchPlugin);
openmct.install(new CouchDBSearchFolderPlugin(couchPlugin, {
"selector": {
"model": {
"type": "plan"
}
}
}));
openmct.on('start', done);
openmct.startHeadless();
composition = openmct.composition.get({identifier});
spyOn(couchPlugin.couchProvider, 'getObjectsByFilter').and.returnValue(Promise.resolve([
{
identifier: {
key: "1",
namespace: "mct"
}
},
{
identifier: {
key: "2",
namespace: "mct"
}
}
]));
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('provides a folder to hold plans', () => {
openmct.objects.get(identifier).then((object) => {
expect(object).toEqual({
identifier,
type: 'folder',
name: "CouchDB Documents"
});
});
});
it('provides composition for couch search folders', () => {
composition.load().then((objects) => {
expect(objects.length).toEqual(2);
});
});
});

View File

@ -44,15 +44,11 @@ export default function LADTableViewProvider(openmct) {
LadTableComponent: LadTable
},
provide: {
openmct
},
data: () => {
return {
openmct,
domainObject,
objectPath
};
},
template: '<lad-table-component :domain-object="domainObject" :object-path="objectPath"></lad-table-component>'
template: '<lad-table-component></lad-table-component>'
});
},
destroy: function (element) {

View File

@ -26,7 +26,7 @@
class="js-lad-table__body__row"
@contextmenu.prevent="showContextMenu"
>
<td class="js-first-data">{{ domainObject.name }}</td>
<td class="js-first-data">{{ name }}</td>
<td class="js-second-data">{{ formattedTimestamp }}</td>
<td
class="js-third-data"
@ -50,16 +50,12 @@ const CONTEXT_MENU_ACTIONS = [
];
export default {
inject: ['openmct'],
inject: ['openmct', 'objectPath'],
props: {
domainObject: {
type: Object,
required: true
},
objectPath: {
type: Array,
required: true
},
hasUnits: {
type: Boolean,
requred: true
@ -70,6 +66,7 @@ export default {
currentObjectPath.unshift(this.domainObject);
return {
name: this.domainObject.name,
timestamp: undefined,
value: '---',
valueClass: '',
@ -92,6 +89,14 @@ export default {
.telemetry
.limitEvaluator(this.domainObject);
this.stopWatchingMutation = this.openmct
.objects
.observe(
this.domainObject,
'*',
this.updateName
);
this.openmct.time.on('timeSystem', this.updateTimeSystem);
this.openmct.time.on('bounds', this.updateBounds);
@ -114,6 +119,7 @@ export default {
}
},
destroyed() {
this.stopWatchingMutation();
this.unsubscribe();
this.openmct.time.off('timeSystem', this.updateTimeSystem);
this.openmct.time.off('bounds', this.updateBounds);
@ -154,6 +160,9 @@ export default {
})
.then((array) => this.updateValues(array[array.length - 1]));
},
updateName(name) {
this.name = name;
},
updateBounds(bounds, isTick) {
this.bounds = bounds;
if (!isTick) {

View File

@ -36,7 +36,6 @@
v-for="item in items"
:key="item.key"
:domain-object="item.domainObject"
:object-path="objectPath"
:has-units="hasUnits"
/>
</tbody>
@ -48,20 +47,10 @@
import LadRow from './LADRow.vue';
export default {
inject: ['openmct', 'domainObject', 'objectPath'],
components: {
LadRow
},
inject: ['openmct'],
props: {
domainObject: {
type: Object,
required: true
},
objectPath: {
type: Array,
required: true
}
},
data() {
return {
items: []

View File

@ -57,10 +57,10 @@
import LadRow from './LADRow.vue';
export default {
inject: ['openmct', 'domainObject'],
components: {
LadRow
},
inject: ['openmct', 'domainObject'],
data() {
return {
ladTableObjects: [],

View File

@ -37,12 +37,12 @@ define([
return function install(openmct) {
if (installIndicator) {
let component = new Vue ({
components: {
GlobalClearIndicator: GlobaClearIndicator.default
},
provide: {
openmct
},
components: {
GlobalClearIndicator: GlobaClearIndicator.default
},
template: '<GlobalClearIndicator></GlobalClearIndicator>'
});

View File

@ -34,9 +34,6 @@ export default class ConditionManager extends EventEmitter {
this.composition = this.openmct.composition.get(conditionSetDomainObject);
this.composition.on('add', this.subscribeToTelemetry, this);
this.composition.on('remove', this.unsubscribeFromTelemetry, this);
this.shouldEvaluateNewTelemetry = this.shouldEvaluateNewTelemetry.bind(this);
this.compositionLoad = this.composition.load();
this.subscriptions = {};
this.telemetryObjects = {};
@ -340,10 +337,6 @@ export default class ConditionManager extends EventEmitter {
return false;
}
shouldEvaluateNewTelemetry(currentTimestamp) {
return this.openmct.time.bounds().end >= currentTimestamp;
}
telemetryReceived(endpoint, datum) {
if (!this.isTelemetryUsed(endpoint)) {
return;
@ -352,13 +345,11 @@ export default class ConditionManager extends EventEmitter {
const normalizedDatum = this.createNormalizedDatum(datum, endpoint);
const timeSystemKey = this.openmct.time.timeSystem().key;
let timestamp = {};
const currentTimestamp = normalizedDatum[timeSystemKey];
timestamp[timeSystemKey] = currentTimestamp;
if (this.shouldEvaluateNewTelemetry(currentTimestamp)) {
timestamp[timeSystemKey] = normalizedDatum[timeSystemKey];
this.updateConditionResults(normalizedDatum);
this.updateCurrentCondition(timestamp);
}
}
updateConditionResults(normalizedDatum) {
//We want to stop when the first condition evaluates to true.

View File

@ -195,11 +195,11 @@ import { TRIGGER, TRIGGER_LABEL } from "@/plugins/condition/utils/constants";
import uuid from 'uuid';
export default {
inject: ['openmct'],
components: {
Criterion,
ConditionDescription
},
inject: ['openmct'],
props: {
currentConditionId: {
type: String,

View File

@ -81,10 +81,10 @@ import Condition from './Condition.vue';
import ConditionManager from '../ConditionManager';
export default {
inject: ['openmct', 'domainObject'],
components: {
Condition
},
inject: ['openmct', 'domainObject'],
props: {
isEditing: Boolean,
testData: {

View File

@ -58,11 +58,11 @@ import TestData from './TestData.vue';
import ConditionCollection from './ConditionCollection.vue';
export default {
inject: ["openmct", "domainObject"],
components: {
TestData,
ConditionCollection
},
inject: ["openmct", "domainObject"],
props: {
isEditing: Boolean
},

View File

@ -31,6 +31,7 @@
v-model="expanded"
class="c-tree__item__view-control"
:enabled="hasChildren"
:propagate="false"
/>
<div class="c-tree__item__label c-object-label">
<div
@ -41,7 +42,7 @@
</div>
</div>
<ul
v-if="expanded && !isLoading"
v-if="expanded"
class="c-tree"
>
<li
@ -68,10 +69,10 @@ import viewControl from '@/ui/components/viewControl.vue';
export default {
name: 'ConditionSetDialogTreeItem',
inject: ['openmct'],
components: {
viewControl
},
inject: ['openmct'],
props: {
node: {
type: Object,

View File

@ -41,7 +41,7 @@
></div>
<!-- end loading -->
<div v-if="shouldDisplayNoResultsText"
<div v-if="(allTreeItems.length === 0) || (searchValue && filteredTreeItems.length === 0)"
class="c-tree-and-search__no-results"
>
No results found
@ -63,7 +63,7 @@
<!-- end main tree -->
<!-- search tree -->
<ul v-if="searchValue && !isLoading"
<ul v-if="searchValue"
class="c-tree-and-search__tree c-tree"
>
<condition-set-dialog-tree-item
@ -80,17 +80,16 @@
</template>
<script>
import debounce from 'lodash/debounce';
import search from '@/ui/components/search.vue';
import ConditionSetDialogTreeItem from './ConditionSetDialogTreeItem.vue';
export default {
inject: ['openmct'],
name: 'ConditionSetSelectorDialog',
components: {
search,
ConditionSetDialogTreeItem
},
inject: ['openmct'],
data() {
return {
expanded: false,
@ -101,20 +100,8 @@ export default {
selectedItem: undefined
};
},
computed: {
shouldDisplayNoResultsText() {
if (this.isLoading) {
return false;
}
return this.allTreeItems.length === 0
|| (this.searchValue && this.filteredTreeItems.length === 0);
}
},
created() {
this.getDebouncedFilteredChildren = debounce(this.getFilteredChildren, 400);
},
mounted() {
this.searchService = this.openmct.$injector.get('searchService');
this.getAllChildren();
},
methods: {
@ -137,44 +124,37 @@ export default {
});
},
getFilteredChildren() {
// clear any previous search results
this.filteredTreeItems = [];
this.searchService.query(this.searchValue).then(children => {
this.filteredTreeItems = children.hits.map(child => {
const promises = this.openmct.objects.search(this.searchValue)
.map(promise => promise
.then(results => this.aggregateFilteredChildren(results)));
let context = child.object.getCapability('context');
let object = child.object.useCapability('adapter');
let objectPath = [];
let navigateToParent;
Promise.all(promises).then(() => {
this.isLoading = false;
});
},
async aggregateFilteredChildren(results) {
for (const object of results) {
const objectPath = await this.openmct.objects.getOriginalPath(object.identifier);
const navigateToParent = '/browse/'
+ objectPath.slice(1)
.map(parent => this.openmct.objects.makeKeyString(parent.identifier))
if (context) {
objectPath = context.getPath().slice(1)
.map(oldObject => oldObject.useCapability('adapter'))
.reverse();
navigateToParent = '/browse/' + objectPath.slice(1)
.map((parent) => this.openmct.objects.makeKeyString(parent.identifier))
.join('/');
}
const filteredChild = {
return {
id: this.openmct.objects.makeKeyString(object.identifier),
object,
objectPath,
navigateToParent
};
this.filteredTreeItems.push(filteredChild);
}
});
});
},
searchTree(value) {
this.searchValue = value;
this.isLoading = true;
if (this.searchValue !== '') {
this.getDebouncedFilteredChildren();
} else {
this.isLoading = false;
this.getFilteredChildren();
}
},
handleItemSelection(item, node) {

View File

@ -401,15 +401,15 @@ describe('the plugin', function () {
let viewContainer = document.createElement('div');
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
StylesView
},
provide: {
openmct: openmct,
selection: selection,
stylesManager
},
el: viewContainer,
components: {
StylesView
},
template: '<styles-view/>'
});
@ -543,6 +543,7 @@ describe('the plugin', function () {
});
it('should evaluate as stale when telemetry is not received in the allotted time', (done) => {
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
conditionMgr.on('conditionSetResultUpdated', mockListener);
conditionMgr.telemetryObjects = {
@ -564,7 +565,7 @@ describe('the plugin', function () {
});
it('should not evaluate as stale when telemetry is received in the allotted time', (done) => {
const date = 1;
const date = Date.now();
conditionSetDomainObject.configuration.conditionCollection[0].configuration.criteria[0].input = ["0.4"];
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
conditionMgr.on('conditionSetResultUpdated', mockListener);

View File

@ -56,14 +56,14 @@ define([
return {
show: function (element) {
component = new Vue({
el: element,
components: {
AlphanumericFormatView: AlphanumericFormatView.default
},
provide: {
openmct,
objectPath
},
el: element,
components: {
AlphanumericFormatView: AlphanumericFormatView.default
},
template: '<alphanumeric-format-view ref="alphanumericFormatView"></alphanumeric-format-view>'
});
},

View File

@ -51,11 +51,11 @@ export default {
height: 5
};
},
inject: ['openmct'],
components: {
LayoutFrame
},
mixins: [conditionalStylesMixin],
inject: ['openmct'],
props: {
item: {
type: Object,

View File

@ -152,7 +152,10 @@ export default {
}
},
data() {
let domainObject = JSON.parse(JSON.stringify(this.domainObject));
return {
internalDomainObject: domainObject,
initSelectIndex: undefined,
selection: [],
showGrid: true
@ -160,10 +163,10 @@ export default {
},
computed: {
gridSize() {
return this.domainObject.configuration.layoutGrid;
return this.internalDomainObject.configuration.layoutGrid;
},
layoutItems() {
return this.domainObject.configuration.items;
return this.internalDomainObject.configuration.items;
},
selectedLayoutItems() {
return this.layoutItems.filter(item => {
@ -171,7 +174,7 @@ export default {
});
},
layoutDimensions() {
return this.domainObject.configuration.layoutDimensions;
return this.internalDomainObject.configuration.layoutDimensions;
},
shouldDisplayLayoutDimensions() {
return this.layoutDimensions
@ -203,9 +206,12 @@ export default {
}
},
mounted() {
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', function (obj) {
this.internalDomainObject = JSON.parse(JSON.stringify(obj));
}.bind(this));
this.openmct.selection.on('change', this.setSelection);
this.initializeItems();
this.composition = this.openmct.composition.get(this.domainObject);
this.composition = this.openmct.composition.get(this.internalDomainObject);
this.composition.on('add', this.addChild);
this.composition.on('remove', this.removeChild);
this.composition.load();
@ -214,6 +220,7 @@ export default {
this.openmct.selection.off('change', this.setSelection);
this.composition.off('add', this.addChild);
this.composition.off('remove', this.removeChild);
this.unlisten();
},
methods: {
addElement(itemType, element) {
@ -340,7 +347,7 @@ export default {
this.startingMinY2 = undefined;
},
mutate(path, value) {
this.openmct.objects.mutate(this.domainObject, path, value);
this.openmct.objects.mutate(this.internalDomainObject, path, value);
},
handleDrop($event) {
if (!$event.dataTransfer.types.includes('openmct/domain-object-path')) {
@ -380,11 +387,11 @@ export default {
}
},
containsObject(identifier) {
return _.get(this.domainObject, 'composition')
return _.get(this.internalDomainObject, 'composition')
.some(childId => this.openmct.objects.areIdsEqual(childId, identifier));
},
handleDragOver($event) {
if (this.domainObject.locked) {
if (this.internalDomainObject.locked) {
return;
}
@ -413,7 +420,7 @@ export default {
item.id = uuid();
this.trackItem(item);
this.layoutItems.push(item);
this.openmct.objects.mutate(this.domainObject, "configuration.items", this.layoutItems);
this.openmct.objects.mutate(this.internalDomainObject, "configuration.items", this.layoutItems);
this.initSelectIndex = this.layoutItems.length - 1;
},
trackItem(item) {
@ -470,7 +477,7 @@ export default {
}
},
removeFromComposition(keyString) {
let composition = _.get(this.domainObject, 'composition');
let composition = _.get(this.internalDomainObject, 'composition');
composition = composition.filter(identifier => {
return this.openmct.objects.makeKeyString(identifier) !== keyString;
});
@ -622,10 +629,10 @@ export default {
createNewDomainObject(domainObject, composition, viewType, nameExtension, model) {
let identifier = {
key: uuid(),
namespace: this.domainObject.identifier.namespace
namespace: this.internalDomainObject.identifier.namespace
};
let type = this.openmct.types.get(viewType);
let parentKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
let parentKeyString = this.openmct.objects.makeKeyString(this.internalDomainObject.identifier);
let objectName = nameExtension ? `${domainObject.name}-${nameExtension}` : domainObject.name;
let object = {};
@ -682,7 +689,7 @@ export default {
});
},
duplicateItem(selectedItems) {
let objectStyles = this.domainObject.configuration.objectStyles || {};
let objectStyles = this.internalDomainObject.configuration.objectStyles || {};
let selectItemsArray = [];
let newDomainObjectsArray = [];
@ -721,8 +728,8 @@ export default {
});
this.$nextTick(() => {
this.openmct.objects.mutate(this.domainObject, "configuration.items", this.layoutItems);
this.openmct.objects.mutate(this.domainObject, "configuration.objectStyles", objectStyles);
this.openmct.objects.mutate(this.internalDomainObject, "configuration.items", this.layoutItems);
this.openmct.objects.mutate(this.internalDomainObject, "configuration.objectStyles", objectStyles);
this.$el.click(); //clear selection;
newDomainObjectsArray.forEach(domainObject => {
@ -761,13 +768,13 @@ export default {
};
this.createNewDomainObject(mockDomainObject, overlayPlotIdentifiers, viewType).then((newDomainObject) => {
let newDomainObjectKeyString = this.openmct.objects.makeKeyString(newDomainObject.identifier);
let domainObjectKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
let internalDomainObjectKeyString = this.openmct.objects.makeKeyString(this.internalDomainObject.identifier);
this.composition.add(newDomainObject);
this.addItem('subobject-view', newDomainObject, position);
overlayPlots.forEach(overlayPlot => {
if (overlayPlot.location === domainObjectKeyString) {
if (overlayPlot.location === internalDomainObjectKeyString) {
this.openmct.objects.mutate(overlayPlot, 'location', newDomainObjectKeyString);
}
});

View File

@ -51,11 +51,11 @@ export default {
url: element.url
};
},
inject: ['openmct'],
components: {
LayoutFrame
},
mixins: [conditionalStylesMixin],
inject: ['openmct'],
props: {
item: {
type: Object,

View File

@ -99,8 +99,8 @@ export default {
stroke: '#717171'
};
},
mixins: [conditionalStylesMixin],
inject: ['openmct'],
mixins: [conditionalStylesMixin],
props: {
item: {
type: Object,

View File

@ -80,11 +80,11 @@ export default {
viewKey
};
},
inject: ['openmct', 'objectPath'],
components: {
ObjectFrame,
LayoutFrame
},
inject: ['openmct', 'objectPath'],
props: {
item: {
type: Object,
@ -129,22 +129,13 @@ export default {
}
},
mounted() {
if (this.openmct.objects.supportsMutation(this.item.identifier)) {
this.openmct.objects.getMutable(this.item.identifier)
.then(this.setObject);
} else {
this.openmct.objects.get(this.item.identifier)
.then(this.setObject);
}
},
beforeDestroy() {
destroyed() {
if (this.removeSelectable) {
this.removeSelectable();
}
if (this.domainObject.isMutable) {
this.openmct.objects.destroyMutable(this.domainObject);
}
},
methods: {
setObject(domainObject) {

View File

@ -98,11 +98,11 @@ export default {
font: 'default'
};
},
inject: ['openmct', 'objectPath'],
components: {
LayoutFrame
},
mixins: [conditionalStylesMixin],
inject: ['openmct', 'objectPath'],
props: {
item: {
type: Object,
@ -212,20 +212,14 @@ export default {
}
},
mounted() {
if (this.openmct.objects.supportsMutation(this.item.identifier)) {
this.openmct.objects.getMutable(this.item.identifier)
.then(this.setObject);
} else {
this.openmct.objects.get(this.item.identifier)
.then(this.setObject);
}
this.openmct.time.on("bounds", this.refreshData);
this.status = this.openmct.status.get(this.item.identifier);
this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus);
},
beforeDestroy() {
destroyed() {
this.removeSubscription();
this.removeStatusListener();
@ -234,10 +228,6 @@ export default {
}
this.openmct.time.off("bounds", this.refreshData);
if (this.domainObject.isMutable) {
this.openmct.objects.destroyMutable(this.domainObject);
}
},
methods: {
formattedValueForCopy() {

View File

@ -59,11 +59,11 @@ export default {
font: 'default'
};
},
inject: ['openmct'],
components: {
LayoutFrame
},
mixins: [conditionalStylesMixin],
inject: ['openmct'],
props: {
item: {
type: Object,

View File

@ -57,7 +57,6 @@ describe("The Duplicate Action plugin", () => {
overwrite: {
folder: {
name: "Parent Folder",
type: "folder",
composition: [childObject.identifier]
}
}
@ -105,7 +104,6 @@ describe("The Duplicate Action plugin", () => {
// already installed by default, but never hurts, just adds to context menu
openmct.install(DuplicateActionPlugin());
openmct.types.addType('folder', {creatable: true});
openmct.on('start', done);
openmct.startHeadless();

View File

@ -47,13 +47,13 @@ define([
return {
show: function (element) {
component = new Vue({
provide: {
openmct
},
el: element,
components: {
FiltersView: FiltersView.default
},
provide: {
openmct
},
template: '<filters-view></filters-view>'
});
},

View File

@ -65,11 +65,11 @@ import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
import isEmpty from 'lodash/isEmpty';
export default {
inject: ['openmct'],
components: {
FilterField,
ToggleSwitch
},
inject: ['openmct'],
props: {
filterObject: {
type: Object,

View File

@ -41,10 +41,10 @@
import FilterField from './FilterField.vue';
export default {
inject: ['openmct'],
components: {
FilterField
},
inject: ['openmct'],
props: {
globalMetadata: {
type: Object,

View File

@ -87,12 +87,12 @@ import DropHint from './dropHint.vue';
const MIN_FRAME_SIZE = 5;
export default {
inject: ['openmct'],
components: {
FrameComponent,
ResizeHandle,
DropHint
},
inject: ['openmct'],
props: {
container: {
type: Object,

View File

@ -28,7 +28,7 @@
></div>
<div
v-if="allContainersAreEmpty"
v-if="areAllContainersEmpty()"
class="c-fl__empty"
>
<span class="c-fl__empty-message">This Flexible Layout is currently empty</span>
@ -94,6 +94,7 @@ import Container from '../utils/container';
import Frame from '../utils/frame';
import ResizeHandle from './resizeHandle.vue';
import DropHint from './dropHint.vue';
import RemoveAction from '../../remove/RemoveAction.js';
const MIN_CONTAINER_SIZE = 5;
@ -139,20 +140,19 @@ function sizeToFill(items) {
}
export default {
inject: ['openmct', 'objectPath', 'layoutObject'],
components: {
ContainerComponent,
ResizeHandle,
DropHint
},
inject: ['openmct', 'objectPath', 'layoutObject'],
props: {
isEditing: Boolean
},
data() {
return {
domainObject: this.layoutObject,
newFrameLocation: [],
identifierMap: {}
newFrameLocation: []
};
},
computed: {
@ -168,30 +168,26 @@ export default {
},
rowsLayout() {
return this.domainObject.configuration.rowsLayout;
},
allContainersAreEmpty() {
return this.containers.every(container => container.frames.length === 0);
}
},
mounted() {
this.buildIdentifierMap();
this.composition = this.openmct.composition.get(this.domainObject);
this.composition.on('remove', this.removeChildObject);
this.composition.on('add', this.addFrame);
this.composition.load();
this.RemoveAction = new RemoveAction(this.openmct);
this.unobserve = this.openmct.objects.observe(this.domainObject, '*', this.updateDomainObject);
},
beforeDestroy() {
this.composition.off('remove', this.removeChildObject);
this.composition.off('add', this.addFrame);
this.unobserve();
},
methods: {
buildIdentifierMap() {
this.containers.forEach(container => {
container.frames.forEach(frame => {
let keystring = this.openmct.objects.makeKeyString(frame.domainObjectIdentifier);
this.identifierMap[keystring] = true;
});
});
areAllContainersEmpty() {
return !this.containers.filter(container => container.frames.length).length;
},
addContainer() {
let container = new Container();
@ -240,9 +236,6 @@ export default {
this.newFrameLocation = [containerIndex, insertFrameIndex];
},
addFrame(domainObject) {
let keystring = this.openmct.objects.makeKeyString(domainObject.identifier);
if (!this.identifierMap[keystring]) {
let containerIndex = this.newFrameLocation.length ? this.newFrameLocation[0] : 0;
let container = this.containers[containerIndex];
let frameIndex = this.newFrameLocation.length ? this.newFrameLocation[1] : container.frames.length;
@ -253,8 +246,6 @@ export default {
this.newFrameLocation = [];
this.persist(containerIndex);
this.identifierMap[keystring] = true;
}
},
deleteFrame(frameId) {
let container = this.containers
@ -263,20 +254,16 @@ export default {
.frames
.filter((f => f.id === frameId))[0];
this.removeFromComposition(frame.domainObjectIdentifier);
this.$nextTick().then(() => {
this.removeFromComposition(frame.domainObjectIdentifier)
.then(() => {
sizeToFill(container.frames);
this.setSelectionToParent();
});
},
removeFromComposition(identifier) {
let keystring = this.openmct.objects.makeKeyString(identifier);
this.identifierMap[keystring] = undefined;
delete this.identifierMap[keystring];
this.composition.remove({identifier});
return this.openmct.objects.get(identifier).then((childDomainObject) => {
this.RemoveAction.removeFromComposition(this.domainObject, childDomainObject);
});
},
setSelectionToParent() {
this.$el.click();

View File

@ -58,10 +58,10 @@
import ObjectFrame from '../../../ui/components/ObjectFrame.vue';
export default {
inject: ['openmct'],
components: {
ObjectFrame
},
inject: ['openmct'],
props: {
frame: {
type: Object,

View File

@ -44,15 +44,15 @@ define([
return {
show: function (element, isEditing) {
component = new Vue({
el: element,
components: {
FlexibleLayoutComponent: FlexibleLayoutComponent.default
},
provide: {
openmct,
objectPath,
layoutObject: domainObject
},
el: element,
components: {
FlexibleLayoutComponent: FlexibleLayoutComponent.default
},
data() {
return {
isEditing: isEditing

View File

@ -22,22 +22,18 @@
define([
'./components/GridView.vue',
'./constants.js',
'vue'
], function (
GridViewComponent,
constants,
Vue
) {
function FolderGridView(openmct) {
const ALLOWED_FOLDER_TYPES = constants.ALLOWED_FOLDER_TYPES;
return {
key: 'grid',
name: 'Grid View',
cssClass: 'icon-thumbs-strip',
canView: function (domainObject) {
return ALLOWED_FOLDER_TYPES.includes(domainObject.type);
return domainObject.type === 'folder';
},
view: function (domainObject) {
let component;

View File

@ -22,24 +22,20 @@
define([
'./components/ListView.vue',
'./constants.js',
'vue',
'moment'
], function (
ListViewComponent,
constants,
Vue,
Moment
) {
function FolderListView(openmct) {
const ALLOWED_FOLDER_TYPES = constants.ALLOWED_FOLDER_TYPES;
return {
key: 'list-view',
name: 'List View',
cssClass: 'icon-list-view',
canView: function (domainObject) {
return ALLOWED_FOLDER_TYPES.includes(domainObject.type);
return domainObject.type === 'folder';
},
view: function (domainObject) {
let component;

View File

@ -1,73 +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.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
describe("the plugin", () => {
let openmct;
let goToFolderAction;
let mockObjectPath;
beforeEach((done) => {
openmct = createOpenMct();
openmct.on('start', done);
openmct.startHeadless();
goToFolderAction = openmct.actions._allActions.goToOriginal;
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('installs the go to folder action', () => {
expect(goToFolderAction).toBeDefined();
});
describe('when invoked', () => {
beforeEach(() => {
mockObjectPath = [{
name: 'mock folder',
type: 'folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
}];
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({
identifier: {
namespace: '',
key: 'test'
}
}));
goToFolderAction.invoke(mockObjectPath);
});
it('goes to the original location', () => {
expect(window.location.href).toContain('context.html#/browse/?tc.mode=fixed&tc.startBound=0&tc.endBound=1&tc.timeSystem=utc');
});
});
});

View File

@ -1,99 +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.
*****************************************************************************/
import { createOpenMct, resetApplicationState } from "utils/testing";
import InterceptorPlugin from "./plugin";
describe('the plugin', function () {
let element;
let child;
let openmct;
const TEST_NAMESPACE = 'test';
beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct();
openmct.install(new InterceptorPlugin(openmct));
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);
openmct.on('start', done);
openmct.startHeadless(appHolder);
});
afterEach(() => {
return resetApplicationState(openmct);
});
describe('the missingObjectInterceptor', () => {
let mockProvider;
beforeEach(() => {
mockProvider = jasmine.createSpyObj("mock provider", [
"get"
]);
mockProvider.get.and.returnValue(Promise.resolve(undefined));
openmct.objects.addProvider(TEST_NAMESPACE, mockProvider);
});
it('returns missing objects', (done) => {
const identifier = {
namespace: TEST_NAMESPACE,
key: 'hello'
};
openmct.objects.get(identifier).then((testObject) => {
expect(testObject).toEqual({
identifier,
type: 'unknown',
name: 'Missing: test:hello'
});
done();
});
});
it('returns the My items object if not found', (done) => {
const identifier = {
namespace: TEST_NAMESPACE,
key: 'mine'
};
openmct.objects.get(identifier).then((testObject) => {
expect(testObject).toEqual({
identifier,
"name": "My Items",
"type": "folder",
"composition": [],
"location": "ROOT"
});
done();
});
});
});
});

View File

@ -1,33 +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.
*****************************************************************************/
export default function () {
return function (openmct) {
openmct.types.addType("noneditable.folder", {
name: "Non-Editable Folder",
key: "noneditable.folder",
description: "Create folders to organize other objects or links to objects without the ability to edit it's properties.",
cssClass: "icon-folder",
creatable: false
});
};
}

View File

@ -1,50 +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.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
describe("the plugin", () => {
const NON_EDITABLE_FOLDER_KEY = 'noneditable.folder';
let openmct;
beforeEach((done) => {
openmct = createOpenMct();
openmct.install(openmct.plugins.NonEditableFolder());
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('adds the new non-editable folder type', () => {
const type = openmct.types.get(NON_EDITABLE_FOLDER_KEY);
expect(type).toBeDefined();
expect(type.definition.creatable).toBeFalse();
});
});

View File

@ -97,8 +97,7 @@
:selected-page="getSelectedPage()"
:selected-section="getSelectedSection()"
:read-only="false"
@deleteEntry="deleteEntry"
@updateEntry="updateEntry"
@updateEntries="updateEntries"
/>
</div>
</div>
@ -112,20 +111,19 @@ import Search from '@/ui/components/search.vue';
import SearchResults from './SearchResults.vue';
import Sidebar from './Sidebar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage';
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
import { addNotebookEntry, createNewEmbed, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
import objectUtils from 'objectUtils';
import { throttle } from 'lodash';
import objectLink from '../../../ui/mixins/object-link';
export default {
inject: ['openmct', 'domainObject', 'snapshotContainer'],
components: {
NotebookEntry,
Search,
SearchResults,
Sidebar
},
inject: ['openmct', 'domainObject', 'snapshotContainer'],
data() {
return {
defaultPageId: getDefaultNotebook() ? getDefaultNotebook().page.id : '',
@ -184,9 +182,7 @@ export default {
mounted() {
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
this.formatSidebar();
window.addEventListener('orientationchange', this.formatSidebar);
window.addEventListener("hashchange", this.navigateToSectionPage, false);
this.navigateToSectionPage();
},
@ -194,9 +190,6 @@ export default {
if (this.unlisten) {
this.unlisten();
}
window.removeEventListener('orientationchange', this.formatSidebar);
window.removeEventListener("hashchange", this.navigateToSectionPage);
},
updated: function () {
this.$nextTick(() => {
@ -232,50 +225,17 @@ export default {
},
createNotebookStorageObject() {
const notebookMeta = {
name: this.internalDomainObject.name,
identifier: this.internalDomainObject.identifier,
link: this.getLinktoNotebook()
identifier: this.internalDomainObject.identifier
};
const page = this.getSelectedPage();
const section = this.getSelectedSection();
return {
notebookMeta,
page,
section
section,
page
};
},
deleteEntry(entryId) {
const self = this;
const entryPos = getEntryPosById(entryId, this.internalDomainObject, this.selectedSection, this.selectedPage);
if (entryPos === -1) {
this.openmct.notifications.alert('Warning: unable to delete entry');
console.error(`unable to delete entry ${entryId} from section ${this.selectedSection}, page ${this.selectedPage}`);
return;
}
const dialog = this.openmct.overlays.dialog({
iconClass: 'alert',
message: 'This action will permanently delete this entry. Do you wish to continue?',
buttons: [
{
label: "Ok",
emphasis: true,
callback: () => {
const entries = getNotebookEntries(self.internalDomainObject, self.selectedSection, self.selectedPage);
entries.splice(entryPos, 1);
self.updateEntries(entries);
dialog.dismiss();
}
},
{
label: "Cancel",
callback: () => dialog.dismiss()
}
]
});
},
dragOver(event) {
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
@ -349,20 +309,6 @@ export default {
return this.openmct.objects.get(oldNotebookStorage.notebookMeta.identifier);
},
getLinktoNotebook() {
const objectPath = this.openmct.router.path;
const link = objectLink.computed.objectLink.call({
objectPath,
openmct: this.openmct
});
const selectedSection = this.selectedSection;
const selectedPage = this.selectedPage;
const sectionId = selectedSection ? selectedSection.id : '';
const pageId = selectedPage ? selectedPage.id : '';
return `${link}?sectionId=${sectionId}&pageId=${pageId}`;
},
getPage(section, id) {
return section.pages.find(p => p.id === id);
},
@ -447,12 +393,6 @@ export default {
return s;
});
const selectedSectionId = this.selectedSection && this.selectedSection.id;
const selectedPageId = this.selectedPage && this.selectedPage.id;
if (selectedPageId === pageId && selectedSectionId === sectionId) {
return;
}
this.sectionsChanged({ sections });
},
newEntry(embed = null) {
@ -572,13 +512,6 @@ export default {
setDefaultNotebookSection(section);
},
updateEntry(entry) {
const entries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage);
const entryPos = getEntryPosById(entry.id, this.internalDomainObject, this.selectedSection, this.selectedPage);
entries[entryPos] = entry;
this.updateEntries(entries);
},
updateEntries(entries) {
const configuration = this.internalDomainObject.configuration;
const notebookEntries = configuration.entries || {};

View File

@ -33,10 +33,10 @@ import SnapshotTemplate from './snapshot-template.html';
import Vue from 'vue';
export default {
inject: ['openmct'],
components: {
PopupMenu
},
inject: ['openmct'],
props: {
embed: {
type: Object,

View File

@ -12,15 +12,11 @@
<div class="c-ne__content">
<div :id="entry.id"
class="c-ne__text"
tabindex="0"
:class="{ 'c-ne__input' : !readOnly }"
:class="{'c-ne__input' : !readOnly }"
:contenteditable="!readOnly"
@blur="updateEntryValue($event)"
@keydown.enter.exact.prevent
@keyup.enter.exact.prevent="forceBlur($event)"
v-text="entry.text"
>
</div>
@blur="updateEntryValue($event, entry.id)"
@focus="updateCurrentEntryValue($event, entry.id)"
>{{ entry.text }}</div>
<div class="c-snapshots c-ne__embeds">
<NotebookEmbed v-for="embed in entry.embeds"
:key="embed.id"
@ -37,7 +33,6 @@
>
<button class="c-icon-button c-icon-button--major icon-trash"
title="Delete this entry"
tabindex="-1"
@click="deleteEntry"
>
</button>
@ -62,14 +57,14 @@
<script>
import NotebookEmbed from './NotebookEmbed.vue';
import { createNewEmbed } from '../utils/notebook-entries';
import { createNewEmbed, getEntryPosById, getNotebookEntries } from '../utils/notebook-entries';
import Moment from 'moment';
export default {
inject: ['openmct', 'snapshotContainer'],
components: {
NotebookEmbed
},
inject: ['openmct', 'snapshotContainer'],
props: {
domainObject: {
type: Object,
@ -108,6 +103,11 @@ export default {
}
}
},
data() {
return {
currentEntryValue: ''
};
},
computed: {
createdOnDate() {
return this.formatTime(this.entry.createdOn, 'YYYY-MM-DD');
@ -117,20 +117,10 @@ export default {
}
},
mounted() {
this.updateEntries = this.updateEntries.bind(this);
this.dropOnEntry = this.dropOnEntry.bind(this);
},
methods: {
addNewEmbed(objectPath) {
const bounds = this.openmct.time.bounds();
const snapshotMeta = {
bounds,
link: null,
objectPath,
openmct: this.openmct
};
const newEmbed = createNewEmbed(snapshotMeta);
this.entry.embeds.push(newEmbed);
},
cancelEditMode(event) {
const isEditing = this.openmct.editor.isEditing();
if (isEditing) {
@ -142,23 +132,63 @@ export default {
event.dataTransfer.dropEffect = "copy";
},
deleteEntry() {
this.$emit('deleteEntry', this.entry.id);
const self = this;
const entryPosById = self.entryPosById(self.entry.id);
if (entryPosById === -1) {
return;
}
const dialog = this.openmct.overlays.dialog({
iconClass: 'alert',
message: 'This action will permanently delete this entry. Do you wish to continue?',
buttons: [
{
label: "Ok",
emphasis: true,
callback: () => {
const entries = getNotebookEntries(self.domainObject, self.selectedSection, self.selectedPage);
entries.splice(entryPosById, 1);
self.updateEntries(entries);
dialog.dismiss();
}
},
{
label: "Cancel",
callback: () => {
dialog.dismiss();
}
}
]
});
},
dropOnEntry($event) {
event.stopImmediatePropagation();
const snapshotId = $event.dataTransfer.getData('openmct/snapshot/id');
if (snapshotId.length) {
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
this.snapshotContainer.removeSnapshot(snapshotId);
this.entry.embeds.push(snapshot);
} else {
const data = $event.dataTransfer.getData('openmct/domain-object-path');
const objectPath = JSON.parse(data);
this.addNewEmbed(objectPath);
this.moveSnapshot(snapshotId);
return;
}
this.$emit('updateEntry', this.entry);
const data = $event.dataTransfer.getData('openmct/domain-object-path');
const objectPath = JSON.parse(data);
const entryPos = this.entryPosById(this.entry.id);
const bounds = this.openmct.time.bounds();
const snapshotMeta = {
bounds,
link: null,
objectPath,
openmct: this.openmct
};
const newEmbed = createNewEmbed(snapshotMeta);
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
const currentEntryEmbeds = entries[entryPos].embeds;
currentEntryEmbeds.push(newEmbed);
this.updateEntries(entries);
},
entryPosById(entryId) {
return getEntryPosById(entryId, this.domainObject, this.selectedSection, this.selectedPage);
},
findPositionInArray(array, id) {
let position = -1;
@ -173,12 +203,15 @@ export default {
return position;
},
forceBlur(event) {
event.target.blur();
},
formatTime(unixTime, timeFormat) {
return Moment.utc(unixTime).format(timeFormat);
},
moveSnapshot(snapshotId) {
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
this.entry.embeds.push(snapshot);
this.updateEntry(this.entry);
this.snapshotContainer.removeSnapshot(snapshotId);
},
navigateToPage() {
this.$emit('changeSectionPage', {
sectionId: this.result.section.id,
@ -194,8 +227,15 @@ export default {
removeEmbed(id) {
const embedPosition = this.findPositionInArray(this.entry.embeds, id);
this.entry.embeds.splice(embedPosition, 1);
this.updateEntry(this.entry);
},
updateCurrentEntryValue($event) {
if (this.readOnly) {
return;
}
this.$emit('updateEntry', this.entry);
const target = $event.target;
this.currentEntryValue = target ? target.textContent : '';
},
updateEmbed(newEmbed) {
this.entry.embeds.some(e => {
@ -207,14 +247,44 @@ export default {
return found;
});
this.$emit('updateEntry', this.entry);
this.updateEntry(this.entry);
},
updateEntryValue($event) {
const value = $event.target.innerText;
if (value !== this.entry.text && value.match(/\S/)) {
this.entry.text = value;
this.$emit('updateEntry', this.entry);
updateEntry(newEntry) {
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
entries.some(entry => {
const found = (entry.id === newEntry.id);
if (found) {
entry = newEntry;
}
return found;
});
this.updateEntries(entries);
},
updateEntryValue($event, entryId) {
if (!this.domainObject || !this.selectedSection || !this.selectedPage) {
return;
}
const target = $event.target;
if (!target) {
return;
}
const entryPos = this.entryPosById(entryId);
const value = target.textContent.trim();
if (this.currentEntryValue !== value) {
target.textContent = value;
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
entries[entryPos].text = value;
this.updateEntries(entries);
}
},
updateEntries(entries) {
this.$emit('updateEntries', entries);
}
}
};

View File

@ -56,11 +56,11 @@ import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container';
import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants';
export default {
inject: ['openmct', 'snapshotContainer'],
components: {
NotebookEmbed,
PopupMenu
},
inject: ['openmct', 'snapshotContainer'],
props: {
toggleSnapshot: {
type: Function,

View File

@ -69,14 +69,14 @@ export default {
const divElement = document.querySelector('.l-shell__drawer div');
this.component = new Vue({
el: divElement,
components: {
SnapshotContainerComponent
},
provide: {
openmct,
snapshotContainer
},
el: divElement,
components: {
SnapshotContainerComponent
},
data() {
return {
toggleSnapshot

View File

@ -22,10 +22,10 @@ import { getDefaultNotebook } from '../utils/notebook-storage';
import Page from './PageComponent.vue';
export default {
inject: ['openmct'],
components: {
Page
},
inject: ['openmct'],
props: {
defaultPageId: {
type: String,

View File

@ -18,10 +18,10 @@ import PopupMenu from './PopupMenu.vue';
import RemoveDialog from '../utils/removeDialog';
export default {
inject: ['openmct'],
components: {
PopupMenu
},
inject: ['openmct'],
props: {
defaultPageId: {
type: String,

View File

@ -21,10 +21,10 @@
import NotebookEntry from './NotebookEntry.vue';
export default {
inject: ['openmct', 'snapshotContainer'],
components: {
NotebookEntry
},
inject: ['openmct', 'snapshotContainer'],
props: {
domainObject: {
type: Object,

View File

@ -22,10 +22,10 @@ import { getDefaultNotebook } from '../utils/notebook-storage';
import sectionComponent from './SectionComponent.vue';
export default {
inject: ['openmct'],
components: {
sectionComponent
},
inject: ['openmct'],
props: {
defaultSectionId: {
type: String,

View File

@ -21,10 +21,10 @@ import PopupMenu from './PopupMenu.vue';
import RemoveDialog from '../utils/removeDialog';
export default {
inject: ['openmct'],
components: {
PopupMenu
},
inject: ['openmct'],
props: {
defaultSectionId: {
type: String,

View File

@ -61,11 +61,11 @@ import PageCollection from './PageCollection.vue';
import uuid from 'uuid';
export default {
inject: ['openmct'],
components: {
SectionCollection,
PageCollection
},
inject: ['openmct'],
props: {
defaultPageId: {
type: String,

View File

@ -88,13 +88,13 @@ export default function NotebookPlugin() {
const snapshotContainer = new SnapshotContainer(openmct);
const notebookSnapshotIndicator = new Vue ({
components: {
NotebookSnapshotIndicator
},
provide: {
openmct,
snapshotContainer
},
components: {
NotebookSnapshotIndicator
},
template: '<NotebookSnapshotIndicator></NotebookSnapshotIndicator>'
});
const indicator = {

View File

@ -1,5 +1,5 @@
import { addNotebookEntry, createNewEmbed } from './utils/notebook-entries';
import { getDefaultNotebook, getDefaultNotebookLink, setDefaultNotebook } from './utils/notebook-storage';
import { getDefaultNotebook } from './utils/notebook-storage';
import { NOTEBOOK_DEFAULT } from '@/plugins/notebook/notebook-constants';
import SnapshotContainer from './snapshot-container';
@ -45,21 +45,12 @@ export default class Snapshot {
_saveToDefaultNoteBook(embed) {
const notebookStorage = getDefaultNotebook();
this.openmct.objects.get(notebookStorage.notebookMeta.identifier)
.then(async (domainObject) => {
.then(domainObject => {
addNotebookEntry(this.openmct, domainObject, notebookStorage, embed);
let link = notebookStorage.notebookMeta.link;
// Backwards compatibility fix (old notebook model without link)
if (!link) {
link = await getDefaultNotebookLink(this.openmct, domainObject);
notebookStorage.notebookMeta.link = link;
setDefaultNotebook(this.openmct, notebookStorage);
}
const defaultPath = `${domainObject.name} - ${notebookStorage.section.name} - ${notebookStorage.page.name}`;
const msg = `Saved to Notebook ${defaultPath}`;
this._showNotification(msg, link);
this._showNotification(msg);
});
}
@ -67,29 +58,16 @@ export default class Snapshot {
* @private
*/
_saveToNotebookSnapshots(embed) {
this.snapshotContainer.addSnapshot(embed);
const saved = this.snapshotContainer.addSnapshot(embed);
if (!saved) {
return;
}
_showNotification(msg, url) {
const options = {
autoDismissTimeout: 30000,
link: {
cssClass: '',
text: 'click to view',
onClick: this._navigateToNotebook(url)
}
};
this.openmct.notifications.info(msg, options);
const msg = 'Saved to Notebook Snapshots - click to view.';
this._showNotification(msg);
}
_navigateToNotebook(url = null) {
if (!url) {
return () => {};
}
return () => {
window.location.href = window.location.origin + url;
};
_showNotification(msg) {
this.openmct.notifications.info(msg);
}
}

View File

@ -109,30 +109,10 @@ const selectedPage = {
};
let openmct;
let mockIdentifierService;
describe('Notebook Entries:', () => {
beforeEach(done => {
openmct = createOpenMct();
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
mockIdentifierService = jasmine.createSpyObj(
'identifierService',
['parse']
);
mockIdentifierService.parse.and.returnValue({
getSpace: () => {
return '';
}
});
openmct.$injector.get.and.returnValue(mockIdentifierService);
openmct.types.addType('notebook', {
creatable: true
});
openmct.objects.addProvider('', jasmine.createSpyObj('mockNotebookProvider', [
'create',
'update'
]));
window.localStorage.setItem('notebook-storage', null);
done();

View File

@ -48,29 +48,14 @@ export function getDefaultNotebook() {
return JSON.parse(notebookStorage);
}
export async function getDefaultNotebookLink(openmct, domainObject = null) {
if (!domainObject) {
return null;
}
const path = await openmct.objects.getOriginalPath(domainObject.identifier)
.then(objectPath => objectPath
.map(o => o && openmct.objects.makeKeyString(o.identifier))
.reverse()
.join('/')
);
const { page, section } = getDefaultNotebook();
return `#/browse/${path}?sectionId=${section.id}&pageId=${page.id}`;
}
export function setDefaultNotebook(openmct, notebookStorage, domainObject) {
observeDefaultNotebookObject(openmct, notebookStorage, domainObject);
observeDefaultNotebookObject(openmct, notebookStorage.notebookMeta, domainObject);
saveDefaultNotebook(notebookStorage);
}
export function setDefaultNotebookSection(section) {
const notebookStorage = getDefaultNotebook();
notebookStorage.section = section;
saveDefaultNotebook(notebookStorage);
}

View File

@ -62,10 +62,7 @@ describe('Notebook Storage:', () => {
beforeEach((done) => {
openmct = createOpenMct();
window.localStorage.setItem('notebook-storage', null);
openmct.objects.addProvider('', jasmine.createSpyObj('mockNotebookProvider', [
'create',
'update'
]));
done();
});

View File

@ -27,10 +27,10 @@
import NotificationsList from './NotificationsList.vue';
export default {
inject: ['openmct'],
components: {
NotificationsList
},
inject: ['openmct'],
data() {
return {
notifications: this.openmct.notifications.notifications,

View File

@ -25,12 +25,12 @@ import NotificationIndicator from './components/NotificationIndicator.vue';
export default function plugin() {
return function install(openmct) {
let component = new Vue ({
components: {
NotificationIndicator: NotificationIndicator
},
provide: {
openmct
},
components: {
NotificationIndicator: NotificationIndicator
},
template: '<NotificationIndicator></NotificationIndicator>'
});

View File

@ -25,36 +25,13 @@ import CouchObjectQueue from "./CouchObjectQueue";
const REV = "_rev";
const ID = "_id";
const HEARTBEAT = 50000;
export default class CouchObjectProvider {
// options {
// url: couchdb url,
// disableObserve: disable auto feed from couchdb to keep objects in sync,
// filter: selector to find objects to sync in couchdb
// }
constructor(openmct, options, namespace) {
options = this._normalize(options);
constructor(openmct, url, namespace) {
this.openmct = openmct;
this.url = options.url;
this.url = url;
this.namespace = namespace;
this.objectQueue = {};
this.observeEnabled = options.disableObserve !== true;
this.observers = {};
if (this.observeEnabled) {
this.observeObjectChanges(options.filter);
}
}
//backwards compatibility, options used to be a url. Now it's an object
_normalize(options) {
if (typeof options === 'string') {
return {
url: options
};
}
return options;
}
request(subPath, method, value) {
@ -125,162 +102,6 @@ export default class CouchObjectProvider {
return this.request(identifier.key, "GET").then(this.getModel.bind(this));
}
async getObjectsByFilter(filter) {
let objects = [];
let url = `${this.url}/_find`;
let body = {};
if (filter) {
body = JSON.stringify(filter);
}
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body
});
const reader = response.body.getReader();
let completed = false;
while (!completed) {
const {done, value} = await reader.read();
//done is true when we lose connection with the provider
if (done) {
completed = true;
}
if (value) {
let chunk = new Uint8Array(value.length);
chunk.set(value, 0);
const decodedChunk = new TextDecoder("utf-8").decode(chunk);
try {
const json = JSON.parse(decodedChunk);
if (json) {
let docs = json.docs;
docs.forEach(doc => {
let object = this.getModel(doc);
if (object) {
objects.push(object);
}
});
}
} catch (e) {
//do nothing
}
}
}
return objects;
}
observe(identifier, callback) {
if (!this.observeEnabled) {
return;
}
const keyString = this.openmct.objects.makeKeyString(identifier);
this.observers[keyString] = this.observers[keyString] || [];
this.observers[keyString].push(callback);
return () => {
this.observers[keyString] = this.observers[keyString].filter(observer => observer !== callback);
};
}
abortGetChanges() {
if (this.controller) {
this.controller.abort();
this.controller = undefined;
}
return true;
}
async observeObjectChanges(filter) {
let intermediateResponse = this.getIntermediateResponse();
if (!this.observeEnabled) {
intermediateResponse.reject('Observe for changes is disabled');
}
const controller = new AbortController();
const signal = controller.signal;
if (this.controller) {
this.abortGetChanges();
}
this.controller = controller;
// feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection
// style=main_only returns only the current winning revision of the document
let url = `${this.url}/_changes?feed=continuous&style=main_only&heartbeat=${HEARTBEAT}`;
let body = {};
if (filter) {
url = `${url}&filter=_selector`;
body = JSON.stringify(filter);
}
const response = await fetch(url, {
method: 'POST',
signal,
headers: {
"Content-Type": 'application/json'
},
body
});
const reader = response.body.getReader();
let completed = false;
while (!completed) {
const {done, value} = await reader.read();
//done is true when we lose connection with the provider
if (done) {
completed = true;
}
if (value) {
let chunk = new Uint8Array(value.length);
chunk.set(value, 0);
const decodedChunk = new TextDecoder("utf-8").decode(chunk).split('\n');
if (decodedChunk.length && decodedChunk[decodedChunk.length - 1] === '') {
decodedChunk.forEach((doc, index) => {
try {
const object = JSON.parse(doc);
object.identifier = {
namespace: this.namespace,
key: object.id
};
let keyString = this.openmct.objects.makeKeyString(object.identifier);
let observersForObject = this.observers[keyString];
if (observersForObject) {
observersForObject.forEach(async (observer) => {
const updatedObject = await this.get(object.identifier);
observer(updatedObject);
});
}
} catch (e) {
//do nothing;
}
});
}
}
}
//We're done receiving from the provider. No more chunks.
intermediateResponse.resolve(true);
return intermediateResponse.promise;
}
getIntermediateResponse() {
let intermediateResponse = {};
intermediateResponse.promise = new Promise(function (resolve, reject) {

View File

@ -24,9 +24,8 @@ import CouchObjectProvider from './CouchObjectProvider';
const NAMESPACE = '';
const PERSISTENCE_SPACE = 'mct';
export default function CouchPlugin(options) {
export default function CouchPlugin(url) {
return function install(openmct) {
install.couchProvider = new CouchObjectProvider(openmct, options, NAMESPACE);
openmct.objects.addProvider(PERSISTENCE_SPACE, install.couchProvider);
openmct.objects.addProvider(PERSISTENCE_SPACE, new CouchObjectProvider(openmct, url, NAMESPACE));
};
}

View File

@ -32,43 +32,17 @@ describe('the plugin', () => {
let child;
let provider;
let testPath = '/test/db';
let options;
let mockIdentifierService;
let mockDomainObject;
beforeEach((done) => {
mockDomainObject = {
identifier: {
namespace: '',
namespace: 'mct',
key: 'some-value'
},
type: 'mock-type'
};
options = {
url: testPath,
filter: {},
disableObserve: true
}
};
openmct = createOpenMct(false);
spyOnBuiltins(['fetch'], window);
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
mockIdentifierService = jasmine.createSpyObj(
'identifierService',
['parse']
);
mockIdentifierService.parse.and.returnValue({
getSpace: () => {
return 'mct';
}
});
openmct.$injector.get.and.returnValue(mockIdentifierService);
openmct.install(new CouchPlugin(options));
openmct.types.addType('mock-type', {creatable: true});
openmct.install(new CouchPlugin(testPath));
element = document.createElement('div');
child = document.createElement('div');
@ -81,16 +55,9 @@ describe('the plugin', () => {
spyOn(provider, 'get').and.callThrough();
spyOn(provider, 'create').and.callThrough();
spyOn(provider, 'update').and.callThrough();
});
afterEach(() => {
return resetApplicationState(openmct);
});
describe('the provider', () => {
let mockPromise;
beforeEach(() => {
mockPromise = Promise.resolve({
spyOnBuiltins(['fetch'], window);
fetch.and.returnValue(Promise.resolve({
json: () => {
return {
ok: true,
@ -99,8 +66,11 @@ describe('the plugin', () => {
model: {}
};
}
}));
});
fetch.and.returnValue(mockPromise);
afterEach(() => {
return resetApplicationState(openmct);
});
it('gets an object', () => {
@ -128,7 +98,7 @@ describe('the plugin', () => {
});
it('updates queued objects', () => {
let couchProvider = new CouchObjectProvider(openmct, options, '');
let couchProvider = new CouchObjectProvider(openmct, 'http://localhost', '');
let intermediateResponse = couchProvider.getIntermediateResponse();
spyOn(couchProvider, 'updateQueued');
couchProvider.enqueueObject(mockDomainObject.identifier.key, mockDomainObject, intermediateResponse);
@ -143,5 +113,4 @@ describe('the plugin', () => {
expect(couchProvider.updateQueued).toHaveBeenCalledTimes(2);
});
});
});

View File

@ -136,7 +136,7 @@ define([
};
}
if (objectPath && (typeof objectPath !== "function")) {
if (objectPath && !_.isFunction(objectPath)) {
const staticObjectPath = objectPath;
objectPath = function () {
return staticObjectPath;

View File

@ -1,63 +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.
*****************************************************************************/
import Plot from '../single/Plot.vue';
import Vue from 'vue';
export default function OverlayPlotViewProvider(openmct) {
return {
key: 'plot-overlay',
name: 'Overlay Plot',
cssClass: 'icon-telemetry',
canView(domainObject) {
return domainObject.type === 'telemetry.plot.overlay';
},
canEdit(domainObject) {
return domainObject.type === 'telemetry.plot.overlay';
},
view: function (domainObject) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
Plot
},
provide: {
openmct,
domainObject
},
template: '<plot></plot>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
}
};
}

View File

@ -1,83 +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.
*****************************************************************************/
/*jscs:disable disallowDanglingUnderscores */
/**
* A scale has an input domain and an output range. It provides functions
* `scale` return the range value associated with a domain value.
* `invert` return the domain value associated with range value.
*/
class LinearScale {
constructor(domain) {
this.domain(domain);
}
domain(newDomain) {
if (newDomain) {
this._domain = newDomain;
this._domainDenominator = newDomain.max - newDomain.min;
}
return this._domain;
}
range(newRange) {
if (newRange) {
this._range = newRange;
this._rangeDenominator = newRange.max - newRange.min;
}
return this._range;
}
scale(domainValue) {
if (!this._domain || !this._range) {
return;
}
const domainOffset = domainValue - this._domain.min;
const rangeFraction = domainOffset - this._domainDenominator;
const rangeOffset = rangeFraction * this._rangeDenominator;
const rangeValue = rangeOffset + this._range.min;
return rangeValue;
}
invert(rangeValue) {
if (!this._domain || !this._range) {
return;
}
const rangeOffset = rangeValue - this._range.min;
const domainFraction = rangeOffset / this._rangeDenominator;
const domainOffset = domainFraction * this._domainDenominator;
const domainValue = domainOffset + this._domain.min;
return domainValue;
}
}
export default LinearScale;
/**
*
*/

View File

@ -1,892 +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>
<div v-if="loaded"
class="gl-plot"
:class="[plotLegendExpandedStateClass, plotLegendPositionClass]"
>
<plot-legend :cursor-locked="!!lockHighlightPoint"
:series="config.series.models"
:highlights="highlights"
:legend="config.legend"
/>
<div class="plot-wrapper-axis-and-display-area flex-elem grows">
<y-axis v-if="config.series.models.length > 0"
:tick-width="tickWidth"
:single-series="config.series.models.length === 1"
:series-model="config.series.models[0]"
@yKeyChanged="setYAxisKey"
@tickWidthChanged="onTickWidthChange"
/>
<div class="gl-plot-wrapper-display-area-and-x-axis"
:style="{
left: (tickWidth + 20) + 'px'
}"
>
<div class="gl-plot-display-area has-local-controls has-cursor-guides">
<div 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>
<mct-ticks v-show="gridLines"
:axis-type="'xAxis'"
:position="'right'"
@plotTickWidth="onTickWidthChange"
/>
<mct-ticks v-show="gridLines"
:axis-type="'yAxis'"
:position="'bottom'"
@plotTickWidth="onTickWidthChange"
/>
<div ref="chartContainer"
class="gl-plot-chart-wrapper"
>
<mct-chart :series-config="config"
:rectangles="rectangles"
:highlights="highlights"
@plotReinitializeCanvas="initCanvas"
/>
</div>
<div class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover">
<div class="c-button-set c-button-set--strip-h">
<button class="c-button icon-minus"
title="Zoom out"
@click="zoom('out', 0.2)"
>
</button>
<button class="c-button icon-plus"
title="Zoom in"
@click="zoom('in', 0.2)"
>
</button>
</div>
<div class="c-button-set c-button-set--strip-h"
:disabled="!plotHistory.length"
>
<button class="c-button icon-arrow-left"
title="Restore previous pan/zoom"
@click="back()"
>
</button>
<button class="c-button icon-reset"
title="Reset pan/zoom"
@click="clear()"
>
</button>
</div>
</div>
<!--Cursor guides-->
<div v-show="cursorGuide"
ref="cursorGuideVertical"
class="c-cursor-guide--v js-cursor-guide--v"
>
</div>
<div v-show="cursorGuide"
ref="cursorGuideHorizontal"
class="c-cursor-guide--h js-cursor-guide--h"
>
</div>
</div>
<x-axis v-if="config.series.models.length > 0"
:series-model="config.series.models[0]"
/>
</div>
</div>
</div>
</template>
<script>
import eventHelpers from './lib/eventHelpers';
import LinearScale from "./LinearScale";
import PlotConfigurationModel from './configuration/PlotConfigurationModel';
import configStore from './configuration/configStore';
import PlotLegend from "./legend/PlotLegend.vue";
import MctTicks from "./MctTicks.vue";
import MctChart from "./chart/MctChart.vue";
import XAxis from "./axis/XAxis.vue";
import YAxis from "./axis/YAxis.vue";
export default {
components: {
XAxis,
YAxis,
PlotLegend,
MctTicks,
MctChart
},
inject: ['openmct', 'domainObject'],
props: {
gridLines: {
type: Boolean,
default() {
return true;
}
},
cursorGuide: {
type: Boolean,
default() {
return true;
}
},
plotTickWidth: {
type: Number,
default() {
return 0;
}
}
},
data() {
return {
highlights: [],
lockHighlightPoint: false,
tickWidth: 0,
yKeyOptions: [],
yAxisLabel: '',
rectangles: [],
plotHistory: [],
selectedXKeyOption: {},
xKeyOptions: [],
config: {},
pending: 0,
loaded: false
};
},
computed: {
plotLegendPositionClass() {
return `plot-legend-${this.config.legend.get('position')}`;
},
plotLegendExpandedStateClass() {
if (this.config.legend.get('expanded')) {
return 'plot-legend-expanded';
} else {
return 'plot-legend-collapsed';
}
}
},
watch: {
plotTickWidth(newTickWidth) {
this.onTickWidthChange(newTickWidth, true);
},
gridLines(newGridLines) {
this.setGridLinesVisibility(newGridLines);
},
cursorGuide(newCursorGuide) {
this.setCursorGuideVisibility(newCursorGuide);
}
},
mounted() {
eventHelpers.extend(this);
this.config = this.getConfig();
this.listenTo(this.config.series, 'add', this.addSeries, this);
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
this.config.series.models.forEach(this.addSeries, this);
this.filterObserver = this.openmct.objects.observe(
this.domainObject,
'configuration.filters',
this.updateFiltersAndResubscribe
);
this.openmct.objectViews.on('clearData', this.clearData);
this.followTimeConductor();
this.loaded = true;
//We're referencing the canvas elements from the mct-chart in the initialize method.
// So we need $nextTick to ensure the component is fully mounted before we can initialize stuff.
this.$nextTick(this.initialize);
},
beforeDestroy() {
this.destroy();
},
methods: {
followTimeConductor() {
this.openmct.time.on('bounds', this.updateDisplayBounds);
this.synchronized(true);
},
getConfig() {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
let config = configStore.get(configId);
if (!config) {
config = new PlotConfigurationModel({
id: configId,
domainObject: this.domainObject,
openmct: this.openmct
});
configStore.add(configId, config);
}
return config;
},
addSeries(series) {
this.listenTo(series, 'change:xKey', (xKey) => {
this.setDisplayRange(series, xKey);
}, this);
this.listenTo(series, 'change:yKey', () => {
this.loadSeriesData(series);
}, this);
this.listenTo(series, 'change:interpolate', () => {
this.loadSeriesData(series);
}, this);
this.loadSeriesData(series);
},
removeSeries(plotSeries) {
this.stopListening(plotSeries);
},
loadSeriesData(series) {
if (this.$parent.$refs.plotWrapper.offsetWidth === 0) {
this.scheduleLoad(series);
return;
}
this.startLoading();
const options = {
size: this.$parent.$refs.plotWrapper.offsetWidth,
domain: this.config.xAxis.get('key')
};
series.load(options)
.then(this.stopLoading.bind(this));
},
loadMoreData(range, purge) {
this.config.series.forEach(plotSeries => {
this.startLoading();
plotSeries.load({
size: this.$parent.$refs.plotWrapper.offsetWidth,
start: range.min,
end: range.max,
domain: this.config.xAxis.get('key')
})
.then(this.stopLoading());
if (purge) {
plotSeries.purgeRecordsOutsideRange(range);
}
});
},
scheduleLoad(series) {
if (!this.scheduledLoads) {
this.startLoading();
this.scheduledLoads = [];
this.checkForSize = setInterval(function () {
if (this.$parent.$refs.plotWrapper.offsetWidth === 0) {
return;
}
this.stopLoading();
this.scheduledLoads.forEach(this.loadSeriesData, this);
delete this.scheduledLoads;
clearInterval(this.checkForSize);
delete this.checkForSize;
}.bind(this));
}
if (this.scheduledLoads.indexOf(series) === -1) {
this.scheduledLoads.push(series);
}
},
startLoading() {
this.pending += 1;
this.updateLoading();
},
stopLoading() {
//TODO: Is Vue.$nextTick ok to replace $scope.$evalAsync?
this.$nextTick().then(() => {
this.pending -= 1;
this.updateLoading();
});
},
updateLoading() {
this.$emit('loadingUpdated', this.pending > 0);
},
updateFiltersAndResubscribe(updatedFilters) {
this.config.series.forEach(function (series) {
series.updateFiltersAndRefresh(updatedFilters[series.keyString]);
});
},
clearData() {
this.config.series.forEach(function (series) {
series.reset();
});
},
setDisplayRange(series, xKey) {
if (this.config.series.length !== 1) {
return;
}
const displayRange = series.getDisplayRange(xKey);
this.config.xAxis.set('range', displayRange);
},
/**
* Track latest display bounds. Forces update when not receiving ticks.
*/
updateDisplayBounds(bounds, isTick) {
const newRange = {
min: bounds.start,
max: bounds.end
};
this.config.xAxis.set('range', newRange);
if (!isTick) {
this.skipReloadOnInteraction = true;
this.clear();
this.skipReloadOnInteraction = false;
this.loadMoreData(newRange, true);
} else {
// Drop any data that is more than 1x (max-min) before min.
// Limit these purges to once a second.
if (!this.nextPurge || this.nextPurge < Date.now()) {
const keepRange = {
min: newRange.min - (newRange.max - newRange.min),
max: newRange.max
};
this.config.series.forEach(function (series) {
series.purgeRecordsOutsideRange(keepRange);
});
this.nextPurge = Date.now() + 1000;
}
}
},
/**
* Handle end of user viewport change: load more data for current display
* bounds, and mark view as synchronized if bounds match configured bounds.
*/
userViewportChangeEnd() {
const xDisplayRange = this.config.xAxis.get('displayRange');
const xRange = this.config.xAxis.get('range');
if (!this.skipReloadOnInteraction) {
this.loadMoreData(xDisplayRange);
}
this.synchronized(xRange.min === xDisplayRange.min
&& xRange.max === xDisplayRange.max);
},
/**
* Getter/setter for "synchronized" value. If not synchronized and
* time conductor is in clock mode, will mark objects as unsynced so that
* displays can update accordingly.
*/
synchronized(value) {
if (typeof value !== 'undefined') {
this._synchronized = value;
const isUnsynced = !value && this.openmct.time.clock();
const domainObject = this.openmct.legacyObject(this.domainObject);
if (domainObject.getCapability('status')) {
domainObject.getCapability('status')
.set('timeconductor-unsynced', isUnsynced);
}
}
return this._synchronized;
},
initCanvas() {
if (this.canvas) {
this.stopListening(this.canvas);
}
this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1];
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
},
initialize() {
// Setup canvas etc.
this.xScale = new LinearScale(this.config.xAxis.get('displayRange'));
this.yScale = new LinearScale(this.config.yAxis.get('displayRange'));
this.pan = undefined;
this.marquee = undefined;
this.chartElementBounds = undefined;
this.tickUpdate = false;
this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1];
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
this.config.yAxisLabel = this.config.yAxis.get('label');
this.cursorGuideVertical = this.$refs.cursorGuideVertical;
this.cursorGuideHorizontal = this.$refs.cursorGuideHorizontal;
this.listenTo(this.config.xAxis, 'change:displayRange', this.onXAxisChange, this);
this.listenTo(this.config.yAxis, 'change:displayRange', this.onYAxisChange, this);
},
onXAxisChange(displayBounds) {
if (displayBounds) {
this.xScale.domain(displayBounds);
}
},
onYAxisChange(displayBounds) {
if (displayBounds) {
this.yScale.domain(displayBounds);
}
},
onTickWidthChange(width, fromDifferentObject) {
if (fromDifferentObject) {
// Always accept tick width if it comes from a different object.
this.tickWidth = width;
} else {
// Otherwise, only accept tick with if it's larger.
const newWidth = Math.max(width, this.tickWidth);
if (newWidth !== this.tickWidth) {
this.tickWidth = newWidth;
}
}
this.$emit('plotTickWidth', this.tickWidth);
},
trackMousePosition(event) {
this.trackChartElementBounds(event);
this.xScale.range({
min: 0,
max: this.chartElementBounds.width
});
this.yScale.range({
min: 0,
max: this.chartElementBounds.height
});
this.positionOverElement = {
x: event.clientX - this.chartElementBounds.left,
y: this.chartElementBounds.height
- (event.clientY - this.chartElementBounds.top)
};
this.positionOverPlot = {
x: this.xScale.invert(this.positionOverElement.x),
y: this.yScale.invert(this.positionOverElement.y)
};
if (this.cursorGuide) {
this.updateCrosshairs(event);
}
this.highlightValues(this.positionOverPlot.x);
this.updateMarquee();
this.updatePan();
event.preventDefault();
},
updateCrosshairs(event) {
this.cursorGuideVertical.style.left = (event.clientX - this.chartElementBounds.x) + 'px';
this.cursorGuideHorizontal.style.top = (event.clientY - this.chartElementBounds.y) + 'px';
},
trackChartElementBounds(event) {
if (event.target === this.canvas) {
this.chartElementBounds = event.target.getBoundingClientRect();
}
},
onPlotHighlightSet($e, point) {
if (point === this.highlightPoint) {
return;
}
this.highlightValues(point);
},
highlightValues(point) {
this.highlightPoint = point;
// TODO: used in StackedPlotController
this.$emit('plotHighlightUpdate', point);
if (this.lockHighlightPoint) {
return;
}
if (!point) {
this.highlights = [];
this.config.series.models.forEach(series => delete series.closest);
} else {
this.highlights = this.config.series.models
.filter(series => series.data.length > 0)
.map(series => {
series.closest = series.nearestPoint(point);
return {
series: series,
point: series.closest
};
});
}
},
untrackMousePosition() {
this.positionOverElement = undefined;
this.positionOverPlot = undefined;
this.highlightValues();
},
onMouseDown(event) {
// do not monitor drag events on browser context click
if (event.ctrlKey) {
return;
}
this.listenTo(window, 'mouseup', this.onMouseUp, this);
this.listenTo(window, 'mousemove', this.trackMousePosition, this);
if (event.altKey) {
return this.startPan(event);
} else {
return this.startMarquee(event);
}
},
onMouseUp(event) {
this.stopListening(window, 'mouseup', this.onMouseUp, this);
this.stopListening(window, 'mousemove', this.trackMousePosition, this);
if (this.isMouseClick()) {
this.lockHighlightPoint = !this.lockHighlightPoint;
}
if (this.pan) {
return this.endPan(event);
}
if (this.marquee) {
return this.endMarquee(event);
}
},
isMouseClick() {
if (!this.marquee) {
return false;
}
const { start, end } = this.marquee;
return start.x === end.x && start.y === end.y;
},
updateMarquee() {
if (!this.marquee) {
return;
}
this.marquee.end = this.positionOverPlot;
this.marquee.endPixels = this.positionOverElement;
},
startMarquee(event) {
this.canvas.classList.remove('plot-drag');
this.canvas.classList.add('plot-marquee');
this.trackMousePosition(event);
if (this.positionOverPlot) {
this.freeze();
this.marquee = {
startPixels: this.positionOverElement,
endPixels: this.positionOverElement,
start: this.positionOverPlot,
end: this.positionOverPlot,
color: [1, 1, 1, 0.5]
};
this.rectangles.push(this.marquee);
this.trackHistory();
}
},
endMarquee() {
const startPixels = this.marquee.startPixels;
const endPixels = this.marquee.endPixels;
const marqueeDistance = Math.sqrt(
Math.pow(startPixels.x - endPixels.x, 2)
+ Math.pow(startPixels.y - endPixels.y, 2)
);
// Don't zoom if mouse moved less than 7.5 pixels.
if (marqueeDistance > 7.5) {
this.config.xAxis.set('displayRange', {
min: Math.min(this.marquee.start.x, this.marquee.end.x),
max: Math.max(this.marquee.start.x, this.marquee.end.x)
});
this.config.yAxis.set('displayRange', {
min: Math.min(this.marquee.start.y, this.marquee.end.y),
max: Math.max(this.marquee.start.y, this.marquee.end.y)
});
this.userViewportChangeEnd();
} else {
// A history entry is created by startMarquee, need to remove
// if marquee zoom doesn't occur.
this.plotHistory.pop();
}
this.rectangles = [];
this.marquee = undefined;
},
zoom(zoomDirection, zoomFactor) {
const currentXaxis = this.config.xAxis.get('displayRange');
const currentYaxis = this.config.yAxis.get('displayRange');
// when there is no plot data, the ranges can be undefined
// in which case we should not perform zoom
if (!currentXaxis || !currentYaxis) {
return;
}
this.freeze();
this.trackHistory();
const xAxisDist = (currentXaxis.max - currentXaxis.min) * zoomFactor;
const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor;
if (zoomDirection === 'in') {
this.config.xAxis.set('displayRange', {
min: currentXaxis.min + xAxisDist,
max: currentXaxis.max - xAxisDist
});
this.config.yAxis.set('displayRange', {
min: currentYaxis.min + yAxisDist,
max: currentYaxis.max - yAxisDist
});
} else if (zoomDirection === 'out') {
this.config.xAxis.set('displayRange', {
min: currentXaxis.min - xAxisDist,
max: currentXaxis.max + xAxisDist
});
this.config.yAxis.set('displayRange', {
min: currentYaxis.min - yAxisDist,
max: currentYaxis.max + yAxisDist
});
}
this.userViewportChangeEnd();
},
wheelZoom(event) {
const ZOOM_AMT = 0.1;
event.preventDefault();
if (!this.positionOverPlot) {
return;
}
let xDisplayRange = this.config.xAxis.get('displayRange');
let yDisplayRange = this.config.yAxis.get('displayRange');
// when there is no plot data, the ranges can be undefined
// in which case we should not perform zoom
if (!xDisplayRange || !yDisplayRange) {
return;
}
this.freeze();
window.clearTimeout(this.stillZooming);
let xAxisDist = (xDisplayRange.max - xDisplayRange.min);
let yAxisDist = (yDisplayRange.max - yDisplayRange.min);
let xDistMouseToMax = xDisplayRange.max - this.positionOverPlot.x;
let xDistMouseToMin = this.positionOverPlot.x - xDisplayRange.min;
let yDistMouseToMax = yDisplayRange.max - this.positionOverPlot.y;
let yDistMouseToMin = this.positionOverPlot.y - yDisplayRange.min;
let xAxisMaxDist = xDistMouseToMax / xAxisDist;
let xAxisMinDist = xDistMouseToMin / xAxisDist;
let yAxisMaxDist = yDistMouseToMax / yAxisDist;
let yAxisMinDist = yDistMouseToMin / yAxisDist;
let plotHistoryStep;
if (!plotHistoryStep) {
plotHistoryStep = {
x: xDisplayRange,
y: yDisplayRange
};
}
if (event.wheelDelta < 0) {
this.config.xAxis.set('displayRange', {
min: xDisplayRange.min + ((xAxisDist * ZOOM_AMT) * xAxisMinDist),
max: xDisplayRange.max - ((xAxisDist * ZOOM_AMT) * xAxisMaxDist)
});
this.config.yAxis.set('displayRange', {
min: yDisplayRange.min + ((yAxisDist * ZOOM_AMT) * yAxisMinDist),
max: yDisplayRange.max - ((yAxisDist * ZOOM_AMT) * yAxisMaxDist)
});
} else if (event.wheelDelta >= 0) {
this.config.xAxis.set('displayRange', {
min: xDisplayRange.min - ((xAxisDist * ZOOM_AMT) * xAxisMinDist),
max: xDisplayRange.max + ((xAxisDist * ZOOM_AMT) * xAxisMaxDist)
});
this.config.yAxis.set('displayRange', {
min: yDisplayRange.min - ((yAxisDist * ZOOM_AMT) * yAxisMinDist),
max: yDisplayRange.max + ((yAxisDist * ZOOM_AMT) * yAxisMaxDist)
});
}
this.stillZooming = window.setTimeout(function () {
this.plotHistory.push(plotHistoryStep);
plotHistoryStep = undefined;
this.userViewportChangeEnd();
}.bind(this), 250);
},
startPan(event) {
this.canvas.classList.add('plot-drag');
this.canvas.classList.remove('plot-marquee');
this.trackMousePosition(event);
this.freeze();
this.pan = {
start: this.positionOverPlot
};
event.preventDefault();
this.trackHistory();
return false;
},
updatePan() {
// calculate offset between points. Apply that offset to viewport.
if (!this.pan) {
return;
}
const dX = this.pan.start.x - this.positionOverPlot.x;
const dY = this.pan.start.y - this.positionOverPlot.y;
const xRange = this.config.xAxis.get('displayRange');
const yRange = this.config.yAxis.get('displayRange');
this.config.xAxis.set('displayRange', {
min: xRange.min + dX,
max: xRange.max + dX
});
this.config.yAxis.set('displayRange', {
min: yRange.min + dY,
max: yRange.max + dY
});
},
trackHistory() {
this.plotHistory.push({
x: this.config.xAxis.get('displayRange'),
y: this.config.yAxis.get('displayRange')
});
},
endPan() {
this.pan = undefined;
this.userViewportChangeEnd();
},
freeze() {
this.config.yAxis.set('frozen', true);
this.config.xAxis.set('frozen', true);
},
clear() {
this.config.yAxis.set('frozen', false);
this.config.xAxis.set('frozen', false);
this.plotHistory = [];
this.userViewportChangeEnd();
},
back() {
const previousAxisRanges = this.plotHistory.pop();
if (this.plotHistory.length === 0) {
this.clear();
return;
}
this.config.xAxis.set('displayRange', previousAxisRanges.x);
this.config.yAxis.set('displayRange', previousAxisRanges.y);
this.userViewportChangeEnd();
},
setCursorGuideVisibility(cursorGuide) {
this.cursorGuide = cursorGuide === true;
},
setGridLinesVisibility(gridLines) {
this.gridLines = gridLines === true;
},
setYAxisKey(yKey) {
this.config.series.models[0].emit('change:yKey', yKey);
},
destroy() {
configStore.deleteStore(this.config.id);
this.stopListening();
if (this.checkForSize) {
clearInterval(this.checkForSize);
delete this.checkForSize;
}
if (this.filterObserver) {
this.filterObserver();
}
}
}
};
</script>

View File

@ -1,269 +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>
<div ref="tickContainer"
class="u-contents js-ticks"
>
<div v-if="position === 'left'"
class="gl-plot-tick-wrapper"
>
<div v-for="tick in ticks"
:key="tick.value"
class="gl-plot-tick gl-plot-x-tick-label"
:style="{
left: (100 * (tick.value - min) / interval) + '%'
}"
:title="tick.fullText || tick.text"
>
{{ tick.text }}
</div>
</div>
<div v-if="position === 'top'"
class="gl-plot-tick-wrapper"
>
<div v-for="tick in ticks"
:key="tick.value"
class="gl-plot-tick gl-plot-y-tick-label"
:style="{ top: (100 * (max - tick.value) / interval) + '%' }"
:title="tick.fullText || tick.text"
style="margin-top: -0.50em; direction: ltr;"
>
<span>{{ tick.text }}</span>
</div>
</div>
<!-- grid lines follow -->
<template v-if="position === 'right'">
<div v-for="tick in ticks"
:key="tick.value"
class="gl-plot-hash hash-v"
:style="{
right: (100 * (max - tick.value) / interval) + '%',
height: '100%'
}"
>
</div>
</template>
<template v-if="position === 'bottom'">
<div v-for="tick in ticks"
:key="tick.value"
class="gl-plot-hash hash-h"
:style="{ bottom: (100 * (tick.value - min) / interval) + '%', width: '100%' }"
>
</div>
</template>
</div>
</template>
<script>
import eventHelpers from "./lib/eventHelpers";
import { ticks, commonPrefix, commonSuffix } from "./tickUtils";
import configStore from "./configuration/configStore";
export default {
inject: ['openmct', 'domainObject'],
props: {
axisType: {
type: String,
default() {
return '';
},
required: true
},
position: {
required: true,
type: String,
default() {
return '';
}
}
},
data() {
return {
ticks: []
};
},
mounted() {
eventHelpers.extend(this);
this.axis = this.getAxisFromConfig();
this.tickCount = 4;
this.tickUpdate = false;
this.listenTo(this.axis, 'change:displayRange', this.updateTicks, this);
this.listenTo(this.axis, 'change:format', this.updateTicks, this);
this.listenTo(this.axis, 'change:key', this.updateTicksForceRegeneration, this);
this.updateTicks();
},
beforeDestroy() {
this.stopListening();
},
methods: {
getAxisFromConfig() {
if (!this.axisType) {
return;
}
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
let config = configStore.get(configId);
if (config) {
return config[this.axisType];
}
},
/**
* Determine whether ticks should be regenerated for a given range.
* Ticks are updated
* a) if they don't exist,
* b) if existing ticks are outside of given range,
* c) if range exceeds size of tick range by more than one tick step,
* d) if forced to regenerate (ex. changing x-axis metadata).
*
*/
shouldRegenerateTicks(range, forceRegeneration) {
if (forceRegeneration) {
return true;
}
if (!this.tickRange || !this.ticks || !this.ticks.length) {
return true;
}
if (this.tickRange.max > range.max || this.tickRange.min < range.min) {
return true;
}
if (Math.abs(range.max - this.tickRange.max) > this.tickRange.step) {
return true;
}
if (Math.abs(this.tickRange.min - range.min) > this.tickRange.step) {
return true;
}
return false;
},
getTicks() {
const number = this.tickCount;
const clampRange = this.axis.get('values');
const range = this.axis.get('displayRange');
if (clampRange) {
return clampRange.filter(function (value) {
return value <= range.max && value >= range.min;
}, this);
}
return ticks(range.min, range.max, number);
},
updateTicksForceRegeneration() {
this.updateTicks(true);
},
updateTicks(forceRegeneration = false) {
const range = this.axis.get('displayRange');
if (!range) {
delete this.min;
delete this.max;
delete this.interval;
delete this.tickRange;
this.ticks = [];
delete this.shouldCheckWidth;
return;
}
const format = this.axis.get('format');
if (!format) {
return;
}
this.min = range.min;
this.max = range.max;
this.interval = Math.abs(range.min - range.max);
if (this.shouldRegenerateTicks(range, forceRegeneration)) {
let newTicks = this.getTicks();
this.tickRange = {
min: Math.min.apply(Math, newTicks),
max: Math.max.apply(Math, newTicks),
step: newTicks[1] - newTicks[0]
};
newTicks = newTicks
.map(function (tickValue) {
return {
value: tickValue,
text: format(tickValue)
};
}, this);
if (newTicks.length && typeof newTicks[0].text === 'string') {
const tickText = newTicks.map(function (t) {
return t.text;
});
const prefix = tickText.reduce(commonPrefix);
const suffix = tickText.reduce(commonSuffix);
newTicks.forEach(function (t) {
t.fullText = t.text;
if (suffix.length) {
t.text = t.text.slice(prefix.length, -suffix.length);
} else {
t.text = t.text.slice(prefix.length);
}
});
}
this.ticks = newTicks;
this.shouldCheckWidth = true;
}
this.scheduleTickUpdate();
},
scheduleTickUpdate() {
if (this.tickUpdate) {
return;
}
this.tickUpdate = true;
setTimeout(this.doTickUpdate.bind(this), 0);
},
doTickUpdate() {
if (this.shouldCheckWidth) {
const tickElements = this.$refs.tickContainer.querySelectorAll('.gl-plot-tick > span');
const tickWidth = Number([].reduce.call(tickElements, function (memo, first) {
return Math.max(memo, first.offsetWidth);
}, 0));
this.tickWidth = tickWidth;
this.$emit('plotTickWidth', tickWidth);
this.shouldCheckWidth = false;
}
this.tickUpdate = false;
}
}
};
</script>

View File

@ -1,125 +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>
<div ref="plotWrapper"
class="c-plot holder holder-plot has-control-bar"
>
<div class="c-control-bar">
<span class="c-button-set c-button-set--strip-h">
<button class="c-button icon-download"
title="Export This View's Data as PNG"
@click="exportPNG()"
>
<span class="c-button__label">PNG</span>
</button>
<button class="c-button"
title="Export This View's Data as JPG"
@click="exportJPG()"
>
<span class="c-button__label">JPG</span>
</button>
</span>
<button class="c-button icon-crosshair"
:class="{ 'is-active': cursorGuide }"
title="Toggle cursor guides"
@click="toggleCursorGuide"
>
</button>
<button class="c-button"
:class="{ 'icon-grid-on': gridLines, 'icon-grid-off': !gridLines }"
title="Toggle grid lines"
@click="toggleGridLines"
>
</button>
</div>
<div ref="plotContainer"
class="l-view-section u-style-receiver js-style-receiver"
>
<div v-show="!!loading"
class="c-loading--overlay loading"
></div>
<mct-plot :grid-lines="gridLines"
:cursor-guide="cursorGuide"
@loadingUpdated="loadingUpdated"
/>
</div>
</div>
</template>
<script>
import eventHelpers from "./lib/eventHelpers";
import MctPlot from './MctPlot.vue';
export default {
components: {
MctPlot
},
inject: ['openmct', 'domainObject'],
data() {
return {
//Don't think we need this as it appears to be stacked plot specific
// hideExportButtons: false
cursorGuide: false,
gridLines: true,
loading: false
};
},
mounted() {
eventHelpers.extend(this);
this.exportImageService = this.openmct.$injector.get('exportImageService');
},
beforeDestroy() {
this.destroy();
},
methods: {
loadingUpdated(loading) {
this.loading = loading;
},
destroy() {
this.stopListening();
},
exportJPG() {
const plotElement = this.$refs.plotContainer;
this.exportImageService.exportJPG(plotElement, 'plot.jpg', 'export-plot');
},
exportPNG() {
const plotElement = this.$refs.plotContainer;
this.exportImageService.exportPNG(plotElement, 'plot.png', 'export-plot');
},
toggleCursorGuide() {
this.cursorGuide = !this.cursorGuide;
},
toggleGridLines() {
this.gridLines = !this.gridLines;
}
}
};
</script>

View File

@ -1,74 +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.
*****************************************************************************/
import Plot from './Plot.vue';
import Vue from 'vue';
export default function PlotViewProvider(openmct) {
function hasTelemetry(domainObject) {
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {
return false;
}
let metadata = openmct.telemetry.getMetadata(domainObject);
return metadata.values().length > 0 && hasDomainAndRange(metadata);
}
function hasDomainAndRange(metadata) {
return (metadata.valuesForHints(['range']).length > 0
&& metadata.valuesForHints(['domain']).length > 0);
}
return {
key: 'plot-single',
name: 'Plot',
cssClass: 'icon-telemetry',
canView(domainObject) {
return domainObject.type === 'plot-single' || hasTelemetry(domainObject);
},
view: function (domainObject) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
Plot
},
provide: {
openmct,
domainObject
},
template: '<plot></plot>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
}
};
}

View File

@ -1,151 +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>
<div v-if="loaded"
class="gl-plot-axis-area gl-plot-x has-local-controls"
>
<mct-ticks :axis-type="'xAxis'"
:position="'left'"
@plotTickWidth="onTickWidthChange"
/>
<div
class="gl-plot-label gl-plot-x-label"
:class="{'icon-gear': isEnabledXKeyToggle()}"
>
{{ xAxisLabel }}
</div>
<select
v-show="isEnabledXKeyToggle()"
v-model="selectedXKeyOptionKey"
class="gl-plot-x-label__select local-controls--hidden"
@change="toggleXKeyOption()"
>
<option v-for="option in xKeyOptions"
:key="option.key"
:value="option.key"
>{{ option.name }}
</option>
</select>
</div>
</template>
<script>
import MctTicks from "../MctTicks.vue";
import eventHelpers from '../lib/eventHelpers';
import configStore from "../configuration/configStore";
export default {
components: {
MctTicks
},
inject: ['openmct', 'domainObject'],
props: {
seriesModel: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
selectedXKeyOptionKey: '',
xKeyOptions: [],
xAxis: {},
loaded: false,
xAxisLabel: ''
};
},
mounted() {
eventHelpers.extend(this);
this.xAxis = this.getXAxisFromConfig();
this.loaded = true;
this.setUpXAxisOptions();
this.openmct.time.on('timeSystem', this.syncXAxisToTimeSystem);
this.listenTo(this.xAxis, 'change', this.setUpXAxisOptions);
},
beforeDestroy() {
this.openmct.time.off('timeSystem', this.syncXAxisToTimeSystem);
},
methods: {
isEnabledXKeyToggle() {
const isSinglePlot = this.xKeyOptions && this.xKeyOptions.length > 1 && this.seriesModel;
const isFrozen = this.xAxis.get('frozen');
const inRealTimeMode = this.openmct.time.clock();
return isSinglePlot && !isFrozen && !inRealTimeMode;
},
getXAxisFromConfig() {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
let config = configStore.get(configId);
if (config) {
return config.xAxis;
}
},
toggleXKeyOption() {
const selectedXKey = this.selectedXKeyOptionKey;
const dataForSelectedXKey = this.seriesModel.data
? this.seriesModel.data[0][selectedXKey]
: undefined;
if (dataForSelectedXKey !== undefined) {
this.xAxis.set('key', selectedXKey);
} else {
this.openmct.notifications.error('Cannot change x-axis view as no data exists for this view type.');
const xAxisKey = this.xAxis.get('key');
this.selectedXKeyOptionKey = this.getXKeyOption(xAxisKey).key;
}
},
getXKeyOption(key) {
return this.xKeyOptions.find(option => option.key === key);
},
syncXAxisToTimeSystem(timeSystem) {
const xAxisKey = this.xAxis.get('key');
if (xAxisKey !== timeSystem.key) {
this.xAxis.set('key', timeSystem.key);
this.xAxis.resetSeries();
this.setUpXAxisOptions();
}
},
setUpXAxisOptions() {
const xAxisKey = this.xAxis.get('key');
this.xKeyOptions = this.seriesModel.metadata
.valuesForHints(['domain'])
.map(function (o) {
return {
name: o.name,
key: o.key
};
});
this.xAxisLabel = this.xAxis.get('label');
this.selectedXKeyOptionKey = this.getXKeyOption(xAxisKey).key;
},
onTickWidthChange(width) {
this.$emit('tickWidthChanged', width);
}
}
};
</script>

View File

@ -1,137 +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>
<div v-if="loaded"
class="gl-plot-axis-area gl-plot-y has-local-controls"
:style="{
width: (tickWidth + 20) + 'px'
}"
>
<div v-if="singleSeries"
class="gl-plot-label gl-plot-y-label"
:class="{'icon-gear': (yKeyOptions.length > 1)}"
>{{ yAxisLabel }}
</div>
<select v-if="yKeyOptions.length > 1 && singleSeries"
v-model="yAxisLabel"
class="gl-plot-y-label__select local-controls--hidden"
@change="toggleYAxisLabel"
>
<option v-for="(option, index) in yKeyOptions"
:key="index"
:value="option.name"
:selected="option.name === yAxisLabel"
>
{{ option.name }}
</option>
</select>
<mct-ticks :axis-type="'yAxis'"
class="gl-plot-ticks"
:position="'top'"
@plotTickWidth="onTickWidthChange"
/>
</div>
</template>
<script>
import MctTicks from "../MctTicks.vue";
import configStore from "../configuration/configStore";
export default {
components: {
MctTicks
},
inject: ['openmct', 'domainObject'],
props: {
singleSeries: {
type: Boolean,
default() {
return true;
}
},
seriesModel: {
type: Object,
default() {
return {};
}
},
tickWidth: {
type: Number,
default() {
return 0;
}
}
},
data() {
return {
yAxisLabel: 'none',
loaded: false
};
},
mounted() {
this.yAxis = this.getYAxisFromConfig();
this.loaded = true;
this.setUpYAxisOptions();
},
methods: {
getYAxisFromConfig() {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
let config = configStore.get(configId);
if (config) {
return config.yAxis;
}
},
setUpYAxisOptions() {
this.yKeyOptions = this.seriesModel.metadata
.valuesForHints(['range'])
.map(function (o) {
return {
name: o.name,
key: o.key
};
});
// set yAxisLabel if none is set yet
if (this.yAxisLabel === 'none') {
let yKey = this.seriesModel.model.yKey;
let yKeyModel = this.yKeyOptions.filter(o => o.key === yKey)[0];
this.yAxisLabel = yKeyModel.name;
}
},
toggleYAxisLabel() {
let yAxisObject = this.yKeyOptions.filter(o => o.name === this.yAxisLabel)[0];
if (yAxisObject) {
this.$emit('yKeyChanged', yAxisObject.key);
this.yAxis.set('label', this.yAxisLabel);
}
},
onTickWidthChange(width) {
this.$emit('tickWidthChanged', width);
}
}
};
</script>

View File

@ -1,67 +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.
*****************************************************************************/
import eventHelpers from '../lib/eventHelpers';
export default class MCTChartAlarmPointSet {
constructor(series, chart, offset) {
this.series = series;
this.chart = chart;
this.offset = offset;
this.points = [];
eventHelpers.extend(this);
this.listenTo(series, 'add', this.append, this);
this.listenTo(series, 'remove', this.remove, this);
this.listenTo(series, 'reset', this.reset, this);
this.listenTo(series, 'destroy', this.destroy, this);
series.data.forEach(function (point, index) {
this.append(point, index, series);
}, this);
}
append(datum) {
if (datum.mctLimitState) {
this.points.push({
x: this.offset.xVal(datum, this.series),
y: this.offset.yVal(datum, this.series),
datum: datum
});
}
}
remove(datum) {
this.points = this.points.filter(function (p) {
return p.datum !== datum;
});
}
reset() {
this.points = [];
}
destroy() {
this.stopListening();
}
}

View File

@ -1,74 +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.
*****************************************************************************/
import MCTChartSeriesElement from './MCTChartSeriesElement';
export default class MCTChartLineStepAfter extends MCTChartSeriesElement {
removePoint(point, index, count) {
if (index > 0 && index / 2 < this.count) {
this.buffer[index + 1] = this.buffer[index - 1];
}
}
vertexCountForPointAtIndex(index) {
if (index === 0 && this.count === 0) {
return 2;
}
return 4;
}
startIndexForPointAtIndex(index) {
if (index === 0) {
return 0;
}
return 2 + ((index - 1) * 4);
}
addPoint(point, start, count) {
if (start === 0 && this.count === 0) {
// First point is easy.
this.buffer[start] = point.x;
this.buffer[start + 1] = point.y; // one point
} else if (start === 0 && this.count > 0) {
// Unshifting requires adding an extra point.
this.buffer[start] = point.x;
this.buffer[start + 1] = point.y;
this.buffer[start + 2] = this.buffer[start + 4];
this.buffer[start + 3] = point.y;
} else {
// Appending anywhere in line, insert standard two points.
this.buffer[start] = point.x;
this.buffer[start + 1] = this.buffer[start - 1];
this.buffer[start + 2] = point.x;
this.buffer[start + 3] = point.y;
if (start < this.count * 2) {
// Insert into the middle, need to update the following
// point.
this.buffer[start + 5] = point.y;
}
}
}
}

View File

@ -1,32 +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.
*****************************************************************************/
import MCTChartSeriesElement from './MCTChartSeriesElement';
// TODO: Is this needed? This is identical to MCTChartLineLinear. Why is it a different class?
export default class MCTChartPointSet extends MCTChartSeriesElement {
addPoint(point, start, count) {
this.buffer[start] = point.x;
this.buffer[start + 1] = point.y;
}
}

View File

@ -1,157 +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.
*****************************************************************************/
import eventHelpers from '../lib/eventHelpers';
export default class MCTChartSeriesElement {
constructor(series, chart, offset) {
this.series = series;
this.chart = chart;
this.offset = offset;
this.buffer = new Float32Array(20000);
this.count = 0;
eventHelpers.extend(this);
this.listenTo(series, 'add', this.append, this);
this.listenTo(series, 'remove', this.remove, this);
this.listenTo(series, 'reset', this.reset, this);
this.listenTo(series, 'destroy', this.destroy, this);
series.data.forEach(function (point, index) {
this.append(point, index, series);
}, this);
}
getBuffer() {
if (this.isTempBuffer) {
this.buffer = new Float32Array(this.buffer);
this.isTempBuffer = false;
}
return this.buffer;
}
color() {
return this.series.get('color');
}
vertexCountForPointAtIndex(index) {
return 2;
}
startIndexForPointAtIndex(index) {
return 2 * index;
}
removeSegments(index, count) {
const target = index;
const start = index + count;
const end = this.count * 2;
this.buffer.copyWithin(target, start, end);
for (let zero = end - count; zero < end; zero++) {
this.buffer[zero] = 0;
}
}
removePoint(point, index, count) {
// by default, do nothing.
}
remove(point, index, series) {
const vertexCount = this.vertexCountForPointAtIndex(index);
const removalPoint = this.startIndexForPointAtIndex(index);
this.removeSegments(removalPoint, vertexCount);
this.removePoint(
this.makePoint(point, series),
removalPoint,
vertexCount
);
this.count -= (vertexCount / 2);
}
makePoint(point, series) {
if (!this.offset.xVal) {
this.chart.setOffset(point, undefined, series);
}
return {
x: this.offset.xVal(point, series),
y: this.offset.yVal(point, series)
};
}
append(point, index, series) {
const pointsRequired = this.vertexCountForPointAtIndex(index);
const insertionPoint = this.startIndexForPointAtIndex(index);
this.growIfNeeded(pointsRequired);
this.makeInsertionPoint(insertionPoint, pointsRequired);
this.addPoint(
this.makePoint(point, series),
insertionPoint,
pointsRequired
);
this.count += (pointsRequired / 2);
}
makeInsertionPoint(insertionPoint, pointsRequired) {
if (this.count * 2 > insertionPoint) {
if (!this.isTempBuffer) {
this.buffer = Array.prototype.slice.apply(this.buffer);
this.isTempBuffer = true;
}
const target = insertionPoint + pointsRequired;
let start = insertionPoint;
for (; start < target; start++) {
this.buffer.splice(start, 0, 0);
}
}
}
reset() {
this.buffer = new Float32Array(20000);
this.count = 0;
if (this.offset.x) {
this.series.data.forEach(function (point, index) {
this.append(point, index, this.series);
}, this);
}
}
growIfNeeded(pointsRequired) {
const remainingPoints = this.buffer.length - this.count * 2;
let temp;
if (remainingPoints <= pointsRequired) {
temp = new Float32Array(this.buffer.length + 20000);
temp.set(this.buffer);
this.buffer = temp;
}
}
destroy() {
this.stopListening();
}
}

View File

@ -1,449 +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>
<div class="gl-plot-chart-area">
<span v-html="canvasTemplate"></span>
<span v-html="canvasTemplate"></span>
</div>
</template>
<script>
import eventHelpers from "../lib/eventHelpers";
import { DrawLoader } from '../draw/DrawLoader';
import MCTChartLineLinear from './MCTChartLineLinear';
import MCTChartLineStepAfter from './MCTChartLineStepAfter';
import MCTChartPointSet from './MCTChartPointSet';
import MCTChartAlarmPointSet from './MCTChartAlarmPointSet';
import configStore from "../configuration/configStore";
import PlotConfigurationModel from "../configuration/PlotConfigurationModel";
const MARKER_SIZE = 6.0;
const HIGHLIGHT_SIZE = MARKER_SIZE * 2.0;
export default {
inject: ['openmct', 'domainObject'],
props: {
rectangles: {
type: Array,
default() {
return [];
}
},
highlights: {
type: Array,
default() {
return [];
}
}
},
data() {
return {
canvasTemplate: '<canvas style="position: absolute; background: none; width: 100%; height: 100%;"></canvas>'
};
},
watch: {
highlights() {
this.scheduleDraw();
},
rectangles() {
this.scheduleDraw();
}
},
mounted() {
eventHelpers.extend(this);
this.config = this.getConfig();
this.isDestroyed = false;
this.lines = [];
this.pointSets = [];
this.alarmSets = [];
this.offset = {};
this.seriesElements = new WeakMap();
let canvasEls = this.$parent.$refs.chartContainer.querySelectorAll("canvas");
const mainCanvas = canvasEls[1];
const overlayCanvas = canvasEls[0];
if (this.initializeCanvas(mainCanvas, overlayCanvas)) {
this.draw();
}
this.listenTo(this.config.series, 'add', this.onSeriesAdd, this);
this.listenTo(this.config.series, 'remove', this.onSeriesRemove, this);
this.listenTo(this.config.yAxis, 'change:key', this.clearOffset, this);
this.listenTo(this.config.yAxis, 'change', this.scheduleDraw);
this.listenTo(this.config.xAxis, 'change', this.scheduleDraw);
this.config.series.forEach(this.onSeriesAdd, this);
},
beforeDestroy() {
this.destroy();
},
methods: {
getConfig() {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
let config = configStore.get(configId);
if (!config) {
config = new PlotConfigurationModel({
id: configId,
domainObject: this.domainObject,
openmct: this.openmct
});
configStore.add(configId, config);
}
return config;
},
reDraw(mode, o, series) {
this.changeInterpolate(mode, o, series);
this.changeMarkers(mode, o, series);
this.changeAlarmMarkers(mode, o, series);
},
onSeriesAdd(series) {
this.listenTo(series, 'change:xKey', this.reDraw, this);
this.listenTo(series, 'change:interpolate', this.changeInterpolate, this);
this.listenTo(series, 'change:markers', this.changeMarkers, this);
this.listenTo(series, 'change:alarmMarkers', this.changeAlarmMarkers, this);
this.listenTo(series, 'change', this.scheduleDraw);
this.listenTo(series, 'add', this.scheduleDraw);
this.makeChartElement(series);
},
changeInterpolate(mode, o, series) {
if (mode === o) {
return;
}
const elements = this.seriesElements.get(series);
elements.lines.forEach(function (line) {
this.lines.splice(this.lines.indexOf(line), 1);
line.destroy();
}, this);
elements.lines = [];
const newLine = this.lineForSeries(series);
if (newLine) {
elements.lines.push(newLine);
this.lines.push(newLine);
}
},
changeAlarmMarkers(mode, o, series) {
if (mode === o) {
return;
}
const elements = this.seriesElements.get(series);
if (elements.alarmSet) {
elements.alarmSet.destroy();
this.alarmSets.splice(this.alarmSets.indexOf(elements.alarmSet), 1);
}
elements.alarmSet = this.alarmPointSetForSeries(series);
if (elements.alarmSet) {
this.alarmSets.push(elements.alarmSet);
}
},
changeMarkers(mode, o, series) {
if (mode === o) {
return;
}
const elements = this.seriesElements.get(series);
elements.pointSets.forEach(function (pointSet) {
this.pointSets.splice(this.pointSets.indexOf(pointSet), 1);
pointSet.destroy();
}, this);
elements.pointSets = [];
const pointSet = this.pointSetForSeries(series);
if (pointSet) {
elements.pointSets.push(pointSet);
this.pointSets.push(pointSet);
}
},
onSeriesRemove(series) {
this.stopListening(series);
this.removeChartElement(series);
this.scheduleDraw();
},
destroy() {
this.isDestroyed = true;
this.stopListening();
this.lines.forEach(line => line.destroy());
DrawLoader.releaseDrawAPI(this.drawAPI);
},
clearOffset() {
delete this.offset.x;
delete this.offset.y;
delete this.offset.xVal;
delete this.offset.yVal;
delete this.offset.xKey;
delete this.offset.yKey;
this.lines.forEach(function (line) {
line.reset();
});
this.pointSets.forEach(function (pointSet) {
pointSet.reset();
});
},
setOffset(offsetPoint, index, series) {
if (this.offset.x && this.offset.y) {
return;
}
const offsets = {
x: series.getXVal(offsetPoint),
y: series.getYVal(offsetPoint)
};
this.offset.x = function (x) {
return x - offsets.x;
}.bind(this);
this.offset.y = function (y) {
return y - offsets.y;
}.bind(this);
this.offset.xVal = function (point, pSeries) {
return this.offset.x(pSeries.getXVal(point));
}.bind(this);
this.offset.yVal = function (point, pSeries) {
return this.offset.y(pSeries.getYVal(point));
}.bind(this);
},
initializeCanvas(canvas, overlay) {
this.canvas = canvas;
this.overlay = overlay;
this.drawAPI = DrawLoader.getDrawAPI(canvas, overlay);
if (this.drawAPI) {
this.listenTo(this.drawAPI, 'error', this.fallbackToCanvas, this);
}
return Boolean(this.drawAPI);
},
fallbackToCanvas() {
this.stopListening(this.drawAPI);
DrawLoader.releaseDrawAPI(this.drawAPI);
// Have to throw away the old canvas elements and replace with new
// canvas elements in order to get new drawing contexts.
const div = document.createElement('div');
div.innerHTML = this.TEMPLATE;
const mainCanvas = div.querySelectorAll("canvas")[1];
const overlayCanvas = div.querySelectorAll("canvas")[0];
this.canvas.parentNode.replaceChild(mainCanvas, this.canvas);
this.canvas = mainCanvas;
this.overlay.parentNode.replaceChild(overlayCanvas, this.overlay);
this.overlay = overlayCanvas;
this.drawAPI = DrawLoader.getFallbackDrawAPI(this.canvas, this.overlay);
this.$emit('plotReinitializeCanvas');
},
removeChartElement(series) {
const elements = this.seriesElements.get(series);
elements.lines.forEach(function (line) {
this.lines.splice(this.lines.indexOf(line), 1);
line.destroy();
}, this);
elements.pointSets.forEach(function (pointSet) {
this.pointSets.splice(this.pointSets.indexOf(pointSet), 1);
pointSet.destroy();
}, this);
if (elements.alarmSet) {
elements.alarmSet.destroy();
this.alarmSets.splice(this.alarmSets.indexOf(elements.alarmSet), 1);
}
this.seriesElements.delete(series);
},
lineForSeries(series) {
if (series.get('interpolate') === 'linear') {
return new MCTChartLineLinear(
series,
this,
this.offset
);
}
if (series.get('interpolate') === 'stepAfter') {
return new MCTChartLineStepAfter(
series,
this,
this.offset
);
}
},
pointSetForSeries(series) {
if (series.get('markers')) {
return new MCTChartPointSet(
series,
this,
this.offset
);
}
},
alarmPointSetForSeries(series) {
if (series.get('alarmMarkers')) {
return new MCTChartAlarmPointSet(
series,
this,
this.offset
);
}
},
makeChartElement(series) {
const elements = {
lines: [],
pointSets: []
};
const line = this.lineForSeries(series);
if (line) {
elements.lines.push(line);
this.lines.push(line);
}
const pointSet = this.pointSetForSeries(series);
if (pointSet) {
elements.pointSets.push(pointSet);
this.pointSets.push(pointSet);
}
elements.alarmSet = this.alarmPointSetForSeries(series);
if (elements.alarmSet) {
this.alarmSets.push(elements.alarmSet);
}
this.seriesElements.set(series, elements);
},
canDraw() {
if (!this.offset.x || !this.offset.y) {
return false;
}
return true;
},
scheduleDraw() {
if (!this.drawScheduled) {
requestAnimationFrame(this.draw);
this.drawScheduled = true;
}
},
draw() {
this.drawScheduled = false;
if (this.isDestroyed) {
return;
}
this.drawAPI.clear();
if (this.canDraw()) {
this.updateViewport();
this.drawSeries();
this.drawRectangles();
this.drawHighlights();
}
},
updateViewport() {
const xRange = this.config.xAxis.get('displayRange');
const yRange = this.config.yAxis.get('displayRange');
if (!xRange || !yRange) {
return;
}
const dimensions = [
xRange.max - xRange.min,
yRange.max - yRange.min
];
const origin = [
this.offset.x(xRange.min),
this.offset.y(yRange.min)
];
this.drawAPI.setDimensions(
dimensions,
origin
);
},
drawSeries() {
this.lines.forEach(this.drawLine, this);
this.pointSets.forEach(this.drawPoints, this);
this.alarmSets.forEach(this.drawAlarmPoints, this);
},
drawAlarmPoints(alarmSet) {
this.drawAPI.drawLimitPoints(
alarmSet.points,
alarmSet.series.get('color').asRGBAArray(),
alarmSet.series.get('markerSize')
);
},
drawPoints(chartElement) {
this.drawAPI.drawPoints(
chartElement.getBuffer(),
chartElement.color().asRGBAArray(),
chartElement.count,
chartElement.series.get('markerSize'),
chartElement.series.get('markerShape')
);
},
drawLine(chartElement) {
this.drawAPI.drawLine(
chartElement.getBuffer(),
chartElement.color().asRGBAArray(),
chartElement.count
);
},
drawHighlights() {
if (this.highlights && this.highlights.length) {
this.highlights.forEach(this.drawHighlight, this);
}
},
drawHighlight(highlight) {
const points = new Float32Array([
this.offset.xVal(highlight.point, highlight.series),
this.offset.yVal(highlight.point, highlight.series)
]);
const color = highlight.series.get('color').asRGBAArray();
const pointCount = 1;
const shape = highlight.series.get('markerShape');
this.drawAPI.drawPoints(points, color, pointCount, HIGHLIGHT_SIZE, shape);
},
drawRectangles() {
if (this.rectangles) {
this.rectangles.forEach(this.drawRectangle, this);
}
},
drawRectangle(rect) {
this.drawAPI.drawSquare(
[
this.offset.x(rect.start.x),
this.offset.y(rect.start.y)
],
[
this.offset.x(rect.end.x),
this.offset.y(rect.end.y)
],
rect.color
);
}
}
};
</script>

View File

@ -1,109 +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.
*****************************************************************************/
import Model from './Model';
export default class Collection extends Model {
initialize(options) {
super.initialize(options);
this.modelClass = Model;
if (options.models) {
this.models = options.models.map(this.modelFn, this);
} else {
this.models = [];
}
}
modelFn(model) {
//TODO: Come back to this - why are we doing this?
if (model instanceof this.modelClass) {
model.collection = this;
return model;
}
return new this.modelClass({
collection: this,
model: model
});
}
first() {
return this.at(0);
}
forEach(iteree, context) {
this.models.forEach(iteree, context);
}
map(iteree, context) {
return this.models.map(iteree, context);
}
filter(iteree, context) {
return this.models.filter(iteree, context);
}
size() {
return this.models.length;
}
at(index) {
return this.models[index];
}
add(model) {
model = this.modelFn(model);
const index = this.models.length;
this.models.push(model);
this.emit('add', model, index);
}
insert(model, index) {
model = this.modelFn(model);
this.models.splice(index, 0, model);
this.emit('add', model, index + 1);
}
indexOf(model) {
return this.models.findIndex(m => m === model);
}
remove(model) {
const index = this.indexOf(model);
if (index === -1) {
throw new Error('model not found in collection.');
}
this.emit('remove', model, index);
this.models.splice(index, 1);
}
destroy(model) {
this.forEach(function (m) {
m.destroy();
});
this.stopListening();
}
}

View File

@ -1,58 +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.
*****************************************************************************/
import Model from "./Model";
/**
* TODO: doc strings.
*/
export default class LegendModel extends Model {
listenToSeriesCollection(seriesCollection) {
this.seriesCollection = seriesCollection;
this.listenTo(this.seriesCollection, 'add', this.setHeight, this);
this.listenTo(this.seriesCollection, 'remove', this.setHeight, this);
this.listenTo(this, 'change:expanded', this.setHeight, this);
this.set('expanded', this.get('expandByDefault'));
}
setHeight() {
const expanded = this.get('expanded');
if (this.get('position') !== 'top') {
this.set('height', '0px');
} else {
this.set('height', expanded ? (20 * (this.seriesCollection.size() + 1) + 40) + 'px' : '21px');
}
}
defaults(options) {
return {
position: 'top',
expandByDefault: false,
hideLegendWhenSmall: false,
valueToShowWhenCollapsed: 'nearestValue',
showTimestampWhenExpanded: true,
showValueWhenExpanded: true,
showMaximumWhenExpanded: true,
showMinimumWhenExpanded: true,
showUnitsWhenExpanded: true
};
}
}

View File

@ -1,93 +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.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import eventHelpers from "../lib/eventHelpers";
import _ from 'lodash';
export default class Model extends EventEmitter {
constructor(options) {
super();
//need to do this as we're already extending EventEmitter
eventHelpers.extend(this);
if (!options) {
options = {};
}
this.id = options.id;
this.model = options.model;
this.collection = options.collection;
const defaults = this.defaults(options);
if (!this.model) {
this.model = options.model = defaults;
} else {
_.defaultsDeep(this.model, defaults);
}
this.initialize(options);
this.idAttr = 'id';
}
defaults(options) {
return {};
}
initialize(model) {
}
/**
* Destroy the model, removing all listeners and subscriptions.
*/
destroy() {
this.emit('destroy');
this.removeAllListeners();
}
id() {
return this.get(this.idAttr);
}
get(attribute) {
return this.model[attribute];
}
has(attribute) {
return _.has(this.model, attribute);
}
set(attribute, value) {
const oldValue = this.model[attribute];
this.model[attribute] = value;
this.emit('change', attribute, value, oldValue, this);
this.emit('change:' + attribute, value, oldValue, this);
}
unset(attribute) {
const oldValue = this.model[attribute];
delete this.model[attribute];
this.emit('change', attribute, undefined, oldValue, this);
this.emit('change:' + attribute, undefined, oldValue, this);
}
}

View File

@ -1,136 +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.
*****************************************************************************/
import _ from 'lodash';
import Model from "./Model";
import SeriesCollection from "./SeriesCollection";
import XAxisModel from "./XAxisModel";
import YAxisModel from "./YAxisModel";
import LegendModel from "./LegendModel";
/**
* PlotConfiguration model stores the configuration of a plot and some
* limited state. The indiidual parts of the plot configuration model
* handle setting defaults and updating in response to various changes.
*
*/
export default class PlotConfigurationModel extends Model {
/**
* Initializes all sub models and then passes references to submodels
* to those that need it.
*/
initialize(options) {
this.openmct = options.openmct;
this.xAxis = new XAxisModel({
model: options.model.xAxis,
plot: this,
openmct: options.openmct
});
this.yAxis = new YAxisModel({
model: options.model.yAxis,
plot: this,
openmct: options.openmct
});
this.legend = new LegendModel({
model: options.model.legend,
plot: this,
openmct: options.openmct
});
this.series = new SeriesCollection({
models: options.model.series,
plot: this,
openmct: options.openmct
});
if (this.get('domainObject').type === 'telemetry.plot.overlay') {
this.removeMutationListener = this.openmct.objects.observe(
this.get('domainObject'),
'*',
this.updateDomainObject.bind(this)
);
}
this.yAxis.listenToSeriesCollection(this.series);
this.legend.listenToSeriesCollection(this.series);
this.listenTo(this, 'destroy', this.onDestroy, this);
}
/**
* Retrieve the persisted series config for a given identifier.
*/
getPersistedSeriesConfig(identifier) {
const domainObject = this.get('domainObject');
if (!domainObject.configuration || !domainObject.configuration.series) {
return;
}
return domainObject.configuration.series.filter(function (seriesConfig) {
return seriesConfig.identifier.key === identifier.key
&& seriesConfig.identifier.namespace === identifier.namespace;
})[0];
}
/**
* Retrieve the persisted filters for a given identifier.
*/
getPersistedFilters(identifier) {
const domainObject = this.get('domainObject');
const keystring = this.openmct.objects.makeKeyString(identifier);
if (!domainObject.configuration || !domainObject.configuration.filters) {
return;
}
return domainObject.configuration.filters[keystring];
}
/**
* Update the domain object with the given value.
*/
updateDomainObject(domainObject) {
this.set('domainObject', domainObject);
}
/**
* Clean up all objects and remove all listeners.
*/
onDestroy() {
this.xAxis.destroy();
this.yAxis.destroy();
this.series.destroy();
this.legend.destroy();
if (this.removeMutationListener) {
this.removeMutationListener();
}
}
/**
* Return defaults, which are extracted from the passed in domain
* object.
*/
defaults(options) {
return {
series: [],
domainObject: options.domainObject,
xAxis: {
},
yAxis: _.cloneDeep(_.get(options.domainObject, 'configuration.yAxis', {})),
legend: _.cloneDeep(_.get(options.domainObject, 'configuration.legend', {}))
};
}
}

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