Compare commits

..

17 Commits

Author SHA1 Message Date
Shefali Joshi
2e02b51241 Merge branch 'master' into plots-independent-time 2022-01-07 10:07:37 -08:00
Charles Hacskaylo
22a7537974 Fix default plot color palette (#4621) 2022-01-06 11:19:51 -08:00
Joshi
03f5615029 Add timeContext to bar graphs 2022-01-06 06:18:20 -08:00
Joshi
929d4296d7 Add independent time conductor to bar graphs 2022-01-06 06:09:17 -08:00
Charles Hacskaylo
1f9c602d4d Merge branch 'plots-independent-time' of github.com:nasa/openmct into plots-independent-time 2022-01-05 14:50:22 -08:00
Charles Hacskaylo
65a2b64835 Fixes for #4503 and #4606
- Added `flex: 0 0 auto` to toggle switch when in ITC to prevent
element from being crunched when window or frame is very small;
2022-01-05 14:50:16 -08:00
Joshi
09160ce38d Lint fixes 2022-01-05 14:34:45 -08:00
Shefali Joshi
b661d10fb4 Merge branch 'master' into plots-independent-time 2022-01-05 14:27:20 -08:00
Shefali Joshi
3620760991 Condition set output label (#4233)
* Show condition set label for condition widgets
* CSS changes
* Ensure condition set output as labels also works when condition widget is part of a display layout
* Adds tests for conditionWidget
* Tests for condition label output. Fix breaking tests.
* Don't remove event listeners when conditionset changes

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
2022-01-05 13:54:11 -08:00
Joshi
5bffb7c84c Merge branch 'master' of https://github.com/nasa/openmct into plots-independent-time 2022-01-05 11:56:32 -08:00
Scott Bell
88a94c80be Unable to create domain objects (#4672)
* Run full regression suite on PR
* rename job
* specify new testsuites to run
* use newer objects types
* Limit concurrency to 2 workers
* CI!
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: John Hill <jchill2.spam@gmail.com>
2022-01-05 09:57:25 -08:00
Jamie V
2fc0d34b8f [Root Objects] Order by specified priority (#4658)
* Updated objectAPI to support root priority
* Updated to new ES6 module for root registry and updated docs for new priority API and root object priority
* Set "My Items" to default priority of low, for root object order

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-01-04 16:34:48 -08:00
David 'Epper' Marshall
d53ca3ec9a grid toggle (#4632)
Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Joe Pea <trusktr@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-01-04 15:46:11 -08:00
John Hill
86e5d10fc1 Add npm badge for the lazy (#4619)
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2022-01-04 11:41:19 -08:00
Shefali Joshi
dc8b7af497 Merge branch 'master' into plots-independent-time 2022-01-03 13:43:03 -08:00
Shefali Joshi
3915e5085d Merge branch 'master' into plots-independent-time 2021-12-30 14:31:23 -08:00
Joshi
c1e13e4d0c Enable independent time conductor for stacked plot and overlay plot. 2021-12-30 14:30:23 -08:00
31 changed files with 633 additions and 987 deletions

View File

@@ -153,7 +153,7 @@ workflows:
post-steps:
- upload_code_covio
- e2e-test:
name: e2e-smoke
name: e2e-ci
node-version: lts/fermium
suite: ci
the-nightly: #These jobs do not run on PRs, but against master at night

46
API.md
View File

@@ -52,6 +52,8 @@
- [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 -->
@@ -247,16 +249,24 @@ To do so, use the `addRoot` method of the object API.
eg.
```javascript
openmct.objects.addRoot({
namespace: "example.namespace",
key: "my-key"
});
namespace: "example.namespace",
key: "my-key"
},
openmct.priority.HIGH);
```
The `addRoot` function takes a single [object identifier](#domain-objects-and-identifiers)
as an argument.
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.
Root objects are loaded just like any other objects, i.e. via an object
provider.
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.
## Object Providers
@@ -1051,3 +1061,25 @@ 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)
# 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 (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,6 +13,7 @@ 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

@@ -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",
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js smoke default condition.e2e",
"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",

View File

@@ -41,7 +41,7 @@ function ObjectAPI(typeRegistry, openmct) {
this.typeRegistry = typeRegistry;
this.eventEmitter = new EventEmitter();
this.providers = {};
this.rootRegistry = new RootRegistry();
this.rootRegistry = new RootRegistry(openmct);
this.inMemorySearchProvider = new InMemorySearchProvider(openmct);
this.rootProvider = new RootObjectProvider(this.rootRegistry);
@@ -367,14 +367,17 @@ ObjectAPI.prototype.endTransaction = function () {
/**
* Add a root-level object.
* @param {module:openmct.ObjectAPI~Identifier|function} an array of
* identifiers for root level objects, or a function that returns a
* @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
* 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 (key) {
this.rootRegistry.addRoot(key);
ObjectAPI.prototype.addRoot = function (identifier, priority) {
this.rootRegistry.addRoot(identifier, priority);
};
/**

View File

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

View File

@@ -172,6 +172,7 @@ define([
}
return {
isIdentifier: isIdentifier,
toOldFormat: toOldFormat,
toNewFormat: toNewFormat,
makeKeyString: makeKeyString,

View File

@@ -19,83 +19,113 @@
* 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;
beforeEach(function () {
idA = {
key: 'keyA',
namespace: 'something'
};
idB = {
key: 'keyB',
namespace: 'something'
};
idC = {
key: 'keyC',
namespace: 'something'
};
registry = new RootRegistry();
});
import { createOpenMct, resetApplicationState } from '../../../utils/testing';
it('can register a root by key', function () {
registry.addRoot(idA);
describe('RootRegistry', () => {
let openmct;
let idA;
let idB;
let idC;
let idD;
return registry.getRoots()
.then(function (roots) {
expect(roots).toEqual([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'
};
it('can register multiple roots by key', function () {
registry.addRoot([idA, idB]);
openmct.on('start', done);
openmct.startHeadless();
});
return registry.getRoots()
.then(function (roots) {
expect(roots).toEqual([idA, idB]);
});
});
afterEach(async () => {
await resetApplicationState(openmct);
});
it('can register an asynchronous root ', function () {
registry.addRoot(function () {
return Promise.resolve(idA);
it('can register a root by identifier', () => {
openmct.objects.addRoot(idA);
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition).toEqual([idA]);
});
});
return registry.getRoots()
.then(function (roots) {
expect(roots).toEqual([idA]);
});
});
it('can register multiple roots by identifier', () => {
openmct.objects.addRoot([idA, idB]);
it('can register multiple asynchronous roots', function () {
registry.addRoot(function () {
return Promise.resolve([idA, idB]);
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition).toEqual([idA, idB]);
});
});
return registry.getRoots()
.then(function (roots) {
expect(roots).toEqual([idA, idB]);
});
});
it('can register an asynchronous root ', () => {
openmct.objects.addRoot(() => Promise.resolve(idA));
it('can combine different types of registration', function () {
registry.addRoot([idA, idB]);
registry.addRoot(function () {
return Promise.resolve([idC]);
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition).toEqual([idA]);
});
});
return registry.getRoots()
.then(function (roots) {
expect(roots).toEqual([idA, idB, 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);
});
});
});

View File

@@ -62,12 +62,14 @@ export default {
}
},
mounted() {
this.refreshData = this.refreshData.bind(this);
this.setTimeContext();
this.loadComposition();
this.openmct.time.on('bounds', this.refreshData);
},
beforeDestroy() {
this.openmct.time.off('bounds', this.refreshData);
this.stopFollowingTimeContext();
this.removeAllSubscriptions();
@@ -79,6 +81,21 @@ 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);
@@ -147,7 +164,7 @@ export default {
};
},
getOptions() {
const { start, end } = this.openmct.time.bounds();
const { start, end } = this.timeContext.bounds();
return {
end,
@@ -247,10 +264,10 @@ export default {
this.addTrace(trace, key);
},
isDataInTimeRange(datum, key) {
const timeSystemKey = this.openmct.time.timeSystem().key;
const timeSystemKey = this.timeContext.timeSystem().key;
let currentTimestamp = this.parse(key, timeSystemKey, datum);
return currentTimestamp && this.openmct.time.bounds().end >= currentTimestamp;
return currentTimestamp && this.timeContext.bounds().end >= currentTimestamp;
},
format(telemetryObjectKey, metadataKey, data) {
const formats = this.telemetryObjectFormats[telemetryObjectKey];

View File

@@ -39,10 +39,8 @@ export default class ConditionSetViewProvider {
return isConditionSet && this.openmct.router.isNavigatedObject(objectPath);
}
canEdit(domainObject, objectPath) {
const isConditionSet = domainObject.type === 'conditionSet';
return isConditionSet && this.openmct.router.isNavigatedObject(objectPath);
canEdit(domainObject) {
return domainObject.type === 'conditionSet';
}
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();
this.destroy(true);
} else {
let isNewConditionSet = !this.conditionSetIdentifier
|| !this.openmct.objects.areIdsEqual(this.conditionSetIdentifier, styleConfiguration.conditionSetIdentifier);
@@ -180,15 +180,17 @@ export default class StyleRuleManager extends EventEmitter {
this.updateDomainObjectStyle();
}
destroy() {
destroy(skipEventListeners) {
if (this.stopProvidingTelemetry) {
this.stopProvidingTelemetry();
delete this.stopProvidingTelemetry;
}
this.openmct.time.off("bounds", this.refreshData);
this.openmct.editor.off('isEditing', this.toggleSubscription);
if (!skipEventListeners) {
this.openmct.time.off("bounds", this.refreshData);
this.openmct.editor.off('isEditing', this.toggleSubscription);
}
this.conditionSetIdentifier = undefined;
}

View File

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

@@ -1,280 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div class="c-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">
<div class="c-inspect-styles__content c-inspect-styles__condition-set c-inspect-styles__elem">
<a v-if="conditionSetDomainObject"
class="c-object-label"
@click="navigateOrPreview"
@@ -87,6 +87,27 @@
</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"
@@ -172,7 +193,8 @@ export default {
selectedConditionId: '',
items: [],
domainObject: undefined,
consolidatedFontStyle: {}
consolidatedFontStyle: {},
useConditionSetOutputAsLabel: false
};
},
computed: {
@@ -187,6 +209,11 @@ 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;
@@ -205,28 +232,6 @@ 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 [];
@@ -247,6 +252,8 @@ 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);
@@ -264,6 +271,12 @@ 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) {
@@ -487,13 +500,14 @@ 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);
foundStyle.style = Object.assign((this.canHide ? { isStyleInvisible: '' } : {}), this.initialStyles, foundStyle.style, output);
conditionalStyles.push(foundStyle);
} else {
conditionalStyles.splice(index, 0, {
conditionId: conditionConfiguration.id,
style: Object.assign((this.canHide ? { isStyleInvisible: '' } : {}), this.initialStyles)
style: Object.assign((this.canHide ? { isStyleInvisible: '' } : {}), this.initialStyles, output)
});
}
});
@@ -715,6 +729,12 @@ 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];
});
@@ -731,10 +751,21 @@ export default {
}
});
} else {
domainObjectStyles = {
...domainObjectStyles,
...objectStyle
};
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
};
}
}
return domainObjectStyles;
@@ -743,6 +774,17 @@ 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);
},
@@ -863,6 +905,10 @@ 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,12 +39,15 @@
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,6 +133,168 @@ 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;
@@ -449,6 +611,10 @@ 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.label }}
{{ internalDomainObject.conditionalLabel || internalDomainObject.label }}
</div>
</component>
</template>

View File

@@ -27,12 +27,15 @@ 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

@@ -0,0 +1,103 @@
import { createOpenMct, resetApplicationState } from "utils/testing";
import ConditionWidgetPlugin from "./plugin";
import Vue from 'vue';
describe('the plugin', function () {
let objectDef;
let element;
let child;
let openmct;
let mockObjectPath;
beforeEach((done) => {
mockObjectPath = [
{
name: 'mock folder',
type: 'fake-folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
},
{
name: 'mock parent folder',
type: 'conditionWidget',
identifier: {
key: 'mock-parent-folder',
namespace: ''
}
}
];
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);
});
let mockObject = {
name: 'Condition Widget',
key: 'conditionWidget',
creatable: true
};
it('defines a conditionWidget object type with the correct key', () => {
expect(objectDef.key).toEqual(mockObject.key);
});
describe('the conditionWidget object', () => {
it('is creatable', () => {
expect(objectDef.creatable).toEqual(mockObject.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, mockObjectPath);
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();
});
});
});

View File

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

View File

@@ -99,7 +99,6 @@ export default class CreateAction extends PropertiesAction {
*/
async _navigateAndEdit(domainObject, parentDomainObjectpath) {
let objectPath;
let self = this;
if (parentDomainObjectpath) {
objectPath = parentDomainObjectpath && [domainObject].concat(parentDomainObjectpath);
} else {
@@ -111,18 +110,13 @@ export default class CreateAction extends PropertiesAction {
.reverse()
.join('/');
function editObject() {
const objectView = self.openmct.objectViews.get(domainObject, objectPath)[0];
const canEdit = objectView && objectView.canEdit && objectView.canEdit(domainObject, objectPath);
if (canEdit) {
self.openmct.editor.edit();
}
}
this.openmct.router.once('afterNavigation', editObject);
this.openmct.router.navigate(url);
const objectView = this.openmct.objectViews.get(domainObject, objectPath)[0];
const canEdit = objectView && objectView.canEdit && objectView.canEdit(domainObject, objectPath);
if (canEdit) {
this.openmct.editor.edit();
}
}
/**

View File

@@ -420,11 +420,6 @@ export default {
imageIndex = newSize > 0 ? newSize - 1 : undefined;
}
// preserve previous image if a subscription updates history size
if (this.isPaused) {
this.previousFocusedImage = this.focusedImage ? JSON.parse(JSON.stringify(this.focusedImage)) : undefined;
}
this.setFocusedImage(imageIndex, false);
this.scrollToRight();
},
@@ -515,6 +510,12 @@ 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();
@@ -675,6 +676,14 @@ export default {
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
});
},
matchIndexOfPreviousImage(previous, imageHistory) {
// match logic uses a composite of url and time to account
// for example imagery not having fully unique urls
return imageHistory.findIndex((x) => (
x.url === previous.url
&& x.time === previous.time
));
},
setFocusedImage(index, thumbnailClick = false) {
let focusedIndex = index;
if (!(Number.isInteger(index) && index > -1)) {
@@ -688,6 +697,8 @@ export default {
this.imageHistory
);
focusedIndex = matchIndex > -1 ? matchIndex : this.imageHistory.length - 1;
delete this.previousFocusedImage;
}
if (thumbnailClick) {
@@ -695,10 +706,7 @@ export default {
this.focusedImageTimestamp = undefined;
}
this.focusedImageIndex = focusedIndex;
delete this.previousFocusedImage;
if (this.isPaused && !thumbnailClick && this.initFocusedImageIndex === undefined) {
if (this.isPaused && !thumbnailClick && this.focusedImageTimestamp === undefined) {
this.nextImageIndex = focusedIndex;
//this could happen if bounds changes
if (this.focusedImageIndex > this.imageHistory.length - 1) {
@@ -708,6 +716,8 @@ export default {
return;
}
this.focusedImageIndex = focusedIndex;
if (thumbnailClick && !this.isPaused) {
this.paused(true);
}

View File

@@ -120,49 +120,14 @@ export default {
return this.timeFormatter.parse(datum);
},
boundsChange(bounds, isTick) {
if (isTick) {
return;
if (!isTick) {
this.requestHistory();
}
// the focusedImageIndex is calculated twice; once before and after request history arrives
const focusedImage = this.imageHistory[this.focusedImageIndex];
const previousFocusedImage = focusedImage ? JSON.parse(JSON.stringify(focusedImage)) : undefined;
this.updateFocusedImageIndex(previousFocusedImage, this.imageHistory);
// forcibly reset the imageContainer size to prevent an aspect ratio distortion
delete this.imageContainerWidth;
delete this.imageContainerHeight;
return this.requestHistory();
},
matchIndexOfPreviousImage(previous, imageHistory) {
// match logic uses a composite of url and time to account
// for example imagery not having fully unique urls
return imageHistory.findIndex((x) => (
x.url === previous.url
&& x.time === previous.time
));
},
updateFocusedImageIndex(previous, imageHistory) {
if (!previous) {
return;
}
const matchIndex = this.matchIndexOfPreviousImage(
previous,
imageHistory
);
this.focusedImageIndex = matchIndex > -1 ? matchIndex : this.imageHistory.length - 1;
},
async requestHistory() {
let bounds = this.timeContext.bounds();
this.requestCount++;
const requestId = this.requestCount;
// maintain previous focused image value to discern new index
const focusedImage = this.imageHistory[this.focusedImageIndex];
const previousFocusedImage = focusedImage ? JSON.parse(JSON.stringify(focusedImage)) : undefined;
this.imageHistory = [];
let data = await this.openmct.telemetry
@@ -176,8 +141,7 @@ export default {
imagery.push(image);
}
});
this.updateFocusedImageIndex(previousFocusedImage, imagery);
//this is to optimize anything that reacts to imageHistory length
this.imageHistory = imagery;
}
},

View File

@@ -25,11 +25,15 @@ import myItemsInterceptor from "./myItemsInterceptor";
const MY_ITEMS_DEFAULT_NAME = 'My Items';
export default function MyItemsPlugin(name = MY_ITEMS_DEFAULT_NAME, namespace = '') {
export default function MyItemsPlugin(name = MY_ITEMS_DEFAULT_NAME, namespace = '', priority = undefined) {
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);
openmct.objects.addRoot(identifier, priority);
};
}

View File

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

View File

@@ -185,6 +185,11 @@
&__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

@@ -21,7 +21,7 @@
*****************************************************************************/
export const COLOR_PALETTE = [
[0x00, 0x37, 0xFF],
[0x43, 0xB0, 0xFF],
[0xF0, 0x60, 0x00],
[0x00, 0x70, 0x40],
[0xFB, 0x49, 0x49],
@@ -30,25 +30,25 @@ export const COLOR_PALETTE = [
[0xFF, 0xA6, 0x3D],
[0x05, 0xA3, 0x00],
[0xF0, 0x00, 0x6C],
[0x77, 0x17, 0x7A],
[0xAC, 0x54, 0xAE],
[0x23, 0xA9, 0xDB],
[0xFA, 0xF0, 0x6F],
[0x4E, 0xF0, 0x48],
[0xC7, 0xBE, 0x52],
[0x5A, 0xBD, 0x56],
[0xAD, 0x50, 0x72],
[0x94, 0x25, 0xEA],
[0x21, 0x87, 0x82],
[0x8F, 0x6E, 0x47],
[0xf0, 0x59, 0xcb],
[0x34, 0xB6, 0x7D],
[0x6A, 0x36, 0xFF],
[0x56, 0xF0, 0xE8],
[0x7F, 0x52, 0xFF],
[0x46, 0xC7, 0xC0],
[0xA1, 0x8C, 0x1C],
[0xCB, 0xE1, 0x44],
[0x95, 0xB1, 0x26],
[0xFF, 0x84, 0x9E],
[0xB7, 0x79, 0xE7],
[0x8C, 0xC9, 0xFD],
[0xDB, 0xAA, 0x6E],
[0xB8, 0xDF, 0x97],
[0x93, 0xB5, 0x77],
[0xFF, 0xBC, 0xDA],
[0xD3, 0xB6, 0xDE]
];

View File

@@ -1,6 +1,6 @@
<template>
<div>
<div v-if="domainObject && domainObject.type === 'time-strip'"
<div v-if="supportsIndependentTime"
class="c-conductor-holder--compact l-shell__main-independent-time-conductor"
>
<independent-time-conductor :domain-object="domainObject"
@@ -20,6 +20,12 @@ 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
@@ -64,6 +70,11 @@ export default {
},
font() {
return this.objectFontStyle ? this.objectFontStyle.font : this.layoutFont;
},
supportsIndependentTime() {
const viewKey = this.getViewKey();
return this.domainObject && SupportedViewTypes.includes(viewKey);
}
},
destroyed() {
@@ -191,6 +202,12 @@ 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

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