Compare commits

..

2 Commits

Author SHA1 Message Date
Joshi
1a6ccc77fc Add controls for forms 2021-04-16 11:21:34 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
a5946aa10e WIP: form refactor 2021-04-15 11:26:55 -07:00
79 changed files with 2330 additions and 2876 deletions

1
API.md
View File

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

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

@@ -26,7 +26,6 @@ define([
"./src/controllers/EditObjectController",
"./src/actions/EditAndComposeAction",
"./src/actions/EditAction",
"./src/actions/PropertiesAction",
"./src/actions/SaveAction",
"./src/actions/SaveAndStopEditingAction",
"./src/actions/SaveAsAction",
@@ -55,7 +54,6 @@ define([
EditObjectController,
EditAndComposeAction,
EditAction,
PropertiesAction,
SaveAction,
SaveAndStopEditingAction,
SaveAsAction,
@@ -143,22 +141,6 @@ define([
"group": "action",
"priority": 10
},
{
"key": "properties",
"category": [
"contextual",
"view-control"
],
"implementation": PropertiesAction,
"cssClass": "major icon-pencil",
"name": "Edit Properties...",
"group": "action",
"priority": 10,
"description": "Edit properties of this object.",
"depends": [
"dialogService"
]
},
{
"key": "save-and-stop-editing",
"category": "save",

View File

@@ -21,11 +21,11 @@
*****************************************************************************/
define([
'../creation/CreateWizard',
// '../creation/CreateWizard',
'./SaveInProgressDialog'
],
function (
CreateWizard,
// CreateWizard,
SaveInProgressDialog
) {
@@ -100,7 +100,8 @@ function (
toUndirty = [];
function doWizardSave(parent) {
var wizard = self.createWizard(parent);
console.log('SaveAsAction');
// var wizard = self.createWizard(parent);
return self.dialogService
.getUserInput(wizard.getFormStructure(true),

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,6 +252,8 @@ define([
this.status = new api.StatusAPI(this);
this.forms = new api.FormsAPI.default(this);
this.router = new ApplicationRouter();
this.branding = BrandingAPI.default;

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
import _ from 'lodash';
const INSIDE_EDIT_PATH_BLACKLIST = ["copy", "follow", "link", "locate", "move", "link"];
const OUTSIDE_EDIT_PATH_BLACKLIST = ["copy", "follow", "properties", "move", "link", "remove", "locate"];
const OUTSIDE_EDIT_PATH_BLACKLIST = ["copy", "follow", "move", "link", "remove", "locate"];
export default class LegacyContextMenuAction {
constructor(openmct, LegacyAction) {

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

@@ -24,13 +24,6 @@ define([
return false;
}
//TODO: Remove this when plots Angular implementation is deprecated
let parent = selection[0].length > 1 && selection[0][1].context.item;
if (parent && parent.type === 'time-strip') {
return (selectionContext.item.type === typeDefinition.key)
&& (typeDefinition.key !== 'telemetry.plot.overlay');
}
return selectionContext.item.type === typeDefinition.key;
},
view: function (selection) {

View File

@@ -21,41 +21,44 @@
*****************************************************************************/
define([
'./time/TimeAPI',
'./objects/ObjectAPI',
'./composition/CompositionAPI',
'./types/TypeRegistry',
'./telemetry/TelemetryAPI',
'./indicators/IndicatorAPI',
'./notifications/NotificationAPI',
'./Editor',
'./menu/MenuAPI',
'./actions/ActionsAPI',
'./status/StatusAPI'
'./composition/CompositionAPI',
'./Editor',
'./forms/FormsAPI',
'./indicators/IndicatorAPI',
'./menu/MenuAPI',
'./notifications/NotificationAPI',
'./objects/ObjectAPI',
'./status/StatusAPI',
'./telemetry/TelemetryAPI',
'./time/TimeAPI',
'./types/TypeRegistry'
], function (
TimeAPI,
ObjectAPI,
CompositionAPI,
TypeRegistry,
TelemetryAPI,
IndicatorAPI,
NotificationAPI,
EditorAPI,
MenuAPI,
ActionsAPI,
StatusAPI
CompositionAPI,
EditorAPI,
FormsAPI,
IndicatorAPI,
MenuAPI,
NotificationAPI,
ObjectAPI,
StatusAPI,
TelemetryAPI,
TimeAPI,
TypeRegistry
) {
return {
TimeAPI: TimeAPI,
ObjectAPI: ObjectAPI,
CompositionAPI: CompositionAPI,
TypeRegistry: TypeRegistry,
TelemetryAPI: TelemetryAPI,
IndicatorAPI: IndicatorAPI,
NotificationAPI: NotificationAPI.default,
EditorAPI: EditorAPI,
MenuAPI: MenuAPI.default,
ActionsAPI: ActionsAPI.default,
StatusAPI: StatusAPI.default
CompositionAPI: CompositionAPI,
EditorAPI: EditorAPI,
FormsAPI: FormsAPI,
IndicatorAPI: IndicatorAPI,
MenuAPI: MenuAPI.default,
NotificationAPI: NotificationAPI.default,
ObjectAPI: ObjectAPI,
StatusAPI: StatusAPI.default,
TelemetryAPI: TelemetryAPI,
TimeAPI: TimeAPI,
TypeRegistry: TypeRegistry,
};
});

View File

@@ -0,0 +1,178 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/**
* A class for capturing user input data from an object creation
* dialog, and populating a domain object with that data.
*
* @param {DomainObject} domainObject the newly created object to
* populate with user input
* @param {DomainObject} parent the domain object to serve as
* the initial parent for the created object, in the dialog
* @memberof platform/commonUI/browse
* @constructor
*/
export default class CreateWizard {
constructor(domainObject, parent, openmct) {
this.type = openmct.types.get(domainObject.type);
this.model = domainObject;
this.domainObject = domainObject;
this.properties = this.type.definition.form || [];
this.parent = parent;
this.openmct = openmct;
}
/**
* Get the form model for this wizard; this is a description
* that will be rendered to an HTML form. See the
* platform/forms bundle
* @param {boolean} includeLocation if true, a 'location' section
* will be included that will allow the user to select the location
* of the newly created object, otherwise the .location property of
* the model will be used.
* @return {FormModel} formModel the form model to
* show in the create dialog
*/
getFormStructure(includeLocation) {
let sections = [];
let domainObject = this.domainObject;
let self = this;
function validateLocation(parent) {
return parent && self.openmct.composition.checkPolicy(parent.useCapability('adapter'), domainObject.useCapability('adapter'));
}
sections.push({
name: "Properties",
rows: this.properties.map(function (property, index) {
// Property definition is same as form row definition
let row = JSON.parse(JSON.stringify(property));
// Use index as the key into the formValue;
// this correlates to the indexing provided by
// getInitialFormValue
row.key = index;
return row;
}).filter(function (row) {
// Only show rows which have defined controls
return row && row.control;
})
});
// Ensure there is always a "save in" section
if (includeLocation) {
sections.push({
name: 'Location',
cssClass: "grows",
rows: [{
name: "Save In",
control: "locator",
validate: validateLocation.bind(this),
key: "createParent"
}]
});
}
return {
sections: sections,
name: "Create a New " + this.type.definition.name
};
}
/**
* Given some form input values and a domain object, populate the
* domain object used to create this wizard from the given form values.
* @param formValue
* @returns {DomainObject}
*/
populateObjectFromInput(formValue) {
let parent = this.getLocation(formValue);
let formModel = this.createModel(formValue);
formModel.location = parent.getId();
this.updateNamespaceFromParent(parent);
this.domainObject.useCapability("mutation", function () {
return formModel;
});
return this.domainObject;
}
updateNamespaceFromParent(parent) {
let childIdentifier = this.domainObject.useCapability('adapter').identifier;
let parentIdentifier = parent.useCapability('adapter').identifier;
childIdentifier.namespace = parentIdentifier.namespace;
this.domainObject.id = this.openmct.objects.makeKeyString(childIdentifier);
}
/**
* Get the initial value for the form being described.
* This will include the values for all properties described
* in the structure.
*
* @returns {object} the initial value of the form
*/
getInitialFormValue() {
// Start with initial values for properties
let model = this.model;
let formValue = this.properties.map(function (property) {
return property.getValue(model);
});
// Include the createParent
formValue.createParent = this.parent;
return formValue;
}
/**
* Based on a populated form, get the domain object which
* should be used as a parent for the newly-created object.
* @private
* @return {DomainObject}
*/
getLocation(formValue) {
return formValue.createParent || this.parent;
}
/**
* Create the domain object model for a newly-created object,
* based on user input read from a formModel.
* @private
* @return {object} the domain object model
*/
createModel(formValue) {
// Clone
let newModel = JSON.parse(JSON.stringify(this.domainObject));
// Update all properties
this.properties.forEach(function (property, index) {
property.setValue(newModel, formValue[index]);
});
return newModel;
}
}

40
src/api/forms/FormsAPI.js Normal file
View File

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

View File

@@ -0,0 +1,108 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* 'License'); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import PropertiesAction from './PropertiesAction';
import FormProperties from '../components/Form-properties.vue';
import CreateWizard from '../CreateWizard';
import Vue from "vue";
export default class CreateAction extends PropertiesAction {
constructor(openmct, key, parentDomainObject) {
super(openmct);
this.key = key;
this.parentDomainObject = parentDomainObject;
}
invoke() {
const definition = this._getTypeDefination(this.key);
console.log('CreateAction invoke, Show form', definition.form);
let newModel = {
type: this.key,
location: this.openmct.objects.makeKeyString(this.parentDomainObject.identifier)
};
if (definition.initialize) {
definition.initialize(newModel);
}
let openmct = this.openmct;
newModel.modified = Date.now();
let wizard = new CreateWizard(newModel, this.parentDomainObject, openmct);
let formStructure = wizard.getFormStructure(true);
// let initialFormValue = wizard.getInitialFormValue();
// if save navigateAndEdit
// this.openmct.editor.cancel();
this.showForm(formStructure);
}
showForm(formStructure) {
let vm = new Vue({
components: { FormProperties },
provide: {
openmct: this.openmct
},
data() {
return {
formStructure
};
},
template: '<form-properties :model="formStructure"></form-properties>'
}).$mount();
function dismissDialog(overlay, initialize) {
overlay.dismiss();
}
let overlay = this.openmct.overlays.overlay({
element: vm.$el, //TODO: create and show new form component
size: 'small',
buttons: [
{
label: 'OK',
emphasis: 'true',
callback: () => dismissDialog(overlay, true)
},
{
label: 'Cancel',
callback: () => dismissDialog(overlay, false)
}
],
onDestroy: () => vm.$destroy()
});
}
navigateAndEdit(object) {
let objectPath = object.getCapability('context').getPath();
let url = '#/browse/' + objectPath
.slice(1)
.map(function (o) {
return o && openmct.objects.makeKeyString(o.getId());
})
.join('/');
window.location.href = url;
if (isFirstViewEditable(object.useCapability('adapter'), objectPath)) {
openmct.editor.edit();
}
}
}

View File

@@ -0,0 +1,73 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* 'License'); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import PropertiesAction from './PropertiesAction';
export default class EditPropertiesAction extends PropertiesAction {
constructor(openmct) {
super(openmct);
this.name = 'Edit Properties...';
this.key = 'properties';
this.description = 'Edit properties of this object.';
this.cssClass = 'major icon-pencil';
this.hideInDefaultMenu = true;
this.group= 'action';
this.priority= 10;
}
appliesTo(objectPath) {
return Boolean(this._getForm(objectPath));
}
invoke(objectPath) {
const formElements = this._getFormElements(objectPath);
// Start:Testing
const parent = document.createElement('div');
formElements.forEach(e => {
const child = document.createElement('div');
const spanKey = document.createElement('span');
const spanValue = document.createElement('span');
spanKey.textContent = e.key;
spanValue.textContent = e.value;
child.appendChild(spanKey);
child.appendChild(spanValue);
parent.appendChild(child);
});
// End:Testing
let overlay = this.openmct.overlays.overlay({
element: parent, //TODO: create and show new form component
size: 'small',
buttons: [
{
label: 'Done',
// TODO: save form values into domain object properties
callback: () => overlay.dismiss()
}
],
onDestroy: () => {
}
});
}
}

View File

@@ -0,0 +1,91 @@
/*****************************************************************************
* 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 class PropertiesAction {
constructor(openmct) {
this.openmct = openmct;
}
invoke(objectPath) {
// Start:Testing
// const parent = document.createElement('div');
// formElements.forEach(e => {
// const child = document.createElement('div');
// const spanKey = document.createElement('span');
// const spanValue = document.createElement('span');
// spanKey.textContent = e.key;
// spanValue.textContent = e.value;
// child.appendChild(spanKey);
// child.appendChild(spanValue);
// parent.appendChild(child);
// });
// End:Testing
// let overlay = this.openmct.overlays.overlay({
// element: parent, //TODO: create and show new form component
// size: 'small',
// buttons: [
// {
// label: 'Done',
// // TODO: save form values into domain object properties
// callback: () => overlay.dismiss()
// }
// ],
// onDestroy: () => {
// }
// });
}
/**
* @private
*/
_getForm(objectPath) {
const definition = this._getTypeDefination(objectPath[0].type);
return definition.form;
}
/**
* @private
*/
_getFormElements(objectPath) {
const form = this._getForm(objectPath);
const domainObject = objectPath[0];
const formElements = [];
form.forEach(element => {
const value = element.property.reduce((acc, property) => acc[property], domainObject);
formElements.push({key: element.key, value});
});
return formElements;
}
/**
* @private
*/
_getTypeDefination(type) {
const TypeDefinition = this.openmct.types.get(type);
return TypeDefinition.definition;
}
}

View File

@@ -0,0 +1,53 @@
<template>
<form name="mctForm"
novalidate
class="form c-form"
autocomplete="off"
>
<span v-for="section in model.sections"
:key="section.name"
class="l-form-section c-form__section"
:class="section.cssClass"
>
<h2 class="c-form__header"
ng-if="section.name"
>
{{ section.name }}
</h2>
<div v-for="(row, index) in section.rows"
:key="row.name"
>
<form-row :css-class="section.cssClass"
:first="index < 1"
:row="row"
/>
</div>
</span>
</form>
</template>
<script>
import FormRow from "@/api/forms/components/FormRow.vue";
export default {
components: {
FormRow
},
props: {
model: {
type: Object,
required: true
},
value: {
type: Object,
required: false,
default() {
return {};
}
}
},
mounted() {
console.log(this.model, this.value);
}
};
</script>

View File

@@ -0,0 +1,115 @@
<template>
<div class="form-row c-form__row validates"
:class="rowClass"
>
<div class="c-form__row__label label flex-elem"
:title="row.description"
>
{{ row.name }}
</div>
<div class="c-form__row__controls controls flex-elem">
<div v-if="row.control && row.control !== 'locator'"
class="c-form__controls-wrapper wrapper"
>
<component
:is="row.control"
:key="row.key"
:ref="`form-control-${row.key}`"
:model="row"
:required="isRequired"
/>
</div>
</div>
</div>
</template>
<script>
import TextField from "@/api/forms/components/controls/TextField.vue";
import NumberField from "@/api/forms/components/controls/NumberField.vue";
import Composite from "@/api/forms/components/controls/Composite.vue";
import AutoCompleteField from "@/api/forms/components/controls/AutoCompleteField.vue";
const CONTROL_TYPE_VIEW_MAP = {
'textfield': TextField,
'numberfield': NumberField,
'composite': Composite,
'autocomplete': AutoCompleteField
};
export default {
name: 'FormRow',
components: CONTROL_TYPE_VIEW_MAP,
props: {
cssClass: {
type: String,
default: '',
required: true
},
first: {
type: Boolean,
default: false,
required: true
},
row: {
type: Object,
required: true
}
},
data() {
return {
valid: true
};
},
computed: {
isRequired() {
//TODO: Check if field is required
return false;
},
rowClass() {
let cssClass = this.cssClass;
if (this.first === true) {
cssClass = `${cssClass} first`;
}
if (this.row.required) {
cssClass = `${cssClass} req`;
}
if (this.row.layout === 'controls-first') {
cssClass = `${cssClass} l-controls-first`;
}
if (this.row.layout === 'controls-under') {
cssClass = `${cssClass} l-controls-under`;
}
if (this.valid === true) {
cssClass = `${cssClass} valid`;
} else {
cssClass = `${cssClass} invalid`;
}
return cssClass;
}
},
methods: {
// Check if an element is defined; the map step of isNonEmpty
isDefined(element) {
return typeof element !== 'undefined';
},
/**
* Check if an array contains anything other than
* undefined elements.
* @param {Array} value the array to check
* @returns {boolean} true if any non-undefined
* element is in the array
* @memberof platform/forms.CompositeController#
*/
isNonEmpty(value) {
return Array.isArray(value) && value.some(this.isDefined);
}
}
};
</script>

View File

@@ -0,0 +1,172 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div class="form-control autocomplete">
<input v-model="field"
class="autocompleteInput"
type="text"
@change="filterOptions(field)"
@click="inputClicked()"
@keydown="keyDown($event)"
>
<span class="icon-arrow-down"
@click="arrowClicked()"
></span>
<div v-show="hideOptions"
class="autocompleteOptions"
@blur="hideOptions = true"
>
<ul>
<li v-for="opt in filteredOptions"
:key="opt.optionId"
:class="{'optionPreSelected': optionIndex === opt.optionId}"
@click="fillInput(opt.name)"
@mouseover="optionMouseover(opt.optionId)"
>
<span class="optionText">{{ opt.name }}</span>
</li>
</ul>
</div>
</div>
</template>
<script>
const key = {
down: 40,
up: 38,
enter: 13
};
export default {
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
hideOptions: true,
filteredOptions: [],
optionIndex: 0,
field: ''
};
},
mounted() {
this.options = this.model.options;
this.autocompleteInputElement = this.$el.getElementsByClassName('autocompleteInput')[0];
if (this.options[0].name) {
// If "options" include name, value pair
this.optionNames = this.options.map((opt) => {
return opt.name;
});
} else {
// If options is only an array of string.
this.optionNames = this.options;
}
},
methods: {
fillInputWithIndexedOption() {
if (this.filteredOptions[this.optionIndex]) {
this.ngModel[this.field] = this.filteredOptions[this.optionIndex].name;
}
},
decrementOptionIndex() {
if (this.optionIndex === 0) {
this.optionIndex = this.filteredOptions.length;
}
this.optionIndex--;
this.fillInputWithIndexedOption();
},
incrementOptionIndex() {
if (this.optionIndex === this.filteredOptions.length - 1) {
this.optionIndex = -1;
}
this.optionIndex++;
this.fillInputWithIndexedOption();
},
fillInputWithString(string) {
this.hideOptions = true;
this.ngModel[this.field] = string;
},
showOptions(string) {
this.hideOptions = false;
this.filterOptions(string);
this.optionIndex = 0;
},
keyDown($event) {
if (this.filteredOptions) {
let keyCode = $event.keyCode;
switch (keyCode) {
case key.down:
this.incrementOptionIndex();
break;
case key.up:
$event.preventDefault(); // Prevents cursor jumping back and forth
this.decrementOptionIndex();
break;
case key.enter:
if (this.filteredOptions[this.optionIndex]) {
this.fillInputWithString(this.filteredOptions[this.optionIndex].name);
}
}
}
},
filterOptions() {
this.hideOptions = false;
this.filteredOptions = this.optionNames.filter((option) => {
return option.toLowerCase().indexOf(this.field.toLowerCase()) >= 0;
}).map((option, index) => {
return {
optionId: index,
name: option
};
});
},
inputClicked() {
this.autocompleteInputElement.select();
this.showOptions(this.autocompleteInputElement.value);
},
arrowClicked() {
this.autocompleteInputElement.select();
this.showOptions('');
},
fillInput(string) {
this.fillInputWithString(string);
},
optionMouseover(optionId) {
this.optionIndex = optionId;
}
}
};
</script>

View File

@@ -0,0 +1,46 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<span>
<composite-item v-for="(item, index) in model.items"
:key="item.name"
:first="index < 1"
:item="item"
/>
</span>
</template>
<script>
import CompositeItem from "@/api/forms/components/controls/CompositeItem.vue";
export default {
components: {
CompositeItem
},
props: {
model: {
type: Object,
required: true
}
}
};
</script>

View File

@@ -0,0 +1,57 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div :class="compositeCssClass">
<form-row :css-class="item.cssClass"
:first="first"
:row="item"
/>
<span class="composite-control-label">
{{ item.name }}
</span>
</div>
</template>
<script>
import FormRow from "@/api/forms/components/FormRow.vue";
export default {
components: {
FormRow
},
props: {
item: {
type: Object,
required: true
},
first: {
type: Boolean,
required: true
}
},
computed: {
compositeCssClass() {
return `l-composite-control l-${this.item.control}`;
}
}
};
</script>

View File

@@ -1,5 +1,5 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
@@ -20,44 +20,38 @@
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>
<span class="form-control shell">
<span class="field control"
:class="model.cssClass"
>
<input v-model="field"
type="number"
:min="model.min"
:max="model.max"
:step="model.step"
@blur="blur()"
>
<!-- ng-pattern="ngPattern"-->
</span>
</span>
</template>
<script>
import PlotOptionsBrowse from "@/plugins/plot/vue/inspector/PlotOptionsBrowse.vue";
import PlotOptionsEdit from "@/plugins/plot/vue/inspector/PlotOptionsEdit.vue";
export default {
components: {
PlotOptionsBrowse,
PlotOptionsEdit
},
inject: ['openmct', 'domainObject'],
data() {
return {
isEditing: this.openmct.editor.isEditing()
};
},
computed: {
canEdit() {
return this.isEditing && !this.domainObject.locked;
props: {
model: {
type: Object,
required: true
}
},
mounted() {
this.openmct.editor.on('isEditing', this.setEditState);
},
beforeDestroy() {
this.openmct.editor.off('isEditing', this.setEditState);
data() {
return {
field: ''
};
},
methods: {
setEditState(isEditing) {
this.isEditing = isEditing;
blur() {
}
}
};

View File

@@ -0,0 +1,56 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<span class="form-control shell">
<span class="field control"
:class="model.cssClass"
>
<input v-model="field"
type="text"
:size="model.size"
@blur="blur()"
>
<!-- ng-pattern="ngPattern"-->
</span>
</span>
</template>
<script>
export default {
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
field: ''
};
},
methods: {
blur() {
}
}
};
</script>

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

@@ -161,7 +161,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 +176,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;
};
/**
@@ -522,10 +520,8 @@ ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) {
*/
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

@@ -40,7 +40,6 @@ const DEFAULTS = [
'platform/features/clock',
'platform/features/hyperlink',
'platform/features/timeline',
'platform/forms',
'platform/identity',
'platform/persistence/aggregator',
'platform/persistence/queue',
@@ -85,11 +84,11 @@ define([
'../platform/features/hyperlink/bundle',
'../platform/features/static-markup/bundle',
'../platform/features/timeline/bundle',
'../platform/forms/bundle',
'../platform/framework/bundle',
'../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

@@ -82,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);
}
});
@@ -104,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;
});
}

View File

@@ -344,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

@@ -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();

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

@@ -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);
}
},

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

@@ -23,7 +23,7 @@
<template>
<div
class="c-compass"
:style="`width: ${ sizedImageDimensions.width }px; height: ${ sizedImageDimensions.height }px`"
:style="compassDimensionsStyle"
>
<CompassHUD
v-if="hasCameraFieldOfView"
@@ -34,7 +34,6 @@
<CompassRose
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

@@ -20,252 +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%;
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'];
@@ -541,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;
@@ -673,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);
@@ -796,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

@@ -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
*/
// 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;
@@ -116,9 +106,6 @@ export default class CouchObjectProvider {
}
}
/**
* @private
*/
getModel(response) {
if (response && response.model) {
let key = response[ID];
@@ -144,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`;
@@ -270,7 +149,6 @@ export default class CouchObjectProvider {
headers: {
"Content-Type": "application/json"
},
signal: abortSignal,
body
});
@@ -325,9 +203,6 @@ export default class CouchObjectProvider {
};
}
/**
* @private
*/
abortGetChanges() {
if (this.controller) {
this.controller.abort();
@@ -337,9 +212,6 @@ export default class CouchObjectProvider {
return true;
}
/**
* @private
*/
async observeObjectChanges(filter) {
let intermediateResponse = this.getIntermediateResponse();
@@ -420,9 +292,6 @@ export default class CouchObjectProvider {
}
/**
* @private
*/
getIntermediateResponse() {
let intermediateResponse = {};
intermediateResponse.promise = new Promise(function (resolve, reject) {
@@ -433,9 +302,6 @@ export default class CouchObjectProvider {
return intermediateResponse;
}
/**
* @private
*/
enqueueObject(key, model, intermediateResponse) {
if (this.objectQueue[key]) {
this.objectQueue[key].enqueue({
@@ -464,9 +330,6 @@ export default class CouchObjectProvider {
return intermediateResponse.promise;
}
/**
* @private
*/
updateQueued(key) {
if (!this.objectQueue[key].pending) {
this.objectQueue[key].pending = true;

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,130 +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', () => {
return openmct.objects.save(mockDomainObject).then((result) => {
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();
});
});
});
});
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

@@ -413,10 +413,6 @@ define([
* @public
*/
updateFiltersAndRefresh: function (updatedFilters) {
if (updatedFilters === undefined) {
return;
}
let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters));
if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) {

View File

@@ -1,198 +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="config && loaded"
class="js-plot-options-browse"
>
<ul class="c-tree">
<h2 title="Plot series display properties in this object">Plot Series</h2>
<plot-options-item v-for="series in plotSeries"
:key="series.key"
:series="series"
/>
</ul>
<div class="grid-properties">
<ul class="l-inspector-part">
<h2 title="Y axis settings for this object">Y Axis</h2>
<li class="grid-row">
<div class="grid-cell label"
title="Manually override how the Y axis is labeled."
>Label</div>
<div class="grid-cell value">{{ label ? label : "Not defined" }}</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Automatically scale the Y axis to keep all values in view."
>Autoscale</div>
<div class="grid-cell value">
{{ autoscale ? "Enabled: " : "Disabled" }}
{{ autoscale ? autoscalePadding : "" }}
</div>
</li>
<li v-if="!autoscale && rangeMin"
class="grid-row"
>
<div class="grid-cell label"
title="Minimum Y axis value."
>Minimum value</div>
<div class="grid-cell value">{{ rangeMin }}</div>
</li>
<li v-if="!autoscale && rangeMax"
class="grid-row"
>
<div class="grid-cell label"
title="Maximum Y axis value."
>Maximum value</div>
<div class="grid-cell value">{{ rangeMax }}</div>
</li>
</ul>
<ul class="l-inspector-part">
<h2 title="Legend settings for this object">Legend</h2>
<li class="grid-row">
<div class="grid-cell label"
title="The position of the legend relative to the plot display area."
>Position</div>
<div class="grid-cell value capitalize">{{ position }}</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Hide the legend when the plot is small"
>Hide when plot small</div>
<div class="grid-cell value">{{ hideLegendWhenSmall ? "Yes" : "No" }}</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Show the legend expanded by default"
>Expand by Default</div>
<div class="grid-cell value">{{ expandByDefault ? "Yes" : "No" }}</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="What to display in the legend when it's collapsed."
>Show when collapsed:</div>
<div class="grid-cell value">{{
valueToShowWhenCollapsed.replace('nearest', '')
}}
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="What to display in the legend when it's expanded."
>Show when expanded:</div>
<div class="grid-cell value comma-list">
<span v-if="showTimestampWhenExpanded">Timestamp</span>
<span v-if="showValueWhenExpanded">Value</span>
<span v-if="showMinimumWhenExpanded">Min</span>
<span v-if="showMaximumWhenExpanded">Max</span>
<span v-if="showUnitsWhenExpanded">Units</span>
</div>
</li>
</ul>
</div>
</div>
</template>
<script>
import PlotOptionsItem from "./PlotOptionsItem.vue";
import configStore from "../single/configuration/configStore";
import eventHelpers from "../single/lib/eventHelpers";
export default {
components: {
PlotOptionsItem
},
inject: ['openmct', 'domainObject'],
data() {
return {
config: undefined,
label: '',
autoscale: '',
autoscalePadding: '',
rangeMin: '',
rangeMax: '',
position: '',
hideLegendWhenSmall: '',
expandByDefault: '',
valueToShowWhenCollapsed: '',
showTimestampWhenExpanded: '',
showValueWhenExpanded: '',
showMinimumWhenExpanded: '',
showMaximumWhenExpanded: '',
showUnitsWhenExpanded: '',
loaded: false,
plotSeries: []
};
},
mounted() {
eventHelpers.extend(this);
this.config = this.getConfig();
this.initConfiguration();
this.registerListeners();
this.loaded = true;
},
beforeDestroy() {
this.stopListening();
},
methods: {
initConfiguration() {
this.label = this.config.yAxis.get('label');
this.autoscale = this.config.yAxis.get('autoscale');
this.autoscalePadding = this.config.yAxis.get('autoscalePadding');
const range = this.config.yAxis.get('range');
if (range) {
this.rangeMin = range.min;
this.rangeMax = range.max;
}
this.position = this.config.legend.get('position');
this.hideLegendWhenSmall = this.config.legend.get('hideLegendWhenSmall');
this.expandByDefault = this.config.legend.get('expandByDefault');
this.valueToShowWhenCollapsed = this.config.legend.get('valueToShowWhenCollapsed');
this.showTimestampWhenExpanded = this.config.legend.get('showTimestampWhenExpanded');
this.showValueWhenExpanded = this.config.legend.get('showValueWhenExpanded');
this.showMinimumWhenExpanded = this.config.legend.get('showMinimumWhenExpanded');
this.showMaximumWhenExpanded = this.config.legend.get('showMaximumWhenExpanded');
this.showUnitsWhenExpanded = this.config.legend.get('showUnitsWhenExpanded');
},
getConfig() {
this.configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
return configStore.get(this.configId);
},
registerListeners() {
this.config.series.forEach(this.addSeries, this);
this.listenTo(this.config.series, 'add', this.addSeries, this);
this.listenTo(this.config.series, 'remove', this.resetAllSeries, this);
},
addSeries(series, index) {
this.plotSeries[index] = series;
},
resetAllSeries() {
this.plotSeries = [];
this.config.series.forEach(this.addSeries, this);
}
}
};
</script>

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="config && loaded"
class="js-plot-options-edit"
>
<ul class="c-tree">
<h2 title="Display properties for this object">Plot Series</h2>
<li v-for="series in plotSeries"
:key="series.key"
>
<series-form :series="series" />
</li>
</ul>
<y-axis-form v-show="!!plotSeries.length"
class="grid-properties"
:y-axis="config.yAxis"
/>
<ul class="l-inspector-part">
<h2 title="Legend options">Legend</h2>
<legend-form v-show="!!plotSeries.length"
class="grid-properties"
:legend="config.legend"
/>
</ul>
</div>
</template>
<script>
import SeriesForm from "@/plugins/plot/vue/inspector/forms/SeriesForm.vue";
import YAxisForm from "@/plugins/plot/vue/inspector/forms/YAxisForm.vue";
import LegendForm from "@/plugins/plot/vue/inspector/forms/LegendForm.vue";
import eventHelpers from "@/plugins/plot/vue/single/lib/eventHelpers";
import configStore from "@/plugins/plot/vue/single/configuration/configStore";
export default {
components: {
LegendForm,
SeriesForm,
YAxisForm
},
inject: ['openmct', 'domainObject'],
data() {
return {
config: {},
plotSeries: [],
loaded: false
};
},
mounted() {
eventHelpers.extend(this);
this.config = this.getConfig();
this.registerListeners();
this.loaded = true;
},
beforeDestroy() {
this.stopListening();
},
methods: {
getConfig() {
this.configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
return configStore.get(this.configId);
},
registerListeners() {
this.config.series.forEach(this.addSeries, this);
this.listenTo(this.config.series, 'add', this.addSeries, this);
this.listenTo(this.config.series, 'remove', this.resetAllSeries, this);
},
addSeries(series, index) {
this.plotSeries[index] = series;
},
resetAllSeries() {
this.plotSeries = [];
this.config.series.forEach(this.addSeries, this);
}
}
};
</script>

View File

@@ -1,155 +0,0 @@
<template>
<ul>
<li class="c-tree__item menus-to-left">
<span class="c-disclosure-triangle is-enabled flex-elem"
:class="expandedCssClass"
@click="toggleExpanded"
>
</span>
<div class="c-object-label"
:class="statusClass"
>
<div class="c-object-label__type-icon"
:class="getSeriesClass"
>
<span class="is-status__indicator"
title="This item is missing or suspect"
></span>
</div>
<div class="c-object-label__name">{{ series.domainObject.name }}</div>
</div>
</li>
<li v-show="expanded"
class="c-tree__item menus-to-left"
>
<ul class="grid-properties js-plot-options-browse-properties">
<li class="grid-row">
<div class="grid-cell label"
title="The field to be plotted as a value for this series."
>Value</div>
<div class="grid-cell value">
{{ yKey }}
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="The rendering method to join lines for this series."
>Line Method</div>
<div class="grid-cell value">{{ {
'none': 'None',
'linear': 'Linear interpolation',
'stepAfter': 'Step After'
}[interpolate] }}
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Whether markers are displayed, and their size."
>Markers</div>
<div class="grid-cell value">
{{ markerOptionsDisplayText }}
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Display markers visually denoting points in alarm."
>Alarm Markers</div>
<div class="grid-cell value">
{{ alarmMarkers ? "Enabled" : "Disabled" }}
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="The plot line and marker color for this series."
>Color</div>
<div class="grid-cell value">
<span class="c-color-swatch"
:style="{
'background': seriesHexColor
}"
>
</span>
</div>
</li>
</ul>
</li>
</ul>
</template>
<script>
export default {
inject: ['openmct'],
props: {
series: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
expanded: false
};
},
computed: {
getSeriesClass() {
let cssClass = '';
let legacyObject = this.openmct.legacyObject(this.series.domainObject);
let location = legacyObject.getCapability('location');
if (location && location.isLink()) {
cssClass = 'l-icon-link';
}
let type = legacyObject.getCapability('type');
if (type) {
cssClass = `${cssClass} ${type.getCssClass()}`;
}
return cssClass;
},
expandedCssClass() {
if (this.expanded === true) {
return 'c-disclosure-triangle--expanded';
}
return '';
},
statusClass() {
return (this.status) ? `is-status--${this.status}` : '';
},
yKey() {
return this.series.get('yKey');
},
interpolate() {
return this.series.get('interpolate');
},
markerOptionsDisplayText() {
return this.series.markerOptionsDisplayText();
},
alarmMarkers() {
return this.series.get('alarmMarkers');
},
seriesHexColor() {
return this.series.get('color').asHexString();
}
},
mounted() {
this.status = this.openmct.status.get(this.series.domainObject.identifier);
this.removeStatusListener = this.openmct.status.observe(this.series.domainObject.identifier, this.setStatus);
},
beforeDestroy() {
if (this.removeStatusListener) {
this.removeStatusListener();
}
},
methods: {
toggleExpanded() {
this.expanded = !this.expanded;
},
setStatus(status) {
this.status = status;
}
}
};
</script>

View File

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

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 "@/plugins/plot/vue/inspector/forms/formUtil";
import _ from "lodash";
export default {
inject: ['openmct', 'domainObject'],
props: {
legend: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
position: '',
hideLegendWhenSmall: '',
expandByDefault: '',
valueToShowWhenCollapsed: '',
showTimestampWhenExpanded: '',
showValueWhenExpanded: '',
showMinimumWhenExpanded: '',
showMaximumWhenExpanded: '',
showUnitsWhenExpanded: '',
validation: {}
};
},
mounted() {
this.initialize();
this.initFormValues();
},
methods: {
initialize() {
this.fields = [
{
modelProp: 'position',
objectPath: 'configuration.legend.position'
},
{
modelProp: 'hideLegendWhenSmall',
coerce: Boolean,
objectPath: 'configuration.legend.hideLegendWhenSmall'
},
{
modelProp: 'expandByDefault',
coerce: Boolean,
objectPath: 'configuration.legend.expandByDefault'
},
{
modelProp: 'valueToShowWhenCollapsed',
objectPath: 'configuration.legend.valueToShowWhenCollapsed'
},
{
modelProp: 'showValueWhenExpanded',
coerce: Boolean,
objectPath: 'configuration.legend.showValueWhenExpanded'
},
{
modelProp: 'showTimestampWhenExpanded',
coerce: Boolean,
objectPath: 'configuration.legend.showTimestampWhenExpanded'
},
{
modelProp: 'showMaximumWhenExpanded',
coerce: Boolean,
objectPath: 'configuration.legend.showMaximumWhenExpanded'
},
{
modelProp: 'showMinimumWhenExpanded',
coerce: Boolean,
objectPath: 'configuration.legend.showMinimumWhenExpanded'
},
{
modelProp: 'showUnitsWhenExpanded',
coerce: Boolean,
objectPath: 'configuration.legend.showUnitsWhenExpanded'
}
];
},
initFormValues() {
this.position = this.legend.get('position');
this.hideLegendWhenSmall = this.legend.get('hideLegendWhenSmall');
this.expandByDefault = this.legend.get('expandByDefault');
this.valueToShowWhenCollapsed = this.legend.get('valueToShowWhenCollapsed');
this.showTimestampWhenExpanded = this.legend.get('showTimestampWhenExpanded');
this.showValueWhenExpanded = this.legend.get('showValueWhenExpanded');
this.showMinimumWhenExpanded = this.legend.get('showMinimumWhenExpanded');
this.showMaximumWhenExpanded = this.legend.get('showMaximumWhenExpanded');
this.showUnitsWhenExpanded = this.legend.get('showUnitsWhenExpanded');
},
updateForm(formKey) {
const newVal = this[formKey];
const oldVal = this.legend.get(formKey);
const formField = this.fields.find((field) => field.modelProp === formKey);
const path = objectPath(formField.objectPath);
const validationResult = validate(newVal, this.legend, formField.validate);
if (validationResult === true) {
delete this.validation[formKey];
} else {
this.validation[formKey] = validationResult;
return;
}
if (!_.isEqual(coerce(newVal, formField.coerce), coerce(oldVal, formField.coerce))) {
this.legend.set(formKey, coerce(newVal, formField.coerce));
if (path) {
this.openmct.objects.mutate(
this.domainObject,
path(this.domainObject, this.legend),
coerce(newVal, formField.coerce)
);
}
}
},
setStatus(status) {
this.status = status;
}
}
};
</script>

View File

@@ -1,347 +0,0 @@
<template>
<ul>
<li class="c-tree__item menus-to-left">
<span class="c-disclosure-triangle is-enabled flex-elem"
:class="expandedCssClass"
@click="toggleExpanded"
>
</span>
<div :class="objectLabelCss">
<div class="c-object-label__type-icon"
:class="[seriesCss, linkCss]"
>
<span class="is-status__indicator"
title="This item is missing or suspect"
></span>
</div>
<div class="c-object-label__name">{{ series.domainObject.name }}</div>
</div>
</li>
<ul v-show="expanded"
class="grid-properties js-plot-options-edit-properties"
>
<li class="grid-row">
<!-- Value to be displayed -->
<div class="grid-cell label"
title="The field to be plotted as a value for this series."
>Value</div>
<div class="grid-cell value">
<select v-model="yKey"
@change="updateForm('yKey')"
>
<option v-for="option in yKeyOptions"
:key="option.value"
:value="option.value"
:selected="option.value == yKey"
>
{{ option.name }}
</option>
</select>
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="The rendering method to join lines for this series."
>Line Method</div>
<div class="grid-cell value">
<select v-model="interpolate"
@change="updateForm('interpolate')"
>
<option value="none">None</option>
<option value="linear">Linear interpolate</option>
<option value="stepAfter">Step after</option>
</select>
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Whether markers are displayed."
>Markers</div>
<div class="grid-cell value">
<input v-model="markers"
type="checkbox"
@change="updateForm('markers')"
>
<select
v-show="markers"
v-model="markerShape"
@change="updateForm('markerShape')"
>
<option
v-for="option in markerShapeOptions"
:key="option.value"
:value="option.value"
:selected="option.value == markerShape"
>
{{ option.name }}
</option>
</select>
</div>
</li>
<li class="grid-row">
<div class="grid-cell label"
title="Display markers visually denoting points in alarm."
>Alarm Markers</div>
<div class="grid-cell value">
<input v-model="alarmMarkers"
type="checkbox"
@change="updateForm('alarmMarkers')"
>
</div>
</li>
<li v-show="markers || alarmMarkers"
class="grid-row"
>
<div class="grid-cell label"
title="The size of regular and alarm markers for this series."
>Marker Size:</div>
<div class="grid-cell value"><input v-model="markerSize"
class="c-input--flex"
type="text"
@change="updateForm('markerSize')"
></div>
</li>
<li v-show="interpolate !== 'none' || markers"
class="grid-row"
>
<div class="grid-cell label"
title="Manually set the plot line and marker color for this series."
>Color</div>
<div class="grid-cell value">
<div class="c-click-swatch c-click-swatch--menu"
@click="toggleSwatch()"
>
<span class="c-color-swatch"
:style="{ background: seriesColorAsHex }"
>
</span>
</div>
<div class="c-palette c-palette--color">
<div v-show="swatchActive"
class="c-palette__items"
>
<div v-for="(group, index) in colorPalette"
:key="index"
class="u-contents"
>
<div v-for="(color, colorIndex) in group"
:key="colorIndex"
class="c-palette__item"
:class="{ 'selected': series.get('color').equalTo(color) }"
:style="{ background: color.asHexString() }"
@click="setColor(color)"
>
</div>
</div>
</div>
</div>
</div>
</li>
</ul>
</ul>
</template>
<script>
import { MARKER_SHAPES } from "../../single/draw/MarkerShapes";
import { objectPath, validate, coerce } from "./formUtil";
import _ from 'lodash';
export default {
inject: ['openmct', 'domainObject'],
props: {
series: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
expanded: false,
markerShapeOptions: [],
yKey: this.series.get('yKey'),
yKeyOptions: [],
interpolate: this.series.get('interpolate'),
markers: this.series.get('markers'),
markerShape: this.series.get('markerShape'),
alarmMarkers: this.series.get('alarmMarkers'),
markerSize: this.series.get('markerSize'),
validation: {},
swatchActive: false
};
},
computed: {
colorPalette() {
return this.series.collection.palette.groups();
},
objectLabelCss() {
return this.status ? `c-object-label is-status--${this.status}'` : 'c-object-label';
},
seriesCss() {
let legacyObject = this.openmct.legacyObject(this.series.domainObject);
let type = legacyObject.getCapability('type');
return type ? `c-object-label__type-icon ${type.getCssClass()}` : `c-object-label__type-icon`;
},
linkCss() {
let cssClass = '';
let legacyObject = this.openmct.legacyObject(this.series.domainObject);
let location = legacyObject.getCapability('location');
if (location && location.isLink()) {
cssClass = 'l-icon-link';
}
return cssClass;
},
expandedCssClass() {
return this.expanded ? 'c-disclosure-triangle--expanded' : '';
},
seriesColorAsHex() {
return this.series.get('color').asHexString();
}
},
mounted() {
this.initialize();
this.status = this.openmct.status.get(this.series.domainObject.identifier);
this.removeStatusListener = this.openmct.status.observe(this.series.domainObject.identifier, this.setStatus);
},
beforeDestroy() {
if (this.removeStatusListener) {
this.removeStatusListener();
}
},
methods: {
initialize: function () {
this.fields = [
{
modelProp: 'yKey',
objectPath: this.dynamicPathForKey('yKey')
},
{
modelProp: 'interpolate',
objectPath: this.dynamicPathForKey('interpolate')
},
{
modelProp: 'markers',
objectPath: this.dynamicPathForKey('markers')
},
{
modelProp: 'markerShape',
objectPath: this.dynamicPathForKey('markerShape')
},
{
modelProp: 'markerSize',
coerce: Number,
objectPath: this.dynamicPathForKey('markerSize')
},
{
modelProp: 'alarmMarkers',
coerce: Boolean,
objectPath: this.dynamicPathForKey('alarmMarkers')
}
];
const metadata = this.series.metadata;
this.yKeyOptions = metadata
.valuesForHints(['range'])
.map(function (o) {
return {
name: o.key,
value: o.key
};
});
this.markerShapeOptions = Object.entries(MARKER_SHAPES)
.map(([key, obj]) => {
return {
name: obj.label,
value: key
};
});
},
dynamicPathForKey(key) {
return function (object, model) {
const modelIdentifier = model.get('identifier');
const index = object.configuration.series.findIndex(s => {
return _.isEqual(s.identifier, modelIdentifier);
});
return 'configuration.series[' + index + '].' + key;
};
},
/**
* Set the color for the current plot series. If the new color was
* already assigned to a different plot series, then swap the colors.
*/
setColor: function (color) {
const oldColor = this.series.get('color');
const otherSeriesWithColor = this.series.collection.filter(function (s) {
return s.get('color') === color;
})[0];
this.series.set('color', color);
const getPath = this.dynamicPathForKey('color');
const seriesColorPath = getPath(this.domainObject, this.series);
this.openmct.objects.mutate(
this.domainObject,
seriesColorPath,
color.asHexString()
);
if (otherSeriesWithColor) {
otherSeriesWithColor.set('color', oldColor);
const otherSeriesColorPath = getPath(
this.domainObject,
otherSeriesWithColor
);
this.openmct.objects.mutate(
this.domainObject,
otherSeriesColorPath,
oldColor.asHexString()
);
}
},
toggleExpanded() {
this.expanded = !this.expanded;
},
updateForm(formKey) {
const newVal = this[formKey];
const oldVal = this.series.get(formKey);
const formField = this.fields.find((field) => field.modelProp === formKey);
const path = objectPath(formField.objectPath);
const validationResult = validate(newVal, this.series, formField.validate);
if (validationResult === true) {
delete this.validation[formKey];
} else {
this.validation[formKey] = validationResult;
return;
}
if (!_.isEqual(coerce(newVal, formField.coerce), coerce(oldVal, formField.coerce))) {
this.series.set(formKey, coerce(newVal, formField.coerce));
if (path) {
this.openmct.objects.mutate(
this.domainObject,
path(this.domainObject, this.series),
coerce(newVal, formField.coerce)
);
}
}
},
setStatus(status) {
this.status = status;
},
toggleSwatch() {
this.swatchActive = !this.swatchActive;
}
}
};
</script>

View File

@@ -1,226 +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 = 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

@@ -45,14 +45,9 @@
<div class="gl-plot-display-area has-local-controls has-cursor-guides">
<div class="l-state-indicators">
<span v-if="plotHistory.length"
class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle"
<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>
<span v-else
class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle"
title="This plot is not currently displaying the latest data. Resume to view latest data."
></span>
</div>
<mct-ticks v-show="gridLines && !options.compact"
@@ -90,8 +85,8 @@
>
</button>
</div>
<div v-if="plotHistory.length"
class="c-button-set c-button-set--strip-h"
<div class="c-button-set c-button-set--strip-h"
:disabled="!plotHistory.length"
>
<button class="c-button icon-arrow-left"
title="Restore previous pan/zoom"
@@ -104,22 +99,6 @@
>
</button>
</div>
<div v-if="isRealTime"
class="c-button-set c-button-set--strip-h"
>
<button v-if="!isFrozen"
class="c-button icon-pause"
title="Pause incoming real-time data"
@click="pause()"
>
</button>
<button v-if="isFrozen"
class="c-button icon-arrow-right pause-play is-paused"
title="Resume displaying real-time data"
@click="play()"
>
</button>
</div>
</div>
<!--Cursor guides-->
@@ -207,14 +186,10 @@ export default {
xKeyOptions: [],
config: {},
pending: 0,
isRealTime: this.openmct.time.clock() !== undefined,
loaded: false
};
},
computed: {
isFrozen() {
return this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
},
plotLegendPositionClass() {
return `plot-legend-${this.config.legend.get('position')}`;
},
@@ -252,7 +227,6 @@ export default {
'configuration.filters',
this.updateFiltersAndResubscribe
);
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.updateStatus);
this.openmct.objectViews.on('clearData', this.clearData);
this.followTimeConductor();
@@ -269,7 +243,6 @@ export default {
},
methods: {
followTimeConductor() {
this.openmct.time.on('clock', this.updateRealTime);
this.openmct.time.on('bounds', this.updateDisplayBounds);
this.synchronized(true);
},
@@ -398,9 +371,6 @@ export default {
const displayRange = series.getDisplayRange(xKey);
this.config.xAxis.set('range', displayRange);
},
updateRealTime(clock) {
this.isRealTime = clock !== undefined;
},
/**
* Track latest display bounds. Forces update when not receiving ticks.
@@ -454,28 +424,19 @@ export default {
* displays can update accordingly.
*/
synchronized(value) {
const isLocalClock = this.openmct.time.clock();
if (typeof value !== 'undefined') {
this._synchronized = value;
const isUnsynced = isLocalClock && !value;
this.setStatus(isUnsynced);
const isUnsynced = !value && this.openmct.time.clock();
const domainObject = this.openmct.legacyObject(this.domainObject);
if (domainObject.getCapability('status')) {
domainObject.getCapability('status')
.set('timeconductor-unsynced', isUnsynced);
}
}
return this._synchronized;
},
setStatus(isNotInSync) {
const outOfSync = isNotInSync === true
|| this.isFrozen === true;
if (outOfSync === true) {
this.openmct.status.set(this.domainObject.identifier, 'timeconductor-unsynced');
} else {
this.openmct.status.set(this.domainObject.identifier, '');
}
},
initCanvas() {
if (this.canvas) {
this.stopListening(this.canvas);
@@ -768,8 +729,7 @@ export default {
const ZOOM_AMT = 0.1;
event.preventDefault();
if (event.wheelDelta === undefined
|| !this.positionOverPlot) {
if (!this.positionOverPlot) {
return;
}
@@ -887,13 +847,11 @@ export default {
freeze() {
this.config.yAxis.set('frozen', true);
this.config.xAxis.set('frozen', true);
this.setStatus();
},
clear() {
this.config.yAxis.set('frozen', false);
this.config.xAxis.set('frozen', false);
this.setStatus();
this.plotHistory = [];
this.userViewportChangeEnd();
},
@@ -923,14 +881,6 @@ export default {
this.config.series.models[0].emit('change:yKey', yKey);
},
pause() {
this.freeze();
},
play() {
this.clear();
},
destroy() {
configStore.deleteStore(this.config.id);
@@ -944,16 +894,8 @@ export default {
this.filterObserver();
}
if (this.removeStatusListener) {
this.removeStatusListener();
}
this.openmct.time.off('clock', this.updateRealTime);
this.openmct.time.off('bounds', this.updateDisplayBounds);
this.openmct.objectViews.off('clearData', this.clearData);
},
updateStatus(status) {
this.$emit('statusUpdated', status);
}
}
};

View File

@@ -56,7 +56,6 @@
<div ref="plotContainer"
class="l-view-section u-style-receiver js-style-receiver"
:class="{'s-status-timeconductor-unsynced': status && status === 'timeconductor-unsynced'}"
>
<div v-show="!!loading"
class="c-loading--overlay loading"
@@ -65,7 +64,6 @@
:cursor-guide="cursorGuide"
:options="options"
@loadingUpdated="loadingUpdated"
@statusUpdated="setStatus"
/>
</div>
</div>
@@ -96,8 +94,7 @@ export default {
// hideExportButtons: false
cursorGuide: false,
gridLines: !this.options.compact,
loading: false,
status: ''
loading: false
};
},
mounted() {
@@ -134,9 +131,6 @@ export default {
toggleGridLines() {
this.gridLines = !this.gridLines;
},
setStatus(status) {
this.status = status;
}
}
};

View File

@@ -403,10 +403,6 @@ export default class PlotSeries extends Model {
* @public
*/
updateFiltersAndRefresh(updatedFilters) {
if (updatedFilters === undefined) {
return;
}
let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters));
if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) {

View File

@@ -23,14 +23,12 @@
import PlotViewProvider from './PlotViewProvider';
import OverlayPlotViewProvider from '../overlayPlot/OverlayPlotViewProvider';
import StackedPlotViewProvider from '../stackedPlot/StackedPlotViewProvider';
import PlotsInspectorViewProvider from '../inspector/PlotsInspectorViewProvider';
export default function () {
return function install(openmct) {
openmct.objectViews.addProvider(new StackedPlotViewProvider(openmct));
openmct.objectViews.addProvider(new OverlayPlotViewProvider(openmct));
openmct.objectViews.addProvider(new PlotViewProvider(openmct));
openmct.inspectorViews.addProvider(new PlotsInspectorViewProvider(openmct));
};
}

View File

@@ -26,8 +26,6 @@ import Vue from "vue";
import StackedPlot from "../stackedPlot/StackedPlot.vue";
import configStore from "@/plugins/plot/vue/single/configuration/configStore";
import EventEmitter from "EventEmitter";
import PlotOptions from "../inspector/PlotOptions.vue";
import PlotConfigurationModel from "@/plugins/plot/vue/single/configuration/PlotConfigurationModel";
describe("the plugin", function () {
let element;
@@ -176,35 +174,6 @@ describe("the plugin", function () {
expect(plotView).toBeDefined();
});
it('provides an inspector view for overlay plots', () => {
let selection = [
[
{
context: {
item: {
id: "test-object",
type: "telemetry.plot.overlay",
telemetry: {
values: [{
key: "some-key"
}]
}
}
}
},
{
context: {
item: {
type: 'time-strip'
}
}
}
]
];
const plotInspectorView = openmct.inspectorViews.get(selection);
expect(plotInspectorView.length).toEqual(1);
});
it("provides a stacked plot view for objects with telemetry", () => {
const testTelemetryObject = {
id: "test-object",
@@ -313,46 +282,6 @@ describe("the plugin", function () {
expect(options[0].value).toBe("Some attribute");
expect(options[1].value).toBe("Another attribute");
});
it('hides the pause and play controls', () => {
let pauseEl = element.querySelectorAll(".c-button-set .icon-pause");
let playEl = element.querySelectorAll(".c-button-set .icon-arrow-right");
expect(pauseEl.length).toBe(0);
expect(playEl.length).toBe(0);
});
describe('pause and play controls', () => {
beforeEach(() => {
openmct.time.clock('local', {
start: -1000,
end: 100
});
return Vue.nextTick();
});
it('shows the pause controls', (done) => {
Vue.nextTick(() => {
let pauseEl = element.querySelectorAll(".c-button-set .icon-pause");
expect(pauseEl.length).toBe(1);
done();
});
});
it('shows the play control if plot is paused', (done) => {
let pauseEl = element.querySelector(".c-button-set .icon-pause");
const clickEvent = createMouseEvent("click");
pauseEl.dispatchEvent(clickEvent);
Vue.nextTick(() => {
let playEl = element.querySelectorAll(".c-button-set .is-paused");
expect(playEl.length).toBe(1);
done();
});
});
});
});
describe("The stacked plot view", () => {
@@ -649,218 +578,4 @@ describe("the plugin", function () {
});
});
describe('the inspector view', () => {
let component;
let viewComponentObject;
let mockComposition;
let testTelemetryObject;
let selection;
let config;
beforeEach((done) => {
testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
selection = [
[
{
context: {
item: {
id: "test-object",
identifier: {
key: "test-object",
namespace: ''
},
type: "telemetry.plot.overlay",
configuration: {
series: [
{
identifier: {
key: "test-object",
namespace: ''
}
}
]
},
composition: []
}
}
},
{
context: {
item: {
type: 'time-strip',
identifier: {
key: 'some-other-key',
namespace: ''
}
}
}
}
]
];
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testTelemetryObject);
return [testTelemetryObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
const configId = openmct.objects.makeKeyString(selection[0][0].context.item.identifier);
config = new PlotConfigurationModel({
id: configId,
domainObject: selection[0][0].context.item,
openmct: openmct
});
configStore.add(configId, config);
let viewContainer = document.createElement('div');
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
PlotOptions
},
provide: {
openmct: openmct,
domainObject: selection[0][0].context.item,
path: [selection[0][0].context.item, selection[0][1].context.item]
},
template: '<plot-options/>'
});
Vue.nextTick(() => {
viewComponentObject = component.$root.$children[0];
done();
});
});
describe('in view only mode', () => {
let browseOptionsEl;
let editOptionsEl;
beforeEach(() => {
browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse');
editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit');
});
it('does not show the edit options', () => {
expect(editOptionsEl).toBeNull();
});
it('shows the name', () => {
const seriesEl = browseOptionsEl.querySelector('.c-object-label__name');
expect(seriesEl.innerHTML).toEqual(testTelemetryObject.name);
});
it('shows in collapsed mode', () => {
const seriesEl = browseOptionsEl.querySelectorAll('.c-disclosure-triangle--expanded');
expect(seriesEl.length).toEqual(0);
});
it('shows in expanded mode', () => {
let expandControl = browseOptionsEl.querySelector(".c-disclosure-triangle");
const clickEvent = createMouseEvent("click");
expandControl.dispatchEvent(clickEvent);
const plotOptionsProperties = browseOptionsEl.querySelectorAll('.js-plot-options-browse-properties .grid-row');
expect(plotOptionsProperties.length).toEqual(5);
});
});
describe('in edit mode', () => {
let editOptionsEl;
let browseOptionsEl;
beforeEach((done) => {
viewComponentObject.setEditState(true);
Vue.nextTick(() => {
editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit');
browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse');
done();
});
});
it('does not show the browse options', () => {
expect(browseOptionsEl).toBeNull();
});
it('shows the name', () => {
const seriesEl = editOptionsEl.querySelector('.c-object-label__name');
expect(seriesEl.innerHTML).toEqual(testTelemetryObject.name);
});
it('shows in collapsed mode', () => {
const seriesEl = editOptionsEl.querySelectorAll('.c-disclosure-triangle--expanded');
expect(seriesEl.length).toEqual(0);
});
it('shows in collapsed mode', () => {
const seriesEl = editOptionsEl.querySelectorAll('.c-disclosure-triangle--expanded');
expect(seriesEl.length).toEqual(0);
});
it('renders expanded', () => {
const expandControl = editOptionsEl.querySelector(".c-disclosure-triangle");
const clickEvent = createMouseEvent("click");
expandControl.dispatchEvent(clickEvent);
const plotOptionsProperties = editOptionsEl.querySelectorAll(".js-plot-options-edit-properties .grid-row");
expect(plotOptionsProperties.length).toEqual(6);
});
it('shows yKeyOptions', () => {
const expandControl = editOptionsEl.querySelector(".c-disclosure-triangle");
const clickEvent = createMouseEvent("click");
expandControl.dispatchEvent(clickEvent);
const plotOptionsProperties = editOptionsEl.querySelectorAll(".js-plot-options-edit-properties .grid-row");
const yKeySelection = plotOptionsProperties[0].querySelector('select');
const options = Array.from(yKeySelection.options).map((option) => {
return option.value;
});
expect(options).toEqual([testTelemetryObject.telemetry.values[1].key, testTelemetryObject.telemetry.values[2].key]);
});
it('shows yAxis options', () => {
const expandControl = editOptionsEl.querySelector(".c-disclosure-triangle");
const clickEvent = createMouseEvent("click");
expandControl.dispatchEvent(clickEvent);
const yAxisProperties = editOptionsEl.querySelectorAll("div.grid-properties:first-of-type .l-inspector-part");
expect(yAxisProperties.length).toEqual(3);
});
});
});
});

View File

@@ -90,7 +90,6 @@ export default {
const onTickWidthChange = this.onTickWidthChange;
const loadingUpdated = this.loadingUpdated;
const setStatus = this.setStatus;
const openmct = this.openmct;
const object = this.object;
@@ -112,23 +111,17 @@ export default {
return {
...getProps(),
onTickWidthChange,
loadingUpdated,
setStatus
loadingUpdated
};
},
template: '<div ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver" :data-status="status" :class="{\'s-status-timeconductor-unsynced\': status && status === \'timeconductor-unsynced\'}"><div v-show="!!loading" class="c-loading--overlay loading"></div><mct-plot :grid-lines="gridLines" :cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" :options="options" @plotTickWidth="onTickWidthChange" @statusUpdated="setStatus" @loadingUpdated="loadingUpdated"/></div>'
template: '<div ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver"><div v-show="!!loading" class="c-loading--overlay loading"></div><mct-plot :grid-lines="gridLines" :cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" :options="options" @plotTickWidth="onTickWidthChange" @loadingUpdated="loadingUpdated"/></div>'
});
},
onTickWidthChange() {
this.$emit('plotTickWidth', ...arguments);
},
setStatus(status) {
this.status = status;
this.updateComponentProp('status', status);
},
loadingUpdated(loaded) {
this.loading = loaded;
this.updateComponentProp('loading', loaded);
},
getProps() {
return {
@@ -136,8 +129,7 @@ export default {
cursorGuide: this.cursorGuide,
plotTickWidth: this.plotTickWidth,
loading: this.loading,
options: this.options,
status: this.status
options: this.options
};
}
}

View File

@@ -94,7 +94,6 @@ define([
initialize() {
if (this.domainObject.type === 'table') {
this.filterObserver = this.openmct.objects.observe(this.domainObject, 'configuration.filters', this.updateFilters);
this.filters = this.domainObject.configuration.filters;
this.loadComposition();
} else {
this.addTelemetryObject(this.domainObject);
@@ -139,18 +138,7 @@ define([
this.emit('object-added', telemetryObject);
}
updateFilters(updatedFilters) {
let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters));
if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) {
this.filters = deepCopiedFilters;
this.clearAndResubscribe();
} else {
this.filters = deepCopiedFilters;
}
}
clearAndResubscribe() {
updateFilters() {
this.filteredRows.clear();
this.boundedRows.clear();
Object.keys(this.subscriptions).forEach(this.unsubscribe, this);
@@ -234,15 +222,11 @@ define([
getColumnMapForObject(objectKeyString) {
let columns = this.configuration.getColumns();
if (columns[objectKeyString]) {
return columns[objectKeyString].reduce((map, column) => {
map[column.getKey()] = column;
return columns[objectKeyString].reduce((map, column) => {
map[column.getKey()] = column;
return map;
}, {});
}
return {};
return map;
}, {});
}
addColumnsForObject(telemetryObject) {

View File

@@ -37,6 +37,8 @@ define([
this.objectMutated = this.objectMutated.bind(this);
//Make copy of configuration, otherwise change detection is impossible if shared instance is being modified.
this.oldConfiguration = JSON.parse(JSON.stringify(this.getConfiguration()));
this.unlistenFromMutation = openmct.objects.observe(domainObject, '*', this.objectMutated);
}
getConfiguration() {
@@ -162,7 +164,9 @@ define([
this.updateConfiguration(configuration);
}
destroy() {}
destroy() {
this.unlistenFromMutation();
}
}
return TelemetryTableConfiguration;

View File

@@ -100,9 +100,6 @@ define([
destroy: function (element) {
component.$destroy();
component = undefined;
},
_getTable: function () {
return table;
}
};

View File

@@ -46,7 +46,6 @@ define(
filter = filter.trim().toLowerCase();
let rowsToFilter = this.getRowsToFilter(columnKey, filter);
if (filter.length === 0) {
delete this.columnFilters[columnKey];
} else {
@@ -57,16 +56,6 @@ define(
this.emit('filter');
}
setColumnRegexFilter(columnKey, filter) {
filter = filter.trim();
let rowsToFilter = this.masterCollection.getRows();
this.columnFilters[columnKey] = new RegExp(filter);
this.rows = rowsToFilter.filter(this.matchesFilters, this);
this.emit('filter');
}
/**
* @private
*/
@@ -82,10 +71,6 @@ define(
* @private
*/
isSubsetOfCurrentFilter(columnKey, filter) {
if (this.columnFilters[columnKey] instanceof RegExp) {
return false;
}
return this.columnFilters[columnKey]
&& filter.startsWith(this.columnFilters[columnKey])
// startsWith check will otherwise fail when filter cleared
@@ -112,11 +97,7 @@ define(
return false;
}
if (this.columnFilters[key] instanceof RegExp) {
doesMatchFilters = this.columnFilters[key].test(formattedValue);
} else {
doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1;
}
doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1;
});
return doesMatchFilters;

View File

@@ -188,17 +188,7 @@
class="c-table__search"
@input="filterChanged(key)"
@clear="clearFilter(key)"
>
<button
class="c-search__use-regex"
:class="{ 'is-active': enableRegexSearch[key] }"
title="Click to enable regex: enter a string with slashes, like this: /regex_exp/"
@click="toggleRegex(key)"
>
/R/
</button>
</search>
/>
</table-column-header>
</tr>
</thead>
@@ -371,7 +361,6 @@ export default {
paused: false,
markedRows: [],
isShowingMarkedRowsOnly: false,
enableRegexSearch: {},
hideHeaders: configuration.hideHeaders,
totalNumberOfRows: 0
};
@@ -629,16 +618,7 @@ export default {
this.headersHolderEl.scrollLeft = this.scrollable.scrollLeft;
},
filterChanged(columnKey) {
if (this.enableRegexSearch[columnKey]) {
if (this.isCompleteRegex(this.filters[columnKey])) {
this.table.filteredRows.setColumnRegexFilter(columnKey, this.filters[columnKey].slice(1, -1));
} else {
return;
}
} else {
this.table.filteredRows.setColumnFilter(columnKey, this.filters[columnKey]);
}
this.table.filteredRows.setColumnFilter(columnKey, this.filters[columnKey]);
this.setHeight();
},
clearFilter(columnKey) {
@@ -976,18 +956,6 @@ export default {
this.$nextTick().then(this.calculateColumnWidths);
},
toggleRegex(key) {
this.$set(this.filters, key, '');
if (this.enableRegexSearch[key] === undefined) {
this.$set(this.enableRegexSearch, key, true);
} else {
this.$set(this.enableRegexSearch, key, !this.enableRegexSearch[key]);
}
},
isCompleteRegex(string) {
return (string.length > 2 && string[0] === '/' && string[string.length - 1] === '/');
},
getViewContext() {
return {
type: 'telemetry-table',

View File

@@ -113,7 +113,6 @@ describe("the plugin", () => {
let applicableViews;
let tableViewProvider;
let tableView;
let tableInstance;
beforeEach(() => {
testTelemetryObject = {
@@ -180,8 +179,6 @@ describe("the plugin", () => {
tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]);
tableView.show(child, true);
tableInstance = tableView._getTable();
return telemetryPromise.then(() => Vue.nextTick());
});
@@ -231,41 +228,5 @@ describe("the plugin", () => {
expect(toColumnText).toEqual(firstColumnText);
});
});
it("Supports filtering telemetry by regular text search", () => {
tableInstance.filteredRows.setColumnFilter("some-key", "1");
return Vue.nextTick().then(() => {
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(filteredRowElements.length).toEqual(1);
tableInstance.filteredRows.setColumnFilter("some-key", "");
return Vue.nextTick().then(() => {
let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(allRowElements.length).toEqual(3);
});
});
});
it("Supports filtering using Regex", () => {
tableInstance.filteredRows.setColumnRegexFilter("some-key", "^some-value$");
return Vue.nextTick().then(() => {
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(filteredRowElements.length).toEqual(0);
tableInstance.filteredRows.setColumnRegexFilter("some-key", "^some-value");
return Vue.nextTick().then(() => {
let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(allRowElements.length).toEqual(3);
});
});
});
});
});

View File

@@ -39,9 +39,6 @@ const DEFAULT_DURATION_FORMATTER = 'duration';
const LOCAL_STORAGE_HISTORY_KEY_FIXED = 'tcHistory';
const LOCAL_STORAGE_HISTORY_KEY_REALTIME = 'tcHistoryRealtime';
const DEFAULT_RECORDS = 10;
const ONE_MINUTE = 60 * 1000;
const ONE_HOUR = ONE_MINUTE * 60;
const ONE_DAY = ONE_HOUR * 24;
export default {
inject: ['openmct', 'configuration'],
@@ -138,20 +135,10 @@ export default {
methods: {
getHistoryMenuItems() {
const history = this.historyForCurrentTimeSystem.map(timespan => {
let name;
let startTime = this.formatTime(timespan.start);
let description = `${startTime} - ${this.formatTime(timespan.end)}`;
if (this.timeSystem.isUTCBased && !this.openmct.time.clock()) {
name = `${startTime} ${this.getDuration(timespan.end - timespan.start)}`;
} else {
name = description;
}
return {
cssClass: 'icon-history',
name,
description,
name: `${this.formatTime(timespan.start)} - ${this.formatTime(timespan.end)}`,
description: `${this.formatTime(timespan.start)} - ${this.formatTime(timespan.end)}`,
callBack: () => this.selectTimespan(timespan)
};
});
@@ -176,41 +163,6 @@ export default {
};
});
},
getDuration(numericDuration) {
let result;
let age;
if (numericDuration > ONE_DAY - 1) {
age = this.normalizeAge((numericDuration / ONE_DAY).toFixed(2));
result = `+ ${age} day`;
if (age !== 1) {
result += 's';
}
} else if (numericDuration > ONE_HOUR - 1) {
age = this.normalizeAge((numericDuration / ONE_HOUR).toFixed(2));
result = `+ ${age} hour`;
if (age !== 1) {
result += 's';
}
} else {
age = this.normalizeAge((numericDuration / ONE_MINUTE).toFixed(2));
result = `+ ${age} min`;
if (age !== 1) {
result += 's';
}
}
return result;
},
normalizeAge(num) {
const hundredtized = num * 100;
const isWhole = hundredtized % 100 === 0;
return isWhole ? hundredtized / 100 : num;
},
getHistoryFromLocalStorage() {
const localStorageHistory = localStorage.getItem(this.storageKey);
const history = localStorageHistory ? JSON.parse(localStorageHistory) : undefined;
@@ -232,16 +184,24 @@ export default {
start: this.isFixed ? this.bounds.start : this.offsets.start,
end: this.isFixed ? this.bounds.end : this.offsets.end
};
let self = this;
// no dupes
currentHistory = currentHistory.filter(ts => !(ts.start === timespan.start && ts.end === timespan.end));
currentHistory.unshift(timespan); // add to front
function isNotEqual(entry) {
const start = entry.start !== self.start;
const end = entry.end !== self.end;
if (currentHistory.length > this.records) {
currentHistory.length = this.records;
return start || end;
}
currentHistory = currentHistory.filter(isNotEqual, timespan);
while (currentHistory.length >= this.records) {
currentHistory.pop();
}
currentHistory.unshift(timespan);
this.$set(this[this.currentHistory], key, currentHistory);
this.persistHistoryToLocalStorage();
},
selectTimespan(timespan) {

View File

@@ -1,113 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<swim-lane :icon-class="item.type.definition.cssClass"
:min-height="item.height"
:show-ucontents="item.domainObject.type === 'plan'"
:span-rows-count="item.rowCount"
>
<template slot="label">
{{ item.domainObject.name }}
</template>
<object-view
ref="objectView"
slot="object"
class="u-contents"
:default-object="item.domainObject"
:object-view-key="item.viewKey"
:object-path="item.objectPath"
/>
</swim-lane>
</template>
<script>
import ObjectView from '@/ui/components/ObjectView.vue';
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
export default {
components: {
ObjectView,
SwimLane
},
inject: ['openmct'],
props: {
item: {
type: Object,
required: true
}
},
data() {
return {
domainObject: undefined,
mutablePromise: undefined
};
},
watch: {
item(newItem) {
if (!this.context) {
return;
}
this.context.item = newItem.domainObject;
}
},
mounted() {
if (this.openmct.objects.supportsMutation(this.item.domainObject.identifier)) {
this.mutablePromise = this.openmct.objects.getMutable(this.item.domainObject.identifier)
.then(this.setObject);
} else {
this.openmct.objects.get(this.item.domainObject.identifier)
.then(this.setObject);
}
},
beforeDestroy() {
if (this.removeSelectable) {
this.removeSelectable();
}
if (this.mutablePromise) {
this.mutablePromise.then(() => {
this.openmct.objects.destroyMutable(this.domainObject);
});
} else {
this.openmct.objects.destroyMutable(this.domainObject);
}
},
methods: {
setObject(domainObject) {
this.domainObject = domainObject;
this.mutablePromise = undefined;
this.$nextTick(() => {
let reference = this.$refs.objectView;
if (reference) {
let childContext = this.$refs.objectView.getSelectionContext();
childContext.item = domainObject;
this.context = childContext;
this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context);
}
});
}
}
};
</script>

View File

@@ -52,10 +52,22 @@
:key="item.keyString"
class="u-contents c-timeline__content"
>
<timeline-object-view
class="u-contents"
:item="item"
/>
<swim-lane :icon-class="item.type.definition.cssClass"
:min-height="item.height"
:show-ucontents="item.domainObject.type === 'plan'"
:span-rows-count="item.rowCount"
>
<template slot="label">
{{ item.domainObject.name }}
</template>
<object-view
slot="object"
class="u-contents"
:default-object="item.domainObject"
:object-view-key="item.viewKey"
:object-path="item.objectPath"
/>
</swim-lane>
</div>
</div>
</div>
@@ -63,7 +75,7 @@
</template>
<script>
import TimelineObjectView from './TimelineObjectView.vue';
import ObjectView from '@/ui/components/ObjectView.vue';
import TimelineAxis from '../../ui/components/TimeSystemAxis.vue';
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
import { getValidatedPlan } from "../plan/util";
@@ -89,7 +101,7 @@ function getViewKey(domainObject, objectPath, openmct) {
export default {
components: {
TimelineObjectView,
ObjectView,
TimelineAxis,
SwimLane
},
@@ -146,7 +158,6 @@ export default {
},
removeItem(identifier) {
let index = this.items.findIndex(item => this.openmct.objects.areIdsEqual(identifier, item.domainObject.identifier));
this.removeSelectable(this.items[index]);
this.items.splice(index, 1);
},
reorder(reorderPlan) {

View File

@@ -237,13 +237,11 @@ define(
const capture = this.capture.bind(this, selectable);
const selectCapture = this.selectCapture.bind(this, selectable);
let removeMutable = false;
element.addEventListener('click', capture, true);
element.addEventListener('click', selectCapture);
if (context.item && context.item.isMutable !== true) {
removeMutable = true;
if (context.item) {
context.item = this.openmct.objects._toMutable(context.item);
}
@@ -259,7 +257,7 @@ define(
element.removeEventListener('click', capture, true);
element.removeEventListener('click', selectCapture);
if (context.item !== undefined && context.item.isMutable && removeMutable === true) {
if (context.item !== undefined && context.item.isMutable) {
this.openmct.objects.destroyMutable(context.item);
}
}).bind(this);

View File

@@ -638,16 +638,6 @@
box-shadow: $shdw;
}
@mixin smallerControlButtons() {
.c-click-icon,
.c-button,
.c-icon-button {
// Shrink buttons a bit when they appear in containers
font-size: 0.9em;
padding: 4px;
}
}
@mixin wrappedInput() {
// An input that is wrapped. Optionally includes a __label or icon element.
// Based on .c-search.

View File

@@ -129,7 +129,14 @@
}
}
@include smallerControlButtons;
.c-click-icon,
.c-button,
.c-icon-button {
// Shrink buttons a bit when they appear in a frame
border-radius: $smallCr !important;
font-size: 0.9em;
padding: 5px;
}
&.has-complex-content {
> .c-so-view__view-large { display: block; }

View File

@@ -1,11 +1,6 @@
@mixin visibleRegexButton {
opacity: 1;
padding: 1px 3px;
width: 24px;
}
.c-search {
@include wrappedInput();
padding-top: 2px;
padding-bottom: 2px;
@@ -14,46 +9,11 @@
content: $glyph-icon-magnify;
}
&__use-regex {
// Button
$c: $colorBodyFg;
background: rgba($c, 0.2);
border: 1px solid rgba($c, 0.3);
color: $c;
border-radius: $controlCr;
font-weight: bold;
letter-spacing: 1px;
font-size: 0.8em;
margin-left: $interiorMarginSm;
min-width: 0;
opacity: 0;
order: 2;
overflow: hidden;
padding: 1px 0;
transform-origin: left;
transition: $transOut;
width: 0;
&.is-active {
$c: $colorBtnActiveBg;
@include visibleRegexButton();
background: rgba($c, 0.3);
border-color: $c;
color: $c;
}
}
&__clear-input {
display: none;
order: 99;
padding: 1px 0;
}
&.is-active {
.c-search__use-regex {
margin-left: 0;
}
.c-search__clear-input {
display: block;
}
@@ -61,15 +21,6 @@
input[type='text'],
input[type='search'] {
margin-left: $interiorMargin;
order: 3;
text-align: left;
}
&:hover {
.c-search__use-regex {
@include visibleRegexButton();
transition: $transIn;
}
}
}

View File

@@ -15,7 +15,6 @@
class="c-search__clear-input icon-x-in-circle"
@click="clearInput"
></a>
<slot></slot>
</div>
</template>

View File

@@ -21,6 +21,7 @@
<div class="c-swimlane__lane-object"
:style="{'min-height': minHeight}"
:class="{'u-contents': showUcontents}"
data-selectable
>
<slot name="object"></slot>
</div>

View File

@@ -22,7 +22,5 @@
.c-plan {
display: contents;
}
@include smallerControlButtons;
}
}

View File

@@ -12,7 +12,7 @@
</template>
<script>
import CreateAction from '../../../platform/commonUI/edit/src/creation/CreateAction';
import CreateAction from '@/api/forms/actions/CreateAction';
import objectUtils from 'objectUtils';
export default {
@@ -74,6 +74,12 @@ export default {
// 4. perform action.
return this.openmct.objects.get(this.openmct.router.path[0].identifier)
.then((currentObject) => {
console.log('type', key);
const createAction = new CreateAction(this.openmct, key, currentObject);
createAction.invoke();
let legacyContextualParent = this.convertToLegacy(currentObject);
let legacyType = this.openmct.$injector.get('typeService').getType(key);
let context = {
@@ -87,7 +93,6 @@ export default {
this.openmct
);
return action.perform();
});
},
convertToLegacy(domainObject) {

View File

@@ -710,25 +710,25 @@ export default {
}
});
},
aggregateSearchResults(results, abortSignal) {
async aggregateSearchResults(results, abortSignal) {
for (const result of results) {
if (!abortSignal.aborted) {
this.openmct.objects.getOriginalPath(result.identifier).then((objectPath) => {
// removing the item itself, as the path we pass to buildTreeItem is a parent path
objectPath.shift();
const objectPath = await this.openmct.objects.getOriginalPath(result.identifier);
// if root, remove, we're not using in object path for tree
let lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;
if (lastObject && lastObject.type === 'root') {
objectPath.pop();
}
// removing the item itself, as the path we pass to buildTreeItem is a parent path
objectPath.shift();
// we reverse the objectPath in the tree, so have to do it here first,
// since this one is already in the correct direction
let resultObject = this.buildTreeItem(result, objectPath.reverse());
// if root, remove, we're not using in object path for tree
let lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;
if (lastObject && lastObject.type === 'root') {
objectPath.pop();
}
this.searchResultItems.push(resultObject);
});
// we reverse the objectPath in the tree, so have to do it here first,
// since this one is already in the correct direction
let resultObject = this.buildTreeItem(result, objectPath.reverse());
this.searchResultItems.push(resultObject);
}
}
},