Compare commits

...

41 Commits

Author SHA1 Message Date
4992b99aa5 Refresh openmct require cache before each test 2020-04-21 13:40:34 -07:00
26838635b6 Ensures correct results are returned for conditions and criteria for a given telemetry datapoint (#2904)
* Ensures that results for a specific datapoint are evaluated atomically.
* Remove generating timestamp for telemetry data
* Get results directly instead of using events
* Refactor all/any telemetry criterion to use new evaluator
* Use current timesystem to compare latest
* AllTelemetryCriterion extends TelemetryCriterion

Co-authored-by: David Tsay <david.e.tsay@nasa.gov>
Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-10 15:57:38 -07:00
11f2c35bb2 [Notebook]: Entries filter #2820 (#2864)
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-10 15:33:50 -07:00
766f48c1ba Handles static and mixed styles for multiple items in a layout (#2907)
* Show non specific styles when updating multiple item styles
* Save sub object styles to it's domain object
* Layout UI tweak
* Fixes flexible layout bug.
* Fixes font size bug in telemetry view
* Fixes issues with newly places TVOs including transparent properties.
* Fixes #2908
* Say NO to 'transparent' === '__no_value'
- Fixes #2895;
* Ensure styles are correctly applied to domain objects and drawing objects when selected individually
* Ensure none treatment is correctly applied to objects when multple selecting
* Fix intial box border
* Tweaks to c-text-view layout
- Vertically center text;
- Normalize padding;
- Overflow: hidden;

* Tweaks to Clock and Timer layout
- Fixes #2893;
- Vertically center text;
- Normalize padding;
- Overflow: hidden;
- `position: absolute` when in Layout;

Co-authored-by: charlesh88 <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-10 15:22:47 -07:00
da7b93f9b3 Notebook context menu (#2888)
Notebook popup menu fix
Co-authored-by: charlesh88 <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-04-10 15:17:01 -07:00
99c095a69f Fixed condition-improve-reorder branch (#2912)
* wip: changing to condition as drag target

* wip

* wip

* wip

* fixed dragging issues

* fixed dragging classes and added temp border on condition with dragging class

* Conditionals sanding and shimming

- CSS and `all-dragging`;

* wip

* fixed drag end issue and changed dragging class to go on parent condition h

* drag with counter

* wip

* wip

* wip

* return to logic in ConditionCollection.vue

* wip

* completed js part with highlighted c-condition-h on dragover

* restored grippy as draggable elem, improved isValidTarget

* fixed drag text bug

* added moveIndex prop in Condition.vue

* Conditionals drag reorder styling

- Moved `.is-drag-target` class up to conditions-h element;
- Renamed `.all-dragging` to `is-active-dragging`;
- Styling for `__drop-target` elements;

* fixed incorrect default for moveIndex in condition collection, unnecessary reset in condition

* fixed downward move reorder

* removed prevent from dragenter and drag leave, changed @blur to @change for name and output fields

* removed console log

* Repair merge-damaged conditionals.scss

- Manual merge from latest master;

* Test data layout tweaked

- Prevent c-cs__test-data__controls from collapsing;

Co-authored-by: Joel McKinnon <joel.g.mckinnon@nasa.gov>
Co-authored-by: Joel McKinnon <JoelMcKinnon@users.noreply.github.com>
2020-04-10 15:04:04 -07:00
f885e83505 Merge pull request #2911 from nasa/revert-2818-condition-improve-reorder
Revert "Condition improve reorder"
2020-04-10 14:35:41 -07:00
928bc4c68a Revert "Condition improve reorder (#2818)"
This reverts commit 46fedc1a30.
2020-04-10 14:32:48 -07:00
d5539c7ae4 Merge pull request #2898 from nasa/fix-enum-comparison
Fixes enum comparisons
2020-04-10 14:03:05 -07:00
c86a104fb6 Merge branch 'master' into fix-enum-comparison 2020-04-10 13:54:04 -07:00
46fedc1a30 Condition improve reorder (#2818)
* wip: changing to condition as drag target

* wip

* wip

* wip

* fixed dragging issues

* fixed dragging classes and added temp border on condition with dragging class

* Conditionals sanding and shimming

- CSS and `all-dragging`;

* wip

* fixed drag end issue and changed dragging class to go on parent condition h

* drag with counter

* wip

* wip

* wip

* return to logic in ConditionCollection.vue

* wip

* completed js part with highlighted c-condition-h on dragover

* restored grippy as draggable elem, improved isValidTarget

* fixed drag text bug

* added moveIndex prop in Condition.vue

* Conditionals drag reorder styling

- Moved `.is-drag-target` class up to conditions-h element;
- Renamed `.all-dragging` to `is-active-dragging`;
- Styling for `__drop-target` elements;

* fixed incorrect default for moveIndex in condition collection, unnecessary reset in condition

* fixed downward move reorder

* removed prevent from dragenter and drag leave, changed @blur to @change for name and output fields

* removed console log

Co-authored-by: charlesh88 <charlesh88@gmail.com>
2020-04-10 10:02:33 -07:00
3b6ef9b44b Refactor duplicate code into functions 2020-04-09 15:33:52 -07:00
c68edd9b7d Fixes enum comparisons
Adds check for undefined
2020-04-09 09:14:31 -07:00
11574b7c40 Merge pull request #2861 from nasa/fix-telemetryview-styles
Styles for telemetry are stored on their container domain objects
2020-04-08 15:19:15 -07:00
abc2cd2413 Merge branch 'master' into fix-telemetryview-styles 2020-04-08 14:40:54 -07:00
5d74882646 Merge pull request #2871 from nasa/dave/conditionals-own-results
[Conditionals] Request and subscription results are mutually exclusive
2020-04-08 14:21:31 -07:00
9fe7f230e6 Merge branch 'master' into dave/conditionals-own-results 2020-04-08 14:16:50 -07:00
de4c5b3729 Disables conditional and static styles for hyperlinks and summary widgets. (#2887) 2020-04-08 12:30:59 -07:00
2a7901914a Merge branch 'master' into fix-telemetryview-styles 2020-04-08 11:57:33 -07:00
73b0fc6f79 Removes preventNone as per conversation with UX designer. 2020-04-08 11:46:12 -07:00
ddef16795c Conditionals and Notebook UI fixes (#2868)
- Significant fixes for Safari-compatible Flex layout in Condition Set
view;
- Changed visual approach to current-value section;
- Firefox scrollbar coloring
- Fix layout issues in Firefox;
- Consolidate Conditionals styles into single scss file;
- Fix test datum elements layout, better wrapping;
- Better approach to presence/absence of URL property in Condition
Widget;
- Fixes #2853;
- Fix errors in URL property handling in Condition Widget;
- Fixes #2853;
- Fixes #2867 - hide the View Switcher when an object is being edited;
- Refined titling on View Switcher and Notebook menu button;
- Cleaned up styles in l-browse-bar and moved into
ui/layout/layout.scss;
- Removed styles/_layout.scss;
- Hide the main view Edit button when in mobile
2020-04-08 09:36:23 -07:00
d188b9a056 do not mutate function args for criteria results either 2020-04-07 13:22:03 -07:00
f510f3edd0 Removes missed code 2020-04-07 11:59:24 -07:00
e05b0bb562 Address review comments:
Fixes telemetry view visibility and styling issue
Removes none option for border and background styles for drawing objects
2020-04-07 11:34:48 -07:00
713c5e9fb7 Merge branch 'master' into dave/conditionals-own-results 2020-04-06 18:56:08 -07:00
17bca04560 do not mutate function args 2020-04-06 18:53:48 -07:00
e0c5bca47d fix unit tests 2020-04-06 14:25:55 -07:00
cdc7c1af64 Removes unused data 2020-04-06 14:04:11 -07:00
3158baa998 Merge branch 'fix-telemetryview-styles' of https://github.com/nasa/openmct into fix-telemetryview-styles 2020-04-06 14:02:45 -07:00
698508fde4 Use the subobject view type to determine where styles should be saved 2020-04-06 14:02:06 -07:00
68a96989e1 Merge branch 'master' into fix-telemetryview-styles 2020-04-06 13:57:41 -07:00
46a6a43234 Merge branch 'master' of https://github.com/nasa/openmct into fix-telemetryview-styles 2020-04-06 13:56:30 -07:00
d41fc27b55 subscriptions should use latest timestamp 2020-04-06 11:17:59 -07:00
24bb96cc90 WIP detach request data from subscription data
extract getLatestTimestamp function
2020-04-03 18:39:26 -07:00
483ee173d6 pass correct args into off listener 2020-04-02 16:34:40 -07:00
8f81a45b9b Removes coverage branch 2020-04-02 10:30:23 -07:00
666459be87 Merge branch 'master' of https://github.com/nasa/openmct into fix-telemetryview-styles 2020-04-02 10:30:07 -07:00
d3fe2a6811 Merge branch 'master' of https://github.com/nasa/openmct into fix-telemetryview-styles 2020-04-01 15:52:55 -07:00
97b37edce4 Store telemetry styles on their container domain objects. 2020-04-01 15:51:40 -07:00
dd70bb470f Merge branch 'master' of https://github.com/nasa/openmct into fix-telemetryview-styles 2020-04-01 10:18:30 -07:00
072bf361de Store styles for telemetry on domain objects 2020-03-30 16:20:22 -07:00
61 changed files with 2180 additions and 1416 deletions

View File

@ -23,8 +23,8 @@
import EventEmitter from 'EventEmitter'; import EventEmitter from 'EventEmitter';
import uuid from 'uuid'; import uuid from 'uuid';
import TelemetryCriterion from "./criterion/TelemetryCriterion"; import TelemetryCriterion from "./criterion/TelemetryCriterion";
import { TRIGGER } from "./utils/constants"; import { evaluateResults } from './utils/evaluator';
import {computeCondition, computeConditionByLimit} from "./utils/evaluator"; import { getLatestTimestamp } from './utils/time';
import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion"; import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion";
/* /*
@ -56,28 +56,40 @@ export default class ConditionClass extends EventEmitter {
this.conditionManager = conditionManager; this.conditionManager = conditionManager;
this.id = conditionConfiguration.id; this.id = conditionConfiguration.id;
this.criteria = []; this.criteria = [];
this.criteriaResults = {};
this.result = undefined; this.result = undefined;
this.latestTimestamp = {}; this.timeSystems = this.openmct.time.getAllTimeSystems();
if (conditionConfiguration.configuration.criteria) { if (conditionConfiguration.configuration.criteria) {
this.createCriteria(conditionConfiguration.configuration.criteria); this.createCriteria(conditionConfiguration.configuration.criteria);
} }
this.trigger = conditionConfiguration.configuration.trigger; this.trigger = conditionConfiguration.configuration.trigger;
this.conditionManager.on('broadcastTelemetry', this.handleBroadcastTelemetry, this);
} }
handleBroadcastTelemetry(datum) { getResult(datum) {
if (!datum || !datum.id) { if (!datum || !datum.id) {
console.log('no data received'); console.log('no data received');
return; return;
} }
this.criteria.forEach(criterion => { if (!this.isTelemetryUsed(datum.id)) {
if (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any')) { return;
criterion.handleSubscription(datum, this.conditionManager.telemetryObjects);
} else {
criterion.emit(`subscription:${datum.id}`, datum);
} }
this.criteria.forEach(criterion => {
if (this.isAnyOrAllTelemetry(criterion)) {
criterion.getResult(datum, this.conditionManager.telemetryObjects);
} else {
criterion.getResult(datum);
}
});
this.result = evaluateResults(this.criteria.map(criterion => criterion.result), this.trigger);
}
isAnyOrAllTelemetry(criterion) {
return (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any'));
}
isTelemetryUsed(id) {
return this.criteria.some(criterion => {
return this.isAnyOrAllTelemetry(criterion) || criterion.telemetryObjectIdAsString === id;
}); });
} }
@ -89,7 +101,6 @@ export default class ConditionClass extends EventEmitter {
updateTrigger(trigger) { updateTrigger(trigger) {
if (this.trigger !== trigger) { if (this.trigger !== trigger) {
this.trigger = trigger; this.trigger = trigger;
this.handleConditionUpdated();
} }
} }
@ -133,7 +144,6 @@ export default class ConditionClass extends EventEmitter {
criterion = new TelemetryCriterion(criterionConfigurationWithId, this.openmct); criterion = new TelemetryCriterion(criterionConfigurationWithId, this.openmct);
} }
criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.on('criterionResultUpdated', (obj) => this.handleCriterionResult(obj));
if (!this.criteria) { if (!this.criteria) {
this.criteria = []; this.criteria = [];
} }
@ -162,22 +172,11 @@ export default class ConditionClass extends EventEmitter {
const newCriterionConfiguration = this.generateCriterion(criterionConfiguration); const newCriterionConfiguration = this.generateCriterion(criterionConfiguration);
let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct); let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct);
newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
newCriterion.on('criterionResultUpdated', (obj) => this.handleCriterionResult(obj));
let criterion = found.item; let criterion = found.item;
criterion.unsubscribe(); criterion.unsubscribe();
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.off('criterionResultUpdated', (obj) => this.handleCriterionResult(obj));
this.criteria.splice(found.index, 1, newCriterion); this.criteria.splice(found.index, 1, newCriterion);
if (this.criteriaResults[criterion.id] !== undefined) {
delete this.criteriaResults[criterion.id];
}
}
}
removeCriterion(id) {
if (this.destroyCriterion(id)) {
this.handleConditionUpdated();
} }
} }
@ -185,15 +184,12 @@ export default class ConditionClass extends EventEmitter {
let found = this.findCriterion(id); let found = this.findCriterion(id);
if (found) { if (found) {
let criterion = found.item; let criterion = found.item;
criterion.destroy(); criterion.off('criterionUpdated', (obj) => {
// TODO this is passing the wrong args this.handleCriterionUpdated(obj);
criterion.off('criterionUpdated', (result) => {
this.handleCriterionUpdated(id, result);
}); });
criterion.destroy();
this.criteria.splice(found.index, 1); this.criteria.splice(found.index, 1);
if (this.criteriaResults[criterion.id] !== undefined) {
delete this.criteriaResults[criterion.id];
}
return true; return true;
} }
return false; return false;
@ -203,59 +199,40 @@ export default class ConditionClass extends EventEmitter {
let found = this.findCriterion(criterion.id); let found = this.findCriterion(criterion.id);
if (found) { if (found) {
this.criteria[found.index] = criterion.data; this.criteria[found.index] = criterion.data;
// TODO nothing is listening to this
this.emitEvent('conditionUpdated', {
trigger: this.trigger,
criteria: this.criteria
});
} }
} }
updateCriteriaResults(eventData) {
const id = eventData.id;
if (this.findCriterion(id)) {
// The !! here is important to convert undefined to false otherwise the criteriaResults won't get deleted when the criteria is destroyed
this.criteriaResults[id] = !!eventData.data.result;
}
}
handleCriterionResult(eventData) {
this.updateCriteriaResults(eventData);
this.handleConditionUpdated(eventData.data);
}
requestLADConditionResult() { requestLADConditionResult() {
const criteriaResults = this.criteria let latestTimestamp;
let criteriaResults = {};
const criteriaRequests = this.criteria
.map(criterion => criterion.requestLAD({telemetryObjects: this.conditionManager.telemetryObjects})); .map(criterion => criterion.requestLAD({telemetryObjects: this.conditionManager.telemetryObjects}));
return Promise.all(criteriaResults) return Promise.all(criteriaRequests)
.then(results => { .then(results => {
results.forEach(result => { results.forEach(resultObj => {
this.updateCriteriaResults(result); const { id, data, data: { result } } = resultObj;
this.latestTimestamp = this.getLatestTimestamp(this.latestTimestamp, result.data) if (this.findCriterion(id)) {
criteriaResults[id] = !!result;
}
latestTimestamp = getLatestTimestamp(
latestTimestamp,
data,
this.timeSystems,
this.openmct.time.timeSystem()
);
}); });
this.evaluate();
return { return {
id: this.id, id: this.id,
data: Object.assign({}, this.latestTimestamp, { result: this.result }) data: Object.assign(
} {},
latestTimestamp,
{ result: evaluateResults(Object.values(criteriaResults), this.trigger) }
)
};
}); });
} }
getTelemetrySubscriptions() {
return this.criteria.map(criterion => criterion.telemetryObjectIdAsString);
}
handleConditionUpdated(datum) {
// trigger an updated event so that consumers can react accordingly
this.evaluate();
this.emitEvent('conditionResultUpdated',
Object.assign({}, datum, { result: this.result })
);
}
getCriteria() { getCriteria() {
return this.criteria; return this.criteria;
} }
@ -269,41 +246,7 @@ export default class ConditionClass extends EventEmitter {
return success; return success;
} }
evaluate() {
if (this.trigger && this.trigger === TRIGGER.XOR) {
this.result = computeConditionByLimit(this.criteriaResults, 1);
} else if (this.trigger && this.trigger === TRIGGER.NOT) {
this.result = computeConditionByLimit(this.criteriaResults, 0);
} else {
this.result = computeCondition(this.criteriaResults, this.trigger === TRIGGER.ALL);
}
}
getLatestTimestamp(current, compare) {
const timestamp = Object.assign({}, current);
this.openmct.time.getAllTimeSystems().forEach(timeSystem => {
if (!timestamp[timeSystem.key]
|| compare[timeSystem.key] > timestamp[timeSystem.key]
) {
timestamp[timeSystem.key] = compare[timeSystem.key];
}
});
return timestamp;
}
emitEvent(eventName, data) {
this.emit(eventName, {
id: this.id,
data: data
});
}
destroy() { destroy() {
this.conditionManager.off('broadcastTelemetry', this.handleBroadcastTelemetry, this);
if (typeof this.stopObservingForChanges === 'function') {
this.stopObservingForChanges();
}
this.destroyCriteria(); this.destroyCriteria();
} }
} }

View File

@ -21,6 +21,7 @@
*****************************************************************************/ *****************************************************************************/
import Condition from "./Condition"; import Condition from "./Condition";
import { getLatestTimestamp } from './utils/time';
import uuid from "uuid"; import uuid from "uuid";
import EventEmitter from 'EventEmitter'; import EventEmitter from 'EventEmitter';
@ -29,8 +30,7 @@ export default class ConditionManager extends EventEmitter {
super(); super();
this.openmct = openmct; this.openmct = openmct;
this.conditionSetDomainObject = conditionSetDomainObject; this.conditionSetDomainObject = conditionSetDomainObject;
this.timeAPI = this.openmct.time; this.timeSystems = this.openmct.time.getAllTimeSystems();
this.latestTimestamp = {};
this.composition = this.openmct.composition.get(conditionSetDomainObject); this.composition = this.openmct.composition.get(conditionSetDomainObject);
this.composition.on('add', this.subscribeToTelemetry, this); this.composition.on('add', this.subscribeToTelemetry, this);
this.composition.on('remove', this.unsubscribeFromTelemetry, this); this.composition.on('remove', this.unsubscribeFromTelemetry, this);
@ -55,8 +55,9 @@ export default class ConditionManager extends EventEmitter {
this.telemetryObjects[id] = Object.assign({}, endpoint, {telemetryMetaData: this.openmct.telemetry.getMetadata(endpoint).valueMetadatas}); this.telemetryObjects[id] = Object.assign({}, endpoint, {telemetryMetaData: this.openmct.telemetry.getMetadata(endpoint).valueMetadatas});
this.subscriptions[id] = this.openmct.telemetry.subscribe( this.subscriptions[id] = this.openmct.telemetry.subscribe(
endpoint, endpoint,
this.broadcastTelemetry.bind(this, id) this.telemetryReceived.bind(this, id)
); );
// TODO check if this is needed
this.updateConditionTelemetry(); this.updateConditionTelemetry();
} }
@ -73,7 +74,6 @@ export default class ConditionManager extends EventEmitter {
} }
initialize() { initialize() {
this.conditionResults = {};
this.conditionClassCollection = []; this.conditionClassCollection = [];
if (this.conditionSetDomainObject.configuration.conditionCollection.length) { if (this.conditionSetDomainObject.configuration.conditionCollection.length) {
this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, index) => { this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, index) => {
@ -95,7 +95,6 @@ export default class ConditionManager extends EventEmitter {
initCondition(conditionConfiguration, index) { initCondition(conditionConfiguration, index) {
let condition = new Condition(conditionConfiguration, this.openmct, this); let condition = new Condition(conditionConfiguration, this.openmct, this);
condition.on('conditionResultUpdated', this.handleConditionResult.bind(this));
if (index !== undefined) { if (index !== undefined) {
this.conditionClassCollection.splice(index + 1, 0, condition); this.conditionClassCollection.splice(index + 1, 0, condition);
} else { } else {
@ -159,22 +158,16 @@ export default class ConditionManager extends EventEmitter {
removeCondition(index) { removeCondition(index) {
let condition = this.conditionClassCollection[index]; let condition = this.conditionClassCollection[index];
condition.destroyCriteria(); condition.destroy();
condition.off('conditionResultUpdated', this.handleConditionResult.bind(this));
this.conditionClassCollection.splice(index, 1); this.conditionClassCollection.splice(index, 1);
this.conditionSetDomainObject.configuration.conditionCollection.splice(index, 1); this.conditionSetDomainObject.configuration.conditionCollection.splice(index, 1);
if (this.conditionResults[condition.id] !== undefined) {
delete this.conditionResults[condition.id];
}
this.persistConditions(); this.persistConditions();
this.handleConditionResult();
} }
findConditionById(id) { findConditionById(id) {
return this.conditionClassCollection.find(conditionClass => conditionClass.id === id); return this.conditionClassCollection.find(conditionClass => conditionClass.id === id);
} }
//this.$set(this.conditionClassCollection, reorderEvent.newIndex, oldConditions[reorderEvent.oldIndex]);
reorderConditions(reorderPlan) { reorderConditions(reorderPlan) {
let oldConditions = Array.from(this.conditionSetDomainObject.configuration.conditionCollection); let oldConditions = Array.from(this.conditionSetDomainObject.configuration.conditionCollection);
let newCollection = []; let newCollection = [];
@ -191,7 +184,23 @@ export default class ConditionManager extends EventEmitter {
let currentCondition = conditionCollection[conditionCollection.length-1]; let currentCondition = conditionCollection[conditionCollection.length-1];
for (let i = 0; i < conditionCollection.length - 1; i++) { for (let i = 0; i < conditionCollection.length - 1; i++) {
if (this.conditionResults[conditionCollection[i].id]) { const condition = this.findConditionById(conditionCollection[i].id)
if (condition.result) {
//first condition to be true wins
currentCondition = conditionCollection[i];
break;
}
}
return currentCondition;
}
getCurrentConditionLAD(conditionResults) {
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
let currentCondition = conditionCollection[conditionCollection.length-1];
for (let i = 0; i < conditionCollection.length - 1; i++) {
if (conditionResults[conditionCollection[i].id]) {
//first condition to be true wins //first condition to be true wins
currentCondition = conditionCollection[i]; currentCondition = conditionCollection[i];
break; break;
@ -200,59 +209,32 @@ export default class ConditionManager extends EventEmitter {
return currentCondition; return currentCondition;
} }
updateConditionResults(resultObj) {
if (!resultObj) {
return;
}
const id = resultObj.id;
if (this.findConditionById(id)) {
this.conditionResults[id] = resultObj.data.result;
}
this.updateTimestamp(resultObj.data);
}
handleConditionResult(resultObj) {
// update conditions results and then calculate the current condition
this.updateConditionResults(resultObj);
const currentCondition = this.getCurrentCondition();
this.emit('conditionSetResultUpdated',
Object.assign(
{
output: currentCondition.configuration.output,
id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id
},
this.latestTimestamp
)
)
}
updateTimestamp(timestamp) {
this.timeAPI.getAllTimeSystems().forEach(timeSystem => {
if (!this.latestTimestamp[timeSystem.key]
|| timestamp[timeSystem.key] > this.latestTimestamp[timeSystem.key]
) {
this.latestTimestamp[timeSystem.key] = timestamp[timeSystem.key];
}
});
}
requestLADConditionSetOutput() { requestLADConditionSetOutput() {
if (!this.conditionClassCollection.length) { if (!this.conditionClassCollection.length) {
return Promise.resolve([]); return Promise.resolve([]);
} }
return this.compositionLoad.then(() => { return this.compositionLoad.then(() => {
const ladConditionResults = this.conditionClassCollection let latestTimestamp;
let conditionResults = {};
const conditionRequests = this.conditionClassCollection
.map(condition => condition.requestLADConditionResult()); .map(condition => condition.requestLADConditionResult());
return Promise.all(ladConditionResults) return Promise.all(conditionRequests)
.then((results) => { .then((results) => {
results.forEach(resultObj => { this.updateConditionResults(resultObj); }); results.forEach(resultObj => {
const currentCondition = this.getCurrentCondition(); const { id, data, data: { result } } = resultObj;
if (this.findConditionById(id)) {
conditionResults[id] = !!result;
}
latestTimestamp = getLatestTimestamp(
latestTimestamp,
data,
this.timeSystems,
this.openmct.time.timeSystem()
);
});
const currentCondition = this.getCurrentConditionLAD(conditionResults);
return Object.assign( return Object.assign(
{ {
@ -260,14 +242,34 @@ export default class ConditionManager extends EventEmitter {
id: this.conditionSetDomainObject.identifier, id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id conditionId: currentCondition.id
}, },
this.latestTimestamp latestTimestamp
); );
}); });
}); });
} }
broadcastTelemetry(id, datum) { telemetryReceived(id, datum) {
this.emit(`broadcastTelemetry`, Object.assign({}, this.createNormalizedDatum(datum, id), {id: id})); const normalizedDatum = this.createNormalizedDatum(datum, id);
const timeSystemKey = this.openmct.time.timeSystem().key;
let timestamp = {};
timestamp[timeSystemKey] = normalizedDatum[timeSystemKey];
this.conditionClassCollection.forEach(condition => {
condition.getResult(normalizedDatum);
});
const currentCondition = this.getCurrentCondition();
this.emit('conditionSetResultUpdated',
Object.assign(
{
output: currentCondition.configuration.output,
id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id
},
timestamp
)
)
} }
getTestData(metadatum) { getTestData(metadatum) {
@ -282,12 +284,16 @@ export default class ConditionManager extends EventEmitter {
} }
createNormalizedDatum(telemetryDatum, id) { createNormalizedDatum(telemetryDatum, id) {
return Object.values(this.telemetryObjects[id].telemetryMetaData).reduce((normalizedDatum, metadatum) => { const normalizedDatum = Object.values(this.telemetryObjects[id].telemetryMetaData).reduce((datum, metadatum) => {
const testValue = this.getTestData(metadatum); const testValue = this.getTestData(metadatum);
const formatter = this.openmct.telemetry.getValueFormatter(metadatum); const formatter = this.openmct.telemetry.getValueFormatter(metadatum);
normalizedDatum[metadatum.key] = testValue !== undefined ? formatter.parse(testValue) : formatter.parse(telemetryDatum[metadatum.source]); datum[metadatum.key] = testValue !== undefined ? formatter.parse(testValue) : formatter.parse(telemetryDatum[metadatum.source]);
return normalizedDatum; return datum;
}, {}); }, {});
normalizedDatum.id = id;
return normalizedDatum;
} }
updateTestData(testData) { updateTestData(testData) {
@ -303,14 +309,13 @@ export default class ConditionManager extends EventEmitter {
this.composition.off('add', this.subscribeToTelemetry, this); this.composition.off('add', this.subscribeToTelemetry, this);
this.composition.off('remove', this.unsubscribeFromTelemetry, this); this.composition.off('remove', this.unsubscribeFromTelemetry, this);
Object.values(this.subscriptions).forEach(unsubscribe => unsubscribe()); Object.values(this.subscriptions).forEach(unsubscribe => unsubscribe());
this.subscriptions = undefined; delete this.subscriptions;
if(this.stopObservingForChanges) { if(this.stopObservingForChanges) {
this.stopObservingForChanges(); this.stopObservingForChanges();
} }
this.conditionClassCollection.forEach((condition) => { this.conditionClassCollection.forEach((condition) => {
condition.off('conditionResultUpdated', this.handleConditionResult);
condition.destroy(); condition.destroy();
}) })
} }

View File

@ -49,6 +49,7 @@ describe('ConditionManager', () => {
}; };
let mockComposition; let mockComposition;
let loader; let loader;
let mockTimeSystems;
function mockAngularComponents() { function mockAngularComponents() {
let mockInjector = jasmine.createSpyObj('$injector', ['get']); let mockInjector = jasmine.createSpyObj('$injector', ['get']);
@ -111,10 +112,16 @@ describe('ConditionManager', () => {
openmct.objects.observe.and.returnValue(function () {}); openmct.objects.observe.and.returnValue(function () {});
openmct.objects.mutate.and.returnValue(function () {}); openmct.objects.mutate.and.returnValue(function () {});
mockTimeSystems = {
key: 'utc'
};
openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems']);
openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]);
conditionMgr = new ConditionManager(conditionSetDomainObject, openmct); conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
conditionMgr.on('conditionSetResultUpdated', mockListener); conditionMgr.on('conditionSetResultUpdated', mockListener);
conditionMgr.on('broadcastTelemetry', mockListener); conditionMgr.on('telemetryReceived', mockListener);
}); });
it('creates a conditionCollection with a default condition', function () { it('creates a conditionCollection with a default condition', function () {

View File

@ -25,12 +25,12 @@ import {TRIGGER} from "./utils/constants";
import TelemetryCriterion from "./criterion/TelemetryCriterion"; import TelemetryCriterion from "./criterion/TelemetryCriterion";
let openmct = {}, let openmct = {},
mockListener,
testConditionDefinition, testConditionDefinition,
testTelemetryObject, testTelemetryObject,
conditionObj, conditionObj,
conditionManager, conditionManager,
mockBroadcastTelemetry; mockTelemetryReceived,
mockTimeSystems;
describe("The condition", function () { describe("The condition", function () {
@ -38,10 +38,9 @@ describe("The condition", function () {
conditionManager = jasmine.createSpyObj('conditionManager', conditionManager = jasmine.createSpyObj('conditionManager',
['on'] ['on']
); );
mockBroadcastTelemetry = jasmine.createSpy('listener'); mockTelemetryReceived = jasmine.createSpy('listener');
conditionManager.on('broadcastTelemetry', mockBroadcastTelemetry); conditionManager.on('telemetryReceived', mockTelemetryReceived);
mockListener = jasmine.createSpy('listener');
testTelemetryObject = { testTelemetryObject = {
identifier:{ namespace: "", key: "test-object"}, identifier:{ namespace: "", key: "test-object"},
type: "test-object", type: "test-object",
@ -74,6 +73,12 @@ describe("The condition", function () {
openmct.telemetry.subscribe.and.returnValue(function () {}); openmct.telemetry.subscribe.and.returnValue(function () {});
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry.values); openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry.values);
mockTimeSystems = {
key: 'utc'
};
openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems']);
openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]);
testConditionDefinition = { testConditionDefinition = {
id: '123-456', id: '123-456',
configuration: { configuration: {
@ -97,8 +102,6 @@ describe("The condition", function () {
openmct, openmct,
conditionManager conditionManager
); );
conditionObj.on('conditionUpdated', mockListener);
}); });
it("generates criteria with the correct properties", function () { it("generates criteria with the correct properties", function () {

View File

@ -109,7 +109,7 @@ export default class StyleRuleManager extends EventEmitter {
} else { } else {
if (this.currentStyle) { if (this.currentStyle) {
Object.keys(this.currentStyle).forEach(key => { Object.keys(this.currentStyle).forEach(key => {
this.currentStyle[key] = 'transparent'; this.currentStyle[key] = '__no_value';
}); });
} }
} }

View File

@ -21,9 +21,17 @@
*****************************************************************************/ *****************************************************************************/
<template> <template>
<div v-if="isEditing" <div class="c-condition-h"
class="c-condition c-condition--edit js-condition-drag-wrapper" :class="{ 'is-drag-target': draggingOver }"
@dragover.prevent
@drop.prevent="dropCondition($event, conditionIndex)"
@dragenter="dragEnter($event, conditionIndex)"
@dragleave="dragLeave($event, conditionIndex)"
> >
<div class="c-condition-h__drop-target"></div>
<div v-if="isEditing"
class="c-condition c-condition--edit"
>
<!-- Edit view --> <!-- Edit view -->
<div class="c-condition__header"> <div class="c-condition__header">
<span class="c-condition__drag-grippy c-grippy c-grippy--vertical-drag" <span class="c-condition__drag-grippy c-grippy c-grippy--vertical-drag"
@ -31,8 +39,7 @@
:class="[{ 'is-enabled': !condition.isDefault }, { 'hide-nice': condition.isDefault }]" :class="[{ 'is-enabled': !condition.isDefault }, { 'hide-nice': condition.isDefault }]"
:draggable="!condition.isDefault" :draggable="!condition.isDefault"
@dragstart="dragStart" @dragstart="dragStart"
@dragstop="dragStop" @dragend="dragEnd"
@dragover.stop
></span> ></span>
<span class="c-condition__disclosure c-disclosure-triangle c-tree__item__view-control is-enabled" <span class="c-condition__disclosure c-disclosure-triangle c-tree__item__view-control is-enabled"
@ -75,7 +82,7 @@
<input v-model="condition.configuration.name" <input v-model="condition.configuration.name"
class="t-condition-input__name" class="t-condition-input__name"
type="text" type="text"
@blur="persist" @change="persist"
> >
</span> </span>
@ -98,7 +105,7 @@
v-model="condition.configuration.output" v-model="condition.configuration.output"
class="t-condition-name-input" class="t-condition-name-input"
type="text" type="text"
@blur="persist" @change="persist"
> >
</span> </span>
</span> </span>
@ -157,10 +164,10 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div v-else <div v-else
class="c-condition c-condition--browse" class="c-condition c-condition--browse"
> >
<!-- Browse view --> <!-- Browse view -->
<div class="c-condition__header"> <div class="c-condition__header">
<span class="c-condition__name"> <span class="c-condition__name">
@ -175,6 +182,7 @@
:condition="condition" :condition="condition"
/> />
</div> </div>
</div>
</div> </div>
</template> </template>
@ -207,6 +215,14 @@ export default {
type: Array, type: Array,
required: true, required: true,
default: () => [] default: () => []
},
isDragging: {
type: Boolean,
default: false
},
moveIndex: {
type: Number,
default: 0
} }
}, },
data() { data() {
@ -217,8 +233,8 @@ export default {
selectedOutputSelection: '', selectedOutputSelection: '',
outputOptions: ['false', 'true', 'string'], outputOptions: ['false', 'true', 'string'],
criterionIndex: 0, criterionIndex: 0,
selectedTelemetryName: '', draggingOver: false,
selectedFieldName: '' isDefault: this.condition.isDefault
}; };
}, },
computed: { computed: {
@ -286,11 +302,39 @@ export default {
dragStart(e) { dragStart(e) {
e.dataTransfer.setData('dragging', e.target); // required for FF to initiate drag e.dataTransfer.setData('dragging', e.target); // required for FF to initiate drag
e.dataTransfer.effectAllowed = "copyMove"; e.dataTransfer.effectAllowed = "copyMove";
e.dataTransfer.setDragImage(e.target.closest('.js-condition-drag-wrapper'), 0, 0); e.dataTransfer.setDragImage(e.target.closest('.c-condition-h'), 0, 0);
this.$emit('setMoveIndex', this.conditionIndex); this.$emit('setMoveIndex', this.conditionIndex);
}, },
dragStop(e) { dragEnd(event) {
e.dataTransfer.clearData(); this.dragStarted = false;
event.dataTransfer.clearData();
this.$emit('dragComplete');
},
dropCondition(event, targetIndex) {
if (!this.isDragging) { return }
if (targetIndex > this.moveIndex) { targetIndex-- } // for 'downward' move
if (this.isValidTarget(targetIndex)) {
this.dragElement = undefined;
this.draggingOver = false;
this.$emit('dropCondition', targetIndex);
}
},
dragEnter(event, targetIndex) {
if (!this.isDragging) { return }
if (targetIndex > this.moveIndex) { targetIndex-- } // for 'downward' move
if (this.isValidTarget(targetIndex)) {
this.dragElement = event.target.parentElement;
this.draggingOver = true;
}
},
dragLeave(event) {
if (event.target.parentElement === this.dragElement) {
this.draggingOver = false;
this.dragElement = undefined;
}
},
isValidTarget(targetIndex) {
return this.moveIndex !== targetIndex;
}, },
destroy() { destroy() {
}, },

View File

@ -22,7 +22,6 @@
<template> <template>
<section id="conditionCollection" <section id="conditionCollection"
class="c-cs__conditions"
:class="{ 'is-expanded': expanded }" :class="{ 'is-expanded': expanded }"
> >
<div class="c-cs__header c-section__header"> <div class="c-cs__header c-section__header">
@ -53,30 +52,26 @@
<span class="c-cs-button__label">Add Condition</span> <span class="c-cs-button__label">Add Condition</span>
</button> </button>
<div class="c-cs__conditions-h"> <div class="c-cs__conditions-h"
<div v-for="(condition, index) in conditionCollection" :class="{ 'is-active-dragging': isDragging }"
:key="condition.id"
class="c-condition-h"
> >
<div v-if="isEditing" <Condition v-for="(condition, index) in conditionCollection"
class="c-c__drag-ghost" :key="condition.id"
@drop.prevent="dropCondition" :condition="condition"
@dragenter="dragEnter"
@dragleave="dragLeave"
@dragover.prevent
></div>
<Condition :condition="condition"
:condition-index="index" :condition-index="index"
:telemetry="telemetryObjs" :telemetry="telemetryObjs"
:is-editing="isEditing" :is-editing="isEditing"
:move-index="moveIndex"
:is-dragging="isDragging"
@updateCondition="updateCondition" @updateCondition="updateCondition"
@removeCondition="removeCondition" @removeCondition="removeCondition"
@cloneCondition="cloneCondition" @cloneCondition="cloneCondition"
@setMoveIndex="setMoveIndex" @setMoveIndex="setMoveIndex"
@dragComplete="dragComplete"
@dropCondition="dropCondition"
/> />
</div> </div>
</div> </div>
</div>
</section> </section>
</template> </template>
@ -109,9 +104,10 @@ export default {
conditionResults: {}, conditionResults: {},
conditions: [], conditions: [],
telemetryObjs: [], telemetryObjs: [],
moveIndex: Number, moveIndex: undefined,
isDragging: false, isDragging: false,
defaultOutput: undefined defaultOutput: undefined,
dragCounter: 0
}; };
}, },
watch: { watch: {
@ -166,9 +162,7 @@ export default {
this.moveIndex = index; this.moveIndex = index;
this.isDragging = true; this.isDragging = true;
}, },
dropCondition(e) { dropCondition(targetIndex) {
let targetIndex = Array.from(document.querySelectorAll('.c-c__drag-ghost')).indexOf(e.target);
if (targetIndex > this.moveIndex) { targetIndex-- } // for 'downward' move
const oldIndexArr = Object.keys(this.conditionCollection); const oldIndexArr = Object.keys(this.conditionCollection);
const move = function (arr, old_index, new_index) { const move = function (arr, old_index, new_index) {
while (old_index < 0) { while (old_index < 0) {
@ -194,20 +188,10 @@ export default {
} }
this.reorder(reorderPlan); this.reorder(reorderPlan);
},
e.target.classList.remove("dragging"); dragComplete() {
this.isDragging = false; this.isDragging = false;
}, },
dragEnter(e) {
if (!this.isDragging) { return }
let targetIndex = Array.from(document.querySelectorAll('.c-c__drag-ghost')).indexOf(e.target);
if (targetIndex > this.moveIndex) { targetIndex-- } // for 'downward' move
if (this.moveIndex === targetIndex) { return }
e.target.classList.add("dragging");
},
dragLeave(e) {
e.target.classList.remove("dragging");
},
addTelemetryObject(domainObject) { addTelemetryObject(domainObject) {
this.telemetryObjs.push(domainObject); this.telemetryObjs.push(domainObject);
this.$emit('telemetryUpdated', this.telemetryObjs); this.$emit('telemetryUpdated', this.telemetryObjs);

View File

@ -23,30 +23,33 @@
<template> <template>
<div class="c-cs"> <div class="c-cs">
<section class="c-cs__current-output c-section"> <section class="c-cs__current-output c-section">
<div class="c-cs__header c-section__header">
<span class="c-cs__header-label c-section__label">Current Output</span>
</div>
<div class="c-cs__content c-cs__current-output-value"> <div class="c-cs__content c-cs__current-output-value">
<span class="c-cs__current-output-value__label">Current Output</span>
<span class="c-cs__current-output-value__value">
<template v-if="currentConditionOutput"> <template v-if="currentConditionOutput">
{{ currentConditionOutput }} {{ currentConditionOutput }}
</template> </template>
<template v-else> <template v-else>
{{ defaultConditionOutput }} {{ defaultConditionOutput }}
</template> </template>
</span>
</div> </div>
</section> </section>
<TestData :is-editing="isEditing" <div class="c-cs__test-data-and-conditions-w">
<TestData class="c-cs__test-data"
:is-editing="isEditing"
:test-data="testData" :test-data="testData"
:telemetry="telemetryObjs" :telemetry="telemetryObjs"
@updateTestData="updateTestData" @updateTestData="updateTestData"
/> />
<ConditionCollection <ConditionCollection class="c-cs__conditions"
:is-editing="isEditing" :is-editing="isEditing"
:test-data="testData" :test-data="testData"
@conditionSetResultUpdated="updateCurrentOutput" @conditionSetResultUpdated="updateCurrentOutput"
@updateDefaultOutput="updateDefaultOutput" @updateDefaultOutput="updateDefaultOutput"
@telemetryUpdated="updateTelemetry" @telemetryUpdated="updateTelemetry"
/> />
</div>
</div> </div>
</template> </template>

View File

@ -23,7 +23,6 @@
<template> <template>
<section v-show="isEditing" <section v-show="isEditing"
id="test-data" id="test-data"
class="c-cs__test-data"
:class="{ 'is-expanded': expanded }" :class="{ 'is-expanded': expanded }"
> >
<div class="c-cs__header c-section__header"> <div class="c-cs__header c-section__header">
@ -37,7 +36,7 @@
<div v-if="expanded" <div v-if="expanded"
class="c-cs__content" class="c-cs__content"
> >
<div class="c-cdef__controls" <div class="c-cs__test-data__controls c-cdef__controls"
:disabled="!telemetry.length" :disabled="!telemetry.length"
> >
<label class="c-toggle-switch"> <label class="c-toggle-switch">
@ -96,7 +95,7 @@
> >
</span> </span>
</span> </span>
<div class="c-test-datum__buttons"> <div class="c-cs-test__buttons">
<button class="c-click-icon c-test-data__duplicate-button icon-duplicate" <button class="c-click-icon c-test-data__duplicate-button icon-duplicate"
title="Duplicate this test datum" title="Duplicate this test datum"
@click="addTestInput(testInput)" @click="addTestInput(testInput)"

View File

@ -1,116 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
.c-cs {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
&__content {
display: flex;
flex-direction: column;
flex: 0 1 auto;
overflow: hidden;
> * {
flex: 0 0 auto;
overflow: hidden;
+ * {
margin-top: $interiorMarginSm;
}
}
.c-button {
align-self: start;
}
}
.is-editing & {
// Add some space to kick away from blue editing border indication
padding: $interiorMargin;
}
section {
display: flex;
flex-direction: column;
overflow: hidden;
}
&__conditions-h {
display: flex;
flex-direction: column;
flex: 1 1 auto;
overflow: auto;
padding-right: $interiorMarginSm;
> * + * {
margin-top: $interiorMarginSm;
}
}
&__conditions {
> * + * {
margin-top: $interiorMarginSm;
}
}
.hint {
padding: $interiorMarginSm;
}
/************************** SPECIFIC ITEMS */
&__current-output-value {
font-size: 1.25em;
padding: $interiorMargin;
}
}
/***************************** TEST DATA */
.c-cs-tests {
flex: 0 1 auto;
overflow: auto;
padding-right: $interiorMarginSm;
> * + * {
margin-top: $interiorMarginSm;
}
}
.c-cs-test {
> * {
flex: 0 0 auto;
+ * {
margin-left: $interiorMargin;
}
}
&__controls {
display: flex;
flex: 1 1 auto;
> * + * {
margin-left: $interiorMargin;
}
}
}

View File

@ -1,133 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
.c-condition,
.c-test-datum {
@include discreteItem();
display: flex;
padding: $interiorMargin;
&--edit {
line-height: 160%; // For layout when inputs wrap, like in criteria
}
}
.c-condition {
flex-direction: column;
min-width: 400px;
> * + * {
margin-top: $interiorMarginSm;
}
&--browse {
.c-condition__summary {
border-top: 1px solid $colorInteriorBorder;
padding-top: $interiorMargin;
}
}
/***************************** HEADER */
&__header {
$h: 22px;
display: flex;
align-items: start;
align-content: stretch;
overflow: hidden;
min-height: $h;
line-height: $h;
> * {
flex: 0 0 auto;
+ * {
margin-left: $interiorMarginSm;
}
}
}
&__drag-grippy {
transform: translateY(50%);
}
&__name {
font-weight: bold;
align-self: baseline; // Fixes bold line-height offset problem
}
&__output,
&__summary {
flex: 1 1 auto;
}
}
/***************************** CONDITION DEFINITION, EDITING */
.c-cdef {
display: grid;
grid-row-gap: $interiorMarginSm;
grid-column-gap: $interiorMargin;
grid-auto-columns: min-content 1fr max-content;
align-items: start;
min-width: 150px;
margin-left: 29px;
overflow: hidden;
&__criteria,
&__match-and-criteria {
display: contents;
}
&__label {
grid-column: 1;
text-align: right;
white-space: nowrap;
}
&__separator {
grid-column: 1 / span 3;
}
&__controls {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
grid-column: 2;
> * > * {
margin-right: $interiorMarginSm;
}
}
&__buttons {
grid-column: 3;
}
}
.c-c__drag-ghost {
width: 100%;
min-height: $interiorMarginSm;
&.dragging {
min-height: 5em;
background-color: lightblue;
border-radius: 2px;
}
}

View File

@ -0,0 +1,311 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/***************************** DRAGGING */
.is-active-dragging {
.c-condition-h__drop-target {
height: 3px;
margin-bottom: $interiorMarginSm;
}
}
.c-condition-h {
&__drop-target {
border-radius: $controlCr;
height: 0;
min-height: 0;
transition: background-color, height;
transition-duration: 150ms;
}
&.is-drag-target {
.c-condition > * {
pointer-events: none; // Keeps the JS drop handler from being intercepted by internal elements
}
.c-condition-h__drop-target {
background-color: rgba($colorKey, 0.7);
}
}
}
.c-cs {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
/************************** CONDITION SET LAYOUT */
&__current-output {
flex: 0 0 auto;
}
&__test-data-and-conditions-w {
display: flex;
flex-direction: column;
flex: 1 1 auto;
height: 100%;
overflow: hidden;
}
&__test-data,
&__conditions {
flex: 0 0 auto;
overflow: hidden;
}
&__test-data {
flex: 0 0 auto;
max-height: 50%;
&.is-expanded {
margin-bottom: $interiorMargin * 4;
}
}
&__conditions {
flex: 1 1 auto;
> * + * {
margin-top: $interiorMarginSm;
}
}
&__content {
display: flex;
flex-direction: column;
flex: 0 1 auto;
overflow: hidden;
> * {
flex: 0 0 auto;
overflow: hidden;
+ * {
margin-top: $interiorMarginSm;
}
}
.c-button {
align-self: start;
}
}
.is-editing & {
// Add some space to kick away from blue editing border indication
padding: $interiorMargin;
}
section {
display: flex;
flex-direction: column;
overflow: hidden;
}
&__conditions-h {
display: flex;
flex-direction: column;
flex: 1 1 auto;
overflow: auto;
padding-right: $interiorMarginSm;
> * + * {
margin-top: $interiorMarginSm;
}
}
.hint {
padding: $interiorMarginSm;
}
/************************** SPECIFIC ITEMS */
&__current-output-value {
flex-direction: row;
align-items: baseline;
padding: 0 $interiorMargin $interiorMarginLg $interiorMargin;
> * {
padding: $interiorMargin 0; // Must do this to align label and value
}
&__label {
color: $colorInspectorSectionHeaderFg;
opacity: 0.9;
text-transform: uppercase;
}
&__value {
$p: $interiorMargin * 3;
font-size: 1.25em;
margin-left: $interiorMargin;
padding-left: $p;
padding-right: $p;
background: rgba(black, 0.2);
border-radius: 5px;
}
}
}
/***************************** CONDITIONS AND TEST DATUM ELEMENTS */
.c-condition,
.c-test-datum {
@include discreteItem();
display: flex;
padding: $interiorMargin;
line-height: 170%; // Aligns text with controls like selects
}
.c-cdef,
.c-cs-test {
&__controls {
display: flex;
flex: 1 1 auto;
flex-wrap: wrap;
> * > * {
margin-right: $interiorMarginSm;
}
}
&__buttons {
white-space: nowrap;
}
}
.c-condition {
flex-direction: column;
min-width: 400px;
> * + * {
margin-top: $interiorMarginSm;
}
&--browse {
.c-condition__summary {
border-top: 1px solid $colorInteriorBorder;
padding-top: $interiorMargin;
}
}
/***************************** HEADER */
&__header {
$h: 22px;
display: flex;
align-items: start;
align-content: stretch;
overflow: hidden;
min-height: $h;
line-height: $h;
> * {
flex: 0 0 auto;
+ * {
margin-left: $interiorMarginSm;
}
}
}
&__drag-grippy {
transform: translateY(50%);
}
&__name {
font-weight: bold;
align-self: baseline; // Fixes bold line-height offset problem
}
&__output,
&__summary {
flex: 1 1 auto;
}
}
/***************************** CONDITION DEFINITION, EDITING */
.c-cdef {
display: grid;
grid-row-gap: $interiorMarginSm;
grid-column-gap: $interiorMargin;
grid-auto-columns: min-content 1fr max-content;
align-items: start;
min-width: 150px;
margin-left: 29px;
overflow: hidden;
&__criteria,
&__match-and-criteria {
display: contents;
}
&__label {
grid-column: 1;
text-align: right;
white-space: nowrap;
}
&__separator {
grid-column: 1 / span 3;
}
&__controls {
align-items: flex-start;
grid-column: 2;
> * > * {
margin-right: $interiorMarginSm;
}
}
&__buttons {
grid-column: 3;
}
}
.c-c__drag-ghost {
width: 100%;
min-height: $interiorMarginSm;
&.dragging {
min-height: 5em;
background-color: lightblue;
border-radius: 2px;
}
}
/***************************** TEST DATA */
.c-cs__test-data {
&__controls {
flex: 0 0 auto;
}
}
.c-cs-tests {
flex: 0 1 auto;
overflow: auto;
padding-right: $interiorMarginSm;
> * + * {
margin-top: $interiorMarginSm;
}
}
.c-cs-test {
> * + * {
margin-left: $interiorMargin;
}
}

View File

@ -105,6 +105,7 @@ import ConditionDescription from "@/plugins/condition/components/ConditionDescri
import ConditionError from "@/plugins/condition/components/ConditionError.vue"; import ConditionError from "@/plugins/condition/components/ConditionError.vue";
import Vue from 'vue'; import Vue from 'vue';
import PreviewAction from "@/ui/preview/PreviewAction.js"; import PreviewAction from "@/ui/preview/PreviewAction.js";
import {getApplicableStylesForItem} from "@/plugins/condition/utils/styleUtils";
export default { export default {
name: 'ConditionalStylesView', name: 'ConditionalStylesView',
@ -115,24 +116,8 @@ export default {
}, },
inject: [ inject: [
'openmct', 'openmct',
'domainObject' 'selection'
], ],
props: {
itemId: {
type: String,
default: ''
},
initialStyles: {
type: Object,
default() {
return undefined;
}
},
canHide: {
type: Boolean,
default: false
}
},
data() { data() {
return { return {
conditionalStyles: [], conditionalStyles: [],
@ -145,9 +130,11 @@ export default {
} }
}, },
destroyed() { destroyed() {
this.openmct.editor.off('isEditing', this.setEditState); this.removeListeners();
}, },
mounted() { mounted() {
this.itemId = '';
this.getDomainObjectFromSelection();
this.previewAction = new PreviewAction(this.openmct); this.previewAction = new PreviewAction(this.openmct);
if (this.domainObject.configuration && this.domainObject.configuration.objectStyles) { if (this.domainObject.configuration && this.domainObject.configuration.objectStyles) {
let objectStyles = this.itemId ? this.domainObject.configuration.objectStyles[this.itemId] : this.domainObject.configuration.objectStyles; let objectStyles = this.itemId ? this.domainObject.configuration.objectStyles[this.itemId] : this.domainObject.configuration.objectStyles;
@ -162,6 +149,49 @@ export default {
this.openmct.editor.on('isEditing', this.setEditState); this.openmct.editor.on('isEditing', this.setEditState);
}, },
methods: { 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();
}
},
initialize(conditionSetDomainObject) { initialize(conditionSetDomainObject) {
//If there are new conditions in the conditionSet we need to set those styles to default //If there are new conditions in the conditionSet we need to set those styles to default
this.conditionSetDomainObject = conditionSetDomainObject; this.conditionSetDomainObject = conditionSetDomainObject;
@ -258,6 +288,36 @@ export default {
this.persist(domainObjectStyles); this.persist(domainObjectStyles);
}, },
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);
keys.forEach((key) => {
if ((key !== 'styles') &&
(key !== 'staticStyle') &&
(key !== 'conditionSetIdentifier')) {
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() { initializeConditionalStyles() {
if (!this.conditions) { if (!this.conditions) {
this.conditions = {}; this.conditions = {};
@ -283,9 +343,15 @@ export default {
}, },
initializeStaticStyle(objectStyles) { initializeStaticStyle(objectStyles) {
let staticStyle = objectStyles && objectStyles.staticStyle; let staticStyle = objectStyles && objectStyles.staticStyle;
this.staticStyle = staticStyle || { if (staticStyle) {
this.staticStyle = {
style: Object.assign({}, this.initialStyles, staticStyle.style)
};
} else {
this.staticStyle = {
style: Object.assign({}, this.initialStyles) style: Object.assign({}, this.initialStyles)
}; };
}
}, },
findStyleByConditionId(id) { findStyleByConditionId(id) {
return this.conditionalStyles.find(conditionalStyle => conditionalStyle.conditionId === id); return this.conditionalStyles.find(conditionalStyle => conditionalStyle.conditionId === id);

View File

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

@ -22,38 +22,41 @@
<template> <template>
<div class="c-style"> <div class="c-style">
<span class="c-style-thumb" <span :class="[
:class="{ 'is-style-invisible': styleItem.style.isStyleInvisible }" { 'is-style-invisible': styleItem.style.isStyleInvisible },
:style="[styleItem.style.imageUrl ? { backgroundImage:'url(' + styleItem.style.imageUrl + ')'} : styleItem.style ]" { 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 }
]"
:style="[styleItem.style.imageUrl ? { backgroundImage:'url(' + styleItem.style.imageUrl + ')'} : itemStyle ]"
class="c-style-thumb"
> >
<span class="c-style-thumb__text" <span class="c-style-thumb__text"
:class="{ 'hide-nice': !styleItem.style.color }" :class="{ 'hide-nice': !hasProperty(styleItem.style.color) }"
> >
ABC ABC
</span> </span>
</span> </span>
<span class="c-toolbar"> <span class="c-toolbar">
<toolbar-color-picker v-if="styleItem.style.border" <toolbar-color-picker v-if="hasProperty(styleItem.style.border)"
class="c-style__toolbar-button--border-color u-menu-to--center" class="c-style__toolbar-button--border-color u-menu-to--center"
:options="borderColorOption" :options="borderColorOption"
@change="updateStyleValue" @change="updateStyleValue"
/> />
<toolbar-color-picker v-if="styleItem.style.backgroundColor" <toolbar-color-picker v-if="hasProperty(styleItem.style.backgroundColor)"
class="c-style__toolbar-button--background-color u-menu-to--center" class="c-style__toolbar-button--background-color u-menu-to--center"
:options="backgroundColorOption" :options="backgroundColorOption"
@change="updateStyleValue" @change="updateStyleValue"
/> />
<toolbar-color-picker v-if="styleItem.style.color" <toolbar-color-picker v-if="hasProperty(styleItem.style.color)"
class="c-style__toolbar-button--color u-menu-to--center" class="c-style__toolbar-button--color u-menu-to--center"
:options="colorOption" :options="colorOption"
@change="updateStyleValue" @change="updateStyleValue"
/> />
<toolbar-button v-if="styleItem.style.imageUrl !== undefined" <toolbar-button v-if="hasProperty(styleItem.style.imageUrl)"
class="c-style__toolbar-button--image-url" class="c-style__toolbar-button--image-url"
:options="imageUrlOption" :options="imageUrlOption"
@change="updateStyleValue" @change="updateStyleValue"
/> />
<toolbar-toggle-button v-if="styleItem.style.isStyleInvisible !== undefined" <toolbar-toggle-button v-if="hasProperty(styleItem.style.isStyleInvisible)"
class="c-style__toolbar-button--toggle-visible" class="c-style__toolbar-button--toggle-visible"
:options="isStyleInvisibleOption" :options="isStyleInvisibleOption"
@change="updateStyleValue" @change="updateStyleValue"
@ -68,6 +71,7 @@ import ToolbarColorPicker from "@/ui/toolbar/components/toolbar-color-picker.vue
import ToolbarButton from "@/ui/toolbar/components/toolbar-button.vue"; import ToolbarButton from "@/ui/toolbar/components/toolbar-button.vue";
import ToolbarToggleButton from "@/ui/toolbar/components/toolbar-toggle-button.vue"; import ToolbarToggleButton from "@/ui/toolbar/components/toolbar-toggle-button.vue";
import {STYLE_CONSTANTS} from "@/plugins/condition/utils/constants"; import {STYLE_CONSTANTS} from "@/plugins/condition/utils/constants";
import {getStylesWithoutNoneValue} from "@/plugins/condition/utils/styleUtils";
export default { export default {
name: 'StyleEditor', name: 'StyleEditor',
@ -83,37 +87,52 @@ export default {
isEditing: { isEditing: {
type: Boolean type: Boolean
}, },
mixedStyles: {
type: Array,
default() {
return [];
}
},
styleItem: { styleItem: {
type: Object, type: Object,
required: true required: true
} }
}, },
computed: { computed: {
itemStyle() {
return getStylesWithoutNoneValue(this.styleItem.style);
},
borderColorOption() { borderColorOption() {
let value = this.styleItem.style.border.replace('1px solid ', '');
return { return {
icon: 'icon-line-horz', icon: 'icon-line-horz',
title: STYLE_CONSTANTS.borderColorTitle, title: STYLE_CONSTANTS.borderColorTitle,
value: this.styleItem.style.border.replace('1px solid ', ''), value: this.normalizeValueForSwatch(value),
property: 'border', property: 'border',
isEditing: this.isEditing isEditing: this.isEditing,
nonSpecific: this.mixedStyles.indexOf('border') > -1
} }
}, },
backgroundColorOption() { backgroundColorOption() {
let value = this.styleItem.style.backgroundColor;
return { return {
icon: 'icon-paint-bucket', icon: 'icon-paint-bucket',
title: STYLE_CONSTANTS.backgroundColorTitle, title: STYLE_CONSTANTS.backgroundColorTitle,
value: this.styleItem.style.backgroundColor, value: this.normalizeValueForSwatch(value),
property: 'backgroundColor', property: 'backgroundColor',
isEditing: this.isEditing isEditing: this.isEditing,
nonSpecific: this.mixedStyles.indexOf('backgroundColor') > -1
} }
}, },
colorOption() { colorOption() {
let value = this.styleItem.style.color;
return { return {
icon: 'icon-font', icon: 'icon-font',
title: STYLE_CONSTANTS.textColorTitle, title: STYLE_CONSTANTS.textColorTitle,
value: this.styleItem.style.color, value: this.normalizeValueForSwatch(value),
property: 'color', property: 'color',
isEditing: this.isEditing isEditing: this.isEditing,
nonSpecific: this.mixedStyles.indexOf('color') > -1
} }
}, },
imageUrlOption() { imageUrlOption() {
@ -138,7 +157,8 @@ export default {
property: 'imageUrl', property: 'imageUrl',
formKeys: ['url'], formKeys: ['url'],
value: {url: this.styleItem.style.imageUrl}, value: {url: this.styleItem.style.imageUrl},
isEditing: this.isEditing isEditing: this.isEditing,
nonSpecific: this.mixedStyles.indexOf('imageUrl') > -1
} }
}, },
isStyleInvisibleOption() { isStyleInvisibleOption() {
@ -163,7 +183,23 @@ export default {
} }
}, },
methods: { methods: {
hasProperty(property) {
return property !== undefined;
},
normalizeValueForSwatch(value) {
if (value && value.indexOf('__no_value') > -1) {
return value.replace('__no_value', 'transparent');
}
return value;
},
normalizeValueForStyle(value) {
if (value && value === 'transparent') {
return '__no_value';
}
return value;
},
updateStyleValue(value, item) { updateStyleValue(value, item) {
value = this.normalizeValueForStyle(value);
if (item.property === 'border') { if (item.property === 'border') {
value = '1px solid ' + value; value = '1px solid ' + value;
} }
@ -172,7 +208,7 @@ export default {
} else { } else {
this.styleItem.style[item.property] = value; this.styleItem.style[item.property] = value;
} }
this.$emit('persist', this.styleItem); this.$emit('persist', this.styleItem, item.property);
} }
} }
} }

View File

@ -20,11 +20,10 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import EventEmitter from 'EventEmitter'; import TelemetryCriterion from './TelemetryCriterion';
import {OPERATIONS} from '../utils/operations'; import { evaluateResults } from "../utils/evaluator";
import {computeCondition} from "@/plugins/condition/utils/evaluator";
export default class TelemetryCriterion extends EventEmitter { export default class AllTelemetryCriterion extends TelemetryCriterion {
/** /**
* Subscribes/Unsubscribes to telemetry and emits the result * Subscribes/Unsubscribes to telemetry and emits the result
@ -34,23 +33,20 @@ export default class TelemetryCriterion extends EventEmitter {
* @param openmct * @param openmct
*/ */
constructor(telemetryDomainObjectDefinition, openmct) { constructor(telemetryDomainObjectDefinition, openmct) {
super(); super(telemetryDomainObjectDefinition, openmct);
}
this.openmct = openmct; initialize() {
this.objectAPI = this.openmct.objects; this.telemetryObjects = { ...this.telemetryDomainObjectDefinition.telemetryObjects };
this.telemetryAPI = this.openmct.telemetry;
this.timeAPI = this.openmct.time;
this.id = telemetryDomainObjectDefinition.id;
this.telemetry = telemetryDomainObjectDefinition.telemetry;
this.operation = telemetryDomainObjectDefinition.operation;
this.telemetryObjects = Object.assign({}, telemetryDomainObjectDefinition.telemetryObjects);
this.input = telemetryDomainObjectDefinition.input;
this.metadata = telemetryDomainObjectDefinition.metadata;
this.telemetryDataCache = {}; this.telemetryDataCache = {};
} }
isValid() {
return (this.telemetry === 'any' || this.telemetry === 'all') && this.metadata && this.operation;
}
updateTelemetry(telemetryObjects) { updateTelemetry(telemetryObjects) {
this.telemetryObjects = Object.assign({}, telemetryObjects); this.telemetryObjects = { ...telemetryObjects };
} }
formatData(data, telemetryObjects) { formatData(data, telemetryObjects) {
@ -68,60 +64,32 @@ export default class TelemetryCriterion extends EventEmitter {
}); });
const datum = { const datum = {
result: computeCondition(this.telemetryDataCache, this.telemetry === 'all') result: evaluateResults(Object.values(this.telemetryDataCache), this.telemetry)
}; };
if (data) { if (data) {
// TODO check back to see if we should format times here this.openmct.time.getAllTimeSystems().forEach(timeSystem => {
this.timeAPI.getAllTimeSystems().forEach(timeSystem => {
datum[timeSystem.key] = data[timeSystem.key] datum[timeSystem.key] = data[timeSystem.key]
}); });
} }
return datum; return datum;
} }
handleSubscription(data, telemetryObjects) { getResult(data, telemetryObjects) {
if(this.isValid()) { const validatedData = this.isValid() ? data : {};
this.emitEvent('criterionResultUpdated', this.formatData(data, telemetryObjects));
} else { if (validatedData) {
this.emitEvent('criterionResultUpdated', this.formatData({}, telemetryObjects)); this.telemetryDataCache[validatedData.id] = this.computeResult(validatedData);
}
} }
findOperation(operation) { Object.values(telemetryObjects).forEach(telemetryObject => {
for (let i=0; i < OPERATIONS.length; i++) { const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (operation === OPERATIONS[i].name) { if (this.telemetryDataCache[id] === undefined) {
return OPERATIONS[i].operation; this.telemetryDataCache[id] = false;
} }
}
return null;
}
computeResult(data) {
let result = false;
if (data) {
let comparator = this.findOperation(this.operation);
let params = [];
params.push(data[this.metadata]);
if (this.input instanceof Array && this.input.length) {
this.input.forEach(input => params.push(input));
}
if (typeof comparator === 'function') {
result = comparator(params);
}
}
return result;
}
emitEvent(eventName, data) {
this.emit(eventName, {
id: this.id,
data: data
}); });
}
isValid() { this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
return (this.telemetry === 'any' || this.telemetry === 'all') && this.metadata && this.operation;
} }
requestLAD(options) { requestLAD(options) {
@ -139,7 +107,7 @@ export default class TelemetryCriterion extends EventEmitter {
let keys = Object.keys(Object.assign({}, options.telemetryObjects)); let keys = Object.keys(Object.assign({}, options.telemetryObjects));
const telemetryRequests = keys const telemetryRequests = keys
.map(key => this.telemetryAPI.request( .map(key => this.openmct.telemetry.request(
options.telemetryObjects[key], options.telemetryObjects[key],
options options
)); ));
@ -163,10 +131,7 @@ export default class TelemetryCriterion extends EventEmitter {
} }
destroy() { destroy() {
this.emitEvent('criterionRemoved');
delete this.telemetryObjects; delete this.telemetryObjects;
delete this.telemetryDataCache; delete this.telemetryDataCache;
delete this.telemetryObjectIdAsString;
delete this.telemetryObject;
} }
} }

View File

@ -21,7 +21,7 @@
*****************************************************************************/ *****************************************************************************/
import EventEmitter from 'EventEmitter'; import EventEmitter from 'EventEmitter';
import {OPERATIONS} from '../utils/operations'; import { OPERATIONS } from '../utils/operations';
export default class TelemetryCriterion extends EventEmitter { export default class TelemetryCriterion extends EventEmitter {
@ -36,20 +36,27 @@ export default class TelemetryCriterion extends EventEmitter {
super(); super();
this.openmct = openmct; this.openmct = openmct;
this.objectAPI = this.openmct.objects; this.telemetryDomainObjectDefinition = telemetryDomainObjectDefinition;
this.telemetryAPI = this.openmct.telemetry;
this.timeAPI = this.openmct.time;
this.id = telemetryDomainObjectDefinition.id; this.id = telemetryDomainObjectDefinition.id;
this.telemetry = telemetryDomainObjectDefinition.telemetry; this.telemetry = telemetryDomainObjectDefinition.telemetry;
this.operation = telemetryDomainObjectDefinition.operation; this.operation = telemetryDomainObjectDefinition.operation;
this.input = telemetryDomainObjectDefinition.input; this.input = telemetryDomainObjectDefinition.input;
this.metadata = telemetryDomainObjectDefinition.metadata; this.metadata = telemetryDomainObjectDefinition.metadata;
this.telemetryObject = telemetryDomainObjectDefinition.telemetryObject; this.result = undefined;
this.telemetryObjectIdAsString = this.objectAPI.makeKeyString(telemetryDomainObjectDefinition.telemetry);
this.on(`subscription:${this.telemetryObjectIdAsString}`, this.handleSubscription); this.initialize();
this.emitEvent('criterionUpdated', this); this.emitEvent('criterionUpdated', this);
} }
initialize() {
this.telemetryObject = this.telemetryDomainObjectDefinition.telemetryObject;
this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
}
isValid() {
return this.telemetryObject && this.metadata && this.operation;
}
updateTelemetry(telemetryObjects) { updateTelemetry(telemetryObjects) {
this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString]; this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString];
} }
@ -60,20 +67,44 @@ export default class TelemetryCriterion extends EventEmitter {
}; };
if (data) { if (data) {
// TODO check back to see if we should format times here this.openmct.time.getAllTimeSystems().forEach(timeSystem => {
this.timeAPI.getAllTimeSystems().forEach(timeSystem => {
datum[timeSystem.key] = data[timeSystem.key] datum[timeSystem.key] = data[timeSystem.key]
}); });
} }
return datum; return datum;
} }
handleSubscription(data) { getResult(data) {
if(this.isValid()) { const validatedData = this.isValid() ? data : {};
this.emitEvent('criterionResultUpdated', this.formatData(data)); this.result = this.computeResult(validatedData);
} else {
this.emitEvent('criterionResultUpdated', this.formatData({}));
} }
requestLAD(options) {
options = Object.assign({},
options,
{
strategy: 'latest',
size: 1
}
);
if (!this.isValid()) {
return {
id: this.id,
data: this.formatData({})
};
}
return this.openmct.telemetry.request(
this.telemetryObject,
options
).then(results => {
const latestDatum = results.length ? results[results.length - 1] : {};
return {
id: this.id,
data: this.formatData(latestDatum)
};
});
} }
findOperation(operation) { findOperation(operation) {
@ -95,7 +126,7 @@ export default class TelemetryCriterion extends EventEmitter {
this.input.forEach(input => params.push(input)); this.input.forEach(input => params.push(input));
} }
if (typeof comparator === 'function') { if (typeof comparator === 'function') {
result = comparator(params); result = !!comparator(params);
} }
} }
return result; return result;
@ -108,42 +139,9 @@ export default class TelemetryCriterion extends EventEmitter {
}); });
} }
isValid() {
return this.telemetryObject && this.metadata && this.operation;
}
requestLAD(options) {
options = Object.assign({},
options,
{
strategy: 'latest',
size: 1
}
);
if (!this.isValid()) {
return {
id: this.id,
data: this.formatData({})
};
}
return this.telemetryAPI.request(
this.telemetryObject,
options
).then(results => {
const latestDatum = results.length ? results[results.length - 1] : {};
return {
id: this.id,
data: this.formatData(latestDatum)
};
});
}
destroy() { destroy() {
this.off(`subscription:${this.telemetryObjectIdAsString}`, this.handleSubscription);
this.emitEvent('criterionRemoved');
delete this.telemetryObjectIdAsString;
delete this.telemetryObject; delete this.telemetryObject;
delete this.telemetryObjectIdAsString;
} }
} }

View File

@ -54,7 +54,7 @@ describe("The telemetry criterion", function () {
key: "testSource", key: "testSource",
source: "value", source: "value",
name: "Test", name: "Test",
format: "enum" format: "string"
}] }]
} }
}; };
@ -80,8 +80,9 @@ describe("The telemetry criterion", function () {
testCriterionDefinition = { testCriterionDefinition = {
id: 'test-criterion-id', id: 'test-criterion-id',
telemetry: openmct.objects.makeKeyString(testTelemetryObject.identifier), telemetry: openmct.objects.makeKeyString(testTelemetryObject.identifier),
operation: 'lessThan', operation: 'textContains',
metadata: 'sin', metadata: 'value',
input: ['Hell'],
telemetryObject: testTelemetryObject telemetryObject: testTelemetryObject
}; };
@ -100,12 +101,21 @@ describe("The telemetry criterion", function () {
expect(telemetryCriterion.telemetryObjectIdAsString).toEqual(testTelemetryObject.identifier.key); expect(telemetryCriterion.telemetryObjectIdAsString).toEqual(testTelemetryObject.identifier.key);
}); });
it("updates and emits event on new data from telemetry providers", function () { it("returns a result on new data from relevant telemetry providers", function () {
spyOn(telemetryCriterion, 'emitEvent').and.callThrough(); telemetryCriterion.getResult({
telemetryCriterion.handleSubscription({
value: 'Hello', value: 'Hello',
utc: 'Hi' utc: 'Hi',
id: testTelemetryObject.identifier.key
}); });
expect(telemetryCriterion.emitEvent).toHaveBeenCalled(); expect(telemetryCriterion.result).toBeTrue();
}); });
// it("does not return a result on new data from irrelavant telemetry providers", function () {
// telemetryCriterion.getResult({
// value: 'Hello',
// utc: 'Hi',
// id: '1234'
// });
// expect(telemetryCriterion.result).toBeFalse();
// });
}); });

View File

@ -19,36 +19,50 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import { TRIGGER } from "./constants";
export const computeCondition = (resultMap, allMustBeTrue) => { export const evaluateResults = (results, trigger) => {
let result = false; if (trigger && trigger === TRIGGER.XOR) {
for (let key in resultMap) { return matchExact(results, 1);
if (resultMap.hasOwnProperty(key)) { } else if (trigger && trigger === TRIGGER.NOT) {
result = resultMap[key]; return matchExact(results, 0);
if (allMustBeTrue && !result) { } else if (trigger && trigger === TRIGGER.ALL) {
//If we want all conditions to be true, then even one negative result should break. return matchAll(results);
break; } else {
} else if (!allMustBeTrue && result) { return matchAny(results);
//If we want at least one condition to be true, then even one positive result should break.
break;
} }
} }
}
return result;
};
//Returns true only if limit number of results are satisfied function matchAll(results) {
export const computeConditionByLimit = (resultMap, limit) => { for (const result of results) {
let trueCount = 0; if (!result) {
for (let key in resultMap) { return false;
if (resultMap.hasOwnProperty(key)) {
if (resultMap[key]) {
trueCount++;
}
if (trueCount > limit) {
break;
} }
} }
return true;
}
function matchAny(results) {
for (const result of results) {
if (result) {
return true;
} }
return trueCount === limit; }
};
return false;
}
function matchExact(results, target) {
let matches = 0;
for (const result of results) {
if (result) {
matches++;
}
if (matches > target) {
return false;
}
}
return matches === target;
}

View File

@ -20,47 +20,185 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import { computeConditionByLimit } from "./evaluator"; import { evaluateResults } from './evaluator';
import { TRIGGER } from './constants';
describe('evaluate results based on trigger', function () { describe('evaluate results', () => {
// const allTrue = [true, true, true, true, true];
// const oneTrue = [false, false, false, false, true];
// const multipleTrue = [false, true, false, true, false];
// const noneTrue = [false, false, false, false, false];
// const allTrueWithUndefined = [true, true, true, undefined, true];
// const oneTrueWithUndefined = [undefined, undefined, undefined, undefined, true];
// const multipleTrueWithUndefined = [true, undefined, true, undefined, true];
// const allUndefined = [undefined, undefined, undefined, undefined, undefined];
// const singleTrue = [true];
// const singleFalse = [false];
// const singleUndefined = [undefined];
// const empty = [];
it('should evaluate to true if trigger is NOT', () => { const tests = [
const results = { {
result: false, name: 'allTrue',
result1: false, values: [true, true, true, true, true],
result2: false any: true,
}; all: true,
const result = computeConditionByLimit(results, 0); not: false,
expect(result).toBeTrue(); xor: false
}, {
name: 'oneTrue',
values: [false, false, false, false, true],
any: true,
all: false,
not: false,
xor: true
}, {
name: 'multipleTrue',
values: [false, true, false, true, false],
any: true,
all: false,
not: false,
xor: false
}, {
name: 'noneTrue',
values: [false, false, false, false, false],
any: false,
all: false,
not: true,
xor: false
}, {
name: 'allTrueWithUndefined',
values: [true, true, true, undefined, true],
any: true,
all: false,
not: false,
xor: false
}, {
name: 'oneTrueWithUndefined',
values: [undefined, undefined, undefined, undefined, true],
any: true,
all: false,
not: false,
xor: true
}, {
name: 'multipleTrueWithUndefined',
values: [true, undefined, true, undefined, true],
any: true,
all: false,
not: false,
xor: false
}, {
name: 'allUndefined',
values: [undefined, undefined, undefined, undefined, undefined],
any: false,
all: false,
not: true,
xor: false
}, {
name: 'singleTrue',
values: [true],
any: true,
all: true,
not: false,
xor: true
}, {
name: 'singleFalse',
values: [false],
any: false,
all: false,
not: true,
xor: false
}, {
name: 'singleUndefined',
values: [undefined],
any: false,
all: false,
not: true,
xor: false
}
// , {
// name: 'empty',
// values: [],
// any: false,
// all: false,
// not: true,
// xor: false
// }
];
describe(`based on trigger ${TRIGGER.ANY}`, () => {
it('should evaluate to expected result', () => {
tests.forEach(test => {
const result = evaluateResults(test.values, TRIGGER.ANY);
expect(result).toEqual(test[TRIGGER.ANY])
});
});
}); });
it('should evaluate to false if trigger is NOT', () => { describe(`based on trigger ${TRIGGER.ALL}`, () => {
const results = { it('should evaluate to expected result', () => {
result: true, tests.forEach(test => {
result1: false, const result = evaluateResults(test.values, TRIGGER.ALL);
result2: false expect(result).toEqual(test[TRIGGER.ALL])
}; });
const result = computeConditionByLimit(results, 0); });
expect(result).toBeFalse();
}); });
it('should evaluate to true if trigger is XOR', () => { describe(`based on trigger ${TRIGGER.NOT}`, () => {
const results = { it('should evaluate to expected result', () => {
result: false, tests.forEach(test => {
result1: true, const result = evaluateResults(test.values, TRIGGER.NOT);
result2: false expect(result).toEqual(test[TRIGGER.NOT])
}; });
const result = computeConditionByLimit(results, 1); });
expect(result).toBeTrue();
}); });
it('should evaluate to false if trigger is XOR', () => { describe(`based on trigger ${TRIGGER.XOR}`, () => {
const results = { it('should evaluate to expected result', () => {
result: false, tests.forEach(test => {
result1: true, const result = evaluateResults(test.values, TRIGGER.XOR);
result2: true expect(result).toEqual(test[TRIGGER.XOR])
};
const result = computeConditionByLimit(results, 1);
expect(result).toBeFalse();
}); });
});
});
// it('should evaluate to true if trigger is NOT', () => {
// const results = {
// result: false,
// result1: false,
// result2: false
// };
// const result = computeConditionByLimit(results, 0);
// expect(result).toBeTrue();
// });
// it('should evaluate to false if trigger is NOT', () => {
// const results = {
// result: true,
// result1: false,
// result2: false
// };
// const result = computeConditionByLimit(results, 0);
// expect(result).toBeFalse();
// });
// it('should evaluate to true if trigger is XOR', () => {
// const results = {
// result: false,
// result1: true,
// result2: false
// };
// const result = computeConditionByLimit(results, 1);
// expect(result).toBeTrue();
// });
// it('should evaluate to false if trigger is XOR', () => {
// const results = {
// result: false,
// result1: true,
// result2: true
// };
// const result = computeConditionByLimit(results, 1);
// expect(result).toBeFalse();
// });
}); });

View File

@ -22,6 +22,18 @@
import _ from 'lodash'; import _ from 'lodash';
const convertToNumbers = (input) => {
let numberInputs = [];
input.forEach(inputValue => numberInputs.push(Number(inputValue)));
return numberInputs;
};
const convertToStrings = (input) => {
let stringInputs = [];
input.forEach(inputValue => stringInputs.push(inputValue !== undefined ? inputValue.toString() : ''));
return stringInputs;
};
export const OPERATIONS = [ export const OPERATIONS = [
{ {
name: 'equalTo', name: 'equalTo',
@ -98,8 +110,7 @@ export const OPERATIONS = [
{ {
name: 'between', name: 'between',
operation: function (input) { operation: function (input) {
let numberInputs = []; let numberInputs = convertToNumbers(input);
input.forEach(inputValue => numberInputs.push(Number(inputValue)));
let larger = Math.max(...numberInputs.slice(1,3)); let larger = Math.max(...numberInputs.slice(1,3));
let smaller = Math.min(...numberInputs.slice(1,3)); let smaller = Math.min(...numberInputs.slice(1,3));
return (numberInputs[0] > smaller) && (numberInputs[0] < larger); return (numberInputs[0] > smaller) && (numberInputs[0] < larger);
@ -114,8 +125,7 @@ export const OPERATIONS = [
{ {
name: 'notBetween', name: 'notBetween',
operation: function (input) { operation: function (input) {
let numberInputs = []; let numberInputs = convertToNumbers(input);
input.forEach(inputValue => numberInputs.push(Number(inputValue)));
let larger = Math.max(...numberInputs.slice(1,3)); let larger = Math.max(...numberInputs.slice(1,3));
let smaller = Math.min(...numberInputs.slice(1,3)); let smaller = Math.min(...numberInputs.slice(1,3));
return (numberInputs[0] < smaller) || (numberInputs[0] > larger); return (numberInputs[0] < smaller) || (numberInputs[0] > larger);
@ -214,7 +224,8 @@ export const OPERATIONS = [
{ {
name: 'enumValueIs', name: 'enumValueIs',
operation: function (input) { operation: function (input) {
return input[0] === input[1]; let stringInputs = convertToStrings(input);
return stringInputs[0] === stringInputs[1];
}, },
text: 'is', text: 'is',
appliesTo: ['enum'], appliesTo: ['enum'],
@ -226,7 +237,8 @@ export const OPERATIONS = [
{ {
name: 'enumValueIsNot', name: 'enumValueIsNot',
operation: function (input) { operation: function (input) {
return input[0] !== input[1]; let stringInputs = convertToStrings(input);
return stringInputs[0] !== stringInputs[1];
}, },
text: 'is not', text: 'is not',
appliesTo: ['enum'], appliesTo: ['enum'],
@ -238,9 +250,10 @@ export const OPERATIONS = [
{ {
name: 'valueIs', name: 'valueIs',
operation: function (input) { operation: function (input) {
const lhsValue = input[0] !== undefined ? input[0].toString() : '';
if (input[1]) { if (input[1]) {
const values = input[1].split(','); const values = input[1].split(',');
return values.find((value) => input[0].toString() === _.trim(value.toString())); return values.find((value) => lhsValue === _.trim(value.toString()));
} }
return false; return false;
}, },
@ -254,9 +267,10 @@ export const OPERATIONS = [
{ {
name: 'valueIsNot', name: 'valueIsNot',
operation: function (input) { operation: function (input) {
const lhsValue = input[0] !== undefined ? input[0].toString() : '';
if (input[1]) { if (input[1]) {
const values = input[1].split(','); const values = input[1].split(',');
const found = values.find((value) => input[0].toString() === _.trim(value.toString())); const found = values.find((value) => lhsValue === _.trim(value.toString()));
return !found; return !found;
} }
return false; return false;

View File

@ -25,8 +25,10 @@ let isOneOfOperation = OPERATIONS.find((operation) => operation.name === 'valueI
let isNotOneOfOperation = OPERATIONS.find((operation) => operation.name === 'valueIsNot'); let isNotOneOfOperation = OPERATIONS.find((operation) => operation.name === 'valueIsNot');
let isBetween = OPERATIONS.find((operation) => operation.name === 'between'); let isBetween = OPERATIONS.find((operation) => operation.name === 'between');
let isNotBetween = OPERATIONS.find((operation) => operation.name === 'notBetween'); let isNotBetween = OPERATIONS.find((operation) => operation.name === 'notBetween');
let enumIsOperation = OPERATIONS.find((operation) => operation.name === 'enumValueIs');
let enumIsNotOperation = OPERATIONS.find((operation) => operation.name === 'enumValueIsNot');
describe('Is one of and is not one of operations', function () { describe('operations', function () {
it('should evaluate isOneOf to true for number inputs', () => { it('should evaluate isOneOf to true for number inputs', () => {
const inputs = [45, "5,6,45,8"]; const inputs = [45, "5,6,45,8"];
@ -87,4 +89,54 @@ describe('Is one of and is not one of operations', function () {
const inputs = ["45", "30", "50"]; const inputs = ["45", "30", "50"];
expect(!!isNotBetween.operation(inputs)).toBeFalse(); expect(!!isNotBetween.operation(inputs)).toBeFalse();
}); });
it('should evaluate enumValueIs to true for number inputs', () => {
const inputs = [1, "1"];
expect(!!enumIsOperation.operation(inputs)).toBeTrue();
});
it('should evaluate enumValueIs to true for string inputs', () => {
const inputs = ["45", "45"];
expect(!!enumIsOperation.operation(inputs)).toBeTrue();
});
it('should evaluate enumValueIsNot to true for number inputs', () => {
const inputs = [45, "46"];
expect(!!enumIsNotOperation.operation(inputs)).toBeTrue();
});
it('should evaluate enumValueIsNot to true for string inputs', () => {
const inputs = ["45", "46"];
expect(!!enumIsNotOperation.operation(inputs)).toBeTrue();
});
it('should evaluate enumValueIs to false for number inputs', () => {
const inputs = [1, "2"];
expect(!!enumIsOperation.operation(inputs)).toBeFalse();
});
it('should evaluate enumValueIs to false for string inputs', () => {
const inputs = ["45", "46"];
expect(!!enumIsOperation.operation(inputs)).toBeFalse();
});
it('should evaluate enumValueIsNot to false for number inputs', () => {
const inputs = [45, "45"];
expect(!!enumIsNotOperation.operation(inputs)).toBeFalse();
});
it('should evaluate enumValueIsNot to false for string inputs', () => {
const inputs = ["45", "45"];
expect(!!enumIsNotOperation.operation(inputs)).toBeFalse();
});
it('should evaluate enumValueIs to false for undefined input', () => {
const inputs = [undefined, "45"];
expect(!!enumIsOperation.operation(inputs)).toBeFalse();
});
it('should evaluate enumValueIsNot to true for undefined input', () => {
const inputs = [undefined, "45"];
expect(!!enumIsNotOperation.operation(inputs)).toBeTrue();
});
}); });

View File

@ -19,27 +19,154 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
const NONE_VALUE = '__no_value';
export const getStyleProp = (key, defaultValue) => { const styleProps = {
let styleProp = undefined; backgroundColor: {
switch(key) { svgProperty: 'fill',
case 'fill': styleProp = { noneValue: NONE_VALUE,
backgroundColor: defaultValue || 'transparent' applicableForType: type => {
}; return !type ? true : (type === 'text-view' ||
break; type === 'telemetry-view' ||
case 'stroke': styleProp = { type === 'box-view' ||
border: '1px solid ' + (defaultValue || 'transparent') type === 'subobject-view');
}; }
break; },
case 'color': styleProp = { border: {
color: defaultValue || 'transparent' svgProperty: 'stroke',
}; noneValue: NONE_VALUE,
break; applicableForType: type => {
case 'url': styleProp = { return !type ? true : (type === 'text-view' ||
imageUrl: defaultValue || 'transparent' type === 'telemetry-view' ||
}; type === 'box-view' ||
break; type === 'image-view' ||
type === 'line-view'||
type === 'subobject-view');
}
},
color: {
svgProperty: 'color',
noneValue: NONE_VALUE,
applicableForType: type => {
return !type ? true : (type === 'text-view' ||
type === 'telemetry-view'||
type === 'subobject-view');
}
},
imageUrl: {
svgProperty: 'url',
noneValue: '',
applicableForType: type => {
return !type ? false : type === 'image-view';
}
} }
};
return styleProp;
const aggregateStyleValues = (accumulator, currentStyle) => {
const styleKeys = Object.keys(currentStyle);
const properties = Object.keys(styleProps);
properties.forEach((property) => {
if (!accumulator[property]) {
accumulator[property] = [];
}
const found = styleKeys.find(key => key === property);
if (found) {
accumulator[property].push(currentStyle[found]);
}
});
return accumulator;
};
// Returns a union of styles used by multiple items.
// Styles that are common to all items but don't have the same value are added to the mixedStyles list
export const getConsolidatedStyleValues = (multipleItemStyles) => {
let aggregatedStyleValues = multipleItemStyles.reduce(aggregateStyleValues, {});
let styleValues = {};
let mixedStyles = [];
const properties = Object.keys(styleProps);
properties.forEach((property) => {
const values = aggregatedStyleValues[property];
if (values.length) {
if (values.every(value => value === values[0])) {
styleValues[property] = values[0];
} else {
styleValues[property] = '';
mixedStyles.push(property);
}
}
});
return {
styles: styleValues,
mixedStyles
};
};
const getStaticStyleForItem = (domainObject, id) => {
let domainObjectStyles = domainObject && domainObject.configuration && domainObject.configuration.objectStyles;
if (domainObjectStyles) {
if (id) {
if(domainObjectStyles[id] && domainObjectStyles[id].staticStyle) {
return domainObjectStyles[id].staticStyle.style;
}
} else if (domainObjectStyles.staticStyle) {
return domainObjectStyles.staticStyle.style;
}
}
};
export const getConditionalStyleForItem = (domainObject, id) => {
let domainObjectStyles = domainObject && domainObject.configuration && domainObject.configuration.objectStyles;
if (domainObjectStyles) {
if (id) {
if (domainObjectStyles[id] && domainObjectStyles[id].conditionSetIdentifier) {
return domainObjectStyles[id].styles;
}
} else if (domainObjectStyles.staticStyle) {
return domainObjectStyles.styles;
}
}
};
//Returns either existing static styles or uses SVG defaults if available
export const getApplicableStylesForItem = (domainObject, item) => {
const type = item && item.type;
const id = item && item.id;
let style = {};
let staticStyle = getStaticStyleForItem(domainObject, id);
const properties = Object.keys(styleProps);
properties.forEach(property => {
const styleProp = styleProps[property];
if (styleProp.applicableForType(type)) {
let defaultValue;
if (staticStyle) {
defaultValue = staticStyle[property];
} else if (item) {
defaultValue = item[styleProp.svgProperty];
}
style[property] = defaultValue === undefined ? styleProp.noneValue : defaultValue;
}
});
return style;
};
export const getStylesWithoutNoneValue = (style) => {
if (_.isEmpty(style) || !style) {
return;
}
let styleObj = {};
const keys = Object.keys(style);
keys.forEach(key => {
if ((typeof style[key] === 'string')) {
if (style[key].indexOf('__no_value') > -1) {
style[key] = '';
} else {
styleObj[key] = style[key];
}
}
});
return styleObj;
}; };

View File

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

View File

@ -21,13 +21,14 @@
*****************************************************************************/ *****************************************************************************/
<template> <template>
<a class="c-condition-widget" <component :is="urlDefined ? 'a' : 'span'"
:href="internalDomainObject.url" class="c-condition-widget"
:href="urlDefined ? internalDomainObject.url : null"
> >
<div class="c-condition-widget__label"> <div class="c-condition-widget__label">
{{ internalDomainObject.label }} {{ internalDomainObject.label }}
</div> </div>
</a> </component>
</template> </template>
<script> <script>
@ -38,6 +39,11 @@ export default {
internalDomainObject: this.domainObject internalDomainObject: this.domainObject
} }
}, },
computed: {
urlDefined() {
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
}
},
mounted() { mounted() {
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject); this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
}, },

View File

@ -28,10 +28,12 @@
border: 1px solid transparent; border: 1px solid transparent;
display: inline-block; display: inline-block;
padding: $interiorMarginLg $interiorMarginLg * 2; padding: $interiorMarginLg $interiorMarginLg * 2;
cursor: inherit !important; }
&[href] {
a.c-condition-widget {
// Widget is conditionally made into a <a> when URL property has been defined
cursor: pointer !important; cursor: pointer !important;
} pointer-events: inherit;
} }
// Make Condition Widget expand when in a hidden frame Layout context // Make Condition Widget expand when in a hidden frame Layout context

View File

@ -43,7 +43,7 @@ export default {
makeDefinition() { makeDefinition() {
return { return {
fill: '#717171', fill: '#717171',
stroke: 'transparent', stroke: '',
x: 1, x: 1,
y: 1, y: 1,
width: 10, width: 10,
@ -74,13 +74,14 @@ export default {
}, },
computed: { computed: {
style() { style() {
return Object.assign({ if (this.itemStyle) {
return this.itemStyle;
} else {
return {
backgroundColor: this.item.fill, backgroundColor: this.item.fill,
border: '1px solid ' + this.item.stroke border: this.item.stroke ? '1px solid ' + this.item.stroke : ''
}, this.itemStyle); };
}, }
styleClass() {
return this.itemStyle && this.itemStyle.isStyleInvisible;
} }
}, },
watch: { watch: {

View File

@ -74,13 +74,18 @@ export default {
}, },
computed: { computed: {
style() { style() {
let backgroundImage = 'url(' + this.item.url + ')';
let border = '1px solid ' + this.item.stroke;
if (this.itemStyle) {
if (this.itemStyle.imageUrl !== undefined) {
backgroundImage = 'url(' + this.itemStyle.imageUrl + ')';
}
border = this.itemStyle.border;
}
return { return {
backgroundImage: this.itemStyle ? ('url(' + this.itemStyle.imageUrl + ')') : 'url(' + this.item.url + ')', backgroundImage,
border: (this.itemStyle && this.itemStyle.border) ? this.itemStyle.border : ('1px solid ' + this.item.stroke) border
}; };
},
styleClass() {
return this.itemStyle && this.itemStyle.isStyleInvisible;
} }
}, },
watch: { watch: {

View File

@ -127,8 +127,11 @@ export default {
return {x, y, x2, y2}; return {x, y, x2, y2};
}, },
stroke() { stroke() {
if (this.itemStyle && this.itemStyle.border) { if (this.itemStyle) {
if (this.itemStyle.border) {
return this.itemStyle.border.replace('1px solid ', ''); return this.itemStyle.border.replace('1px solid ', '');
}
return '';
} else { } else {
return this.item.stroke; return this.item.stroke;
} }
@ -146,9 +149,6 @@ export default {
height: `${height}px` height: `${height}px`
}; };
}, },
styleClass() {
return this.itemStyle && this.itemStyle.isStyleInvisible;
},
startHandleClass() { startHandleClass() {
return START_HANDLE_QUADRANTS[this.vectorQuadrant]; return START_HANDLE_QUADRANTS[this.vectorQuadrant];
}, },

View File

@ -30,14 +30,13 @@
<div <div
v-if="domainObject" v-if="domainObject"
class="c-telemetry-view" class="c-telemetry-view"
:class="styleClass"
:style="styleObject" :style="styleObject"
@contextmenu.prevent="showContextMenu" @contextmenu.prevent="showContextMenu"
> >
<div <div
v-if="showLabel" v-if="showLabel"
class="c-telemetry-view__label" class="c-telemetry-view__label"
:class="[styleClass]"
:style="objectStyle"
> >
<div class="c-telemetry-view__label-text"> <div class="c-telemetry-view__label-text">
{{ domainObject.name }} {{ domainObject.name }}
@ -48,8 +47,7 @@
v-if="showValue" v-if="showValue"
:title="fieldName" :title="fieldName"
class="c-telemetry-view__value" class="c-telemetry-view__value"
:class="[telemetryClass, !telemetryClass && styleClass]" :class="[telemetryClass]"
:style="!telemetryClass && objectStyle"
> >
<div class="c-telemetry-view__value-text"> <div class="c-telemetry-view__value-text">
{{ telemetryValue }} {{ telemetryValue }}
@ -62,7 +60,7 @@
<script> <script>
import LayoutFrame from './LayoutFrame.vue' import LayoutFrame from './LayoutFrame.vue'
import printj from 'printj' import printj from 'printj'
import StyleRuleManager from "../../condition/StyleRuleManager"; import conditionalStylesMixin from "../mixins/objectStyles-mixin";
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5], const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5],
DEFAULT_POSITION = [1, 1], DEFAULT_POSITION = [1, 1],
@ -81,8 +79,8 @@ export default {
height: DEFAULT_TELEMETRY_DIMENSIONS[1], height: DEFAULT_TELEMETRY_DIMENSIONS[1],
displayMode: 'all', displayMode: 'all',
value: metadata.getDefaultDisplayValue(), value: metadata.getDefaultDisplayValue(),
stroke: "transparent", stroke: "",
fill: "transparent", fill: "",
color: "", color: "",
size: "13px" size: "13px"
}; };
@ -91,6 +89,7 @@ export default {
components: { components: {
LayoutFrame LayoutFrame
}, },
mixins: [conditionalStylesMixin],
props: { props: {
item: { item: {
type: Object, type: Object,
@ -113,8 +112,7 @@ export default {
datum: undefined, datum: undefined,
formats: undefined, formats: undefined,
domainObject: undefined, domainObject: undefined,
currentObjectPath: undefined, currentObjectPath: undefined
objectStyle: ''
} }
}, },
computed: { computed: {
@ -127,15 +125,10 @@ export default {
return displayMode === 'all' || displayMode === 'value'; return displayMode === 'all' || displayMode === 'value';
}, },
styleObject() { styleObject() {
return { return Object.assign({}, {
backgroundColor: this.item.fill,
borderColor: this.item.stroke,
color: this.item.color,
fontSize: this.item.size fontSize: this.item.size
} }, this.itemStyle);
},
styleClass() {
return this.objectStyle && this.objectStyle.isStyleInvisible;
}, },
fieldName() { fieldName() {
return this.valueMetadata && this.valueMetadata.name; return this.valueMetadata && this.valueMetadata.name;
@ -190,15 +183,6 @@ export default {
this.removeSelectable(); this.removeSelectable();
} }
if (this.unlistenStyles) {
this.unlistenStyles();
}
if (this.styleRuleManager) {
this.styleRuleManager.destroy();
delete this.styleRuleManager;
}
this.openmct.time.off("bounds", this.refreshData); this.openmct.time.off("bounds", this.refreshData);
}, },
methods: { methods: {
@ -241,7 +225,6 @@ export default {
}, },
setObject(domainObject) { setObject(domainObject) {
this.domainObject = domainObject; this.domainObject = domainObject;
this.initObjectStyles();
this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier); this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject); this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);
@ -266,30 +249,6 @@ export default {
}, },
showContextMenu(event) { showContextMenu(event) {
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS); this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
},
initObjectStyles() {
if (this.domainObject.configuration) {
this.styleRuleManager = new StyleRuleManager(this.domainObject.configuration.objectStyles, this.openmct, this.updateStyle.bind(this));
if (this.unlistenStyles) {
this.unlistenStyles();
}
this.unlistenStyles = this.openmct.objects.observe(this.domainObject, 'configuration.objectStyles', (newObjectStyle) => {
//Updating object styles in the inspector view will trigger this so that the changes are reflected immediately
this.styleRuleManager.updateObjectStyleConfig(newObjectStyle);
});
}
},
updateStyle(styleObj) {
let keys = Object.keys(styleObj);
keys.forEach(key => {
if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('transparent') > -1)) {
if (styleObj[key]) {
styleObj[key] = '';
}
}
});
this.objectStyle = styleObj;
} }
} }
} }

View File

@ -32,7 +32,7 @@
:class="[styleClass]" :class="[styleClass]"
:style="style" :style="style"
> >
{{ item.text }} <div class="c-text-view__text">{{ item.text }}</div>
</div> </div>
</layout-frame> </layout-frame>
</template> </template>
@ -44,8 +44,8 @@ import conditionalStylesMixin from "../mixins/objectStyles-mixin";
export default { export default {
makeDefinition(openmct, gridSize, element) { makeDefinition(openmct, gridSize, element) {
return { return {
fill: 'transparent', fill: '',
stroke: 'transparent', stroke: '',
size: '13px', size: '13px',
color: '', color: '',
x: 1, x: 1,
@ -80,14 +80,8 @@ export default {
computed: { computed: {
style() { style() {
return Object.assign({ return Object.assign({
backgroundColor: this.item.fill,
border: '1px solid ' + this.item.stroke,
color: this.item.color,
fontSize: this.item.size fontSize: this.item.size
}, this.itemStyle); }, this.itemStyle);
},
styleClass() {
return this.itemStyle && this.itemStyle.isStyleInvisible;
} }
}, },
watch: { watch: {

View File

@ -16,6 +16,8 @@
.is-editing { .is-editing {
/******************* STYLES FOR C-FRAME WHILE EDITING */ /******************* STYLES FOR C-FRAME WHILE EDITING */
.c-frame { .c-frame {
border: 1px solid rgba($editFrameColorHov, 0.3);
&:not([s-selected]) { &:not([s-selected]) {
&:hover { &:hover {
border: $editFrameBorderHov; border: $editFrameBorderHov;

View File

@ -1,6 +1,8 @@
.c-text-view { .c-text-view {
display: flex; display: flex;
align-items: stretch; align-items: center; // Vertically center text
overflow: hidden;
padding: $interiorMargin;
.c-frame & { .c-frame & {
@include abs(); @include abs();

View File

@ -21,18 +21,20 @@
*****************************************************************************/ *****************************************************************************/
import StyleRuleManager from "@/plugins/condition/StyleRuleManager"; import StyleRuleManager from "@/plugins/condition/StyleRuleManager";
import {getStylesWithoutNoneValue} from "@/plugins/condition/utils/styleUtils";
export default { export default {
inject: ['openmct'], inject: ['openmct'],
data() { data() {
return { return {
itemStyle: this.itemStyle itemStyle: undefined,
styleClass: ''
} }
}, },
mounted() { mounted() {
this.domainObject = this.$parent.domainObject; this.parentDomainObject = this.$parent.domainObject;
this.itemId = this.item.id; this.itemId = this.item.id;
this.objectStyle = this.getObjectStyleForItem(this.domainObject.configuration.objectStyles); this.objectStyle = this.getObjectStyleForItem(this.parentDomainObject.configuration.objectStyles);
this.initObjectStyles(); this.initObjectStyles();
}, },
destroyed() { destroyed() {
@ -59,7 +61,7 @@ export default {
this.stopListeningObjectStyles(); this.stopListeningObjectStyles();
} }
this.stopListeningObjectStyles = this.openmct.objects.observe(this.domainObject, 'configuration.objectStyles', (newObjectStyle) => { this.stopListeningObjectStyles = this.openmct.objects.observe(this.parentDomainObject, 'configuration.objectStyles', (newObjectStyle) => {
//Updating object styles in the inspector view will trigger this so that the changes are reflected immediately //Updating object styles in the inspector view will trigger this so that the changes are reflected immediately
let newItemObjectStyle = this.getObjectStyleForItem(newObjectStyle); let newItemObjectStyle = this.getObjectStyleForItem(newObjectStyle);
if (this.objectStyle !== newItemObjectStyle) { if (this.objectStyle !== newItemObjectStyle) {
@ -69,13 +71,8 @@ export default {
}); });
}, },
updateStyle(style) { updateStyle(style) {
this.itemStyle = style; this.itemStyle = getStylesWithoutNoneValue(style);
let keys = Object.keys(this.itemStyle); this.styleClass = this.itemStyle && this.itemStyle.isStyleInvisible;
keys.forEach((key) => {
if ((typeof this.itemStyle[key] === 'string') && (this.itemStyle[key].indexOf('transparent') > -1)) {
delete this.itemStyle[key];
}
});
} }
} }
}; };

View File

@ -0,0 +1,21 @@
<template>
<div class="c-menu">
<ul>
<li
v-for="(item, index) in popupMenuItems"
:key="index"
:class="item.cssClass"
:title="item.name"
@click="item.callback"
>
{{ item.name }}
</li>
</ul>
</div>
</template>
<script>
export default {
inject: ['popupMenuItems']
}
</script>

View File

@ -12,24 +12,7 @@
:class="embed.cssClass" :class="embed.cssClass"
@click="changeLocation" @click="changeLocation"
>{{ embed.name }}</a> >{{ embed.name }}</a>
<a class="c-ne__embed__context-available icon-arrow-down" <PopupMenu :popup-menu-items="popupMenuItems" />
@click="toggleActionMenu"
></a>
</div>
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform(embed)"
>
{{ action.name }}
</li>
</ul>
</div>
</div>
</div> </div>
<div v-if="embed.snapshot" <div v-if="embed.snapshot"
class="c-ne__embed__time" class="c-ne__embed__time"
@ -42,15 +25,17 @@
<script> <script>
import Moment from 'moment'; import Moment from 'moment';
import PopupMenu from './popup-menu.vue';
import PreviewAction from '../../../ui/preview/PreviewAction'; import PreviewAction from '../../../ui/preview/PreviewAction';
import Painterro from 'painterro'; import Painterro from 'painterro';
import RemoveDialog from '../utils/removeDialog';
import SnapshotTemplate from './snapshot-template.html'; import SnapshotTemplate from './snapshot-template.html';
import { togglePopupMenu } from '../utils/popup-menu';
import Vue from 'vue'; import Vue from 'vue';
export default { export default {
inject: ['openmct'], inject: ['openmct'],
components: { components: {
PopupMenu
}, },
props: { props: {
embed: { embed: {
@ -62,23 +47,35 @@ export default {
removeActionString: { removeActionString: {
type: String, type: String,
default() { default() {
return 'Remove Embed'; return 'Remove This Embed';
} }
} }
}, },
data() { data() {
return { return {
actions: [this.removeEmbedAction()], popupMenuItems: []
agentService: this.openmct.$injector.get('agentService'),
popupService: this.openmct.$injector.get('popupService')
} }
}, },
watch: { watch: {
}, },
beforeMount() { mounted() {
this.populateActionMenu(); this.addPopupMenuItems();
}, },
methods: { methods: {
addPopupMenuItems() {
const removeEmbed = {
cssClass: 'icon-trash',
name: this.removeActionString,
callback: this.getRemoveDialog.bind(this)
}
const preview = {
cssClass: 'icon-eye-open',
name: 'Preview',
callback: this.previewEmbed.bind(this)
}
this.popupMenuItems = [removeEmbed, preview];
},
annotateSnapshot() { annotateSnapshot() {
const self = this; const self = this;
@ -183,6 +180,14 @@ export default {
formatTime(unixTime, timeFormat) { formatTime(unixTime, timeFormat) {
return Moment.utc(unixTime).format(timeFormat); return Moment.utc(unixTime).format(timeFormat);
}, },
getRemoveDialog() {
const options = {
name: this.removeActionString,
callback: this.removeEmbed.bind(this)
}
const removeDialog = new RemoveDialog(this.openmct, options);
removeDialog.show();
},
openSnapshot() { openSnapshot() {
const self = this; const self = this;
const snapshot = new Vue({ const snapshot = new Vue({
@ -214,53 +219,17 @@ export default {
] ]
}); });
}, },
populateActionMenu() { previewEmbed() {
const self = this; const self = this;
const actions = [new PreviewAction(self.openmct)]; const previewAction = new PreviewAction(self.openmct);
previewAction.invoke(JSON.parse(self.embed.objectPath));
},
removeEmbed(success) {
if (!success) {
return;
}
actions.forEach((action) => { this.$emit('removeEmbed', this.embed.id);
self.actions.push({
cssClass: action.cssClass,
name: action.name,
perform: () => {
action.invoke(JSON.parse(self.embed.objectPath));
}
});
});
},
removeEmbed(id) {
this.$emit('removeEmbed', id);
},
removeEmbedAction() {
const self = this;
return {
name: self.removeActionString,
cssClass: 'icon-trash',
perform: function (embed) {
const dialog = self.openmct.overlays.dialog({
iconClass: "error",
message: `This action will permanently ${self.removeActionString.toLowerCase()}. Do you wish to continue?`,
buttons: [{
label: "No",
callback: function () {
dialog.dismiss();
}
},
{
label: "Yes",
emphasis: true,
callback: function () {
dialog.dismiss();
self.removeEmbed(embed.id);
}
}]
});
}
};
},
toggleActionMenu(event) {
togglePopupMenu(event, this.openmct);
}, },
updateEmbed(embed) { updateEmbed(embed) {
this.$emit('updateEmbed', embed); this.$emit('updateEmbed', embed);

View File

@ -1,8 +1,8 @@
<template> <template>
<div class="l-browse-bar__view-switcher c-ctrl-wrapper c-ctrl-wrapper--menus-left"> <div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button <button
class="c-button--menu icon-notebook" class="c-button--menu icon-notebook"
title="Switch view type" title="Take a Notebook Snapshot"
@click="setNotebookTypes" @click="setNotebookTypes"
@click.stop="toggleMenu" @click.stop="toggleMenu"
> >

View File

@ -10,24 +10,9 @@
>&nbsp;{{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }} >&nbsp;{{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }}
</span> </span>
</div> </div>
<a class="l-browse-bar__context-actions c-disclosure-button" <PopupMenu v-if="snapshots.length > 0"
@click="toggleActionMenu" :popup-menu-items="popupMenuItems"
></a> />
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform()"
>
{{ action.name }}
</li>
</ul>
</div>
</div>
</div>
</div> </div>
</div> </div>
@ -62,14 +47,16 @@
<script> <script>
import NotebookEmbed from './notebook-embed.vue'; import NotebookEmbed from './notebook-embed.vue';
import PopupMenu from './popup-menu.vue';
import RemoveDialog from '../utils/removeDialog';
import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container'; import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container';
import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants'; import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants';
import { togglePopupMenu } from '../utils/popup-menu';
export default { export default {
inject: ['openmct', 'snapshotContainer'], inject: ['openmct', 'snapshotContainer'],
components: { components: {
NotebookEmbed NotebookEmbed,
PopupMenu
}, },
props: { props: {
toggleSnapshot: { toggleSnapshot: {
@ -81,54 +68,47 @@ export default {
}, },
data() { data() {
return { return {
actions: [this.removeAllSnapshotAction()], popupMenuItems: [],
removeActionString: 'Delete all snapshots',
snapshots: [] snapshots: []
} }
}, },
mounted() { mounted() {
this.addPopupMenuItems();
this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated); this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated);
this.snapshots = this.snapshotContainer.getSnapshots(); this.snapshots = this.snapshotContainer.getSnapshots();
}, },
beforeDestory() { beforeDestory() {
}, },
methods: { methods: {
addPopupMenuItems() {
const removeSnapshot = {
cssClass: 'icon-trash',
name: this.removeActionString,
callback: this.getRemoveDialog.bind(this)
}
this.popupMenuItems = [removeSnapshot];
},
close() { close() {
this.toggleSnapshot(); this.toggleSnapshot();
}, },
getNotebookSnapshotMaxCount() { getNotebookSnapshotMaxCount() {
return NOTEBOOK_SNAPSHOT_MAX_COUNT; return NOTEBOOK_SNAPSHOT_MAX_COUNT;
}, },
removeAllSnapshotAction() { getRemoveDialog() {
const self = this; const options = {
name: this.removeActionString,
callback: this.removeAllSnapshots.bind(this)
}
const removeDialog = new RemoveDialog(this.openmct, options);
removeDialog.show();
},
removeAllSnapshots(success) {
if (!success) {
return;
}
return {
name: 'Delete All Snapshots',
cssClass: 'icon-trash',
perform: function (embed) {
const dialog = self.openmct.overlays.dialog({
iconClass: "error",
message: 'This action will delete all notebook snapshots. Do you want to continue?',
buttons: [
{
label: "No",
callback: () => {
dialog.dismiss();
}
},
{
label: "Yes",
emphasis: true,
callback: () => {
self.removeAllSnapshots();
dialog.dismiss();
}
}
]
});
}
};
},
removeAllSnapshots() {
this.snapshotContainer.removeAllSnapshots(); this.snapshotContainer.removeAllSnapshots();
}, },
removeSnapshot(id) { removeSnapshot(id) {
@ -141,9 +121,6 @@ export default {
event.dataTransfer.setData('text/plain', snapshot.id); event.dataTransfer.setData('text/plain', snapshot.id);
event.dataTransfer.setData('snapshot/id', snapshot.id); event.dataTransfer.setData('snapshot/id', snapshot.id);
}, },
toggleActionMenu(event) {
togglePopupMenu(event, this.openmct);
},
updateSnapshot(snapshot) { updateSnapshot(snapshot) {
this.snapshotContainer.updateSnapshot(snapshot); this.snapshotContainer.updateSnapshot(snapshot);
} }

View File

@ -49,13 +49,19 @@
class="c-notebook__controls__time" class="c-notebook__controls__time"
> >
<option value="0" <option value="0"
selected="selected" :selected="showTime === 0"
> >
Show all Show all
</option> </option>
<option value="1">Last hour</option> <option value="1"
<option value="8">Last 8 hours</option> :selected="showTime === 1"
<option value="24">Last 24 hours</option> >Last hour</option>
<option value="8"
:selected="showTime === 8"
>Last 8 hours</option>
<option value="24"
:selected="showTime === 24"
>Last 24 hours</option>
</select> </select>
<select v-model="defaultSort" <select v-model="defaultSort"
class="c-notebook__controls__time" class="c-notebook__controls__time"
@ -132,9 +138,17 @@ export default {
}, },
computed: { computed: {
filteredAndSortedEntries() { filteredAndSortedEntries() {
const filterTime = Date.now();
const pageEntries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage) || []; const pageEntries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage) || [];
return pageEntries.sort(this.sortEntries); const hours = parseInt(this.showTime, 10);
const filteredPageEntriesByTime = hours
? pageEntries.filter(entry => (filterTime - entry.createdOn) <= hours * 60 * 60 * 1000)
: pageEntries;
return this.defaultSort === 'oldest'
? filteredPageEntriesByTime
: [...filteredPageEntriesByTime].reverse();
}, },
pages() { pages() {
return this.getPages() || []; return this.getPages() || [];
@ -420,11 +434,6 @@ export default {
searchItem(input) { searchItem(input) {
this.search = input; this.search = input;
}, },
sortEntries(right, left) {
return this.defaultSort === 'newest'
? left.createdOn - right.createdOn
: right.createdOn - left.createdOn;
},
toggleNav() { toggleNav() {
this.showNav = !this.showNav; this.showNav = !this.showNav;
}, },

View File

@ -9,32 +9,19 @@
@keydown.enter="updateName" @keydown.enter="updateName"
@blur="updateName" @blur="updateName"
>{{ page.name.length ? page.name : `Unnamed ${pageTitle}` }}</span> >{{ page.name.length ? page.name : `Unnamed ${pageTitle}` }}</span>
<a class="c-list__item__menu-indicator icon-arrow-down" <PopupMenu :popup-menu-items="popupMenuItems" />
@click="toggleActionMenu"
></a>
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform(page.id)"
>
{{ action.name }}
</li>
</ul>
</div>
</div>
</div>
</div> </div>
</template> </template>
<script> <script>
import { togglePopupMenu } from '../utils/popup-menu'; import PopupMenu from './popup-menu.vue';
import RemoveDialog from '../utils/removeDialog';
export default { export default {
inject: ['openmct'], inject: ['openmct'],
components: {
PopupMenu
},
props: { props: {
defaultPageId: { defaultPageId: {
type: String, type: String,
@ -55,7 +42,8 @@ export default {
}, },
data() { data() {
return { return {
actions: [this.deletePage()] popupMenuItems: [],
removeActionString: `Delete ${this.pageTitle}`
} }
}, },
watch: { watch: {
@ -64,40 +52,37 @@ export default {
} }
}, },
mounted() { mounted() {
this.addPopupMenuItems();
this.toggleContentEditable(); this.toggleContentEditable();
}, },
destroyed() { destroyed() {
}, },
methods: { methods: {
deletePage() { addPopupMenuItems() {
const self = this; const removePage = {
return {
name: `Delete ${this.pageTitle}`,
cssClass: 'icon-trash', cssClass: 'icon-trash',
perform: function (id) { name: this.removeActionString,
const dialog = self.openmct.overlays.dialog({ callback: this.getRemoveDialog.bind(this)
iconClass: "error",
message: 'This action will delete this page and all of its entries. Do you want to continue?',
buttons: [
{
label: "No",
callback: () => {
dialog.dismiss();
} }
this.popupMenuItems = [removePage];
}, },
{ deletePage(success) {
label: "Yes", if (!success) {
emphasis: true, return;
callback: () => {
self.$emit('deletePage', id);
dialog.dismiss();
} }
this.$emit('deletePage', this.page.id);
},
getRemoveDialog() {
const message = 'This action will delete this page and all of its entries. Do you want to continue?';
const options = {
name: this.removeActionString,
callback: this.deletePage.bind(this),
message
} }
] const removeDialog = new RemoveDialog(this.openmct, options);
}); removeDialog.show();
}
};
}, },
selectPage(event) { selectPage(event) {
const target = event.target; const target = event.target;
@ -117,10 +102,6 @@ export default {
this.$emit('selectPage', id); this.$emit('selectPage', id);
}, },
toggleActionMenu(event) {
event.preventDefault();
togglePopupMenu(event, this.openmct);
},
toggleContentEditable(page = this.page) { toggleContentEditable(page = this.page) {
const pageTitle = this.$el.querySelector('span'); const pageTitle = this.$el.querySelector('span');
pageTitle.contentEditable = page.isSelected; pageTitle.contentEditable = page.isSelected;

View File

@ -0,0 +1,93 @@
<template>
<button
class="c-popup-menu-button c-disclosure-button"
title="popup menu"
@click="showMenuItems"
>
</button>
</template>
<script>
import MenuItems from './menu-items.vue';
import Vue from 'vue';
export default {
inject: ['openmct'],
props: {
domainObject: {
type: Object,
default() {
return {};
}
},
popupMenuItems: {
type: Array,
default() {
return [];
}
}
},
data() {
return {
menuItems: null
}
},
mounted() {
},
methods: {
calculateMenuPosition(event, element) {
let eventPosX = event.clientX;
let eventPosY = event.clientY;
let menuDimensions = element.getBoundingClientRect();
let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth;
let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight;
if (overflowX > 0) {
eventPosX = eventPosX - overflowX;
}
if (overflowY > 0) {
eventPosY = eventPosY - overflowY;
}
return {
x: eventPosX,
y: eventPosY
}
},
hideMenuItems() {
document.body.removeChild(this.menuItems.$el);
this.menuItems.$destroy();
this.menuItems = null;
document.removeEventListener('click', this.hideMenuItems);
return;
},
showMenuItems($event) {
const menuItems = new Vue({
components: {
MenuItems
},
provide: {
popupMenuItems: this.popupMenuItems
},
template: '<MenuItems />'
});
this.menuItems = menuItems;
menuItems.$mount();
const element = this.menuItems.$el;
document.body.appendChild(element);
const position = this.calculateMenuPosition($event, element);
element.style.left = `${position.x}px`;
element.style.top = `${position.y}px`;
setTimeout(() => {
document.addEventListener('click', this.hideMenuItems);
}, 0);
}
}
}
</script>

View File

@ -9,24 +9,7 @@
@keydown.enter="updateName" @keydown.enter="updateName"
@blur="updateName" @blur="updateName"
>{{ section.name.length ? section.name : `Unnamed ${sectionTitle}` }}</span> >{{ section.name.length ? section.name : `Unnamed ${sectionTitle}` }}</span>
<a class="c-list__item__menu-indicator icon-arrow-down" <PopupMenu :popup-menu-items="popupMenuItems" />
@click="toggleActionMenu"
></a>
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform(section.id)"
>
{{ action.name }}
</li>
</ul>
</div>
</div>
</div>
</div> </div>
</template> </template>
@ -34,10 +17,14 @@
</style> </style>
<script> <script>
import { togglePopupMenu } from '../utils/popup-menu'; import PopupMenu from './popup-menu.vue';
import RemoveDialog from '../utils/removeDialog';
export default { export default {
inject: ['openmct'], inject: ['openmct'],
components: {
PopupMenu
},
props: { props: {
defaultSectionId: { defaultSectionId: {
type: String, type: String,
@ -58,7 +45,8 @@ export default {
}, },
data() { data() {
return { return {
actions: [this.deleteSectionAction()] popupMenuItems: [],
removeActionString: `Delete ${this.sectionTitle}`
} }
}, },
watch: { watch: {
@ -67,40 +55,38 @@ export default {
} }
}, },
mounted() { mounted() {
this.addPopupMenuItems();
this.toggleContentEditable(); this.toggleContentEditable();
}, },
destroyed() { destroyed() {
}, },
methods: { methods: {
deleteSectionAction() { addPopupMenuItems() {
const self = this; const removeSection = {
return {
name: `Delete ${this.sectionTitle}`,
cssClass: 'icon-trash', cssClass: 'icon-trash',
perform: function (id) { name: this.removeActionString,
const dialog = self.openmct.overlays.dialog({ callback: this.getRemoveDialog.bind(this)
iconClass: "error",
message: 'This action will delete this section and all of its pages and entries. Do you want to continue?',
buttons: [
{
label: "No",
callback: () => {
dialog.dismiss();
} }
this.popupMenuItems = [removeSection];
}, },
{ deleteSection(success) {
label: "Yes", if (!success) {
emphasis: true, return;
callback: () => {
self.$emit('deleteSection', id);
dialog.dismiss();
} }
this.$emit('deleteSection', this.section.id);
},
getRemoveDialog() {
const message = 'This action will delete this section and all of its pages and entries. Do you want to continue?';
const options = {
name: this.removeActionString,
callback: this.deleteSection.bind(this),
message
} }
]
}); const removeDialog = new RemoveDialog(this.openmct, options);
} removeDialog.show();
};
}, },
selectSection(event) { selectSection(event) {
const target = event.target; const target = event.target;
@ -121,9 +107,6 @@ export default {
this.$emit('selectSection', id); this.$emit('selectSection', id);
}, },
toggleActionMenu(event) {
togglePopupMenu(event, this.openmct);
},
toggleContentEditable(section = this.section) { toggleContentEditable(section = this.section) {
const sectionTitle = this.$el.querySelector('span'); const sectionTitle = this.$el.querySelector('span');
sectionTitle.contentEditable = section.isSelected; sectionTitle.contentEditable = section.isSelected;

View File

@ -1,45 +0,0 @@
import $ from 'zepto';
export const togglePopupMenu = (event, openmct) => {
event.preventDefault();
const body = $(document.body);
const container = $(event.target.parentElement.parentElement);
const classList = document.querySelector('body').classList;
const isPhone = Array.from(classList).includes('phone');
const isTablet = Array.from(classList).includes('tablet');
const initiatingEvent = isPhone || isTablet
? 'touchstart'
: 'mousedown';
const menu = container.find('.menu-element');
let dismissExistingMenu;
function dismiss() {
container.find('.hide-menu').append(menu);
body.off(initiatingEvent, menuClickHandler);
dismissExistingMenu = undefined;
}
function menuClickHandler(e) {
window.setTimeout(() => {
dismiss();
}, 100);
}
// Dismiss any menu which was already showing
if (dismissExistingMenu) {
dismissExistingMenu();
}
// ...and record the presence of this menu.
dismissExistingMenu = dismiss;
const popupService = openmct.$injector.get('popupService');
popupService.display(menu, [event.pageX,event.pageY], {
marginX: 0,
marginY: -50
});
body.on(initiatingEvent, menuClickHandler);
}

View File

@ -0,0 +1,36 @@
export default class RemoveDialog {
constructor(openmct, options) {
this.name = options.name;
this.openmct = openmct;
this.callback = options.callback;
this.cssClass = options.cssClass || 'icon-trash';
this.description = options.description || 'Remove action dialog';
this.iconClass = "error";
this.key = 'remove';
this.message = options.message || `This action will permanently ${this.name.toLowerCase()}. Do you wish to continue?`;
}
show() {
const dialog = this.openmct.overlays.dialog({
iconClass: this.iconClass,
message: this.message,
buttons: [
{
label: "Ok",
callback: () => {
this.callback(true);
dialog.dismiss();
}
},
{
label: "Cancel",
callback: () => {
this.callback(false);
dialog.dismiss();
}
}
]
});
}
}

View File

@ -13,7 +13,6 @@
@import "~styles/controls"; @import "~styles/controls";
@import "~styles/forms"; @import "~styles/forms";
@import "~styles/table"; @import "~styles/table";
@import "~styles/layout";
@import "~styles/legacy"; @import "~styles/legacy";
@import "~styles/legacy-plots"; @import "~styles/legacy-plots";
@import "~styles/plotly"; @import "~styles/plotly";

View File

@ -13,7 +13,6 @@
@import "~styles/controls"; @import "~styles/controls";
@import "~styles/forms"; @import "~styles/forms";
@import "~styles/table"; @import "~styles/table";
@import "~styles/layout";
@import "~styles/legacy"; @import "~styles/legacy";
@import "~styles/legacy-plots"; @import "~styles/legacy-plots";
@import "~styles/plotly"; @import "~styles/plotly";

View File

@ -13,7 +13,6 @@
@import "~styles/controls"; @import "~styles/controls";
@import "~styles/forms"; @import "~styles/forms";
@import "~styles/table"; @import "~styles/table";
@import "~styles/layout";
@import "~styles/legacy"; @import "~styles/legacy";
@import "~styles/legacy-plots"; @import "~styles/legacy-plots";
@import "~styles/plotly"; @import "~styles/plotly";

View File

@ -122,13 +122,8 @@ button {
margin-left: $interiorMargin; margin-left: $interiorMargin;
} }
$c1: nth($mixedSettingBg, 1);
$c2: nth($mixedSettingBg, 2);
$mixedBgD: $mixedSettingBgSize $mixedSettingBgSize;
&--mixed { &--mixed {
// E.g. click-icons in toolbar that apply to multiple selected items with different settings @include mixedBg();
@include bgStripes2Color($c1, $c2, $bgSize: $mixedBgD);
} }
&--swatched { &--swatched {
@ -151,13 +146,6 @@ button {
flex: 1 1 auto; flex: 1 1 auto;
font-size: 1.1em; font-size: 1.1em;
} }
&--mixed {
// Styling for swatched buttons when settings are mixed
> [class*='swatch'] {
@include bgStripes2Color($c1, $c2, $bgSize: $mixedBgD);
}
}
} }
} }
@ -244,18 +232,10 @@ button {
/******************************************************** SECTION */ /******************************************************** SECTION */
section { section {
flex: 0 0 auto; flex: 0 1 auto;
overflow: hidden; overflow: hidden;
+ section { + section {
margin-top: $interiorMargin; margin-top: $interiorMargin;
&.is-expanded {
margin-bottom: $interiorMargin * 3;
}
}
&.is-expanded {
flex: 0 1 auto;
} }
.c-section__header { .c-section__header {
@ -829,6 +809,10 @@ select {
box-shadow: rgba($colorBodyFg, 0.4) 0 0 3px; box-shadow: rgba($colorBodyFg, 0.4) 0 0 3px;
flex: 0 0 auto; flex: 0 0 auto;
padding: $interiorMargin $interiorMarginLg; padding: $interiorMargin $interiorMarginLg;
&--mixed {
@include mixedBg();
}
} }
/******************************************************** SLIDERS */ /******************************************************** SLIDERS */

View File

@ -88,6 +88,12 @@ body.desktop {
background: $scrollbarThumbColorMenuHov; background: $scrollbarThumbColorMenuHov;
} }
} }
div, span {
// Firefox
scrollbar-color: $scrollbarThumbColor $scrollbarTrackColorBg;
scrollbar-width: thin;
}
} }
/******************************************************** HTML ENTITIES */ /******************************************************** HTML ENTITIES */

View File

@ -1,87 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, 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.
*****************************************************************************/
/************************** BROWSE BAR */
.l-browse-bar {
display: flex;
align-items: center;
justify-content: space-between;
[class*="__"] {
// Removes extraneous horizontal white space
display: inline-flex;
}
&__start {
display: flex;
align-items: center;
flex: 1 1 auto;
margin-right: $interiorMargin;
min-width: 0; // Forces interior to compress when pushed on
}
&__end {
display: flex;
align-items: center;
flex: 0 0 auto;
[class*="__"] + [class*="__"] {
margin-left: $interiorMarginSm;
}
}
&__nav-to-parent-button,
&__disclosure-button {
flex: 0 0 auto;
}
&__nav-to-parent-button {
// This is an icon-button
$p: $interiorMargin;
margin-right: $interiorMargin;
padding-left: $p;
padding-right: $p;
.is-editing & {
display: none;
}
}
&__object-name--w {
flex: 0 1 auto;
@include headerFont(1.4em);
min-width: 0;
&:before {
// Icon
margin-right: $interiorMargin;
}
}
&__object-name {
flex: 0 1 auto;
}
&__object-details {
opacity: 0.5;
}
}

View File

@ -72,6 +72,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 1.25em; font-size: 1.25em;
overflow: hidden;
> * { > * {
flex: 0 0 auto; flex: 0 0 auto;
@ -82,6 +83,12 @@
&__value { &__value {
color: $colorBodyFgEm; color: $colorBodyFgEm;
} }
.c-frame & {
// When in a Display or Flexible Layout
@include abs();
padding: $interiorMargin;
}
} }
.c-clock { .c-clock {

View File

@ -50,6 +50,14 @@
} }
/************************** EFFECTS */ /************************** EFFECTS */
@mixin mixedBg() {
$c1: nth($mixedSettingBg, 1);
$c2: nth($mixedSettingBg, 2);
$mixedBgD: $mixedSettingBgSize $mixedSettingBgSize;
@include bgStripes2Color($c1, $c2, $bgSize: $mixedBgD);
}
@mixin pulse($animName: pulse, $dur: 500ms, $iteration: infinite, $opacity0: 0.5, $opacity100: 1) { @mixin pulse($animName: pulse, $dur: 500ms, $iteration: infinite, $opacity0: 0.5, $opacity100: 1) {
@keyframes #{$animName} { @keyframes #{$animName} {
0% { opacity: $opacity0; } 0% { opacity: $opacity0; }

View File

@ -1,7 +1,6 @@
@import "../api/overlays/components/dialog-component.scss"; @import "../api/overlays/components/dialog-component.scss";
@import "../api/overlays/components/overlay-component.scss"; @import "../api/overlays/components/overlay-component.scss";
@import "../plugins/condition/components/condition.scss"; @import "../plugins/condition/components/conditionals.scss";
@import "../plugins/condition/components/condition-set.scss";
@import "../plugins/conditionWidget/components/condition-widget.scss"; @import "../plugins/conditionWidget/components/condition-widget.scss";
@import "../plugins/condition/components/inspector/conditional-styles.scss"; @import "../plugins/condition/components/inspector/conditional-styles.scss";
@import "../plugins/displayLayout/components/box-view.scss"; @import "../plugins/displayLayout/components/box-view.scss";

View File

@ -1,6 +1,8 @@
import MCT from 'MCT'; import MCT from 'MCT';
export function createOpenMct() { export function createOpenMct() {
delete require.cache[require.resolve('openmct')];
const openmct = new MCT(); const openmct = new MCT();
openmct.install(openmct.plugins.LocalStorage()); openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.UTCTimeSystem()); openmct.install(openmct.plugins.UTCTimeSystem());

View File

@ -104,7 +104,7 @@ export default {
keys.forEach(key => { keys.forEach(key => {
let firstChild = this.$el.querySelector(':first-child'); let firstChild = this.$el.querySelector(':first-child');
if (firstChild) { if (firstChild) {
if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('transparent') > -1)) { if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('__no_value') > -1)) {
if (firstChild.style[key]) { if (firstChild.style[key]) {
firstChild.style[key] = ''; firstChild.style[key] = '';
} }

View File

@ -81,7 +81,7 @@ export default {
} }
}, },
mounted() { mounted() {
this.excludeObjectTypes = ['folder', 'webPage', 'conditionSet']; this.excludeObjectTypes = ['folder', 'webPage', 'conditionSet', 'summary-widget', 'hyperlink'];
this.openmct.selection.on('change', this.updateInspectorViews); this.openmct.selection.on('change', this.updateInspectorViews);
}, },
destroyed() { destroyed() {

View File

@ -26,8 +26,8 @@
<script> <script>
import ConditionalStylesView from '../../plugins/condition/components/inspector/ConditionalStylesView.vue'; import ConditionalStylesView from '../../plugins/condition/components/inspector/ConditionalStylesView.vue';
import MultiSelectStylesView from '../../plugins/condition/components/inspector/MultiSelectStylesView.vue';
import Vue from 'vue'; import Vue from 'vue';
import { getStyleProp } from "../../plugins/condition/utils/styleUtils";
export default { export default {
inject: ['openmct'], inject: ['openmct'],
@ -44,35 +44,9 @@ export default {
this.openmct.selection.off('change', this.updateSelection); this.openmct.selection.off('change', this.updateSelection);
}, },
methods: { methods: {
getStyleProperties(item) {
let styleProps = {};
Object.keys(item).forEach((key) => {
Object.assign(styleProps, getStyleProp(key, item[key]));
});
return styleProps;
},
updateSelection(selection) { updateSelection(selection) {
if (selection.length > 0 && selection[0].length > 0) { if (selection.length > 0 && selection[0].length > 0) {
let isChildItem = false; let template = selection.length > 1 ? '<multi-select-styles-view></multi-select-styles-view>' : '<conditional-styles-view></conditional-styles-view>';
let domainObject = selection[0][0].context.item;
let layoutItem = {};
let styleProps = this.getStyleProperties({
fill: 'transparent',
stroke: 'transparent',
color: 'transparent'
});
if (selection[0].length > 1) {
isChildItem = true;
//If there are more than 1 items in the selection[0] list, the first one could either be a sub domain object OR a layout drawing control.
//The second item in the selection[0] list is the container object (usually a layout)
if (!domainObject) {
styleProps = {};
layoutItem = selection[0][0].context.layoutItem;
styleProps = this.getStyleProperties(layoutItem);
domainObject = selection[0][1].context.item;
}
}
if (this.component) { if (this.component) {
this.component.$destroy(); this.component.$destroy();
this.component = undefined; this.component = undefined;
@ -83,20 +57,14 @@ export default {
this.component = new Vue({ this.component = new Vue({
provide: { provide: {
openmct: this.openmct, openmct: this.openmct,
domainObject: domainObject selection: selection
}, },
el: viewContainer, el: viewContainer,
components: { components: {
ConditionalStylesView ConditionalStylesView,
MultiSelectStylesView
}, },
data() { template: template
return {
layoutItem,
styleProps,
isChildItem
}
},
template: '<conditional-styles-view :can-hide="isChildItem" :item-id="layoutItem.id" :initial-styles="styleProps"></conditional-styles-view>'
}); });
} }
} }

View File

@ -28,6 +28,7 @@
<div class="l-browse-bar__end"> <div class="l-browse-bar__end">
<view-switcher <view-switcher
v-if="!isEditing"
:current-view="currentView" :current-view="currentView"
:views="views" :views="views"
@setView="setView" @setView="setView"

View File

@ -6,7 +6,7 @@
<button <button
class="c-button--menu" class="c-button--menu"
:class="currentView.cssClass" :class="currentView.cssClass"
title="Switch view type" title="Change the current view"
@click.stop="toggleViewMenu" @click.stop="toggleViewMenu"
> >
<span class="c-button__label"> <span class="c-button__label">

View File

@ -1,3 +1,24 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/******************************* SHELL */ /******************************* SHELL */
.l-shell { .l-shell {
position: absolute; position: absolute;
@ -126,6 +147,9 @@
body.mobile & .l-shell__main-view-browse-bar { body.mobile & .l-shell__main-view-browse-bar {
margin-left: $mobileMenuIconD; // Make room for the hamburger! margin-left: $mobileMenuIconD; // Make room for the hamburger!
.c-button[class*='__actions__edit'] {
display: none; // Hide the main view edit button when in mobile context
}
} }
&__head { &__head {
@ -269,6 +293,79 @@
} }
} }
/************************** BROWSE BAR */
.l-browse-bar {
display: flex;
align-items: center;
justify-content: space-between;
[class*="__"] {
// Removes extraneous horizontal white space
display: inline-flex;
}
&__start,
&__end,
&__actions {
display: flex;
align-items: center;
}
&__actions,
&__end {
> * + * {
margin-left: $interiorMarginSm;
}
}
&__start {
flex: 1 1 auto;
margin-right: $interiorMargin;
min-width: 0; // Forces interior to compress when pushed on
}
&__end {
flex: 0 0 auto;
}
&__nav-to-parent-button,
&__disclosure-button {
flex: 0 0 auto;
}
&__nav-to-parent-button {
// This is an icon-button
$p: $interiorMargin;
margin-right: $interiorMargin;
padding-left: $p;
padding-right: $p;
.is-editing & {
display: none;
}
}
&__object-name--w,
&__object-name {
flex: 0 1 auto;
}
&__object-name--w {
@include headerFont(1.4em);
min-width: 0;
&:before {
// Icon
margin-right: $interiorMargin;
}
}
&__object-details {
opacity: 0.5;
}
}
/************************** DRAWER */
.c-drawer { .c-drawer {
/* New sliding overlay or push element to contain things /* New sliding overlay or push element to contain things
* Designed for mobile and compact desktop scenarios * Designed for mobile and compact desktop scenarios
@ -332,4 +429,3 @@
} }
} }
} }