Compare commits

..

2 Commits

Author SHA1 Message Date
cf5ef9fa60 Remove angular, hack around things that don't work 2021-04-05 15:09:22 -07:00
756e5d301e Removing angular 2021-04-02 18:27:09 -07:00
222 changed files with 9679 additions and 13023 deletions

2
.gitignore vendored
View File

@ -39,3 +39,5 @@ npm-debug.log
# karma reports
report.*.json
package-lock.json

3
API.md
View File

@ -423,14 +423,13 @@ attribute | type | flags | notes
###### Value Hints
Each telemetry value description has an object defining hints. Keys in this object represent the hint itself, and the value represents the weight of that hint. A lower weight means the hint has a higher priority. For example, multiple values could be hinted for use as the y-axis of a plot (raw, engineering), but the highest priority would be the default choice. Likewise, a table will use hints to determine the default order of columns.
Each telemetry value description has an object defining hints. Keys in this this object represent the hint itself, and the value represents the weight of that hint. A lower weight means the hint has a higher priority. For example, multiple values could be hinted for use as the y-axis of a plot (raw, engineering), but the highest priority would be the default choice. Likewise, a table will use hints to determine the default order of columns.
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

10
app.js
View File

@ -7,14 +7,6 @@
* node app.js [options]
*/
class WatchRunPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('WatchRunPlugin', (compilation, callback) => {
console.log('Begin compile at ' + new Date());
callback();
});
}
}
const options = require('minimist')(process.argv.slice(2));
const express = require('express');
@ -51,7 +43,7 @@ app.use('/proxyUrl', function proxyRequest(req, res, next) {
const webpack = require('webpack');
const webpackConfig = require('./webpack.config.js');
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
webpackConfig.plugins.push(new WatchRunPlugin());
webpackConfig.plugins.push(function() { this.plugin('watch-run', function(watching, callback) { console.log('Begin compile at ' + new Date()); callback(); }) });
webpackConfig.entry.openmct = [
'webpack-hot-middleware/client?reload=true',

View File

@ -50,16 +50,11 @@ 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,
imageDownloadName
url: IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length]
};
}
@ -144,14 +139,6 @@ define([
hints: {
image: 1
}
},
{
name: 'Image Download Name',
key: 'imageDownloadName',
format: 'imageDownloadName',
hints: {
imageDownloadName: 1
}
}
]
};

View File

@ -88,6 +88,7 @@
openmct.install(openmct.plugins.ExampleImagery());
openmct.install(openmct.plugins.PlanLayout());
openmct.install(openmct.plugins.Timeline());
openmct.install(openmct.plugins.PlotVue());
openmct.install(openmct.plugins.UTCTimeSystem());
openmct.install(openmct.plugins.AutoflowView({
type: "telemetry.panel"

View File

@ -78,7 +78,6 @@ module.exports = (config) => {
preserveDescribeNesting: true,
foldAll: false
},
browserConsoleLogOptions: { level: "error", format: "%b %T: %m", terminal: true },
coverageIstanbulReporter: {
fixWebpackSourcePaths: true,
dir: process.env.CIRCLE_ARTIFACTS ?

8613
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "openmct",
"version": "1.7.3-SNAPSHOT",
"version": "1.6.3-SNAPSHOT",
"description": "The Open MCT core platform",
"dependencies": {},
"devDependencies": {
@ -9,7 +9,7 @@
"babel-eslint": "10.0.3",
"comma-separated-values": "^3.6.4",
"concurrently": "^3.6.1",
"copy-webpack-plugin": "^9.0.0",
"copy-webpack-plugin": "^4.5.2",
"cross-env": "^6.0.3",
"css-loader": "^1.0.0",
"d3-array": "1.2.x",
@ -41,19 +41,19 @@
"jsdoc": "^3.3.2",
"karma": "5.1.1",
"karma-chrome-launcher": "3.1.0",
"karma-firefox-launcher": "1.3.0",
"karma-cli": "2.0.0",
"karma-coverage": "2.0.3",
"karma-coverage-istanbul-reporter": "3.0.3",
"karma-firefox-launcher": "1.3.0",
"karma-html-reporter": "0.2.7",
"karma-jasmine": "3.3.1",
"karma-sourcemap-loader": "0.3.7",
"karma-webpack": "^5.0.0",
"karma-webpack": "4.0.2",
"location-bar": "^3.0.1",
"lodash": "^4.17.12",
"markdown-toc": "^0.11.7",
"marked": "^0.3.5",
"mini-css-extract-plugin": "^1.6.0",
"mini-css-extract-plugin": "^0.4.1",
"minimist": "^1.2.5",
"moment": "2.25.3",
"moment-duration-format": "^2.2.2",
@ -69,17 +69,16 @@
"uuid": "^3.3.3",
"v8-compile-cache": "^1.1.0",
"vue": "2.5.6",
"vue-loader": "^15.9.7",
"vue-loader": "^15.2.6",
"vue-template-compiler": "2.5.6",
"webpack": "^5.37.0",
"webpack-cli": "^3.3.12",
"webpack": "^4.16.2",
"webpack-cli": "^3.1.0",
"webpack-dev-middleware": "^3.1.3",
"webpack-hot-middleware": "^2.22.3",
"zepto": "^1.2.0"
},
"scripts": {
"clean": "rm -rf ./dist /node_modules; rm package-lock.json",
"clean-test-lint": "npm run clean; npm install ; npm run test; npm run lint",
"clean": "rm -rf ./dist",
"start": "node app.js",
"lint": "eslint platform example src --ext .js,.vue openmct.js",
"lint:fix": "eslint platform example src --ext .js,.vue openmct.js --fix",

View File

@ -86,7 +86,7 @@ define(
})
.join('/');
openmct.router.navigate(url);
window.location.href = url;
if (isFirstViewEditable(object.useCapability('adapter'), objectPath)) {
openmct.editor.edit();

View File

@ -141,17 +141,11 @@ define(
if (mutationResult !== false) {
// Copy values if result was a different object
// (either our clone or some other new thing)
let modelHasChanged = _.isEqual(model, result) === false;
if (modelHasChanged) {
if (model !== result) {
copyValues(model, result);
}
if (modelHasChanged
|| (useTimestamp !== undefined)
|| (model.modified === undefined)) {
model.modified = useTimestamp ? timestamp : now();
}
model.modified = useTimestamp ? timestamp : now();
notifyListeners(model);
}

View File

@ -154,9 +154,7 @@ define(['zepto', 'objectUtils'], function ($, objectUtils) {
tree = JSON.stringify(tree).replace(new RegExp(oldIdKeyString, 'g'), newIdKeyString);
return JSON.parse(tree, (key, value) => {
if (value !== undefined
&& value !== null
&& Object.prototype.hasOwnProperty.call(value, 'key')
if (Object.prototype.hasOwnProperty.call(value, 'key')
&& Object.prototype.hasOwnProperty.call(value, 'namespace')
&& value.key === oldId.key
&& value.namespace === oldId.namespace) {

View File

@ -0,0 +1,8 @@
# 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'))
```

View File

@ -0,0 +1,78 @@
/*****************************************************************************
* 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"
]
}
]
}
}
};
});

View File

@ -0,0 +1,61 @@
/*****************************************************************************
* 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;
}
);

View File

@ -0,0 +1,119 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[],
function () {
// 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;
}
);

View File

@ -0,0 +1,145 @@
/*****************************************************************************
* 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;
}
);

View File

@ -0,0 +1,63 @@
/*****************************************************************************
* 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);
});
});
}
);

View File

@ -0,0 +1,129 @@
/*****************************************************************************
* 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");
});
});
}
);

View File

@ -0,0 +1,223 @@
/*****************************************************************************
* 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);
});
});
}
);

View File

@ -252,7 +252,7 @@ define([
this.status = new api.StatusAPI(this);
this.router = new ApplicationRouter(this);
this.router = new ApplicationRouter();
this.branding = BrandingAPI.default;
@ -284,6 +284,7 @@ define([
this.install(this.plugins.ViewDatumAction());
this.install(this.plugins.ObjectInterceptors());
this.install(this.plugins.NonEditableFolder());
this.install(this.plugins.Devices());
}
MCT.prototype = Object.create(EventEmitter.prototype);
@ -403,39 +404,24 @@ define([
this.router.setPath('/browse/');
});
/**
* Fired by [MCT]{@link module:openmct.MCT} when the application
* is started.
* @event start
* @memberof module:openmct.MCT~
*/
const startPromise = new Main();
startPromise.run(this)
.then(function (angular) {
this.$angular = angular;
// OpenMCT Object provider doesn't operate properly unless
// something has depended upon objectService. Cool, right?
this.$injector.get('objectService');
if (!isHeadlessMode) {
const appLayout = new Vue({
components: {
'Layout': Layout.default
},
provide: {
openmct: this
},
template: '<Layout ref="layout"></Layout>'
});
domElement.appendChild(appLayout.$mount().$el);
if (!isHeadlessMode) {
const appLayout = new Vue({
components: {
'Layout': Layout.default
},
provide: {
openmct: this
},
template: '<Layout ref="layout"></Layout>'
});
domElement.appendChild(appLayout.$mount().$el);
this.layout = appLayout.$refs.layout;
Browse(this);
}
this.layout = appLayout.$refs.layout;
Browse(this);
}
this.router.start();
this.emit('start');
}.bind(this));
this.router.start();
this.emit('start');
};
MCT.prototype.startHeadless = function () {

View File

@ -36,8 +36,7 @@ define([
'./views/installLegacyViews',
'./policies/LegacyCompositionPolicyAdapter',
'./actions/LegacyActionAdapter',
'./services/LegacyPersistenceAdapter',
'./services/ExportImageService'
'./services/LegacyPersistenceAdapter'
], function (
ActionDialogDecorator,
AdapterCapability,
@ -54,8 +53,7 @@ define([
installLegacyViews,
legacyCompositionPolicyAdapter,
LegacyActionAdapter,
LegacyPersistenceAdapter,
ExportImageService
LegacyPersistenceAdapter
) {
return {
name: 'src/adapter',
@ -84,13 +82,6 @@ define([
"identifierService",
"cacheService"
]
},
{
"key": "exportImageService",
"implementation": ExportImageService,
"depends": [
"dialogService"
]
}
],
components: [

View File

@ -52,7 +52,7 @@ define([
oldStyleObject.getCapability('mutation').mutate(function () {
return utils.toOldFormat(newStyleObject);
}, newStyleObject.modified);
});
removeGeneralTopicListener = this.generalTopic.listen(handleLegacyMutation);
}.bind(this);

View File

@ -119,8 +119,7 @@ describe('The ActionCollection', () => {
afterEach(() => {
actionCollection.destroy();
return resetApplicationState(openmct);
resetApplicationState(openmct);
});
describe("disable method invoked with action keys", () => {

View File

@ -99,7 +99,7 @@ describe('The Actions API', () => {
});
afterEach(() => {
return resetApplicationState(openmct);
resetApplicationState(openmct);
});
describe("register method", () => {

View File

@ -76,7 +76,7 @@ describe ('The Menu API', () => {
});
afterEach(() => {
return resetApplicationState(openmct);
resetApplicationState(openmct);
});
describe("showMenu method", () => {

View File

@ -76,10 +76,7 @@ class MutableDomainObject {
}
$set(path, value) {
_.set(this, path, value);
if (path !== 'persisted' && path !== 'modified') {
_.set(this, 'modified', Date.now());
}
_.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);
@ -115,11 +112,9 @@ class MutableDomainObject {
return () => this._instanceEventEmitter.off(event, callback);
}
$destroy() {
while (this._observers.length > 0) {
const observer = this._observers.pop();
observer();
}
this._observers.forEach(observer => observer());
delete this._globalEventEmitter;
delete this._observers;
this._instanceEventEmitter.emit('$_destroy');
}

View File

@ -38,9 +38,6 @@ function ObjectAPI(typeRegistry, openmct) {
this.eventEmitter = new EventEmitter();
this.providers = {};
this.rootRegistry = new RootRegistry();
this.injectIdentifierService = function () {
this.identifierService = openmct.$injector.get("identifierService");
};
this.rootProvider = new RootObjectProvider(this.rootRegistry);
this.cache = {};
@ -55,33 +52,16 @@ ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) {
this.fallbackProvider = p;
};
/**
* @private
*/
ObjectAPI.prototype.getIdentifierService = function () {
// Lazily acquire identifier service
if (!this.identifierService) {
this.injectIdentifierService();
}
return this.identifierService;
};
/**
* Retrieve the provider for a given identifier.
* @private
*/
ObjectAPI.prototype.getProvider = function (identifier) {
//handles the '' vs 'mct' namespace issue
const keyString = utils.makeKeyString(identifier);
const identifierService = this.getIdentifierService();
const namespace = identifierService.parse(keyString).getSpace();
if (identifier.key === 'ROOT') {
return this.rootProvider;
}
return this.providers[namespace] || this.fallbackProvider;
return this.providers[identifier.namespace] || this.fallbackProvider;
};
/**
@ -161,7 +141,6 @@ 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];
}
@ -177,16 +156,15 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) {
throw new Error('Provider does not support get!');
}
let objectPromise = provider.get(identifier, abortSignal).then(result => {
let objectPromise = provider.get(identifier, abortSignal);
this.cache[keystring] = objectPromise;
return objectPromise.then(result => {
delete this.cache[keystring];
result = this.applyGetInterceptors(identifier, result);
return result;
});
this.cache[keystring] = objectPromise;
return objectPromise;
};
/**
@ -486,12 +464,6 @@ ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) {
});
};
ObjectAPI.prototype.isObjectPathToALink = function (domainObject, objectPath) {
return objectPath !== undefined
&& objectPath.length > 1
&& domainObject.location !== this.makeKeyString(objectPath[1].identifier);
};
/**
* Uniquely identifies a domain object.
*
@ -528,10 +500,8 @@ ObjectAPI.prototype.isObjectPathToALink = function (domainObject, objectPath) {
*/
function hasAlreadyBeenPersisted(domainObject) {
const result = domainObject.persisted !== undefined
&& domainObject.persisted >= domainObject.modified;
return result;
return domainObject.persisted !== undefined
&& domainObject.persisted === domainObject.modified;
}
export default ObjectAPI;

View File

@ -22,7 +22,7 @@ describe("The Status API", () => {
});
afterEach(() => {
return resetApplicationState(openmct);
resetApplicationState(openmct);
});
describe("set function", () => {

View File

@ -116,11 +116,9 @@ define([
* @private
*/
DefaultMetadataProvider.prototype.typeHasTelemetry = function (domainObject) {
if (!this.typeService) {
this.typeService = this.openmct.$injector.get('typeService');
}
const type = this.openmct.types.get(domainObject.type);
return Boolean(this.typeService.getType(domainObject.type).typeDef.telemetry);
return Boolean(type.definition.telemetry);
};
return DefaultMetadataProvider;

View File

@ -142,6 +142,8 @@ define([
this.metadataCache = new WeakMap();
this.formatMapCache = new WeakMap();
this.valueFormatterCache = new WeakMap();
this.formatters = new Map();
}
/**
@ -413,17 +415,6 @@ define([
return _.sortBy(options, sortKeys);
};
/**
* @private
*/
TelemetryAPI.prototype.getFormatService = function () {
if (!this.formatService) {
this.formatService = this.openmct.$injector.get('formatService');
}
return this.formatService;
};
/**
* Get a value formatter for a given valueMetadata.
*
@ -433,7 +424,7 @@ define([
if (!this.valueFormatterCache.has(valueMetadata)) {
this.valueFormatterCache.set(
valueMetadata,
new TelemetryValueFormatter(valueMetadata, this.getFormatService())
new TelemetryValueFormatter(valueMetadata, this.formatters)
);
}
@ -447,9 +438,11 @@ define([
* @returns {Format}
*/
TelemetryAPI.prototype.getFormatter = function (key) {
const formatMap = this.getFormatService().formatMap;
return formatMap[key];
if (this.formatters.has(key)) {
return this.formatters.get(key);
} else {
throw new Error(`Unknown type ${key}`);
}
};
/**
@ -476,12 +469,7 @@ define([
* @param {Format} format the
*/
TelemetryAPI.prototype.addFormat = function (format) {
this.openmct.legacyExtension('formats', {
key: format.key,
implementation: function () {
return format;
}
});
this.formatters.set(format.key, format);
};
/**

View File

@ -28,8 +28,7 @@ define([
printj
) {
// TODO: needs reference to formatService;
function TelemetryValueFormatter(valueMetadata, formatService) {
function TelemetryValueFormatter(valueMetadata, formatters) {
const numberFormatter = {
parse: function (x) {
return Number(x);
@ -43,13 +42,7 @@ define([
};
this.valueMetadata = valueMetadata;
try {
this.formatter = formatService
.getFormat(valueMetadata.format, valueMetadata);
} catch (e) {
// TODO: Better formatting
this.formatter = numberFormatter;
}
this.formatter = formatters.get(valueMetadata.format) || numberFormatter;
if (valueMetadata.format === 'enum') {
this.formatter = {};

View File

@ -90,6 +90,7 @@ 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',

View File

@ -43,16 +43,12 @@ export default function LADTableSetViewProvider(openmct) {
components: {
LadTableSet: LadTableSet
},
data() {
return {
domainObject
};
},
provide: {
openmct,
domainObject,
objectPath
},
template: '<lad-table-set :domain-object="domainObject"></lad-table-set>'
template: '<lad-table-set></lad-table-set>'
});
},
destroy: function (element) {

View File

@ -56,7 +56,7 @@ export default {
type: Object,
required: true
},
pathToTable: {
objectPath: {
type: Array,
required: true
},
@ -66,19 +66,20 @@ export default {
}
},
data() {
let currentObjectPath = this.objectPath.slice();
currentObjectPath.unshift(this.domainObject);
return {
timestamp: undefined,
value: '---',
valueClass: '',
currentObjectPath,
unit: ''
};
},
computed: {
formattedTimestamp() {
return this.timestamp !== undefined ? this.getFormattedTimestamp(this.timestamp) : '---';
},
objectPath() {
return [this.domainObject, ...this.pathToTable];
}
},
mounted() {
@ -181,7 +182,7 @@ export default {
};
},
showContextMenu(event) {
let actionCollection = this.openmct.actions.get(this.objectPath, this.getView());
let actionCollection = this.openmct.actions.get(this.currentObjectPath, this.getView());
let allActions = actionCollection.getActionsObject();
let applicableActions = CONTEXT_MENU_ACTIONS.map(key => allActions[key]);

View File

@ -33,10 +33,10 @@
</thead>
<tbody>
<lad-row
v-for="ladRow in items"
:key="ladRow.key"
:domain-object="ladRow.domainObject"
:path-to-table="objectPath"
v-for="item in items"
:key="item.key"
:domain-object="item.domainObject"
:object-path="objectPath"
:has-units="hasUnits"
/>
</tbody>

View File

@ -43,10 +43,9 @@
</td>
</tr>
<lad-row
v-for="ladRow in ladTelemetryObjects[ladTable.key]"
:key="ladRow.key"
:domain-object="ladRow.domainObject"
:path-to-table="ladTable.objectPath"
v-for="telemetryObject in ladTelemetryObjects[ladTable.key]"
:key="telemetryObject.key"
:domain-object="telemetryObject.domainObject"
:has-units="hasUnits"
/>
</template>
@ -61,13 +60,7 @@ export default {
components: {
LadRow
},
inject: ['openmct', 'objectPath'],
props: {
domainObject: {
type: Object,
required: true
}
},
inject: ['openmct', 'domainObject'],
data() {
return {
ladTableObjects: [],
@ -113,7 +106,6 @@ export default {
let ladTable = {};
ladTable.domainObject = domainObject;
ladTable.key = this.openmct.objects.makeKeyString(domainObject.identifier);
ladTable.objectPath = [domainObject, ...this.objectPath];
this.$set(this.ladTelemetryObjects, ladTable.key, []);
this.ladTableObjects.push(ladTable);

View File

@ -292,11 +292,6 @@ describe("The LAD Table Set", () => {
});
afterEach(() => {
openmct.time.timeSystem('utc', {
start: 0,
end: 1
});
return resetApplicationState(openmct);
});

View File

@ -19,6 +19,10 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
getAllSearchParams,
setAllSearchParams
} from 'utils/openmctLocation';
const TIME_EVENTS = ['timeSystem', 'clock', 'clockOffsets'];
const SEARCH_MODE = 'tc.mode';
@ -45,8 +49,9 @@ export default class URLTimeSettingsSynchronizer {
}
initialize() {
this.openmct.router.on('change:params', this.updateTimeSettings);
this.updateTimeSettings();
window.addEventListener('hashchange', this.updateTimeSettings);
TIME_EVENTS.forEach(event => {
this.openmct.time.on(event, this.setUrlFromTimeApi);
});
@ -54,8 +59,7 @@ export default class URLTimeSettingsSynchronizer {
}
destroy() {
this.openmct.router.off('change:params', this.updateTimeSettings);
window.removeEventListener('hashchange', this.updateTimeSettings);
this.openmct.off('start', this.initialize);
this.openmct.off('destroy', this.destroy);
@ -66,18 +70,22 @@ export default class URLTimeSettingsSynchronizer {
}
updateTimeSettings() {
let timeParameters = this.parseParametersFromUrl();
// Prevent from triggering self
if (!this.isUrlUpdateInProgress) {
let timeParameters = this.parseParametersFromUrl();
if (this.areTimeParametersValid(timeParameters)) {
this.setTimeApiFromUrl(timeParameters);
this.openmct.router.setLocationFromUrl();
if (this.areTimeParametersValid(timeParameters)) {
this.setTimeApiFromUrl(timeParameters);
} else {
this.setUrlFromTimeApi();
}
} else {
this.setUrlFromTimeApi();
this.isUrlUpdateInProgress = false;
}
}
parseParametersFromUrl() {
let searchParams = this.openmct.router.getAllSearchParams();
let searchParams = getAllSearchParams();
let mode = searchParams.get(SEARCH_MODE);
let timeSystem = searchParams.get(SEARCH_TIME_SYSTEM);
@ -140,7 +148,7 @@ export default class URLTimeSettingsSynchronizer {
}
setUrlFromTimeApi() {
let searchParams = this.openmct.router.getAllSearchParams();
let searchParams = getAllSearchParams();
let clock = this.openmct.time.clock();
let bounds = this.openmct.time.bounds();
let clockOffsets = this.openmct.time.clockOffsets();
@ -168,7 +176,8 @@ export default class URLTimeSettingsSynchronizer {
}
searchParams.set(SEARCH_TIME_SYSTEM, this.openmct.time.timeSystem().key);
this.openmct.router.setAllSearchParams(searchParams);
this.isUrlUpdateInProgress = true;
setAllSearchParams(searchParams);
}
areTimeParametersValid(timeParameters) {

View File

@ -25,118 +25,306 @@ import {
} from 'utils/testing';
describe("The URLTimeSettingsSynchronizer", () => {
let appHolder;
let openmct;
let resolveFunction;
let oldHash;
let testClock;
beforeEach((done) => {
openmct = createOpenMct();
openmct.install(openmct.plugins.MyItems());
openmct.install(openmct.plugins.LocalTimeSystem());
openmct.install(openmct.plugins.UTCTimeSystem());
testClock = jasmine.createSpyObj("testClock", ["start", "stop", "tick", "currentValue", "on", "off"]);
testClock.key = "test-clock";
testClock.currentValue.and.returnValue(0);
openmct.time.addClock(testClock);
openmct.on('start', done);
appHolder = document.createElement("div");
openmct.start(appHolder);
openmct.startHeadless();
});
afterEach(() => {
openmct.time.stopClock();
openmct.router.removeListener('change:hash', resolveFunction);
afterEach(() => resetApplicationState(openmct));
appHolder = undefined;
openmct = undefined;
resolveFunction = undefined;
return resetApplicationState(openmct);
});
it("initial clock is set to fixed is reflected in URL", (done) => {
resolveFunction = () => {
oldHash = window.location.hash;
describe("realtime mode", () => {
it("when the clock is set via the time API, it is immediately reflected in the URL", () => {
//Test expected initial conditions
expect(window.location.hash.includes('tc.mode=fixed')).toBe(true);
openmct.router.removeListener('change:hash', resolveFunction);
done();
};
openmct.router.on('change:hash', resolveFunction);
});
it("when the clock is set via the time API, it is reflected in the URL", (done) => {
let success;
resolveFunction = () => {
openmct.time.clock('local', {
start: -1000,
end: 100
});
expect(window.location.hash.includes('tc.mode=local')).toBe(true);
//Test that expected initial conditions are no longer true
expect(window.location.hash.includes('tc.mode=fixed')).toBe(false);
});
it("when offsets are set via the time API, they are immediately reflected in the URL", () => {
//Test expected initial conditions
expect(window.location.hash.includes('tc.startDelta')).toBe(false);
expect(window.location.hash.includes('tc.endDelta')).toBe(false);
openmct.time.clock('local', {
start: -1000,
end: 100
});
expect(window.location.hash.includes('tc.startDelta=1000')).toBe(true);
expect(window.location.hash.includes('tc.endDelta=100')).toBe(true);
openmct.time.clockOffsets({
start: -2000,
end: 200
});
expect(window.location.hash.includes('tc.startDelta=2000')).toBe(true);
expect(window.location.hash.includes('tc.endDelta=200')).toBe(true);
const hasStartDelta = window.location.hash.includes('tc.startDelta=2000');
const hasEndDelta = window.location.hash.includes('tc.endDelta=200');
const hasLocalClock = window.location.hash.includes('tc.mode=local');
success = hasStartDelta && hasEndDelta && hasLocalClock;
if (success) {
expect(success).toBe(true);
//Test that expected initial conditions are no longer true
expect(window.location.hash.includes('tc.mode=fixed')).toBe(false);
});
describe("when set in the url", () => {
it("will change from fixed to realtime mode when the mode changes", () => {
expectLocationToBeInFixedMode();
openmct.router.removeListener('change:hash', resolveFunction);
done();
}
};
return switchToRealtimeMode().then(() => {
let clock = openmct.time.clock();
openmct.router.on('change:hash', resolveFunction);
expect(clock).toBeDefined();
expect(clock.key).toBe('local');
});
});
it("the clock is correctly set in the API from the URL parameters", () => {
return switchToRealtimeMode().then(() => {
let resolveFunction;
return new Promise((resolve) => {
resolveFunction = resolve;
//The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been
//detected in the API.
openmct.time.on('clock', resolveFunction);
let hash = window.location.hash;
hash = hash.replace('tc.mode=local', 'tc.mode=test-clock');
window.location.hash = hash;
}).then(() => {
let clock = openmct.time.clock();
expect(clock).toBeDefined();
expect(clock.key).toBe('test-clock');
openmct.time.off('clock', resolveFunction);
});
});
});
it("the clock offsets are correctly set in the API from the URL parameters", () => {
return switchToRealtimeMode().then(() => {
let resolveFunction;
return new Promise((resolve) => {
resolveFunction = resolve;
//The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been
//detected in the API.
openmct.time.on('clockOffsets', resolveFunction);
let hash = window.location.hash;
hash = hash.replace('tc.startDelta=1000', 'tc.startDelta=2000');
hash = hash.replace('tc.endDelta=100', 'tc.endDelta=200');
window.location.hash = hash;
}).then(() => {
let clockOffsets = openmct.time.clockOffsets();
expect(clockOffsets).toBeDefined();
expect(clockOffsets.start).toBe(-2000);
expect(clockOffsets.end).toBe(200);
openmct.time.off('clockOffsets', resolveFunction);
});
});
});
it("the time system is correctly set in the API from the URL parameters", () => {
return switchToRealtimeMode().then(() => {
let resolveFunction;
return new Promise((resolve) => {
resolveFunction = resolve;
//The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been
//detected in the API.
openmct.time.on('timeSystem', resolveFunction);
let hash = window.location.hash;
hash = hash.replace('tc.timeSystem=utc', 'tc.timeSystem=local');
window.location.hash = hash;
}).then(() => {
let timeSystem = openmct.time.timeSystem();
expect(timeSystem).toBeDefined();
expect(timeSystem.key).toBe('local');
openmct.time.off('timeSystem', resolveFunction);
});
});
});
});
});
describe("fixed timespan mode", () => {
beforeEach(() => {
openmct.time.stopClock();
openmct.time.timeSystem('utc', {
start: 0,
end: 1
});
});
it("when bounds are set via the time API, they are immediately reflected in the URL", () => {
//Test expected initial conditions
expect(window.location.hash.includes('tc.startBound=0')).toBe(true);
expect(window.location.hash.includes('tc.endBound=1')).toBe(true);
openmct.time.bounds({
start: 10,
end: 20
});
expect(window.location.hash.includes('tc.startBound=10')).toBe(true);
expect(window.location.hash.includes('tc.endBound=20')).toBe(true);
//Test that expected initial conditions are no longer true
expect(window.location.hash.includes('tc.startBound=0')).toBe(false);
expect(window.location.hash.includes('tc.endBound=1')).toBe(false);
});
it("when time system is set via the time API, it is immediately reflected in the URL", () => {
//Test expected initial conditions
expect(window.location.hash.includes('tc.timeSystem=utc')).toBe(true);
openmct.time.timeSystem('local', {
start: 20,
end: 30
});
expect(window.location.hash.includes('tc.timeSystem=local')).toBe(true);
//Test that expected initial conditions are no longer true
expect(window.location.hash.includes('tc.timeSystem=utc')).toBe(false);
});
describe("when set in the url", () => {
it("time system changes are reflected in the API", () => {
let resolveFunction;
return new Promise((resolve) => {
let timeSystem = openmct.time.timeSystem();
resolveFunction = resolve;
expect(timeSystem.key).toBe('utc');
window.location.hash = window.location.hash.replace('tc.timeSystem=utc', 'tc.timeSystem=local');
openmct.time.on('timeSystem', resolveFunction);
}).then(() => {
let timeSystem = openmct.time.timeSystem();
expect(timeSystem.key).toBe('local');
openmct.time.off('timeSystem', resolveFunction);
});
});
it("mode can be changed from realtime to fixed", () => {
return switchToRealtimeMode().then(() => {
expectLocationToBeInRealtimeMode();
expect(openmct.time.clock()).toBeDefined();
}).then(switchToFixedMode).then(() => {
let clock = openmct.time.clock();
expect(clock).not.toBeDefined();
});
});
it("bounds are correctly set in the API from the URL parameters", () => {
let resolveFunction;
expectLocationToBeInFixedMode();
return new Promise((resolve) => {
resolveFunction = resolve;
openmct.time.on('bounds', resolveFunction);
let hash = window.location.hash;
hash = hash.replace('tc.startBound=0', 'tc.startBound=222')
.replace('tc.endBound=1', 'tc.endBound=333');
window.location.hash = hash;
}).then(() => {
let bounds = openmct.time.bounds();
expect(bounds).toBeDefined();
expect(bounds.start).toBe(222);
expect(bounds.end).toBe(333);
});
});
it("bounds are correctly set in the API from the URL parameters where only the end bound changes", () => {
let resolveFunction;
expectLocationToBeInFixedMode();
return new Promise((resolve) => {
resolveFunction = resolve;
openmct.time.on('bounds', resolveFunction);
let hash = window.location.hash;
hash = hash.replace('tc.endBound=1', 'tc.endBound=333');
window.location.hash = hash;
}).then(() => {
let bounds = openmct.time.bounds();
expect(bounds).toBeDefined();
expect(bounds.start).toBe(0);
expect(bounds.end).toBe(333);
});
});
});
});
it("when the clock mode is set to local, it is reflected in the URL", (done) => {
let success;
function setRealtimeLocationParameters() {
let hash = window.location.hash.toString()
.replace('tc.mode=fixed', 'tc.mode=local')
.replace('tc.startBound=0', 'tc.startDelta=1000')
.replace('tc.endBound=1', 'tc.endDelta=100');
resolveFunction = () => {
let hash = window.location.hash;
hash = hash.replace('tc.mode=fixed', 'tc.mode=local');
window.location.hash = hash;
window.location.hash = hash;
}
success = window.location.hash.includes('tc.mode=local');
if (success) {
expect(success).toBe(true);
done();
}
};
function setFixedLocationParameters() {
let hash = window.location.hash.toString()
.replace('tc.mode=local', 'tc.mode=fixed')
.replace('tc.timeSystem=utc', 'tc.timeSystem=local')
.replace('tc.startDelta=1000', 'tc.startBound=50')
.replace('tc.endDelta=100', 'tc.endBound=60');
openmct.router.on('change:hash', resolveFunction);
});
window.location.hash = hash;
}
it("when the clock mode is set to local, it is reflected in the URL", (done) => {
let success;
function switchToRealtimeMode() {
let resolveFunction;
resolveFunction = () => {
let hash = window.location.hash;
return new Promise((resolve) => {
resolveFunction = resolve;
openmct.time.on('clock', resolveFunction);
setRealtimeLocationParameters();
}).then(() => {
openmct.time.off('clock', resolveFunction);
});
}
hash = hash.replace('tc.mode=fixed', 'tc.mode=local');
window.location.hash = hash;
success = window.location.hash.includes('tc.mode=local');
if (success) {
expect(success).toBe(true);
done();
}
};
function switchToFixedMode() {
let resolveFunction;
openmct.router.on('change:hash', resolveFunction);
});
return new Promise((resolve) => {
resolveFunction = resolve;
//The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been
//detected in the API.
openmct.time.on('clock', resolveFunction);
setFixedLocationParameters();
}).then(() => {
openmct.time.off('clock', resolveFunction);
});
}
it("reset hash", (done) => {
let success;
function expectLocationToBeInRealtimeMode() {
expect(window.location.hash.includes('tc.mode=local')).toBe(true);
expect(window.location.hash.includes('tc.startDelta=1000')).toBe(true);
expect(window.location.hash.includes('tc.endDelta=100')).toBe(true);
expect(window.location.hash.includes('tc.mode=fixed')).toBe(false);
}
window.location.hash = oldHash;
resolveFunction = () => {
success = window.location.hash === oldHash;
if (success) {
expect(success).toBe(true);
done();
}
};
openmct.router.on('change:hash', resolveFunction);
});
function expectLocationToBeInFixedMode() {
expect(window.location.hash.includes('tc.mode=fixed')).toBe(true);
expect(window.location.hash.includes('tc.startBound=0')).toBe(true);
expect(window.location.hash.includes('tc.endBound=1')).toBe(true);
expect(window.location.hash.includes('tc.mode=local')).toBe(false);
}
});

View File

@ -43,6 +43,7 @@ import {TRIGGER_CONJUNCTION, TRIGGER_LABEL} from "./utils/constants";
* }
*/
export default class Condition extends EventEmitter {
/**
* Manages criteria and emits the result of - true or false - based on criteria evaluated.
* @constructor
@ -81,9 +82,7 @@ export default class Condition extends EventEmitter {
if (this.isAnyOrAllTelemetry(criterion)) {
criterion.updateResult(datum, this.conditionManager.telemetryObjects);
} else {
if (criterion.usesTelemetry(datum.id)) {
criterion.updateResult(datum);
}
criterion.updateResult(datum);
}
});
@ -103,7 +102,7 @@ export default class Condition extends EventEmitter {
isTelemetryUsed(id) {
return this.criteria.some(criterion => {
return this.isAnyOrAllTelemetry(criterion) || criterion.usesTelemetry(id);
return this.isAnyOrAllTelemetry(criterion) || criterion.telemetryObjectIdAsString === id;
});
}
@ -271,11 +270,11 @@ export default class Condition extends EventEmitter {
}
}
requestLADConditionResult(options) {
requestLADConditionResult() {
let latestTimestamp;
let criteriaResults = {};
const criteriaRequests = this.criteria
.map(criterion => criterion.requestLAD(this.conditionManager.telemetryObjects, options));
.map(criterion => criterion.requestLAD(this.conditionManager.telemetryObjects));
return Promise.all(criteriaRequests)
.then(results => {

View File

@ -282,7 +282,7 @@ export default class ConditionManager extends EventEmitter {
return currentCondition;
}
requestLADConditionSetOutput(options) {
requestLADConditionSetOutput() {
if (!this.conditions.length) {
return Promise.resolve([]);
}
@ -291,7 +291,7 @@ export default class ConditionManager extends EventEmitter {
let latestTimestamp;
let conditionResults = {};
const conditionRequests = this.conditions
.map(condition => condition.requestLADConditionResult(options));
.map(condition => condition.requestLADConditionResult());
return Promise.all(conditionRequests)
.then((results) => {

View File

@ -40,10 +40,10 @@ export default class ConditionSetTelemetryProvider {
return domainObject.type === 'conditionSet';
}
request(domainObject, options) {
request(domainObject) {
let conditionManager = this.getConditionManager(domainObject);
return conditionManager.requestLADConditionSetOutput(options)
return conditionManager.requestLADConditionSetOutput()
.then(latestOutput => {
return latestOutput;
});
@ -52,9 +52,7 @@ export default class ConditionSetTelemetryProvider {
subscribe(domainObject, callback) {
let conditionManager = this.getConditionManager(domainObject);
conditionManager.on('conditionSetResultUpdated', (data) => {
callback(data);
});
conditionManager.on('conditionSetResultUpdated', callback);
return this.destroyConditionManager.bind(this, this.openmct.objects.makeKeyString(domainObject.identifier));
}

View File

@ -35,7 +35,6 @@ export default class StyleRuleManager extends EventEmitter {
if (styleConfiguration) {
this.initialize(styleConfiguration);
if (styleConfiguration.conditionSetIdentifier) {
this.openmct.time.on("bounds", this.refreshData.bind(this));
this.subscribeToConditionSet();
} else {
this.applyStaticStyle();
@ -84,25 +83,6 @@ export default class StyleRuleManager extends EventEmitter {
});
}
refreshData(bounds, isTick) {
if (!isTick) {
let options = {
start: bounds.start,
end: bounds.end,
size: 1,
strategy: 'latest'
};
this.openmct.objects.get(this.conditionSetIdentifier).then((conditionSetDomainObject) => {
this.openmct.telemetry.request(conditionSetDomainObject, options)
.then(output => {
if (output && output.length) {
this.handleConditionSetResultUpdated(output[0]);
}
});
});
}
}
updateObjectStyleConfig(styleConfiguration) {
if (!styleConfiguration || !styleConfiguration.conditionSetIdentifier) {
this.initialize(styleConfiguration || {});
@ -180,14 +160,10 @@ export default class StyleRuleManager extends EventEmitter {
destroy() {
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
delete this.stopProvidingTelemetry;
}
this.openmct.time.off("bounds", this.refreshData);
this.openmct.editor.off('isEditing', this.toggleSubscription);
this.conditionSetIdentifier = undefined;
}

View File

@ -52,6 +52,7 @@
<div class="c-inspect-styles__content c-inspect-styles__condition-set">
<a v-if="conditionSetDomainObject"
class="c-object-label icon-conditional"
:href="navigateToPath"
@click="navigateOrPreview"
>
<span class="c-object-label__name">{{ conditionSetDomainObject.name }}</span>
@ -285,8 +286,6 @@ export default {
if (this.openmct.editor.isEditing()) {
event.preventDefault();
this.previewAction.invoke(this.objectPath);
} else {
this.openmct.router.navigate(this.navigateToPath);
}
},
removeConditionSet() {

View File

@ -66,6 +66,7 @@
<div class="c-inspect-styles__content c-inspect-styles__condition-set">
<a v-if="conditionSetDomainObject"
class="c-object-label"
:href="navigateToPath"
@click="navigateOrPreview"
>
<span class="c-object-label__type-icon icon-conditional"></span>
@ -308,8 +309,6 @@ export default {
if (this.openmct.editor.isEditing()) {
event.preventDefault();
this.previewAction.invoke(this.objectPath);
} else {
this.openmct.router.navigate(this.navigateToPath);
}
},
isItemType(type, item) {
@ -345,11 +344,6 @@ 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);

View File

@ -147,16 +147,12 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
}
requestLAD(telemetryObjects, requestOptions) {
let options = {
requestLAD(telemetryObjects) {
const options = {
strategy: 'latest',
size: 1
};
if (requestOptions !== undefined) {
options = Object.assign(options, requestOptions);
}
if (!this.isValid()) {
return this.formatData({}, telemetryObjects);
}

View File

@ -58,10 +58,6 @@ export default class TelemetryCriterion extends EventEmitter {
}
}
usesTelemetry(id) {
return this.telemetryObjectIdAsString && (this.telemetryObjectIdAsString === id);
}
subscribeForStaleData() {
if (this.stalenessSubscription) {
this.stalenessSubscription.clear();
@ -137,16 +133,12 @@ export default class TelemetryCriterion extends EventEmitter {
}
}
requestLAD(telemetryObjects, requestOptions) {
let options = {
requestLAD() {
const options = {
strategy: 'latest',
size: 1
};
if (requestOptions !== undefined) {
options = Object.assign(options, requestOptions);
}
if (!this.isValid()) {
return {
id: this.id,

View File

@ -104,7 +104,7 @@ export function getConsolidatedStyleValues(multipleItemStyles) {
const properties = Object.keys(styleProps);
properties.forEach((property) => {
const values = aggregatedStyleValues[property];
if (values && values.length) {
if (values.length) {
if (values.every(value => value === values[0])) {
styleValues[property] = values[0];
} else {

View File

@ -46,7 +46,7 @@ xdescribe("the plugin", () => {
});
afterEach(() => {
return resetApplicationState(openmct);
resetApplicationState(openmct);
});
it('installs the new folder action', () => {

View File

@ -0,0 +1,31 @@
/*****************************************************************************
* 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.
*****************************************************************************/
export default function plugin() {
return function install(openmct) {
openmct.on('start', () => {
const body = document.getElementsByTagName('body')[0];
body.classList.add('desktop');
body.classList.add('portrait');
});
};
}

View File

@ -235,7 +235,7 @@ define(['lodash'], function (_) {
message: `Warning! This action will remove this item from the Display Layout. Do you want to continue?`,
buttons: [
{
label: 'OK',
label: 'Ok',
emphasis: 'true',
callback: function () {
removeItem(getAllTypes(selection));

View File

@ -147,7 +147,7 @@ export default {
this.mutablePromise.then(() => {
this.openmct.objects.destroyMutable(this.domainObject);
});
} else if (this.domainObject.isMutable) {
} else {
this.openmct.objects.destroyMutable(this.domainObject);
}
},

View File

@ -240,7 +240,7 @@ export default {
this.mutablePromise.then(() => {
this.openmct.objects.destroyMutable(this.domainObject);
});
} else if (this.domainObject.isMutable) {
} else {
this.openmct.objects.destroyMutable(this.domainObject);
}
},
@ -269,12 +269,7 @@ export default {
},
subscribeToObject() {
this.subscription = this.openmct.telemetry.subscribe(this.domainObject, function (datum) {
const key = this.openmct.time.timeSystem().key;
const datumTimeStamp = datum[key];
if (this.openmct.time.clock() !== undefined
|| (datumTimeStamp
&& (this.openmct.time.bounds().end >= datumTimeStamp))
) {
if (this.openmct.time.clock() !== undefined) {
this.updateView(datum);
}
}.bind(this));

View File

@ -112,7 +112,7 @@ describe("The Duplicate Action plugin", () => {
});
afterEach(() => {
return resetApplicationState(openmct);
resetApplicationState(openmct);
});
it("should be defined", () => {

View File

@ -90,12 +90,14 @@ export default {
this.composition.load();
this.unobserve = this.openmct.objects.observe(this.providedObject, 'configuration.filters', this.updatePersistedFilters);
this.unobserveGlobalFilters = this.openmct.objects.observe(this.providedObject, 'configuration.globalFilters', this.updateGlobalFilters);
this.unobserveAllMutation = this.openmct.objects.observe(this.providedObject, '*', (mutatedObject) => this.providedObject = mutatedObject);
},
beforeDestroy() {
this.composition.off('add', this.addChildren);
this.composition.off('remove', this.removeChildren);
this.unobserve();
this.unobserveGlobalFilters();
this.unobserveAllMutation();
},
methods: {
addChildren(domainObject) {
@ -156,28 +158,25 @@ export default {
},
getGlobalFiltersToRemove(keyString) {
let filtersToRemove = new Set();
const child = this.children[keyString];
if (child && child.metadataWithFilters) {
const metadataWithFilters = child.metadataWithFilters;
metadataWithFilters.forEach(metadatum => {
let keepFilter = false;
Object.keys(this.children).forEach(childKeyString => {
if (childKeyString !== keyString) {
let filterMatched = this.children[childKeyString].metadataWithFilters.some(childMetadatum => childMetadatum.key === metadatum.key);
if (filterMatched) {
keepFilter = true;
this.children[keyString].metadataWithFilters.forEach(metadatum => {
let keepFilter = false;
Object.keys(this.children).forEach(childKeyString => {
if (childKeyString !== keyString) {
let filterMatched = this.children[childKeyString].metadataWithFilters.some(childMetadatum => childMetadatum.key === metadatum.key);
return;
}
if (filterMatched) {
keepFilter = true;
return;
}
});
if (!keepFilter) {
filtersToRemove.add(metadatum.key);
}
});
}
if (!keepFilter) {
filtersToRemove.add(metadatum.key);
}
});
return Array.from(filtersToRemove);
},

View File

@ -97,7 +97,7 @@ function ToolbarProvider(openmct) {
message: `This action will remove this frame from this Flexible Layout. Do you want to continue?`,
buttons: [
{
label: 'OK',
label: 'Ok',
emphasis: 'true',
callback: function () {
deleteFrameAction(primary.context.frameId);
@ -162,7 +162,7 @@ function ToolbarProvider(openmct) {
message: 'This action will permanently delete this container from this Flexible Layout',
buttons: [
{
label: 'OK',
label: 'Ok',
emphasis: 'true',
callback: function () {
removeContainer(containerId);

View File

@ -5,7 +5,7 @@
'is-alias': item.isAlias === true,
'c-grid-item--unknown': item.type.cssClass === undefined || item.type.cssClass.indexOf('unknown') !== -1
}, statusClass]"
@click="navigate"
:href="objectLink"
>
<div
class="c-grid-item__type-icon"
@ -49,17 +49,11 @@ import statusListener from './status-listener';
export default {
mixins: [contextMenuGesture, objectLink, statusListener],
inject: ['openmct'],
props: {
item: {
type: Object,
required: true
}
},
methods: {
navigate() {
this.openmct.router.navigate(this.objectLink);
}
}
};
</script>

View File

@ -11,7 +11,7 @@
ref="objectLink"
class="c-object-label"
:class="[statusClass]"
@click="navigate"
:href="objectLink"
>
<div
class="c-object-label__type-icon c-list-item__name__type-icon"
@ -45,7 +45,6 @@ import statusListener from './status-listener';
export default {
mixins: [contextMenuGesture, objectLink, statusListener],
inject: ['openmct'],
props: {
item: {
type: Object,
@ -57,7 +56,7 @@ export default {
return moment(timestamp).format(format);
},
navigate() {
this.openmct.router.navigate(this.objectLink);
this.$refs.objectLink.click();
}
}
};

View File

@ -29,6 +29,13 @@ define([
) {
return function plugin() {
return function install(openmct) {
openmct.types.addType('folder', {
name: "Folder",
key: "folder",
description: "Create folders to organize other objects or links to objects without the ability to edit it's properties.",
cssClass: "icon-folder",
creatable: true
});
openmct.objectViews.addProvider(new FolderGridView(openmct));
openmct.objectViews.addProvider(new FolderListView(openmct));
};

View File

@ -41,7 +41,7 @@ export default class GoToOriginalAction {
.slice(1)
.join('/');
this._openmct.router.navigate(url);
window.location.href = url;
});
}
appliesTo(objectPath) {

View File

@ -47,6 +47,7 @@ describe("the plugin", () => {
});
describe('when invoked', () => {
beforeEach(() => {
mockObjectPath = [{
name: 'mock folder',
@ -62,15 +63,11 @@ describe("the plugin", () => {
key: 'test'
}
}));
goToFolderAction.invoke(mockObjectPath);
});
it('goes to the original location', (done) => {
setTimeout(() => {
expect(window.location.href).toContain('context.html#/browse/?tc.mode=fixed&tc.startBound=0&tc.endBound=1&tc.timeSystem=utc');
done();
}, 2500);
it('goes to the original location', () => {
expect(window.location.href).toContain('context.html#/browse/?tc.mode=fixed&tc.startBound=0&tc.endBound=1&tc.timeSystem=utc');
});
});
});

View File

@ -23,7 +23,7 @@
<template>
<div
class="c-compass"
:style="`width: ${ sizedImageDimensions.width }px; height: ${ sizedImageDimensions.height }px`"
:style="compassDimensionsStyle"
>
<CompassHUD
v-if="hasCameraFieldOfView"
@ -32,9 +32,8 @@
:camera-pan="cameraPan"
/>
<CompassRose
v-if="true"
v-if="hasCameraFieldOfView"
:heading="heading"
:sized-image-width="sizedImageDimensions.width"
:sun-heading="sunHeading"
:camera-angle-of-view="cameraAngleOfView"
:camera-pan="cameraPan"
@ -78,20 +77,6 @@ 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;
},
@ -109,6 +94,25 @@ 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: {

View File

@ -22,134 +22,129 @@
<template>
<div
class="w-direction-rose"
:class="compassRoseSizingClasses"
class="c-direction-rose"
@click="toggleLockCompass"
>
<div
class="c-direction-rose"
@click="toggleLockCompass"
class="c-nsew"
:style="compassRoseStyle"
>
<div
class="c-nsew"
:style="compassRoseStyle"
<svg
class="c-nsew__minor-ticks"
viewBox="0 0 100 100"
>
<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"
/>
<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 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"
/>
<svg
class="c-nsew__ticks"
viewBox="0 0 100 100"
>
<polygon
class="c-nsew__tick c-tick-n"
points="50,0 57,5 43,5"
/>
<rect
class="c-nsew__tick c-tick-e"
x="95"
y="49"
width="5"
height="2"
/>
<rect
class="c-nsew__tick c-tick-w"
x="0"
y="49"
width="5"
height="2"
/>
<rect
class="c-nsew__tick c-tick-s"
x="49"
y="95"
width="2"
height="5"
/>
<text
class="c-nsew__label c-label-n"
text-anchor="middle"
:transform="northTextTransform"
>N</text>
<text
class="c-nsew__label c-label-e"
text-anchor="middle"
:transform="eastTextTransform"
>E</text>
<text
class="c-nsew__label c-label-w"
text-anchor="middle"
:transform="southTextTransform"
>W</text>
<text
class="c-nsew__label c-label-s"
text-anchor="middle"
:transform="westTextTransform"
>S</text>
</svg>
<text
class="c-nsew__label c-label-n"
text-anchor="middle"
:transform="northTextTransform"
>N</text>
<text
class="c-nsew__label c-label-e"
text-anchor="middle"
:transform="eastTextTransform"
>E</text>
<text
class="c-nsew__label c-label-w"
text-anchor="middle"
:transform="southTextTransform"
>W</text>
<text
class="c-nsew__label c-label-s"
text-anchor="middle"
:transform="westTextTransform"
>S</text>
</svg>
</div>
<div
v-if="hasHeading"
class="c-spacecraft-body"
:style="headingStyle"
>
</div>
<div
v-if="hasSunHeading"
class="c-sun"
:style="sunHeadingStyle"
></div>
<div
class="c-cam-field"
:style="cameraPanStyle"
>
<div class="cam-field-half cam-field-half-l">
<div
class="cam-field-area"
:style="cameraFOVStyleLeftHalf"
></div>
</div>
<div
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 class="cam-field-half cam-field-half-r">
<div
class="cam-field-area"
:style="cameraFOVStyleRightHalf"
></div>
</div>
</div>
</div>
@ -160,10 +155,6 @@ import { rotate } from './utils';
export default {
props: {
sizedImageWidth: {
type: Number,
required: true
},
heading: {
type: Number,
required: true
@ -186,24 +177,12 @@ export default {
}
},
computed: {
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;
north() {
return this.lockCompass ? rotate(-this.cameraPan) : 0;
},
compassRoseStyle() {
return { transform: `rotate(${ this.north }deg)` };
},
north() {
return this.lockCompass ? rotate(-this.cameraPan) : 0;
},
northTextTransform() {
return this.cardinalPointsTextTransform.north;
},
@ -225,10 +204,10 @@ export default {
const rotation = `rotate(${ -this.north })`;
return {
north: `translate(50,23) ${ rotation }`,
east: `translate(82,50) ${ rotation }`,
south: `translate(18,50) ${ rotation }`,
west: `translate(50,82) ${ rotation }`
north: `translate(50,15) ${ rotation }`,
east: `translate(87,50) ${ rotation }`,
south: `translate(13,50) ${ rotation }`,
west: `translate(50,87) ${ rotation }`
};
},
hasHeading() {

View File

@ -10,7 +10,6 @@ $elemBg: rgba(black, 0.7);
}
.c-compass {
pointer-events: none; // This allows the image element to receive a browser-level context click
position: absolute;
left: 50%;
top: 50%;
@ -21,253 +20,195 @@ $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;
// To be placed within a imagery view, in the bounding box of the image
$m: 1px;
$padTB: 2px;
$padLR: $padTB;
color: $interfaceKeyColor;
font-size: 0.8em;
position: absolute;
top: $m; right: $m; left: $m;
height: 18px;
svg, div {
position: absolute;
top: $m;
right: $m;
left: $m;
height: 18px;
}
svg, div {
position: absolute;
}
&__display {
height: 30px;
pointer-events: all;
position: absolute;
top: 0;
right: 0;
left: 0;
}
&__display {
height: 30px;
pointer-events: all;
position: absolute;
top: 0;
right: 0;
left: 0;
}
&__range {
border: 1px solid $interfaceKeyColor;
border-top-color: transparent;
position: absolute;
top: 50%; right: $padLR; bottom: $padTB; left: $padLR;
}
&__range {
border: 1px solid $interfaceKeyColor;
border-top-color: transparent;
position: absolute;
top: 50%;
right: $padLR;
bottom: $padTB;
left: $padLR;
}
[class*="__dir"] {
// NSEW
display: inline-block;
font-weight: bold;
text-shadow: 0 1px 2px black;
top: 50%;
transform: translate(-50%,-50%);
z-index: 2;
}
[class*="__dir"] {
// NSEW
display: inline-block;
font-weight: bold;
text-shadow: 0 1px 2px black;
top: 50%;
transform: translate(-50%, -50%);
z-index: 2;
}
[class*="__dir--sub"] {
font-weight: normal;
opacity: 0.5;
}
[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;
}
&__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: 5%;
$tickHeightPerc: 15%;
text-shadow: black 0 0 10px;
top: $inset;
right: $inset;
bottom: $inset;
left: $inset;
z-index: 3;
$color: $interfaceKeyColor;
$inset: 7%;
$tickHeightPerc: 15%;
text-shadow: black 0 0 10px;
top: $inset; right: $inset; bottom: $inset; left: $inset;
z-index: 3;
&__tick,
&__label {
fill: $color;
}
&__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: 1.25em;
font-weight: bold;
}
&__label {
dominant-baseline: central;
font-size: 0.8em;
font-weight: bold;
}
.c-label-n {
font-size: 2em;
}
.c-label-n {
font-size: 1.1em;
}
}
/***************************** CAMERA FIELD ANGLE */
.c-cam-field {
$color: white;
opacity: 0.3;
$color: white;
opacity: 0.2;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
.cam-field-half {
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
.cam-field-half {
top: 0;
right: 0;
bottom: 0;
left: 0;
.cam-field-area {
background: $color;
top: -30%;
right: 0;
bottom: -30%;
left: 0;
}
// clip-paths overlap a bit to avoid a gap between halves
&-l {
clip-path: polygon(0 0, 50.5% 0, 50.5% 100%, 0 100%);
.cam-field-area {
transform-origin: left center;
}
}
&-r {
clip-path: polygon(49.5% 0, 100% 0, 100% 100%, 49.5% 100%);
.cam-field-area {
transform-origin: right center;
}
}
.cam-field-area {
background: $color;
top: -30%;
right: 0;
bottom: -30%;
left: 0;
}
// clip-paths overlap a bit to avoid a gap between halves
&-l {
clip-path: polygon(0 0, 50.5% 0, 50.5% 100%, 0 100%);
.cam-field-area {
transform-origin: left center;
}
}
&-r {
clip-path: polygon(49.5% 0, 100% 0, 100% 100%, 49.5% 100%);
.cam-field-area {
transform-origin: right center;
}
}
}
}
/***************************** SPACECRAFT BODY */
.c-spacecraft-body {
$color: $interfaceKeyColor;
$s: 30%;
background: $color;
border-radius: 3px;
height: $s;
width: $s;
left: 50%;
top: 50%;
opacity: 0.4;
transform-origin: center top;
transform: translateX(-50%); // center by default, overridden by CompassRose.vue / headingStyle()
$color: $interfaceKeyColor;
$s: 30%;
background: $color;
border-radius: 3px;
height: $s; width: $s;
left: 50%; top: 50%;
opacity: 0.4;
transform-origin: center top;
&:before {
// Direction arrow
$color: rgba(black, 0.5);
$arwPointerY: 60%;
$arwBodyOffset: 25%;
background: $color;
content: '';
display: block;
position: absolute;
top: 10%;
right: 20%;
bottom: 50%;
left: 20%;
clip-path: polygon(50% 0, 100% $arwPointerY, 100%-$arwBodyOffset $arwPointerY, 100%-$arwBodyOffset 100%, $arwBodyOffset 100%, $arwBodyOffset $arwPointerY, 0 $arwPointerY);
}
&: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 */
.w-direction-rose {
$s: 10%;
$m: 2%;
position: absolute;
bottom: $m;
left: $m;
width: $s;
padding-top: $s;
&.--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;
$d: 100px;
$c2: rgba(white, 0.1);
background: $elemBg;
background-image: radial-gradient(circle closest-side, transparent, transparent 80%, $c2);
width: $d;
height: $d;
transform-origin: 0 0;
position: absolute;
bottom: 10px; left: 10px;
clip-path: circle(50% at 50% 50%);
border-radius: 100%;
svg, div {
position: absolute;
}
// Sun
.c-sun {
top: 0;
right: 0;
bottom: 0;
left: 0;
clip-path: circle(50% at 50% 50%);
border-radius: 100%;
pointer-events: all;
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%);
}
&: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%);
}
}
}

View File

@ -135,14 +135,9 @@
:class="{ selected: focusedImageIndex === index && isPaused }"
@click="setFocusedImage(index, thumbnailClick)"
>
<a href=""
:download="image.imageDownloadName"
@click.prevent
<img class="c-thumb__image"
:src="image.url"
>
<img class="c-thumb__image"
:src="image.url"
>
</a>
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
</div>
</div>
@ -223,9 +218,6 @@ export default {
canTrackDuration() {
return this.openmct.time.clock() && this.timeSystem.isUTCBased;
},
focusedImageDownloadName() {
return this.getImageDownloadName(this.focusedImage);
},
isNextDisabled() {
let disabled = false;
@ -353,7 +345,6 @@ 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'];
@ -390,9 +381,7 @@ export default {
delete this.unsubscribe;
}
if (this.imageContainerResizeObserver) {
this.imageContainerResizeObserver.disconnect();
}
this.imageContainerResizeObserver.disconnect();
if (this.relatedTelemetry.hasRelatedTelemetry) {
this.relatedTelemetry.destroy();
@ -543,15 +532,6 @@ 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;
@ -675,7 +655,6 @@ 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);
@ -704,7 +683,7 @@ export default {
window.clearInterval(this.durationTracker);
},
updateDuration() {
let currentTime = this.openmct.time.clock() && this.openmct.time.clock().currentValue();
let currentTime = this.openmct.time.clock().currentValue();
this.numericDuration = currentTime - this.parsedSelectedTime;
},
resetAgeCSS() {
@ -798,9 +777,6 @@ export default {
this.focusedImageNaturalAspectRatio = undefined;
const img = this.$refs.focusedImage;
if (!img) {
return;
}
// TODO - should probably cache this
img.addEventListener('load', () => {

View File

@ -19,7 +19,7 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ImageryPlugin from './plugin.js';
import Vue from 'vue';
import {
createOpenMct,
@ -89,11 +89,15 @@ describe("The Imagery View Layout", () => {
const START = Date.now();
const COUNT = 10;
let resolveFunction;
let openmct;
let imageryPlugin;
let parent;
let child;
let timeFormat = 'utc';
let bounds = {
start: START - TEN_MINUTES,
end: START
};
let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT);
let imageryObject = {
identifier: {
@ -201,10 +205,6 @@ describe("The Imagery View Layout", () => {
openmct = createOpenMct();
openmct.install(openmct.plugins.MyItems());
openmct.install(openmct.plugins.LocalTimeSystem());
openmct.install(openmct.plugins.UTCTimeSystem());
parent = document.createElement('div');
child = document.createElement('div');
parent.appendChild(child);
@ -215,18 +215,22 @@ describe("The Imagery View Layout", () => {
});
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
imageryPlugin = new ImageryPlugin();
openmct.install(imageryPlugin);
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
openmct.time.timeSystem(timeFormat, {
start: 0,
end: 4
});
openmct.on('start', done);
openmct.start(appHolder);
openmct.startHeadless(appHolder);
});
afterEach(() => {
openmct.time.timeSystem('utc', {
start: 0,
end: 1
});
return resetApplicationState(openmct);
});
@ -244,7 +248,7 @@ describe("The Imagery View Layout", () => {
let imageryViewProvider;
let imageryView;
beforeEach(async () => {
beforeEach(async (done) => {
let telemetryRequestResolve;
let telemetryRequestPromise = new Promise((resolve) => {
telemetryRequestResolve = resolve;
@ -256,18 +260,23 @@ describe("The Imagery View Layout", () => {
return telemetryRequestPromise;
});
openmct.time.clock('local', {
start: bounds.start,
end: bounds.end + 100
});
applicableViews = openmct.objectViews.get(imageryObject, []);
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey);
imageryView = imageryViewProvider.view(imageryObject);
imageryView.show(child);
await telemetryRequestPromise;
await Vue.nextTick();
return done();
});
afterEach(() => {
openmct.time.stopClock();
openmct.router.removeListener('change:hash', resolveFunction);
imageryView.destroy();
});
@ -277,44 +286,43 @@ describe("The Imagery View Layout", () => {
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
});
it("should show the clicked thumbnail as the main image", (done) => {
it("should show the clicked thumbnail as the main image", async () => {
const target = imageTelemetry[5].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
Vue.nextTick(() => {
const imageInfo = getImageInfo(parent);
await Vue.nextTick();
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);
expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);
});
it("should show that an image is new", async (done) => {
await Vue.nextTick();
// used in code, need to wait to the 500ms here too
setTimeout(() => {
const imageIsNew = isNew(parent);
expect(imageIsNew).toBeTrue();
done();
});
}, REFRESH_CSS_MS);
});
xit("should show that an image is new", (done) => {
Vue.nextTick(() => {
// used in code, need to wait to the 500ms here too
setTimeout(() => {
const imageIsNew = isNew(parent);
expect(imageIsNew).toBeTrue();
done();
}, REFRESH_CSS_MS);
});
});
xit("should show that an image is not new", (done) => {
it("should show that an image is not new", async (done) => {
const target = imageTelemetry[2].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
Vue.nextTick(() => {
// used in code, need to wait to the 500ms here too
setTimeout(() => {
const imageIsNew = isNew(parent);
await Vue.nextTick();
expect(imageIsNew).toBeFalse();
done();
}, REFRESH_CSS_MS);
});
// used in code, need to wait to the 500ms here too
setTimeout(() => {
const imageIsNew = isNew(parent);
expect(imageIsNew).toBeFalse();
done();
}, REFRESH_CSS_MS);
});
it("should navigate via arrow keys", (done) => {
it("should navigate via arrow keys", async () => {
let keyOpts = {
element: parent.querySelector('.c-imagery'),
key: 'ArrowLeft',
@ -324,15 +332,14 @@ describe("The Imagery View Layout", () => {
simulateKeyEvent(keyOpts);
Vue.nextTick(() => {
const imageInfo = getImageInfo(parent);
await Vue.nextTick();
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);
done();
});
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);
});
it("should navigate via numerous arrow keys", (done) => {
it("should navigate via numerous arrow keys", async () => {
let element = parent.querySelector('.c-imagery');
let type = 'keyup';
let leftKeyOpts = {
@ -355,12 +362,12 @@ describe("The Imagery View Layout", () => {
// right once
simulateKeyEvent(rightKeyOpts);
Vue.nextTick(() => {
const imageInfo = getImageInfo(parent);
await Vue.nextTick();
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);
done();
});
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);
});
});
});

View File

@ -3,6 +3,10 @@ import myItemsInterceptor from "./myItemsInterceptor";
export default function plugin() {
return function install(openmct) {
openmct.objects.addRoot({
namespace: '',
key: 'mine'
});
myItemsInterceptor(openmct);
missingObjectInterceptor(openmct);
};

View File

@ -0,0 +1,41 @@
export default class LocalStorageObjectProvider {
constructor({spaceKey = 'mct'}) {
this.localStorage = window.localStorage;
this.space = this.initializeSpace(spaceKey);
}
get(identifier) {
if (this.getSpaceAsObject()[identifier.key] !== undefined) {
const persistedModel = this.getSpaceAsObject()[identifier.key];
const domainObject = {
identifier,
...persistedModel
};
return Promise.resolve(domainObject);
} else {
return Promise.resolve(undefined);
}
}
getSpaceAsObject() {
return JSON.parse(this.space);
}
create(model) {
return this.setModel(model);
}
update(model) {
return this.setModel(model);
}
setModel(model) {
this.space[model.identifier.key] = JSON.stringify(model);
this.persist();
return Promise.resolve(true);
}
initializeSpace(spaceKey) {
if (this.localStorage[spaceKey] === undefined) {
this.localStorage[spaceKey] = JSON.stringify({});
}
return this.localStorage[spaceKey];
}
}

View File

@ -0,0 +1,29 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import LocalStorageObjectProvider from './LocalStorageObjectProvider';
export default function (namespace = '', storageSpace = 'mct') {
return function (openmct) {
openmct.objects.addProvider(namespace, new LocalStorageObjectProvider(storageSpace));
};
}

View File

@ -55,7 +55,7 @@ describe("The local time", () => {
beforeEach(() => {
localTimeSystem = openmct.time.timeSystem(LOCAL_SYSTEM_KEY, {
start: 0,
end: 1
end: 4
});
});

View File

@ -81,7 +81,7 @@ describe("The Move Action plugin", () => {
});
afterEach(() => {
return resetApplicationState(openmct);
resetApplicationState(openmct);
});
it("should be defined", () => {

View File

@ -135,7 +135,6 @@ import SearchResults from './SearchResults.vue';
import Sidebar from './Sidebar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage';
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
import { NOTEBOOK_VIEW_TYPE } from '../notebook-constants';
import objectUtils from 'objectUtils';
import { debounce } from 'lodash';
@ -190,14 +189,14 @@ export default {
selectedPage() {
const pages = this.getPages();
if (!pages) {
return {};
return null;
}
return pages.find(page => page.isSelected);
},
selectedSection() {
if (!this.sections.length) {
return {};
return null;
}
return this.sections.find(section => section.isSelected);
@ -217,7 +216,6 @@ export default {
window.addEventListener('orientationchange', this.formatSidebar);
window.addEventListener("hashchange", this.navigateToSectionPage, false);
this.openmct.router.on('change:params', this.changeSectionPage);
this.navigateToSectionPage();
},
@ -228,7 +226,6 @@ export default {
window.removeEventListener('orientationchange', this.formatSidebar);
window.removeEventListener("hashchange", this.navigateToSectionPage);
this.openmct.router.off('change:params', this.changeSectionPage);
},
updated: function () {
this.$nextTick(() => {
@ -236,28 +233,6 @@ export default {
});
},
methods: {
changeSectionPage(newParams, oldParams, changedParams) {
if (newParams.view !== NOTEBOOK_VIEW_TYPE) {
return;
}
let pageId = newParams.pageId;
let sectionId = newParams.sectionId;
if (!pageId && !sectionId) {
return;
}
this.sections.forEach(section => {
section.isSelected = Boolean(section.id === sectionId);
if (section.isSelected) {
section.pages.forEach(page => {
page.isSelected = Boolean(page.id === pageId);
});
}
});
},
changeSelectedSection({ sectionId, pageId }) {
const sections = this.sections.map(s => {
s.isSelected = false;
@ -455,7 +430,7 @@ export default {
}
// check for no entries first
if (entries[section.id] && entries[section.id][page.id]) {
if (entries[section.id]) {
const pageEntries = entries[section.id][page.id];
pageEntries.forEach(entry => {
@ -543,11 +518,9 @@ export default {
return this.sections.find(section => section.isSelected);
},
navigateToSectionPage() {
let { pageId, sectionId } = this.openmct.router.getParams();
const { pageId, sectionId } = this.openmct.router.getParams();
if (!pageId || !sectionId) {
sectionId = this.selectedSection.id;
pageId = this.selectedPage.id;
return;
}
const sections = this.sections.map(s => {

View File

@ -145,7 +145,7 @@ export default {
const relativeHash = hash.slice(hash.indexOf('#'));
const url = new URL(relativeHash, `${location.protocol}//${location.host}${location.pathname}`);
this.openmct.router.navigate(url.hash);
window.location.hash = url.hash;
},
formatTime(unixTime, timeFormat) {
return Moment.utc(unixTime).format(timeFormat);

View File

@ -22,7 +22,7 @@
<template>
<div class="c-notebook__search-results">
<div class="c-notebook__search-results__header">Search Results ({{ results.length }})</div>
<div class="c-notebook__search-results__header">Search Results</div>
<div class="c-notebook__entries">
<NotebookEntry v-for="(result, index) in results"
:key="index"

View File

@ -111,6 +111,10 @@ export default {
}
}
},
data() {
return {
};
},
computed: {
pages() {
const selectedSection = this.sections.find(section => section.isSelected);

View File

@ -1,4 +1,3 @@
export const EVENT_SNAPSHOTS_UPDATED = 'SNAPSHOTS_UPDATED';
export const NOTEBOOK_DEFAULT = 'DEFAULT';
export const NOTEBOOK_SNAPSHOT = 'SNAPSHOT';
export const NOTEBOOK_VIEW_TYPE = 'notebook-vue';

View File

@ -65,8 +65,7 @@ describe("Notebook plugin:", () => {
afterAll(() => {
appHolder.remove();
return resetApplicationState(openmct);
resetApplicationState(openmct);
});
it("has type as Notebook", () => {

View File

@ -140,8 +140,7 @@ describe('Notebook Entries:', () => {
afterEach(() => {
notebookDomainObject.configuration.entries[selectedSection.id][selectedPage.id] = [];
return resetApplicationState(openmct);
resetApplicationState(openmct);
});
it('getNotebookEntries has no entries', () => {

View File

@ -83,7 +83,7 @@ describe('Notebook Storage:', () => {
});
afterEach(() => {
return resetApplicationState(openmct);
resetApplicationState(openmct);
});
it('has empty local Storage', () => {

View File

@ -26,7 +26,6 @@ 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 {
@ -42,8 +41,6 @@ export default class CouchObjectProvider {
this.objectQueue = {};
this.observeEnabled = options.disableObserve !== true;
this.observers = {};
this.batchIds = [];
if (this.observeEnabled) {
this.observeObjectChanges(options.filter);
}
@ -70,9 +67,6 @@ 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)
@ -84,18 +78,14 @@ 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
* @private
*/
checkResponse(response, intermediateResponse, key) {
// 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
checkResponse(response, intermediateResponse) {
let requestSuccess = false;
const id = response ? response.id : undefined;
let rev;
if (response && response.ok) {
rev = response.rev;
requestSuccess = true;
@ -113,14 +103,9 @@ export default class CouchObjectProvider {
if (this.objectQueue[id].hasNext()) {
this.updateQueued(id);
}
} else {
this.objectQueue[key].pending = false;
}
}
/**
* @private
*/
getModel(response) {
if (response && response.model) {
let key = response[ID];
@ -134,7 +119,8 @@ export default class CouchObjectProvider {
}
//Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress
if (!this.objectQueue[key].pending) {
//Only update the rev if it's the first time we're getting the object from CouchDB. Subsequent revs should only be updated by updates.
if (!this.objectQueue[key].pending && !this.objectQueue[key].rev) {
this.objectQueue[key].updateRevision(response[REV]);
}
@ -145,118 +131,10 @@ export default class CouchObjectProvider {
}
get(identifier, abortSignal) {
this.batchIds.push(identifier.key);
if (this.bulkPromise === undefined) {
this.bulkPromise = this.deferBatchedGet(abortSignal);
}
return this.bulkPromise
.then((domainObjectMap) => {
return domainObjectMap[identifier.key];
});
return this.request(identifier.key, "GET", undefined, abortSignal).then(this.getModel.bind(this));
}
/**
* @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) {
async getObjectsByFilter(filter) {
let objects = [];
let url = `${this.url}/_find`;
@ -271,7 +149,6 @@ export default class CouchObjectProvider {
headers: {
"Content-Type": "application/json"
},
signal: abortSignal,
body
});
@ -326,9 +203,6 @@ export default class CouchObjectProvider {
};
}
/**
* @private
*/
abortGetChanges() {
if (this.controller) {
this.controller.abort();
@ -338,9 +212,6 @@ export default class CouchObjectProvider {
return true;
}
/**
* @private
*/
async observeObjectChanges(filter) {
let intermediateResponse = this.getIntermediateResponse();
@ -421,9 +292,6 @@ export default class CouchObjectProvider {
}
/**
* @private
*/
getIntermediateResponse() {
let intermediateResponse = {};
intermediateResponse.promise = new Promise(function (resolve, reject) {
@ -434,9 +302,6 @@ export default class CouchObjectProvider {
return intermediateResponse;
}
/**
* @private
*/
enqueueObject(key, model, intermediateResponse) {
if (this.objectQueue[key]) {
this.objectQueue[key].enqueue({
@ -459,22 +324,19 @@ export default class CouchObjectProvider {
const queued = this.objectQueue[key].dequeue();
let document = new CouchDocument(key, queued.model);
this.request(key, "PUT", document).then((response) => {
this.checkResponse(response, queued.intermediateResponse, key);
this.checkResponse(response, queued.intermediateResponse);
});
return intermediateResponse.promise;
}
/**
* @private
*/
updateQueued(key) {
if (!this.objectQueue[key].pending) {
this.objectQueue[key].pending = true;
const queued = this.objectQueue[key].dequeue();
let document = new CouchDocument(key, queued.model, this.objectQueue[key].rev);
this.request(key, "PUT", document).then((response) => {
this.checkResponse(response, queued.intermediateResponse, key);
this.checkResponse(response, queued.intermediateResponse);
});
}
}

View File

@ -24,6 +24,7 @@ import {
createOpenMct,
resetApplicationState, spyOnBuiltins
} from 'utils/testing';
import CouchObjectProvider from './CouchObjectProvider';
describe('the plugin', () => {
let openmct;
@ -41,8 +42,7 @@ describe('the plugin', () => {
namespace: '',
key: 'some-value'
},
type: 'mock-type',
modified: 0
type: 'mock-type'
};
options = {
url: testPath,
@ -95,7 +95,6 @@ describe('the plugin', () => {
return {
ok: true,
_id: 'some-value',
id: 'some-value',
_rev: 1,
model: {}
};
@ -105,131 +104,44 @@ describe('the plugin', () => {
});
it('gets an object', () => {
return openmct.objects.get(mockDomainObject.identifier).then((result) => {
openmct.objects.get(mockDomainObject.identifier).then((result) => {
expect(result.identifier.key).toEqual(mockDomainObject.identifier.key);
});
});
it('creates an object', () => {
return openmct.objects.save(mockDomainObject).then((result) => {
openmct.objects.save(mockDomainObject).then((result) => {
expect(provider.create).toHaveBeenCalled();
expect(result).toBeTrue();
});
});
it('updates an object', (done) => {
return openmct.objects.save(mockDomainObject).then((result) => {
it('updates an object', () => {
openmct.objects.save(mockDomainObject).then((result) => {
expect(result).toBeTrue();
expect(provider.create).toHaveBeenCalled();
//Set modified timestamp it detects a change and persists the updated model.
mockDomainObject.modified = Date.now();
return openmct.objects.save(mockDomainObject).then((updatedResult) => {
openmct.objects.save(mockDomainObject).then((updatedResult) => {
expect(updatedResult).toBeTrue();
expect(provider.update).toHaveBeenCalled();
done();
});
});
});
});
describe('batches requests', () => {
let mockPromise;
beforeEach(() => {
mockPromise = Promise.resolve({
json: () => {
return {
total_rows: 0,
rows: []
};
}
});
fetch.and.returnValue(mockPromise);
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);
expect(couchProvider.updateQueued).toHaveBeenCalledTimes(2);
});
it('for multiple simultaneous gets', () => {
const objectIds = [
{
namespace: '',
key: 'object-1'
}, {
namespace: '',
key: 'object-2'
}, {
namespace: '',
key: 'object-3'
}
];
const getAllObjects = Promise.all(
objectIds.map((identifier) =>
openmct.objects.get(identifier)
));
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');
});
});
});
});

View File

@ -1,64 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div>
<div v-if="canEdit">
<plot-options-edit />
</div>
<div v-else>
<plot-options-browse />
</div>
</div>
</template>
<script>
import PlotOptionsBrowse from "./PlotOptionsBrowse.vue";
import PlotOptionsEdit from "./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>

View File

@ -1,201 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div v-if="loaded"
class="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 v-if="plotSeries.length"
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 "../configuration/configStore";
import eventHelpers from "../lib/eventHelpers";
export default {
components: {
PlotOptionsItem
},
inject: ['openmct', 'domainObject'],
data() {
return {
config: {},
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.registerListeners();
this.initConfiguration();
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.$set(this.plotSeries, index, series);
this.initConfiguration();
},
resetAllSeries() {
this.plotSeries = [];
this.config.series.forEach(this.addSeries, this);
}
}
};
</script>

View File

@ -1,100 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div v-if="loaded"
class="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-if="plotSeries.length"
class="grid-properties"
:y-axis="config.yAxis"
/>
<ul class="l-inspector-part">
<h2 title="Legend options">Legend</h2>
<legend-form v-if="plotSeries.length"
class="grid-properties"
:legend="config.legend"
/>
</ul>
</div>
</template>
<script>
import SeriesForm from "./forms/SeriesForm.vue";
import YAxisForm from "./forms/YAxisForm.vue";
import LegendForm from "./forms/LegendForm.vue";
import eventHelpers from "../lib/eventHelpers";
import configStore from "../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.$set(this.plotSeries, index, series);
},
resetAllSeries() {
this.plotSeries = [];
this.config.series.forEach(this.addSeries, this);
}
}
};
</script>

View File

@ -1,160 +0,0 @@
<template>
<ul>
<li class="c-tree__item menus-to-left"
:class="isAliasClass"
>
<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', 'domainObject', 'path'],
props: {
series: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
expanded: false
};
},
computed: {
isAliasClass() {
let cssClass = '';
const domainObjectPath = [this.series.domainObject, ...this.path];
if (this.openmct.objects.isObjectPathToALink(this.series.domainObject, domainObjectPath)) {
cssClass = 'is-alias';
}
return cssClass;
},
getSeriesClass() {
let cssClass = '';
let type = this.openmct.types.get(this.series.domainObject.type);
if (type.definition.cssClass) {
cssClass = `${cssClass} ${type.definition.cssClass}`;
}
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>

View File

@ -1,56 +0,0 @@
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 object = selection[0][0].context.item;
return object
&& object.type === 'telemetry.plot.overlay';
},
view: function (selection) {
let component;
let objectPath;
if (selection.length) {
objectPath = selection[0].map((selectionItem) => {
return selectionItem.context.item;
});
}
return {
show: function (element) {
component = new Vue({
el: element,
components: {
PlotOptions: PlotOptions
},
provide: {
openmct,
domainObject: selection[0][0].context.item,
path: objectPath
},
template: '<plot-options></plot-options>'
});
},
destroy: function () {
if (component) {
component.$destroy();
component = undefined;
}
}
};
},
priority: function () {
return 1;
}
};
}

View File

@ -1,207 +0,0 @@
<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 "./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>

View File

@ -1,347 +0,0 @@
<template>
<ul>
<li class="c-tree__item menus-to-left"
:class="isAliasCss"
>
<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]"
>
<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 "../../draw/MarkerShapes";
import { objectPath, validate, coerce } from "./formUtil";
import _ from 'lodash';
export default {
inject: ['openmct', 'domainObject', 'path'],
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 type = this.openmct.types.get(this.series.domainObject.type);
return type.definition.cssClass ? `c-object-label__type-icon ${type.definition.cssClass}` : `c-object-label__type-icon`;
},
isAliasCss() {
let cssClass = '';
const domainObjectPath = [this.series.domainObject, ...this.path];
if (this.openmct.objects.isObjectPathToALink(this.series.domainObject, domainObjectPath)) {
cssClass = 'is-alias';
}
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>

View File

@ -1,229 +0,0 @@
<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 = undefined;
this.rangeMax = undefined;
} else {
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>

View File

@ -1,29 +0,0 @@
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;
}
}

View File

@ -1,29 +0,0 @@
export default function OverlayPlotCompositionPolicy(openmct) {
function hasNumericTelemetry(domainObject) {
const hasTelemetry = openmct.telemetry.isTelemetryObject(domainObject);
if (!hasTelemetry) {
return false;
}
let metadata = openmct.telemetry.getMetadata(domainObject);
return metadata.values().length > 0 && hasDomainAndRange(metadata);
}
function hasDomainAndRange(metadata) {
return (metadata.valuesForHints(['range']).length > 0
&& metadata.valuesForHints(['domain']).length > 0);
}
return {
allow: function (parent, child) {
if (parent.type === 'telemetry.plot.overlay'
&& (hasNumericTelemetry(child) === false)) {
return false;
}
return true;
}
};
}

View File

@ -20,52 +20,262 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import PlotViewProvider from './PlotViewProvider';
import OverlayPlotViewProvider from './overlayPlot/OverlayPlotViewProvider';
import StackedPlotViewProvider from './stackedPlot/StackedPlotViewProvider';
import PlotsInspectorViewProvider from './inspector/PlotsInspectorViewProvider';
import OverlayPlotCompositionPolicy from './overlayPlot/OverlayPlotCompositionPolicy';
import StackedPlotCompositionPolicy from './stackedPlot/StackedPlotCompositionPolicy';
define([
"./src/chart/MCTChartDirective",
"./src/plot/MCTPlotDirective",
'./src/plot/MCTTicksDirective',
"./src/telemetry/MCTOverlayPlot",
"./src/telemetry/PlotController",
"./src/telemetry/StackedPlotController",
"./src/inspector/PlotInspector",
"./src/inspector/PlotOptionsController",
"./src/inspector/PlotLegendFormController",
"./src/inspector/PlotYAxisFormController",
"./src/inspector/PlotSeriesFormController",
"./src/inspector/HideElementPoolDirective",
"./src/services/ExportImageService",
'./src/PlotViewPolicy',
"./res/templates/plot-options.html",
"./res/templates/plot-options-browse.html",
"./res/templates/plot-options-edit.html",
"./res/templates/stacked-plot.html",
"./res/templates/plot.html"
], function (
MCTChartDirective,
MCTPlotDirective,
MCTTicksDirective,
MCTOverlayPlot,
PlotController,
StackedPlotController,
PlotInspector,
PlotOptionsController,
PlotLegendFormController,
PlotYAxisFormController,
PlotSeriesFormController,
HideElementPool,
ExportImageService,
PlotViewPolicy,
plotOptionsTemplate,
plotOptionsBrowseTemplate,
plotOptionsEditTemplate,
StackedPlotTemplate,
PlotTemplate
) {
export default function () {
return function install(openmct) {
let installed = false;
openmct.types.addType('telemetry.plot.overlay', {
key: "telemetry.plot.overlay",
name: "Overlay Plot",
cssClass: "icon-plot-overlay",
description: "Combine multiple telemetry elements and view them together as a plot with common X and Y axes. Can be added to Display Layouts.",
creatable: "true",
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {
series: [],
yAxis: {},
xAxis: {}
};
},
priority: 891
});
function PlotPlugin() {
return function install(openmct) {
if (installed) {
return;
}
openmct.types.addType('telemetry.plot.stacked', {
key: "telemetry.plot.stacked",
name: "Stacked Plot",
cssClass: "icon-plot-stacked",
description: "Combine multiple telemetry elements and view them together as a plot with a common X axis and individual Y axes. Can be added to Display Layouts.",
creatable: true,
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {};
},
priority: 890
});
installed = true;
openmct.objectViews.addProvider(new StackedPlotViewProvider(openmct));
openmct.objectViews.addProvider(new OverlayPlotViewProvider(openmct));
openmct.objectViews.addProvider(new PlotViewProvider(openmct));
openmct.inspectorViews.addProvider(new PlotsInspectorViewProvider(openmct));
openmct.composition.addPolicy(new OverlayPlotCompositionPolicy(openmct).allow);
openmct.composition.addPolicy(new StackedPlotCompositionPolicy(openmct).allow);
};
}
openmct.legacyRegistry.register("openmct/plot", {
"name": "Plot view for telemetry, reborn",
"extensions": {
"policies": [
{
"category": "view",
"implementation": PlotViewPolicy,
"depends": [
"openmct"
]
}
],
"views": [
// {
// "name": "Plot",
// "key": "plot-single",
// "cssClass": "icon-telemetry",
// "template": PlotTemplate,
// "needs": [
// "telemetry"
// ],
// "delegation": false,
// "priority": "mandatory"
// },
// {
// "name": "Overlay Plot",
// "key": "overlayPlot",
// "cssClass": "icon-plot-overlay",
// "type": "telemetry.plot.overlay",
// "template": PlotTemplate,
// "editable": true
// },
// {
// "name": "Stacked Plot",
// "key": "stackedPlot",
// "cssClass": "icon-plot-stacked",
// "type": "telemetry.plot.stacked",
// "template": StackedPlotTemplate,
// "editable": true
// }
],
"directives": [
{
"key": "mctTicks",
"implementation": MCTTicksDirective,
"depends": []
},
{
"key": "mctChart",
"implementation": MCTChartDirective,
"depends": [
"$interval",
"$log"
]
},
{
"key": "mctPlot",
"implementation": MCTPlotDirective,
"depends": [],
"templateUrl": "templates/mct-plot.html"
},
{
"key": "mctOverlayPlot",
"implementation": MCTOverlayPlot,
"depends": []
},
{
"key": "hideElementPool",
"implementation": HideElementPool,
"depends": []
}
],
"controllers": [
{
"key": "PlotController",
"implementation": PlotController,
"depends": [
"$scope",
"$element",
"formatService",
"openmct",
"objectService",
"exportImageService"
]
},
{
"key": "StackedPlotController",
"implementation": StackedPlotController,
"depends": [
"$scope",
"openmct",
"objectService",
"$element",
"exportImageService"
]
},
{
"key": "PlotOptionsController",
"implementation": PlotOptionsController,
"depends": [
"$scope",
"openmct",
"$timeout"
]
},
{
key: "PlotLegendFormController",
implementation: PlotLegendFormController,
depends: [
"$scope",
"openmct",
"$attrs"
]
},
{
key: "PlotYAxisFormController",
implementation: PlotYAxisFormController,
depends: [
"$scope",
"openmct",
"$attrs"
]
},
{
key: "PlotSeriesFormController",
implementation: PlotSeriesFormController,
depends: [
"$scope",
"openmct",
"$attrs"
]
}
],
"services": [
{
"key": "exportImageService",
"implementation": ExportImageService,
"depends": [
"dialogService"
]
}
],
"types": [
{
"key": "telemetry.plot.overlay",
"name": "Overlay Plot",
"cssClass": "icon-plot-overlay",
"description": "Combine multiple telemetry elements and view them together as a plot with common X and Y axes. Can be added to Display Layouts.",
"features": "creation",
"contains": [
{
"has": "telemetry"
}
],
"model": {
composition: [],
configuration: {
series: [],
yAxis: {},
xAxis: {}
}
},
"properties": [],
"inspector": "plot-options",
"priority": 891
},
{
"key": "telemetry.plot.stacked",
"name": "Stacked Plot",
"cssClass": "icon-plot-stacked",
"description": "Combine multiple telemetry elements and view them together as a plot with a common X axis and individual Y axes. Can be added to Display Layouts.",
"features": "creation",
"contains": [
"telemetry.plot.overlay",
{"has": "telemetry"}
],
"model": {
"composition": [],
"configuration": {}
},
"properties": [],
"priority": 890
}
],
"representations": [
{
"key": "plot-options",
"template": plotOptionsTemplate
},
{
"key": "plot-options-browse",
"template": plotOptionsBrowseTemplate
},
{
"key": "plot-options-edit",
"template": plotOptionsEditTemplate
}
]
}
});
openmct.legacyRegistry.enable("openmct/plot");
};
}
return PlotPlugin;
});

View File

@ -0,0 +1,279 @@
<!--
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.
-->
<div class="gl-plot plot-legend-{{legend.get('position')}} {{legend.get('expanded')? 'plot-legend-expanded' : 'plot-legend-collapsed'}}">
<div class="c-plot-legend gl-plot-legend"
ng-class="{
'hover-on-plot': !!highlights.length,
'is-legend-hidden': legend.get('hideLegendWhenSmall')
}"
>
<div class="c-plot-legend__view-control gl-plot-legend__view-control c-disclosure-triangle is-enabled"
ng-class="{ 'c-disclosure-triangle--expanded': legend.get('expanded') }"
ng-click="legend.set('expanded', !legend.get('expanded'));">
</div>
<div class="c-plot-legend__wrapper"
ng-class="{ 'is-cursor-locked': !!lockHighlightPoint }">
<!-- COLLAPSED PLOT LEGEND -->
<div class="plot-wrapper-collapsed-legend"
ng-class="{'is-cursor-locked': !!lockHighlightPoint }">
<div class="c-state-indicator__alert-cursor-lock icon-cursor-lock" title="Cursor is point locked. Click anywhere in the plot to unlock."></div>
<div class="plot-legend-item"
ng-class="{
'is-status--missing': series.domainObject.status === 'missing'
}"
ng-repeat="series in series track by $index"
>
<div class="plot-series-swatch-and-name">
<span class="plot-series-color-swatch"
ng-style="{ 'background-color': series.get('color').asHexString() }">
</span>
<span class="is-status__indicator" title="This item is missing or suspect"></span>
<span class="plot-series-name">{{ series.nameWithUnit() }}</span>
</div>
<div class="plot-series-value hover-value-enabled value-to-display-{{ legend.get('valueToShowWhenCollapsed') }} {{ series.closest.mctLimitState.cssClass }}"
ng-class="{ 'cursor-hover': (legend.get('valueToShowWhenCollapsed').indexOf('nearest') != -1) }"
ng-show="!!highlights.length && legend.get('valueToShowWhenCollapsed') !== 'none'">
{{ legend.get('valueToShowWhenCollapsed') === 'nearestValue' ?
series.formatY(series.closest) :
legend.get('valueToShowWhenCollapsed') === 'nearestTimestamp' ?
series.closest && series.formatX(series.closest) :
series.formatY(series.get('stats')[legend.get('valueToShowWhenCollapsed') + 'Point']);
}}
</div>
</div>
</div>
<!-- EXPANDED PLOT LEGEND -->
<div class="plot-wrapper-expanded-legend"
ng-class="{'is-cursor-locked': !!lockHighlightPoint }"
>
<div class="c-state-indicator__alert-cursor-lock--verbose icon-cursor-lock" title="Click anywhere in the plot to unlock."> Cursor locked to point</div>
<table>
<thead>
<tr>
<th>Name</th>
<th ng-if="legend.get('showTimestampWhenExpanded')">
Timestamp
</th>
<th ng-if="legend.get('showValueWhenExpanded')">
Value
</th>
<th ng-if="legend.get('showUnitsWhenExpanded')">
Unit
</th>
<th ng-if="legend.get('showMinimumWhenExpanded')"
class="mobile-hide">
Min
</th>
<th ng-if="legend.get('showMaximumWhenExpanded')"
class="mobile-hide">
Max
</th>
</tr>
</thead>
<tr ng-repeat="series in series"
class="plot-legend-item"
ng-class="{
'is-status--missing': series.domainObject.status === 'missing'
}"
>
<td class="plot-series-swatch-and-name">
<span class="plot-series-color-swatch"
ng-style="{ 'background-color': series.get('color').asHexString() }">
</span>
<span class="is-status__indicator" title="This item is missing or suspect"></span>
<span class="plot-series-name">{{ series.get('name') }}</span>
</td>
<td ng-if="legend.get('showTimestampWhenExpanded')">
<span class="plot-series-value cursor-hover hover-value-enabled">
{{ series.closest && series.formatX(series.closest) }}
</span>
</td>
<td ng-if="legend.get('showValueWhenExpanded')">
<span class="plot-series-value cursor-hover hover-value-enabled"
ng-class="series.closest.mctLimitState.cssClass">
{{ series.formatY(series.closest) }}
</span>
</td>
<td ng-if="legend.get('showUnitsWhenExpanded')">
<span class="plot-series-value cursor-hover hover-value-enabled">
{{ series.get('unit') }}
</span>
</td>
<td ng-if="legend.get('showMinimumWhenExpanded')"
class="mobile-hide">
<span class="plot-series-value">
{{ series.formatY(series.get('stats').minPoint) }}
</span>
</td>
<td ng-if="legend.get('showMaximumWhenExpanded')"
class="mobile-hide">
<span class="plot-series-value">
{{ series.formatY(series.get('stats').maxPoint) }}
</span>
</td>
</tr>
</table>
</div>
</div>
</div>
<div class="plot-wrapper-axis-and-display-area flex-elem grows">
<div class="gl-plot-axis-area gl-plot-y has-local-controls"
ng-style="{
width: (tickWidth + 20) + 'px'
}">
<div class="gl-plot-label gl-plot-y-label"
ng-class="{'icon-gear': (yKeyOptions.length > 1 && series.length === 1)}"
>{{yAxis.get('label')}}
</div>
<select class="gl-plot-y-label__select local-controls--hidden"
ng-if="yKeyOptions.length > 1 && series.length === 1"
ng-model="yAxisLabel" ng-change="plot.toggleYAxisLabel(yAxisLabel, yKeyOptions, series[0])">
<option ng-repeat="option in yKeyOptions"
value="{{option.name}}"
ng-selected="option.name === yAxisLabel">
{{option.name}}
</option>
</select>
<mct-ticks axis="yAxis">
<div ng-repeat="tick in ticks track by tick.value"
class="gl-plot-tick gl-plot-y-tick-label"
ng-style="{ top: (100 * (max - tick.value) / interval) + '%' }"
title="{{:: tick.fullText || tick.text }}"
style="margin-top: -0.50em; direction: ltr;">
<span>{{:: tick.text}}</span>
</div>
</mct-ticks>
</div>
<div class="gl-plot-wrapper-display-area-and-x-axis"
ng-style="{
left: (tickWidth + 20) + 'px'
}">
<div class="gl-plot-display-area has-local-controls has-cursor-guides">
<div class="l-state-indicators">
<span class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle"
title="This plot is not currently displaying the latest data. Reset pan/zoom to view latest data."></span>
</div>
<mct-ticks axis="xAxis">
<div class="gl-plot-hash hash-v"
ng-repeat="tick in ticks track by tick.value"
ng-style="{
right: (100 * (max - tick.value) / interval) + '%',
height: '100%'
}"
ng-show="plot.gridLines"
>
</div>
</mct-ticks>
<mct-ticks axis="yAxis">
<div class="gl-plot-hash hash-h"
ng-repeat="tick in ticks track by tick.value"
ng-style="{ bottom: (100 * (tick.value - min) / interval) + '%', width: '100%' }"
ng-show="plot.gridLines"
>
</div>
</mct-ticks>
<mct-chart config="config"
series="series"
rectangles="rectangles"
highlights="highlights"
the-x-axis="xAxis"
the-y-axis="yAxis">
</mct-chart>
<div class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover">
<div class="c-button-set c-button-set--strip-h">
<button class="c-button icon-minus"
ng-click="plot.zoom('out', 0.2)"
title="Zoom out">
</button>
<button class="c-button icon-plus"
ng-click="plot.zoom('in', 0.2)"
title="Zoom in">
</button>
</div>
<div class="c-button-set c-button-set--strip-h"
ng-disabled="!plotHistory.length">
<button class="c-button icon-arrow-left"
ng-click="plot.back()"
title="Restore previous pan/zoom">
</button>
<button class="c-button icon-reset"
ng-click="plot.clear()"
title="Reset pan/zoom">
</button>
</div>
</div>
<!--Cursor guides-->
<div class="c-cursor-guide--v js-cursor-guide--v"
ng-show="plot.cursorGuide">
</div>
<div class="c-cursor-guide--h js-cursor-guide--h"
ng-show="plot.cursorGuide">
</div>
</div>
<div class="gl-plot-axis-area gl-plot-x has-local-controls">
<mct-ticks axis="xAxis">
<div ng-repeat="tick in ticks track by tick.text"
class="gl-plot-tick gl-plot-x-tick-label"
ng-style="{
left: (100 * (tick.value - min) / interval) + '%'
}"
ng-title=":: tick.fullText || tick.text">
{{:: tick.text }}
</div>
</mct-ticks>
<div
class="gl-plot-label gl-plot-x-label"
ng-class="{'icon-gear': isEnabledXKeyToggle()}"
>
{{ xAxis.get('label') }}
</div>
<select
ng-show="plot.isEnabledXKeyToggle()"
ng-model="selectedXKeyOption.key"
ng-change="plot.toggleXKeyOption('{{selectedXKeyOption.key}}', series[0])"
class="gl-plot-x-label__select local-controls--hidden"
ng-options="option.key as option.name for option in xKeyOptions"
>
</select>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,148 @@
<!--
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.
-->
<div ng-controller="PlotOptionsController">
<ul class="c-tree">
<h2 title="Plot series display properties in this object">Plot Series</h2>
<li ng-repeat="series in config.series.models">
<div class="c-tree__item menus-to-left">
<span class='c-disclosure-triangle is-enabled flex-elem'
ng-class="{ 'c-disclosure-triangle--expanded': series.expanded }"
ng-click="series.expanded = !series.expanded">
</span>
<mct-representation
class="rep-object-label c-tree__item__label"
key="'label'"
mct-object="series.oldObject">
</mct-representation>
</div>
<ul class="grid-properties" ng-show="series.expanded">
<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">
{{series.get('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'
}[series.get('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">
{{ series.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">
{{series.get('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"
ng-style="{
'background': series.get('color').asHexString()
}">
</span>
</div>
</li>
</ul>
</li><!-- end repeat -->
</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">{{ config.yAxis.get('label') ? config.yAxis.get('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">
{{ config.yAxis.get('autoscale') ? "Enabled: " : "Disabled" }}
{{ config.yAxis.get('autoscale') ? (config.yAxis.get('autoscalePadding')) : ""}}
</div>
</li>
<li class="grid-row" ng-if="!form.yAxis.autoscale">
<div class="grid-cell label"
title="Minimum Y axis value.">Minimum value</div>
<div class="grid-cell value">{{ config.yAxis.get('range').min }}</div>
</li>
<li class="grid-row" ng-if="!form.yAxis.autoscale">
<div class="grid-cell label"
title="Maximum Y axis value.">Maximum value</div>
<div class="grid-cell value">{{ config.yAxis.get('range').max }}</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">{{ config.legend.get('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">{{ config.legend.get('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">{{ config.legend.get('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">{{
config.legend.get('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 ng-if="config.legend.get('showTimestampWhenExpanded')">Timestamp</span>
<span ng-if="config.legend.get('showValueWhenExpanded')">Value</span>
<span ng-if="config.legend.get('showMinimumWhenExpanded')">Min</span>
<span ng-if="config.legend.get('showMaximumWhenExpanded')">Max</span>
<span ng-if="config.legend.get('showUnitsWhenExpanded')">Units</span>
</div>
</li>
</ul>
</div>
</div>

View File

@ -0,0 +1,229 @@
<!--
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.
-->
<div ng-controller="PlotOptionsController">
<ul class="c-tree">
<h2 title="Display properties for this object">Plot Series</h2>
<li ng-repeat="series in plotSeries"
ng-controller="PlotSeriesFormController"
form-model="series">
<div class="c-tree__item menus-to-left">
<span class='c-disclosure-triangle is-enabled flex-elem'
ng-class="{ 'c-disclosure-triangle--expanded': expanded }"
ng-click="expanded = !expanded">
</span>
<mct-representation class="rep-object-label c-tree__item__label"
key="'label'"
mct-object="series.oldObject">
</mct-representation>
</div>
<ul class="grid-properties" ng-show="expanded">
<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 ng-model="form.yKey">
<option ng-repeat="option in yKeyOptions"
value="{{option.value}}"
ng-selected="option.value == form.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 ng-model="form.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 type="checkbox" ng-model="form.markers"/>
<select
ng-show="form.markers"
ng-model="form.markerShape">
<option
ng-repeat="option in markerShapeOptions"
value="{{ option.value }}"
ng-selected="option.value == form.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 type="checkbox" ng-model="form.alarmMarkers"/>
</div>
</li>
<li class="grid-row" ng-show="form.markers || form.alarmMarkers">
<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 class="c-input--flex" type="text" ng-model="form.markerSize"/></div>
</li>
<li class="grid-row"
ng-controller="ClickAwayController as toggle"
ng-show="form.interpolate !== 'none' || form.markers">
<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" ng-click="toggle.toggle()">
<span class="c-color-swatch"
ng-style="{ background: series.get('color').asHexString() }">
</span>
</div>
<div class="c-palette c-palette--color">
<div class="c-palette__items" ng-show="toggle.isActive()">
<div class="u-contents" ng-repeat="group in config.series.palette.groups()">
<div class="c-palette__item"
ng-repeat="color in group"
ng-class="{ 'selected': series.get('color').equalTo(color) }"
ng-style="{ background: color.asHexString() }"
ng-click="setColor(color)">
</div>
</div>
</div>
</div>
</div>
</li>
</ul>
</li>
</ul>
<div class="grid-properties"
ng-show="!!config.series.models.length"
ng-controller="PlotYAxisFormController"
form-model="config.yAxis">
<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 class="c-input--flex" type="text" ng-model="form.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.">Autoscale</div>
<div class="grid-cell value"><input type="checkbox" ng-model="form.autoscale"/></div>
</li>
<li class="grid-row" ng-show="form.autoscale">
<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 class="c-input--flex" type="text" ng-model="form.autoscalePadding"/>
</div>
</li>
</ul>
<ul class="l-inspector-part" ng-show="!form.autoscale">
<div class="grid-span-all form-error"
ng-show="!form.autoscale && validation.range">
{{ 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 class="c-input--flex" type="number" ng-model="form.range.min"/>
</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 class="c-input--flex" type="number" ng-model="form.range.max"/></div>
</li>
</ul>
</div>
<div class="grid-properties" ng-show="!!config.series.models.length">
<ul class="l-inspector-part" ng-controller="PlotLegendFormController" form-model="config.legend">
<h2 title="Legend options">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">
<select ng-model="form.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 type="checkbox" ng-model="form.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 type="checkbox" ng-model="form.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 ng-model="form.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">showUnitsWhenExpanded</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 type="checkbox"
ng-model="form.showTimestampWhenExpanded"/> Nearest timestamp</li>
<li><input type="checkbox"
ng-model="form.showValueWhenExpanded"/> Nearest value</li>
<li><input type="checkbox"
ng-model="form.showMinimumWhenExpanded"/> Minimum value</li>
<li><input type="checkbox"
ng-model="form.showMaximumWhenExpanded"/> Maximum value</li>
<li><input type="checkbox"
ng-model="form.showUnitsWhenExpanded"/> Units</li>
</ul>
</div>
</li>
</ul>
</div>
</div>

View File

@ -0,0 +1,31 @@
<!--
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.
-->
<div ng-if="!domainObject.model.locked && domainObject.getCapability('editor').inEditContext()">
<mct-representation key="'plot-options-edit'"
mct-object="domainObject">
</mct-representation>
</div>
<div ng-if="domainObject.model.locked || !domainObject.getCapability('editor').inEditContext()">
<mct-representation key="'plot-options-browse'"
mct-object="domainObject">
</mct-representation>
</div>

View File

@ -0,0 +1,58 @@
<!--
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.
-->
<div ng-controller="PlotController as controller"
class="c-plot holder holder-plot has-control-bar">
<div class="c-control-bar" ng-show="!controller.hideExportButtons">
<span class="c-button-set c-button-set--strip-h">
<button class="c-button icon-download"
ng-click="controller.exportPNG()"
title="Export This View's Data as PNG">
<span class="c-button__label">PNG</span>
</button>
<button class="c-button"
ng-click="controller.exportJPG()"
title="Export This View's Data as JPG">
<span class="c-button__label">JPG</span>
</button>
</span>
<button class="c-button icon-crosshair"
ng-class="{ 'is-active': controller.cursorGuide }"
ng-click="controller.toggleCursorGuide($event)"
title="Toggle cursor guides">
</button>
<button class="c-button"
ng-class="{ 'icon-grid-on': controller.gridLines, 'icon-grid-off': !controller.gridLines }"
ng-click="controller.toggleGridLines($event)"
title="Toggle grid lines">
</button>
</div>
<div class="l-view-section u-style-receiver js-style-receiver">
<div class="c-loading--overlay loading"
ng-show="!!pending"></div>
<mct-plot config="controller.config"
series="series"
the-y-axis="yAxis"
the-x-axis="xAxis">
</mct-plot>
</div>
</div>

View File

@ -0,0 +1,65 @@
<!--
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.
-->
<div ng-controller="StackedPlotController as stackedPlot"
class="c-plot c-plot--stacked holder holder-plot has-control-bar">
<div class="c-control-bar" ng-show="!stackedPlot.hideExportButtons">
<span class="c-button-set c-button-set--strip-h">
<button class="c-button icon-download"
ng-click="stackedPlot.exportPNG()"
title="Export This View's Data as PNG">
<span class="c-button__label">PNG</span>
</button>
<button class="c-button"
ng-click="stackedPlot.exportJPG()"
title="Export This View's Data as JPG">
<span class="c-button__label">JPG</span>
</button>
</span>
<button class="c-button icon-crosshair"
ng-class="{ 'is-active': stackedPlot.cursorGuide }"
ng-click="stackedPlot.toggleCursorGuide($event)"
title="Toggle cursor guides">
</button>
<button class="c-button"
ng-class="{ 'icon-grid-on': stackedPlot.gridLines, 'icon-grid-off': !stackedPlot.gridLines }"
ng-click="stackedPlot.toggleGridLines($event)"
title="Toggle grid lines">
</button>
</div>
<div class="l-view-section u-style-receiver js-style-receiver">
<div class="c-loading--overlay loading"
ng-show="!!currentRequest.pending"></div>
<div class="gl-plot child-frame u-inspectable"
ng-repeat="telemetryObject in telemetryObjects"
ng-class="{
's-status-timeconductor-unsynced': telemetryObject
.getCapability('status')
.get('timeconductor-unsynced')
}"
mct-selectable="{
item: telemetryObject.useCapability('adapter'),
oldItem: telemetryObject
}">
<mct-overlay-plot domain-object="telemetryObject"></mct-overlay-plot>
</div>
</div>
</div>

View File

@ -0,0 +1,68 @@
/*****************************************************************************
* 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 () {
/**
* Policy preventing the Plot view from being made available for
* domain objects which have non-numeric telemetry.
* @implements {Policy.<View, DomainObject>}
* @constructor
* @memberof platform/features/plot
*/
function PlotViewPolicy(openmct) {
this.openmct = openmct;
}
PlotViewPolicy.prototype.hasNumericTelemetry = function (domainObject) {
const adaptedObject = domainObject.useCapability('adapter');
if (!adaptedObject.telemetry) {
return domainObject.hasCapability('delegation')
&& domainObject.getCapability('delegation')
.doesDelegateCapability('telemetry');
}
const metadata = this.openmct.telemetry.getMetadata(adaptedObject);
const rangeValues = metadata.valuesForHints(['range']);
if (rangeValues.length === 0) {
return false;
}
return !rangeValues.every(function (value) {
return value.format === 'string';
});
};
PlotViewPolicy.prototype.allow = function (view, domainObject) {
if (view.key === 'plot-single') {
return this.hasNumericTelemetry(domainObject);
}
return true;
};
return PlotViewPolicy;
}
);

View File

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

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