Compare commits

...

50 Commits

Author SHA1 Message Date
a07eb07090 Set the mode of the independent time conductor timecontext so that it's clock can tick. 2023-06-21 21:16:18 -07:00
9f55e4c98e WIP 2023-06-20 12:54:55 -07:00
17a1a74eed stil WIP, but progress made on independent and regular conductor popup finctionality, next steps polishing api 2023-06-15 11:58:10 -07:00
ec95faa57f WIP 2023-06-01 11:35:49 -07:00
488902f436 various changes, but mainly changes to independent time context 2023-05-24 16:34:43 -07:00
0db9a5e2a7 cleanup 2023-05-24 11:30:57 -07:00
1950839f80 itc work, moving mutating the domain object into itc so its in one place 2023-05-24 11:29:01 -07:00
0ddb49c11d forgot one 2023-05-24 09:10:45 -07:00
5f6cf7b0df using constants for events in the conductor components 2023-05-24 09:09:52 -07:00
295acdb36b cleaning up some items 2023-05-23 21:30:55 -07:00
d68a58f7e0 updated xAxis to new api, it was thrwoing many warnings for calling .clock, may want to look into that 2023-05-23 13:41:41 -07:00
4a2d903050 realtime working on load 2023-05-23 13:23:48 -07:00
7aa3762684 WIP 2023-05-22 16:05:13 -07:00
74cfab0ec0 working on independent time conductor part 2023-05-22 09:46:33 -07:00
1ebf566bb7 WIP: lots of updates to methods and moving component creation from programatic to v-ifs, mostly focused on conductor at the moment, independent next 2023-05-18 17:07:34 -07:00
784113012e moved conductor popup into conductor instead of creating the component on the fly, this way we are prepping for vue 3 and we can use v-if to destroy it 2023-05-10 12:50:09 -07:00
0260e06222 various changes for setting modes and clocks, also updates to conductor history for new api methods 2023-05-10 11:22:11 -07:00
266470a755 WIP 2023-05-09 17:34:48 -07:00
d851b01363 WIP 2023-05-02 12:05:40 -07:00
f41df68458 Merge branch 'time-api-enhancements' into mode-dropdown
Merging in shefali's time api updatesy
2023-04-28 13:41:19 -07:00
ac5c579874 WIP 2023-04-28 12:34:25 -07:00
204fb8c0e0 manually cherry picking over my changes to this existing branch 2023-04-26 14:32:42 -07:00
c605cd7a17 Stubs for the new Time API methods 2023-04-25 21:43:15 -07:00
a5db0f3b71 Ensure that clock changes are reflected downstream 2023-04-25 09:06:15 -07:00
62483583eb Ensure independent time conductor mode works as expected 2023-04-24 21:46:28 -07:00
6c63773641 object path for independent time conductor 2023-04-24 20:19:23 -07:00
e27e315784 Independent time conductor popup draft 2023-04-24 13:18:31 -07:00
5dc82742bf Add independent time conductor popup logic 2023-04-21 15:48:35 -07:00
bd4d30f481 Ensure conductor history works when mode is switched. Submit and cancel work as expected 2023-04-19 21:03:26 -07:00
b8322a8311 Merge branch 'master' of https://github.com/nasa/openmct into time-conductor-4975 2023-04-19 14:35:07 -07:00
2d6c6a6b38 Save fixed time bounds 2023-04-18 23:08:25 -07:00
f4e747a85e Ensure toggling between fixed timespan and clock modes works. Save clock offsets. 2023-04-18 21:51:19 -07:00
9ccdbcede8 Merge branch 'master' of https://github.com/nasa/openmct into time-conductor-4975 2023-04-18 11:12:38 -07:00
2e04c686f4 Initial work on getting the conductor popup working 2023-04-17 13:36:05 -07:00
508c2ebd87 Closes #4975
- CSS fix for to-be-deprecated division operation.
2023-04-06 15:56:09 -07:00
892963aa0e Merge branch 'master' of https://github.com/nasa/openmct into time-conductor-4975 2023-04-06 09:38:13 -07:00
34864771b3 Merge branch 'time-conductor-4975' of https://github.com/nasa/openmct into time-conductor-4975 2022-11-03 13:40:57 -07:00
2f1eb7f1bc Fixes #4975 - Compact Time Conductor styling
- Styling for Time Conductor in layout frames.
2022-05-17 23:08:03 -07:00
2506dfb25d Fixes #4975 - Compact Time Conductor styling
- Convert Time Conductor symbol to use SVG instead of font glyph
2022-05-17 22:33:29 -07:00
6ec07490ff Fixes #4975 - Compact Time Conductor styling
- Layout, display behavior for Time Conductor in layout frames.
- Code cleanups.
2022-05-17 22:07:31 -07:00
04c76a0d0d Fixes #4975 - Compact Time Conductor styling
- Fix SCSS error.
2022-05-17 13:24:13 -07:00
e5d701dea2 Fixes #4975 - Compact Time Conductor styling
- Layout, display behavior.
- Hide functional buttons to be moved.
- Remove unneeded markup.
- Input widths for fixed popup date and time.
- Add new `c-not-button` class.
- Add new `u-flex-spreader` class.
- Code cleanups.
2022-05-17 13:21:03 -07:00
f22f826546 Fixes #4975 - Compact Time Conductor styling
- Stubbed buttons into popups.
- More `$colorTime*` theme constants defined and applied.
- Still quite WIP!
2022-05-16 23:03:30 -07:00
fc83d88670 Fixes #4975 - Compact Time Conductor styling
- Fixed inputs popup layout with style and layout.
- More `$colorTime*` theme constants defined and applied.
- Better CSS organization.
2022-05-16 22:31:04 -07:00
47e4c3af67 Fixes #4975 - Compact Time Conductor styling
- Styling for new mini toggle slider switch.
2022-05-16 18:43:29 -07:00
d0cc125867 Fixes #4975 - Compact Time Conductor styling
- Markup and script moved into new timePopup* components.
- Significant work on styling.
- Theme constants augmented, better naming.
2022-05-16 18:42:58 -07:00
65be53ba18 Merge branch 'master' of https://github.com/nasa/openmct into time-conductor-4975 2022-05-16 10:42:09 -07:00
1e302e9f5e Fixes #4975 - Compact Time Conductor styling
- Added CSS class `is-expanded` to main view TC component.
2022-05-13 17:28:37 -07:00
9e174c40ed Fixes #4975 - Compact Time Conductor styling
- Significant CSS and markup work.
- Refinements to `c-icon-button` classes, including new `--compact` definition.
- Styling for independent and main conductor components.
- Code cleanups.
- STILL WIP!
2022-05-13 17:23:17 -07:00
453311272e Fixed #4975 - Compact Time Conductor styling
- Moved IndependentTimeConductor.vue into BrowseBar.vue.
- Styling for read-only conductor views.
- VERY WIP!
2022-05-13 12:07:19 -07:00
49 changed files with 3119 additions and 1359 deletions

View File

@ -20,13 +20,15 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import TimeContext, { TIME_CONTEXT_EVENTS } from "./TimeContext"; import TimeContext from "./TimeContext";
import { TIME_CONTEXT_EVENTS } from './constants';
/** /**
* The IndependentTimeContext handles getting and setting time of the openmct application in general. * 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. * Views will use the GlobalTimeContext unless they specify an alternate/independent time context here.
*/ */
class IndependentTimeContext extends TimeContext { class IndependentTimeContext extends TimeContext {
constructor(openmct, globalTimeContext, objectPath) { constructor(openmct, globalTimeContext, objectPath) {
super(); super();
this.openmct = openmct; this.openmct = openmct;
@ -46,7 +48,7 @@ class IndependentTimeContext extends TimeContext {
this.globalTimeContext.on('removeOwnContext', this.removeIndependentContext); this.globalTimeContext.on('removeOwnContext', this.removeIndependentContext);
} }
bounds(newBounds) { bounds() {
if (this.upstreamTimeContext) { if (this.upstreamTimeContext) {
return this.upstreamTimeContext.bounds(...arguments); return this.upstreamTimeContext.bounds(...arguments);
} else { } else {
@ -54,7 +56,23 @@ class IndependentTimeContext extends TimeContext {
} }
} }
tick(timestamp) { getBounds() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getBounds();
} else {
return super.getBounds();
}
}
setBounds() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setBounds(...arguments);
} else {
return super.setBounds(...arguments);
}
}
tick() {
if (this.upstreamTimeContext) { if (this.upstreamTimeContext) {
return this.upstreamTimeContext.tick(...arguments); return this.upstreamTimeContext.tick(...arguments);
} else { } else {
@ -62,7 +80,7 @@ class IndependentTimeContext extends TimeContext {
} }
} }
clockOffsets(offsets) { clockOffsets() {
if (this.upstreamTimeContext) { if (this.upstreamTimeContext) {
return this.upstreamTimeContext.clockOffsets(...arguments); return this.upstreamTimeContext.clockOffsets(...arguments);
} else { } else {
@ -70,11 +88,27 @@ class IndependentTimeContext extends TimeContext {
} }
} }
getClockOffsets() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getClockOffsets();
} else {
return super.getClockOffsets();
}
}
setClockOffsets() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setClockOffsets(...arguments);
} else {
return super.setClockOffsets(...arguments);
}
}
stopClock() { stopClock() {
if (this.upstreamTimeContext) { if (this.upstreamTimeContext) {
this.upstreamTimeContext.stopClock(); // this.upstreamTimeContext.stopClock();
} else { } else {
super.stopClock(); // super.stopClock();
} }
} }
@ -86,6 +120,16 @@ class IndependentTimeContext extends TimeContext {
return this.globalTimeContext.timeSystem(...arguments); return this.globalTimeContext.timeSystem(...arguments);
} }
/**
* Get the time system of the TimeAPI.
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method getTimeSystem
*/
getTimeSystem() {
return this.globalTimeContext.getTimeSystem();
}
/** /**
* Set the active clock. Tick source will be immediately subscribed to * Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock * and ticking will begin. Offsets from 'now' must also be provided. A clock
@ -146,6 +190,121 @@ class IndependentTimeContext extends TimeContext {
return this.activeClock; return this.activeClock;
} }
/**
* Get the active clock.
* @return {Clock} the currently active clock;
*/
getClock() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getClock();
}
return this.activeClock;
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and the currently ticking will begin.
* Offsets from 'now', if provided, will be used to set realtime mode offsets
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds when in realtime mode.
* This maintains a sliding time window of a fixed width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
setClock(keyOrClock, offsets) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setClock(...arguments);
}
let clock;
if (typeof keyOrClock === 'string') {
clock = this.globalTimeContext.clocks.get(keyOrClock);
if (clock === undefined) {
throw `Unknown clock ${keyOrClock}. Has it been registered with 'addClock'?`;
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.globalTimeContext.clocks.has(clock.key)) {
throw `Unknown clock ${keyOrClock.key}. Has it been registered with 'addClock'?`;
}
}
// this.setMode(REALTIME_MODE_KEY);
const previousClock = this.activeClock;
if (previousClock) {
previousClock.off('tick', this.tick);
}
this.activeClock = clock;
this.activeClock.on('tick', this.tick);
/**
* The active clock has changed.
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);
if (offsets !== undefined) {
this.setClockOffsets(offsets);
}
return this.activeClock;
}
/**
* Get the current mode.
* @return {Mode} the current mode;
*/
getMode() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getMode();
}
return this.mode;
}
/**
* Set the mode to either fixed or realtime.
*
* @param {Mode} mode The mode to activate
* @fires module:openmct.TimeAPI~clock
* @return {Mode} the currently active mode;
*/
setMode(mode) {
if (!mode || mode === this.mode) {
return;
}
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setMode(...arguments);
}
this.mode = mode;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.mode));
return this.mode;
}
#copy(object) {
return JSON.parse(JSON.stringify(object));
}
/** /**
* Causes this time context to follow another time context (either the global context, or another upstream time context) * 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. * This allows views to have their own time context which points to the appropriate upstream context as necessary, achieving nesting.
@ -153,7 +312,7 @@ class IndependentTimeContext extends TimeContext {
followTimeContext() { followTimeContext() {
this.stopFollowingTimeContext(); this.stopFollowingTimeContext();
if (this.upstreamTimeContext) { if (this.upstreamTimeContext) {
TIME_CONTEXT_EVENTS.forEach((eventName) => { Object.values(TIME_CONTEXT_EVENTS).forEach((eventName) => {
const thisTimeContext = this; const thisTimeContext = this;
this.upstreamTimeContext.on(eventName, passthrough); this.upstreamTimeContext.on(eventName, passthrough);
this.unlisteners.push(() => { this.unlisteners.push(() => {
@ -187,6 +346,7 @@ class IndependentTimeContext extends TimeContext {
*/ */
refreshContext(viewKey) { refreshContext(viewKey) {
const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier); const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier);
if (viewKey && key === viewKey) { if (viewKey && key === viewKey) {
return; return;
} }
@ -199,6 +359,7 @@ class IndependentTimeContext extends TimeContext {
// Emit bounds so that views that are changing context get the upstream bounds // Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.bounds()); this.emit('bounds', this.bounds());
this.emit('boundsChanged', this.getBounds());
} }
hasOwnContext() { hasOwnContext() {
@ -236,6 +397,7 @@ class IndependentTimeContext extends TimeContext {
*/ */
removeIndependentContext(viewKey) { removeIndependentContext(viewKey) {
const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier); const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier);
if (viewKey && key === viewKey) { if (viewKey && key === viewKey) {
//this is necessary as the upstream context gets reassigned after this //this is necessary as the upstream context gets reassigned after this
this.stopFollowingTimeContext(); this.stopFollowingTimeContext();
@ -261,7 +423,8 @@ class IndependentTimeContext extends TimeContext {
this.followTimeContext(); this.followTimeContext();
// Emit bounds so that views that are changing context get the upstream bounds // Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.bounds()); this.emit('bounds', this.getBounds());
this.emit('boundsChanged', this.getBounds());
// now that the view's context is set, tell others to check theirs in case they were following this view's context. // now that the view's context is set, tell others to check theirs in case they were following this view's context.
this.globalTimeContext.emit('refreshContext', viewKey); this.globalTimeContext.emit('refreshContext', viewKey);
} }

View File

@ -22,6 +22,7 @@
import GlobalTimeContext from "./GlobalTimeContext"; import GlobalTimeContext from "./GlobalTimeContext";
import IndependentTimeContext from "@/api/time/IndependentTimeContext"; import IndependentTimeContext from "@/api/time/IndependentTimeContext";
import {FIXED_MODE_KEY, REALTIME_MODE_KEY} from "@/api/time/constants";
/** /**
* The public API for setting and querying the temporal state of the * The public API for setting and querying the temporal state of the
@ -134,14 +135,27 @@ class TimeAPI extends GlobalTimeContext {
*/ */
addIndependentContext(key, value, clockKey) { addIndependentContext(key, value, clockKey) {
let timeContext = this.getIndependentContext(key); let timeContext = this.getIndependentContext(key);
let upstreamClock;
if (timeContext.upstreamTimeContext) {
upstreamClock = timeContext.upstreamTimeContext.getClock();
}
//stop following upstream time context since the view has it's own //stop following upstream time context since the view has it's own
timeContext.resetContext(); timeContext.resetContext();
if (clockKey) { if (clockKey) {
timeContext.clock(clockKey, value); timeContext.setMode(REALTIME_MODE_KEY);
timeContext.setClock(clockKey, value);
} else { } else {
timeContext.setMode(FIXED_MODE_KEY);
//TODO: Should the clock be stopped here?
timeContext.stopClock(); timeContext.stopClock();
timeContext.bounds(value); //upstream clock was active, but now we don't have one
if (upstreamClock) {
timeContext.emit('clockChanged', timeContext.activeClock);
}
timeContext.setBounds(value);
} }
// Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context // Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context
@ -179,12 +193,14 @@ class TimeAPI extends GlobalTimeContext {
const viewKey = objectPath.length && this.openmct.objects.makeKeyString(objectPath[0].identifier); const viewKey = objectPath.length && this.openmct.objects.makeKeyString(objectPath[0].identifier);
if (!viewKey) { if (!viewKey) {
// Return the global time context // Return the global time contex
return this; return this;
} }
let viewTimeContext = this.getIndependentContext(viewKey); let viewTimeContext = this.getIndependentContext(viewKey);
if (!viewTimeContext) { if (!viewTimeContext) {
console.log('no view context for viewKey', viewKey, this.openmct.objects.getRelativePath(objectPath));
// If the context doesn't exist yet, create it. // If the context doesn't exist yet, create it.
viewTimeContext = new IndependentTimeContext(this.openmct, this, objectPath); viewTimeContext = new IndependentTimeContext(this.openmct, this, objectPath);
this.independentContexts.set(viewKey, viewTimeContext); this.independentContexts.set(viewKey, viewTimeContext);
@ -193,6 +209,8 @@ class TimeAPI extends GlobalTimeContext {
const currentPath = this.openmct.objects.getRelativePath(viewTimeContext.objectPath); const currentPath = this.openmct.objects.getRelativePath(viewTimeContext.objectPath);
const newPath = this.openmct.objects.getRelativePath(objectPath); const newPath = this.openmct.objects.getRelativePath(objectPath);
console.log('view context exists, paths match?', !(currentPath !== newPath), currentPath, newPath, 'view key: ', viewKey);
if (currentPath !== newPath) { if (currentPath !== newPath) {
// If the path has changed, update the context. // If the path has changed, update the context.
this.independentContexts.delete(viewKey); this.independentContexts.delete(viewKey);

View File

@ -21,13 +21,7 @@
*****************************************************************************/ *****************************************************************************/
import EventEmitter from 'EventEmitter'; import EventEmitter from 'EventEmitter';
import { TIME_CONTEXT_EVENTS, MODES, REALTIME_MODE_KEY } from './constants';
export const TIME_CONTEXT_EVENTS = [
'bounds',
'clock',
'timeSystem',
'clockOffsets'
];
class TimeContext extends EventEmitter { class TimeContext extends EventEmitter {
constructor() { constructor() {
@ -47,6 +41,7 @@ class TimeContext extends EventEmitter {
this.activeClock = undefined; this.activeClock = undefined;
this.offsets = undefined; this.offsets = undefined;
this.mode = undefined;
this.tick = this.tick.bind(this); this.tick = this.tick.bind(this);
} }
@ -61,6 +56,8 @@ class TimeContext extends EventEmitter {
* @method timeSystem * @method timeSystem
*/ */
timeSystem(timeSystemOrKey, bounds) { timeSystem(timeSystemOrKey, bounds) {
this.#warnMethodDeprecated('"timeSystem"', '"getTimeSystem" and "setTimeSystem"');
if (arguments.length >= 1) { if (arguments.length >= 1) {
if (arguments.length === 1 && !this.activeClock) { if (arguments.length === 1 && !this.activeClock) {
throw new Error( throw new Error(
@ -91,7 +88,7 @@ class TimeContext extends EventEmitter {
throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key"; throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key";
} }
this.system = timeSystem; this.system = this.#copy(timeSystem);
/** /**
* The time system used by the time * The time system used by the time
@ -102,7 +99,7 @@ class TimeContext extends EventEmitter {
* @property {TimeSystem} The value of the currently applied * @property {TimeSystem} The value of the currently applied
* Time System * Time System
* */ * */
this.emit('timeSystem', this.system); this.emit('timeSystem', this.#copy(this.system));
if (bounds) { if (bounds) {
this.bounds(bounds); this.bounds(bounds);
} }
@ -163,6 +160,8 @@ class TimeContext extends EventEmitter {
* @method bounds * @method bounds
*/ */
bounds(newBounds) { bounds(newBounds) {
this.#warnMethodDeprecated('"bounds"', '"getBounds" and "setBounds"');
if (arguments.length > 0) { if (arguments.length > 0) {
const validationResult = this.validateBounds(newBounds); const validationResult = this.validateBounds(newBounds);
if (validationResult.valid !== true) { if (validationResult.valid !== true) {
@ -170,7 +169,7 @@ class TimeContext extends EventEmitter {
} }
//Create a copy to avoid direct mutation of conductor bounds //Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = JSON.parse(JSON.stringify(newBounds)); this.boundsVal = this.#copy(newBounds);
/** /**
* The start time, end time, or both have been updated. * The start time, end time, or both have been updated.
* @event bounds * @event bounds
@ -183,7 +182,7 @@ class TimeContext extends EventEmitter {
} }
//Return a copy to prevent direct mutation of time conductor bounds. //Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal)); return this.#copy(this.boundsVal);
} }
/** /**
@ -247,6 +246,8 @@ class TimeContext extends EventEmitter {
* @returns {ClockOffsets} * @returns {ClockOffsets}
*/ */
clockOffsets(offsets) { clockOffsets(offsets) {
this.#warnMethodDeprecated('"clockOffsets"', '"getClockOffsets" and "setClockOffsets"');
if (arguments.length > 0) { if (arguments.length > 0) {
const validationResult = this.validateOffsets(offsets); const validationResult = this.validateOffsets(offsets);
@ -278,11 +279,14 @@ class TimeContext extends EventEmitter {
} }
/** /**
* Stop the currently active clock from ticking, and unset it. This will * Stop following the currently active clock. This will
* revert all views to showing a static time frame defined by the current * revert all views to showing a static time frame defined by the current
* bounds. * bounds.
*/ */
stopClock() { stopClock() {
console.log('stop clock');
this.#warnMethodDeprecated('"stopClock"');
if (this.activeClock) { if (this.activeClock) {
this.clock(undefined, undefined); this.clock(undefined, undefined);
} }
@ -301,6 +305,8 @@ class TimeContext extends EventEmitter {
* @return {Clock} the currently active clock; * @return {Clock} the currently active clock;
*/ */
clock(keyOrClock, offsets) { clock(keyOrClock, offsets) {
this.#warnMethodDeprecated('"clock"', '"getClock" and "setClock"');
if (arguments.length === 2) { if (arguments.length === 2) {
let clock; let clock;
@ -354,25 +360,300 @@ class TimeContext extends EventEmitter {
return; return;
} }
const newBounds = { if (this.mode === REALTIME_MODE_KEY) {
start: timestamp + this.offsets.start, const newBounds = {
end: timestamp + this.offsets.end start: timestamp + this.offsets.start,
}; end: timestamp + this.offsets.end
};
this.boundsVal = newBounds; this.boundsVal = newBounds;
this.emit('bounds', this.boundsVal, true); // "bounds" will be deprecated in a future release
this.emit('bounds', this.boundsVal, true);
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsVal, true);
}
} }
/** /**
* Checks if this time context is in real-time mode or not. * Get the time system of the TimeAPI.
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method getTimeSystem
*/
getTimeSystem() {
return this.system;
}
/**
* Set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystemOrKey
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method setTimeSystem
*/
setTimeSystem(timeSystemOrKey, bounds) {
if (!this.isRealTime() && !bounds) {
throw new Error(
'Must specify bounds when changing time system without an active clock.'
);
}
if (timeSystemOrKey === undefined) {
throw 'Please provide a time system';
}
let timeSystem;
if (typeof timeSystemOrKey === 'string') {
timeSystem = this.timeSystems.get(timeSystemOrKey);
if (timeSystem === undefined) {
throw `Unknown time system ${timeSystemOrKey}. Has it been registered with 'addTimeSystem'?`;
}
} else if (typeof timeSystemOrKey === 'object') {
timeSystem = timeSystemOrKey;
if (!this.timeSystems.has(timeSystem.key)) {
throw `Unknown time system ${timeSystemOrKey.key}. Has it been registered with 'addTimeSystem'?`;
}
} else {
throw 'Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key';
}
this.system = this.#copy(timeSystem);
/**
* The time system used by the time
* conductor has changed. A change in Time System will always be
* followed by a bounds event specifying new query bounds.
*
* @event module:openmct.TimeAPI~timeSystem
* @property {TimeSystem} The value of the currently applied
* Time System
* */
this.emit(TIME_CONTEXT_EVENTS.timeSystemChanged, this.#copy(this.system));
if (bounds) {
this.setBounds(bounds);
}
return this.system;
}
/**
* Get the start and end time of the time conductor. Basic validation
* of bounds is performed.
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
getBounds() {
//Return a copy to prevent direct mutation of time conductor bounds.
return this.#copy(this.boundsVal);
}
/**
* Set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
setBounds(newBounds) {
const validationResult = this.validateBounds(newBounds);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
//Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = this.#copy(newBounds);
/**
* The start time, end time, or both have been updated.
* @event bounds
* @memberof module:openmct.TimeAPI~
* @property {TimeConductorBounds} bounds The newly updated bounds
* @property {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (i.e. was an automatic update), false otherwise.
*/
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsVal, false);
//Return a copy to prevent direct mutation of time conductor bounds.
return this.#copy(this.boundsVal);
}
/**
* Get the active clock.
* @return {Clock} the currently active clock;
*/
getClock() {
return this.activeClock;
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and the currently ticking will begin.
* Offsets from 'now', if provided, will be used to set realtime mode offsets
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds when in realtime mode.
* This maintains a sliding time window of a fixed width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
setClock(keyOrClock, offsets) {
let clock;
if (typeof keyOrClock === 'string') {
clock = this.clocks.get(keyOrClock);
if (clock === undefined) {
throw `Unknown clock ${keyOrClock}. Has it been registered with 'addClock'?`;
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.clocks.has(clock.key)) {
throw `Unknown clock ${keyOrClock.key}. Has it been registered with 'addClock'?`;
}
}
// this.setMode(REALTIME_MODE_KEY);
const previousClock = this.activeClock;
if (previousClock) {
previousClock.off('tick', this.tick);
}
this.activeClock = clock;
this.activeClock.on('tick', this.tick);
/**
* The active clock has changed.
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);
if (offsets !== undefined) {
this.setClockOffsets(offsets);
}
return this.activeClock;
}
/**
* Get the current mode.
* @return {Mode} the current mode;
*/
getMode() {
return this.mode;
}
/**
* Set the mode to either fixed or realtime.
*
* @param {Mode} mode The mode to activate
* @fires module:openmct.TimeAPI~clock
* @return {Mode} the currently active mode;
*/
setMode(mode) {
if (!mode || mode === this.mode) {
return;
}
this.mode = mode;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.mode));
return this.mode;
}
/**
* Checks if this time context is in realtime mode or not.
* @returns {boolean} true if this context is in real-time mode, false if not * @returns {boolean} true if this context is in real-time mode, false if not
*/ */
isRealTime() { isRealTime() {
if (this.clock()) { return this.mode === MODES.realtime;
return true; }
/**
* Checks if this time context is in fixed mode or not.
* @returns {boolean} true if this context is in fixed mode, false if not
*/
isFixed() {
return this.mode === MODES.fixed;
}
/**
* Get the currently applied clock offsets.
* @returns {ClockOffsets}
*/
getClockOffsets() {
return this.offsets;
}
/**
* Set the currently applied clock offsets. If no parameter is provided,
* the current value will be returned. If provided, the new value will be
* used as the new clock offsets.
* @param {ClockOffsets} offsets
* @returns {ClockOffsets}
*/
setClockOffsets(offsets) {
const validationResult = this.validateOffsets(offsets);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
} }
return false; this.offsets = this.#copy(offsets);
const currentValue = this.activeClock.currentValue();
const newBounds = {
start: currentValue + offsets.start,
end: currentValue + offsets.end
};
this.setBounds(newBounds);
/**
* Event that is triggered when clock offsets change.
* @event clockOffsets
* @memberof module:openmct.TimeAPI~
* @property {ClockOffsets} clockOffsets The newly activated clock
* offsets.
*/
this.emit(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.#copy(offsets));
return this.offsets;
}
#warnMethodDeprecated(method, newMethod) {
let message = `[DEPRECATION WARNING]: The ${method} API method is deprecated and will be removed in a future version of Open MCT.`;
if (newMethod) {
message += ` Please use the ${newMethod} API method(s) instead.`;
}
// TODO: add docs and point to them in warning.
// For more information and migration instructions, visit [link to documentation or migration guide].
console.warn(message);
}
#copy(object) {
return JSON.parse(JSON.stringify(object));
} }
} }

22
src/api/time/constants.js Normal file
View File

@ -0,0 +1,22 @@
export const TIME_CONTEXT_EVENTS = {
//old API events - to be deprecated
bounds: 'bounds',
clock: 'clock',
timeSystem: 'timeSystem',
clockOffsets: 'clockOffsets',
//new API events
tick: 'tick',
modeChanged: 'modeChanged',
boundsChanged: 'boundsChanged',
clockChanged: 'clockChanged',
timeSystemChanged: 'timeSystemChanged',
clockOffsetsChanged: 'clockOffsetsChanged'
};
export const REALTIME_MODE_KEY = 'realtime';
export const FIXED_MODE_KEY = 'fixed';
export const MODES = {
[FIXED_MODE_KEY]: FIXED_MODE_KEY,
[REALTIME_MODE_KEY]: REALTIME_MODE_KEY
};

View File

@ -20,14 +20,15 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
const TIME_EVENTS = ['timeSystem', 'clock', 'clockOffsets']; const TIME_EVENTS = ['timeSystemChanged', 'modeChanged', 'clockChanged', 'clockOffsetsChanged'];
const SEARCH_MODE = 'tc.mode'; const SEARCH_MODE = 'tc.mode';
const SEARCH_TIME_SYSTEM = 'tc.timeSystem'; const SEARCH_TIME_SYSTEM = 'tc.timeSystem';
const SEARCH_START_BOUND = 'tc.startBound'; const SEARCH_START_BOUND = 'tc.startBound';
const SEARCH_END_BOUND = 'tc.endBound'; const SEARCH_END_BOUND = 'tc.endBound';
const SEARCH_START_DELTA = 'tc.startDelta'; const SEARCH_START_DELTA = 'tc.startDelta';
const SEARCH_END_DELTA = 'tc.endDelta'; const SEARCH_END_DELTA = 'tc.endDelta';
const MODE_FIXED = 'fixed';
import { FIXED_MODE_KEY } from "../../api/time/constants";
export default class URLTimeSettingsSynchronizer { export default class URLTimeSettingsSynchronizer {
constructor(openmct) { constructor(openmct) {
@ -67,7 +68,7 @@ export default class URLTimeSettingsSynchronizer {
} }
updateTimeSettings() { updateTimeSettings() {
let timeParameters = this.parseParametersFromUrl(); const timeParameters = this.parseParametersFromUrl();
if (this.areTimeParametersValid(timeParameters)) { if (this.areTimeParametersValid(timeParameters)) {
this.setTimeApiFromUrl(timeParameters); this.setTimeApiFromUrl(timeParameters);
@ -78,21 +79,18 @@ export default class URLTimeSettingsSynchronizer {
} }
parseParametersFromUrl() { parseParametersFromUrl() {
let searchParams = this.openmct.router.getAllSearchParams(); const searchParams = this.openmct.router.getAllSearchParams();
const mode = searchParams.get(SEARCH_MODE);
let mode = searchParams.get(SEARCH_MODE); const timeSystem = searchParams.get(SEARCH_TIME_SYSTEM);
let timeSystem = searchParams.get(SEARCH_TIME_SYSTEM); const startBound = parseInt(searchParams.get(SEARCH_START_BOUND), 10);
const endBound = parseInt(searchParams.get(SEARCH_END_BOUND), 10);
let startBound = parseInt(searchParams.get(SEARCH_START_BOUND), 10); const bounds = {
let endBound = parseInt(searchParams.get(SEARCH_END_BOUND), 10);
let bounds = {
start: startBound, start: startBound,
end: endBound end: endBound
}; };
const startOffset = parseInt(searchParams.get(SEARCH_START_DELTA), 10);
let startOffset = parseInt(searchParams.get(SEARCH_START_DELTA), 10); const endOffset = parseInt(searchParams.get(SEARCH_END_DELTA), 10);
let endOffset = parseInt(searchParams.get(SEARCH_END_DELTA), 10); const clockOffsets = {
let clockOffsets = {
start: 0 - startOffset, start: 0 - startOffset,
end: endOffset end: endOffset
}; };
@ -106,30 +104,30 @@ export default class URLTimeSettingsSynchronizer {
} }
setTimeApiFromUrl(timeParameters) { setTimeApiFromUrl(timeParameters) {
if (timeParameters.mode === 'fixed') { const timeSystem = this.openmct.time.getTimeSystem();
if (this.openmct.time.timeSystem().key !== timeParameters.timeSystem) {
this.openmct.time.timeSystem( if (timeParameters.mode === FIXED_MODE_KEY) {
timeParameters.timeSystem, // should update timesystem
timeParameters.bounds if (timeSystem.key !== timeParameters.timeSystem) {
); this.openmct.time.setTimeSystem(timeParameters.timeSystem, timeParameters.bounds);
} else if (!this.areStartAndEndEqual(this.openmct.time.bounds(), timeParameters.bounds)) { } else if (!this.areStartAndEndEqual(this.openmct.time.getBounds(), timeParameters.bounds)) {
this.openmct.time.bounds(timeParameters.bounds); this.openmct.time.setBounds(timeParameters.bounds);
} }
if (this.openmct.time.clock()) { this.openmct.time.setMode('fixed');
this.openmct.time.stopClock();
}
} else { } else {
if (!this.openmct.time.clock() const clock = this.openmct.time.getClock();
|| this.openmct.time.clock().key !== timeParameters.mode) { console.log('setting as realtime', clock?.key, timeParameters.mode);
this.openmct.time.clock(timeParameters.mode, timeParameters.clockOffsets); if (clock?.key !== timeParameters.mode) {
} else if (!this.areStartAndEndEqual(this.openmct.time.clockOffsets(), timeParameters.clockOffsets)) { this.openmct.time.setClock(timeParameters.mode, timeParameters.clockOffsets);
this.openmct.time.clockOffsets(timeParameters.clockOffsets); } else if (!this.areStartAndEndEqual(this.openmct.time.getClockOffsets(), timeParameters.clockOffsets)) {
this.openmct.time.setClockOffsets(timeParameters.clockOffsets);
} }
if (!this.openmct.time.timeSystem() this.openmct.time.setMode('realtime');
|| this.openmct.time.timeSystem().key !== timeParameters.timeSystem) {
this.openmct.time.timeSystem(timeParameters.timeSystem); if (timeSystem?.key !== timeParameters.timeSystem) {
this.openmct.time.setTimeSystem(timeParameters.timeSystem);
} }
} }
} }
@ -141,13 +139,14 @@ export default class URLTimeSettingsSynchronizer {
} }
setUrlFromTimeApi() { setUrlFromTimeApi() {
let searchParams = this.openmct.router.getAllSearchParams(); const searchParams = this.openmct.router.getAllSearchParams();
let clock = this.openmct.time.clock(); const clock = this.openmct.time.getClock();
let bounds = this.openmct.time.bounds(); const mode = this.openmct.time.getMode();
let clockOffsets = this.openmct.time.clockOffsets(); const bounds = this.openmct.time.getBounds();
const clockOffsets = this.openmct.time.getClockOffsets();
if (clock === undefined) { if (mode === FIXED_MODE_KEY) {
searchParams.set(SEARCH_MODE, MODE_FIXED); searchParams.set(SEARCH_MODE, FIXED_MODE_KEY);
searchParams.set(SEARCH_START_BOUND, bounds.start); searchParams.set(SEARCH_START_BOUND, bounds.start);
searchParams.set(SEARCH_END_BOUND, bounds.end); searchParams.set(SEARCH_END_BOUND, bounds.end);
@ -168,8 +167,9 @@ export default class URLTimeSettingsSynchronizer {
searchParams.delete(SEARCH_END_BOUND); searchParams.delete(SEARCH_END_BOUND);
} }
searchParams.set(SEARCH_TIME_SYSTEM, this.openmct.time.timeSystem().key); searchParams.set(SEARCH_TIME_SYSTEM, this.openmct.time.getTimeSystem().key);
this.openmct.router.setAllSearchParams(searchParams); // this.openmct.router.setAllSearchParams(searchParams);
this.openmct.router.updateParams(searchParams);
} }
areTimeParametersValid(timeParameters) { areTimeParametersValid(timeParameters) {
@ -178,7 +178,7 @@ export default class URLTimeSettingsSynchronizer {
if (this.isModeValid(timeParameters.mode) if (this.isModeValid(timeParameters.mode)
&& this.isTimeSystemValid(timeParameters.timeSystem)) { && this.isTimeSystemValid(timeParameters.timeSystem)) {
if (timeParameters.mode === 'fixed') { if (timeParameters.mode === FIXED_MODE_KEY) {
isValid = this.areStartAndEndValid(timeParameters.bounds); isValid = this.areStartAndEndValid(timeParameters.bounds);
} else { } else {
isValid = this.areStartAndEndValid(timeParameters.clockOffsets); isValid = this.areStartAndEndValid(timeParameters.clockOffsets);
@ -200,8 +200,9 @@ export default class URLTimeSettingsSynchronizer {
isTimeSystemValid(timeSystem) { isTimeSystemValid(timeSystem) {
let isValid = timeSystem !== undefined; let isValid = timeSystem !== undefined;
if (isValid) { if (isValid) {
let timeSystemObject = this.openmct.time.timeSystems.get(timeSystem); const timeSystemObject = this.openmct.time.timeSystems.get(timeSystem);
isValid = timeSystemObject !== undefined; isValid = timeSystemObject !== undefined;
} }
@ -217,7 +218,7 @@ export default class URLTimeSettingsSynchronizer {
} }
if (isValid) { if (isValid) {
if (mode.toLowerCase() === MODE_FIXED) { if (mode.toLowerCase() === FIXED_MODE_KEY) {
isValid = true; isValid = true;
} else { } else {
isValid = this.openmct.time.clocks.get(mode) !== undefined; isValid = this.openmct.time.clocks.get(mode) !== undefined;
@ -228,7 +229,7 @@ export default class URLTimeSettingsSynchronizer {
} }
areStartAndEndEqual(firstBounds, secondBounds) { areStartAndEndEqual(firstBounds, secondBounds) {
return firstBounds.start === secondBounds.start return firstBounds?.start === secondBounds.start
&& firstBounds.end === secondBounds.end; && firstBounds?.end === secondBounds.end;
} }
} }

View File

@ -103,11 +103,11 @@ export default {
}, },
followTimeContext() { followTimeContext() {
this.timeContext.on('bounds', this.refreshData); this.timeContext.on('boundsChanged', this.refreshData);
}, },
stopFollowingTimeContext() { stopFollowingTimeContext() {
if (this.timeContext) { if (this.timeContext) {
this.timeContext.off('bounds', this.refreshData); this.timeContext.off('boundsChanged', this.refreshData);
} }
}, },
addToComposition(telemetryObject) { addToComposition(telemetryObject) {

View File

@ -288,7 +288,7 @@ export default {
seriesModels: [], seriesModels: [],
legend: {}, legend: {},
pending: 0, pending: 0,
isRealTime: this.openmct.time.clock() !== undefined, isRealTime: this.openmct.time.isRealTime(),
loaded: false, loaded: false,
isTimeOutOfSync: false, isTimeOutOfSync: false,
isFrozenOnMouseDown: false, isFrozenOnMouseDown: false,
@ -369,7 +369,7 @@ export default {
document.addEventListener('keydown', this.handleKeyDown); document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp); document.addEventListener('keyup', this.handleKeyUp);
eventHelpers.extend(this); eventHelpers.extend(this);
this.updateRealTime = this.updateRealTime.bind(this); this.updateMode = this.updateMode.bind(this);
this.updateDisplayBounds = this.updateDisplayBounds.bind(this); this.updateDisplayBounds = this.updateDisplayBounds.bind(this);
this.setTimeContext = this.setTimeContext.bind(this); this.setTimeContext = this.setTimeContext.bind(this);
@ -533,19 +533,20 @@ export default {
this.stopFollowingTimeContext(); this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.path); this.timeContext = this.openmct.time.getContextForView(this.path);
console.log('time context mctplot', this.timeContext, this.path);
this.followTimeContext(); this.followTimeContext();
}, },
followTimeContext() { followTimeContext() {
this.updateDisplayBounds(this.timeContext.bounds()); this.updateDisplayBounds(this.timeContext.getBounds());
this.timeContext.on('clock', this.updateRealTime); this.timeContext.on('modeChanged', this.updateMode);
this.timeContext.on('bounds', this.updateDisplayBounds); this.timeContext.on('boundsChanged', this.updateDisplayBounds);
this.synchronized(true); this.synchronized(true);
}, },
stopFollowingTimeContext() { stopFollowingTimeContext() {
if (this.timeContext) { if (this.timeContext) {
this.timeContext.off("clock", this.updateRealTime); this.timeContext.off("clockChanged", this.updateMode);
this.timeContext.off("bounds", this.updateDisplayBounds); this.timeContext.off("boundsChanged", this.updateDisplayBounds);
} }
}, },
getConfig() { getConfig() {
@ -753,8 +754,8 @@ export default {
const displayRange = series.getDisplayRange(xKey); const displayRange = series.getDisplayRange(xKey);
this.config.xAxis.set('range', displayRange); this.config.xAxis.set('range', displayRange);
}, },
updateRealTime(clock) { updateMode() {
this.isRealTime = clock !== undefined; this.isRealTime = this.timeContext.isRealTime();
}, },
/** /**
@ -815,13 +816,13 @@ export default {
* displays can update accordingly. * displays can update accordingly.
*/ */
synchronized(value) { synchronized(value) {
const isLocalClock = this.timeContext.clock(); const isRealTime = this.timeContext.isRealTime();
if (typeof value !== 'undefined') { if (typeof value !== 'undefined') {
this._synchronized = value; this._synchronized = value;
this.isTimeOutOfSync = value !== true; this.isTimeOutOfSync = value !== true;
const isUnsynced = isLocalClock && !value; const isUnsynced = isRealTime && !value;
this.setStatus(isUnsynced); this.setStatus(isUnsynced);
} }

View File

@ -86,17 +86,17 @@ export default {
this.xAxis = this.getXAxisFromConfig(); this.xAxis = this.getXAxisFromConfig();
this.loaded = true; this.loaded = true;
this.setUpXAxisOptions(); this.setUpXAxisOptions();
this.openmct.time.on('timeSystem', this.syncXAxisToTimeSystem); this.openmct.time.on('timeSystemChanged', this.syncXAxisToTimeSystem);
this.listenTo(this.xAxis, 'change', this.setUpXAxisOptions); this.listenTo(this.xAxis, 'change', this.setUpXAxisOptions);
}, },
beforeDestroy() { beforeDestroy() {
this.openmct.time.off('timeSystem', this.syncXAxisToTimeSystem); this.openmct.time.off('timeSystemChanged', this.syncXAxisToTimeSystem);
}, },
methods: { methods: {
isEnabledXKeyToggle() { isEnabledXKeyToggle() {
const isSinglePlot = this.xKeyOptions && this.xKeyOptions.length > 1 && this.seriesModel; const isSinglePlot = this.xKeyOptions && this.xKeyOptions.length > 1 && this.seriesModel;
const isFrozen = this.xAxis.get('frozen'); const isFrozen = this.xAxis.get('frozen');
const inRealTimeMode = this.openmct.time.clock(); const inRealTimeMode = this.openmct.time.getClock();
return isSinglePlot && !isFrozen && !inRealTimeMode; return isSinglePlot && !isFrozen && !inRealTimeMode;
}, },

View File

@ -21,60 +21,71 @@
*****************************************************************************/ *****************************************************************************/
<template> <template>
<div <div
class="c-conductor" ref="timeConductorOptionsHolder"
class="c-compact-tc is-expanded"
:class="[ :class="[
{ 'is-zooming': isZooming }, { 'is-zooming': isZooming },
{ 'is-panning': isPanning }, { 'is-panning': isPanning },
{ 'alt-pressed': altPressed }, { 'alt-pressed': altPressed },
isFixed ? 'is-fixed-mode' : 'is-realtime-mode' isFixed ? 'is-fixed-mode' : 'is-realtime-mode'
]" ]"
style="overflow: visible"
> >
<div class="c-conductor__time-bounds"> <ConductorModeIcon class="c-conductor__mode-icon" />
<conductor-inputs-fixed <!-- TODO - NEED TO ADD MODE, CLOCK AND TIMESYSTEM VIEW ONLY INFORMATION HERE -->
v-if="isFixed" <conductor-inputs-fixed
:input-bounds="viewBounds" v-if="isFixed"
@updated="saveFixedOffsets" :input-bounds="viewBounds"
/> :read-only="true"
<conductor-inputs-realtime />
v-else <conductor-inputs-realtime
:input-bounds="viewBounds" v-else
@updated="saveClockOffsets" :input-bounds="viewBounds"
/> :read-only="true"
<ConductorModeIcon class="c-conductor__mode-icon" /> />
<conductor-axis <conductor-axis
class="c-conductor__ticks" v-if="isFixed"
:view-bounds="viewBounds" class="c-conductor__ticks"
:is-fixed="isFixed" :view-bounds="viewBounds"
:alt-pressed="altPressed" :is-fixed="isFixed"
@endPan="endPan" :alt-pressed="altPressed"
@endZoom="endZoom" @endPan="endPan"
@panAxis="pan" @endZoom="endZoom"
@zoomAxis="zoom" @panAxis="pan"
/> @zoomAxis="zoom"
</div> />
<div class="c-conductor__controls"> <div
<ConductorMode class="c-conductor__mode-select" /> v-else
<ConductorTimeSystem class="c-conductor__time-system-select" /> class="u-flex-spreader"
<ConductorHistory ></div>
class="c-conductor__history-select" <div class="c-not-button c-not-button--compact c-compact-tc__gear icon-gear"></div>
:offsets="openmct.time.clockOffsets()"
:bounds="bounds" <conductor-pop-up
:time-system="timeSystem" v-if="showConductorPopup"
:mode="timeMode" ref="conductorPopup"
/> :bottom="false"
</div> :position-x="positionX"
:position-y="positionY"
:is-fixed="isFixed"
@popupLoaded="initializePopup"
@modeUpdated="saveMode"
@clockUpdated="saveClock"
@fixedBoundsUpdated="saveFixedBounds"
@clockOffsetsUpdated="saveClockOffsets"
@dismiss="clearPopup"
/>
</div> </div>
</template> </template>
<script> <script>
import _ from 'lodash'; import _ from 'lodash';
import ConductorMode from './ConductorMode.vue'; import { FIXED_MODE_KEY, TIME_CONTEXT_EVENTS } from '../../api/time/constants';
import ConductorTimeSystem from './ConductorTimeSystem.vue';
import ConductorAxis from './ConductorAxis.vue'; import ConductorAxis from './ConductorAxis.vue';
import ConductorModeIcon from './ConductorModeIcon.vue'; import ConductorModeIcon from './ConductorModeIcon.vue';
import ConductorHistory from './ConductorHistory.vue';
import ConductorInputsFixed from "./ConductorInputsFixed.vue"; import ConductorInputsFixed from "./ConductorInputsFixed.vue";
import ConductorInputsRealtime from "./ConductorInputsRealtime.vue"; import ConductorInputsRealtime from "./ConductorInputsRealtime.vue";
import conductorPopUpManager from "./conductorPopUpManager";
import ConductorPopUp from "./ConductorPopUp.vue";
const DEFAULT_DURATION_FORMATTER = 'duration'; const DEFAULT_DURATION_FORMATTER = 'duration';
@ -82,24 +93,24 @@ export default {
components: { components: {
ConductorInputsRealtime, ConductorInputsRealtime,
ConductorInputsFixed, ConductorInputsFixed,
ConductorMode,
ConductorTimeSystem,
ConductorAxis, ConductorAxis,
ConductorModeIcon, ConductorModeIcon,
ConductorHistory ConductorPopUp
}, },
mixins: [conductorPopUpManager],
inject: ['openmct', 'configuration'], inject: ['openmct', 'configuration'],
data() { data() {
let bounds = this.openmct.time.bounds(); const isFixed = this.openmct.time.isFixed();
let offsets = this.openmct.time.clockOffsets(); const bounds = this.openmct.time.getBounds();
let timeSystem = this.openmct.time.timeSystem(); const offsets = this.openmct.time.getClockOffsets();
let timeFormatter = this.getFormatter(timeSystem.timeFormat); const timeSystem = this.openmct.time.getTimeSystem();
let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); const timeFormatter = this.getFormatter(timeSystem.timeFormat);
const durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
return { return {
timeSystem: timeSystem, timeSystem,
timeFormatter: timeFormatter, timeFormatter,
durationFormatter: durationFormatter, durationFormatter,
offsets: { offsets: {
start: offsets && durationFormatter.format(Math.abs(offsets.start)), start: offsets && durationFormatter.format(Math.abs(offsets.start)),
end: offsets && durationFormatter.format(Math.abs(offsets.end)) end: offsets && durationFormatter.format(Math.abs(offsets.end))
@ -116,37 +127,41 @@ export default {
start: bounds.start, start: bounds.start,
end: bounds.end end: bounds.end
}, },
isFixed: this.openmct.time.clock() === undefined, isFixed,
isUTCBased: timeSystem.isUTCBased, isUTCBased: timeSystem.isUTCBased,
showDatePicker: false, showDatePicker: false,
showConductorPopup: false,
altPressed: false, altPressed: false,
isPanning: false, isPanning: false,
isZooming: false, isZooming: false
showTCInputStart: false,
showTCInputEnd: false
}; };
}, },
computed: {
timeMode() {
return this.isFixed ? 'fixed' : 'realtime';
}
},
mounted() { mounted() {
document.addEventListener('keydown', this.handleKeyDown); document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp); document.addEventListener('keyup', this.handleKeyUp);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem())));
this.openmct.time.on('bounds', _.throttle(this.handleNewBounds, 300)); this.setTimeSystem(this.copy(this.openmct.time.getTimeSystem()));
this.openmct.time.on('timeSystem', this.setTimeSystem);
this.openmct.time.on('clock', this.setViewFromClock); this.openmct.time.on(TIME_CONTEXT_EVENTS.boundsChanged, _.throttle(this.handleNewBounds, 300));
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.openmct.time.on(TIME_CONTEXT_EVENTS.modeChanged, this.setMode);
// this.openmct.time.on('clockChanged', this.setViewFromClock);
}, },
beforeDestroy() { beforeDestroy() {
document.removeEventListener('keydown', this.handleKeyDown); document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp); document.removeEventListener('keyup', this.handleKeyUp);
this.openmct.time.off(TIME_CONTEXT_EVENTS.boundsChanged, _.throttle(this.handleNewBounds, 300));
this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.openmct.time.off(TIME_CONTEXT_EVENTS.modeChanged, this.setMode);
// this.openmct.time.off('clockChanged', this.setViewFromClock);
}, },
methods: { methods: {
handleNewBounds(bounds) { handleNewBounds(bounds, isTick) {
this.setBounds(bounds); if (this.openmct.time.isRealTime() || !isTick) {
this.setViewFromBounds(bounds); this.setBounds(bounds);
this.setViewFromBounds(bounds);
}
}, },
setBounds(bounds) { setBounds(bounds) {
this.bounds = bounds; this.bounds = bounds;
@ -195,9 +210,8 @@ export default {
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.isUTCBased = timeSystem.isUTCBased; this.isUTCBased = timeSystem.isUTCBased;
}, },
setViewFromClock(clock) { setMode(mode) {
// this.clearAllValidation(); this.isFixed = mode === FIXED_MODE_KEY;
this.isFixed = clock === undefined;
}, },
setViewFromBounds(bounds) { setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start); this.formattedBounds.start = this.timeFormatter.format(bounds.start);
@ -210,11 +224,20 @@ export default {
format: key format: key
}).formatter; }).formatter;
}, },
saveClockOffsets(offsets) { saveFixedBounds(bounds) {
this.openmct.time.clockOffsets(offsets);
},
saveFixedOffsets(bounds) {
this.openmct.time.bounds(bounds); this.openmct.time.bounds(bounds);
},
saveClockOffsets(offsets) {
this.openmct.time.setClockOffsets(offsets);
},
saveClock(clockOptions) {
this.openmct.time.setClock(clockOptions.clockKey, clockOptions.offsets);
},
saveMode(mode) {
this.openmct.time.setMode(mode);
},
copy(object) {
return JSON.parse(JSON.stringify(object));
} }
} }
}; };

View File

@ -38,6 +38,7 @@ import * as d3Selection from 'd3-selection';
import * as d3Axis from 'd3-axis'; import * as d3Axis from 'd3-axis';
import * as d3Scale from 'd3-scale'; import * as d3Scale from 'd3-scale';
import utcMultiTimeFormat from './utcMultiTimeFormat.js'; import utcMultiTimeFormat from './utcMultiTimeFormat.js';
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants';
const PADDING = 1; const PADDING = 1;
const DEFAULT_DURATION_FORMATTER = 'duration'; const DEFAULT_DURATION_FORMATTER = 'duration';
@ -92,13 +93,16 @@ export default {
this.axisElement = vis.append("g") this.axisElement = vis.append("g")
.attr("class", "axis"); .attr("class", "axis");
this.setViewFromTimeSystem(this.openmct.time.timeSystem()); this.setViewFromTimeSystem(this.openmct.time.getTimeSystem());
this.setAxisDimensions(); this.setAxisDimensions();
this.setScale(); this.setScale();
//Respond to changes in conductor //Respond to changes in conductor
this.openmct.time.on("timeSystem", this.setViewFromTimeSystem); this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setViewFromTimeSystem);
setInterval(this.resize, RESIZE_POLL_INTERVAL); this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
},
beforeDestroy() {
clearInterval(this.resizeTimer);
}, },
methods: { methods: {
setAxisDimensions() { setAxisDimensions() {
@ -113,7 +117,7 @@ export default {
return; return;
} }
let timeSystem = this.openmct.time.timeSystem(); let timeSystem = this.openmct.time.getTimeSystem();
if (timeSystem.isUTCBased) { if (timeSystem.isUTCBased) {
this.xScale.domain( this.xScale.domain(
@ -153,7 +157,7 @@ export default {
this.setScale(); this.setScale();
}, },
getActiveFormatter() { getActiveFormatter() {
let timeSystem = this.openmct.time.timeSystem(); let timeSystem = this.openmct.time.getTimeSystem();
if (this.isFixed) { if (this.isFixed) {
return this.getFormatter(timeSystem.timeFormat); return this.getFormatter(timeSystem.timeFormat);
@ -224,7 +228,7 @@ export default {
this.inPanMode = false; this.inPanMode = false;
}, },
getPanBounds() { getPanBounds() {
const bounds = this.openmct.time.bounds(); const bounds = this.openmct.time.getBounds();
const deltaTime = bounds.end - bounds.start; const deltaTime = bounds.end - bounds.start;
const deltaX = this.dragX - this.dragStartX; const deltaX = this.dragX - this.dragStartX;
const percX = deltaX / this.width; const percX = deltaX / this.width;
@ -291,7 +295,7 @@ export default {
}; };
}, },
scaleToBounds(value) { scaleToBounds(value) {
const bounds = this.openmct.time.bounds(); const bounds = this.openmct.time.getBounds();
const timeDelta = bounds.end - bounds.start; const timeDelta = bounds.end - bounds.start;
const valueDelta = value - this.left; const valueDelta = value - this.left;
const offset = valueDelta / this.width * timeDelta; const offset = valueDelta / this.width * timeDelta;

View File

@ -0,0 +1,142 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web 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 Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div
ref="clockButton"
class="c-tc-input-popup__options"
>
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
class="c-button--menu c-button--compact js-clock-button"
:class="[
buttonCssClass,
selectedClock.cssClass
]"
@click.prevent.stop="showClocksMenu"
>
<span class="c-button__label">{{ selectedClock.name }}</span>
</button>
</div>
</div>
</template>
<script>
import modeMixin from './mode-mixin';
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants';
export default {
mixins: [modeMixin],
inject: {
openmct: 'openmct',
configuration: {
from: 'configuration',
default: undefined
}
},
data: function () {
const activeClock = this.getActiveClock();
return {
selectedClock: activeClock ? this.getClockMetadata(activeClock) : undefined,
clocks: []
};
},
mounted: function () {
this.loadClocks(this.configuration.menuOptions);
// this.setOffsets();
this.openmct.time.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
},
destroyed: function () {
this.openmct.time.off(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
},
methods: {
// setOffsets() {
// if (!this.openmct.time.getClockOffsets()) {
// const activeClock = this.getActiveClock();
// const clockConfig = this.getMatchingConfig({
// clock: activeClock.key
// });
// this.openmct.time.setClockOffsets(clockConfig.clockOffsets);
// }
// },
showClocksMenu() {
const elementBoundingClientRect = this.$refs.clockButton.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y;
const menuOptions = {
menuClass: 'c-conductor__clock-menu',
placement: this.openmct.menus.menuPlacement.TOP_RIGHT
};
this.dismiss = this.openmct.menus.showSuperMenu(x, y, this.clocks, menuOptions);
},
setClock(clockKey) {
const option = {
clockKey
};
let configuration = this.getMatchingConfig({
clock: clockKey,
timeSystem: this.openmct.time.getTimeSystem().key
});
if (configuration === undefined) {
configuration = this.getMatchingConfig({
clock: clockKey
});
option.timeSystem = configuration.timeSystem;
option.bounds = configuration.bounds;
// this.openmct.time.timeSystem(configuration.timeSystem, configuration.bounds);
}
const offsets = this.openmct.time.getClockOffsets() ?? configuration.clockOffsets;
option.offsets = offsets;
this.$emit('clockUpdated', option);
},
getMatchingConfig(options) {
const matchers = {
clock(config) {
return options.clock === config.clock;
},
timeSystem(config) {
return options.timeSystem === config.timeSystem;
}
};
function configMatches(config) {
return Object.keys(options).reduce((match, option) => {
return match && matchers[option](config);
}, true);
}
return this.configuration.menuOptions.filter(configMatches)[0];
},
setViewFromClock(clock) {
this.activeClock = clock;
}
}
};
</script>

View File

@ -26,8 +26,9 @@
> >
<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">
<button <button
class="c-button--menu c-button--compact c-history-button icon-history"
:class="buttonCssClass"
aria-label="Time Conductor History" aria-label="Time Conductor History"
class="c-button--menu c-history-button icon-history"
@click.prevent.stop="showHistoryMenu" @click.prevent.stop="showHistoryMenu"
> >
<span class="c-button__label">History</span> <span class="c-button__label">History</span>
@ -64,6 +65,13 @@ export default {
mode: { mode: {
type: String, type: String,
required: true required: true
},
buttonCssClass: {
type: String,
required: false,
default() {
return '';
}
} }
}, },
data() { data() {
@ -81,7 +89,7 @@ export default {
*/ */
fixedHistory: {}, fixedHistory: {},
presets: [], presets: [],
isFixed: this.openmct.time.clock() === undefined isFixed: this.openmct.time.getClock() === undefined
}; };
}, },
computed: { computed: {
@ -95,7 +103,7 @@ export default {
}, },
storageKey() { storageKey() {
let key = LOCAL_STORAGE_HISTORY_KEY_FIXED; let key = LOCAL_STORAGE_HISTORY_KEY_FIXED;
if (!this.isFixed) { if (this.openmct.time.isRealTime()) {
key = LOCAL_STORAGE_HISTORY_KEY_REALTIME; key = LOCAL_STORAGE_HISTORY_KEY_REALTIME;
} }
@ -106,8 +114,8 @@ export default {
bounds: { bounds: {
handler() { handler() {
// only for fixed time since we track offsets for realtime // only for fixed time since we track offsets for realtime
if (this.isFixed) { this.updateMode();
this.updateMode(); if (this.openmct.time.isFixed()) {
this.addTimespan(); this.addTimespan();
} }
}, },
@ -116,7 +124,9 @@ export default {
offsets: { offsets: {
handler() { handler() {
this.updateMode(); this.updateMode();
this.addTimespan(); if (this.openmct.time.isRealTime()) {
this.addTimespan();
}
}, },
deep: true deep: true
}, },
@ -140,7 +150,6 @@ export default {
}, },
methods: { methods: {
updateMode() { updateMode() {
this.isFixed = this.openmct.time.clock() === undefined;
this.getHistoryFromLocalStorage(); this.getHistoryFromLocalStorage();
this.initializeHistoryIfNoHistory(); this.initializeHistoryIfNoHistory();
}, },
@ -151,7 +160,7 @@ export default {
const startTime = this.formatTime(timespan.start); const startTime = this.formatTime(timespan.start);
const description = `${this.formatTime(timespan.start, descriptionDateFormat)} - ${this.formatTime(timespan.end, descriptionDateFormat)}`; const description = `${this.formatTime(timespan.start, descriptionDateFormat)} - ${this.formatTime(timespan.end, descriptionDateFormat)}`;
if (this.timeSystem.isUTCBased && !this.openmct.time.clock()) { if (this.timeSystem.isUTCBased && !this.openmct.time.isRealTime()) {
name = `${startTime} ${millisecondsToDHMS(timespan.end - timespan.start)}`; name = `${startTime} ${millisecondsToDHMS(timespan.end - timespan.start)}`;
} else { } else {
name = description; name = description;
@ -201,10 +210,11 @@ export default {
}, },
addTimespan() { addTimespan() {
const key = this.timeSystem.key; const key = this.timeSystem.key;
const isFixed = this.openmct.time.isFixed();
let [...currentHistory] = this[this.currentHistory][key] || []; let [...currentHistory] = this[this.currentHistory][key] || [];
const timespan = { const timespan = {
start: this.isFixed ? this.bounds.start : this.offsets.start, start: isFixed ? this.bounds.start : this.offsets.start,
end: this.isFixed ? this.bounds.end : this.offsets.end end: isFixed ? this.bounds.end : this.offsets.end
}; };
// no dupes // no dupes
@ -219,10 +229,10 @@ export default {
this.persistHistoryToLocalStorage(); this.persistHistoryToLocalStorage();
}, },
selectTimespan(timespan) { selectTimespan(timespan) {
if (this.isFixed) { if (this.openmct.time.isFixed()) {
this.openmct.time.bounds(timespan); this.openmct.time.getBounds(timespan);
} else { } else {
this.openmct.time.clockOffsets(timespan); this.openmct.time.getClockOffsets(timespan);
} }
}, },
selectPresetBounds(bounds) { selectPresetBounds(bounds) {
@ -259,7 +269,7 @@ export default {
let format = this.timeSystem.timeFormat; let format = this.timeSystem.timeFormat;
let isNegativeOffset = false; let isNegativeOffset = false;
if (!this.isFixed) { if (!this.openmct.time.isFixed()) {
if (time < 0) { if (time < 0) {
isNegativeOffset = true; isNegativeOffset = true;
} }

View File

@ -1,78 +1,35 @@
<template> <template>
<form <time-popup-fixed
ref="fixedDeltaInput" v-if="readOnly === false"
class="c-conductor__inputs" :input-bounds="bounds"
:input-time-system="timeSystem"
@focus.native="$event.target.select()"
@update="setBoundsFromView"
@dismiss="dismiss"
/>
<div
v-else
class="c-compact-tc__bounds"
> >
<div <div class="c-compact-tc__bounds__value">{{ formattedBounds.start }}</div>
class="c-ctrl-wrapper c-conductor-input c-conductor__start-fixed" <div class="c-compact-tc__bounds__start-end-sep icon-arrows-right-left"></div>
> <div class="c-compact-tc__bounds__value">{{ formattedBounds.end }}</div>
<!-- Fixed start --> </div>
<div class="c-conductor__start-fixed__label">
Start
</div>
<input
ref="startDate"
v-model="formattedBounds.start"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds('startDate'); submitForm()"
>
<date-picker
v-if="isUTCBased"
class="c-ctrl-wrapper--menus-left"
:bottom="keyString !== undefined"
:default-date-time="formattedBounds.start"
:formatter="timeFormatter"
@date-selected="startDateSelected"
/>
</div>
<div class="c-ctrl-wrapper c-conductor-input c-conductor__end-fixed">
<!-- Fixed end and RT 'last update' display -->
<div class="c-conductor__end-fixed__label">
End
</div>
<input
ref="endDate"
v-model="formattedBounds.end"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds('endDate'); submitForm()"
>
<date-picker
v-if="isUTCBased"
class="c-ctrl-wrapper--menus-left"
:bottom="keyString !== undefined"
:default-date-time="formattedBounds.end"
:formatter="timeFormatter"
@date-selected="endDateSelected"
/>
</div>
</form>
</template> </template>
<script> <script>
import TimePopupFixed from "./timePopupFixed.vue";
import DatePicker from "./DatePicker.vue";
import _ from "lodash"; import _ from "lodash";
import { TIME_CONTEXT_EVENTS } from "../../api/time/constants";
const DEFAULT_DURATION_FORMATTER = 'duration'; // const DEFAULT_DURATION_FORMATTER = 'duration';
export default { export default {
components: { components: {
DatePicker TimePopupFixed
}, },
inject: ['openmct'], inject: ['openmct'],
props: { props: {
keyString: {
type: String,
default() {
return undefined;
}
},
inputBounds: { inputBounds: {
type: Object, type: Object,
default() { default() {
@ -84,18 +41,29 @@ export default {
default() { default() {
return []; return [];
} }
},
readOnly: {
type: Boolean,
default() {
return false;
}
},
compact: {
type: Boolean,
default() {
return false;
}
} }
}, },
data() { data() {
let timeSystem = this.openmct.time.timeSystem(); const timeSystem = this.openmct.time.getTimeSystem();
let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); // let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
let timeFormatter = this.getFormatter(timeSystem.timeFormat); const timeFormatter = this.getFormatter(timeSystem.timeFormat);
let bounds = this.bounds || this.openmct.time.bounds(); let bounds = this.inputBounds || this.openmct.time.getBounds();
console.log('fixed input bounds', this.inputBounds);
return { return {
showTCInputStart: true, timeSystem: timeSystem,
showTCInputEnd: true, // durationFormatter,
durationFormatter,
timeFormatter, timeFormatter,
bounds: { bounds: {
start: bounds.start, start: bounds.start,
@ -109,8 +77,15 @@ export default {
}; };
}, },
watch: { watch: {
keyString() { objectPath: {
this.setTimeContext(); handler(newPath, oldPath) {
if (newPath === oldPath) {
return;
}
this.setTimeContext();
},
deep: true
}, },
inputBounds: { inputBounds: {
handler(newBounds) { handler(newBounds) {
@ -122,40 +97,30 @@ export default {
mounted() { mounted() {
this.handleNewBounds = _.throttle(this.handleNewBounds, 300); this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem()))); this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem())));
this.openmct.time.on('timeSystem', this.setTimeSystem); this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.setTimeContext(); this.setTimeContext();
}, },
beforeDestroy() { beforeDestroy() {
this.clearAllValidation(); this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.openmct.time.off('timeSystem', this.setTimeSystem);
this.stopFollowingTimeContext(); this.stopFollowingTimeContext();
}, },
methods: { methods: {
setTimeContext() { setTimeContext() {
this.stopFollowingTimeContext(); this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.keyString ? this.objectPath : []); this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.handleNewBounds(this.timeContext.bounds()); this.handleNewBounds(this.timeContext.getBounds());
this.timeContext.on('bounds', this.handleNewBounds); this.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
this.timeContext.on('clock', this.clearAllValidation);
}, },
stopFollowingTimeContext() { stopFollowingTimeContext() {
if (this.timeContext) { if (this.timeContext) {
this.timeContext.off('bounds', this.handleNewBounds); this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
this.timeContext.off('clock', this.clearAllValidation);
} }
}, },
handleNewBounds(bounds) { handleNewBounds(bounds) {
this.setBounds(bounds); this.setBounds(bounds);
this.setViewFromBounds(bounds); this.setViewFromBounds(bounds);
}, },
clearAllValidation() {
[this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput);
},
clearValidationForInput(input) {
input.setCustomValidity('');
input.title = '';
},
setBounds(bounds) { setBounds(bounds) {
this.bounds = bounds; this.bounds = bounds;
}, },
@ -166,8 +131,8 @@ export default {
setTimeSystem(timeSystem) { setTimeSystem(timeSystem) {
this.timeSystem = timeSystem; this.timeSystem = timeSystem;
this.timeFormatter = this.getFormatter(timeSystem.timeFormat); this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
this.durationFormatter = this.getFormatter( // this.durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); // timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.isUTCBased = timeSystem.isUTCBased; this.isUTCBased = timeSystem.isUTCBased;
}, },
getFormatter(key) { getFormatter(key) {
@ -175,116 +140,15 @@ export default {
format: key format: key
}).formatter; }).formatter;
}, },
setBoundsFromView($event) { setBoundsFromView(bounds) {
if (this.$refs.fixedDeltaInput.checkValidity()) { console.log('conductor fixed bounds set bounds from view', bounds);
let start = this.timeFormatter.parse(this.formattedBounds.start); this.$emit('boundsUpdated', {
let end = this.timeFormatter.parse(this.formattedBounds.end); start: bounds.start,
end: bounds.end
this.$emit('updated', {
start: start,
end: end
});
}
if ($event) {
$event.preventDefault();
return false;
}
},
submitForm() {
// Allow Vue model to catch up to user input.
// Submitting form will cause validation messages to display (but only if triggered by button click)
this.$nextTick(() => this.setBoundsFromView());
},
validateAllBounds(ref) {
if (!this.areBoundsFormatsValid()) {
return false;
}
let validationResult = {
valid: true
};
const currentInput = this.$refs[ref];
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
let boundsValues = {
start: this.timeFormatter.parse(this.formattedBounds.start),
end: this.timeFormatter.parse(this.formattedBounds.end)
};
//TODO: Do we need limits here? We have conductor limits disabled right now
// const limit = this.getBoundsLimit();
const limit = false;
if (this.timeSystem.isUTCBased && limit
&& boundsValues.end - boundsValues.start > limit) {
if (input === currentInput) {
validationResult = {
valid: false,
message: "Start and end difference exceeds allowable limit"
};
}
} else {
if (input === currentInput) {
validationResult = this.openmct.time.validateBounds(boundsValues);
}
}
return this.handleValidationResults(input, validationResult);
}); });
}, },
areBoundsFormatsValid() { dismiss() {
let validationResult = { this.$emit('dismissInputsFixed');
valid: true
};
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
const formattedDate = input === this.$refs.startDate
? this.formattedBounds.start
: this.formattedBounds.end
;
if (!this.timeFormatter.validate(formattedDate)) {
validationResult = {
valid: false,
message: 'Invalid date'
};
}
return this.handleValidationResults(input, validationResult);
});
},
getBoundsLimit() {
const configuration = this.configuration.menuOptions
.filter(option => option.timeSystem === this.timeSystem.key)
.find(option => option.limit);
const limit = configuration ? configuration.limit : undefined;
return limit;
},
handleValidationResults(input, validationResult) {
if (validationResult.valid !== true) {
input.setCustomValidity(validationResult.message);
input.title = validationResult.message;
} else {
input.setCustomValidity('');
input.title = '';
}
this.$refs.fixedDeltaInput.reportValidity();
return validationResult.valid;
},
startDateSelected(date) {
this.formattedBounds.start = this.timeFormatter.format(date);
this.validateAllBounds('startDate');
this.submitForm();
},
endDateSelected(date) {
this.formattedBounds.end = this.timeFormatter.format(date);
this.validateAllBounds('endDate');
this.submitForm();
} }
} }
}; };

View File

@ -1,95 +1,44 @@
<template> <template>
<form <time-popup-realtime
ref="deltaInput" v-if="readOnly === false"
class="c-conductor__inputs" :offsets="offsets"
@focus.native="$event.target.select()"
@update="timePopUpdate"
@dismiss="dismiss"
/>
<div
v-else
class="c-compact-tc__bounds"
> >
<div class="c-compact-tc__bounds__value icon-minus">{{ offsets.start }}</div>
<div <div
class="c-ctrl-wrapper c-conductor-input c-conductor__start-delta" v-if="compact"
> class="c-compact-tc__bounds__start-end-sep icon-arrows-right-left"
<!-- RT start --> ></div>
<div class="c-direction-indicator icon-minus"></div>
<time-popup
v-if="showTCInputStart"
class="pr-tc-input-menu--start"
:bottom="keyString !== undefined"
:type="'start'"
:offset="offsets.start"
@focus.native="$event.target.select()"
@hide="hideAllTimePopups"
@update="timePopUpdate"
/>
<button
ref="startOffset"
class="c-button c-conductor__delta-button"
title="Set the time offset after now"
data-testid="conductor-start-offset-button"
@click.prevent.stop="showTimePopupStart"
>
{{ offsets.start }}
</button>
</div>
<div class="c-ctrl-wrapper c-conductor-input c-conductor__end-fixed">
<!-- RT 'last update' display -->
<div class="c-conductor__end-fixed__label">
Current
</div>
<input
ref="endDate"
v-model="formattedCurrentValue"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
:disabled="true"
>
</div>
<div <div
class="c-ctrl-wrapper c-conductor-input c-conductor__end-delta" v-else
class="c-compact-tc__current-update"
> >
<!-- RT end --> LAST UPDATE {{ formattedBounds.end }}
<div class="c-direction-indicator icon-plus"></div>
<time-popup
v-if="showTCInputEnd"
class="pr-tc-input-menu--end"
:bottom="keyString !== undefined"
:type="'end'"
:offset="offsets.end"
@focus.native="$event.target.select()"
@hide="hideAllTimePopups"
@update="timePopUpdate"
/>
<button
ref="endOffset"
class="c-button c-conductor__delta-button"
title="Set the time offset preceding now"
data-testid="conductor-end-offset-button"
@click.prevent.stop="showTimePopupEnd"
>
{{ offsets.end }}
</button>
</div> </div>
</form> <div class="c-compact-tc__bounds__value icon-plus">{{ offsets.end }}</div>
</div>
</template> </template>
<script> <script>
import timePopup from "./timePopup.vue"; import TimePopupRealtime from "./timePopupRealtime.vue";
import _ from "lodash"; import _ from "lodash";
import { TIME_CONTEXT_EVENTS } from "../../api/time/constants";
const DEFAULT_DURATION_FORMATTER = 'duration'; const DEFAULT_DURATION_FORMATTER = 'duration';
export default { export default {
components: { components: {
timePopup TimePopupRealtime
}, },
inject: ['openmct'], inject: ['openmct'],
props: { props: {
keyString: {
type: String,
default() {
return undefined;
}
},
objectPath: { objectPath: {
type: Array, type: Array,
default() { default() {
@ -101,15 +50,27 @@ export default {
default() { default() {
return undefined; return undefined;
} }
},
readOnly: {
type: Boolean,
default() {
return false;
}
},
compact: {
type: Boolean,
default() {
return false;
}
} }
}, },
data() { data() {
let timeSystem = this.openmct.time.timeSystem(); const timeSystem = this.openmct.time.getTimeSystem();
let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); const durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
let timeFormatter = this.getFormatter(timeSystem.timeFormat); const timeFormatter = this.getFormatter(timeSystem.timeFormat);
let bounds = this.bounds || this.openmct.time.bounds(); const bounds = this.bounds ?? this.openmct.time.getBounds();
let offsets = this.openmct.time.clockOffsets(); const offsets = this.offsets ?? this.openmct.time.getClockOffsets();
let currentValue = this.openmct.time.clock()?.currentValue(); const currentValue = this.openmct.time.getClock()?.currentValue();
return { return {
showTCInputStart: false, showTCInputStart: false,
@ -134,8 +95,15 @@ export default {
}; };
}, },
watch: { watch: {
keyString() { objectPath: {
this.setTimeContext(); handler(newPath, oldPath) {
if (newPath === oldPath) {
return;
}
this.setTimeContext();
},
deep: true
}, },
inputBounds: { inputBounds: {
handler(newBounds) { handler(newBounds) {
@ -146,45 +114,50 @@ export default {
}, },
mounted() { mounted() {
this.handleNewBounds = _.throttle(this.handleNewBounds, 300); this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem()))); this.setTimeSystem(this.copy(this.openmct.time.getTimeSystem()));
this.openmct.time.on('timeSystem', this.setTimeSystem); this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.setTimeContext(); this.setTimeContext();
}, },
beforeDestroy() { beforeDestroy() {
this.openmct.time.off('timeSystem', this.setTimeSystem); this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.stopFollowingTime(); this.stopFollowingTime();
}, },
methods: { methods: {
followTime() { followTime() {
this.handleNewBounds(this.timeContext.bounds()); const bounds = this.timeContext ? this.timeContext.getBounds() : this.openmct.time.getBounds();
this.setViewFromOffsets(this.timeContext.clockOffsets()); const offsets = this.timeContext ? this.timeContext.getClockOffsets() : this.openmct.time.getClockOffsets();
this.timeContext.on('bounds', this.handleNewBounds);
this.timeContext.on('clock', this.clearAllValidation); this.handleNewBounds(bounds);
this.timeContext.on('clockOffsets', this.setViewFromOffsets); this.setViewFromOffsets(offsets);
if (this.timeContext) {
this.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
this.timeContext.on(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.setViewFromOffsets);
} else {
this.openmct.time.on(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
this.openmct.time.on(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.setViewFromOffsets);
}
}, },
stopFollowingTime() { stopFollowingTime() {
if (this.timeContext) { if (this.timeContext) {
this.timeContext.off('bounds', this.handleNewBounds); this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
this.timeContext.off('clock', this.clearAllValidation); this.timeContext.off(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.setViewFromOffsets);
this.timeContext.off('clockOffsets', this.setViewFromOffsets); } else {
this.openmct.time.off(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
this.openmct.time.off(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.setViewFromOffsets);
} }
}, },
setTimeContext() { setTimeContext() {
this.stopFollowingTime(); this.stopFollowingTime();
this.timeContext = this.openmct.time.getContextForView(this.keyString ? this.objectPath : []); this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.followTime(); this.followTime();
}, },
handleNewBounds(bounds) { handleNewBounds(bounds, isTick) {
this.setBounds(bounds); if (this.timeContext.isRealTime() || !isTick) {
this.setViewFromBounds(bounds); this.setBounds(bounds);
this.updateCurrentValue(); this.setViewFromBounds(bounds);
}, this.updateCurrentValue();
clearAllValidation() { }
[this.$refs.startOffset, this.$refs.endOffset].forEach(this.clearValidationForInput);
},
clearValidationForInput(input) {
input.setCustomValidity('');
input.title = '';
}, },
setViewFromOffsets(offsets) { setViewFromOffsets(offsets) {
if (offsets) { if (offsets) {
@ -200,7 +173,7 @@ export default {
this.formattedBounds.end = this.timeFormatter.format(bounds.end); this.formattedBounds.end = this.timeFormatter.format(bounds.end);
}, },
updateCurrentValue() { updateCurrentValue() {
const currentValue = this.openmct.time.clock()?.currentValue(); const currentValue = this.openmct.time.getClock()?.currentValue();
if (currentValue !== undefined) { if (currentValue !== undefined) {
this.setCurrentValue(currentValue); this.setCurrentValue(currentValue);
@ -222,86 +195,25 @@ export default {
format: key format: key
}).formatter; }).formatter;
}, },
hideAllTimePopups() { timePopUpdate({ start, end }) {
this.showTCInputStart = false; this.offsets.start = [start.hours, start.minutes, start.seconds].join(':');
this.showTCInputEnd = false; this.offsets.end = [end.hours, end.minutes, end.seconds].join(':');
},
showTimePopupStart() {
this.hideAllTimePopups();
this.showTCInputStart = !this.showTCInputStart;
},
showTimePopupEnd() {
this.hideAllTimePopups();
this.showTCInputEnd = !this.showTCInputEnd;
},
timePopUpdate({ type, hours, minutes, seconds }) {
this.offsets[type] = [hours, minutes, seconds].join(':');
this.setOffsetsFromView(); this.setOffsetsFromView();
this.hideAllTimePopups();
}, },
setOffsetsFromView($event) { setOffsetsFromView() {
if (this.$refs.deltaInput.checkValidity()) { let startOffset = 0 - this.durationFormatter.parse(this.offsets.start);
let startOffset = 0 - this.durationFormatter.parse(this.offsets.start); let endOffset = this.durationFormatter.parse(this.offsets.end);
let endOffset = this.durationFormatter.parse(this.offsets.end);
this.$emit('updated', { this.$emit('offsetsUpdated', {
start: startOffset, start: startOffset,
end: endOffset end: endOffset
});
}
if ($event) {
$event.preventDefault();
return false;
}
},
validateAllBounds(ref) {
if (!this.areBoundsFormatsValid()) {
return false;
}
let validationResult = {
valid: true
};
const currentInput = this.$refs[ref];
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
let boundsValues = {
start: this.timeFormatter.parse(this.formattedBounds.start),
end: this.timeFormatter.parse(this.formattedBounds.end)
};
//TODO: Do we need limits here? We have conductor limits disabled right now
// const limit = this.getBoundsLimit();
const limit = false;
if (this.timeSystem.isUTCBased && limit
&& boundsValues.end - boundsValues.start > limit) {
if (input === currentInput) {
validationResult = {
valid: false,
message: "Start and end difference exceeds allowable limit"
};
}
} else {
if (input === currentInput) {
validationResult = this.openmct.time.validateBounds(boundsValues);
}
}
return this.handleValidationResults(input, validationResult);
}); });
}, },
handleValidationResults(input, validationResult) { dismiss() {
if (validationResult.valid !== true) { this.$emit('dismissInputsRealtime');
input.setCustomValidity(validationResult.message); },
input.title = validationResult.message; copy(object) {
} else { return JSON.parse(JSON.stringify(object));
input.setCustomValidity('');
input.title = '';
}
return validationResult.valid;
} }
} }
}; };

View File

@ -22,11 +22,15 @@
<template> <template>
<div <div
ref="modeButton" ref="modeButton"
class="c-ctrl-wrapper c-ctrl-wrapper--menus-up" class="c-tc-input-popup__options"
> >
<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">
<button <button
class="c-button--menu c-mode-button" class="c-button--menu c-button--compact js-mode-button"
:class="[
buttonCssClass,
selectedMode.cssClass
]"
@click.prevent.stop="showModesMenu" @click.prevent.stop="showModesMenu"
> >
<span class="c-button__label">{{ selectedMode.name }}</span> <span class="c-button__label">{{ selectedMode.name }}</span>
@ -36,32 +40,23 @@
</template> </template>
<script> <script>
import toggleMixin from '../../ui/mixins/toggle-mixin'; import modeMixin from './mode-mixin';
const TEST_IDS = true;
export default { export default {
mixins: [toggleMixin], mixins: [modeMixin],
inject: ['openmct', 'configuration'], inject: ['openmct', 'configuration'],
data: function () { data: function () {
let activeClock = this.openmct.time.clock(); const mode = this.openmct.time.getMode();
if (activeClock !== undefined) {
//Create copy of active clock so the time API does not get reactified.
activeClock = Object.create(activeClock);
}
return { return {
selectedMode: this.getModeOptionForClock(activeClock), selectedMode: this.getModeMetadata(mode, TEST_IDS),
selectedTimeSystem: JSON.parse(JSON.stringify(this.openmct.time.timeSystem())), modes: []
modes: [],
hoveredMode: {}
}; };
}, },
mounted: function () { mounted: function () {
this.loadClocksFromConfiguration(); this.loadModes();
this.openmct.time.on('clock', this.setViewFromClock);
},
destroyed: function () {
this.openmct.time.off('clock', this.setViewFromClock);
}, },
methods: { methods: {
showModesMenu() { showModesMenu() {
@ -74,108 +69,11 @@ export default {
placement: this.openmct.menus.menuPlacement.TOP_RIGHT placement: this.openmct.menus.menuPlacement.TOP_RIGHT
}; };
this.openmct.menus.showSuperMenu(x, y, this.modes, menuOptions); this.dismiss = this.openmct.menus.showSuperMenu(x, y, this.modes, menuOptions);
}, },
setMode(modeKey) {
loadClocksFromConfiguration() { this.selectedMode = this.getModeMetadata(modeKey, TEST_IDS);
let clocks = this.configuration.menuOptions this.$emit('modeUpdated', modeKey);
.map(menuOption => menuOption.clock)
.filter(isDefinedAndUnique)
.map(this.getClock);
/*
* Populate the modes menu with metadata from the available clocks
* "Fixed Mode" is always first, and has no defined clock
*/
this.modes = [undefined]
.concat(clocks)
.map(this.getModeOptionForClock);
function isDefinedAndUnique(key, index, array) {
return key !== undefined && array.indexOf(key) === index;
}
},
getModeOptionForClock(clock) {
if (clock === undefined) {
const key = 'fixed';
return {
key,
name: 'Fixed Timespan',
description: 'Query and explore data that falls between two fixed datetimes.',
cssClass: 'icon-tabular',
testId: 'conductor-modeOption-fixed',
onItemClicked: () => this.setOption(key)
};
} else {
const key = clock.key;
return {
key,
name: clock.name,
description: "Monitor streaming data in real-time. The Time "
+ "Conductor and displays will automatically advance themselves based on this clock. " + clock.description,
cssClass: clock.cssClass || 'icon-clock',
testId: 'conductor-modeOption-realtime',
onItemClicked: () => this.setOption(key)
};
}
},
getClock(key) {
return this.openmct.time.getAllClocks().filter(function (clock) {
return clock.key === key;
})[0];
},
setOption(clockKey) {
if (clockKey === 'fixed') {
clockKey = undefined;
}
let configuration = this.getMatchingConfig({
clock: clockKey,
timeSystem: this.openmct.time.timeSystem().key
});
if (configuration === undefined) {
configuration = this.getMatchingConfig({
clock: clockKey
});
this.openmct.time.timeSystem(configuration.timeSystem, configuration.bounds);
}
if (clockKey === undefined) {
this.openmct.time.stopClock();
} else {
const offsets = this.openmct.time.clockOffsets() || configuration.clockOffsets;
this.openmct.time.clock(clockKey, offsets);
}
},
getMatchingConfig(options) {
const matchers = {
clock(config) {
return options.clock === config.clock;
},
timeSystem(config) {
return options.timeSystem === config.timeSystem;
}
};
function configMatches(config) {
return Object.keys(options).reduce((match, option) => {
return match && matchers[option](config);
}, true);
}
return this.configuration.menuOptions.filter(configMatches)[0];
},
setViewFromClock(clock) {
this.selectedMode = this.getModeOptionForClock(clock);
} }
} }
}; };

View File

@ -21,6 +21,17 @@
*****************************************************************************/ *****************************************************************************/
<template> <template>
<div class="c-clock-symbol"> <div class="c-clock-symbol">
<svg
class="c-clock-symbol__outer"
viewBox="0 0 16 16"
>
<path
d="M6 0L3 0C1.34315 0 0 1.34315 0 3V13C0 14.6569 1.34315 16 3 16H6V13H3V3H6V0Z"
/>
<path
d="M10 13H13V3H10V0H13C14.6569 0 16 1.34315 16 3V13C16 14.6569 14.6569 16 13 16H10V13Z"
/>
</svg>
<div class="hand-little"></div> <div class="hand-little"></div>
<div class="hand-big"></div> <div class="hand-big"></div>
</div> </div>

View File

@ -0,0 +1,261 @@
<template>
<div
class="c-tc-input-popup"
:class="modeClass"
:style="position"
@click.stop
>
<div
class="c-tc-input-popup__options"
>
<IndependentMode
v-if="isIndependent"
class="c-button--compact c-conductor__mode-select"
:mode="timeOptionMode"
:button-css-class="'c-button--compact'"
@independentModeUpdated="saveIndependentMode"
/>
<ConductorMode
v-else
class="c-conductor__mode-select"
:button-css-class="'c-icon-button'"
@modeUpdated="saveMode"
/>
<IndependentClock
v-if="isIndependent"
class="c-conductor__mode-select"
:clock="timeOptionClock"
:button-css-class="'c-icon-button'"
@independentClockUpdated="saveIndependentClock"
/>
<ConductorClock
v-else
class="c-conductor__mode-select"
:button-css-class="'c-icon-button'"
@clockUpdated="saveClock"
/>
<!-- TODO: Time system and history must work even with ITC later -->
<ConductorTimeSystem
v-if="!isIndependent"
class="c-conductor__time-system-select"
:button-css-class="'c-icon-button'"
/>
<ConductorHistory
v-if="!isIndependent"
class="c-conductor__history-select"
:button-css-class="'c-icon-button'"
:offsets="timeOffsets"
:bounds="bounds"
:time-system="timeSystem"
:mode="timeMode"
/>
</div>
<conductor-inputs-fixed
v-if="isFixed"
:input-bounds="bounds"
:object-path="objectPath"
@boundsUpdated="saveFixedBounds"
@dismissInputsFixed="dismiss"
/>
<conductor-inputs-realtime
v-else
:input-bounds="bounds"
:object-path="objectPath"
@offsetsUpdated="saveClockOffsets"
@dismissInputsRealtime="dismiss"
/>
</div>
</template>
<script>
import ConductorMode from './ConductorMode.vue';
import ConductorClock from './ConductorClock.vue';
import IndependentMode from './independent/IndependentMode.vue';
import IndependentClock from './independent/IndependentClock.vue';
import ConductorTimeSystem from "./ConductorTimeSystem.vue";
import ConductorHistory from "./ConductorHistory.vue";
import ConductorInputsFixed from "./ConductorInputsFixed.vue";
import ConductorInputsRealtime from "./ConductorInputsRealtime.vue";
import { TIME_CONTEXT_EVENTS, REALTIME_MODE_KEY, FIXED_MODE_KEY } from '../../api/time/constants';
export default {
components: {
ConductorMode,
ConductorClock,
IndependentMode,
IndependentClock,
ConductorTimeSystem,
ConductorHistory,
ConductorInputsFixed,
ConductorInputsRealtime
},
inject: {
openmct: 'openmct',
configuration: {
from: 'configuration',
default: undefined
}
},
props: {
positionX: {
type: Number,
required: true
},
positionY: {
type: Number,
required: true
},
isFixed: {
type: Boolean,
required: true
},
isIndependent: {
type: Boolean,
default() {
return false;
}
},
timeOptions: {
type: Object,
default() {
return undefined;
}
},
bottom: {
type: Boolean,
default() {
return false;
}
},
objectPath: {
type: Array,
default() {
return [];
}
}
},
data() {
const bounds = this.openmct.time.getBounds();
const timeSystem = this.openmct.time.getTimeSystem();
// const isFixed = this.openmct.time.isFixed();
return {
timeSystem,
bounds: {
start: bounds.start,
end: bounds.end
}
};
},
computed: {
position() {
const position = {
left: `${this.positionX}px`
};
if (this.isIndependent) {
position.top = `${this.positionY}px`;
}
return position;
},
timeOffsets() {
return this.isFixed ? this.openmct.time.getBounds() : this.openmct.time.getClockOffsets();
},
timeMode() {
return this.isFixed ? FIXED_MODE_KEY : REALTIME_MODE_KEY;
},
modeClass() {
const value = this.bottom ? 'c-tc-input-popup--bottom' : '';
return this.isFixed ? `${value} c-tc-input-popup--fixed-mode` : `${value} c-tc-input-popup--realtime-mode`;
},
timeOptionMode() {
return this.timeOptions?.mode;
},
timeOptionClock() {
return this.timeOptions?.clock;
}
},
watch: {
objectPath: {
handler(newPath, oldPath) {
//domain object or view has probably changed
if (newPath === oldPath) {
return;
}
this.setTimeContext();
},
deep: true
}
},
mounted() {
this.$emit('popupLoaded');
this.setTimeContext();
},
beforeDestroy() {
this.stopFollowingTimeContext();
},
methods: {
setTimeContext() {
if (this.timeContext) {
this.stopFollowingTimeContext();
}
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
this.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this.setBounds);
this.timeContext.on(TIME_CONTEXT_EVENTS.modeChanged, this.setMode);
this.setViewFromClock(this.timeContext.getClock());
this.setBounds(this.timeContext.getBounds());
},
stopFollowingTimeContext() {
this.timeContext.off(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.setBounds);
this.timeContext.off(TIME_CONTEXT_EVENTS.modeChanged, this.setMode);
},
setViewFromClock() {
this.bounds = this.isFixed ? this.timeContext.getBounds() : this.openmct.time.getClockOffsets();
console.log('set view from clock popup', this.bounds);
},
setBounds(bounds, isTick) {
if (this.isFixed || !isTick) {
console.log('set bounds popup', bounds);
this.bounds = bounds;
}
},
setMode(mode) {
// this.isFixed = mode === FIXED_MODE_KEY;
},
saveFixedBounds(bounds) {
this.$emit('fixedBoundsUpdated', bounds);
},
saveClockOffsets(offsets) {
this.$emit('clockOffsetsUpdated', offsets);
},
saveClock(clockOptions) {
this.$emit('clockUpdated', clockOptions);
},
saveMode(mode) {
this.$emit('modeUpdated', mode);
},
saveIndependentMode(mode) {
this.$emit('independentModeUpdated', mode);
},
saveIndependentClock(clockKey) {
this.$emit('independentClockUpdated', clockKey);
},
dismiss() {
this.$emit('dismiss');
}
}
};
</script>

View File

@ -26,33 +26,46 @@
class="c-ctrl-wrapper c-ctrl-wrapper--menus-up" class="c-ctrl-wrapper c-ctrl-wrapper--menus-up"
> >
<button <button
class="c-button--menu c-time-system-button" class="c-button--menu c-button--compact c-time-system-button"
:class="selectedTimeSystem.cssClass" :class="[
buttonCssClass
]"
@click.prevent.stop="showTimeSystemMenu" @click.prevent.stop="showTimeSystemMenu"
> >
<span class="c-button__label">{{ selectedTimeSystem.name }}</span> {{ selectedTimeSystem.name }}
</button> </button>
</div> </div>
</template> </template>
<script> <script>
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants';
export default { export default {
inject: ['openmct', 'configuration'], inject: ['openmct', 'configuration'],
props: {
buttonCssClass: {
type: String,
required: false,
default() {
return '';
}
}
},
data: function () { data: function () {
let activeClock = this.openmct.time.clock(); let activeClock = this.openmct.time.getClock();
return { return {
selectedTimeSystem: JSON.parse(JSON.stringify(this.openmct.time.timeSystem())), selectedTimeSystem: JSON.parse(JSON.stringify(this.openmct.time.getTimeSystem())),
timeSystems: this.getValidTimesystemsForClock(activeClock) timeSystems: this.getValidTimesystemsForClock(activeClock)
}; };
}, },
mounted: function () { mounted: function () {
this.openmct.time.on('timeSystem', this.setViewFromTimeSystem); this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSysteChanged, this.setViewFromTimeSystem);
this.openmct.time.on('clock', this.setViewFromClock); this.openmct.time.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
}, },
destroyed: function () { destroyed: function () {
this.openmct.time.off('timeSystem', this.setViewFromTimeSystem); this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setViewFromTimeSystem);
this.openmct.time.on('clock', this.setViewFromClock); this.openmct.time.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
}, },
methods: { methods: {
showTimeSystemMenu() { showTimeSystemMenu() {
@ -78,7 +91,7 @@ export default {
}, },
setTimeSystemFromView(timeSystem) { setTimeSystemFromView(timeSystem) {
if (timeSystem.key !== this.selectedTimeSystem.key) { if (timeSystem.key !== this.selectedTimeSystem.key) {
let activeClock = this.openmct.time.clock(); let activeClock = this.openmct.time.getClock();
let configuration = this.getMatchingConfig({ let configuration = this.getMatchingConfig({
clock: activeClock && activeClock.key, clock: activeClock && activeClock.key,
timeSystem: timeSystem.key timeSystem: timeSystem.key
@ -87,15 +100,15 @@ export default {
let bounds; let bounds;
if (this.selectedTimeSystem.isUTCBased && timeSystem.isUTCBased) { if (this.selectedTimeSystem.isUTCBased && timeSystem.isUTCBased) {
bounds = this.openmct.time.bounds(); bounds = this.openmct.time.getBounds();
} else { } else {
bounds = configuration.bounds; bounds = configuration.bounds;
} }
this.openmct.time.timeSystem(timeSystem.key, bounds); this.openmct.time.setTimeSystem(timeSystem.key, bounds);
} else { } else {
this.openmct.time.timeSystem(timeSystem.key); this.openmct.time.setTimeSystem(timeSystem.key);
this.openmct.time.clockOffsets(configuration.clockOffsets); this.openmct.time.setClockOffsets(configuration.clockOffsets);
} }
} }
}, },
@ -124,7 +137,7 @@ export default {
}, },
setViewFromClock(clock) { setViewFromClock(clock) {
let activeClock = this.openmct.time.clock(); let activeClock = this.openmct.time.getClock();
this.timeSystems = this.getValidTimesystemsForClock(activeClock); this.timeSystems = this.getValidTimesystemsForClock(activeClock);
} }
} }

View File

@ -57,11 +57,11 @@
} }
.is-realtime-mode & { .is-realtime-mode & {
$c: 1px solid rgba($colorTime, 0.7); $c: 1px solid rgba($colorTimeRealtime, 0.7);
border-left: $c; border-left: $c;
border-right: $c; border-right: $c;
svg text { svg text {
fill: $colorTime; fill: $colorTimeRealtime;
} }
} }
} }

View File

@ -33,22 +33,17 @@
.c-clock-symbol { .c-clock-symbol {
$c: $colorBtnBg; //$colorObjHdrIc; $c: rgba($colorBodyFg, 0.5);
$d: 18px; $d: 16px;
height: $d; height: $d;
width: $d; width: $d;
position: relative; position: relative;
&:before { &__outer {
font-family: symbolsfont; // SVG brackets shape
color: $c;
content: $glyph-icon-brackets;
font-size: $d;
line-height: normal;
display: block;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 1; fill: $c;
} }
// Clock hands // Clock hands
@ -93,14 +88,15 @@
// Modes // Modes
.is-realtime-mode &, .is-realtime-mode &,
.is-lad-mode & { .is-lad-mode & {
&:before { $c: $colorTimeRealtimeFgSubtle;
.c-clock-symbol__outer {
// Brackets icon // Brackets icon
color: $colorTime; fill: $c;
} }
div[class*="hand"] { div[class*="hand"] {
animation-name: clock-hands; animation-name: clock-hands;
&:before { &:before {
background: $colorTime; background: $c;
} }
} }
} }

View File

@ -1,8 +1,9 @@
.c-conductor__mode-menu { .c-conductor__mode-menu {
max-height: 80vh; max-height: 80vh;
max-width: 500px; max-width: 500px;
min-height: 250px; min-height: 50px;
z-index: 70; //We don't need the z-index now that we're using the popup
//z-index: 70;
[class*="__icon"] { [class*="__icon"] {
filter: $colorKeyFilter; filter: $colorKeyFilter;

View File

@ -9,10 +9,15 @@
/*********************************************** CONDUCTOR LAYOUT */ /*********************************************** CONDUCTOR LAYOUT */
.c-conductor { .c-conductor {
&__inputs { &__inputs {
display: contents; display: flex;
flex: 0 0 auto;
> * + * {
margin-left: $interiorMargin;
}
} }
&__time-bounds { /* &__time-bounds {
display: grid; display: grid;
grid-column-gap: $interiorMargin; grid-column-gap: $interiorMargin;
grid-row-gap: $interiorMargin; grid-row-gap: $interiorMargin;
@ -39,16 +44,17 @@
grid-area: tc-end; grid-area: tc-end;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
} }*/
&__ticks { &__ticks {
grid-area: tc-ticks; flex: 1 1 auto;
} }
&__controls { &__controls {
grid-area: tc-controls; grid-area: tc-controls;
display: flex; display: flex;
align-items: center; align-items: center;
> * + * { > * + * {
margin-left: $interiorMargin; margin-left: $interiorMargin;
} }
@ -107,7 +113,8 @@
background: rgba($timeConductorActiveBg, 0.4); background: rgba($timeConductorActiveBg, 0.4);
border-left-color: $timeConductorActiveBg; border-left-color: $timeConductorActiveBg;
border-right-color: $timeConductorActiveBg; border-right-color: $timeConductorActiveBg;
top: 0; bottom: 0; top: 0;
bottom: 0;
} }
} }
} }
@ -123,7 +130,7 @@
} }
} }
body.phone.portrait & { /* body.phone.portrait & {
.c-conductor__time-bounds { .c-conductor__time-bounds {
grid-row-gap: $interiorMargin; grid-row-gap: $interiorMargin;
grid-template-rows: auto auto; grid-template-rows: auto auto;
@ -160,8 +167,8 @@
grid-template-areas: grid-template-areas:
"tc-mode-icon tc-start tc-start" "tc-mode-icon tc-start tc-start"
"tc-mode-icon tc-end tc-end" "tc-mode-icon tc-end tc-end"
}
} }
}
&.is-realtime-mode { &.is-realtime-mode {
.c-conductor__time-bounds { .c-conductor__time-bounds {
@ -174,21 +181,20 @@
justify-content: flex-end; justify-content: flex-end;
} }
} }
} }*/
} }
.c-conductor-holder--compact { .c-conductor-holder--compact {
min-height: 22px; //min-height: 22px;
flex: 0 1 auto;
overflow: hidden;
.c-conductor { .c-conductor {
&__inputs, &__inputs,
&__time-bounds { &__time-bounds {
display: flex; display: flex;
flex: 0 1 auto;
.c-toggle-switch { overflow: hidden;
// Used in independent Time Conductor
flex: 0 0 auto;
}
} }
&__inputs { &__inputs {
@ -218,38 +224,32 @@
margin-right: $interiorMarginSm; margin-right: $interiorMarginSm;
} }
.c-direction-indicator {
// Holds realtime-mode + and - symbols
font-size: 0.7em;
}
input:invalid { input:invalid {
background: rgba($colorFormInvalid, 0.5); background: rgba($colorFormInvalid, 0.5);
} }
} }
.is-realtime-mode { .is-realtime-mode {
.c-conductor__controls button,
.c-conductor__delta-button { .c-conductor__delta-button {
@include themedButton($colorTimeBg); //@include themedButton($colorTimeRealtimeBg);
color: $colorTimeFg; color: $colorTimeRealtimeFg;
} }
.c-conductor-input { .c-conductor-input {
&:before { &:before {
color: $colorTime; color: $colorTimeRealtimeFgSubtle;
} }
} }
.c-conductor__end-fixed { .c-conductor__end-fixed {
// Displays last RT udpate // Displays last RT update
color: $colorTime; color: $colorTimeRealtimeFgSubtle;
input { input {
// Remove input look // Remove input look
background: none; background: none;
box-shadow: none; box-shadow: none;
color: $colorTime; color: $colorTimeRealtimeFgSubtle;
pointer-events: none; pointer-events: none;
&[disabled] { &[disabled] {
@ -259,6 +259,7 @@
} }
} }
//TODO: Do we need this?
[class^='pr-tc-input-menu'] { [class^='pr-tc-input-menu'] {
// Uses ^= here to target both start and end menus // Uses ^= here to target both start and end menus
background: $colorBodyBg; background: $colorBodyBg;
@ -281,30 +282,250 @@
} }
} }
.l-shell__time-conductor .pr-tc-input-menu--end { .l-shell__time-conductor .c-tc-input-popup--end {
left: auto; left: auto;
right: 0; right: 0;
} }
.pr-time-label {
font-size: 0.9em;
text-transform: uppercase;
[class^='pr-time'] { &:before {
&[class*='label'] {
font-size: 0.8em; font-size: 0.8em;
opacity: 0.6; margin-right: $interiorMarginSm;
text-transform: uppercase; }
}
.pr-time-input {
display: flex;
align-items: center;
white-space: nowrap;
> * + * {
margin-left: $interiorMarginSm;
} }
&[class*='controls'] { input {
height: 22px;
line-height: 1em;
font-size: 1.25em;
}
&--date input {
width: 120px;
}
&--time input {
width: 70px;
}
&--buttons {
> * + * {
margin-left: $interiorMargin;
}
}
&__start-end-sep {
height: 100%;
}
&--input-and-button {
@include wrappedInput();
}
}
/*********************************************** COMPACT TIME CONDUCTOR */
.c-compact-tc,
.c-tc-input-popup {
[class*='start-end-sep'] {
opacity: 0.5;
}
}
.c-compact-tc {
border-radius: $controlCr;
display: flex;
flex-direction: row;
flex: 0 1 auto;
overflow: hidden;
align-items: center;
padding: 2px $interiorMarginSm;
> * + * {
margin-left: $interiorMargin;
}
&__bounds,
&__bounds__value {
display: flex; display: flex;
align-items: center; align-items: center;
white-space: nowrap;
input { > * + * {
height: 22px; margin-left: $interiorMargin;
line-height: 22px; }
}
&__bounds {
cursor: pointer;
flex: 0 1 auto;
overflow: hidden;
> * + * {
flex: 0 0 auto;
}
}
&__bounds__value {
@include ellipsize();
color: $colorTimeRealtimeFg;
flex: 0 1 auto;
&:before {
font-size: 0.85em;
margin-right: $interiorMarginSm; margin-right: $interiorMarginSm;
font-size: 1.25em; }
width: 42px; }
&__current-update {
@include ellipsize();
flex: 0 1 auto;
}
.c-direction-indicator {
// Holds realtime-mode + and - symbols
font-size: 0.7em;
}
.c-toggle-switch,
.c-clock-symbol {
// Used in independent Time Conductor
flex: 0 0 auto;
}
.c-so-view & {
// Time Conductor in a Layout frame
.c-clock-symbol {
$h: 14px;
height: $h;
width: $h;
}
[class*='button'] {
$p: 0px;
padding: $p $p + 1;
} }
} }
} }
.is-fixed-mode.is-expanded {
&.c-compact-tc,
.c-tc-input-popup {
background: $colorTimeFixedBg;
color: $colorTimeFixedFgSubtle;
em,
.pr-time-label:before {
color: $colorTimeFixedFg;
}
&__bounds__valuelue {
color: $colorTimeFixedFg;
}
&__time-value {
color: $colorTimeFixedFg;
}
[class*='c-button'] {
background: $colorTimeFixedBtnBg;
color: $colorTimeFixedBtnFg;
[class*='label'] {
color: $colorTimeRealtimeFg;
}
}
}
}
.is-realtime-mode.is-expanded {
&.c-compact-tc,
.c-tc-input-popup {
background: rgba($colorTimeRealtimeBg, 1);
color: $colorTimeRealtimeFgSubtle;
em,
.pr-time-label:before {
color: $colorTimeRealtimeFg;
}
&__bounds__valuelue {
color: $colorTimeRealtimeFg;
}
&__time-value {
color: $colorTimeRealtimeFg;
}
[class*='c-button'] {
background: $colorTimeRealtimeBtnBg;
color: $colorTimeRealtimeBtnFg;
[class*='label'] {
color: $colorTimeRealtimeFg;
}
}
}
}
.c-compact-tc {
&.l-shell__time-conductor {
// Main view
min-height: 24px;
}
}
/*********************************************** INPUTS POPUP DIALOG */
.c-tc-input-popup {
@include menuOuter();
padding: $interiorMarginLg;
position: absolute;
width: min-content;
bottom: 35px;
> * + * {
margin-top: $interiorMarginLg;
}
&[class*='--bottom'] {
bottom: auto;
top: 35px;
}
&__options {
display: flex;
> * + * {
margin-left: $interiorMargin;
}
}
&--fixed-mode {
.c-tc-input-popup__input-grid {
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 2fr;
}
}
&--realtime-mode {
.c-tc-input-popup__input-grid {
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2fr;
}
}
&__input-grid {
display: grid;
grid-column-gap: 3px;
grid-row-gap: $interiorMargin;
align-items: start;
}
}

View File

@ -0,0 +1,83 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, 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 raf from '@/utils/raf';
export default {
inject: ['openmct', 'configuration'],
data() {
return {
showConductorPopup: false,
positionX: 0,
positionY: 0,
conductorPopup: null
};
},
mounted() {
this.positionBox = raf(this.positionBox);
this.timeConductorOptionsHolder = this.$el;
this.timeConductorOptionsHolder.addEventListener('click', this.showPopup);
},
methods: {
initializePopup() {
this.conductorPopup = this.$refs.conductorPopup.$el;
this.$nextTick(() => {
window.addEventListener('resize', this.positionBox);
document.addEventListener('click', this.handleClickAway);
this.positionBox();
});
},
showPopup() {
if (this.conductorPopup) {
return;
}
this.showConductorPopup = true;
},
positionBox() {
const timeConductorOptionsBox = this.timeConductorOptionsHolder.getBoundingClientRect();
const offsetTop = this.conductorPopup.getBoundingClientRect().height;
//TODO: PositionY should be calculated to be top or bottom based on the location of the conductor options
this.positionY = timeConductorOptionsBox.top - offsetTop;
this.positionX = 0;
},
clearPopup() {
this.showConductorPopup = false;
this.conductorPopup = null;
document.removeEventListener('click', this.handleClickAway);
window.removeEventListener('resize', this.positionBox);
},
handleClickAway(clickAwayEvent) {
if (this.canClose(clickAwayEvent)) {
clickAwayEvent.stopPropagation();
this.clearPopup();
}
},
canClose(clickAwayEvent) {
const isChildMenu = clickAwayEvent.target.closest('.c-menu') !== null;
const isPopupElementItem = this.timeConductorOptionsHolder.contains(clickAwayEvent.target);
return !isChildMenu && !isPopupElementItem;
}
}
};

View File

@ -0,0 +1,134 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web 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 Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div
ref="clockMenuButton"
class="c-ctrl-wrapper c-ctrl-wrapper--menus-up"
>
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
v-if="selectedClock"
class="c-icon-button c-button--menu js-clock-button"
:class="[
buttonCssClass,
selectedClock.cssClass
]"
@click.prevent.stop="showClocksMenu"
>
<span class="c-button__label">{{ selectedClock.name }}</span>
</button>
</div>
</div>
</template>
<script>
import toggleMixin from '../../../ui/mixins/toggle-mixin';
import modeMixin from '../mode-mixin';
import { TIME_CONTEXT_EVENTS, FIXED_MODE_KEY } from '../../../api/time/constants'
export default {
mixins: [toggleMixin, modeMixin],
inject: ['openmct'],
props: {
clock: {
type: String,
default() {
return undefined;
}
},
enabled: {
type: Boolean,
default() {
return false;
}
}
},
data: function () {
const activeClock = this.getActiveClock();
return {
selectedClock: activeClock ? this.getClockMetadata(activeClock) : undefined,
clocks: []
};
},
watch: {
clock(newClock, oldClock) {
this.setViewFromClock(newClock);
},
enabled(newValue, oldValue) {
if (newValue !== undefined && (newValue !== oldValue) && (newValue === true)) {
this.setViewFromClock(this.clock);
}
}
},
beforeDestroy() {
this.openmct.time.off(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
},
mounted: function () {
this.loadClocks(this.getMenuOptions());
this.setViewFromClock(this.clock);
this.openmct.time.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
},
methods: {
showClocksMenu() {
const elementBoundingClientRect = this.$refs.clockMenuButton.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y + elementBoundingClientRect.height;
const menuOptions = {
menuClass: 'c-conductor__clock-menu',
placement: this.openmct.menus.menuPlacement.BOTTOM_RIGHT
};
this.openmct.menus.showSuperMenu(x, y, this.clocks, menuOptions);
},
getMenuOptions() {
let currentGlobalClock = this.getActiveClock();
//Create copy of active clock so the time API does not get reactified.
currentGlobalClock = Object.assign({}, {
name: currentGlobalClock.name,
clock: currentGlobalClock.key,
timeSystem: this.openmct.time.timeSystem().key
});
return [currentGlobalClock];
},
setClock(clockKey) {
this.setViewFromClock(clockKey);
this.$emit('independentClockUpdated', clockKey);
},
setViewFromClock(clockOrKey) {
let clock = clockOrKey;
if (!clock.key) {
clock = this.getClock(clockOrKey);
}
// if global clock changes, reload and pull it
this.loadModes(this.getMenuOptions());
this.selectedClock = this.getClockMetadata(clock);
}
}
};
</script>

View File

@ -0,0 +1,127 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web 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 Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div
ref="modeMenuButton"
class="c-ctrl-wrapper c-ctrl-wrapper--menus-up"
>
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
class="c-icon-button c-button--menu js-mode-button"
:class="[
buttonCssClass,
selectedMode.cssClass
]"
@click.prevent.stop="showModesMenu"
>
<span class="c-button__label">{{ selectedMode.name }}</span>
</button>
</div>
</div>
</template>
<script>
import toggleMixin from '../../../ui/mixins/toggle-mixin';
import modeMixin from '../mode-mixin';
import { TIME_CONTEXT_EVENTS, REALTIME_MODE_KEY, FIXED_MODE_KEY } from '../../../api/time/constants';
export default {
mixins: [toggleMixin, modeMixin],
inject: ['openmct'],
props: {
mode: {
type: String,
default() {
return undefined;
}
},
enabled: {
type: Boolean,
default() {
return false;
}
}
},
data: function () {
return {
selectedMode: this.getModeMetadata(this.mode),
modes: []
};
},
watch: {
mode: {
handler(newMode) {
this.setViewFromMode(newMode);
}
},
enabled(newValue, oldValue) {
if (newValue !== undefined && (newValue !== oldValue) && (newValue === true)) {
this.setViewFromMode(this.mode);
}
}
},
mounted: function () {
this.loadModes();
},
methods: {
showModesMenu() {
const elementBoundingClientRect = this.$refs.modeMenuButton.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y + elementBoundingClientRect.height;
const menuOptions = {
menuClass: 'c-conductor__mode-menu',
placement: this.openmct.menus.menuPlacement.BOTTOM_RIGHT
};
this.openmct.menus.showSuperMenu(x, y, this.modes, menuOptions);
},
getMenuOptions() {
let menuOptions = [{
name: 'Fixed Timespan',
timeSystem: 'utc'
}];
let currentGlobalClock = this.getActiveClock();
if (currentGlobalClock !== undefined) {
//Create copy of active clock so the time API does not get reactified.
currentGlobalClock = Object.assign({}, {
name: currentGlobalClock.name,
clock: currentGlobalClock.key,
timeSystem: this.openmct.time.timeSystem().key
});
menuOptions.push(currentGlobalClock);
}
return menuOptions;
},
setViewFromMode(mode) {
this.selectedMode = this.getModeMetadata(mode);
},
setMode(mode) {
this.setViewFromMode(mode);
this.$emit('independentModeUpdated', mode);
}
}
};
</script>

View File

@ -21,68 +21,85 @@
*****************************************************************************/ *****************************************************************************/
<template> <template>
<div <div
class="c-conductor" ref="timeConductorOptionsHolder"
class="c-compact-tc"
:class="[ :class="[
isFixed ? 'is-fixed-mode' : independentTCEnabled ? 'is-realtime-mode' : 'is-fixed-mode' isFixed ? 'is-fixed-mode' : independentTCEnabled ? 'is-realtime-mode' : 'is-fixed-mode',
{ 'is-expanded' : independentTCEnabled }
]" ]"
> >
<div class="c-conductor__time-bounds"> <toggle-switch
<toggle-switch id="independentTCToggle"
id="independentTCToggle" class="c-toggle-switch--mini"
:checked="independentTCEnabled" :checked="independentTCEnabled"
:title="`${independentTCEnabled ? 'Disable' : 'Enable'} independent Time Conductor`" :title="toggleTitle"
@change="toggleIndependentTC" @change="toggleIndependentTC"
/> />
<ConductorModeIcon /> <ConductorModeIcon v-if="independentTCEnabled" />
<div <conductor-inputs-fixed
v-if="timeOptions && independentTCEnabled" v-if="showFixedInputs"
class="c-conductor__controls" class="c-compact-tc__bounds--fixed"
> :object-path="objectPath"
<Mode :read-only="true"
v-if="mode" :compact="true"
class="c-conductor__mode-select" />
:key-string="domainObject.identifier.key"
:mode="timeOptions.mode"
:enabled="independentTCEnabled"
@modeChanged="saveMode"
/>
<conductor-inputs-fixed <conductor-inputs-realtime
v-if="isFixed" v-if="showRealtimeInputs"
:key-string="domainObject.identifier.key" class="c-compact-tc__bounds--real-time"
:object-path="objectPath" :object-path="objectPath"
@updated="saveFixedOffsets" :read-only="true"
/> :compact="true"
/>
<div class="c-not-button c-not-button--compact c-compact-tc__gear icon-gear"></div>
<conductor-inputs-realtime <conductor-pop-up
v-else v-if="showConductorPopup"
:key-string="domainObject.identifier.key" ref="conductorPopup"
:object-path="objectPath" :object-path="objectPath"
@updated="saveClockOffsets" :is-independent="true"
/> :time-options="timeOptions"
</div> :is-fixed="isFixed"
</div> :bottom="true"
:position-x="positionX"
:position-y="positionY"
@popupLoaded="initializePopup"
@independentModeUpdated="saveMode"
@independentClockUpdated="saveClock"
@fixedBoundsUpdated="saveFixedBounds"
@clockOffsetsUpdated="saveClockOffsets"
@dismiss="clearPopup"
/>
</div> </div>
</template> </template>
<script> <script>
import { TIME_CONTEXT_EVENTS, FIXED_MODE_KEY } from '../../../api/time/constants';
import ConductorInputsFixed from "../ConductorInputsFixed.vue"; import ConductorInputsFixed from "../ConductorInputsFixed.vue";
import ConductorInputsRealtime from "../ConductorInputsRealtime.vue"; import ConductorInputsRealtime from "../ConductorInputsRealtime.vue";
import ConductorModeIcon from "@/plugins/timeConductor/ConductorModeIcon.vue"; import ConductorModeIcon from "@/plugins/timeConductor/ConductorModeIcon.vue";
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue'; import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
import Mode from "./Mode.vue"; import ConductorPopUp from '../ConductorPopUp.vue';
import independentTimeConductorPopUpManager from "./independentTimeConductorPopUpManager";
export default { export default {
components: { components: {
Mode,
ConductorModeIcon, ConductorModeIcon,
ConductorInputsRealtime, ConductorInputsRealtime,
ConductorInputsFixed, ConductorInputsFixed,
ConductorPopUp,
ToggleSwitch ToggleSwitch
}, },
inject: ['openmct'], mixins: [independentTimeConductorPopUpManager],
inject: {
openmct: 'openmct',
configuration: {
from: 'configuration',
default: undefined
}
},
props: { props: {
domainObject: { domainObject: {
type: Object, type: Object,
@ -94,42 +111,83 @@ export default {
} }
}, },
data() { data() {
const fixedOffsets = this.openmct.time.getBounds();
const clockOffsets = this.openmct.time.getClockOffsets();
const clock = this.openmct.time.getClock().key;
const mode = this.openmct.time.getMode();
const timeOptions = this.domainObject.configuration.timeOptions ?? {
clockOffsets,
fixedOffsets
};
timeOptions.clock = timeOptions.clock ?? clock;
timeOptions.mode = timeOptions.mode ?? mode;
// check for older configurations that stored a key
if (timeOptions.mode.key) {
timeOptions.mode = timeOptions.mode.key;
}
const isFixed = timeOptions.mode === FIXED_MODE_KEY;
return { return {
timeOptions: this.domainObject.configuration.timeOptions || { timeOptions,
clockOffsets: this.openmct.time.clockOffsets(), isFixed,
fixedOffsets: this.openmct.time.bounds() independentTCEnabled: this.domainObject.configuration.useIndependentTime === true,
}, viewBounds: {
mode: undefined, start: fixedOffsets.start,
independentTCEnabled: this.domainObject.configuration.useIndependentTime === true end: fixedOffsets.end
}
}; };
}, },
computed: { computed: {
isFixed() { toggleTitle() {
if (!this.mode || !this.mode.key) { return `${this.independentTCEnabled ? 'Disable' : 'Enable'} independent Time Conductor`;
return this.openmct.time.clock() === undefined; },
} else { showFixedInputs() {
return this.mode.key === 'fixed'; return this.isFixed && this.independentTCEnabled;
} },
showRealtimeInputs() {
return !this.isFixed && this.independentTCEnabled;
} }
}, },
watch: { watch: {
domainObject: { domainObject: {
handler(domainObject) { handler(domainObject) {
const key = this.openmct.objects.makeKeyString(domainObject.identifier); const key = this.openmct.objects.makeKeyString(domainObject.identifier);
if (key !== this.keyString) { if (key !== this.keyString) {
//domain object has changed //domain object has changed
this.destroyIndependentTime(); this.destroyIndependentTime();
this.independentTCEnabled = domainObject.configuration.useIndependentTime === true; this.independentTCEnabled = domainObject.configuration.useIndependentTime === true;
this.timeOptions = domainObject.configuration.timeOptions || { this.timeOptions = domainObject.configuration.timeOptions ?? {
clockOffsets: this.openmct.time.clockOffsets(), clockOffsets: this.openmct.time.getClockOffsets(),
fixedOffsets: this.openmct.time.bounds() fixedOffsets: this.openmct.time.getBounds()
}; };
// these may not be set due to older configurations
this.timeOptions.clock = this.timeOptions.clock ?? this.openmct.time.getClock().key;
this.timeOptions.mode = this.timeOptions.mode ?? this.openmct.time.getMode();
// check for older configurations that stored a key
if (this.timeOptions.mode.key) {
this.timeOptions.mode = this.timeOptions.mode.key;
}
this.isFixed = this.timeOptions.mode === FIXED_MODE_KEY;
this.initialize(); this.initialize();
} }
}, },
deep: true deep: true
},
objectPath: {
handler(newPath, oldPath) {
//domain object or view has probably changed
this.setTimeContext();
},
deep: true
} }
}, },
mounted() { mounted() {
@ -144,84 +202,85 @@ export default {
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.setTimeContext(); this.setTimeContext();
if (this.timeOptions.mode) {
this.mode = this.timeOptions.mode;
} else {
if (this.timeContext.clock() === undefined) {
this.timeOptions.mode = this.mode = { key: 'fixed' };
} else {
this.timeOptions.mode = this.mode = { key: Object.create(this.timeContext.clock()).key};
}
}
if (this.independentTCEnabled) { if (this.independentTCEnabled) {
this.registerIndependentTimeOffsets(); this.registerIndependentTimeOffsets();
} }
}, },
toggleIndependentTC() { toggleIndependentTC() {
this.independentTCEnabled = !this.independentTCEnabled; this.independentTCEnabled = !this.independentTCEnabled;
if (this.independentTCEnabled) { if (this.independentTCEnabled) {
this.registerIndependentTimeOffsets(); this.registerIndependentTimeOffsets();
} else { } else {
this.clearPopup();
this.destroyIndependentTime(); this.destroyIndependentTime();
} }
this.$emit('stateChanged', this.independentTCEnabled); this.$emit('stateChanged', this.independentTCEnabled); // no longer use this, but may be used elsewhere
this.openmct.objects.mutate(this.domainObject, 'configuration.useIndependentTime', this.independentTCEnabled);
}, },
setTimeContext() { setTimeContext() {
this.stopFollowingTimeContext(); if (this.timeContext) {
this.stopFollowingTimeContext();
}
this.timeContext = this.openmct.time.getContextForView(this.objectPath); this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on('clock', this.setTimeOptions); this.timeContext.on(TIME_CONTEXT_EVENTS.clockChanged, this.setTimeOptionsClock);
this.timeContext.on(TIME_CONTEXT_EVENTS.modeChanged, this.setTimeOptionsMode);
}, },
stopFollowingTimeContext() { stopFollowingTimeContext() {
if (this.timeContext) { this.timeContext.off(TIME_CONTEXT_EVENTS.clockChanged, this.setTimeOptionsClock);
this.timeContext.off('clock', this.setTimeOptions); this.timeContext.off(TIME_CONTEXT_EVENTS.modeChanged, this.setTimeOptionsMode);
}
}, },
setTimeOptions(clock) { setTimeOptionsClock(clock) {
this.timeOptions.clockOffsets = this.timeOptions.clockOffsets || this.timeContext.clockOffsets(); this.setTimeOptionsOffsets();
this.timeOptions.fixedOffsets = this.timeOptions.fixedOffsets || this.timeContext.bounds(); this.timeOptions.clock = clock.key;
if (!this.timeOptions.mode) {
this.mode = this.timeContext.clock() === undefined ? {key: 'fixed'} : {key: Object.create(this.timeContext.clock()).key};
this.registerIndependentTimeOffsets();
}
}, },
saveFixedOffsets(offsets) { setTimeOptionsMode(mode) {
const newOptions = Object.assign({}, this.timeOptions, { this.setTimeOptionsOffsets();
fixedOffsets: offsets this.timeOptions.mode = mode;
},
setTimeOptionsOffsets() {
this.timeOptions.clockOffsets = this.timeOptions.clockOffsets ?? this.timeContext.getClockOffsets();
this.timeOptions.fixedOffsets = this.timeOptions.fixedOffsets ?? this.timeContext.getBounds();
},
saveFixedBounds(bounds) {
const newOptions = this.updateTimeOptionProperty({
fixedOffsets: bounds
}); });
this.updateTimeOptions(newOptions); this.updateTimeOptions(newOptions);
}, },
saveClockOffsets(offsets) { saveClockOffsets(offsets) {
const newOptions = Object.assign({}, this.timeOptions, { const newOptions = this.updateTimeOptionProperty({
clockOffsets: offsets clockOffsets: offsets
}); });
this.updateTimeOptions(newOptions); this.updateTimeOptions(newOptions);
}, },
saveMode(mode) { saveMode(mode) {
this.mode = mode; this.isFixed = mode === FIXED_MODE_KEY;
const newOptions = Object.assign({}, this.timeOptions, { const newOptions = this.updateTimeOptionProperty({
mode: this.mode mode: mode
}); });
this.updateTimeOptions(newOptions);
},
saveClock(clock) {
const newOptions = this.updateTimeOptionProperty({
clock
});
this.updateTimeOptions(newOptions); this.updateTimeOptions(newOptions);
}, },
updateTimeOptions(options) { updateTimeOptions(options) {
this.timeOptions = options; this.timeOptions = options;
if (!this.timeOptions.mode) {
this.timeOptions.mode = this.mode;
}
this.registerIndependentTimeOffsets(); this.registerIndependentTimeOffsets();
this.$emit('updated', this.timeOptions); this.$emit('updated', this.timeOptions); // no longer use this, but may be used elsewhere
this.openmct.objects.mutate(this.domainObject, 'configuration.timeOptions', this.timeOptions);
}, },
registerIndependentTimeOffsets() { registerIndependentTimeOffsets() {
if (!this.timeOptions.mode) { const timeContext = this.openmct.time.getIndependentContext(this.keyString);
return;
}
let offsets; let offsets;
if (this.isFixed) { if (this.isFixed) {
@ -234,15 +293,21 @@ export default {
offsets = this.timeOptions.clockOffsets; offsets = this.timeOptions.clockOffsets;
} }
const timeContext = this.openmct.time.getIndependentContext(this.keyString);
if (!timeContext.hasOwnContext()) { if (!timeContext.hasOwnContext()) {
this.unregisterIndependentTime = this.openmct.time.addIndependentContext(this.keyString, offsets, this.isFixed ? undefined : this.mode.key); this.unregisterIndependentTime = this.openmct.time.addIndependentContext(
this.keyString,
offsets,
this.isFixed ? undefined : this.timeOptions.clock,
this.timeOptions.mode
);
} else { } else {
if (this.isFixed) { timeContext.setMode(this.timeOptions.mode);
if (timeContext.isFixed()) {
//TODO: Do we need to stopClock here? I think technically we should never stop the clock, ever
timeContext.stopClock(); timeContext.stopClock();
timeContext.bounds(offsets); timeContext.setBounds(offsets);
} else { } else {
timeContext.clock(this.mode.key, offsets); timeContext.setClock(this.timeOptions.clock, offsets);
} }
} }
}, },
@ -250,6 +315,9 @@ export default {
if (this.unregisterIndependentTime) { if (this.unregisterIndependentTime) {
this.unregisterIndependentTime(); this.unregisterIndependentTime();
} }
},
updateTimeOptionProperty(option) {
return Object.assign({}, this.timeOptions, option);
} }
} }
}; };

View File

@ -1,225 +0,0 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web 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 Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div
v-if="modes.length > 1"
ref="modeMenuButton"
class="c-ctrl-wrapper c-ctrl-wrapper--menus-up"
>
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
v-if="selectedMode"
class="c-button--menu c-mode-button"
@click.prevent.stop="showModesMenu"
>
<span class="c-button__label">{{ selectedMode.name }}</span>
</button>
</div>
</div>
</template>
<script>
import toggleMixin from '../../../ui/mixins/toggle-mixin';
export default {
mixins: [toggleMixin],
inject: ['openmct'],
props: {
mode: {
type: Object,
default() {
return undefined;
}
},
enabled: {
type: Boolean,
default() {
return false;
}
}
},
data: function () {
let clock;
if (this.mode && this.mode.key === 'fixed') {
clock = undefined;
} else {
//We want the clock from the global time context here
clock = this.openmct.time.clock();
}
if (clock !== undefined) {
//Create copy of active clock so the time API does not get reactified.
clock = Object.create(clock);
}
return {
selectedMode: this.getModeOptionForClock(clock),
modes: []
};
},
watch: {
mode: {
deep: true,
handler(newMode) {
if (newMode) {
this.setViewFromClock(newMode.key === 'fixed' ? undefined : newMode);
}
}
},
enabled(newValue, oldValue) {
if (newValue !== undefined && (newValue !== oldValue) && (newValue === true)) {
this.setViewFromClock(this.mode.key === 'fixed' ? undefined : this.mode);
}
}
},
mounted: function () {
if (this.mode) {
this.setViewFromClock(this.mode.key === 'fixed' ? undefined : this.mode);
}
this.followTimeConductor();
},
destroyed: function () {
this.stopFollowTimeConductor();
},
methods: {
followTimeConductor() {
this.openmct.time.on('clock', this.setViewFromClock);
},
stopFollowTimeConductor() {
this.openmct.time.off('clock', this.setViewFromClock);
},
showModesMenu() {
const elementBoundingClientRect = this.$refs.modeMenuButton.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y + elementBoundingClientRect.height;
const menuOptions = {
menuClass: 'c-conductor__mode-menu',
placement: this.openmct.menus.menuPlacement.BOTTOM_RIGHT
};
this.openmct.menus.showSuperMenu(x, y, this.modes, menuOptions);
},
getMenuOptions() {
let clocks = [{
name: 'Fixed Timespan',
timeSystem: 'utc'
}];
let currentGlobalClock = this.openmct.time.clock();
if (currentGlobalClock !== undefined) {
//Create copy of active clock so the time API does not get reactified.
currentGlobalClock = Object.assign({}, {
name: currentGlobalClock.name,
clock: currentGlobalClock.key,
timeSystem: this.openmct.time.timeSystem().key
});
clocks.push(currentGlobalClock);
}
return clocks;
},
loadClocks() {
let clocks = this.getMenuOptions()
.map(menuOption => menuOption.clock)
.filter(isDefinedAndUnique)
.map(this.getClock);
/*
* Populate the modes menu with metadata from the available clocks
* "Fixed Mode" is always first, and has no defined clock
*/
this.modes = [undefined]
.concat(clocks)
.map(this.getModeOptionForClock);
function isDefinedAndUnique(key, index, array) {
return key !== undefined && array.indexOf(key) === index;
}
},
getModeOptionForClock(clock) {
if (clock === undefined) {
const key = 'fixed';
return {
key,
name: 'Fixed Timespan',
description: 'Query and explore data that falls between two fixed datetimes.',
cssClass: 'icon-tabular',
onItemClicked: () => this.setOption(key)
};
} else {
const key = clock.key;
return {
key,
name: clock.name,
description: "Monitor streaming data in real-time. The Time "
+ "Conductor and displays will automatically advance themselves based on this clock. " + clock.description,
cssClass: clock.cssClass || 'icon-clock',
onItemClicked: () => this.setOption(key)
};
}
},
getClock(key) {
return this.openmct.time.getAllClocks().filter(function (clock) {
return clock.key === key;
})[0];
},
setOption(clockKey) {
let key = clockKey;
if (clockKey === 'fixed') {
key = undefined;
}
const matchingOptions = this.getMenuOptions().filter(option => option.clock === key);
const clock = matchingOptions.length && matchingOptions[0].clock ? Object.assign({}, matchingOptions[0], { key: matchingOptions[0].clock }) : undefined;
this.selectedMode = this.getModeOptionForClock(clock);
if (this.mode) {
this.$emit('modeChanged', { key: clockKey });
}
},
setViewFromClock(clock) {
this.loadClocks();
//retain the mode chosen by the user
if (this.mode) {
let found = this.modes.find(mode => mode.key === this.selectedMode.key);
if (!found) {
found = this.modes.find(mode => mode.key === clock && clock.key);
this.setOption(found ? this.getModeOptionForClock(clock).key : this.getModeOptionForClock().key);
} else if (this.mode.key !== this.selectedMode.key) {
this.setOption(this.selectedMode.key);
}
} else {
this.setOption(this.getModeOptionForClock(clock).key);
}
}
}
};
</script>

View File

@ -0,0 +1,115 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, 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 raf from '@/utils/raf';
import debounce from '@/utils/debounce';
export default {
data() {
return {
showConductorPopup: false,
positionX: 0,
positionY: 0,
conductorPopup: null
};
},
mounted() {
this.positionBox = debounce(raf(this.positionBox), 250);
this.timeConductorOptionsHolder = this.$el;
this.timeConductorOptionsHolder.addEventListener('click', this.showPopup);
},
beforeDestroy() {
this.clearPopup();
},
methods: {
initializePopup() {
this.conductorPopup = this.$refs.conductorPopup.$el;
document.body.appendChild(this.conductorPopup); // remove from container as it (and it's ancestors) have overflow:hidden
this.$nextTick(() => {
window.addEventListener('resize', this.positionBox);
document.addEventListener('click', this.handleClickAway);
this.positionBox();
});
},
showPopup(clickEvent) {
const isToggle = clickEvent.target.classList.contains('c-toggle-switch__slider');
// no current popup, itc toggled
if (!this.conductorPopup && !isToggle) {
this.showConductorPopup = true;
}
},
handleClickAway(clickEvent) {
const isToggle = clickEvent.target.classList.contains('c-toggle-switch__slider');
if (!isToggle && this.canClose(clickEvent)) {
clickEvent.stopPropagation();
this.clearPopup();
}
},
positionBox() {
if (!this.conductorPopup) {
return;
}
const timeConductorOptionsBox = this.timeConductorOptionsHolder.getBoundingClientRect();
const topHalf = timeConductorOptionsBox.top < (window.innerHeight / 2);
const padding = 5;
this.positionX = timeConductorOptionsBox.left;
if (topHalf) {
this.positionY = timeConductorOptionsBox.bottom + this.conductorPopup.clientHeight + padding;
} else {
this.positionY = timeConductorOptionsBox.top - padding;
}
const offsetTop = this.conductorPopup.getBoundingClientRect().height;
const popupRight = this.positionX + this.conductorPopup.clientWidth;
const offsetLeft = Math.min(window.innerWidth - popupRight, 0);
this.positionX = this.positionX + offsetLeft;
this.positionY = this.positionY - offsetTop;
},
clearPopup() {
if (!this.conductorPopup) {
return;
}
if (this.conductorPopup.parentNode === document.body) {
document.body.removeChild(this.conductorPopup);
}
this.showConductorPopup = false;
this.conductorPopup = null;
document.removeEventListener('click', this.handleClickAway);
window.removeEventListener('resize', this.positionBox);
},
canClose(clickAwayEvent) {
const isChildMenu = clickAwayEvent.target.closest('.c-menu') !== null;
const isPopupElementItem = this.timeConductorOptionsHolder.contains(clickAwayEvent.target);
return !isChildMenu && !isPopupElementItem;
}
}
};

View File

@ -0,0 +1,88 @@
import { FIXED_MODE_KEY, REALTIME_MODE_KEY } from '../../api/time/constants';
export default {
props: {
buttonCssClass: {
type: String,
required: false,
default() {
return '';
}
}
},
methods: {
loadModes() {
this.modes = [FIXED_MODE_KEY, REALTIME_MODE_KEY].map(this.getModeMetadata);
},
loadClocks(menuOptions) {
let clocks;
if (menuOptions) {
clocks = menuOptions
.map(menuOption => menuOption.clock)
.filter(isDefinedAndUnique)
.map(this.getClock);
}
this.clocks = clocks.map(this.getClockMetadata);
function isDefinedAndUnique(key, index, array) {
return key !== undefined && array.indexOf(key) === index;
}
},
getActiveClock() {
const activeClock = this.openmct.time.getClock();
//Create copy of active clock so the time API does not get reactified.
return Object.create(activeClock);
},
getClock(key) {
return this.openmct.time.getAllClocks().find(clock => clock.key === key);
},
getModeMetadata(mode, testIds = false) {
let modeOptions;
const key = mode;
if (key === FIXED_MODE_KEY) {
modeOptions = {
key,
name: 'Fixed Timespan',
description: 'Query and explore data that falls between two fixed datetimes.',
cssClass: 'icon-tabular',
onItemClicked: () => this.setMode(key)
};
if (testIds) {
modeOptions.testId = 'conductor-modeOption-fixed';
}
} else {
modeOptions = {
key,
name: 'Real-Time',
description: 'Monitor streaming data in real-time. The Time Conductor and displays will automatically advance themselves based on the active clock.',
cssClass: 'icon-clock',
onItemClicked: () => this.setMode(key)
};
if (testIds) {
modeOptions.testId = 'conductor-modeOption-realtime';
}
}
return modeOptions;
},
getClockMetadata(clock) {
const key = clock.key;
const clockOptions = {
key,
name: clock.name,
description: "Monitor streaming data in real-time. The Time "
+ "Conductor and displays will automatically advance themselves based on this clock. " + clock.description,
cssClass: clock.cssClass || 'icon-clock',
onItemClicked: () => this.setClock(key)
};
return clockOptions;
}
}
};

View File

@ -109,11 +109,20 @@ export default function (config) {
throwIfError(configResult); throwIfError(configResult);
const defaults = config.menuOptions[0]; const defaults = config.menuOptions[0];
if (defaults.clock) { const defaultClock = defaults.clock;
openmct.time.clock(defaults.clock, defaults.clockOffsets);
openmct.time.timeSystem(defaults.timeSystem, openmct.time.bounds()); if (defaultClock) {
openmct.time.setClock(defaults.clock, defaults.clockOffsets);
openmct.time.setTimeSystem(defaults.timeSystem, openmct.time.getBounds());
} else { } else {
openmct.time.timeSystem(defaults.timeSystem, defaults.bounds); // always have an active clock, regardless of mode
const firstClock = config.menuOptions.find(option => option.clock);
if (firstClock) {
openmct.time.setClock(firstClock.clock, firstClock.clockOffsets);
}
openmct.time.setTimeSystem(defaults.timeSystem, defaults.bounds);
} }
openmct.on('start', function () { openmct.on('start', function () {

View File

@ -106,7 +106,7 @@ describe('time conductor', () => {
describe('in realtime mode', () => { describe('in realtime mode', () => {
beforeEach((done) => { beforeEach((done) => {
const switcher = appHolder.querySelector('.c-mode-button'); const switcher = appHolder.querySelector('.js-mode-button');
const clickEvent = createMouseEvent("click"); const clickEvent = createMouseEvent("click");
switcher.dispatchEvent(clickEvent); switcher.dispatchEvent(clickEvent);

View File

@ -1,181 +0,0 @@
<template>
<div
class="pr-tc-input-menu"
:class="{'pr-tc-input-menu--bottom' : bottom === true}"
@keydown.enter.prevent
@keyup.enter.prevent="submit"
@keydown.esc.prevent
@keyup.esc.prevent="hide"
@click.stop
>
<div class="pr-time-label__hrs">Hrs</div>
<div class="pr-time-label__mins">Mins</div>
<div class="pr-time-label__secs">Secs</div>
<div class="pr-time-controls">
<input
ref="inputHrs"
v-model="inputHrs"
class="pr-time-controls__hrs"
step="1"
type="number"
min="0"
max="23"
title="Enter 0 - 23"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('inputHrs')"
@wheel="increment($event, 'inputHrs')"
>
:
</div>
<div class="pr-time-controls">
<input
ref="inputMins"
v-model="inputMins"
type="number"
class="pr-time-controls__mins"
min="0"
max="59"
title="Enter 0 - 59"
step="1"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('inputMins')"
@wheel="increment($event, 'inputMins')"
>
:
</div>
<div class="pr-time-controls">
<input
ref="inputSecs"
v-model="inputSecs"
type="number"
class="pr-time-controls__secs"
min="0"
max="59"
title="Enter 0 - 59"
step="1"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('inputSecs')"
@wheel="increment($event, 'inputSecs')"
>
<div class="pr-time__buttons">
<button
class="c-button c-button--major icon-check"
:disabled="isDisabled"
@click.prevent="submit"
></button>
<button
class="c-button icon-x"
@click.prevent="hide"
></button>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
type: {
type: String,
required: true
},
offset: {
type: String,
required: true
},
bottom: {
type: Boolean,
default() {
return false;
}
}
},
data() {
return {
inputHrs: '00',
inputMins: '00',
inputSecs: '00',
isDisabled: false
};
},
mounted() {
this.setOffset();
document.addEventListener('click', this.hide);
},
beforeDestroy() {
document.removeEventListener('click', this.hide);
},
methods: {
format(ref) {
const curVal = this[ref];
this[ref] = curVal.padStart(2, '0');
},
validate() {
let disabled = false;
let refs = ['inputHrs', 'inputMins', 'inputSecs'];
for (let ref of refs) {
let min = Number(this.$refs[ref].min);
let max = Number(this.$refs[ref].max);
let value = Number(this.$refs[ref].value);
if (value > max || value < min) {
disabled = true;
break;
}
}
this.isDisabled = disabled;
},
submit() {
this.$emit('update', {
type: this.type,
hours: this.inputHrs,
minutes: this.inputMins,
seconds: this.inputSecs
});
},
hide() {
this.$emit('hide');
},
increment($ev, ref) {
$ev.preventDefault();
const step = (ref === 'inputHrs') ? 1 : 5;
const maxVal = (ref === 'inputHrs') ? 23 : 59;
let cv = Math.round(parseInt(this[ref], 10) / step) * step;
cv = Math.min(maxVal, Math.max(0, ($ev.deltaY < 0) ? cv + step : cv - step));
this[ref] = cv.toString().padStart(2, '0');
this.validate();
},
setOffset() {
[this.inputHrs, this.inputMins, this.inputSecs] = this.offset.split(':');
this.numberSelect('inputHrs');
},
numberSelect(input) {
this.$refs[input].focus();
// change to text, select, then change back to number
// number inputs do not support select()
this.$nextTick(() => {
this.$refs[input].setAttribute('type', 'text');
this.$refs[input].select();
this.$nextTick(() => {
this.$refs[input].setAttribute('type', 'number');
});
});
},
selectAll($ev) {
$ev.target.select();
}
}
};
</script>

View File

@ -0,0 +1,314 @@
<template>
<form
ref="fixedDeltaInput"
class="c-tc-input-popup__input-grid"
>
<div class="pr-time-label"><em>Start</em> Date</div>
<div class="pr-time-label">Time Z</div>
<div class="pr-time-label"></div>
<div class="pr-time-label"><em>End</em> Date</div>
<div class="pr-time-label">Time Z</div>
<div class="pr-time-label"></div>
<div class="pr-time-input pr-time-input--date pr-time-input--input-and-button">
<input
ref="startDate"
v-model="formattedBounds.start"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds('startDate'); submitForm()"
>
<date-picker
v-if="isUTCBased"
class="c-ctrl-wrapper--menus-left"
:default-date-time="formattedBounds.start"
:formatter="timeFormatter"
@date-selected="startDateSelected"
/>
</div>
<div class="pr-time-input pr-time-input--time">
<input
ref="startTime"
v-model="formattedBounds.startTime"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds('startDate'); submitForm()"
>
</div>
<div class="pr-time-input pr-time-input__start-end-sep icon-arrows-right-left"></div>
<div class="pr-time-input pr-time-input--date pr-time-input--input-and-button">
<input
ref="endDate"
v-model="formattedBounds.end"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds('endDate'); submitForm()"
>
<date-picker
v-if="isUTCBased"
class="c-ctrl-wrapper--menus-left"
:default-date-time="formattedBounds.end"
:formatter="timeFormatter"
@date-selected="endDateSelected"
/>
</div>
<div class="pr-time-input pr-time-input--time">
<input
ref="endTime"
v-model="formattedBounds.endTime"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds('endDate'); submitForm()"
>
</div>
<div class="pr-time-input pr-time-input--buttons">
<button
class="c-button c-button--major icon-check"
:disabled="isDisabled"
@click.prevent="submit"
></button>
<button
class="c-button icon-x"
@click.prevent="hide"
></button>
</div>
</form>
</template>
<script>
import _ from "lodash";
import DatePicker from "./DatePicker.vue";
const DEFAULT_DURATION_FORMATTER = 'duration';
export default {
components: {
DatePicker
},
inject: ['openmct'],
props: {
inputBounds: {
type: Object,
required: true
},
inputTimeSystem: {
type: Object,
required: true
}
},
data() {
let timeSystem = this.openmct.time.getTimeSystem();
let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
let timeFormatter = this.getFormatter(timeSystem.timeFormat);
let bounds = this.bounds || this.openmct.time.bounds();
return {
timeFormatter,
durationFormatter,
bounds: {
start: bounds.start,
end: bounds.end
},
formattedBounds: {
start: timeFormatter.format(bounds.start).split(' ')[0],
end: timeFormatter.format(bounds.end).split(' ')[0],
startTime: durationFormatter.format(Math.abs(bounds.start)),
endTime: durationFormatter.format(Math.abs(bounds.end))
},
isUTCBased: timeSystem.isUTCBased,
isDisabled: false
};
},
watch: {
inputBounds: {
handler(newBounds) {
this.handleNewBounds(newBounds);
},
deep: true
},
inputTimeSystem: {
handler(newTimeSystem) {
this.setTimeSystem(newTimeSystem);
},
deep: true
}
},
mounted() {
this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.getTimeSystem())));
},
beforeDestroy() {
this.clearAllValidation();
},
methods: {
handleNewBounds(bounds) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
},
clearAllValidation() {
[this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput);
},
clearValidationForInput(input) {
input.setCustomValidity('');
input.title = '';
},
setBounds(bounds) {
this.bounds = bounds;
},
setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start).split(' ')[0];
this.formattedBounds.end = this.timeFormatter.format(bounds.end).split(' ')[0];
this.formattedBounds.startTime = this.durationFormatter.format(Math.abs(bounds.start));
this.formattedBounds.endTime = this.durationFormatter.format(Math.abs(bounds.end));
},
setTimeSystem(timeSystem) {
this.timeSystem = timeSystem;
this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
this.durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.isUTCBased = timeSystem.isUTCBased;
},
getFormatter(key) {
return this.openmct.telemetry.getValueFormatter({
format: key
}).formatter;
},
setBoundsFromView(dismiss) {
if (this.$refs.fixedDeltaInput.checkValidity()) {
let start = this.timeFormatter.parse(`${this.formattedBounds.start} ${this.formattedBounds.startTime}`);
let end = this.timeFormatter.parse(`${this.formattedBounds.end} ${this.formattedBounds.endTime}`);
this.$emit('update', {
start: start,
end: end
});
}
if (dismiss) {
this.$emit('dismiss');
return false;
}
},
submit() {
this.validateAllBounds('startDate');
this.validateAllBounds('endDate');
this.submitForm(!this.isDisabled);
},
submitForm(dismiss) {
// Allow Vue model to catch up to user input.
// Submitting form will cause validation messages to display (but only if triggered by button click)
this.$nextTick(() => this.setBoundsFromView(dismiss));
},
validateAllBounds(ref) {
if (!this.areBoundsFormatsValid()) {
return false;
}
let validationResult = {
valid: true
};
const currentInput = this.$refs[ref];
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
let boundsValues = {
start: this.timeFormatter.parse(`${this.formattedBounds.start} ${this.formattedBounds.startTime}`),
end: this.timeFormatter.parse(`${this.formattedBounds.end} ${this.formattedBounds.endTime}`)
};
//TODO: Do we need limits here? We have conductor limits disabled right now
// const limit = this.getBoundsLimit();
const limit = false;
if (this.timeSystem.isUTCBased && limit
&& boundsValues.end - boundsValues.start > limit) {
if (input === currentInput) {
validationResult = {
valid: false,
message: "Start and end difference exceeds allowable limit"
};
}
} else {
if (input === currentInput) {
validationResult = this.openmct.time.validateBounds(boundsValues);
}
}
return this.handleValidationResults(input, validationResult);
});
},
areBoundsFormatsValid() {
let validationResult = {
valid: true
};
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
const formattedDate = input === this.$refs.startDate
? `${this.formattedBounds.start} ${this.formattedBounds.startTime}`
: `${this.formattedBounds.end} ${this.formattedBounds.endTime}`
;
if (!this.timeFormatter.validate(formattedDate)) {
validationResult = {
valid: false,
message: 'Invalid date'
};
}
return this.handleValidationResults(input, validationResult);
});
},
getBoundsLimit() {
const configuration = this.configuration.menuOptions
.filter(option => option.timeSystem === this.timeSystem.key)
.find(option => option.limit);
const limit = configuration ? configuration.limit : undefined;
return limit;
},
handleValidationResults(input, validationResult) {
if (validationResult.valid !== true) {
input.setCustomValidity(validationResult.message);
input.title = validationResult.message;
this.isDisabled = true;
} else {
input.setCustomValidity('');
input.title = '';
this.isDisabled = false;
}
this.$refs.fixedDeltaInput.reportValidity();
return validationResult.valid;
},
startDateSelected(date) {
this.formattedBounds.start = this.timeFormatter.format(date).split(' ')[0];
this.validateAllBounds('startDate');
this.submitForm();
},
endDateSelected(date) {
this.formattedBounds.end = this.timeFormatter.format(date).split(' ')[0];
this.validateAllBounds('endDate');
this.submitForm();
},
hide($event) {
if ($event.target.className.indexOf('c-button icon-x') > -1) {
this.$emit('dismiss');
}
}
}
};
</script>

View File

@ -0,0 +1,252 @@
<template>
<form
ref="deltaInput"
class="c-tc-input-popup__input-grid"
>
<div class="pr-time-label icon-minus">Hrs</div>
<div class="pr-time-label">Mins</div>
<div class="pr-time-label">Secs</div>
<div class="pr-time-label"></div>
<div class="pr-time-label icon-plus">Hrs</div>
<div class="pr-time-label">Mins</div>
<div class="pr-time-label">Secs</div>
<div class="pr-time-label"></div>
<div class="pr-time-input">
<input
ref="startInputHrs"
v-model="startInputHrs"
class="pr-time-input__hrs"
step="1"
type="number"
min="0"
max="23"
title="Enter 0 - 23"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('startInputHrs')"
@wheel="increment($event, 'startInputHrs')"
>
<b>:</b>
</div>
<div class="pr-time-input">
<input
ref="startInputMins"
v-model="startInputMins"
type="number"
class="pr-time-input__mins"
min="0"
max="59"
title="Enter 0 - 59"
step="1"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('startInputMins')"
@wheel="increment($event, 'startInputMins')"
>
<b>:</b>
</div>
<div class="pr-time-input">
<input
ref="startInputSecs"
v-model="startInputSecs"
type="number"
class="pr-time-input__secs"
min="0"
max="59"
title="Enter 0 - 59"
step="1"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('startInputSecs')"
@wheel="increment($event, 'startInputSecs')"
>
</div>
<div class="pr-time-input pr-time-input__start-end-sep icon-arrows-right-left"></div>
<div class="pr-time-input">
<input
ref="endInputHrs"
v-model="endInputHrs"
class="pr-time-input__hrs"
step="1"
type="number"
min="0"
max="23"
title="Enter 0 - 23"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('endInputHrs')"
@wheel="increment($event, 'endInputHrs')"
>
<b>:</b>
</div>
<div class="pr-time-input">
<input
ref="endInputMins"
v-model="endInputMins"
type="number"
class="pr-time-input__mins"
min="0"
max="59"
title="Enter 0 - 59"
step="1"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('endInputMins')"
@wheel="increment($event, 'endInputMins')"
>
<b>:</b>
</div>
<div class="pr-time-input">
<input
ref="endInputSecs"
v-model="endInputSecs"
type="number"
class="pr-time-input__secs"
min="0"
max="59"
title="Enter 0 - 59"
step="1"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('endInputSecs')"
@wheel="increment($event, 'endInputSecs')"
>
</div>
<div class="pr-time-input pr-time-input--buttons">
<button
class="c-button c-button--major icon-check"
:disabled="isDisabled"
@click.prevent="submit"
></button>
<button
class="c-button icon-x"
@click.prevent="hide"
></button>
</div>
</form>
</template>
<script>
export default {
props: {
offsets: {
type: Object,
required: true
}
},
data() {
return {
startInputHrs: '00',
startInputMins: '00',
startInputSecs: '00',
endInputHrs: '00',
endInputMins: '00',
endInputSecs: '00',
isDisabled: false
};
},
watch: {
offsets: {
handler() {
this.setOffsets();
},
deep: true
}
},
mounted() {
this.setOffsets();
document.addEventListener('click', this.hide);
},
beforeDestroy() {
document.removeEventListener('click', this.hide);
},
methods: {
format(ref) {
const curVal = this[ref];
this[ref] = curVal.padStart(2, '0');
},
validate() {
let disabled = false;
let refs = ['startInputHrs', 'startInputMins', 'startInputSecs', 'endInputHrs', 'endInputMins', 'endInputSecs'];
for (let ref of refs) {
let min = Number(this.$refs[ref].min);
let max = Number(this.$refs[ref].max);
let value = Number(this.$refs[ref].value);
if (value > max || value < min) {
disabled = true;
break;
}
}
this.isDisabled = disabled;
},
submit() {
this.$emit('update', {
start: {
hours: this.startInputHrs,
minutes: this.startInputMins,
seconds: this.startInputSecs
},
end: {
hours: this.endInputHrs,
minutes: this.endInputMins,
seconds: this.endInputSecs
}
});
this.$emit('dismiss');
},
hide($event) {
if ($event.target.className.indexOf('c-button icon-x') > -1) {
this.$emit('dismiss');
}
},
increment($ev, ref) {
$ev.preventDefault();
const step = (ref === 'startInputHrs' || ref === 'endInputHrs') ? 1 : 5;
const maxVal = (ref === 'startInputHrs' || ref === 'endInputHrs') ? 23 : 59;
let cv = Math.round(parseInt(this[ref], 10) / step) * step;
cv = Math.min(maxVal, Math.max(0, ($ev.deltaY < 0) ? cv + step : cv - step));
this[ref] = cv.toString().padStart(2, '0');
this.validate();
},
setOffsets() {
[this.startInputHrs, this.startInputMins, this.startInputSecs] = this.offsets.start.split(':');
[this.endInputHrs, this.endInputMins, this.endInputSecs] = this.offsets.end.split(':');
this.numberSelect('startInputHrs');
},
numberSelect(input) {
this.$refs[input].focus();
// change to text, select, then change back to number
// number inputs do not support select()
this.$nextTick(() => {
if (this.$refs[input] === undefined) {
return;
}
this.$refs[input].setAttribute('type', 'text');
this.$refs[input].select();
this.$nextTick(() => {
this.$refs[input].setAttribute('type', 'number');
});
});
},
selectAll($ev) {
$ev.target.select();
}
}
};
</script>

View File

@ -81,6 +81,7 @@ $colorInteriorBorder: rgba($colorBodyFg, 0.2);
$colorA: #ccc; $colorA: #ccc;
$colorAHov: #fff; $colorAHov: #fff;
$filterHov: brightness(1.3) contrast(1.5); // Tree, location items $filterHov: brightness(1.3) contrast(1.5); // Tree, location items
$filterHovSubtle: brightness(1.2) contrast(1.2);
$colorSelectedBg: rgba($colorKey, 0.3); $colorSelectedBg: rgba($colorKey, 0.3);
$colorSelectedFg: pullForward($colorBodyFg, 20%); $colorSelectedFg: pullForward($colorBodyFg, 20%);
@ -139,13 +140,30 @@ $colorBodyBgSubtleHov: pullForward($colorBodyBg, 10%);
$colorKeySubtle: pushBack($colorKey, 10%); $colorKeySubtle: pushBack($colorKey, 10%);
// Time Colors // Time Colors
$colorTime: #618cff; $colorTimeFixed: #59554C;
$colorTimeBg: $colorTime; $colorTimeFixedBg: $colorTimeFixed;
$colorTimeFg: pullForward($colorTimeBg, 30%); $colorTimeFixedFg: #eee;
$colorTimeHov: pullForward($colorTime, 10%); $colorTimeFixedFgSubtle: #B2AA98;
$colorTimeSubtle: pushBack($colorTime, 20%); $colorTimeFixedHov: pullForward($colorTimeFixed, 10%);
$colorTimeFixedSubtle: pushBack($colorTimeFixed, 20%);
$colorTimeFixedBtnBg: pullForward($colorTimeFixed, 5%);
$colorTimeFixedBtnFg: $colorTimeFixedFgSubtle;
$colorTimeFixedBtnBgMajor: #a09375;
$colorTimeFixedBtnFgMajor: #fff;
$colorTimeRealtime: #445890;
$colorTimeRealtimeBg: $colorTimeRealtime;
$colorTimeRealtimeFg: #eee;
$colorTimeRealtimeFgSubtle: #88B0FF;
$colorTimeRealtimeHov: pullForward($colorTimeRealtime, 10%);
$colorTimeRealtimeSubtle: pushBack($colorTimeRealtime, 20%);
$colorTimeRealtimeBtnBg: pullForward($colorTimeRealtime, 5%);
$colorTimeRealtimeBtnFg: $colorTimeRealtimeFgSubtle;
$colorTimeRealtimeBtnBgMajor: #588ffa;
$colorTimeRealtimeBtnFgMajor: #fff;
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor $colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov $colorTOIHov: $colorTimeRealtime; // was $timeControllerToiLineColorHov
$timeConductorAxisHoverFilter: brightness(1.2); $timeConductorAxisHoverFilter: brightness(1.2);
$timeConductorActiveBg: $colorKey; $timeConductorActiveBg: $colorKey;
$timeConductorActivePanBg: #226074; $timeConductorActivePanBg: #226074;
@ -241,7 +259,7 @@ $controlDisabledOpacity: 0.2;
$colorMenuBg: $colorBodyBg; $colorMenuBg: $colorBodyBg;
$colorMenuFg: $colorBodyFg; $colorMenuFg: $colorBodyFg;
$colorMenuIc: $colorKey; $colorMenuIc: $colorKey;
$filterMenu: brightness(1.4); $filterMenu: brightness(1.2);
$colorMenuHovBg: rgba($colorKey, 0.5); $colorMenuHovBg: rgba($colorKey, 0.5);
$colorMenuHovFg: $colorBodyFgEm; $colorMenuHovFg: $colorBodyFgEm;
$colorMenuHovIc: $colorMenuHovFg; $colorMenuHovIc: $colorMenuHovFg;

View File

@ -85,6 +85,7 @@ $colorInteriorBorder: rgba($colorBodyFg, 0.2);
$colorA: #ccc; $colorA: #ccc;
$colorAHov: #fff; $colorAHov: #fff;
$filterHov: brightness(1.3) contrast(1.5); // Tree, location items $filterHov: brightness(1.3) contrast(1.5); // Tree, location items
$filterHovSubtle: brightness(1.2) contrast(1.2);
$colorSelectedBg: rgba($colorKey, 0.3); $colorSelectedBg: rgba($colorKey, 0.3);
$colorSelectedFg: pullForward($colorBodyFg, 20%); $colorSelectedFg: pullForward($colorBodyFg, 20%);
@ -143,13 +144,30 @@ $colorBodyBgSubtleHov: pushBack($colorKey, 50%);
$colorKeySubtle: pushBack($colorKey, 10%); $colorKeySubtle: pushBack($colorKey, 10%);
// Time Colors // Time Colors
$colorTime: #618cff; $colorTimeFixed: #59554C;
$colorTimeBg: $colorTime; $colorTimeFixedBg: $colorTimeFixed;
$colorTimeFg: pullForward($colorTimeBg, 30%); $colorTimeFixedFg: #eee;
$colorTimeHov: pullForward($colorTime, 10%); $colorTimeFixedFgSubtle: #B2AA98;
$colorTimeSubtle: pushBack($colorTime, 20%); $colorTimeFixedHov: pullForward($colorTimeFixed, 10%);
$colorTimeFixedSubtle: pushBack($colorTimeFixed, 20%);
$colorTimeFixedBtnBg: pullForward($colorTimeFixed, 5%);
$colorTimeFixedBtnFg: $colorTimeFixedFgSubtle;
$colorTimeFixedBtnBgMajor: #a09375;
$colorTimeFixedBtnFgMajor: #fff;
$colorTimeRealtime: #445890;
$colorTimeRealtimeBg: $colorTimeRealtime;
$colorTimeRealtimeFg: #eee;
$colorTimeRealtimeFgSubtle: #88B0FF;
$colorTimeRealtimeHov: pullForward($colorTimeRealtime, 10%);
$colorTimeRealtimeSubtle: pushBack($colorTimeRealtime, 20%);
$colorTimeRealtimeBtnBg: pullForward($colorTimeRealtime, 5%);
$colorTimeRealtimeBtnFg: $colorTimeRealtimeFgSubtle;
$colorTimeRealtimeBtnBgMajor: #588ffa;
$colorTimeRealtimeBtnFgMajor: #fff;
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor $colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov $colorTOIHov: $colorTimeRealtime; // was $timeControllerToiLineColorHov
$timeConductorAxisHoverFilter: brightness(1.2); $timeConductorAxisHoverFilter: brightness(1.2);
$timeConductorActiveBg: $colorKey; $timeConductorActiveBg: $colorKey;
$timeConductorActivePanBg: #226074; $timeConductorActivePanBg: #226074;

View File

@ -81,6 +81,7 @@ $colorInteriorBorder: rgba($colorBodyFg, 0.2);
$colorA: $colorBodyFg; $colorA: $colorBodyFg;
$colorAHov: $colorKey; $colorAHov: $colorKey;
$filterHov: hue-rotate(-10deg) brightness(0.8) contrast(2); // Tree, location items $filterHov: hue-rotate(-10deg) brightness(0.8) contrast(2); // Tree, location items
$filterHovSubtle: hue-rotate(-8deg) brightness(0.5) contrast(1.2);
$colorSelectedBg: pushBack($colorKey, 40%); $colorSelectedBg: pushBack($colorKey, 40%);
$colorSelectedFg: pullForward($colorBodyFg, 10%); $colorSelectedFg: pullForward($colorBodyFg, 10%);
@ -139,13 +140,30 @@ $colorBodyBgSubtleHov: pullForward($colorBodyBg, 10%);
$colorKeySubtle: pushBack($colorKey, 20%); $colorKeySubtle: pushBack($colorKey, 20%);
// Time Colors // Time Colors
$colorTime: #618cff; $colorTimeFixed: #59554C;
$colorTimeBg: $colorTime; $colorTimeFixedBg: $colorTimeFixed;
$colorTimeFg: $colorBodyBg; $colorTimeFixedFg: #eee;
$colorTimeHov: pushBack($colorTime, 5%); $colorTimeFixedFgSubtle: #B2AA98;
$colorTimeSubtle: pushBack($colorTime, 20%); $colorTimeFixedHov: pullForward($colorTimeFixed, 10%);
$colorTimeFixedSubtle: pushBack($colorTimeFixed, 20%);
$colorTimeFixedBtnBg: pullForward($colorTimeFixed, 5%);
$colorTimeFixedBtnFg: $colorTimeFixedFgSubtle;
$colorTimeFixedBtnBgMajor: #a09375;
$colorTimeFixedBtnFgMajor: #fff;
$colorTimeRealtime: #445890;
$colorTimeRealtimeBg: $colorTimeRealtime;
$colorTimeRealtimeFg: #eee;
$colorTimeRealtimeFgSubtle: #88B0FF;
$colorTimeRealtimeHov: pullForward($colorTimeRealtime, 10%);
$colorTimeRealtimeSubtle: pushBack($colorTimeRealtime, 20%);
$colorTimeRealtimeBtnBg: pullForward($colorTimeRealtime, 5%);
$colorTimeRealtimeBtnFg: $colorTimeRealtimeFgSubtle;
$colorTimeRealtimeBtnBgMajor: #588ffa;
$colorTimeRealtimeBtnFgMajor: #fff;
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor $colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov $colorTOIHov: $colorTimeRealtime; // was $timeControllerToiLineColorHov
$timeConductorAxisHoverFilter: brightness(0.8); $timeConductorAxisHoverFilter: brightness(0.8);
$timeConductorActiveBg: $colorKey; $timeConductorActiveBg: $colorKey;
$timeConductorActivePanBg: #A0CDE1; $timeConductorActivePanBg: #A0CDE1;

View File

@ -244,6 +244,13 @@ button {
} }
} }
.c-not-button {
// Use within a holder that's clickable; use to indicate interactability
@include cButtonLayout();
cursor: pointer;
}
/******************************************************** DISCLOSURE CONTROLS */ /******************************************************** DISCLOSURE CONTROLS */
/********* Disclosure Button */ /********* Disclosure Button */
// Provides a downward arrow icon that when clicked displays additional options and/or info. // Provides a downward arrow icon that when clicked displays additional options and/or info.

View File

@ -63,6 +63,11 @@ div {
} }
} }
.u-flex-spreader {
// Pushes against elements in a flex layout to spread them out
flex: 1 1 auto;
}
/******************************************************** BROWSER ELEMENTS */ /******************************************************** BROWSER ELEMENTS */
body.desktop { body.desktop {
::-webkit-scrollbar { ::-webkit-scrollbar {

View File

@ -521,24 +521,33 @@
} }
} }
@mixin cButton() { @mixin cButtonLayout() {
@include cControl(); $pad: $interiorMargin;
@include cControlHov(); padding: $pad floor($pad * 1.25);
@include themedButton();
border-radius: $controlCr;
color: $colorBtnFg;
cursor: pointer;
padding: $interiorMargin floor($interiorMargin * 1.25);
&:after, &:after,
> * + * { > * + * {
margin-left: $interiorMarginSm; margin-left: $interiorMarginSm;
} }
&[class*='--compact'] {
padding: floor(math.div($pad, 1.5)) $pad;
}
}
@mixin cButton() {
@include cControl();
@include cControlHov();
@include themedButton();
@include cButtonLayout();
border-radius: $controlCr;
color: $colorBtnFg;
cursor: pointer;
&[class*="--major"], &[class*="--major"],
&[class*='is-active']{ &[class*='is-active']{
background: $colorBtnMajorBg; background: $colorBtnMajorBg !important;
color: $colorBtnMajorFg; color: $colorBtnMajorFg !important;
} }
&[class*='--caution'] { &[class*='--caution'] {
@ -576,7 +585,7 @@
*:before { *:before {
// *:before handles any nested containers that may contain glyph elements // *:before handles any nested containers that may contain glyph elements
// Needed for c-togglebutton. // Needed for c-togglebutton.
font-size: 1.25em; font-size: 1.15em;
} }
} }

View File

@ -61,6 +61,15 @@
'has-complex-content': complexContent 'has-complex-content': complexContent
}" }"
> >
<div
v-if="supportsIndependentTime"
class="c-conductor-holder--compact"
>
<independent-time-conductor
:domain-object="domainObject"
:object-path="objectPath"
/>
</div>
<NotebookMenuSwitcher <NotebookMenuSwitcher
v-if="notebookEnabled" v-if="notebookEnabled"
:domain-object="domainObject" :domain-object="domainObject"
@ -105,6 +114,7 @@
<script> <script>
import ObjectView from './ObjectView.vue'; import ObjectView from './ObjectView.vue';
import NotebookMenuSwitcher from '@/plugins/notebook/components/NotebookMenuSwitcher.vue'; import NotebookMenuSwitcher from '@/plugins/notebook/components/NotebookMenuSwitcher.vue';
import IndependentTimeConductor from '@/plugins/timeConductor/independent/IndependentTimeConductor.vue';
const SIMPLE_CONTENT_TYPES = [ const SIMPLE_CONTENT_TYPES = [
'clock', 'clock',
@ -118,7 +128,8 @@ const CSS_WIDTH_LESS_STR = '--width-less-than-';
export default { export default {
components: { components: {
ObjectView, ObjectView,
NotebookMenuSwitcher NotebookMenuSwitcher,
IndependentTimeConductor
}, },
inject: ['openmct'], inject: ['openmct'],
props: { props: {
@ -163,6 +174,11 @@ export default {
computed: { computed: {
statusClass() { statusClass() {
return (this.status) ? `is-status--${this.status}` : ''; return (this.status) ? `is-status--${this.status}` : '';
},
supportsIndependentTime() {
// const viewKey = this.getViewKey();
return true; //this.domainObject && SupportedViewTypes.includes(viewKey);
} }
}, },
mounted() { mounted() {
@ -233,6 +249,14 @@ export default {
} }
this.widthClass = wClass.trimStart(); this.widthClass = wClass.trimStart();
},
getViewKey() {
let viewKey = this.this.$refs.objectView.viewKey;
if (this.objectViewKey) {
viewKey = this.objectViewKey;
}
return viewKey;
} }
} }
}; };

View File

@ -1,14 +1,12 @@
<template> <template>
<div> <div>
<div <div
v-if="supportsIndependentTime" v-if="supportsIndependentTime && false"
class="c-conductor-holder--compact l-shell__main-independent-time-conductor" class="c-conductor-holder--compact l-shell__main-independent-time-conductor"
> >
<independent-time-conductor <independent-time-conductor
:domain-object="domainObject" :domain-object="domainObject"
:object-path="path" :object-path="path"
@stateChanged="updateIndependentTimeState"
@updated="saveTimeOptions"
/> />
</div> </div>
<div <div
@ -457,13 +455,6 @@ export default {
if (elemToStyle !== undefined) { if (elemToStyle !== undefined) {
elemToStyle.dataset.font = newFont; elemToStyle.dataset.font = newFont;
} }
},
//Should the domainObject be updated in the Independent Time conductor component itself?
updateIndependentTimeState(useIndependentTime) {
this.openmct.objects.mutate(this.domainObject, 'configuration.useIndependentTime', useIndependentTime);
},
saveTimeOptions(options) {
this.openmct.objects.mutate(this.domainObject, 'configuration.timeOptions', options);
} }
} }
}; };

View File

@ -91,7 +91,7 @@ export default {
} }
}, },
updateNowMarker() { updateNowMarker() {
if (this.openmct.time.clock() === undefined) { if (this.openmct.time.getClock() === undefined) {
let nowMarker = document.querySelector('.nowMarker'); let nowMarker = document.querySelector('.nowMarker');
if (nowMarker) { if (nowMarker) {
nowMarker.classList.add('hidden'); nowMarker.classList.add('hidden');
@ -101,7 +101,7 @@ export default {
if (nowMarker) { if (nowMarker) {
nowMarker.classList.remove('hidden'); nowMarker.classList.remove('hidden');
nowMarker.style.height = this.contentHeight + 'px'; nowMarker.style.height = this.contentHeight + 'px';
const nowTimeStamp = this.openmct.time.clock().currentValue(); const nowTimeStamp = this.openmct.time.getClock().currentValue();
const now = this.xScale(nowTimeStamp); const now = this.xScale(nowTimeStamp);
nowMarker.style.left = now + this.offset + 'px'; nowMarker.style.left = now + this.offset + 'px';
} }
@ -136,7 +136,7 @@ export default {
} }
if (timeSystem === undefined) { if (timeSystem === undefined) {
timeSystem = this.openmct.time.timeSystem(); timeSystem = this.openmct.time.getTimeSystem();
} }
if (timeSystem.isUTCBased) { if (timeSystem.isUTCBased) {

View File

@ -15,6 +15,8 @@
.c-object-label { .c-object-label {
font-size: 1.05em; font-size: 1.05em;
min-width: 20%;
&__type-icon { &__type-icon {
opacity: $objectLabelTypeIconOpacity; opacity: $objectLabelTypeIconOpacity;
} }
@ -37,7 +39,8 @@
/*************************** FRAME CONTROLS */ /*************************** FRAME CONTROLS */
&__frame-controls { &__frame-controls {
display: flex; display: flex;
flex: 0 0 auto; flex: 0 1 auto;
overflow: hidden;
&__btns, &__btns,
&__more { &__more {

View File

@ -1,11 +1,10 @@
.c-object-label { .c-object-label {
// <a> tag and draggable element that holds type icon and name. // <a> tag and draggable element that holds type icon and name.
// Used mostly in trees and lists // Used mostly in trees and lists
@include ellipsize();
display: flex; display: flex;
align-items: center; align-items: center;
flex: 0 1 auto; flex: 0 1 auto;
overflow: hidden;
white-space: nowrap;
> * + * { margin-left: $interiorMargin; } > * + * { margin-left: $interiorMargin; }

View File

@ -1,9 +1,25 @@
@use 'sass:math'; @use 'sass:math';
.c-toggle-switch { @mixin toggleSwitch($d: 12px, $m: 2px, $bg: $colorBtnBg) {
$d: 12px;
$m: 2px;
$br: math.div($d, 1.5); $br: math.div($d, 1.5);
.c-toggle-switch__slider {
background: $bg;
border-radius: $br;
height: $d + ($m*2);
width: $d*2 + $m*2;
&:before {
// Knob
border-radius: floor($br * 0.8);
box-shadow: rgba(black, 0.4) 0 0 2px;
height: $d; width: $d;
top: $m; left: $m; right: auto;
}
}
}
.c-toggle-switch {
cursor: pointer; cursor: pointer;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@ -20,6 +36,26 @@
display: block; display: block;
} }
&__slider {
// Sits within __switch
display: inline-block;
position: relative;
&:before {
// Knob
background: $colorBtnFg; // TODO: make discrete theme constants for these colors
content: '';
display: block;
position: absolute;
transition: transform 100ms ease-in-out;
}
}
&__label {
margin-left: $interiorMarginSm;
white-space: nowrap;
}
input { input {
opacity: 0; opacity: 0;
width: 0; width: 0;
@ -35,31 +71,9 @@
} }
} }
&__slider { @include toggleSwitch();
// Sits within __switch }
background: $colorBtnBg; // TODO: make discrete theme constants for these colors
border-radius: $br; .c-toggle-switch--mini {
display: inline-block; @include toggleSwitch($d: 9px, $m: 0px);
height: $d + ($m*2);
position: relative;
width: $d*2 + $m*2;
&:before {
// Knob
background: $colorBtnFg; // TODO: make discrete theme constants for these colors
border-radius: floor($br * 0.8);
box-shadow: rgba(black, 0.4) 0 0 2px;
content: '';
display: block;
position: absolute;
height: $d; width: $d;
top: $m; left: $m; right: auto;
transition: transform 100ms ease-in-out;
}
}
&__label {
margin-left: $interiorMarginSm;
white-space: nowrap;
}
} }

View File

@ -34,6 +34,15 @@
</div> </div>
<div class="l-browse-bar__end"> <div class="l-browse-bar__end">
<div
v-if="supportsIndependentTime"
class="c-conductor-holder--compact l-shell__main-independent-time-conductor"
>
<independent-time-conductor
:domain-object="domainObject"
:object-path="openmct.router.path"
/>
</div>
<ViewSwitcher <ViewSwitcher
v-if="!isEditing" v-if="!isEditing"
:current-view="currentView" :current-view="currentView"
@ -126,11 +135,20 @@
<script> <script>
import ViewSwitcher from './ViewSwitcher.vue'; import ViewSwitcher from './ViewSwitcher.vue';
import NotebookMenuSwitcher from '@/plugins/notebook/components/NotebookMenuSwitcher.vue'; import NotebookMenuSwitcher from '@/plugins/notebook/components/NotebookMenuSwitcher.vue';
import IndependentTimeConductor from '@/plugins/timeConductor/independent/IndependentTimeConductor.vue';
const SupportedViewTypes = [
'plot-stacked',
'plot-overlay',
'bar-graph.view',
'time-strip.view'
];
const PLACEHOLDER_OBJECT = {}; const PLACEHOLDER_OBJECT = {};
export default { export default {
components: { components: {
IndependentTimeConductor,
NotebookMenuSwitcher, NotebookMenuSwitcher,
ViewSwitcher ViewSwitcher
}, },
@ -220,6 +238,11 @@ export default {
} else { } else {
return 'Unlocked for editing - click to lock.'; return 'Unlocked for editing - click to lock.';
} }
},
supportsIndependentTime() {
const viewKey = this.getViewKey();
return this.domainObject && SupportedViewTypes.includes(viewKey);
} }
}, },
watch: { watch: {
@ -291,6 +314,14 @@ export default {
edit() { edit() {
this.openmct.editor.edit(); this.openmct.editor.edit();
}, },
getViewKey() {
let viewKey = this.viewKey;
if (this.objectViewKey) {
viewKey = this.objectViewKey;
}
return viewKey;
},
promptUserandCancelEditing() { promptUserandCancelEditing() {
let dialog = this.openmct.overlays.dialog({ let dialog = this.openmct.overlays.dialog({
iconClass: 'alert', iconClass: 'alert',

View File

@ -283,17 +283,6 @@
flex: 1 1 auto !important; flex: 1 1 auto !important;
} }
&__time-conductor {
border-top: 1px solid $colorInteriorBorder;
display: flex;
flex-direction: column;
padding-top: $interiorMargin;
> * + * {
margin-top: $interiorMargin;
}
}
&__main { &__main {
> .l-pane { > .l-pane {
padding: nth($shellPanePad, 1) 0; padding: nth($shellPanePad, 1) 0;
@ -377,10 +366,10 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
[class*="__"] { //[class*="__"] {
// Removes extraneous horizontal white space // // Removes extraneous horizontal white space
display: inline-flex; // display: inline-flex;
} //}
> * + * { > * + * {
margin-left: $interiorMarginSm; margin-left: $interiorMarginSm;

8
src/utils/debounce.js Normal file
View File

@ -0,0 +1,8 @@
export default function debounce(func, delay) {
let debounceTimer;
return function (...args) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => func(...args), delay);
};
}