Compare commits

..

20 Commits

Author SHA1 Message Date
David Tsay
3b316ed491 fix identifier issues 2022-03-14 13:53:33 -07:00
David Tsay
282ead581a object get in onWorkerMessage call should use identifier 2022-03-14 11:21:04 -07:00
David Tsay
c4f18a4797 object get in indexing process should use identifier not key 2022-03-10 14:27:22 -08:00
David Tsay
c273e83093 debug for search 2022-03-10 14:26:40 -08:00
David Tsay
e4b9242864 hack to parse identifier if namespace and key was mashed 2022-03-01 16:10:53 -08:00
David Tsay
ceddadcac6 fix canvas fallback in plots
https://github.com/nasa/openmct/pull/4784
2022-01-26 14:27:33 -08:00
David Tsay
6e2437b09e fix navigate after create path 2022-01-18 11:19:35 -08:00
David Tsay
5a44931537 default to string not undefined 2022-01-13 17:15:24 -08:00
David Tsay
f165d9c064 Revert "bring back identifierService for getProvider"
This reverts commit 830f321f90.
2022-01-13 17:08:08 -08:00
David Tsay
613973d936 set namespace for malformed identifiers 2022-01-13 17:06:24 -08:00
David Tsay
830f321f90 bring back identifierService for getProvider 2022-01-13 14:16:28 -08:00
David Tsay
a14cd62878 Merge branch 'release/1.8.3' into vista-r4.9-release 2022-01-10 13:49:05 -08:00
Shefali Joshi
8314d03af5 Fix Link Action to accept the right input for validation (#4696) 2022-01-07 14:46:31 -08:00
Nikhil
187da3c462 Snapshot notice link not navigating as expected #4194 (#4686) 2022-01-07 11:51:11 -08:00
Shefali Joshi
e4f134ca59 Mct 4555 rebased v3 (#4689)
* Preserve the previousFocusedImage for subscription updates, bound change for local and fixed time

* Only preserve previous focused image if paused

* Forcibly reset imageContainer size to prevent aspect ratio distortion

* Remove unneccesary mixin invocation

* Use image history instead of imagehistory size for watcher. Revert other changes

* Added check if last image index is selected

* isPaused instead of paused

Co-authored-by: Michael Rogers <contact@mhrogers.com>
2022-01-07 11:09:09 -06:00
Shefali Joshi
76829ad252 Check for ref when trying to get the style receiver. (#4687)
Also ensure that the property being displayed in Properties is actually a part of the domainObject.
2022-01-07 10:57:10 -06:00
Nikhil
a8da0d5917 Notebook Snapshotting to the default Notebook isn't working (#4475)
* Notebook Snapshotting to the default Notebook isn't working #4469
2022-01-06 16:44:21 -08:00
David Tsay
488beb5b3f new colors show better on espresso and snow themes 2022-01-06 11:25:33 -08:00
David Tsay
2f63718385 use relative path (#4683) 2022-01-06 11:14:21 -08:00
Shefali Joshi
433f1bf28e Fix object creation (#4675)
* Save the object before adding it to the parent so that transaction committing works properly
* Fix object creation - composition policy changes
2022-01-05 11:05:02 -08:00
46 changed files with 1217 additions and 1096 deletions

View File

@@ -2,7 +2,7 @@ version: 2.1
executors:
pw-focal-development:
docker:
- image: mcr.microsoft.com/playwright:v1.17.2-focal
- image: mcr.microsoft.com/playwright:focal
environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
parameters:
@@ -76,14 +76,14 @@ jobs:
node-version: <<parameters.node-version>>
- run: npm audit --audit-level=low
- generate_and_store_version_and_filesystem_artifacts
node14-lint:
parameters:
node-version:
type: string
node10-lint:
executor: pw-focal-development
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- checkout
- node/install:
install-npm: false #Cannot install latest npm version with node10.
node-version: lts/dubnium
- run: npm install
- run: npm run lint
- generate_and_store_version_and_filesystem_artifacts
unit-test:
@@ -141,8 +141,7 @@ jobs:
workflows:
overall-circleci-commit-status: #These jobs run on every commit
jobs:
- node14-lint:
node-version: lts/fermium
- node10-lint
- unit-test:
name: node12-chrome
node-version: lts/erbium
@@ -154,11 +153,15 @@ workflows:
post-steps:
- upload_code_covio
- e2e-test:
name: e2e-ci
name: e2e-smoke
node-version: lts/fermium
suite: ci
the-nightly: #These jobs do not run on PRs, but against master at night
jobs:
- unit-test:
name: node10-chrome-nightly
node-version: lts/dubnium
browser: ChromeHeadless
- unit-test:
name: node12-firefoxESR-nightly
node-version: lts/erbium

46
API.md
View File

@@ -52,8 +52,6 @@
- [The URL Status Indicator](#the-url-status-indicator)
- [Creating a Simple Indicator](#creating-a-simple-indicator)
- [Custom Indicators](#custom-indicators)
- [Priority API](#priority-api)
- [Priority Types](#priority-types)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
@@ -249,24 +247,16 @@ To do so, use the `addRoot` method of the object API.
eg.
```javascript
openmct.objects.addRoot({
namespace: "example.namespace",
key: "my-key"
},
openmct.priority.HIGH);
namespace: "example.namespace",
key: "my-key"
});
```
The `addRoot` function takes a two arguments, the first can be an [object identifier](#domain-objects-and-identifiers) for a root level object, or an array of identifiers for root
level objects, or a function that returns a promise for an identifier or an array of root level objects, the second is a [priority](#priority-api) or numeric value.
The `addRoot` function takes a single [object identifier](#domain-objects-and-identifiers)
as an argument.
When using the `getAll` method of the object API, they will be returned in order of priority.
eg.
```javascript
openmct.objects.addRoot(identifier, openmct.priority.LOW); // low = -1000, will appear last in composition or tree
openmct.objects.addRoot(otherIdentifier, openmct.priority.HIGH); // high = 1000, will appear first in composition or tree
```
Root objects are loaded just like any other objects, i.e. via an object provider.
Root objects are loaded just like any other objects, i.e. via an object
provider.
## Object Providers
@@ -1061,25 +1051,3 @@ A completely custom indicator can be added by simply providing a DOM element to
element: domNode
});
```
## Priority API
Open MCT provides some built-in priority values that can be used in the application for view providers, indicators, root object order, and more.
### Priority Types
Currently, the Open MCT Priority API provides (type: numeric value):
- HIGH: 1000
- Default: 0
- LOW: -1000
View provider Example:
``` javascript
class ViewProvider {
...
priority() {
return openmct.priority.HIGH;
}
}
```

View File

@@ -1,4 +1,4 @@
# Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/nasa/openmct.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/nasa/openmct/context:javascript) [![codecov](https://codecov.io/gh/nasa/openmct/branch/master/graph/badge.svg?token=7DQIipp3ej)](https://codecov.io/gh/nasa/openmct) [![This project is using Percy.io for visual regression testing.](https://percy.io/static/images/percy-badge.svg)](https://percy.io/b2e34b17/openmct) [![npm version](https://img.shields.io/npm/v/openmct.svg)](https://www.npmjs.com/package/openmct)
# Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/nasa/openmct.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/nasa/openmct/context:javascript) [![codecov](https://codecov.io/gh/nasa/openmct/branch/master/graph/badge.svg?token=7DQIipp3ej)](https://codecov.io/gh/nasa/openmct) [![This project is using Percy.io for visual regression testing.](https://percy.io/static/images/percy-badge.svg)](https://percy.io/b2e34b17/openmct)
Open MCT (Open Mission Control Technologies) is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data.

View File

@@ -13,7 +13,6 @@ const config = {
timeout: 200 * 1000,
reuseExistingServer: !process.env.CI
},
workers: 2, //Limit to 2 for CircleCI Agent
use: {
browserName: "chromium",
baseURL: 'http://localhost:8080/',

View File

@@ -75,36 +75,8 @@
const TWO_HOURS = ONE_HOUR * 2;
const ONE_DAY = ONE_HOUR * 24;
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.LocalStorage('namespace1', 'namespace1'));
openmct.install(openmct.plugins.LocalStorage('namespace2', 'namespace2'));
openmct.install((openmct) => {
openmct.objects.addRoot({
namespace: 'namespace1',
key: 'root'
});
openmct.objects.addRoot({
namespace: 'namespace2',
key: 'root'
});
openmct.objects.addGetInterceptor({
appliesTo: (identifier, object) => {
return (identifier.namespace === 'namespace1'
|| identifier.namespace === 'namespace2')
&& identifier.key === 'root'
&& (object === undefined || object.type === 'missing');
},
invoke: (identifier, domainObject) => {
return {
identifier: identifier,
type: 'folder',
location: 'mine',
name: identifier.namespace,
composition: []
}
}
});
});
openmct.install(openmct.plugins.Espresso());
openmct.install(openmct.plugins.MyItems());

View File

@@ -4,16 +4,16 @@
"description": "The Open MCT core platform",
"devDependencies": {
"@braintree/sanitize-url": "^5.0.2",
"@percy/cli": "^1.0.0-beta.71",
"@percy/cli": "^1.0.0-beta.70",
"@percy/playwright": "^1.0.1",
"@playwright/test": "^1.17.2",
"@playwright/test": "^1.16.3",
"allure-playwright": "^2.0.0-beta.14",
"angular": ">=1.8.0",
"angular-route": "1.4.14",
"babel-eslint": "10.1.0",
"comma-separated-values": "^3.6.4",
"concurrently": "^3.6.1",
"copy-webpack-plugin": "^10.2.0",
"copy-webpack-plugin": "^9.0.0",
"cross-env": "^6.0.3",
"css-loader": "^4.0.0",
"d3-axis": "1.0.x",
@@ -36,7 +36,7 @@
"istanbul-instrumenter-loader": "^3.0.1",
"jasmine-core": "^4.0.0",
"jsdoc": "^3.3.2",
"karma": "6.3.10",
"karma": "6.3.9",
"karma-chrome-launcher": "3.1.0",
"karma-cli": "2.0.0",
"karma-coverage": "2.1.0",
@@ -45,20 +45,20 @@
"karma-jasmine": "4.0.1",
"karma-junit-reporter": "2.0.1",
"karma-sourcemap-loader": "0.3.8",
"karma-spec-reporter": "0.0.33",
"karma-spec-reporter": "0.0.32",
"karma-webpack": "^5.0.0",
"location-bar": "^3.0.1",
"lodash": "^4.17.12",
"markdown-toc": "^0.11.7",
"marked": "^0.3.5",
"mini-css-extract-plugin": "2.4.5",
"mini-css-extract-plugin": "^1.6.0",
"minimist": "^1.2.5",
"moment": "2.25.3",
"moment-duration-format": "^2.2.2",
"moment-timezone": "0.5.28",
"node-bourbon": "^4.2.3",
"painterro": "^1.2.56",
"playwright": "^1.17.2",
"playwright": "^1.16.3",
"plotly.js-basic-dist": "^2.5.0",
"plotly.js-gl2d-dist": "^2.5.0",
"printj": "^1.2.1",
@@ -76,7 +76,7 @@
"vue-eslint-parser": "8.0.1",
"vue-loader": "15.9.8",
"vue-template-compiler": "2.5.6",
"webpack": "^5.65.0",
"webpack": "^5.53.0",
"webpack-cli": "^4.0.0",
"webpack-dev-middleware": "^3.1.3",
"webpack-hot-middleware": "^2.22.3",
@@ -96,7 +96,7 @@
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:coverage": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" COVERAGE=true karma start --single-run",
"test:coverage:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js smoke default condition.e2e",
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js smoke",
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js",
"test:e2e:visual": "percy exec -- npx playwright test --config=e2e/playwright-visual.config.js default",
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js",
@@ -112,7 +112,7 @@
"url": "https://github.com/nasa/openmct.git"
},
"engines": {
"node": ">=12.0.1 <15.0.0"
"node": ">=10.12.2 <16.0.0"
},
"author": "",
"license": "Apache-2.0",

View File

@@ -1,5 +1,5 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
@@ -36,8 +36,8 @@ class InMemorySearchProvider {
*/
this.MAX_CONCURRENT_REQUESTS = 100;
/**
* If max results is not specified in query, use this as default.
*/
* If max results is not specified in query, use this as default.
*/
this.DEFAULT_MAX_RESULTS = 100;
this.openmct = openmct;
@@ -68,6 +68,14 @@ class InMemorySearchProvider {
this.worker.port.onmessageerror = null;
this.worker.port.close();
}
Object.entries(this.indexedIds).forEach(([keyString, unobserve]) => {
if (typeof unobserve === 'function') {
unobserve();
}
delete this.indexedIds[keyString];
});
});
}
@@ -137,7 +145,7 @@ class InMemorySearchProvider {
};
modelResults.hits = await Promise.all(event.data.results.map(async (hit) => {
const identifier = this.openmct.objects.parseKeyString(hit.keyString);
const domainObject = await this.openmct.objects.get(identifier.key);
const domainObject = await this.openmct.objects.get(identifier);
return domainObject;
}));
@@ -215,27 +223,27 @@ class InMemorySearchProvider {
onMutationOfIndexedObject(domainObject) {
const provider = this;
provider.index(domainObject.identifier, domainObject);
provider.index(domainObject);
}
/**
* Pass an id and model to the worker to be indexed. If the model has
* composition, schedule those ids for later indexing.
* Pass a domainObject to the worker to be indexed.
* If the object has composition, schedule those ids for later indexing.
* Watch for object changes and re-index object and children if so
*
* @private
* @param id a model id
* @param model a model
* @param domainObject a domainObject
*/
async index(id, domainObject) {
async index(domainObject) {
const provider = this;
const keyString = this.openmct.objects.makeKeyString(id);
const identifier = domainObject.identifier;
const keyString = this.openmct.objects.makeKeyString(identifier);
if (!this.indexedIds[keyString]) {
this.openmct.objects.observe(domainObject, `*`, this.onMutationOfIndexedObject);
this.indexedIds[keyString] = this.openmct.objects.observe(domainObject, '*', this.onMutationOfIndexedObject);
}
this.indexedIds[keyString] = true;
if ((id.key !== 'ROOT')) {
if ((identifier.key !== 'ROOT')) {
if (this.worker) {
this.worker.port.postMessage({
request: 'index',
@@ -247,11 +255,9 @@ class InMemorySearchProvider {
}
}
const composition = this.openmct.composition.registry.find(foundComposition => {
return foundComposition.appliesTo(domainObject);
});
const composition = this.openmct.composition.get(domainObject);
if (composition) {
if (composition !== undefined) {
const childIdentifiers = await composition.load(domainObject);
childIdentifiers.forEach(function (childIdentifier) {
provider.scheduleForIndexing(childIdentifier);
@@ -272,11 +278,11 @@ class InMemorySearchProvider {
this.pendingRequests += 1;
const identifier = await this.openmct.objects.parseKeyString(keyString);
const domainObject = await this.openmct.objects.get(identifier.key);
const domainObject = await this.openmct.objects.get(identifier);
delete provider.pendingIndex[keyString];
try {
if (domainObject) {
await provider.index(identifier, domainObject);
await provider.index(domainObject);
}
} catch (error) {
console.warn('Failed to index domain object ' + keyString, error);
@@ -305,9 +311,9 @@ class InMemorySearchProvider {
}
/**
* A local version of the same SharedWorker function
* if we don't have SharedWorkers available (e.g., iOS)
*/
* A local version of the same SharedWorker function
* if we don't have SharedWorkers available (e.g., iOS)
*/
localIndexItem(keyString, model) {
this.localIndexedItems[keyString] = {
type: model.type,

View File

@@ -33,8 +33,10 @@
port.onmessage = function (event) {
if (event.data.request === 'index') {
console.log('onmessage index: ', event.data);
indexItem(event.data.keyString, event.data.model);
} else if (event.data.request === 'search') {
console.log('onmessage search: ', event.data);
port.postMessage(search(event.data));
}
};
@@ -77,6 +79,8 @@
queryId: data.queryId
};
console.log('indexed on search: ', indexedItems);
results = Object.values(indexedItems).filter((indexedItem) => {
return indexedItem.name.toLowerCase().includes(input);
});

View File

@@ -41,7 +41,7 @@ function ObjectAPI(typeRegistry, openmct) {
this.typeRegistry = typeRegistry;
this.eventEmitter = new EventEmitter();
this.providers = {};
this.rootRegistry = new RootRegistry(openmct);
this.rootRegistry = new RootRegistry();
this.inMemorySearchProvider = new InMemorySearchProvider(openmct);
this.rootProvider = new RootObjectProvider(this.rootRegistry);
@@ -367,17 +367,14 @@ ObjectAPI.prototype.endTransaction = function () {
/**
* Add a root-level object.
* @param {module:openmct.ObjectAPI~Identifier|array|function} identifier an identifier or
* an array of identifiers for root level objects, or a function that returns a
* @param {module:openmct.ObjectAPI~Identifier|function} an array of
* identifiers for root level objects, or a function that returns a
* promise for an identifier or an array of root level objects.
* @param {module:openmct.PriorityAPI~priority|Number} priority a number representing
* this item(s) position in the root object's composition (example: order in object tree).
* For arrays, they are treated as blocks.
* @method addRoot
* @memberof module:openmct.ObjectAPI#
*/
ObjectAPI.prototype.addRoot = function (identifier, priority) {
this.rootRegistry.addRoot(identifier, priority);
ObjectAPI.prototype.addRoot = function (key) {
this.rootRegistry.addRoot(key);
};
/**

View File

@@ -20,43 +20,39 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import utils from './object-utils';
define([
'lodash'
], function (
_
) {
export default class RootRegistry {
constructor(openmct) {
this._rootItems = [];
this._openmct = openmct;
function RootRegistry() {
this.providers = [];
}
getRoots() {
const sortedItems = this._rootItems.sort((a, b) => b.priority - a.priority);
const promises = sortedItems.map((rootItem) => rootItem.provider());
return Promise.all(promises).then(rootItems => rootItems.flat());
}
addRoot(rootItem, priority) {
if (!this._isValid(rootItem)) {
return;
}
this._rootItems.push({
priority: priority || this._openmct.priority.DEFAULT,
provider: typeof rootItem === 'function' ? rootItem : () => rootItem
RootRegistry.prototype.getRoots = function () {
const promises = this.providers.map(function (provider) {
return provider();
});
return Promise.all(promises)
.then(_.flatten);
};
function isKey(key) {
return _.isObject(key) && _.has(key, 'key') && _.has(key, 'namespace');
}
_isValid(rootItem) {
if (utils.isIdentifier(rootItem) || typeof rootItem === 'function') {
return true;
RootRegistry.prototype.addRoot = function (key) {
if (isKey(key) || (Array.isArray(key) && key.every(isKey))) {
this.providers.push(function () {
return key;
});
} else if (typeof key === "function") {
this.providers.push(key);
}
};
if (Array.isArray(rootItem)) {
return rootItem.every(utils.isIdentifier);
}
return RootRegistry;
return false;
}
}
});

View File

@@ -55,6 +55,11 @@ define([
*/
function parseKeyString(keyString) {
if (isIdentifier(keyString)) {
// hack to workaround a bug mashing keyString into identifier.key
if (!keyString.namespace && keyString.key.includes(':')) {
return parseKeyString(keyString.key);
}
return keyString;
}
@@ -172,7 +177,6 @@ define([
}
return {
isIdentifier: isIdentifier,
toOldFormat: toOldFormat,
toNewFormat: toNewFormat,
makeKeyString: makeKeyString,

View File

@@ -19,113 +19,83 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'../RootRegistry'
], function (
RootRegistry
) {
describe('RootRegistry', function () {
let idA;
let idB;
let idC;
let registry;
import { createOpenMct, resetApplicationState } from '../../../utils/testing';
beforeEach(function () {
idA = {
key: 'keyA',
namespace: 'something'
};
idB = {
key: 'keyB',
namespace: 'something'
};
idC = {
key: 'keyC',
namespace: 'something'
};
registry = new RootRegistry();
});
describe('RootRegistry', () => {
let openmct;
let idA;
let idB;
let idC;
let idD;
it('can register a root by key', function () {
registry.addRoot(idA);
beforeEach((done) => {
openmct = createOpenMct();
idA = {
key: 'keyA',
namespace: 'something'
};
idB = {
key: 'keyB',
namespace: 'something'
};
idC = {
key: 'keyC',
namespace: 'something'
};
idD = {
key: 'keyD',
namespace: 'something'
};
return registry.getRoots()
.then(function (roots) {
expect(roots).toEqual([idA]);
});
});
openmct.on('start', done);
openmct.startHeadless();
});
it('can register multiple roots by key', function () {
registry.addRoot([idA, idB]);
afterEach(async () => {
await resetApplicationState(openmct);
});
return registry.getRoots()
.then(function (roots) {
expect(roots).toEqual([idA, idB]);
});
});
it('can register a root by identifier', () => {
openmct.objects.addRoot(idA);
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition).toEqual([idA]);
it('can register an asynchronous root ', function () {
registry.addRoot(function () {
return Promise.resolve(idA);
});
});
it('can register multiple roots by identifier', () => {
openmct.objects.addRoot([idA, idB]);
return registry.getRoots()
.then(function (roots) {
expect(roots).toEqual([idA]);
});
});
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition).toEqual([idA, idB]);
it('can register multiple asynchronous roots', function () {
registry.addRoot(function () {
return Promise.resolve([idA, idB]);
});
});
it('can register an asynchronous root ', () => {
openmct.objects.addRoot(() => Promise.resolve(idA));
return registry.getRoots()
.then(function (roots) {
expect(roots).toEqual([idA, idB]);
});
});
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition).toEqual([idA]);
it('can combine different types of registration', function () {
registry.addRoot([idA, idB]);
registry.addRoot(function () {
return Promise.resolve([idC]);
});
});
it('can register multiple asynchronous roots', () => {
openmct.objects.addRoot(() => Promise.resolve([idA, idB]));
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition).toEqual([idA, idB]);
});
});
it('can combine different types of registration', () => {
openmct.objects.addRoot([idA, idB]);
openmct.objects.addRoot(() => Promise.resolve([idC]));
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition).toEqual([idA, idB, idC]);
});
});
it('supports priority ordering for identifiers', () => {
openmct.objects.addRoot(idA, openmct.priority.LOW);
openmct.objects.addRoot(idB, openmct.priority.HIGH);
openmct.objects.addRoot(idC); // DEFAULT
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition[0]).toEqual(idB);
expect(rootObject.composition[1]).toEqual(idC);
expect(rootObject.composition[2]).toEqual(idA);
});
});
it('supports priority ordering for different types of registration', () => {
openmct.objects.addRoot(() => Promise.resolve([idC]), openmct.priority.LOW);
openmct.objects.addRoot(idB, openmct.priority.HIGH);
openmct.objects.addRoot([idA, idD]); // default
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition[0]).toEqual(idB);
expect(rootObject.composition[1]).toEqual(idA);
expect(rootObject.composition[2]).toEqual(idD);
expect(rootObject.composition[3]).toEqual(idC);
});
return registry.getRoots()
.then(function (roots) {
expect(roots).toEqual([idA, idB, idC]);
});
});
});
});

View File

@@ -62,14 +62,12 @@ export default {
}
},
mounted() {
this.refreshData = this.refreshData.bind(this);
this.setTimeContext();
this.loadComposition();
this.openmct.time.on('bounds', this.refreshData);
},
beforeDestroy() {
this.stopFollowingTimeContext();
this.openmct.time.off('bounds', this.refreshData);
this.removeAllSubscriptions();
@@ -81,21 +79,6 @@ export default {
this.composition.off('remove', this.removeTelemetryObject);
},
methods: {
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.path);
this.followTimeContext();
},
followTimeContext() {
this.timeContext.on('bounds', this.refreshData);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.refreshData);
}
},
addTelemetryObject(telemetryObject) {
// grab information we need from the added telmetry object
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
@@ -164,7 +147,7 @@ export default {
};
},
getOptions() {
const { start, end } = this.timeContext.bounds();
const { start, end } = this.openmct.time.bounds();
return {
end,
@@ -264,10 +247,10 @@ export default {
this.addTrace(trace, key);
},
isDataInTimeRange(datum, key) {
const timeSystemKey = this.timeContext.timeSystem().key;
const timeSystemKey = this.openmct.time.timeSystem().key;
let currentTimestamp = this.parse(key, timeSystemKey, datum);
return currentTimestamp && this.timeContext.bounds().end >= currentTimestamp;
return currentTimestamp && this.openmct.time.bounds().end >= currentTimestamp;
},
format(telemetryObjectKey, metadataKey, data) {
const formats = this.telemetryObjectFormats[telemetryObjectKey];

View File

@@ -48,7 +48,7 @@ define([
let indicator = {
element: component.$mount().$el,
key: 'global-clear-indicator',
key: 'clear-data-indicator',
priority: openmct.priority.DEFAULT
};

View File

@@ -1,228 +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.
*****************************************************************************/
import ClearDataPlugin from './plugin.js';
import Vue from 'vue';
import { createOpenMct, resetApplicationState, createMouseEvent } from 'utils/testing';
describe('The Clear Data Plugin:', () => {
let clearDataPlugin;
describe('The clear data action:', () => {
let openmct;
let selection;
let mockObjectPath;
let clearDataAction;
let testViewObject;
beforeEach((done) => {
openmct = createOpenMct();
clearDataPlugin = new ClearDataPlugin(
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'],
{indicator: true}
);
openmct.install(clearDataPlugin);
clearDataAction = openmct.actions.getAction('clear-data-action');
testViewObject = [{
identifier: {
key: "foo-table",
namespace: ''
},
type: "table"
}];
openmct.router.path = testViewObject;
mockObjectPath = [
{
name: 'Mock Table',
type: 'table',
identifier: {
key: "foo-table",
namespace: ''
}
}
];
selection = [
{
context: {
item: mockObjectPath[0]
}
}
];
openmct.selection.select(selection);
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(() => {
openmct.router.path = null;
return resetApplicationState(openmct);
});
it('is installed', () => {
expect(clearDataAction).toBeDefined();
});
it('is applicable on applicable objects', () => {
const gatheredActions = openmct.actions.getActionsCollection(mockObjectPath);
expect(gatheredActions.applicableActions['clear-data-action']).toBeDefined();
});
it('is not applicable on inapplicable objects', () => {
testViewObject = [{
identifier: {
key: "foo-widget",
namespace: ''
},
type: "widget"
}];
mockObjectPath = [
{
name: 'Mock Widget',
type: 'widget',
identifier: {
key: "foo-widget",
namespace: ''
}
}
];
selection = [
{
context: {
item: mockObjectPath[0]
}
}
];
openmct.selection.select(selection);
const gatheredActions = openmct.actions.getActionsCollection(mockObjectPath);
expect(gatheredActions.applicableActions['clear-data-action']).toBeUndefined();
});
it('is not applicable if object not in the selection path and not a layout', () => {
selection = [
{
context: {
item: {
name: 'Some Random Widget',
type: 'not-in-path-widget',
identifier: {
key: "something-else-widget",
namespace: ''
}
}
}
}
];
openmct.selection.select(selection);
const gatheredActions = openmct.actions.getActionsCollection(mockObjectPath);
expect(gatheredActions.applicableActions['clear-data-action']).toBeUndefined();
});
it('is applicable if object not in the selection path and is a layout', () => {
selection = [
{
context: {
item: {
name: 'Some Random Widget',
type: 'not-in-path-widget',
identifier: {
key: "something-else-widget",
namespace: ''
}
}
}
}
];
openmct.selection.select(selection);
testViewObject = [{
identifier: {
key: "foo-layout",
namespace: ''
},
type: "layout"
}];
openmct.router.path = testViewObject;
const gatheredActions = openmct.actions.getActionsCollection(mockObjectPath);
expect(gatheredActions.applicableActions['clear-data-action']).toBeDefined();
});
it('fires an event upon invocation', (done) => {
openmct.objectViews.on('clearData', (domainObject) => {
expect(domainObject).toEqual(testViewObject[0]);
done();
});
clearDataAction.invoke(testViewObject);
});
});
describe('The clear data indicator:', () => {
let openmct;
let appHolder;
beforeEach((done) => {
openmct = createOpenMct();
clearDataPlugin = new ClearDataPlugin(
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'],
{indicator: true}
);
openmct.install(clearDataPlugin);
appHolder = document.createElement('div');
document.body.appendChild(appHolder);
openmct.on('start', done);
openmct.start(appHolder);
});
it('installs', () => {
const globalClearIndicator = openmct.indicators.indicatorObjects
.find(indicator => indicator.key === 'global-clear-indicator').element;
expect(globalClearIndicator).toBeDefined();
});
it("renders its major elements", async () => {
await Vue.nextTick();
const indicatorClass = appHolder.querySelector('.c-indicator');
const iconClass = appHolder.querySelector('.icon-clear-data');
const indicatorLabel = appHolder.querySelector('.c-indicator__label');
const buttonElement = indicatorLabel.querySelector('button');
const hasMajorElements = Boolean(indicatorClass && iconClass && buttonElement);
expect(hasMajorElements).toBe(true);
expect(buttonElement.innerText).toEqual('Clear Data');
});
it("clicking the button fires the global clear", (done) => {
const indicatorLabel = appHolder.querySelector('.c-indicator__label');
const buttonElement = indicatorLabel.querySelector('button');
const clickEvent = createMouseEvent('click');
openmct.objectViews.on('clearData', () => {
// when we click the button, this event should fire
done();
});
buttonElement.dispatchEvent(clickEvent);
});
});
});

View File

@@ -0,0 +1,143 @@
/*****************************************************************************
* 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 ClearDataActionPlugin from '../plugin.js';
import ClearDataAction from '../ClearDataAction.js';
describe('When the Clear Data Plugin is installed,', () => {
const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']);
const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']);
const mockActionsProvider = jasmine.createSpyObj('actions', ['register']);
const goodMockSelectionPath = [[{
context: {
item: {
identifier: {
key: 'apple',
namespace: ''
}
}
}
}]];
const openmct = {
objectViews: mockObjectViews,
indicators: mockIndicatorProvider,
priority: {
DEFAULT: 0
},
actions: mockActionsProvider,
install: function (plugin) {
plugin(this);
},
selection: {
get: function () {
return goodMockSelectionPath;
}
},
objects: {
areIdsEqual: function (obj1, obj2) {
return true;
}
}
};
const mockObjectPath = [
{
name: 'mockObject1',
type: 'apple'
},
{
name: 'mockObject2',
type: 'banana'
}
];
it('Global Clear Indicator is installed', () => {
openmct.install(ClearDataActionPlugin(openmct, {indicator: true}));
expect(mockIndicatorProvider.add).toHaveBeenCalled();
});
it('Clear Data context menu action is installed', () => {
openmct.install(ClearDataActionPlugin(openmct, []));
expect(mockActionsProvider.register).toHaveBeenCalled();
});
it('clear data action emits a clearData event when invoked', () => {
const action = new ClearDataAction(openmct);
action.invoke(mockObjectPath);
expect(mockObjectViews.emit).toHaveBeenCalledWith('clearData', mockObjectPath[0]);
});
it('clears data on applicable objects', () => {
let action = new ClearDataAction(openmct, ['apple']);
const actionApplies = action.appliesTo(mockObjectPath);
expect(actionApplies).toBe(true);
});
it('does not clear data on inapplicable objects', () => {
let action = new ClearDataAction(openmct, ['pineapple']);
const actionApplies = action.appliesTo(mockObjectPath);
expect(actionApplies).toBe(false);
});
it('does not clear data if not in the selection path and not a layout', () => {
openmct.objects = {
areIdsEqual: function (obj1, obj2) {
return false;
}
};
openmct.router = {
path: [{type: 'not-a-layout'}]
};
let action = new ClearDataAction(openmct, ['apple']);
const actionApplies = action.appliesTo(mockObjectPath);
expect(actionApplies).toBe(false);
});
it('does clear data if not in the selection path and is a layout', () => {
openmct.objects = {
areIdsEqual: function (obj1, obj2) {
return false;
}
};
openmct.router = {
path: [{type: 'layout'}]
};
let action = new ClearDataAction(openmct, ['apple']);
const actionApplies = action.appliesTo(mockObjectPath);
expect(actionApplies).toBe(true);
});
});

View File

@@ -39,8 +39,10 @@ export default class ConditionSetViewProvider {
return isConditionSet && this.openmct.router.isNavigatedObject(objectPath);
}
canEdit(domainObject) {
return domainObject.type === 'conditionSet';
canEdit(domainObject, objectPath) {
const isConditionSet = domainObject.type === 'conditionSet';
return isConditionSet && this.openmct.router.isNavigatedObject(objectPath);
}
view(domainObject, objectPath) {

View File

@@ -109,7 +109,7 @@ export default class StyleRuleManager extends EventEmitter {
if (!styleConfiguration || !styleConfiguration.conditionSetIdentifier) {
this.initialize(styleConfiguration || {});
this.applyStaticStyle();
this.destroy(true);
this.destroy();
} else {
let isNewConditionSet = !this.conditionSetIdentifier
|| !this.openmct.objects.areIdsEqual(this.conditionSetIdentifier, styleConfiguration.conditionSetIdentifier);
@@ -180,17 +180,15 @@ export default class StyleRuleManager extends EventEmitter {
this.updateDomainObjectStyle();
}
destroy(skipEventListeners) {
destroy() {
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
delete this.stopProvidingTelemetry;
}
if (!skipEventListeners) {
this.openmct.time.off("bounds", this.refreshData);
this.openmct.editor.off('isEditing', this.toggleSubscription);
}
this.openmct.time.off("bounds", this.refreshData);
this.openmct.editor.off('isEditing', this.toggleSubscription);
this.conditionSetIdentifier = undefined;
}

View File

@@ -0,0 +1,475 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div class="c-inspector__styles c-inspect-styles">
<template v-if="!conditionSetDomainObject">
<div class="c-inspect-styles__header">
Object Style
</div>
<div class="c-inspect-styles__content">
<div v-if="staticStyle"
class="c-inspect-styles__style"
>
<StyleEditor class="c-inspect-styles__editor"
:style-item="staticStyle"
:is-editing="isEditing"
@persist="updateStaticStyle"
/>
</div>
<button
id="addConditionSet"
class="c-button c-button--major c-toggle-styling-button labeled"
@click="addConditionSet"
>
<span class="c-cs-button__label">Use Conditional Styling...</span>
</button>
</div>
</template>
<template v-else>
<div class="c-inspect-styles__header">
Conditional Object Styles
</div>
<div class="c-inspect-styles__content c-inspect-styles__condition-set">
<a v-if="conditionSetDomainObject"
class="c-object-label icon-conditional"
@click="navigateOrPreview"
>
<span class="c-object-label__name">{{ conditionSetDomainObject.name }}</span>
</a>
<template v-if="isEditing">
<button
id="changeConditionSet"
class="c-button labeled"
@click="addConditionSet"
>
<span class="c-button__label">Change...</span>
</button>
<button class="c-click-icon icon-x"
title="Remove conditional styles"
@click="removeConditionSet"
></button>
</template>
</div>
<div v-if="conditionsLoaded"
class="c-inspect-styles__conditions"
>
<div v-for="(conditionStyle, index) in conditionalStyles"
:key="index"
class="c-inspect-styles__condition"
:class="{'is-current': conditionStyle.conditionId === selectedConditionId}"
@click="applySelectedConditionStyle(conditionStyle.conditionId)"
>
<condition-error :show-label="true"
:condition="getCondition(conditionStyle.conditionId)"
/>
<condition-description :show-label="true"
:condition="getCondition(conditionStyle.conditionId)"
/>
<StyleEditor class="c-inspect-styles__editor"
:style-item="conditionStyle"
:is-editing="isEditing"
@persist="updateConditionalStyle"
/>
</div>
</div>
</template>
</div>
</template>
<script>
import StyleEditor from "./StyleEditor.vue";
import SelectorDialogTree from '@/ui/components/SelectorDialogTree.vue';
import ConditionDescription from "@/plugins/condition/components/ConditionDescription.vue";
import ConditionError from "@/plugins/condition/components/ConditionError.vue";
import Vue from 'vue';
import PreviewAction from "@/ui/preview/PreviewAction.js";
import { getApplicableStylesForItem } from "@/plugins/condition/utils/styleUtils";
import isEmpty from 'lodash/isEmpty';
export default {
name: 'ConditionalStylesView',
components: {
ConditionDescription,
ConditionError,
StyleEditor
},
inject: [
'openmct',
'selection'
],
data() {
return {
conditionalStyles: [],
staticStyle: undefined,
conditionSetDomainObject: undefined,
isEditing: this.openmct.editor.isEditing(),
conditions: undefined,
conditionsLoaded: false,
navigateToPath: '',
selectedConditionId: ''
};
},
destroyed() {
this.removeListeners();
},
mounted() {
this.itemId = '';
this.getDomainObjectFromSelection();
this.previewAction = new PreviewAction(this.openmct);
if (this.domainObject.configuration && this.domainObject.configuration.objectStyles) {
let objectStyles = this.itemId ? this.domainObject.configuration.objectStyles[this.itemId] : this.domainObject.configuration.objectStyles;
this.initializeStaticStyle(objectStyles);
if (objectStyles && objectStyles.conditionSetIdentifier) {
this.openmct.objects.get(objectStyles.conditionSetIdentifier).then(this.initialize);
this.conditionalStyles = objectStyles.styles;
}
} else {
this.initializeStaticStyle();
}
this.openmct.editor.on('isEditing', this.setEditState);
},
methods: {
isItemType(type, item) {
return item && (item.type === type);
},
getDomainObjectFromSelection() {
let layoutItem;
let domainObject;
if (this.selection[0].length > 1) {
//If there are more than 1 items in the this.selection[0] list, the first one could either be a sub domain object OR a layout drawing control.
//The second item in the this.selection[0] list is the container object (usually a layout)
layoutItem = this.selection[0][0].context.layoutItem;
const item = this.selection[0][0].context.item;
this.canHide = true;
if (item
&& (!layoutItem || (this.isItemType('subobject-view', layoutItem)))) {
domainObject = item;
} else {
domainObject = this.selection[0][1].context.item;
if (layoutItem) {
this.itemId = layoutItem.id;
}
}
} else {
domainObject = this.selection[0][0].context.item;
}
this.domainObject = domainObject;
this.initialStyles = getApplicableStylesForItem(domainObject, layoutItem);
this.$nextTick(() => {
this.removeListeners();
if (this.domainObject) {
this.stopObserving = this.openmct.objects.observe(this.domainObject, '*', newDomainObject => this.domainObject = newDomainObject);
this.stopObservingItems = this.openmct.objects.observe(this.domainObject, 'configuration.items', this.updateDomainObjectItemStyles);
}
});
},
removeListeners() {
if (this.stopObserving) {
this.stopObserving();
}
if (this.stopObservingItems) {
this.stopObservingItems();
}
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
delete this.stopProvidingTelemetry;
}
},
initialize(conditionSetDomainObject) {
//If there are new conditions in the conditionSet we need to set those styles to default
this.conditionSetDomainObject = conditionSetDomainObject;
this.enableConditionSetNav();
this.initializeConditionalStyles();
},
setEditState(isEditing) {
this.isEditing = isEditing;
if (this.isEditing) {
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
delete this.stopProvidingTelemetry;
}
} else {
this.subscribeToConditionSet();
}
},
addConditionSet() {
let conditionSetDomainObject;
let self = this;
function handleItemSelection({ item }) {
if (item) {
conditionSetDomainObject = item;
}
}
function dismissDialog(overlay, initialize) {
overlay.dismiss();
if (initialize && conditionSetDomainObject) {
self.conditionSetDomainObject = conditionSetDomainObject;
self.conditionalStyles = [];
self.initializeConditionalStyles();
}
}
let vm = new Vue({
components: { SelectorDialogTree },
provide: {
openmct: this.openmct
},
data() {
return {
handleItemSelection,
title: 'Select Condition Set'
};
},
template: '<selector-dialog-tree :title="title" @treeItemSelected="handleItemSelection"></selector-dialog-tree>'
}).$mount();
let overlay = this.openmct.overlays.overlay({
element: vm.$el,
size: 'small',
buttons: [
{
label: 'OK',
emphasis: 'true',
callback: () => dismissDialog(overlay, true)
},
{
label: 'Cancel',
callback: () => dismissDialog(overlay, false)
}
],
onDestroy: () => vm.$destroy()
});
},
enableConditionSetNav() {
this.openmct.objects.getOriginalPath(this.conditionSetDomainObject.identifier).then(
(objectPath) => {
this.objectPath = objectPath;
this.navigateToPath = '#/browse/' + this.openmct.objects.getRelativePath(this.objectPath);
}
);
},
navigateOrPreview(event) {
// If editing, display condition set in Preview overlay; otherwise nav to it while browsing
if (this.openmct.editor.isEditing()) {
event.preventDefault();
this.previewAction.invoke(this.objectPath);
} else {
this.openmct.router.navigate(this.navigateToPath);
}
},
removeConditionSet() {
this.conditionSetDomainObject = undefined;
this.conditionalStyles = [];
let domainObjectStyles = (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {};
if (this.itemId) {
domainObjectStyles[this.itemId].conditionSetIdentifier = undefined;
domainObjectStyles[this.itemId].selectedConditionId = undefined;
domainObjectStyles[this.itemId].defaultConditionId = undefined;
delete domainObjectStyles[this.itemId].conditionSetIdentifier;
domainObjectStyles[this.itemId].styles = undefined;
delete domainObjectStyles[this.itemId].styles;
if (isEmpty(domainObjectStyles[this.itemId])) {
delete domainObjectStyles[this.itemId];
}
} else {
domainObjectStyles.conditionSetIdentifier = undefined;
domainObjectStyles.selectedConditionId = undefined;
domainObjectStyles.defaultConditionId = undefined;
delete domainObjectStyles.conditionSetIdentifier;
domainObjectStyles.styles = undefined;
delete domainObjectStyles.styles;
}
if (isEmpty(domainObjectStyles)) {
domainObjectStyles = undefined;
}
this.persist(domainObjectStyles);
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
delete this.stopProvidingTelemetry;
}
},
updateDomainObjectItemStyles(newItems) {
//check that all items that have been styles still exist. Otherwise delete those styles
let domainObjectStyles = (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {};
let itemsToRemove = [];
let keys = Object.keys(domainObjectStyles);
//TODO: Need an easier way to find which properties are itemIds
keys.forEach((key) => {
const keyIsItemId = (key !== 'styles')
&& (key !== 'staticStyle')
&& (key !== 'defaultConditionId')
&& (key !== 'selectedConditionId')
&& (key !== 'conditionSetIdentifier');
if (keyIsItemId) {
if (!(newItems.find(item => item.id === key))) {
itemsToRemove.push(key);
}
}
});
if (itemsToRemove.length) {
this.removeItemStyles(itemsToRemove, domainObjectStyles);
}
},
removeItemStyles(itemIds, domainObjectStyles) {
itemIds.forEach(itemId => {
if (domainObjectStyles[itemId]) {
domainObjectStyles[itemId] = undefined;
delete domainObjectStyles[this.itemId];
}
});
if (isEmpty(domainObjectStyles)) {
domainObjectStyles = undefined;
}
this.persist(domainObjectStyles);
},
initializeConditionalStyles() {
if (!this.conditions) {
this.conditions = {};
}
let conditionalStyles = [];
this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, index) => {
if (conditionConfiguration.isDefault) {
this.selectedConditionId = conditionConfiguration.id;
}
this.conditions[conditionConfiguration.id] = conditionConfiguration;
let foundStyle = this.findStyleByConditionId(conditionConfiguration.id);
if (foundStyle) {
foundStyle.style = Object.assign((this.canHide ? { isStyleInvisible: '' } : {}), this.initialStyles, foundStyle.style);
conditionalStyles.push(foundStyle);
} else {
conditionalStyles.splice(index, 0, {
conditionId: conditionConfiguration.id,
style: Object.assign((this.canHide ? { isStyleInvisible: '' } : {}), this.initialStyles)
});
}
});
//we're doing this so that we remove styles for any conditions that have been removed from the condition set
this.conditionalStyles = conditionalStyles;
this.conditionsLoaded = true;
this.persist(this.getDomainObjectConditionalStyle(this.selectedConditionId));
if (!this.isEditing) {
this.subscribeToConditionSet();
}
},
subscribeToConditionSet() {
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
delete this.stopProvidingTelemetry;
}
if (this.conditionSetDomainObject) {
this.openmct.telemetry.request(this.conditionSetDomainObject)
.then(output => {
if (output && output.length) {
this.handleConditionSetResultUpdated(output[0]);
}
});
this.stopProvidingTelemetry = this.openmct.telemetry.subscribe(this.conditionSetDomainObject, this.handleConditionSetResultUpdated.bind(this));
}
},
handleConditionSetResultUpdated(resultData) {
this.selectedConditionId = resultData ? resultData.conditionId : '';
},
initializeStaticStyle(objectStyles) {
let staticStyle = objectStyles && objectStyles.staticStyle;
if (staticStyle) {
this.staticStyle = {
style: Object.assign({}, this.initialStyles, staticStyle.style)
};
} else {
this.staticStyle = {
style: Object.assign({}, this.initialStyles)
};
}
},
findStyleByConditionId(id) {
return this.conditionalStyles.find(conditionalStyle => conditionalStyle.conditionId === id);
},
updateStaticStyle(staticStyle) {
this.staticStyle = staticStyle;
this.persist(this.getDomainObjectConditionalStyle());
},
updateConditionalStyle(conditionStyle) {
let found = this.findStyleByConditionId(conditionStyle.conditionId);
if (found) {
found.style = conditionStyle.style;
this.selectedConditionId = found.conditionId;
this.persist(this.getDomainObjectConditionalStyle());
}
},
getDomainObjectConditionalStyle(defaultConditionId) {
let objectStyle = {
styles: this.conditionalStyles,
staticStyle: this.staticStyle,
selectedConditionId: this.selectedConditionId
};
if (defaultConditionId) {
objectStyle.defaultConditionId = defaultConditionId;
}
if (this.conditionSetDomainObject) {
objectStyle.conditionSetIdentifier = this.conditionSetDomainObject.identifier;
}
let domainObjectStyles = (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {};
if (this.itemId) {
domainObjectStyles[this.itemId] = objectStyle;
} else {
//we're deconstructing here to ensure that if an item within a domainObject already had a style we don't lose it
domainObjectStyles = {
...domainObjectStyles,
...objectStyle
};
}
return domainObjectStyles;
},
getCondition(id) {
return this.conditions ? this.conditions[id] : {};
},
applySelectedConditionStyle(conditionId) {
this.selectedConditionId = conditionId;
this.persist(this.getDomainObjectConditionalStyle());
},
persist(style) {
this.openmct.objects.mutate(this.domainObject, 'configuration.objectStyles', style);
}
}
};
</script>

View File

@@ -0,0 +1,280 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div class="c-inspector__styles c-inspect-styles">
<div class="c-inspect-styles__header">
Object Style
</div>
<div class="c-inspect-styles__content">
<div v-if="isStaticAndConditionalStyles"
class="c-inspect-styles__mixed-static-and-conditional u-alert u-alert--block u-alert--with-icon"
>
Your selection includes one or more items that use Conditional Styling. Applying a static style below will replace any Conditional Styling with the new choice.
</div>
<div v-if="staticStyle"
class="c-inspect-styles__style"
>
<style-editor class="c-inspect-styles__editor"
:style-item="staticStyle"
:is-editing="isEditing"
:mixed-styles="mixedStyles"
@persist="updateStaticStyle"
/>
</div>
</div>
</div>
</template>
<script>
import StyleEditor from "./StyleEditor.vue";
import PreviewAction from "@/ui/preview/PreviewAction.js";
import { getApplicableStylesForItem, getConsolidatedStyleValues, getConditionalStyleForItem } from "@/plugins/condition/utils/styleUtils";
import isEmpty from 'lodash/isEmpty';
export default {
name: 'MultiSelectStylesView',
components: {
StyleEditor
},
inject: [
'openmct',
'selection'
],
data() {
return {
staticStyle: undefined,
isEditing: this.openmct.editor.isEditing(),
mixedStyles: [],
isStaticAndConditionalStyles: false
};
},
destroyed() {
this.removeListeners();
},
mounted() {
this.items = [];
this.previewAction = new PreviewAction(this.openmct);
this.getObjectsAndItemsFromSelection();
this.initializeStaticStyle();
this.openmct.editor.on('isEditing', this.setEditState);
},
methods: {
isItemType(type, item) {
return item && (item.type === type);
},
hasConditionalStyles(domainObject, id) {
return getConditionalStyleForItem(domainObject, id) !== undefined;
},
getObjectsAndItemsFromSelection() {
let domainObject;
let subObjects = [];
//multiple selection
let itemInitialStyles = [];
let itemStyle;
this.selection.forEach((selectionItem) => {
const item = selectionItem[0].context.item;
const layoutItem = selectionItem[0].context.layoutItem;
if (item && this.isItemType('subobject-view', layoutItem)) {
subObjects.push(item);
itemStyle = getApplicableStylesForItem(item);
if (!this.isStaticAndConditionalStyles) {
this.isStaticAndConditionalStyles = this.hasConditionalStyles(item);
}
} else {
domainObject = selectionItem[1].context.item;
itemStyle = getApplicableStylesForItem(domainObject, layoutItem || item);
this.items.push({
id: layoutItem.id,
applicableStyles: itemStyle
});
if (!this.isStaticAndConditionalStyles) {
this.isStaticAndConditionalStyles = this.hasConditionalStyles(domainObject, layoutItem.id);
}
}
itemInitialStyles.push(itemStyle);
});
const {styles, mixedStyles} = getConsolidatedStyleValues(itemInitialStyles);
this.initialStyles = styles;
this.mixedStyles = mixedStyles;
this.domainObject = domainObject;
this.removeListeners();
if (this.domainObject) {
this.stopObserving = this.openmct.objects.observe(this.domainObject, '*', newDomainObject => this.domainObject = newDomainObject);
this.stopObservingItems = this.openmct.objects.observe(this.domainObject, 'configuration.items', this.updateDomainObjectItemStyles);
}
subObjects.forEach(this.registerListener);
},
updateDomainObjectItemStyles(newItems) {
//check that all items that have been styles still exist. Otherwise delete those styles
let keys = Object.keys(this.domainObject.configuration.objectStyles || {});
keys.forEach((key) => {
if ((key !== 'styles')
&& (key !== 'staticStyle')
&& (key !== 'conditionSetIdentifier')) {
if (!(newItems.find(item => item.id === key))) {
this.removeItemStyles(key);
}
}
});
},
registerListener(domainObject) {
let id = this.openmct.objects.makeKeyString(domainObject.identifier);
if (!this.domainObjectsById) {
this.domainObjectsById = {};
}
if (!this.domainObjectsById[id]) {
this.domainObjectsById[id] = domainObject;
this.observeObject(domainObject, id);
}
},
observeObject(domainObject, id) {
let unobserveObject = this.openmct.objects.observe(domainObject, '*', function (newObject) {
this.domainObjectsById[id] = JSON.parse(JSON.stringify(newObject));
}.bind(this));
this.unObserveObjects.push(unobserveObject);
},
removeListeners() {
if (this.stopObserving) {
this.stopObserving();
}
if (this.stopObservingItems) {
this.stopObservingItems();
}
if (this.unObserveObjects) {
this.unObserveObjects.forEach((unObserveObject) => {
unObserveObject();
});
}
this.unObserveObjects = [];
},
removeItemStyles(itemId) {
let domainObjectStyles = (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {};
if (itemId && domainObjectStyles[itemId]) {
domainObjectStyles[itemId] = undefined;
delete domainObjectStyles[this.itemId];
if (isEmpty(domainObjectStyles)) {
domainObjectStyles = undefined;
}
this.persist(this.domainObject, domainObjectStyles);
}
},
removeConditionalStyles(domainObjectStyles, itemId) {
if (itemId) {
domainObjectStyles[itemId].conditionSetIdentifier = undefined;
delete domainObjectStyles[itemId].conditionSetIdentifier;
domainObjectStyles[itemId].styles = undefined;
delete domainObjectStyles[itemId].styles;
} else {
domainObjectStyles.conditionSetIdentifier = undefined;
delete domainObjectStyles.conditionSetIdentifier;
domainObjectStyles.styles = undefined;
delete domainObjectStyles.styles;
}
},
setEditState(isEditing) {
this.isEditing = isEditing;
},
initializeStaticStyle() {
this.staticStyle = {
style: Object.assign({}, this.initialStyles)
};
},
updateStaticStyle(staticStyle, property) {
//update the static style for each of the layoutItems as well as each sub object item
this.staticStyle = staticStyle;
this.persist(this.domainObject, this.getDomainObjectStyle(this.domainObject, property, this.items));
if (this.domainObjectsById) {
const keys = Object.keys(this.domainObjectsById);
keys.forEach(key => {
let domainObject = this.domainObjectsById[key];
this.persist(domainObject, this.getDomainObjectStyle(domainObject, property));
});
}
this.isStaticAndConditionalStyles = false;
let foundIndex = this.mixedStyles.indexOf(property);
if (foundIndex > -1) {
this.mixedStyles.splice(foundIndex, 1);
}
},
getDomainObjectStyle(domainObject, property, items) {
let domainObjectStyles = (domainObject.configuration && domainObject.configuration.objectStyles) || {};
if (items) {
items.forEach(item => {
let itemStaticStyle = {};
if (domainObjectStyles[item.id] && domainObjectStyles[item.id].staticStyle) {
itemStaticStyle = domainObjectStyles[item.id].staticStyle.style;
}
Object.keys(item.applicableStyles).forEach(key => {
if (property === key) {
itemStaticStyle[key] = this.staticStyle.style[key];
}
});
if (this.isStaticAndConditionalStyles) {
this.removeConditionalStyles(domainObjectStyles, item.id);
}
if (isEmpty(itemStaticStyle)) {
itemStaticStyle = undefined;
domainObjectStyles[item.id] = undefined;
} else {
domainObjectStyles[item.id] = Object.assign({}, { staticStyle: { style: itemStaticStyle } });
}
});
} else {
if (!domainObjectStyles.staticStyle) {
domainObjectStyles.staticStyle = {
style: {}
};
}
if (this.isStaticAndConditionalStyles) {
this.removeConditionalStyles(domainObjectStyles);
}
domainObjectStyles.staticStyle.style[property] = this.staticStyle.style[property];
}
return domainObjectStyles;
},
persist(domainObject, style) {
this.openmct.objects.mutate(domainObject, 'configuration.objectStyles', style);
}
}
};
</script>

View File

@@ -63,7 +63,7 @@
<div class="c-inspect-styles__header">
Conditional Object Styles
</div>
<div class="c-inspect-styles__content c-inspect-styles__condition-set c-inspect-styles__elem">
<div class="c-inspect-styles__content c-inspect-styles__condition-set">
<a v-if="conditionSetDomainObject"
class="c-object-label"
@click="navigateOrPreview"
@@ -87,27 +87,6 @@
</template>
</div>
<div v-if="isConditionWidget && allowEditing"
class="c-inspect-styles__elem c-inspect-styles__output-label-toggle"
>
<label class="c-toggle-switch">
<input
type="checkbox"
:checked="useConditionSetOutputAsLabel"
@change="updateConditionSetOutputLabel"
>
<span class="c-toggle-switch__slider"></span>
<span class="c-toggle-switch__label">Use Condition Set output as label</span>
</label>
</div>
<div v-if="isConditionWidget && !allowEditing"
class="c-inspect-styles__elem"
>
<span class="c-toggle-switch__label">Condition Set output as label:
<span v-if="useConditionSetOutputAsLabel"> Yes</span><span v-else> No</span>
</span>
</div>
<FontStyleEditor
v-if="canStyleFont"
:font-style="consolidatedFontStyle"
@@ -193,8 +172,7 @@ export default {
selectedConditionId: '',
items: [],
domainObject: undefined,
consolidatedFontStyle: {},
useConditionSetOutputAsLabel: false
consolidatedFontStyle: {}
};
},
computed: {
@@ -209,11 +187,6 @@ export default {
allowEditing() {
return this.isEditing && !this.locked;
},
isConditionWidget() {
const hasConditionWidgetObjects = this.domainObjectsById && Object.values(this.domainObjectsById).some((object) => object.type === 'conditionWidget');
return (hasConditionWidgetObjects || (this.domainObject && this.domainObject.type === 'conditionWidget'));
},
styleableFontItems() {
return this.selection.filter(selectionPath => {
const item = selectionPath[0].context.item;
@@ -232,6 +205,28 @@ export default {
return true;
});
},
computedconsolidatedFontStyle() {
let consolidatedFontStyle;
const styles = [];
this.styleableFontItems.forEach(styleable => {
const fontStyle = this.getFontStyle(styleable[0]);
styles.push(fontStyle);
});
if (styles.length) {
const hasConsolidatedFontSize = styles.length && styles.every((fontStyle, i, arr) => fontStyle.fontSize === arr[0].fontSize);
const hasConsolidatedFont = styles.length && styles.every((fontStyle, i, arr) => fontStyle.font === arr[0].font);
consolidatedFontStyle = {
fontSize: hasConsolidatedFontSize ? styles[0].fontSize : NON_SPECIFIC,
font: hasConsolidatedFont ? styles[0].font : NON_SPECIFIC
};
}
return consolidatedFontStyle;
},
nonSpecificFontProperties() {
if (!this.consolidatedFontStyle) {
return [];
@@ -252,8 +247,6 @@ export default {
this.previewAction = new PreviewAction(this.openmct);
this.isMultipleSelection = this.selection.length > 1;
this.getObjectsAndItemsFromSelection();
this.useConditionSetOutputAsLabel = this.getConfigurationForLabel();
if (!this.isMultipleSelection) {
let objectStyles = this.getObjectStyles();
this.initializeStaticStyle(objectStyles);
@@ -271,12 +264,6 @@ export default {
this.stylesManager.on('styleSelected', this.applyStyleToSelection);
},
methods: {
getConfigurationForLabel() {
const childObjectUsesLabels = Object.values(this.domainObjectsById || {}).some((object) => object.configuration && object.configuration.useConditionSetOutputAsLabel);
const domainObjectUsesLabels = this.domainObject && this.domainObject.configuration && this.domainObject.configuration.useConditionSetOutputAsLabel;
return childObjectUsesLabels || domainObjectUsesLabels;
},
getObjectStyles() {
let objectStyles;
if (this.domainObjectsById) {
@@ -500,14 +487,13 @@ export default {
this.conditions[conditionConfiguration.id] = conditionConfiguration;
let foundStyle = this.findStyleByConditionId(conditionConfiguration.id);
let output = { output: conditionConfiguration.configuration.output };
if (foundStyle) {
foundStyle.style = Object.assign((this.canHide ? { isStyleInvisible: '' } : {}), this.initialStyles, foundStyle.style, output);
foundStyle.style = Object.assign((this.canHide ? { isStyleInvisible: '' } : {}), this.initialStyles, foundStyle.style);
conditionalStyles.push(foundStyle);
} else {
conditionalStyles.splice(index, 0, {
conditionId: conditionConfiguration.id,
style: Object.assign((this.canHide ? { isStyleInvisible: '' } : {}), this.initialStyles, output)
style: Object.assign((this.canHide ? { isStyleInvisible: '' } : {}), this.initialStyles)
});
}
});
@@ -729,12 +715,6 @@ export default {
} else {
objectStyle.styles.forEach((conditionalStyle, index) => {
let style = {};
if (domainObject.configuration.useConditionSetOutputAsLabel) {
style.output = conditionalStyle.style.output;
} else {
style.output = '';
}
Object.keys(item.applicableStyles).concat(['isStyleInvisible']).forEach(key => {
style[key] = conditionalStyle.style[key];
});
@@ -751,21 +731,10 @@ export default {
}
});
} else {
if (domainObject.configuration.useConditionSetOutputAsLabel !== true) {
let objectConditionStyle = JSON.parse(JSON.stringify(objectStyle));
objectConditionStyle.styles.forEach((conditionalStyle) => {
conditionalStyle.style.output = '';
});
domainObjectStyles = {
...domainObjectStyles,
...objectConditionStyle
};
} else {
domainObjectStyles = {
...domainObjectStyles,
...objectStyle
};
}
domainObjectStyles = {
...domainObjectStyles,
...objectStyle
};
}
return domainObjectStyles;
@@ -774,17 +743,6 @@ export default {
this.selectedConditionId = conditionId;
this.getAndPersistStyles();
},
persistLabelConfiguration() {
if (this.domainObjectsById) {
Object.values(this.domainObjectsById).forEach((object) => {
this.openmct.objects.mutate(object, 'configuration.useConditionSetOutputAsLabel', this.useConditionSetOutputAsLabel);
});
} else {
this.openmct.objects.mutate(this.domainObject, 'configuration.useConditionSetOutputAsLabel', this.useConditionSetOutputAsLabel);
}
this.getAndPersistStyles();
},
persist(domainObject, style) {
this.openmct.objects.mutate(domainObject, 'configuration.objectStyles', style);
},
@@ -905,10 +863,6 @@ export default {
const layoutItemType = selectionPath[0].context.layoutItem && selectionPath[0].context.layoutItem.type;
return layoutItemType && layoutItemType !== 'subobject-view';
},
updateConditionSetOutputLabel(event) {
this.useConditionSetOutputAsLabel = event.target.checked === true;
this.persistLabelConfiguration();
}
}
};

View File

@@ -39,15 +39,12 @@
flex-direction: column;
}
&__elem {
border-bottom: 1px solid $colorInteriorBorder;
padding-bottom: $interiorMargin;
}
&__condition-set {
align-items: baseline;
border-bottom: 1px solid $colorInteriorBorder;
display: flex;
flex-direction: row;
padding-bottom: $interiorMargin;
.c-object-label {
flex: 1 1 auto;

View File

@@ -133,168 +133,6 @@ describe('the plugin', function () {
});
});
describe('the condition set usage for condition widgets', () => {
let conditionWidgetItem;
let selection;
let component;
let styleViewComponentObject;
const conditionSetDomainObject = {
"configuration": {
"conditionTestData": [
{
"telemetry": "",
"metadata": "",
"input": ""
}
],
"conditionCollection": [
{
"id": "39584410-cbf9-499e-96dc-76f27e69885d",
"configuration": {
"name": "Unnamed Condition",
"output": "Sine > 0",
"trigger": "all",
"criteria": [
{
"id": "85fbb2f7-7595-42bd-9767-a932266c5225",
"telemetry": {
"namespace": "",
"key": "be0ba97f-b510-4f40-a18d-4ff121d5ea1a"
},
"operation": "greaterThan",
"input": [
"0"
],
"metadata": "sin"
},
{
"id": "35400132-63b0-425c-ac30-8197df7d5862",
"telemetry": "any",
"operation": "enumValueIs",
"input": [
"0"
],
"metadata": "state"
}
]
},
"summary": "Match if all criteria are met: Sine Wave Generator Sine > 0 and any telemetry State is OFF "
},
{
"isDefault": true,
"id": "2532d90a-e0d6-4935-b546-3123522da2de",
"configuration": {
"name": "Default",
"output": "Default",
"trigger": "all",
"criteria": [
]
},
"summary": ""
}
]
},
"composition": [
{
"namespace": "",
"key": "be0ba97f-b510-4f40-a18d-4ff121d5ea1a"
},
{
"namespace": "",
"key": "077ffa67-e78f-4e99-80e0-522ac33a3888"
}
],
"telemetry": {
},
"name": "Condition Set",
"type": "conditionSet",
"identifier": {
"namespace": "",
"key": "863012c1-f6ca-4ab0-aed7-fd43d5e4cd12"
}
};
beforeEach(() => {
conditionWidgetItem = {
"label": "Condition Widget",
"conditionalLabel": "",
"configuration": {
},
"name": "Condition Widget",
"type": "conditionWidget",
"identifier": {
"namespace": "",
"key": "c5e636c1-6771-4c9c-b933-8665cab189b3"
}
};
selection = [
[{
context: {
"item": conditionWidgetItem,
"supportsMultiSelect": false
}
}]
];
let viewContainer = document.createElement('div');
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
StylesView
},
provide: {
openmct: openmct,
selection: selection,
stylesManager
},
template: '<styles-view/>'
});
return Vue.nextTick().then(() => {
styleViewComponentObject = component.$root.$children[0];
styleViewComponentObject.setEditState(true);
});
});
afterEach(() => {
component.$destroy();
});
it('does not include the output label when the flag is disabled', () => {
styleViewComponentObject.conditionSetDomainObject = conditionSetDomainObject;
styleViewComponentObject.conditionalStyles = [];
styleViewComponentObject.initializeConditionalStyles();
expect(styleViewComponentObject.conditionalStyles.length).toBe(2);
return Vue.nextTick().then(() => {
const hasNoOutput = styleViewComponentObject.domainObject.configuration.objectStyles.styles.every((style) => {
return style.style.output === '' || style.style.output === undefined;
});
expect(hasNoOutput).toBeTrue();
});
});
it('includes the output label when the flag is enabled', () => {
styleViewComponentObject.conditionSetDomainObject = conditionSetDomainObject;
styleViewComponentObject.conditionalStyles = [];
styleViewComponentObject.initializeConditionalStyles();
expect(styleViewComponentObject.conditionalStyles.length).toBe(2);
styleViewComponentObject.useConditionSetOutputAsLabel = true;
styleViewComponentObject.persistLabelConfiguration();
return Vue.nextTick().then(() => {
const outputs = styleViewComponentObject.domainObject.configuration.objectStyles.styles.map((style) => {
return style.style.output;
});
expect(outputs.join(',')).toEqual('Sine > 0,Default');
});
});
});
describe('the condition set usage for multiple display layout items', () => {
let displayLayoutItem;
let lineLayoutItem;
@@ -611,10 +449,6 @@ describe('the plugin', function () {
const applicableStyles = getApplicableStylesForItem(styleViewComponentObject.domainObject, item);
const applicableStylesKeys = Object.keys(applicableStyles).concat(['isStyleInvisible']);
Object.keys(foundStyle.style).forEach((key) => {
if (key === 'output') {
return;
}
expect(applicableStylesKeys.indexOf(key)).toBeGreaterThan(-1);
expect(foundStyle.style[key]).toEqual(conditionalStyle.style[key]);
});

View File

@@ -26,7 +26,7 @@
:href="url"
>
<div class="c-condition-widget__label">
{{ internalDomainObject.conditionalLabel || internalDomainObject.label }}
{{ internalDomainObject.label }}
</div>
</component>
</template>

View File

@@ -27,15 +27,12 @@ export default function plugin() {
openmct.objectViews.addProvider(new ConditionWidgetViewProvider(openmct));
openmct.types.addType('conditionWidget', {
key: 'conditionWidget',
name: "Condition Widget",
description: "A button that can be used on its own, or dynamically styled with a Condition Set.",
creatable: true,
cssClass: 'icon-condition-widget',
initialize(domainObject) {
domainObject.configuration = {};
domainObject.label = 'Condition Widget';
domainObject.conditionalLabel = '';
},
form: [
{

View File

@@ -1,195 +0,0 @@
import { createOpenMct, resetApplicationState } from "utils/testing";
import ConditionWidgetPlugin from "./plugin";
import Vue from 'vue';
describe('the plugin', function () {
const CONDITION_WIDGET_KEY = 'conditionWidget';
let objectDef;
let element;
let child;
let openmct;
let mockConditionObjectDefinition;
let mockConditionObject;
let mockConditionObjectPath;
beforeEach((done) => {
mockConditionObjectPath = [
{
name: 'mock folder',
type: 'fake-folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
},
{
name: 'mock parent folder',
type: 'conditionWidget',
identifier: {
key: 'mock-parent-folder',
namespace: ''
}
}
];
mockConditionObjectDefinition = {
name: 'Condition Widget',
key: 'conditionWidget',
creatable: true
};
mockConditionObject = {
"conditionWidget": {
"identifier": {
"namespace": "",
"key": "condition-widget-object"
},
"url": "https://nasa.github.io/openmct/",
"label": "Foo Widget",
"type": "conditionWidget",
"composition": []
},
"telemetry": {
"identifier": {
"namespace": "",
"key": "telemetry-object"
},
"type": "test-telemetry-object",
"name": "Test Telemetry Object",
"telemetry": {
"values": [
{
"key": "name",
"name": "Name",
"format": "string"
},
{
"key": "utc",
"name": "Time",
"format": "utc",
"hints": {
"domain": 1
}
},
{
"name": "Some attribute 1",
"key": "some-key-1",
"hints": {
"range": 1
}
},
{
"name": "Some attribute 2",
"key": "some-key-2"
}
]
}
}
};
const timeSystem = {
timeSystemKey: 'utc',
bounds: {
start: 1597160002854,
end: 1597181232854
}
};
openmct = createOpenMct(timeSystem);
openmct.install(new ConditionWidgetPlugin());
objectDef = openmct.types.get('conditionWidget').definition;
element = document.createElement('div');
element.style.width = '640px';
element.style.height = '480px';
child = document.createElement('div');
child.style.width = '640px';
child.style.height = '480px';
element.appendChild(child);
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('defines a conditionWidget object type with the correct key', () => {
expect(objectDef.key).toEqual(mockConditionObjectDefinition.key);
});
describe('the conditionWidget object', () => {
it('is creatable', () => {
expect(objectDef.creatable).toEqual(mockConditionObjectDefinition.creatable);
});
});
describe('the view', () => {
let conditionWidgetView;
let testViewObject;
beforeEach(() => {
testViewObject = {
id: "test-object",
identifier: {
key: "test-object",
namespace: ''
},
type: "conditionWidget"
};
const applicableViews = openmct.objectViews.get(testViewObject, mockConditionObjectPath);
conditionWidgetView = applicableViews.find((viewProvider) => viewProvider.key === 'conditionWidget');
let view = conditionWidgetView.view(testViewObject, element);
view.show(child, true);
return Vue.nextTick();
});
it('provides a view', () => {
expect(conditionWidgetView).toBeDefined();
});
});
it("should have a view provider for condition widget objects", () => {
const applicableViews = openmct.objectViews.get(mockConditionObject[CONDITION_WIDGET_KEY], []);
const conditionWidgetViewProvider = applicableViews.find(
(viewProvider) => viewProvider.key === CONDITION_WIDGET_KEY
);
expect(applicableViews.length).toEqual(1);
expect(conditionWidgetViewProvider).toBeDefined();
});
it("should render a view with a URL and label", async () => {
const urlParent = document.createElement('div');
const urlChild = document.createElement('div');
urlParent.appendChild(urlChild);
const applicableViews = openmct.objectViews.get(mockConditionObject[CONDITION_WIDGET_KEY], []);
const conditionWidgetViewProvider = applicableViews.find(
(viewProvider) => viewProvider.key === CONDITION_WIDGET_KEY
);
const conditionWidgetView = conditionWidgetViewProvider.view(mockConditionObject[CONDITION_WIDGET_KEY], [mockConditionObject[CONDITION_WIDGET_KEY]]);
conditionWidgetView.show(urlChild);
await Vue.nextTick();
const domainUrl = mockConditionObject[CONDITION_WIDGET_KEY].url;
expect(urlParent.innerHTML).toContain(`<a href="${domainUrl}"`);
const conditionWidgetRender = urlParent.querySelector('.c-condition-widget');
expect(conditionWidgetRender).toBeDefined();
expect(conditionWidgetRender.innerHTML).toContain('<div class="c-condition-widget__label">');
const conditionWidgetLabel = conditionWidgetRender.querySelector('.c-condition-widget__label');
expect(conditionWidgetLabel).toBeDefined();
const domainLabel = mockConditionObject[CONDITION_WIDGET_KEY].label;
expect(conditionWidgetLabel.textContent).toContain(domainLabel);
});
});

View File

@@ -38,14 +38,10 @@ export default {
this.objectStyle = this.getObjectStyleForItem(this.parentDomainObject.configuration.objectStyles);
this.initObjectStyles();
},
beforeDestroy() {
destroyed() {
if (this.stopListeningObjectStyles) {
this.stopListeningObjectStyles();
}
if (this.styleRuleManager) {
this.styleRuleManager.destroy();
}
},
methods: {
getObjectStyleForItem(objectStyle) {

View File

@@ -66,10 +66,11 @@ export default class CreateAction extends PropertiesAction {
});
const parentDomainObject = parentDomainObjectPath[0];
const namespace = parentDomainObject.identifier.namespace || parentDomainObject.key || '';
this.domainObject.modified = Date.now();
this.domainObject.location = this.openmct.objects.makeKeyString(parentDomainObject.identifier);
this.domainObject.identifier.namespace = parentDomainObject.identifier.namespace;
this.domainObject.identifier.namespace = namespace;
// Show saving progress dialog
let dialog = this.openmct.overlays.progressDialog({
@@ -99,6 +100,7 @@ export default class CreateAction extends PropertiesAction {
*/
async _navigateAndEdit(domainObject, parentDomainObjectpath) {
let objectPath;
let self = this;
if (parentDomainObjectpath) {
objectPath = parentDomainObjectpath && [domainObject].concat(parentDomainObjectpath);
} else {
@@ -110,13 +112,18 @@ export default class CreateAction extends PropertiesAction {
.reverse()
.join('/');
this.openmct.router.navigate(url);
function editObject() {
const objectView = self.openmct.objectViews.get(domainObject, objectPath)[0];
const canEdit = objectView && objectView.canEdit && objectView.canEdit(domainObject, objectPath);
const objectView = this.openmct.objectViews.get(domainObject, objectPath)[0];
const canEdit = objectView && objectView.canEdit && objectView.canEdit(domainObject, objectPath);
if (canEdit) {
this.openmct.editor.edit();
if (canEdit) {
self.openmct.editor.edit();
}
}
this.openmct.router.once('afterNavigation', editObject);
this.openmct.router.navigate(url);
}
/**

View File

@@ -65,13 +65,8 @@ export default {
keyString: undefined
};
},
computed: {
imageHistorySize() {
return this.imageHistory.length;
}
},
watch: {
imageHistorySize(newSize, oldSize) {
imageHistory(newHistory, oldHistory) {
this.updatePlotImagery();
}
},

View File

@@ -240,9 +240,6 @@ export default {
};
},
computed: {
imageHistorySize() {
return this.imageHistory.length;
},
compassRoseSizingClasses() {
let compassRoseSizingClasses = '';
if (this.sizedImageDimensions.width < 300) {
@@ -409,19 +406,23 @@ export default {
}
},
watch: {
imageHistorySize(newSize, oldSize) {
let imageIndex;
if (this.focusedImageTimestamp !== undefined) {
const foundImageIndex = this.imageHistory.findIndex(image => {
return image.time === this.focusedImageTimestamp;
});
imageIndex = foundImageIndex > -1 ? foundImageIndex : newSize - 1;
} else {
imageIndex = newSize > 0 ? newSize - 1 : undefined;
}
imageHistory: {
handler(newHistory, oldHistory) {
const newSize = newHistory.length;
let imageIndex;
if (this.focusedImageTimestamp !== undefined) {
const foundImageIndex = this.imageHistory.findIndex(image => {
return image.time === this.focusedImageTimestamp;
});
imageIndex = foundImageIndex > -1 ? foundImageIndex : newSize - 1;
} else {
imageIndex = newSize > 0 ? newSize - 1 : undefined;
}
this.setFocusedImage(imageIndex, false);
this.scrollToRight();
this.setFocusedImage(imageIndex, false);
this.scrollToRight();
},
deep: true
},
focusedImageIndex() {
this.trackDuration();
@@ -510,12 +511,6 @@ export default {
this.timeContext.off("clock", this.trackDuration);
}
},
boundsChange(bounds, isTick) {
if (!isTick) {
this.previousFocusedImage = this.focusedImage ? JSON.parse(JSON.stringify(this.focusedImage)) : undefined;
this.requestHistory();
}
},
expand() {
const actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView);
const visibleActions = actionCollection.getVisibleActions();
@@ -690,22 +685,32 @@ export default {
return;
}
if (this.previousFocusedImage) {
// determine if the previous image exists in the new bounds of imageHistory
const matchIndex = this.matchIndexOfPreviousImage(
this.previousFocusedImage,
this.imageHistory
);
focusedIndex = matchIndex > -1 ? matchIndex : this.imageHistory.length - 1;
delete this.previousFocusedImage;
}
if (thumbnailClick) {
//We use the props till the user changes what they want to see
this.focusedImageTimestamp = undefined;
//set the previousFocusedImage when a user chooses an image
this.previousFocusedImage = this.imageHistory[focusedIndex] ? JSON.parse(JSON.stringify(this.imageHistory[focusedIndex])) : undefined;
}
if (this.previousFocusedImage) {
// determine if the previous image exists in the new bounds of imageHistory
if (!thumbnailClick) {
const matchIndex = this.matchIndexOfPreviousImage(
this.previousFocusedImage,
this.imageHistory
);
focusedIndex = matchIndex > -1 ? matchIndex : this.imageHistory.length - 1;
}
if (!(this.isPaused || thumbnailClick)
|| focusedIndex === this.imageHistory.length - 1) {
delete this.previousFocusedImage;
}
}
this.focusedImageIndex = focusedIndex;
//TODO: do we even need this anymore?
if (this.isPaused && !thumbnailClick && this.focusedImageTimestamp === undefined) {
this.nextImageIndex = focusedIndex;
//this could happen if bounds changes
@@ -716,8 +721,6 @@ export default {
return;
}
this.focusedImageIndex = focusedIndex;
if (thumbnailClick && !this.isPaused) {
this.paused(true);
}

View File

@@ -120,9 +120,15 @@ export default {
return this.timeFormatter.parse(datum);
},
boundsChange(bounds, isTick) {
if (!isTick) {
this.requestHistory();
if (isTick) {
return;
}
// forcibly reset the imageContainer size to prevent an aspect ratio distortion
delete this.imageContainerWidth;
delete this.imageContainerHeight;
return this.requestHistory();
},
async requestHistory() {
let bounds = this.timeContext.bounds();

View File

@@ -91,11 +91,11 @@ export default class LinkAction {
}
validate(currentParent) {
return (object, data) => {
const parentCandidate = data.value;
return (data) => {
const parentCandidate = data.value[0];
const currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
const parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
const objectKeystring = this.openmct.objects.makeKeyString(object.identifier);
const objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
if (!parentCandidateKeystring || !currentParentKeystring) {
return false;
@@ -114,7 +114,7 @@ export default class LinkAction {
return false;
}
return parentCandidate && this.openmct.composition.checkPolicy(parentCandidate, object);
return parentCandidate && this.openmct.composition.checkPolicy(parentCandidate, this.object);
};
}
}

View File

@@ -25,15 +25,11 @@ import myItemsInterceptor from "./myItemsInterceptor";
const MY_ITEMS_DEFAULT_NAME = 'My Items';
export default function MyItemsPlugin(name = MY_ITEMS_DEFAULT_NAME, namespace = '', priority = undefined) {
export default function MyItemsPlugin(name = MY_ITEMS_DEFAULT_NAME, namespace = '') {
return function install(openmct) {
const identifier = createMyItemsIdentifier(namespace);
if (priority === undefined) {
priority = openmct.priority.LOW;
}
openmct.objects.addGetInterceptor(myItemsInterceptor(openmct, identifier, name));
openmct.objects.addRoot(identifier, priority);
openmct.objects.addRoot(identifier);
};
}

View File

@@ -142,7 +142,6 @@ import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaul
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
import { NOTEBOOK_VIEW_TYPE } from '../notebook-constants';
import objectUtils from 'objectUtils';
import { debounce } from 'lodash';
import objectLink from '../../../ui/mixins/object-link';
@@ -175,7 +174,7 @@ export default {
focusEntryId: null,
search: '',
searchResults: [],
showTime: this.domainObject.configuration.showTime || 0,
showTime: 0,
showNav: false,
sidebarCoversEntries: false
};
@@ -239,12 +238,6 @@ export default {
watch: {
search() {
this.getSearchResults();
},
defaultSort() {
mutateObject(this.openmct, this.domainObject, 'configuration.defaultSort', this.defaultSort);
},
showTime() {
mutateObject(this.openmct, this.domainObject, 'configuration.showTime', this.showTime);
}
},
beforeMount() {
@@ -461,11 +454,6 @@ export default {
? getDefaultNotebook().defaultSectionId
: undefined;
},
getDefaultNotebookObject() {
const defaultNotebook = getDefaultNotebook();
return defaultNotebook && this.openmct.objects.get(defaultNotebook.identifier);
},
getLinktoNotebook() {
const objectPath = this.openmct.router.path;
const link = objectLink.computed.objectLink.call({
@@ -625,12 +613,12 @@ export default {
this.sectionsChanged({ sections });
},
removeDefaultClass(domainObject) {
if (!domainObject) {
removeDefaultClass(defaultNotebookIdentifier) {
if (!defaultNotebookIdentifier) {
return;
}
this.openmct.status.delete(domainObject.identifier);
this.openmct.status.delete(defaultNotebookIdentifier);
},
resetSearch() {
this.search = '';
@@ -639,15 +627,16 @@ export default {
toggleNav() {
this.showNav = !this.showNav;
},
async updateDefaultNotebook(notebookStorage) {
const defaultNotebookObject = await this.getDefaultNotebookObject();
const isSameNotebook = defaultNotebookObject
&& objectUtils.makeKeyString(defaultNotebookObject.identifier) === objectUtils.makeKeyString(notebookStorage.identifier);
updateDefaultNotebook(notebookStorage) {
const defaultNotebook = getDefaultNotebook();
const defaultNotebookIdentifier = defaultNotebook && defaultNotebook.identifier;
const isSameNotebook = defaultNotebookIdentifier
&& this.openmct.objects.areIdsEqual(defaultNotebookIdentifier, notebookStorage.identifier);
if (!isSameNotebook) {
this.removeDefaultClass(defaultNotebookObject);
this.removeDefaultClass(defaultNotebookIdentifier);
}
if (!defaultNotebookObject || !isSameNotebook) {
if (!defaultNotebookIdentifier || !isSameNotebook) {
setDefaultNotebook(this.openmct, notebookStorage, this.domainObject);
}

View File

@@ -110,7 +110,8 @@ export default class Snapshot {
}
return () => {
window.location.href = window.location.origin + url;
const path = window.location.href.split('#');
window.location.href = path[0] + url;
};
}
}

View File

@@ -105,11 +105,6 @@ export function addNotebookEntry(openmct, domainObject, notebookStorage, embed =
const date = Date.now();
const configuration = domainObject.configuration;
const entries = configuration.entries || {};
if (!entries) {
return;
}
const embeds = embed
? [embed]
: [];
@@ -125,7 +120,8 @@ export function addNotebookEntry(openmct, domainObject, notebookStorage, embed =
const newEntries = addEntryIntoPage(notebookStorage, entries, entry);
addDefaultClass(domainObject, openmct);
domainObject.configuration.entries = newEntries;
mutateObject(openmct, domainObject, 'configuration.entries', newEntries);
return id;
}

View File

@@ -278,7 +278,7 @@ export default {
// Have to throw away the old canvas elements and replace with new
// canvas elements in order to get new drawing contexts.
const div = document.createElement('div');
div.innerHTML = this.TEMPLATE;
div.innerHTML = this.canvasTemplate + this.canvasTemplate;
const mainCanvas = div.querySelectorAll("canvas")[1];
const overlayCanvas = div.querySelectorAll("canvas")[0];
this.canvas.parentNode.replaceChild(mainCanvas, this.canvas);

View File

@@ -29,7 +29,9 @@ define(
}
SummaryWidgetsCompositionPolicy.prototype.allow = function (parent, child) {
if (parent.type === 'summary-widget' && !this.openmct.telemetry.isTelemetryObject(child)) {
const parentType = parent.type;
if (parentType === 'summary-widget' && !this.openmct.telemetry.isTelemetryObject(child)) {
return false;
}

View File

@@ -185,11 +185,6 @@
&__inputs,
&__time-bounds {
display: flex;
.c-toggle-switch {
// Used in independent Time Conductor
flex: 0 0 auto;
}
}
&__inputs {

View File

@@ -229,7 +229,7 @@ body.desktop .has-local-controls {
@include abs();
}
.c-grid .c-grid {
.c-grid {
pointer-events: none;
&__x { @include bgTicks($editUIGridColorFg, 'x'); }

View File

@@ -1,6 +1,6 @@
<template>
<div>
<div v-if="supportsIndependentTime"
<div v-if="domainObject && domainObject.type === 'time-strip'"
class="c-conductor-holder--compact l-shell__main-independent-time-conductor"
>
<independent-time-conductor :domain-object="domainObject"
@@ -20,12 +20,6 @@ import StyleRuleManager from "@/plugins/condition/StyleRuleManager";
import {STYLE_CONSTANTS} from "@/plugins/condition/utils/constants";
import IndependentTimeConductor from '@/plugins/timeConductor/independent/IndependentTimeConductor.vue';
const SupportedViewTypes = [
'plot-stacked',
'plot-overlay',
'bar-graph.view',
'time-strip.view'
];
export default {
components: {
IndependentTimeConductor
@@ -70,11 +64,6 @@ export default {
},
font() {
return this.objectFontStyle ? this.objectFontStyle.font : this.layoutFont;
},
supportsIndependentTime() {
const viewKey = this.getViewKey();
return this.domainObject && SupportedViewTypes.includes(viewKey);
}
},
destroyed() {
@@ -152,11 +141,15 @@ export default {
this.openmct.objectViews.off('clearData', this.clearData);
},
getStyleReceiver() {
let styleReceiver = this.$refs.objectViewWrapper.querySelector('.js-style-receiver')
|| this.$refs.objectViewWrapper.querySelector(':first-child');
let styleReceiver;
if (styleReceiver === null) {
styleReceiver = undefined;
if (this.$refs.objectViewWrapper !== undefined) {
styleReceiver = this.$refs.objectViewWrapper.querySelector('.js-style-receiver')
|| this.$refs.objectViewWrapper.querySelector(':first-child');
if (styleReceiver === null) {
styleReceiver = undefined;
}
}
return styleReceiver;
@@ -202,12 +195,6 @@ export default {
}
}
});
if (this.domainObject && this.domainObject.type === 'conditionWidget' && keys.includes('output')) {
this.openmct.objects.mutate(this.domainObject, 'conditionalLabel', styleObj.output);
} else {
this.openmct.objects.mutate(this.domainObject, 'conditionalLabel', '');
}
},
updateView(immediatelySelect) {
this.clear();

View File

@@ -186,6 +186,10 @@ export default {
return {
name: field.name,
value: field.path.reduce((object, key) => {
if (object === undefined) {
return object;
}
return object[key];
}, this.domainObject)
};

View File

@@ -445,6 +445,10 @@ export default {
}
// sorting composition items
if (!a.name || !b.name) {
return 0;
}
if (a.name.toLowerCase()
> b.name.toLowerCase()) {
return 1;

View File

@@ -1,4 +1,4 @@
import objectPathToUrl from '/src/tools/url';
import objectPathToUrl from '../../tools/url';
export default {
inject: ['openmct'],

View File

@@ -1,13 +0,0 @@
/* Note: Open MCT does not intend to support the entire Typescript ecosystem at this time.
* This file is intended to add Intellisense for IDEs like VSCode. For more information
* about Typescript, please discuss in https://github.com/nasa/openmct/discussions/4693
*/
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"strict": true,
"module": "esnext",
"moduleResolution": "node"
}
}

View File

@@ -1,5 +1,3 @@
/* global __dirname */
const path = require('path');
const packageDefinition = require('./package.json');
const CopyWebpackPlugin = require('copy-webpack-plugin');
@@ -14,8 +12,7 @@ const gitBranch = require('child_process')
.execSync('git rev-parse --abbrev-ref HEAD')
.toString().trim();
/** @type {import('webpack').Configuration} */
const config = {
module.exports = {
entry: {
openmct: './openmct.js',
couchDBChangesFeed: './src/plugins/persistence/couch/CouchChangesFeed.js',
@@ -30,7 +27,6 @@ const config = {
library: '[name]',
libraryTarget: 'umd',
publicPath: '',
hashFunction: 'xxhash64',
clean: true
},
resolve: {
@@ -133,5 +129,3 @@ const config = {
},
stats: 'errors-warnings'
};
module.exports = config;