Compare commits
	
		
			5 Commits
		
	
	
		
			style-time
			...
			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.2-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",
 | 
			
		||||
 
 | 
			
		||||
@@ -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 = {
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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']
 | 
			
		||||
            }));
 | 
			
		||||
 
 | 
			
		||||
@@ -61,7 +61,6 @@ define([
 | 
			
		||||
            const newStyleObject = utils.toNewFormat(legacyObject.getModel(), legacyObject.getId());
 | 
			
		||||
            const keystring = utils.makeKeyString(newStyleObject.identifier);
 | 
			
		||||
 | 
			
		||||
            this.eventEmitter.emit(keystring + ':$_synchronize_model', newStyleObject);
 | 
			
		||||
            this.eventEmitter.emit(keystring + ":*", newStyleObject);
 | 
			
		||||
            this.eventEmitter.emit('mutation', newStyleObject);
 | 
			
		||||
        }.bind(this);
 | 
			
		||||
@@ -139,12 +138,6 @@ define([
 | 
			
		||||
            });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    ObjectServiceProvider.prototype.superSecretFallbackSearch = function (query, options) {
 | 
			
		||||
        const searchService = this.$injector.get('searchService');
 | 
			
		||||
 | 
			
		||||
        return searchService.query(query);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Injects new object API as a decorator so that it hijacks all requests.
 | 
			
		||||
    // Object providers implemented on new API should just work, old API should just work, many things may break.
 | 
			
		||||
    function LegacyObjectAPIInterceptor(openmct, ROOTS, instantiate, topic, objectService) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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,492 +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
 | 
			
		||||
 * @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) {
 | 
			
		||||
    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');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!provider.get) {
 | 
			
		||||
        throw new Error('Provider does not support get!');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let objectPromise = provider.get(identifier);
 | 
			
		||||
    this.cache[keystring] = objectPromise;
 | 
			
		||||
 | 
			
		||||
    return objectPromise.then(result => {
 | 
			
		||||
        delete this.cache[keystring];
 | 
			
		||||
        const interceptors = this.listGetInterceptors(identifier, result);
 | 
			
		||||
        interceptors.forEach(interceptor => {
 | 
			
		||||
            result = interceptor.invoke(identifier, result);
 | 
			
		||||
            return result;
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    ObjectAPI.prototype.delete = function () {
 | 
			
		||||
        throw new Error('Delete not implemented');
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    ObjectAPI.prototype.isPersistable = function (domainObject) {
 | 
			
		||||
        let provider = this.getProvider(domainObject.identifier);
 | 
			
		||||
 | 
			
		||||
        return provider !== undefined
 | 
			
		||||
            && provider.create !== undefined
 | 
			
		||||
            && provider.update !== undefined;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save this domain object in its current state. EXPERIMENTAL
 | 
			
		||||
     *
 | 
			
		||||
     * @private
 | 
			
		||||
     * @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 {Object} options search options
 | 
			
		||||
 * @returns {Array.<Promise.<module:openmct.DomainObject>>}
 | 
			
		||||
 *          an array of promises returned from each object provider's search function
 | 
			
		||||
 *          each resolving to domain objects matching provided search query and options.
 | 
			
		||||
 */
 | 
			
		||||
ObjectAPI.prototype.search = function (query, options) {
 | 
			
		||||
    const searchPromises = Object.values(this.providers)
 | 
			
		||||
        .filter(provider => provider.search !== undefined)
 | 
			
		||||
        .map(provider => provider.search(query, options));
 | 
			
		||||
    /**
 | 
			
		||||
     * 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, options)
 | 
			
		||||
        .then(results => results.hits
 | 
			
		||||
            .map(hit => utils.toNewFormat(hit.object.getModel(), hit.object.getId()))));
 | 
			
		||||
    /**
 | 
			
		||||
     * 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 searchPromises;
 | 
			
		||||
};
 | 
			
		||||
        return mutableObject.set(path, value);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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.`);
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * 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);
 | 
			
		||||
 | 
			
		||||
    return this.get(identifier).then((object) => {
 | 
			
		||||
        return this._toMutable(object);
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
        return mutableObject.stopListening.bind(mutableObject);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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");
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
    /**
 | 
			
		||||
     * @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);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
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);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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: [],
 | 
			
		||||
 
 | 
			
		||||
@@ -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";
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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,20 +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) {
 | 
			
		||||
            let keystring = this.openmct.objects.makeKeyString(identifier);
 | 
			
		||||
 | 
			
		||||
            this.identifierMap[keystring] = undefined;
 | 
			
		||||
            delete this.identifierMap[keystring];
 | 
			
		||||
 | 
			
		||||
            this.composition.remove({identifier});
 | 
			
		||||
            return this.openmct.objects.get(identifier).then((childDomainObject) => {
 | 
			
		||||
                this.RemoveAction.removeFromComposition(this.domainObject, childDomainObject);
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        setSelectionToParent() {
 | 
			
		||||
            this.$el.click();
 | 
			
		||||
 
 | 
			
		||||
@@ -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,131 +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"
 | 
			
		||||
        :heading="heading"
 | 
			
		||||
        :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.heading !== undefined && this.cameraPan !== undefined;
 | 
			
		||||
        },
 | 
			
		||||
        // compass direction from north in degrees
 | 
			
		||||
        heading() {
 | 
			
		||||
            return this.image.heading;
 | 
			
		||||
        },
 | 
			
		||||
        pitch() {
 | 
			
		||||
            return this.image.pitch;
 | 
			
		||||
        },
 | 
			
		||||
        // compass direction from north in degrees
 | 
			
		||||
        sunHeading() {
 | 
			
		||||
            return this.image.sunOrientation;
 | 
			
		||||
        },
 | 
			
		||||
        // relative direction from heading in degrees
 | 
			
		||||
        cameraPan() {
 | 
			
		||||
            return this.image.cameraPan;
 | 
			
		||||
        },
 | 
			
		||||
        cameraTilt() {
 | 
			
		||||
            return this.image.cameraTilt;
 | 
			
		||||
        },
 | 
			
		||||
        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,145 +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: {
 | 
			
		||||
        heading: {
 | 
			
		||||
            type: Number,
 | 
			
		||||
            required: true
 | 
			
		||||
        },
 | 
			
		||||
        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.heading, this.cameraPan, -this.cameraAngleOfView / 2),
 | 
			
		||||
                rotate(this.heading, this.cameraPan, this.cameraAngleOfView / 2)
 | 
			
		||||
            ];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,267 +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="rotateFrameStyle"
 | 
			
		||||
    >
 | 
			
		||||
        <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
 | 
			
		||||
        class="c-spacecraft-body"
 | 
			
		||||
        :style="headingStyle"
 | 
			
		||||
    >
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div
 | 
			
		||||
        v-if="showSunHeading"
 | 
			
		||||
        class="c-sun"
 | 
			
		||||
        :style="sunHeadingStyle"
 | 
			
		||||
    ></div>
 | 
			
		||||
 | 
			
		||||
    <div
 | 
			
		||||
        v-if="showCameraFOV"
 | 
			
		||||
        class="c-cam-field"
 | 
			
		||||
        :style="cameraHeadingStyle"
 | 
			
		||||
    >
 | 
			
		||||
        <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: {
 | 
			
		||||
        cameraHeading() {
 | 
			
		||||
            return rotate(this.heading, this.cameraPan);
 | 
			
		||||
        },
 | 
			
		||||
        compassHeading() {
 | 
			
		||||
            return this.lockCompass ? this.cameraHeading : 0;
 | 
			
		||||
        },
 | 
			
		||||
        north() {
 | 
			
		||||
            return rotate(this.compassHeading, -this.cameraHeading);
 | 
			
		||||
        },
 | 
			
		||||
        rotateFrameStyle() {
 | 
			
		||||
            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 }`
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        headingStyle() {
 | 
			
		||||
            const rotation = rotate(this.north, this.heading);
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                transform: `translateX(-50%) rotate(${ rotation }deg)`
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        cameraHeadingStyle() {
 | 
			
		||||
            const rotation = rotate(this.north, this.cameraHeading);
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                transform: `rotate(${ rotation }deg)`
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        showSunHeading() {
 | 
			
		||||
            return this.sunHeading !== undefined;
 | 
			
		||||
        },
 | 
			
		||||
        sunHeadingStyle() {
 | 
			
		||||
            const rotation = rotate(this.north, this.sunHeading);
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                transform: `rotate(${ rotation }deg)`
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        showCameraFOV() {
 | 
			
		||||
            return this.cameraPan !== undefined && this.cameraAngleOfView > 0;
 | 
			
		||||
        },
 | 
			
		||||
        // left half of camera field of view
 | 
			
		||||
        // rotated counter-clockwise from camera field of view heading
 | 
			
		||||
        cameraFOVStyleLeftHalf() {
 | 
			
		||||
            return {
 | 
			
		||||
                transform: `translateX(50%) rotate(${ -this.cameraAngleOfView / 2 }deg)`
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        // right half of camera field of view
 | 
			
		||||
        // rotated clockwise from camera field of view heading
 | 
			
		||||
        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,33 +0,0 @@
 | 
			
		||||
export function rotate(direction, ...rotations) {
 | 
			
		||||
    const rotation = rotations.reduce((a, b) => a + b, 0);
 | 
			
		||||
 | 
			
		||||
    return normalizeCompassDirection(direction + rotation);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function normalizeCompassDirection(degrees) {
 | 
			
		||||
    const base = degrees % 360;
 | 
			
		||||
 | 
			
		||||
    return base >= 0 ? base : 360 + base;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function inRange(degrees, [min, max]) {
 | 
			
		||||
    return min > max
 | 
			
		||||
        ? (degrees >= min && degrees < 360) || (degrees <= max && degrees >= 0)
 | 
			
		||||
        : degrees >= min && degrees <= max;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function percentOfRange(degrees, [min, max]) {
 | 
			
		||||
    let distance = degrees;
 | 
			
		||||
    let minRange = min;
 | 
			
		||||
    let maxRange = max;
 | 
			
		||||
 | 
			
		||||
    if (min > max) {
 | 
			
		||||
        if (distance < max) {
 | 
			
		||||
            distance += 360;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        maxRange += 360;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (distance - minRange) / (maxRange - minRange);
 | 
			
		||||
}
 | 
			
		||||
@@ -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,69 +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.spacecraftKeys) {
 | 
			
		||||
                    if (this.relatedTelemetry[key] && latest[key] && focused[key]) {
 | 
			
		||||
                        if (!this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key])) {
 | 
			
		||||
                            isFresh = false;
 | 
			
		||||
                        }
 | 
			
		||||
                    } 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) {
 | 
			
		||||
                    for (let key of this.cameraKeys) {
 | 
			
		||||
                        if (this.relatedTelemetry[key] && latest[key] && focused[key]) {
 | 
			
		||||
                            if (!this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key])) {
 | 
			
		||||
                                isFresh = false;
 | 
			
		||||
                            }
 | 
			
		||||
                        } 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);
 | 
			
		||||
@@ -328,14 +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.spacecraftKeys = ['heading', 'roll', 'pitch'];
 | 
			
		||||
        this.cameraKeys = ['cameraPan', 'cameraTilt'];
 | 
			
		||||
        this.sunKeys = ['sunOrientation'];
 | 
			
		||||
        this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]);
 | 
			
		||||
 | 
			
		||||
        // initialize
 | 
			
		||||
        this.timeKey = this.timeSystem.key;
 | 
			
		||||
@@ -344,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();
 | 
			
		||||
@@ -366,115 +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].unsubscribe) {
 | 
			
		||||
                    this.relatedTelemetry[key].unsubscribe();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        async initializeRelatedTelemetry() {
 | 
			
		||||
            this.relatedTelemetry = new RelatedTelemetry(
 | 
			
		||||
                this.openmct,
 | 
			
		||||
                this.domainObject,
 | 
			
		||||
                [...this.spacecraftKeys, ...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) {
 | 
			
		||||
                    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.spacecraftKeys, ...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();
 | 
			
		||||
        },
 | 
			
		||||
@@ -595,7 +358,6 @@ export default {
 | 
			
		||||
            this.requestCount++;
 | 
			
		||||
            const requestId = this.requestCount;
 | 
			
		||||
            this.imageHistory = [];
 | 
			
		||||
 | 
			
		||||
            let data = await this.openmct.telemetry
 | 
			
		||||
                .request(this.domainObject, bounds) || [];
 | 
			
		||||
 | 
			
		||||
@@ -631,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);
 | 
			
		||||
@@ -752,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,162 +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].historical) {
 | 
			
		||||
                    await this._initializeHistorical(key);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (this[key].realtime && this[key].realtime.telemetryObjectId) {
 | 
			
		||||
                    await this._intializeRealtime(key);
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async _initializeHistorical(key) {
 | 
			
		||||
        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];
 | 
			
		||||
            };
 | 
			
		||||
        } else {
 | 
			
		||||
            this[key].historical.hasTelemetryOnDatum = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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].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
 | 
			
		||||
 
 | 
			
		||||
@@ -22,10 +22,10 @@ import { getDefaultNotebook } from '../utils/notebook-storage';
 | 
			
		||||
import Page from './PageComponent.vue';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    inject: ['openmct'],
 | 
			
		||||
    components: {
 | 
			
		||||
        Page
 | 
			
		||||
    },
 | 
			
		||||
    inject: ['openmct'],
 | 
			
		||||
    props: {
 | 
			
		||||
        defaultPageId: {
 | 
			
		||||
            type: String,
 | 
			
		||||
 
 | 
			
		||||
@@ -18,10 +18,10 @@ import PopupMenu from './PopupMenu.vue';
 | 
			
		||||
import RemoveDialog from '../utils/removeDialog';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    inject: ['openmct'],
 | 
			
		||||
    components: {
 | 
			
		||||
        PopupMenu
 | 
			
		||||
    },
 | 
			
		||||
    inject: ['openmct'],
 | 
			
		||||
    props: {
 | 
			
		||||
        defaultPageId: {
 | 
			
		||||
            type: String,
 | 
			
		||||
 
 | 
			
		||||
@@ -21,10 +21,10 @@
 | 
			
		||||
import NotebookEntry from './NotebookEntry.vue';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    inject: ['openmct', 'snapshotContainer'],
 | 
			
		||||
    components: {
 | 
			
		||||
        NotebookEntry
 | 
			
		||||
    },
 | 
			
		||||
    inject: ['openmct', 'snapshotContainer'],
 | 
			
		||||
    props: {
 | 
			
		||||
        domainObject: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
 
 | 
			
		||||
@@ -22,10 +22,10 @@ import { getDefaultNotebook } from '../utils/notebook-storage';
 | 
			
		||||
import sectionComponent from './SectionComponent.vue';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    inject: ['openmct'],
 | 
			
		||||
    components: {
 | 
			
		||||
        sectionComponent
 | 
			
		||||
    },
 | 
			
		||||
    inject: ['openmct'],
 | 
			
		||||
    props: {
 | 
			
		||||
        defaultSectionId: {
 | 
			
		||||
            type: String,
 | 
			
		||||
 
 | 
			
		||||
@@ -21,10 +21,10 @@ import PopupMenu from './PopupMenu.vue';
 | 
			
		||||
import RemoveDialog from '../utils/removeDialog';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    inject: ['openmct'],
 | 
			
		||||
    components: {
 | 
			
		||||
        PopupMenu
 | 
			
		||||
    },
 | 
			
		||||
    inject: ['openmct'],
 | 
			
		||||
    props: {
 | 
			
		||||
        defaultSectionId: {
 | 
			
		||||
            type: String,
 | 
			
		||||
 
 | 
			
		||||
@@ -61,11 +61,11 @@ import PageCollection from './PageCollection.vue';
 | 
			
		||||
import uuid from 'uuid';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    inject: ['openmct'],
 | 
			
		||||
    components: {
 | 
			
		||||
        SectionCollection,
 | 
			
		||||
        PageCollection
 | 
			
		||||
    },
 | 
			
		||||
    inject: ['openmct'],
 | 
			
		||||
    props: {
 | 
			
		||||
        defaultPageId: {
 | 
			
		||||
            type: String,
 | 
			
		||||
 
 | 
			
		||||
@@ -88,13 +88,13 @@ export default function NotebookPlugin() {
 | 
			
		||||
 | 
			
		||||
        const snapshotContainer = new SnapshotContainer(openmct);
 | 
			
		||||
        const notebookSnapshotIndicator = new Vue ({
 | 
			
		||||
            components: {
 | 
			
		||||
                NotebookSnapshotIndicator
 | 
			
		||||
            },
 | 
			
		||||
            provide: {
 | 
			
		||||
                openmct,
 | 
			
		||||
                snapshotContainer
 | 
			
		||||
            },
 | 
			
		||||
            components: {
 | 
			
		||||
                NotebookSnapshotIndicator
 | 
			
		||||
            },
 | 
			
		||||
            template: '<NotebookSnapshotIndicator></NotebookSnapshotIndicator>'
 | 
			
		||||
        });
 | 
			
		||||
        const indicator = {
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
import { createOpenMct, createMouseEvent, resetApplicationState } from 'utils/testing';
 | 
			
		||||
import { createOpenMct, resetApplicationState } from 'utils/testing';
 | 
			
		||||
import NotebookPlugin from './plugin';
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
 | 
			
		||||
@@ -133,89 +133,4 @@ describe("Notebook plugin:", () => {
 | 
			
		||||
            expect(hasMajorElements).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("Notebook Snapshots view:", () => {
 | 
			
		||||
        let snapshotIndicator;
 | 
			
		||||
        let drawerElement;
 | 
			
		||||
 | 
			
		||||
        function clickSnapshotIndicator() {
 | 
			
		||||
            const indicator = element.querySelector('.icon-camera');
 | 
			
		||||
            const button = indicator.querySelector('button');
 | 
			
		||||
            const clickEvent = createMouseEvent('click');
 | 
			
		||||
 | 
			
		||||
            button.dispatchEvent(clickEvent);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        beforeAll(() => {
 | 
			
		||||
            snapshotIndicator = openmct.indicators.indicatorObjects
 | 
			
		||||
                .find(indicator => indicator.key === 'notebook-snapshot-indicator').element;
 | 
			
		||||
 | 
			
		||||
            element.append(snapshotIndicator);
 | 
			
		||||
 | 
			
		||||
            return Vue.nextTick();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        afterAll(() => {
 | 
			
		||||
            snapshotIndicator.remove();
 | 
			
		||||
            snapshotIndicator = undefined;
 | 
			
		||||
 | 
			
		||||
            if (drawerElement) {
 | 
			
		||||
                drawerElement.remove();
 | 
			
		||||
                drawerElement = undefined;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        beforeEach(() => {
 | 
			
		||||
            drawerElement = document.querySelector('.l-shell__drawer');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        afterEach(() => {
 | 
			
		||||
            if (drawerElement) {
 | 
			
		||||
                drawerElement.classList.remove('is-expanded');
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("has Snapshots indicator", () => {
 | 
			
		||||
            const hasSnapshotIndicator = snapshotIndicator !== null && snapshotIndicator !== undefined;
 | 
			
		||||
            expect(hasSnapshotIndicator).toBe(true);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("snapshots container has class isExpanded", () => {
 | 
			
		||||
            let classes = drawerElement.classList;
 | 
			
		||||
            const isExpandedBefore = classes.contains('is-expanded');
 | 
			
		||||
 | 
			
		||||
            clickSnapshotIndicator();
 | 
			
		||||
            classes = drawerElement.classList;
 | 
			
		||||
            const isExpandedAfterFirstClick = classes.contains('is-expanded');
 | 
			
		||||
 | 
			
		||||
            expect(isExpandedBefore).toBeFalse();
 | 
			
		||||
            expect(isExpandedAfterFirstClick).toBeTrue();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("snapshots container does not have class isExpanded", () => {
 | 
			
		||||
            let classes = drawerElement.classList;
 | 
			
		||||
            const isExpandedBefore = classes.contains('is-expanded');
 | 
			
		||||
 | 
			
		||||
            clickSnapshotIndicator();
 | 
			
		||||
            classes = drawerElement.classList;
 | 
			
		||||
            const isExpandedAfterFirstClick = classes.contains('is-expanded');
 | 
			
		||||
 | 
			
		||||
            clickSnapshotIndicator();
 | 
			
		||||
            classes = drawerElement.classList;
 | 
			
		||||
            const isExpandedAfterSecondClick = classes.contains('is-expanded');
 | 
			
		||||
 | 
			
		||||
            expect(isExpandedBefore).toBeFalse();
 | 
			
		||||
            expect(isExpandedAfterFirstClick).toBeTrue();
 | 
			
		||||
            expect(isExpandedAfterSecondClick).toBeFalse();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("show notebook snapshots container text", () => {
 | 
			
		||||
            clickSnapshotIndicator();
 | 
			
		||||
 | 
			
		||||
            const notebookSnapshots = drawerElement.querySelector('.l-browse-bar__object-name');
 | 
			
		||||
            const snapshotsText = notebookSnapshots.textContent.trim();
 | 
			
		||||
 | 
			
		||||
            expect(snapshotsText).toBe('Notebook Snapshots');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user