Compare commits

...

35 Commits

Author SHA1 Message Date
ab7e2c5747 Change version to 1.7.8 2021-10-19 12:36:25 -07:00
4d0487631b Fix plots dup requests (#4324)
* If there is a pending create request for an id, queue a duplicate request.
* Add time context to imagery for time strip
* Revert previous fix for plots duplicate requests.
Instead, check for timeContext before loading series data on load. Also, pass in start and end timestamps since plots could be using an independent time context
* Adds test for fix
2021-10-15 11:47:40 -07:00
8b53618adb Remove additional request to load plots when mounted. (#4314)
* If there is a pending create request for an id, queue a duplicate request.
* Add time context to imagery for time strip
* Remove duplicate request
2021-10-13 15:18:41 -07:00
364a97d8b7 Mct4177 fix for telemetry endpoints with '.' in the key (#4308)
* use quotes to avoid telmetery ids with dots screwing up mutate
* added more tests
* Save domain object after telemetry configuration has been removed

Co-authored-by: Joshi <simplyrender@gmail.com>
2021-10-13 13:51:44 -07:00
de5da9445b Add braintree sanitize url lib and sanitize form urls (#4296) 2021-10-07 12:21:56 -07:00
53d63d69fe Duplicate Request Fixes (#4295)
normalizing start/end dates for pan/zoom in conductor to prevent decimal timestamps that cause 2 bounds events
removed un-needed submit input that was causing "random" refreshes on conductor inputs
moving parseInt to scale method
2021-10-06 11:59:36 -07:00
c5350659e2 Stacked plots need to align the Y axis (#4286)
adjust style for y axis text
Remove watcher for plotWidth
2021-10-06 11:57:04 -07:00
76830b24ac Bar graph style nullcheck (#4291)
When a new telemetry endpoint is added to a bar graph, initialize colors correctly.
2021-10-06 11:51:11 -07:00
99e90c7488 added icon for inspector (#4275)
added icons for inspector for object type, alias
2021-10-05 21:44:22 -07:00
2c4fb4cb9d Show clicked image in large view (#4280) 2021-10-05 15:38:34 -07:00
c91b9f7825 Bargraph time metadata should consider 'source' (#4289) 2021-10-05 15:02:20 -07:00
166211a8be doing the easy thing first (#4278) 2021-10-05 14:39:17 -07:00
c4e41d784a [Telemetry Collection] No duplicate requests on load (#4274)
* Do not request telemetry twice on load
2021-09-30 14:44:50 -07:00
35eceea793 Resume plot if no pan, zoom, or drag action is taken (#4138) (#4256) 2021-09-30 14:22:43 -07:00
903a44fd80 Fix plot zoom when child of time strip (#4272) 2021-09-30 10:17:00 -07:00
3c546a0a1f Mct4196 - Fixes Conditional Styling not being applied when editing a Condition Widget (#4255)
* destroy style manager when clearing object view
2021-09-30 06:58:48 -07:00
b6786b2be3 Bar graph review comment fixes (#4232)
Addresses review comments and cleans up code for bar graphs
2021-09-29 14:27:45 -07:00
4e13b3ff43 Fix Bar Graph related CSS (#4270)
- Cleaned up commenting;
- Removed suppression of 0 line;
2021-09-29 12:52:36 -07:00
c20369d9bf Equal stacked plot y widths for all it's sub-plots (#4267) 2021-09-29 11:01:21 -07:00
f58cd4b9ce Imagery views should follow time context (#4264)
* If there is a pending create request for an id, queue a duplicate request.
* Add time context to imagery for time strip
* Fix tests
2021-09-28 12:43:38 -07:00
476128ced8 Condition sets only persist if actively editing (#4262) 2021-09-28 12:43:14 -07:00
22c812d67d [Table/Collection Fixes] Clearing correctly, no mutating options, no duplicate requests (#4261)
* do not process undefined data safety net
* using initiateHistoricalRequests anytime we make new reqeusts, so options are handle correctly, previously dates were being reused
* passing request options into request method
* removing form submit button from time conductor fixed inputs form
* moving both request functions into one, as they are both done each time
* fixing case where very last datum if duplicate was not being checked correctly
2021-09-28 12:38:57 -07:00
648f3532c1 Remove alert styling and hide pause button if in Fixed Time mode. (#4263) 2021-09-28 09:57:00 -07:00
4fe44a5619 [Telemetry Table] Progress bar tests (#4249)
* telemetry table progress bar tests
2021-09-28 07:28:30 -07:00
d221610df9 Flexible Layouts display Condition Sets as their editing/browsing interface (#4179)
* Flexible Layouts display Condition Sets as their editing/browsing interface #4141

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-09-28 07:19:24 -07:00
8243cf5d7b Fix missing object handling in several vues (#4259)
* If there is a pending create request for an id, queue a duplicate request.
* Fix downsteam errors when objects are missing
* Changed error logging from console.log to console.warn
2021-09-27 14:25:33 -07:00
c4c1fea17f snapshot clicked while in edit mode should open in preview mode #4115 (#4257) 2021-09-27 10:14:03 -07:00
5e920e90ce Fix bargraph color selection (#4253)
* Fix typo for attribute key
* Adds section heading for Bar Graph Series
2021-09-24 10:07:53 -07:00
886db23eb6 Hide independent time conductor mode if only 1 mode option is available. (#4250)
* If there is a pending create request for an id, queue a duplicate request.
* Hide independent time conductor mode if there is only 1 mode available
2021-09-23 10:47:35 -07:00
0ccb546a2e starting loading as false, since that makes sense (#4247) 2021-09-23 09:42:39 -07:00
271f8ed38f Fix file selection on pressing enter key (#4246)
* Invoke angular digest cycle after file input selection returns
2021-09-22 15:14:30 -07:00
650f84e95c [Telemetry Tables] Handling Request Loading (#4245)
* added two new events for telemetry collections to denote historical requests starting and ending (can be used for loading indicators)

* updating refresh data to use correct outstanding requests variable, binding request count update methods

* removing loading spinner (replaced with progress bar)

* if making a request, cancel any existing ones

* reverting edge case code updates
2021-09-22 15:01:28 -07:00
b70af5a1bb If there is a pending create request for an id, queue a duplicate request. (#4243) 2021-09-22 09:44:22 -07:00
0af21632db Use timeFormatter.parse to get the timestamp of imagery since the source could be something other than key (#4238) 2021-09-21 11:10:18 -07:00
e2f1ff5442 Notebook conflict auto retry 1.7.7 (#4230) 2021-09-20 14:33:38 -07:00
100 changed files with 1990 additions and 1882 deletions

View File

@ -118,100 +118,6 @@ define([
} }
} }
] ]
},
'example.spectral-generator': {
values: [
{
key: "name",
name: "Name",
format: "string"
},
{
key: "utc",
name: "Time",
format: "utc",
hints: {
domain: 1
}
},
{
key: "wavelength",
name: "Wavelength",
unit: "Hz",
formatString: '%0.2f',
hints: {
domain: 2,
spectralAttribute: true
}
},
{
key: "cos",
name: "Cosine",
unit: "deg",
formatString: '%0.2f',
hints: {
range: 2,
spectralAttribute: true
}
}
]
},
'example.spectral-aggregate-generator': {
values: [
{
key: "name",
name: "Name",
format: "string"
},
{
key: "utc",
name: "Time",
format: "utc",
hints: {
domain: 1
}
},
{
key: "ch1",
name: "Channel 1",
format: "string",
hints: {
range: 1
}
},
{
key: "ch2",
name: "Channel 2",
format: "string",
hints: {
range: 2
}
},
{
key: "ch3",
name: "Channel 3",
format: "string",
hints: {
range: 3
}
},
{
key: "ch4",
name: "Channel 4",
format: "string",
hints: {
range: 4
}
},
{
key: "ch5",
name: "Channel 5",
format: "string",
hints: {
range: 5
}
}
]
} }
}; };

View File

@ -1,86 +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([
], function (
) {
function SpectralAggregateGeneratorProvider() {
}
function pointForTimestamp(timestamp, count, name) {
return {
name: name,
utc: String(Math.floor(timestamp / count) * count),
ch1: String(Math.floor(timestamp / count) % 1),
ch2: String(Math.floor(timestamp / count) % 2),
ch3: String(Math.floor(timestamp / count) % 3),
ch4: String(Math.floor(timestamp / count) % 4),
ch5: String(Math.floor(timestamp / count) % 5)
};
}
SpectralAggregateGeneratorProvider.prototype.supportsSubscribe = function (domainObject) {
return domainObject.type === 'example.spectral-aggregate-generator';
};
SpectralAggregateGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
var count = 5000;
var interval = setInterval(function () {
var now = Date.now();
var datum = pointForTimestamp(now, count, domainObject.name);
callback(datum);
}, count);
return function () {
clearInterval(interval);
};
};
SpectralAggregateGeneratorProvider.prototype.supportsRequest = function (domainObject, options) {
return domainObject.type === 'example.spectral-aggregate-generator';
};
SpectralAggregateGeneratorProvider.prototype.request = function (domainObject, options) {
var start = options.start;
var end = Math.min(Date.now(), options.end); // no future values
var count = 5000;
if (options.strategy === 'latest' || options.size === 1) {
start = end;
}
var data = [];
while (start <= end && data.length < 5000) {
data.push(pointForTimestamp(start, count, domainObject.name));
start += count;
}
return Promise.resolve(data);
};
return SpectralAggregateGeneratorProvider;
});

View File

@ -1,102 +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([
'./WorkerInterface'
], function (
WorkerInterface
) {
var REQUEST_DEFAULTS = {
amplitude: 1,
wavelength: 1,
period: 10,
offset: 0,
dataRateInHz: 1,
randomness: 0,
phase: 0
};
function SpectralGeneratorProvider() {
this.workerInterface = new WorkerInterface();
}
SpectralGeneratorProvider.prototype.canProvideTelemetry = function (domainObject) {
return domainObject.type === 'example.spectral-generator';
};
SpectralGeneratorProvider.prototype.supportsRequest =
SpectralGeneratorProvider.prototype.supportsSubscribe =
SpectralGeneratorProvider.prototype.canProvideTelemetry;
SpectralGeneratorProvider.prototype.makeWorkerRequest = function (domainObject, request = {}) {
var props = [
'amplitude',
'wavelength',
'period',
'offset',
'dataRateInHz',
'phase',
'randomness'
];
var workerRequest = {};
props.forEach(function (prop) {
if (domainObject.telemetry && Object.prototype.hasOwnProperty.call(domainObject.telemetry, prop)) {
workerRequest[prop] = domainObject.telemetry[prop];
}
if (request && Object.prototype.hasOwnProperty.call(request, prop)) {
workerRequest[prop] = request[prop];
}
if (!Object.prototype.hasOwnProperty.call(workerRequest, prop)) {
workerRequest[prop] = REQUEST_DEFAULTS[prop];
}
workerRequest[prop] = Number(workerRequest[prop]);
});
workerRequest.name = domainObject.name;
return workerRequest;
};
SpectralGeneratorProvider.prototype.request = function (domainObject, request) {
var workerRequest = this.makeWorkerRequest(domainObject, request);
workerRequest.start = request.start;
workerRequest.end = request.end;
workerRequest.spectra = true;
return this.workerInterface.request(workerRequest);
};
SpectralGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
var workerRequest = this.makeWorkerRequest(domainObject, {});
workerRequest.spectra = true;
return this.workerInterface.subscribe(workerRequest, callback);
};
return SpectralGeneratorProvider;
});

View File

@ -24,15 +24,11 @@ define([
"./GeneratorProvider", "./GeneratorProvider",
"./SinewaveLimitProvider", "./SinewaveLimitProvider",
"./StateGeneratorProvider", "./StateGeneratorProvider",
"./SpectralGeneratorProvider",
"./SpectralAggregateGeneratorProvider",
"./GeneratorMetadataProvider" "./GeneratorMetadataProvider"
], function ( ], function (
GeneratorProvider, GeneratorProvider,
SinewaveLimitProvider, SinewaveLimitProvider,
StateGeneratorProvider, StateGeneratorProvider,
SpectralGeneratorProvider,
SpectralAggregateGeneratorProvider,
GeneratorMetadataProvider GeneratorMetadataProvider
) { ) {
@ -65,37 +61,6 @@ define([
openmct.telemetry.addProvider(new StateGeneratorProvider()); openmct.telemetry.addProvider(new StateGeneratorProvider());
openmct.types.addType("example.spectral-generator", {
name: "Spectral Generator",
description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",
cssClass: "icon-generator-telemetry",
creatable: true,
initialize: function (object) {
object.telemetry = {
period: 10,
amplitude: 1,
wavelength: 1,
frequency: 1,
offset: 0,
dataRateInHz: 1,
phase: 0,
randomness: 0
};
}
});
openmct.telemetry.addProvider(new SpectralGeneratorProvider());
openmct.types.addType("example.spectral-aggregate-generator", {
name: "Spectral Aggregate Generator",
description: "For development use. Generates example streaming telemetry data using a simple state algorithm.",
cssClass: "icon-generator-telemetry",
creatable: true,
initialize: function (object) {
object.telemetry = {};
}
});
openmct.telemetry.addProvider(new SpectralAggregateGeneratorProvider());
openmct.types.addType("generator", { openmct.types.addType("generator", {
name: "Sine Wave Generator", name: "Sine Wave Generator",
description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.", description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",

View File

@ -1,8 +1,9 @@
{ {
"name": "openmct", "name": "openmct",
"version": "1.7.8-SNAPSHOT", "version": "1.7.8",
"description": "The Open MCT core platform", "description": "The Open MCT core platform",
"devDependencies": { "devDependencies": {
"@braintree/sanitize-url": "^5.0.2",
"angular": ">=1.8.0", "angular": ">=1.8.0",
"angular-route": "1.4.14", "angular-route": "1.4.14",
"babel-eslint": "10.0.3", "babel-eslint": "10.0.3",

View File

@ -44,9 +44,11 @@ define(
setText(result.name); setText(result.name);
scope.ngModel[scope.field] = result; scope.ngModel[scope.field] = result;
control.$setValidity("file-input", true); control.$setValidity("file-input", true);
scope.$digest();
}, function () { }, function () {
setText('Select File'); setText('Select File');
control.$setValidity("file-input", false); control.$setValidity("file-input", false);
scope.$digest();
}); });
} }

View File

@ -263,6 +263,7 @@ define([
// Plugins that are installed by default // Plugins that are installed by default
this.install(this.plugins.Plot()); this.install(this.plugins.Plot());
this.install(this.plugins.Chart());
this.install(this.plugins.TelemetryTable.default()); this.install(this.plugins.TelemetryTable.default());
this.install(PreviewPlugin.default()); this.install(PreviewPlugin.default());
this.install(LegacyIndicatorsPlugin()); this.install(LegacyIndicatorsPlugin());

View File

@ -81,14 +81,8 @@ define([
return models; return models;
} }
return this.apiFetch(missingIds) //Temporary fix for missing models - don't retry using this.apiFetch
.then(function (apiResults) { return models;
Object.keys(apiResults).forEach(function (k) {
models[k] = apiResults[k];
});
return models;
});
}.bind(this)); }.bind(this));
}; };

View File

@ -110,7 +110,7 @@ class ActionsAPI extends EventEmitter {
return actionsObject; return actionsObject;
} }
_groupAndSortActions(actionsArray) { _groupAndSortActions(actionsArray = []) {
if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') { if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') {
actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]); actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]);
} }

View File

@ -0,0 +1,2 @@
export default class ConflictError extends Error {
}

View File

@ -26,6 +26,7 @@ import RootRegistry from './RootRegistry';
import RootObjectProvider from './RootObjectProvider'; import RootObjectProvider from './RootObjectProvider';
import EventEmitter from 'EventEmitter'; import EventEmitter from 'EventEmitter';
import InterceptorRegistry from './InterceptorRegistry'; import InterceptorRegistry from './InterceptorRegistry';
import ConflictError from './ConflictError';
/** /**
* Utilities for loading, saving, and manipulating domain objects. * Utilities for loading, saving, and manipulating domain objects.
@ -34,6 +35,7 @@ import InterceptorRegistry from './InterceptorRegistry';
*/ */
function ObjectAPI(typeRegistry, openmct) { function ObjectAPI(typeRegistry, openmct) {
this.openmct = openmct;
this.typeRegistry = typeRegistry; this.typeRegistry = typeRegistry;
this.eventEmitter = new EventEmitter(); this.eventEmitter = new EventEmitter();
this.providers = {}; this.providers = {};
@ -47,6 +49,10 @@ function ObjectAPI(typeRegistry, openmct) {
this.interceptorRegistry = new InterceptorRegistry(); this.interceptorRegistry = new InterceptorRegistry();
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan']; this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan'];
this.errors = {
Conflict: ConflictError
};
} }
/** /**
@ -181,8 +187,17 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) {
let objectPromise = provider.get(identifier, abortSignal).then(result => { let objectPromise = provider.get(identifier, abortSignal).then(result => {
delete this.cache[keystring]; delete this.cache[keystring];
result = this.applyGetInterceptors(identifier, result); result = this.applyGetInterceptors(identifier, result);
return result;
}).catch((result) => {
console.warn(`Failed to retrieve ${keystring}:`, result);
delete this.cache[keystring];
result = this.applyGetInterceptors(identifier);
return result; return result;
}); });
@ -285,6 +300,7 @@ ObjectAPI.prototype.isPersistable = function (idOrKeyString) {
ObjectAPI.prototype.save = function (domainObject) { ObjectAPI.prototype.save = function (domainObject) {
let provider = this.getProvider(domainObject.identifier); let provider = this.getProvider(domainObject.identifier);
let savedResolve; let savedResolve;
let savedReject;
let result; let result;
if (!this.isPersistable(domainObject.identifier)) { if (!this.isPersistable(domainObject.identifier)) {
@ -294,14 +310,18 @@ ObjectAPI.prototype.save = function (domainObject) {
} else { } else {
const persistedTime = Date.now(); const persistedTime = Date.now();
if (domainObject.persisted === undefined) { if (domainObject.persisted === undefined) {
result = new Promise((resolve) => { result = new Promise((resolve, reject) => {
savedResolve = resolve; savedResolve = resolve;
savedReject = reject;
}); });
domainObject.persisted = persistedTime; domainObject.persisted = persistedTime;
provider.create(domainObject).then((response) => { provider.create(domainObject)
this.mutate(domainObject, 'persisted', persistedTime); .then((response) => {
savedResolve(response); this.mutate(domainObject, 'persisted', persistedTime);
}); savedResolve(response);
}).catch((error) => {
savedReject(error);
});
} else { } else {
domainObject.persisted = persistedTime; domainObject.persisted = persistedTime;
this.mutate(domainObject, 'persisted', persistedTime); this.mutate(domainObject, 'persisted', persistedTime);

View File

@ -483,6 +483,10 @@ define([
* @returns {Object<String, {TelemetryValueFormatter}>} * @returns {Object<String, {TelemetryValueFormatter}>}
*/ */
TelemetryAPI.prototype.getFormatMap = function (metadata) { TelemetryAPI.prototype.getFormatMap = function (metadata) {
if (!metadata) {
return {};
}
if (!this.formatMapCache.has(metadata)) { if (!this.formatMapCache.has(metadata)) {
const formatMap = metadata.values().reduce(function (map, valueMetadata) { const formatMap = metadata.values().reduce(function (map, valueMetadata) {
map[valueMetadata.key] = this.getValueFormatter(valueMetadata); map[valueMetadata.key] = this.getValueFormatter(valueMetadata);

View File

@ -49,7 +49,6 @@ export class TelemetryCollection extends EventEmitter {
this.parseTime = undefined; this.parseTime = undefined;
this.metadata = this.openmct.telemetry.getMetadata(domainObject); this.metadata = this.openmct.telemetry.getMetadata(domainObject);
this.unsubscribe = undefined; this.unsubscribe = undefined;
this.historicalProvider = undefined;
this.options = options; this.options = options;
this.pageState = undefined; this.pageState = undefined;
this.lastBounds = undefined; this.lastBounds = undefined;
@ -65,13 +64,13 @@ export class TelemetryCollection extends EventEmitter {
this._error(ERRORS.LOADED); this._error(ERRORS.LOADED);
} }
this._timeSystem(this.openmct.time.timeSystem()); this._setTimeSystem(this.openmct.time.timeSystem());
this.lastBounds = this.openmct.time.bounds(); this.lastBounds = this.openmct.time.bounds();
this._watchBounds(); this._watchBounds();
this._watchTimeSystem(); this._watchTimeSystem();
this._initiateHistoricalRequests(); this._requestHistoricalTelemetry();
this._initiateSubscriptionTelemetry(); this._initiateSubscriptionTelemetry();
this.loaded = true; this.loaded = true;
@ -103,36 +102,35 @@ export class TelemetryCollection extends EventEmitter {
return this.boundedTelemetry; 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 * If a historical provider exists, then historical requests will be made
* @private * @private
*/ */
async _requestHistoricalTelemetry() { async _requestHistoricalTelemetry() {
if (!this.historicalProvider) { let options = { ...this.options };
let historicalProvider;
this.openmct.telemetry.standardizeRequestOptions(options);
historicalProvider = this.openmct.telemetry.
findRequestProvider(this.domainObject, options);
if (!historicalProvider) {
return; return;
} }
let historicalData; let historicalData;
this.options.onPartialResponse = this._processNewTelemetry.bind(this); options.onPartialResponse = this._processNewTelemetry.bind(this);
try { try {
if (this.requestAbort) {
this.requestAbort.abort();
}
this.requestAbort = new AbortController(); this.requestAbort = new AbortController();
this.options.signal = this.requestAbort.signal; options.signal = this.requestAbort.signal;
historicalData = await this.historicalProvider.request(this.domainObject, this.options); this.emit('requestStarted');
historicalData = await historicalProvider.request(this.domainObject, options);
} catch (error) { } catch (error) {
if (error.name !== 'AbortError') { if (error.name !== 'AbortError') {
console.error('Error requesting telemetry data...'); console.error('Error requesting telemetry data...');
@ -140,6 +138,7 @@ export class TelemetryCollection extends EventEmitter {
} }
} }
this.emit('requestEnded');
this.requestAbort = undefined; this.requestAbort = undefined;
this._processNewTelemetry(historicalData); this._processNewTelemetry(historicalData);
@ -173,6 +172,10 @@ export class TelemetryCollection extends EventEmitter {
* @private * @private
*/ */
_processNewTelemetry(telemetryData) { _processNewTelemetry(telemetryData) {
if (telemetryData === undefined) {
return;
}
let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData]; let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData];
let parsedValue; let parsedValue;
let beforeStartOfBounds; let beforeStartOfBounds;
@ -199,9 +202,10 @@ export class TelemetryCollection extends EventEmitter {
if (endIndex > startIndex) { if (endIndex > startIndex) {
let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex); let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex);
isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, datum)); isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, datum));
} }
} else if (startIndex === this.boundedTelemetry.length) {
isDuplicate = _.isEqual(datum, this.boundedTelemetry[this.boundedTelemetry.length - 1]);
} }
if (!isDuplicate) { if (!isDuplicate) {
@ -317,7 +321,7 @@ export class TelemetryCollection extends EventEmitter {
* Time System * Time System
* @private * @private
*/ */
_timeSystem(timeSystem) { _setTimeSystem(timeSystem) {
let domains = this.metadata.valuesForHints(['domain']); let domains = this.metadata.valuesForHints(['domain']);
let domain = domains.find((d) => d.key === timeSystem.key); let domain = domains.find((d) => d.key === timeSystem.key);
@ -333,7 +337,10 @@ export class TelemetryCollection extends EventEmitter {
this.parseTime = (datum) => { this.parseTime = (datum) => {
return valueFormatter.parse(datum); return valueFormatter.parse(datum);
}; };
}
_setTimeSystemAndFetchData(timeSystem) {
this._setTimeSystem(timeSystem);
this._reset(); this._reset();
} }
@ -370,19 +377,19 @@ export class TelemetryCollection extends EventEmitter {
} }
/** /**
* adds the _timeSystem callback to the 'timeSystem' timeAPI listener * adds the _setTimeSystemAndFetchData callback to the 'timeSystem' timeAPI listener
* @private * @private
*/ */
_watchTimeSystem() { _watchTimeSystem() {
this.openmct.time.on('timeSystem', this._timeSystem, this); this.openmct.time.on('timeSystem', this._setTimeSystemAndFetchData, this);
} }
/** /**
* removes the _timeSystem callback from the 'timeSystem' timeAPI listener * removes the _setTimeSystemAndFetchData callback from the 'timeSystem' timeAPI listener
* @private * @private
*/ */
_unwatchTimeSystem() { _unwatchTimeSystem() {
this.openmct.time.off('timeSystem', this._timeSystem, this); this.openmct.time.off('timeSystem', this._setTimeSystemAndFetchData, this);
} }
/** /**

View File

@ -172,7 +172,7 @@ class TimeAPI extends GlobalTimeContext {
* @memberof module:openmct.TimeAPI# * @memberof module:openmct.TimeAPI#
* @method getContextForView * @method getContextForView
*/ */
getContextForView(objectPath) { getContextForView(objectPath = []) {
let timeContext = this; let timeContext = this;
objectPath.forEach(item => { objectPath.forEach(item => {

View File

@ -41,7 +41,6 @@ const DEFAULTS = [
'platform/forms', 'platform/forms',
'platform/identity', 'platform/identity',
'platform/persistence/aggregator', 'platform/persistence/aggregator',
'platform/persistence/queue',
'platform/policy', 'platform/policy',
'platform/entanglement', 'platform/entanglement',
'platform/search', 'platform/search',

View File

@ -32,7 +32,7 @@ describe('the plugin', function () {
let openmct; let openmct;
let composition; let composition;
beforeEach((done) => { beforeEach(() => {
openmct = createOpenMct(); openmct = createOpenMct();
@ -47,11 +47,6 @@ describe('the plugin', function () {
} }
})); }));
openmct.on('start', done);
openmct.startHeadless();
composition = openmct.composition.get({identifier});
spyOn(couchPlugin.couchProvider, 'getObjectsByFilter').and.returnValue(Promise.resolve([ spyOn(couchPlugin.couchProvider, 'getObjectsByFilter').and.returnValue(Promise.resolve([
{ {
identifier: { identifier: {
@ -66,6 +61,19 @@ describe('the plugin', function () {
} }
} }
])); ]));
spyOn(couchPlugin.couchProvider, "get").and.callFake((id) => {
return Promise.resolve({
identifier: id
});
});
return new Promise((resolve) => {
openmct.once('start', resolve);
openmct.startHeadless();
}).then(() => {
composition = openmct.composition.get({identifier});
});
}); });
afterEach(() => { afterEach(() => {

View File

@ -96,11 +96,11 @@ export default {
this.timestampKey = this.openmct.time.timeSystem().key; this.timestampKey = this.openmct.time.timeSystem().key;
this.valueMetadata = this this.valueMetadata = this.metadata ? this
.metadata .metadata
.valuesForHints(['range'])[0]; .valuesForHints(['range'])[0] : undefined;
this.valueKey = this.valueMetadata.key; this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;
this.unsubscribe = this.openmct this.unsubscribe = this.openmct
.telemetry .telemetry
@ -151,7 +151,10 @@ export default {
size: 1, size: 1,
strategy: 'latest' strategy: 'latest'
}) })
.then((array) => this.updateValues(array[array.length - 1])); .then((array) => this.updateValues(array[array.length - 1]))
.catch((error) => {
console.warn('Error fetching data', error);
});
}, },
updateBounds(bounds, isTick) { updateBounds(bounds, isTick) {
this.bounds = bounds; this.bounds = bounds;

View File

@ -73,8 +73,9 @@ export default {
hasUnits() { hasUnits() {
let itemsWithUnits = this.items.filter((item) => { let itemsWithUnits = this.items.filter((item) => {
let metadata = this.openmct.telemetry.getMetadata(item.domainObject); let metadata = this.openmct.telemetry.getMetadata(item.domainObject);
const valueMetadatas = metadata ? metadata.valueMetadatas : [];
return this.metadataHasUnits(metadata.valueMetadatas); return this.metadataHasUnits(valueMetadatas);
}); });

View File

@ -23,30 +23,26 @@
import { BAR_GRAPH_KEY } from './BarGraphConstants'; import { BAR_GRAPH_KEY } from './BarGraphConstants';
export default function BarGraphCompositionPolicy(openmct) { export default function BarGraphCompositionPolicy(openmct) {
function hasAggregateDomainAndRange(metadata) { function hasRange(metadata) {
const rangeValues = metadata.valuesForHints(['range']); const rangeValues = metadata.valuesForHints(['range']);
return rangeValues.length > 0; return rangeValues.length > 0;
} }
function hasBarGraphTelemetry(domainObject) { function hasBarGraphTelemetry(domainObject) {
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) { if (!openmct.telemetry.isTelemetryObject(domainObject)) {
return false; return false;
} }
let metadata = openmct.telemetry.getMetadata(domainObject); let metadata = openmct.telemetry.getMetadata(domainObject);
return metadata.values().length > 0 && hasAggregateDomainAndRange(metadata); return metadata.values().length > 0 && hasRange(metadata);
}
function hasNoChildren(parentObject) {
return parentObject.composition && parentObject.composition.length < 1;
} }
return { return {
allow: function (parent, child) { allow: function (parent, child) {
if ((parent.type === BAR_GRAPH_KEY) if ((parent.type === BAR_GRAPH_KEY)
&& ((child.type !== 'telemetry.plot.overlay') && (hasBarGraphTelemetry(child) === false)) && (hasBarGraphTelemetry(child) === false)
) { ) {
return false; return false;
} }

View File

@ -1,5 +1,3 @@
export const BAR_GRAPH_VIEW = 'bar-graph.view'; export const BAR_GRAPH_VIEW = 'bar-graph.view';
export const BAR_GRAPH_KEY = 'telemetry.plot.bar-graph'; export const BAR_GRAPH_KEY = 'telemetry.plot.bar-graph';
export const BAR_GRAPH_INSPECTOR_KEY = 'telemetry.plot.bar-graph.inspector'; export const BAR_GRAPH_INSPECTOR_KEY = 'telemetry.plot.bar-graph.inspector';
export const SUBSCRIBE = 'subscribe';
export const UNSUBSCRIBE = 'unsubscribe';

View File

@ -12,6 +12,7 @@
</div> </div>
<div ref="plot" <div ref="plot"
class="c-bar-chart" class="c-bar-chart"
@plotly_relayout="zoom"
></div> ></div>
<div v-if="false" <div v-if="false"
ref="localControl" ref="localControl"
@ -28,8 +29,7 @@
</div> </div>
</template> </template>
<script> <script>
import Plotly from 'plotly.js-basic-dist'; import Plotly from 'plotly-basic';
import { SUBSCRIBE, UNSUBSCRIBE } from './BarGraphConstants';
const MULTI_AXES_X_PADDING_PERCENT = { const MULTI_AXES_X_PADDING_PERCENT = {
LEFT: 8, LEFT: 8,
@ -79,8 +79,6 @@ export default {
this.registerListeners(); this.registerListeners();
}, },
beforeDestroy() { beforeDestroy() {
this.$refs.plot.removeAllListeners();
if (this.plotResizeObserver) { if (this.plotResizeObserver) {
this.plotResizeObserver.unobserve(this.$refs.plotWrapper); this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
clearTimeout(this.resizeTimer); clearTimeout(this.resizeTimer);
@ -139,8 +137,8 @@ export default {
getYAxisMeta() { getYAxisMeta() {
const yAxisMeta = {}; const yAxisMeta = {};
this.data.forEach(d => { this.data.forEach(datum => {
const yAxisMetadata = d.yAxisMetadata; const yAxisMetadata = datum.yAxisMetadata;
const range = '1'; const range = '1';
const side = 'left'; const side = 'left';
const name = ''; const name = '';
@ -203,8 +201,6 @@ export default {
return yaxis; return yaxis;
}, },
registerListeners() { registerListeners() {
this.$refs.plot.on('plotly_relayout', this.zoom);
this.removeBarColorListener = this.openmct.objects.observe( this.removeBarColorListener = this.openmct.objects.observe(
this.domainObject, this.domainObject,
'configuration.barStyles', 'configuration.barStyles',
@ -226,17 +222,17 @@ export default {
this.updatePlot(); this.updatePlot();
this.isZoomed = false; this.isZoomed = false;
this.$emit(SUBSCRIBE); this.$emit('subscribe');
}, },
barColorChanged() { barColorChanged() {
const colors = []; const colors = [];
const indices = []; const indices = [];
this.data.forEach((item, index) => { this.data.forEach((item, index) => {
const key = item.key; const key = item.key;
const color = this.domainObject.configuration.barStyles[key] && this.domainObject.configuration.barStyles[key].color; const colorExists = this.domainObject.configuration.barStyles.series[key] && this.domainObject.configuration.barStyles.series[key].color;
indices.push(index); indices.push(index);
if (color) { if (colorExists) {
colors.push(); colors.push(this.domainObject.configuration.barStyles.series[key].color);
} else { } else {
colors.push(item.marker.color); colors.push(item.marker.color);
} }
@ -285,7 +281,7 @@ export default {
} }
this.isZoomed = true; this.isZoomed = true;
this.$emit(UNSUBSCRIBE); this.$emit('unsubscribe');
} }
} }
}; };

View File

@ -25,33 +25,31 @@
class="c-plot c-bar-chart-view" class="c-plot c-bar-chart-view"
:data="trace" :data="trace"
:plot-axis-title="plotAxisTitle" :plot-axis-title="plotAxisTitle"
@subscribe="subscribeToAll"
@unsubscribe="removeAllSubscriptions"
/> />
</template> </template>
<script> <script>
import * as SPECTRAL_AGGREGATE from './BarGraphConstants';
import ColorPalette from '../lib/ColorPalette';
import BarGraph from './BarGraphPlot.vue'; import BarGraph from './BarGraphPlot.vue';
import Color from "@/plugins/plot/lib/Color"; import _ from 'lodash';
export default { export default {
components: { components: {
BarGraph BarGraph
}, },
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject', 'path'],
data() { data() {
this.telemetryObjects = {};
this.telemetryObjectFormats = {};
this.subscriptions = [];
this.composition = {};
return { return {
composition: {},
currentDomainObject: this.domainObject,
subscriptions: [],
telemetryObjects: {},
trace: [] trace: []
}; };
}, },
computed: { computed: {
activeClock() {
return this.openmct.time.activeClock;
},
plotAxisTitle() { plotAxisTitle() {
const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {}; const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {};
const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : ''; const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : '';
@ -64,24 +62,14 @@ export default {
} }
}, },
mounted() { mounted() {
this.colorPalette = new ColorPalette();
this.loadComposition(); this.loadComposition();
this.openmct.time.on('bounds', this.refreshData); this.openmct.time.on('bounds', this.refreshData);
this.openmct.time.on('clock', this.clockChanged);
this.$refs.barGraph.$on(SPECTRAL_AGGREGATE.SUBSCRIBE, this.subscribeToAll);
this.$refs.barGraph.$on(SPECTRAL_AGGREGATE.UNSUBSCRIBE, this.removeAllSubscriptions);
this.unobserve = this.openmct.objects.observe(this.currentDomainObject, '*', this.updateDomainObject);
}, },
beforeDestroy() { beforeDestroy() {
this.$refs.barGraph.$off();
this.openmct.time.off('bounds', this.refreshData); this.openmct.time.off('bounds', this.refreshData);
this.openmct.time.off('clock', this.clockChanged);
this.removeAllSubscriptions(); this.removeAllSubscriptions();
this.unobserve();
if (!this.composition) { if (!this.composition) {
return; return;
@ -92,35 +80,34 @@ export default {
}, },
methods: { methods: {
addTelemetryObject(telemetryObject) { addTelemetryObject(telemetryObject) {
// grab information we need from the added telmetry object
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier); const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
this.telemetryObjects[key] = telemetryObject;
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
this.telemetryObjectFormats[key] = this.openmct.telemetry.getFormatMap(metadata);
const telemetryObjectPath = [telemetryObject, ...this.path];
const telemetryIsAlias = this.openmct.objects.isObjectPathToALink(telemetryObject, telemetryObjectPath);
if (!this.domainObject.configuration.barStyles) { // make an update object that's a clone of the existing styles object so we preserve existing choices
this.domainObject.configuration.barStyles = {}; let stylesUpdate = {};
if (this.domainObject.configuration.barStyles.series[key]) {
stylesUpdate = _.clone(this.domainObject.configuration.barStyles.series[key]);
} }
// check to see if we've set a bar color stylesUpdate.name = telemetryObject.name;
if (!this.domainObject.configuration.barStyles[key] || !this.domainObject.configuration.barStyles[key].color) { stylesUpdate.type = telemetryObject.type;
const color = this.colorPalette.getNextColor().asHexString(); stylesUpdate.isAlias = telemetryIsAlias;
this.domainObject.configuration.barStyles[key] = {
name: telemetryObject.name, // if something has changed, mutate and notify listeners
color if (!_.isEqual(stylesUpdate, this.domainObject.configuration.barStyles.series[key])) {
};
this.openmct.objects.mutate( this.openmct.objects.mutate(
this.domainObject, this.domainObject,
`configuration.barStyles[${this.key}]`, `configuration.barStyles.series["${key}"]`,
this.domainObject.configuration.barStyles[key] stylesUpdate
); );
} else {
let color = this.domainObject.configuration.barStyles[key].color;
if (!(color instanceof Color)) {
color = Color.fromHexString(color);
}
this.colorPalette.remove(color);
} }
this.telemetryObjects[key] = telemetryObject; // ask for the current telemetry data, then subcribe for changes
this.requestDataFor(telemetryObject); this.requestDataFor(telemetryObject);
this.subscribeToObject(telemetryObject); this.subscribeToObject(telemetryObject);
}, },
@ -144,12 +131,12 @@ export default {
this.trace = isInTrace ? newTrace : newTrace.concat([trace]); this.trace = isInTrace ? newTrace : newTrace.concat([trace]);
}, },
clockChanged() {
this.removeAllSubscriptions();
this.subscribeToAll();
},
getAxisMetadata(telemetryObject) { getAxisMetadata(telemetryObject) {
const metadata = this.openmct.telemetry.getMetadata(telemetryObject); const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
if (!metadata) {
return {};
}
const yAxisMetadata = metadata.valuesForHints(['range'])[0]; const yAxisMetadata = metadata.valuesForHints(['range'])[0];
//Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only //Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only
const xAxisMetadata = metadata.valuesForHints(['range']); const xAxisMetadata = metadata.valuesForHints(['range']);
@ -159,21 +146,19 @@ export default {
yAxisMetadata yAxisMetadata
}; };
}, },
getOptions(telemetryObject) { getOptions() {
const { start, end } = this.openmct.time.bounds(); const { start, end } = this.openmct.time.bounds();
return { return {
end, end,
start, start
startTime: null,
spectra: true
}; };
}, },
loadComposition() { loadComposition() {
this.composition = this.openmct.composition.get(this.currentDomainObject); this.composition = this.openmct.composition.get(this.domainObject);
if (!this.composition) { if (!this.composition) {
this.addTelemetryObject(this.currentDomainObject); this.addTelemetryObject(this.domainObject);
return; return;
} }
@ -202,21 +187,34 @@ export default {
removeTelemetryObject(identifier) { removeTelemetryObject(identifier) {
const key = this.openmct.objects.makeKeyString(identifier); const key = this.openmct.objects.makeKeyString(identifier);
delete this.telemetryObjects[key]; delete this.telemetryObjects[key];
if (this.domainObject.configuration.barStyles[key]) { if (this.telemetryObjectFormats && this.telemetryObjectFormats[key]) {
delete this.domainObject.configuration.barStyles[key]; delete this.telemetryObjectFormats[key];
}
if (this.domainObject.configuration.barStyles.series[key]) {
delete this.domainObject.configuration.barStyles.series[key];
this.openmct.objects.mutate(
this.domainObject,
`configuration.barStyles.series["${key}"]`,
undefined
);
} }
this.removeSubscription(key); this.removeSubscription(key);
this.trace = this.trace.filter(t => t.key !== key); this.trace = this.trace.filter(t => t.key !== key);
}, },
processData(telemetryObject, data, axisMetadata) { addDataToGraph(telemetryObject, data, axisMetadata) {
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier); const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (data.message) { if (data.message) {
this.openmct.notifications.alert(data.message); this.openmct.notifications.alert(data.message);
} }
if (!this.isDataInTimeRange(data, key)) {
return;
}
let xValues = []; let xValues = [];
let yValues = []; let yValues = [];
@ -224,10 +222,10 @@ export default {
axisMetadata.xAxisMetadata.forEach((metadata) => { axisMetadata.xAxisMetadata.forEach((metadata) => {
xValues.push(metadata.name); xValues.push(metadata.name);
if (data[metadata.key]) { if (data[metadata.key]) {
//TODO: Format the data? const formattedValue = this.format(key, metadata.key, data);
yValues.push(data[metadata.key]); yValues.push(formattedValue);
} else { } else {
yValues.push(''); yValues.push(null);
} }
}); });
@ -241,20 +239,43 @@ export default {
yAxisMetadata: axisMetadata.yAxisMetadata, yAxisMetadata: axisMetadata.yAxisMetadata,
type: 'bar', type: 'bar',
marker: { marker: {
color: this.domainObject.configuration.barStyles[key].color color: this.domainObject.configuration.barStyles.series[key].color
}, },
hoverinfo: 'skip' hoverinfo: 'skip'
}; };
this.addTrace(trace, key); this.addTrace(trace, key);
}, },
isDataInTimeRange(datum, key) {
const timeSystemKey = this.openmct.time.timeSystem().key;
let currentTimestamp = this.parse(key, timeSystemKey, datum);
return currentTimestamp && this.openmct.time.bounds().end >= currentTimestamp;
},
format(telemetryObjectKey, metadataKey, data) {
const formats = this.telemetryObjectFormats[telemetryObjectKey];
return formats[metadataKey].format(data);
},
parse(telemetryObjectKey, metadataKey, datum) {
if (!datum) {
return;
}
const formats = this.telemetryObjectFormats[telemetryObjectKey];
return formats[metadataKey].parse(datum);
},
requestDataFor(telemetryObject) { requestDataFor(telemetryObject) {
const axisMetadata = this.getAxisMetadata(telemetryObject); const axisMetadata = this.getAxisMetadata(telemetryObject);
this.openmct.telemetry.request(telemetryObject, this.getOptions(telemetryObject)) this.openmct.telemetry.request(telemetryObject)
.then(data => { .then(data => {
data.forEach((datum) => { data.forEach((datum) => {
this.processData(telemetryObject, datum, axisMetadata); this.addDataToGraph(telemetryObject, datum, axisMetadata);
}); });
})
.catch((error) => {
console.warn(`Error fetching data`, error);
}); });
}, },
subscribeToObject(telemetryObject) { subscribeToObject(telemetryObject) {
@ -262,10 +283,10 @@ export default {
this.removeSubscription(key); this.removeSubscription(key);
const options = this.getOptions(telemetryObject); const options = this.getOptions();
const axisMetadata = this.getAxisMetadata(telemetryObject); const axisMetadata = this.getAxisMetadata(telemetryObject);
const unsubscribe = this.openmct.telemetry.subscribe(telemetryObject, const unsubscribe = this.openmct.telemetry.subscribe(telemetryObject,
data => this.processData(telemetryObject, data, axisMetadata) data => this.addDataToGraph(telemetryObject, data, axisMetadata)
, options); , options);
this.subscriptions.push({ this.subscriptions.push({
@ -276,9 +297,6 @@ export default {
subscribeToAll() { subscribeToAll() {
const telemetryObjects = Object.values(this.telemetryObjects); const telemetryObjects = Object.values(this.telemetryObjects);
telemetryObjects.forEach(this.subscribeToObject); telemetryObjects.forEach(this.subscribeToObject);
},
updateDomainObject(newDomainObject) {
this.currentDomainObject = newDomainObject;
} }
} }
}; };

View File

@ -26,12 +26,14 @@ import Vue from 'vue';
export default function BarGraphViewProvider(openmct) { export default function BarGraphViewProvider(openmct) {
function isCompactView(objectPath) { function isCompactView(objectPath) {
return objectPath.find(object => object.type === 'time-strip'); let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
} }
return { return {
key: BAR_GRAPH_VIEW, key: BAR_GRAPH_VIEW,
name: 'Spectral Aggregate Plot', name: 'Bar Graph',
cssClass: 'icon-telemetry', cssClass: 'icon-telemetry',
canView(domainObject, objectPath) { canView(domainObject, objectPath) {
return domainObject && domainObject.type === BAR_GRAPH_KEY; return domainObject && domainObject.type === BAR_GRAPH_KEY;
@ -54,7 +56,8 @@ export default function BarGraphViewProvider(openmct) {
}, },
provide: { provide: {
openmct, openmct,
domainObject domainObject,
path: objectPath
}, },
data() { data() {
return { return {

View File

@ -1,6 +1,6 @@
import { BAR_GRAPH_INSPECTOR_KEY, BAR_GRAPH_KEY } from '../BarGraphConstants'; import { BAR_GRAPH_INSPECTOR_KEY, BAR_GRAPH_KEY } from '../BarGraphConstants';
import Vue from 'vue'; import Vue from 'vue';
import Options from "./Options.vue"; import BarGraphOptions from "./BarGraphOptions.vue";
export default function BarGraphInspectorViewProvider(openmct) { export default function BarGraphInspectorViewProvider(openmct) {
return { return {
@ -24,13 +24,13 @@ export default function BarGraphInspectorViewProvider(openmct) {
component = new Vue({ component = new Vue({
el: element, el: element,
components: { components: {
Options BarGraphOptions
}, },
provide: { provide: {
openmct, openmct,
domainObject: selection[0][0].context.item domainObject: selection[0][0].context.item
}, },
template: '<options></options>' template: '<bar-graph-options></bar-graph-options>'
}); });
}, },
destroy: function () { destroy: function () {

View File

@ -20,27 +20,31 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<template> <template>
<div> <ul class="c-tree c-bar-graph-options">
<ul class="c-tree"> <h2 title="Display properties for this object">Bar Graph Series</h2>
<li v-for="series in domainObject.composition" <li v-for="series in domainObject.composition"
:key="series.key" :key="series.key"
> >
<bar-graph-options :item="series" /> <series-options :item="series"
</li> :color-palette="colorPalette"
</ul> />
</div> </li>
</ul>
</template> </template>
<script> <script>
import BarGraphOptions from "./BarGraphOptions.vue"; import SeriesOptions from "./SeriesOptions.vue";
import ColorPalette from '@/ui/color/ColorPalette';
export default { export default {
components: { components: {
BarGraphOptions SeriesOptions
}, },
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject'],
data() { data() {
return { return {
isEditing: this.openmct.editor.isEditing() isEditing: this.openmct.editor.isEditing(),
colorPalette: this.colorPalette
}; };
}, },
computed: { computed: {
@ -48,6 +52,9 @@ export default {
return this.isEditing && !this.domainObject.locked; return this.isEditing && !this.domainObject.locked;
} }
}, },
beforeMount() {
this.colorPalette = new ColorPalette();
},
mounted() { mounted() {
this.openmct.editor.on('isEditing', this.setEditState); this.openmct.editor.on('isEditing', this.setEditState);
}, },

View File

@ -21,21 +21,26 @@
--> -->
<template> <template>
<ul> <ul>
<li class="c-tree__item menus-to-left"> <li class="c-tree__item menus-to-left"
:class="aliasCss"
>
<span class="c-disclosure-triangle is-enabled flex-elem" <span class="c-disclosure-triangle is-enabled flex-elem"
:class="expandedCssClass" :class="expandedCssClass"
@click="expanded = !expanded" @click="expanded = !expanded"
> >
</span> </span>
<div>
<div class="c-object-label">
<div :class="[seriesCss]">
</div>
<div class="c-object-label__name">{{ name }}</div> <div class="c-object-label__name">{{ name }}</div>
</div> </div>
</li> </li>
<ColorSwatch v-if="expanded" <ColorSwatch v-if="expanded"
:current-color="currentColor" :current-color="currentColor"
title="Manually set the color for this bar graph." title="Manually set the color for this bar graph series."
edit-title="Manually set the color for this bar graph" edit-title="Manually set the color for this bar graph series"
view-title="The color for this bar graph." view-title="The color for this bar graph series."
short-label="Color" short-label="Color"
class="grid-properties" class="grid-properties"
@colorSet="setColor" @colorSet="setColor"
@ -44,7 +49,8 @@
</template> </template>
<script> <script>
import ColorSwatch from '../../ColorSwatch.vue'; import ColorSwatch from '@/ui/color/ColorSwatch.vue';
import Color from "@/ui/color/Color";
export default { export default {
components: { components: {
@ -55,50 +61,90 @@ export default {
item: { item: {
type: Object, type: Object,
required: true required: true
},
colorPalette: {
type: Object,
required: true
} }
}, },
data() { data() {
return { return {
currentColor: undefined, currentColor: undefined,
name: '', name: '',
type: '',
isAlias: false,
expanded: false expanded: false
}; };
}, },
computed: { computed: {
expandedCssClass() { expandedCssClass() {
return this.expanded ? 'c-disclosure-triangle--expanded' : ''; return this.expanded ? 'c-disclosure-triangle--expanded' : '';
},
seriesCss() {
const type = this.openmct.types.get(this.type);
if (type && type.definition && type.definition.cssClass) {
return `c-object-label__type-icon ${type.definition.cssClass}`;
}
return 'c-object-label__type-icon';
},
aliasCss() {
let cssClass = '';
if (this.isAlias) {
cssClass = 'is-alias';
}
return cssClass;
} }
}, },
watch: { watch: {
item: { item: {
handler() { handler() {
this.initColor(); this.initColorAndName();
}, },
deep: true deep: true
} }
}, },
mounted() { mounted() {
this.key = this.openmct.objects.makeKeyString(this.item); this.key = this.openmct.objects.makeKeyString(this.item);
this.initColor(); this.initColorAndName();
this.unObserve = this.openmct.objects.observe(this.domainObject, `this.domainObject.configuration.barStyles[${this.key}]`, this.initColor); this.removeBarStylesListener = this.openmct.objects.observe(this.domainObject, `configuration.barStyles.series["${this.key}"]`, this.initColorAndName);
}, },
beforeDestroy() { beforeDestroy() {
if (this.unObserve) { if (this.removeBarStylesListener) {
this.unObserve(); this.removeBarStylesListener();
} }
}, },
methods: { methods: {
initColor() { initColorAndName() {
if (this.domainObject.configuration.barStyles && this.domainObject.configuration.barStyles[this.key]) { // this is called before the plot is initialized
this.currentColor = this.domainObject.configuration.barStyles[this.key].color; if (!this.domainObject.configuration.barStyles.series[this.key]) {
this.name = this.domainObject.configuration.barStyles[this.key].name; const color = this.colorPalette.getNextColor().asHexString();
this.domainObject.configuration.barStyles.series[this.key] = {
color,
type: '',
name: '',
isAlias: false
};
} else if (!this.domainObject.configuration.barStyles.series[this.key].color) {
this.domainObject.configuration.barStyles.series[this.key].color = this.colorPalette.getNextColor().asHexString();
} }
this.currentColor = this.domainObject.configuration.barStyles.series[this.key].color;
this.name = this.domainObject.configuration.barStyles.series[this.key].name;
this.type = this.domainObject.configuration.barStyles.series[this.key].type;
this.isAlias = this.domainObject.configuration.barStyles.series[this.key].isAlias;
let colorHexString = this.currentColor;
const colorObject = Color.fromHexString(colorHexString);
this.colorPalette.remove(colorObject);
}, },
setColor(chosenColor) { setColor(chosenColor) {
this.currentColor = chosenColor.asHexString(); this.currentColor = chosenColor.asHexString();
this.openmct.objects.mutate( this.openmct.objects.mutate(
this.domainObject, this.domainObject,
`configuration.barStyles[${this.key}].color`, `configuration.barStyles.series["${this.key}"].color`,
this.currentColor this.currentColor
); );
} }

View 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.
*****************************************************************************/
import { BAR_GRAPH_KEY } from './BarGraphConstants';
import BarGraphViewProvider from './BarGraphViewProvider';
import BarGraphInspectorViewProvider from './inspector/BarGraphInspectorViewProvider';
import BarGraphCompositionPolicy from './BarGraphCompositionPolicy';
export default function () {
return function install(openmct) {
openmct.types.addType(BAR_GRAPH_KEY, {
key: BAR_GRAPH_KEY,
name: "Bar Graph",
cssClass: "icon-bar-chart",
description: "View data as a bar graph. Can be added to Display Layouts.",
creatable: true,
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {
barStyles: { series: {} }
};
},
priority: 891
});
openmct.objectViews.addProvider(new BarGraphViewProvider(openmct));
openmct.inspectorViews.addProvider(new BarGraphInspectorViewProvider(openmct));
openmct.composition.addPolicy(new BarGraphCompositionPolicy(openmct).allow);
};
}

View File

@ -0,0 +1,486 @@
/*****************************************************************************
* 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 {createOpenMct, resetApplicationState, createMouseEvent} from "utils/testing";
import Vue from "vue";
import BarGraphPlugin from "./plugin";
import BarGraph from './BarGraphPlot.vue';
import EventEmitter from "EventEmitter";
import { BAR_GRAPH_VIEW, BAR_GRAPH_KEY, BAR_GRAPH_INSPECTOR_KEY } from './BarGraphConstants';
import BarGraphOptions from "./inspector/BarGraphOptions.vue";
describe("the plugin", function () {
let element;
let child;
let openmct;
let telemetryPromise;
let telemetryPromiseResolve;
let mockObjectPath;
beforeEach((done) => {
mockObjectPath = [
{
name: 'mock folder',
type: 'fake-folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
},
{
name: 'mock parent folder',
type: 'time-strip',
identifier: {
key: 'mock-parent-folder',
namespace: ''
}
}
];
const testTelemetry = [
{
'utc': 1,
'some-key': 'some-value 1',
'some-other-key': 'some-other-value 1'
},
{
'utc': 2,
'some-key': 'some-value 2',
'some-other-key': 'some-other-value 2'
},
{
'utc': 3,
'some-key': 'some-value 3',
'some-other-key': 'some-other-value 3'
}
];
openmct = createOpenMct();
telemetryPromise = new Promise((resolve) => {
telemetryPromiseResolve = resolve;
});
spyOn(openmct.telemetry, 'request').and.callFake(() => {
telemetryPromiseResolve(testTelemetry);
return telemetryPromise;
});
openmct.install(new BarGraphPlugin());
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);
document.body.appendChild(element);
spyOn(window, 'ResizeObserver').and.returnValue({
observe() {},
unobserve() {},
disconnect() {}
});
openmct.time.timeSystem("utc", {
start: 0,
end: 4
});
openmct.types.addType("test-object", {
creatable: true
});
openmct.on("start", done);
openmct.startHeadless();
});
afterEach((done) => {
openmct.time.timeSystem('utc', {
start: 0,
end: 1
});
resetApplicationState(openmct).then(done).catch(done);
});
describe("The bar graph view", () => {
let testDomainObject;
let barGraphObject;
// eslint-disable-next-line no-unused-vars
let component;
let mockComposition;
beforeEach(async () => {
const getFunc = openmct.$injector.get;
spyOn(openmct.$injector, "get")
.withArgs("exportImageService").and.returnValue({
exportPNG: () => {},
exportJPG: () => {}
})
.and.callFake(getFunc);
barGraphObject = {
identifier: {
namespace: "",
key: "test-plot"
},
type: "telemetry.plot.bar-graph",
name: "Test Bar Graph"
};
testDomainObject = {
identifier: {
namespace: "",
key: "test-object"
},
configuration: {
barStyles: {
series: {}
}
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testDomainObject);
return [testDomainObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
let viewContainer = document.createElement("div");
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
BarGraph
},
provide: {
openmct: openmct,
domainObject: barGraphObject,
composition: openmct.composition.get(barGraphObject)
},
template: "<BarGraph></BarGraph>"
});
await Vue.nextTick();
});
it("provides a bar graph view", () => {
const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);
const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW);
expect(plotViewProvider).toBeDefined();
});
it("Renders plotly bar graph", () => {
let barChartElement = element.querySelectorAll(".plotly");
expect(barChartElement.length).toBe(1);
});
it("Handles dots in telemetry id", () => {
const dotFullTelemetryObject = {
identifier: {
namespace: "someNamespace",
key: "~OpenMCT~outer.test-object.foo.bar"
},
type: "test-dotful-object",
name: "A Dotful Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key.foo.name.45",
name: "Some dotful attribute",
hints: {
range: 1
}
}, {
key: "some-other-key.bar.344.rad",
name: "Another dotful attribute",
hints: {
range: 2
}
}]
}
};
const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);
const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW);
const barGraphView = plotViewProvider.view(testDomainObject, [testDomainObject]);
barGraphView.show(child, true);
expect(testDomainObject.configuration.barStyles.series["test-object"].name).toEqual("Test Object");
mockComposition.emit('add', dotFullTelemetryObject);
expect(testDomainObject.configuration.barStyles.series["someNamespace:~OpenMCT~outer.test-object.foo.bar"].name).toEqual("A Dotful Object");
barGraphView.destroy();
});
});
describe("the bar graph objects", () => {
const mockObject = {
name: 'A very nice bar graph',
key: BAR_GRAPH_KEY,
creatable: true
};
it('defines a bar graph object type with the correct key', () => {
const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition;
expect(objectDef.key).toEqual(mockObject.key);
});
it('is creatable', () => {
const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition;
expect(objectDef.creatable).toEqual(mockObject.creatable);
});
});
describe("The bar graph composition policy", () => {
it("allows composition for telemetry that contain at least one range", () => {
const parent = {
"composition": [],
"configuration": {},
"name": "Some Bar Graph",
"type": "telemetry.plot.bar-graph",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "some-key",
name: "Some attribute",
hints: {
domain: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 1
}
}]
}
};
const composition = openmct.composition.get(parent);
expect(() => {
composition.add(testTelemetryObject);
}).not.toThrow();
expect(parent.composition.length).toBe(1);
});
it("disallows composition for telemetry that don't contain any range hints", () => {
const parent = {
"composition": [],
"configuration": {},
"name": "Some Bar Graph",
"type": "telemetry.plot.bar-graph",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "some-key",
name: "Some attribute"
}, {
key: "some-other-key",
name: "Another attribute"
}]
}
};
const composition = openmct.composition.get(parent);
expect(() => {
composition.add(testTelemetryObject);
}).toThrow();
expect(parent.composition.length).toBe(0);
});
});
describe('the inspector view', () => {
let mockComposition;
let testDomainObject;
let selection;
let plotInspectorView;
let viewContainer;
let optionsElement;
beforeEach(async () => {
testDomainObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
selection = [
[
{
context: {
item: {
id: "test-object",
identifier: {
key: "test-object",
namespace: ''
},
type: "telemetry.plot.bar-graph",
configuration: {
barStyles: {
series: {
'~Some~foo.bar': {
name: 'A telemetry object',
type: 'some-type',
isAlias: true
}
}
}
},
composition: [
{
key: '~Some~foo.bar'
}
]
}
}
},
{
context: {
item: {
type: 'time-strip',
identifier: {
key: 'some-other-key',
namespace: ''
}
}
}
}
]
];
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testDomainObject);
return [testDomainObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
viewContainer = document.createElement('div');
child.append(viewContainer);
const applicableViews = openmct.inspectorViews.get(selection);
plotInspectorView = applicableViews[0];
plotInspectorView.show(viewContainer);
await Vue.nextTick();
optionsElement = element.querySelector('.c-bar-graph-options');
});
afterEach(() => {
plotInspectorView.destroy();
});
it('it renders the options', () => {
expect(optionsElement).toBeDefined();
});
it('shows the name', () => {
const seriesEl = optionsElement.querySelector('.c-object-label__name');
expect(seriesEl.innerHTML).toEqual('A telemetry object');
});
});
});

View File

@ -65,7 +65,7 @@ export default class Condition extends EventEmitter {
} }
this.trigger = conditionConfiguration.configuration.trigger; this.trigger = conditionConfiguration.configuration.trigger;
this.description = ''; this.summary = '';
} }
updateResult(datum) { updateResult(datum) {
@ -134,7 +134,6 @@ export default class Condition extends EventEmitter {
criterionConfigurations.forEach((criterionConfiguration) => { criterionConfigurations.forEach((criterionConfiguration) => {
this.addCriterion(criterionConfiguration); this.addCriterion(criterionConfiguration);
}); });
this.updateDescription();
} }
updateCriteria(criterionConfigurations) { updateCriteria(criterionConfigurations) {
@ -146,7 +145,6 @@ export default class Condition extends EventEmitter {
this.criteria.forEach((criterion) => { this.criteria.forEach((criterion) => {
criterion.updateTelemetryObjects(this.conditionManager.telemetryObjects); criterion.updateTelemetryObjects(this.conditionManager.telemetryObjects);
}); });
this.updateDescription();
} }
/** /**
@ -200,7 +198,6 @@ export default class Condition extends EventEmitter {
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.off('telemetryIsStale', (obj) => this.handleStaleCriterion(obj)); criterion.off('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
this.criteria.splice(found.index, 1, newCriterion); this.criteria.splice(found.index, 1, newCriterion);
this.updateDescription();
} }
} }
@ -216,7 +213,6 @@ export default class Condition extends EventEmitter {
}); });
criterion.destroy(); criterion.destroy();
this.criteria.splice(found.index, 1); this.criteria.splice(found.index, 1);
this.updateDescription();
return true; return true;
} }
@ -228,7 +224,6 @@ export default class Condition extends EventEmitter {
let found = this.findCriterion(criterion.id); let found = this.findCriterion(criterion.id);
if (found) { if (found) {
this.criteria[found.index] = criterion.data; this.criteria[found.index] = criterion.data;
this.updateDescription();
} }
} }
@ -254,8 +249,7 @@ export default class Condition extends EventEmitter {
description = `${description} ${criterion.getDescription()} ${(index < this.criteria.length - 1) ? triggerDescription.conjunction : ''}`; description = `${description} ${criterion.getDescription()} ${(index < this.criteria.length - 1) ? triggerDescription.conjunction : ''}`;
}); });
this.description = description; this.summary = description;
this.conditionManager.updateConditionDescription(this);
} }
getTriggerDescription() { getTriggerDescription() {

View File

@ -105,7 +105,14 @@ export default class ConditionManager extends EventEmitter {
} }
updateConditionTelemetryObjects() { updateConditionTelemetryObjects() {
this.conditions.forEach((condition) => condition.updateTelemetryObjects()); this.conditions.forEach((condition) => {
condition.updateTelemetryObjects();
let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(item => item.id === condition.id);
if (index > -1) {
//Only assign the summary, don't mutate the domain object
this.conditionSetDomainObject.configuration.conditionCollection[index].summary = this.updateConditionDescription(condition);
}
});
} }
removeConditionTelemetryObjects() { removeConditionTelemetryObjects() {
@ -139,10 +146,17 @@ export default class ConditionManager extends EventEmitter {
} }
} }
updateConditionDescription(condition) {
condition.updateDescription();
return condition.summary;
}
updateCondition(conditionConfiguration) { updateCondition(conditionConfiguration) {
let condition = this.findConditionById(conditionConfiguration.id); let condition = this.findConditionById(conditionConfiguration.id);
if (condition) { if (condition) {
condition.update(conditionConfiguration); condition.update(conditionConfiguration);
conditionConfiguration.summary = this.updateConditionDescription(condition);
} }
let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(item => item.id === conditionConfiguration.id); let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(item => item.id === conditionConfiguration.id);
@ -152,16 +166,10 @@ export default class ConditionManager extends EventEmitter {
} }
} }
updateConditionDescription(condition) {
const found = this.conditionSetDomainObject.configuration.conditionCollection.find(conditionConfiguration => (conditionConfiguration.id === condition.id));
if (found.summary !== condition.description) {
found.summary = condition.description;
this.persistConditions();
}
}
initCondition(conditionConfiguration, index) { initCondition(conditionConfiguration, index) {
let condition = new Condition(conditionConfiguration, this.openmct, this); let condition = new Condition(conditionConfiguration, this.openmct, this);
conditionConfiguration.summary = this.updateConditionDescription(condition);
if (index !== undefined) { if (index !== undefined) {
this.conditions.splice(index + 1, 0, condition); this.conditions.splice(index + 1, 0, condition);
} else { } else {

View File

@ -33,8 +33,10 @@ export default class ConditionSetViewProvider {
this.cssClass = 'icon-conditional'; this.cssClass = 'icon-conditional';
} }
canView(domainObject) { canView(domainObject, objectPath) {
return domainObject.type === 'conditionSet'; const isConditionSet = domainObject.type === 'conditionSet';
return isConditionSet && this.openmct.router.isNavigatedObject(objectPath);
} }
canEdit(domainObject) { canEdit(domainObject) {

View File

@ -244,7 +244,7 @@ export default {
this.telemetryMetadataOptions = []; this.telemetryMetadataOptions = [];
telemetryObjects.forEach(telemetryObject => { telemetryObjects.forEach(telemetryObject => {
let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject); let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
this.addMetaDataOptions(telemetryMetadata.values()); this.addMetaDataOptions(telemetryMetadata ? telemetryMetadata.values() : []);
}); });
this.updateOperations(); this.updateOperations();
} }

View File

@ -192,7 +192,11 @@ export default {
this.telemetry.forEach((telemetryObject) => { this.telemetry.forEach((telemetryObject) => {
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier); const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject); let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
this.telemetryMetadataOptions[id] = telemetryMetadata.values().slice(); if (telemetryMetadata) {
this.telemetryMetadataOptions[id] = telemetryMetadata.values().slice();
} else {
this.telemetryMetadataOptions[id] = [];
}
}); });
}, },
addTestInput(testInput) { addTestInput(testInput) {

View File

@ -177,7 +177,7 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
const timeSystem = this.openmct.time.timeSystem(); const timeSystem = this.openmct.time.timeSystem();
telemetryRequestsResults.forEach((results, index) => { telemetryRequestsResults.forEach((results, index) => {
const latestDatum = results.length ? results[results.length - 1] : {}; const latestDatum = (Array.isArray(results) && results.length) ? results[results.length - 1] : {};
const datumId = keys[index]; const datumId = keys[index];
const normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObjects[datumId]); const normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObjects[datumId]);

View File

@ -167,6 +167,11 @@ export default class TelemetryCriterion extends EventEmitter {
id: this.id, id: this.id,
data: this.formatData(normalizedDatum) data: this.formatData(normalizedDatum)
}; };
}).catch((error) => {
return {
id: this.id,
data: this.formatData()
};
}); });
} }

View File

@ -27,6 +27,7 @@ import StylesView from "./components/inspector/StylesView.vue";
import Vue from 'vue'; import Vue from 'vue';
import {getApplicableStylesForItem} from "./utils/styleUtils"; import {getApplicableStylesForItem} from "./utils/styleUtils";
import ConditionManager from "@/plugins/condition/ConditionManager"; import ConditionManager from "@/plugins/condition/ConditionManager";
import StyleRuleManager from "./StyleRuleManager";
describe('the plugin', function () { describe('the plugin', function () {
let conditionSetDefinition; let conditionSetDefinition;
@ -96,8 +97,12 @@ describe('the plugin', function () {
mockListener = jasmine.createSpy('mockListener'); mockListener = jasmine.createSpy('mockListener');
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true);
conditionSetDefinition.initialize(mockConditionSetDomainObject); conditionSetDefinition.initialize(mockConditionSetDomainObject);
spyOn(openmct.objects, "save").and.returnValue(Promise.resolve(true));
openmct.on('start', done); openmct.on('start', done);
openmct.startHeadless(); openmct.startHeadless();
}); });
@ -126,21 +131,6 @@ describe('the plugin', function () {
expect(mockConditionSetDomainObject.composition instanceof Array).toBeTrue(); expect(mockConditionSetDomainObject.composition instanceof Array).toBeTrue();
expect(mockConditionSetDomainObject.composition.length).toEqual(0); expect(mockConditionSetDomainObject.composition.length).toEqual(0);
}); });
it('provides a view', () => {
const testViewObject = {
id: "test-object",
type: "conditionSet",
configuration: {
conditionCollection: []
}
};
const applicableViews = openmct.objectViews.get(testViewObject, []);
let conditionSetView = applicableViews.find((viewProvider) => viewProvider.key === 'conditionSet.view');
expect(conditionSetView).toBeDefined();
});
}); });
describe('the condition set usage for multiple display layout items', () => { describe('the condition set usage for multiple display layout items', () => {
@ -722,4 +712,123 @@ describe('the plugin', function () {
expect(result[2]).toBeUndefined(); expect(result[2]).toBeUndefined();
}); });
}); });
describe('canView of ConditionSetViewProvider', () => {
let conditionSetView;
const testViewObject = {
id: "test-object",
type: "conditionSet",
configuration: {
conditionCollection: []
}
};
beforeEach(() => {
const applicableViews = openmct.objectViews.get(testViewObject, []);
conditionSetView = applicableViews.find((viewProvider) => viewProvider.key === 'conditionSet.view');
});
it('provides a view', () => {
expect(conditionSetView).toBeDefined();
});
it('returns true for type `conditionSet` and is a navigated Object', () => {
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true);
const isCanView = conditionSetView.canView(testViewObject, []);
expect(isCanView).toBe(true);
});
it('returns false for type `conditionSet` and is not a navigated Object', () => {
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(false);
const isCanView = conditionSetView.canView(testViewObject, []);
expect(isCanView).toBe(false);
});
it('returns false for type `notConditionSet` and is a navigated Object', () => {
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true);
testViewObject.type = 'notConditionSet';
const isCanView = conditionSetView.canView(testViewObject, []);
expect(isCanView).toBe(false);
});
});
describe('The Style Rule Manager', () => {
it('should subscribe to the conditionSet after the editor saves', async () => {
const stylesObject = {
"styles": [
{
"conditionId": "a8bf7d1a-c1bb-4fc7-936a-62056a51b5cd",
"style": {
"backgroundColor": "#38761d",
"border": "",
"color": "#073763",
"isStyleInvisible": ""
}
},
{
"conditionId": "0558fa77-9bdc-4142-9f9a-7a28fe95182e",
"style": {
"backgroundColor": "#980000",
"border": "",
"color": "#ff9900",
"isStyleInvisible": ""
}
}
],
"staticStyle": {
"style": {
"backgroundColor": "",
"border": "",
"color": ""
}
},
"selectedConditionId": "0558fa77-9bdc-4142-9f9a-7a28fe95182e",
"defaultConditionId": "0558fa77-9bdc-4142-9f9a-7a28fe95182e",
"conditionSetIdentifier": {
"namespace": "",
"key": "035c589c-d98f-429e-8b89-d76bd8d22b29"
}
};
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
const mockTransactionService = jasmine.createSpyObj(
'transactionService',
['commit']
);
openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject', "subscribe", "getMetadata", "getValueFormatter", "request"]);
openmct.telemetry.isTelemetryObject.and.returnValue(true);
openmct.telemetry.subscribe.and.returnValue(function () {});
openmct.telemetry.getValueFormatter.and.returnValue({
parse: function (value) {
return value;
}
});
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);
openmct.telemetry.request.and.returnValue(Promise.resolve([]));
mockTransactionService.commit = async () => {};
const mockIdentifierService = jasmine.createSpyObj(
'identifierService',
['parse']
);
mockIdentifierService.parse.and.returnValue({
getSpace: () => {
return '';
}
});
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
openmct.$injector.get.withArgs('identifierService').and.returnValue(mockIdentifierService)
.withArgs('transactionService').and.returnValue(mockTransactionService);
const styleRuleManger = new StyleRuleManager(stylesObject, openmct, null, true);
spyOn(styleRuleManger, 'subscribeToConditionSet');
await openmct.editor.save();
expect(styleRuleManger.subscribeToConditionSet).toHaveBeenCalledTimes(1);
});
});
}); });

View File

@ -23,7 +23,7 @@
<template> <template>
<component :is="urlDefined ? 'a' : 'span'" <component :is="urlDefined ? 'a' : 'span'"
class="c-condition-widget u-style-receiver js-style-receiver" class="c-condition-widget u-style-receiver js-style-receiver"
:href="urlDefined ? internalDomainObject.url : null" :href="url"
> >
<div class="c-condition-widget__label"> <div class="c-condition-widget__label">
{{ internalDomainObject.label }} {{ internalDomainObject.label }}
@ -32,6 +32,8 @@
</template> </template>
<script> <script>
const sanitizeUrl = require("@braintree/sanitize-url").sanitizeUrl;
export default { export default {
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject'],
data: function () { data: function () {
@ -42,6 +44,9 @@ export default {
computed: { computed: {
urlDefined() { urlDefined() {
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0; return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
},
url() {
return this.urlDefined ? sanitizeUrl(this.internalDomainObject.url) : null;
} }
}, },
mounted() { mounted() {

View File

@ -101,7 +101,7 @@ export default {
addChildren(domainObject) { addChildren(domainObject) {
let keyString = this.openmct.objects.makeKeyString(domainObject.identifier); let keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
let metadata = this.openmct.telemetry.getMetadata(domainObject); let metadata = this.openmct.telemetry.getMetadata(domainObject);
let metadataWithFilters = metadata.valueMetadatas.filter(value => value.filters); let metadataWithFilters = metadata ? metadata.valueMetadatas.filter(value => value.filters) : [];
let hasFiltersWithKeyString = this.persistedFilters[keyString] !== undefined; let hasFiltersWithKeyString = this.persistedFilters[keyString] !== undefined;
let mutateFilters = false; let mutateFilters = false;
let childObject = { let childObject = {

View File

@ -27,7 +27,7 @@
'c-hyperlink--button' : isButton 'c-hyperlink--button' : isButton
}" }"
:target="domainObject.linkTarget" :target="domainObject.linkTarget"
:href="domainObject.url" :href="url"
> >
<span class="c-hyperlink__label">{{ domainObject.displayText }}</span> <span class="c-hyperlink__label">{{ domainObject.displayText }}</span>
</a> </a>
@ -35,6 +35,7 @@
</template> </template>
<script> <script>
const sanitizeUrl = require("@braintree/sanitize-url").sanitizeUrl;
export default { export default {
inject: ['domainObject'], inject: ['domainObject'],
@ -45,6 +46,9 @@ export default {
} }
return true; return true;
},
url() {
return sanitizeUrl(this.domainObject.url);
} }
} }
}; };

View File

@ -66,6 +66,10 @@ export default function ImageryTimestripViewProvider(openmct) {
destroy: function () { destroy: function () {
component.$destroy(); component.$destroy();
component = undefined; component = undefined;
},
getComponent() {
return component;
} }
}; };
} }

View File

@ -10,7 +10,14 @@ export default class ImageryView {
this.component = undefined; this.component = undefined;
} }
show(element) { show(element, isEditing, viewOptions) {
let alternateObjectPath;
let indexForFocusedImage;
if (viewOptions) {
indexForFocusedImage = viewOptions.indexForFocusedImage;
alternateObjectPath = viewOptions.objectPath;
}
this.component = new Vue({ this.component = new Vue({
el: element, el: element,
components: { components: {
@ -19,10 +26,15 @@ export default class ImageryView {
provide: { provide: {
openmct: this.openmct, openmct: this.openmct,
domainObject: this.domainObject, domainObject: this.domainObject,
objectPath: this.objectPath, objectPath: alternateObjectPath || this.objectPath,
currentView: this currentView: this
}, },
template: '<imagery-view ref="ImageryContainer"></imagery-view>' data() {
return {
indexForFocusedImage
};
},
template: '<imagery-view :index-for-focused-image="indexForFocusedImage" ref="ImageryContainer"></imagery-view>'
}); });
} }

View File

@ -41,7 +41,13 @@ import _ from "lodash";
const PADDING = 1; const PADDING = 1;
const ROW_HEIGHT = 100; const ROW_HEIGHT = 100;
const IMAGE_WIDTH_THRESHOLD = 40; const IMAGE_SIZE = 85;
const IMAGE_WIDTH_THRESHOLD = 25;
const CONTAINER_CLASS = 'c-imagery-tsv-container';
const NO_ITEMS_CLASS = 'c-imagery-tsv__no-items';
const IMAGE_WRAPPER_CLASS = 'c-imagery-tsv__image-wrapper';
const ID_PREFIX = 'wrapper-';
const IMAGE_ID_PREFIX = 'image-';
export default { export default {
mixins: [imageryData], mixins: [imageryData],
@ -78,10 +84,12 @@ export default {
this.canvasContext = this.canvas.getContext('2d'); this.canvasContext = this.canvas.getContext('2d');
this.setDimensions(); this.setDimensions();
this.updateViewBounds(); this.setScaleAndPlotImagery = this.setScaleAndPlotImagery.bind(this);
this.updateViewBounds = this.updateViewBounds.bind(this);
this.setTimeContext = this.setTimeContext.bind(this);
this.setTimeContext();
this.openmct.time.on("timeSystem", this.setScaleAndPlotImagery); this.updateViewBounds();
this.openmct.time.on("bounds", this.updateViewBounds);
this.resize = _.debounce(this.resize, 400); this.resize = _.debounce(this.resize, 400);
this.imageryStripResizeObserver = new ResizeObserver(this.resize); this.imageryStripResizeObserver = new ResizeObserver(this.resize);
@ -90,25 +98,36 @@ export default {
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges); this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
}, },
beforeDestroy() { beforeDestroy() {
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
}
if (this.imageryStripResizeObserver) { if (this.imageryStripResizeObserver) {
this.imageryStripResizeObserver.disconnect(); this.imageryStripResizeObserver.disconnect();
} }
this.openmct.time.off("timeSystem", this.setScaleAndPlotImagery); this.stopFollowingTimeContext();
this.openmct.time.off("bounds", this.updateViewBounds);
if (this.unlisten) { if (this.unlisten) {
this.unlisten(); this.unlisten();
} }
}, },
methods: { methods: {
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on("timeSystem", this.setScaleAndPlotImagery);
this.timeContext.on("bounds", this.updateViewBounds);
this.timeContext.on("timeContext", this.setTimeContext);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off("timeSystem", this.setScaleAndPlotImagery);
this.timeContext.off("bounds", this.updateViewBounds);
this.timeContext.off("timeContext", this.setTimeContext);
}
},
expand(index) { expand(index) {
const path = this.objectPath[0]; const path = this.objectPath[0];
this.previewAction.invoke([path]); this.previewAction.invoke([path], {
indexForFocusedImage: index,
objectPath: this.objectPath
});
}, },
observeForChanges(mutatedObject) { observeForChanges(mutatedObject) {
this.updateViewBounds(); this.updateViewBounds();
@ -134,14 +153,10 @@ export default {
return clientWidth; return clientWidth;
}, },
updateViewBounds(bounds, isTick) { updateViewBounds(bounds, isTick) {
this.viewBounds = this.openmct.time.bounds(); this.viewBounds = this.timeContext.bounds();
//Add a 50% padding to the end bounds to look ahead
let timespan = (this.viewBounds.end - this.viewBounds.start);
let padding = timespan / 2;
this.viewBounds.end = this.viewBounds.end + padding;
if (this.timeSystem === undefined) { if (this.timeSystem === undefined) {
this.timeSystem = this.openmct.time.timeSystem(); this.timeSystem = this.timeContext.timeSystem();
} }
this.setScaleAndPlotImagery(this.timeSystem, !isTick); this.setScaleAndPlotImagery(this.timeSystem, !isTick);
@ -172,18 +187,18 @@ export default {
}, },
clearPreviousImagery(clearAllImagery) { clearPreviousImagery(clearAllImagery) {
//TODO: Only clear items that are out of bounds //TODO: Only clear items that are out of bounds
let noItemsEl = this.$el.querySelectorAll(".c-imagery-tsv__no-items"); let noItemsEl = this.$el.querySelectorAll(`.${NO_ITEMS_CLASS}`);
noItemsEl.forEach(item => { noItemsEl.forEach(item => {
item.remove(); item.remove();
}); });
let imagery = this.$el.querySelectorAll(".c-imagery-tsv__image-wrapper"); let imagery = this.$el.querySelectorAll(`.${IMAGE_WRAPPER_CLASS}`);
imagery.forEach(item => { imagery.forEach(item => {
if (clearAllImagery) { if (clearAllImagery) {
item.remove(); item.remove();
} else { } else {
const id = this.getNSAttributesForElement(item, 'id'); const id = item.getAttributeNS(null, 'id');
if (id) { if (id) {
const timestamp = id.replace('id-', ''); const timestamp = id.replace(ID_PREFIX, '');
if (!this.isImageryInBounds({ if (!this.isImageryInBounds({
time: timestamp time: timestamp
})) { })) {
@ -205,7 +220,7 @@ export default {
} }
if (timeSystem === undefined) { if (timeSystem === undefined) {
timeSystem = this.openmct.time.timeSystem(); timeSystem = this.timeContext.timeSystem();
} }
if (timeSystem.isUTCBased) { if (timeSystem.isUTCBased) {
@ -223,19 +238,17 @@ export default {
this.xScale.range([PADDING, this.width - PADDING * 2]); this.xScale.range([PADDING, this.width - PADDING * 2]);
}, },
isImageryInBounds(imageObj) { isImageryInBounds(imageObj) {
return (imageObj.time < this.viewBounds.end) && (imageObj.time > this.viewBounds.start); return (imageObj.time <= this.viewBounds.end) && (imageObj.time >= this.viewBounds.start);
}, },
getImageryContainer() { getImageryContainer() {
let svgHeight = 100; let containerHeight = 100;
let svgWidth = this.imageHistory.length ? this.width : 200; let containerWidth = this.imageHistory.length ? this.width : 200;
let groupSVG; let imageryContainer;
let existingSVG = this.$el.querySelector(".c-imagery-tsv__contents svg"); let existingContainer = this.$el.querySelector(`.${CONTAINER_CLASS}`);
if (existingSVG) { if (existingContainer) {
groupSVG = existingSVG; imageryContainer = existingContainer;
this.setNSAttributesForElement(groupSVG, { imageryContainer.style.maxWidth = `${containerWidth}px`;
width: svgWidth
});
} else { } else {
let component = new Vue({ let component = new Vue({
components: { components: {
@ -246,26 +259,20 @@ export default {
}, },
data() { data() {
return { return {
isNested: true, isNested: true
height: svgHeight,
width: svgWidth
}; };
}, },
template: `<swim-lane :is-nested="isNested" :hide-label="true"><template slot="object"><svg class="c-imagery-tsv-container" :height="height" :width="width"></svg></template></swim-lane>` template: `<swim-lane :is-nested="isNested" :hide-label="true"><template slot="object"><div class="c-imagery-tsv-container"></div></template></swim-lane>`
}); });
this.$refs.imageryHolder.appendChild(component.$mount().$el); this.$refs.imageryHolder.appendChild(component.$mount().$el);
groupSVG = component.$el.querySelector('svg'); imageryContainer = component.$el.querySelector(`.${CONTAINER_CLASS}`);
imageryContainer.style.maxWidth = `${containerWidth}px`;
groupSVG.addEventListener('mouseout', (event) => { imageryContainer.style.height = `${containerHeight}px`;
if (event.target.nodeName === 'svg' || event.target.nodeName === 'use') {
this.removeFromForeground();
}
});
} }
return groupSVG; return imageryContainer;
}, },
isImageryWidthAcceptable() { isImageryWidthAcceptable() {
// We're calculating if there is enough space between images to show the thumbnails. // We're calculating if there is enough space between images to show the thumbnails.
@ -281,194 +288,123 @@ export default {
return imageContainerWidth < IMAGE_WIDTH_THRESHOLD; return imageContainerWidth < IMAGE_WIDTH_THRESHOLD;
}, },
drawImagery() { drawImagery() {
let groupSVG = this.getImageryContainer(); let imageryContainer = this.getImageryContainer();
const showImagePlaceholders = this.isImageryWidthAcceptable(); const showImagePlaceholders = this.isImageryWidthAcceptable();
let index = 0;
if (this.imageHistory.length) { if (this.imageHistory.length) {
this.imageHistory.forEach((currentImageObject, index) => { this.imageHistory.forEach((currentImageObject) => {
if (this.isImageryInBounds(currentImageObject)) { if (this.isImageryInBounds(currentImageObject)) {
this.plotImagery(currentImageObject, showImagePlaceholders, groupSVG, index); this.plotImagery(currentImageObject, showImagePlaceholders, imageryContainer, index);
index = index + 1;
} }
}); });
} else { } else {
this.plotNoItems(groupSVG); this.plotNoItems(imageryContainer);
} }
}, },
plotNoItems(svgElement) { plotNoItems(containerElement) {
let textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text'); let textElement = document.createElement('text');
this.setNSAttributesForElement(textElement, { textElement.classList.add(NO_ITEMS_CLASS);
x: "10",
y: "20",
class: "c-imagery-tsv__no-items"
});
textElement.innerHTML = 'No images within timeframe'; textElement.innerHTML = 'No images within timeframe';
svgElement.appendChild(textElement); containerElement.appendChild(textElement);
}, },
setNSAttributesForElement(element, attributes) { setNSAttributesForElement(element, attributes) {
Object.keys(attributes).forEach((key) => { if (!element) {
if (key === 'url') {
element.setAttributeNS('http://www.w3.org/1999/xlink', 'href', attributes[key]);
} else {
element.setAttributeNS(null, key, attributes[key]);
}
});
},
getNSAttributesForElement(element, attribute) {
return element.getAttributeNS(null, attribute);
},
getImageWrapper(item) {
const id = `id-${item.time}`;
return this.$el.querySelector(`.c-imagery-tsv__contents g[id=${id}]`);
},
plotImagery(item, showImagePlaceholders, svgElement, index) {
//TODO: Placeholder image
let existingImageWrapper = this.getImageWrapper(item);
//imageWrapper wraps the vertical tick rect and the image
if (existingImageWrapper) {
this.updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders);
} else {
let imageWrapper = this.createImageWrapper(index, item, showImagePlaceholders, svgElement);
svgElement.appendChild(imageWrapper);
}
},
updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) {
//Update the x co-ordinates of the handle and image elements and the url of image
//this is to avoid tearing down all elements completely and re-drawing them
this.setNSAttributesForElement(existingImageWrapper, {
showImagePlaceholders
});
let imageTickElement = existingImageWrapper.querySelector('rect.c-imagery-tsv__image-handle');
this.setNSAttributesForElement(imageTickElement, {
x: this.xScale(item.time)
});
let imageRect = existingImageWrapper.querySelector('rect.c-imagery-tsv__image-placeholder');
this.setNSAttributesForElement(imageRect, {
x: this.xScale(item.time) + 2
});
let imageElement = existingImageWrapper.querySelector('image');
const selector = `href*=${existingImageWrapper.id}`;
let hoverEl = this.$el.querySelector(`.c-imagery-tsv__contents use[${selector}]`);
const hideImageUrl = (showImagePlaceholders && !hoverEl);
this.setNSAttributesForElement(imageElement, {
x: this.xScale(item.time) + 2,
url: hideImageUrl ? '' : item.url
});
},
createImageWrapper(index, item, showImagePlaceholders, svgElement) {
const id = `id-${item.time}`;
const imgSize = String(ROW_HEIGHT - 15);
let imageWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
this.setNSAttributesForElement(imageWrapper, {
id,
class: 'c-imagery-tsv__image-wrapper',
showImagePlaceholders
});
//create image tick indicator
let imageTickElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
this.setNSAttributesForElement(imageTickElement, {
class: 'c-imagery-tsv__image-handle',
x: this.xScale(item.time),
y: 5,
rx: 0,
width: 2,
height: String(ROW_HEIGHT - 10)
});
imageWrapper.appendChild(imageTickElement);
let imageRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
this.setNSAttributesForElement(imageRect, {
class: 'c-imagery-tsv__image-placeholder',
x: this.xScale(item.time) + 2,
y: 10,
rx: 0,
width: imgSize,
height: imgSize,
mask: `#image-${item.time}`
});
imageWrapper.appendChild(imageRect);
let imageElement = document.createElementNS('http://www.w3.org/2000/svg', 'image');
this.setNSAttributesForElement(imageElement, {
id: `image-${item.time}`,
x: this.xScale(item.time) + 2,
y: 10,
rx: 0,
width: imgSize,
height: imgSize,
url: showImagePlaceholders ? '' : item.url
});
imageWrapper.appendChild(imageElement);
//TODO: Don't add the hover listener if the width is too small
imageWrapper.addEventListener('mouseover', this.bringToForeground.bind(this, svgElement, imageWrapper, index, item.url));
return imageWrapper;
},
bringToForeground(svgElement, imageWrapper, index, url, event) {
const selector = `href*=${imageWrapper.id}`;
let hoverEls = this.$el.querySelectorAll(`.c-imagery-tsv__contents use:not([${selector}])`);
if (hoverEls.length > 0) {
this.removeFromForeground(hoverEls);
}
hoverEls = this.$el.querySelectorAll(`.c-imagery-tsv__contents use[${selector}]`);
if (hoverEls.length) {
return; return;
} }
let imageElement = imageWrapper.querySelector('image'); Object.keys(attributes).forEach((key) => {
element.setAttributeNS(null, key, attributes[key]);
});
},
setStyles(element, styles) {
if (!element) {
return;
}
Object.keys(styles).forEach((key) => {
element.style[key] = styles[key];
});
},
getImageWrapper(item) {
const id = `${ID_PREFIX}${item.time}`;
return this.$el.querySelector(`.c-imagery-tsv__contents div[id=${id}]`);
},
plotImagery(item, showImagePlaceholders, containerElement, index) {
let existingImageWrapper = this.getImageWrapper(item);
//imageWrapper wraps the vertical tick and the image
if (existingImageWrapper) {
this.updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders);
} else {
let imageWrapper = this.createImageWrapper(index, item, showImagePlaceholders);
containerElement.appendChild(imageWrapper);
}
},
setImageDisplay(imageElement, showImagePlaceholders) {
if (showImagePlaceholders) {
imageElement.style.display = 'none';
} else {
imageElement.style.display = 'block';
}
},
updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) {
//Update the x co-ordinates of the image wrapper and the url of image
//this is to avoid tearing down all elements completely and re-drawing them
this.setNSAttributesForElement(existingImageWrapper, {
'data-show-image-placeholders': showImagePlaceholders
});
existingImageWrapper.style.left = `${this.xScale(item.time)}px`;
let imageElement = existingImageWrapper.querySelector('img');
this.setNSAttributesForElement(imageElement, { this.setNSAttributesForElement(imageElement, {
url: url, src: item.url
fill: 'none'
});
let hoverElement = document.createElementNS('http://www.w3.org/2000/svg', 'use');
this.setNSAttributesForElement(hoverElement, {
class: 'image-highlight',
x: 0,
href: `#${imageWrapper.id}`
}); });
this.setImageDisplay(imageElement, showImagePlaceholders);
},
createImageWrapper(index, item, showImagePlaceholders) {
const id = `${ID_PREFIX}${item.time}`;
let imageWrapper = document.createElement('div');
imageWrapper.classList.add(IMAGE_WRAPPER_CLASS);
imageWrapper.style.left = `${this.xScale(item.time)}px`;
this.setNSAttributesForElement(imageWrapper, { this.setNSAttributesForElement(imageWrapper, {
class: 'c-imagery-tsv__image-wrapper is-hovered' id,
'data-show-image-placeholders': showImagePlaceholders
}); });
// We're using mousedown here and not 'click' because 'click' doesn't seem to be triggered reliably //create image vertical tick indicator
hoverElement.addEventListener('mousedown', (e) => { let imageTickElement = document.createElement('div');
imageTickElement.classList.add('c-imagery-tsv__image-handle');
imageTickElement.style.width = '2px';
imageTickElement.style.height = `${String(ROW_HEIGHT - 10)}px`;
imageWrapper.appendChild(imageTickElement);
//create placeholder - this will also hold the actual image
let imagePlaceholder = document.createElement('div');
imagePlaceholder.classList.add('c-imagery-tsv__image-placeholder');
imagePlaceholder.style.width = `${IMAGE_SIZE}px`;
imagePlaceholder.style.height = `${IMAGE_SIZE}px`;
imageWrapper.appendChild(imagePlaceholder);
//create image element
let imageElement = document.createElement('img');
this.setNSAttributesForElement(imageElement, {
src: item.url
});
imageElement.style.width = `${IMAGE_SIZE}px`;
imageElement.style.height = `${IMAGE_SIZE}px`;
this.setImageDisplay(imageElement, showImagePlaceholders);
//handle mousedown event to show the image in a large view
imageWrapper.addEventListener('mousedown', (e) => {
if (e.button === 0) { if (e.button === 0) {
this.expand(index); this.expand(index);
} }
}); });
svgElement.appendChild(hoverElement); imagePlaceholder.appendChild(imageElement);
}, return imageWrapper;
removeFromForeground(items) {
let hoverEls;
if (items) {
hoverEls = items;
} else {
hoverEls = this.$el.querySelectorAll(".c-imagery-tsv__contents use");
}
hoverEls.forEach(item => {
let selector = `id*=${this.getNSAttributesForElement(item, 'href').replace('#', '')}`;
let imageWrapper = this.$el.querySelector(`.c-imagery-tsv__contents g[${selector}]`);
this.setNSAttributesForElement(imageWrapper, {
class: 'c-imagery-tsv__image-wrapper'
});
let showImagePlaceholders = this.getNSAttributesForElement(imageWrapper, 'showImagePlaceholders');
if (showImagePlaceholders === 'true') {
let imageElement = imageWrapper.querySelector('image');
this.setNSAttributesForElement(imageElement, {
url: ''
});
}
item.remove();
});
} }
} }
}; };

View File

@ -57,7 +57,7 @@
</div> </div>
<div ref="imageBG" <div ref="imageBG"
class="c-imagery__main-image__bg" class="c-imagery__main-image__bg"
:class="{'paused unnsynced': isPaused,'stale':false }" :class="{'paused unnsynced': isPaused && !isFixed,'stale':false }"
@click="expand" @click="expand"
> >
<div class="image-wrapper" <div class="image-wrapper"
@ -122,6 +122,7 @@
</div> </div>
<div class="h-local-controls"> <div class="h-local-controls">
<button <button
v-if="!isFixed"
class="c-button icon-pause pause-play" class="c-button icon-pause pause-play"
:class="{'is-paused': isPaused}" :class="{'is-paused': isPaused}"
@click="paused(!isPaused, 'button')" @click="paused(!isPaused, 'button')"
@ -131,7 +132,7 @@
</div> </div>
<div class="c-imagery__thumbs-wrapper" <div class="c-imagery__thumbs-wrapper"
:class="[ :class="[
{ 'is-paused': isPaused }, { 'is-paused': isPaused && !isFixed },
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused } { 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
]" ]"
> >
@ -199,6 +200,14 @@ export default {
}, },
mixins: [imageryData], mixins: [imageryData],
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'], inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
props: {
indexForFocusedImage: {
type: Number,
default() {
return undefined;
}
}
},
data() { data() {
let timeSystem = this.openmct.time.timeSystem(); let timeSystem = this.openmct.time.timeSystem();
this.metadata = {}; this.metadata = {};
@ -226,7 +235,8 @@ export default {
imageContainerWidth: undefined, imageContainerWidth: undefined,
imageContainerHeight: undefined, imageContainerHeight: undefined,
lockCompass: true, lockCompass: true,
resizingWindow: false resizingWindow: false,
timeContext: undefined
}; };
}, },
computed: { computed: {
@ -258,7 +268,14 @@ export default {
return age < cutoff && !this.refreshCSS; return age < cutoff && !this.refreshCSS;
}, },
canTrackDuration() { canTrackDuration() {
return this.openmct.time.clock() && this.timeSystem.isUTCBased; let hasClock;
if (this.timeContext) {
hasClock = this.timeContext.clock();
} else {
hasClock = this.openmct.time.clock();
}
return hasClock && this.timeSystem.isUTCBased;
}, },
isNextDisabled() { isNextDisabled() {
let disabled = false; let disabled = false;
@ -379,11 +396,28 @@ export default {
} }
return sizedImageDimensions; return sizedImageDimensions;
},
isFixed() {
let clock;
if (this.timeContext) {
clock = this.timeContext.clock();
} else {
clock = this.openmct.time.clock();
}
return clock === undefined;
} }
}, },
watch: { watch: {
imageHistorySize(newSize, oldSize) { imageHistorySize(newSize, oldSize) {
this.setFocusedImage(newSize - 1, false); let imageIndex;
if (this.indexForFocusedImage !== undefined) {
imageIndex = this.initFocusedImageIndex;
} else {
imageIndex = newSize - 1;
}
this.setFocusedImage(imageIndex, false);
this.scrollToRight(); this.scrollToRight();
}, },
focusedImageIndex() { focusedImageIndex() {
@ -394,9 +428,14 @@ export default {
} }
}, },
async mounted() { async mounted() {
//listen //We only need to use this till the user focuses an image manually
this.openmct.time.on('timeSystem', this.trackDuration); if (this.indexForFocusedImage !== undefined) {
this.openmct.time.on('clock', this.trackDuration); this.initFocusedImageIndex = this.indexForFocusedImage;
this.isPaused = true;
}
this.setTimeContext = this.setTimeContext.bind(this);
this.setTimeContext();
// related telemetry keys // related telemetry keys
this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ']; this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ'];
@ -432,8 +471,7 @@ export default {
}, },
beforeDestroy() { beforeDestroy() {
this.openmct.time.off('timeSystem', this.trackDuration); this.stopFollowingTimeContext();
this.openmct.time.off('clock', this.trackDuration);
if (this.thumbWrapperResizeObserver) { if (this.thumbWrapperResizeObserver) {
this.thumbWrapperResizeObserver.disconnect(); this.thumbWrapperResizeObserver.disconnect();
@ -457,6 +495,21 @@ export default {
} }
}, },
methods: { methods: {
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
//listen
this.timeContext.on('timeSystem', this.trackDuration);
this.timeContext.on('clock', this.trackDuration);
this.timeContext.on("timeContext", this.setTimeContext);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off("timeSystem", this.trackDuration);
this.timeContext.off("clock", this.trackDuration);
this.timeContext.off("timeContext", this.setTimeContext);
}
},
expand() { expand() {
const actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView); const actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView);
const visibleActions = actionCollection.getVisibleActions(); const visibleActions = actionCollection.getVisibleActions();
@ -618,7 +671,12 @@ export default {
}); });
}, },
setFocusedImage(index, thumbnailClick = false) { setFocusedImage(index, thumbnailClick = false) {
if (this.isPaused && !thumbnailClick) { if (thumbnailClick) {
//We use the props till the user changes what they want to see
this.initFocusedImageIndex = undefined;
}
if (this.isPaused && !thumbnailClick && this.initFocusedImageIndex === undefined) {
this.nextImageIndex = index; this.nextImageIndex = index;
//this could happen if bounds changes //this could happen if bounds changes
if (this.focusedImageIndex > this.imageHistory.length - 1) { if (this.focusedImageIndex > this.imageHistory.length - 1) {
@ -649,8 +707,12 @@ export default {
window.clearInterval(this.durationTracker); window.clearInterval(this.durationTracker);
}, },
updateDuration() { updateDuration() {
let currentTime = this.openmct.time.clock() && this.openmct.time.clock().currentValue(); let currentTime = this.timeContext.clock() && this.timeContext.clock().currentValue();
this.numericDuration = currentTime - this.parsedSelectedTime; if (currentTime === undefined) {
this.numericDuration = currentTime;
} else {
this.numericDuration = currentTime - this.parsedSelectedTime;
}
}, },
resetAgeCSS() { resetAgeCSS() {
this.refreshCSS = true; this.refreshCSS = true;

View File

@ -315,13 +315,31 @@
/*************************************** IMAGERY IN TIMESTRIP VIEWS */ /*************************************** IMAGERY IN TIMESTRIP VIEWS */
.c-imagery-tsv { .c-imagery-tsv {
g.c-imagery-tsv__image-wrapper { div.c-imagery-tsv__image-wrapper {
cursor: pointer; cursor: pointer;
position: absolute;
top: 0;
display: flex;
z-index: 1;
margin-top: 5px;
img {
align-self: flex-end;
}
&:hover {
z-index: 2;
&.is-hovered {
filter: brightness(1) contrast(1) !important; filter: brightness(1) contrast(1) !important;
[class*='__image-handle'] { [class*='__image-handle'] {
fill: $colorBodyFg; background-color: $colorBodyFg;
}
//[class*='__image-placeholder'] {
// display: none;
//}
img {
display: block !important;
} }
} }
} }
@ -331,14 +349,16 @@
} }
&__image-handle { &__image-handle {
fill: rgba($colorBodyFg, 0.5); background-color: rgba($colorBodyFg, 0.5);
} }
&__image-placeholder { &__image-placeholder {
fill: pushBack($colorBodyBg, 0.3); background-color: pushBack($colorBodyBg, 0.3);
display: block;
align-self: flex-end;
} }
&:hover g.c-imagery-tsv__image-wrapper { &:hover div.c-imagery-tsv__image-wrapper {
// TODO CH: convert to theme constants // TODO CH: convert to theme constants
filter: brightness(0.5) contrast(0.7); filter: brightness(0.5) contrast(0.7);
} }

View File

@ -26,8 +26,10 @@ export default {
inject: ['openmct', 'domainObject', 'objectPath'], inject: ['openmct', 'domainObject', 'objectPath'],
mounted() { mounted() {
// listen // listen
this.openmct.time.on('bounds', this.boundsChange); this.boundsChange = this.boundsChange.bind(this);
this.openmct.time.on('timeSystem', this.timeSystemChange); this.timeSystemChange = this.timeSystemChange.bind(this);
this.setDataTimeContext = this.setDataTimeContext.bind(this);
this.setDataTimeContext();
// set // set
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
@ -51,10 +53,24 @@ export default {
delete this.unsubscribe; delete this.unsubscribe;
} }
this.openmct.time.off('bounds', this.boundsChange); this.stopFollowingDataTimeContext();
this.openmct.time.off('timeSystem', this.timeSystemChange);
}, },
methods: { methods: {
setDataTimeContext() {
this.stopFollowingDataTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on('bounds', this.boundsChange);
this.boundsChange(this.timeContext.bounds());
this.timeContext.on('timeSystem', this.timeSystemChange);
this.timeContext.on("timeContext", this.setDataTimeContext);
},
stopFollowingDataTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.boundsChange);
this.timeContext.off('timeSystem', this.timeSystemChange);
this.timeContext.off("timeContext", this.setDataTimeContext);
}
},
datumIsNotValid(datum) { datumIsNotValid(datum) {
if (this.imageHistory.length === 0) { if (this.imageHistory.length === 0) {
return false; return false;
@ -111,7 +127,7 @@ export default {
} }
}, },
async requestHistory() { async requestHistory() {
let bounds = this.openmct.time.bounds(); let bounds = this.timeContext.bounds();
this.requestCount++; this.requestCount++;
const requestId = this.requestCount; const requestId = this.requestCount;
this.imageHistory = []; this.imageHistory = [];
@ -132,7 +148,7 @@ export default {
} }
}, },
timeSystemChange() { timeSystemChange() {
this.timeSystem = this.openmct.time.timeSystem(); this.timeSystem = this.timeContext.timeSystem();
this.timeKey = this.timeSystem.key; this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey); this.timeFormatter = this.getFormatter(this.timeKey);
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
@ -141,7 +157,7 @@ export default {
this.unsubscribe = this.openmct.telemetry this.unsubscribe = this.openmct.telemetry
.subscribe(this.domainObject, (datum) => { .subscribe(this.domainObject, (datum) => {
let parsedTimestamp = this.parseTime(datum); let parsedTimestamp = this.parseTime(datum);
let bounds = this.openmct.time.bounds(); let bounds = this.timeContext.bounds();
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) { if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
let image = this.normalizeDatum(datum); let image = this.normalizeDatum(datum);
@ -159,7 +175,7 @@ export default {
let image = { ...datum }; let image = { ...datum };
image.formattedTime = this.formatTime(datum); image.formattedTime = this.formatTime(datum);
image.url = this.formatImageUrl(datum); image.url = this.formatImageUrl(datum);
image.time = datum[this.timeKey]; image.time = this.parseTime(image.formattedTime);
image.imageDownloadName = this.getImageDownloadName(datum); image.imageDownloadName = this.getImageDownloadName(datum);
return image; return image;

View File

@ -22,6 +22,7 @@
import Vue from 'vue'; import Vue from 'vue';
import { import {
createMouseEvent,
createOpenMct, createOpenMct,
resetApplicationState, resetApplicationState,
simulateKeyEvent simulateKeyEvent
@ -32,19 +33,6 @@ const TEN_MINUTES = ONE_MINUTE * 10;
const MAIN_IMAGE_CLASS = '.js-imageryView-image'; const MAIN_IMAGE_CLASS = '.js-imageryView-image';
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new'; const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
const REFRESH_CSS_MS = 500; const REFRESH_CSS_MS = 500;
// const TOLERANCE = 0.50;
// function comparisonFunction(valueOne, valueTwo) {
// let larger = valueOne;
// let smaller = valueTwo;
//
// if (larger < smaller) {
// larger = valueTwo;
// smaller = valueOne;
// }
//
// return (larger - smaller) < TOLERANCE;
// }
function getImageInfo(doc) { function getImageInfo(doc) {
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0]; let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
@ -90,11 +78,13 @@ describe("The Imagery View Layouts", () => {
const START = Date.now(); const START = Date.now();
const COUNT = 10; const COUNT = 10;
let resolveFunction; // let resolveFunction;
let originalRouterPath; let originalRouterPath;
let telemetryPromise;
let telemetryPromiseResolve;
let cleanupFirst;
let openmct; let openmct;
let appHolder;
let parent; let parent;
let child; let child;
let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT); let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT);
@ -198,44 +188,63 @@ describe("The Imagery View Layouts", () => {
// this setups up the app // this setups up the app
beforeEach((done) => { beforeEach((done) => {
appHolder = document.createElement('div'); cleanupFirst = [];
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct(); openmct = createOpenMct();
openmct.time.timeSystem('utc', {
start: START - (5 * ONE_MINUTE),
end: START + (5 * ONE_MINUTE)
});
openmct.install(openmct.plugins.MyItems()); telemetryPromise = new Promise((resolve) => {
openmct.install(openmct.plugins.LocalTimeSystem()); telemetryPromiseResolve = resolve;
openmct.install(openmct.plugins.UTCTimeSystem()); });
spyOn(openmct.telemetry, 'request').and.callFake(() => {
telemetryPromiseResolve(imageTelemetry);
return telemetryPromise;
});
parent = document.createElement('div'); parent = document.createElement('div');
child = document.createElement('div'); parent.style.width = '640px';
parent.appendChild(child); parent.style.height = '480px';
// document.querySelector('body').append(parent); child = document.createElement('div');
child.style.width = '640px';
child.style.height = '480px';
parent.appendChild(child);
document.body.appendChild(parent);
spyOn(window, 'ResizeObserver').and.returnValue({ spyOn(window, 'ResizeObserver').and.returnValue({
observe() {}, observe() {},
disconnect() {} disconnect() {}
}); });
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([])); // spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({})); spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
originalRouterPath = openmct.router.path; originalRouterPath = openmct.router.path;
openmct.on('start', done); openmct.on('start', done);
openmct.start(appHolder); openmct.startHeadless();
}); });
afterEach(() => { afterEach((done) => {
openmct.time.timeSystem('utc', {
start: 0,
end: 1
});
openmct.router.path = originalRouterPath; openmct.router.path = originalRouterPath;
return resetApplicationState(openmct); // Needs to be in a timeout because plots use a bunch of setTimeouts, some of which can resolve during or after
// teardown, which causes problems
// This is hacky, we should find a better approach here.
setTimeout(() => {
//Cleanup code that needs to happen before dom elements start being destroyed
cleanupFirst.forEach(cleanup => cleanup());
cleanupFirst = [];
document.body.removeChild(parent);
resetApplicationState(openmct).then(done).catch(done);
});
}); });
it("should provide an imagery time strip view when in a time strip", () => { it("should provide an imagery time strip view when in a time strip", () => {
@ -262,7 +271,7 @@ describe("The Imagery View Layouts", () => {
}); });
it("should provide an imagery view only for imagery producing objects", () => { it("should provide an imagery view only for imagery producing objects", () => {
let applicableViews = openmct.objectViews.get(imageryObject, []); let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]);
let imageryView = applicableViews.find( let imageryView = applicableViews.find(
viewProvider => viewProvider.key === imageryKey viewProvider => viewProvider.key === imageryKey
); );
@ -315,51 +324,53 @@ describe("The Imagery View Layouts", () => {
let imageryViewProvider; let imageryViewProvider;
let imageryView; let imageryView;
beforeEach(async () => { beforeEach(() => {
let telemetryRequestResolve;
let telemetryRequestPromise = new Promise((resolve) => {
telemetryRequestResolve = resolve;
});
openmct.telemetry.request.and.callFake(() => { applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]);
telemetryRequestResolve(imageTelemetry);
return telemetryRequestPromise;
});
applicableViews = openmct.objectViews.get(imageryObject, []);
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey); imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey);
imageryView = imageryViewProvider.view(imageryObject); imageryView = imageryViewProvider.view(imageryObject, [imageryObject]);
imageryView.show(child); imageryView.show(child);
await telemetryRequestPromise; return Vue.nextTick();
}); });
afterEach(() => { // afterEach(() => {
openmct.time.stopClock(); // openmct.time.stopClock();
openmct.router.removeListener('change:hash', resolveFunction); // openmct.router.removeListener('change:hash', resolveFunction);
//
// imageryView.destroy();
// });
imageryView.destroy(); it("on mount should show the the most recent image", (done) => {
}); //Looks like we need Vue.nextTick here so that computed properties settle down
it("on mount should show the the most recent image", () => {
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
});
xit("should show the clicked thumbnail as the main image", (done) => {
const target = imageTelemetry[5].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
Vue.nextTick(() => { Vue.nextTick(() => {
const imageInfo = getImageInfo(parent); const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1); expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
done(); done();
}); });
}); });
it("should show the clicked thumbnail as the main image", (done) => {
//Looks like we need Vue.nextTick here so that computed properties settle down
Vue.nextTick(() => {
const target = imageTelemetry[5].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
Vue.nextTick(() => {
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);
done();
});
});
});
xit("should show that an image is new", (done) => { xit("should show that an image is new", (done) => {
openmct.time.clock('local', {
start: -1000,
end: 1000
});
Vue.nextTick(() => { Vue.nextTick(() => {
// used in code, need to wait to the 500ms here too // used in code, need to wait to the 500ms here too
setTimeout(() => { setTimeout(() => {
@ -385,69 +396,148 @@ describe("The Imagery View Layouts", () => {
}); });
}); });
xit("should navigate via arrow keys", (done) => { it("should navigate via arrow keys", (done) => {
let keyOpts = {
element: parent.querySelector('.c-imagery'),
key: 'ArrowLeft',
keyCode: 37,
type: 'keyup'
};
simulateKeyEvent(keyOpts);
Vue.nextTick(() => { Vue.nextTick(() => {
const imageInfo = getImageInfo(parent); let keyOpts = {
element: parent.querySelector('.c-imagery'),
key: 'ArrowLeft',
keyCode: 37,
type: 'keyup'
};
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1); simulateKeyEvent(keyOpts);
done();
Vue.nextTick(() => {
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);
done();
});
}); });
}); });
it("should navigate via numerous arrow keys", (done) => { it("should navigate via numerous arrow keys", (done) => {
let element = parent.querySelector('.c-imagery');
let type = 'keyup';
let leftKeyOpts = {
element,
type,
key: 'ArrowLeft',
keyCode: 37
};
let rightKeyOpts = {
element,
type,
key: 'ArrowRight',
keyCode: 39
};
// left thrice
simulateKeyEvent(leftKeyOpts);
simulateKeyEvent(leftKeyOpts);
simulateKeyEvent(leftKeyOpts);
// right once
simulateKeyEvent(rightKeyOpts);
Vue.nextTick(() => { Vue.nextTick(() => {
const imageInfo = getImageInfo(parent); let element = parent.querySelector('.c-imagery');
let type = 'keyup';
let leftKeyOpts = {
element,
type,
key: 'ArrowLeft',
keyCode: 37
};
let rightKeyOpts = {
element,
type,
key: 'ArrowRight',
keyCode: 39
};
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1); // left thrice
simulateKeyEvent(leftKeyOpts);
simulateKeyEvent(leftKeyOpts);
simulateKeyEvent(leftKeyOpts);
// right once
simulateKeyEvent(rightKeyOpts);
Vue.nextTick(() => {
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);
done();
});
});
});
it ('shows an auto scroll button when scroll to left', (done) => {
Vue.nextTick(() => {
// to mock what a scroll would do
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
Vue.nextTick(() => {
let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button');
expect(autoScrollButton).toBeTruthy();
done();
});
});
});
it ('scrollToRight is called when clicking on auto scroll button', (done) => {
Vue.nextTick(() => {
// use spyon to spy the scroll function
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight');
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
Vue.nextTick(() => {
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset');
done();
});
});
});
});
describe("imagery time strip view", () => {
let applicableViews;
let imageryViewProvider;
let imageryView;
let componentView;
beforeEach(() => {
openmct.time.timeSystem('utc', {
start: START - (5 * ONE_MINUTE),
end: START + (5 * ONE_MINUTE)
});
openmct.router.path = [{
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}];
applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, {
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}]);
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryForTimeStripKey);
imageryView = imageryViewProvider.view(imageryObject, [imageryObject, {
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}]);
imageryView.show(child);
componentView = imageryView.getComponent().$children[0];
spyOn(componentView.previewAction, 'invoke').and.callThrough();
return Vue.nextTick();
});
it("on mount should show imagery within the given bounds", (done) => {
Vue.nextTick(() => {
const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper');
expect(imageElements.length).toEqual(6);
done(); done();
}); });
}); });
it ('shows an auto scroll button when scroll to left', async () => {
// to mock what a scroll would do
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
await Vue.nextTick();
let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button');
expect(autoScrollButton).toBeTruthy();
});
it ('scrollToRight is called when clicking on auto scroll button', async () => {
// use spyon to spy the scroll function
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight');
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
await Vue.nextTick();
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset');
it("should show the clicked thumbnail as the preview image", (done) => {
Vue.nextTick(() => {
const mouseDownEvent = createMouseEvent("mousedown");
let imageWrapper = parent.querySelectorAll(`.c-imagery-tsv__image-wrapper`);
imageWrapper[2].dispatchEvent(mouseDownEvent);
Vue.nextTick(() => {
expect(componentView.previewAction.invoke).toHaveBeenCalledWith([componentView.objectPath[0]], {
indexForFocusedImage: 2,
objectPath: componentView.objectPath
});
done();
});
});
}); });
}); });
}); });

View File

@ -180,9 +180,13 @@ export default {
this.openmct.notifications.alert(message); this.openmct.notifications.alert(message);
} }
const relativeHash = hash.slice(hash.indexOf('#')); if (this.openmct.editor.isEditing()) {
const url = new URL(relativeHash, `${location.protocol}//${location.host}${location.pathname}`); this.previewEmbed();
this.openmct.router.navigate(url.hash); } else {
const relativeHash = hash.slice(hash.indexOf('#'));
const url = new URL(relativeHash, `${location.protocol}//${location.host}${location.pathname}`);
this.openmct.router.navigate(url.hash);
}
}, },
formatTime(unixTime, timeFormat) { formatTime(unixTime, timeFormat) {
return Moment.utc(unixTime).format(timeFormat); return Moment.utc(unixTime).format(timeFormat);

View File

@ -0,0 +1,72 @@
import {NOTEBOOK_TYPE} from './notebook-constants';
export default function (openmct) {
const apiSave = openmct.objects.save.bind(openmct.objects);
openmct.objects.save = async (domainObject) => {
if (domainObject.type !== NOTEBOOK_TYPE) {
return apiSave(domainObject);
}
const localMutable = openmct.objects._toMutable(domainObject);
let result;
try {
result = await apiSave(localMutable);
} catch (error) {
if (error instanceof openmct.objects.errors.Conflict) {
result = resolveConflicts(localMutable, openmct);
} else {
result = Promise.reject(error);
}
} finally {
openmct.objects.destroyMutable(localMutable);
}
return result;
};
}
function resolveConflicts(localMutable, openmct) {
return openmct.objects.getMutable(localMutable.identifier).then((remoteMutable) => {
const localEntries = localMutable.configuration.entries;
remoteMutable.$refresh(remoteMutable);
applyLocalEntries(remoteMutable, localEntries);
openmct.objects.destroyMutable(remoteMutable);
return true;
});
}
function applyLocalEntries(mutable, entries) {
Object.entries(entries).forEach(([sectionKey, pagesInSection]) => {
Object.entries(pagesInSection).forEach(([pageKey, localEntries]) => {
const remoteEntries = mutable.configuration.entries[sectionKey][pageKey];
const mergedEntries = [].concat(remoteEntries);
let shouldMutate = false;
const locallyAddedEntries = _.differenceBy(localEntries, remoteEntries, 'id');
const locallyModifiedEntries = _.differenceWith(localEntries, remoteEntries, (localEntry, remoteEntry) => {
return localEntry.id === remoteEntry.id && localEntry.text === remoteEntry.text;
});
locallyAddedEntries.forEach((localEntry) => {
mergedEntries.push(localEntry);
shouldMutate = true;
});
locallyModifiedEntries.forEach((locallyModifiedEntry) => {
let mergedEntry = mergedEntries.find(entry => entry.id === locallyModifiedEntry.id);
if (mergedEntry !== undefined) {
mergedEntry.text = locallyModifiedEntry.text;
shouldMutate = true;
}
});
if (shouldMutate) {
mutable.$set(`configuration.entries.${sectionKey}.${pageKey}`, mergedEntries);
}
});
});
}

View File

@ -2,6 +2,7 @@ import CopyToNotebookAction from './actions/CopyToNotebookAction';
import Notebook from './components/Notebook.vue'; import Notebook from './components/Notebook.vue';
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue'; import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
import SnapshotContainer from './snapshot-container'; import SnapshotContainer from './snapshot-container';
import monkeyPatchObjectAPIForNotebooks from './monkeyPatchObjectAPIForNotebooks.js';
import { notebookImageMigration } from '../notebook/utils/notebook-migration'; import { notebookImageMigration } from '../notebook/utils/notebook-migration';
import { NOTEBOOK_TYPE } from './notebook-constants'; import { NOTEBOOK_TYPE } from './notebook-constants';
@ -165,5 +166,7 @@ export default function NotebookPlugin() {
return domainObject; return domainObject;
} }
}); });
monkeyPatchObjectAPIForNotebooks(openmct);
}; };
} }

View File

@ -154,6 +154,8 @@ describe("Notebook plugin:", () => {
testObjectProvider.get.and.returnValue(Promise.resolve(notebookViewObject)); testObjectProvider.get.and.returnValue(Promise.resolve(notebookViewObject));
openmct.objects.addProvider('test-namespace', testObjectProvider); openmct.objects.addProvider('test-namespace', testObjectProvider);
testObjectProvider.observe.and.returnValue(() => {}); testObjectProvider.observe.and.returnValue(() => {});
testObjectProvider.create.and.returnValue(Promise.resolve(true));
testObjectProvider.update.and.returnValue(Promise.resolve(true));
return openmct.objects.getMutable(notebookViewObject.identifier).then((mutableObject) => { return openmct.objects.getMutable(notebookViewObject.identifier).then((mutableObject) => {
mutableNotebookObject = mutableObject; mutableNotebookObject = mutableObject;

View File

@ -125,7 +125,7 @@ export function addNotebookEntry(openmct, domainObject, notebookStorage, embed =
const newEntries = addEntryIntoPage(notebookStorage, entries, entry); const newEntries = addEntryIntoPage(notebookStorage, entries, entry);
addDefaultClass(domainObject, openmct); addDefaultClass(domainObject, openmct);
openmct.objects.mutate(domainObject, 'configuration.entries', newEntries); domainObject.configuration.entries = newEntries;
return id; return id;
} }

View File

@ -15,12 +15,16 @@
port.onmessage = async function (event) { port.onmessage = async function (event) {
if (event.data.request === 'close') { if (event.data.request === 'close') {
console.log('Closing connection');
connections.splice(event.data.connectionId - 1, 1); connections.splice(event.data.connectionId - 1, 1);
if (connections.length <= 0) { if (connections.length <= 0) {
// abort any outstanding requests if there's nobody listening to it. // abort any outstanding requests if there's nobody listening to it.
controller.abort(); controller.abort();
} }
console.log('Closed.');
connected = false;
return; return;
} }
@ -29,68 +33,9 @@
return; return;
} }
connected = true; do {
await self.listenForChanges(event.data.url, event.data.body, port);
let url = event.data.url; } while (connected);
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
});
}
} }
}; };
@ -103,4 +48,64 @@
console.log('Error on feed'); console.log('Error on feed');
}; };
self.listenForChanges = async function (url, body, port) {
connected = true;
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
console.log('Opening changes feed connection.');
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');
console.log('Received chunk');
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);
}
});
}
}
}
console.log('Done reading changes feed');
};
}()); }());

View File

@ -29,7 +29,7 @@ const ID = "_id";
const HEARTBEAT = 50000; const HEARTBEAT = 50000;
const ALL_DOCS = "_all_docs?include_docs=true"; const ALL_DOCS = "_all_docs?include_docs=true";
export default class CouchObjectProvider { class CouchObjectProvider {
constructor(openmct, options, namespace) { constructor(openmct, options, namespace) {
options = this._normalize(options); options = this._normalize(options);
this.openmct = openmct; this.openmct = openmct;
@ -74,13 +74,6 @@ export default class CouchObjectProvider {
if (event.data.type === 'connection') { if (event.data.type === 'connection') {
this.changesFeedSharedWorkerConnectionId = event.data.connectionId; this.changesFeedSharedWorkerConnectionId = event.data.connectionId;
} else { } else {
const error = event.data.error;
if (error && Object.keys(this.observers).length > 0) {
this.observeObjectChanges();
return;
}
let objectChanges = event.data.objectChanges; let objectChanges = event.data.objectChanges;
objectChanges.identifier = { objectChanges.identifier = {
namespace: this.namespace, namespace: this.namespace,
@ -126,11 +119,12 @@ export default class CouchObjectProvider {
} }
return fetch(this.url + '/' + subPath, fetchOptions) return fetch(this.url + '/' + subPath, fetchOptions)
.then(response => response.json()) .then((response) => {
.then(function (response) { if (response.status === CouchObjectProvider.HTTP_CONFLICT) {
return response; throw new this.openmct.objects.errors.Conflict(`Conflict persisting ${fetchOptions.body.name}`);
}, function () { }
return undefined;
return response.json();
}); });
} }
@ -561,12 +555,18 @@ export default class CouchObjectProvider {
let intermediateResponse = this.getIntermediateResponse(); let intermediateResponse = this.getIntermediateResponse();
const key = model.identifier.key; const key = model.identifier.key;
this.enqueueObject(key, model, intermediateResponse); this.enqueueObject(key, model, intermediateResponse);
this.objectQueue[key].pending = true; if (!this.objectQueue[key].pending) {
const queued = this.objectQueue[key].dequeue(); this.objectQueue[key].pending = true;
let document = new CouchDocument(key, queued.model); const queued = this.objectQueue[key].dequeue();
this.request(key, "PUT", document).then((response) => { let document = new CouchDocument(key, queued.model);
this.checkResponse(response, queued.intermediateResponse, key); this.request(key, "PUT", document).then((response) => {
}); console.log('create check response', key);
this.checkResponse(response, queued.intermediateResponse, key);
}).catch(error => {
queued.intermediateResponse.reject(error);
this.objectQueue[key].pending = false;
});
}
return intermediateResponse.promise; return intermediateResponse.promise;
} }
@ -581,6 +581,9 @@ export default class CouchObjectProvider {
let document = new CouchDocument(key, queued.model, this.objectQueue[key].rev); let document = new CouchDocument(key, queued.model, this.objectQueue[key].rev);
this.request(key, "PUT", document).then((response) => { this.request(key, "PUT", document).then((response) => {
this.checkResponse(response, queued.intermediateResponse, key); this.checkResponse(response, queued.intermediateResponse, key);
}).catch((error) => {
queued.intermediateResponse.reject(error);
this.objectQueue[key].pending = false;
}); });
} }
} }
@ -594,3 +597,7 @@ export default class CouchObjectProvider {
return intermediateResponse.promise; return intermediateResponse.promise;
} }
} }
CouchObjectProvider.HTTP_CONFLICT = 409;
export default CouchObjectProvider;

View File

@ -25,7 +25,9 @@ import Vue from 'vue';
export default function PlanViewProvider(openmct) { export default function PlanViewProvider(openmct) {
function isCompactView(objectPath) { function isCompactView(objectPath) {
return objectPath.find(object => object.type === 'time-strip') !== undefined; let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
} }
return { return {

View File

@ -30,6 +30,7 @@ describe('the plugin', function () {
let child; let child;
let openmct; let openmct;
let appHolder; let appHolder;
let originalRouterPath;
beforeEach((done) => { beforeEach((done) => {
appHolder = document.createElement('div'); appHolder = document.createElement('div');
@ -49,11 +50,15 @@ describe('the plugin', function () {
child.style.height = '480px'; child.style.height = '480px';
element.appendChild(child); element.appendChild(child);
originalRouterPath = openmct.router.path;
openmct.on('start', done); openmct.on('start', done);
openmct.start(appHolder); openmct.start(appHolder);
}); });
afterEach(() => { afterEach(() => {
openmct.router.path = originalRouterPath;
return resetApplicationState(openmct); return resetApplicationState(openmct);
}); });
@ -78,8 +83,9 @@ describe('the plugin', function () {
id: "test-object", id: "test-object",
type: "plan" type: "plan"
}; };
openmct.router.path = [testViewObject];
const applicableViews = openmct.objectViews.get(testViewObject, []); const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]);
let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view'); let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');
expect(planView).toBeDefined(); expect(planView).toBeDefined();
}); });
@ -137,7 +143,9 @@ describe('the plugin', function () {
} }
}; };
const applicableViews = openmct.objectViews.get(planDomainObject, []); openmct.router.path = [planDomainObject];
const applicableViews = openmct.objectViews.get(planDomainObject, [planDomainObject]);
planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view'); planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');
let view = planView.view(planDomainObject, mockObjectPath); let view = planView.view(planDomainObject, mockObjectPath);
view.show(child, true); view.show(child, true);

View File

@ -35,12 +35,15 @@
:tick-width="tickWidth" :tick-width="tickWidth"
:single-series="seriesModels.length === 1" :single-series="seriesModels.length === 1"
:series-model="seriesModels[0]" :series-model="seriesModels[0]"
:style="{
left: (plotWidth - tickWidth) + 'px'
}"
@yKeyChanged="setYAxisKey" @yKeyChanged="setYAxisKey"
@tickWidthChanged="onTickWidthChange" @tickWidthChanged="onTickWidthChange"
/> />
<div class="gl-plot-wrapper-display-area-and-x-axis" <div class="gl-plot-wrapper-display-area-and-x-axis"
:style="{ :style="{
left: (tickWidth + 20) + 'px' left: (plotWidth + 20) + 'px'
}" }"
> >
@ -219,7 +222,8 @@ export default {
isRealTime: this.openmct.time.clock() !== undefined, isRealTime: this.openmct.time.clock() !== undefined,
loaded: false, loaded: false,
isTimeOutOfSync: false, isTimeOutOfSync: false,
showLimitLineLabels: undefined showLimitLineLabels: undefined,
isFrozenOnMouseDown: false
}; };
}, },
computed: { computed: {
@ -235,11 +239,9 @@ export default {
} else { } else {
return 'plot-legend-collapsed'; return 'plot-legend-collapsed';
} }
} },
}, plotWidth() {
watch: { return this.plotTickWidth || this.tickWidth;
plotTickWidth(newTickWidth) {
this.onTickWidthChange(newTickWidth, true);
} }
}, },
mounted() { mounted() {
@ -336,6 +338,11 @@ export default {
}, },
loadSeriesData(series) { loadSeriesData(series) {
//this check ensures that duplicate requests don't happen on load
if (!this.timeContext) {
return;
}
if (this.$parent.$refs.plotWrapper.offsetWidth === 0) { if (this.$parent.$refs.plotWrapper.offsetWidth === 0) {
this.scheduleLoad(series); this.scheduleLoad(series);
@ -345,9 +352,12 @@ export default {
this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth; this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;
this.startLoading(); this.startLoading();
const bounds = this.timeContext.bounds();
const options = { const options = {
size: this.$parent.$refs.plotWrapper.offsetWidth, size: this.$parent.$refs.plotWrapper.offsetWidth,
domain: this.config.xAxis.get('key') domain: this.config.xAxis.get('key'),
start: bounds.start,
end: bounds.end
}; };
series.load(options) series.load(options)
@ -356,9 +366,10 @@ export default {
loadMoreData(range, purge) { loadMoreData(range, purge) {
this.config.series.forEach(plotSeries => { this.config.series.forEach(plotSeries => {
this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;
this.startLoading(); this.startLoading();
plotSeries.load({ plotSeries.load({
size: this.$parent.$refs.plotWrapper.offsetWidth, size: this.offsetWidth,
start: range.min, start: range.min,
end: range.max, end: range.max,
domain: this.config.xAxis.get('key') domain: this.config.xAxis.get('key')
@ -593,7 +604,8 @@ export default {
} }
} }
this.$emit('plotTickWidth', this.tickWidth); const id = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.$emit('plotTickWidth', this.tickWidth, id);
}, },
trackMousePosition(event) { trackMousePosition(event) {
@ -686,6 +698,11 @@ export default {
this.listenTo(window, 'mouseup', this.onMouseUp, this); this.listenTo(window, 'mouseup', this.onMouseUp, this);
this.listenTo(window, 'mousemove', this.trackMousePosition, this); this.listenTo(window, 'mousemove', this.trackMousePosition, this);
// track frozen state on mouseDown to be read on mouseUp
const isFrozen = this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
this.isFrozenOnMouseDown = isFrozen;
if (event.altKey) { if (event.altKey) {
return this.startPan(event); return this.startPan(event);
} else { } else {
@ -706,7 +723,14 @@ export default {
} }
if (this.marquee) { if (this.marquee) {
return this.endMarquee(event); this.endMarquee(event);
}
// resume the plot if no pan, zoom, or drag action is taken
// needs to follow endMarquee so that plotHistory is pruned
const isAction = Boolean(this.plotHistory.length);
if (!isAction && !this.isFrozenOnMouseDown) {
return this.play();
} }
}, },

View File

@ -44,7 +44,9 @@ export default function PlotViewProvider(openmct) {
} }
function isCompactView(objectPath) { function isCompactView(objectPath) {
return objectPath.find(object => object.type === 'time-strip'); let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
} }
return { return {

View File

@ -1,346 +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.
*****************************************************************************/
import BarGraphCompositionPolicy from "./BarGraphCompositionPolicy";
import { createOpenMct } from "utils/testing";
describe("The bar graph composition policy", () => {
let openmct;
const mockMetaDataWithNoRangeHints = {
"period": 10,
"amplitude": 1,
"offset": 0,
"dataRateInHz": 1,
"phase": 0,
"randomness": 0,
valuesForHints: () => {
return [];
},
values: [
{
"key": "name",
"name": "Name",
"format": "string"
},
{
"key": "utc",
"name": "Time",
"format": "utc",
"hints": {
"domain": 1,
"priority": 1
},
"source": "utc"
}
]
};
const mockMetaDataWithRangeHints = {
"period": 10,
"amplitude": 1,
"offset": 0,
"dataRateInHz": 1,
"phase": 0,
"randomness": 0,
"wavelength": 0,
valuesForHints: () => {
return [
{
"key": "sin",
"name": "Sine",
"unit": "Hz",
"formatString": "%0.2f",
"hints": {
"range": 1,
"priority": 4
},
"source": "sin"
},
{
"key": "cos",
"name": "Cosine",
"unit": "deg",
"formatString": "%0.2f",
"hints": {
"range": 2,
"priority": 5
},
"source": "cos"
}
];
},
values: [
{
"key": "name",
"name": "Name",
"format": "string",
"source": "name",
"hints": {
"priority": 0
}
},
{
"key": "utc",
"name": "Time",
"format": "utc",
"hints": {
"domain": 1,
"priority": 1
},
"source": "utc"
},
{
"key": "yesterday",
"name": "Yesterday",
"format": "utc",
"hints": {
"domain": 2,
"priority": 2
},
"source": "yesterday"
},
{
"key": "sin",
"name": "Sine",
"unit": "Hz",
"formatString": "%0.2f",
"hints": {
"range": 1,
"spectralAttribute": true
},
"source": "sin"
},
{
"key": "cos",
"name": "Cosine",
"unit": "deg",
"formatString": "%0.2f",
"hints": {
"range": 2,
"priority": 5
},
"source": "cos"
}
]
};
beforeEach(() => {
openmct = createOpenMct();
const mockTypeDef = {
telemetry: mockMetaDataWithRangeHints
};
const mockTypeService = {
getType: () => {
return {
typeDef: mockTypeDef
};
}
};
openmct.$injector = {
get: () => {
return mockTypeService;
}
};
openmct.telemetry.isTelemetryObject = function (domainObject) {
return true;
};
});
it("exists", () => {
expect(BarGraphCompositionPolicy(openmct).allow).toBeDefined();
});
xit("allow composition for telemetry that provides/supports bar graph meta data", () => {
const parent = {
"composition": [],
"configuration": {},
"name": "Some Bar Graph",
"type": "telemetry.plot.bar-graph",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const child = {
"telemetry": {
"period": 10,
"amplitude": 1,
"offset": 0,
"dataRateInHz": 1,
"phase": 0,
"randomness": 0
},
"name": "Unnamed Sine Wave Generator",
"type": "generator",
"location": "mine",
"modified": 1630399715531,
"persisted": 1630399715531,
"identifier": {
"namespace": "",
"key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c"
}
};
expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(true);
});
it("allows composition for telemetry that contain at least one range", () => {
const mockTypeDef = {
telemetry: mockMetaDataWithRangeHints
};
const mockTypeService = {
getType: () => {
return {
typeDef: mockTypeDef
};
}
};
openmct.$injector = {
get: () => {
return mockTypeService;
}
};
const parent = {
"composition": [],
"configuration": {},
"name": "Some Bar Graph",
"type": "telemetry.plot.bar-graph",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const child = {
"telemetry": {
"period": 10,
"amplitude": 1,
"offset": 0,
"dataRateInHz": 1,
"phase": 0,
"randomness": 0
},
"name": "Unnamed Sine Wave Generator",
"type": "generator",
"location": "mine",
"modified": 1630399715531,
"persisted": 1630399715531,
"identifier": {
"namespace": "",
"key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c"
}
};
expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(true);
});
it("disallows composition for telemetry that don't contain any range hints", () => {
const mockTypeDef = {
telemetry: mockMetaDataWithNoRangeHints
};
const mockTypeService = {
getType: () => {
return {
typeDef: mockTypeDef
};
}
};
openmct.$injector = {
get: () => {
return mockTypeService;
}
};
const parent = {
"composition": [],
"configuration": {},
"name": "Some Bar Graph",
"type": "telemetry.plot.bar-graph",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const child = {
"telemetry": {
"period": 10,
"amplitude": 1,
"offset": 0,
"dataRateInHz": 1,
"phase": 0,
"randomness": 0
},
"name": "Unnamed Sine Wave Generator",
"type": "generator",
"location": "mine",
"modified": 1630399715531,
"persisted": 1630399715531,
"identifier": {
"namespace": "",
"key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c"
}
};
expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(false);
});
it("passthrough for composition for non bar graph plots", () => {
const parent = {
"composition": [],
"configuration": {},
"name": "Some Stacked Plot",
"type": "telemetry.plot.stacked",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const child = {
"telemetry": {
"period": 10,
"amplitude": 1,
"offset": 0,
"dataRateInHz": 1,
"phase": 0,
"randomness": 0
},
"name": "Unnamed Sine Wave Generator",
"type": "generator",
"location": "mine",
"modified": 1630399715531,
"persisted": 1630399715531,
"identifier": {
"namespace": "",
"key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c"
}
};
expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(true);
});
});

View File

@ -82,12 +82,17 @@ export default class PlotSeries extends Model {
.openmct .openmct
.telemetry .telemetry
.getMetadata(options.domainObject); .getMetadata(options.domainObject);
this.formats = options this.formats = options
.openmct .openmct
.telemetry .telemetry
.getFormatMap(this.metadata); .getFormatMap(this.metadata);
const range = this.metadata.valuesForHints(['range'])[0]; //if the object is missing or doesn't have metadata for some reason
let range = {};
if (this.metadata) {
range = this.metadata.valuesForHints(['range'])[0];
}
return { return {
name: options.domainObject.name, name: options.domainObject.name,
@ -191,7 +196,10 @@ export default class PlotSeries extends Model {
.uniq(true, point => [this.getXVal(point), this.getYVal(point)].join()) .uniq(true, point => [this.getXVal(point), this.getYVal(point)].join())
.value(); .value();
this.reset(newPoints); this.reset(newPoints);
}.bind(this)); }.bind(this))
.catch((error) => {
console.warn('Error fetching data', error);
});
/* eslint-enable you-dont-need-lodash-underscore/concat */ /* eslint-enable you-dont-need-lodash-underscore/concat */
} }
/** /**
@ -199,7 +207,9 @@ export default class PlotSeries extends Model {
*/ */
onXKeyChange(xKey) { onXKeyChange(xKey) {
const format = this.formats[xKey]; const format = this.formats[xKey];
this.getXVal = format.parse.bind(format); if (format) {
this.getXVal = format.parse.bind(format);
}
} }
/** /**
* Update y formatter on change, default to stepAfter interpolation if * Update y formatter on change, default to stepAfter interpolation if

View File

@ -23,8 +23,8 @@ import _ from 'lodash';
import PlotSeries from "./PlotSeries"; import PlotSeries from "./PlotSeries";
import Collection from "./Collection"; import Collection from "./Collection";
import Color from "../lib/Color"; import Color from "@/ui/color/Color";
import ColorPalette from "../lib/ColorPalette"; import ColorPalette from "@/ui/color/ColorPalette";
export default class SeriesCollection extends Collection { export default class SeriesCollection extends Collection {

View File

@ -184,7 +184,7 @@ export default class YAxisModel extends Model {
this.set('values', yMetadata.values); this.set('values', yMetadata.values);
if (!label) { if (!label) {
const labelName = series.map(function (s) { const labelName = series.map(function (s) {
return s.metadata.value(s.get('yKey')).name; return s.metadata ? s.metadata.value(s.get('yKey')).name : '';
}).reduce(function (a, b) { }).reduce(function (a, b) {
if (a === undefined) { if (a === undefined) {
return b; return b;
@ -204,7 +204,7 @@ export default class YAxisModel extends Model {
} }
const labelUnits = series.map(function (s) { const labelUnits = series.map(function (s) {
return s.metadata.value(s.get('yKey')).units; return s.metadata ? s.metadata.value(s.get('yKey')).units : '';
}).reduce(function (a, b) { }).reduce(function (a, b) {
if (a === undefined) { if (a === undefined) {
return b; return b;

View File

@ -79,7 +79,7 @@
</template> </template>
<script> <script>
import ColorSwatch from "@/plugins/plot/ColorSwatch.vue"; import ColorSwatch from '@/ui/color/ColorSwatch.vue';
export default { export default {
components: { components: {

View File

@ -129,7 +129,7 @@
</template> </template>
<script> <script>
import ColorSwatch from '../../ColorSwatch.vue'; import ColorSwatch from '@/ui/color/ColorSwatch.vue';
import { MARKER_SHAPES } from "../../draw/MarkerShapes"; import { MARKER_SHAPES } from "../../draw/MarkerShapes";
import { objectPath, validate, coerce } from "./formUtil"; import { objectPath, validate, coerce } from "./formUtil";
import _ from 'lodash'; import _ from 'lodash';

View File

@ -25,7 +25,9 @@ import Vue from 'vue';
export default function OverlayPlotViewProvider(openmct) { export default function OverlayPlotViewProvider(openmct) {
function isCompactView(objectPath) { function isCompactView(objectPath) {
return objectPath.find(object => object.type === 'time-strip'); let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
} }
return { return {

View File

@ -19,18 +19,12 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import { BAR_GRAPH_KEY } from './barGraph/BarGraphConstants';
import PlotViewProvider from './PlotViewProvider'; import PlotViewProvider from './PlotViewProvider';
import SpectralPlotViewProvider from './spectralPlot/SpectralPlotViewProvider';
import BarGraphViewProvider from './barGraph/BarGraphViewProvider';
import OverlayPlotViewProvider from './overlayPlot/OverlayPlotViewProvider'; import OverlayPlotViewProvider from './overlayPlot/OverlayPlotViewProvider';
import StackedPlotViewProvider from './stackedPlot/StackedPlotViewProvider'; import StackedPlotViewProvider from './stackedPlot/StackedPlotViewProvider';
import PlotsInspectorViewProvider from './inspector/PlotsInspectorViewProvider'; import PlotsInspectorViewProvider from './inspector/PlotsInspectorViewProvider';
import BarGraphInspectorViewProvider from './barGraph/inspector/BarGraphInspectorViewProvider';
import OverlayPlotCompositionPolicy from './overlayPlot/OverlayPlotCompositionPolicy'; import OverlayPlotCompositionPolicy from './overlayPlot/OverlayPlotCompositionPolicy';
import StackedPlotCompositionPolicy from './stackedPlot/StackedPlotCompositionPolicy'; import StackedPlotCompositionPolicy from './stackedPlot/StackedPlotCompositionPolicy';
import SpectralPlotCompositionPolicy from './spectralPlot/SpectralPlotCompositionPolicy';
import BarGraphCompositionPolicy from './barGraph/BarGraphCompositionPolicy';
export default function () { export default function () {
return function install(openmct) { return function install(openmct) {
@ -64,48 +58,15 @@ export default function () {
}, },
priority: 890 priority: 890
}); });
openmct.types.addType('telemetry.plot.spectral', {
key: "telemetry.plot.spectral",
name: "Spectral Plot",
cssClass: "icon-plot-stacked",
description: "View Spectra on Y Axes with non-time domain on the X axis. Can be added to Display Layouts.",
//Temporarily disabling spectral plots
creatable: false,
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {};
},
priority: 890
});
openmct.types.addType(BAR_GRAPH_KEY, {
key: BAR_GRAPH_KEY,
name: "Bar Graph",
cssClass: "icon-bar-chart",
description: "View data as a bar graph. Can be added to Display Layouts.",
creatable: true,
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {
plotType: 'bar'
};
},
priority: 891
});
openmct.objectViews.addProvider(new StackedPlotViewProvider(openmct)); openmct.objectViews.addProvider(new StackedPlotViewProvider(openmct));
openmct.objectViews.addProvider(new OverlayPlotViewProvider(openmct)); openmct.objectViews.addProvider(new OverlayPlotViewProvider(openmct));
openmct.objectViews.addProvider(new PlotViewProvider(openmct)); openmct.objectViews.addProvider(new PlotViewProvider(openmct));
openmct.objectViews.addProvider(new SpectralPlotViewProvider(openmct));
openmct.objectViews.addProvider(new BarGraphViewProvider(openmct));
openmct.inspectorViews.addProvider(new PlotsInspectorViewProvider(openmct)); openmct.inspectorViews.addProvider(new PlotsInspectorViewProvider(openmct));
openmct.inspectorViews.addProvider(new BarGraphInspectorViewProvider(openmct));
openmct.composition.addPolicy(new OverlayPlotCompositionPolicy(openmct).allow); openmct.composition.addPolicy(new OverlayPlotCompositionPolicy(openmct).allow);
openmct.composition.addPolicy(new StackedPlotCompositionPolicy(openmct).allow); openmct.composition.addPolicy(new StackedPlotCompositionPolicy(openmct).allow);
openmct.composition.addPolicy(new SpectralPlotCompositionPolicy(openmct).allow);
openmct.composition.addPolicy(new BarGraphCompositionPolicy(openmct).allow);
}; };
} }

View File

@ -24,12 +24,10 @@ import {createMouseEvent, createOpenMct, resetApplicationState, spyOnBuiltins} f
import PlotVuePlugin from "./plugin"; import PlotVuePlugin from "./plugin";
import Vue from "vue"; import Vue from "vue";
import StackedPlot from "./stackedPlot/StackedPlot.vue"; import StackedPlot from "./stackedPlot/StackedPlot.vue";
// import SpectralPlot from "./spectralPlot/SpectralPlot.vue";
import configStore from "./configuration/ConfigStore"; import configStore from "./configuration/ConfigStore";
import EventEmitter from "EventEmitter"; import EventEmitter from "EventEmitter";
import PlotOptions from "./inspector/PlotOptions.vue"; import PlotOptions from "./inspector/PlotOptions.vue";
import PlotConfigurationModel from "./configuration/PlotConfigurationModel"; import PlotConfigurationModel from "./configuration/PlotConfigurationModel";
import { BAR_GRAPH_VIEW, BAR_GRAPH_KEY } from './barGraph/BarGraphConstants';
describe("the plugin", function () { describe("the plugin", function () {
let element; let element;
@ -143,6 +141,7 @@ describe("the plugin", function () {
spyOn(window, 'ResizeObserver').and.returnValue({ spyOn(window, 'ResizeObserver').and.returnValue({
observe() {}, observe() {},
unobserve() {},
disconnect() {} disconnect() {}
}); });
@ -315,37 +314,6 @@ describe("the plugin", function () {
expect(plotView).toBeDefined(); expect(plotView).toBeDefined();
}); });
it("provides a spectral plot view for objects with telemetry", () => {
const testTelemetryObject = {
id: "test-object",
type: "telemetry.plot.spectral",
telemetry: {
values: [{
key: "a-very-fine-key"
}]
}
};
const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-spectral");
expect(plotView).toBeDefined();
});
it("provides a spectral aggregate plot view for objects with telemetry", () => {
const testTelemetryObject = {
id: "test-object",
type: BAR_GRAPH_KEY,
telemetry: {
values: [{
key: "lots-of-aggregate-telemetry"
}]
}
};
const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
let plotView = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW);
expect(plotView).toBeDefined();
});
}); });
describe("The single plot view", () => { describe("The single plot view", () => {
@ -402,6 +370,10 @@ describe("the plugin", function () {
return Vue.nextTick(); return Vue.nextTick();
}); });
it("Makes only one request for telemetry on load", () => {
expect(openmct.telemetry.request).toHaveBeenCalledTimes(1);
});
it("Renders a collapsed legend for every telemetry", () => { it("Renders a collapsed legend for every telemetry", () => {
let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name"); let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
expect(legend.length).toBe(1); expect(legend.length).toBe(1);
@ -476,6 +448,64 @@ describe("the plugin", function () {
}); });
}); });
describe('resume actions on errant click', () => {
beforeEach(() => {
openmct.time.clock('local', {
start: -1000,
end: 100
});
return Vue.nextTick();
});
it("clicking the plot view without movement resumes the plot while active", async () => {
const pauseEl = element.querySelectorAll(".c-button-set .icon-pause");
// if the pause button is present, the chart is running
expect(pauseEl.length).toBe(1);
// simulate an errant mouse click
// the second item is the canvas we need to use
const canvas = element.querySelectorAll("canvas")[1];
const mouseDownEvent = new MouseEvent('mousedown');
const mouseUpEvent = new MouseEvent('mouseup');
canvas.dispatchEvent(mouseDownEvent);
// mouseup event is bound to the window
window.dispatchEvent(mouseUpEvent);
await Vue.nextTick();
const pauseElAfterClick = element.querySelectorAll(".c-button-set .icon-pause");
console.log('pauseElAfterClick', pauseElAfterClick);
expect(pauseElAfterClick.length).toBe(1);
});
it("clicking the plot view without movement leaves the plot paused", async () => {
const pauseEl = element.querySelector(".c-button-set .icon-pause");
// pause the plot
pauseEl.dispatchEvent(createMouseEvent('click'));
await Vue.nextTick();
const playEl = element.querySelectorAll('.c-button-set .is-paused');
expect(playEl.length).toBe(1);
// simulate an errant mouse click
// the second item is the canvas we need to use
const canvas = element.querySelectorAll("canvas")[1];
const mouseDownEvent = new MouseEvent('mousedown');
const mouseUpEvent = new MouseEvent('mouseup');
canvas.dispatchEvent(mouseDownEvent);
// mouseup event is bound to the window
window.dispatchEvent(mouseUpEvent);
await Vue.nextTick();
const playElAfterChartClick = element.querySelectorAll(".c-button-set .is-paused");
expect(playElAfterChartClick.length).toBe(1);
});
});
describe('controls in time strip view', () => { describe('controls in time strip view', () => {
it('zoom controls are hidden', () => { it('zoom controls are hidden', () => {
@ -496,146 +526,6 @@ describe("the plugin", function () {
}); });
}); });
/*
* disabling this until we develop the plot view
describe("The spectral plot view", () => {
let testTelemetryObject;
// eslint-disable-next-line no-unused-vars
let testTelemetryObject2;
// eslint-disable-next-line no-unused-vars
let config;
let spectralPlotObject;
let component;
let mockComposition;
// eslint-disable-next-line no-unused-vars
let plotViewComponentObject;
beforeEach(() => {
const getFunc = openmct.$injector.get;
spyOn(openmct.$injector, "get")
.withArgs("exportImageService").and.returnValue({
exportPNG: () => {},
exportJPG: () => {}
})
.and.callFake(getFunc);
spectralPlotObject = {
identifier: {
namespace: "",
key: "test-spectral-plot"
},
type: "telemetry.plot.spectral",
name: "Test Spectral Plot"
};
testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
testTelemetryObject2 = {
identifier: {
namespace: "",
key: "test-object2"
},
type: "test-object",
name: "Test Object2",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "wavelength",
name: "Wavelength",
hints: {
range: 1
}
}, {
key: "some-other-key2",
name: "Another attribute2",
hints: {
range: 2
}
}]
}
};
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testTelemetryObject);
return [testTelemetryObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
let viewContainer = document.createElement("div");
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
SpectralPlot
},
provide: {
openmct: openmct,
domainObject: spectralPlotObject,
composition: openmct.composition.get(spectralPlotObject)
},
template: "<spectral-plot></spectral-plot>"
});
cleanupFirst.push(() => {
component.$destroy();
component = undefined;
});
return telemetryPromise
.then(Vue.nextTick())
.then(() => {
plotViewComponentObject = component.$root.$children[0];
const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier);
config = configStore.get(configId);
});
});
it("Renders a collapsed legend for every telemetry", () => {
let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
expect(legend.length).toBe(1);
expect(legend[0].innerHTML).toEqual("Test Object");
});
}); */
describe("The stacked plot view", () => { describe("The stacked plot view", () => {
let testTelemetryObject; let testTelemetryObject;
let testTelemetryObject2; let testTelemetryObject2;
@ -1162,42 +1052,11 @@ describe("the plugin", function () {
const yAxisProperties = editOptionsEl.querySelectorAll("div.grid-properties:first-of-type .l-inspector-part"); const yAxisProperties = editOptionsEl.querySelectorAll("div.grid-properties:first-of-type .l-inspector-part");
expect(yAxisProperties.length).toEqual(3); expect(yAxisProperties.length).toEqual(3);
}); });
});
});
describe("the spectral plot", () => { it('renders color palette options', () => {
const mockObject = { const colorSwatch = editOptionsEl.querySelector(".c-click-swatch");
name: 'A Very Nice Spectral Plot', expect(colorSwatch).toBeDefined();
key: 'telemetry.plot.spectral', });
creatable: true
};
it('defines a spectral plot object type with the correct key', () => {
const objectDef = openmct.types.get('telemetry.plot.spectral').definition;
expect(objectDef.key).toEqual(mockObject.key);
});
xit('is creatable', () => {
const objectDef = openmct.types.get('telemetry.plot.spectral').definition;
expect(objectDef.creatable).toEqual(mockObject.creatable);
});
});
describe("the aggregate spectral plot", () => {
const mockObject = {
name: 'An Even Nicer Aggregate Spectral Plot',
key: BAR_GRAPH_KEY,
creatable: true
};
it('defines a spectral plot object type with the correct key', () => {
const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition;
expect(objectDef.key).toEqual(mockObject.key);
});
it('is creatable', () => {
const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition;
expect(objectDef.creatable).toEqual(mockObject.creatable);
}); });
}); });
}); });

View File

@ -1,36 +0,0 @@
export default function SpectralPlotCompositionPolicy(openmct) {
function hasSpectralDomainAndRange(metadata) {
const rangeValues = metadata.valuesForHints(['range']);
const domainValues = metadata.valuesForHints(['domain']);
const containsSomeSpectralData = domainValues.some(value => {
return ((value.key === 'wavelength') || (value.key === 'frequency'));
});
return rangeValues.length > 0
&& domainValues.length > 0
&& containsSomeSpectralData;
}
function hasSpectralTelemetry(domainObject) {
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {
return false;
}
let metadata = openmct.telemetry.getMetadata(domainObject);
return metadata.values().length > 0 && hasSpectralDomainAndRange(metadata);
}
return {
allow: function (parent, child) {
if ((parent.type === 'telemetry.plot.spectral')
&& ((child.type !== 'telemetry.plot.overlay') && (hasSpectralTelemetry(child) === false))
) {
return false;
}
return true;
}
};
}

View File

@ -1,75 +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.
*****************************************************************************/
import SpectralView from './SpectralView.vue';
import Vue from 'vue';
export default function SpectralPlotViewProvider(openmct) {
function isCompactView(objectPath) {
return objectPath.find(object => object.type === 'time-strip');
}
return {
key: 'plot-spectral',
name: 'Spectral Plot',
cssClass: 'icon-telemetry',
canView(domainObject, objectPath) {
return domainObject && domainObject.type === 'telemetry.plot.spectral';
},
canEdit(domainObject, objectPath) {
return domainObject && domainObject.type === 'telemetry.plot.spectral';
},
view: function (domainObject, objectPath) {
let component;
return {
show: function (element) {
let isCompact = isCompactView(objectPath);
component = new Vue({
el: element,
components: {
SpectralView
},
provide: {
openmct,
domainObject
},
data() {
return {
options: {
compact: isCompact
}
};
},
template: '<spectral-view :options="options"></spectral-view>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
}
};
}

View File

@ -1,13 +0,0 @@
<template>
<div>
</div>
</template>
<script>
export default {
inject: ['openmct', 'domainObject']
};
</script>

View File

@ -90,7 +90,8 @@ export default {
cursorGuide: false, cursorGuide: false,
gridLines: true, gridLines: true,
loading: false, loading: false,
compositionObjects: [] compositionObjects: [],
tickWidthMap: {}
}; };
}, },
computed: { computed: {
@ -106,8 +107,6 @@ export default {
this.imageExporter = new ImageExporter(this.openmct); this.imageExporter = new ImageExporter(this.openmct);
this.tickWidthMap = {};
this.composition.on('add', this.addChild); this.composition.on('add', this.addChild);
this.composition.on('remove', this.removeChild); this.composition.on('remove', this.removeChild);
this.composition.on('reorder', this.compositionReorder); this.composition.on('reorder', this.compositionReorder);
@ -127,13 +126,15 @@ export default {
addChild(child) { addChild(child) {
const id = this.openmct.objects.makeKeyString(child.identifier); const id = this.openmct.objects.makeKeyString(child.identifier);
this.tickWidthMap[id] = 0; this.$set(this.tickWidthMap, id, 0);
this.compositionObjects.push(child); this.compositionObjects.push(child);
}, },
removeChild(childIdentifier) { removeChild(childIdentifier) {
const id = this.openmct.objects.makeKeyString(childIdentifier); const id = this.openmct.objects.makeKeyString(childIdentifier);
delete this.tickWidthMap[id];
this.$delete(this.tickWidthMap, id);
const childObj = this.compositionObjects.filter((c) => { const childObj = this.compositionObjects.filter((c) => {
const identifier = this.openmct.objects.makeKeyString(c.identifier); const identifier = this.openmct.objects.makeKeyString(c.identifier);
@ -191,14 +192,7 @@ export default {
return; return;
} }
//update the tickWidth for this plotId, the computed max tick width of the stacked plot will be cascaded down this.$set(this.tickWidthMap, plotId, width);
//TODO: Might need to do this using $set
this.tickWidthMap[plotId] = Math.max(width, this.tickWidthMap[plotId]);
// const newTickWidth = Math.max(...Object.values(this.tickWidthMap));
// if (newTickWidth !== tickWidth || width !== tickWidth) {
// tickWidth = newTickWidth;
// $scope.$broadcast('plot:tickWidth', tickWidth);
// }
} }
} }
}; };

View File

@ -25,7 +25,9 @@ import Vue from 'vue';
export default function StackedPlotViewProvider(openmct) { export default function StackedPlotViewProvider(openmct) {
function isCompactView(objectPath) { function isCompactView(objectPath) {
return objectPath.find(object => object.type === 'time-strip'); let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
} }
return { return {

View File

@ -36,6 +36,7 @@ define([
'./URLIndicatorPlugin/URLIndicatorPlugin', './URLIndicatorPlugin/URLIndicatorPlugin',
'./telemetryMean/plugin', './telemetryMean/plugin',
'./plot/plugin', './plot/plugin',
'./charts/plugin',
'./telemetryTable/plugin', './telemetryTable/plugin',
'./staticRootPlugin/plugin', './staticRootPlugin/plugin',
'./notebook/plugin', './notebook/plugin',
@ -87,6 +88,7 @@ define([
URLIndicatorPlugin, URLIndicatorPlugin,
TelemetryMean, TelemetryMean,
PlotPlugin, PlotPlugin,
ChartPlugin,
TelemetryTablePlugin, TelemetryTablePlugin,
StaticRootPlugin, StaticRootPlugin,
Notebook, Notebook,
@ -189,6 +191,7 @@ define([
plugins.ExampleImagery = ExampleImagery; plugins.ExampleImagery = ExampleImagery;
plugins.ImageryPlugin = ImageryPlugin; plugins.ImageryPlugin = ImageryPlugin;
plugins.Plot = PlotPlugin.default; plugins.Plot = PlotPlugin.default;
plugins.Chart = ChartPlugin.default;
plugins.TelemetryTable = TelemetryTablePlugin; plugins.TelemetryTable = TelemetryTablePlugin;
plugins.SummaryWidget = SummaryWidget; plugins.SummaryWidget = SummaryWidget;

View File

@ -7,7 +7,8 @@ define([
'./eventHelpers', './eventHelpers',
'objectUtils', 'objectUtils',
'lodash', 'lodash',
'zepto' 'zepto',
'@braintree/sanitize-url'
], function ( ], function (
widgetTemplate, widgetTemplate,
Rule, Rule,
@ -17,7 +18,8 @@ define([
eventHelpers, eventHelpers,
objectUtils, objectUtils,
_, _,
$ $,
urlSanitizeLib
) { ) {
//default css configuration for new rules //default css configuration for new rules
@ -88,7 +90,7 @@ define([
function toggleTestData() { function toggleTestData() {
self.outerWrapper.toggleClass('expanded-widget-test-data'); self.outerWrapper.toggleClass('expanded-widget-test-data');
self.toggleTestDataControl.toggleClass('c-disclosure-triangle--expanded'); self.toggleTestDataControl.toggleClass('c-disclosure-triangle--expanded');
} }
this.listenTo(this.toggleTestDataControl, 'click', toggleTestData); this.listenTo(this.toggleTestDataControl, 'click', toggleTestData);
@ -99,7 +101,7 @@ define([
function toggleRules() { function toggleRules() {
self.outerWrapper.toggleClass('expanded-widget-rules'); self.outerWrapper.toggleClass('expanded-widget-rules');
self.toggleRulesControl.toggleClass('c-disclosure-triangle--expanded'); self.toggleRulesControl.toggleClass('c-disclosure-triangle--expanded');
} }
this.listenTo(this.toggleRulesControl, 'click', toggleRules); this.listenTo(this.toggleRulesControl, 'click', toggleRules);
@ -114,7 +116,7 @@ define([
*/ */
SummaryWidget.prototype.addHyperlink = function (url, openNewTab) { SummaryWidget.prototype.addHyperlink = function (url, openNewTab) {
if (url) { if (url) {
this.widgetButton.attr('href', url); this.widgetButton.attr('href', urlSanitizeLib.sanitizeUrl(url));
} else { } else {
this.widgetButton.removeAttr('href'); this.widgetButton.removeAttr('href');
} }
@ -317,7 +319,7 @@ define([
expanded: 'true' expanded: 'true'
}; };
} }
ruleConfig = this.domainObject.configuration.ruleConfigById[ruleId]; ruleConfig = this.domainObject.configuration.ruleConfigById[ruleId];
this.rulesById[ruleId] = new Rule(ruleConfig, this.domainObject, this.openmct, this.rulesById[ruleId] = new Rule(ruleConfig, this.domainObject, this.openmct,
@ -345,7 +347,7 @@ define([
ruleOrder.splice(targetIndex + 1, 0, event.draggingId); ruleOrder.splice(targetIndex + 1, 0, event.draggingId);
this.domainObject.configuration.ruleOrder = ruleOrder; this.domainObject.configuration.ruleOrder = ruleOrder;
this.updateDomainObject(); this.updateDomainObject();
} }
this.refreshRules(); this.refreshRules();
}; };

View File

@ -1,7 +1,9 @@
define([ define([
'./summary-widget.html' './summary-widget.html',
'@braintree/sanitize-url'
], function ( ], function (
summaryWidgetTemplate summaryWidgetTemplate,
urlSanitizeLib
) { ) {
const WIDGET_ICON_CLASS = 'c-sw__icon js-sw__icon'; const WIDGET_ICON_CLASS = 'c-sw__icon js-sw__icon';
@ -35,8 +37,9 @@ define([
this.icon = this.container.querySelector('#widgetIcon'); this.icon = this.container.querySelector('#widgetIcon');
this.label = this.container.querySelector('.js-sw__label'); this.label = this.container.querySelector('.js-sw__label');
if (this.domainObject.url) { let url = this.domainObject.url;
this.widget.setAttribute('href', this.domainObject.url); if (url) {
this.widget.setAttribute('href', urlSanitizeLib.sanitizeUrl(url));
} else { } else {
this.widget.removeAttribute('href'); this.widget.removeAttribute('href');
} }

View File

@ -60,18 +60,17 @@ define([
this.addTelemetryObject = this.addTelemetryObject.bind(this); this.addTelemetryObject = this.addTelemetryObject.bind(this);
this.removeTelemetryObject = this.removeTelemetryObject.bind(this); this.removeTelemetryObject = this.removeTelemetryObject.bind(this);
this.removeTelemetryCollection = this.removeTelemetryCollection.bind(this); this.removeTelemetryCollection = this.removeTelemetryCollection.bind(this);
this.incrementOutstandingRequests = this.incrementOutstandingRequests.bind(this);
this.decrementOutstandingRequests = this.decrementOutstandingRequests.bind(this);
this.resetRowsFromAllData = this.resetRowsFromAllData.bind(this); this.resetRowsFromAllData = this.resetRowsFromAllData.bind(this);
this.isTelemetryObject = this.isTelemetryObject.bind(this); this.isTelemetryObject = this.isTelemetryObject.bind(this);
this.refreshData = this.refreshData.bind(this);
this.updateFilters = this.updateFilters.bind(this); this.updateFilters = this.updateFilters.bind(this);
this.clearData = this.clearData.bind(this);
this.buildOptionsFromConfiguration = this.buildOptionsFromConfiguration.bind(this); this.buildOptionsFromConfiguration = this.buildOptionsFromConfiguration.bind(this);
this.filterObserver = undefined; this.filterObserver = undefined;
this.createTableRowCollections(); this.createTableRowCollections();
openmct.time.on('bounds', this.refreshData);
openmct.time.on('timeSystem', this.refreshData);
} }
/** /**
@ -141,8 +140,6 @@ define([
let columnMap = this.getColumnMapForObject(keyString); let columnMap = this.getColumnMapForObject(keyString);
let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject); let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject);
this.incrementOutstandingRequests();
const telemetryProcessor = this.getTelemetryProcessor(keyString, columnMap, limitEvaluator); const telemetryProcessor = this.getTelemetryProcessor(keyString, columnMap, limitEvaluator);
const telemetryRemover = this.getTelemetryRemover(); const telemetryRemover = this.getTelemetryRemover();
@ -151,13 +148,13 @@ define([
this.telemetryCollections[keyString] = this.openmct.telemetry this.telemetryCollections[keyString] = this.openmct.telemetry
.requestCollection(telemetryObject, requestOptions); .requestCollection(telemetryObject, requestOptions);
this.telemetryCollections[keyString].on('requestStarted', this.incrementOutstandingRequests);
this.telemetryCollections[keyString].on('requestEnded', this.decrementOutstandingRequests);
this.telemetryCollections[keyString].on('remove', telemetryRemover); this.telemetryCollections[keyString].on('remove', telemetryRemover);
this.telemetryCollections[keyString].on('add', telemetryProcessor); this.telemetryCollections[keyString].on('add', telemetryProcessor);
this.telemetryCollections[keyString].on('clear', this.tableRows.clear); this.telemetryCollections[keyString].on('clear', this.clearData);
this.telemetryCollections[keyString].load(); this.telemetryCollections[keyString].load();
this.decrementOutstandingRequests();
this.telemetryObjects[keyString] = { this.telemetryObjects[keyString] = {
telemetryObject, telemetryObject,
keyString, keyString,
@ -268,17 +265,6 @@ define([
this.emit('object-removed', objectIdentifier); this.emit('object-removed', objectIdentifier);
} }
refreshData(bounds, isTick) {
if (!isTick && this.tableRows.outstandingRequests === 0) {
this.tableRows.clear();
this.tableRows.sortBy({
key: this.openmct.time.timeSystem().key,
direction: 'asc'
});
this.tableRows.resubscribe();
}
}
clearData() { clearData() {
this.tableRows.clear(); this.tableRows.clear();
this.emit('refresh'); this.emit('refresh');
@ -378,9 +364,6 @@ define([
let keystrings = Object.keys(this.telemetryCollections); let keystrings = Object.keys(this.telemetryCollections);
keystrings.forEach(this.removeTelemetryCollection); keystrings.forEach(this.removeTelemetryCollection);
this.openmct.time.off('bounds', this.refreshData);
this.openmct.time.off('timeSystem', this.refreshData);
if (this.filterObserver) { if (this.filterObserver) {
this.filterObserver(); this.filterObserver();
} }

View File

@ -131,7 +131,8 @@ export default {
objects.forEach(object => this.addColumnsForObject(object, false)); objects.forEach(object => this.addColumnsForObject(object, false));
}, },
addColumnsForObject(telemetryObject) { addColumnsForObject(telemetryObject) {
let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values(); const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
let metadataValues = metadata ? metadata.values() : [];
metadataValues.forEach(metadatum => { metadataValues.forEach(metadatum => {
let column = new TelemetryTableColumn(this.openmct, metadatum); let column = new TelemetryTableColumn(this.openmct, metadatum);
this.tableConfiguration.addSingleColumnForObject(telemetryObject, column); this.tableConfiguration.addSingleColumnForObject(telemetryObject, column);

View File

@ -105,7 +105,8 @@ export default {
composition.load().then((domainObjects) => { composition.load().then((domainObjects) => {
domainObjects.forEach(telemetryObject => { domainObjects.forEach(telemetryObject => {
let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values(); const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
let metadataValues = metadata ? metadata.values() : [];
let filters = this.filteredTelemetry[keyString]; let filters = this.filteredTelemetry[keyString];
if (filters !== undefined) { if (filters !== undefined) {

View File

@ -125,7 +125,6 @@
<div <div
class="c-table c-telemetry-table c-table--filterable c-table--sortable has-control-bar u-style-receiver js-style-receiver" class="c-table c-telemetry-table c-table--filterable c-table--sortable has-control-bar u-style-receiver js-style-receiver"
:class="{ :class="{
'loading': loading,
'is-paused' : paused 'is-paused' : paused
}" }"
> >
@ -362,7 +361,7 @@ export default {
autoScroll: true, autoScroll: true,
sortOptions: {}, sortOptions: {},
filters: {}, filters: {},
loading: true, loading: false,
scrollable: undefined, scrollable: undefined,
tableEl: undefined, tableEl: undefined,
headersHolderEl: undefined, headersHolderEl: undefined,
@ -422,6 +421,14 @@ export default {
} }
}, },
watch: { watch: {
loading: {
handler(isLoading) {
if (this.viewActionsCollection) {
let action = isLoading ? 'disable' : 'enable';
this.viewActionsCollection[action](['export-csv-all']);
}
}
},
markedRows: { markedRows: {
handler(newVal, oldVal) { handler(newVal, oldVal) {
this.$emit('marked-rows-updated', newVal, oldVal); this.$emit('marked-rows-updated', newVal, oldVal);
@ -1020,6 +1027,12 @@ export default {
this.viewActionsCollection.disable(['export-csv-marked', 'unmark-all-rows']); this.viewActionsCollection.disable(['export-csv-marked', 'unmark-all-rows']);
} }
if (this.loading) {
this.viewActionsCollection.disable(['export-csv-all']);
} else {
this.viewActionsCollection.enable(['export-csv-all']);
}
if (this.paused) { if (this.paused) {
this.viewActionsCollection.hide(['pause-data']); this.viewActionsCollection.hide(['pause-data']);
this.viewActionsCollection.show(['play-data']); this.viewActionsCollection.show(['play-data']);

View File

@ -222,6 +222,24 @@ describe("the plugin", () => {
openmct.router.path = originalRouterPath; openmct.router.path = originalRouterPath;
}); });
it("Shows no progress bar initially", () => {
let progressBar = element.querySelector('.c-progress-bar');
expect(tableInstance.outstandingRequests).toBe(0);
expect(progressBar).toBeNull();
});
it("Shows a progress bar while making requests", async () => {
tableInstance.incrementOutstandingRequests();
await Vue.nextTick();
let progressBar = element.querySelector('.c-progress-bar');
expect(tableInstance.outstandingRequests).toBe(1);
expect(progressBar).not.toBeNull();
});
it("Renders a row for every telemetry datum returned", () => { it("Renders a row for every telemetry datum returned", () => {
let rows = element.querySelectorAll('table.c-telemetry-table__body tr'); let rows = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(rows.length).toBe(3); expect(rows.length).toBe(3);

View File

@ -231,8 +231,8 @@ export default {
const panStart = bounds.start - percX * deltaTime; const panStart = bounds.start - percX * deltaTime;
return { return {
start: panStart, start: parseInt(panStart, 10),
end: panStart + deltaTime end: parseInt(panStart + deltaTime, 10)
}; };
}, },
startZoom() { startZoom() {
@ -296,7 +296,7 @@ export default {
const valueDelta = value - this.left; const valueDelta = value - this.left;
const offset = valueDelta / this.width * timeDelta; const offset = valueDelta / this.width * timeDelta;
return bounds.start + offset; return parseInt(bounds.start + offset, 10);
}, },
isChangingViewBounds() { isChangingViewBounds() {
return this.dragStartX && this.dragX && this.dragStartX !== this.dragX; return this.dragStartX && this.dragX && this.dragStartX !== this.dragX;

View File

@ -1,13 +1,7 @@
<template> <template>
<form ref="fixedDeltaInput" <form ref="fixedDeltaInput"
class="c-conductor__inputs" class="c-conductor__inputs"
@submit.prevent="updateTimeFromConductor"
> >
<button
ref="submitButton"
class="c-input--submit"
type="submit"
></button>
<div <div
class="c-ctrl-wrapper c-conductor-input c-conductor__start-fixed" class="c-ctrl-wrapper c-conductor-input c-conductor__start-fixed"
> >
@ -56,10 +50,6 @@
@date-selected="endDateSelected" @date-selected="endDateSelected"
/> />
</div> </div>
<input
type="submit"
class="invisible"
>
</form> </form>
</template> </template>
@ -183,10 +173,7 @@ export default {
submitForm() { submitForm() {
// Allow Vue model to catch up to user input. // Allow Vue model to catch up to user input.
// Submitting form will cause validation messages to display (but only if triggered by button click) // Submitting form will cause validation messages to display (but only if triggered by button click)
this.$nextTick(() => this.$refs.submitButton.click()); this.$nextTick(() => this.setBoundsFromView());
},
updateTimeFromConductor() {
this.setBoundsFromView();
}, },
validateAllBounds(ref) { validateAllBounds(ref) {
if (!this.areBoundsFormatsValid()) { if (!this.areBoundsFormatsValid()) {

View File

@ -151,29 +151,22 @@ export default {
this.stopFollowingTimeContext(); this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView([this.domainObject]); this.timeContext = this.openmct.time.getContextForView([this.domainObject]);
this.timeContext.on('timeContext', this.setTimeContext); this.timeContext.on('timeContext', this.setTimeContext);
this.timeContext.on('clock', this.setViewFromClock); this.timeContext.on('clock', this.setTimeOptions);
}, },
stopFollowingTimeContext() { stopFollowingTimeContext() {
if (this.timeContext) { if (this.timeContext) {
this.timeContext.off('timeContext', this.setTimeContext); this.timeContext.off('timeContext', this.setTimeContext);
this.timeContext.off('clock', this.setViewFromClock); this.timeContext.off('clock', this.setTimeOptions);
} }
}, },
setViewFromClock(clock) { setTimeOptions(clock) {
if (!this.timeOptions.mode) { this.timeOptions.clockOffsets = this.timeOptions.clockOffsets || this.timeContext.clockOffsets();
this.setTimeOptions(clock); this.timeOptions.fixedOffsets = this.timeOptions.fixedOffsets || this.timeContext.bounds();
}
},
setTimeOptions() {
if (!this.timeOptions || !this.timeOptions.mode) {
this.mode = this.timeContext.clock() === undefined ? { key: 'fixed' } : { key: Object.create(this.timeContext.clock()).key};
this.timeOptions = {
clockOffsets: this.timeContext.clockOffsets(),
fixedOffsets: this.timeContext.bounds()
};
}
this.registerIndependentTimeOffsets(); if (!this.timeOptions.mode) {
this.mode = this.timeContext.clock() === undefined ? {key: 'fixed'} : {key: Object.create(this.timeContext.clock()).key};
this.registerIndependentTimeOffsets();
}
}, },
saveFixedOffsets(offsets) { saveFixedOffsets(offsets) {
const newOptions = Object.assign({}, this.timeOptions, { const newOptions = Object.assign({}, this.timeOptions, {

View File

@ -20,7 +20,8 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
<template> <template>
<div ref="modeMenuButton" <div v-if="modes.length > 1"
ref="modeMenuButton"
class="c-ctrl-wrapper c-ctrl-wrapper--menus-up" class="c-ctrl-wrapper c-ctrl-wrapper--menus-up"
> >
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left"> <div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">

View File

@ -88,7 +88,7 @@ export default {
this.mutablePromise.then(() => { this.mutablePromise.then(() => {
this.openmct.objects.destroyMutable(this.domainObject); this.openmct.objects.destroyMutable(this.domainObject);
}); });
} else { } else if (this.domainObject.isMutable) {
this.openmct.objects.destroyMutable(this.domainObject); this.openmct.objects.destroyMutable(this.domainObject);
} }
}, },

View File

@ -1,16 +1,23 @@
<template> <template>
<div class="l-iframe abs"> <div class="l-iframe abs">
<iframe :src="currentDomainObject.url"></iframe> <iframe :src="url"></iframe>
</div> </div>
</template> </template>
<script> <script>
const sanitizeUrl = require("@braintree/sanitize-url").sanitizeUrl;
export default { export default {
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject'],
data: function () { data: function () {
return { return {
currentDomainObject: this.domainObject currentDomainObject: this.domainObject
}; };
},
computed: {
url() {
return sanitizeUrl(this.currentDomainObject.url);
}
} }
}; };
</script> </script>

View File

@ -729,7 +729,7 @@ mct-plot {
} }
/********************************************************************* BAR CHARTS */ /***************** BAR GRAPHS */
.c-bar-chart { .c-bar-chart {
flex: 1 1 auto; flex: 1 1 auto;
overflow: hidden; overflow: hidden;

View File

@ -40,11 +40,6 @@
} }
} }
.zerolinelayer {
// Hide unneeded plotly-styled horizontal line
display: none;
}
path.xy2-y { path.xy2-y {
stroke: $colorPlotHash !important; // Using this instead of $colorPlotAreaBorder because that is an rgba stroke: $colorPlotHash !important; // Using this instead of $colorPlotAreaBorder because that is an rgba
opacity: $opacityPlotHash !important; opacity: $opacityPlotHash !important;

View File

@ -21,72 +21,62 @@
--> -->
<template> <template>
<div class="u-contents"> <div class="u-contents">
<ul v-if="canEdit" <div v-if="canEdit"
class="l-inspector-part" class="grid-row"
> >
<h2 v-if="heading" <div class="grid-cell label"
:title="heading" :title="editTitle"
>{{ heading }}</h2> >{{ shortLabel }}</div>
<li class="grid-row"> <div class="grid-cell value">
<div class="grid-cell label" <div class="c-click-swatch c-click-swatch--menu"
:title="editTitle" @click="toggleSwatch()"
>{{ shortLabel }}</div> >
<div class="grid-cell value"> <span class="c-color-swatch"
<div class="c-click-swatch c-click-swatch--menu" :style="{ background: currentColor }"
@click="toggleSwatch()"
> >
<span class="c-color-swatch" </span>
:style="{ background: currentColor }" </div>
<div class="c-palette c-palette--color">
<div v-show="swatchActive"
class="c-palette__items"
>
<div v-for="group in colorPaletteGroups"
:key="group.id"
class="u-contents"
> >
</span> <div v-for="color in group"
</div> :key="color.id"
<div class="c-palette c-palette--color"> class="c-palette__item"
<div v-show="swatchActive" :class="{ 'selected': currentColor === color.hexString }"
class="c-palette__items" :style="{ background: color.hexString }"
> @click="setColor(color)"
<div v-for="group in colorPaletteGroups"
:key="group.id"
class="u-contents"
> >
<div v-for="color in group"
:key="color.id"
class="c-palette__item"
:class="{ 'selected': currentColor === color.hexString }"
:style="{ background: color.hexString }"
@click="setColor(color)"
>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</li> </div>
</ul> </div>
<ul v-else <div v-else
class="l-inspector-part" class="grid-row"
> >
<h2 v-if="heading" <div class="grid-cell label"
:title="heading" :title="viewTitle"
>{{ heading }}</h2> >{{ shortLabel }}</div>
<li class="grid-row"> <div class="grid-cell value">
<div class="grid-cell label" <span class="c-color-swatch"
:title="viewTitle" :style="{
>{{ shortLabel }}</div> 'background': currentColor
<div class="grid-cell value"> }"
<span class="c-color-swatch" >
:style="{ </span>
'background': currentColor </div>
}" </div>
>
</span>
</div>
</li>
</ul>
</div> </div>
</template> </template>
<script> <script>
import ColorPalette from './lib/ColorPalette'; import ColorPalette from './ColorPalette';
export default { export default {
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject'],
@ -114,12 +104,6 @@ export default {
default() { default() {
return 'Color'; return 'Color';
} }
},
heading: {
type: String,
default() {
return '';
}
} }
}, },
data() { data() {

View File

@ -160,7 +160,9 @@ export default {
this.status = this.openmct.status.get(this.domainObject.identifier); this.status = this.openmct.status.get(this.domainObject.identifier);
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus); this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus);
const provider = this.openmct.objectViews.get(this.domainObject, this.objectPath)[0]; const provider = this.openmct.objectViews.get(this.domainObject, this.objectPath)[0];
this.$refs.objectView.show(this.domainObject, provider.key, false, this.objectPath); if (provider) {
this.$refs.objectView.show(this.domainObject, provider.key, false, this.objectPath);
}
}, },
beforeDestroy() { beforeDestroy() {
this.removeStatusListener(); this.removeStatusListener();
@ -193,8 +195,10 @@ export default {
}, },
showMenuItems(event) { showMenuItems(event) {
const sortedActions = this.openmct.actions._groupAndSortActions(this.menuActionItems); const sortedActions = this.openmct.actions._groupAndSortActions(this.menuActionItems);
const menuItems = this.openmct.menus.actionsToMenuItems(sortedActions, this.actionCollection.objectPath, this.actionCollection.view); if (sortedActions.length) {
this.openmct.menus.showMenu(event.x, event.y, menuItems); const menuItems = this.openmct.menus.actionsToMenuItems(sortedActions, this.actionCollection.objectPath, this.actionCollection.view);
this.openmct.menus.showMenu(event.x, event.y, menuItems);
}
}, },
setStatus(status) { setStatus(status) {
this.status = status; this.status = status;

View File

@ -126,6 +126,11 @@ export default {
this.releaseEditModeHandler(); this.releaseEditModeHandler();
delete this.releaseEditModeHandler; delete this.releaseEditModeHandler;
} }
if (this.styleRuleManager) {
this.styleRuleManager.destroy();
delete this.styleRuleManager;
}
} }
delete this.viewContainer; delete this.viewContainer;

View File

@ -49,6 +49,7 @@ describe("the inspector", () => {
beforeEach((done) => { beforeEach((done) => {
openmct = createOpenMct(); openmct = createOpenMct();
spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true));
openmct.on('start', done); openmct.on('start', done);
openmct.startHeadless(); openmct.startHeadless();
}); });
@ -77,12 +78,12 @@ describe("the inspector", () => {
expect(savedStylesViewComponent.$children[0].$children.length).toBe(0); expect(savedStylesViewComponent.$children[0].$children.length).toBe(0);
stylesViewComponent.$children[0].saveStyle(mockStyle); stylesViewComponent.$children[0].saveStyle(mockStyle);
stylesViewComponent.$nextTick().then(() => { return stylesViewComponent.$nextTick().then(() => {
expect(savedStylesViewComponent.$children[0].$children.length).toBe(1); expect(savedStylesViewComponent.$children[0].$children.length).toBe(1);
}); });
}); });
it("should allow a saved style to be applied", () => { xit("should allow a saved style to be applied", () => {
spyOn(openmct.editor, 'isEditing').and.returnValue(true); spyOn(openmct.editor, 'isEditing').and.returnValue(true);
selection = mockTelemetryTableSelection; selection = mockTelemetryTableSelection;
@ -91,12 +92,12 @@ describe("the inspector", () => {
stylesViewComponent.$children[0].saveStyle(mockStyle); stylesViewComponent.$children[0].saveStyle(mockStyle);
stylesViewComponent.$nextTick().then(() => { return stylesViewComponent.$nextTick().then(() => {
const styleSelectorComponent = savedStylesViewComponent.$children[0].$children[0]; const styleSelectorComponent = savedStylesViewComponent.$children[0].$children[0];
styleSelectorComponent.selectStyle(); styleSelectorComponent.selectStyle();
savedStylesViewComponent.$nextTick().then(() => { return savedStylesViewComponent.$nextTick().then(() => {
const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1; const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1;
const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex]; const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex];
const styles = styleEditorComponent.$children.filter(component => component.options.value === mockStyle.color); const styles = styleEditorComponent.$children.filter(component => component.options.value === mockStyle.color);
@ -147,7 +148,7 @@ describe("the inspector", () => {
stylesViewComponent = createViewComponent(StylesView, selection, openmct); stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct); savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
stylesViewComponent.$nextTick().then(() => { return stylesViewComponent.$nextTick().then(() => {
const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1; const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1;
const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex]; const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex];
const saveStyleButtonIndex = styleEditorComponent.$children.length - 1; const saveStyleButtonIndex = styleEditorComponent.$children.length - 1;
@ -168,7 +169,7 @@ describe("the inspector", () => {
stylesViewComponent = createViewComponent(StylesView, selection, openmct); stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct); savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
stylesViewComponent.$nextTick().then(() => { return stylesViewComponent.$nextTick().then(() => {
const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1; const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1;
const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex]; const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex];
const saveStyleButtonIndex = styleEditorComponent.$children.length - 1; const saveStyleButtonIndex = styleEditorComponent.$children.length - 1;
@ -185,7 +186,7 @@ describe("the inspector", () => {
stylesViewComponent = createViewComponent(StylesView, selection, openmct); stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct); savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
stylesViewComponent.$nextTick().then(() => { return stylesViewComponent.$nextTick().then(() => {
const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1; const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1;
const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex]; const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex];
const saveStyleButtonIndex = styleEditorComponent.$children.length - 1; const saveStyleButtonIndex = styleEditorComponent.$children.length - 1;

View File

@ -46,6 +46,14 @@ export default {
'openmct', 'openmct',
'objectPath' 'objectPath'
], ],
props: {
viewOptions: {
type: Object,
default() {
return undefined;
}
}
},
data() { data() {
let domainObject = this.objectPath[0]; let domainObject = this.objectPath[0];
@ -109,7 +117,7 @@ export default {
this.view = this.currentView.view(this.domainObject, this.objectPath); this.view = this.currentView.view(this.domainObject, this.objectPath);
this.getActionsCollection(); this.getActionsCollection();
this.view.show(this.viewContainer, false); this.view.show(this.viewContainer, false, this.viewOptions);
this.initObjectStyles(); this.initObjectStyles();
}, },
getActionsCollection() { getActionsCollection() {

View File

@ -44,7 +44,7 @@ export default class PreviewAction {
} }
} }
invoke(objectPath) { invoke(objectPath, viewOptions) {
let preview = new Vue({ let preview = new Vue({
components: { components: {
Preview Preview
@ -53,7 +53,12 @@ export default class PreviewAction {
openmct: this._openmct, openmct: this._openmct,
objectPath: objectPath objectPath: objectPath
}, },
template: '<Preview></Preview>' data() {
return {
viewOptions
};
},
template: '<Preview :view-options="viewOptions"></Preview>'
}); });
preview.$mount(); preview.$mount();

View File

@ -4,7 +4,8 @@
<div <div
class="l-browse-bar__object-name--w c-object-label" class="l-browse-bar__object-name--w c-object-label"
> >
<div class="c-object-label__type-icon" <div v-if="type"
class="c-object-label__type-icon"
:class="type.definition.cssClass" :class="type.definition.cssClass"
></div> ></div>
<span class="l-browse-bar__object-name c-object-label__name"> <span class="l-browse-bar__object-name c-object-label__name">

View File

@ -90,9 +90,9 @@ define(['EventEmitter'], function (EventEmitter) {
provider.view = (domainObject, objectPath) => { provider.view = (domainObject, objectPath) => {
const viewObject = wrappedView(domainObject, objectPath); const viewObject = wrappedView(domainObject, objectPath);
const wrappedShow = viewObject.show.bind(viewObject); const wrappedShow = viewObject.show.bind(viewObject);
viewObject.show = (element, isEditing) => { viewObject.show = (element, isEditing, viewOptions) => {
viewObject.parentElement = element.parentElement; viewObject.parentElement = element.parentElement;
wrappedShow(element, isEditing); wrappedShow(element, isEditing, viewOptions);
}; };
return viewObject; return viewObject;

View File

@ -126,7 +126,7 @@ class ApplicationRouter extends EventEmitter {
} }
/** /**
* Navgate to given hash and update current location object and notify listeners about location change * Navigate to given hash and update current location object and notify listeners about location change
* *
* @param {string} paramName name of searchParam to get from current url searchParams * @param {string} paramName name of searchParam to get from current url searchParams
* *
@ -136,6 +136,13 @@ class ApplicationRouter extends EventEmitter {
this.handleLocationChange(hash.substring(1)); this.handleLocationChange(hash.substring(1));
} }
/**
* Check if a given object and current location object are same
*
* @param {Array<Object>} objectPath Object path of a given Domain Object
*
* @returns {Boolean}
*/
isNavigatedObject(objectPath) { isNavigatedObject(objectPath) {
let targetObject = objectPath[0]; let targetObject = objectPath[0];
let navigatedObject = this.path[0]; let navigatedObject = this.path[0];

View File

@ -42,6 +42,8 @@ const webpackConfig = {
"csv": "comma-separated-values", "csv": "comma-separated-values",
"EventEmitter": "eventemitter3", "EventEmitter": "eventemitter3",
"bourbon": "bourbon.scss", "bourbon": "bourbon.scss",
"plotly-basic": "plotly.js-basic-dist",
"plotly-gl2d": "plotly.js-gl2d-dist",
"vue": vueFile, "vue": vueFile,
"d3-scale": path.join(__dirname, "node_modules/d3-scale/build/d3-scale.min.js"), "d3-scale": path.join(__dirname, "node_modules/d3-scale/build/d3-scale.min.js"),
"printj": path.join(__dirname, "node_modules/printj/dist/printj.min.js"), "printj": path.join(__dirname, "node_modules/printj/dist/printj.min.js"),