mirror of
https://github.com/nasa/openmct.git
synced 2025-06-25 18:50:11 +00:00
Compare commits
5 Commits
notebook-d
...
tests-acti
Author | SHA1 | Date | |
---|---|---|---|
c567692c23 | |||
e20c838837 | |||
2c838c0acd | |||
c669f34ebc | |||
e15110ae97 |
20
.eslintrc.js
20
.eslintrc.js
@ -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": [
|
||||
|
@ -182,7 +182,7 @@ The following guidelines are provided for anyone contributing source code to the
|
||||
1. Avoid the use of "magic" values.
|
||||
eg.
|
||||
```JavaScript
|
||||
const UNAUTHORIZED = 401;
|
||||
Const UNAUTHORIZED = 401
|
||||
if (responseCode === UNAUTHORIZED)
|
||||
```
|
||||
is preferable to
|
||||
|
@ -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": [
|
||||
|
@ -86,9 +86,7 @@
|
||||
openmct.install(openmct.plugins.MyItems());
|
||||
openmct.install(openmct.plugins.Generator());
|
||||
openmct.install(openmct.plugins.ExampleImagery());
|
||||
openmct.install(openmct.plugins.PlanLayout());
|
||||
openmct.install(openmct.plugins.Timeline());
|
||||
openmct.install(openmct.plugins.PlotVue());
|
||||
openmct.install(openmct.plugins.UTCTimeSystem());
|
||||
openmct.install(openmct.plugins.AutoflowView({
|
||||
type: "telemetry.panel"
|
||||
@ -133,10 +131,10 @@
|
||||
}
|
||||
],
|
||||
// maximum recent bounds to retain in conductor history
|
||||
records: 10
|
||||
records: 10,
|
||||
// maximum duration between start and end bounds
|
||||
// for utc-based time systems this is in milliseconds
|
||||
// limit: ONE_DAY
|
||||
limit: ONE_DAY
|
||||
},
|
||||
{
|
||||
name: "Realtime",
|
||||
|
@ -86,7 +86,7 @@ module.exports = (config) => {
|
||||
reports: ['html', 'lcovonly', 'text-summary'],
|
||||
thresholds: {
|
||||
global: {
|
||||
lines: 66
|
||||
lines: 65
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "1.6.3-SNAPSHOT",
|
||||
"version": "1.4.1-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",
|
||||
|
@ -71,10 +71,10 @@ define(
|
||||
openmct.editor.cancel();
|
||||
}
|
||||
|
||||
function isFirstViewEditable(domainObject, objectPath) {
|
||||
let firstView = openmct.objectViews.get(domainObject, objectPath)[0];
|
||||
function isFirstViewEditable(domainObject) {
|
||||
let firstView = openmct.objectViews.get(domainObject)[0];
|
||||
|
||||
return firstView && firstView.canEdit && firstView.canEdit(domainObject, objectPath);
|
||||
return firstView && firstView.canEdit && firstView.canEdit(domainObject);
|
||||
}
|
||||
|
||||
function navigateAndEdit(object) {
|
||||
@ -88,7 +88,7 @@ define(
|
||||
|
||||
window.location.href = url;
|
||||
|
||||
if (isFirstViewEditable(object.useCapability('adapter'), objectPath)) {
|
||||
if (isFirstViewEditable(object.useCapability('adapter'))) {
|
||||
openmct.editor.edit();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -43,8 +43,7 @@ define(
|
||||
);
|
||||
|
||||
mockObjectAPI = jasmine.createSpyObj('objectAPI', [
|
||||
'isPersistable',
|
||||
'parseKeyString'
|
||||
'isPersistable'
|
||||
]);
|
||||
|
||||
mockAPI = {
|
||||
|
@ -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;
|
||||
|
@ -33,8 +33,7 @@ define(
|
||||
|
||||
beforeEach(function () {
|
||||
objectAPI = jasmine.createSpyObj('objectsAPI', [
|
||||
'isPersistable',
|
||||
'parseKeyString'
|
||||
'isPersistable'
|
||||
]);
|
||||
|
||||
mockOpenMCT = {
|
||||
|
@ -37,7 +37,7 @@ define(
|
||||
this.$q = $q;
|
||||
}
|
||||
|
||||
LocatingObjectDecorator.prototype.getObjects = function (ids, abortSignal) {
|
||||
LocatingObjectDecorator.prototype.getObjects = function (ids) {
|
||||
var $q = this.$q,
|
||||
$log = this.$log,
|
||||
objectService = this.objectService,
|
||||
@ -79,7 +79,7 @@ define(
|
||||
});
|
||||
}
|
||||
|
||||
return objectService.getObjects([id], abortSignal).then(attachContext);
|
||||
return objectService.getObjects([id]).then(attachContext);
|
||||
}
|
||||
|
||||
ids.forEach(function (id) {
|
||||
|
@ -32,8 +32,7 @@
|
||||
function indexItem(id, model) {
|
||||
indexedItems.push({
|
||||
id: id,
|
||||
name: model.name.toLowerCase(),
|
||||
type: model.type
|
||||
name: model.name.toLowerCase()
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -125,12 +125,13 @@ define([
|
||||
* @param topic the topicService.
|
||||
*/
|
||||
GenericSearchProvider.prototype.indexOnMutation = function (topic) {
|
||||
let mutationTopic = topic('mutation');
|
||||
var mutationTopic = topic('mutation'),
|
||||
provider = this;
|
||||
|
||||
mutationTopic.listen(mutatedObject => {
|
||||
let editor = mutatedObject.getCapability('editor');
|
||||
mutationTopic.listen(function (mutatedObject) {
|
||||
var editor = mutatedObject.getCapability('editor');
|
||||
if (!editor || !editor.inEditContext()) {
|
||||
this.index(
|
||||
provider.index(
|
||||
mutatedObject.getId(),
|
||||
mutatedObject.getModel()
|
||||
);
|
||||
@ -146,15 +147,10 @@ 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);
|
||||
}
|
||||
if (!this.indexedIds[id] && !this.pendingIndex[id]) {
|
||||
this.indexedIds[id] = true;
|
||||
this.pendingIndex[id] = true;
|
||||
this.idsToIndex.push(id);
|
||||
}
|
||||
|
||||
this.keepIndexing();
|
||||
@ -266,7 +262,6 @@ define([
|
||||
return {
|
||||
id: hit.item.id,
|
||||
model: hit.item.model,
|
||||
type: hit.item.type,
|
||||
score: hit.matchCount
|
||||
};
|
||||
});
|
||||
|
@ -41,8 +41,7 @@
|
||||
indexedItems.push({
|
||||
id: id,
|
||||
vector: vector,
|
||||
model: model,
|
||||
type: model.type
|
||||
model: model
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -80,15 +80,12 @@ define([
|
||||
* @param {Function} [filter] if provided, will be called for every
|
||||
* potential modelResult. If it returns false, the model result will be
|
||||
* excluded from the search results.
|
||||
* @param {AbortController.signal} abortSignal (optional) can pass in an abortSignal to cancel any
|
||||
* downstream fetch requests.
|
||||
* @returns {Promise} A Promise for a search result object.
|
||||
*/
|
||||
SearchAggregator.prototype.query = function (
|
||||
inputText,
|
||||
maxResults,
|
||||
filter,
|
||||
abortSignal
|
||||
filter
|
||||
) {
|
||||
|
||||
var aggregator = this,
|
||||
@ -123,7 +120,7 @@ define([
|
||||
modelResults = aggregator.applyFilter(modelResults, filter);
|
||||
modelResults = aggregator.removeDuplicates(modelResults);
|
||||
|
||||
return aggregator.asObjectResults(modelResults, abortSignal);
|
||||
return aggregator.asObjectResults(modelResults);
|
||||
});
|
||||
};
|
||||
|
||||
@ -196,19 +193,16 @@ define([
|
||||
* Convert modelResults to objectResults by fetching them from the object
|
||||
* service.
|
||||
*
|
||||
* @param {Object} modelResults an object containing the results from the search
|
||||
* @param {AbortController.signal} abortSignal (optional) abort signal to cancel any
|
||||
* downstream fetch requests
|
||||
* @returns {Promise} for an objectResults object.
|
||||
*/
|
||||
SearchAggregator.prototype.asObjectResults = function (modelResults, abortSignal) {
|
||||
SearchAggregator.prototype.asObjectResults = function (modelResults) {
|
||||
var objectIds = modelResults.hits.map(function (modelResult) {
|
||||
return modelResult.id;
|
||||
});
|
||||
|
||||
return this
|
||||
.objectService
|
||||
.getObjects(objectIds, abortSignal)
|
||||
.getObjects(objectIds)
|
||||
.then(function (objects) {
|
||||
|
||||
var objectResults = {
|
||||
|
@ -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']
|
||||
}));
|
||||
|
@ -37,7 +37,7 @@ define([
|
||||
context.domainObject.getModel(),
|
||||
objectUtils.parseKeyString(context.domainObject.getId())
|
||||
);
|
||||
const providers = mct.propertyEditors.get(domainObject, mct.router.path);
|
||||
const providers = mct.propertyEditors.get(domainObject);
|
||||
|
||||
if (providers.length > 0) {
|
||||
action.dialogService = Object.create(action.dialogService);
|
||||
|
@ -32,7 +32,7 @@ define([], function () {
|
||||
if (Object.prototype.hasOwnProperty.call(view, 'provider')) {
|
||||
const domainObject = legacyObject.useCapability('adapter');
|
||||
|
||||
return view.provider.canView(domainObject, this.openmct.router.path);
|
||||
return view.provider.canView(domainObject);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -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,26 +138,18 @@ define([
|
||||
});
|
||||
};
|
||||
|
||||
ObjectServiceProvider.prototype.superSecretFallbackSearch = function (query, abortSignal) {
|
||||
const searchService = this.$injector.get('searchService');
|
||||
|
||||
// need to pass the abortSignal down, so need to
|
||||
// pass in undefined for maxResults and filter on query
|
||||
return searchService.query(query, undefined, undefined, abortSignal);
|
||||
};
|
||||
|
||||
// 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) {
|
||||
const eventEmitter = openmct.objects.eventEmitter;
|
||||
|
||||
this.getObjects = function (keys, abortSignal) {
|
||||
this.getObjects = function (keys) {
|
||||
const results = {};
|
||||
|
||||
const promises = keys.map(function (keyString) {
|
||||
const key = utils.parseKeyString(keyString);
|
||||
|
||||
return openmct.objects.get(key, abortSignal)
|
||||
return openmct.objects.get(key)
|
||||
.then(function (object) {
|
||||
object = utils.toOldFormat(object);
|
||||
results[keyString] = instantiate(object, keyString);
|
||||
|
@ -29,22 +29,9 @@ describe('The ActionCollection', () => {
|
||||
let mockApplicableActions;
|
||||
let mockObjectPath;
|
||||
let mockView;
|
||||
let mockIdentifierService;
|
||||
|
||||
beforeEach(() => {
|
||||
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);
|
||||
mockObjectPath = [
|
||||
{
|
||||
name: 'mock folder',
|
||||
@ -63,10 +50,6 @@ describe('The ActionCollection', () => {
|
||||
}
|
||||
}
|
||||
];
|
||||
openmct.objects.addProvider('', jasmine.createSpyObj('mockMutableObjectProvider', [
|
||||
'create',
|
||||
'update'
|
||||
]));
|
||||
mockView = {
|
||||
getViewContext: () => {
|
||||
return {
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -38,7 +38,7 @@ class MenuAPI {
|
||||
this._showObjectMenu = this._showObjectMenu.bind(this);
|
||||
}
|
||||
|
||||
showMenu(x, y, actions, onDestroy) {
|
||||
showMenu(x, y, actions) {
|
||||
if (this.menuComponent) {
|
||||
this.menuComponent.dismiss();
|
||||
}
|
||||
@ -46,8 +46,7 @@ class MenuAPI {
|
||||
let options = {
|
||||
x,
|
||||
y,
|
||||
actions,
|
||||
onDestroy
|
||||
actions
|
||||
};
|
||||
|
||||
this.menuComponent = new Menu(options);
|
||||
|
@ -31,7 +31,6 @@ describe ('The Menu API', () => {
|
||||
let x;
|
||||
let y;
|
||||
let result;
|
||||
let onDestroy;
|
||||
|
||||
beforeEach(() => {
|
||||
openmct = createOpenMct();
|
||||
@ -74,9 +73,7 @@ describe ('The Menu API', () => {
|
||||
let vueComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
onDestroy = jasmine.createSpy('onDestroy');
|
||||
|
||||
menuAPI.showMenu(x, y, actionsArray, onDestroy);
|
||||
menuAPI.showMenu(x, y, actionsArray);
|
||||
vueComponent = menuAPI.menuComponent.component;
|
||||
menuComponent = document.querySelector(".c-menu");
|
||||
|
||||
@ -123,12 +120,6 @@ describe ('The Menu API', () => {
|
||||
|
||||
expect(vueComponent.$destroy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("invokes the onDestroy callback if passed in", () => {
|
||||
document.body.click();
|
||||
|
||||
expect(onDestroy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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 />'
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
102
src/api/objects/MutableObject.js
Normal file
102
src/api/objects/MutableObject.js
Normal 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;
|
||||
});
|
@ -20,508 +20,365 @@
|
||||
* 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
|
||||
*/
|
||||
/**
|
||||
* Utilities for loading, saving, and manipulating domain objects.
|
||||
* @interface ObjectAPI
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
|
||||
function ObjectAPI(typeRegistry, openmct) {
|
||||
this.typeRegistry = typeRegistry;
|
||||
this.eventEmitter = new EventEmitter();
|
||||
this.providers = {};
|
||||
this.rootRegistry = new RootRegistry();
|
||||
this.injectIdentifierService = function () {
|
||||
this.identifierService = openmct.$injector.get("identifierService");
|
||||
function ObjectAPI() {
|
||||
this.eventEmitter = new EventEmitter();
|
||||
this.providers = {};
|
||||
this.rootRegistry = new RootRegistry();
|
||||
this.rootProvider = new RootObjectProvider.default(this.rootRegistry);
|
||||
this.cache = {};
|
||||
this.interceptorRegistry = new InterceptorRegistry.default();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set fallback provider, this is an internal API for legacy reasons.
|
||||
* @private
|
||||
*/
|
||||
ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) {
|
||||
this.fallbackProvider = p;
|
||||
};
|
||||
|
||||
this.rootProvider = new RootObjectProvider(this.rootRegistry);
|
||||
this.cache = {};
|
||||
this.interceptorRegistry = new InterceptorRegistry();
|
||||
}
|
||||
/**
|
||||
* Retrieve the provider for a given identifier.
|
||||
* @private
|
||||
*/
|
||||
ObjectAPI.prototype.getProvider = function (identifier) {
|
||||
if (identifier.key === 'ROOT') {
|
||||
return this.rootProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set fallback provider, this is an internal API for legacy reasons.
|
||||
* @private
|
||||
*/
|
||||
ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) {
|
||||
this.fallbackProvider = p;
|
||||
};
|
||||
return this.providers[identifier.namespace] || this.fallbackProvider;
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
ObjectAPI.prototype.getIdentifierService = function () {
|
||||
// Lazily acquire identifier service
|
||||
if (!this.identifierService) {
|
||||
this.injectIdentifierService();
|
||||
}
|
||||
/**
|
||||
* Get the root-level object.
|
||||
* @returns {Promise.<DomainObject>} a promise for the root object
|
||||
*/
|
||||
ObjectAPI.prototype.getRoot = function () {
|
||||
return this.rootProvider.get();
|
||||
};
|
||||
|
||||
return this.identifierService;
|
||||
};
|
||||
/**
|
||||
* Register a new object provider for a particular namespace.
|
||||
*
|
||||
* @param {string} namespace the namespace for which to provide objects
|
||||
* @param {module:openmct.ObjectProvider} provider the provider which
|
||||
* will handle loading domain objects from this namespace
|
||||
* @memberof {module:openmct.ObjectAPI#}
|
||||
* @name addProvider
|
||||
*/
|
||||
ObjectAPI.prototype.addProvider = function (namespace, provider) {
|
||||
this.providers[namespace] = provider;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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();
|
||||
/**
|
||||
* Provides the ability to read, write, and delete domain objects.
|
||||
*
|
||||
* When registering a new object provider, all methods on this interface
|
||||
* are optional.
|
||||
*
|
||||
* @interface ObjectProvider
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
|
||||
if (identifier.key === 'ROOT') {
|
||||
return this.rootProvider;
|
||||
}
|
||||
/**
|
||||
* Create the given domain object in the corresponding persistence store
|
||||
*
|
||||
* @method create
|
||||
* @memberof module:openmct.ObjectProvider#
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object to
|
||||
* create
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been created, or be rejected if it cannot be saved
|
||||
*/
|
||||
|
||||
return this.providers[namespace] || this.fallbackProvider;
|
||||
};
|
||||
/**
|
||||
* Update this domain object in its persistence store
|
||||
*
|
||||
* @method update
|
||||
* @memberof module:openmct.ObjectProvider#
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object to
|
||||
* update
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been updated, or be rejected if it cannot be saved
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the root-level object.
|
||||
* @returns {Promise.<DomainObject>} a promise for the root object
|
||||
*/
|
||||
ObjectAPI.prototype.getRoot = function () {
|
||||
return this.rootProvider.get();
|
||||
};
|
||||
/**
|
||||
* Delete this domain object.
|
||||
*
|
||||
* @method delete
|
||||
* @memberof module:openmct.ObjectProvider#
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object to
|
||||
* delete
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been deleted, or be rejected if it cannot be deleted
|
||||
*/
|
||||
|
||||
/**
|
||||
* Register a new object provider for a particular namespace.
|
||||
*
|
||||
* @param {string} namespace the namespace for which to provide objects
|
||||
* @param {module:openmct.ObjectProvider} provider the provider which
|
||||
* will handle loading domain objects from this namespace
|
||||
* @memberof {module:openmct.ObjectAPI#}
|
||||
* @name addProvider
|
||||
*/
|
||||
ObjectAPI.prototype.addProvider = function (namespace, provider) {
|
||||
this.providers[namespace] = provider;
|
||||
};
|
||||
/**
|
||||
* Get a domain object.
|
||||
*
|
||||
* @method get
|
||||
* @memberof module:openmct.ObjectProvider#
|
||||
* @param {string} key the key 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides the ability to read, write, and delete domain objects.
|
||||
*
|
||||
* When registering a new object provider, all methods on this interface
|
||||
* are optional.
|
||||
*
|
||||
* @interface ObjectProvider
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the given domain object in the corresponding persistence store
|
||||
*
|
||||
* @method create
|
||||
* @memberof module:openmct.ObjectProvider#
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object to
|
||||
* create
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been created, or be rejected if it cannot be saved
|
||||
*/
|
||||
identifier = utils.parseKeyString(identifier);
|
||||
const provider = this.getProvider(identifier);
|
||||
|
||||
/**
|
||||
* Update this domain object in its persistence store
|
||||
*
|
||||
* @method update
|
||||
* @memberof module:openmct.ObjectProvider#
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object to
|
||||
* update
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been updated, or be rejected if it cannot be saved
|
||||
*/
|
||||
if (!provider) {
|
||||
throw new Error('No Provider Matched');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this domain object.
|
||||
*
|
||||
* @method delete
|
||||
* @memberof module:openmct.ObjectProvider#
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object to
|
||||
* delete
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been deleted, or be rejected if it cannot be deleted
|
||||
*/
|
||||
if (!provider.get) {
|
||||
throw new Error('Provider does not support get!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a domain object.
|
||||
*
|
||||
* @method get
|
||||
* @memberof module:openmct.ObjectProvider#
|
||||
* @param {string} key the key for the domain object to load
|
||||
* @param {AbortController.signal} abortSignal (optional) signal to abort fetch requests
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been saved, or be rejected if it cannot be saved
|
||||
*/
|
||||
let objectPromise = provider.get(identifier);
|
||||
|
||||
ObjectAPI.prototype.get = function (identifier, abortSignal) {
|
||||
let keystring = this.makeKeyString(identifier);
|
||||
if (this.cache[keystring] !== undefined) {
|
||||
return this.cache[keystring];
|
||||
}
|
||||
this.cache[keystring] = objectPromise;
|
||||
|
||||
identifier = utils.parseKeyString(identifier);
|
||||
const provider = this.getProvider(identifier);
|
||||
return objectPromise.then(result => {
|
||||
delete this.cache[keystring];
|
||||
const interceptors = this.listGetInterceptors(identifier, result);
|
||||
interceptors.forEach(interceptor => {
|
||||
result = interceptor.invoke(identifier, result);
|
||||
});
|
||||
|
||||
if (!provider) {
|
||||
throw new Error('No Provider Matched');
|
||||
}
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
if (!provider.get) {
|
||||
throw new Error('Provider does not support get!');
|
||||
}
|
||||
ObjectAPI.prototype.delete = function () {
|
||||
throw new Error('Delete not implemented');
|
||||
};
|
||||
|
||||
let objectPromise = provider.get(identifier, abortSignal);
|
||||
this.cache[keystring] = objectPromise;
|
||||
ObjectAPI.prototype.isPersistable = function (domainObject) {
|
||||
let provider = this.getProvider(domainObject.identifier);
|
||||
|
||||
return objectPromise.then(result => {
|
||||
delete this.cache[keystring];
|
||||
result = this.applyGetInterceptors(identifier, result);
|
||||
return provider !== undefined
|
||||
&& provider.create !== undefined
|
||||
&& provider.update !== undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Save this domain object in its current state. EXPERIMENTAL
|
||||
*
|
||||
* @private
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object to
|
||||
* save
|
||||
* @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) {
|
||||
let provider = this.getProvider(domainObject.identifier);
|
||||
let savedResolve;
|
||||
let result;
|
||||
|
||||
if (!this.isPersistable(domainObject)) {
|
||||
result = Promise.reject('Object provider does not support saving');
|
||||
} else if (hasAlreadyBeenPersisted(domainObject)) {
|
||||
result = Promise.resolve(true);
|
||||
} else {
|
||||
const persistedTime = Date.now();
|
||||
if (domainObject.persisted === undefined) {
|
||||
result = new Promise((resolve) => {
|
||||
savedResolve = resolve;
|
||||
});
|
||||
domainObject.persisted = persistedTime;
|
||||
provider.create(domainObject).then((response) => {
|
||||
this.mutate(domainObject, 'persisted', persistedTime);
|
||||
savedResolve(response);
|
||||
});
|
||||
} else {
|
||||
domainObject.persisted = persistedTime;
|
||||
this.mutate(domainObject, 'persisted', persistedTime);
|
||||
result = provider.update(domainObject);
|
||||
}
|
||||
}
|
||||
|
||||
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 {AbortController.signal} abortSignal (optional) signal to cancel downstream fetch requests
|
||||
* @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, abortSignal) {
|
||||
const searchPromises = Object.values(this.providers)
|
||||
.filter(provider => provider.search !== undefined)
|
||||
.map(provider => provider.search(query, abortSignal));
|
||||
/**
|
||||
* 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
|
||||
* promise for an identifier or an array of root level objects.
|
||||
* @method addRoot
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
*/
|
||||
ObjectAPI.prototype.addRoot = function (key) {
|
||||
this.rootRegistry.addRoot(key);
|
||||
};
|
||||
|
||||
searchPromises.push(this.fallbackProvider.superSecretFallbackSearch(query, abortSignal)
|
||||
.then(results => results.hits
|
||||
.map(hit => {
|
||||
let domainObject = utils.toNewFormat(hit.object.getModel(), hit.object.getId());
|
||||
domainObject = this.applyGetInterceptors(domainObject.identifier, domainObject);
|
||||
/**
|
||||
* Modify a domain object.
|
||||
* @param {module:openmct.DomainObject} object the object to mutate
|
||||
* @param {string} path the property to modify
|
||||
* @param {*} value the new value for this property
|
||||
* @method mutate
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
*/
|
||||
ObjectAPI.prototype.mutate = function (domainObject, path, value) {
|
||||
const mutableObject =
|
||||
new MutableObject(this.eventEmitter, domainObject);
|
||||
|
||||
return domainObject;
|
||||
})));
|
||||
return mutableObject.set(path, value);
|
||||
};
|
||||
|
||||
return searchPromises;
|
||||
};
|
||||
/**
|
||||
* Observe changes to a domain object.
|
||||
* @param {module:openmct.DomainObject} object the object to observe
|
||||
* @param {string} path the property to observe
|
||||
* @param {Function} callback a callback to invoke when new values for
|
||||
* this property are observed
|
||||
* @method observe
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
*/
|
||||
ObjectAPI.prototype.observe = function (domainObject, path, callback) {
|
||||
const mutableObject =
|
||||
new MutableObject(this.eventEmitter, domainObject);
|
||||
mutableObject.on(path, callback);
|
||||
|
||||
/**
|
||||
* 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 (identifier) {
|
||||
if (!this.supportsMutation(identifier)) {
|
||||
throw new Error(`Object "${this.makeKeyString(identifier)}" does not support mutation.`);
|
||||
}
|
||||
return mutableObject.stopListening.bind(mutableObject);
|
||||
};
|
||||
|
||||
return this.get(identifier).then((object) => {
|
||||
return this._toMutable(object);
|
||||
});
|
||||
};
|
||||
/**
|
||||
* @param {module:openmct.ObjectAPI~Identifier} identifier
|
||||
* @returns {string} A string representation of the given identifier, including namespace and key
|
||||
*/
|
||||
ObjectAPI.prototype.makeKeyString = function (identifier) {
|
||||
return utils.makeKeyString(identifier);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 () {
|
||||
throw new Error('Delete not implemented');
|
||||
};
|
||||
|
||||
ObjectAPI.prototype.isPersistable = function (idOrKeyString) {
|
||||
let identifier = utils.parseKeyString(idOrKeyString);
|
||||
let provider = this.getProvider(identifier);
|
||||
|
||||
return provider !== undefined
|
||||
&& provider.create !== undefined
|
||||
&& provider.update !== undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Save this domain object in its current state. EXPERIMENTAL
|
||||
*
|
||||
* @private
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object to
|
||||
* save
|
||||
* @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) {
|
||||
let provider = this.getProvider(domainObject.identifier);
|
||||
let savedResolve;
|
||||
let result;
|
||||
|
||||
if (!this.isPersistable(domainObject.identifier)) {
|
||||
result = Promise.reject('Object provider does not support saving');
|
||||
} else if (hasAlreadyBeenPersisted(domainObject)) {
|
||||
result = Promise.resolve(true);
|
||||
} else {
|
||||
const persistedTime = Date.now();
|
||||
if (domainObject.persisted === undefined) {
|
||||
result = new Promise((resolve) => {
|
||||
savedResolve = resolve;
|
||||
/**
|
||||
* 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) {
|
||||
return identifiers.map(utils.parseKeyString)
|
||||
.every(identifier => {
|
||||
return identifier === identifiers[0]
|
||||
|| (identifier.namespace === identifiers[0].namespace
|
||||
&& identifier.key === identifiers[0].key);
|
||||
});
|
||||
domainObject.persisted = persistedTime;
|
||||
provider.create(domainObject).then((response) => {
|
||||
this.mutate(domainObject, 'persisted', persistedTime);
|
||||
savedResolve(response);
|
||||
});
|
||||
} else {
|
||||
domainObject.persisted = persistedTime;
|
||||
this.mutate(domainObject, 'persisted', persistedTime);
|
||||
result = provider.update(domainObject);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) {
|
||||
return this.get(identifier).then((domainObject) => {
|
||||
path.push(domainObject);
|
||||
let location = domainObject.location;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* promise for an identifier or an array of root level objects.
|
||||
* @method addRoot
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
*/
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
* Inovke interceptors if applicable for a given domain object.
|
||||
* @private
|
||||
*/
|
||||
ObjectAPI.prototype.applyGetInterceptors = function (identifier, domainObject) {
|
||||
const interceptors = this.listGetInterceptors(identifier, domainObject);
|
||||
interceptors.forEach(interceptor => {
|
||||
domainObject = interceptor.invoke(identifier, domainObject);
|
||||
});
|
||||
|
||||
return domainObject;
|
||||
};
|
||||
|
||||
/**
|
||||
* Modify a domain object.
|
||||
* @param {module:openmct.DomainObject} object the object to mutate
|
||||
* @param {string} path the property to modify
|
||||
* @param {*} value the new value for this property
|
||||
* @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}`;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
//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) {
|
||||
let mutableObject;
|
||||
|
||||
if (object.isMutable) {
|
||||
mutableObject = object;
|
||||
} else {
|
||||
mutableObject = MutableDomainObject.createMutable(object, this.eventEmitter);
|
||||
}
|
||||
|
||||
// Check if provider supports realtime updates
|
||||
let identifier = utils.parseKeyString(mutableObject.identifier);
|
||||
let provider = this.getProvider(identifier);
|
||||
|
||||
if (provider !== undefined
|
||||
&& provider.observe !== undefined) {
|
||||
let unobserve = provider.observe(identifier, (updatedModel) => {
|
||||
mutableObject.$refresh(updatedModel);
|
||||
});
|
||||
mutableObject.$on('$destroy', () => {
|
||||
unobserve();
|
||||
if (location) {
|
||||
return this.getOriginalPath(utils.parseKeyString(location), path);
|
||||
} else {
|
||||
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
|
||||
* @memberof module:openmct.ObjectAPI~
|
||||
* @property {string} namespace the namespace to/from which this domain
|
||||
* object should be loaded/stored.
|
||||
* @property {string} key a unique identifier for the domain object
|
||||
* 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,
|
||||
* and so forth.
|
||||
*
|
||||
* A few common properties are defined for domain objects. Beyond these,
|
||||
* individual types of domain objects may add more as they see fit.
|
||||
*
|
||||
* @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which
|
||||
* uniquely identifies this domain object
|
||||
* @property {string} type the type of domain object
|
||||
* @property {string} name the human-readable name for this domain object
|
||||
* @property {string} [creator] the user name of the creator of this domain
|
||||
* object
|
||||
* @property {number} [modified] the time, in milliseconds since the UNIX
|
||||
* epoch, at which this domain object was last modified
|
||||
* @property {module:openmct.ObjectAPI~Identifier[]} [composition] if
|
||||
* present, this will be used by the default composition provider
|
||||
* to load domain objects
|
||||
* @typedef DomainObject
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
|
||||
function hasAlreadyBeenPersisted(domainObject) {
|
||||
return domainObject.persisted !== undefined
|
||||
&& domainObject.persisted === domainObject.modified;
|
||||
}
|
||||
|
||||
return mutableObject;
|
||||
};
|
||||
|
||||
/**
|
||||
* @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
|
||||
* @param {Function} callback a callback to invoke when new values for
|
||||
* this property are observed
|
||||
* @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);
|
||||
|
||||
return () => mutable.$destroy();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {module:openmct.ObjectAPI~Identifier} identifier
|
||||
* @returns {string} A string representation of the given identifier, including namespace and key
|
||||
*/
|
||||
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) {
|
||||
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 = []) {
|
||||
return this.get(identifier).then((domainObject) => {
|
||||
path.push(domainObject);
|
||||
let location = domainObject.location;
|
||||
|
||||
if (location) {
|
||||
return this.getOriginalPath(utils.parseKeyString(location), path);
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Uniquely identifies a domain object.
|
||||
*
|
||||
* @typedef Identifier
|
||||
* @memberof module:openmct.ObjectAPI~
|
||||
* @property {string} namespace the namespace to/from which this domain
|
||||
* object should be loaded/stored.
|
||||
* @property {string} key a unique identifier for the domain object
|
||||
* 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,
|
||||
* and so forth.
|
||||
*
|
||||
* A few common properties are defined for domain objects. Beyond these,
|
||||
* individual types of domain objects may add more as they see fit.
|
||||
*
|
||||
* @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which
|
||||
* uniquely identifies this domain object
|
||||
* @property {string} type the type of domain object
|
||||
* @property {string} name the human-readable name for this domain object
|
||||
* @property {string} [creator] the user name of the creator of this domain
|
||||
* object
|
||||
* @property {number} [modified] the time, in milliseconds since the UNIX
|
||||
* epoch, at which this domain object was last modified
|
||||
* @property {module:openmct.ObjectAPI~Identifier[]} [composition] if
|
||||
* present, this will be used by the default composition provider
|
||||
* to load domain objects
|
||||
* @typedef DomainObject
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
|
||||
function hasAlreadyBeenPersisted(domainObject) {
|
||||
return domainObject.persisted !== undefined
|
||||
&& domainObject.persisted === domainObject.modified;
|
||||
}
|
||||
|
||||
export default ObjectAPI;
|
||||
return ObjectAPI;
|
||||
});
|
||||
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ define([
|
||||
this.providers.push(function () {
|
||||
return key;
|
||||
});
|
||||
} else if (typeof key === "function") {
|
||||
} else if (_.isFunction(key)) {
|
||||
this.providers.push(key);
|
||||
}
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
@ -20,4 +20,13 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
export const ALLOWED_FOLDER_TYPES = ['folder', 'noneditable.folder'];
|
||||
define([
|
||||
"EventEmitter"
|
||||
], function (
|
||||
EventEmitter
|
||||
) {
|
||||
/**
|
||||
* Provides a singleton event bus for sharing between objects.
|
||||
*/
|
||||
return new EventEmitter();
|
||||
});
|
@ -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();
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -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);
|
||||
|
@ -1,37 +0,0 @@
|
||||
export default function (folderName, 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: folderName || "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);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
}
|
@ -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('CouchDB Documents', 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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@ -44,15 +44,11 @@ export default function LADTableViewProvider(openmct) {
|
||||
LadTableComponent: LadTable
|
||||
},
|
||||
provide: {
|
||||
openmct
|
||||
openmct,
|
||||
domainObject,
|
||||
objectPath
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
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) {
|
||||
|
@ -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) {
|
||||
|
@ -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: []
|
||||
|
@ -57,10 +57,10 @@
|
||||
import LadRow from './LADRow.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
components: {
|
||||
LadRow
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
return {
|
||||
ladTableObjects: [],
|
||||
|
@ -98,7 +98,7 @@ describe("The LAD Table", () => {
|
||||
});
|
||||
|
||||
it("should provide a table view only for lad table objects", () => {
|
||||
let applicableViews = openmct.objectViews.get(mockObj.ladTable, []);
|
||||
let applicableViews = openmct.objectViews.get(mockObj.ladTable);
|
||||
|
||||
let ladTableView = applicableViews.find(
|
||||
(viewProvider) => viewProvider.key === ladTableKey
|
||||
@ -185,7 +185,7 @@ describe("The LAD Table", () => {
|
||||
end: bounds.end
|
||||
});
|
||||
|
||||
applicableViews = openmct.objectViews.get(mockObj.ladTable, []);
|
||||
applicableViews = openmct.objectViews.get(mockObj.ladTable);
|
||||
ladTableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === ladTableKey);
|
||||
ladTableView = ladTableViewProvider.view(mockObj.ladTable, [mockObj.ladTable]);
|
||||
ladTableView.show(child, true);
|
||||
@ -296,7 +296,7 @@ describe("The LAD Table Set", () => {
|
||||
});
|
||||
|
||||
it("should provide a lad table set view only for lad table set objects", () => {
|
||||
let applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []);
|
||||
let applicableViews = openmct.objectViews.get(mockObj.ladTableSet);
|
||||
|
||||
let ladTableSetView = applicableViews.find(
|
||||
(viewProvider) => viewProvider.key === ladTableSetKey
|
||||
@ -391,7 +391,7 @@ describe("The LAD Table Set", () => {
|
||||
end: bounds.end
|
||||
});
|
||||
|
||||
applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []);
|
||||
applicableViews = openmct.objectViews.get(mockObj.ladTableSet);
|
||||
ladTableSetViewProvider = applicableViews.find((viewProvider) => viewProvider.key === ladTableSetKey);
|
||||
ladTableSetView = ladTableSetViewProvider.view(mockObj.ladTableSet, [mockObj.ladTableSet]);
|
||||
ladTableSetView.show(child, true);
|
||||
|
@ -67,11 +67,11 @@ describe("AutoflowTabularPlugin", () => {
|
||||
});
|
||||
|
||||
it("applies its view to the type from options", () => {
|
||||
expect(provider.canView(testObject, [])).toBe(true);
|
||||
expect(provider.canView(testObject)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not apply to other types", () => {
|
||||
expect(provider.canView({ type: 'foo' }, [])).toBe(false);
|
||||
expect(provider.canView({ type: 'foo' })).toBe(false);
|
||||
});
|
||||
|
||||
describe("provides a view which", () => {
|
||||
|
@ -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>'
|
||||
});
|
||||
|
||||
|
@ -75,8 +75,7 @@ export default class Condition extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
// if all the criteria in this condition have no telemetry, we want to force the condition result to evaluate
|
||||
if (this.hasNoTelemetry() || this.isTelemetryUsed(datum.id)) {
|
||||
if (this.isTelemetryUsed(datum.id)) {
|
||||
|
||||
this.criteria.forEach(criterion => {
|
||||
if (this.isAnyOrAllTelemetry(criterion)) {
|
||||
@ -94,12 +93,6 @@ export default class Condition extends EventEmitter {
|
||||
return (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any'));
|
||||
}
|
||||
|
||||
hasNoTelemetry() {
|
||||
return this.criteria.every((criterion) => {
|
||||
return !this.isAnyOrAllTelemetry(criterion) && criterion.telemetry === '';
|
||||
});
|
||||
}
|
||||
|
||||
isTelemetryUsed(id) {
|
||||
return this.criteria.some(criterion => {
|
||||
return this.isAnyOrAllTelemetry(criterion) || criterion.telemetryObjectIdAsString === id;
|
||||
@ -257,17 +250,10 @@ export default class Condition extends EventEmitter {
|
||||
}
|
||||
|
||||
getTriggerDescription() {
|
||||
if (this.trigger) {
|
||||
return {
|
||||
conjunction: TRIGGER_CONJUNCTION[this.trigger],
|
||||
prefix: `${TRIGGER_LABEL[this.trigger]}: `
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
conjunction: '',
|
||||
prefix: ''
|
||||
};
|
||||
}
|
||||
return {
|
||||
conjunction: TRIGGER_CONJUNCTION[this.trigger],
|
||||
prefix: `${TRIGGER_LABEL[this.trigger]}: `
|
||||
};
|
||||
}
|
||||
|
||||
requestLADConditionResult() {
|
||||
|
@ -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 = {};
|
||||
@ -82,17 +79,6 @@ export default class ConditionManager extends EventEmitter {
|
||||
delete this.subscriptions[id];
|
||||
delete this.telemetryObjects[id];
|
||||
this.removeConditionTelemetryObjects();
|
||||
|
||||
//force re-computation of condition set result as we might be in a state where
|
||||
// there is no telemetry datum coming in for a while or at all.
|
||||
let latestTimestamp = getLatestTimestamp(
|
||||
{},
|
||||
{},
|
||||
this.timeSystems,
|
||||
this.openmct.time.timeSystem()
|
||||
);
|
||||
this.updateConditionResults({id: id});
|
||||
this.updateCurrentCondition(latestTimestamp);
|
||||
}
|
||||
|
||||
initialize() {
|
||||
@ -340,10 +326,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,21 +334,16 @@ 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)) {
|
||||
this.updateConditionResults(normalizedDatum);
|
||||
this.updateCurrentCondition(timestamp);
|
||||
}
|
||||
}
|
||||
timestamp[timeSystemKey] = normalizedDatum[timeSystemKey];
|
||||
|
||||
updateConditionResults(normalizedDatum) {
|
||||
//We want to stop when the first condition evaluates to true.
|
||||
this.conditions.some((condition) => {
|
||||
condition.updateResult(normalizedDatum);
|
||||
|
||||
return condition.result === true;
|
||||
});
|
||||
|
||||
this.updateCurrentCondition(timestamp);
|
||||
}
|
||||
|
||||
updateCurrentCondition(timestamp) {
|
||||
|
@ -86,7 +86,6 @@ export default class StyleRuleManager extends EventEmitter {
|
||||
updateObjectStyleConfig(styleConfiguration) {
|
||||
if (!styleConfiguration || !styleConfiguration.conditionSetIdentifier) {
|
||||
this.initialize(styleConfiguration || {});
|
||||
this.applyStaticStyle();
|
||||
this.destroy();
|
||||
} else {
|
||||
let isNewConditionSet = !this.conditionSetIdentifier
|
||||
@ -159,6 +158,7 @@ export default class StyleRuleManager extends EventEmitter {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.applyStaticStyle();
|
||||
if (this.stopProvidingTelemetry) {
|
||||
this.stopProvidingTelemetry();
|
||||
delete this.stopProvidingTelemetry;
|
||||
|
@ -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,
|
||||
|
@ -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: {
|
||||
|
@ -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
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
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('/');
|
||||
}
|
||||
|
||||
return {
|
||||
id: this.openmct.objects.makeKeyString(object.identifier),
|
||||
object,
|
||||
objectPath,
|
||||
navigateToParent
|
||||
};
|
||||
});
|
||||
});
|
||||
},
|
||||
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))
|
||||
.join('/');
|
||||
|
||||
const filteredChild = {
|
||||
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) {
|
||||
|
@ -22,7 +22,6 @@
|
||||
|
||||
import { createOpenMct, resetApplicationState } from "utils/testing";
|
||||
import ConditionPlugin from "./plugin";
|
||||
import stylesManager from '@/ui/inspector/styles/StylesManager';
|
||||
import StylesView from "./components/inspector/StylesView.vue";
|
||||
import Vue from 'vue';
|
||||
import {getApplicableStylesForItem} from "./utils/styleUtils";
|
||||
@ -136,7 +135,7 @@ describe('the plugin', function () {
|
||||
}
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(testViewObject, []);
|
||||
const applicableViews = openmct.objectViews.get(testViewObject);
|
||||
let conditionSetView = applicableViews.find((viewProvider) => viewProvider.key === 'conditionSet.view');
|
||||
expect(conditionSetView).toBeDefined();
|
||||
});
|
||||
@ -401,15 +400,14 @@ describe('the plugin', function () {
|
||||
let viewContainer = document.createElement('div');
|
||||
child.append(viewContainer);
|
||||
component = new Vue({
|
||||
provide: {
|
||||
openmct: openmct,
|
||||
selection: selection
|
||||
},
|
||||
el: viewContainer,
|
||||
components: {
|
||||
StylesView
|
||||
},
|
||||
provide: {
|
||||
openmct: openmct,
|
||||
selection: selection,
|
||||
stylesManager
|
||||
},
|
||||
template: '<styles-view/>'
|
||||
});
|
||||
|
||||
@ -543,6 +541,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 +563,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);
|
||||
|
@ -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>'
|
||||
});
|
||||
},
|
||||
|
@ -51,11 +51,11 @@ export default {
|
||||
height: 5
|
||||
};
|
||||
},
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
LayoutFrame
|
||||
},
|
||||
mixins: [conditionalStylesMixin],
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
@ -51,11 +51,11 @@ export default {
|
||||
url: element.url
|
||||
};
|
||||
},
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
LayoutFrame
|
||||
},
|
||||
mixins: [conditionalStylesMixin],
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
|
@ -99,8 +99,8 @@ export default {
|
||||
stroke: '#717171'
|
||||
};
|
||||
},
|
||||
mixins: [conditionalStylesMixin],
|
||||
inject: ['openmct'],
|
||||
mixins: [conditionalStylesMixin],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
|
@ -80,11 +80,11 @@ export default {
|
||||
viewKey
|
||||
};
|
||||
},
|
||||
inject: ['openmct', 'objectPath'],
|
||||
components: {
|
||||
ObjectFrame,
|
||||
LayoutFrame
|
||||
},
|
||||
inject: ['openmct', 'objectPath'],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
@ -109,8 +109,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
domainObject: undefined,
|
||||
currentObjectPath: [],
|
||||
mutablePromise: undefined
|
||||
currentObjectPath: []
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@ -130,31 +129,17 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.openmct.objects.supportsMutation(this.item.identifier)) {
|
||||
this.mutablePromise = this.openmct.objects.getMutable(this.item.identifier)
|
||||
.then(this.setObject);
|
||||
} else {
|
||||
this.openmct.objects.get(this.item.identifier)
|
||||
.then(this.setObject);
|
||||
}
|
||||
this.openmct.objects.get(this.item.identifier)
|
||||
.then(this.setObject);
|
||||
},
|
||||
beforeDestroy() {
|
||||
destroyed() {
|
||||
if (this.removeSelectable) {
|
||||
this.removeSelectable();
|
||||
}
|
||||
|
||||
if (this.mutablePromise) {
|
||||
this.mutablePromise.then(() => {
|
||||
this.openmct.objects.destroyMutable(this.domainObject);
|
||||
});
|
||||
} else {
|
||||
this.openmct.objects.destroyMutable(this.domainObject);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setObject(domainObject) {
|
||||
this.domainObject = domainObject;
|
||||
this.mutablePromise = undefined;
|
||||
this.currentObjectPath = [this.domainObject].concat(this.objectPath.slice());
|
||||
this.$nextTick(() => {
|
||||
let reference = this.$refs.objectFrame;
|
||||
|
@ -98,11 +98,11 @@ export default {
|
||||
font: 'default'
|
||||
};
|
||||
},
|
||||
inject: ['openmct', 'objectPath'],
|
||||
components: {
|
||||
LayoutFrame
|
||||
},
|
||||
mixins: [conditionalStylesMixin],
|
||||
inject: ['openmct', 'objectPath'],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
@ -131,8 +131,7 @@ export default {
|
||||
domainObject: undefined,
|
||||
formats: undefined,
|
||||
viewKey: `alphanumeric-format-${Math.random()}`,
|
||||
status: '',
|
||||
mutablePromise: undefined
|
||||
status: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -213,20 +212,14 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.openmct.objects.supportsMutation(this.item.identifier)) {
|
||||
this.mutablePromise = this.openmct.objects.getMutable(this.item.identifier)
|
||||
.then(this.setObject);
|
||||
} else {
|
||||
this.openmct.objects.get(this.item.identifier)
|
||||
.then(this.setObject);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@ -235,22 +228,13 @@ export default {
|
||||
}
|
||||
|
||||
this.openmct.time.off("bounds", this.refreshData);
|
||||
|
||||
if (this.mutablePromise) {
|
||||
this.mutablePromise.then(() => {
|
||||
this.openmct.objects.destroyMutable(this.domainObject);
|
||||
});
|
||||
} else {
|
||||
this.openmct.objects.destroyMutable(this.domainObject);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formattedValueForCopy() {
|
||||
const timeFormatterKey = this.openmct.time.timeSystem().key;
|
||||
const timeFormatter = this.formats[timeFormatterKey];
|
||||
const unit = this.unit ? ` ${this.unit}` : '';
|
||||
|
||||
return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue}${unit}`;
|
||||
return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue} ${this.unit}`;
|
||||
},
|
||||
requestHistoricalData() {
|
||||
let bounds = this.openmct.time.bounds();
|
||||
@ -301,7 +285,6 @@ export default {
|
||||
},
|
||||
setObject(domainObject) {
|
||||
this.domainObject = domainObject;
|
||||
this.mutablePromise = undefined;
|
||||
this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);
|
||||
|
@ -59,11 +59,11 @@ export default {
|
||||
font: 'default'
|
||||
};
|
||||
},
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
LayoutFrame
|
||||
},
|
||||
mixins: [conditionalStylesMixin],
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
|
@ -83,7 +83,7 @@ describe('the plugin', function () {
|
||||
}
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(testViewObject, []);
|
||||
const applicableViews = openmct.objectViews.get(testViewObject);
|
||||
let displayLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'layout.view');
|
||||
expect(displayLayoutViewProvider).toBeDefined();
|
||||
});
|
||||
|
@ -89,7 +89,7 @@ export default class DuplicateAction {
|
||||
{
|
||||
key: "name",
|
||||
control: "textfield",
|
||||
name: "Name",
|
||||
name: "Folder Name",
|
||||
pattern: "\\S+",
|
||||
required: true,
|
||||
cssClass: "l-input-lg"
|
||||
|
@ -48,14 +48,13 @@ export default class DuplicateTask {
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the duplicate/copy task with the objects provided.
|
||||
* Execute the duplicate/copy task with the objects provided in the constructor.
|
||||
* @returns {promise} Which will resolve with a clone of the object
|
||||
* once complete.
|
||||
*/
|
||||
async duplicate(domainObject, parent, filter) {
|
||||
this.domainObject = domainObject;
|
||||
this.parent = parent;
|
||||
this.namespace = parent.identifier.namespace;
|
||||
this.filter = filter || this.isCreatable;
|
||||
|
||||
await this.buildDuplicationPlan();
|
||||
@ -79,9 +78,8 @@ export default class DuplicateTask {
|
||||
*/
|
||||
async buildDuplicationPlan() {
|
||||
let domainObjectClone = await this.duplicateObject(this.domainObject);
|
||||
|
||||
if (domainObjectClone !== this.domainObject) {
|
||||
domainObjectClone.location = this.getKeyString(this.parent);
|
||||
domainObjectClone.location = this.getId(this.parent);
|
||||
}
|
||||
|
||||
this.firstClone = domainObjectClone;
|
||||
@ -98,14 +96,13 @@ export default class DuplicateTask {
|
||||
let initialCount = this.clones.length;
|
||||
let dialog = this.openmct.overlays.progressDialog({
|
||||
progressPerc: 0,
|
||||
message: `Duplicating ${initialCount} objects.`,
|
||||
message: `Duplicating ${initialCount} files.`,
|
||||
iconClass: 'info',
|
||||
title: 'Duplicating'
|
||||
});
|
||||
|
||||
let clonesDone = Promise.all(this.clones.map((clone) => {
|
||||
let clonesDone = Promise.all(this.clones.map(clone => {
|
||||
let percentPersisted = Math.ceil(100 * (++this.persisted / initialCount));
|
||||
let message = `Duplicating ${initialCount - this.persisted} objects.`;
|
||||
let message = `Duplicating ${initialCount - this.persisted} files.`;
|
||||
|
||||
dialog.updateProgress(percentPersisted, message);
|
||||
|
||||
@ -113,7 +110,6 @@ export default class DuplicateTask {
|
||||
}));
|
||||
|
||||
await clonesDone;
|
||||
|
||||
dialog.dismiss();
|
||||
this.openmct.notifications.info(`Duplicated ${this.persisted} objects.`);
|
||||
|
||||
@ -145,7 +141,10 @@ export default class DuplicateTask {
|
||||
async duplicateObject(originalObject) {
|
||||
// Check if the creatable (or other passed in filter).
|
||||
if (this.filter(originalObject)) {
|
||||
// Clone original object
|
||||
let clone = this.cloneObjectModel(originalObject);
|
||||
|
||||
// Get children, if any
|
||||
let composeesCollection = this.openmct.composition.get(originalObject);
|
||||
let composees;
|
||||
|
||||
@ -153,6 +152,7 @@ export default class DuplicateTask {
|
||||
composees = await composeesCollection.load();
|
||||
}
|
||||
|
||||
// Recursively duplicate children
|
||||
return this.duplicateComposees(clone, composees);
|
||||
}
|
||||
|
||||
@ -160,6 +160,36 @@ export default class DuplicateTask {
|
||||
return originalObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update identifiers in a cloned object model (or part of
|
||||
* a cloned object model) to reflect new identifiers after
|
||||
* duplicating.
|
||||
* @private
|
||||
*/
|
||||
rewriteIdentifiers(obj, idMap) {
|
||||
function lookupValue(value) {
|
||||
return (typeof value === 'string' && idMap[value]) || value;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
obj.forEach((value, index) => {
|
||||
obj[index] = lookupValue(value);
|
||||
this.rewriteIdentifiers(obj[index], idMap);
|
||||
});
|
||||
} else if (obj && typeof obj === 'object') {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
let value = obj[key];
|
||||
obj[key] = lookupValue(value);
|
||||
if (idMap[key]) {
|
||||
delete obj[key];
|
||||
obj[idMap[key]] = value;
|
||||
}
|
||||
|
||||
this.rewriteIdentifiers(value, idMap);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array of objects composed by a parent, clone them, then
|
||||
* add them to the parent.
|
||||
@ -167,67 +197,34 @@ export default class DuplicateTask {
|
||||
* @returns {*}
|
||||
*/
|
||||
async duplicateComposees(clonedParent, composees = []) {
|
||||
let idMappings = [];
|
||||
let idMap = {};
|
||||
|
||||
let allComposeesDuplicated = composees.reduce(async (previousPromise, nextComposee) => {
|
||||
await previousPromise;
|
||||
|
||||
let clonedComposee = await this.duplicateObject(nextComposee);
|
||||
|
||||
if (clonedComposee) {
|
||||
idMappings.push({
|
||||
newId: clonedComposee.identifier,
|
||||
oldId: nextComposee.identifier
|
||||
});
|
||||
this.composeChild(clonedComposee, clonedParent, clonedComposee !== nextComposee);
|
||||
}
|
||||
idMap[this.getId(nextComposee)] = this.getId(clonedComposee);
|
||||
await this.composeChild(clonedComposee, clonedParent, clonedComposee !== nextComposee);
|
||||
|
||||
return;
|
||||
}, Promise.resolve());
|
||||
|
||||
await allComposeesDuplicated;
|
||||
|
||||
clonedParent = this.rewriteIdentifiers(clonedParent, idMappings);
|
||||
this.rewriteIdentifiers(clonedParent, idMap);
|
||||
this.clones.push(clonedParent);
|
||||
|
||||
return clonedParent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update identifiers in a cloned object model (or part of
|
||||
* a cloned object model) to reflect new identifiers after
|
||||
* duplicating.
|
||||
* @private
|
||||
*/
|
||||
rewriteIdentifiers(clonedParent, childIdMappings) {
|
||||
for (let { newId, oldId } of childIdMappings) {
|
||||
let newIdKeyString = this.openmct.objects.makeKeyString(newId);
|
||||
let oldIdKeyString = this.openmct.objects.makeKeyString(oldId);
|
||||
|
||||
// regex replace keystrings
|
||||
clonedParent = JSON.stringify(clonedParent).replace(new RegExp(oldIdKeyString, 'g'), newIdKeyString);
|
||||
|
||||
// parse reviver to replace identifiers
|
||||
clonedParent = JSON.parse(clonedParent, (key, value) => {
|
||||
if (Object.prototype.hasOwnProperty.call(value, 'key')
|
||||
&& Object.prototype.hasOwnProperty.call(value, 'namespace')
|
||||
&& value.key === oldId.key
|
||||
&& value.namespace === oldId.namespace) {
|
||||
return newId;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return clonedParent;
|
||||
}
|
||||
|
||||
composeChild(child, parent, setLocation) {
|
||||
parent.composition.push(child.identifier);
|
||||
async composeChild(child, parent, setLocation) {
|
||||
const PERSIST_BOOL = false;
|
||||
let parentComposition = this.openmct.composition.get(parent);
|
||||
await parentComposition.load();
|
||||
parentComposition.add(child, PERSIST_BOOL);
|
||||
|
||||
//If a location is not specified, set it.
|
||||
if (setLocation && child.location === undefined) {
|
||||
let parentKeyString = this.getKeyString(parent);
|
||||
let parentKeyString = this.getId(parent);
|
||||
child.location = parentKeyString;
|
||||
}
|
||||
}
|
||||
@ -242,7 +239,7 @@ export default class DuplicateTask {
|
||||
let clone = JSON.parse(JSON.stringify(domainObject));
|
||||
let identifier = {
|
||||
key: uuid(),
|
||||
namespace: this.namespace // set to NEW parent's namespace
|
||||
namespace: domainObject.identifier.namespace
|
||||
};
|
||||
|
||||
if (clone.modified || clone.persisted || clone.location) {
|
||||
@ -263,7 +260,7 @@ export default class DuplicateTask {
|
||||
return clone;
|
||||
}
|
||||
|
||||
getKeyString(domainObject) {
|
||||
getId(domainObject) {
|
||||
return this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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>'
|
||||
});
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -41,10 +41,10 @@
|
||||
import FilterField from './FilterField.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
FilterField
|
||||
},
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
globalMetadata: {
|
||||
type: Object,
|
||||
|
@ -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,
|
||||
|
@ -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,21 +236,16 @@ export default {
|
||||
this.newFrameLocation = [containerIndex, insertFrameIndex];
|
||||
},
|
||||
addFrame(domainObject) {
|
||||
let keystring = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
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;
|
||||
let frame = new Frame(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;
|
||||
let frame = new Frame(domainObject.identifier);
|
||||
container.frames.splice(frameIndex + 1, 0, frame);
|
||||
sizeItems(container.frames, frame);
|
||||
|
||||
container.frames.splice(frameIndex + 1, 0, frame);
|
||||
sizeItems(container.frames, frame);
|
||||
|
||||
this.newFrameLocation = [];
|
||||
this.persist(containerIndex);
|
||||
this.identifierMap[keystring] = true;
|
||||
}
|
||||
this.newFrameLocation = [];
|
||||
this.persist(containerIndex);
|
||||
},
|
||||
deleteFrame(frameId) {
|
||||
let container = this.containers
|
||||
@ -263,15 +254,16 @@ export default {
|
||||
.frames
|
||||
.filter((f => f.id === frameId))[0];
|
||||
|
||||
this.removeFromComposition(frame.domainObjectIdentifier);
|
||||
|
||||
this.$nextTick().then(() => {
|
||||
sizeToFill(container.frames);
|
||||
this.setSelectionToParent();
|
||||
});
|
||||
this.removeFromComposition(frame.domainObjectIdentifier)
|
||||
.then(() => {
|
||||
sizeToFill(container.frames);
|
||||
this.setSelectionToParent();
|
||||
});
|
||||
},
|
||||
removeFromComposition(identifier) {
|
||||
this.composition.remove({identifier});
|
||||
return this.openmct.objects.get(identifier).then((childDomainObject) => {
|
||||
this.RemoveAction.removeFromComposition(this.domainObject, childDomainObject);
|
||||
});
|
||||
},
|
||||
setSelectionToParent() {
|
||||
this.$el.click();
|
||||
@ -350,9 +342,6 @@ export default {
|
||||
removeChildObject(identifier) {
|
||||
let removeIdentifier = this.openmct.objects.makeKeyString(identifier);
|
||||
|
||||
this.identifierMap[removeIdentifier] = undefined;
|
||||
delete this.identifierMap[removeIdentifier];
|
||||
|
||||
this.containers.forEach(container => {
|
||||
container.frames = container.frames.filter(frame => {
|
||||
let frameIdentifier = this.openmct.objects.makeKeyString(frame.domainObjectIdentifier);
|
||||
|
@ -58,10 +58,10 @@
|
||||
import ObjectFrame from '../../../ui/components/ObjectFrame.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
ObjectFrame
|
||||
},
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
frame: {
|
||||
type: Object,
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -1,9 +1,7 @@
|
||||
<template>
|
||||
<tr
|
||||
class="c-list-item"
|
||||
:class="{
|
||||
'is-alias': item.isAlias === true
|
||||
}"
|
||||
:class="{ 'is-alias': item.isAlias === true }"
|
||||
@click="navigate"
|
||||
>
|
||||
<td class="c-list-item__name">
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
@ -1,25 +1,3 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import ImageryViewLayout from './components/ImageryViewLayout.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
|
@ -1,124 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="c-compass"
|
||||
:style="compassDimensionsStyle"
|
||||
>
|
||||
<CompassHUD
|
||||
v-if="hasCameraFieldOfView"
|
||||
:sun-heading="sunHeading"
|
||||
:camera-angle-of-view="cameraAngleOfView"
|
||||
:camera-pan="cameraPan"
|
||||
/>
|
||||
<CompassRose
|
||||
v-if="hasCameraFieldOfView"
|
||||
:heading="heading"
|
||||
:sun-heading="sunHeading"
|
||||
:camera-angle-of-view="cameraAngleOfView"
|
||||
:camera-pan="cameraPan"
|
||||
:lock-compass="lockCompass"
|
||||
@toggle-lock-compass="toggleLockCompass"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CompassHUD from './CompassHUD.vue';
|
||||
import CompassRose from './CompassRose.vue';
|
||||
|
||||
const CAMERA_ANGLE_OF_VIEW = 70;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CompassHUD,
|
||||
CompassRose
|
||||
},
|
||||
props: {
|
||||
containerWidth: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
containerHeight: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
naturalAspectRatio: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
image: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
lockCompass: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasCameraFieldOfView() {
|
||||
return this.cameraPan !== undefined && this.cameraAngleOfView > 0;
|
||||
},
|
||||
// horizontal rotation from north in degrees
|
||||
heading() {
|
||||
return this.image.heading;
|
||||
},
|
||||
// horizontal rotation from north in degrees
|
||||
sunHeading() {
|
||||
return this.image.sunOrientation;
|
||||
},
|
||||
// horizontal rotation from north in degrees
|
||||
cameraPan() {
|
||||
return this.image.cameraPan;
|
||||
},
|
||||
cameraAngleOfView() {
|
||||
return CAMERA_ANGLE_OF_VIEW;
|
||||
},
|
||||
compassDimensionsStyle() {
|
||||
const containerAspectRatio = this.containerWidth / this.containerHeight;
|
||||
|
||||
let width;
|
||||
let height;
|
||||
|
||||
if (containerAspectRatio < this.naturalAspectRatio) {
|
||||
width = '100%';
|
||||
height = `${ this.containerWidth / this.naturalAspectRatio }px`;
|
||||
} else {
|
||||
width = `${ this.containerHeight * this.naturalAspectRatio }px`;
|
||||
height = '100%';
|
||||
}
|
||||
|
||||
return {
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleLockCompass() {
|
||||
this.$emit('toggle-lock-compass');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -1,141 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="c-compass__hud c-hud"
|
||||
>
|
||||
<div
|
||||
v-for="point in visibleCompassPoints"
|
||||
:key="point.direction"
|
||||
:class="point.class"
|
||||
:style="point.style"
|
||||
>
|
||||
{{ point.direction }}
|
||||
</div>
|
||||
<div
|
||||
v-if="isSunInRange"
|
||||
ref="sun"
|
||||
class="c-hud__sun"
|
||||
:style="sunPositionStyle"
|
||||
></div>
|
||||
<div class="c-hud__range"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
rotate,
|
||||
inRange,
|
||||
percentOfRange
|
||||
} from './utils';
|
||||
|
||||
const COMPASS_POINTS = [
|
||||
{
|
||||
direction: 'N',
|
||||
class: 'c-hud__dir',
|
||||
degrees: 0
|
||||
},
|
||||
{
|
||||
direction: 'NE',
|
||||
class: 'c-hud__dir--sub',
|
||||
degrees: 45
|
||||
},
|
||||
{
|
||||
direction: 'E',
|
||||
class: 'c-hud__dir',
|
||||
degrees: 90
|
||||
},
|
||||
{
|
||||
direction: 'SE',
|
||||
class: 'c-hud__dir--sub',
|
||||
degrees: 135
|
||||
},
|
||||
{
|
||||
direction: 'S',
|
||||
class: 'c-hud__dir',
|
||||
degrees: 180
|
||||
},
|
||||
{
|
||||
direction: 'SW',
|
||||
class: 'c-hud__dir--sub',
|
||||
degrees: 225
|
||||
},
|
||||
{
|
||||
direction: 'W',
|
||||
class: 'c-hud__dir',
|
||||
degrees: 270
|
||||
},
|
||||
{
|
||||
direction: 'NW',
|
||||
class: 'c-hud__dir--sub',
|
||||
degrees: 315
|
||||
}
|
||||
];
|
||||
|
||||
export default {
|
||||
props: {
|
||||
sunHeading: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
cameraAngleOfView: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
cameraPan: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
visibleCompassPoints() {
|
||||
return COMPASS_POINTS
|
||||
.filter(point => inRange(point.degrees, this.visibleRange))
|
||||
.map(point => {
|
||||
const percentage = percentOfRange(point.degrees, this.visibleRange);
|
||||
point.style = Object.assign(
|
||||
{ left: `${ percentage * 100 }%` }
|
||||
);
|
||||
|
||||
return point;
|
||||
});
|
||||
},
|
||||
isSunInRange() {
|
||||
return inRange(this.sunHeading, this.visibleRange);
|
||||
},
|
||||
sunPositionStyle() {
|
||||
const percentage = percentOfRange(this.sunHeading, this.visibleRange);
|
||||
|
||||
return {
|
||||
left: `${ percentage * 100 }%`
|
||||
};
|
||||
},
|
||||
visibleRange() {
|
||||
return [
|
||||
rotate(this.cameraPan, -this.cameraAngleOfView / 2),
|
||||
rotate(this.cameraPan, this.cameraAngleOfView / 2)
|
||||
];
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -1,261 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="c-direction-rose"
|
||||
@click="toggleLockCompass"
|
||||
>
|
||||
<div
|
||||
class="c-nsew"
|
||||
:style="compassRoseStyle"
|
||||
>
|
||||
<svg
|
||||
class="c-nsew__minor-ticks"
|
||||
viewBox="0 0 100 100"
|
||||
>
|
||||
<rect
|
||||
class="c-nsew__tick c-tick-ne"
|
||||
x="49"
|
||||
y="0"
|
||||
width="2"
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
class="c-nsew__tick c-tick-se"
|
||||
x="95"
|
||||
y="49"
|
||||
width="5"
|
||||
height="2"
|
||||
/>
|
||||
<rect
|
||||
class="c-nsew__tick c-tick-sw"
|
||||
x="49"
|
||||
y="95"
|
||||
width="2"
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
class="c-nsew__tick c-tick-nw"
|
||||
x="0"
|
||||
y="49"
|
||||
width="5"
|
||||
height="2"
|
||||
/>
|
||||
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
class="c-nsew__ticks"
|
||||
viewBox="0 0 100 100"
|
||||
>
|
||||
<polygon
|
||||
class="c-nsew__tick c-tick-n"
|
||||
points="50,0 57,5 43,5"
|
||||
/>
|
||||
<rect
|
||||
class="c-nsew__tick c-tick-e"
|
||||
x="95"
|
||||
y="49"
|
||||
width="5"
|
||||
height="2"
|
||||
/>
|
||||
<rect
|
||||
class="c-nsew__tick c-tick-w"
|
||||
x="0"
|
||||
y="49"
|
||||
width="5"
|
||||
height="2"
|
||||
/>
|
||||
<rect
|
||||
class="c-nsew__tick c-tick-s"
|
||||
x="49"
|
||||
y="95"
|
||||
width="2"
|
||||
height="5"
|
||||
/>
|
||||
|
||||
<text
|
||||
class="c-nsew__label c-label-n"
|
||||
text-anchor="middle"
|
||||
:transform="northTextTransform"
|
||||
>N</text>
|
||||
<text
|
||||
class="c-nsew__label c-label-e"
|
||||
text-anchor="middle"
|
||||
:transform="eastTextTransform"
|
||||
>E</text>
|
||||
<text
|
||||
class="c-nsew__label c-label-w"
|
||||
text-anchor="middle"
|
||||
:transform="southTextTransform"
|
||||
>W</text>
|
||||
<text
|
||||
class="c-nsew__label c-label-s"
|
||||
text-anchor="middle"
|
||||
:transform="westTextTransform"
|
||||
>S</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="hasHeading"
|
||||
class="c-spacecraft-body"
|
||||
:style="headingStyle"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="hasSunHeading"
|
||||
class="c-sun"
|
||||
:style="sunHeadingStyle"
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="c-cam-field"
|
||||
:style="cameraPanStyle"
|
||||
>
|
||||
<div class="cam-field-half cam-field-half-l">
|
||||
<div
|
||||
class="cam-field-area"
|
||||
:style="cameraFOVStyleLeftHalf"
|
||||
></div>
|
||||
</div>
|
||||
<div class="cam-field-half cam-field-half-r">
|
||||
<div
|
||||
class="cam-field-area"
|
||||
:style="cameraFOVStyleRightHalf"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { rotate } from './utils';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
heading: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
sunHeading: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
cameraAngleOfView: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
cameraPan: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
lockCompass: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
north() {
|
||||
return this.lockCompass ? rotate(-this.cameraPan) : 0;
|
||||
},
|
||||
compassRoseStyle() {
|
||||
return { transform: `rotate(${ this.north }deg)` };
|
||||
},
|
||||
northTextTransform() {
|
||||
return this.cardinalPointsTextTransform.north;
|
||||
},
|
||||
eastTextTransform() {
|
||||
return this.cardinalPointsTextTransform.east;
|
||||
},
|
||||
southTextTransform() {
|
||||
return this.cardinalPointsTextTransform.south;
|
||||
},
|
||||
westTextTransform() {
|
||||
return this.cardinalPointsTextTransform.west;
|
||||
},
|
||||
cardinalPointsTextTransform() {
|
||||
/**
|
||||
* cardinal points text must be rotated
|
||||
* in the opposite direction that north is rotated
|
||||
* to keep text vertically oriented
|
||||
*/
|
||||
const rotation = `rotate(${ -this.north })`;
|
||||
|
||||
return {
|
||||
north: `translate(50,15) ${ rotation }`,
|
||||
east: `translate(87,50) ${ rotation }`,
|
||||
south: `translate(13,50) ${ rotation }`,
|
||||
west: `translate(50,87) ${ rotation }`
|
||||
};
|
||||
},
|
||||
hasHeading() {
|
||||
return this.heading !== undefined;
|
||||
},
|
||||
headingStyle() {
|
||||
const rotation = rotate(this.north, this.heading);
|
||||
|
||||
return {
|
||||
transform: `translateX(-50%) rotate(${ rotation }deg)`
|
||||
};
|
||||
},
|
||||
hasSunHeading() {
|
||||
return this.sunHeading !== undefined;
|
||||
},
|
||||
sunHeadingStyle() {
|
||||
const rotation = rotate(this.north, this.sunHeading);
|
||||
|
||||
return {
|
||||
transform: `rotate(${ rotation }deg)`
|
||||
};
|
||||
},
|
||||
cameraPanStyle() {
|
||||
const rotation = rotate(this.north, this.cameraPan);
|
||||
|
||||
return {
|
||||
transform: `rotate(${ rotation }deg)`
|
||||
};
|
||||
},
|
||||
// left half of camera field of view
|
||||
// rotated counter-clockwise from camera pan angle
|
||||
cameraFOVStyleLeftHalf() {
|
||||
return {
|
||||
transform: `translateX(50%) rotate(${ -this.cameraAngleOfView / 2 }deg)`
|
||||
};
|
||||
},
|
||||
// right half of camera field of view
|
||||
// rotated clockwise from camera pan angle
|
||||
cameraFOVStyleRightHalf() {
|
||||
return {
|
||||
transform: `translateX(-50%) rotate(${ this.cameraAngleOfView / 2 }deg)`
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleLockCompass() {
|
||||
this.$emit('toggle-lock-compass');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -1,214 +0,0 @@
|
||||
/***************************** THEME/UI CONSTANTS AND MIXINS */
|
||||
$interfaceKeyColor: #00B9C5;
|
||||
$elemBg: rgba(black, 0.7);
|
||||
|
||||
@mixin sun($position: 'circle closest-side') {
|
||||
$color: #ff9900;
|
||||
$gradEdgePerc: 60%;
|
||||
background: radial-gradient(#{$position}, $color, $color $gradEdgePerc, rgba($color, 0.4) $gradEdgePerc + 5%, transparent);
|
||||
|
||||
}
|
||||
|
||||
.c-compass {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1;
|
||||
@include userSelectNone;
|
||||
}
|
||||
|
||||
/***************************** COMPASS HUD */
|
||||
.c-hud {
|
||||
// To be placed within a imagery view, in the bounding box of the image
|
||||
$m: 1px;
|
||||
$padTB: 2px;
|
||||
$padLR: $padTB;
|
||||
color: $interfaceKeyColor;
|
||||
font-size: 0.8em;
|
||||
position: absolute;
|
||||
top: $m; right: $m; left: $m;
|
||||
height: 18px;
|
||||
|
||||
svg, div {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&__display {
|
||||
height: 30px;
|
||||
pointer-events: all;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&__range {
|
||||
border: 1px solid $interfaceKeyColor;
|
||||
border-top-color: transparent;
|
||||
position: absolute;
|
||||
top: 50%; right: $padLR; bottom: $padTB; left: $padLR;
|
||||
}
|
||||
|
||||
[class*="__dir"] {
|
||||
// NSEW
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 1px 2px black;
|
||||
top: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
[class*="__dir--sub"] {
|
||||
font-weight: normal;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&__sun {
|
||||
$s: 10px;
|
||||
@include sun('circle farthest-side at bottom');
|
||||
bottom: $padTB + 2px;
|
||||
height: $s; width: $s*2;
|
||||
opacity: 0.8;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/***************************** COMPASS DIRECTIONS */
|
||||
.c-nsew {
|
||||
$color: $interfaceKeyColor;
|
||||
$inset: 7%;
|
||||
$tickHeightPerc: 15%;
|
||||
text-shadow: black 0 0 10px;
|
||||
top: $inset; right: $inset; bottom: $inset; left: $inset;
|
||||
z-index: 3;
|
||||
|
||||
&__tick,
|
||||
&__label {
|
||||
fill: $color;
|
||||
}
|
||||
|
||||
&__minor-ticks {
|
||||
opacity: 0.5;
|
||||
transform-origin: center;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
&__label {
|
||||
dominant-baseline: central;
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.c-label-n {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
}
|
||||
|
||||
/***************************** CAMERA FIELD ANGLE */
|
||||
.c-cam-field {
|
||||
$color: white;
|
||||
opacity: 0.2;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
|
||||
.cam-field-half {
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
.cam-field-area {
|
||||
background: $color;
|
||||
top: -30%;
|
||||
right: 0;
|
||||
bottom: -30%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
// clip-paths overlap a bit to avoid a gap between halves
|
||||
&-l {
|
||||
clip-path: polygon(0 0, 50.5% 0, 50.5% 100%, 0 100%);
|
||||
.cam-field-area {
|
||||
transform-origin: left center;
|
||||
}
|
||||
}
|
||||
|
||||
&-r {
|
||||
clip-path: polygon(49.5% 0, 100% 0, 100% 100%, 49.5% 100%);
|
||||
.cam-field-area {
|
||||
transform-origin: right center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/***************************** SPACECRAFT BODY */
|
||||
.c-spacecraft-body {
|
||||
$color: $interfaceKeyColor;
|
||||
$s: 30%;
|
||||
background: $color;
|
||||
border-radius: 3px;
|
||||
height: $s; width: $s;
|
||||
left: 50%; top: 50%;
|
||||
opacity: 0.4;
|
||||
transform-origin: center top;
|
||||
|
||||
&:before {
|
||||
// Direction arrow
|
||||
$color: rgba(black, 0.5);
|
||||
$arwPointerY: 60%;
|
||||
$arwBodyOffset: 25%;
|
||||
background: $color;
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 10%; right: 20%; bottom: 50%; left: 20%;
|
||||
clip-path: polygon(50% 0, 100% $arwPointerY, 100%-$arwBodyOffset $arwPointerY, 100%-$arwBodyOffset 100%, $arwBodyOffset 100%, $arwBodyOffset $arwPointerY, 0 $arwPointerY);
|
||||
}
|
||||
}
|
||||
|
||||
/***************************** DIRECTION ROSE */
|
||||
.c-direction-rose {
|
||||
$d: 100px;
|
||||
$c2: rgba(white, 0.1);
|
||||
background: $elemBg;
|
||||
background-image: radial-gradient(circle closest-side, transparent, transparent 80%, $c2);
|
||||
width: $d;
|
||||
height: $d;
|
||||
transform-origin: 0 0;
|
||||
position: absolute;
|
||||
bottom: 10px; left: 10px;
|
||||
clip-path: circle(50% at 50% 50%);
|
||||
border-radius: 100%;
|
||||
|
||||
svg, div {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
// Sun
|
||||
.c-sun {
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
&:before {
|
||||
$s: 35%;
|
||||
@include sun();
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
opacity: 0.7;
|
||||
top: 0; left: 50%;
|
||||
height:$s; width: $s;
|
||||
transform: translate(-50%, -60%);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,84 +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 Compass from './Compass.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
const COMPASS_ROSE_CLASS = '.c-direction-rose';
|
||||
const COMPASS_HUD_CLASS = '.c-compass__hud';
|
||||
|
||||
describe("The Compass component", () => {
|
||||
let app;
|
||||
let instance;
|
||||
|
||||
beforeEach(() => {
|
||||
let imageDatum = {
|
||||
heading: 100,
|
||||
roll: 90,
|
||||
pitch: 90,
|
||||
cameraTilt: 100,
|
||||
cameraPan: 90,
|
||||
sunAngle: 30
|
||||
};
|
||||
let propsData = {
|
||||
containerWidth: 600,
|
||||
containerHeight: 600,
|
||||
naturalAspectRatio: 0.9,
|
||||
image: imageDatum
|
||||
};
|
||||
|
||||
app = new Vue({
|
||||
components: { Compass },
|
||||
data() {
|
||||
return propsData;
|
||||
},
|
||||
template: `<Compass
|
||||
:container-width="containerWidth"
|
||||
:container-height="containerHeight"
|
||||
:natural-aspect-ratio="naturalAspectRatio"
|
||||
:image="image" />`
|
||||
});
|
||||
instance = app.$mount();
|
||||
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
app.$destroy();
|
||||
});
|
||||
|
||||
describe("when a heading value exists on the image", () => {
|
||||
|
||||
it("should display a compass rose", () => {
|
||||
let compassRoseElement = instance.$el.querySelector(COMPASS_ROSE_CLASS
|
||||
);
|
||||
|
||||
expect(compassRoseElement).toBeDefined();
|
||||
});
|
||||
|
||||
it("should display a compass HUD", () => {
|
||||
let compassHUDElement = instance.$el.querySelector(COMPASS_HUD_CLASS);
|
||||
|
||||
expect(compassHUDElement).toBeDefined();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,44 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* sums an arbitrary number of absolute rotations
|
||||
* (meaning rotations relative to one common direction 0)
|
||||
* normalizes the rotation to the range [0, 360)
|
||||
*
|
||||
* @param {...number} rotations in degrees
|
||||
* @returns {number} normalized sum of all rotations - [0, 360) degrees
|
||||
*/
|
||||
export function rotate(...rotations) {
|
||||
const rotation = rotations.reduce((a, b) => a + b, 0);
|
||||
|
||||
return normalizeCompassDirection(rotation);
|
||||
}
|
||||
|
||||
export function inRange(degrees, [min, max]) {
|
||||
const point = rotate(degrees);
|
||||
|
||||
return min > max
|
||||
? (point >= min && point < 360) || (point <= max && point >= 0)
|
||||
: point >= min && point <= max;
|
||||
}
|
||||
|
||||
export function percentOfRange(degrees, [min, max]) {
|
||||
let distance = rotate(degrees);
|
||||
let minRange = min;
|
||||
let maxRange = max;
|
||||
|
||||
if (min > max) {
|
||||
if (distance < max) {
|
||||
distance += 360;
|
||||
}
|
||||
|
||||
maxRange += 360;
|
||||
}
|
||||
|
||||
return (distance - minRange) / (maxRange - minRange);
|
||||
}
|
||||
|
||||
function normalizeCompassDirection(degrees) {
|
||||
const base = degrees % 360;
|
||||
|
||||
return base >= 0 ? base : 360 + base;
|
||||
}
|
@ -1,25 +1,3 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div
|
||||
tabindex="0"
|
||||
@ -58,25 +36,14 @@
|
||||
<div class="c-imagery__main-image__bg"
|
||||
:class="{'paused unnsynced': isPaused,'stale':false }"
|
||||
>
|
||||
<img
|
||||
ref="focusedImage"
|
||||
class="c-imagery__main-image__image js-imageryView-image"
|
||||
:src="imageUrl"
|
||||
:style="{
|
||||
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`
|
||||
}"
|
||||
:data-openmct-image-timestamp="time"
|
||||
:data-openmct-object-keystring="keyString"
|
||||
>
|
||||
<Compass
|
||||
v-if="shouldDisplayCompass"
|
||||
:container-width="imageContainerWidth"
|
||||
:container-height="imageContainerHeight"
|
||||
:natural-aspect-ratio="focusedImageNaturalAspectRatio"
|
||||
:image="focusedImage"
|
||||
:lock-compass="lockCompass"
|
||||
@toggle-lock-compass="toggleLockCompass"
|
||||
/>
|
||||
<div class="c-imagery__main-image__image"
|
||||
:style="{
|
||||
'background-image': imageUrl ? `url(${imageUrl})` : 'none',
|
||||
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`
|
||||
}"
|
||||
:data-openmct-image-timestamp="time"
|
||||
:data-openmct-object-keystring="keyString"
|
||||
></div>
|
||||
</div>
|
||||
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
|
||||
<button class="c-nav c-nav--prev"
|
||||
@ -94,25 +61,11 @@
|
||||
<div class="c-imagery__control-bar">
|
||||
<div class="c-imagery__time">
|
||||
<div class="c-imagery__timestamp u-style-receiver js-style-receiver">{{ time }}</div>
|
||||
|
||||
<!-- image fresh -->
|
||||
<div
|
||||
v-if="canTrackDuration"
|
||||
:class="{'c-imagery--new': isImageNew && !refreshCSS}"
|
||||
class="c-imagery__age icon-timer"
|
||||
>{{ formattedDuration }}</div>
|
||||
|
||||
<!-- spacecraft position fresh -->
|
||||
<div
|
||||
v-if="relatedTelemetry.hasRelatedTelemetry && isSpacecraftPositionFresh"
|
||||
class="c-imagery__age icon-check c-imagery--new"
|
||||
>POS</div>
|
||||
|
||||
<!-- camera position fresh -->
|
||||
<div
|
||||
v-if="relatedTelemetry.hasRelatedTelemetry && isCameraPositionFresh"
|
||||
class="c-imagery__age icon-check c-imagery--new"
|
||||
>CAM</div>
|
||||
</div>
|
||||
<div class="h-local-controls">
|
||||
<button
|
||||
@ -123,32 +76,28 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="thumbsWrapper"
|
||||
class="c-imagery__thumbs-wrapper"
|
||||
:class="{'is-paused': isPaused}"
|
||||
@scroll="handleScroll"
|
||||
<div ref="thumbsWrapper"
|
||||
class="c-imagery__thumbs-wrapper"
|
||||
:class="{'is-paused': isPaused}"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div v-for="(image, index) in imageHistory"
|
||||
:key="image.url + image.time"
|
||||
<div v-for="(datum, index) in imageHistory"
|
||||
:key="datum.url"
|
||||
class="c-imagery__thumb c-thumb"
|
||||
:class="{ selected: focusedImageIndex === index && isPaused }"
|
||||
@click="setFocusedImage(index, thumbnailClick)"
|
||||
>
|
||||
<img class="c-thumb__image"
|
||||
:src="image.url"
|
||||
:src="formatImageUrl(datum)"
|
||||
>
|
||||
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
|
||||
<div class="c-thumb__timestamp">{{ formatTime(datum) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import Compass from './Compass/Compass.vue';
|
||||
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
|
||||
|
||||
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||
const REFRESH_CSS_MS = 500;
|
||||
@ -167,9 +116,6 @@ const ARROW_RIGHT = 39;
|
||||
const ARROW_LEFT = 37;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Compass
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
let timeSystem = this.openmct.time.timeSystem();
|
||||
@ -191,15 +137,7 @@ export default {
|
||||
refreshCSS: false,
|
||||
keyString: undefined,
|
||||
focusedImageIndex: undefined,
|
||||
focusedImageRelatedTelemetry: {},
|
||||
numericDuration: undefined,
|
||||
metadataEndpoints: {},
|
||||
relatedTelemetry: {},
|
||||
latestRelatedTelemetry: {},
|
||||
focusedImageNaturalAspectRatio: undefined,
|
||||
imageContainerWidth: undefined,
|
||||
imageContainerHeight: undefined,
|
||||
lockCompass: true
|
||||
numericDuration: undefined
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -257,83 +195,15 @@ export default {
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
shouldDisplayCompass() {
|
||||
return this.focusedImage !== undefined
|
||||
&& this.focusedImageNaturalAspectRatio !== undefined
|
||||
&& this.imageContainerWidth !== undefined
|
||||
&& this.imageContainerHeight !== undefined;
|
||||
},
|
||||
isSpacecraftPositionFresh() {
|
||||
let isFresh = undefined;
|
||||
let latest = this.latestRelatedTelemetry;
|
||||
let focused = this.focusedImageRelatedTelemetry;
|
||||
|
||||
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
isFresh = true;
|
||||
for (let key of this.spacecraftPositionKeys) {
|
||||
if (this.relatedTelemetry[key] && latest[key] && focused[key]) {
|
||||
isFresh = isFresh && Boolean(this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key]));
|
||||
} else {
|
||||
isFresh = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return isFresh;
|
||||
},
|
||||
isSpacecraftOrientationFresh() {
|
||||
let isFresh = undefined;
|
||||
let latest = this.latestRelatedTelemetry;
|
||||
let focused = this.focusedImageRelatedTelemetry;
|
||||
|
||||
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
isFresh = true;
|
||||
for (let key of this.spacecraftOrientationKeys) {
|
||||
if (this.relatedTelemetry[key] && latest[key] && focused[key]) {
|
||||
isFresh = isFresh && Boolean(this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key]));
|
||||
} else {
|
||||
isFresh = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return isFresh;
|
||||
},
|
||||
isCameraPositionFresh() {
|
||||
let isFresh = undefined;
|
||||
let latest = this.latestRelatedTelemetry;
|
||||
let focused = this.focusedImageRelatedTelemetry;
|
||||
|
||||
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
isFresh = true;
|
||||
|
||||
// camera freshness relies on spacecraft position freshness
|
||||
if (this.isSpacecraftPositionFresh && this.isSpacecraftOrientationFresh) {
|
||||
for (let key of this.cameraKeys) {
|
||||
if (this.relatedTelemetry[key] && latest[key] && focused[key]) {
|
||||
isFresh = isFresh && Boolean(this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key]));
|
||||
} else {
|
||||
isFresh = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
isFresh = false;
|
||||
}
|
||||
}
|
||||
|
||||
return isFresh;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
focusedImageIndex() {
|
||||
this.trackDuration();
|
||||
this.resetAgeCSS();
|
||||
this.updateRelatedTelemetryForFocusedImage();
|
||||
this.getImageNaturalDimensions();
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
mounted() {
|
||||
// listen
|
||||
this.openmct.time.on('bounds', this.boundsChange);
|
||||
this.openmct.time.on('timeSystem', this.timeSystemChange);
|
||||
@ -342,15 +212,8 @@ export default {
|
||||
// set
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] };
|
||||
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
|
||||
|
||||
// related telemetry keys
|
||||
this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ'];
|
||||
this.spacecraftOrientationKeys = ['heading'];
|
||||
this.cameraKeys = ['cameraPan', 'cameraTilt'];
|
||||
this.sunKeys = ['sunOrientation'];
|
||||
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]);
|
||||
|
||||
// initialize
|
||||
this.timeKey = this.timeSystem.key;
|
||||
@ -359,18 +222,6 @@ export default {
|
||||
// kickoff
|
||||
this.subscribe();
|
||||
this.requestHistory();
|
||||
|
||||
// related telemetry
|
||||
await this.initializeRelatedTelemetry();
|
||||
this.updateRelatedTelemetryForFocusedImage();
|
||||
this.trackLatestRelatedTelemetry();
|
||||
|
||||
// for scrolling through images quickly and resizing the object view
|
||||
_.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
|
||||
_.debounce(this.resizeImageContainer, 400);
|
||||
|
||||
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
|
||||
this.imageContainerResizeObserver.observe(this.$refs.focusedImage);
|
||||
},
|
||||
updated() {
|
||||
this.scrollToRight();
|
||||
@ -381,120 +232,12 @@ export default {
|
||||
delete this.unsubscribe;
|
||||
}
|
||||
|
||||
this.imageContainerResizeObserver.disconnect();
|
||||
|
||||
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
this.relatedTelemetry.destroy();
|
||||
}
|
||||
|
||||
this.stopDurationTracking();
|
||||
this.openmct.time.off('bounds', this.boundsChange);
|
||||
this.openmct.time.off('timeSystem', this.timeSystemChange);
|
||||
this.openmct.time.off('clock', this.clockChange);
|
||||
|
||||
// unsubscribe from related telemetry
|
||||
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
for (let key of this.relatedTelemetry.keys) {
|
||||
if (this.relatedTelemetry[key] && this.relatedTelemetry[key].unsubscribe) {
|
||||
this.relatedTelemetry[key].unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async initializeRelatedTelemetry() {
|
||||
this.relatedTelemetry = new RelatedTelemetry(
|
||||
this.openmct,
|
||||
this.domainObject,
|
||||
[...this.spacecraftPositionKeys, ...this.spacecraftOrientationKeys, ...this.cameraKeys, ...this.sunKeys]
|
||||
);
|
||||
|
||||
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
await this.relatedTelemetry.load();
|
||||
}
|
||||
},
|
||||
async getMostRecentRelatedTelemetry(key, targetDatum) {
|
||||
if (!this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
throw new Error(`${this.domainObject.name} does not have any related telemetry`);
|
||||
}
|
||||
|
||||
if (!this.relatedTelemetry[key]) {
|
||||
throw new Error(`${key} does not exist on related telemetry`);
|
||||
}
|
||||
|
||||
let mostRecent;
|
||||
let valueKey = this.relatedTelemetry[key].historical.valueKey;
|
||||
let valuesOnTelemetry = this.relatedTelemetry[key].hasTelemetryOnDatum;
|
||||
|
||||
if (valuesOnTelemetry) {
|
||||
mostRecent = targetDatum[valueKey];
|
||||
|
||||
if (mostRecent) {
|
||||
return mostRecent;
|
||||
} else {
|
||||
console.warn(`Related Telemetry for ${key} does NOT exist on this telemetry datum as configuration implied.`);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
mostRecent = await this.relatedTelemetry[key].requestLatestFor(targetDatum);
|
||||
|
||||
return mostRecent[valueKey];
|
||||
},
|
||||
// will subscribe to data for this key if not already done
|
||||
subscribeToDataForKey(key) {
|
||||
if (this.relatedTelemetry[key].isSubscribed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.relatedTelemetry[key].realtimeDomainObject) {
|
||||
this.relatedTelemetry[key].unsubscribe = this.openmct.telemetry.subscribe(
|
||||
this.relatedTelemetry[key].realtimeDomainObject, datum => {
|
||||
this.relatedTelemetry[key].listeners.forEach(callback => {
|
||||
callback(datum);
|
||||
});
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
this.relatedTelemetry[key].isSubscribed = true;
|
||||
}
|
||||
},
|
||||
async updateRelatedTelemetryForFocusedImage() {
|
||||
if (!this.relatedTelemetry.hasRelatedTelemetry || !this.focusedImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
// set data ON image telemetry as well as in focusedImageRelatedTelemetry
|
||||
for (let key of this.relatedTelemetry.keys) {
|
||||
if (
|
||||
this.relatedTelemetry[key]
|
||||
&& this.relatedTelemetry[key].historical
|
||||
&& this.relatedTelemetry[key].requestLatestFor
|
||||
|
||||
) {
|
||||
let valuesOnTelemetry = this.relatedTelemetry[key].hasTelemetryOnDatum;
|
||||
let value = await this.getMostRecentRelatedTelemetry(key, this.focusedImage);
|
||||
|
||||
if (!valuesOnTelemetry) {
|
||||
this.$set(this.imageHistory[this.focusedImageIndex], key, value); // manually add to telemetry
|
||||
}
|
||||
|
||||
this.$set(this.focusedImageRelatedTelemetry, key, value);
|
||||
}
|
||||
}
|
||||
},
|
||||
trackLatestRelatedTelemetry() {
|
||||
[...this.spacecraftPositionKeys, ...this.spacecraftOrientationKeys, ...this.cameraKeys, ...this.sunKeys].forEach(key => {
|
||||
if (this.relatedTelemetry[key] && this.relatedTelemetry[key].subscribe) {
|
||||
this.relatedTelemetry[key].subscribe((datum) => {
|
||||
let valueKey = this.relatedTelemetry[key].realtime.valueKey;
|
||||
this.$set(this.latestRelatedTelemetry, key, datum[valueKey]);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
focusElement() {
|
||||
this.$el.focus();
|
||||
},
|
||||
@ -615,7 +358,6 @@ export default {
|
||||
this.requestCount++;
|
||||
const requestId = this.requestCount;
|
||||
this.imageHistory = [];
|
||||
|
||||
let data = await this.openmct.telemetry
|
||||
.request(this.domainObject, bounds) || [];
|
||||
|
||||
@ -651,12 +393,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
let image = { ...datum };
|
||||
image.formattedTime = this.formatTime(datum);
|
||||
image.url = this.formatImageUrl(datum);
|
||||
image.time = datum[this.timeKey];
|
||||
|
||||
this.imageHistory.push(image);
|
||||
this.imageHistory.push(datum);
|
||||
|
||||
if (setFocused) {
|
||||
this.setFocusedImage(this.imageHistory.length - 1);
|
||||
@ -772,28 +509,6 @@ export default {
|
||||
},
|
||||
isLeftOrRightArrowKey(keyCode) {
|
||||
return [ARROW_RIGHT, ARROW_LEFT].includes(keyCode);
|
||||
},
|
||||
getImageNaturalDimensions() {
|
||||
this.focusedImageNaturalAspectRatio = undefined;
|
||||
|
||||
const img = this.$refs.focusedImage;
|
||||
|
||||
// TODO - should probably cache this
|
||||
img.addEventListener('load', () => {
|
||||
this.focusedImageNaturalAspectRatio = img.naturalWidth / img.naturalHeight;
|
||||
}, { once: true });
|
||||
},
|
||||
resizeImageContainer() {
|
||||
if (this.$refs.focusedImage.clientWidth !== this.imageContainerWidth) {
|
||||
this.imageContainerWidth = this.$refs.focusedImage.clientWidth;
|
||||
}
|
||||
|
||||
if (this.$refs.focusedImage.clientHeight !== this.imageContainerHeight) {
|
||||
this.imageContainerHeight = this.$refs.focusedImage.clientHeight;
|
||||
}
|
||||
},
|
||||
toggleLockCompass() {
|
||||
this.lockCompass = !this.lockCompass;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,164 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
function copyRelatedMetadata(metadata) {
|
||||
let compare = metadata.comparisonFunction;
|
||||
let copiedMetadata = JSON.parse(JSON.stringify(metadata));
|
||||
copiedMetadata.comparisonFunction = compare;
|
||||
|
||||
return copiedMetadata;
|
||||
}
|
||||
|
||||
export default class RelatedTelemetry {
|
||||
|
||||
constructor(openmct, domainObject, telemetryKeys) {
|
||||
this._openmct = openmct;
|
||||
this._domainObject = domainObject;
|
||||
|
||||
let metadata = this._openmct.telemetry.getMetadata(this._domainObject);
|
||||
let imageHints = metadata.valuesForHints(['image'])[0];
|
||||
|
||||
this.hasRelatedTelemetry = imageHints.relatedTelemetry !== undefined;
|
||||
|
||||
if (this.hasRelatedTelemetry) {
|
||||
this.keys = telemetryKeys;
|
||||
|
||||
this._timeFormatter = undefined;
|
||||
this._timeSystemChange(this._openmct.time.timeSystem());
|
||||
|
||||
// grab related telemetry metadata
|
||||
for (let key of this.keys) {
|
||||
if (imageHints.relatedTelemetry[key]) {
|
||||
this[key] = copyRelatedMetadata(imageHints.relatedTelemetry[key]);
|
||||
}
|
||||
}
|
||||
|
||||
this.load = this.load.bind(this);
|
||||
this._parseTime = this._parseTime.bind(this);
|
||||
this._timeSystemChange = this._timeSystemChange.bind(this);
|
||||
this.destroy = this.destroy.bind(this);
|
||||
|
||||
this._openmct.time.on('timeSystem', this._timeSystemChange);
|
||||
}
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (!this.hasRelatedTelemetry) {
|
||||
throw new Error('This domain object does not have related telemetry, use "hasRelatedTelemetry" to check before loading.');
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
this.keys.map(async (key) => {
|
||||
if (this[key]) {
|
||||
if (this[key].historical) {
|
||||
await this._initializeHistorical(key);
|
||||
}
|
||||
|
||||
if (this[key].realtime && this[key].realtime.telemetryObjectId && this[key].realtime.telemetryObjectId !== '') {
|
||||
await this._intializeRealtime(key);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async _initializeHistorical(key) {
|
||||
if (!this[key].historical.telemetryObjectId) {
|
||||
this[key].historical.hasTelemetryOnDatum = true;
|
||||
} else if (this[key].historical.telemetryObjectId !== '') {
|
||||
this[key].historicalDomainObject = await this._openmct.objects.get(this[key].historical.telemetryObjectId);
|
||||
|
||||
this[key].requestLatestFor = async (datum) => {
|
||||
const options = {
|
||||
start: this._openmct.time.bounds().start,
|
||||
end: this._parseTime(datum),
|
||||
strategy: 'latest'
|
||||
};
|
||||
let results = await this._openmct.telemetry
|
||||
.request(this[key].historicalDomainObject, options);
|
||||
|
||||
return results[results.length - 1];
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async _intializeRealtime(key) {
|
||||
this[key].realtimeDomainObject = await this._openmct.objects.get(this[key].realtime.telemetryObjectId);
|
||||
this[key].listeners = [];
|
||||
this[key].subscribe = (callback) => {
|
||||
|
||||
if (!this[key].isSubscribed) {
|
||||
this._subscribeToDataForKey(key);
|
||||
}
|
||||
|
||||
if (!this[key].listeners.includes(callback)) {
|
||||
this[key].listeners.push(callback);
|
||||
|
||||
return () => {
|
||||
this[key].listeners.remove(callback);
|
||||
};
|
||||
} else {
|
||||
return () => {};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_subscribeToDataForKey(key) {
|
||||
if (this[key].isSubscribed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this[key].realtimeDomainObject) {
|
||||
this[key].unsubscribe = this._openmct.telemetry.subscribe(
|
||||
this[key].realtimeDomainObject, datum => {
|
||||
this[key].listeners.forEach(callback => {
|
||||
callback(datum);
|
||||
});
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
this[key].isSubscribed = true;
|
||||
}
|
||||
}
|
||||
|
||||
_parseTime(datum) {
|
||||
return this._timeFormatter.parse(datum);
|
||||
}
|
||||
|
||||
_timeSystemChange(system) {
|
||||
let key = system.key;
|
||||
let metadata = this._openmct.telemetry.getMetadata(this._domainObject);
|
||||
let metadataValue = metadata.value(key) || { format: key };
|
||||
this._timeFormatter = this._openmct.telemetry.getValueFormatter(metadataValue);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._openmct.time.off('timeSystem', this._timeSystemChange);
|
||||
for (let key of this.keys) {
|
||||
if (this[key] && this[key].unsubscribe) {
|
||||
this[key].unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -23,7 +23,6 @@
|
||||
background-color: $colorPlotBg;
|
||||
border: 1px solid transparent;
|
||||
flex: 1 1 auto;
|
||||
height: 0;
|
||||
|
||||
&.unnsynced{
|
||||
@include sUnsynced();
|
||||
@ -31,9 +30,10 @@
|
||||
}
|
||||
|
||||
&__image {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
@include abs(); // Safari fix
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,14 +71,13 @@
|
||||
}
|
||||
|
||||
&__age {
|
||||
border-radius: $smallCr;
|
||||
border-radius: $controlCr;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
padding: 2px $interiorMarginSm;
|
||||
align-items: baseline;
|
||||
padding: 1px $interiorMarginSm;
|
||||
|
||||
&:before {
|
||||
font-size: 0.9em;
|
||||
opacity: 0.5;
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
@ -87,9 +86,8 @@
|
||||
&--new {
|
||||
// New imagery
|
||||
$bgColor: $colorOk;
|
||||
color: $colorOkFg;
|
||||
background: rgba($bgColor, 0.5);
|
||||
@include flash($animName: flashImageAge, $iter: 2, $dur: 250ms, $valStart: rgba($colorOk, 0.7), $valEnd: rgba($colorOk, 0));
|
||||
@include flash($animName: flashImageAge, $dur: 250ms, $valStart: rgba($colorOk, 0.7), $valEnd: rgba($colorOk, 0));
|
||||
}
|
||||
|
||||
&__thumbs-wrapper {
|
||||
|
@ -1,25 +1,3 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import ImageryViewProvider from './ImageryViewProvider';
|
||||
|
||||
export default function () {
|
||||
|
@ -1,373 +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 ImageryPlugin from './plugin.js';
|
||||
import Vue from 'vue';
|
||||
import {
|
||||
createOpenMct,
|
||||
resetApplicationState,
|
||||
simulateKeyEvent
|
||||
} from 'utils/testing';
|
||||
|
||||
const ONE_MINUTE = 1000 * 60;
|
||||
const TEN_MINUTES = ONE_MINUTE * 10;
|
||||
const MAIN_IMAGE_CLASS = '.js-imageryView-image';
|
||||
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
|
||||
const REFRESH_CSS_MS = 500;
|
||||
const TOLERANCE = 0.50;
|
||||
|
||||
function comparisonFunction(valueOne, valueTwo) {
|
||||
let larger = valueOne;
|
||||
let smaller = valueTwo;
|
||||
|
||||
if (larger < smaller) {
|
||||
larger = valueTwo;
|
||||
smaller = valueOne;
|
||||
}
|
||||
|
||||
return (larger - smaller) < TOLERANCE;
|
||||
}
|
||||
|
||||
function getImageInfo(doc) {
|
||||
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
|
||||
let timestamp = imageElement.dataset.openmctImageTimestamp;
|
||||
let identifier = imageElement.dataset.openmctObjectKeystring;
|
||||
let url = imageElement.src;
|
||||
|
||||
return {
|
||||
timestamp,
|
||||
identifier,
|
||||
url
|
||||
};
|
||||
}
|
||||
|
||||
function isNew(doc) {
|
||||
let newIcon = doc.querySelectorAll(NEW_IMAGE_CLASS);
|
||||
|
||||
return newIcon.length !== 0;
|
||||
}
|
||||
|
||||
function generateTelemetry(start, count) {
|
||||
let telemetry = [];
|
||||
|
||||
for (let i = 1, l = count + 1; i < l; i++) {
|
||||
let stringRep = i + 'minute';
|
||||
let logo = 'images/logo-openmct.svg';
|
||||
|
||||
telemetry.push({
|
||||
"name": stringRep + " Imagery",
|
||||
"utc": start + (i * ONE_MINUTE),
|
||||
"url": location.host + '/' + logo + '?time=' + stringRep,
|
||||
"timeId": stringRep,
|
||||
"value": 100
|
||||
});
|
||||
}
|
||||
|
||||
return telemetry;
|
||||
}
|
||||
|
||||
describe("The Imagery View Layout", () => {
|
||||
const imageryKey = 'example.imagery';
|
||||
const START = Date.now();
|
||||
const COUNT = 10;
|
||||
|
||||
let openmct;
|
||||
let imageryPlugin;
|
||||
let parent;
|
||||
let child;
|
||||
let timeFormat = 'utc';
|
||||
let bounds = {
|
||||
start: START - TEN_MINUTES,
|
||||
end: START
|
||||
};
|
||||
let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT);
|
||||
let imageryObject = {
|
||||
identifier: {
|
||||
namespace: "",
|
||||
key: "imageryId"
|
||||
},
|
||||
name: "Example Imagery",
|
||||
type: "example.imagery",
|
||||
location: "parentId",
|
||||
modified: 0,
|
||||
persisted: 0,
|
||||
telemetry: {
|
||||
values: [
|
||||
{
|
||||
"name": "Image",
|
||||
"key": "url",
|
||||
"format": "image",
|
||||
"hints": {
|
||||
"image": 1,
|
||||
"priority": 3
|
||||
},
|
||||
"source": "url",
|
||||
"relatedTelemetry": {
|
||||
"heading": {
|
||||
"comparisonFunction": comparisonFunction,
|
||||
"historical": {
|
||||
"telemetryObjectId": "heading",
|
||||
"valueKey": "value"
|
||||
}
|
||||
},
|
||||
"roll": {
|
||||
"comparisonFunction": comparisonFunction,
|
||||
"historical": {
|
||||
"telemetryObjectId": "roll",
|
||||
"valueKey": "value"
|
||||
}
|
||||
},
|
||||
"pitch": {
|
||||
"comparisonFunction": comparisonFunction,
|
||||
"historical": {
|
||||
"telemetryObjectId": "pitch",
|
||||
"valueKey": "value"
|
||||
}
|
||||
},
|
||||
"cameraPan": {
|
||||
"comparisonFunction": comparisonFunction,
|
||||
"historical": {
|
||||
"telemetryObjectId": "cameraPan",
|
||||
"valueKey": "value"
|
||||
}
|
||||
},
|
||||
"cameraTilt": {
|
||||
"comparisonFunction": comparisonFunction,
|
||||
"historical": {
|
||||
"telemetryObjectId": "cameraTilt",
|
||||
"valueKey": "value"
|
||||
}
|
||||
},
|
||||
"sunOrientation": {
|
||||
"comparisonFunction": comparisonFunction,
|
||||
"historical": {
|
||||
"telemetryObjectId": "sunOrientation",
|
||||
"valueKey": "value"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Name",
|
||||
"key": "name",
|
||||
"source": "name",
|
||||
"hints": {
|
||||
"priority": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Time",
|
||||
"key": "utc",
|
||||
"format": "utc",
|
||||
"hints": {
|
||||
"domain": 2,
|
||||
"priority": 1
|
||||
},
|
||||
"source": "utc"
|
||||
},
|
||||
{
|
||||
"name": "Local Time",
|
||||
"key": "local",
|
||||
"format": "local-format",
|
||||
"hints": {
|
||||
"domain": 1,
|
||||
"priority": 2
|
||||
},
|
||||
"source": "local"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// this setups up the app
|
||||
beforeEach((done) => {
|
||||
const appHolder = document.createElement('div');
|
||||
appHolder.style.width = '640px';
|
||||
appHolder.style.height = '480px';
|
||||
|
||||
openmct = createOpenMct();
|
||||
|
||||
parent = document.createElement('div');
|
||||
child = document.createElement('div');
|
||||
parent.appendChild(child);
|
||||
|
||||
spyOn(window, 'ResizeObserver').and.returnValue({
|
||||
observe() {},
|
||||
disconnect() {}
|
||||
});
|
||||
|
||||
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
|
||||
|
||||
imageryPlugin = new ImageryPlugin();
|
||||
openmct.install(imageryPlugin);
|
||||
|
||||
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
|
||||
|
||||
openmct.time.timeSystem(timeFormat, {
|
||||
start: 0,
|
||||
end: 4
|
||||
});
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless(appHolder);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it("should provide an imagery view only for imagery producing objects", () => {
|
||||
let applicableViews = openmct.objectViews.get(imageryObject, []);
|
||||
let imageryView = applicableViews.find(
|
||||
viewProvider => viewProvider.key === imageryKey
|
||||
);
|
||||
|
||||
expect(imageryView).toBeDefined();
|
||||
});
|
||||
|
||||
describe("imagery view", () => {
|
||||
let applicableViews;
|
||||
let imageryViewProvider;
|
||||
let imageryView;
|
||||
|
||||
beforeEach(async (done) => {
|
||||
let telemetryRequestResolve;
|
||||
let telemetryRequestPromise = new Promise((resolve) => {
|
||||
telemetryRequestResolve = resolve;
|
||||
});
|
||||
|
||||
openmct.telemetry.request.and.callFake(() => {
|
||||
telemetryRequestResolve(imageTelemetry);
|
||||
|
||||
return telemetryRequestPromise;
|
||||
});
|
||||
|
||||
openmct.time.clock('local', {
|
||||
start: bounds.start,
|
||||
end: bounds.end + 100
|
||||
});
|
||||
|
||||
applicableViews = openmct.objectViews.get(imageryObject, []);
|
||||
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey);
|
||||
imageryView = imageryViewProvider.view(imageryObject);
|
||||
imageryView.show(child);
|
||||
|
||||
await telemetryRequestPromise;
|
||||
await Vue.nextTick();
|
||||
|
||||
return done();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
imageryView.destroy();
|
||||
});
|
||||
|
||||
it("on mount should show the the most recent image", () => {
|
||||
const imageInfo = getImageInfo(parent);
|
||||
|
||||
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should show the clicked thumbnail as the main image", async () => {
|
||||
const target = imageTelemetry[5].url;
|
||||
parent.querySelectorAll(`img[src='${target}']`)[0].click();
|
||||
await Vue.nextTick();
|
||||
const imageInfo = getImageInfo(parent);
|
||||
|
||||
expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should show that an image is new", async (done) => {
|
||||
await Vue.nextTick();
|
||||
|
||||
// used in code, need to wait to the 500ms here too
|
||||
setTimeout(() => {
|
||||
const imageIsNew = isNew(parent);
|
||||
|
||||
expect(imageIsNew).toBeTrue();
|
||||
done();
|
||||
}, REFRESH_CSS_MS);
|
||||
});
|
||||
|
||||
it("should show that an image is not new", async (done) => {
|
||||
const target = imageTelemetry[2].url;
|
||||
parent.querySelectorAll(`img[src='${target}']`)[0].click();
|
||||
|
||||
await Vue.nextTick();
|
||||
|
||||
// used in code, need to wait to the 500ms here too
|
||||
setTimeout(() => {
|
||||
const imageIsNew = isNew(parent);
|
||||
|
||||
expect(imageIsNew).toBeFalse();
|
||||
done();
|
||||
}, REFRESH_CSS_MS);
|
||||
});
|
||||
|
||||
it("should navigate via arrow keys", async () => {
|
||||
let keyOpts = {
|
||||
element: parent.querySelector('.c-imagery'),
|
||||
key: 'ArrowLeft',
|
||||
keyCode: 37,
|
||||
type: 'keyup'
|
||||
};
|
||||
|
||||
simulateKeyEvent(keyOpts);
|
||||
|
||||
await Vue.nextTick();
|
||||
|
||||
const imageInfo = getImageInfo(parent);
|
||||
|
||||
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should navigate via numerous arrow keys", async () => {
|
||||
let element = parent.querySelector('.c-imagery');
|
||||
let type = 'keyup';
|
||||
let leftKeyOpts = {
|
||||
element,
|
||||
type,
|
||||
key: 'ArrowLeft',
|
||||
keyCode: 37
|
||||
};
|
||||
let rightKeyOpts = {
|
||||
element,
|
||||
type,
|
||||
key: 'ArrowRight',
|
||||
keyCode: 39
|
||||
};
|
||||
|
||||
// left thrice
|
||||
simulateKeyEvent(leftKeyOpts);
|
||||
simulateKeyEvent(leftKeyOpts);
|
||||
simulateKeyEvent(leftKeyOpts);
|
||||
// right once
|
||||
simulateKeyEvent(rightKeyOpts);
|
||||
|
||||
await Vue.nextTick();
|
||||
|
||||
const imageInfo = getImageInfo(parent);
|
||||
|
||||
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
@ -1,114 +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 local time", () => {
|
||||
const LOCAL_FORMAT_KEY = 'local-format';
|
||||
const LOCAL_SYSTEM_KEY = 'local';
|
||||
const JUNK = "junk";
|
||||
const TIMESTAMP = -14256000000;
|
||||
const DATESTRING = '1969-07-20 12:00:00.000 am';
|
||||
let openmct;
|
||||
|
||||
beforeEach((done) => {
|
||||
|
||||
openmct = createOpenMct();
|
||||
|
||||
openmct.install(openmct.plugins.LocalTimeSystem());
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
describe("system", function () {
|
||||
|
||||
let localTimeSystem;
|
||||
|
||||
beforeEach(() => {
|
||||
localTimeSystem = openmct.time.timeSystem(LOCAL_SYSTEM_KEY, {
|
||||
start: 0,
|
||||
end: 4
|
||||
});
|
||||
});
|
||||
|
||||
it("is installed", () => {
|
||||
let timeSystems = openmct.time.getAllTimeSystems();
|
||||
let local = timeSystems.find(ts => ts.key === LOCAL_SYSTEM_KEY);
|
||||
|
||||
expect(local).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("can be set to be the main time system", () => {
|
||||
expect(openmct.time.timeSystem().key).toBe(LOCAL_SYSTEM_KEY);
|
||||
});
|
||||
|
||||
it("uses the local-format time format", () => {
|
||||
expect(localTimeSystem.timeFormat).toBe(LOCAL_FORMAT_KEY);
|
||||
});
|
||||
|
||||
it("is UTC based", () => {
|
||||
expect(localTimeSystem.isUTCBased).toBe(true);
|
||||
});
|
||||
|
||||
it("defines expected metadata", () => {
|
||||
expect(localTimeSystem.key).toBe(LOCAL_SYSTEM_KEY);
|
||||
expect(localTimeSystem.name).toBeDefined();
|
||||
expect(localTimeSystem.cssClass).toBeDefined();
|
||||
expect(localTimeSystem.durationFormat).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatter can be obtained from the telemetry API and", () => {
|
||||
|
||||
let localTimeFormatter;
|
||||
let dateString;
|
||||
let timeStamp;
|
||||
|
||||
beforeEach(() => {
|
||||
localTimeFormatter = openmct.telemetry.getFormatter(LOCAL_FORMAT_KEY);
|
||||
dateString = localTimeFormatter.format(TIMESTAMP);
|
||||
timeStamp = localTimeFormatter.parse(DATESTRING);
|
||||
});
|
||||
|
||||
it("will format a timestamp in local time format", () => {
|
||||
expect(localTimeFormatter.format(TIMESTAMP)).toBe(dateString);
|
||||
});
|
||||
|
||||
it("will parse an local time Date String into milliseconds", () => {
|
||||
expect(localTimeFormatter.parse(DATESTRING)).toBe(timeStamp);
|
||||
});
|
||||
|
||||
it("will validate correctly", () => {
|
||||
expect(localTimeFormatter.validate(DATESTRING)).toBe(true);
|
||||
expect(localTimeFormatter.validate(JUNK)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
@ -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
|
||||
});
|
||||
};
|
||||
}
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
@ -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(() => {
|
||||
@ -233,49 +226,18 @@ 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 {
|
||||
domainObject: this.internalDomainObject,
|
||||
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 +311,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 +395,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) {
|
||||
@ -500,10 +442,10 @@ export default {
|
||||
async updateDefaultNotebook(notebookStorage) {
|
||||
const defaultNotebookObject = await this.getDefaultNotebookObject();
|
||||
if (!defaultNotebookObject) {
|
||||
setDefaultNotebook(this.openmct, notebookStorage, this.internalDomainObject);
|
||||
setDefaultNotebook(this.openmct, notebookStorage);
|
||||
} else if (objectUtils.makeKeyString(defaultNotebookObject.identifier) !== objectUtils.makeKeyString(notebookStorage.notebookMeta.identifier)) {
|
||||
this.removeDefaultClass(defaultNotebookObject);
|
||||
setDefaultNotebook(this.openmct, notebookStorage, this.internalDomainObject);
|
||||
setDefaultNotebook(this.openmct, notebookStorage);
|
||||
}
|
||||
|
||||
if (this.defaultSectionId && this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) {
|
||||
@ -572,13 +514,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 || {};
|
||||
|
@ -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,
|
||||
@ -143,9 +143,7 @@ export default {
|
||||
this.openmct.notifications.alert(message);
|
||||
}
|
||||
|
||||
const relativeHash = hash.slice(hash.indexOf('#'));
|
||||
const url = new URL(relativeHash, `${location.protocol}//${location.host}${location.pathname}`);
|
||||
window.location.hash = url.hash;
|
||||
window.location.hash = hash;
|
||||
},
|
||||
formatTime(unixTime, timeFormat) {
|
||||
return Moment.utc(unixTime).format(timeFormat);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -44,46 +44,38 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
notebookSnapshot: undefined,
|
||||
notebookSnapshot: null,
|
||||
notebookTypes: []
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
validateNotebookStorageObject();
|
||||
this.getDefaultNotebookObject();
|
||||
|
||||
this.notebookSnapshot = new Snapshot(this.openmct);
|
||||
this.setDefaultNotebookStatus();
|
||||
},
|
||||
methods: {
|
||||
async getDefaultNotebookObject() {
|
||||
const defaultNotebook = getDefaultNotebook();
|
||||
const defaultNotebookObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier);
|
||||
|
||||
return defaultNotebookObject;
|
||||
},
|
||||
async showMenu(event) {
|
||||
showMenu(event) {
|
||||
const notebookTypes = [];
|
||||
const defaultNotebook = getDefaultNotebook();
|
||||
const elementBoundingClientRect = this.$el.getBoundingClientRect();
|
||||
const x = elementBoundingClientRect.x;
|
||||
const y = elementBoundingClientRect.y + elementBoundingClientRect.height;
|
||||
|
||||
const defaultNotebookObject = await this.getDefaultNotebookObject();
|
||||
if (defaultNotebookObject) {
|
||||
const name = defaultNotebookObject.name;
|
||||
if (defaultNotebook) {
|
||||
const domainObject = defaultNotebook.domainObject;
|
||||
|
||||
const defaultNotebook = getDefaultNotebook();
|
||||
const sectionName = defaultNotebook.section.name;
|
||||
const pageName = defaultNotebook.page.name;
|
||||
const defaultPath = `${name} - ${sectionName} - ${pageName}`;
|
||||
if (domainObject.location) {
|
||||
const defaultPath = `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`;
|
||||
|
||||
notebookTypes.push({
|
||||
cssClass: 'icon-notebook',
|
||||
name: `Save to Notebook ${defaultPath}`,
|
||||
callBack: () => {
|
||||
return this.snapshot(NOTEBOOK_DEFAULT);
|
||||
}
|
||||
});
|
||||
notebookTypes.push({
|
||||
cssClass: 'icon-notebook',
|
||||
name: `Save to Notebook ${defaultPath}`,
|
||||
callBack: () => {
|
||||
return this.snapshot(NOTEBOOK_DEFAULT);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
notebookTypes.push({
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user