Compare commits

...

7 Commits

Author SHA1 Message Date
Andrew Henry
ee80c5c14e de-reactify all the things 2022-07-07 15:00:39 -07:00
rukmini-bose
eacbac6aad Fix for Fault Management Visual Bugs (#5376)
* Closes #5365
* General visual improvements

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-07-07 20:56:54 +00:00
Jesse Mazzella
69153fe8f0 [CouchDB] Always subscribe to the CouchDB changes feed (#5434)
* Add unknown state, remove maintenance state

* Handle all CouchDB status codes

- Set unknown status if we receive an unhandled code

* Include status code in error messages

* SharedWorker can send unknown status

* Add test for unknown status

* Always subscribe to CouchDB changes feed

- Always subscribe to the CouchDB changes feed, even if there are no observable objects, since we are also checking the status of CouchDB via this feed.

* Update indicator status if not using SharedWorker

* Start listening to changes feed on first request

* fix test

* adjust test to hopefully avoid race condition

* lint

Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Scott Bell <scott@traclabs.com>
2022-07-07 14:30:30 -05:00
John Hill
51196530fd No gauge (#5451)
* Installed gauge plugin by default
* Make gauge part of standard install in e2e suite and add restrictednotebook

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-07-07 11:04:50 -07:00
Andrew Henry
fefa46ce7e Debounce status summary (#5448)
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-07-07 09:34:31 -07:00
Scott Bell
e08ab8ef24 fix sourcemaps (#5373)
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-07-07 08:19:35 -07:00
Jamie V
7011877e64 [Telemetry Collections] Respect "Latest" Strategy Option (#5421)
* Respect latest strategy in Telemetry Collections to limit potential memory growth.
2022-07-06 16:53:41 -07:00
19 changed files with 531 additions and 442 deletions

View File

@@ -1,30 +0,0 @@
/*****************************************************************************
* 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.
*****************************************************************************/
// this will be called from the test suite with
// await page.addInitScript({ path: path.join(__dirname, 'addInitGauge.js') });
// it will install the Gauge since it is not installed by default
document.addEventListener('DOMContentLoaded', () => {
const openmct = window.openmct;
openmct.install(openmct.plugins.Gauge());
});

View File

@@ -1,22 +0,0 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "mct-tree-expanded",
"value": "[]"
},
{
"name": "tcHistory",
"value": "{\"utc\":[{\"start\":1656473493306,\"end\":1656475293306},{\"start\":1655769110258,\"end\":1655770910258},{\"start\":1652301954635,\"end\":1652303754635}]}"
},
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"18ba28bf-152e-4e0f-9b9c-638fb2ade0c3\",\"namespace\":\"\"},{\"key\":\"fa64bd6c-9351-4d94-a54e-e062a93be3b6\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1656475294042,\"modified\":1656475294042},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002},\"18ba28bf-152e-4e0f-9b9c-638fb2ade0c3\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"18ba28bf-152e-4e0f-9b9c-638fb2ade0c3\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"43cfb4b1-348c-43c0-a681-c4cf53b5335f\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1655770911020,\"location\":\"mine\",\"persisted\":1655770911020},\"fa64bd6c-9351-4d94-a54e-e062a93be3b6\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"fa64bd6c-9351-4d94-a54e-e062a93be3b6\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"26739ce0-9a56-466c-91dd-f08bd9bfc9d7\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1656475294040,\"location\":\"mine\",\"persisted\":1656475294040}}"
}
]
}
]
}

View File

@@ -38,6 +38,8 @@ const sinon = require('sinon');
const VISUAL_GRACE_PERIOD = 5 * 1000; //Lets the application "simmer" before the snapshot is taken
const CUSTOM_NAME = 'CUSTOM_NAME';
// Snippet from https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758
// Will replace with cy.clock() equivalent
test.beforeEach(async ({ context }) => {
@@ -52,21 +54,23 @@ test.beforeEach(async ({ context }) => {
});
});
test('Visual - Default Gauge is correct @addInit', async ({ page }) => {
await page.addInitScript({ path: path.join(__dirname, '../plugins/gauge', './addInitGauge.js') });
test('Visual - Restricted Notebook is visually correct @addInit', async ({ page }) => {
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, '../plugins/notebook', './addRestrictedNotebook.js') });
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
// Click text=CUSTOM_NAME
await page.click(`text=${CUSTOM_NAME}`);
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('text=OK')
]);
await page.click('text=Gauge');
await page.click('text=OK');
// Take a snapshot of the newly created Gauge object
// Take a snapshot of the newly created CUSTOM_NAME notebook
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'Default Gauge');
await percySnapshot(page, 'Restricted Notebook with CUSTOM_NAME');
});

View File

@@ -211,3 +211,22 @@ test('Visual - Display Layout Icon is correct', async ({ page }) => {
await percySnapshot(page, 'Display Layout Create Menu');
});
test('Visual - Default Gauge is correct', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
await page.click('text=Gauge');
await page.click('text=OK');
// Take a snapshot of the newly created Gauge object
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'Default Gauge');
});

View File

@@ -96,161 +96,166 @@ define([
};
this.destroy = this.destroy.bind(this);
/**
* Tracks current selection state of the application.
* @private
*/
this.selection = new Selection(this);
[
/**
* Tracks current selection state of the application.
* @private
*/
['selection', () => new Selection(this)],
/**
* MCT's time conductor, which may be used to synchronize view contents
* for telemetry- or time-based views.
* @type {module:openmct.TimeConductor}
* @memberof module:openmct.MCT#
* @name conductor
*/
this.time = new api.TimeAPI(this);
/**
* MCT's time conductor, which may be used to synchronize view contents
* for telemetry- or time-based views.
* @type {module:openmct.TimeConductor}
* @memberof module:openmct.MCT#
* @name conductor
*/
['time', () => new api.TimeAPI(this)],
/**
* An interface for interacting with the composition of domain objects.
* The composition of a domain object is the list of other domain
* objects it "contains" (for instance, that should be displayed
* beneath it in the tree.)
*
* `composition` may be called as a function, in which case it acts
* as [`composition.get`]{@link module:openmct.CompositionAPI#get}.
*
* @type {module:openmct.CompositionAPI}
* @memberof module:openmct.MCT#
* @name composition
*/
this.composition = new api.CompositionAPI(this);
/**
* An interface for interacting with the composition of domain objects.
* The composition of a domain object is the list of other domain
* objects it "contains" (for instance, that should be displayed
* beneath it in the tree.)
*
* `composition` may be called as a function, in which case it acts
* as [`composition.get`]{@link module:openmct.CompositionAPI#get}.
*
* @type {module:openmct.CompositionAPI}
* @memberof module:openmct.MCT#
* @name composition
*/
['composition', () => new api.CompositionAPI(this)],
/**
* Registry for views of domain objects which should appear in the
* main viewing area.
*
* @type {module:openmct.ViewRegistry}
* @memberof module:openmct.MCT#
* @name objectViews
*/
this.objectViews = new ViewRegistry();
/**
* Registry for views of domain objects which should appear in the
* main viewing area.
*
* @type {module:openmct.ViewRegistry}
* @memberof module:openmct.MCT#
* @name objectViews
*/
['objectViews', () => new ViewRegistry()],
/**
* Registry for views which should appear in the Inspector area.
* These views will be chosen based on the selection state.
*
* @type {module:openmct.InspectorViewRegistry}
* @memberof module:openmct.MCT#
* @name inspectorViews
*/
this.inspectorViews = new InspectorViewRegistry();
/**
* Registry for views which should appear in the Inspector area.
* These views will be chosen based on the selection state.
*
* @type {module:openmct.InspectorViewRegistry}
* @memberof module:openmct.MCT#
* @name inspectorViews
*/
['inspectorViews', () => new InspectorViewRegistry()],
/**
* Registry for views which should appear in Edit Properties
* dialogs, and similar user interface elements used for
* modifying domain objects external to its regular views.
*
* @type {module:openmct.ViewRegistry}
* @memberof module:openmct.MCT#
* @name propertyEditors
*/
this.propertyEditors = new ViewRegistry();
/**
* Registry for views which should appear in Edit Properties
* dialogs, and similar user interface elements used for
* modifying domain objects external to its regular views.
*
* @type {module:openmct.ViewRegistry}
* @memberof module:openmct.MCT#
* @name propertyEditors
*/
['propertyEditors', () => new ViewRegistry()],
/**
* Registry for views which should appear in the status indicator area.
* @type {module:openmct.ViewRegistry}
* @memberof module:openmct.MCT#
* @name indicators
*/
this.indicators = new ViewRegistry();
/**
* Registry for views which should appear in the toolbar area while
* editing. These views will be chosen based on the selection state.
*
* @type {module:openmct.ToolbarRegistry}
* @memberof module:openmct.MCT#
* @name toolbars
*/
['toolbars', () => new ToolbarRegistry()],
/**
* Registry for views which should appear in the toolbar area while
* editing. These views will be chosen based on the selection state.
*
* @type {module:openmct.ToolbarRegistry}
* @memberof module:openmct.MCT#
* @name toolbars
*/
this.toolbars = new ToolbarRegistry();
/**
* Registry for domain object types which may exist within this
* instance of Open MCT.
*
* @type {module:openmct.TypeRegistry}
* @memberof module:openmct.MCT#
* @name types
*/
['types', () => new api.TypeRegistry()],
/**
* Registry for domain object types which may exist within this
* instance of Open MCT.
*
* @type {module:openmct.TypeRegistry}
* @memberof module:openmct.MCT#
* @name types
*/
this.types = new api.TypeRegistry();
/**
* An interface for interacting with domain objects and the domain
* object hierarchy.
*
* @type {module:openmct.ObjectAPI}
* @memberof module:openmct.MCT#
* @name objects
*/
['objects', () => new api.ObjectAPI.default(this.types, this)],
/**
* An interface for interacting with domain objects and the domain
* object hierarchy.
*
* @type {module:openmct.ObjectAPI}
* @memberof module:openmct.MCT#
* @name objects
*/
this.objects = new api.ObjectAPI.default(this.types, this);
/**
* An interface for retrieving and interpreting telemetry data associated
* with a domain object.
*
* @type {module:openmct.TelemetryAPI}
* @memberof module:openmct.MCT#
* @name telemetry
*/
['telemetry', () => new api.TelemetryAPI(this)],
/**
* An interface for retrieving and interpreting telemetry data associated
* with a domain object.
*
* @type {module:openmct.TelemetryAPI}
* @memberof module:openmct.MCT#
* @name telemetry
*/
this.telemetry = new api.TelemetryAPI(this);
/**
* An interface for creating new indicators and changing them dynamically.
*
* @type {module:openmct.IndicatorAPI}
* @memberof module:openmct.MCT#
* @name indicators
*/
['indicators', () => new api.IndicatorAPI(this)],
/**
* An interface for creating new indicators and changing them dynamically.
*
* @type {module:openmct.IndicatorAPI}
* @memberof module:openmct.MCT#
* @name indicators
*/
this.indicators = new api.IndicatorAPI(this);
/**
* MCT's user awareness management, to enable user and
* role specific functionality.
* @type {module:openmct.UserAPI}
* @memberof module:openmct.MCT#
* @name user
*/
['user', () => new api.UserAPI(this)],
/**
* MCT's user awareness management, to enable user and
* role specific functionality.
* @type {module:openmct.UserAPI}
* @memberof module:openmct.MCT#
* @name user
*/
this.user = new api.UserAPI(this);
['notifications', () => new api.NotificationAPI()],
this.notifications = new api.NotificationAPI();
['editor', () => new api.EditorAPI.default(this)],
this.editor = new api.EditorAPI.default(this);
['overlays', () => new OverlayAPI.default()],
this.overlays = new OverlayAPI.default();
['menus', () => new api.MenuAPI(this)],
this.menus = new api.MenuAPI(this);
['actions', () => new api.ActionsAPI(this)],
this.actions = new api.ActionsAPI(this);
['status', () => new api.StatusAPI(this)],
this.status = new api.StatusAPI(this);
['priority', () => api.PriorityAPI],
this.priority = api.PriorityAPI;
['router', () => new ApplicationRouter(this)],
this.router = new ApplicationRouter(this);
this.faults = new api.FaultManagementAPI.default(this);
this.forms = new api.FormsAPI.default(this);
['faults', () => new api.FaultManagementAPI.default(this)],
this.branding = BrandingAPI.default;
['forms', () => new api.FormsAPI.default(this)],
/**
* MCT's annotation API that enables
* human-created comments and categorization linked to data products
* @type {module:openmct.AnnotationAPI}
* @memberof module:openmct.MCT#
* @name annotation
*/
this.annotation = new api.AnnotationAPI(this);
['branding', () => BrandingAPI.default],
/**
* MCT's annotation API that enables
* human-created comments and categorization linked to data products
* @type {module:openmct.AnnotationAPI}
* @memberof module:openmct.MCT#
* @name annotation
*/
['annotation', () => new api.AnnotationAPI(this)]
].forEach(apiEntry => {
const apiName = apiEntry[0];
const apiObject = apiEntry[1]();
Object.defineProperty(this, apiName, {
value: apiObject,
enumerable: false,
configurable: false
});
});
// Plugins that are installed by default
this.install(this.plugins.Plot());
@@ -281,6 +286,7 @@ define([
this.install(this.plugins.ObjectInterceptors());
this.install(this.plugins.DeviceClassifier());
this.install(this.plugins.UserIndicator());
this.install(this.plugins.Gauge());
}
MCT.prototype = Object.create(EventEmitter.prototype);

View File

@@ -49,6 +49,7 @@ export class TelemetryCollection extends EventEmitter {
this.pageState = undefined;
this.lastBounds = undefined;
this.requestAbort = undefined;
this.isStrategyLatest = this.options.strategy === 'latest';
}
/**
@@ -173,12 +174,14 @@ export class TelemetryCollection extends EventEmitter {
return;
}
let latestBoundedDatum = this.boundedTelemetry[this.boundedTelemetry.length - 1];
let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData];
let parsedValue;
let beforeStartOfBounds;
let afterEndOfBounds;
let added = [];
// loop through, sort and dedupe
for (let datum of data) {
parsedValue = this.parseTime(datum);
beforeStartOfBounds = parsedValue < this.lastBounds.start;
@@ -218,7 +221,17 @@ export class TelemetryCollection extends EventEmitter {
}
if (added.length) {
this.emit('add', added);
// if latest strategy is requested, we need to check if the value is the latest unmitted value
if (this.isStrategyLatest) {
this.boundedTelemetry = [this.boundedTelemetry[this.boundedTelemetry.length - 1]];
// if true, then this value has yet to be emitted
if (this.boundedTelemetry[0] !== latestBoundedDatum) {
this.emit('add', this.boundedTelemetry);
}
} else {
this.emit('add', added);
}
}
}
@@ -278,13 +291,20 @@ export class TelemetryCollection extends EventEmitter {
if (startChanged) {
testDatum[this.timeKey] = bounds.start;
// Calculate the new index of the first item within the bounds
startIndex = _.sortedIndexBy(
this.boundedTelemetry,
testDatum,
datum => this.parseTime(datum)
);
discarded = this.boundedTelemetry.splice(0, startIndex);
// a little more complicated if not latest strategy
if (!this.isStrategyLatest) {
// Calculate the new index of the first item within the bounds
startIndex = _.sortedIndexBy(
this.boundedTelemetry,
testDatum,
datum => this.parseTime(datum)
);
discarded = this.boundedTelemetry.splice(0, startIndex);
} else if (this.parseTime(testDatum) > this.parseTime(this.boundedTelemetry[0])) {
discarded = this.boundedTelemetry;
this.boundedTelemetry = [];
}
}
if (endChanged) {
@@ -296,7 +316,6 @@ export class TelemetryCollection extends EventEmitter {
datum => this.parseTime(datum)
);
added = this.futureBuffer.splice(0, endIndex);
this.boundedTelemetry = [...this.boundedTelemetry, ...added];
}
if (discarded.length > 0) {
@@ -304,6 +323,13 @@ export class TelemetryCollection extends EventEmitter {
}
if (added.length > 0) {
if (!this.isStrategyLatest) {
this.boundedTelemetry = [...this.boundedTelemetry, ...added];
} else {
added = [added[added.length - 1]];
this.boundedTelemetry = added;
}
this.emit('add', added);
}
} else {

View File

@@ -21,23 +21,25 @@
*****************************************************************************/
<template>
<div class="c-fault-mgmt__list-header c-fault-mgmt__list">
<div class="c-fault-mgmt__checkbox">
<div class="c-fault-mgmt-item-header c-fault-mgmt__list-header c-fault-mgmt__list">
<div class="c-fault-mgmt-item-header c-fault-mgmt__checkbox">
<input
type="checkbox"
:checked="isSelectAll"
@input="selectAll"
>
</div>
<div class="c-fault-mgmt__list-content">
<div class="c-fault-mgmt__list-header-results"> {{ totalFaultsCount }} Results </div>
<div class="c-fault-mgmt-item-header c-fault-mgmt__list-header-results c-fault-mgmt__list-severity">
{{ totalFaultsCount }} Results
</div>
<div class="c-fault-mgmt__list-header-content">
<div class="c-fault-mgmt__list-content-right">
<div class="c-fault-mgmt__list-header-tripVal c-fault-mgmt__list-trigVal">Trip Value</div>
<div class="c-fault-mgmt__list-header-liveVal c-fault-mgmt__list-curVal">Live Value</div>
<div class="c-fault-mgmt__list-header-trigTime c-fault-mgmt__list-trigTime">Trigger Time</div>
<div class="c-fault-mgmt-item-header c-fault-mgmt__list-header-tripVal">Trip Value</div>
<div class="c-fault-mgmt-item-header c-fault-mgmt__list-header-liveVal">Live Value</div>
<div class="c-fault-mgmt-item-header c-fault-mgmt__list-header-trigTime">Trigger Time</div>
</div>
</div>
<div class="c-fault-mgmt__list-action-wrapper">
<div class=" c-fault-mgmt-item-header c-fault-mgmt__list-header-action-wrapper">
<div class="c-fault-mgmt__list-header-sortButton c-fault-mgmt__list-action-button">
<SelectField
class="c-fault-mgmt-viewButton"

View File

@@ -26,49 +26,57 @@
:class="[
{'is-selected': isSelected},
{'is-unacknowledged': !fault.acknowledged},
{'is-shelved': fault.shelved}
{'is-shelved': fault.shelved},
{'is-acknowledged': fault.acknowledged}
]"
>
<div class="c-fault-mgmt__checkbox">
<div class="c-fault-mgmt-item c-fault-mgmt__list-checkbox">
<input
type="checkbox"
:checked="isSelected"
@input="toggleSelected"
>
</div>
<div
class="c-fault-mgmt__list-severity"
:title="fault.severity"
:class="[
'is-severity-' + severity
]"
>
<div class="c-fault-mgmt-item">
<div
class="c-fault-mgmt__list-severity"
:title="fault.severity"
:class="[
'is-severity-' + severity
]"
>
</div>
</div>
<div class="c-fault-mgmt__list-content">
<div class="c-fault-mgmt__list-pathname">
<div class="c-fault-mgmt-item c-fault-mgmt__list-content">
<div class="c-fault-mgmt-item c-fault-mgmt__list-pathname">
<div class="c-fault-mgmt__list-path">{{ fault.namespace }}</div>
<div class="c-fault-mgmt__list-faultname">{{ fault.name }}</div>
</div>
<div class="c-fault-mgmt__list-content-right">
<div
class="c-fault-mgmt__list-trigVal"
:class="tripValueClassname"
title="Trip Value"
>{{ fault.triggerValueInfo.value }}</div>
<div
class="c-fault-mgmt__list-curVal"
:class="liveValueClassname"
title="Live Value"
>
{{ fault.currentValueInfo.value }}
<div class="c-fault-mgmt-item c-fault-mgmt__list-trigVal">
<div
class="c-fault-mgmt-item__value"
:class="tripValueClassname"
title="Trip Value"
>{{ fault.triggerValueInfo.value }}</div>
</div>
<div
class="c-fault-mgmt__list-trigTime"
>{{ fault.triggerTime }}
<div class="c-fault-mgmt-item c-fault-mgmt__list-curVal">
<div
class="c-fault-mgmt-item__value"
:class="liveValueClassname"
title="Live Value"
>{{ fault.currentValueInfo.value }}</div>
</div>
<div class="c-fault-mgmt-item c-fault-mgmt__list-trigTime">
<div
class="c-fault-mgmt-item__value"
title="Last Trigger Time"
>{{ fault.triggerTime }}
</div>
</div>
</div>
</div>
<div class="c-fault-mgmt__list-action-wrapper">
<div class="c-fault-mgmt-item c-fault-mgmt__list-action-wrapper">
<button
class="c-fault-mgmt__list-action-button l-browse-bar__actions c-icon-button icon-3-dots"
title="Disposition Actions"
@@ -77,7 +85,6 @@
</div>
</div>
</template>
<script>
const RANGE_CONDITION_CLASS = {
@@ -149,7 +156,7 @@ export default {
const menuItems = [
{
cssClass: 'icon-bell',
cssClass: 'icon-check',
isDisabled: this.fault.acknowledged,
name: 'Acknowledge',
description: '',

View File

@@ -35,25 +35,31 @@
@shelveSelected="toggleShelveSelected"
/>
<FaultManagementListHeader
class="header"
:selected-faults="Object.values(selectedFaults)"
:total-faults-count="filteredFaultsList.length"
@selectAll="selectAll"
@sortChanged="sortChanged"
/>
<div class="c-faults-list-view-header-item-container-wrapper">
<div class="c-faults-list-view-header-item-container">
<FaultManagementListHeader
class="header"
:selected-faults="Object.values(selectedFaults)"
:total-faults-count="filteredFaultsList.length"
@selectAll="selectAll"
@sortChanged="sortChanged"
/>
<template v-if="filteredFaultsList.length > 0">
<FaultManagementListItem
v-for="fault of filteredFaultsList"
:key="fault.id"
:fault="fault"
:is-selected="isSelected(fault)"
@toggleSelected="toggleSelected"
@acknowledgeSelected="toggleAcknowledgeSelected"
@shelveSelected="toggleShelveSelected"
/>
</template>
<div class="c-faults-list-view-item-body">
<template v-if="filteredFaultsList.length > 0">
<FaultManagementListItem
v-for="fault of filteredFaultsList"
:key="fault.id"
:fault="fault"
:is-selected="isSelected(fault)"
@toggleSelected="toggleSelected"
@acknowledgeSelected="toggleAcknowledgeSelected"
@shelveSelected="toggleShelveSelected"
/>
</template>
</div>
</div>
</div>
</div>
</template>
@@ -195,7 +201,7 @@ export default {
{
key: 'comment',
control: 'textarea',
name: 'Comment',
name: 'Optional comment',
pattern: '\\S+',
required: false,
cssClass: 'l-input-lg',
@@ -237,7 +243,7 @@ export default {
{
key: 'comment',
control: 'textarea',
name: 'Comment',
name: 'Optional comment',
pattern: '\\S+',
required: false,
cssClass: 'l-input-lg',
@@ -246,7 +252,7 @@ export default {
{
key: 'shelveDuration',
control: 'select',
name: 'Shelve Duration',
name: 'Shelve duration',
options: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS,
required: false,
cssClass: 'l-input-lg',

View File

@@ -32,7 +32,7 @@ export default function FaultManagementPlugin() {
name: 'Fault Management',
creatable: false,
description: 'Fault Management View',
cssClass: 'icon-telemetry'
cssClass: 'icon-bell'
});
openmct.objectViews.addProvider(new FaultManagementViewProvider(openmct));

View File

@@ -23,7 +23,7 @@
<template>
<div class="c-fault-mgmt__toolbar">
<button
class="c-icon-button icon-bell"
class="c-icon-button icon-check"
title="Acknowledge selected faults"
:disabled="disableAcknowledge"
@click="acknowledgeSelected"

View File

@@ -21,14 +21,13 @@
*****************************************************************************/
<template>
<div class="c-fault-mgmt">
<FaultManagementListView
:faults-list="faultsList"
/>
</div>
<FaultManagementListView
:faults-list="faultsList"
/>
</template>
<script>
import FaultManagementListView from './FaultManagementListView.vue';
import { FAULT_MANAGEMENT_ALARMS, FAULT_MANAGEMENT_GLOBAL_ALARMS } from './constants';

View File

@@ -19,216 +19,250 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*********************************************** FAULT PROPERTIES */
.is-severity-critical{
@include glyphBefore($glyph-icon-alert-triangle);
color: $colorStatusError;
}
.is-severity-warning{
@include glyphBefore($glyph-icon-alert-rect);
color: $colorStatusAlert;
}
.is-severity-watch{
@include glyphBefore($glyph-icon-info);
color: $colorCommand;
}
.is-unacknowledged{
.c-fault-mgmt__list-severity{
@include pulse($animName: severityAnim, $dur: 200ms);
}
}
.is-selected {
background: $colorSelectedBg;
}
.is-shelved{
.c-fault-mgmt__list-content{
opacity: 50% !important;
font-style: italic;
}
.c-fault-mgmt__list-severity{
@include pulse($animName: shelvedAnim, $dur: 0ms);
}
}
$colorFaultItemFg: $colorBodyFg;
$colorFaultItemFgEmphasis: $colorBodyFgEm;
$colorFaultItemBg: pullForward($colorBodyBg, 5%);
/*********************************************** SEARCH */
.c-fault-mgmt__search-row{
.c-fault-mgmt__search-row {
display: flex;
align-items: center;
flex: 0 0 auto;
> * + * {
margin-left: 10px;
float: right;
}
}
.c-fault-mgmt-search{
.c-fault-mgmt-search {
width: 95%;
}
/*********************************************** TOOLBAR */
.c-fault-mgmt__toolbar{
display: flex;
.c-fault-mgmt__toolbar {
display: flex;
justify-content: center;
> * {
font-size: 1.25em;
flex: 0 0 auto;
> * + * {
margin-left: $interiorMargin;
}
}
/*********************************************** LIST VIEW */
.c-faults-list-view {
.c-faults-list-view {
display: flex;
flex-direction: column;
height: 100%;
> * + * {
margin-top: $interiorMargin;
margin-top: $interiorMargin;
}
}
.c-faults-list-view-header-item-container {
display: grid;
width: 100%;
grid-template-columns: max-content max-content repeat(5,minmax(max-content, 20%)) max-content;
grid-row-gap: $interiorMargin;
/*********************************************** FAULT ITEM */
.c-fault-mgmt__list{
background: rgba($colorBodyFg, 0.1);
margin-bottom: 5px;
padding: 4px;
display: flex;
align-items: center;
> * {
margin-left: $interiorMargin;
&-wrapper {
flex: 1 1 auto;
padding-right: $interiorMargin; // Fend of from scrollbar
overflow-y: auto;
}
&-severity{
.--width-less-than-600 & {
grid-template-columns: max-content max-content 1fr 1fr max-content;
}
}
.c-faults-list-view-item-body {
display: contents;
}
/*********************************************** LIST */
.c-fault-mgmt__list {
display: contents;
color: $colorFaultItemFg;
&-checkbox{
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
&-severity {
font-size: 2em;
margin-left: $interiorMarginLg;
}
&-pathname{
flex-wrap: wrap;
flex: 1 1 auto;
}
&-path{
font-size: .75em;
}
&.is-severity-critical {
@include glyphBefore($glyph-icon-alert-triangle);
color: $colorStatusError;
}
&-faultname{
font-weight: bold;
font-size: 1.3em;
}
&.is-severity-warning {
@include glyphBefore($glyph-icon-alert-rect);
color: $colorStatusAlert;
}
&-content{
display: flex;
flex-wrap: wrap;
flex: 1 1 auto;
align-items: center;
}
&-content-right{
margin-left: auto;
display: flex;
flex-wrap: wrap;
}
&-trigVal, &-curVal, &-trigTime{
@include ellipsize;
border-radius: $controlCr;
padding: $interiorMargin;
width: 80px;
margin-right: $interiorMarginLg;
}
&-trigVal {
@include isLimit();
background: rgba($colorBodyFg, 0.25);
}
&-curVal {
@include isLimit();
background: rgba($colorBodyFg, 0.25);
&-alert{
background: $colorWarningHi;
&.is-severity-watch {
@include glyphBefore($glyph-icon-info);
color: $colorCommand;
}
}
&-trigTime{
width: auto;
&-content {
display: contents;
.--width-less-than-600 & {
display: flex;
flex-wrap: wrap;
grid-column: span 2;
}
}
&-action-wrapper{
display: flex;
align-content: right;
width: 100px;
&-pathname {
padding-right: $interiorMarginLg;
overflow-wrap: anywhere;
min-width: 100px;
}
&-path {
font-size: .85em;
margin-left: $interiorMargin;
}
&-action-button{
&-faultname{
font-size: 1.3em;
margin-left: $interiorMargin;
}
&-content-right {
display: contents;
}
&-trigTime {
grid-column: 6 / span 2;
}
&-action-wrapper {
text-align: right;
flex: 0 0 auto;
align-items: stretch;
}
&-action-button {
flex: 0 0 auto;
margin-left: auto;
justify-content: right;
text-align: right;
}
// STATES
&.is-unacknowledged {
color: $colorFaultItemFgEmphasis;
.c-fault-mgmt__list-severity {
@include pulse($animName: severityAnim, $dur: 200ms);
}
}
&.is-acknowledged,
&.is-shelved {
.c-fault-mgmt__list-severity {
&:before {
opacity: 60%;
//font-size: 1.5em;
}
&:after {
color: $colorFaultItemFgEmphasis;
display: block;
font-family: symbolsfont;
position: absolute;
//text-shadow: black 0 0 2px;
right: -3px;
bottom: -3px;
transform-origin: right bottom;
transform: scale(0.6);
}
}
}
&.is-shelved {
.c-fault-mgmt__list-pathname {
font-style: italic;
}
}
&.is-acknowledged .c-fault-mgmt__list-severity:after {
content: $glyph-icon-check;
}
&.is-shelved .c-fault-mgmt__list-severity:after {
content: $glyph-icon-timer;
}
}
/*********************************************** LIST HEADER */
.c-fault-mgmt__list-header{
display: flex;
background: rgba($colorBodyFg, .23);
.c-fault-mgmt__list-header {
display: contents;
border-radius: $controlCr;
align-items: center;
&-tripVal, &-liveVal, &-trigTime{
background: none;
* {
margin: 0px;
border-radius: 0px;
}
&-trigTime{
width: 160px;
}
&-sortButton{
flex: 0 0 auto;
margin-left: auto;
justify-content: right;
display: flex;
align-content: right;
width: 100px;
.--width-less-than-600 & {
.c-fault-mgmt__list-content-right {
display:none;
}
}
}
&-content {
display: contents;
}
.is-severity-critical{
@include glyphBefore($glyph-icon-alert-triangle);
color: $colorStatusError;
}
&-results {
grid-column: 2 / span 2;
font-size: 1em;
height: auto;
}
.is-severity-warning{
@include glyphBefore($glyph-icon-alert-rect);
color: $colorStatusAlert;
}
&-action-wrapper {
grid-column: 7 / span 2;
.is-severity-watch{
@include glyphBefore($glyph-icon-info);
color: $colorCommand;
}
.is-unacknowledged{
.c-fault-mgmt__list-severity{
@include pulse($animName: severityAnim, $dur: 200ms);
.--width-less-than-600 & {
grid-column: 4 / span 2;
}
}
}
.is-selected {
background: $colorSelectedBg;
}
/*********************************************** GRID ITEM */
.c-fault-mgmt-item {
$p: $interiorMargin;
padding: $p;
background: $colorFaultItemBg;
white-space: nowrap;
.is-shelved{
.c-fault-mgmt__list-content{
opacity: 60% !important;
font-style: italic;
&-header {
$c: $colorBodyBg;
background: $c;
border-bottom: 5px solid $c; // Creates illusion of "space" beneath header
min-height: 30px; // Needed to align cells
padding: $p;
position: sticky;
top: 0;
z-index: 2;
}
.c-fault-mgmt__list-severity{
@include pulse($animName: shelvedAnim, $dur: 0ms);
&__value {
@include isLimit();
background: rgba($colorBodyFg, 0.1);
padding: $p;
border-radius: $controlCr;
display: inline-flex;
}
.is-selected & {
background: $colorSelectedBg;
}
}

View File

@@ -78,6 +78,7 @@
</template>
<script>
import _ from 'lodash';
export default {
inject: ['openmct', 'indicator', 'configuration'],
@@ -118,6 +119,9 @@ export default {
this.openmct.user.status.off('statusChange', this.fetchStatusSummary);
this.openmct.user.status.off('pollQuestionChange', this.setPollQuestion);
},
created() {
this.fetchStatusSummary = _.debounce(this.fetchStatusSummary);
},
methods: {
async fetchCurrentPoll() {
const pollQuestion = await this.openmct.user.status.getPollQuestion();

View File

@@ -199,6 +199,11 @@ class CouchObjectProvider {
}
let response = null;
if (!this.isObservingObjectChanges()) {
this.#observeObjectChanges();
}
try {
response = await fetch(this.url + '/' + subPath, fetchOptions);
const { status } = response;
@@ -471,9 +476,6 @@ class CouchObjectProvider {
this.observers[keyString] = this.observers[keyString].filter(observer => observer !== callback);
if (this.observers[keyString].length === 0) {
delete this.observers[keyString];
if (Object.keys(this.observers).length === 0 && this.isObservingObjectChanges()) {
this.stopObservingObjectChanges();
}
}
}
};
@@ -498,7 +500,6 @@ class CouchObjectProvider {
} else {
this.#initiateSharedWorkerFetchChanges(sseURL.toString());
}
}
/**
@@ -525,18 +526,24 @@ class CouchObjectProvider {
onEventError(error) {
console.error('Error on feed', error);
if (Object.keys(this.observers).length > 0) {
this.#observeObjectChanges();
}
const { readyState } = error.target;
this.#updateIndicatorStatus(readyState);
}
onEventOpen(event) {
const { readyState } = event.target;
this.#updateIndicatorStatus(readyState);
}
onEventMessage(event) {
const { readyState } = event.target;
const eventData = JSON.parse(event.data);
const identifier = {
namespace: this.namespace,
key: eventData.id
};
const keyString = this.openmct.objects.makeKeyString(identifier);
this.#updateIndicatorStatus(readyState);
let observersForObject = this.observers[keyString];
if (observersForObject) {
@@ -559,17 +566,18 @@ class CouchObjectProvider {
this.stopObservingObjectChanges = () => {
controller.abort();
couchEventSource.removeEventListener('message', this.onEventMessage);
couchEventSource.removeEventListener('message', this.onEventMessage.bind(this));
delete this.stopObservingObjectChanges;
};
console.debug('⇿ Opening CouchDB change feed connection ⇿');
couchEventSource = new EventSource(url);
couchEventSource.onerror = this.onEventError;
couchEventSource.onerror = this.onEventError.bind(this);
couchEventSource.onopen = this.onEventOpen.bind(this);
// start listening for events
couchEventSource.addEventListener('message', this.onEventMessage);
couchEventSource.addEventListener('message', this.onEventMessage.bind(this));
console.debug('⇿ Opened connection ⇿');
}
@@ -587,6 +595,31 @@ class CouchObjectProvider {
return intermediateResponse;
}
/**
* Update the indicator status based on the readyState of the EventSource
* @private
*/
#updateIndicatorStatus(readyState) {
let message;
switch (readyState) {
case EventSource.CONNECTING:
message = 'pending';
break;
case EventSource.OPEN:
message = 'open';
break;
case EventSource.CLOSED:
message = 'close';
break;
default:
message = 'unknown';
break;
}
const indicatorState = this.#messageToIndicatorState(message);
this.indicator.setIndicatorToState(indicatorState);
}
/**
* @private
*/

View File

@@ -152,7 +152,10 @@ describe('the plugin', () => {
mockDomainObject.id = mockDomainObject.identifier.key;
const fakeUpdateEvent = {
data: JSON.stringify(mockDomainObject)
data: JSON.stringify(mockDomainObject),
target: {
readyState: EventSource.CONNECTED
}
};
// eslint-disable-next-line require-await
@@ -170,7 +173,6 @@ describe('the plugin', () => {
expect(provider.create).toHaveBeenCalled();
expect(provider.observe).toHaveBeenCalled();
expect(provider.isObservingObjectChanges).toHaveBeenCalled();
expect(provider.isObservingObjectChanges.calls.mostRecent().returnValue).toBe(true);
//Set modified timestamp it detects a change and persists the updated model.
mockDomainObject.modified = mockDomainObject.persisted + 1;
@@ -181,6 +183,7 @@ describe('the plugin', () => {
expect(updatedResult).toBeTrue();
expect(provider.update).toHaveBeenCalled();
expect(provider.fetchChanges).toHaveBeenCalled();
expect(provider.isObservingObjectChanges.calls.mostRecent().returnValue).toBe(true);
sharedWorkerCallback(fakeUpdateEvent);
expect(provider.onEventMessage).toHaveBeenCalled();

View File

@@ -223,15 +223,16 @@ export default {
},
resizeSoView() {
let cW = this.$refs.soView.offsetWidth;
let widths = [220, 600];
let wClass = '';
if (cW < 220) {
wClass = CSS_WIDTH_LESS_STR + '220';
} else if (cW < 600) {
wClass = CSS_WIDTH_LESS_STR + '600';
for (let width of widths) {
if (cW < width) {
wClass = wClass.concat(' ', CSS_WIDTH_LESS_STR, width);
}
}
this.widthClass = wClass;
this.widthClass = wClass.trimStart();
}
}
};

View File

@@ -11,6 +11,7 @@
margin-bottom: $interiorMarginSm;
overflow: hidden;
padding: 3px;
@include smallerControlButtons; // Make button in frame headers a bit smaller
.c-object-label {
font-size: 1.05em;
@@ -132,8 +133,6 @@
}
}
@include smallerControlButtons;
&.has-complex-content {
> .c-so-view__view-large { display: block; }
}

View File

@@ -5,8 +5,6 @@ const config = require('./webpack.dev');
const path = require('path');
config.devtool = false;
const vueLoaderRule = config.module.rules.find(r => r.use === 'vue-loader');
vueLoaderRule.use = {