Compare commits
12 Commits
memory-lea
...
custom-col
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b6cf2daa8 | ||
|
|
6112d47134 | ||
|
|
1a07f7604a | ||
|
|
6dde54bd25 | ||
|
|
359e7377ac | ||
|
|
9f4190f781 | ||
|
|
f3fc991a74 | ||
|
|
2564e75fc9 | ||
|
|
f42fe78acf | ||
|
|
fe928a1386 | ||
|
|
b329ed6ed5 | ||
|
|
9b7a0d7e4c |
@@ -63,7 +63,7 @@ define([
|
||||
|
||||
StateGeneratorProvider.prototype.request = function (domainObject, options) {
|
||||
var start = options.start;
|
||||
var end = options.end;
|
||||
var end = Math.min(Date.now(), options.end); // no future values
|
||||
var duration = domainObject.telemetry.duration * 1000;
|
||||
if (options.strategy === 'latest' || options.size === 1) {
|
||||
start = end;
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
openmct.install(openmct.plugins.ExampleImagery());
|
||||
openmct.install(openmct.plugins.PlanLayout());
|
||||
openmct.install(openmct.plugins.Timeline());
|
||||
openmct.install(openmct.plugins.Hyperlink());
|
||||
openmct.install(openmct.plugins.UTCTimeSystem());
|
||||
openmct.install(openmct.plugins.AutoflowView({
|
||||
type: "telemetry.panel"
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"jsdoc": "^3.3.2",
|
||||
"karma": "6.3.4",
|
||||
"karma-chrome-launcher": "3.1.0",
|
||||
"karma-firefox-launcher": "2.1.0",
|
||||
"karma-firefox-launcher": "2.1.1",
|
||||
"karma-cli": "2.0.0",
|
||||
"karma-coverage": "2.0.3",
|
||||
"karma-coverage-istanbul-reporter": "3.0.3",
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2009-2016, 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.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'./src/HyperlinkController',
|
||||
'./res/templates/hyperlink.html'
|
||||
], function (
|
||||
HyperlinkController,
|
||||
hyperlinkTemplate
|
||||
) {
|
||||
return {
|
||||
name: "platform/features/hyperlink",
|
||||
definition: {
|
||||
"name": "Hyperlink",
|
||||
"description": "Insert a hyperlink to reference a link",
|
||||
"extensions": {
|
||||
"types": [
|
||||
{
|
||||
"key": "hyperlink",
|
||||
"name": "Hyperlink",
|
||||
"cssClass": "icon-chain-links",
|
||||
"description": "A hyperlink to redirect to a different link",
|
||||
"features": ["creation"],
|
||||
"properties": [
|
||||
{
|
||||
"key": "url",
|
||||
"name": "URL",
|
||||
"control": "textfield",
|
||||
"required": true,
|
||||
"cssClass": "l-input-lg"
|
||||
},
|
||||
|
||||
{
|
||||
"key": "displayText",
|
||||
"name": "Text to Display",
|
||||
"control": "textfield",
|
||||
"required": true,
|
||||
"cssClass": "l-input-lg"
|
||||
},
|
||||
{
|
||||
"key": "displayFormat",
|
||||
"name": "Display Format",
|
||||
"control": "select",
|
||||
"options": [
|
||||
{
|
||||
"name": "Link",
|
||||
"value": "link"
|
||||
},
|
||||
{
|
||||
"value": "button",
|
||||
"name": "Button"
|
||||
}
|
||||
],
|
||||
"cssClass": "l-inline"
|
||||
},
|
||||
{
|
||||
"key": "openNewTab",
|
||||
"name": "Tab to Open Hyperlink",
|
||||
"control": "select",
|
||||
"options": [
|
||||
{
|
||||
"name": "Open in this tab",
|
||||
"value": "thisTab"
|
||||
},
|
||||
{
|
||||
"value": "newTab",
|
||||
"name": "Open in a new tab"
|
||||
}
|
||||
],
|
||||
"cssClass": "l-inline"
|
||||
|
||||
}
|
||||
],
|
||||
"model": {
|
||||
"displayFormat": "link",
|
||||
"openNewTab": "thisTab",
|
||||
"removeTitle": true
|
||||
}
|
||||
|
||||
}
|
||||
],
|
||||
"views": [
|
||||
{
|
||||
"key": "hyperlink",
|
||||
"type": "hyperlink",
|
||||
"name": "Hyperlink Display",
|
||||
"template": hyperlinkTemplate,
|
||||
"editable": false
|
||||
}
|
||||
],
|
||||
"controllers": [
|
||||
{
|
||||
"key": "HyperlinkController",
|
||||
"implementation": HyperlinkController,
|
||||
"depends": ["$scope"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<a class="c-hyperlink u-links" ng-controller="HyperlinkController as hyperlink" href="{{domainObject.getModel().url}}"
|
||||
ng-attr-target="{{hyperlink.openNewTab() ? '_blank' : undefined}}"
|
||||
ng-class="{
|
||||
'c-hyperlink--button u-fills-container' : hyperlink.isButton(),
|
||||
'c-hyperlink--link' : !hyperlink.isButton() }">
|
||||
<span class="c-hyperlink__label">{{domainObject.getModel().displayText}}</span>
|
||||
</a>
|
||||
@@ -1,61 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2009-2016, 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 bundle adds the Hyperlink object type, which can be used to add hyperlinks as a domain Object type
|
||||
and into display Layouts as either a button or link that can be chosen to open in either the same tab or
|
||||
create a new tab to open the link in
|
||||
* @namespace platform/features/hyperlink
|
||||
*/
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
function HyperlinkController($scope) {
|
||||
this.$scope = $scope;
|
||||
}
|
||||
|
||||
/**Function to analyze the location in which to open the hyperlink
|
||||
@returns true if the hyperlink is chosen to open in a different tab, false if the same tab
|
||||
**/
|
||||
HyperlinkController.prototype.openNewTab = function () {
|
||||
if (this.$scope.domainObject.getModel().openNewTab === "thisTab") {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**Function to specify the format in which the hyperlink should be created
|
||||
@returns true if the hyperlink is chosen to be created as a button, false if a link
|
||||
**/
|
||||
HyperlinkController.prototype.isButton = function () {
|
||||
if (this.$scope.domainObject.getModel().displayFormat === "link") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
return HyperlinkController;
|
||||
}
|
||||
|
||||
);
|
||||
@@ -1,89 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2009-2016, 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.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
["../src/HyperlinkController"],
|
||||
function (HyperlinkController) {
|
||||
|
||||
describe("The controller for hyperlinks", function () {
|
||||
var domainObject,
|
||||
controller,
|
||||
scope;
|
||||
beforeEach(function () {
|
||||
scope = jasmine.createSpyObj(
|
||||
"$scope",
|
||||
["domainObject"]
|
||||
);
|
||||
domainObject = jasmine.createSpyObj(
|
||||
"domainObject",
|
||||
["getModel"]
|
||||
);
|
||||
scope.domainObject = domainObject;
|
||||
controller = new HyperlinkController(scope);
|
||||
});
|
||||
it("knows when it should open a new tab", function () {
|
||||
scope.domainObject.getModel.and.returnValue({
|
||||
"displayFormat": "link",
|
||||
"openNewTab": "newTab",
|
||||
"showTitle": false
|
||||
}
|
||||
);
|
||||
controller = new HyperlinkController(scope);
|
||||
expect(controller.openNewTab())
|
||||
.toBe(true);
|
||||
});
|
||||
it("knows when it is a button", function () {
|
||||
scope.domainObject.getModel.and.returnValue({
|
||||
"displayFormat": "button",
|
||||
"openNewTab": "thisTab",
|
||||
"showTitle": false
|
||||
}
|
||||
);
|
||||
controller = new HyperlinkController(scope);
|
||||
expect(controller.isButton())
|
||||
.toEqual(true);
|
||||
});
|
||||
it("knows when it should open in the same tab", function () {
|
||||
scope.domainObject.getModel.and.returnValue({
|
||||
"displayFormat": "link",
|
||||
"openNewTab": "thisTab",
|
||||
"showTitle": false
|
||||
}
|
||||
);
|
||||
controller = new HyperlinkController(scope);
|
||||
expect(controller.openNewTab())
|
||||
.toBe(false);
|
||||
});
|
||||
it("knows when it is a link", function () {
|
||||
scope.domainObject.getModel.and.returnValue({
|
||||
"displayFormat": "link",
|
||||
"openNewTab": "thisTab",
|
||||
"showTitle": false
|
||||
}
|
||||
);
|
||||
controller = new HyperlinkController(scope);
|
||||
expect(controller.openNewTab())
|
||||
.toBe(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -122,6 +122,7 @@ define([
|
||||
}
|
||||
};
|
||||
|
||||
this.destroy = this.destroy.bind(this);
|
||||
/**
|
||||
* Tracks current selection state of the application.
|
||||
* @private
|
||||
@@ -435,6 +436,8 @@ define([
|
||||
Browse(this);
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', this.destroy);
|
||||
|
||||
this.router.start();
|
||||
this.emit('start');
|
||||
}.bind(this));
|
||||
@@ -458,6 +461,7 @@ define([
|
||||
};
|
||||
|
||||
MCT.prototype.destroy = function () {
|
||||
window.removeEventListener('beforeunload', this.destroy);
|
||||
this.emit('destroy');
|
||||
this.router.destroy();
|
||||
};
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { TelemetryCollection } = require("./TelemetryCollection");
|
||||
|
||||
define([
|
||||
'../../plugins/displayLayout/CustomStringFormatter',
|
||||
'./TelemetryMetadataManager',
|
||||
@@ -273,6 +275,28 @@ define([
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Request telemetry collection for a domain object.
|
||||
* The `options` argument allows you to specify filters
|
||||
* (start, end, etc.), sort order, and strategies for retrieving
|
||||
* telemetry (aggregation, latest available, etc.).
|
||||
*
|
||||
* @method requestTelemetryCollection
|
||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||
* @param {module:openmct.DomainObject} domainObject the object
|
||||
* which has associated telemetry
|
||||
* @param {module:openmct.TelemetryAPI~TelemetryRequest} options
|
||||
* options for this telemetry collection request
|
||||
* @returns {TelemetryCollection} a TelemetryCollection instance
|
||||
*/
|
||||
TelemetryAPI.prototype.requestTelemetryCollection = function (domainObject, options = {}) {
|
||||
return new TelemetryCollection(
|
||||
this.openmct,
|
||||
domainObject,
|
||||
options
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Request historical telemetry for a domain object.
|
||||
* The `options` argument allows you to specify filters
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
366
src/api/telemetry/TelemetryCollection.js
Normal file
366
src/api/telemetry/TelemetryCollection.js
Normal file
@@ -0,0 +1,366 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import _ from 'lodash';
|
||||
import EventEmitter from 'EventEmitter';
|
||||
|
||||
/** Class representing a Telemetry Collection. */
|
||||
|
||||
export class TelemetryCollection extends EventEmitter {
|
||||
/**
|
||||
* Creates a Telemetry Collection
|
||||
*
|
||||
* @param {object} openmct - Openm MCT
|
||||
* @param {object} domainObject - Domain Object to user for telemetry collection
|
||||
* @param {object} options - Any options passed in for request/subscribe
|
||||
*/
|
||||
constructor(openmct, domainObject, options) {
|
||||
super();
|
||||
|
||||
this.loaded = false;
|
||||
this.openmct = openmct;
|
||||
this.domainObject = domainObject;
|
||||
this.boundedTelemetry = [];
|
||||
this.futureBuffer = [];
|
||||
this.parseTime = undefined;
|
||||
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
|
||||
this.unsubscribe = undefined;
|
||||
this.historicalProvider = undefined;
|
||||
this.options = options;
|
||||
this.pageState = undefined;
|
||||
this.lastBounds = undefined;
|
||||
this.requestAbort = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* This will start the requests for historical and realtime data,
|
||||
* as well as setting up initial values and watchers
|
||||
*/
|
||||
load() {
|
||||
if (this.loaded) {
|
||||
throw new Error('Telemetry Collection has already been loaded.');
|
||||
}
|
||||
|
||||
this._timeSystem(this.openmct.time.timeSystem());
|
||||
this.lastBounds = this.openmct.time.bounds();
|
||||
|
||||
this._watchBounds();
|
||||
this._watchTimeSystem();
|
||||
|
||||
this._initiateHistoricalRequests();
|
||||
this._initiateSubscriptionTelemetry();
|
||||
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* can/should be called by the requester of the telemetry collection
|
||||
* to remove any listeners
|
||||
*/
|
||||
destroy() {
|
||||
if (this.requestAbort) {
|
||||
this.requestAbort.abort();
|
||||
}
|
||||
|
||||
this._unwatchBounds();
|
||||
this._unwatchTimeSystem();
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* This will start the requests for historical and realtime data,
|
||||
* as well as setting up initial values and watchers
|
||||
*/
|
||||
getAll() {
|
||||
return this.boundedTelemetry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the telemetry collection for historical requests,
|
||||
* this uses the "standardizeRequestOptions" from Telemetry API
|
||||
* @private
|
||||
*/
|
||||
_initiateHistoricalRequests() {
|
||||
this.openmct.telemetry.standardizeRequestOptions(this.options);
|
||||
this.historicalProvider = this.openmct.telemetry.
|
||||
findRequestProvider(this.domainObject, this.options);
|
||||
|
||||
this._requestHistoricalTelemetry();
|
||||
}
|
||||
/**
|
||||
* If a historical provider exists, then historical requests will be made
|
||||
* @private
|
||||
*/
|
||||
async _requestHistoricalTelemetry() {
|
||||
if (!this.historicalProvider) {
|
||||
return;
|
||||
}
|
||||
|
||||
let historicalData;
|
||||
|
||||
try {
|
||||
this.requestAbort = new AbortController();
|
||||
this.options.abortSignal = this.requestAbort.signal;
|
||||
historicalData = await this.historicalProvider.request(this.domainObject, this.options);
|
||||
this.requestAbort = undefined;
|
||||
} catch (error) {
|
||||
console.error('Error requesting telemetry data...');
|
||||
this.requestAbort = undefined;
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
this._processNewTelemetry(historicalData);
|
||||
|
||||
}
|
||||
/**
|
||||
* This uses the built in subscription function from Telemetry API
|
||||
* @private
|
||||
*/
|
||||
_initiateSubscriptionTelemetry() {
|
||||
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
this.unsubscribe = this.openmct.telemetry
|
||||
.subscribe(
|
||||
this.domainObject,
|
||||
datum => this._processNewTelemetry(datum),
|
||||
this.options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter any new telemetry (add/page, historical, subscription) based on
|
||||
* time bounds and dupes
|
||||
*
|
||||
* @param {(Object|Object[])} telemetryData - telemetry data object or
|
||||
* array of telemetry data objects
|
||||
* @private
|
||||
*/
|
||||
_processNewTelemetry(telemetryData) {
|
||||
let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData];
|
||||
let parsedValue;
|
||||
let beforeStartOfBounds;
|
||||
let afterEndOfBounds;
|
||||
let added = [];
|
||||
|
||||
for (let datum of data) {
|
||||
parsedValue = this.parseTime(datum);
|
||||
beforeStartOfBounds = parsedValue < this.lastBounds.start;
|
||||
afterEndOfBounds = parsedValue > this.lastBounds.end;
|
||||
|
||||
if (!afterEndOfBounds && !beforeStartOfBounds) {
|
||||
let isDuplicate = false;
|
||||
let startIndex = this._sortedIndex(datum);
|
||||
let endIndex = undefined;
|
||||
|
||||
// dupe check
|
||||
if (startIndex !== this.boundedTelemetry.length) {
|
||||
endIndex = _.sortedLastIndexBy(
|
||||
this.boundedTelemetry,
|
||||
datum,
|
||||
boundedDatum => this.parseTime(boundedDatum)
|
||||
);
|
||||
|
||||
if (endIndex > startIndex) {
|
||||
let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex);
|
||||
|
||||
isDuplicate = potentialDupes.some(_.isEqual(undefined, datum));
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDuplicate) {
|
||||
let index = endIndex || startIndex;
|
||||
|
||||
this.boundedTelemetry.splice(index, 0, datum);
|
||||
added.push(datum);
|
||||
}
|
||||
|
||||
} else if (afterEndOfBounds) {
|
||||
this.futureBuffer.push(datum);
|
||||
}
|
||||
}
|
||||
|
||||
if (added.length) {
|
||||
this.emit('add', added);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the correct insertion point for the given telemetry datum.
|
||||
* Leverages lodash's `sortedIndexBy` function which implements a binary search.
|
||||
* @private
|
||||
*/
|
||||
_sortedIndex(datum) {
|
||||
if (this.boundedTelemetry.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let parsedValue = this.parseTime(datum);
|
||||
let lastValue = this.parseTime(this.boundedTelemetry[this.boundedTelemetry.length - 1]);
|
||||
|
||||
if (parsedValue > lastValue || parsedValue === lastValue) {
|
||||
return this.boundedTelemetry.length;
|
||||
} else {
|
||||
return _.sortedIndexBy(
|
||||
this.boundedTelemetry,
|
||||
datum,
|
||||
boundedDatum => this.parseTime(boundedDatum)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* when the start time, end time, or both have been updated.
|
||||
* data could be added OR removed here we update the current
|
||||
* bounded telemetry
|
||||
*
|
||||
* @param {TimeConductorBounds} bounds The newly updated bounds
|
||||
* @param {boolean} [tick] `true` if the bounds update was due to
|
||||
* a "tick" event (ie. was an automatic update), false otherwise.
|
||||
* @private
|
||||
*/
|
||||
_bounds(bounds, isTick) {
|
||||
let startChanged = this.lastBounds.start !== bounds.start;
|
||||
let endChanged = this.lastBounds.end !== bounds.end;
|
||||
|
||||
this.lastBounds = bounds;
|
||||
|
||||
if (isTick) {
|
||||
// need to check futureBuffer and need to check
|
||||
// if anything has fallen out of bounds
|
||||
let startIndex = 0;
|
||||
let endIndex = 0;
|
||||
|
||||
let discarded = [];
|
||||
let added = [];
|
||||
let testDatum = {};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (endChanged) {
|
||||
testDatum[this.timeKey] = bounds.end;
|
||||
// Calculate the new index of the last item in bounds
|
||||
endIndex = _.sortedLastIndexBy(
|
||||
this.futureBuffer,
|
||||
testDatum,
|
||||
datum => this.parseTime(datum)
|
||||
);
|
||||
added = this.futureBuffer.splice(0, endIndex);
|
||||
this.boundedTelemetry = [...this.boundedTelemetry, ...added];
|
||||
}
|
||||
|
||||
if (discarded.length > 0) {
|
||||
this.emit('remove', discarded);
|
||||
}
|
||||
|
||||
if (added.length > 0) {
|
||||
this.emit('add', added);
|
||||
}
|
||||
|
||||
} else {
|
||||
// user bounds change, reset
|
||||
this._reset();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* whenever the time system is updated need to update related values in
|
||||
* the Telemetry Collection and reset the telemetry collection
|
||||
*
|
||||
* @param {TimeSystem} timeSystem - the value of the currently applied
|
||||
* Time System
|
||||
* @private
|
||||
*/
|
||||
_timeSystem(timeSystem) {
|
||||
this.timeKey = timeSystem.key;
|
||||
let metadataValue = this.metadata.value(this.timeKey) || { format: this.timeKey };
|
||||
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||
|
||||
this.parseTime = (datum) => {
|
||||
return valueFormatter.parse(datum);
|
||||
};
|
||||
|
||||
this._reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the telemetry data of the collection, and re-request
|
||||
* historical telemetry
|
||||
* @private
|
||||
*
|
||||
* @todo handle subscriptions more granually
|
||||
*/
|
||||
_reset() {
|
||||
this.boundedTelemetry = [];
|
||||
this.futureBuffer = [];
|
||||
|
||||
this._requestHistoricalTelemetry();
|
||||
}
|
||||
|
||||
/**
|
||||
* adds the _bounds callback to the 'bounds' timeAPI listener
|
||||
* @private
|
||||
*/
|
||||
_watchBounds() {
|
||||
this.openmct.time.on('bounds', this._bounds, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* removes the _bounds callback from the 'bounds' timeAPI listener
|
||||
* @private
|
||||
*/
|
||||
_unwatchBounds() {
|
||||
this.openmct.time.off('bounds', this._bounds, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* adds the _timeSystem callback to the 'timeSystem' timeAPI listener
|
||||
* @private
|
||||
*/
|
||||
_watchTimeSystem() {
|
||||
this.openmct.time.on('timeSystem', this._timeSystem, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* removes the _timeSystem callback from the 'timeSystem' timeAPI listener
|
||||
* @private
|
||||
*/
|
||||
_unwatchTimeSystem() {
|
||||
this.openmct.time.off('timeSystem', this._timeSystem, this);
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,9 @@ class ImageExporter {
|
||||
}
|
||||
|
||||
return html2canvas(element, {
|
||||
useCORS: true,
|
||||
allowTaint: true,
|
||||
logging: false,
|
||||
onclone: function (document) {
|
||||
if (className) {
|
||||
const clonedElement = document.getElementById(exportId);
|
||||
|
||||
@@ -38,7 +38,6 @@ const DEFAULTS = [
|
||||
'platform/exporters',
|
||||
'platform/telemetry',
|
||||
'platform/features/clock',
|
||||
'platform/features/hyperlink',
|
||||
'platform/forms',
|
||||
'platform/identity',
|
||||
'platform/persistence/aggregator',
|
||||
@@ -81,7 +80,6 @@ define([
|
||||
'../platform/exporters/bundle',
|
||||
'../platform/features/clock/bundle',
|
||||
'../platform/features/my-items/bundle',
|
||||
'../platform/features/hyperlink/bundle',
|
||||
'../platform/features/static-markup/bundle',
|
||||
'../platform/forms/bundle',
|
||||
'../platform/framework/bundle',
|
||||
|
||||
50
src/plugins/LADTable/components/LADHead.vue
Normal file
50
src/plugins/LADTable/components/LADHead.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Timestamp</th>
|
||||
<th v-for="name in columnNames"
|
||||
:key="name"
|
||||
>
|
||||
{{ name }}
|
||||
</th>
|
||||
<th v-if="hasUnits">Unit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
columnNames: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
hasUnits() {
|
||||
// let itemsWithUnits = this.items.filter((item) => {
|
||||
// let metadata = this.openmct.telemetry.getMetadata(item.domainObject);
|
||||
|
||||
// return this.metadataHasUnits(metadata.valueMetadatas);
|
||||
|
||||
// });
|
||||
|
||||
// return itemsWithUnits.length !== 0;
|
||||
return false;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// console.log(this.names);
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
@@ -29,9 +29,11 @@
|
||||
<td class="js-first-data">{{ domainObject.name }}</td>
|
||||
<td class="js-second-data">{{ formattedTimestamp }}</td>
|
||||
<td
|
||||
v-for="name in columnNames"
|
||||
:key="name"
|
||||
class="js-third-data"
|
||||
:class="valueClass"
|
||||
>{{ value }}</td>
|
||||
>{{ value[name] }}</td>
|
||||
<td
|
||||
v-if="hasUnits"
|
||||
class="js-units"
|
||||
@@ -63,6 +65,10 @@ export default {
|
||||
hasUnits: {
|
||||
type: Boolean,
|
||||
requred: true
|
||||
},
|
||||
columnNames: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -82,6 +88,7 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log(this.colNames);
|
||||
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
@@ -96,12 +103,17 @@ export default {
|
||||
|
||||
this.timestampKey = this.openmct.time.timeSystem().key;
|
||||
|
||||
// this.valueMetadata = this
|
||||
// .metadata
|
||||
// .valuesForHints(['range'])[0];
|
||||
|
||||
// this.valueKey = this.valueMetadata.key;
|
||||
this.valueMetadata = this
|
||||
.metadata
|
||||
.valuesForHints(['range'])[0];
|
||||
|
||||
this.valueKey = this.valueMetadata.key;
|
||||
|
||||
.valuesForHints(['range']);
|
||||
// console.log(this.valueMetadata);
|
||||
this.valueKey = this.valueMetadata.map(value => value.key);
|
||||
// console.log(this.valueKey);
|
||||
this.unsubscribe = this.openmct
|
||||
.telemetry
|
||||
.subscribe(this.domainObject, this.updateValues);
|
||||
@@ -123,9 +135,18 @@ export default {
|
||||
let limit;
|
||||
|
||||
if (this.shouldUpdate(newTimestamp)) {
|
||||
// console.log(datum);
|
||||
this.datum = datum;
|
||||
this.timestamp = newTimestamp;
|
||||
this.value = this.formats[this.valueKey].format(datum);
|
||||
// console.log(this.formats);
|
||||
// this.value = this.formats[this.valueKey].format(datum);
|
||||
this.value = {};
|
||||
this.valueKey.forEach(key => {
|
||||
let formattedDatum = this.formats[key].format(datum);
|
||||
this.value[key] = formattedDatum;
|
||||
limit = this.limitEvaluator.evaluate(formattedDatum, this.valueMetadata);
|
||||
});
|
||||
// console.log(this.value);
|
||||
limit = this.limitEvaluator.evaluate(datum, this.valueMetadata);
|
||||
if (limit) {
|
||||
this.valueClass = limit.cssClass;
|
||||
|
||||
@@ -22,19 +22,17 @@
|
||||
|
||||
<template>
|
||||
<div class="c-lad-table-wrapper u-style-receiver js-style-receiver">
|
||||
<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>
|
||||
<table v-for="ladRow in items"
|
||||
:key="ladRow.key"
|
||||
class="c-table c-lad-table"
|
||||
>
|
||||
<lad-head
|
||||
:item="ladRow"
|
||||
:column-names="columnNames(ladRow)"
|
||||
/>
|
||||
<tbody>
|
||||
<lad-row
|
||||
v-for="ladRow in items"
|
||||
:key="ladRow.key"
|
||||
:column-names="columnNames(ladRow)"
|
||||
:domain-object="ladRow.domainObject"
|
||||
:path-to-table="objectPath"
|
||||
:has-units="hasUnits"
|
||||
@@ -47,10 +45,12 @@
|
||||
|
||||
<script>
|
||||
import LadRow from './LADRow.vue';
|
||||
import LadHead from './LADHead.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LadRow
|
||||
LadRow,
|
||||
LadHead
|
||||
},
|
||||
inject: ['openmct', 'currentView'],
|
||||
props: {
|
||||
@@ -94,6 +94,13 @@ export default {
|
||||
this.composition.off('reorder', this.reorder);
|
||||
},
|
||||
methods: {
|
||||
columnNames(item) {
|
||||
let metadata = this.openmct.telemetry.getMetadata(item.domainObject);
|
||||
let valueMetadata = metadata
|
||||
.valuesForHints(['range']);
|
||||
|
||||
return valueMetadata.map(value => value.key);
|
||||
},
|
||||
addItem(domainObject) {
|
||||
let item = {};
|
||||
item.domainObject = domainObject;
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
<script>
|
||||
import LayoutFrame from './LayoutFrame.vue';
|
||||
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
|
||||
import { getDefaultNotebook } from '@/plugins/notebook/utils/notebook-storage.js';
|
||||
import { getDefaultNotebook, getNotebookSectionAndPage } from '@/plugins/notebook/utils/notebook-storage.js';
|
||||
|
||||
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
|
||||
const DEFAULT_POSITION = [1, 1];
|
||||
@@ -336,12 +336,15 @@ export default {
|
||||
},
|
||||
async getContextMenuActions() {
|
||||
const defaultNotebook = getDefaultNotebook();
|
||||
const domainObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier);
|
||||
|
||||
let defaultNotebookName;
|
||||
if (defaultNotebook) {
|
||||
const defaultPath = domainObject && `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`;
|
||||
defaultNotebookName = `Copy to Notebook ${defaultPath}`;
|
||||
const domainObject = await this.openmct.objects.get(defaultNotebook.identifier);
|
||||
const { section, page } = getNotebookSectionAndPage(domainObject, defaultNotebook.defaultSectionId, defaultNotebook.defaultPageId);
|
||||
if (section && page) {
|
||||
const defaultPath = domainObject && `${domainObject.name} - ${section.name} - ${page.name}`;
|
||||
defaultNotebookName = `Copy to Notebook ${defaultPath}`;
|
||||
}
|
||||
}
|
||||
|
||||
return CONTEXT_MENU_ACTIONS
|
||||
|
||||
51
src/plugins/hyperlink/HyperlinkLayout.vue
Normal file
51
src/plugins/hyperlink/HyperlinkLayout.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
|
||||
<a class="c-hyperlink"
|
||||
:class="{
|
||||
'c-hyperlink--button' : isButton
|
||||
}"
|
||||
:target="domainObject.linkTarget"
|
||||
:href="domainObject.url"
|
||||
>
|
||||
<span class="c-hyperlink__label">{{ domainObject.displayText }}</span>
|
||||
</a>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
inject: ['domainObject'],
|
||||
computed: {
|
||||
isButton() {
|
||||
if (this.domainObject.displayFormat === "link") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
59
src/plugins/hyperlink/HyperlinkProvider.js
Normal file
59
src/plugins/hyperlink/HyperlinkProvider.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import HyperlinkLayout from './HyperlinkLayout.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default function HyperlinkProvider(openmct) {
|
||||
|
||||
return {
|
||||
key: 'hyperlink.view',
|
||||
name: 'Hyperlink',
|
||||
cssClass: 'icon-chain-links',
|
||||
canView(domainObject) {
|
||||
return domainObject.type === 'hyperlink';
|
||||
},
|
||||
|
||||
view: function (domainObject) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
HyperlinkLayout
|
||||
},
|
||||
provide: {
|
||||
domainObject
|
||||
},
|
||||
template: '<hyperlink-layout></hyperlink-layout>'
|
||||
});
|
||||
},
|
||||
destroy: function () {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
89
src/plugins/hyperlink/plugin.js
Normal file
89
src/plugins/hyperlink/plugin.js
Normal file
@@ -0,0 +1,89 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import HyperlinkProvider from './HyperlinkProvider';
|
||||
|
||||
export default function () {
|
||||
return function install(openmct) {
|
||||
openmct.types.addType('hyperlink', {
|
||||
name: 'Hyperlink',
|
||||
key: 'hyperlink',
|
||||
description: 'A hyperlink to redirect to a different link',
|
||||
creatable: true,
|
||||
cssClass: 'icon-chain-links',
|
||||
initialize: function (domainObject) {
|
||||
domainObject.displayFormat = "link";
|
||||
domainObject.linkTarget = "_self";
|
||||
},
|
||||
form: [
|
||||
{
|
||||
"key": "url",
|
||||
"name": "URL",
|
||||
"control": "textfield",
|
||||
"required": true,
|
||||
"cssClass": "l-input-lg"
|
||||
},
|
||||
{
|
||||
"key": "displayText",
|
||||
"name": "Text to Display",
|
||||
"control": "textfield",
|
||||
"required": true,
|
||||
"cssClass": "l-input-lg"
|
||||
},
|
||||
{
|
||||
"key": "displayFormat",
|
||||
"name": "Display Format",
|
||||
"control": "select",
|
||||
"options": [
|
||||
{
|
||||
"name": "Link",
|
||||
"value": "link"
|
||||
},
|
||||
{
|
||||
"name": "Button",
|
||||
"value": "button"
|
||||
}
|
||||
],
|
||||
"cssClass": "l-inline"
|
||||
},
|
||||
{
|
||||
"key": "linkTarget",
|
||||
"name": "Tab to Open Hyperlink",
|
||||
"control": "select",
|
||||
"options": [
|
||||
{
|
||||
"name": "Open in this tab",
|
||||
"value": "_self"
|
||||
},
|
||||
{
|
||||
"name": "Open in a new tab",
|
||||
"value": "_blank"
|
||||
}
|
||||
],
|
||||
"cssClass": "l-inline"
|
||||
|
||||
}
|
||||
]
|
||||
});
|
||||
openmct.objectViews.addProvider(new HyperlinkProvider(openmct));
|
||||
};
|
||||
}
|
||||
130
src/plugins/hyperlink/pluginSpec.js
Normal file
130
src/plugins/hyperlink/pluginSpec.js
Normal file
@@ -0,0 +1,130 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2009-2016, 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 { createOpenMct, resetApplicationState } from "utils/testing";
|
||||
import HyperlinkPlugin from "./plugin";
|
||||
|
||||
function getView(openmct, domainObj, objectPath) {
|
||||
const applicableViews = openmct.objectViews.get(domainObj, objectPath);
|
||||
const hyperLinkView = applicableViews.find((viewProvider) => viewProvider.key === 'hyperlink.view');
|
||||
|
||||
return hyperLinkView.view(domainObj);
|
||||
}
|
||||
|
||||
function destroyView(view) {
|
||||
return view.destroy();
|
||||
}
|
||||
|
||||
describe("The controller for hyperlinks", function () {
|
||||
let mockDomainObject;
|
||||
let mockObjectPath;
|
||||
let openmct;
|
||||
let element;
|
||||
let child;
|
||||
let view;
|
||||
|
||||
beforeEach((done) => {
|
||||
mockObjectPath = [
|
||||
{
|
||||
name: 'mock hyperlink',
|
||||
type: 'hyperlink',
|
||||
identifier: {
|
||||
key: 'mock-hyperlink',
|
||||
namespace: ''
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
mockDomainObject = {
|
||||
displayFormat: "",
|
||||
linkTarget: "",
|
||||
name: "Unnamed HyperLink",
|
||||
type: "hyperlink",
|
||||
location: "f69c21ac-24ef-450c-8e2f-3d527087d285",
|
||||
modified: 1627483839783,
|
||||
url: "123",
|
||||
displayText: "123",
|
||||
persisted: 1627483839783,
|
||||
id: "3d9c243d-dffb-446b-8474-d9931a99d679",
|
||||
identifier: {
|
||||
namespace: "",
|
||||
key: "3d9c243d-dffb-446b-8474-d9931a99d679"
|
||||
}
|
||||
};
|
||||
|
||||
openmct = createOpenMct();
|
||||
openmct.install(new HyperlinkPlugin());
|
||||
|
||||
element = document.createElement('div');
|
||||
element.style.width = '640px';
|
||||
element.style.height = '480px';
|
||||
child = document.createElement('div');
|
||||
child.style.width = '640px';
|
||||
child.style.height = '480px';
|
||||
element.appendChild(child);
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
destroyView(view);
|
||||
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
it("knows when it should open a new tab", () => {
|
||||
mockDomainObject.displayFormat = "link";
|
||||
mockDomainObject.linkTarget = "_blank";
|
||||
|
||||
view = getView(openmct, mockDomainObject, mockObjectPath);
|
||||
view.show(child, true);
|
||||
|
||||
expect(element.querySelector('.c-hyperlink').target).toBe('_blank');
|
||||
});
|
||||
it("knows when it should open in the same tab", function () {
|
||||
mockDomainObject.displayFormat = "button";
|
||||
mockDomainObject.linkTarget = "_self";
|
||||
|
||||
view = getView(openmct, mockDomainObject, mockObjectPath);
|
||||
view.show(child, true);
|
||||
|
||||
expect(element.querySelector('.c-hyperlink').target).toBe('_self');
|
||||
});
|
||||
|
||||
it("knows when it is a button", function () {
|
||||
mockDomainObject.displayFormat = "button";
|
||||
|
||||
view = getView(openmct, mockDomainObject, mockObjectPath);
|
||||
view.show(child, true);
|
||||
|
||||
expect(element.querySelector('.c-hyperlink--button')).toBeDefined();
|
||||
});
|
||||
it("knows when it is a link", function () {
|
||||
mockDomainObject.displayFormat = "link";
|
||||
|
||||
view = getView(openmct, mockDomainObject, mockObjectPath);
|
||||
view.show(child, true);
|
||||
|
||||
expect(element.querySelector('.c-hyperlink')).not.toHaveClass('c-hyperlink--button');
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
.c-imagery {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&:focus {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getDefaultNotebook } from '../utils/notebook-storage';
|
||||
import { getDefaultNotebook, getNotebookSectionAndPage } from '../utils/notebook-storage';
|
||||
import { addNotebookEntry } from '../utils/notebook-entries';
|
||||
|
||||
export default class CopyToNotebookAction {
|
||||
@@ -15,11 +15,16 @@ export default class CopyToNotebookAction {
|
||||
|
||||
copyToNotebook(entryText) {
|
||||
const notebookStorage = getDefaultNotebook();
|
||||
this.openmct.objects.get(notebookStorage.notebookMeta.identifier)
|
||||
this.openmct.objects.get(notebookStorage.identifier)
|
||||
.then(domainObject => {
|
||||
addNotebookEntry(this.openmct, domainObject, notebookStorage, null, entryText);
|
||||
|
||||
const defaultPath = `${domainObject.name} - ${notebookStorage.section.name} - ${notebookStorage.page.name}`;
|
||||
const { section, page } = getNotebookSectionAndPage(domainObject, notebookStorage.defaultSectionId, notebookStorage.defaultPageId);
|
||||
if (!section || !page) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultPath = `${domainObject.name} - ${section.name} - ${page.name}`;
|
||||
const msg = `Saved to Notebook ${defaultPath}`;
|
||||
this.openmct.notifications.info(msg);
|
||||
});
|
||||
|
||||
@@ -43,14 +43,16 @@
|
||||
class="c-notebook__nav c-sidebar c-drawer c-drawer--align-left"
|
||||
:class="[{'is-expanded': showNav}, {'c-drawer--push': !sidebarCoversEntries}, {'c-drawer--overlays': sidebarCoversEntries}]"
|
||||
:default-page-id="defaultPageId"
|
||||
:selected-page-id="selectedPageId"
|
||||
:selected-page-id="getSelectedPageId()"
|
||||
:default-section-id="defaultSectionId"
|
||||
:selected-section-id="selectedSectionId"
|
||||
:selected-section-id="getSelectedSectionId()"
|
||||
:domain-object="domainObject"
|
||||
:page-title="domainObject.configuration.pageTitle"
|
||||
:section-title="domainObject.configuration.sectionTitle"
|
||||
:sections="sections"
|
||||
:sidebar-covers-entries="sidebarCoversEntries"
|
||||
@defaultPageDeleted="cleanupDefaultNotebook"
|
||||
@defaultSectionDeleted="cleanupDefaultNotebook"
|
||||
@pagesChanged="pagesChanged"
|
||||
@selectPage="selectPage"
|
||||
@sectionsChanged="sectionsChanged"
|
||||
@@ -136,7 +138,7 @@ import NotebookEntry from './NotebookEntry.vue';
|
||||
import Search from '@/ui/components/search.vue';
|
||||
import SearchResults from './SearchResults.vue';
|
||||
import Sidebar from './Sidebar.vue';
|
||||
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage';
|
||||
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSectionId, setDefaultNotebookPageId } from '../utils/notebook-storage';
|
||||
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
|
||||
import { NOTEBOOK_VIEW_TYPE } from '../notebook-constants';
|
||||
import objectUtils from 'objectUtils';
|
||||
@@ -164,8 +166,10 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedSectionId: this.getDefaultSectionId(),
|
||||
selectedPageId: this.getDefaultPageId(),
|
||||
defaultPageId: this.getDefaultPageId(),
|
||||
defaultSectionId: this.getDefaultSectionId(),
|
||||
selectedSectionId: this.getSelectedSectionId(),
|
||||
selectedPageId: this.getSelectedPageId(),
|
||||
defaultSort: this.domainObject.configuration.defaultSort,
|
||||
focusEntryId: null,
|
||||
search: '',
|
||||
@@ -176,12 +180,6 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
defaultPageId() {
|
||||
return this.getDefaultPageId();
|
||||
},
|
||||
defaultSectionId() {
|
||||
return this.getDefaultSectionId();
|
||||
},
|
||||
filteredAndSortedEntries() {
|
||||
const filterTime = Date.now();
|
||||
const pageEntries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage) || [];
|
||||
@@ -203,24 +201,38 @@ export default {
|
||||
},
|
||||
selectedPage() {
|
||||
const pages = this.getPages();
|
||||
const selectedPage = pages.find(page => page.id === this.selectedPageId);
|
||||
if (!pages.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const selectedPage = pages.find(page => page.id === this.selectedPageId);
|
||||
if (selectedPage) {
|
||||
return selectedPage;
|
||||
}
|
||||
|
||||
if (!selectedPage && !pages.length) {
|
||||
return undefined;
|
||||
const defaultPage = pages.find(page => page.id === this.defaultPageId);
|
||||
if (defaultPage) {
|
||||
return defaultPage;
|
||||
}
|
||||
|
||||
return pages[0];
|
||||
return this.pages[0];
|
||||
},
|
||||
selectedSection() {
|
||||
if (!this.sections.length) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.sections.find(section => section.id === this.selectedSectionId);
|
||||
const selectedSection = this.sections.find(section => section.id === this.selectedSectionId);
|
||||
if (selectedSection) {
|
||||
return selectedSection;
|
||||
}
|
||||
|
||||
const defaultSection = this.sections.find(section => section.id === this.defaultSectionId);
|
||||
if (defaultSection) {
|
||||
return defaultSection;
|
||||
}
|
||||
|
||||
return this.sections[0];
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -301,26 +313,29 @@ export default {
|
||||
this.sectionsChanged({ sections });
|
||||
this.resetSearch();
|
||||
},
|
||||
cleanupDefaultNotebook() {
|
||||
this.defaultPageId = undefined;
|
||||
this.defaultSectionId = undefined;
|
||||
this.removeDefaultClass(this.domainObject);
|
||||
clearDefaultNotebook();
|
||||
},
|
||||
setSectionAndPageFromUrl() {
|
||||
let sectionId = this.getSectionIdFromUrl() || this.selectedSectionId;
|
||||
let pageId = this.getPageIdFromUrl() || this.selectedPageId;
|
||||
let sectionId = this.getSectionIdFromUrl() || this.getDefaultSectionId() || this.getSelectedSectionId();
|
||||
let pageId = this.getPageIdFromUrl() || this.getDefaultPageId() || this.getSelectedPageId();
|
||||
|
||||
this.selectSection(sectionId);
|
||||
this.selectPage(pageId);
|
||||
},
|
||||
createNotebookStorageObject() {
|
||||
const notebookMeta = {
|
||||
name: this.domainObject.name,
|
||||
identifier: this.domainObject.identifier,
|
||||
link: this.getLinktoNotebook()
|
||||
};
|
||||
const page = this.selectedPage;
|
||||
const section = this.selectedSection;
|
||||
|
||||
return {
|
||||
notebookMeta,
|
||||
page,
|
||||
section
|
||||
name: this.domainObject.name,
|
||||
identifier: this.domainObject.identifier,
|
||||
link: this.getLinktoNotebook(),
|
||||
defaultSectionId: section.id,
|
||||
defaultPageId: page.id
|
||||
};
|
||||
},
|
||||
deleteEntry(entryId) {
|
||||
@@ -419,35 +434,21 @@ export default {
|
||||
this.sidebarCoversEntries = sidebarCoversEntries;
|
||||
},
|
||||
getDefaultPageId() {
|
||||
let defaultPageId;
|
||||
|
||||
if (this.isDefaultNotebook()) {
|
||||
defaultPageId = getDefaultNotebook().page.id;
|
||||
} else {
|
||||
const firstSection = this.getSections()[0];
|
||||
defaultPageId = firstSection && firstSection.pages[0].id;
|
||||
}
|
||||
|
||||
return defaultPageId;
|
||||
return this.isDefaultNotebook()
|
||||
? getDefaultNotebook().defaultPageId
|
||||
: undefined;
|
||||
},
|
||||
isDefaultNotebook() {
|
||||
const defaultNotebook = getDefaultNotebook();
|
||||
const defaultNotebookIdentifier = defaultNotebook && defaultNotebook.notebookMeta.identifier;
|
||||
const defaultNotebookIdentifier = defaultNotebook && defaultNotebook.identifier;
|
||||
|
||||
return defaultNotebookIdentifier !== null
|
||||
&& this.openmct.objects.areIdsEqual(defaultNotebookIdentifier, this.domainObject.identifier);
|
||||
},
|
||||
getDefaultSectionId() {
|
||||
let defaultSectionId;
|
||||
|
||||
if (this.isDefaultNotebook()) {
|
||||
defaultSectionId = getDefaultNotebook().section.id;
|
||||
} else {
|
||||
const firstSection = this.getSections()[0];
|
||||
defaultSectionId = firstSection && firstSection.id;
|
||||
}
|
||||
|
||||
return defaultSectionId;
|
||||
return this.isDefaultNotebook()
|
||||
? getDefaultNotebook().defaultSectionId
|
||||
: undefined;
|
||||
},
|
||||
getDefaultNotebookObject() {
|
||||
const oldNotebookStorage = getDefaultNotebook();
|
||||
@@ -455,7 +456,7 @@ export default {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.openmct.objects.get(oldNotebookStorage.notebookMeta.identifier);
|
||||
return this.openmct.objects.get(oldNotebookStorage.identifier);
|
||||
},
|
||||
getLinktoNotebook() {
|
||||
const objectPath = this.openmct.router.path;
|
||||
@@ -573,6 +574,22 @@ export default {
|
||||
|
||||
return selectedSection.pages;
|
||||
},
|
||||
getSelectedPageId() {
|
||||
const page = this.selectedPage;
|
||||
if (!page) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return page.id;
|
||||
},
|
||||
getSelectedSectionId() {
|
||||
const section = this.selectedSection;
|
||||
if (!section) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return section.id;
|
||||
},
|
||||
newEntry(embed = null) {
|
||||
this.resetSearch();
|
||||
const notebookStorage = this.createNotebookStorageObject();
|
||||
@@ -616,51 +633,26 @@ export default {
|
||||
},
|
||||
async updateDefaultNotebook(notebookStorage) {
|
||||
const defaultNotebookObject = await this.getDefaultNotebookObject();
|
||||
if (!defaultNotebookObject) {
|
||||
setDefaultNotebook(this.openmct, notebookStorage, this.domainObject);
|
||||
} else if (objectUtils.makeKeyString(defaultNotebookObject.identifier) !== objectUtils.makeKeyString(notebookStorage.notebookMeta.identifier)) {
|
||||
const isSameNotebook = defaultNotebookObject
|
||||
&& objectUtils.makeKeyString(defaultNotebookObject.identifier) === objectUtils.makeKeyString(notebookStorage.identifier);
|
||||
if (!isSameNotebook) {
|
||||
this.removeDefaultClass(defaultNotebookObject);
|
||||
}
|
||||
|
||||
if (!defaultNotebookObject || !isSameNotebook) {
|
||||
setDefaultNotebook(this.openmct, notebookStorage, this.domainObject);
|
||||
}
|
||||
|
||||
if (this.defaultSectionId && this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) {
|
||||
this.defaultSectionId = notebookStorage.section.id;
|
||||
setDefaultNotebookSection(notebookStorage.section);
|
||||
if (this.defaultSectionId !== notebookStorage.defaultSectionId) {
|
||||
setDefaultNotebookSectionId(notebookStorage.defaultSectionId);
|
||||
this.defaultSectionId = notebookStorage.defaultSectionId;
|
||||
}
|
||||
|
||||
if (this.defaultPageId && this.defaultPageId.length === 0 || this.defaultPageId !== notebookStorage.page.id) {
|
||||
this.defaultPageId = notebookStorage.page.id;
|
||||
setDefaultNotebookPage(notebookStorage.page);
|
||||
if (this.defaultPageId !== notebookStorage.defaultPageId) {
|
||||
setDefaultNotebookPageId(notebookStorage.defaultPageId);
|
||||
this.defaultPageId = notebookStorage.defaultPageId;
|
||||
}
|
||||
},
|
||||
updateDefaultNotebookPage(pages, id) {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const notebookStorage = getDefaultNotebook();
|
||||
if (!notebookStorage
|
||||
|| notebookStorage.notebookMeta.identifier.key !== this.domainObject.identifier.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultNotebookPage = notebookStorage.page;
|
||||
const page = pages.find(p => p.id === id);
|
||||
if (!page && defaultNotebookPage.id === id) {
|
||||
this.defaultSectionId = null;
|
||||
this.defaultPageId = null;
|
||||
this.removeDefaultClass(this.domainObject);
|
||||
clearDefaultNotebook();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (id !== defaultNotebookPage.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDefaultNotebookPage(page);
|
||||
},
|
||||
updateDefaultNotebookSection(sections, id) {
|
||||
if (!id) {
|
||||
return;
|
||||
@@ -668,26 +660,26 @@ export default {
|
||||
|
||||
const notebookStorage = getDefaultNotebook();
|
||||
if (!notebookStorage
|
||||
|| notebookStorage.notebookMeta.identifier.key !== this.domainObject.identifier.key) {
|
||||
|| notebookStorage.identifier.key !== this.domainObject.identifier.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultNotebookSection = notebookStorage.section;
|
||||
const section = sections.find(s => s.id === id);
|
||||
if (!section && defaultNotebookSection.id === id) {
|
||||
this.defaultSectionId = null;
|
||||
this.defaultPageId = null;
|
||||
this.removeDefaultClass(this.domainObject);
|
||||
clearDefaultNotebook();
|
||||
const defaultNotebookSectionId = notebookStorage.defaultSectionId;
|
||||
if (defaultNotebookSectionId === id) {
|
||||
const section = sections.find(s => s.id === id);
|
||||
if (!section) {
|
||||
this.removeDefaultClass(this.domainObject);
|
||||
clearDefaultNotebook();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (id !== defaultNotebookSectionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (id !== defaultNotebookSection.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDefaultNotebookSection(section);
|
||||
setDefaultNotebookSectionId(defaultNotebookSectionId);
|
||||
},
|
||||
updateEntry(entry) {
|
||||
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
|
||||
@@ -715,19 +707,27 @@ export default {
|
||||
sectionId: this.selectedSectionId
|
||||
});
|
||||
},
|
||||
sectionsChanged({ sections, id = null }) {
|
||||
sectionsChanged({ sections, id = undefined }) {
|
||||
mutateObject(this.openmct, this.domainObject, 'configuration.sections', sections);
|
||||
this.updateDefaultNotebookSection(sections, id);
|
||||
},
|
||||
selectPage(pageId) {
|
||||
if (!pageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedPageId = pageId;
|
||||
this.syncUrlWithPageAndSection();
|
||||
},
|
||||
selectSection(sectionId) {
|
||||
if (!sectionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedSectionId = sectionId;
|
||||
|
||||
const defaultPageId = this.selectedSection.pages[0].id;
|
||||
this.selectPage(defaultPageId);
|
||||
const pageId = this.selectedSection.pages[0].id;
|
||||
this.selectPage(pageId);
|
||||
|
||||
this.syncUrlWithPageAndSection();
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<script>
|
||||
import Snapshot from '../snapshot';
|
||||
import { getDefaultNotebook, validateNotebookStorageObject } from '../utils/notebook-storage';
|
||||
import { getDefaultNotebook, getNotebookSectionAndPage, validateNotebookStorageObject } from '../utils/notebook-storage';
|
||||
import { NOTEBOOK_DEFAULT, NOTEBOOK_SNAPSHOT } from '../notebook-constants';
|
||||
|
||||
export default {
|
||||
@@ -56,11 +56,10 @@ export default {
|
||||
this.setDefaultNotebookStatus();
|
||||
},
|
||||
methods: {
|
||||
async getDefaultNotebookObject() {
|
||||
getDefaultNotebookObject() {
|
||||
const defaultNotebook = getDefaultNotebook();
|
||||
const defaultNotebookObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier);
|
||||
|
||||
return defaultNotebookObject;
|
||||
return defaultNotebook && this.openmct.objects.get(defaultNotebook.identifier);
|
||||
},
|
||||
async showMenu(event) {
|
||||
const notebookTypes = [];
|
||||
@@ -70,20 +69,22 @@ export default {
|
||||
|
||||
const defaultNotebookObject = await this.getDefaultNotebookObject();
|
||||
if (defaultNotebookObject) {
|
||||
const name = defaultNotebookObject.name;
|
||||
|
||||
const defaultNotebook = getDefaultNotebook();
|
||||
const sectionName = defaultNotebook.section.name;
|
||||
const pageName = defaultNotebook.page.name;
|
||||
const defaultPath = `${name} - ${sectionName} - ${pageName}`;
|
||||
const { section, page } = getNotebookSectionAndPage(defaultNotebookObject, defaultNotebook.defaultSectionId, defaultNotebook.defaultPageId);
|
||||
if (section && page) {
|
||||
const name = defaultNotebookObject.name;
|
||||
const sectionName = section.name;
|
||||
const pageName = page.name;
|
||||
const defaultPath = `${name} - ${sectionName} - ${pageName}`;
|
||||
|
||||
notebookTypes.push({
|
||||
cssClass: 'icon-notebook',
|
||||
name: `Save to Notebook ${defaultPath}`,
|
||||
onItemClicked: () => {
|
||||
return this.snapshot(NOTEBOOK_DEFAULT);
|
||||
}
|
||||
});
|
||||
notebookTypes.push({
|
||||
cssClass: 'icon-notebook',
|
||||
name: `Save to Notebook ${defaultPath}`,
|
||||
onItemClicked: () => {
|
||||
return this.snapshot(NOTEBOOK_DEFAULT);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
notebookTypes.push({
|
||||
@@ -119,9 +120,8 @@ export default {
|
||||
},
|
||||
setDefaultNotebookStatus() {
|
||||
let defaultNotebookObject = getDefaultNotebook();
|
||||
|
||||
if (defaultNotebookObject && defaultNotebookObject.notebookMeta) {
|
||||
let notebookIdentifier = defaultNotebookObject.notebookMeta.identifier;
|
||||
if (defaultNotebookObject) {
|
||||
let notebookIdentifier = defaultNotebookObject.identifier;
|
||||
|
||||
this.openmct.status.set(notebookIdentifier, 'notebook-default');
|
||||
}
|
||||
|
||||
@@ -87,22 +87,26 @@ export default {
|
||||
|
||||
const selectedPage = this.pages.find(p => p.isSelected);
|
||||
const defaultNotebook = getDefaultNotebook();
|
||||
const defaultpage = defaultNotebook && defaultNotebook.page;
|
||||
const defaultPageId = defaultNotebook && defaultNotebook.defaultPageId;
|
||||
const isPageSelected = selectedPage && selectedPage.id === id;
|
||||
const isPageDefault = defaultpage && defaultpage.id === id;
|
||||
const isPageDefault = defaultPageId === id;
|
||||
const pages = this.pages.filter(s => s.id !== id);
|
||||
let selectedPageId;
|
||||
|
||||
if (isPageSelected && defaultpage) {
|
||||
if (isPageSelected && defaultPageId) {
|
||||
pages.forEach(s => {
|
||||
s.isSelected = false;
|
||||
if (defaultpage && defaultpage.id === s.id) {
|
||||
if (defaultPageId === s.id) {
|
||||
selectedPageId = s.id;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (pages.length && isPageSelected && (!defaultpage || isPageDefault)) {
|
||||
if (isPageDefault) {
|
||||
this.$emit('defaultPageDeleted');
|
||||
}
|
||||
|
||||
if (pages.length && isPageSelected && (!defaultPageId || isPageDefault)) {
|
||||
selectedPageId = pages[0].id;
|
||||
}
|
||||
|
||||
|
||||
@@ -75,21 +75,25 @@ export default {
|
||||
|
||||
const selectedSection = this.sections.find(s => s.id === this.selectedSectionId);
|
||||
const defaultNotebook = getDefaultNotebook();
|
||||
const defaultSection = defaultNotebook && defaultNotebook.section;
|
||||
const defaultSectionId = defaultNotebook && defaultNotebook.defaultSectionId;
|
||||
const isSectionSelected = selectedSection && selectedSection.id === id;
|
||||
const isSectionDefault = defaultSection && defaultSection.id === id;
|
||||
const isSectionDefault = defaultSectionId === id;
|
||||
const sections = this.sections.filter(s => s.id !== id);
|
||||
|
||||
if (isSectionSelected && defaultSection) {
|
||||
if (isSectionSelected && defaultSectionId) {
|
||||
sections.forEach(s => {
|
||||
s.isSelected = false;
|
||||
if (defaultSection && defaultSection.id === s.id) {
|
||||
if (defaultSectionId === s.id) {
|
||||
s.isSelected = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (sections.length && isSectionSelected && (!defaultSection || isSectionDefault)) {
|
||||
if (isSectionDefault) {
|
||||
this.$emit('defaultSectionDeleted');
|
||||
}
|
||||
|
||||
if (sections.length && isSectionSelected && (!defaultSectionId || isSectionDefault)) {
|
||||
sections[0].isSelected = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
:domain-object="domainObject"
|
||||
:sections="sections"
|
||||
:section-title="sectionTitle"
|
||||
@defaultSectionDeleted="defaultSectionDeleted"
|
||||
@updateSection="sectionsChanged"
|
||||
@selectSection="selectSection"
|
||||
/>
|
||||
@@ -50,6 +51,7 @@
|
||||
:sections="sections"
|
||||
:sidebar-covers-entries="sidebarCoversEntries"
|
||||
:page-title="pageTitle"
|
||||
@defaultPageDeleted="defaultPageDeleted"
|
||||
@toggleNav="toggleNav"
|
||||
@updatePage="pagesChanged"
|
||||
@selectPage="selectPage"
|
||||
@@ -218,6 +220,12 @@ export default {
|
||||
sectionTitle
|
||||
};
|
||||
},
|
||||
defaultPageDeleted() {
|
||||
this.$emit('defaultPageDeleted');
|
||||
},
|
||||
defaultSectionDeleted() {
|
||||
this.$emit('defaultSectionDeleted');
|
||||
},
|
||||
toggleNav() {
|
||||
this.$emit('toggleNav');
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { addNotebookEntry, createNewEmbed } from './utils/notebook-entries';
|
||||
import { getDefaultNotebook, getDefaultNotebookLink, setDefaultNotebook } from './utils/notebook-storage';
|
||||
import { getDefaultNotebook, getNotebookSectionAndPage, getDefaultNotebookLink, setDefaultNotebook } from './utils/notebook-storage';
|
||||
import { NOTEBOOK_DEFAULT } from '@/plugins/notebook/notebook-constants';
|
||||
import { createNotebookImageDomainObject, DEFAULT_SIZE } from './utils/notebook-image';
|
||||
|
||||
@@ -58,20 +58,25 @@ export default class Snapshot {
|
||||
*/
|
||||
_saveToDefaultNoteBook(embed) {
|
||||
const notebookStorage = getDefaultNotebook();
|
||||
this.openmct.objects.get(notebookStorage.notebookMeta.identifier)
|
||||
this.openmct.objects.get(notebookStorage.identifier)
|
||||
.then(async (domainObject) => {
|
||||
addNotebookEntry(this.openmct, domainObject, notebookStorage, embed);
|
||||
|
||||
let link = notebookStorage.notebookMeta.link;
|
||||
let link = notebookStorage.link;
|
||||
|
||||
// Backwards compatibility fix (old notebook model without link)
|
||||
if (!link) {
|
||||
link = await getDefaultNotebookLink(this.openmct, domainObject);
|
||||
notebookStorage.notebookMeta.link = link;
|
||||
notebookStorage.link = link;
|
||||
setDefaultNotebook(this.openmct, notebookStorage);
|
||||
}
|
||||
|
||||
const defaultPath = `${domainObject.name} - ${notebookStorage.section.name} - ${notebookStorage.page.name}`;
|
||||
const { section, page } = getNotebookSectionAndPage(domainObject, notebookStorage.defaultSectionId, notebookStorage.defaultPageId);
|
||||
if (!section || !page) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultPath = `${domainObject.name} - ${section.name} - ${page.name}`;
|
||||
const msg = `Saved to Notebook ${defaultPath}`;
|
||||
this._showNotification(msg, link);
|
||||
});
|
||||
|
||||
@@ -9,24 +9,24 @@ const TIME_BOUNDS = {
|
||||
};
|
||||
|
||||
export function addEntryIntoPage(notebookStorage, entries, entry) {
|
||||
const defaultSection = notebookStorage.section;
|
||||
const defaultPage = notebookStorage.page;
|
||||
if (!defaultSection || !defaultPage) {
|
||||
const defaultSectionId = notebookStorage.defaultSectionId;
|
||||
const defaultPageId = notebookStorage.defaultPageId;
|
||||
if (!defaultSectionId || !defaultPageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newEntries = JSON.parse(JSON.stringify(entries));
|
||||
let section = newEntries[defaultSection.id];
|
||||
let section = newEntries[defaultSectionId];
|
||||
if (!section) {
|
||||
newEntries[defaultSection.id] = {};
|
||||
newEntries[defaultSectionId] = {};
|
||||
}
|
||||
|
||||
let page = newEntries[defaultSection.id][defaultPage.id];
|
||||
let page = newEntries[defaultSectionId][defaultPageId];
|
||||
if (!page) {
|
||||
newEntries[defaultSection.id][defaultPage.id] = [];
|
||||
newEntries[defaultSectionId][defaultPageId] = [];
|
||||
}
|
||||
|
||||
newEntries[defaultSection.id][defaultPage.id].push(entry);
|
||||
newEntries[defaultSectionId][defaultPageId].push(entry);
|
||||
|
||||
return newEntries;
|
||||
}
|
||||
|
||||
@@ -23,28 +23,13 @@ import * as NotebookEntries from './notebook-entries';
|
||||
import { createOpenMct, resetApplicationState } from 'utils/testing';
|
||||
|
||||
const notebookStorage = {
|
||||
notebookMeta: {
|
||||
name: 'notebook',
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'test-notebook'
|
||||
}
|
||||
name: 'notebook',
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'test-notebook'
|
||||
},
|
||||
section: {
|
||||
id: '03a79b6a-971c-4e56-9892-ec536332c3f0',
|
||||
isDefault: true,
|
||||
isSelected: true,
|
||||
name: 'section',
|
||||
pages: [],
|
||||
sectionTitle: 'Section'
|
||||
},
|
||||
page: {
|
||||
id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00',
|
||||
isDefault: true,
|
||||
isSelected: true,
|
||||
name: 'page',
|
||||
pageTitle: 'Page'
|
||||
}
|
||||
defaultSectionId: '03a79b6a-971c-4e56-9892-ec536332c3f0',
|
||||
defaultPageId: '8b548fd9-2b8a-4b02-93a9-4138e22eba00'
|
||||
};
|
||||
|
||||
const notebookEntries = {
|
||||
|
||||
@@ -19,18 +19,22 @@ function defaultNotebookObjectChanged(newDomainObject) {
|
||||
clearDefaultNotebook();
|
||||
}
|
||||
|
||||
function observeDefaultNotebookObject(openmct, notebookMeta, domainObject) {
|
||||
function observeDefaultNotebookObject(openmct, notebookStorage, domainObject) {
|
||||
if (currentNotebookObjectIdentifier
|
||||
&& objectUtils.makeKeyString(currentNotebookObjectIdentifier) === objectUtils.makeKeyString(notebookMeta.identifier)) {
|
||||
&& objectUtils.makeKeyString(currentNotebookObjectIdentifier) === objectUtils.makeKeyString(notebookStorage.identifier)) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeListener();
|
||||
|
||||
unlisten = openmct.objects.observe(domainObject, '*', defaultNotebookObjectChanged);
|
||||
}
|
||||
|
||||
function removeListener() {
|
||||
if (unlisten) {
|
||||
unlisten();
|
||||
unlisten = null;
|
||||
}
|
||||
|
||||
unlisten = openmct.objects.observe(domainObject, '*', defaultNotebookObjectChanged);
|
||||
}
|
||||
|
||||
function saveDefaultNotebook(notebookStorage) {
|
||||
@@ -39,6 +43,8 @@ function saveDefaultNotebook(notebookStorage) {
|
||||
|
||||
export function clearDefaultNotebook() {
|
||||
currentNotebookObjectIdentifier = null;
|
||||
removeListener();
|
||||
|
||||
window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, null);
|
||||
}
|
||||
|
||||
@@ -48,6 +54,17 @@ export function getDefaultNotebook() {
|
||||
return JSON.parse(notebookStorage);
|
||||
}
|
||||
|
||||
export function getNotebookSectionAndPage(domainObject, sectionId, pageId) {
|
||||
const configuration = domainObject.configuration;
|
||||
const section = configuration && configuration.sections.find(s => s.id === sectionId);
|
||||
const page = section && section.pages.find(p => p.id === pageId);
|
||||
|
||||
return {
|
||||
section,
|
||||
page
|
||||
};
|
||||
}
|
||||
|
||||
export async function getDefaultNotebookLink(openmct, domainObject = null) {
|
||||
if (!domainObject) {
|
||||
return null;
|
||||
@@ -59,9 +76,9 @@ export async function getDefaultNotebookLink(openmct, domainObject = null) {
|
||||
.reverse()
|
||||
.join('/')
|
||||
);
|
||||
const { page, section } = getDefaultNotebook();
|
||||
const { defaultPageId, defaultSectionId } = getDefaultNotebook();
|
||||
|
||||
return `#/browse/${path}?sectionId=${section.id}&pageId=${page.id}`;
|
||||
return `#/browse/${path}?sectionId=${defaultSectionId}&pageId=${defaultPageId}`;
|
||||
}
|
||||
|
||||
export function setDefaultNotebook(openmct, notebookStorage, domainObject) {
|
||||
@@ -69,15 +86,15 @@ export function setDefaultNotebook(openmct, notebookStorage, domainObject) {
|
||||
saveDefaultNotebook(notebookStorage);
|
||||
}
|
||||
|
||||
export function setDefaultNotebookSection(section) {
|
||||
export function setDefaultNotebookSectionId(sectionId) {
|
||||
const notebookStorage = getDefaultNotebook();
|
||||
notebookStorage.section = section;
|
||||
notebookStorage.defaultSectionId = sectionId;
|
||||
saveDefaultNotebook(notebookStorage);
|
||||
}
|
||||
|
||||
export function setDefaultNotebookPage(page) {
|
||||
export function setDefaultNotebookPageId(pageId) {
|
||||
const notebookStorage = getDefaultNotebook();
|
||||
notebookStorage.page = page;
|
||||
notebookStorage.defaultPageId = pageId;
|
||||
saveDefaultNotebook(notebookStorage);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,37 +23,44 @@
|
||||
import * as NotebookStorage from './notebook-storage';
|
||||
import { createOpenMct, resetApplicationState } from 'utils/testing';
|
||||
|
||||
const notebookSection = {
|
||||
id: 'temp-section',
|
||||
isDefault: false,
|
||||
isSelected: true,
|
||||
name: 'section',
|
||||
pages: [
|
||||
{
|
||||
id: 'temp-page',
|
||||
isDefault: false,
|
||||
isSelected: true,
|
||||
name: 'page',
|
||||
pageTitle: 'Page'
|
||||
}
|
||||
],
|
||||
sectionTitle: 'Section'
|
||||
};
|
||||
|
||||
const domainObject = {
|
||||
name: 'notebook',
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'test-notebook'
|
||||
},
|
||||
configuration: {
|
||||
sections: [
|
||||
notebookSection
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const notebookStorage = {
|
||||
notebookMeta: {
|
||||
name: 'notebook',
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'test-notebook'
|
||||
}
|
||||
name: 'notebook',
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'test-notebook'
|
||||
},
|
||||
section: {
|
||||
id: 'temp-section',
|
||||
isDefault: false,
|
||||
isSelected: true,
|
||||
name: 'section',
|
||||
pages: [],
|
||||
sectionTitle: 'Section'
|
||||
},
|
||||
page: {
|
||||
id: 'temp-page',
|
||||
isDefault: false,
|
||||
isSelected: true,
|
||||
name: 'page',
|
||||
pageTitle: 'Page'
|
||||
}
|
||||
defaultSectionId: 'temp-section',
|
||||
defaultPageId: 'temp-page'
|
||||
};
|
||||
|
||||
let openmct;
|
||||
@@ -104,7 +111,7 @@ describe('Notebook Storage:', () => {
|
||||
expect(JSON.stringify(defaultNotebook)).toBe(JSON.stringify(notebookStorage));
|
||||
});
|
||||
|
||||
it('has correct section on setDefaultNotebookSection', () => {
|
||||
it('has correct section on setDefaultNotebookSectionId', () => {
|
||||
const section = {
|
||||
id: 'new-temp-section',
|
||||
isDefault: true,
|
||||
@@ -115,14 +122,14 @@ describe('Notebook Storage:', () => {
|
||||
};
|
||||
|
||||
NotebookStorage.setDefaultNotebook(openmct, notebookStorage, domainObject);
|
||||
NotebookStorage.setDefaultNotebookSection(section);
|
||||
NotebookStorage.setDefaultNotebookSectionId(section.id);
|
||||
|
||||
const defaultNotebook = NotebookStorage.getDefaultNotebook();
|
||||
const newSection = defaultNotebook.section;
|
||||
expect(JSON.stringify(section)).toBe(JSON.stringify(newSection));
|
||||
const defaultSectionId = defaultNotebook.defaultSectionId;
|
||||
expect(section.id).toBe(defaultSectionId);
|
||||
});
|
||||
|
||||
it('has correct page on setDefaultNotebookPage', () => {
|
||||
it('has correct page on setDefaultNotebookPageId', () => {
|
||||
const page = {
|
||||
id: 'new-temp-page',
|
||||
isDefault: true,
|
||||
@@ -132,10 +139,52 @@ describe('Notebook Storage:', () => {
|
||||
};
|
||||
|
||||
NotebookStorage.setDefaultNotebook(openmct, notebookStorage, domainObject);
|
||||
NotebookStorage.setDefaultNotebookPage(page);
|
||||
NotebookStorage.setDefaultNotebookPageId(page.id);
|
||||
|
||||
const defaultNotebook = NotebookStorage.getDefaultNotebook();
|
||||
const newPage = defaultNotebook.page;
|
||||
expect(JSON.stringify(page)).toBe(JSON.stringify(newPage));
|
||||
const newPageId = defaultNotebook.defaultPageId;
|
||||
expect(page.id).toBe(newPageId);
|
||||
});
|
||||
|
||||
describe('is getNotebookSectionAndPage function searches and returns correct,', () => {
|
||||
let section;
|
||||
let page;
|
||||
|
||||
beforeEach(() => {
|
||||
const sectionId = 'temp-section';
|
||||
const pageId = 'temp-page';
|
||||
|
||||
const sectionAndpage = NotebookStorage.getNotebookSectionAndPage(domainObject, sectionId, pageId);
|
||||
section = sectionAndpage.section;
|
||||
page = sectionAndpage.page;
|
||||
});
|
||||
|
||||
it('id for section from notebook domain object', () => {
|
||||
expect(section.id).toEqual('temp-section');
|
||||
});
|
||||
|
||||
it('name for section from notebook domain object', () => {
|
||||
expect(section.name).toEqual('section');
|
||||
});
|
||||
|
||||
it('sectionTitle for section from notebook domain object', () => {
|
||||
expect(section.sectionTitle).toEqual('Section');
|
||||
});
|
||||
|
||||
it('number of pages for section from notebook domain object', () => {
|
||||
expect(section.pages.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('id for page from notebook domain object', () => {
|
||||
expect(page.id).toEqual('temp-page');
|
||||
});
|
||||
|
||||
it('name for page from notebook domain object', () => {
|
||||
expect(page.name).toEqual('page');
|
||||
});
|
||||
|
||||
it('pageTitle for page from notebook domain object', () => {
|
||||
expect(page.pageTitle).toEqual('Page');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
106
src/plugins/persistence/couch/CouchChangesFeed.js
Normal file
106
src/plugins/persistence/couch/CouchChangesFeed.js
Normal file
@@ -0,0 +1,106 @@
|
||||
(function () {
|
||||
const connections = [];
|
||||
let connected = false;
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
self.onconnect = function (e) {
|
||||
let port = e.ports[0];
|
||||
connections.push(port);
|
||||
|
||||
port.postMessage({
|
||||
type: 'connection',
|
||||
connectionId: connections.length
|
||||
});
|
||||
|
||||
port.onmessage = async function (event) {
|
||||
if (event.data.request === 'close') {
|
||||
connections.splice(event.data.connectionId - 1, 1);
|
||||
if (connections.length <= 0) {
|
||||
// abort any outstanding requests if there's nobody listening to it.
|
||||
controller.abort();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data.request === 'changes') {
|
||||
if (connected === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
connected = true;
|
||||
|
||||
let url = event.data.url;
|
||||
let body = event.data.body;
|
||||
let error = false;
|
||||
// feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection
|
||||
// style=main_only returns only the current winning revision of the document
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": 'application/json'
|
||||
},
|
||||
signal,
|
||||
body
|
||||
});
|
||||
|
||||
let reader;
|
||||
|
||||
if (response.body === undefined) {
|
||||
error = true;
|
||||
} else {
|
||||
reader = response.body.getReader();
|
||||
}
|
||||
|
||||
while (!error) {
|
||||
const {done, value} = await reader.read();
|
||||
//done is true when we lose connection with the provider
|
||||
if (done) {
|
||||
error = true;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
let chunk = new Uint8Array(value.length);
|
||||
chunk.set(value, 0);
|
||||
const decodedChunk = new TextDecoder("utf-8").decode(chunk).split('\n');
|
||||
if (decodedChunk.length && decodedChunk[decodedChunk.length - 1] === '') {
|
||||
decodedChunk.forEach((doc, index) => {
|
||||
try {
|
||||
if (doc) {
|
||||
const objectChanges = JSON.parse(doc);
|
||||
connections.forEach(function (connection) {
|
||||
connection.postMessage({
|
||||
objectChanges
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (decodeError) {
|
||||
//do nothing;
|
||||
console.log(decodeError);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (error) {
|
||||
port.postMessage({
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
port.start();
|
||||
|
||||
};
|
||||
|
||||
self.onerror = function () {
|
||||
//do nothing
|
||||
console.log('Error on feed');
|
||||
};
|
||||
|
||||
}());
|
||||
@@ -40,6 +40,65 @@ export default class CouchObjectProvider {
|
||||
this.batchIds = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
startSharedWorker() {
|
||||
let provider = this;
|
||||
let sharedWorker;
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const sharedWorkerURL = `${this.openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}couchDBChangesFeed.js`;
|
||||
|
||||
sharedWorker = new SharedWorker(sharedWorkerURL);
|
||||
sharedWorker.port.onmessage = provider.onSharedWorkerMessage.bind(this);
|
||||
sharedWorker.port.onmessageerror = provider.onSharedWorkerMessageError.bind(this);
|
||||
sharedWorker.port.start();
|
||||
|
||||
this.openmct.on('destroy', () => {
|
||||
this.changesFeedSharedWorker.port.postMessage({
|
||||
request: 'close',
|
||||
connectionId: this.changesFeedSharedWorkerConnectionId
|
||||
});
|
||||
this.changesFeedSharedWorker.port.close();
|
||||
});
|
||||
|
||||
return sharedWorker;
|
||||
}
|
||||
|
||||
onSharedWorkerMessageError(event) {
|
||||
console.log('Error', event);
|
||||
}
|
||||
|
||||
onSharedWorkerMessage(event) {
|
||||
if (event.data.type === 'connection') {
|
||||
this.changesFeedSharedWorkerConnectionId = event.data.connectionId;
|
||||
} else {
|
||||
const error = event.data.error;
|
||||
if (error && Object.keys(this.observers).length > 0) {
|
||||
this.observeObjectChanges();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let objectChanges = event.data.objectChanges;
|
||||
objectChanges.identifier = {
|
||||
namespace: this.namespace,
|
||||
key: objectChanges.id
|
||||
};
|
||||
let keyString = this.openmct.objects.makeKeyString(objectChanges.identifier);
|
||||
//TODO: Optimize this so that we don't 'get' the object if it's current revision (from this.objectQueue) is the same as the one we already have.
|
||||
let observersForObject = this.observers[keyString];
|
||||
|
||||
if (observersForObject) {
|
||||
observersForObject.forEach(async (observer) => {
|
||||
const updatedObject = await this.get(objectChanges.identifier);
|
||||
observer(updatedObject);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//backwards compatibility, options used to be a url. Now it's an object
|
||||
_normalize(options) {
|
||||
if (typeof options === 'string') {
|
||||
@@ -320,7 +379,7 @@ export default 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) {
|
||||
if (Object.keys(this.observers).length === 0 && this.isObservingObjectChanges()) {
|
||||
this.stopObservingObjectChanges();
|
||||
}
|
||||
}
|
||||
@@ -334,9 +393,8 @@ export default class CouchObjectProvider {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
async observeObjectChanges() {
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
observeObjectChanges() {
|
||||
|
||||
let filter = {selector: {}};
|
||||
|
||||
if (this.openmct.objects.SYNCHRONIZED_OBJECT_TYPES.length > 1) {
|
||||
@@ -354,17 +412,6 @@ export default class CouchObjectProvider {
|
||||
};
|
||||
}
|
||||
|
||||
let error = false;
|
||||
|
||||
if (typeof this.stopObservingObjectChanges === 'function') {
|
||||
this.stopObservingObjectChanges();
|
||||
}
|
||||
|
||||
this.stopObservingObjectChanges = () => {
|
||||
controller.abort();
|
||||
delete this.stopObservingObjectChanges;
|
||||
};
|
||||
|
||||
// feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection
|
||||
// style=main_only returns only the current winning revision of the document
|
||||
let url = `${this.url}/_changes?feed=continuous&style=main_only&heartbeat=${HEARTBEAT}`;
|
||||
@@ -375,6 +422,52 @@ export default class CouchObjectProvider {
|
||||
body = JSON.stringify(filter);
|
||||
}
|
||||
|
||||
if (typeof SharedWorker === 'undefined') {
|
||||
this.fetchChanges(url, body);
|
||||
} else {
|
||||
this.initiateSharedWorkerFetchChanges(url, body);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
initiateSharedWorkerFetchChanges(url, body) {
|
||||
if (!this.changesFeedSharedWorker) {
|
||||
this.changesFeedSharedWorker = this.startSharedWorker();
|
||||
|
||||
if (this.isObservingObjectChanges()) {
|
||||
this.stopObservingObjectChanges();
|
||||
}
|
||||
|
||||
this.stopObservingObjectChanges = () => {
|
||||
delete this.stopObservingObjectChanges;
|
||||
};
|
||||
|
||||
this.changesFeedSharedWorker.port.postMessage({
|
||||
request: 'changes',
|
||||
body,
|
||||
url
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fetchChanges(url, body) {
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
let error = false;
|
||||
|
||||
if (this.isObservingObjectChanges()) {
|
||||
this.stopObservingObjectChanges();
|
||||
}
|
||||
|
||||
this.stopObservingObjectChanges = () => {
|
||||
controller.abort();
|
||||
delete this.stopObservingObjectChanges;
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
signal,
|
||||
|
||||
@@ -25,16 +25,16 @@
|
||||
:class="[plotLegendExpandedStateClass, plotLegendPositionClass]"
|
||||
>
|
||||
<plot-legend :cursor-locked="!!lockHighlightPoint"
|
||||
:series="config.series.models"
|
||||
:series="seriesModels"
|
||||
:highlights="highlights"
|
||||
:legend="config.legend"
|
||||
:legend="legend"
|
||||
@legendHoverChanged="legendHoverChanged"
|
||||
/>
|
||||
<div class="plot-wrapper-axis-and-display-area flex-elem grows">
|
||||
<y-axis v-if="config.series.models.length > 0"
|
||||
<y-axis v-if="seriesModels.length > 0"
|
||||
:tick-width="tickWidth"
|
||||
:single-series="config.series.models.length === 1"
|
||||
:series-model="config.series.models[0]"
|
||||
:single-series="seriesModels.length === 1"
|
||||
:series-model="seriesModels[0]"
|
||||
@yKeyChanged="setYAxisKey"
|
||||
@tickWidthChanged="onTickWidthChange"
|
||||
/>
|
||||
@@ -141,8 +141,8 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<x-axis v-if="config.series.models.length > 0 && !options.compact"
|
||||
:series-model="config.series.models[0]"
|
||||
<x-axis v-if="seriesModels.length > 0 && !options.compact"
|
||||
:series-model="seriesModels[0]"
|
||||
/>
|
||||
|
||||
</div>
|
||||
@@ -213,7 +213,8 @@ export default {
|
||||
plotHistory: [],
|
||||
selectedXKeyOption: {},
|
||||
xKeyOptions: [],
|
||||
config: {},
|
||||
seriesModels: [],
|
||||
legend: {},
|
||||
pending: 0,
|
||||
isRealTime: this.openmct.time.clock() !== undefined,
|
||||
loaded: false,
|
||||
@@ -239,18 +240,13 @@ export default {
|
||||
watch: {
|
||||
plotTickWidth(newTickWidth) {
|
||||
this.onTickWidthChange(newTickWidth, true);
|
||||
},
|
||||
gridLines(newGridLines) {
|
||||
this.setGridLinesVisibility(newGridLines);
|
||||
},
|
||||
cursorGuide(newCursorGuide) {
|
||||
this.setCursorGuideVisibility(newCursorGuide);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
eventHelpers.extend(this);
|
||||
|
||||
this.config = this.getConfig();
|
||||
this.legend = this.config.legend;
|
||||
|
||||
this.listenTo(this.config.series, 'add', this.addSeries, this);
|
||||
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
|
||||
@@ -290,14 +286,18 @@ export default {
|
||||
config = new PlotConfigurationModel({
|
||||
id: configId,
|
||||
domainObject: this.domainObject,
|
||||
openmct: this.openmct
|
||||
openmct: this.openmct,
|
||||
callback: (data) => {
|
||||
this.data = data;
|
||||
}
|
||||
});
|
||||
configStore.add(configId, config);
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
addSeries(series) {
|
||||
addSeries(series, index) {
|
||||
this.$set(this.seriesModels, index, series);
|
||||
this.listenTo(series, 'change:xKey', (xKey) => {
|
||||
this.setDisplayRange(series, xKey);
|
||||
}, this);
|
||||
@@ -377,11 +377,8 @@ export default {
|
||||
},
|
||||
|
||||
stopLoading() {
|
||||
//TODO: Is Vue.$nextTick ok to replace $scope.$evalAsync?
|
||||
this.$nextTick().then(() => {
|
||||
this.pending -= 1;
|
||||
this.updateLoading();
|
||||
});
|
||||
this.pending -= 1;
|
||||
this.updateLoading();
|
||||
},
|
||||
|
||||
updateLoading() {
|
||||
@@ -507,7 +504,7 @@ export default {
|
||||
},
|
||||
|
||||
initialize() {
|
||||
_.debounce(this.handleWindowResize, 400);
|
||||
this.handleWindowResize = _.debounce(this.handleWindowResize, 500);
|
||||
this.plotContainerResizeObserver = new ResizeObserver(this.handleWindowResize);
|
||||
this.plotContainerResizeObserver.observe(this.$parent.$refs.plotWrapper);
|
||||
|
||||
@@ -623,7 +620,7 @@ export default {
|
||||
this.config.series.models.forEach(series => delete series.closest);
|
||||
} else {
|
||||
this.highlights = this.config.series.models
|
||||
.filter(series => series.data.length > 0)
|
||||
.filter(series => series.getSeriesData().length > 0)
|
||||
.map(series => {
|
||||
series.closest = series.nearestPoint(point);
|
||||
|
||||
@@ -927,16 +924,8 @@ export default {
|
||||
this.userViewportChangeEnd();
|
||||
},
|
||||
|
||||
setCursorGuideVisibility(cursorGuide) {
|
||||
this.cursorGuide = cursorGuide === true;
|
||||
},
|
||||
|
||||
setGridLinesVisibility(gridLines) {
|
||||
this.gridLines = gridLines === true;
|
||||
},
|
||||
|
||||
setYAxisKey(yKey) {
|
||||
this.config.series.models[0].emit('change:yKey', yKey);
|
||||
this.config.series.models[0].set('yKey', yKey);
|
||||
},
|
||||
|
||||
pause() {
|
||||
|
||||
@@ -106,8 +106,9 @@ export default {
|
||||
},
|
||||
toggleXKeyOption() {
|
||||
const selectedXKey = this.selectedXKeyOptionKey;
|
||||
const dataForSelectedXKey = this.seriesModel.data
|
||||
? this.seriesModel.data[0][selectedXKey]
|
||||
const seriesData = this.seriesModel.getSeriesData();
|
||||
const dataForSelectedXKey = seriesData
|
||||
? seriesData[0][selectedXKey]
|
||||
: undefined;
|
||||
|
||||
if (dataForSelectedXKey !== undefined) {
|
||||
|
||||
@@ -36,7 +36,7 @@ export default class MCTChartAlarmPointSet {
|
||||
this.listenTo(series, 'reset', this.reset, this);
|
||||
this.listenTo(series, 'destroy', this.destroy, this);
|
||||
|
||||
series.data.forEach(function (point, index) {
|
||||
this.series.getSeriesData().forEach(function (point, index) {
|
||||
this.append(point, index, series);
|
||||
}, this);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export default class MCTChartSeriesElement {
|
||||
this.listenTo(series, 'remove', this.remove, this);
|
||||
this.listenTo(series, 'reset', this.reset, this);
|
||||
this.listenTo(series, 'destroy', this.destroy, this);
|
||||
series.data.forEach(function (point, index) {
|
||||
this.series.getSeriesData().forEach(function (point, index) {
|
||||
this.append(point, index, series);
|
||||
}, this);
|
||||
}
|
||||
@@ -133,7 +133,7 @@ export default class MCTChartSeriesElement {
|
||||
this.buffer = new Float32Array(20000);
|
||||
this.count = 0;
|
||||
if (this.offset.x) {
|
||||
this.series.data.forEach(function (point, index) {
|
||||
this.series.getSeriesData().forEach(function (point, index) {
|
||||
this.append(point, index, this.series);
|
||||
}, this);
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ export default class PlotConfigurationModel extends Model {
|
||||
updateDomainObject(domainObject) {
|
||||
this.set('domainObject', domainObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all objects and remove all listeners.
|
||||
*/
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import _ from 'lodash';
|
||||
import Model from "./Model";
|
||||
import { MARKER_SHAPES } from '../draw/MarkerShapes';
|
||||
import configStore from "../configuration/configStore";
|
||||
|
||||
/**
|
||||
* Plot series handle interpreting telemetry metadata for a single telemetry
|
||||
@@ -62,7 +63,6 @@ import { MARKER_SHAPES } from '../draw/MarkerShapes';
|
||||
export default class PlotSeries extends Model {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.data = [];
|
||||
|
||||
this.listenTo(this, 'change:xKey', this.onXKeyChange, this);
|
||||
this.listenTo(this, 'change:yKey', this.onYKeyChange, this);
|
||||
@@ -115,6 +115,8 @@ export default class PlotSeries extends Model {
|
||||
this.openmct = options.openmct;
|
||||
this.domainObject = options.domainObject;
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.dataStoreId = `data-${options.collection.plot.id}-${this.keyString}`;
|
||||
this.updateSeriesData([]);
|
||||
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(options.domainObject);
|
||||
this.limitDefinition = this.openmct.telemetry.limitDefinition(options.domainObject);
|
||||
this.limits = [];
|
||||
@@ -182,7 +184,8 @@ export default class PlotSeries extends Model {
|
||||
.telemetry
|
||||
.request(this.domainObject, options)
|
||||
.then(function (points) {
|
||||
const newPoints = _(this.data)
|
||||
const data = this.getSeriesData();
|
||||
const newPoints = _(data)
|
||||
.concat(points)
|
||||
.sortBy(this.getXVal)
|
||||
.uniq(true, point => [this.getXVal(point), this.getYVal(point)].join())
|
||||
@@ -236,7 +239,7 @@ export default class PlotSeries extends Model {
|
||||
*/
|
||||
resetStats() {
|
||||
this.unset('stats');
|
||||
this.data.forEach(this.updateStats, this);
|
||||
this.getSeriesData().forEach(this.updateStats, this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,7 +247,7 @@ export default class PlotSeries extends Model {
|
||||
* data to series after reset.
|
||||
*/
|
||||
reset(newData) {
|
||||
this.data = [];
|
||||
this.updateSeriesData([]);
|
||||
this.resetStats();
|
||||
this.emit('reset');
|
||||
if (newData) {
|
||||
@@ -258,8 +261,9 @@ export default class PlotSeries extends Model {
|
||||
*/
|
||||
nearestPoint(xValue) {
|
||||
const insertIndex = this.sortedIndex(xValue);
|
||||
const lowPoint = this.data[insertIndex - 1];
|
||||
const highPoint = this.data[insertIndex];
|
||||
const data = this.getSeriesData();
|
||||
const lowPoint = data[insertIndex - 1];
|
||||
const highPoint = data[insertIndex];
|
||||
const indexVal = this.getXVal(xValue);
|
||||
const lowDistance = lowPoint
|
||||
? indexVal - this.getXVal(lowPoint)
|
||||
@@ -292,7 +296,7 @@ export default class PlotSeries extends Model {
|
||||
* @private
|
||||
*/
|
||||
sortedIndex(point) {
|
||||
return _.sortedIndexBy(this.data, point, this.getXVal);
|
||||
return _.sortedIndexBy(this.getSeriesData(), point, this.getXVal);
|
||||
}
|
||||
/**
|
||||
* Update min/max stats for the series.
|
||||
@@ -346,9 +350,10 @@ export default class PlotSeries extends Model {
|
||||
* a point to the end without dupe checking.
|
||||
*/
|
||||
add(point, appendOnly) {
|
||||
let insertIndex = this.data.length;
|
||||
let data = this.getSeriesData();
|
||||
let insertIndex = data.length;
|
||||
const currentYVal = this.getYVal(point);
|
||||
const lastYVal = this.getYVal(this.data[insertIndex - 1]);
|
||||
const lastYVal = this.getYVal(data[insertIndex - 1]);
|
||||
|
||||
if (this.isValueInvalid(currentYVal) && this.isValueInvalid(lastYVal)) {
|
||||
console.warn('[Plot] Invalid Y Values detected');
|
||||
@@ -358,18 +363,19 @@ export default class PlotSeries extends Model {
|
||||
|
||||
if (!appendOnly) {
|
||||
insertIndex = this.sortedIndex(point);
|
||||
if (this.getXVal(this.data[insertIndex]) === this.getXVal(point)) {
|
||||
if (this.getXVal(data[insertIndex]) === this.getXVal(point)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.getXVal(this.data[insertIndex - 1]) === this.getXVal(point)) {
|
||||
if (this.getXVal(data[insertIndex - 1]) === this.getXVal(point)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.updateStats(point);
|
||||
point.mctLimitState = this.evaluate(point);
|
||||
this.data.splice(insertIndex, 0, point);
|
||||
data.splice(insertIndex, 0, point);
|
||||
this.updateSeriesData(data);
|
||||
this.emit('add', point, insertIndex, this);
|
||||
}
|
||||
|
||||
@@ -386,8 +392,10 @@ export default class PlotSeries extends Model {
|
||||
* @private
|
||||
*/
|
||||
remove(point) {
|
||||
const index = this.data.indexOf(point);
|
||||
this.data.splice(index, 1);
|
||||
let data = this.getSeriesData();
|
||||
const index = data.indexOf(point);
|
||||
data.splice(index, 1);
|
||||
this.updateSeriesData(data);
|
||||
this.emit('remove', point, index, this);
|
||||
}
|
||||
/**
|
||||
@@ -403,14 +411,16 @@ export default class PlotSeries extends Model {
|
||||
purgeRecordsOutsideRange(range) {
|
||||
const startIndex = this.sortedIndex(range.min);
|
||||
const endIndex = this.sortedIndex(range.max) + 1;
|
||||
const pointsToRemove = startIndex + (this.data.length - endIndex + 1);
|
||||
let data = this.getSeriesData();
|
||||
const pointsToRemove = startIndex + (data.length - endIndex + 1);
|
||||
if (pointsToRemove > 0) {
|
||||
if (pointsToRemove < 1000) {
|
||||
this.data.slice(0, startIndex).forEach(this.remove, this);
|
||||
this.data.slice(endIndex, this.data.length).forEach(this.remove, this);
|
||||
data.slice(0, startIndex).forEach(this.remove, this);
|
||||
data.slice(endIndex, data.length).forEach(this.remove, this);
|
||||
this.updateSeriesData(data);
|
||||
this.resetStats();
|
||||
} else {
|
||||
const newData = this.data.slice(startIndex, endIndex);
|
||||
const newData = this.getSeriesData().slice(startIndex, endIndex);
|
||||
this.reset(newData);
|
||||
}
|
||||
}
|
||||
@@ -441,12 +451,13 @@ export default class PlotSeries extends Model {
|
||||
}
|
||||
}
|
||||
getDisplayRange(xKey) {
|
||||
const unsortedData = this.data;
|
||||
this.data = [];
|
||||
const unsortedData = this.getSeriesData();
|
||||
this.updateSeriesData([]);
|
||||
unsortedData.forEach(point => this.add(point, false));
|
||||
|
||||
const minValue = this.getXVal(this.data[0]);
|
||||
const maxValue = this.getXVal(this.data[this.data.length - 1]);
|
||||
let data = this.getSeriesData();
|
||||
const minValue = this.getXVal(data[0]);
|
||||
const maxValue = this.getXVal(data[data.length - 1]);
|
||||
|
||||
return {
|
||||
min: minValue,
|
||||
@@ -470,4 +481,18 @@ export default class PlotSeries extends Model {
|
||||
|
||||
return this.get('name') + (unit ? ' ' + unit : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the series data with the given value.
|
||||
*/
|
||||
updateSeriesData(data) {
|
||||
configStore.add(this.dataStoreId, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the series data with the given value.
|
||||
*/
|
||||
getSeriesData() {
|
||||
return configStore.get(this.dataStoreId) || [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,10 @@ function ConfigStore() {
|
||||
|
||||
ConfigStore.prototype.deleteStore = function (id) {
|
||||
if (this.store[id]) {
|
||||
this.store[id].destroy();
|
||||
if (this.store[id].destroy) {
|
||||
this.store[id].destroy();
|
||||
}
|
||||
|
||||
delete this.store[id];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -176,7 +176,9 @@ DrawWebGL.prototype.doDraw = function (drawType, buf, color, points, shape) {
|
||||
this.gl.vertexAttribPointer(this.aVertexPosition, 2, this.gl.FLOAT, false, 0, 0);
|
||||
this.gl.uniform4fv(this.uColor, color);
|
||||
this.gl.uniform1i(this.uMarkerShape, shapeCode);
|
||||
this.gl.drawArrays(drawType, 0, points);
|
||||
if (points !== 0) {
|
||||
this.gl.drawArrays(drawType, 0, points);
|
||||
}
|
||||
};
|
||||
|
||||
DrawWebGL.prototype.clear = function () {
|
||||
|
||||
@@ -715,14 +715,15 @@ describe("the plugin", function () {
|
||||
});
|
||||
|
||||
it("Adds a new point to the plot", (done) => {
|
||||
let originalLength = config.series.models[0].data.length;
|
||||
let originalLength = config.series.models[0].getSeriesData().length;
|
||||
config.series.models[0].add({
|
||||
utc: 2,
|
||||
'some-key': 1,
|
||||
'some-other-key': 2
|
||||
});
|
||||
Vue.nextTick(() => {
|
||||
expect(config.series.models[0].data.length).toEqual(originalLength + 1);
|
||||
const seriesData = config.series.models[0].getSeriesData();
|
||||
expect(seriesData.length).toEqual(originalLength + 1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,7 +67,8 @@ define([
|
||||
'./interceptors/plugin',
|
||||
'./performanceIndicator/plugin',
|
||||
'./CouchDBSearchFolder/plugin',
|
||||
'./timeline/plugin'
|
||||
'./timeline/plugin',
|
||||
'./hyperlink/plugin'
|
||||
], function (
|
||||
_,
|
||||
UTCTimeSystem,
|
||||
@@ -115,7 +116,8 @@ define([
|
||||
ObjectInterceptors,
|
||||
PerformanceIndicator,
|
||||
CouchDBSearchFolder,
|
||||
Timeline
|
||||
Timeline,
|
||||
Hyperlink
|
||||
) {
|
||||
const bundleMap = {
|
||||
LocalStorage: 'platform/persistence/local',
|
||||
@@ -218,6 +220,7 @@ define([
|
||||
plugins.PerformanceIndicator = PerformanceIndicator.default;
|
||||
plugins.CouchDBSearchFolder = CouchDBSearchFolder.default;
|
||||
plugins.Timeline = Timeline.default;
|
||||
plugins.Hyperlink = Hyperlink.default;
|
||||
|
||||
return plugins;
|
||||
});
|
||||
|
||||
@@ -137,7 +137,10 @@ describe("the RemoteClock plugin", () => {
|
||||
|
||||
it('will request the latest datum for the object it received and process the datum returned', () => {
|
||||
expect(openmct.telemetry.request).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, REQ_OPTIONS);
|
||||
expect(boundsCallback).toHaveBeenCalledWith({ start: TIME_VALUE + OFFSET_START, end: TIME_VALUE + OFFSET_END }, true);
|
||||
expect(boundsCallback).toHaveBeenCalledWith({
|
||||
start: TIME_VALUE + OFFSET_START,
|
||||
end: TIME_VALUE + OFFSET_END
|
||||
}, true);
|
||||
});
|
||||
|
||||
it('will set up subscriptions correctly', () => {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
import RemoteClock from "./RemoteClock";
|
||||
/**
|
||||
* Install a clock that uses a configurable telemetry endpoint.
|
||||
* Install a clock that uses a configurable telemetry endpoint.
|
||||
*/
|
||||
|
||||
export default function (identifier) {
|
||||
|
||||
@@ -23,20 +23,18 @@
|
||||
define([
|
||||
'EventEmitter',
|
||||
'lodash',
|
||||
'./collections/BoundedTableRowCollection',
|
||||
'./collections/FilteredTableRowCollection',
|
||||
'./TelemetryTableNameColumn',
|
||||
'./collections/TableRowCollection',
|
||||
'./TelemetryTableRow',
|
||||
'./TelemetryTableNameColumn',
|
||||
'./TelemetryTableColumn',
|
||||
'./TelemetryTableUnitColumn',
|
||||
'./TelemetryTableConfiguration'
|
||||
], function (
|
||||
EventEmitter,
|
||||
_,
|
||||
BoundedTableRowCollection,
|
||||
FilteredTableRowCollection,
|
||||
TelemetryTableNameColumn,
|
||||
TableRowCollection,
|
||||
TelemetryTableRow,
|
||||
TelemetryTableNameColumn,
|
||||
TelemetryTableColumn,
|
||||
TelemetryTableUnitColumn,
|
||||
TelemetryTableConfiguration
|
||||
@@ -48,20 +46,23 @@ define([
|
||||
this.domainObject = domainObject;
|
||||
this.openmct = openmct;
|
||||
this.rowCount = 100;
|
||||
this.subscriptions = {};
|
||||
this.tableComposition = undefined;
|
||||
this.telemetryObjects = [];
|
||||
this.datumCache = [];
|
||||
this.outstandingRequests = 0;
|
||||
this.configuration = new TelemetryTableConfiguration(domainObject, openmct);
|
||||
this.paused = false;
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
|
||||
this.telemetryObjects = {};
|
||||
this.telemetryCollections = {};
|
||||
this.delayedActions = [];
|
||||
this.outstandingRequests = 0;
|
||||
|
||||
this.addTelemetryObject = this.addTelemetryObject.bind(this);
|
||||
this.removeTelemetryObject = this.removeTelemetryObject.bind(this);
|
||||
this.removeTelemetryCollection = this.removeTelemetryCollection.bind(this);
|
||||
this.resetRowsFromAllData = this.resetRowsFromAllData.bind(this);
|
||||
this.isTelemetryObject = this.isTelemetryObject.bind(this);
|
||||
this.refreshData = this.refreshData.bind(this);
|
||||
this.requestDataFor = this.requestDataFor.bind(this);
|
||||
this.updateFilters = this.updateFilters.bind(this);
|
||||
this.buildOptionsFromConfiguration = this.buildOptionsFromConfiguration.bind(this);
|
||||
|
||||
@@ -102,8 +103,7 @@ define([
|
||||
}
|
||||
|
||||
createTableRowCollections() {
|
||||
this.boundedRows = new BoundedTableRowCollection(this.openmct);
|
||||
this.filteredRows = new FilteredTableRowCollection(this.boundedRows);
|
||||
this.tableRows = new TableRowCollection();
|
||||
|
||||
//Fetch any persisted default sort
|
||||
let sortOptions = this.configuration.getConfiguration().sortOptions;
|
||||
@@ -113,11 +113,14 @@ define([
|
||||
key: this.openmct.time.timeSystem().key,
|
||||
direction: 'asc'
|
||||
};
|
||||
this.filteredRows.sortBy(sortOptions);
|
||||
|
||||
this.tableRows.sortBy(sortOptions);
|
||||
this.tableRows.on('resetRowsFromAllData', this.resetRowsFromAllData);
|
||||
}
|
||||
|
||||
loadComposition() {
|
||||
this.tableComposition = this.openmct.composition.get(this.domainObject);
|
||||
|
||||
if (this.tableComposition !== undefined) {
|
||||
this.tableComposition.load().then((composition) => {
|
||||
|
||||
@@ -132,66 +135,64 @@ define([
|
||||
|
||||
addTelemetryObject(telemetryObject) {
|
||||
this.addColumnsForObject(telemetryObject, true);
|
||||
this.requestDataFor(telemetryObject);
|
||||
this.subscribeTo(telemetryObject);
|
||||
this.telemetryObjects.push(telemetryObject);
|
||||
|
||||
const keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
let requestOptions = this.buildOptionsFromConfiguration(telemetryObject);
|
||||
let columnMap = this.getColumnMapForObject(keyString);
|
||||
let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject);
|
||||
|
||||
this.incrementOutstandingRequests();
|
||||
|
||||
const telemetryProcessor = this.getTelemetryProcessor(keyString, columnMap, limitEvaluator);
|
||||
const telemetryRemover = this.getTelemetryRemover();
|
||||
|
||||
this.removeTelemetryCollection(keyString);
|
||||
|
||||
this.telemetryCollections[keyString] = this.openmct.telemetry
|
||||
.requestTelemetryCollection(telemetryObject, requestOptions);
|
||||
|
||||
this.telemetryCollections[keyString].on('remove', telemetryRemover);
|
||||
this.telemetryCollections[keyString].on('add', telemetryProcessor);
|
||||
this.telemetryCollections[keyString].load();
|
||||
|
||||
this.decrementOutstandingRequests();
|
||||
|
||||
this.telemetryObjects[keyString] = {
|
||||
telemetryObject,
|
||||
keyString,
|
||||
requestOptions,
|
||||
columnMap,
|
||||
limitEvaluator
|
||||
};
|
||||
|
||||
this.emit('object-added', telemetryObject);
|
||||
}
|
||||
|
||||
updateFilters(updatedFilters) {
|
||||
let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters));
|
||||
getTelemetryProcessor(keyString, columnMap, limitEvaluator) {
|
||||
return (telemetry) => {
|
||||
//Check that telemetry object has not been removed since telemetry was requested.
|
||||
if (!this.telemetryObjects[keyString]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) {
|
||||
this.filters = deepCopiedFilters;
|
||||
this.clearAndResubscribe();
|
||||
} else {
|
||||
this.filters = deepCopiedFilters;
|
||||
}
|
||||
let telemetryRows = telemetry.map(datum => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator));
|
||||
|
||||
if (this.paused) {
|
||||
this.delayedActions.push(this.tableRows.addRows.bind(this, telemetryRows, 'add'));
|
||||
} else {
|
||||
this.tableRows.addRows(telemetryRows, 'add');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
clearAndResubscribe() {
|
||||
this.filteredRows.clear();
|
||||
this.boundedRows.clear();
|
||||
Object.keys(this.subscriptions).forEach(this.unsubscribe, this);
|
||||
|
||||
this.telemetryObjects.forEach(this.requestDataFor.bind(this));
|
||||
this.telemetryObjects.forEach(this.subscribeTo.bind(this));
|
||||
}
|
||||
|
||||
removeTelemetryObject(objectIdentifier) {
|
||||
this.configuration.removeColumnsForObject(objectIdentifier, true);
|
||||
let keyString = this.openmct.objects.makeKeyString(objectIdentifier);
|
||||
this.boundedRows.removeAllRowsForObject(keyString);
|
||||
this.unsubscribe(keyString);
|
||||
this.telemetryObjects = this.telemetryObjects.filter((object) => !_.eq(objectIdentifier, object.identifier));
|
||||
|
||||
this.emit('object-removed', objectIdentifier);
|
||||
}
|
||||
|
||||
requestDataFor(telemetryObject) {
|
||||
this.incrementOutstandingRequests();
|
||||
let requestOptions = this.buildOptionsFromConfiguration(telemetryObject);
|
||||
|
||||
return this.openmct.telemetry.request(telemetryObject, requestOptions)
|
||||
.then(telemetryData => {
|
||||
//Check that telemetry object has not been removed since telemetry was requested.
|
||||
if (!this.telemetryObjects.includes(telemetryObject)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
let columnMap = this.getColumnMapForObject(keyString);
|
||||
let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject);
|
||||
this.processHistoricalData(telemetryData, columnMap, keyString, limitEvaluator);
|
||||
}).finally(() => {
|
||||
this.decrementOutstandingRequests();
|
||||
});
|
||||
}
|
||||
|
||||
processHistoricalData(telemetryData, columnMap, keyString, limitEvaluator) {
|
||||
let telemetryRows = telemetryData.map(datum => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator));
|
||||
this.boundedRows.add(telemetryRows);
|
||||
getTelemetryRemover() {
|
||||
return (telemetry) => {
|
||||
if (this.paused) {
|
||||
this.delayedActions.push(this.tableRows.removeRowsByData.bind(this, telemetry));
|
||||
} else {
|
||||
this.tableRows.removeRowsByData(telemetry);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -216,35 +217,72 @@ define([
|
||||
}
|
||||
}
|
||||
|
||||
// will pull all necessary information for all existing bounded telemetry
|
||||
// and pass to table row collection to reset without making any new requests
|
||||
// triggered by filtering
|
||||
resetRowsFromAllData() {
|
||||
let allRows = [];
|
||||
|
||||
Object.keys(this.telemetryCollections).forEach(keyString => {
|
||||
let { columnMap, limitEvaluator } = this.telemetryObjects[keyString];
|
||||
|
||||
this.telemetryCollections[keyString].getAll().forEach(datum => {
|
||||
allRows.push(new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator));
|
||||
});
|
||||
});
|
||||
|
||||
this.tableRows.addRows(allRows, 'filter');
|
||||
}
|
||||
|
||||
updateFilters(updatedFilters) {
|
||||
let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters));
|
||||
|
||||
if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) {
|
||||
this.filters = deepCopiedFilters;
|
||||
this.tableRows.clear();
|
||||
this.clearAndResubscribe();
|
||||
} else {
|
||||
this.filters = deepCopiedFilters;
|
||||
}
|
||||
}
|
||||
|
||||
clearAndResubscribe() {
|
||||
let objectKeys = Object.keys(this.telemetryObjects);
|
||||
|
||||
this.tableRows.clear();
|
||||
objectKeys.forEach((keyString) => {
|
||||
this.addTelemetryObject(this.telemetryObjects[keyString].telemetryObject);
|
||||
});
|
||||
}
|
||||
|
||||
removeTelemetryObject(objectIdentifier) {
|
||||
const keyString = this.openmct.objects.makeKeyString(objectIdentifier);
|
||||
|
||||
this.configuration.removeColumnsForObject(objectIdentifier, true);
|
||||
this.tableRows.removeRowsByObject(keyString);
|
||||
|
||||
this.removeTelemetryCollection(keyString);
|
||||
delete this.telemetryObjects[keyString];
|
||||
|
||||
this.emit('object-removed', objectIdentifier);
|
||||
}
|
||||
|
||||
refreshData(bounds, isTick) {
|
||||
if (!isTick && this.outstandingRequests === 0) {
|
||||
this.filteredRows.clear();
|
||||
this.boundedRows.clear();
|
||||
this.boundedRows.sortByTimeSystem(this.openmct.time.timeSystem());
|
||||
this.telemetryObjects.forEach(this.requestDataFor);
|
||||
if (!isTick && this.tableRows.outstandingRequests === 0) {
|
||||
this.tableRows.clear();
|
||||
this.tableRows.sortBy({
|
||||
key: this.openmct.time.timeSystem().key,
|
||||
direction: 'asc'
|
||||
});
|
||||
this.tableRows.resubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
clearData() {
|
||||
this.filteredRows.clear();
|
||||
this.boundedRows.clear();
|
||||
this.tableRows.clear();
|
||||
this.emit('refresh');
|
||||
}
|
||||
|
||||
getColumnMapForObject(objectKeyString) {
|
||||
let columns = this.configuration.getColumns();
|
||||
|
||||
if (columns[objectKeyString]) {
|
||||
return columns[objectKeyString].reduce((map, column) => {
|
||||
map[column.getKey()] = column;
|
||||
|
||||
return map;
|
||||
}, {});
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
addColumnsForObject(telemetryObject) {
|
||||
let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values();
|
||||
|
||||
@@ -264,54 +302,18 @@ define([
|
||||
});
|
||||
}
|
||||
|
||||
createColumn(metadatum) {
|
||||
return new TelemetryTableColumn(this.openmct, metadatum);
|
||||
}
|
||||
getColumnMapForObject(objectKeyString) {
|
||||
let columns = this.configuration.getColumns();
|
||||
|
||||
createUnitColumn(metadatum) {
|
||||
return new TelemetryTableUnitColumn(this.openmct, metadatum);
|
||||
}
|
||||
if (columns[objectKeyString]) {
|
||||
return columns[objectKeyString].reduce((map, column) => {
|
||||
map[column.getKey()] = column;
|
||||
|
||||
subscribeTo(telemetryObject) {
|
||||
let subscribeOptions = this.buildOptionsFromConfiguration(telemetryObject);
|
||||
let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
let columnMap = this.getColumnMapForObject(keyString);
|
||||
let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject);
|
||||
return map;
|
||||
}, {});
|
||||
}
|
||||
|
||||
this.subscriptions[keyString] = this.openmct.telemetry.subscribe(telemetryObject, (datum) => {
|
||||
//Check that telemetry object has not been removed since telemetry was requested.
|
||||
if (!this.telemetryObjects.includes(telemetryObject)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.paused) {
|
||||
let realtimeDatum = {
|
||||
datum,
|
||||
columnMap,
|
||||
keyString,
|
||||
limitEvaluator
|
||||
};
|
||||
|
||||
this.datumCache.push(realtimeDatum);
|
||||
} else {
|
||||
this.processRealtimeDatum(datum, columnMap, keyString, limitEvaluator);
|
||||
}
|
||||
}, subscribeOptions);
|
||||
}
|
||||
|
||||
processDatumCache() {
|
||||
this.datumCache.forEach(cachedDatum => {
|
||||
this.processRealtimeDatum(cachedDatum.datum, cachedDatum.columnMap, cachedDatum.keyString, cachedDatum.limitEvaluator);
|
||||
});
|
||||
this.datumCache = [];
|
||||
}
|
||||
|
||||
processRealtimeDatum(datum, columnMap, keyString, limitEvaluator) {
|
||||
this.boundedRows.add(new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator));
|
||||
}
|
||||
|
||||
isTelemetryObject(domainObject) {
|
||||
return Object.prototype.hasOwnProperty.call(domainObject, 'telemetry');
|
||||
return {};
|
||||
}
|
||||
|
||||
buildOptionsFromConfiguration(telemetryObject) {
|
||||
@@ -323,13 +325,20 @@ define([
|
||||
return {filters} || {};
|
||||
}
|
||||
|
||||
unsubscribe(keyString) {
|
||||
this.subscriptions[keyString]();
|
||||
delete this.subscriptions[keyString];
|
||||
createColumn(metadatum) {
|
||||
return new TelemetryTableColumn(this.openmct, metadatum);
|
||||
}
|
||||
|
||||
createUnitColumn(metadatum) {
|
||||
return new TelemetryTableUnitColumn(this.openmct, metadatum);
|
||||
}
|
||||
|
||||
isTelemetryObject(domainObject) {
|
||||
return Object.prototype.hasOwnProperty.call(domainObject, 'telemetry');
|
||||
}
|
||||
|
||||
sortBy(sortOptions) {
|
||||
this.filteredRows.sortBy(sortOptions);
|
||||
this.tableRows.sortBy(sortOptions);
|
||||
|
||||
if (this.openmct.editor.isEditing()) {
|
||||
let configuration = this.configuration.getConfiguration();
|
||||
@@ -338,21 +347,36 @@ define([
|
||||
}
|
||||
}
|
||||
|
||||
runDelayedActions() {
|
||||
this.delayedActions.forEach(action => action());
|
||||
this.delayedActions = [];
|
||||
}
|
||||
|
||||
removeTelemetryCollection(keyString) {
|
||||
if (this.telemetryCollections[keyString]) {
|
||||
this.telemetryCollections[keyString].destroy();
|
||||
this.telemetryCollections[keyString] = undefined;
|
||||
delete this.telemetryCollections[keyString];
|
||||
}
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.paused = true;
|
||||
this.boundedRows.unsubscribeFromBounds();
|
||||
}
|
||||
|
||||
unpause() {
|
||||
this.paused = false;
|
||||
this.processDatumCache();
|
||||
this.boundedRows.subscribeToBounds();
|
||||
this.runDelayedActions();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.boundedRows.destroy();
|
||||
this.filteredRows.destroy();
|
||||
Object.keys(this.subscriptions).forEach(this.unsubscribe, this);
|
||||
this.tableRows.destroy();
|
||||
|
||||
this.tableRows.off('resetRowsFromAllData', this.resetRowsFromAllData);
|
||||
|
||||
let keystrings = Object.keys(this.telemetryCollections);
|
||||
keystrings.forEach(this.removeTelemetryCollection);
|
||||
|
||||
this.openmct.time.off('bounds', this.refreshData);
|
||||
this.openmct.time.off('timeSystem', this.refreshData);
|
||||
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
[
|
||||
'lodash',
|
||||
'./SortedTableRowCollection'
|
||||
],
|
||||
function (
|
||||
_,
|
||||
SortedTableRowCollection
|
||||
) {
|
||||
|
||||
class BoundedTableRowCollection extends SortedTableRowCollection {
|
||||
constructor(openmct) {
|
||||
super();
|
||||
|
||||
this.futureBuffer = new SortedTableRowCollection();
|
||||
this.openmct = openmct;
|
||||
|
||||
this.sortByTimeSystem = this.sortByTimeSystem.bind(this);
|
||||
this.bounds = this.bounds.bind(this);
|
||||
|
||||
this.sortByTimeSystem(openmct.time.timeSystem());
|
||||
|
||||
this.lastBounds = openmct.time.bounds();
|
||||
|
||||
this.subscribeToBounds();
|
||||
}
|
||||
|
||||
addOne(item) {
|
||||
let parsedValue = this.getValueForSortColumn(item);
|
||||
// Insert into either in-bounds array, or the future buffer.
|
||||
// Data in the future buffer will be re-evaluated for possible
|
||||
// insertion on next bounds change
|
||||
let beforeStartOfBounds = parsedValue < this.lastBounds.start;
|
||||
let afterEndOfBounds = parsedValue > this.lastBounds.end;
|
||||
|
||||
if (!afterEndOfBounds && !beforeStartOfBounds) {
|
||||
return super.addOne(item);
|
||||
} else if (afterEndOfBounds) {
|
||||
this.futureBuffer.addOne(item);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
sortByTimeSystem(timeSystem) {
|
||||
this.sortBy({
|
||||
key: timeSystem.key,
|
||||
direction: 'asc'
|
||||
});
|
||||
let formatter = this.openmct.telemetry.getValueFormatter({
|
||||
key: timeSystem.key,
|
||||
source: timeSystem.key,
|
||||
format: timeSystem.timeFormat
|
||||
});
|
||||
this.parseTime = formatter.parse.bind(formatter);
|
||||
this.futureBuffer.sortBy({
|
||||
key: timeSystem.key,
|
||||
direction: 'asc'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is optimized for ticking - it assumes that start and end
|
||||
* bounds will only increase and as such this cannot be used for decreasing
|
||||
* bounds changes.
|
||||
*
|
||||
* An implication of this is that data will not be discarded that exceeds
|
||||
* the given end bounds. For arbitrary bounds changes, it's assumed that
|
||||
* a telemetry requery is performed anyway, and the collection is cleared
|
||||
* and repopulated.
|
||||
*
|
||||
* @fires TelemetryCollection#added
|
||||
* @fires TelemetryCollection#discarded
|
||||
* @param bounds
|
||||
*/
|
||||
bounds(bounds) {
|
||||
let startChanged = this.lastBounds.start !== bounds.start;
|
||||
let endChanged = this.lastBounds.end !== bounds.end;
|
||||
|
||||
let startIndex = 0;
|
||||
let endIndex = 0;
|
||||
|
||||
let discarded = [];
|
||||
let added = [];
|
||||
let testValue = {
|
||||
datum: {}
|
||||
};
|
||||
|
||||
this.lastBounds = bounds;
|
||||
|
||||
if (startChanged) {
|
||||
testValue.datum[this.sortOptions.key] = bounds.start;
|
||||
// Calculate the new index of the first item within the bounds
|
||||
startIndex = this.sortedIndex(this.rows, testValue);
|
||||
discarded = this.rows.splice(0, startIndex);
|
||||
}
|
||||
|
||||
if (endChanged) {
|
||||
testValue.datum[this.sortOptions.key] = bounds.end;
|
||||
// Calculate the new index of the last item in bounds
|
||||
endIndex = this.sortedLastIndex(this.futureBuffer.rows, testValue);
|
||||
added = this.futureBuffer.rows.splice(0, endIndex);
|
||||
added.forEach((datum) => this.rows.push(datum));
|
||||
}
|
||||
|
||||
if (discarded && discarded.length > 0) {
|
||||
/**
|
||||
* A `discarded` event is emitted when telemetry data fall out of
|
||||
* bounds due to a bounds change event
|
||||
* @type {object[]} discarded the telemetry data
|
||||
* discarded as a result of the bounds change
|
||||
*/
|
||||
this.emit('remove', discarded);
|
||||
}
|
||||
|
||||
if (added && added.length > 0) {
|
||||
/**
|
||||
* An `added` event is emitted when a bounds change results in
|
||||
* received telemetry falling within the new bounds.
|
||||
* @type {object[]} added the telemetry data that is now within bounds
|
||||
*/
|
||||
this.emit('add', added);
|
||||
}
|
||||
}
|
||||
|
||||
getValueForSortColumn(row) {
|
||||
return this.parseTime(row.datum[this.sortOptions.key]);
|
||||
}
|
||||
|
||||
unsubscribeFromBounds() {
|
||||
this.openmct.time.off('bounds', this.bounds);
|
||||
}
|
||||
|
||||
subscribeToBounds() {
|
||||
this.openmct.time.on('bounds', this.bounds);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.unsubscribeFromBounds();
|
||||
}
|
||||
}
|
||||
|
||||
return BoundedTableRowCollection;
|
||||
});
|
||||
@@ -1,136 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
[
|
||||
'./SortedTableRowCollection'
|
||||
],
|
||||
function (
|
||||
SortedTableRowCollection
|
||||
) {
|
||||
class FilteredTableRowCollection extends SortedTableRowCollection {
|
||||
constructor(masterCollection) {
|
||||
super();
|
||||
|
||||
this.masterCollection = masterCollection;
|
||||
this.columnFilters = {};
|
||||
|
||||
//Synchronize with master collection
|
||||
this.masterCollection.on('add', this.add);
|
||||
this.masterCollection.on('remove', this.remove);
|
||||
|
||||
//Default to master collection's sort options
|
||||
this.sortOptions = masterCollection.sortBy();
|
||||
}
|
||||
|
||||
setColumnFilter(columnKey, filter) {
|
||||
filter = filter.trim().toLowerCase();
|
||||
|
||||
let rowsToFilter = this.getRowsToFilter(columnKey, filter);
|
||||
|
||||
if (filter.length === 0) {
|
||||
delete this.columnFilters[columnKey];
|
||||
} else {
|
||||
this.columnFilters[columnKey] = filter;
|
||||
}
|
||||
|
||||
this.rows = rowsToFilter.filter(this.matchesFilters, this);
|
||||
this.emit('filter');
|
||||
}
|
||||
|
||||
setColumnRegexFilter(columnKey, filter) {
|
||||
filter = filter.trim();
|
||||
|
||||
let rowsToFilter = this.masterCollection.getRows();
|
||||
|
||||
this.columnFilters[columnKey] = new RegExp(filter);
|
||||
this.rows = rowsToFilter.filter(this.matchesFilters, this);
|
||||
this.emit('filter');
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
getRowsToFilter(columnKey, filter) {
|
||||
if (this.isSubsetOfCurrentFilter(columnKey, filter)) {
|
||||
return this.getRows();
|
||||
} else {
|
||||
return this.masterCollection.getRows();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
isSubsetOfCurrentFilter(columnKey, filter) {
|
||||
if (this.columnFilters[columnKey] instanceof RegExp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.columnFilters[columnKey]
|
||||
&& filter.startsWith(this.columnFilters[columnKey])
|
||||
// startsWith check will otherwise fail when filter cleared
|
||||
// because anyString.startsWith('') === true
|
||||
&& filter !== '';
|
||||
}
|
||||
|
||||
addOne(row) {
|
||||
return this.matchesFilters(row) && super.addOne(row);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
matchesFilters(row) {
|
||||
let doesMatchFilters = true;
|
||||
Object.keys(this.columnFilters).forEach((key) => {
|
||||
if (!doesMatchFilters || !this.rowHasColumn(row, key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let formattedValue = row.getFormattedValue(key);
|
||||
if (formattedValue === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.columnFilters[key] instanceof RegExp) {
|
||||
doesMatchFilters = this.columnFilters[key].test(formattedValue);
|
||||
} else {
|
||||
doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1;
|
||||
}
|
||||
});
|
||||
|
||||
return doesMatchFilters;
|
||||
}
|
||||
|
||||
rowHasColumn(row, key) {
|
||||
return Object.prototype.hasOwnProperty.call(row.columns, key);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.masterCollection.off('add', this.add);
|
||||
this.masterCollection.off('remove', this.remove);
|
||||
}
|
||||
}
|
||||
|
||||
return FilteredTableRowCollection;
|
||||
});
|
||||
@@ -36,85 +36,72 @@ define(
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
class SortedTableRowCollection extends EventEmitter {
|
||||
class TableRowCollection extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.dupeCheck = false;
|
||||
this.rows = [];
|
||||
this.columnFilters = {};
|
||||
this.addRows = this.addRows.bind(this);
|
||||
this.removeRowsByObject = this.removeRowsByObject.bind(this);
|
||||
this.removeRowsByData = this.removeRowsByData.bind(this);
|
||||
|
||||
this.add = this.add.bind(this);
|
||||
this.remove = this.remove.bind(this);
|
||||
this.clear = this.clear.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a datum or array of data to this telemetry collection
|
||||
* @fires TelemetryCollection#added
|
||||
* @param {object | object[]} rows
|
||||
*/
|
||||
add(rows) {
|
||||
if (Array.isArray(rows)) {
|
||||
this.dupeCheck = false;
|
||||
removeRowsByObject(keyString) {
|
||||
let removed = [];
|
||||
|
||||
let rowsAdded = rows.filter(this.addOne, this);
|
||||
if (rowsAdded.length > 0) {
|
||||
this.emit('add', rowsAdded);
|
||||
}
|
||||
this.rows = this.rows.filter((row) => {
|
||||
if (row.objectKeyString === keyString) {
|
||||
removed.push(row);
|
||||
|
||||
this.dupeCheck = true;
|
||||
} else {
|
||||
let wasAdded = this.addOne(rows);
|
||||
if (wasAdded) {
|
||||
this.emit('add', rows);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.emit('remove', removed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
addOne(row) {
|
||||
addRows(rows, type = 'add') {
|
||||
if (this.sortOptions === undefined) {
|
||||
throw 'Please specify sort options';
|
||||
}
|
||||
|
||||
let isDuplicate = false;
|
||||
let isFilterTriggeredReset = type === 'filter';
|
||||
let anyActiveFilters = Object.keys(this.columnFilters).length > 0;
|
||||
let rowsToAdd = !anyActiveFilters ? rows : rows.filter(this.matchesFilters, this);
|
||||
|
||||
// Going to check for duplicates. Bound the search problem to
|
||||
// items around the given time. Use sortedIndex because it
|
||||
// employs a binary search which is O(log n). Can use binary search
|
||||
// because the array is guaranteed ordered due to sorted insertion.
|
||||
let startIx = this.sortedIndex(this.rows, row);
|
||||
let endIx = undefined;
|
||||
|
||||
if (this.dupeCheck && startIx !== this.rows.length) {
|
||||
endIx = this.sortedLastIndex(this.rows, row);
|
||||
|
||||
// Create an array of potential dupes, based on having the
|
||||
// same time stamp
|
||||
let potentialDupes = this.rows.slice(startIx, endIx + 1);
|
||||
// Search potential dupes for exact dupe
|
||||
isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, row));
|
||||
// if type is filter, then it's a reset of all rows,
|
||||
// need to wipe current rows
|
||||
if (isFilterTriggeredReset) {
|
||||
this.rows = [];
|
||||
}
|
||||
|
||||
if (!isDuplicate) {
|
||||
this.rows.splice(endIx || startIx, 0, row);
|
||||
|
||||
return true;
|
||||
for (let row of rowsToAdd) {
|
||||
let index = this.sortedIndex(this.rows, row);
|
||||
this.rows.splice(index, 0, row);
|
||||
}
|
||||
|
||||
return false;
|
||||
// we emit filter no matter what to trigger
|
||||
// an update of visible rows
|
||||
if (rowsToAdd.length > 0 || isFilterTriggeredReset) {
|
||||
this.emit(type, rowsToAdd);
|
||||
}
|
||||
}
|
||||
|
||||
sortedLastIndex(rows, testRow) {
|
||||
return this.sortedIndex(rows, testRow, _.sortedLastIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the correct insertion point for the given row.
|
||||
* Leverages lodash's `sortedIndex` function which implements a binary search.
|
||||
* @private
|
||||
*/
|
||||
sortedIndex(rows, testRow, lodashFunction) {
|
||||
sortedIndex(rows, testRow, lodashFunction = _.sortedIndexBy) {
|
||||
if (this.rows.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
@@ -123,8 +110,6 @@ define(
|
||||
const firstValue = this.getValueForSortColumn(this.rows[0]);
|
||||
const lastValue = this.getValueForSortColumn(this.rows[this.rows.length - 1]);
|
||||
|
||||
lodashFunction = lodashFunction || _.sortedIndexBy;
|
||||
|
||||
if (this.sortOptions.direction === 'asc') {
|
||||
if (testRowValue > lastValue) {
|
||||
return this.rows.length;
|
||||
@@ -162,6 +147,22 @@ define(
|
||||
}
|
||||
}
|
||||
|
||||
removeRowsByData(data) {
|
||||
let removed = [];
|
||||
|
||||
this.rows = this.rows.filter((row) => {
|
||||
if (data.includes(row.fullDatum)) {
|
||||
removed.push(row);
|
||||
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
this.emit('remove', removed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the telemetry collection based on the provided sort field
|
||||
* specifier. Subsequent inserts are sorted to maintain specified sport
|
||||
@@ -205,6 +206,7 @@ define(
|
||||
if (arguments.length > 0) {
|
||||
this.sortOptions = sortOptions;
|
||||
this.rows = _.orderBy(this.rows, (row) => row.getParsedValue(sortOptions.key), sortOptions.direction);
|
||||
|
||||
this.emit('sort');
|
||||
}
|
||||
|
||||
@@ -212,44 +214,114 @@ define(
|
||||
return Object.assign({}, this.sortOptions);
|
||||
}
|
||||
|
||||
removeAllRowsForObject(objectKeyString) {
|
||||
let removed = [];
|
||||
this.rows = this.rows.filter(row => {
|
||||
if (row.objectKeyString === objectKeyString) {
|
||||
removed.push(row);
|
||||
setColumnFilter(columnKey, filter) {
|
||||
filter = filter.trim().toLowerCase();
|
||||
let wasBlank = this.columnFilters[columnKey] === undefined;
|
||||
let isSubset = this.isSubsetOfCurrentFilter(columnKey, filter);
|
||||
|
||||
if (filter.length === 0) {
|
||||
delete this.columnFilters[columnKey];
|
||||
} else {
|
||||
this.columnFilters[columnKey] = filter;
|
||||
}
|
||||
|
||||
if (isSubset || wasBlank) {
|
||||
this.rows = this.rows.filter(this.matchesFilters, this);
|
||||
this.emit('filter');
|
||||
} else {
|
||||
this.emit('resetRowsFromAllData');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
setColumnRegexFilter(columnKey, filter) {
|
||||
filter = filter.trim();
|
||||
this.columnFilters[columnKey] = new RegExp(filter);
|
||||
|
||||
this.emit('resetRowsFromAllData');
|
||||
}
|
||||
|
||||
getColumnMapForObject(objectKeyString) {
|
||||
let columns = this.configuration.getColumns();
|
||||
|
||||
if (columns[objectKeyString]) {
|
||||
return columns[objectKeyString].reduce((map, column) => {
|
||||
map[column.getKey()] = column;
|
||||
|
||||
return map;
|
||||
}, {});
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @private
|
||||
// */
|
||||
isSubsetOfCurrentFilter(columnKey, filter) {
|
||||
if (this.columnFilters[columnKey] instanceof RegExp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.columnFilters[columnKey]
|
||||
&& filter.startsWith(this.columnFilters[columnKey])
|
||||
// startsWith check will otherwise fail when filter cleared
|
||||
// because anyString.startsWith('') === true
|
||||
&& filter !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
matchesFilters(row) {
|
||||
let doesMatchFilters = true;
|
||||
Object.keys(this.columnFilters).forEach((key) => {
|
||||
if (!doesMatchFilters || !this.rowHasColumn(row, key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
let formattedValue = row.getFormattedValue(key);
|
||||
if (formattedValue === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.columnFilters[key] instanceof RegExp) {
|
||||
doesMatchFilters = this.columnFilters[key].test(formattedValue);
|
||||
} else {
|
||||
doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1;
|
||||
}
|
||||
});
|
||||
|
||||
this.emit('remove', removed);
|
||||
return doesMatchFilters;
|
||||
}
|
||||
|
||||
getValueForSortColumn(row) {
|
||||
return row.getParsedValue(this.sortOptions.key);
|
||||
}
|
||||
|
||||
remove(removedRows) {
|
||||
this.rows = this.rows.filter(row => {
|
||||
return removedRows.indexOf(row) === -1;
|
||||
});
|
||||
|
||||
this.emit('remove', removedRows);
|
||||
rowHasColumn(row, key) {
|
||||
return Object.prototype.hasOwnProperty.call(row.columns, key);
|
||||
}
|
||||
|
||||
getRows() {
|
||||
return this.rows;
|
||||
}
|
||||
|
||||
getRowsLength() {
|
||||
return this.rows.length;
|
||||
}
|
||||
|
||||
getValueForSortColumn(row) {
|
||||
return row.getParsedValue(this.sortOptions.key);
|
||||
}
|
||||
|
||||
clear() {
|
||||
let removedRows = this.rows;
|
||||
this.rows = [];
|
||||
|
||||
this.emit('remove', removedRows);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.removeAllListeners();
|
||||
}
|
||||
}
|
||||
|
||||
return SortedTableRowCollection;
|
||||
return TableRowCollection;
|
||||
});
|
||||
@@ -466,22 +466,21 @@ export default {
|
||||
|
||||
this.table.on('object-added', this.addObject);
|
||||
this.table.on('object-removed', this.removeObject);
|
||||
this.table.on('outstanding-requests', this.outstandingRequests);
|
||||
this.table.on('refresh', this.clearRowsAndRerender);
|
||||
this.table.on('historical-rows-processed', this.checkForMarkedRows);
|
||||
this.table.on('outstanding-requests', this.outstandingRequests);
|
||||
|
||||
this.table.filteredRows.on('add', this.rowsAdded);
|
||||
this.table.filteredRows.on('remove', this.rowsRemoved);
|
||||
this.table.filteredRows.on('sort', this.updateVisibleRows);
|
||||
this.table.filteredRows.on('filter', this.updateVisibleRows);
|
||||
this.table.tableRows.on('add', this.rowsAdded);
|
||||
this.table.tableRows.on('remove', this.rowsRemoved);
|
||||
this.table.tableRows.on('sort', this.updateVisibleRows);
|
||||
this.table.tableRows.on('filter', this.updateVisibleRows);
|
||||
|
||||
//Default sort
|
||||
this.sortOptions = this.table.filteredRows.sortBy();
|
||||
this.sortOptions = this.table.tableRows.sortBy();
|
||||
this.scrollable = this.$el.querySelector('.js-telemetry-table__body-w');
|
||||
this.contentTable = this.$el.querySelector('.js-telemetry-table__content');
|
||||
this.sizingTable = this.$el.querySelector('.js-telemetry-table__sizing');
|
||||
this.headersHolderEl = this.$el.querySelector('.js-table__headers-w');
|
||||
|
||||
this.table.configuration.on('change', this.updateConfiguration);
|
||||
|
||||
this.calculateTableSize();
|
||||
@@ -493,13 +492,14 @@ export default {
|
||||
destroyed() {
|
||||
this.table.off('object-added', this.addObject);
|
||||
this.table.off('object-removed', this.removeObject);
|
||||
this.table.off('outstanding-requests', this.outstandingRequests);
|
||||
this.table.off('historical-rows-processed', this.checkForMarkedRows);
|
||||
this.table.off('refresh', this.clearRowsAndRerender);
|
||||
this.table.off('outstanding-requests', this.outstandingRequests);
|
||||
|
||||
this.table.filteredRows.off('add', this.rowsAdded);
|
||||
this.table.filteredRows.off('remove', this.rowsRemoved);
|
||||
this.table.filteredRows.off('sort', this.updateVisibleRows);
|
||||
this.table.filteredRows.off('filter', this.updateVisibleRows);
|
||||
this.table.tableRows.off('add', this.rowsAdded);
|
||||
this.table.tableRows.off('remove', this.rowsRemoved);
|
||||
this.table.tableRows.off('sort', this.updateVisibleRows);
|
||||
this.table.tableRows.off('filter', this.updateVisibleRows);
|
||||
|
||||
this.table.configuration.off('change', this.updateConfiguration);
|
||||
|
||||
@@ -517,13 +517,13 @@ export default {
|
||||
|
||||
let start = 0;
|
||||
let end = VISIBLE_ROW_COUNT;
|
||||
let filteredRows = this.table.filteredRows.getRows();
|
||||
let filteredRowsLength = filteredRows.length;
|
||||
let tableRows = this.table.tableRows.getRows();
|
||||
let tableRowsLength = tableRows.length;
|
||||
|
||||
this.totalNumberOfRows = filteredRowsLength;
|
||||
this.totalNumberOfRows = tableRowsLength;
|
||||
|
||||
if (filteredRowsLength < VISIBLE_ROW_COUNT) {
|
||||
end = filteredRowsLength;
|
||||
if (tableRowsLength < VISIBLE_ROW_COUNT) {
|
||||
end = tableRowsLength;
|
||||
} else {
|
||||
let firstVisible = this.calculateFirstVisibleRow();
|
||||
let lastVisible = this.calculateLastVisibleRow();
|
||||
@@ -535,15 +535,15 @@ export default {
|
||||
|
||||
if (start < 0) {
|
||||
start = 0;
|
||||
end = Math.min(VISIBLE_ROW_COUNT, filteredRowsLength);
|
||||
} else if (end >= filteredRowsLength) {
|
||||
end = filteredRowsLength;
|
||||
end = Math.min(VISIBLE_ROW_COUNT, tableRowsLength);
|
||||
} else if (end >= tableRowsLength) {
|
||||
end = tableRowsLength;
|
||||
start = end - VISIBLE_ROW_COUNT + 1;
|
||||
}
|
||||
}
|
||||
|
||||
this.rowOffset = start;
|
||||
this.visibleRows = filteredRows.slice(start, end);
|
||||
this.visibleRows = tableRows.slice(start, end);
|
||||
|
||||
this.updatingView = false;
|
||||
});
|
||||
@@ -630,19 +630,19 @@ export default {
|
||||
filterChanged(columnKey) {
|
||||
if (this.enableRegexSearch[columnKey]) {
|
||||
if (this.isCompleteRegex(this.filters[columnKey])) {
|
||||
this.table.filteredRows.setColumnRegexFilter(columnKey, this.filters[columnKey].slice(1, -1));
|
||||
this.table.tableRows.setColumnRegexFilter(columnKey, this.filters[columnKey].slice(1, -1));
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.table.filteredRows.setColumnFilter(columnKey, this.filters[columnKey]);
|
||||
this.table.tableRows.setColumnFilter(columnKey, this.filters[columnKey]);
|
||||
}
|
||||
|
||||
this.setHeight();
|
||||
},
|
||||
clearFilter(columnKey) {
|
||||
this.filters[columnKey] = '';
|
||||
this.table.filteredRows.setColumnFilter(columnKey, '');
|
||||
this.table.tableRows.setColumnFilter(columnKey, '');
|
||||
this.setHeight();
|
||||
},
|
||||
rowsAdded(rows) {
|
||||
@@ -674,8 +674,8 @@ export default {
|
||||
* Calculates height based on total number of rows, and sets table height.
|
||||
*/
|
||||
setHeight() {
|
||||
let filteredRowsLength = this.table.filteredRows.getRows().length;
|
||||
this.totalHeight = this.rowHeight * filteredRowsLength - 1;
|
||||
let tableRowsLength = this.table.tableRows.getRowsLength();
|
||||
this.totalHeight = this.rowHeight * tableRowsLength - 1;
|
||||
// Set element height directly to avoid having to wait for Vue to update DOM
|
||||
// which causes subsequent scroll to use an out of date height.
|
||||
this.contentTable.style.height = this.totalHeight + 'px';
|
||||
@@ -689,13 +689,13 @@ export default {
|
||||
});
|
||||
},
|
||||
exportAllDataAsCSV() {
|
||||
const justTheData = this.table.filteredRows.getRows()
|
||||
const justTheData = this.table.tableRows.getRows()
|
||||
.map(row => row.getFormattedDatum(this.headers));
|
||||
|
||||
this.exportAsCSV(justTheData);
|
||||
},
|
||||
exportMarkedDataAsCSV() {
|
||||
const data = this.table.filteredRows.getRows()
|
||||
const data = this.table.tableRows.getRows()
|
||||
.filter(row => row.marked === true)
|
||||
.map(row => row.getFormattedDatum(this.headers));
|
||||
|
||||
@@ -900,7 +900,7 @@ export default {
|
||||
|
||||
let lastRowToBeMarked = this.visibleRows[rowIndex];
|
||||
|
||||
let allRows = this.table.filteredRows.getRows();
|
||||
let allRows = this.table.tableRows.getRows();
|
||||
let firstRowIndex = allRows.indexOf(this.markedRows[0]);
|
||||
let lastRowIndex = allRows.indexOf(lastRowToBeMarked);
|
||||
|
||||
@@ -923,17 +923,17 @@ export default {
|
||||
},
|
||||
checkForMarkedRows() {
|
||||
this.isShowingMarkedRowsOnly = false;
|
||||
this.markedRows = this.table.filteredRows.getRows().filter(row => row.marked);
|
||||
this.markedRows = this.table.tableRows.getRows().filter(row => row.marked);
|
||||
},
|
||||
showRows(rows) {
|
||||
this.table.filteredRows.rows = rows;
|
||||
this.table.filteredRows.emit('filter');
|
||||
this.table.tableRows.rows = rows;
|
||||
this.table.emit('filter');
|
||||
},
|
||||
toggleMarkedRows(flag) {
|
||||
if (flag) {
|
||||
this.isShowingMarkedRowsOnly = true;
|
||||
this.userScroll = this.scrollable.scrollTop;
|
||||
this.allRows = this.table.filteredRows.getRows();
|
||||
this.allRows = this.table.tableRows.getRows();
|
||||
|
||||
this.showRows(this.markedRows);
|
||||
this.setHeight();
|
||||
|
||||
@@ -48,6 +48,8 @@ describe("the plugin", () => {
|
||||
let tablePlugin;
|
||||
let element;
|
||||
let child;
|
||||
let historicalProvider;
|
||||
let originalRouterPath;
|
||||
let unlistenConfigMutation;
|
||||
|
||||
beforeEach((done) => {
|
||||
@@ -58,7 +60,12 @@ describe("the plugin", () => {
|
||||
tablePlugin = new TablePlugin();
|
||||
openmct.install(tablePlugin);
|
||||
|
||||
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
|
||||
historicalProvider = {
|
||||
request: () => {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
};
|
||||
spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider);
|
||||
|
||||
element = document.createElement('div');
|
||||
child = document.createElement('div');
|
||||
@@ -78,6 +85,8 @@ describe("the plugin", () => {
|
||||
callBack();
|
||||
});
|
||||
|
||||
originalRouterPath = openmct.router.path;
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
});
|
||||
@@ -190,11 +199,12 @@ describe("the plugin", () => {
|
||||
let telemetryPromise = new Promise((resolve) => {
|
||||
telemetryPromiseResolve = resolve;
|
||||
});
|
||||
openmct.telemetry.request.and.callFake(() => {
|
||||
|
||||
historicalProvider.request = () => {
|
||||
telemetryPromiseResolve(testTelemetry);
|
||||
|
||||
return telemetryPromise;
|
||||
});
|
||||
};
|
||||
|
||||
openmct.router.path = [testTelemetryObject];
|
||||
|
||||
@@ -208,6 +218,10 @@ describe("the plugin", () => {
|
||||
return telemetryPromise.then(() => Vue.nextTick());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
openmct.router.path = originalRouterPath;
|
||||
});
|
||||
|
||||
it("Renders a row for every telemetry datum returned", () => {
|
||||
let rows = element.querySelectorAll('table.c-telemetry-table__body tr');
|
||||
expect(rows.length).toBe(3);
|
||||
@@ -256,14 +270,14 @@ describe("the plugin", () => {
|
||||
});
|
||||
|
||||
it("Supports filtering telemetry by regular text search", () => {
|
||||
tableInstance.filteredRows.setColumnFilter("some-key", "1");
|
||||
tableInstance.tableRows.setColumnFilter("some-key", "1");
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
|
||||
|
||||
expect(filteredRowElements.length).toEqual(1);
|
||||
|
||||
tableInstance.filteredRows.setColumnFilter("some-key", "");
|
||||
tableInstance.tableRows.setColumnFilter("some-key", "");
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
|
||||
@@ -274,14 +288,14 @@ describe("the plugin", () => {
|
||||
});
|
||||
|
||||
it("Supports filtering using Regex", () => {
|
||||
tableInstance.filteredRows.setColumnRegexFilter("some-key", "^some-value$");
|
||||
tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value$");
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
|
||||
|
||||
expect(filteredRowElements.length).toEqual(0);
|
||||
|
||||
tableInstance.filteredRows.setColumnRegexFilter("some-key", "^some-value");
|
||||
tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value");
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
|
||||
|
||||
@@ -445,15 +445,22 @@ select {
|
||||
|
||||
/******************************************************** HYPERLINKS AND HYPERLINK BUTTONS */
|
||||
.c-hyperlink {
|
||||
&--link {
|
||||
color: $colorKey;
|
||||
}
|
||||
display: inline-block;
|
||||
color: $colorKey;
|
||||
|
||||
&--button {
|
||||
@include cButton();
|
||||
}
|
||||
}
|
||||
|
||||
.c-so-view--no-frame > .c-so-view__object-view > .c-hyperlink--button {
|
||||
@include abs();
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/******************************************************** MENUS */
|
||||
@mixin menuOuter() {
|
||||
border-radius: $basicCr;
|
||||
|
||||
@@ -138,22 +138,11 @@
|
||||
&.is-status--missing {
|
||||
border: $borderMissing;
|
||||
}
|
||||
|
||||
&__object-view {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
|
||||
.u-fills-container {
|
||||
// Expand component types that fill a container
|
||||
@include abs();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.l-angular-ov-wrapper {
|
||||
// This element is the recipient for object styling; cannot be display: contents
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -233,6 +233,7 @@ export default {
|
||||
},
|
||||
mounted: function () {
|
||||
document.addEventListener('click', this.closeViewAndSaveMenu);
|
||||
this.promptUserbeforeNavigatingAway = this.promptUserbeforeNavigatingAway.bind(this);
|
||||
window.addEventListener('beforeunload', this.promptUserbeforeNavigatingAway);
|
||||
|
||||
this.openmct.editor.on('isEditing', (isEditing) => {
|
||||
@@ -253,7 +254,7 @@ export default {
|
||||
}
|
||||
|
||||
document.removeEventListener('click', this.closeViewAndSaveMenu);
|
||||
window.removeEventListener('click', this.promptUserbeforeNavigatingAway);
|
||||
window.removeEventListener('beforeunload', this.promptUserbeforeNavigatingAway);
|
||||
},
|
||||
methods: {
|
||||
toggleSaveMenu() {
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
/******************************* MAIN AREA */
|
||||
&__main-container {
|
||||
// Wrapper for main views
|
||||
display: flex;
|
||||
//display: flex; NEEDS REGRESSION TESTING!!!
|
||||
flex: 1 1 auto !important;
|
||||
height: 100%; // Chrome 73 overflow bug fix
|
||||
overflow: auto;
|
||||
|
||||
@@ -104,7 +104,6 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
if (this.actionCollection) {
|
||||
this.actionCollection.hide(HIDDEN_ACTIONS);
|
||||
this.actionCollection.on('update', this.updateActionItems);
|
||||
this.updateActionItems(this.actionCollection.getActionsObject());
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
}
|
||||
|
||||
&__object-view {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
height: 100%; // Chrome 73
|
||||
overflow: auto;
|
||||
|
||||
@@ -14,19 +14,21 @@ const gitRevision = require('child_process')
|
||||
const gitBranch = require('child_process')
|
||||
.execSync('git rev-parse --abbrev-ref HEAD')
|
||||
.toString().trim();
|
||||
const vueFile = devMode ?
|
||||
path.join(__dirname, "node_modules/vue/dist/vue.js") :
|
||||
path.join(__dirname, "node_modules/vue/dist/vue.min.js");
|
||||
const vueFile = devMode
|
||||
? path.join(__dirname, "node_modules/vue/dist/vue.js")
|
||||
: path.join(__dirname, "node_modules/vue/dist/vue.min.js");
|
||||
|
||||
const webpackConfig = {
|
||||
mode: devMode ? 'development' : 'production',
|
||||
entry: {
|
||||
openmct: './openmct.js',
|
||||
couchDBChangesFeed: './src/plugins/persistence/couch/CouchChangesFeed.js',
|
||||
espressoTheme: './src/plugins/themes/espresso-theme.scss',
|
||||
snowTheme: './src/plugins/themes/snow-theme.scss',
|
||||
maelstromTheme: './src/plugins/themes/maelstrom-theme.scss'
|
||||
},
|
||||
output: {
|
||||
globalObject: "this",
|
||||
filename: '[name].js',
|
||||
library: '[name]',
|
||||
libraryTarget: 'umd',
|
||||
@@ -105,13 +107,15 @@ const webpackConfig = {
|
||||
name: '[name].[ext]',
|
||||
outputPath(url, resourcePath, context) {
|
||||
if (/\.(jpg|jpeg|png|svg)$/.test(url)) {
|
||||
return `images/${url}`
|
||||
return `images/${url}`;
|
||||
}
|
||||
|
||||
if (/\.ico$/.test(url)) {
|
||||
return `icons/${url}`
|
||||
return `icons/${url}`;
|
||||
}
|
||||
|
||||
if (/\.(woff2?|eot|ttf)$/.test(url)) {
|
||||
return `fonts/${url}`
|
||||
return `fonts/${url}`;
|
||||
} else {
|
||||
return `${url}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user