[Staleness] API and Component Functionality (#6108)

* initial telemetry api updates for staleness support

* modifying staleness to a subsription style

* fixing variable name

* debuggin

* put the subscribe in the wrong place

* stale class for object views

* temp cyan border for testing

* added staleness to swg, working on stacked plot staleness

* WIP: stacked plot staleness

* reverting, going a different route

* staleness on stacked plots

* plot legend staleness

* remove debug code

* staleness for alphanumerics

* lad table and table set staleness

* overlay plot staleness

* remove debug code

* hardened lad staleness functionality fixed plots without composition bug

* adding staleness to gauges

* renaming telemetry age check functionality so it does not conflict with new staleness functionality

* couple one-off fixes here and there, and WIP for condition sets, moving to telemetry tables to facilitate styling of completed components

* small fix on lad tables, added staleness functionality to tables

* finishing up condition sets

* some cleaning up

* adding border to condition sets when an item is stale

* fixing dub sub

* addressing PR comment, moving repeated code to a function

* robustified the SWG stalenes provider, little fixes here and there as far as cleaning up listeners and... whatnot

* removing debug code

* typo fixes

* cleanin up debug code

* created a simple stalenes mixin for more basic usage in components

* more robustification, if a new staleness subscription happens, will now send the current staleness value if we have it, beefed up example stalenes swg provider

* beefed up staleness mixin a bit to give it more use

* copyright

* cleanin up ladtable code

* cleanin up ladtable code

* cleaning up lad table sets

* some minor updates

* Closes #6109
- New staleness glyph and font CSS added.

* Closes #6109
- Normalized staleness colors as theme constants.
- New mixins for staleness application to view elements.
- Applied staleness styling to all relevant view elements.
- TODO: smoke-test in Show theme.

* adding staleness utils helper, mixin and isStale functionalirty for telemtry api

* Closes #6109
- Refined style for Snow theme.

* need to have one domainObject per stalenes utility

* making sure we handle domains correctly while dealing with staleness

* couple fixes

* moving abort controller logic to a spot where it makes more sense

* added some more info for the StalenesProvider interface docs

* returning undefinded for ifStale requests with no provider

* debuggin

* debuggin

* missed "isStale" call in condtioncollections

* removing debug code and using mixin unsubscribe in gauge

* fixing tests

* more targeted tree item click

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Scott Bell <scott@traclabs.com>
This commit is contained in:
Jamie V 2023-01-23 10:27:04 -08:00 committed by GitHub
parent 1b71a3bf33
commit 8847c862fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 1538 additions and 455 deletions

View File

@ -197,11 +197,16 @@ test.describe('Tagging in Notebooks @addInit', () => {
page.goto('./#/browse/mine?hideTree=false'),
page.click('.c-disclosure-triangle')
]);
// Click Clock
await page.click(`text=${clock.name}`);
const treePane = page.locator('#tree-pane');
// Click Clock
await treePane.getByRole('treeitem', {
name: clock.name
}).click();
// Click Notebook
await page.click(`text=${notebook.name}`);
await page.getByRole('treeitem', {
name: notebook.name
}).click();
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;

View File

@ -32,7 +32,7 @@ test.use({
}
});
test.describe('ExportAsJSON', () => {
test.fixme('ExportAsJSON', () => {
test('User can set autoscale with a valid range @snapshot', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;

View File

@ -37,8 +37,9 @@ define([
infinityValues: false
};
function GeneratorProvider(openmct) {
this.workerInterface = new WorkerInterface(openmct);
function GeneratorProvider(openmct, StalenessProvider) {
this.openmct = openmct;
this.workerInterface = new WorkerInterface(openmct, StalenessProvider);
}
GeneratorProvider.prototype.canProvideTelemetry = function (domainObject) {
@ -81,6 +82,7 @@ define([
workerRequest[prop] = Number(workerRequest[prop]);
});
workerRequest.id = this.openmct.objects.makeKeyString(domainObject.identifier);
workerRequest.name = domainObject.name;
return workerRequest;

View File

@ -0,0 +1,151 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
export default class SinewaveLimitProvider extends EventEmitter {
constructor(openmct) {
super();
this.openmct = openmct;
this.observingStaleness = {};
this.watchingTheClock = false;
this.isRealTime = undefined;
}
supportsStaleness(domainObject) {
return domainObject.type === 'generator';
}
isStale(domainObject, options) {
if (!this.providingStaleness(domainObject)) {
return Promise.resolve({
isStale: false,
utc: 0
});
}
const id = this.getObjectKeyString(domainObject);
if (!this.observerExists(id)) {
this.createObserver(id);
}
return Promise.resolve(this.observingStaleness[id].isStale);
}
subscribeToStaleness(domainObject, callback) {
const id = this.getObjectKeyString(domainObject);
if (this.isRealTime === undefined) {
this.updateRealTime(this.openmct.time.clock());
}
this.handleClockUpdate();
if (this.observerExists(id)) {
this.addCallbackToObserver(id, callback);
} else {
this.createObserver(id, callback);
}
const intervalId = setInterval(() => {
if (this.providingStaleness(domainObject)) {
this.updateStaleness(id, !this.observingStaleness[id].isStale);
}
}, 10000);
return () => {
clearInterval(intervalId);
this.updateStaleness(id, false);
this.handleClockUpdate();
this.destroyObserver(id);
};
}
handleClockUpdate() {
let observers = Object.values(this.observingStaleness).length > 0;
if (observers && !this.watchingTheClock) {
this.watchingTheClock = true;
this.openmct.time.on('clock', this.updateRealTime, this);
} else if (!observers && this.watchingTheClock) {
this.watchingTheClock = false;
this.openmct.time.off('clock', this.updateRealTime, this);
}
}
updateRealTime(clock) {
this.isRealTime = clock !== undefined;
if (!this.isRealTime) {
Object.keys(this.observingStaleness).forEach((id) => {
this.updateStaleness(id, false);
});
}
}
updateStaleness(id, isStale) {
this.observingStaleness[id].isStale = isStale;
this.observingStaleness[id].utc = Date.now();
this.observingStaleness[id].callback({
isStale: this.observingStaleness[id].isStale,
utc: this.observingStaleness[id].utc
});
this.emit('stalenessEvent', {
id,
isStale: this.observingStaleness[id].isStale
});
}
createObserver(id, callback) {
this.observingStaleness[id] = {
isStale: false,
utc: Date.now()
};
if (typeof callback === 'function') {
this.addCallbackToObserver(id, callback);
}
}
destroyObserver(id) {
delete this.observingStaleness[id];
}
providingStaleness(domainObject) {
return domainObject.telemetry?.staleness === true && this.isRealTime;
}
getObjectKeyString(object) {
return this.openmct.objects.makeKeyString(object.identifier);
}
addCallbackToObserver(id, callback) {
this.observingStaleness[id].callback = callback;
}
observerExists(id) {
return this.observingStaleness?.[id];
}
}

View File

@ -25,14 +25,24 @@ define([
], function (
{ v4: uuid }
) {
function WorkerInterface(openmct) {
function WorkerInterface(openmct, StalenessProvider) {
// eslint-disable-next-line no-undef
const workerUrl = `${openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}generatorWorker.js`;
this.StalenessProvider = StalenessProvider;
this.worker = new Worker(workerUrl);
this.worker.onmessage = this.onMessage.bind(this);
this.callbacks = {};
this.staleTelemetryIds = {};
this.watchStaleness();
}
WorkerInterface.prototype.watchStaleness = function () {
this.StalenessProvider.on('stalenessEvent', ({ id, isStale}) => {
this.staleTelemetryIds[id] = isStale;
});
};
WorkerInterface.prototype.onMessage = function (message) {
message = message.data;
var callback = this.callbacks[message.id];
@ -83,11 +93,12 @@ define([
};
WorkerInterface.prototype.subscribe = function (request, cb) {
function callback(message) {
cb(message.data);
}
var messageId = this.dispatch('subscribe', request, callback);
const id = request.id;
const messageId = this.dispatch('subscribe', request, (message) => {
if (!this.staleTelemetryIds[id]) {
cb(message.data);
}
});
return function () {
this.dispatch('unsubscribe', {

View File

@ -20,158 +20,163 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
"./GeneratorProvider",
"./SinewaveLimitProvider",
"./StateGeneratorProvider",
"./GeneratorMetadataProvider"
], function (
GeneratorProvider,
SinewaveLimitProvider,
StateGeneratorProvider,
GeneratorMetadataProvider
) {
import GeneratorProvider from "./GeneratorProvider";
import SinewaveLimitProvider from "./SinewaveLimitProvider";
import SinewaveStalenessProvider from "./SinewaveStalenessProvider";
import StateGeneratorProvider from "./StateGeneratorProvider";
import GeneratorMetadataProvider from "./GeneratorMetadataProvider";
return function (openmct) {
export default function (openmct) {
openmct.types.addType("example.state-generator", {
name: "State Generator",
description: "For development use. Generates example enumerated telemetry by cycling through a given set of states.",
cssClass: "icon-generator-telemetry",
creatable: true,
form: [
{
name: "State Duration (seconds)",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "duration",
required: true,
property: [
"telemetry",
"duration"
]
}
],
initialize: function (object) {
object.telemetry = {
duration: 5
};
openmct.types.addType("example.state-generator", {
name: "State Generator",
description: "For development use. Generates example enumerated telemetry by cycling through a given set of states.",
cssClass: "icon-generator-telemetry",
creatable: true,
form: [
{
name: "State Duration (seconds)",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "duration",
required: true,
property: [
"telemetry",
"duration"
]
}
});
],
initialize: function (object) {
object.telemetry = {
duration: 5
};
}
});
openmct.telemetry.addProvider(new StateGeneratorProvider());
openmct.telemetry.addProvider(new StateGeneratorProvider());
openmct.types.addType("generator", {
name: "Sine Wave Generator",
description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",
cssClass: "icon-generator-telemetry",
creatable: true,
form: [
{
name: "Period",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "period",
required: true,
property: [
"telemetry",
"period"
]
},
{
name: "Amplitude",
control: "numberfield",
cssClass: "l-numeric",
key: "amplitude",
required: true,
property: [
"telemetry",
"amplitude"
]
},
{
name: "Offset",
control: "numberfield",
cssClass: "l-numeric",
key: "offset",
required: true,
property: [
"telemetry",
"offset"
]
},
{
name: "Data Rate (hz)",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "dataRateInHz",
required: true,
property: [
"telemetry",
"dataRateInHz"
]
},
{
name: "Phase (radians)",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "phase",
required: true,
property: [
"telemetry",
"phase"
]
},
{
name: "Randomness",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "randomness",
required: true,
property: [
"telemetry",
"randomness"
]
},
{
name: "Loading Delay (ms)",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "loadDelay",
required: true,
property: [
"telemetry",
"loadDelay"
]
},
{
name: "Include Infinity Values",
control: "toggleSwitch",
cssClass: "l-input",
key: "infinityValues",
property: [
"telemetry",
"infinityValues"
]
}
],
initialize: function (object) {
object.telemetry = {
period: 10,
amplitude: 1,
offset: 0,
dataRateInHz: 1,
phase: 0,
randomness: 0,
loadDelay: 0,
infinityValues: false
};
openmct.types.addType("generator", {
name: "Sine Wave Generator",
description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",
cssClass: "icon-generator-telemetry",
creatable: true,
form: [
{
name: "Period",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "period",
required: true,
property: [
"telemetry",
"period"
]
},
{
name: "Amplitude",
control: "numberfield",
cssClass: "l-numeric",
key: "amplitude",
required: true,
property: [
"telemetry",
"amplitude"
]
},
{
name: "Offset",
control: "numberfield",
cssClass: "l-numeric",
key: "offset",
required: true,
property: [
"telemetry",
"offset"
]
},
{
name: "Data Rate (hz)",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "dataRateInHz",
required: true,
property: [
"telemetry",
"dataRateInHz"
]
},
{
name: "Phase (radians)",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "phase",
required: true,
property: [
"telemetry",
"phase"
]
},
{
name: "Randomness",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "randomness",
required: true,
property: [
"telemetry",
"randomness"
]
},
{
name: "Loading Delay (ms)",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "loadDelay",
required: true,
property: [
"telemetry",
"loadDelay"
]
},
{
name: "Include Infinity Values",
control: "toggleSwitch",
cssClass: "l-input",
key: "infinityValues",
property: [
"telemetry",
"infinityValues"
]
},
{
name: "Provide Staleness Updates",
control: "toggleSwitch",
cssClass: "l-input",
key: "staleness",
property: [
"telemetry",
"staleness"
]
}
});
],
initialize: function (object) {
object.telemetry = {
period: 10,
amplitude: 1,
offset: 0,
dataRateInHz: 1,
phase: 0,
randomness: 0,
loadDelay: 0,
infinityValues: false,
staleness: false
};
}
});
const stalenessProvider = new SinewaveStalenessProvider(openmct);
openmct.telemetry.addProvider(new GeneratorProvider(openmct));
openmct.telemetry.addProvider(new GeneratorMetadataProvider());
openmct.telemetry.addProvider(new SinewaveLimitProvider());
};
});
openmct.telemetry.addProvider(new GeneratorProvider(openmct, stalenessProvider));
openmct.telemetry.addProvider(new GeneratorMetadataProvider());
openmct.telemetry.addProvider(new SinewaveLimitProvider());
openmct.telemetry.addProvider(stalenessProvider);
}

View File

@ -36,6 +36,7 @@ export default class TelemetryAPI {
this.formatMapCache = new WeakMap();
this.formatters = new Map();
this.limitProviders = [];
this.stalenessProviders = [];
this.metadataCache = new WeakMap();
this.metadataProviders = [new DefaultMetadataProvider(this.openmct)];
this.noRequestProviderForAllObjects = false;
@ -114,6 +115,10 @@ export default class TelemetryAPI {
if (provider.supportsLimits) {
this.limitProviders.unshift(provider);
}
if (provider.supportsStaleness) {
this.stalenessProviders.unshift(provider);
}
}
/**
@ -125,7 +130,7 @@ export default class TelemetryAPI {
return provider.supportsSubscribe.apply(provider, args);
}
return this.subscriptionProviders.filter(supportsDomainObject)[0];
return this.subscriptionProviders.find(supportsDomainObject);
}
/**
@ -138,25 +143,25 @@ export default class TelemetryAPI {
return provider.supportsRequest.apply(provider, args);
}
return this.requestProviders.filter(supportsDomainObject)[0];
return this.requestProviders.find(supportsDomainObject);
}
/**
* @private
*/
#findMetadataProvider(domainObject) {
return this.metadataProviders.filter(function (p) {
return p.supportsMetadata(domainObject);
})[0];
return this.metadataProviders.find((provider) => {
return provider.supportsMetadata(domainObject);
});
}
/**
* @private
*/
#findLimitEvaluator(domainObject) {
return this.limitProviders.filter(function (p) {
return p.supportsLimits(domainObject);
})[0];
return this.limitProviders.find((provider) => {
return provider.supportsLimits(domainObject);
});
}
/**
@ -351,6 +356,101 @@ export default class TelemetryAPI {
}.bind(this);
}
/**
* Subscribe to staleness updates for a specific domain object.
* The callback will be called whenever staleness changes.
*
* @method subscribeToStaleness
* @memberof module:openmct.TelemetryAPI~StalenessProvider#
* @param {module:openmct.DomainObject} domainObject the object
* to watch for staleness updates
* @param {Function} callback the callback to invoke with staleness data,
* as it is received: ex.
* {
* isStale: <Boolean>,
* timestamp: <timestamp>
* }
* @returns {Function} a function which may be called to terminate
* the subscription to staleness updates
*/
subscribeToStaleness(domainObject, callback) {
const provider = this.#findStalenessProvider(domainObject);
if (!this.stalenessSubscriberCache) {
this.stalenessSubscriberCache = {};
}
const keyString = objectUtils.makeKeyString(domainObject.identifier);
let stalenessSubscriber = this.stalenessSubscriberCache[keyString];
if (!stalenessSubscriber) {
stalenessSubscriber = this.stalenessSubscriberCache[keyString] = {
callbacks: [callback]
};
if (provider) {
stalenessSubscriber.unsubscribe = provider
.subscribeToStaleness(domainObject, (stalenessResponse) => {
stalenessSubscriber.callbacks.forEach((cb) => {
cb(stalenessResponse);
});
});
} else {
stalenessSubscriber.unsubscribe = () => {};
}
} else {
stalenessSubscriber.callbacks.push(callback);
}
return function unsubscribe() {
stalenessSubscriber.callbacks = stalenessSubscriber.callbacks.filter((cb) => {
return cb !== callback;
});
if (stalenessSubscriber.callbacks.length === 0) {
stalenessSubscriber.unsubscribe();
delete this.stalenessSubscriberCache[keyString];
}
}.bind(this);
}
/**
* Request telemetry staleness for a domain object.
*
* @method isStale
* @memberof module:openmct.TelemetryAPI~StalenessProvider#
* @param {module:openmct.DomainObject} domainObject the object
* which has associated telemetry staleness
* @returns {Promise.<StalenessResponseObject>} a promise for a StalenessResponseObject
* or undefined if no provider exists
*/
async isStale(domainObject) {
const provider = this.#findStalenessProvider(domainObject);
if (!provider) {
return;
}
const abortController = new AbortController();
const options = { signal: abortController.signal };
this.requestAbortControllers.add(abortController);
try {
const staleness = await provider.isStale(domainObject, options);
return staleness;
} finally {
this.requestAbortControllers.delete(abortController);
}
}
/**
* @private
*/
#findStalenessProvider(domainObject) {
return this.stalenessProviders.find((provider) => {
return provider.supportsStaleness(domainObject);
});
}
/**
* Get telemetry metadata for a given domain object. Returns a telemetry
* metadata manager which provides methods for interrogating telemetry
@ -661,6 +761,29 @@ export default class TelemetryAPI {
* @memberof module:openmct.TelemetryAPI~
*/
/**
* Provides telemetry staleness data. To subscribe to telemetry stalenes,
* new StalenessProvider implementations should be
* [registered]{@link module:openmct.TelemetryAPI#addProvider}.
*
* @interface StalenessProvider
* @property {function} supportsStaleness receieves a domainObject and
* returns a boolean to indicate it will provide staleness
* @property {function} subscribeToStaleness receieves a domainObject to
* be subscribed to and a callback to invoke with a StalenessResponseObject
* @property {function} isStale an asynchronous method called with a domainObject
* and an options object which currently has an abort signal, ex.
* { signal: <AbortController.signal> }
* this method should return a current StalenessResponseObject
* @memberof module:openmct.TelemetryAPI~
*/
/**
* @typedef {object} StalenessResponseObject
* @property {Boolean} isStale boolean representing the staleness state
* @property {Number} timestamp Unix timestamp in milliseconds
*/
/**
* An interface for retrieving telemetry data associated with a domain
* object.

View File

@ -29,7 +29,7 @@
<td class="js-second-data">{{ formattedTimestamp }}</td>
<td
class="js-third-data"
:class="valueClass"
:class="valueClasses"
>{{ value }}</td>
<td
v-if="hasUnits"
@ -63,6 +63,12 @@ export default {
hasUnits: {
type: Boolean,
requred: true
},
isStale: {
type: Boolean,
default() {
return false;
}
}
},
data() {
@ -81,14 +87,22 @@ export default {
return this.formats[this.valueKey].format(this.datum);
},
valueClass() {
if (!this.datum) {
return '';
valueClasses() {
let classes = [];
if (this.isStale) {
classes.push('is-stale');
}
const limit = this.limitEvaluator.evaluate(this.datum, this.valueMetadata);
if (this.datum) {
const limit = this.limitEvaluator.evaluate(this.datum, this.valueMetadata);
return limit ? limit.cssClass : '';
if (limit) {
classes.push(limit.cssClass);
}
}
return classes;
},
formattedTimestamp() {

View File

@ -21,7 +21,10 @@
*****************************************************************************/
<template>
<div class="c-lad-table-wrapper u-style-receiver js-style-receiver">
<div
class="c-lad-table-wrapper u-style-receiver js-style-receiver"
:class="staleClass"
>
<table class="c-table c-lad-table">
<thead>
<tr>
@ -38,6 +41,7 @@
:domain-object="ladRow.domainObject"
:path-to-table="objectPath"
:has-units="hasUnits"
:is-stale="staleObjects.includes(ladRow.key)"
@rowContextClick="updateViewContext"
/>
</tbody>
@ -46,7 +50,9 @@
</template>
<script>
import LadRow from './LADRow.vue';
import StalenessUtils from '@/utils/staleness';
export default {
components: {
@ -66,7 +72,8 @@ export default {
data() {
return {
items: [],
viewContext: {}
viewContext: {},
staleObjects: []
};
},
computed: {
@ -80,6 +87,13 @@ export default {
});
return itemsWithUnits.length !== 0;
},
staleClass() {
if (this.staleObjects.length !== 0) {
return 'is-stale';
}
return '';
}
},
mounted() {
@ -88,11 +102,17 @@ export default {
this.composition.on('remove', this.removeItem);
this.composition.on('reorder', this.reorder);
this.composition.load();
this.stalenessSubscription = {};
},
destroyed() {
this.composition.off('add', this.addItem);
this.composition.off('remove', this.removeItem);
this.composition.off('reorder', this.reorder);
Object.values(this.stalenessSubscription).forEach(stalenessSubscription => {
stalenessSubscription.unsubscribe();
stalenessSubscription.stalenessUtils.destroy();
});
},
methods: {
addItem(domainObject) {
@ -101,23 +121,55 @@ export default {
item.key = this.openmct.objects.makeKeyString(domainObject.identifier);
this.items.push(item);
this.stalenessSubscription[item.key] = {};
this.stalenessSubscription[item.key].stalenessUtils = new StalenessUtils(this.openmct, domainObject);
this.openmct.telemetry.isStale(domainObject).then((stalenessResponse) => {
if (stalenessResponse !== undefined) {
this.handleStaleness(item.key, stalenessResponse);
}
});
const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(domainObject, (stalenessResponse) => {
this.handleStaleness(item.key, stalenessResponse);
});
this.stalenessSubscription[item.key].unsubscribe = stalenessSubscription;
},
removeItem(identifier) {
let index = this.items.findIndex(item => this.openmct.objects.makeKeyString(identifier) === item.key);
const SKIP_CHECK = true;
const keystring = this.openmct.objects.makeKeyString(identifier);
const index = this.items.findIndex(item => keystring === item.key);
this.items.splice(index, 1);
this.stalenessSubscription[keystring].unsubscribe();
this.handleStaleness(keystring, { isStale: false }, SKIP_CHECK);
},
reorder(reorderPlan) {
let oldItems = this.items.slice();
const oldItems = this.items.slice();
reorderPlan.forEach((reorderEvent) => {
this.$set(this.items, reorderEvent.newIndex, oldItems[reorderEvent.oldIndex]);
});
},
metadataHasUnits(valueMetadatas) {
let metadataWithUnits = valueMetadatas.filter(metadatum => metadatum.unit);
const metadataWithUnits = valueMetadatas.filter(metadatum => metadatum.unit);
return metadataWithUnits.length > 0;
},
handleStaleness(id, stalenessResponse, skipCheck = false) {
if (skipCheck || this.stalenessSubscription[id].stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {
const index = this.staleObjects.indexOf(id);
if (stalenessResponse.isStale) {
if (index === -1) {
this.staleObjects.push(id);
}
} else {
if (index !== -1) {
this.staleObjects.splice(index, 1);
}
}
}
},
updateViewContext(rowContext) {
this.viewContext.row = rowContext;
},

View File

@ -21,42 +21,50 @@
*****************************************************************************/
<template>
<table class="c-table c-lad-table">
<thead>
<tr>
<th>Name</th>
<th>Timestamp</th>
<th>Value</th>
<th v-if="hasUnits">Unit</th>
</tr>
</thead>
<tbody>
<template
v-for="ladTable in ladTableObjects"
>
<tr
:key="ladTable.key"
class="c-table__group-header js-lad-table-set__table-headers"
>
<td colspan="10">
{{ ladTable.domainObject.name }}
</td>
<div
class="c-lad-table-wrapper u-style-receiver js-style-receiver"
:class="staleClass"
>
<table class="c-table c-lad-table">
<thead>
<tr>
<th>Name</th>
<th>Timestamp</th>
<th>Value</th>
<th v-if="hasUnits">Unit</th>
</tr>
<lad-row
v-for="ladRow in ladTelemetryObjects[ladTable.key]"
:key="ladRow.key"
:domain-object="ladRow.domainObject"
:path-to-table="ladTable.objectPath"
:has-units="hasUnits"
@rowContextClick="updateViewContext"
/>
</template>
</tbody>
</table>
</thead>
<tbody>
<template
v-for="ladTable in ladTableObjects"
>
<tr
:key="ladTable.key"
class="c-table__group-header js-lad-table-set__table-headers"
>
<td colspan="10">
{{ ladTable.domainObject.name }}
</td>
</tr>
<lad-row
v-for="ladRow in ladTelemetryObjects[ladTable.key]"
:key="ladRow.key"
:domain-object="ladRow.domainObject"
:path-to-table="ladTable.objectPath"
:has-units="hasUnits"
:is-stale="staleObjects.includes(ladRow.key)"
@rowContextClick="updateViewContext"
/>
</template>
</tbody>
</table>
</div>
</template>
<script>
import LadRow from './LADRow.vue';
import StalenessUtils from '@/utils/staleness';
export default {
components: {
@ -74,7 +82,8 @@ export default {
ladTableObjects: [],
ladTelemetryObjects: {},
compositions: [],
viewContext: {}
viewContext: {},
staleObjects: []
};
},
computed: {
@ -95,6 +104,13 @@ export default {
}
return false;
},
staleClass() {
if (this.staleObjects.length !== 0) {
return 'is-stale';
}
return '';
}
},
mounted() {
@ -103,6 +119,8 @@ export default {
this.composition.on('remove', this.removeLadTable);
this.composition.on('reorder', this.reorderLadTables);
this.composition.load();
this.stalenessSubscription = {};
},
destroyed() {
this.composition.off('add', this.addLadTable);
@ -112,6 +130,11 @@ export default {
c.composition.off('add', c.addCallback);
c.composition.off('remove', c.removeCallback);
});
Object.values(this.stalenessSubscription).forEach(stalenessSubscription => {
stalenessSubscription.unsubscribe();
stalenessSubscription.stalenessUtils.destroy();
});
},
methods: {
addLadTable(domainObject) {
@ -160,18 +183,57 @@ export default {
telemetryObjects.push(telemetryObject);
this.$set(this.ladTelemetryObjects, ladTable.key, telemetryObjects);
// if tracking already, possibly in another table, return
if (this.stalenessSubscription[telemetryObject.key]) {
return;
} else {
this.stalenessSubscription[telemetryObject.key] = {};
this.stalenessSubscription[telemetryObject.key].stalenessUtils = new StalenessUtils(this.openmct, domainObject);
}
this.openmct.telemetry.isStale(domainObject).then((stalenessResponse) => {
if (stalenessResponse !== undefined) {
this.handleStaleness(telemetryObject.key, stalenessResponse);
}
});
const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(domainObject, (stalenessResponse) => {
this.handleStaleness(telemetryObject.key, stalenessResponse);
});
this.stalenessSubscription[telemetryObject.key].unsubscribe = stalenessSubscription;
};
},
removeTelemetryObject(ladTable) {
return (identifier) => {
const SKIP_CHECK = true;
const keystring = this.openmct.objects.makeKeyString(identifier);
let telemetryObjects = this.ladTelemetryObjects[ladTable.key];
let index = telemetryObjects.findIndex(telemetryObject => this.openmct.objects.makeKeyString(identifier) === telemetryObject.key);
let index = telemetryObjects.findIndex(telemetryObject => keystring === telemetryObject.key);
telemetryObjects.splice(index, 1);
this.$set(this.ladTelemetryObjects, ladTable.key, telemetryObjects);
this.stalenessSubscription[keystring].unsubscribe();
this.stalenessSubscription[keystring].stalenessUtils.destroy();
this.handleStaleness(keystring, { isStale: false }, SKIP_CHECK);
};
},
handleStaleness(id, stalenessResponse, skipCheck = false) {
if (skipCheck || this.stalenessSubscription[id].stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {
const index = this.staleObjects.indexOf(id);
if (stalenessResponse.isStale) {
if (index === -1) {
this.staleObjects.push(id);
}
} else {
if (index !== -1) {
this.staleObjects.splice(index, 1);
}
}
}
},
updateViewContext(rowContext) {
this.viewContext.row = rowContext;
},

View File

@ -26,7 +26,7 @@ import TelemetryCriterion from "./criterion/TelemetryCriterion";
import { evaluateResults } from './utils/evaluator';
import { getLatestTimestamp } from './utils/time';
import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion";
import {TRIGGER_CONJUNCTION, TRIGGER_LABEL} from "./utils/constants";
import { TRIGGER_CONJUNCTION, TRIGGER_LABEL } from "./utils/constants";
/*
* conditionConfiguration = {
@ -160,7 +160,8 @@ export default class Condition extends EventEmitter {
}
criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.on('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
criterion.on('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
criterion.on('telemetryStaleness', () => this.handleTelemetryStaleness());
if (!this.criteria) {
this.criteria = [];
}
@ -191,12 +192,14 @@ export default class Condition extends EventEmitter {
const newCriterionConfiguration = this.generateCriterion(criterionConfiguration);
let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct);
newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
newCriterion.on('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
newCriterion.on('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
newCriterion.on('telemetryStaleness', () => this.handleTelemetryStaleness());
let criterion = found.item;
criterion.unsubscribe();
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.off('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
criterion.off('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
newCriterion.off('telemetryStaleness', () => this.handleTelemetryStaleness());
this.criteria.splice(found.index, 1, newCriterion);
}
}
@ -205,12 +208,9 @@ export default class Condition extends EventEmitter {
let found = this.findCriterion(id);
if (found) {
let criterion = found.item;
criterion.off('criterionUpdated', (obj) => {
this.handleCriterionUpdated(obj);
});
criterion.off('telemetryIsStale', (obj) => {
this.handleStaleCriterion(obj);
});
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.off('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
criterion.off('telemetryStaleness', () => this.handleTelemetryStaleness());
criterion.destroy();
this.criteria.splice(found.index, 1);
@ -227,7 +227,7 @@ export default class Condition extends EventEmitter {
}
}
handleStaleCriterion(updatedCriterion) {
handleOldTelemetryCriterion(updatedCriterion) {
this.result = evaluateResults(this.criteria.map(criterion => criterion.result), this.trigger);
let latestTimestamp = {};
latestTimestamp = getLatestTimestamp(
@ -239,6 +239,11 @@ export default class Condition extends EventEmitter {
this.conditionManager.updateCurrentCondition(latestTimestamp);
}
handleTelemetryStaleness() {
this.result = evaluateResults(this.criteria.map(criterion => criterion.result), this.trigger);
this.conditionManager.updateCurrentCondition();
}
updateDescription() {
const triggerDescription = this.getTriggerDescription();
let description = '';

View File

@ -82,8 +82,10 @@
</template>
<script>
import Condition from './Condition.vue';
import ConditionManager from '../ConditionManager';
import StalenessUtils from '@/utils/staleness';
export default {
components: {
@ -139,6 +141,13 @@ export default {
if (this.stopObservingForChanges) {
this.stopObservingForChanges();
}
if (this.stalenessSubscription) {
Object.values(this.stalenessSubscription).forEach(stalenessSubscription => {
stalenessSubscription.unsubscribe();
stalenessSubscription.stalenessUtils.destroy();
});
}
},
mounted() {
this.composition = this.openmct.composition.get(this.domainObject);
@ -150,6 +159,7 @@ export default {
this.conditionManager = new ConditionManager(this.domainObject, this.openmct);
this.conditionManager.on('conditionSetResultUpdated', this.handleConditionSetResultUpdated);
this.updateDefaultCondition();
this.stalenessSubscription = {};
},
methods: {
handleConditionSetResultUpdated(data) {
@ -210,19 +220,57 @@ export default {
return arr;
},
addTelemetryObject(domainObject) {
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
this.telemetryObjs.push(domainObject);
this.$emit('telemetryUpdated', this.telemetryObjs);
if (!this.stalenessSubscription[keyString]) {
this.stalenessSubscription[keyString] = {};
}
this.stalenessSubscription[keyString].stalenessUtils = new StalenessUtils(this.openmct, domainObject);
this.openmct.telemetry.isStale(domainObject).then((stalenessResponse) => {
this.hanldeStaleness(keyString, stalenessResponse);
});
const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(domainObject, (stalenessResponse) => {
this.hanldeStaleness(keyString, stalenessResponse);
});
this.stalenessSubscription[keyString].unsubscribe = stalenessSubscription;
},
removeTelemetryObject(identifier) {
let index = this.telemetryObjs.findIndex(obj => {
const keyString = this.openmct.objects.makeKeyString(identifier);
const index = this.telemetryObjs.findIndex(obj => {
let objId = this.openmct.objects.makeKeyString(obj.identifier);
let id = this.openmct.objects.makeKeyString(identifier);
return objId === id;
return objId === keyString;
});
if (index > -1) {
this.telemetryObjs.splice(index, 1);
}
if (this.stalenessSubscription[keyString]) {
this.stalenessSubscription[keyString].unsubscribe();
this.stalenessSubscription[keyString].stalenessUtils.destroy();
this.emitStaleness({
keyString,
isStale: false
});
}
},
hanldeStaleness(keyString, stalenessResponse) {
if (this.stalenessSubscription[keyString].stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {
this.emitStaleness({
keyString,
isStale: stalenessResponse.isStale
});
}
},
emitStaleness(stalenessObject) {
this.$emit('telemetryStaleness', stalenessObject);
},
addCondition() {
this.conditionManager.addCondition();

View File

@ -21,7 +21,10 @@
*****************************************************************************/
<template>
<div class="c-cs">
<div
class="c-cs"
:class="{'is-stale': isStale }"
>
<section class="c-cs__current-output c-section">
<div class="c-cs__content c-cs__current-output-value">
<span class="c-cs__current-output-value__label">Current Output</span>
@ -50,6 +53,7 @@
@conditionSetResultUpdated="updateCurrentOutput"
@updateDefaultOutput="updateDefaultOutput"
@telemetryUpdated="updateTelemetry"
@telemetryStaleness="handleStaleness"
/>
</div>
</div>
@ -73,9 +77,15 @@ export default {
currentConditionOutput: '',
defaultConditionOutput: '',
telemetryObjs: [],
testData: {}
testData: {},
staleObjects: []
};
},
computed: {
isStale() {
return this.staleObjects.length !== 0;
}
},
mounted() {
this.conditionSetIdentifier = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.testData = {
@ -95,6 +105,18 @@ export default {
},
updateTestData(testData) {
this.testData = testData;
},
handleStaleness({ keyString, isStale }) {
const index = this.staleObjects.indexOf(keyString);
if (isStale) {
if (index === -1) {
this.staleObjects.push(keyString);
}
} else {
if (index !== -1) {
this.staleObjects.splice(index, 1);
}
}
}
}
};

View File

@ -94,7 +94,7 @@
>
<span v-if="inputIndex < inputCount-1">and</span>
</span>
<span v-if="criterion.metadata === 'dataReceived'">seconds</span>
<span v-if="criterion.metadata === 'dataReceived' && criterion.operation.name === IS_OLD_KEY">seconds</span>
</template>
<span v-else>
<span
@ -122,7 +122,7 @@
<script>
import { OPERATIONS, INPUT_TYPES } from '../utils/operations';
import {TRIGGER_CONJUNCTION} from "../utils/constants";
import { TRIGGER_CONJUNCTION, IS_OLD_KEY, IS_STALE_KEY } from "../utils/constants";
export default {
inject: ['openmct'],
@ -153,7 +153,8 @@ export default {
rowLabel: '',
operationFormat: '',
enumerations: [],
inputTypes: INPUT_TYPES
inputTypes: INPUT_TYPES,
IS_OLD_KEY
};
},
computed: {
@ -164,7 +165,7 @@ export default {
},
filteredOps: function () {
if (this.criterion.metadata === 'dataReceived') {
return this.operations.filter(op => op.name === 'isStale');
return this.operations.filter(op => op.name === IS_OLD_KEY || op.name === IS_STALE_KEY);
} else {
return this.operations.filter(op => op.appliesTo.indexOf(this.operationFormat) !== -1);
}

View File

@ -54,6 +54,10 @@
height: 100%;
overflow: hidden;
&.is-stale {
@include isStaleHolder();
}
/************************** CONDITION SET LAYOUT */
&__current-output {
flex: 0 0 auto;

View File

@ -21,8 +21,9 @@
*****************************************************************************/
import TelemetryCriterion from './TelemetryCriterion';
import StalenessUtils from '@/utils/staleness';
import { evaluateResults } from "../utils/evaluator";
import {getLatestTimestamp, subscribeForStaleness} from '../utils/time';
import { getLatestTimestamp, checkIfOld } from '../utils/time';
import { getOperatorText } from "@/plugins/condition/utils/operations";
export default class AllTelemetryCriterion extends TelemetryCriterion {
@ -38,13 +39,41 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
initialize() {
this.telemetryObjects = { ...this.telemetryDomainObjectDefinition.telemetryObjects };
this.telemetryDataCache = {};
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData(this.telemetryObjects || {});
if (this.isValid() && this.isOldCheck() && this.isValidInput()) {
this.checkForOldData(this.telemetryObjects || {});
}
if (this.isValid() && this.isStalenessCheck()) {
this.subscribeToStaleness(this.telemetryObjects || {});
}
}
subscribeForStaleData(telemetryObjects) {
checkForOldData(telemetryObjects) {
if (!this.ageCheck) {
this.ageCheck = {};
}
Object.values(telemetryObjects).forEach((telemetryObject) => {
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (!this.ageCheck[id]) {
this.ageCheck[id] = checkIfOld((data) => {
this.handleOldTelemetry(id, data);
}, this.input[0] * 1000);
}
});
}
handleOldTelemetry(id, data) {
if (this.telemetryDataCache) {
this.telemetryDataCache[id] = true;
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
}
this.emitEvent('telemetryIsOld', data);
}
subscribeToStaleness(telemetryObjects) {
if (!this.stalenessSubscription) {
this.stalenessSubscription = {};
}
@ -52,20 +81,27 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
Object.values(telemetryObjects).forEach((telemetryObject) => {
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (!this.stalenessSubscription[id]) {
this.stalenessSubscription[id] = subscribeForStaleness((data) => {
this.handleStaleTelemetry(id, data);
}, this.input[0] * 1000);
this.stalenessSubscription[id] = {};
this.stalenessSubscription[id].stalenessUtils = new StalenessUtils(this.openmct, telemetryObject);
this.stalenessSubscription[id].unsubscribe = this.openmct.telemetry.subscribeToStaleness(
telemetryObject,
(stalenessResponse) => {
this.handleStaleTelemetry(id, stalenessResponse);
}
);
}
});
}
handleStaleTelemetry(id, data) {
handleStaleTelemetry(id, stalenessResponse) {
if (this.telemetryDataCache) {
this.telemetryDataCache[id] = true;
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
}
if (this.stalenessSubscription[id].stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {
this.telemetryDataCache[id] = stalenessResponse.isStale;
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
this.emitEvent('telemetryIsStale', data);
this.emitEvent('telemetryStaleness');
}
}
}
isValid() {
@ -75,8 +111,13 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
updateTelemetryObjects(telemetryObjects) {
this.telemetryObjects = { ...telemetryObjects };
this.removeTelemetryDataCache();
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData(this.telemetryObjects || {});
if (this.isValid() && this.isOldCheck() && this.isValidInput()) {
this.checkForOldData(this.telemetryObjects || {});
}
if (this.isValid() && this.isStalenessCheck()) {
this.subscribeToStaleness(this.telemetryObjects || {});
}
}
@ -91,6 +132,9 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
});
telemetryCacheIds.forEach(id => {
delete (this.telemetryDataCache[id]);
delete (this.ageCheck[id]);
this.stalenessSubscription[id].unsubscribe();
this.stalenessSubscription[id].stalenessUtils.destroy();
delete (this.stalenessSubscription[id]);
});
}
@ -125,10 +169,10 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
updateResult(data, telemetryObjects) {
const validatedData = this.isValid() ? data : {};
if (validatedData) {
if (this.isStalenessCheck()) {
if (this.stalenessSubscription && this.stalenessSubscription[validatedData.id]) {
this.stalenessSubscription[validatedData.id].update(validatedData);
if (validatedData && !this.isStalenessCheck()) {
if (this.isOldCheck()) {
if (this.ageCheck?.[validatedData.id]) {
this.ageCheck[validatedData.id].update(validatedData);
}
this.telemetryDataCache[validatedData.id] = false;
@ -226,9 +270,17 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
destroy() {
delete this.telemetryObjects;
delete this.telemetryDataCache;
if (this.ageCheck) {
Object.values(this.ageCheck).forEach((subscription) => subscription.clear);
delete this.ageCheck;
}
if (this.stalenessSubscription) {
Object.values(this.stalenessSubscription).forEach((subscription) => subscription.clear);
delete this.stalenessSubscription;
Object.values(this.stalenessSubscription).forEach(subscription => {
subscription.unsubscribe();
subscription.stalenessUtils.destroy();
});
}
}
}

View File

@ -21,8 +21,10 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import StalenessUtils from '@/utils/staleness';
import { IS_OLD_KEY, IS_STALE_KEY } from '../utils/constants';
import { OPERATIONS, getOperatorText } from '../utils/operations';
import { subscribeForStaleness } from "../utils/time";
import { checkIfOld } from "../utils/time";
export default class TelemetryCriterion extends EventEmitter {
@ -44,7 +46,8 @@ export default class TelemetryCriterion extends EventEmitter {
this.input = telemetryDomainObjectDefinition.input;
this.metadata = telemetryDomainObjectDefinition.metadata;
this.result = undefined;
this.stalenessSubscription = undefined;
this.ageCheck = undefined;
this.unsubscribeFromStaleness = undefined;
this.initialize();
this.emitEvent('criterionUpdated', this);
@ -57,8 +60,13 @@ export default class TelemetryCriterion extends EventEmitter {
}
this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects);
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData();
if (this.isValid() && this.isOldCheck() && this.isValidInput()) {
this.checkForOldData();
}
if (this.isValid() && this.isStalenessCheck()) {
this.subscribeToStaleness();
}
}
@ -66,25 +74,52 @@ export default class TelemetryCriterion extends EventEmitter {
return this.telemetryObjectIdAsString && (this.telemetryObjectIdAsString === id);
}
subscribeForStaleData() {
if (this.stalenessSubscription) {
this.stalenessSubscription.clear();
checkForOldData() {
if (this.ageCheck) {
this.ageCheck.clear();
}
this.stalenessSubscription = subscribeForStaleness(this.handleStaleTelemetry.bind(this), this.input[0] * 1000);
this.ageCheck = checkIfOld(this.handleOldTelemetry.bind(this), this.input[0] * 1000);
}
handleStaleTelemetry(data) {
handleOldTelemetry(data) {
this.result = true;
this.emitEvent('telemetryIsStale', data);
this.emitEvent('telemetryIsOld', data);
}
subscribeToStaleness() {
if (this.unsubscribeFromStaleness) {
this.unsubscribeFromStaleness();
}
if (!this.stalenessUtils) {
this.stalenessUtils = new StalenessUtils(this.openmct, this.telemetryObject);
}
this.openmct.telemetry.isStale(this.telemetryObject).then(this.handleStaleTelemetry.bind(this));
this.unsubscribeFromStaleness = this.openmct.telemetry.subscribeToStaleness(
this.telemetryObject,
this.handleStaleTelemetry.bind(this)
);
}
handleStaleTelemetry(stalenessResponse) {
if (stalenessResponse !== undefined && this.stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {
this.result = stalenessResponse.isStale;
this.emitEvent('telemetryStaleness');
}
}
isValid() {
return this.telemetryObject && this.metadata && this.operation;
}
isOldCheck() {
return this.metadata && this.metadata === 'dataReceived' && this.operation === IS_OLD_KEY;
}
isStalenessCheck() {
return this.metadata && this.metadata === 'dataReceived';
return this.metadata && this.metadata === 'dataReceived' && this.operation === IS_STALE_KEY;
}
isValidInput() {
@ -93,8 +128,13 @@ export default class TelemetryCriterion extends EventEmitter {
updateTelemetryObjects(telemetryObjects) {
this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString];
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData();
if (this.isValid() && this.isOldCheck() && this.isValidInput()) {
this.checkForOldData();
}
if (this.isValid() && this.isStalenessCheck()) {
this.subscribeToStaleness();
}
}
@ -130,14 +170,17 @@ export default class TelemetryCriterion extends EventEmitter {
updateResult(data) {
const validatedData = this.isValid() ? data : {};
if (this.isStalenessCheck()) {
if (this.stalenessSubscription) {
this.stalenessSubscription.update(validatedData);
}
this.result = false;
} else {
this.result = this.computeResult(validatedData);
if (!this.isStalenessCheck()) {
if (this.isOldCheck()) {
if (this.ageCheck) {
this.ageCheck.update(validatedData);
}
this.result = false;
} else {
this.result = this.computeResult(validatedData);
}
}
}
@ -268,8 +311,17 @@ export default class TelemetryCriterion extends EventEmitter {
destroy() {
delete this.telemetryObject;
delete this.telemetryObjectIdAsString;
if (this.stalenessSubscription) {
delete this.stalenessSubscription;
if (this.ageCheck) {
delete this.ageCheck;
}
if (this.stalenessUtils) {
this.stalenessUtils.destroy();
}
if (this.unsubscribeFromStaleness) {
this.unsubscribeFromStaleness();
}
}
}

View File

@ -28,6 +28,7 @@ import Vue from 'vue';
import {getApplicableStylesForItem} from "./utils/styleUtils";
import ConditionManager from "@/plugins/condition/ConditionManager";
import StyleRuleManager from "./StyleRuleManager";
import { IS_OLD_KEY } from "./utils/constants";
describe('the plugin', function () {
let conditionSetDefinition;
@ -642,7 +643,7 @@ describe('the plugin', function () {
});
describe('the condition check for staleness', () => {
describe('the condition check if old', () => {
let conditionSetDomainObject;
beforeEach(() => {
@ -660,13 +661,13 @@ describe('the plugin', function () {
"id": "39584410-cbf9-499e-96dc-76f27e69885d",
"configuration": {
"name": "Unnamed Condition",
"output": "Any stale telemetry",
"output": "Any old telemetry",
"trigger": "all",
"criteria": [
{
"id": "35400132-63b0-425c-ac30-8197df7d5862",
"telemetry": "any",
"operation": "isStale",
"operation": IS_OLD_KEY,
"input": [
"0.2"
],
@ -674,7 +675,7 @@ describe('the plugin', function () {
}
]
},
"summary": "Match if all criteria are met: Any telemetry is stale after 5 seconds"
"summary": "Match if all criteria are met: Any telemetry is old after 5 seconds"
},
{
"isDefault": true,
@ -708,7 +709,7 @@ describe('the plugin', function () {
};
});
it('should evaluate as stale when telemetry is not received in the allotted time', (done) => {
it('should evaluate as old when telemetry is not received in the allotted time', (done) => {
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
conditionMgr.on('conditionSetResultUpdated', mockListener);
conditionMgr.telemetryObjects = {
@ -717,7 +718,7 @@ describe('the plugin', function () {
conditionMgr.updateConditionTelemetryObjects();
setTimeout(() => {
expect(mockListener).toHaveBeenCalledWith({
output: 'Any stale telemetry',
output: 'Any old telemetry',
id: {
namespace: '',
key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9'
@ -729,7 +730,7 @@ describe('the plugin', function () {
}, 400);
});
it('should not evaluate as stale when telemetry is received in the allotted time', (done) => {
it('should not evaluate as old when telemetry is received in the allotted time', (done) => {
const date = 1;
conditionSetDomainObject.configuration.conditionCollection[0].configuration.criteria[0].input = ["0.4"];
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);

View File

@ -59,3 +59,6 @@ export const ERROR = {
errorText: 'Condition not found'
}
};
export const IS_OLD_KEY = 'isStale';
export const IS_STALE_KEY = 'isStale.new';

View File

@ -20,6 +20,8 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { IS_OLD_KEY, IS_STALE_KEY } from "./constants";
function convertToNumbers(input) {
let numberInputs = [];
input.forEach(inputValue => numberInputs.push(Number(inputValue)));
@ -295,7 +297,7 @@ export const OPERATIONS = [
}
},
{
name: 'isStale',
name: IS_OLD_KEY,
operation: function () {
return false;
},
@ -305,6 +307,18 @@ export const OPERATIONS = [
getDescription: function (values) {
return ` is older than ${values[0] || ''} seconds`;
}
},
{
name: IS_STALE_KEY,
operation: function () {
return false;
},
text: 'is stale',
appliesTo: ["number"],
inputCount: 0,
getDescription: function () {
return ' is stale';
}
}
];
@ -316,5 +330,5 @@ export const INPUT_TYPES = {
export function getOperatorText(operationName, values) {
const found = OPERATIONS.find((operation) => operation.name === operationName);
return found ? found.getDescription(values) : '';
return found?.getDescription(values) ?? '';
}

View File

@ -51,26 +51,26 @@ export function getLatestTimestamp(
return latest;
}
export function subscribeForStaleness(callback, timeout) {
let stalenessTimer = setTimeout(() => {
clearTimeout(stalenessTimer);
export function checkIfOld(callback, timeout) {
let oldCheckTimer = setTimeout(() => {
clearTimeout(oldCheckTimer);
callback();
}, timeout);
return {
update: (data) => {
if (stalenessTimer) {
clearTimeout(stalenessTimer);
if (oldCheckTimer) {
clearTimeout(oldCheckTimer);
}
stalenessTimer = setTimeout(() => {
clearTimeout(stalenessTimer);
oldCheckTimer = setTimeout(() => {
clearTimeout(oldCheckTimer);
callback(data);
}, timeout);
},
clear: () => {
if (stalenessTimer) {
clearTimeout(stalenessTimer);
if (oldCheckTimer) {
clearTimeout(oldCheckTimer);
}
}
};

View File

@ -19,7 +19,7 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { subscribeForStaleness } from "./time";
import { checkIfOld } from "./time";
describe('time related utils', () => {
let subscription;
@ -27,11 +27,11 @@ describe('time related utils', () => {
beforeEach(() => {
mockListener = jasmine.createSpy('listener');
subscription = subscribeForStaleness(mockListener, 100);
subscription = checkIfOld(mockListener, 100);
});
describe('subscribe for staleness', () => {
it('should call listeners when stale', (done) => {
describe('check if old', () => {
it('should call listeners when old', (done) => {
setTimeout(() => {
expect(mockListener).toHaveBeenCalled();
done();

View File

@ -31,7 +31,7 @@
<div
v-if="domainObject"
class="c-telemetry-view u-style-receiver"
:class="[statusClass]"
:class="[itemClasses]"
:style="styleObject"
:data-font-size="item.fontSize"
:data-font="item.font"
@ -73,6 +73,7 @@
<script>
import LayoutFrame from './LayoutFrame.vue';
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
import stalenessMixin from '@/ui/mixins/staleness-mixin';
import { getDefaultNotebook, getNotebookSectionAndPage } from '@/plugins/notebook/utils/notebook-storage.js';
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
@ -102,7 +103,7 @@ export default {
components: {
LayoutFrame
},
mixins: [conditionalStylesMixin],
mixins: [conditionalStylesMixin, stalenessMixin],
inject: ['openmct', 'objectPath', 'currentView'],
props: {
item: {
@ -137,8 +138,18 @@ export default {
};
},
computed: {
statusClass() {
return (this.status) ? `is-status--${this.status}` : '';
itemClasses() {
let classes = [];
if (this.status) {
classes.push(`is-status--${this.status}`);
}
if (this.isStale) {
classes.push('is-stale');
}
return classes;
},
showLabel() {
let displayMode = this.item.displayMode;
@ -310,6 +321,7 @@ export default {
this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context, this.immediatelySelect || this.initSelect);
delete this.immediatelySelect;
this.subscribeToStaleness(this.domainObject);
},
updateTelemetryFormat(format) {
this.customStringformatter.setFormat(format);

View File

@ -25,6 +25,12 @@
margin-right: $interiorMargin;
}
&.is-stale {
.c-telemetry-view__value {
@include isStaleElement();
}
}
.c-frame & {
@include abs();
border: 1px solid transparent;

View File

@ -22,7 +22,7 @@
<template>
<div
class="c-gauge__wrapper js-gauge-wrapper"
:class="`c-gauge--${gaugeType}`"
:class="gaugeClasses"
:title="gaugeTitle"
>
<template v-if="typeDial">
@ -347,12 +347,14 @@
<script>
import { DIAL_VALUE_DEG_OFFSET, getLimitDegree } from '../gauge-limit-util';
import stalenessMixin from '@/ui/mixins/staleness-mixin';
const LIMIT_PADDING_IN_PERCENT = 10;
const DEFAULT_CURRENT_VALUE = '--';
export default {
name: 'Gauge',
mixins: [stalenessMixin],
inject: ['openmct', 'domainObject', 'composition'],
data() {
let gaugeController = this.domainObject.configuration.gaugeController;
@ -403,6 +405,15 @@ export default {
return VIEWBOX_STR.replace('X', this.digits * DIGITS_RATIO);
},
gaugeClasses() {
let classes = [`c-gauge--${this.gaugeType}`];
if (this.isStale) {
classes.push('is-stale');
}
return classes;
},
rangeFontSize() {
const CHAR_THRESHOLD = 3;
const START_PERC = 8.5;
@ -553,6 +564,8 @@ export default {
this.telemetryObject = domainObject;
this.request();
this.subscribe();
this.subscribeToStaleness(domainObject);
},
addedToComposition(domainObject) {
if (this.telemetryObject) {
@ -611,6 +624,8 @@ export default {
this.unsubscribe = null;
}
this.triggerUnsubscribeFromStaleness();
this.curVal = DEFAULT_CURRENT_VALUE;
this.formats = null;
this.limitHigh = '';

View File

@ -32,6 +32,15 @@ $meterNeedleBorderRadius: 5px;
&__wrapper {
@include abs();
overflow: hidden;
&.is-stale {
@include isStaleHolder();
[class*=__current-value-text] {
fill: $colorTelemStale;
font-style: italic;
}
}
}
&__current-value-text-wrapper {

View File

@ -23,6 +23,7 @@
<div
ref="plotWrapper"
class="c-plot holder holder-plot has-control-bar"
:class="staleClass"
>
<div
ref="plotContainer"
@ -50,6 +51,7 @@ import eventHelpers from './lib/eventHelpers';
import ImageExporter from '../../exporters/ImageExporter';
import MctPlot from './MctPlot.vue';
import ProgressBar from "../../ui/components/ProgressBar.vue";
import StalenessUtils from '@/utils/staleness';
export default {
components: {
@ -74,21 +76,95 @@ export default {
cursorGuide: false,
gridLines: !this.options.compact,
loading: false,
status: ''
status: '',
staleObjects: []
};
},
computed: {
staleClass() {
if (this.staleObjects.length !== 0) {
return 'is-stale';
}
return '';
}
},
mounted() {
eventHelpers.extend(this);
this.imageExporter = new ImageExporter(this.openmct);
this.loadComposition();
this.stalenessSubscription = {};
},
beforeDestroy() {
this.destroy();
},
methods: {
loadComposition() {
this.compositionCollection = this.openmct.composition.get(this.domainObject);
if (this.compositionCollection) {
this.compositionCollection.on('add', this.addItem);
this.compositionCollection.on('remove', this.removeItem);
this.compositionCollection.load();
}
},
addItem(object) {
const keystring = this.openmct.objects.makeKeyString(object.identifier);
if (!this.stalenessSubscription[keystring]) {
this.stalenessSubscription[keystring] = {};
this.stalenessSubscription[keystring].stalenessUtils = new StalenessUtils(this.openmct, object);
}
this.openmct.telemetry.isStale(object).then((stalenessResponse) => {
if (stalenessResponse !== undefined) {
this.handleStaleness(keystring, stalenessResponse);
}
});
const unsubscribeFromStaleness = this.openmct.telemetry.subscribeToStaleness(object, (stalenessResponse) => {
this.handleStaleness(keystring, stalenessResponse);
});
this.stalenessSubscription[keystring].unsubscribe = unsubscribeFromStaleness;
},
removeItem(object) {
const SKIP_CHECK = true;
const keystring = this.openmct.objects.makeKeyString(object);
this.stalenessSubscription[keystring].unsubscribe();
this.stalenessSubscription[keystring].stalenessUtils.destroy();
this.handleStaleness(keystring, { isStale: false }, SKIP_CHECK);
},
handleStaleness(id, stalenessResponse, skipCheck = false) {
if (skipCheck || this.stalenessSubscription[id].stalenessUtils.shouldUpdateStaleness(stalenessResponse, id)) {
const index = this.staleObjects.indexOf(id);
if (stalenessResponse.isStale) {
if (index === -1) {
this.staleObjects.push(id);
}
} else {
if (index !== -1) {
this.staleObjects.splice(index, 1);
}
}
}
},
loadingUpdated(loading) {
this.loading = loading;
},
destroy() {
if (this.stalenessSubscription) {
Object.values(this.stalenessSubscription).forEach(stalenessSubscription => {
stalenessSubscription.unsubscribe();
stalenessSubscription.stalenessUtils.destroy();
});
}
if (this.compositionCollection) {
this.compositionCollection.off('add', this.addItem);
this.compositionCollection.off('remove', this.removeItem);
}
this.stopListening();
},
exportJPG() {

View File

@ -23,6 +23,7 @@
<div
class="plot-legend-item"
:class="{
'is-stale': isStale,
'is-status--missing': isMissing
}"
@mouseover="toggleHover(true)"
@ -55,8 +56,10 @@
import {getLimitClass} from "@/plugins/plot/chart/limitUtil";
import eventHelpers from "../lib/eventHelpers";
import stalenessMixin from '@/ui/mixins/staleness-mixin';
export default {
mixins: [stalenessMixin],
inject: ['openmct', 'domainObject'],
props: {
valueToShowWhenCollapsed: {
@ -112,6 +115,7 @@ export default {
this.listenTo(this.seriesObject, 'change:name', () => {
this.updateName();
}, this);
this.subscribeToStaleness(this.seriesObject.domainObject);
this.initialize();
},
beforeDestroy() {
@ -120,6 +124,7 @@ export default {
methods: {
initialize(highlightedObject) {
const seriesObject = highlightedObject ? highlightedObject.series : this.seriesObject;
this.isMissing = seriesObject.domainObject.status === 'missing';
this.colorAsHexString = seriesObject.get('color').asHexString();
this.nameWithUnit = seriesObject.nameWithUnit();

View File

@ -23,6 +23,7 @@
<tr
class="plot-legend-item"
:class="{
'is-stale': isStale,
'is-status--missing': isMissing
}"
@mouseover="toggleHover(true)"
@ -81,8 +82,10 @@
<script>
import {getLimitClass} from "@/plugins/plot/chart/limitUtil";
import eventHelpers from "@/plugins/plot/lib/eventHelpers";
import stalenessMixin from '@/ui/mixins/staleness-mixin';
export default {
mixins: [stalenessMixin],
inject: ['openmct', 'domainObject'],
props: {
seriesObject: {
@ -149,6 +152,7 @@ export default {
this.listenTo(this.seriesObject, 'change:name', () => {
this.updateName();
}, this);
this.subscribeToStaleness(this.seriesObject.domainObject);
this.initialize();
},
beforeDestroy() {

View File

@ -27,12 +27,13 @@
import MctPlot from '../MctPlot.vue';
import Vue from "vue";
import conditionalStylesMixin from "./mixins/objectStyles-mixin";
import stalenessMixin from '@/ui/mixins/staleness-mixin';
import configStore from "@/plugins/plot/configuration/ConfigStore";
import PlotConfigurationModel from "@/plugins/plot/configuration/PlotConfigurationModel";
import ProgressBar from "../../../ui/components/ProgressBar.vue";
export default {
mixins: [conditionalStylesMixin],
mixins: [conditionalStylesMixin, stalenessMixin],
inject: ['openmct', 'domainObject', 'path'],
props: {
childObject: {
@ -114,6 +115,10 @@ export default {
}
},
updateView() {
this.isStale = false;
this.triggerUnsubscribeFromStaleness();
if (this.component) {
this.component.$destroy();
this.component = undefined;
@ -139,6 +144,10 @@ export default {
let viewContainer = document.createElement('div');
this.$el.append(viewContainer);
this.subscribeToStaleness(object, (isStale) => {
this.updateComponentProp('isStale', isStale);
});
this.component = new Vue({
el: viewContainer,
components: {
@ -169,7 +178,7 @@ export default {
this.loading = loaded;
}
},
template: '<div v-if="!isMissing" ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver" :class="{\'s-status-timeconductor-unsynced\': status && status === \'timeconductor-unsynced\'}"><progress-bar v-show="loading !== false" class="c-telemetry-table__progress-bar" :model="{progressPerc: undefined}" /><mct-plot :init-grid-lines="gridLines" :init-cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" :limit-line-labels="limitLineLabels" :color-palette="colorPalette" :options="options" @plotTickWidth="onTickWidthChange" @lockHighlightPoint="onLockHighlightPointUpdated" @highlights="onHighlightsUpdated" @configLoaded="onConfigLoaded" @cursorGuide="onCursorGuideChange" @gridLines="onGridLinesChange" @statusUpdated="setStatus" @loadingUpdated="loadingUpdated"/></div>'
template: '<div v-if="!isMissing" ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver" :class="{\'s-status-timeconductor-unsynced\': status && status === \'timeconductor-unsynced\', \'is-stale\': isStale}"><progress-bar v-show="loading !== false" class="c-telemetry-table__progress-bar" :model="{progressPerc: undefined}" /><mct-plot :init-grid-lines="gridLines" :init-cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" :limit-line-labels="limitLineLabels" :color-palette="colorPalette" :options="options" @plotTickWidth="onTickWidthChange" @lockHighlightPoint="onLockHighlightPointUpdated" @highlights="onHighlightsUpdated" @configLoaded="onConfigLoaded" @cursorGuide="onCursorGuideChange" @gridLines="onGridLinesChange" @statusUpdated="setStatus" @loadingUpdated="loadingUpdated"/></div>'
});
this.setSelection();
@ -215,7 +224,8 @@ export default {
plotTickWidth: this.plotTickWidth,
options: this.options,
status: this.status,
colorPalette: this.colorPalette
colorPalette: this.colorPalette,
isStale: this.isStale
};
},
getPlotObject() {

View File

@ -155,7 +155,7 @@ define([
plugins.example.ExampleFaultSource = ExampleFaultSource.default;
plugins.example.EventGeneratorPlugin = EventGeneratorPlugin.default;
plugins.example.ExampleTags = ExampleTags.default;
plugins.example.Generator = () => GeneratorPlugin;
plugins.example.Generator = () => GeneratorPlugin.default;
plugins.UTCTimeSystem = UTCTimeSystem.default;
plugins.LocalTimeSystem = LocalTimeSystem;

View File

@ -28,7 +28,8 @@ define([
'./TelemetryTableNameColumn',
'./TelemetryTableColumn',
'./TelemetryTableUnitColumn',
'./TelemetryTableConfiguration'
'./TelemetryTableConfiguration',
'@/utils/staleness'
], function (
EventEmitter,
_,
@ -37,7 +38,8 @@ define([
TelemetryTableNameColumn,
TelemetryTableColumn,
TelemetryTableUnitColumn,
TelemetryTableConfiguration
TelemetryTableConfiguration,
StalenessUtils
) {
class TelemetryTable extends EventEmitter {
constructor(domainObject, openmct) {
@ -56,6 +58,7 @@ define([
this.telemetryCollections = {};
this.delayedActions = [];
this.outstandingRequests = 0;
this.stalenessSubscription = {};
this.addTelemetryObject = this.addTelemetryObject.bind(this);
this.removeTelemetryObject = this.removeTelemetryObject.bind(this);
@ -155,6 +158,19 @@ define([
this.telemetryCollections[keyString].on('clear', this.clearData);
this.telemetryCollections[keyString].load();
this.stalenessSubscription[keyString] = {};
this.stalenessSubscription[keyString].stalenessUtils = new StalenessUtils.default(this.openmct, telemetryObject);
this.openmct.telemetry.isStale(telemetryObject).then(stalenessResponse => {
if (stalenessResponse !== undefined) {
this.handleStaleness(keyString, stalenessResponse);
}
});
const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(telemetryObject, (stalenessResponse) => {
this.handleStaleness(keyString, stalenessResponse);
});
this.stalenessSubscription[keyString].unsubscribe = stalenessSubscription;
this.telemetryObjects[keyString] = {
telemetryObject,
keyString,
@ -166,6 +182,15 @@ define([
this.emit('object-added', telemetryObject);
}
handleStaleness(keyString, stalenessResponse, skipCheck = false) {
if (skipCheck || this.stalenessSubscription[keyString].stalenessUtils.shouldUpdateStaleness(stalenessResponse, keyString)) {
this.emit('telemetry-staleness', {
keyString,
isStale: stalenessResponse.isStale
});
}
}
getTelemetryProcessor(keyString, columnMap, limitEvaluator) {
return (telemetry) => {
//Check that telemetry object has not been removed since telemetry was requested.
@ -255,6 +280,7 @@ define([
removeTelemetryObject(objectIdentifier) {
const keyString = this.openmct.objects.makeKeyString(objectIdentifier);
const SKIP_CHECK = true;
this.configuration.removeColumnsForObject(objectIdentifier, true);
this.tableRows.removeRowsByObject(keyString);
@ -263,6 +289,10 @@ define([
delete this.telemetryObjects[keyString];
this.emit('object-removed', objectIdentifier);
this.stalenessSubscription[keyString].unsubscribe();
this.stalenessSubscription[keyString].stalenessUtils.destroy();
this.handleStaleness(keyString, { isStale: false }, SKIP_CHECK);
}
clearData() {
@ -368,6 +398,11 @@ define([
this.filterObserver();
}
Object.values(this.stalenessSubscription).forEach(stalenessSubscription => {
stalenessSubscription.unsubscribe();
stalenessSubscription.stalenessUtils.destroy();
});
if (this.tableComposition !== undefined) {
this.tableComposition.off('add', this.addTelemetryObject);
this.tableComposition.off('remove', this.removeTelemetryObject);

View File

@ -22,7 +22,7 @@
<template>
<div
class="c-table-wrapper"
:class="{ 'is-paused': paused }"
:class="tableClasses"
>
<div
v-if="enableLegacyToolbar"
@ -381,7 +381,8 @@ export default {
enableRegexSearch: {},
hideHeaders: configuration.hideHeaders,
totalNumberOfRows: 0,
rowContext: {}
rowContext: {},
staleObjects: []
};
},
computed: {
@ -416,6 +417,19 @@ export default {
}
return style;
},
tableClasses() {
let classes = [];
if (this.paused) {
classes.push('is-paused');
}
if (this.staleObjects.length !== 0) {
classes.push('is-stale');
}
return classes;
}
},
watch: {
@ -488,6 +502,7 @@ export default {
this.table.on('refresh', this.clearRowsAndRerender);
this.table.on('historical-rows-processed', this.checkForMarkedRows);
this.table.on('outstanding-requests', this.outstandingRequests);
this.table.on('telemetry-staleness', this.handleStaleness);
this.table.tableRows.on('add', this.rowsAdded);
this.table.tableRows.on('remove', this.rowsRemoved);
@ -516,6 +531,7 @@ export default {
this.table.off('historical-rows-processed', this.checkForMarkedRows);
this.table.off('refresh', this.clearRowsAndRerender);
this.table.off('outstanding-requests', this.outstandingRequests);
this.table.off('telemetry-staleness', this.handleStaleness);
this.table.tableRows.off('add', this.rowsAdded);
this.table.tableRows.off('remove', this.rowsRemoved);
@ -726,6 +742,18 @@ export default {
outstandingRequests(loading) {
this.loading = loading;
},
handleStaleness({ keyString, isStale }) {
const index = this.staleObjects.indexOf(keyString);
if (isStale) {
if (index === -1) {
this.staleObjects.push(keyString);
}
} else {
if (index !== -1) {
this.staleObjects.splice(index, 1);
}
}
},
calculateTableSize() {
this.$nextTick().then(this.calculateColumnWidths);
},

View File

@ -305,8 +305,8 @@ $colorIndicatorMenuFg: $colorHeadFg;
$colorIndicatorMenuFgHov: pullForward($colorHeadFg, 10%);
// Staleness
$colorTelemFresh: pullForward($colorBodyFg, 20%);
$colorTelemStale: pushBack($colorBodyFg, 20%);
$colorTelemStale: cyan;
$colorTelemStaleFg: #002A2A;
$styleTelemStale: italic;
// Limits

View File

@ -309,8 +309,8 @@ $colorIndicatorMenuFg: $colorHeadFg;
$colorIndicatorMenuFgHov: pullForward($colorHeadFg, 10%);
// Staleness
$colorTelemFresh: pullForward($colorBodyFg, 20%);
$colorTelemStale: pushBack($colorBodyFg, 20%);
$colorTelemStale: cyan;
$colorTelemStaleFg: #002A2A;
$styleTelemStale: italic;
// Limits

View File

@ -305,8 +305,8 @@ $colorIndicatorMenuFg: $colorHeadFg;
$colorIndicatorMenuFgHov: pullForward($colorHeadFg, 10%);
// Staleness
$colorTelemFresh: pullForward($colorBodyFg, 20%);
$colorTelemStale: pushBack($colorBodyFg, 20%);
$colorTelemStale: #00c9c9;
$colorTelemStaleFg: #002A2A;
$styleTelemStale: italic;
// Limits

View File

@ -164,6 +164,7 @@ $glyph-icon-status-poll-caution: '\e933';
$glyph-icon-status-poll-circle-slash: '\e934';
$glyph-icon-status-poll-question-mark: '\e935';
$glyph-icon-status-poll-edit: '\e936';
$glyph-icon-stale: '\e937';
$glyph-icon-arrows-right-left: '\ea00';
$glyph-icon-arrows-up-down: '\ea01';
$glyph-icon-bullet: '\ea02';

View File

@ -95,6 +95,7 @@
.icon-status-poll-circle-slash { @include glyphBefore($glyph-icon-status-poll-circle-slash); }
.icon-status-poll-question-mark { @include glyphBefore($glyph-icon-status-poll-question-mark); }
.icon-status-poll-edit { @include glyphBefore($glyph-icon-status-poll-edit); }
.icon-stale { @include glyphBefore($glyph-icon-stale); }
.icon-arrows-right-left { @include glyphBefore($glyph-icon-arrows-right-left); }
.icon-arrows-up-down { @include glyphBefore($glyph-icon-arrows-up-down); }
.icon-bullet { @include glyphBefore($glyph-icon-bullet); }

View File

@ -81,6 +81,10 @@ mct-plot {
overflow-x: hidden;
}
&.is-stale {
@include isStaleHolder();
}
.c-plot--stacked-container {
border: 1px solid transparent;
display: flex;
@ -89,6 +93,10 @@ mct-plot {
min-height: $plotMinH;
overflow: hidden;
.is-stale {
@include isStaleHolder();
}
&[s-selected] {
.is-editing & {
border: $editMarqueeBorder;
@ -596,6 +604,10 @@ mct-plot {
margin-left: $interiorMarginSm;
}
&.is-stale {
@include isStaleElement();
}
.plot-series-color-swatch {
@include colorSwatch();
display: inline-block;
@ -649,8 +661,10 @@ mct-plot {
// .plot-legend-item is a span of spans.
.plot-legend-item {
border-radius: $smallCr;
display: flex;
justify-content: stretch;
padding: 1px;
.plot-series-swatch-and-name,
.plot-series-value {

View File

@ -153,6 +153,37 @@
}
}
@mixin isStaleGlyph() {
content: $glyph-icon-stale;
display: block;
font-family: symbolsfont;
font-style: normal;
pointer-events: none;
}
@mixin isStaleHolder() {
// Applied to objects that frame content, like Display Layout frames, plots, etc.
border-radius: 3px;
border: 2px solid rgba($colorTelemStale, 0.8) !important;
&:before {
@include isStaleGlyph();
color: $colorTelemStale;
position: absolute;
bottom: 5px;
left: 3px;
width: 12px;
z-index: 10;
}
}
@mixin isStaleElement() {
// Applied directly to values, like LAD Table cells, alphanumerics, plot legend items
background: $colorTelemStale !important;
color: $colorTelemStaleFg !important;
font-style: italic;
}
@mixin isLimit() {
&[class*='is-limit'] {
&:before {

View File

@ -90,6 +90,10 @@ div.c-table {
flex: 1 1 auto;
}
&.is-stale {
@include isStaleHolder();
}
.--width-less-than-600 & {
&:not(.is-paused) {
.c-table-control-bar {
@ -179,12 +183,22 @@ div.c-table {
width: 100%;
height: 100%;
padding: $mainViewPad;
&.is-stale {
@include isStaleHolder();
}
}
.c-lad-table {
th, td {
width: 33%; // Needed to prevent size jumping as values dynamically update
}
td {
&.is-stale {
@include isStaleElement();
}
}
}
/************************************** TABLE AND SUMMARY VIEWS */

View File

@ -2,7 +2,7 @@
"metadata": {
"name": "Open MCT Symbols 16px",
"lastOpened": 0,
"created": 1660771219523
"created": 1674103729548
},
"iconSets": [
{
@ -447,13 +447,21 @@
"code": 59702,
"tempChar": ""
},
{
"order": 215,
"id": 185,
"name": "icon-stale",
"prevSize": 16,
"code": 59703,
"tempChar": ""
},
{
"order": 27,
"id": 105,
"name": "icon-arrows-right-left",
"prevSize": 16,
"code": 59904,
"tempChar": ""
"tempChar": ""
},
{
"order": 26,
@ -461,7 +469,7 @@
"name": "icon-arrows-up-down",
"prevSize": 16,
"code": 59905,
"tempChar": ""
"tempChar": ""
},
{
"order": 68,
@ -469,7 +477,7 @@
"name": "icon-bullet",
"prevSize": 16,
"code": 59906,
"tempChar": ""
"tempChar": ""
},
{
"order": 150,
@ -477,7 +485,7 @@
"prevSize": 16,
"code": 59907,
"name": "icon-calendar",
"tempChar": ""
"tempChar": ""
},
{
"order": 45,
@ -485,7 +493,7 @@
"name": "icon-chain-links",
"prevSize": 16,
"code": 59908,
"tempChar": ""
"tempChar": ""
},
{
"order": 73,
@ -493,7 +501,7 @@
"name": "icon-download",
"prevSize": 16,
"code": 59909,
"tempChar": ""
"tempChar": ""
},
{
"order": 39,
@ -501,7 +509,7 @@
"name": "icon-duplicate",
"prevSize": 16,
"code": 59910,
"tempChar": ""
"tempChar": ""
},
{
"order": 50,
@ -509,7 +517,7 @@
"name": "icon-folder-new",
"prevSize": 16,
"code": 59911,
"tempChar": ""
"tempChar": ""
},
{
"order": 138,
@ -517,7 +525,7 @@
"name": "icon-fullscreen-collapse",
"prevSize": 16,
"code": 59912,
"tempChar": ""
"tempChar": ""
},
{
"order": 139,
@ -525,7 +533,7 @@
"name": "icon-fullscreen-expand",
"prevSize": 16,
"code": 59913,
"tempChar": ""
"tempChar": ""
},
{
"order": 122,
@ -533,7 +541,7 @@
"name": "icon-layers",
"prevSize": 16,
"code": 59914,
"tempChar": ""
"tempChar": ""
},
{
"order": 151,
@ -541,7 +549,7 @@
"name": "icon-line-horz",
"prevSize": 16,
"code": 59915,
"tempChar": ""
"tempChar": ""
},
{
"order": 100,
@ -549,7 +557,7 @@
"name": "icon-magnify",
"prevSize": 16,
"code": 59916,
"tempChar": ""
"tempChar": ""
},
{
"order": 99,
@ -557,7 +565,7 @@
"name": "icon-magnify-in",
"prevSize": 16,
"code": 59917,
"tempChar": ""
"tempChar": ""
},
{
"order": 101,
@ -565,7 +573,7 @@
"name": "icon-magnify-out-v2",
"prevSize": 16,
"code": 59918,
"tempChar": ""
"tempChar": ""
},
{
"order": 103,
@ -573,7 +581,7 @@
"name": "icon-menu",
"prevSize": 16,
"code": 59919,
"tempChar": ""
"tempChar": ""
},
{
"order": 124,
@ -581,7 +589,7 @@
"name": "icon-move",
"prevSize": 16,
"code": 59920,
"tempChar": ""
"tempChar": ""
},
{
"order": 7,
@ -589,7 +597,7 @@
"name": "icon-new-window",
"prevSize": 16,
"code": 59921,
"tempChar": ""
"tempChar": ""
},
{
"order": 63,
@ -597,7 +605,7 @@
"name": "icon-paint-bucket-v2",
"prevSize": 16,
"code": 59922,
"tempChar": ""
"tempChar": ""
},
{
"order": 15,
@ -605,7 +613,7 @@
"name": "icon-pencil",
"prevSize": 16,
"code": 59923,
"tempChar": ""
"tempChar": ""
},
{
"order": 54,
@ -613,7 +621,7 @@
"name": "icon-pencil-edit-in-place",
"prevSize": 16,
"code": 59924,
"tempChar": ""
"tempChar": ""
},
{
"order": 40,
@ -621,7 +629,7 @@
"name": "icon-play",
"prevSize": 16,
"code": 59925,
"tempChar": ""
"tempChar": ""
},
{
"order": 125,
@ -629,7 +637,7 @@
"name": "icon-pause",
"prevSize": 16,
"code": 59926,
"tempChar": ""
"tempChar": ""
},
{
"order": 119,
@ -637,7 +645,7 @@
"name": "icon-plot-resource",
"prevSize": 16,
"code": 59927,
"tempChar": ""
"tempChar": ""
},
{
"order": 48,
@ -645,7 +653,7 @@
"name": "icon-pointer-left",
"prevSize": 16,
"code": 59928,
"tempChar": ""
"tempChar": ""
},
{
"order": 47,
@ -653,7 +661,7 @@
"name": "icon-pointer-right",
"prevSize": 16,
"code": 59929,
"tempChar": ""
"tempChar": ""
},
{
"order": 85,
@ -661,7 +669,7 @@
"name": "icon-refresh",
"prevSize": 16,
"code": 59930,
"tempChar": ""
"tempChar": ""
},
{
"order": 55,
@ -669,7 +677,7 @@
"name": "icon-save",
"prevSize": 16,
"code": 59931,
"tempChar": ""
"tempChar": ""
},
{
"order": 56,
@ -677,7 +685,7 @@
"name": "icon-save-as",
"prevSize": 16,
"code": 59932,
"tempChar": ""
"tempChar": ""
},
{
"order": 58,
@ -685,7 +693,7 @@
"name": "icon-sine",
"prevSize": 16,
"code": 59933,
"tempChar": ""
"tempChar": ""
},
{
"order": 113,
@ -693,7 +701,7 @@
"name": "icon-font",
"prevSize": 16,
"code": 59934,
"tempChar": ""
"tempChar": ""
},
{
"order": 41,
@ -701,7 +709,7 @@
"name": "icon-thumbs-strip",
"prevSize": 16,
"code": 59935,
"tempChar": ""
"tempChar": ""
},
{
"order": 146,
@ -709,7 +717,7 @@
"name": "icon-two-parts-both",
"prevSize": 16,
"code": 59936,
"tempChar": ""
"tempChar": ""
},
{
"order": 145,
@ -717,7 +725,7 @@
"name": "icon-two-parts-one-only",
"prevSize": 16,
"code": 59937,
"tempChar": ""
"tempChar": ""
},
{
"order": 82,
@ -725,7 +733,7 @@
"name": "icon-resync",
"prevSize": 16,
"code": 59938,
"tempChar": ""
"tempChar": ""
},
{
"order": 86,
@ -733,7 +741,7 @@
"name": "icon-reset",
"prevSize": 16,
"code": 59939,
"tempChar": ""
"tempChar": ""
},
{
"order": 61,
@ -741,7 +749,7 @@
"name": "icon-x-in-circle",
"prevSize": 16,
"code": 59940,
"tempChar": ""
"tempChar": ""
},
{
"order": 84,
@ -749,7 +757,7 @@
"name": "icon-brightness",
"prevSize": 16,
"code": 59941,
"tempChar": ""
"tempChar": ""
},
{
"order": 83,
@ -757,7 +765,7 @@
"name": "icon-contrast",
"prevSize": 16,
"code": 59942,
"tempChar": ""
"tempChar": ""
},
{
"order": 87,
@ -765,7 +773,7 @@
"name": "icon-expand",
"prevSize": 16,
"code": 59943,
"tempChar": ""
"tempChar": ""
},
{
"order": 89,
@ -773,7 +781,7 @@
"name": "icon-list-view",
"prevSize": 16,
"code": 59944,
"tempChar": ""
"tempChar": ""
},
{
"order": 133,
@ -781,7 +789,7 @@
"name": "icon-grid-snap-to",
"prevSize": 16,
"code": 59945,
"tempChar": ""
"tempChar": ""
},
{
"order": 132,
@ -789,7 +797,7 @@
"name": "icon-grid-snap-no",
"prevSize": 16,
"code": 59946,
"tempChar": ""
"tempChar": ""
},
{
"order": 94,
@ -797,7 +805,7 @@
"name": "icon-frame-show",
"prevSize": 16,
"code": 59947,
"tempChar": ""
"tempChar": ""
},
{
"order": 95,
@ -805,7 +813,7 @@
"name": "icon-frame-hide",
"prevSize": 16,
"code": 59948,
"tempChar": ""
"tempChar": ""
},
{
"order": 97,
@ -813,7 +821,7 @@
"name": "icon-import",
"prevSize": 16,
"code": 59949,
"tempChar": ""
"tempChar": ""
},
{
"order": 96,
@ -821,7 +829,7 @@
"name": "icon-export",
"prevSize": 16,
"code": 59950,
"tempChar": ""
"tempChar": ""
},
{
"order": 194,
@ -829,7 +837,7 @@
"name": "icon-font-size",
"prevSize": 16,
"code": 59951,
"tempChar": ""
"tempChar": ""
},
{
"order": 163,
@ -837,7 +845,7 @@
"name": "icon-clear-data",
"prevSize": 16,
"code": 59952,
"tempChar": ""
"tempChar": ""
},
{
"order": 173,
@ -845,7 +853,7 @@
"name": "icon-history",
"prevSize": 16,
"code": 59953,
"tempChar": ""
"tempChar": ""
},
{
"order": 181,
@ -853,7 +861,7 @@
"name": "icon-arrow-up-to-parent",
"prevSize": 16,
"code": 59954,
"tempChar": ""
"tempChar": ""
},
{
"order": 184,
@ -861,7 +869,7 @@
"name": "icon-crosshair-in-circle",
"prevSize": 16,
"code": 59955,
"tempChar": ""
"tempChar": ""
},
{
"order": 185,
@ -869,7 +877,7 @@
"name": "icon-target",
"prevSize": 16,
"code": 59956,
"tempChar": ""
"tempChar": ""
},
{
"order": 187,
@ -877,7 +885,7 @@
"name": "icon-items-collapse",
"prevSize": 16,
"code": 59957,
"tempChar": ""
"tempChar": ""
},
{
"order": 188,
@ -885,7 +893,7 @@
"name": "icon-items-expand",
"prevSize": 16,
"code": 59958,
"tempChar": ""
"tempChar": ""
},
{
"order": 190,
@ -893,7 +901,7 @@
"name": "icon-3-dots",
"prevSize": 16,
"code": 59959,
"tempChar": ""
"tempChar": ""
},
{
"order": 193,
@ -901,7 +909,7 @@
"name": "icon-grid-on",
"prevSize": 16,
"code": 59960,
"tempChar": ""
"tempChar": ""
},
{
"order": 192,
@ -909,7 +917,7 @@
"name": "icon-grid-off",
"prevSize": 16,
"code": 59961,
"tempChar": ""
"tempChar": ""
},
{
"order": 191,
@ -917,7 +925,7 @@
"name": "icon-camera",
"prevSize": 16,
"code": 59962,
"tempChar": ""
"tempChar": ""
},
{
"order": 196,
@ -925,7 +933,7 @@
"name": "icon-folders-collapse",
"prevSize": 16,
"code": 59963,
"tempChar": ""
"tempChar": ""
},
{
"order": 144,
@ -933,7 +941,7 @@
"name": "icon-activity",
"prevSize": 16,
"code": 60160,
"tempChar": ""
"tempChar": ""
},
{
"order": 104,
@ -941,7 +949,7 @@
"name": "icon-activity-mode",
"prevSize": 16,
"code": 60161,
"tempChar": ""
"tempChar": ""
},
{
"order": 137,
@ -949,7 +957,7 @@
"name": "icon-autoflow-tabular",
"prevSize": 16,
"code": 60162,
"tempChar": ""
"tempChar": ""
},
{
"order": 115,
@ -957,7 +965,7 @@
"name": "icon-clock",
"prevSize": 16,
"code": 60163,
"tempChar": ""
"tempChar": ""
},
{
"order": 2,
@ -965,7 +973,7 @@
"name": "icon-database",
"prevSize": 16,
"code": 60164,
"tempChar": ""
"tempChar": ""
},
{
"order": 3,
@ -973,7 +981,7 @@
"name": "icon-database-query",
"prevSize": 16,
"code": 60165,
"tempChar": ""
"tempChar": ""
},
{
"order": 67,
@ -981,7 +989,7 @@
"name": "icon-dataset",
"prevSize": 16,
"code": 60166,
"tempChar": ""
"tempChar": ""
},
{
"order": 59,
@ -989,7 +997,7 @@
"name": "icon-datatable",
"prevSize": 16,
"code": 60167,
"tempChar": ""
"tempChar": ""
},
{
"order": 136,
@ -997,7 +1005,7 @@
"name": "icon-dictionary",
"prevSize": 16,
"code": 60168,
"tempChar": ""
"tempChar": ""
},
{
"order": 51,
@ -1005,7 +1013,7 @@
"name": "icon-folder",
"prevSize": 16,
"code": 60169,
"tempChar": ""
"tempChar": ""
},
{
"order": 147,
@ -1013,7 +1021,7 @@
"name": "icon-image",
"prevSize": 16,
"code": 60170,
"tempChar": ""
"tempChar": ""
},
{
"order": 4,
@ -1021,7 +1029,7 @@
"name": "icon-layout",
"prevSize": 16,
"code": 60171,
"tempChar": ""
"tempChar": ""
},
{
"order": 24,
@ -1029,7 +1037,7 @@
"name": "icon-object",
"prevSize": 16,
"code": 60172,
"tempChar": ""
"tempChar": ""
},
{
"order": 52,
@ -1037,7 +1045,7 @@
"name": "icon-object-unknown",
"prevSize": 16,
"code": 60173,
"tempChar": ""
"tempChar": ""
},
{
"order": 105,
@ -1045,7 +1053,7 @@
"name": "icon-packet",
"prevSize": 16,
"code": 60174,
"tempChar": ""
"tempChar": ""
},
{
"order": 126,
@ -1053,7 +1061,7 @@
"name": "icon-page",
"prevSize": 16,
"code": 60175,
"tempChar": ""
"tempChar": ""
},
{
"order": 130,
@ -1061,7 +1069,7 @@
"name": "icon-plot-overlay",
"prevSize": 16,
"code": 60176,
"tempChar": ""
"tempChar": ""
},
{
"order": 80,
@ -1069,7 +1077,7 @@
"name": "icon-plot-stacked",
"prevSize": 16,
"code": 60177,
"tempChar": ""
"tempChar": ""
},
{
"order": 134,
@ -1077,7 +1085,7 @@
"name": "icon-session",
"prevSize": 16,
"code": 60178,
"tempChar": ""
"tempChar": ""
},
{
"order": 109,
@ -1085,7 +1093,7 @@
"name": "icon-tabular",
"prevSize": 16,
"code": 60179,
"tempChar": ""
"tempChar": ""
},
{
"order": 107,
@ -1093,7 +1101,7 @@
"name": "icon-tabular-lad",
"prevSize": 16,
"code": 60180,
"tempChar": ""
"tempChar": ""
},
{
"order": 106,
@ -1101,7 +1109,7 @@
"name": "icon-tabular-lad-set",
"prevSize": 16,
"code": 60181,
"tempChar": ""
"tempChar": ""
},
{
"order": 70,
@ -1109,7 +1117,7 @@
"name": "icon-tabular-realtime",
"prevSize": 16,
"code": 60182,
"tempChar": ""
"tempChar": ""
},
{
"order": 60,
@ -1117,7 +1125,7 @@
"name": "icon-tabular-scrolling",
"prevSize": 16,
"code": 60183,
"tempChar": ""
"tempChar": ""
},
{
"order": 131,
@ -1125,7 +1133,7 @@
"name": "icon-telemetry",
"prevSize": 16,
"code": 60184,
"tempChar": ""
"tempChar": ""
},
{
"order": 202,
@ -1133,7 +1141,7 @@
"name": "icon-timeline",
"prevSize": 16,
"code": 60185,
"tempChar": ""
"tempChar": ""
},
{
"order": 81,
@ -1141,7 +1149,7 @@
"name": "icon-timer",
"prevSize": 16,
"code": 60186,
"tempChar": ""
"tempChar": ""
},
{
"order": 69,
@ -1149,7 +1157,7 @@
"name": "icon-topic",
"prevSize": 16,
"code": 60187,
"tempChar": ""
"tempChar": ""
},
{
"order": 79,
@ -1157,7 +1165,7 @@
"name": "icon-box-with-dashed-lines-v2",
"prevSize": 16,
"code": 60188,
"tempChar": ""
"tempChar": ""
},
{
"order": 90,
@ -1165,7 +1173,7 @@
"name": "icon-summary-widget",
"prevSize": 16,
"code": 60189,
"tempChar": ""
"tempChar": ""
},
{
"order": 92,
@ -1173,7 +1181,7 @@
"name": "icon-notebook",
"prevSize": 16,
"code": 60190,
"tempChar": ""
"tempChar": ""
},
{
"order": 168,
@ -1181,7 +1189,7 @@
"name": "icon-tabs-view",
"prevSize": 16,
"code": 60191,
"tempChar": ""
"tempChar": ""
},
{
"order": 117,
@ -1189,7 +1197,7 @@
"name": "icon-flexible-layout",
"prevSize": 16,
"code": 60192,
"tempChar": ""
"tempChar": ""
},
{
"order": 166,
@ -1197,7 +1205,7 @@
"name": "icon-generator-sine",
"prevSize": 16,
"code": 60193,
"tempChar": ""
"tempChar": ""
},
{
"order": 167,
@ -1205,7 +1213,7 @@
"name": "icon-generator-event",
"prevSize": 16,
"code": 60194,
"tempChar": ""
"tempChar": ""
},
{
"order": 165,
@ -1213,7 +1221,7 @@
"name": "icon-gauge-v2",
"prevSize": 16,
"code": 60195,
"tempChar": ""
"tempChar": ""
},
{
"order": 170,
@ -1221,7 +1229,7 @@
"name": "icon-spectra",
"prevSize": 16,
"code": 60196,
"tempChar": ""
"tempChar": ""
},
{
"order": 171,
@ -1229,7 +1237,7 @@
"name": "icon-telemetry-spectra",
"prevSize": 16,
"code": 60197,
"tempChar": ""
"tempChar": ""
},
{
"order": 172,
@ -1237,7 +1245,7 @@
"name": "icon-pushbutton",
"prevSize": 16,
"code": 60198,
"tempChar": ""
"tempChar": ""
},
{
"order": 174,
@ -1245,7 +1253,7 @@
"name": "icon-conditional",
"prevSize": 16,
"code": 60199,
"tempChar": ""
"tempChar": ""
},
{
"order": 178,
@ -1253,7 +1261,7 @@
"name": "icon-condition-widget",
"prevSize": 16,
"code": 60200,
"tempChar": ""
"tempChar": ""
},
{
"order": 180,
@ -1261,7 +1269,7 @@
"name": "icon-alphanumeric",
"prevSize": 16,
"code": 60201,
"tempChar": ""
"tempChar": ""
},
{
"order": 183,
@ -1269,7 +1277,7 @@
"name": "icon-image-telemetry",
"prevSize": 16,
"code": 60202,
"tempChar": ""
"tempChar": ""
},
{
"order": 198,
@ -1277,7 +1285,7 @@
"name": "icon-telemetry-aggregate",
"prevSize": 16,
"code": 60203,
"tempChar": ""
"tempChar": ""
},
{
"order": 199,
@ -1285,7 +1293,7 @@
"name": "icon-bar-graph",
"prevSize": 16,
"code": 60204,
"tempChar": ""
"tempChar": ""
},
{
"order": 200,
@ -1293,7 +1301,7 @@
"name": "icon-map",
"prevSize": 16,
"code": 60205,
"tempChar": ""
"tempChar": ""
},
{
"order": 203,
@ -1301,7 +1309,7 @@
"name": "icon-plan",
"prevSize": 16,
"code": 60206,
"tempChar": ""
"tempChar": ""
},
{
"order": 204,
@ -1309,14 +1317,6 @@
"name": "icon-timelist",
"prevSize": 16,
"code": 60207,
"tempChar": ""
},
{
"order": 214,
"id": 184,
"name": "icon-notebook-restricted",
"prevSize": 16,
"code": 60209,
"tempChar": ""
},
{
@ -1326,6 +1326,14 @@
"prevSize": 16,
"code": 60208,
"tempChar": ""
},
{
"order": 214,
"id": 184,
"name": "icon-notebook-restricted",
"prevSize": 16,
"code": 60209,
"tempChar": ""
}
],
"id": 0,
@ -2327,6 +2335,26 @@
]
}
},
{
"id": 186,
"paths": [
"M832 0h-640c-105.6 0-192 86.4-192 192v640c0 105.6 86.4 192 192 192h640c105.6 0 192-86.4 192-192v-640c0-105.6-86.4-192-192-192zM681.38 365.14c0.68-20.46-2.22-37.7-8.7-51.68-6.5-13.98-15.7-25.4-27.64-34.28-11.94-8.86-26.1-15.18-42.48-18.94-16.38-3.74-33.78-5.62-52.2-5.62-15.020 0-30.2 1.54-45.54 4.6s-29.16 8.18-41.44 15.36-22.18 16.56-29.68 28.14c-7.52 11.62-11.26 25.94-11.26 42.98s6.66 32.6 19.96 44.52c13.3 11.94 29.34 21.84 48.1 29.68 18.76 7.86 38.020 14 57.82 18.42 19.78 4.44 35.82 8.020 48.1 10.74 28.66 7.52 54.92 16.22 78.8 26.1 23.88 9.9 44.52 22.68 61.92 38.38s30.86 34.8 40.42 57.32c9.54 22.52 14.32 50.16 14.32 82.9 0 43.68-9.040 80.86-27.12 111.56s-41.28 55.62-69.6 74.7c-28.32 19.1-60.22 32.92-95.7 41.44s-70.62 12.8-105.42 12.8c-102.34 0-178.6-20.8-228.74-62.44-50.16-41.6-75.22-107.1-75.22-196.5h152.5c-1.38 25.94 1.7 47.58 9.22 64.98 7.5 17.4 18.42 31.22 32.74 41.44 14.32 10.24 31.38 17.58 51.18 22 19.78 4.44 41.28 6.66 64.48 6.66 16.38 0 32.74-2.040 49.12-6.14s31.22-10.24 44.52-18.42c13.3-8.18 24.22-18.76 32.76-31.72 8.52-12.94 12.8-28.66 12.8-47.080s-5.46-32.24-16.38-43.5c-10.92-11.26-25.080-20.98-42.48-29.16s-37.2-15.36-59.36-21.5c-22.18-6.14-44.52-12.62-67.040-19.44-23.2-6.82-45.72-15-67.54-24.56-21.84-9.54-41.44-21.82-58.84-36.84-17.4-15-31.38-33.42-41.96-55.26-10.58-21.82-15.86-48.44-15.86-79.82 0-40.94 8.52-75.74 25.58-104.4 17.040-28.66 39.22-52.020 66.52-70.1 27.28-18.080 58.16-31.38 92.62-39.92 34.44-8.52 69.42-12.8 104.9-12.8 37.52 0 72.82 4.26 105.92 12.8 33.080 8.54 62.080 22.36 87 41.44 24.9 19.1 44.68 43.5 59.36 73.18 14.66 29.68 22 65.68 22 107.98h-152.5z"
],
"attrs": [
{}
],
"isMulticolor": false,
"isMulticolor2": false,
"grid": 16,
"tags": [
"icon-stale"
],
"colorPermutations": {
"12552552551": [
{}
]
}
},
{
"id": 105,
"paths": [
@ -4237,6 +4265,26 @@
]
}
},
{
"id": 185,
"paths": [
"M192 0c-105.6 0-192 86.4-192 192v640c0 105.6 86.4 192 192 192h640c105.6 0 192-86.4 192-192v-640c0-105.6-86.4-192-192-192zM128 352c0-53.019 42.981-96 96-96s96 42.981 96 96c0 53.019-42.981 96-96 96v0c-53.019 0-96-42.981-96-96v0zM288 832c-53.019 0-96-42.981-96-96s42.981-96 96-96c53.019 0 96 42.981 96 96v0c0 53.019-42.981 96-96 96v0zM544 640c-53.019 0-96-42.981-96-96s42.981-96 96-96c53.019 0 96 42.981 96 96v0c0 53.019-42.981 96-96 96v0zM544 320c-53.019 0-96-42.981-96-96s42.981-96 96-96c53.019 0 96 42.981 96 96v0c0 53.019-42.981 96-96 96v0zM800 832c-53.019 0-96-42.981-96-96s42.981-96 96-96c53.019 0 96 42.981 96 96v0c0 53.019-42.981 96-96 96v0z"
],
"attrs": [
{}
],
"isMulticolor": false,
"isMulticolor2": false,
"grid": 16,
"tags": [
"icon-plot-scatter"
],
"colorPermutations": {
"12552552551": [
{}
]
}
},
{
"id": 184,
"paths": [
@ -4259,26 +4307,6 @@
{}
]
}
},
{
"id": 185,
"paths": [
"M192 0c-105.6 0-192 86.4-192 192v640c0 105.6 86.4 192 192 192h640c105.6 0 192-86.4 192-192v-640c0-105.6-86.4-192-192-192zM128 352c0-53.019 42.981-96 96-96s96 42.981 96 96c0 53.019-42.981 96-96 96v0c-53.019 0-96-42.981-96-96v0zM288 832c-53.019 0-96-42.981-96-96s42.981-96 96-96c53.019 0 96 42.981 96 96v0c0 53.019-42.981 96-96 96v0zM544 640c-53.019 0-96-42.981-96-96s42.981-96 96-96c53.019 0 96 42.981 96 96v0c0 53.019-42.981 96-96 96v0zM544 320c-53.019 0-96-42.981-96-96s42.981-96 96-96c53.019 0 96 42.981 96 96v0c0 53.019-42.981 96-96 96v0zM800 832c-53.019 0-96-42.981-96-96s42.981-96 96-96c53.019 0 96 42.981 96 96v0c0 53.019-42.981 96-96 96v0z"
],
"attrs": [
{}
],
"isMulticolor": false,
"isMulticolor2": false,
"grid": 16,
"tags": [
"icon-plot-scatter"
],
"colorPermutations": {
"12552552551": [
{}
]
}
}
],
"invisible": false,

View File

@ -62,6 +62,7 @@
<glyph unicode="&#xe934;" glyph-name="icon-status-poll-circle-slash" d="M391.18 227.3c35.72-22.98 77.32-35.3 120.82-35.3 59.84 0 116.080 23.3 158.4 65.6 42.3 42.3 65.6 98.56 65.6 158.4 0 43.5-12.32 85.080-35.3 120.82l-309.52-309.52zM512 640c-59.84 0-116.080-23.3-158.4-65.6-42.3-42.3-65.6-98.56-65.6-158.4 0-43.5 12.32-85.080 35.3-120.82l309.52 309.52c-35.72 22.98-77.32 35.3-120.82 35.3zM512 896c-282.76 0-512-214.9-512-480 0-92.26 27.8-178.44 75.92-251.6l-75.92-292.4 313.5 101.42c61.040-24.1 128.12-37.42 198.5-37.42 282.76 0 512 214.9 512 480s-229.24 480-512 480zM512 96c-176.74 0-320 143.26-320 320s143.26 320 320 320 320-143.26 320-320-143.26-320-320-320z" />
<glyph unicode="&#xe935;" glyph-name="icon-status-poll-question-mark" d="M512 896c-282.76 0-512-214.9-512-480 0-92.26 27.8-178.44 75.92-251.6l-75.92-292.4 313.5 101.42c61.040-24.1 128.12-37.42 198.5-37.42 282.76 0 512 214.9 512 480s-229.24 480-512 480zM579.020 64h-141.36v136.64h141.36v-136.64zM713.84 462.1c-11.94-17.020-34.9-38.78-68.84-65.24l-33.48-26c-18.24-14.18-30.34-30.74-36.32-49.64-3.78-11.98-5.82-30.58-6.14-55.8h-128.12c1.88 53.26 6.92 90.060 15.080 110.4 8.18 20.34 29.22 43.74 63.16 70.22l34.42 26.94c11.3 8.52 20.42 17.8 27.34 27.9 12.56 17.34 18.86 36.4 18.86 57.2 0 23.94-7 45.78-20.98 65.48-14 19.7-39.54 29.54-76.64 29.54s-62.34-12.14-77.6-36.4c-15.24-24.28-22.88-49.48-22.88-75.64h-136.64c3.78 89.84 35.14 153.5 94.080 191.020 37.18 23.94 82.9 35.94 137.12 35.94 71.22 0 130.42-17.020 177.54-51.060s70.68-84.48 70.68-151.3c0-40.98-10.22-75.5-30.66-103.54z" />
<glyph unicode="&#xe936;" glyph-name="icon-status-poll-edit" d="M1000.080 561.36l-336.6-336.76-20.52-6.88-450.96-153.72 160.68 471.52 332.34 332.34c-54.040 18.2-112.28 28.14-173.020 28.14-282.76 0-512-214.9-512-480 0-92.26 27.8-178.44 75.92-251.6l-75.92-292.4 313.5 101.42c61.040-24.1 128.12-37.42 198.5-37.42 282.76 0 512 214.9 512 480 0 50.68-8.4 99.5-23.92 145.36zM408.42 500.76l-2.16-6.3-111.7-327.9 334.12 113.86 4.62 4.68 350.28 350.28c6.8 6.78 14.96 19.1 14.96 38.9 0 34.86-26.82 83.28-69.88 126.38-26.54 26.54-55.9 47.6-82.7 59.34-47.34 20.8-72.020 6.24-82.64-4.36l-354.9-354.88zM470.56 474.58h44v-88h88v-44l-4.7-12.72-139.68-47.54-47.94 47.94 47.6 139.72 12.72 4.6z" />
<glyph unicode="&#xe937;" glyph-name="icon-stale" d="M832 896h-640c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v640c0 105.6-86.4 192-192 192zM681.38 530.86c0.68 20.46-2.22 37.7-8.7 51.68-6.5 13.98-15.7 25.4-27.64 34.28-11.94 8.86-26.1 15.18-42.48 18.94-16.38 3.74-33.78 5.62-52.2 5.62-15.020 0-30.2-1.54-45.54-4.6s-29.16-8.18-41.44-15.36-22.18-16.56-29.68-28.14c-7.52-11.62-11.26-25.94-11.26-42.98s6.66-32.6 19.96-44.52c13.3-11.94 29.34-21.84 48.1-29.68 18.76-7.86 38.020-14 57.82-18.42 19.78-4.44 35.82-8.020 48.1-10.74 28.66-7.52 54.92-16.22 78.8-26.1 23.88-9.9 44.52-22.68 61.92-38.38s30.86-34.8 40.42-57.32c9.54-22.52 14.32-50.16 14.32-82.9 0-43.68-9.040-80.86-27.12-111.56s-41.28-55.62-69.6-74.7c-28.32-19.1-60.22-32.92-95.7-41.44s-70.62-12.8-105.42-12.8c-102.34 0-178.6 20.8-228.74 62.44-50.16 41.6-75.22 107.1-75.22 196.5h152.5c-1.38-25.94 1.7-47.58 9.22-64.98 7.5-17.4 18.42-31.22 32.74-41.44 14.32-10.24 31.38-17.58 51.18-22 19.78-4.44 41.28-6.66 64.48-6.66 16.38 0 32.74 2.040 49.12 6.14s31.22 10.24 44.52 18.42c13.3 8.18 24.22 18.76 32.76 31.72 8.52 12.94 12.8 28.66 12.8 47.080s-5.46 32.24-16.38 43.5c-10.92 11.26-25.080 20.98-42.48 29.16s-37.2 15.36-59.36 21.5c-22.18 6.14-44.52 12.62-67.040 19.44-23.2 6.82-45.72 15-67.54 24.56-21.84 9.54-41.44 21.82-58.84 36.84-17.4 15-31.38 33.42-41.96 55.26-10.58 21.82-15.86 48.44-15.86 79.82 0 40.94 8.52 75.74 25.58 104.4 17.040 28.66 39.22 52.020 66.52 70.1 27.28 18.080 58.16 31.38 92.62 39.92 34.44 8.52 69.42 12.8 104.9 12.8 37.52 0 72.82-4.26 105.92-12.8 33.080-8.54 62.080-22.36 87-41.44 24.9-19.1 44.68-43.5 59.36-73.18 14.66-29.68 22-65.68 22-107.98h-152.5z" />
<glyph unicode="&#xea00;" glyph-name="icon-arrows-right-left" d="M1024 384l-448-512v1024zM448 896l-448-512 448-512z" />
<glyph unicode="&#xea01;" glyph-name="icon-arrows-up-down" d="M512 896l512-448h-1024zM0 320l512-448 512 448z" />
<glyph unicode="&#xea02;" glyph-name="icon-bullet" d="M832 144c0-44-36-80-80-80h-480c-44 0-80 36-80 80v480c0 44 36 80 80 80h480c44 0 80-36 80-80v-480z" />

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View File

@ -14,7 +14,7 @@
<div
ref="objectViewWrapper"
class="c-object-view"
:class="objectTypeClass"
:class="viewClasses"
></div>
</div>
</template>
@ -24,6 +24,7 @@ import _ from "lodash";
import StyleRuleManager from "@/plugins/condition/StyleRuleManager";
import {STYLE_CONSTANTS} from "@/plugins/condition/utils/constants";
import IndependentTimeConductor from '@/plugins/timeConductor/independent/IndependentTimeConductor.vue';
import stalenessMixin from '@/ui/mixins/staleness-mixin';
const SupportedViewTypes = [
'plot-stacked',
@ -36,6 +37,7 @@ export default {
components: {
IndependentTimeConductor
},
mixins: [stalenessMixin],
inject: ["openmct"],
props: {
showEditView: Boolean,
@ -85,8 +87,14 @@ export default {
return this.domainObject && SupportedViewTypes.includes(viewKey);
},
objectTypeClass() {
return this.domainObject && ('is-object-type-' + this.domainObject.type);
viewClasses() {
let classes;
if (this.domainObject) {
classes = `is-object-type-${this.domainObject.type} ${this.isStale ? 'is-stale' : ''}`;
}
return classes;
}
},
destroyed() {
@ -128,6 +136,7 @@ export default {
if (this.domainObject) {
//This is to apply styles to subobjects in a layout
this.initObjectStyles();
this.triggerStalenessSubscribe(this.domainObject);
}
},
methods: {
@ -161,6 +170,9 @@ export default {
this.composition._destroy();
}
this.isStale = false;
this.triggerUnsubscribeFromStaleness();
this.openmct.objectViews.off('clearData', this.clearData);
},
getStyleReceiver() {
@ -192,6 +204,11 @@ export default {
this.clear();
this.updateView(true);
},
triggerStalenessSubscribe(object) {
if (this.openmct.telemetry.isTelemetryObject(object)) {
this.subscribeToStaleness(object);
}
},
updateStyle(styleObj) {
let elemToStyle = this.getStyleReceiver();
@ -306,6 +323,7 @@ export default {
this.updateView(immediatelySelect);
this.triggerStalenessSubscribe(this.domainObject);
this.initObjectStyles();
},
initObjectStyles() {

View File

@ -320,6 +320,10 @@
display: block;
height: 100%;
overflow: auto;
&.is-stale {
@include isStaleHolder();
}
}
.is-editing {

View File

@ -0,0 +1,68 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import StalenessUtils from '@/utils/staleness';
export default {
data() {
return {
isStale: false
};
},
beforeDestroy() {
this.triggerUnsubscribeFromStaleness();
},
methods: {
subscribeToStaleness(domainObject, callback) {
if (!this.stalenessUtils) {
this.stalenessUtils = new StalenessUtils(this.openmct, domainObject);
}
this.requestStaleness(domainObject);
this.unsubscribeFromStaleness = this.openmct.telemetry.subscribeToStaleness(domainObject, (stalenessResponse) => {
this.handleStalenessResponse(stalenessResponse, callback);
});
},
async requestStaleness(domainObject) {
const stalenessResponse = await this.openmct.telemetry.isStale(domainObject);
if (stalenessResponse !== undefined) {
this.handleStalenessResponse(stalenessResponse);
}
},
handleStalenessResponse(stalenessResponse, callback) {
if (this.stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {
if (typeof callback === 'function') {
callback(stalenessResponse.isStale);
} else {
this.isStale = stalenessResponse.isStale;
}
}
},
triggerUnsubscribeFromStaleness() {
if (this.unsubscribeFromStaleness) {
this.unsubscribeFromStaleness();
delete this.unsubscribeFromStaleness;
this.stalenessUtils.destroy();
}
}
}
};

76
src/utils/staleness.js Normal file
View File

@ -0,0 +1,76 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default class StalenessUtils {
constructor(openmct, domainObject) {
this.openmct = openmct;
this.domainObject = domainObject;
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
this.lastStalenessResponseTime = 0;
this.setTimeSystem(this.openmct.time.timeSystem());
this.watchTimeSystem();
}
shouldUpdateStaleness(stalenessResponse, id) {
const stalenessResponseTime = this.parseTime(stalenessResponse);
if (stalenessResponseTime > this.lastStalenessResponseTime) {
this.lastStalenessResponseTime = stalenessResponseTime;
return true;
} else {
return false;
}
}
watchTimeSystem() {
this.openmct.time.on('timeSystem', this.setTimeSystem, this);
}
unwatchTimeSystem() {
this.openmct.time.off('timeSystem', this.setTimeSystem, this);
}
setTimeSystem(timeSystem) {
let metadataValue = { format: timeSystem.key };
if (this.metadata) {
metadataValue = this.metadata.value(timeSystem.key) ?? metadataValue;
}
const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
this.parseTime = (stalenessResponse) => {
const stalenessDatum = {
...stalenessResponse,
source: stalenessResponse[timeSystem.key]
};
return valueFormatter.parse(stalenessDatum);
};
}
destroy() {
this.unwatchTimeSystem();
}
}