Compare commits
	
		
			16 Commits
		
	
	
		
			couch-sear
			...
			pause-play
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					e84aacd7c8 | ||
| 
						 | 
					161ebfa5f3 | ||
| 
						 | 
					12416b8079 | ||
| 
						 | 
					acd1ee81de | ||
| 
						 | 
					9920e67c83 | ||
| 
						 | 
					0e80a5b8a0 | ||
| 
						 | 
					05f9202fe4 | ||
| 
						 | 
					3fcdf3cbe1 | ||
| 
						 | 
					31edff509a | ||
| 
						 | 
					0da35a44b0 | ||
| 
						 | 
					f2f8132227 | ||
| 
						 | 
					2305cd2e49 | ||
| 
						 | 
					fa1537ab45 | ||
| 
						 | 
					b30b6bc94e | ||
| 
						 | 
					564f254652 | ||
| 
						 | 
					95dbe63b14 | 
							
								
								
									
										1
									
								
								API.md
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								API.md
									
									
									
									
									
								
							@@ -430,6 +430,7 @@ Known hints:
 | 
			
		||||
* `domain`: Values with a `domain` hint will be used for the x-axis of a plot, and tables will render columns for these values first.
 | 
			
		||||
* `range`: Values with a `range` hint will be used as the y-axis on a plot, and tables will render columns for these values after the `domain` values.
 | 
			
		||||
* `image`: Indicates that the value may be interpreted as the URL to an image file, in which case appropriate views will be made available.
 | 
			
		||||
* `imageDownloadName`: Indicates that the value may be interpreted as the name of the image file.
 | 
			
		||||
 | 
			
		||||
##### The Time Conductor and Telemetry 
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -50,11 +50,16 @@ define([
 | 
			
		||||
        const IMAGE_DELAY = 20000;
 | 
			
		||||
 | 
			
		||||
        function pointForTimestamp(timestamp, name) {
 | 
			
		||||
            const url = IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length];
 | 
			
		||||
            const urlItems = url.split('/');
 | 
			
		||||
            const imageDownloadName = `example.imagery.${urlItems[urlItems.length - 1]}`;
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                name: name,
 | 
			
		||||
                name,
 | 
			
		||||
                utc: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
 | 
			
		||||
                local: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
 | 
			
		||||
                url: IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length]
 | 
			
		||||
                url,
 | 
			
		||||
                imageDownloadName
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -139,6 +144,14 @@ define([
 | 
			
		||||
                                hints: {
 | 
			
		||||
                                    image: 1
 | 
			
		||||
                                }
 | 
			
		||||
                            },
 | 
			
		||||
                            {
 | 
			
		||||
                                name: 'Image Download Name',
 | 
			
		||||
                                key: 'imageDownloadName',
 | 
			
		||||
                                format: 'imageDownloadName',
 | 
			
		||||
                                hints: {
 | 
			
		||||
                                    imageDownloadName: 1
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        ]
 | 
			
		||||
                    };
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +0,0 @@
 | 
			
		||||
# Couch DB Persistence Plugin
 | 
			
		||||
An adapter for using CouchDB for persistence of user-created objects. The plugin installation function takes the URL 
 | 
			
		||||
for the CouchDB database as a parameter.
 | 
			
		||||
 | 
			
		||||
## Installation
 | 
			
		||||
```js
 | 
			
		||||
openmct.install(openmct.plugins.CouchDB('http://localhost:5984/openmct'))
 | 
			
		||||
```
 | 
			
		||||
@@ -1,78 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2021, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
define([
 | 
			
		||||
    "./src/CouchPersistenceProvider",
 | 
			
		||||
    "./src/CouchIndicator"
 | 
			
		||||
], function (
 | 
			
		||||
    CouchPersistenceProvider,
 | 
			
		||||
    CouchIndicator
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        name: "platform/persistence/couch",
 | 
			
		||||
        definition: {
 | 
			
		||||
            "name": "Couch Persistence",
 | 
			
		||||
            "description": "Adapter to read and write objects using a CouchDB instance.",
 | 
			
		||||
            "extensions": {
 | 
			
		||||
                "components": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "provides": "persistenceService",
 | 
			
		||||
                        "type": "provider",
 | 
			
		||||
                        "implementation": CouchPersistenceProvider,
 | 
			
		||||
                        "depends": [
 | 
			
		||||
                            "$http",
 | 
			
		||||
                            "$q",
 | 
			
		||||
                            "PERSISTENCE_SPACE",
 | 
			
		||||
                            "COUCHDB_PATH"
 | 
			
		||||
                        ]
 | 
			
		||||
                    }
 | 
			
		||||
                ],
 | 
			
		||||
                "constants": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "key": "PERSISTENCE_SPACE",
 | 
			
		||||
                        "value": "mct"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "key": "COUCHDB_PATH",
 | 
			
		||||
                        "value": "/couch/openmct"
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "key": "COUCHDB_INDICATOR_INTERVAL",
 | 
			
		||||
                        "value": 15000
 | 
			
		||||
                    }
 | 
			
		||||
                ],
 | 
			
		||||
                "indicators": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "implementation": CouchIndicator,
 | 
			
		||||
                        "depends": [
 | 
			
		||||
                            "$http",
 | 
			
		||||
                            "$interval",
 | 
			
		||||
                            "COUCHDB_PATH",
 | 
			
		||||
                            "COUCHDB_INDICATOR_INTERVAL"
 | 
			
		||||
                        ]
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
});
 | 
			
		||||
@@ -1,61 +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.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
define(
 | 
			
		||||
    [],
 | 
			
		||||
    function () {
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * A CouchDocument describes domain object model in a format
 | 
			
		||||
         * which is easily read-written to CouchDB. This includes
 | 
			
		||||
         * Couch's _id and _rev fields, as well as a separate
 | 
			
		||||
         * metadata field which contains a subset of information found
 | 
			
		||||
         * in the model itself (to support search optimization with
 | 
			
		||||
         * CouchDB views.)
 | 
			
		||||
         * @memberof platform/persistence/couch
 | 
			
		||||
         * @constructor
 | 
			
		||||
         * @param {string} id the id under which to store this mode
 | 
			
		||||
         * @param {object} model the model to store
 | 
			
		||||
         * @param {string} rev the revision to include (or undefined,
 | 
			
		||||
         *        if no revision should be noted for couch)
 | 
			
		||||
         * @param {boolean} whether or not to mark this document as
 | 
			
		||||
         *        deleted (see CouchDB docs for _deleted)
 | 
			
		||||
         */
 | 
			
		||||
        function CouchDocument(id, model, rev, markDeleted) {
 | 
			
		||||
            return {
 | 
			
		||||
                "_id": id,
 | 
			
		||||
                "_rev": rev,
 | 
			
		||||
                "_deleted": markDeleted,
 | 
			
		||||
                "metadata": {
 | 
			
		||||
                    "category": "domain object",
 | 
			
		||||
                    "type": model.type,
 | 
			
		||||
                    "owner": "admin",
 | 
			
		||||
                    "name": model.name,
 | 
			
		||||
                    "created": Date.now()
 | 
			
		||||
                },
 | 
			
		||||
                "model": model
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return CouchDocument;
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
@@ -1,119 +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.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
define(
 | 
			
		||||
    [],
 | 
			
		||||
    function () {
 | 
			
		||||
 | 
			
		||||
        // Set of connection states; changing among these states will be
 | 
			
		||||
        // reflected in the indicator's appearance.
 | 
			
		||||
        // CONNECTED: Everything nominal, expect to be able to read/write.
 | 
			
		||||
        // DISCONNECTED: HTTP failed; maybe misconfigured, disconnected.
 | 
			
		||||
        // SEMICONNECTED: Connected to the database, but it reported an error.
 | 
			
		||||
        // PENDING: Still trying to connect, and haven't failed yet.
 | 
			
		||||
        var CONNECTED = {
 | 
			
		||||
                text: "Connected",
 | 
			
		||||
                glyphClass: "ok",
 | 
			
		||||
                statusClass: "s-status-on",
 | 
			
		||||
                description: "Connected to the domain object database."
 | 
			
		||||
            },
 | 
			
		||||
            DISCONNECTED = {
 | 
			
		||||
                text: "Disconnected",
 | 
			
		||||
                glyphClass: "err",
 | 
			
		||||
                statusClass: "s-status-caution",
 | 
			
		||||
                description: "Unable to connect to the domain object database."
 | 
			
		||||
            },
 | 
			
		||||
            SEMICONNECTED = {
 | 
			
		||||
                text: "Unavailable",
 | 
			
		||||
                glyphClass: "caution",
 | 
			
		||||
                statusClass: "s-status-caution",
 | 
			
		||||
                description: "Database does not exist or is unavailable."
 | 
			
		||||
            },
 | 
			
		||||
            PENDING = {
 | 
			
		||||
                text: "Checking connection...",
 | 
			
		||||
                statusClass: "s-status-caution"
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Indicator for the current CouchDB connection. Polls CouchDB
 | 
			
		||||
         * at a regular interval (defined by bundle constants) to ensure
 | 
			
		||||
         * that the database is available.
 | 
			
		||||
         * @constructor
 | 
			
		||||
         * @memberof platform/persistence/couch
 | 
			
		||||
         * @implements {Indicator}
 | 
			
		||||
         * @param $http Angular's $http service
 | 
			
		||||
         * @param $interval Angular's $interval service
 | 
			
		||||
         * @param {string} path the URL to poll to check for couch availability
 | 
			
		||||
         * @param {number} interval the interval, in milliseconds, to poll at
 | 
			
		||||
         */
 | 
			
		||||
        function CouchIndicator($http, $interval, path, interval) {
 | 
			
		||||
            var self = this;
 | 
			
		||||
 | 
			
		||||
            // Track the current connection state
 | 
			
		||||
            this.state = PENDING;
 | 
			
		||||
 | 
			
		||||
            this.$http = $http;
 | 
			
		||||
            this.$interval = $interval;
 | 
			
		||||
            this.path = path;
 | 
			
		||||
            this.interval = interval;
 | 
			
		||||
 | 
			
		||||
            // Callback if the HTTP request to Couch fails
 | 
			
		||||
            function handleError() {
 | 
			
		||||
                self.state = DISCONNECTED;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Callback if the HTTP request succeeds. CouchDB may
 | 
			
		||||
            // report an error, so check for that.
 | 
			
		||||
            function handleResponse(response) {
 | 
			
		||||
                var data = response.data;
 | 
			
		||||
                self.state = data.error ? SEMICONNECTED : CONNECTED;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Try to connect to CouchDB, and update the indicator.
 | 
			
		||||
            function updateIndicator() {
 | 
			
		||||
                $http.get(path).then(handleResponse, handleError);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Update the indicator initially, and start polling.
 | 
			
		||||
            updateIndicator();
 | 
			
		||||
            $interval(updateIndicator, interval);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        CouchIndicator.prototype.getCssClass = function () {
 | 
			
		||||
            return "c-indicator--clickable icon-suitcase " + this.state.statusClass;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        CouchIndicator.prototype.getGlyphClass = function () {
 | 
			
		||||
            return this.state.glyphClass;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        CouchIndicator.prototype.getText = function () {
 | 
			
		||||
            return this.state.text;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        CouchIndicator.prototype.getDescription = function () {
 | 
			
		||||
            return this.state.description;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return CouchIndicator;
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
@@ -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.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This bundle implements a persistence service which uses CouchDB to
 | 
			
		||||
 * store documents.
 | 
			
		||||
 * @namespace platform/persistence/cache
 | 
			
		||||
 */
 | 
			
		||||
define(
 | 
			
		||||
    ["./CouchDocument"],
 | 
			
		||||
    function (CouchDocument) {
 | 
			
		||||
 | 
			
		||||
        // JSLint doesn't like dangling _'s, but CouchDB uses these, so
 | 
			
		||||
        // hide this behind variables.
 | 
			
		||||
        var REV = "_rev",
 | 
			
		||||
            ID = "_id";
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * The CouchPersistenceProvider reads and writes JSON documents
 | 
			
		||||
         * (more specifically, domain object models) to/from a CouchDB
 | 
			
		||||
         * instance.
 | 
			
		||||
         * @memberof platform/persistence/couch
 | 
			
		||||
         * @constructor
 | 
			
		||||
         * @implements {PersistenceService}
 | 
			
		||||
         * @param $http Angular's $http service
 | 
			
		||||
         * @param $interval Angular's $interval service
 | 
			
		||||
         * @param {string} space the name of the persistence space being served
 | 
			
		||||
         * @param {string} path the path to the CouchDB instance
 | 
			
		||||
         */
 | 
			
		||||
        function CouchPersistenceProvider($http, $q, space, path) {
 | 
			
		||||
            this.spaces = [space];
 | 
			
		||||
            this.revs = {};
 | 
			
		||||
            this.$q = $q;
 | 
			
		||||
            this.$http = $http;
 | 
			
		||||
            this.path = path;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Pull out a list of document IDs from CouchDB's
 | 
			
		||||
        // _all_docs response
 | 
			
		||||
        function getIdsFromAllDocs(allDocs) {
 | 
			
		||||
            return allDocs.rows.map(function (r) {
 | 
			
		||||
                return r.id;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check the response to a create/update/delete request;
 | 
			
		||||
        // track the rev if it's valid, otherwise return false to
 | 
			
		||||
        // indicate that the request failed.
 | 
			
		||||
        CouchPersistenceProvider.prototype.checkResponse = function (response) {
 | 
			
		||||
            if (response && response.ok) {
 | 
			
		||||
                this.revs[response.id] = response.rev;
 | 
			
		||||
 | 
			
		||||
                return response.ok;
 | 
			
		||||
            } else {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Get a domain object model out of CouchDB's response
 | 
			
		||||
        CouchPersistenceProvider.prototype.getModel = function (response) {
 | 
			
		||||
            if (response && response.model) {
 | 
			
		||||
                this.revs[response[ID]] = response[REV];
 | 
			
		||||
 | 
			
		||||
                return response.model;
 | 
			
		||||
            } else {
 | 
			
		||||
                return undefined;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Issue a request using $http; get back the plain JS object
 | 
			
		||||
        // from the expected JSON response
 | 
			
		||||
        CouchPersistenceProvider.prototype.request = function (subpath, method, value) {
 | 
			
		||||
            return this.$http({
 | 
			
		||||
                method: method,
 | 
			
		||||
                url: this.path + '/' + subpath,
 | 
			
		||||
                data: value
 | 
			
		||||
            }).then(function (response) {
 | 
			
		||||
                return response.data;
 | 
			
		||||
            }, function () {
 | 
			
		||||
                return undefined;
 | 
			
		||||
            });
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Shorthand methods for GET/PUT methods
 | 
			
		||||
        CouchPersistenceProvider.prototype.get = function (subpath) {
 | 
			
		||||
            return this.request(subpath, "GET");
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        CouchPersistenceProvider.prototype.put = function (subpath, value) {
 | 
			
		||||
            return this.request(subpath, "PUT", value);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        CouchPersistenceProvider.prototype.listSpaces = function () {
 | 
			
		||||
            return this.$q.when(this.spaces);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        CouchPersistenceProvider.prototype.listObjects = function () {
 | 
			
		||||
            return this.get("_all_docs").then(getIdsFromAllDocs.bind(this));
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        CouchPersistenceProvider.prototype.createObject = function (space, key, value) {
 | 
			
		||||
            return this.put(key, new CouchDocument(key, value))
 | 
			
		||||
                .then(this.checkResponse.bind(this));
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        CouchPersistenceProvider.prototype.readObject = function (space, key) {
 | 
			
		||||
            return this.get(key).then(this.getModel.bind(this));
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        CouchPersistenceProvider.prototype.updateObject = function (space, key, value) {
 | 
			
		||||
            var rev = this.revs[key];
 | 
			
		||||
 | 
			
		||||
            return this.put(key, new CouchDocument(key, value, rev))
 | 
			
		||||
                .then(this.checkResponse.bind(this));
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        CouchPersistenceProvider.prototype.deleteObject = function (space, key, value) {
 | 
			
		||||
            var rev = this.revs[key];
 | 
			
		||||
 | 
			
		||||
            return this.put(key, new CouchDocument(key, value, rev, true))
 | 
			
		||||
                .then(this.checkResponse.bind(this));
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return CouchPersistenceProvider;
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
@@ -1,63 +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.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * DomainObjectProviderSpec. Created by vwoeltje on 11/6/14.
 | 
			
		||||
 */
 | 
			
		||||
define(
 | 
			
		||||
    ["../src/CouchDocument"],
 | 
			
		||||
    function (CouchDocument) {
 | 
			
		||||
 | 
			
		||||
        // JSLint doesn't like dangling _'s, but CouchDB uses these, so
 | 
			
		||||
        // hide this behind variables.
 | 
			
		||||
        var REV = "_rev",
 | 
			
		||||
            ID = "_id",
 | 
			
		||||
            DELETED = "_deleted";
 | 
			
		||||
 | 
			
		||||
        describe("A couch document", function () {
 | 
			
		||||
            it("includes an id", function () {
 | 
			
		||||
                expect(new CouchDocument("testId", {})[ID])
 | 
			
		||||
                    .toEqual("testId");
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("includes a rev only when one is provided", function () {
 | 
			
		||||
                expect(new CouchDocument("testId", {})[REV])
 | 
			
		||||
                    .not.toBeDefined();
 | 
			
		||||
                expect(new CouchDocument("testId", {}, "testRev")[REV])
 | 
			
		||||
                    .toEqual("testRev");
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("includes the provided model", function () {
 | 
			
		||||
                var model = { someKey: "some value" };
 | 
			
		||||
                expect(new CouchDocument("testId", model).model)
 | 
			
		||||
                    .toEqual(model);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("marks documents as deleted only on request", function () {
 | 
			
		||||
                expect(new CouchDocument("testId", {}, "testRev")[DELETED])
 | 
			
		||||
                    .not.toBeDefined();
 | 
			
		||||
                expect(new CouchDocument("testId", {}, "testRev", true)[DELETED])
 | 
			
		||||
                    .toBe(true);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
@@ -1,129 +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.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
define(
 | 
			
		||||
    ["../src/CouchIndicator"],
 | 
			
		||||
    function (CouchIndicator) {
 | 
			
		||||
 | 
			
		||||
        xdescribe("The CouchDB status indicator", function () {
 | 
			
		||||
            var mockHttp,
 | 
			
		||||
                mockInterval,
 | 
			
		||||
                testPath,
 | 
			
		||||
                testInterval,
 | 
			
		||||
                mockPromise,
 | 
			
		||||
                indicator;
 | 
			
		||||
 | 
			
		||||
            beforeEach(function () {
 | 
			
		||||
                mockHttp = jasmine.createSpyObj("$http", ["get"]);
 | 
			
		||||
                mockInterval = jasmine.createSpy("$interval");
 | 
			
		||||
                mockPromise = jasmine.createSpyObj("promise", ["then"]);
 | 
			
		||||
                testPath = "/test/path";
 | 
			
		||||
                testInterval = 12321; // Some number
 | 
			
		||||
 | 
			
		||||
                mockHttp.get.and.returnValue(mockPromise);
 | 
			
		||||
 | 
			
		||||
                indicator = new CouchIndicator(
 | 
			
		||||
                    mockHttp,
 | 
			
		||||
                    mockInterval,
 | 
			
		||||
                    testPath,
 | 
			
		||||
                    testInterval
 | 
			
		||||
                );
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("polls for changes", function () {
 | 
			
		||||
                expect(mockInterval).toHaveBeenCalledWith(
 | 
			
		||||
                    jasmine.any(Function),
 | 
			
		||||
                    testInterval
 | 
			
		||||
                );
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("has a database icon", function () {
 | 
			
		||||
                expect(indicator.getCssClass()).toEqual("icon-database s-status-caution");
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("consults the database at the configured path", function () {
 | 
			
		||||
                expect(mockHttp.get).toHaveBeenCalledWith(testPath);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("changes when the database connection is nominal", function () {
 | 
			
		||||
                var initialText = indicator.getText(),
 | 
			
		||||
                    initialDescrption = indicator.getDescription(),
 | 
			
		||||
                    initialGlyphClass = indicator.getGlyphClass();
 | 
			
		||||
 | 
			
		||||
                // Nominal just means getting back an object, without
 | 
			
		||||
                // an error field.
 | 
			
		||||
                mockPromise.then.calls.mostRecent().args[0]({ data: {} });
 | 
			
		||||
 | 
			
		||||
                // Verify that these values changed;
 | 
			
		||||
                // don't test for specific text.
 | 
			
		||||
                expect(indicator.getText()).not.toEqual(initialText);
 | 
			
		||||
                expect(indicator.getGlyphClass()).not.toEqual(initialGlyphClass);
 | 
			
		||||
                expect(indicator.getDescription()).not.toEqual(initialDescrption);
 | 
			
		||||
 | 
			
		||||
                // Do check for specific class
 | 
			
		||||
                expect(indicator.getGlyphClass()).toEqual("ok");
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("changes when the server reports an error", function () {
 | 
			
		||||
                var initialText = indicator.getText(),
 | 
			
		||||
                    initialDescrption = indicator.getDescription(),
 | 
			
		||||
                    initialGlyphClass = indicator.getGlyphClass();
 | 
			
		||||
 | 
			
		||||
                // Nominal just means getting back an object, with
 | 
			
		||||
                // an error field.
 | 
			
		||||
                mockPromise.then.calls.mostRecent().args[0](
 | 
			
		||||
                    { data: { error: "Uh oh." } }
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                // Verify that these values changed;
 | 
			
		||||
                // don't test for specific text.
 | 
			
		||||
                expect(indicator.getText()).not.toEqual(initialText);
 | 
			
		||||
                expect(indicator.getGlyphClass()).not.toEqual(initialGlyphClass);
 | 
			
		||||
                expect(indicator.getDescription()).not.toEqual(initialDescrption);
 | 
			
		||||
 | 
			
		||||
                // Do check for specific class
 | 
			
		||||
                expect(indicator.getGlyphClass()).toEqual("caution");
 | 
			
		||||
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("changes when the server cannot be reached", function () {
 | 
			
		||||
                var initialText = indicator.getText(),
 | 
			
		||||
                    initialDescrption = indicator.getDescription(),
 | 
			
		||||
                    initialGlyphClass = indicator.getGlyphClass();
 | 
			
		||||
 | 
			
		||||
                // Nominal just means getting back an object, without
 | 
			
		||||
                // an error field.
 | 
			
		||||
                mockPromise.then.calls.mostRecent().args[1]({ data: {} });
 | 
			
		||||
 | 
			
		||||
                // Verify that these values changed;
 | 
			
		||||
                // don't test for specific text.
 | 
			
		||||
                expect(indicator.getText()).not.toEqual(initialText);
 | 
			
		||||
                expect(indicator.getGlyphClass()).not.toEqual(initialGlyphClass);
 | 
			
		||||
                expect(indicator.getDescription()).not.toEqual(initialDescrption);
 | 
			
		||||
 | 
			
		||||
                // Do check for specific class
 | 
			
		||||
                expect(indicator.getGlyphClass()).toEqual("err");
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
@@ -1,223 +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.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * DomainObjectProviderSpec. Created by vwoeltje on 11/6/14.
 | 
			
		||||
 */
 | 
			
		||||
define(
 | 
			
		||||
    ["../src/CouchPersistenceProvider"],
 | 
			
		||||
    function (CouchPersistenceProvider) {
 | 
			
		||||
 | 
			
		||||
        describe("The couch persistence provider", function () {
 | 
			
		||||
            var mockHttp,
 | 
			
		||||
                mockQ,
 | 
			
		||||
                testSpace = "testSpace",
 | 
			
		||||
                testPath = "/test/db",
 | 
			
		||||
                capture,
 | 
			
		||||
                provider;
 | 
			
		||||
 | 
			
		||||
            function mockPromise(value) {
 | 
			
		||||
                return {
 | 
			
		||||
                    then: function (callback) {
 | 
			
		||||
                        return mockPromise(callback(value));
 | 
			
		||||
                    }
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            beforeEach(function () {
 | 
			
		||||
                mockHttp = jasmine.createSpy("$http");
 | 
			
		||||
                mockQ = jasmine.createSpyObj("$q", ["when"]);
 | 
			
		||||
 | 
			
		||||
                mockQ.when.and.callFake(mockPromise);
 | 
			
		||||
 | 
			
		||||
                // Capture promise results
 | 
			
		||||
                capture = jasmine.createSpy("capture");
 | 
			
		||||
 | 
			
		||||
                provider = new CouchPersistenceProvider(
 | 
			
		||||
                    mockHttp,
 | 
			
		||||
                    mockQ,
 | 
			
		||||
                    testSpace,
 | 
			
		||||
                    testPath
 | 
			
		||||
                );
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("reports available spaces", function () {
 | 
			
		||||
                provider.listSpaces().then(capture);
 | 
			
		||||
                expect(capture).toHaveBeenCalledWith([testSpace]);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // General pattern of tests below is to simulate CouchDB's
 | 
			
		||||
            // response, verify that request looks like what CouchDB
 | 
			
		||||
            // would expect, and finally verify that CouchPersistenceProvider's
 | 
			
		||||
            // return values match what is expected.
 | 
			
		||||
            it("lists all available documents", function () {
 | 
			
		||||
                mockHttp.and.returnValue(mockPromise({
 | 
			
		||||
                    data: { rows: [{ id: "a" }, { id: "b" }, { id: "c" }] }
 | 
			
		||||
                }));
 | 
			
		||||
                provider.listObjects().then(capture);
 | 
			
		||||
                expect(mockHttp).toHaveBeenCalledWith({
 | 
			
		||||
                    url: "/test/db/_all_docs", // couch document listing
 | 
			
		||||
                    method: "GET",
 | 
			
		||||
                    data: undefined
 | 
			
		||||
                });
 | 
			
		||||
                expect(capture).toHaveBeenCalledWith(["a", "b", "c"]);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("allows object creation", function () {
 | 
			
		||||
                var model = { someKey: "some value" };
 | 
			
		||||
                mockHttp.and.returnValue(mockPromise({
 | 
			
		||||
                    data: {
 | 
			
		||||
                        "_id": "abc",
 | 
			
		||||
                        "_rev": "xyz",
 | 
			
		||||
                        "ok": true
 | 
			
		||||
                    }
 | 
			
		||||
                }));
 | 
			
		||||
                provider.createObject("testSpace", "abc", model).then(capture);
 | 
			
		||||
                expect(mockHttp).toHaveBeenCalledWith({
 | 
			
		||||
                    url: "/test/db/abc",
 | 
			
		||||
                    method: "PUT",
 | 
			
		||||
                    data: {
 | 
			
		||||
                        "_id": "abc",
 | 
			
		||||
                        "_rev": undefined,
 | 
			
		||||
                        "_deleted": undefined,
 | 
			
		||||
                        metadata: jasmine.any(Object),
 | 
			
		||||
                        model: model
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
                expect(capture).toHaveBeenCalledWith(true);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("allows object models to be read back", function () {
 | 
			
		||||
                var model = { someKey: "some value" };
 | 
			
		||||
                mockHttp.and.returnValue(mockPromise({
 | 
			
		||||
                    data: {
 | 
			
		||||
                        "_id": "abc",
 | 
			
		||||
                        "_rev": "xyz",
 | 
			
		||||
                        "model": model
 | 
			
		||||
                    }
 | 
			
		||||
                }));
 | 
			
		||||
                provider.readObject("testSpace", "abc").then(capture);
 | 
			
		||||
                expect(mockHttp).toHaveBeenCalledWith({
 | 
			
		||||
                    url: "/test/db/abc",
 | 
			
		||||
                    method: "GET",
 | 
			
		||||
                    data: undefined
 | 
			
		||||
                });
 | 
			
		||||
                expect(capture).toHaveBeenCalledWith(model);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("allows object update", function () {
 | 
			
		||||
                var model = { someKey: "some value" };
 | 
			
		||||
 | 
			
		||||
                // First do a read to populate rev tags...
 | 
			
		||||
                mockHttp.and.returnValue(mockPromise({
 | 
			
		||||
                    data: {
 | 
			
		||||
                        "_id": "abc",
 | 
			
		||||
                        "_rev": "xyz",
 | 
			
		||||
                        "model": {}
 | 
			
		||||
                    }
 | 
			
		||||
                }));
 | 
			
		||||
                provider.readObject("testSpace", "abc");
 | 
			
		||||
 | 
			
		||||
                // Now perform an update
 | 
			
		||||
                mockHttp.and.returnValue(mockPromise({
 | 
			
		||||
                    data: {
 | 
			
		||||
                        "_id": "abc",
 | 
			
		||||
                        "_rev": "uvw",
 | 
			
		||||
                        "ok": true
 | 
			
		||||
                    }
 | 
			
		||||
                }));
 | 
			
		||||
                provider.updateObject("testSpace", "abc", model).then(capture);
 | 
			
		||||
                expect(mockHttp).toHaveBeenCalledWith({
 | 
			
		||||
                    url: "/test/db/abc",
 | 
			
		||||
                    method: "PUT",
 | 
			
		||||
                    data: {
 | 
			
		||||
                        "_id": "abc",
 | 
			
		||||
                        "_rev": "xyz",
 | 
			
		||||
                        "_deleted": undefined,
 | 
			
		||||
                        metadata: jasmine.any(Object),
 | 
			
		||||
                        model: model
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
                expect(capture).toHaveBeenCalledWith(true);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("allows object deletion", function () {
 | 
			
		||||
                // First do a read to populate rev tags...
 | 
			
		||||
                mockHttp.and.returnValue(mockPromise({
 | 
			
		||||
                    data: {
 | 
			
		||||
                        "_id": "abc",
 | 
			
		||||
                        "_rev": "xyz",
 | 
			
		||||
                        "model": {}
 | 
			
		||||
                    }
 | 
			
		||||
                }));
 | 
			
		||||
                provider.readObject("testSpace", "abc");
 | 
			
		||||
 | 
			
		||||
                // Now perform an update
 | 
			
		||||
                mockHttp.and.returnValue(mockPromise({
 | 
			
		||||
                    data: {
 | 
			
		||||
                        "_id": "abc",
 | 
			
		||||
                        "_rev": "uvw",
 | 
			
		||||
                        "ok": true
 | 
			
		||||
                    }
 | 
			
		||||
                }));
 | 
			
		||||
                provider.deleteObject("testSpace", "abc", {}).then(capture);
 | 
			
		||||
                expect(mockHttp).toHaveBeenCalledWith({
 | 
			
		||||
                    url: "/test/db/abc",
 | 
			
		||||
                    method: "PUT",
 | 
			
		||||
                    data: {
 | 
			
		||||
                        "_id": "abc",
 | 
			
		||||
                        "_rev": "xyz",
 | 
			
		||||
                        "_deleted": true,
 | 
			
		||||
                        metadata: jasmine.any(Object),
 | 
			
		||||
                        model: {}
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
                expect(capture).toHaveBeenCalledWith(true);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("reports failure to create objects", function () {
 | 
			
		||||
                var model = { someKey: "some value" };
 | 
			
		||||
                mockHttp.and.returnValue(mockPromise({
 | 
			
		||||
                    data: {
 | 
			
		||||
                        "_id": "abc",
 | 
			
		||||
                        "_rev": "xyz",
 | 
			
		||||
                        "ok": false
 | 
			
		||||
                    }
 | 
			
		||||
                }));
 | 
			
		||||
                provider.createObject("testSpace", "abc", model).then(capture);
 | 
			
		||||
                expect(capture).toHaveBeenCalledWith(false);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("returns undefined when objects are not found", function () {
 | 
			
		||||
                // Act like a 404
 | 
			
		||||
                mockHttp.and.returnValue({
 | 
			
		||||
                    then: function (success, fail) {
 | 
			
		||||
                        return mockPromise(fail());
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
                provider.readObject("testSpace", "abc").then(capture);
 | 
			
		||||
                expect(capture).toHaveBeenCalledWith(undefined);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
@@ -52,7 +52,7 @@ define([
 | 
			
		||||
 | 
			
		||||
            oldStyleObject.getCapability('mutation').mutate(function () {
 | 
			
		||||
                return utils.toOldFormat(newStyleObject);
 | 
			
		||||
            });
 | 
			
		||||
            }, newStyleObject.modified);
 | 
			
		||||
 | 
			
		||||
            removeGeneralTopicListener = this.generalTopic.listen(handleLegacyMutation);
 | 
			
		||||
        }.bind(this);
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,13 @@ define([
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                //TODO: Remove this when plots Angular implementation is deprecated
 | 
			
		||||
                let parent = selection[0].length > 1 && selection[0][1].context.item;
 | 
			
		||||
                if (parent && parent.type === 'time-strip') {
 | 
			
		||||
                    return (selectionContext.item.type === typeDefinition.key)
 | 
			
		||||
                            && (typeDefinition.key !== 'telemetry.plot.overlay');
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return selectionContext.item.type === typeDefinition.key;
 | 
			
		||||
            },
 | 
			
		||||
            view: function (selection) {
 | 
			
		||||
 
 | 
			
		||||
@@ -76,7 +76,10 @@ class MutableDomainObject {
 | 
			
		||||
    }
 | 
			
		||||
    $set(path, value) {
 | 
			
		||||
        _.set(this, path, value);
 | 
			
		||||
        _.set(this, 'modified', Date.now());
 | 
			
		||||
 | 
			
		||||
        if (path !== 'persisted' && path !== 'modified') {
 | 
			
		||||
            _.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);
 | 
			
		||||
 
 | 
			
		||||
@@ -161,6 +161,7 @@ ObjectAPI.prototype.addProvider = function (namespace, provider) {
 | 
			
		||||
 | 
			
		||||
ObjectAPI.prototype.get = function (identifier, abortSignal) {
 | 
			
		||||
    let keystring = this.makeKeyString(identifier);
 | 
			
		||||
 | 
			
		||||
    if (this.cache[keystring] !== undefined) {
 | 
			
		||||
        return this.cache[keystring];
 | 
			
		||||
    }
 | 
			
		||||
@@ -176,15 +177,16 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) {
 | 
			
		||||
        throw new Error('Provider does not support get!');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let objectPromise = provider.get(identifier, abortSignal);
 | 
			
		||||
    this.cache[keystring] = objectPromise;
 | 
			
		||||
 | 
			
		||||
    return objectPromise.then(result => {
 | 
			
		||||
    let objectPromise = provider.get(identifier, abortSignal).then(result => {
 | 
			
		||||
        delete this.cache[keystring];
 | 
			
		||||
        result = this.applyGetInterceptors(identifier, result);
 | 
			
		||||
 | 
			
		||||
        return result;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.cache[keystring] = objectPromise;
 | 
			
		||||
 | 
			
		||||
    return objectPromise;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -520,8 +522,10 @@ ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) {
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
function hasAlreadyBeenPersisted(domainObject) {
 | 
			
		||||
    return domainObject.persisted !== undefined
 | 
			
		||||
        && domainObject.persisted === domainObject.modified;
 | 
			
		||||
    const result = domainObject.persisted !== undefined
 | 
			
		||||
        && domainObject.persisted >= domainObject.modified;
 | 
			
		||||
 | 
			
		||||
    return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ObjectAPI;
 | 
			
		||||
 
 | 
			
		||||
@@ -90,7 +90,6 @@ define([
 | 
			
		||||
    '../platform/framework/src/load/Bundle',
 | 
			
		||||
    '../platform/identity/bundle',
 | 
			
		||||
    '../platform/persistence/aggregator/bundle',
 | 
			
		||||
    '../platform/persistence/couch/bundle',
 | 
			
		||||
    '../platform/persistence/elastic/bundle',
 | 
			
		||||
    '../platform/persistence/local/bundle',
 | 
			
		||||
    '../platform/persistence/queue/bundle',
 | 
			
		||||
 
 | 
			
		||||
@@ -344,6 +344,11 @@ export default {
 | 
			
		||||
                const layoutItem = selectionItem[0].context.layoutItem;
 | 
			
		||||
                const isChildItem = selectionItem.length > 1;
 | 
			
		||||
 | 
			
		||||
                if (!item && !layoutItem) {
 | 
			
		||||
                    // cases where selection is used for table cells
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!isChildItem) {
 | 
			
		||||
                    domainObject = item;
 | 
			
		||||
                    itemStyle = getApplicableStylesForItem(item);
 | 
			
		||||
 
 | 
			
		||||
@@ -104,7 +104,7 @@ export function getConsolidatedStyleValues(multipleItemStyles) {
 | 
			
		||||
    const properties = Object.keys(styleProps);
 | 
			
		||||
    properties.forEach((property) => {
 | 
			
		||||
        const values = aggregatedStyleValues[property];
 | 
			
		||||
        if (values.length) {
 | 
			
		||||
        if (values && values.length) {
 | 
			
		||||
            if (values.every(value => value === values[0])) {
 | 
			
		||||
                styleValues[property] = values[0];
 | 
			
		||||
            } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -147,7 +147,7 @@ export default {
 | 
			
		||||
            this.mutablePromise.then(() => {
 | 
			
		||||
                this.openmct.objects.destroyMutable(this.domainObject);
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
        } else if (this.domainObject.isMutable) {
 | 
			
		||||
            this.openmct.objects.destroyMutable(this.domainObject);
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -240,7 +240,7 @@ export default {
 | 
			
		||||
            this.mutablePromise.then(() => {
 | 
			
		||||
                this.openmct.objects.destroyMutable(this.domainObject);
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
        } else if (this.domainObject.isMutable) {
 | 
			
		||||
            this.openmct.objects.destroyMutable(this.domainObject);
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div
 | 
			
		||||
    class="c-compass"
 | 
			
		||||
    :style="compassDimensionsStyle"
 | 
			
		||||
    :style="`width: ${ sizedImageDimensions.width }px; height: ${ sizedImageDimensions.height }px`"
 | 
			
		||||
>
 | 
			
		||||
    <CompassHUD
 | 
			
		||||
        v-if="hasCameraFieldOfView"
 | 
			
		||||
@@ -34,6 +34,7 @@
 | 
			
		||||
    <CompassRose
 | 
			
		||||
        v-if="hasCameraFieldOfView"
 | 
			
		||||
        :heading="heading"
 | 
			
		||||
        :sized-image-width="sizedImageDimensions.width"
 | 
			
		||||
        :sun-heading="sunHeading"
 | 
			
		||||
        :camera-angle-of-view="cameraAngleOfView"
 | 
			
		||||
        :camera-pan="cameraPan"
 | 
			
		||||
@@ -77,6 +78,20 @@ export default {
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
        sizedImageDimensions() {
 | 
			
		||||
            let sizedImageDimensions = {};
 | 
			
		||||
            if ((this.containerWidth / this.containerHeight) > this.naturalAspectRatio) {
 | 
			
		||||
                // container is wider than image
 | 
			
		||||
                sizedImageDimensions.width = this.containerHeight * this.naturalAspectRatio;
 | 
			
		||||
                sizedImageDimensions.height = this.containerHeight;
 | 
			
		||||
            } else {
 | 
			
		||||
                // container is taller than image
 | 
			
		||||
                sizedImageDimensions.width = this.containerWidth;
 | 
			
		||||
                sizedImageDimensions.height = this.containerWidth * this.naturalAspectRatio;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return sizedImageDimensions;
 | 
			
		||||
        },
 | 
			
		||||
        hasCameraFieldOfView() {
 | 
			
		||||
            return this.cameraPan !== undefined && this.cameraAngleOfView > 0;
 | 
			
		||||
        },
 | 
			
		||||
@@ -94,25 +109,6 @@ export default {
 | 
			
		||||
        },
 | 
			
		||||
        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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -22,129 +22,134 @@
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div
 | 
			
		||||
    class="c-direction-rose"
 | 
			
		||||
    @click="toggleLockCompass"
 | 
			
		||||
    class="w-direction-rose"
 | 
			
		||||
    :class="compassRoseSizingClasses"
 | 
			
		||||
>
 | 
			
		||||
    <div
 | 
			
		||||
        class="c-nsew"
 | 
			
		||||
        :style="compassRoseStyle"
 | 
			
		||||
        class="c-direction-rose"
 | 
			
		||||
        @click="toggleLockCompass"
 | 
			
		||||
    >
 | 
			
		||||
        <svg
 | 
			
		||||
            class="c-nsew__minor-ticks"
 | 
			
		||||
            viewBox="0 0 100 100"
 | 
			
		||||
        <div
 | 
			
		||||
            class="c-nsew"
 | 
			
		||||
            :style="compassRoseStyle"
 | 
			
		||||
        >
 | 
			
		||||
            <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
 | 
			
		||||
                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>
 | 
			
		||||
 | 
			
		||||
        <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"
 | 
			
		||||
            />
 | 
			
		||||
            <svg
 | 
			
		||||
                class="c-nsew__ticks"
 | 
			
		||||
                viewBox="0 0 100 100"
 | 
			
		||||
            >
 | 
			
		||||
                <polygon
 | 
			
		||||
                    class="c-nsew__tick c-tick-n"
 | 
			
		||||
                    points="50,0 60,10 40,10"
 | 
			
		||||
                />
 | 
			
		||||
                <rect
 | 
			
		||||
                    class="c-nsew__tick c-tick-e"
 | 
			
		||||
                    x="95"
 | 
			
		||||
                    y="49"
 | 
			
		||||
                    width="5"
 | 
			
		||||
                    height="2"
 | 
			
		||||
                />
 | 
			
		||||
                <rect
 | 
			
		||||
                    class="c-nsew__tick c-tick-w"
 | 
			
		||||
                    x="0"
 | 
			
		||||
                    y="49"
 | 
			
		||||
                    width="5"
 | 
			
		||||
                    height="2"
 | 
			
		||||
                />
 | 
			
		||||
                <rect
 | 
			
		||||
                    class="c-nsew__tick c-tick-s"
 | 
			
		||||
                    x="49"
 | 
			
		||||
                    y="95"
 | 
			
		||||
                    width="2"
 | 
			
		||||
                    height="5"
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
            <text
 | 
			
		||||
                class="c-nsew__label c-label-n"
 | 
			
		||||
                text-anchor="middle"
 | 
			
		||||
                :transform="northTextTransform"
 | 
			
		||||
            >N</text>
 | 
			
		||||
            <text
 | 
			
		||||
                class="c-nsew__label c-label-e"
 | 
			
		||||
                text-anchor="middle"
 | 
			
		||||
                :transform="eastTextTransform"
 | 
			
		||||
            >E</text>
 | 
			
		||||
            <text
 | 
			
		||||
                class="c-nsew__label c-label-w"
 | 
			
		||||
                text-anchor="middle"
 | 
			
		||||
                :transform="southTextTransform"
 | 
			
		||||
            >W</text>
 | 
			
		||||
            <text
 | 
			
		||||
                class="c-nsew__label c-label-s"
 | 
			
		||||
                text-anchor="middle"
 | 
			
		||||
                :transform="westTextTransform"
 | 
			
		||||
            >S</text>
 | 
			
		||||
        </svg>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div
 | 
			
		||||
        v-if="hasHeading"
 | 
			
		||||
        class="c-spacecraft-body"
 | 
			
		||||
        :style="headingStyle"
 | 
			
		||||
    >
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div
 | 
			
		||||
        v-if="hasSunHeading"
 | 
			
		||||
        class="c-sun"
 | 
			
		||||
        :style="sunHeadingStyle"
 | 
			
		||||
    ></div>
 | 
			
		||||
 | 
			
		||||
    <div
 | 
			
		||||
        class="c-cam-field"
 | 
			
		||||
        :style="cameraPanStyle"
 | 
			
		||||
    >
 | 
			
		||||
        <div class="cam-field-half cam-field-half-l">
 | 
			
		||||
            <div
 | 
			
		||||
                class="cam-field-area"
 | 
			
		||||
                :style="cameraFOVStyleLeftHalf"
 | 
			
		||||
            ></div>
 | 
			
		||||
                <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="cam-field-half cam-field-half-r">
 | 
			
		||||
            <div
 | 
			
		||||
                class="cam-field-area"
 | 
			
		||||
                :style="cameraFOVStyleRightHalf"
 | 
			
		||||
            ></div>
 | 
			
		||||
 | 
			
		||||
        <div
 | 
			
		||||
            v-if="hasHeading"
 | 
			
		||||
            class="c-spacecraft-body"
 | 
			
		||||
            :style="headingStyle"
 | 
			
		||||
        >
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div
 | 
			
		||||
            v-if="hasSunHeading"
 | 
			
		||||
            class="c-sun"
 | 
			
		||||
            :style="sunHeadingStyle"
 | 
			
		||||
        ></div>
 | 
			
		||||
 | 
			
		||||
        <div
 | 
			
		||||
            class="c-cam-field"
 | 
			
		||||
            :style="cameraPanStyle"
 | 
			
		||||
        >
 | 
			
		||||
            <div class="cam-field-half cam-field-half-l">
 | 
			
		||||
                <div
 | 
			
		||||
                    class="cam-field-area"
 | 
			
		||||
                    :style="cameraFOVStyleLeftHalf"
 | 
			
		||||
                ></div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="cam-field-half cam-field-half-r">
 | 
			
		||||
                <div
 | 
			
		||||
                    class="cam-field-area"
 | 
			
		||||
                    :style="cameraFOVStyleRightHalf"
 | 
			
		||||
                ></div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -155,6 +160,10 @@ import { rotate } from './utils';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    props: {
 | 
			
		||||
        sizedImageWidth: {
 | 
			
		||||
            type: Number,
 | 
			
		||||
            required: true
 | 
			
		||||
        },
 | 
			
		||||
        heading: {
 | 
			
		||||
            type: Number,
 | 
			
		||||
            required: true
 | 
			
		||||
@@ -177,12 +186,24 @@ export default {
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
        north() {
 | 
			
		||||
            return this.lockCompass ? rotate(-this.cameraPan) : 0;
 | 
			
		||||
        compassRoseSizingClasses() {
 | 
			
		||||
            let compassRoseSizingClasses = '';
 | 
			
		||||
            if (this.sizedImageWidth < 300) {
 | 
			
		||||
                compassRoseSizingClasses = '--rose-small --rose-min';
 | 
			
		||||
            } else if (this.sizedImageWidth < 500) {
 | 
			
		||||
                compassRoseSizingClasses = '--rose-small';
 | 
			
		||||
            } else if (this.sizedImageWidth > 1000) {
 | 
			
		||||
                compassRoseSizingClasses = '--rose-max';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return compassRoseSizingClasses;
 | 
			
		||||
        },
 | 
			
		||||
        compassRoseStyle() {
 | 
			
		||||
            return { transform: `rotate(${ this.north }deg)` };
 | 
			
		||||
        },
 | 
			
		||||
        north() {
 | 
			
		||||
            return this.lockCompass ? rotate(-this.cameraPan) : 0;
 | 
			
		||||
        },
 | 
			
		||||
        northTextTransform() {
 | 
			
		||||
            return this.cardinalPointsTextTransform.north;
 | 
			
		||||
        },
 | 
			
		||||
@@ -204,10 +225,10 @@ export default {
 | 
			
		||||
            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 }`
 | 
			
		||||
                north: `translate(50,23) ${ rotation }`,
 | 
			
		||||
                east: `translate(82,50) ${ rotation }`,
 | 
			
		||||
                south: `translate(18,50) ${ rotation }`,
 | 
			
		||||
                west: `translate(50,82) ${ rotation }`
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        hasHeading() {
 | 
			
		||||
 
 | 
			
		||||
@@ -20,195 +20,252 @@ $elemBg: rgba(black, 0.7);
 | 
			
		||||
 | 
			
		||||
/***************************** 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 {
 | 
			
		||||
    // 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;
 | 
			
		||||
 | 
			
		||||
  &__display {
 | 
			
		||||
      height: 30px;
 | 
			
		||||
      pointer-events: all;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 0;
 | 
			
		||||
      right: 0;
 | 
			
		||||
      left: 0;
 | 
			
		||||
  }
 | 
			
		||||
    svg, div {
 | 
			
		||||
        position: absolute;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  &__range {
 | 
			
		||||
    border: 1px solid $interfaceKeyColor;
 | 
			
		||||
    border-top-color: transparent;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 50%; right: $padLR; bottom: $padTB; left: $padLR;
 | 
			
		||||
  }
 | 
			
		||||
    &__display {
 | 
			
		||||
        height: 30px;
 | 
			
		||||
        pointer-events: all;
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        top: 0;
 | 
			
		||||
        right: 0;
 | 
			
		||||
        left: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  [class*="__dir"] {
 | 
			
		||||
    // NSEW
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    text-shadow: 0 1px 2px black;
 | 
			
		||||
    top: 50%;
 | 
			
		||||
    transform: translate(-50%,-50%);
 | 
			
		||||
    z-index: 2;
 | 
			
		||||
  }
 | 
			
		||||
    &__range {
 | 
			
		||||
        border: 1px solid $interfaceKeyColor;
 | 
			
		||||
        border-top-color: transparent;
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        top: 50%;
 | 
			
		||||
        right: $padLR;
 | 
			
		||||
        bottom: $padTB;
 | 
			
		||||
        left: $padLR;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  [class*="__dir--sub"] {
 | 
			
		||||
    font-weight: normal;
 | 
			
		||||
    opacity: 0.5;
 | 
			
		||||
  }
 | 
			
		||||
    [class*="__dir"] {
 | 
			
		||||
        // NSEW
 | 
			
		||||
        display: inline-block;
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
        text-shadow: 0 1px 2px black;
 | 
			
		||||
        top: 50%;
 | 
			
		||||
        transform: translate(-50%, -50%);
 | 
			
		||||
        z-index: 2;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  &__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;
 | 
			
		||||
  }
 | 
			
		||||
    [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;
 | 
			
		||||
    $color: $interfaceKeyColor;
 | 
			
		||||
    $inset: 5%;
 | 
			
		||||
    $tickHeightPerc: 15%;
 | 
			
		||||
    text-shadow: black 0 0 10px;
 | 
			
		||||
    top: $inset;
 | 
			
		||||
    right: $inset;
 | 
			
		||||
    bottom: $inset;
 | 
			
		||||
    left: $inset;
 | 
			
		||||
    z-index: 3;
 | 
			
		||||
 | 
			
		||||
  &__tick,
 | 
			
		||||
  &__label {
 | 
			
		||||
    fill: $color;
 | 
			
		||||
  }
 | 
			
		||||
    &__tick,
 | 
			
		||||
    &__label {
 | 
			
		||||
        fill: $color;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  &__minor-ticks {
 | 
			
		||||
    opacity: 0.5;
 | 
			
		||||
    transform-origin: center;
 | 
			
		||||
    transform: rotate(45deg);
 | 
			
		||||
  }
 | 
			
		||||
    &__minor-ticks {
 | 
			
		||||
        opacity: 0.5;
 | 
			
		||||
        transform-origin: center;
 | 
			
		||||
        transform: rotate(45deg);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  &__label {
 | 
			
		||||
    dominant-baseline: central;
 | 
			
		||||
    font-size: 0.8em;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
  }
 | 
			
		||||
    &__label {
 | 
			
		||||
        dominant-baseline: central;
 | 
			
		||||
        font-size: 1.25em;
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  .c-label-n {
 | 
			
		||||
    font-size: 1.1em;
 | 
			
		||||
  }
 | 
			
		||||
    .c-label-n {
 | 
			
		||||
        font-size: 2em;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/***************************** 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 {
 | 
			
		||||
    $color: white;
 | 
			
		||||
    opacity: 0.3;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    z-index: 2;
 | 
			
		||||
 | 
			
		||||
    .cam-field-area {
 | 
			
		||||
      background: $color;
 | 
			
		||||
      top: -30%;
 | 
			
		||||
      right: 0;
 | 
			
		||||
      bottom: -30%;
 | 
			
		||||
      left: 0;
 | 
			
		||||
    }
 | 
			
		||||
    .cam-field-half {
 | 
			
		||||
        top: 0;
 | 
			
		||||
        right: 0;
 | 
			
		||||
        bottom: 0;
 | 
			
		||||
        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;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
        .cam-field-area {
 | 
			
		||||
            background: $color;
 | 
			
		||||
            top: -30%;
 | 
			
		||||
            right: 0;
 | 
			
		||||
            bottom: -30%;
 | 
			
		||||
            left: 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    &-r {
 | 
			
		||||
      clip-path: polygon(49.5% 0, 100% 0, 100% 100%, 49.5% 100%);
 | 
			
		||||
      .cam-field-area {
 | 
			
		||||
        transform-origin: right center;
 | 
			
		||||
      }
 | 
			
		||||
        // 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%;
 | 
			
		||||
    $color: $interfaceKeyColor;
 | 
			
		||||
    $s: 30%;
 | 
			
		||||
    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);
 | 
			
		||||
  }
 | 
			
		||||
    border-radius: 3px;
 | 
			
		||||
    height: $s;
 | 
			
		||||
    width: $s;
 | 
			
		||||
    left: 50%;
 | 
			
		||||
    top: 50%;
 | 
			
		||||
    opacity: 0.4;
 | 
			
		||||
    transform-origin: center top;
 | 
			
		||||
    transform: translateX(-50%); // center by default, overridden by CompassRose.vue / headingStyle()
 | 
			
		||||
 | 
			
		||||
    &: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 {
 | 
			
		||||
.w-direction-rose {
 | 
			
		||||
    $s: 10%;
 | 
			
		||||
    $m: 2%;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
  }
 | 
			
		||||
    bottom: $m;
 | 
			
		||||
    left: $m;
 | 
			
		||||
    width: $s;
 | 
			
		||||
    padding-top: $s;
 | 
			
		||||
 | 
			
		||||
  // Sun
 | 
			
		||||
  .c-sun {
 | 
			
		||||
    &.--rose-min {
 | 
			
		||||
        $s: 30px;
 | 
			
		||||
        width: $s;
 | 
			
		||||
        padding-top: $s;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.--rose-small {
 | 
			
		||||
        .c-nsew__minor-ticks,
 | 
			
		||||
        .c-tick-w,
 | 
			
		||||
        .c-tick-s,
 | 
			
		||||
        .c-tick-e,
 | 
			
		||||
        .c-label-w,
 | 
			
		||||
        .c-label-s,
 | 
			
		||||
        .c-label-e {
 | 
			
		||||
            display: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .c-label-n {
 | 
			
		||||
            font-size: 2.5em;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.--rose-max {
 | 
			
		||||
        $s: 100px;
 | 
			
		||||
        width: $s;
 | 
			
		||||
        padding-top: $s;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.c-direction-rose {
 | 
			
		||||
    $c2: rgba(white, 0.1);
 | 
			
		||||
    background: $elemBg;
 | 
			
		||||
    background-image: radial-gradient(circle closest-side, transparent, transparent 80%, $c2);
 | 
			
		||||
    transform-origin: 0 0;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    clip-path: circle(50% at 50% 50%);
 | 
			
		||||
    border-radius: 100%;
 | 
			
		||||
 | 
			
		||||
    &: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%);
 | 
			
		||||
    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%);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -135,9 +135,14 @@
 | 
			
		||||
             :class="{ selected: focusedImageIndex === index && isPaused }"
 | 
			
		||||
             @click="setFocusedImage(index, thumbnailClick)"
 | 
			
		||||
        >
 | 
			
		||||
            <img class="c-thumb__image"
 | 
			
		||||
                 :src="image.url"
 | 
			
		||||
            <a href=""
 | 
			
		||||
               :download="image.imageDownloadName"
 | 
			
		||||
               @click.prevent
 | 
			
		||||
            >
 | 
			
		||||
                <img class="c-thumb__image"
 | 
			
		||||
                     :src="image.url"
 | 
			
		||||
                >
 | 
			
		||||
            </a>
 | 
			
		||||
            <div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -218,6 +223,9 @@ export default {
 | 
			
		||||
        canTrackDuration() {
 | 
			
		||||
            return this.openmct.time.clock() && this.timeSystem.isUTCBased;
 | 
			
		||||
        },
 | 
			
		||||
        focusedImageDownloadName() {
 | 
			
		||||
            return this.getImageDownloadName(this.focusedImage);
 | 
			
		||||
        },
 | 
			
		||||
        isNextDisabled() {
 | 
			
		||||
            let disabled = false;
 | 
			
		||||
 | 
			
		||||
@@ -345,6 +353,7 @@ export default {
 | 
			
		||||
        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);
 | 
			
		||||
        this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]};
 | 
			
		||||
 | 
			
		||||
        // related telemetry keys
 | 
			
		||||
        this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ'];
 | 
			
		||||
@@ -532,6 +541,15 @@ export default {
 | 
			
		||||
            // Replace ISO "T" with a space to allow wrapping
 | 
			
		||||
            return dateTimeStr.replace("T", " ");
 | 
			
		||||
        },
 | 
			
		||||
        getImageDownloadName(datum) {
 | 
			
		||||
            let imageDownloadName = '';
 | 
			
		||||
            if (datum) {
 | 
			
		||||
                const key = this.imageDownloadNameHints.key;
 | 
			
		||||
                imageDownloadName = datum[key];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return imageDownloadName;
 | 
			
		||||
        },
 | 
			
		||||
        parseTime(datum) {
 | 
			
		||||
            if (!datum) {
 | 
			
		||||
                return;
 | 
			
		||||
@@ -655,6 +673,7 @@ export default {
 | 
			
		||||
            image.formattedTime = this.formatTime(datum);
 | 
			
		||||
            image.url = this.formatImageUrl(datum);
 | 
			
		||||
            image.time = datum[this.timeKey];
 | 
			
		||||
            image.imageDownloadName = this.getImageDownloadName(datum);
 | 
			
		||||
 | 
			
		||||
            this.imageHistory.push(image);
 | 
			
		||||
 | 
			
		||||
@@ -777,6 +796,9 @@ export default {
 | 
			
		||||
            this.focusedImageNaturalAspectRatio = undefined;
 | 
			
		||||
 | 
			
		||||
            const img = this.$refs.focusedImage;
 | 
			
		||||
            if (!img) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // TODO - should probably cache this
 | 
			
		||||
            img.addEventListener('load', () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,7 @@ import CouchObjectQueue from "./CouchObjectQueue";
 | 
			
		||||
const REV = "_rev";
 | 
			
		||||
const ID = "_id";
 | 
			
		||||
const HEARTBEAT = 50000;
 | 
			
		||||
const ALL_DOCS = "_all_docs?include_docs=true";
 | 
			
		||||
 | 
			
		||||
export default class CouchObjectProvider {
 | 
			
		||||
    // options {
 | 
			
		||||
@@ -41,6 +42,8 @@ export default class CouchObjectProvider {
 | 
			
		||||
        this.objectQueue = {};
 | 
			
		||||
        this.observeEnabled = options.disableObserve !== true;
 | 
			
		||||
        this.observers = {};
 | 
			
		||||
        this.batchIds = [];
 | 
			
		||||
 | 
			
		||||
        if (this.observeEnabled) {
 | 
			
		||||
            this.observeObjectChanges(options.filter);
 | 
			
		||||
        }
 | 
			
		||||
@@ -67,6 +70,9 @@ export default class CouchObjectProvider {
 | 
			
		||||
        // stringify body if needed
 | 
			
		||||
        if (fetchOptions.body) {
 | 
			
		||||
            fetchOptions.body = JSON.stringify(fetchOptions.body);
 | 
			
		||||
            fetchOptions.headers = {
 | 
			
		||||
                "Content-Type": "application/json"
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return fetch(this.url + '/' + subPath, fetchOptions)
 | 
			
		||||
@@ -78,14 +84,18 @@ export default class CouchObjectProvider {
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check the response to a create/update/delete request;
 | 
			
		||||
    // track the rev if it's valid, otherwise return false to
 | 
			
		||||
    // indicate that the request failed.
 | 
			
		||||
    // persist any queued objects
 | 
			
		||||
    /**
 | 
			
		||||
     * Check the response to a create/update/delete request;
 | 
			
		||||
     * track the rev if it's valid, otherwise return false to
 | 
			
		||||
     * indicate that the request failed.
 | 
			
		||||
     * persist any queued objects
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    checkResponse(response, intermediateResponse) {
 | 
			
		||||
        let requestSuccess = false;
 | 
			
		||||
        const id = response ? response.id : undefined;
 | 
			
		||||
        let rev;
 | 
			
		||||
 | 
			
		||||
        if (response && response.ok) {
 | 
			
		||||
            rev = response.rev;
 | 
			
		||||
            requestSuccess = true;
 | 
			
		||||
@@ -106,6 +116,9 @@ export default class CouchObjectProvider {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    getModel(response) {
 | 
			
		||||
        if (response && response.model) {
 | 
			
		||||
            let key = response[ID];
 | 
			
		||||
@@ -131,10 +144,118 @@ export default class CouchObjectProvider {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get(identifier, abortSignal) {
 | 
			
		||||
        return this.request(identifier.key, "GET", undefined, abortSignal).then(this.getModel.bind(this));
 | 
			
		||||
        this.batchIds.push(identifier.key);
 | 
			
		||||
 | 
			
		||||
        if (this.bulkPromise === undefined) {
 | 
			
		||||
            this.bulkPromise = this.deferBatchedGet(abortSignal);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.bulkPromise
 | 
			
		||||
            .then((domainObjectMap) => {
 | 
			
		||||
                return domainObjectMap[identifier.key];
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getObjectsByFilter(filter) {
 | 
			
		||||
    /**
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    deferBatchedGet(abortSignal) {
 | 
			
		||||
        // We until the next event loop cycle to "collect" all of the get
 | 
			
		||||
        // requests triggered in this iteration of the event loop
 | 
			
		||||
 | 
			
		||||
        return this.waitOneEventCycle().then(() => {
 | 
			
		||||
            let batchIds = this.batchIds;
 | 
			
		||||
 | 
			
		||||
            this.clearBatch();
 | 
			
		||||
 | 
			
		||||
            if (batchIds.length === 1) {
 | 
			
		||||
                let objectKey = batchIds[0];
 | 
			
		||||
 | 
			
		||||
                //If there's only one request, just do a regular get
 | 
			
		||||
                return this.request(objectKey, "GET", undefined, abortSignal)
 | 
			
		||||
                    .then(this.returnAsMap(objectKey));
 | 
			
		||||
            } else {
 | 
			
		||||
                return this.bulkGet(batchIds, abortSignal);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    returnAsMap(objectKey) {
 | 
			
		||||
        return (result) => {
 | 
			
		||||
            let objectMap = {};
 | 
			
		||||
            objectMap[objectKey] = this.getModel(result);
 | 
			
		||||
 | 
			
		||||
            return objectMap;
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    clearBatch() {
 | 
			
		||||
        this.batchIds = [];
 | 
			
		||||
        delete this.bulkPromise;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    waitOneEventCycle() {
 | 
			
		||||
        return new Promise((resolve) => {
 | 
			
		||||
            setTimeout(resolve);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    bulkGet(ids, signal) {
 | 
			
		||||
        ids = this.removeDuplicates(ids);
 | 
			
		||||
 | 
			
		||||
        const query = {
 | 
			
		||||
            'keys': ids
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return this.request(ALL_DOCS, 'POST', query, signal).then((response) => {
 | 
			
		||||
            if (response && response.rows !== undefined) {
 | 
			
		||||
                return response.rows.reduce((map, row) => {
 | 
			
		||||
                    if (row.doc !== undefined) {
 | 
			
		||||
                        map[row.key] = this.getModel(row.doc);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return map;
 | 
			
		||||
                }, {});
 | 
			
		||||
            } else {
 | 
			
		||||
                return {};
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    removeDuplicates(array) {
 | 
			
		||||
        return Array.from(new Set(array));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    search(query, abortSignal) {
 | 
			
		||||
        const filter = {
 | 
			
		||||
            "selector": {
 | 
			
		||||
                "model": {
 | 
			
		||||
                    "name": {
 | 
			
		||||
                        "$regex": `(?i)${query}`
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return this.getObjectsByFilter(filter, abortSignal);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getObjectsByFilter(filter, abortSignal) {
 | 
			
		||||
        let objects = [];
 | 
			
		||||
 | 
			
		||||
        let url = `${this.url}/_find`;
 | 
			
		||||
@@ -149,6 +270,7 @@ export default class CouchObjectProvider {
 | 
			
		||||
            headers: {
 | 
			
		||||
                "Content-Type": "application/json"
 | 
			
		||||
            },
 | 
			
		||||
            signal: abortSignal,
 | 
			
		||||
            body
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@@ -203,6 +325,9 @@ export default class CouchObjectProvider {
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    abortGetChanges() {
 | 
			
		||||
        if (this.controller) {
 | 
			
		||||
            this.controller.abort();
 | 
			
		||||
@@ -212,6 +337,9 @@ export default class CouchObjectProvider {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    async observeObjectChanges(filter) {
 | 
			
		||||
        let intermediateResponse = this.getIntermediateResponse();
 | 
			
		||||
 | 
			
		||||
@@ -292,6 +420,9 @@ export default class CouchObjectProvider {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    getIntermediateResponse() {
 | 
			
		||||
        let intermediateResponse = {};
 | 
			
		||||
        intermediateResponse.promise = new Promise(function (resolve, reject) {
 | 
			
		||||
@@ -302,6 +433,9 @@ export default class CouchObjectProvider {
 | 
			
		||||
        return intermediateResponse;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    enqueueObject(key, model, intermediateResponse) {
 | 
			
		||||
        if (this.objectQueue[key]) {
 | 
			
		||||
            this.objectQueue[key].enqueue({
 | 
			
		||||
@@ -330,6 +464,9 @@ export default class CouchObjectProvider {
 | 
			
		||||
        return intermediateResponse.promise;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    updateQueued(key) {
 | 
			
		||||
        if (!this.objectQueue[key].pending) {
 | 
			
		||||
            this.objectQueue[key].pending = true;
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,6 @@ import {
 | 
			
		||||
    createOpenMct,
 | 
			
		||||
    resetApplicationState, spyOnBuiltins
 | 
			
		||||
} from 'utils/testing';
 | 
			
		||||
import CouchObjectProvider from './CouchObjectProvider';
 | 
			
		||||
 | 
			
		||||
describe('the plugin', () => {
 | 
			
		||||
    let openmct;
 | 
			
		||||
@@ -42,7 +41,8 @@ describe('the plugin', () => {
 | 
			
		||||
                namespace: '',
 | 
			
		||||
                key: 'some-value'
 | 
			
		||||
            },
 | 
			
		||||
            type: 'mock-type'
 | 
			
		||||
            type: 'mock-type',
 | 
			
		||||
            modified: 0
 | 
			
		||||
        };
 | 
			
		||||
        options = {
 | 
			
		||||
            url: testPath,
 | 
			
		||||
@@ -95,6 +95,7 @@ describe('the plugin', () => {
 | 
			
		||||
                    return {
 | 
			
		||||
                        ok: true,
 | 
			
		||||
                        _id: 'some-value',
 | 
			
		||||
                        id: 'some-value',
 | 
			
		||||
                        _rev: 1,
 | 
			
		||||
                        model: {}
 | 
			
		||||
                    };
 | 
			
		||||
@@ -104,44 +105,130 @@ describe('the plugin', () => {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('gets an object', () => {
 | 
			
		||||
            openmct.objects.get(mockDomainObject.identifier).then((result) => {
 | 
			
		||||
            return openmct.objects.get(mockDomainObject.identifier).then((result) => {
 | 
			
		||||
                expect(result.identifier.key).toEqual(mockDomainObject.identifier.key);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('creates an object', () => {
 | 
			
		||||
            openmct.objects.save(mockDomainObject).then((result) => {
 | 
			
		||||
            return openmct.objects.save(mockDomainObject).then((result) => {
 | 
			
		||||
                expect(provider.create).toHaveBeenCalled();
 | 
			
		||||
                expect(result).toBeTrue();
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('updates an object', () => {
 | 
			
		||||
            openmct.objects.save(mockDomainObject).then((result) => {
 | 
			
		||||
            return openmct.objects.save(mockDomainObject).then((result) => {
 | 
			
		||||
                expect(result).toBeTrue();
 | 
			
		||||
                expect(provider.create).toHaveBeenCalled();
 | 
			
		||||
                openmct.objects.save(mockDomainObject).then((updatedResult) => {
 | 
			
		||||
 | 
			
		||||
                //Set modified timestamp it detects a change and persists the updated model.
 | 
			
		||||
                mockDomainObject.modified = Date.now();
 | 
			
		||||
 | 
			
		||||
                return openmct.objects.save(mockDomainObject).then((updatedResult) => {
 | 
			
		||||
                    expect(updatedResult).toBeTrue();
 | 
			
		||||
                    expect(provider.update).toHaveBeenCalled();
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    describe('batches requests', () => {
 | 
			
		||||
        let mockPromise;
 | 
			
		||||
        beforeEach(() => {
 | 
			
		||||
            mockPromise = Promise.resolve({
 | 
			
		||||
                json: () => {
 | 
			
		||||
                    return {
 | 
			
		||||
                        total_rows: 0,
 | 
			
		||||
                        rows: []
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            fetch.and.returnValue(mockPromise);
 | 
			
		||||
        });
 | 
			
		||||
        it('for multiple simultaneous gets', () => {
 | 
			
		||||
            const objectIds = [
 | 
			
		||||
                {
 | 
			
		||||
                    namespace: '',
 | 
			
		||||
                    key: 'object-1'
 | 
			
		||||
                }, {
 | 
			
		||||
                    namespace: '',
 | 
			
		||||
                    key: 'object-2'
 | 
			
		||||
                }, {
 | 
			
		||||
                    namespace: '',
 | 
			
		||||
                    key: 'object-3'
 | 
			
		||||
                }
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
        it('updates queued objects', () => {
 | 
			
		||||
            let couchProvider = new CouchObjectProvider(openmct, options, '');
 | 
			
		||||
            let intermediateResponse = couchProvider.getIntermediateResponse();
 | 
			
		||||
            spyOn(couchProvider, 'updateQueued');
 | 
			
		||||
            couchProvider.enqueueObject(mockDomainObject.identifier.key, mockDomainObject, intermediateResponse);
 | 
			
		||||
            couchProvider.objectQueue[mockDomainObject.identifier.key].updateRevision(1);
 | 
			
		||||
            couchProvider.update(mockDomainObject);
 | 
			
		||||
            expect(couchProvider.objectQueue[mockDomainObject.identifier.key].hasNext()).toBe(2);
 | 
			
		||||
            couchProvider.checkResponse({
 | 
			
		||||
                ok: true,
 | 
			
		||||
                rev: 2,
 | 
			
		||||
                id: mockDomainObject.identifier.key
 | 
			
		||||
            }, intermediateResponse);
 | 
			
		||||
            const getAllObjects = Promise.all(
 | 
			
		||||
                objectIds.map((identifier) =>
 | 
			
		||||
                    openmct.objects.get(identifier)
 | 
			
		||||
                ));
 | 
			
		||||
 | 
			
		||||
            expect(couchProvider.updateQueued).toHaveBeenCalledTimes(2);
 | 
			
		||||
            return getAllObjects.then(() => {
 | 
			
		||||
                const requestUrl = fetch.calls.mostRecent().args[0];
 | 
			
		||||
                const requestMethod = fetch.calls.mostRecent().args[1].method;
 | 
			
		||||
 | 
			
		||||
                expect(fetch).toHaveBeenCalledTimes(1);
 | 
			
		||||
                expect(requestUrl.includes('_all_docs')).toBeTrue();
 | 
			
		||||
                expect(requestMethod).toEqual('POST');
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('but not for single gets', () => {
 | 
			
		||||
            const objectId = {
 | 
			
		||||
                namespace: '',
 | 
			
		||||
                key: 'object-1'
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            const getObject = openmct.objects.get(objectId);
 | 
			
		||||
 | 
			
		||||
            return getObject.then(() => {
 | 
			
		||||
                const requestUrl = fetch.calls.mostRecent().args[0];
 | 
			
		||||
                const requestMethod = fetch.calls.mostRecent().args[1].method;
 | 
			
		||||
 | 
			
		||||
                expect(fetch).toHaveBeenCalledTimes(1);
 | 
			
		||||
                expect(requestUrl.endsWith(`${objectId.key}`)).toBeTrue();
 | 
			
		||||
                expect(requestMethod).toEqual('GET');
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    describe('implements server-side search', () => {
 | 
			
		||||
        let mockPromise;
 | 
			
		||||
        beforeEach(() => {
 | 
			
		||||
            mockPromise = Promise.resolve({
 | 
			
		||||
                body: {
 | 
			
		||||
                    getReader() {
 | 
			
		||||
                        return {
 | 
			
		||||
                            read() {
 | 
			
		||||
                                return Promise.resolve({
 | 
			
		||||
                                    done: true,
 | 
			
		||||
                                    value: undefined
 | 
			
		||||
                                });
 | 
			
		||||
                            }
 | 
			
		||||
                        };
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            fetch.and.returnValue(mockPromise);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("using Couch's 'find' endpoint", () => {
 | 
			
		||||
            return Promise.all(openmct.objects.search('test')).then(() => {
 | 
			
		||||
                const requestUrl = fetch.calls.mostRecent().args[0];
 | 
			
		||||
 | 
			
		||||
                expect(fetch).toHaveBeenCalled();
 | 
			
		||||
                expect(requestUrl.endsWith('_find')).toBeTrue();
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("and supports search by object name", () => {
 | 
			
		||||
            return Promise.all(openmct.objects.search('test')).then(() => {
 | 
			
		||||
                const requestPayload = JSON.parse(fetch.calls.mostRecent().args[1].body);
 | 
			
		||||
 | 
			
		||||
                expect(requestPayload).toBeDefined();
 | 
			
		||||
                expect(requestPayload.selector.model.name.$regex).toEqual('(?i)test');
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										64
									
								
								src/plugins/plot/vue/inspector/PlotOptions.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/plugins/plot/vue/inspector/PlotOptions.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,64 @@
 | 
			
		||||
<!--
 | 
			
		||||
 Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 Administration. All rights reserved.
 | 
			
		||||
 | 
			
		||||
 Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 You may obtain a copy of the License at
 | 
			
		||||
 http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 | 
			
		||||
 Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 License for the specific language governing permissions and limitations
 | 
			
		||||
 under the License.
 | 
			
		||||
 | 
			
		||||
 Open MCT includes source code licensed under additional open source
 | 
			
		||||
 licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 this source code distribution or the Licensing information page available
 | 
			
		||||
 at runtime from the About dialog for additional information.
 | 
			
		||||
-->
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
    <div v-if="canEdit">
 | 
			
		||||
        <plot-options-edit />
 | 
			
		||||
    </div>
 | 
			
		||||
    <div v-else>
 | 
			
		||||
        <plot-options-browse />
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import PlotOptionsBrowse from "@/plugins/plot/vue/inspector/PlotOptionsBrowse.vue";
 | 
			
		||||
import PlotOptionsEdit from "@/plugins/plot/vue/inspector/PlotOptionsEdit.vue";
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
        PlotOptionsBrowse,
 | 
			
		||||
        PlotOptionsEdit
 | 
			
		||||
    },
 | 
			
		||||
    inject: ['openmct', 'domainObject'],
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            isEditing: this.openmct.editor.isEditing()
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
        canEdit() {
 | 
			
		||||
            return this.isEditing && !this.domainObject.locked;
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        this.openmct.editor.on('isEditing', this.setEditState);
 | 
			
		||||
    },
 | 
			
		||||
    beforeDestroy() {
 | 
			
		||||
        this.openmct.editor.off('isEditing', this.setEditState);
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        setEditState(isEditing) {
 | 
			
		||||
            this.isEditing = isEditing;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										198
									
								
								src/plugins/plot/vue/inspector/PlotOptionsBrowse.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								src/plugins/plot/vue/inspector/PlotOptionsBrowse.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,198 @@
 | 
			
		||||
<!--
 | 
			
		||||
 Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 Administration. All rights reserved.
 | 
			
		||||
 | 
			
		||||
 Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 You may obtain a copy of the License at
 | 
			
		||||
 http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 | 
			
		||||
 Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 License for the specific language governing permissions and limitations
 | 
			
		||||
 under the License.
 | 
			
		||||
 | 
			
		||||
 Open MCT includes source code licensed under additional open source
 | 
			
		||||
 licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 this source code distribution or the Licensing information page available
 | 
			
		||||
 at runtime from the About dialog for additional information.
 | 
			
		||||
-->
 | 
			
		||||
<template>
 | 
			
		||||
<div v-if="config && loaded"
 | 
			
		||||
     class="js-plot-options-browse"
 | 
			
		||||
>
 | 
			
		||||
    <ul class="c-tree">
 | 
			
		||||
        <h2 title="Plot series display properties in this object">Plot Series</h2>
 | 
			
		||||
        <plot-options-item v-for="series in plotSeries"
 | 
			
		||||
                           :key="series.key"
 | 
			
		||||
                           :series="series"
 | 
			
		||||
        />
 | 
			
		||||
    </ul>
 | 
			
		||||
    <div class="grid-properties">
 | 
			
		||||
        <ul class="l-inspector-part">
 | 
			
		||||
            <h2 title="Y axis settings for this object">Y Axis</h2>
 | 
			
		||||
            <li class="grid-row">
 | 
			
		||||
                <div class="grid-cell label"
 | 
			
		||||
                     title="Manually override how the Y axis is labeled."
 | 
			
		||||
                >Label</div>
 | 
			
		||||
                <div class="grid-cell value">{{ label ? label : "Not defined" }}</div>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="grid-row">
 | 
			
		||||
                <div class="grid-cell label"
 | 
			
		||||
                     title="Automatically scale the Y axis to keep all values in view."
 | 
			
		||||
                >Autoscale</div>
 | 
			
		||||
                <div class="grid-cell value">
 | 
			
		||||
                    {{ autoscale ? "Enabled: " : "Disabled" }}
 | 
			
		||||
                    {{ autoscale ? autoscalePadding : "" }}
 | 
			
		||||
                </div>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li v-if="!autoscale && rangeMin"
 | 
			
		||||
                class="grid-row"
 | 
			
		||||
            >
 | 
			
		||||
                <div class="grid-cell label"
 | 
			
		||||
                     title="Minimum Y axis value."
 | 
			
		||||
                >Minimum value</div>
 | 
			
		||||
                <div class="grid-cell value">{{ rangeMin }}</div>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li v-if="!autoscale && rangeMax"
 | 
			
		||||
                class="grid-row"
 | 
			
		||||
            >
 | 
			
		||||
                <div class="grid-cell label"
 | 
			
		||||
                     title="Maximum Y axis value."
 | 
			
		||||
                >Maximum value</div>
 | 
			
		||||
                <div class="grid-cell value">{{ rangeMax }}</div>
 | 
			
		||||
            </li>
 | 
			
		||||
        </ul>
 | 
			
		||||
        <ul class="l-inspector-part">
 | 
			
		||||
            <h2 title="Legend settings for this object">Legend</h2>
 | 
			
		||||
            <li class="grid-row">
 | 
			
		||||
                <div class="grid-cell label"
 | 
			
		||||
                     title="The position of the legend relative to the plot display area."
 | 
			
		||||
                >Position</div>
 | 
			
		||||
                <div class="grid-cell value capitalize">{{ position }}</div>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="grid-row">
 | 
			
		||||
                <div class="grid-cell label"
 | 
			
		||||
                     title="Hide the legend when the plot is small"
 | 
			
		||||
                >Hide when plot small</div>
 | 
			
		||||
                <div class="grid-cell value">{{ hideLegendWhenSmall ? "Yes" : "No" }}</div>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="grid-row">
 | 
			
		||||
                <div class="grid-cell label"
 | 
			
		||||
                     title="Show the legend expanded by default"
 | 
			
		||||
                >Expand by Default</div>
 | 
			
		||||
                <div class="grid-cell value">{{ expandByDefault ? "Yes" : "No" }}</div>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="grid-row">
 | 
			
		||||
                <div class="grid-cell label"
 | 
			
		||||
                     title="What to display in the legend when it's collapsed."
 | 
			
		||||
                >Show when collapsed:</div>
 | 
			
		||||
                <div class="grid-cell value">{{
 | 
			
		||||
                    valueToShowWhenCollapsed.replace('nearest', '')
 | 
			
		||||
                }}
 | 
			
		||||
                </div>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="grid-row">
 | 
			
		||||
                <div class="grid-cell label"
 | 
			
		||||
                     title="What to display in the legend when it's expanded."
 | 
			
		||||
                >Show when expanded:</div>
 | 
			
		||||
                <div class="grid-cell value comma-list">
 | 
			
		||||
                    <span v-if="showTimestampWhenExpanded">Timestamp</span>
 | 
			
		||||
                    <span v-if="showValueWhenExpanded">Value</span>
 | 
			
		||||
                    <span v-if="showMinimumWhenExpanded">Min</span>
 | 
			
		||||
                    <span v-if="showMaximumWhenExpanded">Max</span>
 | 
			
		||||
                    <span v-if="showUnitsWhenExpanded">Units</span>
 | 
			
		||||
                </div>
 | 
			
		||||
            </li>
 | 
			
		||||
        </ul>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import PlotOptionsItem from "./PlotOptionsItem.vue";
 | 
			
		||||
import configStore from "../single/configuration/configStore";
 | 
			
		||||
import eventHelpers from "../single/lib/eventHelpers";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
        PlotOptionsItem
 | 
			
		||||
    },
 | 
			
		||||
    inject: ['openmct', 'domainObject'],
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            config: undefined,
 | 
			
		||||
            label: '',
 | 
			
		||||
            autoscale: '',
 | 
			
		||||
            autoscalePadding: '',
 | 
			
		||||
            rangeMin: '',
 | 
			
		||||
            rangeMax: '',
 | 
			
		||||
            position: '',
 | 
			
		||||
            hideLegendWhenSmall: '',
 | 
			
		||||
            expandByDefault: '',
 | 
			
		||||
            valueToShowWhenCollapsed: '',
 | 
			
		||||
            showTimestampWhenExpanded: '',
 | 
			
		||||
            showValueWhenExpanded: '',
 | 
			
		||||
            showMinimumWhenExpanded: '',
 | 
			
		||||
            showMaximumWhenExpanded: '',
 | 
			
		||||
            showUnitsWhenExpanded: '',
 | 
			
		||||
            loaded: false,
 | 
			
		||||
            plotSeries: []
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        eventHelpers.extend(this);
 | 
			
		||||
        this.config = this.getConfig();
 | 
			
		||||
        this.initConfiguration();
 | 
			
		||||
        this.registerListeners();
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
    },
 | 
			
		||||
    beforeDestroy() {
 | 
			
		||||
        this.stopListening();
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        initConfiguration() {
 | 
			
		||||
            this.label = this.config.yAxis.get('label');
 | 
			
		||||
            this.autoscale = this.config.yAxis.get('autoscale');
 | 
			
		||||
            this.autoscalePadding = this.config.yAxis.get('autoscalePadding');
 | 
			
		||||
            const range = this.config.yAxis.get('range');
 | 
			
		||||
            if (range) {
 | 
			
		||||
                this.rangeMin = range.min;
 | 
			
		||||
                this.rangeMax = range.max;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.position = this.config.legend.get('position');
 | 
			
		||||
            this.hideLegendWhenSmall = this.config.legend.get('hideLegendWhenSmall');
 | 
			
		||||
            this.expandByDefault = this.config.legend.get('expandByDefault');
 | 
			
		||||
            this.valueToShowWhenCollapsed = this.config.legend.get('valueToShowWhenCollapsed');
 | 
			
		||||
            this.showTimestampWhenExpanded = this.config.legend.get('showTimestampWhenExpanded');
 | 
			
		||||
            this.showValueWhenExpanded = this.config.legend.get('showValueWhenExpanded');
 | 
			
		||||
            this.showMinimumWhenExpanded = this.config.legend.get('showMinimumWhenExpanded');
 | 
			
		||||
            this.showMaximumWhenExpanded = this.config.legend.get('showMaximumWhenExpanded');
 | 
			
		||||
            this.showUnitsWhenExpanded = this.config.legend.get('showUnitsWhenExpanded');
 | 
			
		||||
        },
 | 
			
		||||
        getConfig() {
 | 
			
		||||
            this.configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
 | 
			
		||||
 | 
			
		||||
            return configStore.get(this.configId);
 | 
			
		||||
        },
 | 
			
		||||
        registerListeners() {
 | 
			
		||||
            this.config.series.forEach(this.addSeries, this);
 | 
			
		||||
 | 
			
		||||
            this.listenTo(this.config.series, 'add', this.addSeries, this);
 | 
			
		||||
            this.listenTo(this.config.series, 'remove', this.resetAllSeries, this);
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        addSeries(series, index) {
 | 
			
		||||
            this.plotSeries[index] = series;
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        resetAllSeries() {
 | 
			
		||||
            this.plotSeries = [];
 | 
			
		||||
            this.config.series.forEach(this.addSeries, this);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										100
									
								
								src/plugins/plot/vue/inspector/PlotOptionsEdit.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								src/plugins/plot/vue/inspector/PlotOptionsEdit.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,100 @@
 | 
			
		||||
<!--
 | 
			
		||||
 Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 Administration. All rights reserved.
 | 
			
		||||
 | 
			
		||||
 Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 You may obtain a copy of the License at
 | 
			
		||||
 http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 | 
			
		||||
 Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 License for the specific language governing permissions and limitations
 | 
			
		||||
 under the License.
 | 
			
		||||
 | 
			
		||||
 Open MCT includes source code licensed under additional open source
 | 
			
		||||
 licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 this source code distribution or the Licensing information page available
 | 
			
		||||
 at runtime from the About dialog for additional information.
 | 
			
		||||
-->
 | 
			
		||||
<template>
 | 
			
		||||
<div v-if="config && loaded"
 | 
			
		||||
     class="js-plot-options-edit"
 | 
			
		||||
>
 | 
			
		||||
    <ul class="c-tree">
 | 
			
		||||
        <h2 title="Display properties for this object">Plot Series</h2>
 | 
			
		||||
        <li v-for="series in plotSeries"
 | 
			
		||||
            :key="series.key"
 | 
			
		||||
        >
 | 
			
		||||
            <series-form :series="series" />
 | 
			
		||||
        </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
    <y-axis-form v-show="!!plotSeries.length"
 | 
			
		||||
                 class="grid-properties"
 | 
			
		||||
                 :y-axis="config.yAxis"
 | 
			
		||||
    />
 | 
			
		||||
    <ul class="l-inspector-part">
 | 
			
		||||
        <h2 title="Legend options">Legend</h2>
 | 
			
		||||
        <legend-form v-show="!!plotSeries.length"
 | 
			
		||||
                     class="grid-properties"
 | 
			
		||||
                     :legend="config.legend"
 | 
			
		||||
        />
 | 
			
		||||
    </ul>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
<script>
 | 
			
		||||
import SeriesForm from "@/plugins/plot/vue/inspector/forms/SeriesForm.vue";
 | 
			
		||||
import YAxisForm from "@/plugins/plot/vue/inspector/forms/YAxisForm.vue";
 | 
			
		||||
import LegendForm from "@/plugins/plot/vue/inspector/forms/LegendForm.vue";
 | 
			
		||||
import eventHelpers from "@/plugins/plot/vue/single/lib/eventHelpers";
 | 
			
		||||
import configStore from "@/plugins/plot/vue/single/configuration/configStore";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
        LegendForm,
 | 
			
		||||
        SeriesForm,
 | 
			
		||||
        YAxisForm
 | 
			
		||||
    },
 | 
			
		||||
    inject: ['openmct', 'domainObject'],
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            config: {},
 | 
			
		||||
            plotSeries: [],
 | 
			
		||||
            loaded: false
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        eventHelpers.extend(this);
 | 
			
		||||
        this.config = this.getConfig();
 | 
			
		||||
        this.registerListeners();
 | 
			
		||||
        this.loaded = true;
 | 
			
		||||
    },
 | 
			
		||||
    beforeDestroy() {
 | 
			
		||||
        this.stopListening();
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        getConfig() {
 | 
			
		||||
            this.configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
 | 
			
		||||
 | 
			
		||||
            return configStore.get(this.configId);
 | 
			
		||||
        },
 | 
			
		||||
        registerListeners() {
 | 
			
		||||
            this.config.series.forEach(this.addSeries, this);
 | 
			
		||||
 | 
			
		||||
            this.listenTo(this.config.series, 'add', this.addSeries, this);
 | 
			
		||||
            this.listenTo(this.config.series, 'remove', this.resetAllSeries, this);
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        addSeries(series, index) {
 | 
			
		||||
            this.plotSeries[index] = series;
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        resetAllSeries() {
 | 
			
		||||
            this.plotSeries = [];
 | 
			
		||||
            this.config.series.forEach(this.addSeries, this);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										155
									
								
								src/plugins/plot/vue/inspector/PlotOptionsItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								src/plugins/plot/vue/inspector/PlotOptionsItem.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,155 @@
 | 
			
		||||
<template>
 | 
			
		||||
<ul>
 | 
			
		||||
    <li class="c-tree__item menus-to-left">
 | 
			
		||||
        <span class="c-disclosure-triangle is-enabled flex-elem"
 | 
			
		||||
              :class="expandedCssClass"
 | 
			
		||||
              @click="toggleExpanded"
 | 
			
		||||
        >
 | 
			
		||||
        </span>
 | 
			
		||||
        <div class="c-object-label"
 | 
			
		||||
             :class="statusClass"
 | 
			
		||||
        >
 | 
			
		||||
            <div class="c-object-label__type-icon"
 | 
			
		||||
                 :class="getSeriesClass"
 | 
			
		||||
            >
 | 
			
		||||
                <span class="is-status__indicator"
 | 
			
		||||
                      title="This item is missing or suspect"
 | 
			
		||||
                ></span>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="c-object-label__name">{{ series.domainObject.name }}</div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </li>
 | 
			
		||||
    <li v-show="expanded"
 | 
			
		||||
        class="c-tree__item menus-to-left"
 | 
			
		||||
    >
 | 
			
		||||
        <ul class="grid-properties js-plot-options-browse-properties">
 | 
			
		||||
            <li class="grid-row">
 | 
			
		||||
                <div class="grid-cell label"
 | 
			
		||||
                     title="The field to be plotted as a value for this series."
 | 
			
		||||
                >Value</div>
 | 
			
		||||
                <div class="grid-cell value">
 | 
			
		||||
                    {{ yKey }}
 | 
			
		||||
                </div>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="grid-row">
 | 
			
		||||
                <div class="grid-cell label"
 | 
			
		||||
                     title="The rendering method to join lines for this series."
 | 
			
		||||
                >Line Method</div>
 | 
			
		||||
                <div class="grid-cell value">{{ {
 | 
			
		||||
                    'none': 'None',
 | 
			
		||||
                    'linear': 'Linear interpolation',
 | 
			
		||||
                    'stepAfter': 'Step After'
 | 
			
		||||
                }[interpolate] }}
 | 
			
		||||
                </div>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="grid-row">
 | 
			
		||||
                <div class="grid-cell label"
 | 
			
		||||
                     title="Whether markers are displayed, and their size."
 | 
			
		||||
                >Markers</div>
 | 
			
		||||
                <div class="grid-cell value">
 | 
			
		||||
                    {{ markerOptionsDisplayText }}
 | 
			
		||||
                </div>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="grid-row">
 | 
			
		||||
                <div class="grid-cell label"
 | 
			
		||||
                     title="Display markers visually denoting points in alarm."
 | 
			
		||||
                >Alarm Markers</div>
 | 
			
		||||
                <div class="grid-cell value">
 | 
			
		||||
                    {{ alarmMarkers ? "Enabled" : "Disabled" }}
 | 
			
		||||
                </div>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="grid-row">
 | 
			
		||||
                <div class="grid-cell label"
 | 
			
		||||
                     title="The plot line and marker color for this series."
 | 
			
		||||
                >Color</div>
 | 
			
		||||
                <div class="grid-cell value">
 | 
			
		||||
                    <span class="c-color-swatch"
 | 
			
		||||
                          :style="{
 | 
			
		||||
                              'background': seriesHexColor
 | 
			
		||||
                          }"
 | 
			
		||||
                    >
 | 
			
		||||
                    </span>
 | 
			
		||||
                </div>
 | 
			
		||||
            </li>
 | 
			
		||||
        </ul>
 | 
			
		||||
    </li>
 | 
			
		||||
</ul>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
    inject: ['openmct'],
 | 
			
		||||
    props: {
 | 
			
		||||
        series: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
            default() {
 | 
			
		||||
                return {};
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            expanded: false
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
        getSeriesClass() {
 | 
			
		||||
            let cssClass = '';
 | 
			
		||||
            let legacyObject = this.openmct.legacyObject(this.series.domainObject);
 | 
			
		||||
            let location = legacyObject.getCapability('location');
 | 
			
		||||
            if (location && location.isLink()) {
 | 
			
		||||
                cssClass = 'l-icon-link';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let type = legacyObject.getCapability('type');
 | 
			
		||||
            if (type) {
 | 
			
		||||
                cssClass = `${cssClass} ${type.getCssClass()}`;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return cssClass;
 | 
			
		||||
        },
 | 
			
		||||
        expandedCssClass() {
 | 
			
		||||
            if (this.expanded === true) {
 | 
			
		||||
                return 'c-disclosure-triangle--expanded';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return '';
 | 
			
		||||
        },
 | 
			
		||||
        statusClass() {
 | 
			
		||||
            return (this.status) ? `is-status--${this.status}` : '';
 | 
			
		||||
        },
 | 
			
		||||
        yKey() {
 | 
			
		||||
            return this.series.get('yKey');
 | 
			
		||||
        },
 | 
			
		||||
        interpolate() {
 | 
			
		||||
            return this.series.get('interpolate');
 | 
			
		||||
        },
 | 
			
		||||
        markerOptionsDisplayText() {
 | 
			
		||||
            return this.series.markerOptionsDisplayText();
 | 
			
		||||
        },
 | 
			
		||||
        alarmMarkers() {
 | 
			
		||||
            return this.series.get('alarmMarkers');
 | 
			
		||||
        },
 | 
			
		||||
        seriesHexColor() {
 | 
			
		||||
            return this.series.get('color').asHexString();
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        this.status = this.openmct.status.get(this.series.domainObject.identifier);
 | 
			
		||||
        this.removeStatusListener = this.openmct.status.observe(this.series.domainObject.identifier, this.setStatus);
 | 
			
		||||
    },
 | 
			
		||||
    beforeDestroy() {
 | 
			
		||||
        if (this.removeStatusListener) {
 | 
			
		||||
            this.removeStatusListener();
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        toggleExpanded() {
 | 
			
		||||
            this.expanded = !this.expanded;
 | 
			
		||||
        },
 | 
			
		||||
        setStatus(status) {
 | 
			
		||||
            this.status = status;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										51
									
								
								src/plugins/plot/vue/inspector/PlotsInspectorViewProvider.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/plugins/plot/vue/inspector/PlotsInspectorViewProvider.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
 | 
			
		||||
import PlotOptions from "./PlotOptions.vue";
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
 | 
			
		||||
export default function PlotsInspectorViewProvider(openmct) {
 | 
			
		||||
    return {
 | 
			
		||||
        key: 'plots-inspector',
 | 
			
		||||
        name: 'Plots Inspector View',
 | 
			
		||||
        canView: function (selection) {
 | 
			
		||||
            if (selection.length === 0 || selection[0].length === 0) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let parent = selection[0].length > 1 && selection[0][1].context.item;
 | 
			
		||||
            let object = selection[0][0].context.item;
 | 
			
		||||
 | 
			
		||||
            return parent
 | 
			
		||||
                && parent.type === 'time-strip'
 | 
			
		||||
                && object
 | 
			
		||||
                && object.type === 'telemetry.plot.overlay';
 | 
			
		||||
        },
 | 
			
		||||
        view: function (selection) {
 | 
			
		||||
            let component;
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                show: function (element) {
 | 
			
		||||
                    component = new Vue({
 | 
			
		||||
                        el: element,
 | 
			
		||||
                        components: {
 | 
			
		||||
                            PlotOptions: PlotOptions
 | 
			
		||||
                        },
 | 
			
		||||
                        provide: {
 | 
			
		||||
                            openmct,
 | 
			
		||||
                            domainObject: openmct.selection.get()[0][0].context.item
 | 
			
		||||
                        },
 | 
			
		||||
                        template: '<plot-options></plot-options>'
 | 
			
		||||
                    });
 | 
			
		||||
                },
 | 
			
		||||
                destroy: function () {
 | 
			
		||||
                    if (component) {
 | 
			
		||||
                        component.$destroy();
 | 
			
		||||
                        component = undefined;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        priority: function () {
 | 
			
		||||
            return 1;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										207
									
								
								src/plugins/plot/vue/inspector/forms/LegendForm.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								src/plugins/plot/vue/inspector/forms/LegendForm.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,207 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
    <li class="grid-row">
 | 
			
		||||
        <div class="grid-cell label"
 | 
			
		||||
             title="The position of the legend relative to the plot display area."
 | 
			
		||||
        >Position</div>
 | 
			
		||||
        <div class="grid-cell value">
 | 
			
		||||
            <select v-model="position"
 | 
			
		||||
                    @change="updateForm('position')"
 | 
			
		||||
            >
 | 
			
		||||
                <option value="top">Top</option>
 | 
			
		||||
                <option value="right">Right</option>
 | 
			
		||||
                <option value="bottom">Bottom</option>
 | 
			
		||||
                <option value="left">Left</option>
 | 
			
		||||
            </select>
 | 
			
		||||
        </div>
 | 
			
		||||
    </li>
 | 
			
		||||
    <li class="grid-row">
 | 
			
		||||
        <div class="grid-cell label"
 | 
			
		||||
             title="Hide the legend when the plot is small"
 | 
			
		||||
        >Hide when plot small</div>
 | 
			
		||||
        <div class="grid-cell value"><input v-model="hideLegendWhenSmall"
 | 
			
		||||
                                            type="checkbox"
 | 
			
		||||
                                            @change="updateForm('hideLegendWhenSmall')"
 | 
			
		||||
        ></div>
 | 
			
		||||
    </li>
 | 
			
		||||
    <li class="grid-row">
 | 
			
		||||
        <div class="grid-cell label"
 | 
			
		||||
             title="Show the legend expanded by default"
 | 
			
		||||
        >Expand by default</div>
 | 
			
		||||
        <div class="grid-cell value"><input v-model="expandByDefault"
 | 
			
		||||
                                            type="checkbox"
 | 
			
		||||
                                            @change="updateForm('expandByDefault')"
 | 
			
		||||
        ></div>
 | 
			
		||||
    </li>
 | 
			
		||||
    <li class="grid-row">
 | 
			
		||||
        <div class="grid-cell label"
 | 
			
		||||
             title="What to display in the legend when it's collapsed."
 | 
			
		||||
        >When collapsed show</div>
 | 
			
		||||
        <div class="grid-cell value">
 | 
			
		||||
            <select v-model="valueToShowWhenCollapsed"
 | 
			
		||||
                    @change="updateForm('valueToShowWhenCollapsed')"
 | 
			
		||||
            >
 | 
			
		||||
                <option value="none">Nothing</option>
 | 
			
		||||
                <option value="nearestTimestamp">Nearest timestamp</option>
 | 
			
		||||
                <option value="nearestValue">Nearest value</option>
 | 
			
		||||
                <option value="min">Minimum value</option>
 | 
			
		||||
                <option value="max">Maximum value</option>
 | 
			
		||||
                <option value="units">Units</option>
 | 
			
		||||
            </select>
 | 
			
		||||
        </div>
 | 
			
		||||
    </li>
 | 
			
		||||
    <li class="grid-row">
 | 
			
		||||
        <div class="grid-cell label"
 | 
			
		||||
             title="What to display in the legend when it's expanded."
 | 
			
		||||
        >When expanded show</div>
 | 
			
		||||
        <div class="grid-cell value">
 | 
			
		||||
            <ul>
 | 
			
		||||
                <li><input v-model="showTimestampWhenExpanded"
 | 
			
		||||
                           type="checkbox"
 | 
			
		||||
                           @change="updateForm('showTimestampWhenExpanded')"
 | 
			
		||||
                > Nearest timestamp</li>
 | 
			
		||||
                <li><input v-model="showValueWhenExpanded"
 | 
			
		||||
                           type="checkbox"
 | 
			
		||||
                           @change="updateForm('showValueWhenExpanded')"
 | 
			
		||||
                > Nearest value</li>
 | 
			
		||||
                <li><input v-model="showMinimumWhenExpanded"
 | 
			
		||||
                           type="checkbox"
 | 
			
		||||
                           @change="updateForm('showMinimumWhenExpanded')"
 | 
			
		||||
                > Minimum value</li>
 | 
			
		||||
                <li><input v-model="showMaximumWhenExpanded"
 | 
			
		||||
                           type="checkbox"
 | 
			
		||||
                           @change="updateForm('showMaximumWhenExpanded')"
 | 
			
		||||
                > Maximum value</li>
 | 
			
		||||
                <li><input v-model="showUnitsWhenExpanded"
 | 
			
		||||
                           type="checkbox"
 | 
			
		||||
                           @change="updateForm('showUnitsWhenExpanded')"
 | 
			
		||||
                > Units</li>
 | 
			
		||||
            </ul>
 | 
			
		||||
 | 
			
		||||
        </div>
 | 
			
		||||
    </li>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
<script>
 | 
			
		||||
import {coerce, objectPath, validate} from "@/plugins/plot/vue/inspector/forms/formUtil";
 | 
			
		||||
import _ from "lodash";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    inject: ['openmct', 'domainObject'],
 | 
			
		||||
    props: {
 | 
			
		||||
        legend: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
            default() {
 | 
			
		||||
                return {};
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            position: '',
 | 
			
		||||
            hideLegendWhenSmall: '',
 | 
			
		||||
            expandByDefault: '',
 | 
			
		||||
            valueToShowWhenCollapsed: '',
 | 
			
		||||
            showTimestampWhenExpanded: '',
 | 
			
		||||
            showValueWhenExpanded: '',
 | 
			
		||||
            showMinimumWhenExpanded: '',
 | 
			
		||||
            showMaximumWhenExpanded: '',
 | 
			
		||||
            showUnitsWhenExpanded: '',
 | 
			
		||||
            validation: {}
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        this.initialize();
 | 
			
		||||
        this.initFormValues();
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        initialize() {
 | 
			
		||||
            this.fields = [
 | 
			
		||||
                {
 | 
			
		||||
                    modelProp: 'position',
 | 
			
		||||
                    objectPath: 'configuration.legend.position'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    modelProp: 'hideLegendWhenSmall',
 | 
			
		||||
                    coerce: Boolean,
 | 
			
		||||
                    objectPath: 'configuration.legend.hideLegendWhenSmall'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    modelProp: 'expandByDefault',
 | 
			
		||||
                    coerce: Boolean,
 | 
			
		||||
                    objectPath: 'configuration.legend.expandByDefault'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    modelProp: 'valueToShowWhenCollapsed',
 | 
			
		||||
                    objectPath: 'configuration.legend.valueToShowWhenCollapsed'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    modelProp: 'showValueWhenExpanded',
 | 
			
		||||
                    coerce: Boolean,
 | 
			
		||||
                    objectPath: 'configuration.legend.showValueWhenExpanded'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    modelProp: 'showTimestampWhenExpanded',
 | 
			
		||||
                    coerce: Boolean,
 | 
			
		||||
                    objectPath: 'configuration.legend.showTimestampWhenExpanded'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    modelProp: 'showMaximumWhenExpanded',
 | 
			
		||||
                    coerce: Boolean,
 | 
			
		||||
                    objectPath: 'configuration.legend.showMaximumWhenExpanded'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    modelProp: 'showMinimumWhenExpanded',
 | 
			
		||||
                    coerce: Boolean,
 | 
			
		||||
                    objectPath: 'configuration.legend.showMinimumWhenExpanded'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    modelProp: 'showUnitsWhenExpanded',
 | 
			
		||||
                    coerce: Boolean,
 | 
			
		||||
                    objectPath: 'configuration.legend.showUnitsWhenExpanded'
 | 
			
		||||
                }
 | 
			
		||||
            ];
 | 
			
		||||
        },
 | 
			
		||||
        initFormValues() {
 | 
			
		||||
            this.position = this.legend.get('position');
 | 
			
		||||
            this.hideLegendWhenSmall = this.legend.get('hideLegendWhenSmall');
 | 
			
		||||
            this.expandByDefault = this.legend.get('expandByDefault');
 | 
			
		||||
            this.valueToShowWhenCollapsed = this.legend.get('valueToShowWhenCollapsed');
 | 
			
		||||
            this.showTimestampWhenExpanded = this.legend.get('showTimestampWhenExpanded');
 | 
			
		||||
            this.showValueWhenExpanded = this.legend.get('showValueWhenExpanded');
 | 
			
		||||
            this.showMinimumWhenExpanded = this.legend.get('showMinimumWhenExpanded');
 | 
			
		||||
            this.showMaximumWhenExpanded = this.legend.get('showMaximumWhenExpanded');
 | 
			
		||||
            this.showUnitsWhenExpanded = this.legend.get('showUnitsWhenExpanded');
 | 
			
		||||
        },
 | 
			
		||||
        updateForm(formKey) {
 | 
			
		||||
            const newVal = this[formKey];
 | 
			
		||||
            const oldVal = this.legend.get(formKey);
 | 
			
		||||
            const formField = this.fields.find((field) => field.modelProp === formKey);
 | 
			
		||||
 | 
			
		||||
            const path = objectPath(formField.objectPath);
 | 
			
		||||
            const validationResult = validate(newVal, this.legend, formField.validate);
 | 
			
		||||
            if (validationResult === true) {
 | 
			
		||||
                delete this.validation[formKey];
 | 
			
		||||
            } else {
 | 
			
		||||
                this.validation[formKey] = validationResult;
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!_.isEqual(coerce(newVal, formField.coerce), coerce(oldVal, formField.coerce))) {
 | 
			
		||||
                this.legend.set(formKey, coerce(newVal, formField.coerce));
 | 
			
		||||
                if (path) {
 | 
			
		||||
                    this.openmct.objects.mutate(
 | 
			
		||||
                        this.domainObject,
 | 
			
		||||
                        path(this.domainObject, this.legend),
 | 
			
		||||
                        coerce(newVal, formField.coerce)
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        setStatus(status) {
 | 
			
		||||
            this.status = status;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										347
									
								
								src/plugins/plot/vue/inspector/forms/SeriesForm.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										347
									
								
								src/plugins/plot/vue/inspector/forms/SeriesForm.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,347 @@
 | 
			
		||||
<template>
 | 
			
		||||
<ul>
 | 
			
		||||
    <li class="c-tree__item menus-to-left">
 | 
			
		||||
        <span class="c-disclosure-triangle is-enabled flex-elem"
 | 
			
		||||
              :class="expandedCssClass"
 | 
			
		||||
              @click="toggleExpanded"
 | 
			
		||||
        >
 | 
			
		||||
        </span>
 | 
			
		||||
        <div :class="objectLabelCss">
 | 
			
		||||
            <div class="c-object-label__type-icon"
 | 
			
		||||
                 :class="[seriesCss, linkCss]"
 | 
			
		||||
            >
 | 
			
		||||
                <span class="is-status__indicator"
 | 
			
		||||
                      title="This item is missing or suspect"
 | 
			
		||||
                ></span>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="c-object-label__name">{{ series.domainObject.name }}</div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </li>
 | 
			
		||||
    <ul v-show="expanded"
 | 
			
		||||
        class="grid-properties js-plot-options-edit-properties"
 | 
			
		||||
    >
 | 
			
		||||
        <li class="grid-row">
 | 
			
		||||
            <!-- Value to be displayed -->
 | 
			
		||||
            <div class="grid-cell label"
 | 
			
		||||
                 title="The field to be plotted as a value for this series."
 | 
			
		||||
            >Value</div>
 | 
			
		||||
            <div class="grid-cell value">
 | 
			
		||||
                <select v-model="yKey"
 | 
			
		||||
                        @change="updateForm('yKey')"
 | 
			
		||||
                >
 | 
			
		||||
                    <option v-for="option in yKeyOptions"
 | 
			
		||||
                            :key="option.value"
 | 
			
		||||
                            :value="option.value"
 | 
			
		||||
                            :selected="option.value == yKey"
 | 
			
		||||
                    >
 | 
			
		||||
                        {{ option.name }}
 | 
			
		||||
                    </option>
 | 
			
		||||
                </select>
 | 
			
		||||
            </div>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li class="grid-row">
 | 
			
		||||
            <div class="grid-cell label"
 | 
			
		||||
                 title="The rendering method to join lines for this series."
 | 
			
		||||
            >Line Method</div>
 | 
			
		||||
            <div class="grid-cell value">
 | 
			
		||||
                <select v-model="interpolate"
 | 
			
		||||
                        @change="updateForm('interpolate')"
 | 
			
		||||
                >
 | 
			
		||||
                    <option value="none">None</option>
 | 
			
		||||
                    <option value="linear">Linear interpolate</option>
 | 
			
		||||
                    <option value="stepAfter">Step after</option>
 | 
			
		||||
                </select>
 | 
			
		||||
            </div>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li class="grid-row">
 | 
			
		||||
            <div class="grid-cell label"
 | 
			
		||||
                 title="Whether markers are displayed."
 | 
			
		||||
            >Markers</div>
 | 
			
		||||
            <div class="grid-cell value">
 | 
			
		||||
                <input v-model="markers"
 | 
			
		||||
                       type="checkbox"
 | 
			
		||||
                       @change="updateForm('markers')"
 | 
			
		||||
                >
 | 
			
		||||
                <select
 | 
			
		||||
                    v-show="markers"
 | 
			
		||||
                    v-model="markerShape"
 | 
			
		||||
                    @change="updateForm('markerShape')"
 | 
			
		||||
                >
 | 
			
		||||
                    <option
 | 
			
		||||
                        v-for="option in markerShapeOptions"
 | 
			
		||||
                        :key="option.value"
 | 
			
		||||
                        :value="option.value"
 | 
			
		||||
                        :selected="option.value == markerShape"
 | 
			
		||||
                    >
 | 
			
		||||
                        {{ option.name }}
 | 
			
		||||
                    </option>
 | 
			
		||||
                </select>
 | 
			
		||||
            </div>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li class="grid-row">
 | 
			
		||||
            <div class="grid-cell label"
 | 
			
		||||
                 title="Display markers visually denoting points in alarm."
 | 
			
		||||
            >Alarm Markers</div>
 | 
			
		||||
            <div class="grid-cell value">
 | 
			
		||||
                <input v-model="alarmMarkers"
 | 
			
		||||
                       type="checkbox"
 | 
			
		||||
                       @change="updateForm('alarmMarkers')"
 | 
			
		||||
                >
 | 
			
		||||
            </div>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li v-show="markers || alarmMarkers"
 | 
			
		||||
            class="grid-row"
 | 
			
		||||
        >
 | 
			
		||||
            <div class="grid-cell label"
 | 
			
		||||
                 title="The size of regular and alarm markers for this series."
 | 
			
		||||
            >Marker Size:</div>
 | 
			
		||||
            <div class="grid-cell value"><input v-model="markerSize"
 | 
			
		||||
                                                class="c-input--flex"
 | 
			
		||||
                                                type="text"
 | 
			
		||||
                                                @change="updateForm('markerSize')"
 | 
			
		||||
            ></div>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li v-show="interpolate !== 'none' || markers"
 | 
			
		||||
            class="grid-row"
 | 
			
		||||
        >
 | 
			
		||||
            <div class="grid-cell label"
 | 
			
		||||
                 title="Manually set the plot line and marker color for this series."
 | 
			
		||||
            >Color</div>
 | 
			
		||||
            <div class="grid-cell value">
 | 
			
		||||
                <div class="c-click-swatch c-click-swatch--menu"
 | 
			
		||||
                     @click="toggleSwatch()"
 | 
			
		||||
                >
 | 
			
		||||
                    <span class="c-color-swatch"
 | 
			
		||||
                          :style="{ background: seriesColorAsHex }"
 | 
			
		||||
                    >
 | 
			
		||||
                    </span>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="c-palette c-palette--color">
 | 
			
		||||
                    <div v-show="swatchActive"
 | 
			
		||||
                         class="c-palette__items"
 | 
			
		||||
                    >
 | 
			
		||||
                        <div v-for="(group, index) in colorPalette"
 | 
			
		||||
                             :key="index"
 | 
			
		||||
                             class="u-contents"
 | 
			
		||||
                        >
 | 
			
		||||
                            <div v-for="(color, colorIndex) in group"
 | 
			
		||||
                                 :key="colorIndex"
 | 
			
		||||
                                 class="c-palette__item"
 | 
			
		||||
                                 :class="{ 'selected': series.get('color').equalTo(color) }"
 | 
			
		||||
                                 :style="{ background: color.asHexString() }"
 | 
			
		||||
                                 @click="setColor(color)"
 | 
			
		||||
                            >
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
</ul>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { MARKER_SHAPES } from "../../single/draw/MarkerShapes";
 | 
			
		||||
import { objectPath, validate, coerce } from "./formUtil";
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    inject: ['openmct', 'domainObject'],
 | 
			
		||||
    props: {
 | 
			
		||||
        series: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
            default() {
 | 
			
		||||
                return {};
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            expanded: false,
 | 
			
		||||
            markerShapeOptions: [],
 | 
			
		||||
            yKey: this.series.get('yKey'),
 | 
			
		||||
            yKeyOptions: [],
 | 
			
		||||
            interpolate: this.series.get('interpolate'),
 | 
			
		||||
            markers: this.series.get('markers'),
 | 
			
		||||
            markerShape: this.series.get('markerShape'),
 | 
			
		||||
            alarmMarkers: this.series.get('alarmMarkers'),
 | 
			
		||||
            markerSize: this.series.get('markerSize'),
 | 
			
		||||
            validation: {},
 | 
			
		||||
            swatchActive: false
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
        colorPalette() {
 | 
			
		||||
            return this.series.collection.palette.groups();
 | 
			
		||||
        },
 | 
			
		||||
        objectLabelCss() {
 | 
			
		||||
            return this.status ? `c-object-label is-status--${this.status}'` : 'c-object-label';
 | 
			
		||||
        },
 | 
			
		||||
        seriesCss() {
 | 
			
		||||
            let legacyObject = this.openmct.legacyObject(this.series.domainObject);
 | 
			
		||||
            let type = legacyObject.getCapability('type');
 | 
			
		||||
 | 
			
		||||
            return type ? `c-object-label__type-icon ${type.getCssClass()}` : `c-object-label__type-icon`;
 | 
			
		||||
        },
 | 
			
		||||
        linkCss() {
 | 
			
		||||
            let cssClass = '';
 | 
			
		||||
            let legacyObject = this.openmct.legacyObject(this.series.domainObject);
 | 
			
		||||
            let location = legacyObject.getCapability('location');
 | 
			
		||||
            if (location && location.isLink()) {
 | 
			
		||||
                cssClass = 'l-icon-link';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return cssClass;
 | 
			
		||||
        },
 | 
			
		||||
        expandedCssClass() {
 | 
			
		||||
            return this.expanded ? 'c-disclosure-triangle--expanded' : '';
 | 
			
		||||
        },
 | 
			
		||||
        seriesColorAsHex() {
 | 
			
		||||
            return this.series.get('color').asHexString();
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        this.initialize();
 | 
			
		||||
 | 
			
		||||
        this.status = this.openmct.status.get(this.series.domainObject.identifier);
 | 
			
		||||
        this.removeStatusListener = this.openmct.status.observe(this.series.domainObject.identifier, this.setStatus);
 | 
			
		||||
    },
 | 
			
		||||
    beforeDestroy() {
 | 
			
		||||
        if (this.removeStatusListener) {
 | 
			
		||||
            this.removeStatusListener();
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        initialize: function () {
 | 
			
		||||
            this.fields = [
 | 
			
		||||
                {
 | 
			
		||||
                    modelProp: 'yKey',
 | 
			
		||||
                    objectPath: this.dynamicPathForKey('yKey')
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    modelProp: 'interpolate',
 | 
			
		||||
                    objectPath: this.dynamicPathForKey('interpolate')
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    modelProp: 'markers',
 | 
			
		||||
                    objectPath: this.dynamicPathForKey('markers')
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    modelProp: 'markerShape',
 | 
			
		||||
                    objectPath: this.dynamicPathForKey('markerShape')
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    modelProp: 'markerSize',
 | 
			
		||||
                    coerce: Number,
 | 
			
		||||
                    objectPath: this.dynamicPathForKey('markerSize')
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    modelProp: 'alarmMarkers',
 | 
			
		||||
                    coerce: Boolean,
 | 
			
		||||
                    objectPath: this.dynamicPathForKey('alarmMarkers')
 | 
			
		||||
                }
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            const metadata = this.series.metadata;
 | 
			
		||||
            this.yKeyOptions = metadata
 | 
			
		||||
                .valuesForHints(['range'])
 | 
			
		||||
                .map(function (o) {
 | 
			
		||||
                    return {
 | 
			
		||||
                        name: o.key,
 | 
			
		||||
                        value: o.key
 | 
			
		||||
                    };
 | 
			
		||||
                });
 | 
			
		||||
            this.markerShapeOptions = Object.entries(MARKER_SHAPES)
 | 
			
		||||
                .map(([key, obj]) => {
 | 
			
		||||
                    return {
 | 
			
		||||
                        name: obj.label,
 | 
			
		||||
                        value: key
 | 
			
		||||
                    };
 | 
			
		||||
                });
 | 
			
		||||
        },
 | 
			
		||||
        dynamicPathForKey(key) {
 | 
			
		||||
            return function (object, model) {
 | 
			
		||||
                const modelIdentifier = model.get('identifier');
 | 
			
		||||
                const index = object.configuration.series.findIndex(s => {
 | 
			
		||||
                    return _.isEqual(s.identifier, modelIdentifier);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                return 'configuration.series[' + index + '].' + key;
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
       * Set the color for the current plot series.  If the new color was
 | 
			
		||||
       * already assigned to a different plot series, then swap the colors.
 | 
			
		||||
       */
 | 
			
		||||
        setColor: function (color) {
 | 
			
		||||
            const oldColor = this.series.get('color');
 | 
			
		||||
            const otherSeriesWithColor = this.series.collection.filter(function (s) {
 | 
			
		||||
                return s.get('color') === color;
 | 
			
		||||
            })[0];
 | 
			
		||||
 | 
			
		||||
            this.series.set('color', color);
 | 
			
		||||
 | 
			
		||||
            const getPath = this.dynamicPathForKey('color');
 | 
			
		||||
            const seriesColorPath = getPath(this.domainObject, this.series);
 | 
			
		||||
 | 
			
		||||
            this.openmct.objects.mutate(
 | 
			
		||||
                this.domainObject,
 | 
			
		||||
                seriesColorPath,
 | 
			
		||||
                color.asHexString()
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (otherSeriesWithColor) {
 | 
			
		||||
                otherSeriesWithColor.set('color', oldColor);
 | 
			
		||||
 | 
			
		||||
                const otherSeriesColorPath = getPath(
 | 
			
		||||
                    this.domainObject,
 | 
			
		||||
                    otherSeriesWithColor
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                this.openmct.objects.mutate(
 | 
			
		||||
                    this.domainObject,
 | 
			
		||||
                    otherSeriesColorPath,
 | 
			
		||||
                    oldColor.asHexString()
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        toggleExpanded() {
 | 
			
		||||
            this.expanded = !this.expanded;
 | 
			
		||||
        },
 | 
			
		||||
        updateForm(formKey) {
 | 
			
		||||
            const newVal = this[formKey];
 | 
			
		||||
            const oldVal = this.series.get(formKey);
 | 
			
		||||
            const formField = this.fields.find((field) => field.modelProp === formKey);
 | 
			
		||||
 | 
			
		||||
            const path = objectPath(formField.objectPath);
 | 
			
		||||
            const validationResult = validate(newVal, this.series, formField.validate);
 | 
			
		||||
            if (validationResult === true) {
 | 
			
		||||
                delete this.validation[formKey];
 | 
			
		||||
            } else {
 | 
			
		||||
                this.validation[formKey] = validationResult;
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!_.isEqual(coerce(newVal, formField.coerce), coerce(oldVal, formField.coerce))) {
 | 
			
		||||
                this.series.set(formKey, coerce(newVal, formField.coerce));
 | 
			
		||||
                if (path) {
 | 
			
		||||
                    this.openmct.objects.mutate(
 | 
			
		||||
                        this.domainObject,
 | 
			
		||||
                        path(this.domainObject, this.series),
 | 
			
		||||
                        coerce(newVal, formField.coerce)
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        setStatus(status) {
 | 
			
		||||
            this.status = status;
 | 
			
		||||
        },
 | 
			
		||||
        toggleSwatch() {
 | 
			
		||||
            this.swatchActive = !this.swatchActive;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										226
									
								
								src/plugins/plot/vue/inspector/forms/YAxisForm.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										226
									
								
								src/plugins/plot/vue/inspector/forms/YAxisForm.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,226 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
    <ul class="l-inspector-part">
 | 
			
		||||
        <h2>Y Axis</h2>
 | 
			
		||||
        <li class="grid-row">
 | 
			
		||||
            <div class="grid-cell label"
 | 
			
		||||
                 title="Manually override how the Y axis is labeled."
 | 
			
		||||
            >Label</div>
 | 
			
		||||
            <div class="grid-cell value"><input v-model="label"
 | 
			
		||||
                                                class="c-input--flex"
 | 
			
		||||
                                                type="text"
 | 
			
		||||
                                                @change="updateForm('label')"
 | 
			
		||||
            ></div>
 | 
			
		||||
        </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
    <ul class="l-inspector-part">
 | 
			
		||||
        <h2>Y Axis Scaling</h2>
 | 
			
		||||
        <li class="grid-row">
 | 
			
		||||
            <div class="grid-cell label"
 | 
			
		||||
                 title="Automatically scale the Y axis to keep all values in view."
 | 
			
		||||
            >Auto scale</div>
 | 
			
		||||
            <div class="grid-cell value"><input v-model="autoscale"
 | 
			
		||||
                                                type="checkbox"
 | 
			
		||||
                                                @change="updateForm('autoscale')"
 | 
			
		||||
            ></div>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li v-show="autoscale"
 | 
			
		||||
            class="grid-row"
 | 
			
		||||
        >
 | 
			
		||||
            <div class="grid-cell label"
 | 
			
		||||
                 title="Percentage of padding above and below plotted min and max values. 0.1, 1.0, etc."
 | 
			
		||||
            >
 | 
			
		||||
                Padding</div>
 | 
			
		||||
            <div class="grid-cell value">
 | 
			
		||||
                <input v-model="autoscalePadding"
 | 
			
		||||
                       class="c-input--flex"
 | 
			
		||||
                       type="text"
 | 
			
		||||
                       @change="updateForm('autoscalePadding')"
 | 
			
		||||
                >
 | 
			
		||||
            </div>
 | 
			
		||||
        </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
    <ul v-show="!autoscale"
 | 
			
		||||
        class="l-inspector-part"
 | 
			
		||||
    >
 | 
			
		||||
        <div v-show="!autoscale && validation.range"
 | 
			
		||||
             class="grid-span-all form-error"
 | 
			
		||||
        >
 | 
			
		||||
            {{ validation.range }}
 | 
			
		||||
        </div>
 | 
			
		||||
        <li class="grid-row force-border">
 | 
			
		||||
            <div class="grid-cell label"
 | 
			
		||||
                 title="Minimum Y axis value."
 | 
			
		||||
            >Minimum Value</div>
 | 
			
		||||
            <div class="grid-cell value">
 | 
			
		||||
                <input v-model="rangeMin"
 | 
			
		||||
                       class="c-input--flex"
 | 
			
		||||
                       type="number"
 | 
			
		||||
                       @change="updateForm('range')"
 | 
			
		||||
                >
 | 
			
		||||
            </div>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li class="grid-row">
 | 
			
		||||
            <div class="grid-cell label"
 | 
			
		||||
                 title="Maximum Y axis value."
 | 
			
		||||
            >Maximum Value</div>
 | 
			
		||||
            <div class="grid-cell value"><input v-model="rangeMax"
 | 
			
		||||
                                                class="c-input--flex"
 | 
			
		||||
                                                type="number"
 | 
			
		||||
                                                @change="updateForm('range')"
 | 
			
		||||
            ></div>
 | 
			
		||||
        </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { objectPath, validate, coerce } from "./formUtil";
 | 
			
		||||
import _ from "lodash";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    inject: ['openmct', 'domainObject'],
 | 
			
		||||
    props: {
 | 
			
		||||
        yAxis: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
            default() {
 | 
			
		||||
                return {};
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            label: '',
 | 
			
		||||
            autoscale: '',
 | 
			
		||||
            autoscalePadding: '',
 | 
			
		||||
            rangeMin: '',
 | 
			
		||||
            rangeMax: '',
 | 
			
		||||
            validation: {}
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        this.initialize();
 | 
			
		||||
        this.initFormValues();
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        initialize: function () {
 | 
			
		||||
            this.fields = [
 | 
			
		||||
                {
 | 
			
		||||
                    modelProp: 'label',
 | 
			
		||||
                    objectPath: 'configuration.yAxis.label'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    modelProp: 'autoscale',
 | 
			
		||||
                    coerce: Boolean,
 | 
			
		||||
                    objectPath: 'configuration.yAxis.autoscale'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    modelProp: 'autoscalePadding',
 | 
			
		||||
                    coerce: Number,
 | 
			
		||||
                    objectPath: 'configuration.yAxis.autoscalePadding'
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    modelProp: 'range',
 | 
			
		||||
                    objectPath: 'configuration.yAxis.range',
 | 
			
		||||
                    coerce: function coerceRange(range) {
 | 
			
		||||
                        if (!range) {
 | 
			
		||||
                            return {
 | 
			
		||||
                                min: 0,
 | 
			
		||||
                                max: 0
 | 
			
		||||
                            };
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        const newRange = {};
 | 
			
		||||
                        if (typeof range.min !== 'undefined' && range.min !== null) {
 | 
			
		||||
                            newRange.min = Number(range.min);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (typeof range.max !== 'undefined' && range.max !== null) {
 | 
			
		||||
                            newRange.max = Number(range.max);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        return newRange;
 | 
			
		||||
                    },
 | 
			
		||||
                    validate: function validateRange(range, model) {
 | 
			
		||||
                        if (!range) {
 | 
			
		||||
                            return 'Need range';
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (range.min === '' || range.min === null || typeof range.min === 'undefined') {
 | 
			
		||||
                            return 'Must specify Minimum';
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (range.max === '' || range.max === null || typeof range.max === 'undefined') {
 | 
			
		||||
                            return 'Must specify Maximum';
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (Number.isNaN(Number(range.min))) {
 | 
			
		||||
                            return 'Minimum must be a number.';
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (Number.isNaN(Number(range.max))) {
 | 
			
		||||
                            return 'Maximum must be a number.';
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (Number(range.min) > Number(range.max)) {
 | 
			
		||||
                            return 'Minimum must be less than Maximum.';
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (model.get('autoscale')) {
 | 
			
		||||
                            return false;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        return true;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            ];
 | 
			
		||||
        },
 | 
			
		||||
        initFormValues() {
 | 
			
		||||
            this.label = this.yAxis.get('label');
 | 
			
		||||
            this.autoscale = this.yAxis.get('autoscale');
 | 
			
		||||
            this.autoscalePadding = this.yAxis.get('autoscalePadding');
 | 
			
		||||
            const range = this.yAxis.get('range');
 | 
			
		||||
            if (range) {
 | 
			
		||||
                this.rangeMin = range.min;
 | 
			
		||||
                this.rangeMax = range.max;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        updateForm(formKey) {
 | 
			
		||||
            let newVal;
 | 
			
		||||
            if (formKey === 'range') {
 | 
			
		||||
                newVal = {
 | 
			
		||||
                    min: this.rangeMin,
 | 
			
		||||
                    max: this.rangeMax
 | 
			
		||||
                };
 | 
			
		||||
            } else {
 | 
			
		||||
                newVal = this[formKey];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const oldVal = this.yAxis.get(formKey);
 | 
			
		||||
            const formField = this.fields.find((field) => field.modelProp === formKey);
 | 
			
		||||
 | 
			
		||||
            const path = objectPath(formField.objectPath);
 | 
			
		||||
            const validationResult = validate(newVal, this.yAxis, formField.validate);
 | 
			
		||||
            if (validationResult === true) {
 | 
			
		||||
                delete this.validation[formKey];
 | 
			
		||||
            } else {
 | 
			
		||||
                this.validation[formKey] = validationResult;
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!_.isEqual(coerce(newVal, formField.coerce), coerce(oldVal, formField.coerce))) {
 | 
			
		||||
                this.yAxis.set(formKey, coerce(newVal, formField.coerce));
 | 
			
		||||
                if (path) {
 | 
			
		||||
                    this.openmct.objects.mutate(
 | 
			
		||||
                        this.domainObject,
 | 
			
		||||
                        path(this.domainObject, this.yAxis),
 | 
			
		||||
                        coerce(newVal, formField.coerce)
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										29
									
								
								src/plugins/plot/vue/inspector/forms/formUtil.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/plugins/plot/vue/inspector/forms/formUtil.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
export function coerce(value, coerceFunc) {
 | 
			
		||||
    if (coerceFunc) {
 | 
			
		||||
        return coerceFunc(value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function validate(value, model, validateFunc) {
 | 
			
		||||
    if (validateFunc) {
 | 
			
		||||
        return validateFunc(value, model);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function objectPath(path) {
 | 
			
		||||
    if (path) {
 | 
			
		||||
        if (typeof path !== "function") {
 | 
			
		||||
            const staticObjectPath = path;
 | 
			
		||||
 | 
			
		||||
            return function (object, model) {
 | 
			
		||||
                return staticObjectPath;
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return path;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -45,9 +45,14 @@
 | 
			
		||||
 | 
			
		||||
            <div class="gl-plot-display-area has-local-controls has-cursor-guides">
 | 
			
		||||
                <div class="l-state-indicators">
 | 
			
		||||
                    <span class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle"
 | 
			
		||||
                    <span v-if="plotHistory.length"
 | 
			
		||||
                          class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle"
 | 
			
		||||
                          title="This plot is not currently displaying the latest data. Reset pan/zoom to view latest data."
 | 
			
		||||
                    ></span>
 | 
			
		||||
                    <span v-else
 | 
			
		||||
                          class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle"
 | 
			
		||||
                          title="This plot is not currently displaying the latest data. Resume to view latest data."
 | 
			
		||||
                    ></span>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <mct-ticks v-show="gridLines && !options.compact"
 | 
			
		||||
@@ -85,8 +90,8 @@
 | 
			
		||||
                        >
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="c-button-set c-button-set--strip-h"
 | 
			
		||||
                         :disabled="!plotHistory.length"
 | 
			
		||||
                    <div v-if="plotHistory.length"
 | 
			
		||||
                         class="c-button-set c-button-set--strip-h"
 | 
			
		||||
                    >
 | 
			
		||||
                        <button class="c-button icon-arrow-left"
 | 
			
		||||
                                title="Restore previous pan/zoom"
 | 
			
		||||
@@ -99,6 +104,22 @@
 | 
			
		||||
                        >
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div v-if="isRealTime"
 | 
			
		||||
                         class="c-button-set c-button-set--strip-h"
 | 
			
		||||
                    >
 | 
			
		||||
                        <button v-if="!isFrozen"
 | 
			
		||||
                                class="c-button icon-pause"
 | 
			
		||||
                                title="Pause incoming real-time data"
 | 
			
		||||
                                @click="pause()"
 | 
			
		||||
                        >
 | 
			
		||||
                        </button>
 | 
			
		||||
                        <button v-if="isFrozen"
 | 
			
		||||
                                class="c-button icon-arrow-right pause-play is-paused"
 | 
			
		||||
                                title="Resume displaying real-time data"
 | 
			
		||||
                                @click="play()"
 | 
			
		||||
                        >
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <!--Cursor guides-->
 | 
			
		||||
@@ -186,10 +207,14 @@ export default {
 | 
			
		||||
            xKeyOptions: [],
 | 
			
		||||
            config: {},
 | 
			
		||||
            pending: 0,
 | 
			
		||||
            isRealTime: this.openmct.time.clock() !== undefined,
 | 
			
		||||
            loaded: false
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
        isFrozen() {
 | 
			
		||||
            return this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
 | 
			
		||||
        },
 | 
			
		||||
        plotLegendPositionClass() {
 | 
			
		||||
            return `plot-legend-${this.config.legend.get('position')}`;
 | 
			
		||||
        },
 | 
			
		||||
@@ -227,6 +252,7 @@ export default {
 | 
			
		||||
            'configuration.filters',
 | 
			
		||||
            this.updateFiltersAndResubscribe
 | 
			
		||||
        );
 | 
			
		||||
        this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.updateStatus);
 | 
			
		||||
 | 
			
		||||
        this.openmct.objectViews.on('clearData', this.clearData);
 | 
			
		||||
        this.followTimeConductor();
 | 
			
		||||
@@ -243,6 +269,7 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        followTimeConductor() {
 | 
			
		||||
            this.openmct.time.on('clock', this.updateRealTime);
 | 
			
		||||
            this.openmct.time.on('bounds', this.updateDisplayBounds);
 | 
			
		||||
            this.synchronized(true);
 | 
			
		||||
        },
 | 
			
		||||
@@ -371,6 +398,9 @@ export default {
 | 
			
		||||
            const displayRange = series.getDisplayRange(xKey);
 | 
			
		||||
            this.config.xAxis.set('range', displayRange);
 | 
			
		||||
        },
 | 
			
		||||
        updateRealTime(clock) {
 | 
			
		||||
            this.isRealTime = clock !== undefined;
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
       * Track latest display bounds.  Forces update when not receiving ticks.
 | 
			
		||||
@@ -424,19 +454,28 @@ export default {
 | 
			
		||||
       * displays can update accordingly.
 | 
			
		||||
       */
 | 
			
		||||
        synchronized(value) {
 | 
			
		||||
            const isLocalClock = this.openmct.time.clock();
 | 
			
		||||
 | 
			
		||||
            if (typeof value !== 'undefined') {
 | 
			
		||||
                this._synchronized = value;
 | 
			
		||||
                const isUnsynced = !value && this.openmct.time.clock();
 | 
			
		||||
                const domainObject = this.openmct.legacyObject(this.domainObject);
 | 
			
		||||
                if (domainObject.getCapability('status')) {
 | 
			
		||||
                    domainObject.getCapability('status')
 | 
			
		||||
                        .set('timeconductor-unsynced', isUnsynced);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const isUnsynced = isLocalClock && !value;
 | 
			
		||||
                this.setStatus(isUnsynced);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return this._synchronized;
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        setStatus(isNotInSync) {
 | 
			
		||||
            const outOfSync = isNotInSync === true
 | 
			
		||||
                || this.isFrozen === true;
 | 
			
		||||
            if (outOfSync === true) {
 | 
			
		||||
                this.openmct.status.set(this.domainObject.identifier, 'timeconductor-unsynced');
 | 
			
		||||
            } else {
 | 
			
		||||
                this.openmct.status.set(this.domainObject.identifier, '');
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        initCanvas() {
 | 
			
		||||
            if (this.canvas) {
 | 
			
		||||
                this.stopListening(this.canvas);
 | 
			
		||||
@@ -729,7 +768,8 @@ export default {
 | 
			
		||||
            const ZOOM_AMT = 0.1;
 | 
			
		||||
            event.preventDefault();
 | 
			
		||||
 | 
			
		||||
            if (!this.positionOverPlot) {
 | 
			
		||||
            if (event.wheelDelta === undefined
 | 
			
		||||
                || !this.positionOverPlot) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -847,11 +887,13 @@ export default {
 | 
			
		||||
        freeze() {
 | 
			
		||||
            this.config.yAxis.set('frozen', true);
 | 
			
		||||
            this.config.xAxis.set('frozen', true);
 | 
			
		||||
            this.setStatus();
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        clear() {
 | 
			
		||||
            this.config.yAxis.set('frozen', false);
 | 
			
		||||
            this.config.xAxis.set('frozen', false);
 | 
			
		||||
            this.setStatus();
 | 
			
		||||
            this.plotHistory = [];
 | 
			
		||||
            this.userViewportChangeEnd();
 | 
			
		||||
        },
 | 
			
		||||
@@ -881,6 +923,14 @@ export default {
 | 
			
		||||
            this.config.series.models[0].emit('change:yKey', yKey);
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        pause() {
 | 
			
		||||
            this.freeze();
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        play() {
 | 
			
		||||
            this.clear();
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        destroy() {
 | 
			
		||||
            configStore.deleteStore(this.config.id);
 | 
			
		||||
 | 
			
		||||
@@ -894,8 +944,16 @@ export default {
 | 
			
		||||
                this.filterObserver();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.removeStatusListener) {
 | 
			
		||||
                this.removeStatusListener();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.openmct.time.off('clock', this.updateRealTime);
 | 
			
		||||
            this.openmct.time.off('bounds', this.updateDisplayBounds);
 | 
			
		||||
            this.openmct.objectViews.off('clearData', this.clearData);
 | 
			
		||||
        },
 | 
			
		||||
        updateStatus(status) {
 | 
			
		||||
            this.$emit('statusUpdated', status);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -56,6 +56,7 @@
 | 
			
		||||
 | 
			
		||||
    <div ref="plotContainer"
 | 
			
		||||
         class="l-view-section u-style-receiver js-style-receiver"
 | 
			
		||||
         :class="{'s-status-timeconductor-unsynced': status && status === 'timeconductor-unsynced'}"
 | 
			
		||||
    >
 | 
			
		||||
        <div v-show="!!loading"
 | 
			
		||||
             class="c-loading--overlay loading"
 | 
			
		||||
@@ -64,6 +65,7 @@
 | 
			
		||||
                  :cursor-guide="cursorGuide"
 | 
			
		||||
                  :options="options"
 | 
			
		||||
                  @loadingUpdated="loadingUpdated"
 | 
			
		||||
                  @statusUpdated="setStatus"
 | 
			
		||||
        />
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -94,7 +96,8 @@ export default {
 | 
			
		||||
            // hideExportButtons: false
 | 
			
		||||
            cursorGuide: false,
 | 
			
		||||
            gridLines: !this.options.compact,
 | 
			
		||||
            loading: false
 | 
			
		||||
            loading: false,
 | 
			
		||||
            status: ''
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
@@ -131,6 +134,9 @@ export default {
 | 
			
		||||
 | 
			
		||||
        toggleGridLines() {
 | 
			
		||||
            this.gridLines = !this.gridLines;
 | 
			
		||||
        },
 | 
			
		||||
        setStatus(status) {
 | 
			
		||||
            this.status = status;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -23,12 +23,14 @@
 | 
			
		||||
import PlotViewProvider from './PlotViewProvider';
 | 
			
		||||
import OverlayPlotViewProvider from '../overlayPlot/OverlayPlotViewProvider';
 | 
			
		||||
import StackedPlotViewProvider from '../stackedPlot/StackedPlotViewProvider';
 | 
			
		||||
import PlotsInspectorViewProvider from '../inspector/PlotsInspectorViewProvider';
 | 
			
		||||
 | 
			
		||||
export default function () {
 | 
			
		||||
    return function install(openmct) {
 | 
			
		||||
        openmct.objectViews.addProvider(new StackedPlotViewProvider(openmct));
 | 
			
		||||
        openmct.objectViews.addProvider(new OverlayPlotViewProvider(openmct));
 | 
			
		||||
        openmct.objectViews.addProvider(new PlotViewProvider(openmct));
 | 
			
		||||
        openmct.inspectorViews.addProvider(new PlotsInspectorViewProvider(openmct));
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ import Vue from "vue";
 | 
			
		||||
import StackedPlot from "../stackedPlot/StackedPlot.vue";
 | 
			
		||||
import configStore from "@/plugins/plot/vue/single/configuration/configStore";
 | 
			
		||||
import EventEmitter from "EventEmitter";
 | 
			
		||||
import PlotOptions from "../inspector/PlotOptions.vue";
 | 
			
		||||
import PlotConfigurationModel from "@/plugins/plot/vue/single/configuration/PlotConfigurationModel";
 | 
			
		||||
 | 
			
		||||
describe("the plugin", function () {
 | 
			
		||||
    let element;
 | 
			
		||||
@@ -174,6 +176,35 @@ describe("the plugin", function () {
 | 
			
		||||
            expect(plotView).toBeDefined();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('provides an inspector view for overlay plots', () => {
 | 
			
		||||
            let selection = [
 | 
			
		||||
                [
 | 
			
		||||
                    {
 | 
			
		||||
                        context: {
 | 
			
		||||
                            item: {
 | 
			
		||||
                                id: "test-object",
 | 
			
		||||
                                type: "telemetry.plot.overlay",
 | 
			
		||||
                                telemetry: {
 | 
			
		||||
                                    values: [{
 | 
			
		||||
                                        key: "some-key"
 | 
			
		||||
                                    }]
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        context: {
 | 
			
		||||
                            item: {
 | 
			
		||||
                                type: 'time-strip'
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
            ];
 | 
			
		||||
            const plotInspectorView = openmct.inspectorViews.get(selection);
 | 
			
		||||
            expect(plotInspectorView.length).toEqual(1);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("provides a stacked plot view for objects with telemetry", () => {
 | 
			
		||||
            const testTelemetryObject = {
 | 
			
		||||
                id: "test-object",
 | 
			
		||||
@@ -282,6 +313,46 @@ describe("the plugin", function () {
 | 
			
		||||
            expect(options[0].value).toBe("Some attribute");
 | 
			
		||||
            expect(options[1].value).toBe("Another attribute");
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('hides the pause and play controls', () => {
 | 
			
		||||
            let pauseEl = element.querySelectorAll(".c-button-set .icon-pause");
 | 
			
		||||
            let playEl = element.querySelectorAll(".c-button-set .icon-arrow-right");
 | 
			
		||||
            expect(pauseEl.length).toBe(0);
 | 
			
		||||
            expect(playEl.length).toBe(0);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        describe('pause and play controls', () => {
 | 
			
		||||
            beforeEach(() => {
 | 
			
		||||
                openmct.time.clock('local', {
 | 
			
		||||
                    start: -1000,
 | 
			
		||||
                    end: 100
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                return Vue.nextTick();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it('shows the pause controls', (done) => {
 | 
			
		||||
                Vue.nextTick(() => {
 | 
			
		||||
                    let pauseEl = element.querySelectorAll(".c-button-set .icon-pause");
 | 
			
		||||
                    expect(pauseEl.length).toBe(1);
 | 
			
		||||
                    done();
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it('shows the play control if plot is paused', (done) => {
 | 
			
		||||
                let pauseEl = element.querySelector(".c-button-set .icon-pause");
 | 
			
		||||
                const clickEvent = createMouseEvent("click");
 | 
			
		||||
 | 
			
		||||
                pauseEl.dispatchEvent(clickEvent);
 | 
			
		||||
                Vue.nextTick(() => {
 | 
			
		||||
                    let playEl = element.querySelectorAll(".c-button-set .is-paused");
 | 
			
		||||
                    expect(playEl.length).toBe(1);
 | 
			
		||||
                    done();
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("The stacked plot view", () => {
 | 
			
		||||
@@ -578,4 +649,218 @@ describe("the plugin", function () {
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('the inspector view', () => {
 | 
			
		||||
        let component;
 | 
			
		||||
        let viewComponentObject;
 | 
			
		||||
        let mockComposition;
 | 
			
		||||
        let testTelemetryObject;
 | 
			
		||||
        let selection;
 | 
			
		||||
        let config;
 | 
			
		||||
        beforeEach((done) => {
 | 
			
		||||
            testTelemetryObject = {
 | 
			
		||||
                identifier: {
 | 
			
		||||
                    namespace: "",
 | 
			
		||||
                    key: "test-object"
 | 
			
		||||
                },
 | 
			
		||||
                type: "test-object",
 | 
			
		||||
                name: "Test Object",
 | 
			
		||||
                telemetry: {
 | 
			
		||||
                    values: [{
 | 
			
		||||
                        key: "utc",
 | 
			
		||||
                        format: "utc",
 | 
			
		||||
                        name: "Time",
 | 
			
		||||
                        hints: {
 | 
			
		||||
                            domain: 1
 | 
			
		||||
                        }
 | 
			
		||||
                    }, {
 | 
			
		||||
                        key: "some-key",
 | 
			
		||||
                        name: "Some attribute",
 | 
			
		||||
                        hints: {
 | 
			
		||||
                            range: 1
 | 
			
		||||
                        }
 | 
			
		||||
                    }, {
 | 
			
		||||
                        key: "some-other-key",
 | 
			
		||||
                        name: "Another attribute",
 | 
			
		||||
                        hints: {
 | 
			
		||||
                            range: 2
 | 
			
		||||
                        }
 | 
			
		||||
                    }]
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            selection = [
 | 
			
		||||
                [
 | 
			
		||||
                    {
 | 
			
		||||
                        context: {
 | 
			
		||||
                            item: {
 | 
			
		||||
                                id: "test-object",
 | 
			
		||||
                                identifier: {
 | 
			
		||||
                                    key: "test-object",
 | 
			
		||||
                                    namespace: ''
 | 
			
		||||
                                },
 | 
			
		||||
                                type: "telemetry.plot.overlay",
 | 
			
		||||
                                configuration: {
 | 
			
		||||
                                    series: [
 | 
			
		||||
                                        {
 | 
			
		||||
                                            identifier: {
 | 
			
		||||
                                                key: "test-object",
 | 
			
		||||
                                                namespace: ''
 | 
			
		||||
                                            }
 | 
			
		||||
                                        }
 | 
			
		||||
                                    ]
 | 
			
		||||
                                },
 | 
			
		||||
                                composition: []
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        context: {
 | 
			
		||||
                            item: {
 | 
			
		||||
                                type: 'time-strip',
 | 
			
		||||
                                identifier: {
 | 
			
		||||
                                    key: 'some-other-key',
 | 
			
		||||
                                    namespace: ''
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            mockComposition = new EventEmitter();
 | 
			
		||||
            mockComposition.load = () => {
 | 
			
		||||
                mockComposition.emit('add', testTelemetryObject);
 | 
			
		||||
 | 
			
		||||
                return [testTelemetryObject];
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
 | 
			
		||||
 | 
			
		||||
            const configId = openmct.objects.makeKeyString(selection[0][0].context.item.identifier);
 | 
			
		||||
            config = new PlotConfigurationModel({
 | 
			
		||||
                id: configId,
 | 
			
		||||
                domainObject: selection[0][0].context.item,
 | 
			
		||||
                openmct: openmct
 | 
			
		||||
            });
 | 
			
		||||
            configStore.add(configId, config);
 | 
			
		||||
 | 
			
		||||
            let viewContainer = document.createElement('div');
 | 
			
		||||
            child.append(viewContainer);
 | 
			
		||||
            component = new Vue({
 | 
			
		||||
                el: viewContainer,
 | 
			
		||||
                components: {
 | 
			
		||||
                    PlotOptions
 | 
			
		||||
                },
 | 
			
		||||
                provide: {
 | 
			
		||||
                    openmct: openmct,
 | 
			
		||||
                    domainObject: selection[0][0].context.item,
 | 
			
		||||
                    path: [selection[0][0].context.item, selection[0][1].context.item]
 | 
			
		||||
                },
 | 
			
		||||
                template: '<plot-options/>'
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            Vue.nextTick(() => {
 | 
			
		||||
                viewComponentObject = component.$root.$children[0];
 | 
			
		||||
                done();
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        describe('in view only mode', () => {
 | 
			
		||||
            let browseOptionsEl;
 | 
			
		||||
            let editOptionsEl;
 | 
			
		||||
            beforeEach(() => {
 | 
			
		||||
                browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse');
 | 
			
		||||
                editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit');
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it('does not show the edit options', () => {
 | 
			
		||||
                expect(editOptionsEl).toBeNull();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it('shows the name', () => {
 | 
			
		||||
                const seriesEl = browseOptionsEl.querySelector('.c-object-label__name');
 | 
			
		||||
                expect(seriesEl.innerHTML).toEqual(testTelemetryObject.name);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it('shows in collapsed mode', () => {
 | 
			
		||||
                const seriesEl = browseOptionsEl.querySelectorAll('.c-disclosure-triangle--expanded');
 | 
			
		||||
                expect(seriesEl.length).toEqual(0);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it('shows in expanded mode', () => {
 | 
			
		||||
                let expandControl = browseOptionsEl.querySelector(".c-disclosure-triangle");
 | 
			
		||||
                const clickEvent = createMouseEvent("click");
 | 
			
		||||
                expandControl.dispatchEvent(clickEvent);
 | 
			
		||||
 | 
			
		||||
                const plotOptionsProperties = browseOptionsEl.querySelectorAll('.js-plot-options-browse-properties .grid-row');
 | 
			
		||||
                expect(plotOptionsProperties.length).toEqual(5);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        describe('in edit mode', () => {
 | 
			
		||||
            let editOptionsEl;
 | 
			
		||||
            let browseOptionsEl;
 | 
			
		||||
 | 
			
		||||
            beforeEach((done) => {
 | 
			
		||||
                viewComponentObject.setEditState(true);
 | 
			
		||||
                Vue.nextTick(() => {
 | 
			
		||||
                    editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit');
 | 
			
		||||
                    browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse');
 | 
			
		||||
                    done();
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it('does not show the browse options', () => {
 | 
			
		||||
                expect(browseOptionsEl).toBeNull();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it('shows the name', () => {
 | 
			
		||||
                const seriesEl = editOptionsEl.querySelector('.c-object-label__name');
 | 
			
		||||
                expect(seriesEl.innerHTML).toEqual(testTelemetryObject.name);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it('shows in collapsed mode', () => {
 | 
			
		||||
                const seriesEl = editOptionsEl.querySelectorAll('.c-disclosure-triangle--expanded');
 | 
			
		||||
                expect(seriesEl.length).toEqual(0);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it('shows in collapsed mode', () => {
 | 
			
		||||
                const seriesEl = editOptionsEl.querySelectorAll('.c-disclosure-triangle--expanded');
 | 
			
		||||
                expect(seriesEl.length).toEqual(0);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it('renders expanded', () => {
 | 
			
		||||
                const expandControl = editOptionsEl.querySelector(".c-disclosure-triangle");
 | 
			
		||||
                const clickEvent = createMouseEvent("click");
 | 
			
		||||
                expandControl.dispatchEvent(clickEvent);
 | 
			
		||||
 | 
			
		||||
                const plotOptionsProperties = editOptionsEl.querySelectorAll(".js-plot-options-edit-properties .grid-row");
 | 
			
		||||
                expect(plotOptionsProperties.length).toEqual(6);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it('shows yKeyOptions', () => {
 | 
			
		||||
                const expandControl = editOptionsEl.querySelector(".c-disclosure-triangle");
 | 
			
		||||
                const clickEvent = createMouseEvent("click");
 | 
			
		||||
                expandControl.dispatchEvent(clickEvent);
 | 
			
		||||
 | 
			
		||||
                const plotOptionsProperties = editOptionsEl.querySelectorAll(".js-plot-options-edit-properties .grid-row");
 | 
			
		||||
 | 
			
		||||
                const yKeySelection = plotOptionsProperties[0].querySelector('select');
 | 
			
		||||
                const options = Array.from(yKeySelection.options).map((option) => {
 | 
			
		||||
                    return option.value;
 | 
			
		||||
                });
 | 
			
		||||
                expect(options).toEqual([testTelemetryObject.telemetry.values[1].key, testTelemetryObject.telemetry.values[2].key]);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it('shows yAxis options', () => {
 | 
			
		||||
                const expandControl = editOptionsEl.querySelector(".c-disclosure-triangle");
 | 
			
		||||
                const clickEvent = createMouseEvent("click");
 | 
			
		||||
                expandControl.dispatchEvent(clickEvent);
 | 
			
		||||
 | 
			
		||||
                const yAxisProperties = editOptionsEl.querySelectorAll("div.grid-properties:first-of-type .l-inspector-part");
 | 
			
		||||
                expect(yAxisProperties.length).toEqual(3);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -90,6 +90,7 @@ export default {
 | 
			
		||||
 | 
			
		||||
            const onTickWidthChange = this.onTickWidthChange;
 | 
			
		||||
            const loadingUpdated = this.loadingUpdated;
 | 
			
		||||
            const setStatus = this.setStatus;
 | 
			
		||||
 | 
			
		||||
            const openmct = this.openmct;
 | 
			
		||||
            const object = this.object;
 | 
			
		||||
@@ -111,17 +112,23 @@ export default {
 | 
			
		||||
                    return {
 | 
			
		||||
                        ...getProps(),
 | 
			
		||||
                        onTickWidthChange,
 | 
			
		||||
                        loadingUpdated
 | 
			
		||||
                        loadingUpdated,
 | 
			
		||||
                        setStatus
 | 
			
		||||
                    };
 | 
			
		||||
                },
 | 
			
		||||
                template: '<div ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver"><div v-show="!!loading" class="c-loading--overlay loading"></div><mct-plot :grid-lines="gridLines" :cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" :options="options" @plotTickWidth="onTickWidthChange" @loadingUpdated="loadingUpdated"/></div>'
 | 
			
		||||
                template: '<div ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver" :data-status="status" :class="{\'s-status-timeconductor-unsynced\': status && status === \'timeconductor-unsynced\'}"><div v-show="!!loading" class="c-loading--overlay loading"></div><mct-plot :grid-lines="gridLines" :cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" :options="options" @plotTickWidth="onTickWidthChange" @statusUpdated="setStatus" @loadingUpdated="loadingUpdated"/></div>'
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        onTickWidthChange() {
 | 
			
		||||
            this.$emit('plotTickWidth', ...arguments);
 | 
			
		||||
        },
 | 
			
		||||
        setStatus(status) {
 | 
			
		||||
            this.status = status;
 | 
			
		||||
            this.updateComponentProp('status', status);
 | 
			
		||||
        },
 | 
			
		||||
        loadingUpdated(loaded) {
 | 
			
		||||
            this.loading = loaded;
 | 
			
		||||
            this.updateComponentProp('loading', loaded);
 | 
			
		||||
        },
 | 
			
		||||
        getProps() {
 | 
			
		||||
            return {
 | 
			
		||||
@@ -129,7 +136,8 @@ export default {
 | 
			
		||||
                cursorGuide: this.cursorGuide,
 | 
			
		||||
                plotTickWidth: this.plotTickWidth,
 | 
			
		||||
                loading: this.loading,
 | 
			
		||||
                options: this.options
 | 
			
		||||
                options: this.options,
 | 
			
		||||
                status: this.status
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -94,6 +94,7 @@ define([
 | 
			
		||||
        initialize() {
 | 
			
		||||
            if (this.domainObject.type === 'table') {
 | 
			
		||||
                this.filterObserver = this.openmct.objects.observe(this.domainObject, 'configuration.filters', this.updateFilters);
 | 
			
		||||
                this.filters = this.domainObject.configuration.filters;
 | 
			
		||||
                this.loadComposition();
 | 
			
		||||
            } else {
 | 
			
		||||
                this.addTelemetryObject(this.domainObject);
 | 
			
		||||
@@ -138,7 +139,18 @@ define([
 | 
			
		||||
            this.emit('object-added', telemetryObject);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        updateFilters() {
 | 
			
		||||
        updateFilters(updatedFilters) {
 | 
			
		||||
            let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters));
 | 
			
		||||
 | 
			
		||||
            if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) {
 | 
			
		||||
                this.filters = deepCopiedFilters;
 | 
			
		||||
                this.clearAndResubscribe();
 | 
			
		||||
            } else {
 | 
			
		||||
                this.filters = deepCopiedFilters;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        clearAndResubscribe() {
 | 
			
		||||
            this.filteredRows.clear();
 | 
			
		||||
            this.boundedRows.clear();
 | 
			
		||||
            Object.keys(this.subscriptions).forEach(this.unsubscribe, this);
 | 
			
		||||
 
 | 
			
		||||
@@ -100,6 +100,9 @@ define([
 | 
			
		||||
                    destroy: function (element) {
 | 
			
		||||
                        component.$destroy();
 | 
			
		||||
                        component = undefined;
 | 
			
		||||
                    },
 | 
			
		||||
                    _getTable: function () {
 | 
			
		||||
                        return table;
 | 
			
		||||
                    }
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -46,6 +46,7 @@ define(
 | 
			
		||||
                filter = filter.trim().toLowerCase();
 | 
			
		||||
 | 
			
		||||
                let rowsToFilter = this.getRowsToFilter(columnKey, filter);
 | 
			
		||||
 | 
			
		||||
                if (filter.length === 0) {
 | 
			
		||||
                    delete this.columnFilters[columnKey];
 | 
			
		||||
                } else {
 | 
			
		||||
@@ -56,6 +57,16 @@ define(
 | 
			
		||||
                this.emit('filter');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            setColumnRegexFilter(columnKey, filter) {
 | 
			
		||||
                filter = filter.trim();
 | 
			
		||||
 | 
			
		||||
                let rowsToFilter = this.masterCollection.getRows();
 | 
			
		||||
 | 
			
		||||
                this.columnFilters[columnKey] = new RegExp(filter);
 | 
			
		||||
                this.rows = rowsToFilter.filter(this.matchesFilters, this);
 | 
			
		||||
                this.emit('filter');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            /**
 | 
			
		||||
             * @private
 | 
			
		||||
             */
 | 
			
		||||
@@ -71,6 +82,10 @@ define(
 | 
			
		||||
             * @private
 | 
			
		||||
             */
 | 
			
		||||
            isSubsetOfCurrentFilter(columnKey, filter) {
 | 
			
		||||
                if (this.columnFilters[columnKey] instanceof RegExp) {
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return this.columnFilters[columnKey]
 | 
			
		||||
                    && filter.startsWith(this.columnFilters[columnKey])
 | 
			
		||||
                    // startsWith check will otherwise fail when filter cleared
 | 
			
		||||
@@ -97,7 +112,11 @@ define(
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1;
 | 
			
		||||
                    if (this.columnFilters[key] instanceof RegExp) {
 | 
			
		||||
                        doesMatchFilters = this.columnFilters[key].test(formattedValue);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1;
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                return doesMatchFilters;
 | 
			
		||||
 
 | 
			
		||||
@@ -188,7 +188,17 @@
 | 
			
		||||
                                class="c-table__search"
 | 
			
		||||
                                @input="filterChanged(key)"
 | 
			
		||||
                                @clear="clearFilter(key)"
 | 
			
		||||
                            />
 | 
			
		||||
                            >
 | 
			
		||||
 | 
			
		||||
                                <button
 | 
			
		||||
                                    class="c-search__use-regex"
 | 
			
		||||
                                    :class="{ 'is-active': enableRegexSearch[key] }"
 | 
			
		||||
                                    title="Click to enable regex: enter a string with slashes, like this: /regex_exp/"
 | 
			
		||||
                                    @click="toggleRegex(key)"
 | 
			
		||||
                                >
 | 
			
		||||
                                    /R/
 | 
			
		||||
                                </button>
 | 
			
		||||
                            </search>
 | 
			
		||||
                        </table-column-header>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                </thead>
 | 
			
		||||
@@ -361,6 +371,7 @@ export default {
 | 
			
		||||
            paused: false,
 | 
			
		||||
            markedRows: [],
 | 
			
		||||
            isShowingMarkedRowsOnly: false,
 | 
			
		||||
            enableRegexSearch: {},
 | 
			
		||||
            hideHeaders: configuration.hideHeaders,
 | 
			
		||||
            totalNumberOfRows: 0
 | 
			
		||||
        };
 | 
			
		||||
@@ -618,7 +629,16 @@ export default {
 | 
			
		||||
            this.headersHolderEl.scrollLeft = this.scrollable.scrollLeft;
 | 
			
		||||
        },
 | 
			
		||||
        filterChanged(columnKey) {
 | 
			
		||||
            this.table.filteredRows.setColumnFilter(columnKey, this.filters[columnKey]);
 | 
			
		||||
            if (this.enableRegexSearch[columnKey]) {
 | 
			
		||||
                if (this.isCompleteRegex(this.filters[columnKey])) {
 | 
			
		||||
                    this.table.filteredRows.setColumnRegexFilter(columnKey, this.filters[columnKey].slice(1, -1));
 | 
			
		||||
                } else {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                this.table.filteredRows.setColumnFilter(columnKey, this.filters[columnKey]);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.setHeight();
 | 
			
		||||
        },
 | 
			
		||||
        clearFilter(columnKey) {
 | 
			
		||||
@@ -956,6 +976,18 @@ export default {
 | 
			
		||||
 | 
			
		||||
            this.$nextTick().then(this.calculateColumnWidths);
 | 
			
		||||
        },
 | 
			
		||||
        toggleRegex(key) {
 | 
			
		||||
            this.$set(this.filters, key, '');
 | 
			
		||||
 | 
			
		||||
            if (this.enableRegexSearch[key] === undefined) {
 | 
			
		||||
                this.$set(this.enableRegexSearch, key, true);
 | 
			
		||||
            } else {
 | 
			
		||||
                this.$set(this.enableRegexSearch, key, !this.enableRegexSearch[key]);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        isCompleteRegex(string) {
 | 
			
		||||
            return (string.length > 2 && string[0] === '/' && string[string.length - 1] === '/');
 | 
			
		||||
        },
 | 
			
		||||
        getViewContext() {
 | 
			
		||||
            return {
 | 
			
		||||
                type: 'telemetry-table',
 | 
			
		||||
 
 | 
			
		||||
@@ -113,6 +113,7 @@ describe("the plugin", () => {
 | 
			
		||||
        let applicableViews;
 | 
			
		||||
        let tableViewProvider;
 | 
			
		||||
        let tableView;
 | 
			
		||||
        let tableInstance;
 | 
			
		||||
 | 
			
		||||
        beforeEach(() => {
 | 
			
		||||
            testTelemetryObject = {
 | 
			
		||||
@@ -179,6 +180,8 @@ describe("the plugin", () => {
 | 
			
		||||
            tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]);
 | 
			
		||||
            tableView.show(child, true);
 | 
			
		||||
 | 
			
		||||
            tableInstance = tableView._getTable();
 | 
			
		||||
 | 
			
		||||
            return telemetryPromise.then(() => Vue.nextTick());
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@@ -228,5 +231,41 @@ describe("the plugin", () => {
 | 
			
		||||
                expect(toColumnText).toEqual(firstColumnText);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("Supports filtering telemetry by regular text search", () => {
 | 
			
		||||
            tableInstance.filteredRows.setColumnFilter("some-key", "1");
 | 
			
		||||
 | 
			
		||||
            return Vue.nextTick().then(() => {
 | 
			
		||||
                let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
 | 
			
		||||
 | 
			
		||||
                expect(filteredRowElements.length).toEqual(1);
 | 
			
		||||
 | 
			
		||||
                tableInstance.filteredRows.setColumnFilter("some-key", "");
 | 
			
		||||
 | 
			
		||||
                return Vue.nextTick().then(() => {
 | 
			
		||||
                    let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
 | 
			
		||||
 | 
			
		||||
                    expect(allRowElements.length).toEqual(3);
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("Supports filtering using Regex", () => {
 | 
			
		||||
            tableInstance.filteredRows.setColumnRegexFilter("some-key", "^some-value$");
 | 
			
		||||
 | 
			
		||||
            return Vue.nextTick().then(() => {
 | 
			
		||||
                let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
 | 
			
		||||
 | 
			
		||||
                expect(filteredRowElements.length).toEqual(0);
 | 
			
		||||
 | 
			
		||||
                tableInstance.filteredRows.setColumnRegexFilter("some-key", "^some-value");
 | 
			
		||||
 | 
			
		||||
                return Vue.nextTick().then(() => {
 | 
			
		||||
                    let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
 | 
			
		||||
 | 
			
		||||
                    expect(allRowElements.length).toEqual(3);
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -39,6 +39,9 @@ const DEFAULT_DURATION_FORMATTER = 'duration';
 | 
			
		||||
const LOCAL_STORAGE_HISTORY_KEY_FIXED = 'tcHistory';
 | 
			
		||||
const LOCAL_STORAGE_HISTORY_KEY_REALTIME = 'tcHistoryRealtime';
 | 
			
		||||
const DEFAULT_RECORDS = 10;
 | 
			
		||||
const ONE_MINUTE = 60 * 1000;
 | 
			
		||||
const ONE_HOUR = ONE_MINUTE * 60;
 | 
			
		||||
const ONE_DAY = ONE_HOUR * 24;
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    inject: ['openmct', 'configuration'],
 | 
			
		||||
@@ -135,10 +138,20 @@ export default {
 | 
			
		||||
    methods: {
 | 
			
		||||
        getHistoryMenuItems() {
 | 
			
		||||
            const history = this.historyForCurrentTimeSystem.map(timespan => {
 | 
			
		||||
                let name;
 | 
			
		||||
                let startTime = this.formatTime(timespan.start);
 | 
			
		||||
                let description = `${startTime} - ${this.formatTime(timespan.end)}`;
 | 
			
		||||
 | 
			
		||||
                if (this.timeSystem.isUTCBased && !this.openmct.time.clock()) {
 | 
			
		||||
                    name = `${startTime} ${this.getDuration(timespan.end - timespan.start)}`;
 | 
			
		||||
                } else {
 | 
			
		||||
                    name = description;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return {
 | 
			
		||||
                    cssClass: 'icon-history',
 | 
			
		||||
                    name: `${this.formatTime(timespan.start)} - ${this.formatTime(timespan.end)}`,
 | 
			
		||||
                    description: `${this.formatTime(timespan.start)} - ${this.formatTime(timespan.end)}`,
 | 
			
		||||
                    name,
 | 
			
		||||
                    description,
 | 
			
		||||
                    callBack: () => this.selectTimespan(timespan)
 | 
			
		||||
                };
 | 
			
		||||
            });
 | 
			
		||||
@@ -163,6 +176,41 @@ export default {
 | 
			
		||||
                };
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        getDuration(numericDuration) {
 | 
			
		||||
            let result;
 | 
			
		||||
            let age;
 | 
			
		||||
 | 
			
		||||
            if (numericDuration > ONE_DAY - 1) {
 | 
			
		||||
                age = this.normalizeAge((numericDuration / ONE_DAY).toFixed(2));
 | 
			
		||||
                result = `+ ${age} day`;
 | 
			
		||||
 | 
			
		||||
                if (age !== 1) {
 | 
			
		||||
                    result += 's';
 | 
			
		||||
                }
 | 
			
		||||
            } else if (numericDuration > ONE_HOUR - 1) {
 | 
			
		||||
                age = this.normalizeAge((numericDuration / ONE_HOUR).toFixed(2));
 | 
			
		||||
                result = `+ ${age} hour`;
 | 
			
		||||
 | 
			
		||||
                if (age !== 1) {
 | 
			
		||||
                    result += 's';
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                age = this.normalizeAge((numericDuration / ONE_MINUTE).toFixed(2));
 | 
			
		||||
                result = `+ ${age} min`;
 | 
			
		||||
 | 
			
		||||
                if (age !== 1) {
 | 
			
		||||
                    result += 's';
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return result;
 | 
			
		||||
        },
 | 
			
		||||
        normalizeAge(num) {
 | 
			
		||||
            const hundredtized = num * 100;
 | 
			
		||||
            const isWhole = hundredtized % 100 === 0;
 | 
			
		||||
 | 
			
		||||
            return isWhole ? hundredtized / 100 : num;
 | 
			
		||||
        },
 | 
			
		||||
        getHistoryFromLocalStorage() {
 | 
			
		||||
            const localStorageHistory = localStorage.getItem(this.storageKey);
 | 
			
		||||
            const history = localStorageHistory ? JSON.parse(localStorageHistory) : undefined;
 | 
			
		||||
@@ -184,24 +232,16 @@ export default {
 | 
			
		||||
                start: this.isFixed ? this.bounds.start : this.offsets.start,
 | 
			
		||||
                end: this.isFixed ? this.bounds.end : this.offsets.end
 | 
			
		||||
            };
 | 
			
		||||
            let self = this;
 | 
			
		||||
 | 
			
		||||
            function isNotEqual(entry) {
 | 
			
		||||
                const start = entry.start !== self.start;
 | 
			
		||||
                const end = entry.end !== self.end;
 | 
			
		||||
            // no dupes
 | 
			
		||||
            currentHistory = currentHistory.filter(ts => !(ts.start === timespan.start && ts.end === timespan.end));
 | 
			
		||||
            currentHistory.unshift(timespan); // add to front
 | 
			
		||||
 | 
			
		||||
                return start || end;
 | 
			
		||||
            if (currentHistory.length > this.records) {
 | 
			
		||||
                currentHistory.length = this.records;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            currentHistory = currentHistory.filter(isNotEqual, timespan);
 | 
			
		||||
 | 
			
		||||
            while (currentHistory.length >= this.records) {
 | 
			
		||||
                currentHistory.pop();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            currentHistory.unshift(timespan);
 | 
			
		||||
            this.$set(this[this.currentHistory], key, currentHistory);
 | 
			
		||||
 | 
			
		||||
            this.persistHistoryToLocalStorage();
 | 
			
		||||
        },
 | 
			
		||||
        selectTimespan(timespan) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										113
									
								
								src/plugins/timeline/TimelineObjectView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								src/plugins/timeline/TimelineObjectView.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,113 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
* 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>
 | 
			
		||||
<swim-lane :icon-class="item.type.definition.cssClass"
 | 
			
		||||
           :min-height="item.height"
 | 
			
		||||
           :show-ucontents="item.domainObject.type === 'plan'"
 | 
			
		||||
           :span-rows-count="item.rowCount"
 | 
			
		||||
>
 | 
			
		||||
    <template slot="label">
 | 
			
		||||
        {{ item.domainObject.name }}
 | 
			
		||||
    </template>
 | 
			
		||||
    <object-view
 | 
			
		||||
        ref="objectView"
 | 
			
		||||
        slot="object"
 | 
			
		||||
        class="u-contents"
 | 
			
		||||
        :default-object="item.domainObject"
 | 
			
		||||
        :object-view-key="item.viewKey"
 | 
			
		||||
        :object-path="item.objectPath"
 | 
			
		||||
    />
 | 
			
		||||
</swim-lane>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import ObjectView from '@/ui/components/ObjectView.vue';
 | 
			
		||||
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
        ObjectView,
 | 
			
		||||
        SwimLane
 | 
			
		||||
    },
 | 
			
		||||
    inject: ['openmct'],
 | 
			
		||||
    props: {
 | 
			
		||||
        item: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
            required: true
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            domainObject: undefined,
 | 
			
		||||
            mutablePromise: undefined
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    watch: {
 | 
			
		||||
        item(newItem) {
 | 
			
		||||
            if (!this.context) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.context.item = newItem.domainObject;
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        if (this.openmct.objects.supportsMutation(this.item.domainObject.identifier)) {
 | 
			
		||||
            this.mutablePromise = this.openmct.objects.getMutable(this.item.domainObject.identifier)
 | 
			
		||||
                .then(this.setObject);
 | 
			
		||||
        } else {
 | 
			
		||||
            this.openmct.objects.get(this.item.domainObject.identifier)
 | 
			
		||||
                .then(this.setObject);
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    beforeDestroy() {
 | 
			
		||||
        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.$nextTick(() => {
 | 
			
		||||
                let reference = this.$refs.objectView;
 | 
			
		||||
 | 
			
		||||
                if (reference) {
 | 
			
		||||
                    let childContext = this.$refs.objectView.getSelectionContext();
 | 
			
		||||
                    childContext.item = domainObject;
 | 
			
		||||
                    this.context = childContext;
 | 
			
		||||
                    this.removeSelectable = this.openmct.selection.selectable(
 | 
			
		||||
                        this.$el, this.context);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
@@ -52,22 +52,10 @@
 | 
			
		||||
                :key="item.keyString"
 | 
			
		||||
                class="u-contents c-timeline__content"
 | 
			
		||||
            >
 | 
			
		||||
                <swim-lane :icon-class="item.type.definition.cssClass"
 | 
			
		||||
                           :min-height="item.height"
 | 
			
		||||
                           :show-ucontents="item.domainObject.type === 'plan'"
 | 
			
		||||
                           :span-rows-count="item.rowCount"
 | 
			
		||||
                >
 | 
			
		||||
                    <template slot="label">
 | 
			
		||||
                        {{ item.domainObject.name }}
 | 
			
		||||
                    </template>
 | 
			
		||||
                    <object-view
 | 
			
		||||
                        slot="object"
 | 
			
		||||
                        class="u-contents"
 | 
			
		||||
                        :default-object="item.domainObject"
 | 
			
		||||
                        :object-view-key="item.viewKey"
 | 
			
		||||
                        :object-path="item.objectPath"
 | 
			
		||||
                    />
 | 
			
		||||
                </swim-lane>
 | 
			
		||||
                <timeline-object-view
 | 
			
		||||
                    class="u-contents"
 | 
			
		||||
                    :item="item"
 | 
			
		||||
                />
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -75,7 +63,7 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import ObjectView from '@/ui/components/ObjectView.vue';
 | 
			
		||||
import TimelineObjectView from './TimelineObjectView.vue';
 | 
			
		||||
import TimelineAxis from '../../ui/components/TimeSystemAxis.vue';
 | 
			
		||||
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
 | 
			
		||||
import { getValidatedPlan } from "../plan/util";
 | 
			
		||||
@@ -101,7 +89,7 @@ function getViewKey(domainObject, objectPath, openmct) {
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
        ObjectView,
 | 
			
		||||
        TimelineObjectView,
 | 
			
		||||
        TimelineAxis,
 | 
			
		||||
        SwimLane
 | 
			
		||||
    },
 | 
			
		||||
@@ -158,6 +146,7 @@ export default {
 | 
			
		||||
        },
 | 
			
		||||
        removeItem(identifier) {
 | 
			
		||||
            let index = this.items.findIndex(item => this.openmct.objects.areIdsEqual(identifier, item.domainObject.identifier));
 | 
			
		||||
            this.removeSelectable(this.items[index]);
 | 
			
		||||
            this.items.splice(index, 1);
 | 
			
		||||
        },
 | 
			
		||||
        reorder(reorderPlan) {
 | 
			
		||||
 
 | 
			
		||||
@@ -638,6 +638,16 @@
 | 
			
		||||
    box-shadow: $shdw;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@mixin smallerControlButtons() {
 | 
			
		||||
    .c-click-icon,
 | 
			
		||||
    .c-button,
 | 
			
		||||
    .c-icon-button {
 | 
			
		||||
        // Shrink buttons a bit when they appear in containers
 | 
			
		||||
        font-size: 0.9em;
 | 
			
		||||
        padding: 4px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@mixin wrappedInput() {
 | 
			
		||||
    // An input that is wrapped. Optionally includes a __label or icon element.
 | 
			
		||||
    // Based on .c-search.
 | 
			
		||||
 
 | 
			
		||||
@@ -129,14 +129,7 @@
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .c-click-icon,
 | 
			
		||||
    .c-button,
 | 
			
		||||
    .c-icon-button {
 | 
			
		||||
        // Shrink buttons a bit when they appear in a frame
 | 
			
		||||
        border-radius: $smallCr !important;
 | 
			
		||||
        font-size: 0.9em;
 | 
			
		||||
        padding: 5px;
 | 
			
		||||
    }
 | 
			
		||||
    @include smallerControlButtons;
 | 
			
		||||
 | 
			
		||||
    &.has-complex-content {
 | 
			
		||||
        > .c-so-view__view-large { display: block; }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,11 @@
 | 
			
		||||
@mixin visibleRegexButton {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
    padding: 1px 3px;
 | 
			
		||||
    width: 24px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.c-search {
 | 
			
		||||
    @include wrappedInput();
 | 
			
		||||
 | 
			
		||||
    padding-top: 2px;
 | 
			
		||||
    padding-bottom: 2px;
 | 
			
		||||
 | 
			
		||||
@@ -9,11 +14,46 @@
 | 
			
		||||
        content: $glyph-icon-magnify;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__use-regex {
 | 
			
		||||
        // Button
 | 
			
		||||
        $c: $colorBodyFg;
 | 
			
		||||
        background: rgba($c, 0.2);
 | 
			
		||||
        border: 1px solid rgba($c, 0.3);
 | 
			
		||||
        color: $c;
 | 
			
		||||
        border-radius: $controlCr;
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
        letter-spacing: 1px;
 | 
			
		||||
        font-size: 0.8em;
 | 
			
		||||
        margin-left: $interiorMarginSm;
 | 
			
		||||
        min-width: 0;
 | 
			
		||||
        opacity: 0;
 | 
			
		||||
        order: 2;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
        padding: 1px 0;
 | 
			
		||||
        transform-origin: left;
 | 
			
		||||
        transition: $transOut;
 | 
			
		||||
        width: 0;
 | 
			
		||||
 | 
			
		||||
        &.is-active {
 | 
			
		||||
            $c: $colorBtnActiveBg;
 | 
			
		||||
            @include visibleRegexButton();
 | 
			
		||||
            background: rgba($c, 0.3);
 | 
			
		||||
            border-color: $c;
 | 
			
		||||
            color: $c;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__clear-input {
 | 
			
		||||
        display: none;
 | 
			
		||||
        order: 99;
 | 
			
		||||
        padding: 1px 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.is-active {
 | 
			
		||||
        .c-search__use-regex {
 | 
			
		||||
            margin-left: 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .c-search__clear-input {
 | 
			
		||||
            display: block;
 | 
			
		||||
        }
 | 
			
		||||
@@ -21,6 +61,15 @@
 | 
			
		||||
 | 
			
		||||
    input[type='text'],
 | 
			
		||||
    input[type='search'] {
 | 
			
		||||
        margin-left: $interiorMargin;
 | 
			
		||||
        order: 3;
 | 
			
		||||
        text-align: left;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
        .c-search__use-regex {
 | 
			
		||||
            @include visibleRegexButton();
 | 
			
		||||
            transition: $transIn;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@
 | 
			
		||||
        class="c-search__clear-input icon-x-in-circle"
 | 
			
		||||
        @click="clearInput"
 | 
			
		||||
    ></a>
 | 
			
		||||
    <slot></slot>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,6 @@
 | 
			
		||||
    <div class="c-swimlane__lane-object"
 | 
			
		||||
         :style="{'min-height': minHeight}"
 | 
			
		||||
         :class="{'u-contents': showUcontents}"
 | 
			
		||||
         data-selectable
 | 
			
		||||
    >
 | 
			
		||||
        <slot name="object"></slot>
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -22,5 +22,7 @@
 | 
			
		||||
        .c-plan {
 | 
			
		||||
            display: contents;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @include smallerControlButtons;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -710,25 +710,25 @@ export default {
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        async aggregateSearchResults(results, abortSignal) {
 | 
			
		||||
        aggregateSearchResults(results, abortSignal) {
 | 
			
		||||
            for (const result of results) {
 | 
			
		||||
                if (!abortSignal.aborted) {
 | 
			
		||||
                    const objectPath = await this.openmct.objects.getOriginalPath(result.identifier);
 | 
			
		||||
                    this.openmct.objects.getOriginalPath(result.identifier).then((objectPath) => {
 | 
			
		||||
                        // removing the item itself, as the path we pass to buildTreeItem is a parent path
 | 
			
		||||
                        objectPath.shift();
 | 
			
		||||
 | 
			
		||||
                    // removing the item itself, as the path we pass to buildTreeItem is a parent path
 | 
			
		||||
                    objectPath.shift();
 | 
			
		||||
                        // if root, remove, we're not using in object path for tree
 | 
			
		||||
                        let lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;
 | 
			
		||||
                        if (lastObject && lastObject.type === 'root') {
 | 
			
		||||
                            objectPath.pop();
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                    // if root, remove, we're not using in object path for tree
 | 
			
		||||
                    let lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;
 | 
			
		||||
                    if (lastObject && lastObject.type === 'root') {
 | 
			
		||||
                        objectPath.pop();
 | 
			
		||||
                    }
 | 
			
		||||
                        // we reverse the objectPath in the tree, so have to do it here first,
 | 
			
		||||
                        // since this one is already in the correct direction
 | 
			
		||||
                        let resultObject = this.buildTreeItem(result, objectPath.reverse());
 | 
			
		||||
 | 
			
		||||
                    // we reverse the objectPath in the tree, so have to do it here first,
 | 
			
		||||
                    // since this one is already in the correct direction
 | 
			
		||||
                    let resultObject = this.buildTreeItem(result, objectPath.reverse());
 | 
			
		||||
 | 
			
		||||
                    this.searchResultItems.push(resultObject);
 | 
			
		||||
                        this.searchResultItems.push(resultObject);
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user