Independent time contexts follow upstream contexts as needed (#4556)

* Update independent time context APIs to follow upstream time contexts as necessary
* Removes boilerplate from views.
This commit is contained in:
Shefali Joshi 2021-12-13 13:28:17 -08:00 committed by GitHub
parent 82ea23e20c
commit 2488072d6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 215 additions and 52 deletions

View File

@ -20,18 +20,66 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TimeContext from "./TimeContext";
import TimeContext, { TIME_CONTEXT_EVENTS } from "./TimeContext";
/**
* The IndependentTimeContext handles getting and setting time of the openmct application in general.
* Views will use the GlobalTimeContext unless they specify an alternate/independent time context here.
*/
class IndependentTimeContext extends TimeContext {
constructor(globalTimeContext, key) {
constructor(openmct, globalTimeContext, objectPath) {
super();
this.key = key;
this.openmct = openmct;
this.unlisteners = [];
this.globalTimeContext = globalTimeContext;
this.upstreamTimeContext = undefined;
this.objectPath = objectPath;
this.refreshContext = this.refreshContext.bind(this);
this.resetContext = this.resetContext.bind(this);
this.refreshContext();
this.globalTimeContext.on('refreshContext', this.refreshContext);
}
bounds(newBounds) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.bounds(...arguments);
} else {
return super.bounds(...arguments);
}
}
tick(timestamp) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.tick(...arguments);
} else {
return super.tick(...arguments);
}
}
clockOffsets(offsets) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.clockOffsets(...arguments);
} else {
return super.clockOffsets(...arguments);
}
}
stopClock() {
if (this.upstreamTimeContext) {
this.upstreamTimeContext.stopClock();
} else {
super.stopClock();
}
}
timeOfInterest(newTOI) {
return this.globalTimeContext.timeOfInterest(...arguments);
}
timeSystem(timeSystemOrKey, bounds) {
return this.globalTimeContext.timeSystem(...arguments);
}
/**
@ -47,6 +95,10 @@ class IndependentTimeContext extends TimeContext {
* @return {Clock} the currently active clock;
*/
clock(keyOrClock, offsets) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.clock(...arguments);
}
if (arguments.length === 2) {
let clock;
@ -89,6 +141,81 @@ class IndependentTimeContext extends TimeContext {
return this.activeClock;
}
/**
* Causes this time context to follow another time context (either the global context, or another upstream time context)
* This allows views to have their own time context which points to the appropriate upstream context as necessary, achieving nesting.
* @param {*} upstreamTimeContext
*/
followTimeContext() {
this.stopFollowingTimeContext();
if (this.upstreamTimeContext) {
TIME_CONTEXT_EVENTS.forEach((eventName) => {
const thisTimeContext = this;
this.upstreamTimeContext.on(eventName, passthrough);
this.unlisteners.push(() => this.upstreamTimeContext.off(eventName, passthrough));
function passthrough() {
thisTimeContext.emit(eventName, ...arguments);
}
});
}
}
/**
* Stops following any upstream time context
*/
stopFollowingTimeContext() {
this.unlisteners.forEach(unlisten => unlisten());
}
resetContext() {
if (this.upstreamTimeContext) {
this.stopFollowingTimeContext();
this.upstreamTimeContext = undefined;
}
}
/**
* Refresh the time context, following any upstream time contexts as necessary
*/
refreshContext(viewKey) {
//TODO: find a better way to skip upstream context for the view that just got an independent time context
const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier);
if (viewKey && key === viewKey) {
return;
}
this.upstreamTimeContext = this.getUpstreamContext();
this.followTimeContext();
// Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.upstreamTimeContext.bounds());
}
hasOwnContext() {
return this.upstreamTimeContext === undefined;
}
getUpstreamContext() {
let timeContext = this.globalTimeContext;
this.objectPath.some((item, index) => {
const key = this.openmct.objects.makeKeyString(item.identifier);
//last index is the view object itself
const itemContext = this.globalTimeContext.independentContexts.get(key);
if (index > 0 && itemContext && itemContext.hasOwnContext()) {
//upstream time context
timeContext = itemContext;
return true;
}
return false;
});
return timeContext;
}
}
export default IndependentTimeContext;

View File

@ -133,11 +133,9 @@ class TimeAPI extends GlobalTimeContext {
* @method addIndependentTimeContext
*/
addIndependentContext(key, value, clockKey) {
let timeContext = this.independentContexts.get(key);
if (!timeContext) {
timeContext = new IndependentTimeContext(this, key);
this.independentContexts.set(key, timeContext);
}
let timeContext = this.getIndependentContext(key);
//stop following upstream time context since the view has it's own
timeContext.resetContext();
if (clockKey) {
timeContext.clock(clockKey, value);
@ -146,11 +144,12 @@ class TimeAPI extends GlobalTimeContext {
timeContext.bounds(value);
}
this.emit('timeContext', key);
// Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context
this.emit('refreshContext', key);
return () => {
this.independentContexts.delete(key);
timeContext.emit('timeContext', key);
//follow any upstream time context
this.emit('refreshContext');
};
}
@ -173,16 +172,24 @@ class TimeAPI extends GlobalTimeContext {
* @method getContextForView
*/
getContextForView(objectPath = []) {
let timeContext = this;
const viewKey = objectPath.length && this.openmct.objects.makeKeyString(objectPath[0].identifier);
objectPath.forEach(item => {
const key = this.openmct.objects.makeKeyString(item.identifier);
if (this.independentContexts.get(key)) {
timeContext = this.independentContexts.get(key);
if (viewKey) {
let viewTimeContext = this.getIndependentContext(viewKey);
if (viewTimeContext) {
this.independentContexts.delete(viewKey);
} else {
viewTimeContext = new IndependentTimeContext(this.openmct, this, objectPath);
}
});
return timeContext;
// return a new IndependentContext in case the objectPath is different
this.independentContexts.set(viewKey, viewTimeContext);
return viewTimeContext;
}
// always follow the global time context
return this;
}
}

View File

@ -22,6 +22,13 @@
import EventEmitter from 'EventEmitter';
export const TIME_CONTEXT_EVENTS = [
'bounds',
'clock',
'timeSystem',
'clockOffsets'
];
class TimeContext extends EventEmitter {
constructor() {
super();
@ -46,7 +53,7 @@ class TimeContext extends EventEmitter {
/**
* Get or set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystem
* @param {TimeSystem | string} timeSystemOrKey
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @returns {TimeSystem} The currently applied time system

View File

@ -58,26 +58,31 @@ describe("The Independent Time API", function () {
});
it("Creates an independent time context", () => {
let timeContext = api.getContextForView([{
identifier: {
namespace: '',
key: domainObjectKey
}
}]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getIndependentContext(domainObjectKey);
expect(timeContext.bounds()).toEqual(independentBounds);
destroyTimeContext();
});
it("Gets an independent time context given the objectPath", () => {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{
let timeContext = api.getContextForView([{ identifier: domainObjectKey },
{
identifier: {
namespace: '',
key: 'blah'
}
}, { identifier: domainObjectKey }]);
}]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(timeContext.bounds()).toEqual(independentBounds);
destroyTimeContext();
});
it("defaults to the global time context given the objectPath", () => {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{
identifier: {
namespace: '',
@ -85,7 +90,24 @@ describe("The Independent Time API", function () {
}
}]);
expect(timeContext.bounds()).toEqual(bounds);
});
it("follows a parent time context given the objectPath", () => {
let timeContext = api.getContextForView([{
identifier: {
namespace: '',
key: 'blah'
}
}, {
identifier: {
namespace: '',
key: domainObjectKey
}
}]);
let destroyTimeContext = api.addIndependentContext('blah', independentBounds);
expect(timeContext.bounds()).toEqual(independentBounds);
destroyTimeContext();
expect(timeContext.bounds()).toEqual(bounds);
});
it("Allows setting of valid bounds", function () {
@ -93,8 +115,8 @@ describe("The Independent Time API", function () {
start: 0,
end: 1
};
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(timeContext.bounds()).not.toEqual(bounds);
timeContext.bounds(bounds);
expect(timeContext.bounds()).toEqual(bounds);
@ -107,8 +129,8 @@ describe("The Independent Time API", function () {
end: 0
};
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(timeContext.bounds()).not.toBe(bounds);
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
@ -122,8 +144,8 @@ describe("The Independent Time API", function () {
});
it("Emits an event when bounds change", function () {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(eventListener).not.toHaveBeenCalled();
timeContext.on('bounds', eventListener);
timeContext.bounds(bounds);
@ -131,6 +153,14 @@ describe("The Independent Time API", function () {
destroyTimeContext();
});
it("Emits an event when bounds change on the global context", function () {
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(eventListener).not.toHaveBeenCalled();
timeContext.on('bounds', eventListener);
timeContext.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
});
describe(" when using real time clock", function () {
const mockOffsets = {
start: 10,
@ -138,8 +168,8 @@ describe("The Independent Time API", function () {
};
it("Emits an event when bounds change based on current value", function () {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(eventListener).not.toHaveBeenCalled();
timeContext.clock('someClockKey', mockOffsets);
timeContext.on('bounds', eventListener);

View File

@ -112,13 +112,11 @@ export default {
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) {

View File

@ -501,13 +501,11 @@ export default {
//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);
}
},
boundsChange(bounds, isTick) {

View File

@ -62,13 +62,11 @@ export default {
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) {

View File

@ -120,7 +120,6 @@ export default {
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.path);
this.timeContext.on("timeContext", this.setTimeContext);
this.followTimeContext();
},
followTimeContext() {
@ -133,7 +132,6 @@ export default {
if (this.timeContext) {
this.timeContext.off("timeSystem", this.setScaleAndPlotActivities);
this.timeContext.off("bounds", this.updateViewBounds);
this.timeContext.off("timeContext", this.setTimeContext);
}
},
observeForChanges(mutatedObject) {

View File

@ -283,7 +283,6 @@ export default {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.path);
this.timeContext.on('timeContext', this.setTimeContext);
this.followTimeContext();
},
@ -297,7 +296,6 @@ export default {
if (this.timeContext) {
this.timeContext.off("clock", this.updateRealTime);
this.timeContext.off("bounds", this.updateDisplayBounds);
this.timeContext.off("timeContext", this.setTimeContext);
}
},
getConfig() {

View File

@ -110,7 +110,6 @@ export default {
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.keyString ? [{identifier: this.keyString}] : []);
this.timeContext.on('timeContext', this.setTimeContext);
this.handleNewBounds(this.timeContext.bounds());
this.timeContext.on('bounds', this.handleNewBounds);
@ -120,7 +119,6 @@ export default {
if (this.timeContext) {
this.timeContext.off('bounds', this.handleNewBounds);
this.timeContext.off('clock', this.clearAllValidation);
this.timeContext.off('timeContext', this.setTimeContext);
}
},
handleNewBounds(bounds) {

View File

@ -138,13 +138,11 @@ export default {
this.timeContext.off('bounds', this.handleNewBounds);
this.timeContext.off('clock', this.clearAllValidation);
this.timeContext.off('clockOffsets', this.setViewFromOffsets);
this.timeContext.off('timeContext', this.setTimeContext);
}
},
setTimeContext() {
this.stopFollowingTime();
this.timeContext = this.openmct.time.getContextForView(this.keyString ? [{identifier: this.keyString}] : []);
this.timeContext.on('timeContext', this.setTimeContext);
this.followTime();
},
handleNewBounds(bounds) {

View File

@ -150,12 +150,10 @@ export default {
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView([this.domainObject]);
this.timeContext.on('timeContext', this.setTimeContext);
this.timeContext.on('clock', this.setTimeOptions);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('timeContext', this.setTimeContext);
this.timeContext.off('clock', this.setTimeOptions);
}
},
@ -212,7 +210,17 @@ export default {
}
const key = this.openmct.objects.makeKeyString(this.domainObject.identifier);
const timeContext = this.openmct.time.getIndependentContext(key);
if (!timeContext.hasOwnContext()) {
this.unregisterIndependentTime = this.openmct.time.addIndependentContext(key, offsets, this.isFixed ? undefined : this.mode.key);
} else {
if (this.isFixed) {
timeContext.stopClock();
timeContext.bounds(offsets);
} else {
timeContext.clock(this.mode.key, offsets);
}
}
},
destroyIndependentTime() {
if (this.unregisterIndependentTime) {

View File

@ -161,14 +161,12 @@ export default {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on('timeContext', this.setTimeContext);
this.updateViewBounds(this.timeContext.bounds());
this.timeContext.on('bounds', this.updateViewBounds);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.updateViewBounds);
this.timeContext.off('timeContext', this.setTimeContext);
}
}
}

View File

@ -195,7 +195,7 @@ export default {
return objectType.definition;
},
isPersistable() {
let persistable = this.openmct.objects.isPersistable(this.domainObject.identifier);
let persistable = this.domainObject.identifier && this.openmct.objects.isPersistable(this.domainObject.identifier);
return persistable;
},