[Time] Conductors and API Enhancements (#6768)

* Fixed #4975 - Compact Time Conductor styling
* Fixed #5773 - Ubiquitous global clock
* Mode functionality added to TimeAPI
* TimeAPI modified to always have a ticking clock
* Mode dropdown added to independent and regular time conductors
* Overall conductor appearance modifications and enhancements
* TimeAPI methods deprecated with warnings
* Significant updates to markup, styling and behavior of main Time Conductor and independent version.


---------

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Shefali <simplyrender@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Scott Bell <scott@traclabs.com>
This commit is contained in:
Jamie V
2023-07-18 17:32:05 -07:00
committed by GitHub
parent 85974fc5f1
commit 42b545917c
94 changed files with 3959 additions and 1969 deletions

View File

@ -21,7 +21,8 @@
-->
<template>
<div
class="c-conductor"
ref="timeConductorOptionsHolder"
class="c-compact-tc is-expanded"
:class="[
{ 'is-zooming': isZooming },
{ 'is-panning': isPanning },
@ -29,75 +30,91 @@
isFixed ? 'is-fixed-mode' : 'is-realtime-mode'
]"
>
<div class="c-conductor__time-bounds">
<conductor-inputs-fixed
v-if="isFixed"
:input-bounds="viewBounds"
@updated="saveFixedOffsets"
/>
<conductor-inputs-realtime v-else :input-bounds="viewBounds" @updated="saveClockOffsets" />
<ConductorModeIcon class="c-conductor__mode-icon" />
<conductor-axis
class="c-conductor__ticks"
:view-bounds="viewBounds"
:is-fixed="isFixed"
:alt-pressed="altPressed"
@endPan="endPan"
@endZoom="endZoom"
@panAxis="pan"
@zoomAxis="zoom"
/>
</div>
<div class="c-conductor__controls">
<ConductorMode class="c-conductor__mode-select" />
<ConductorTimeSystem class="c-conductor__time-system-select" />
<ConductorHistory
class="c-conductor__history-select"
:offsets="openmct.time.clockOffsets()"
:bounds="bounds"
:time-system="timeSystem"
:mode="timeMode"
/>
<ConductorModeIcon class="c-conductor__mode-icon" />
<div class="c-compact-tc__setting-value u-fade-truncate">
<conductor-mode :mode="mode" :read-only="true" />
<conductor-clock :read-only="true" />
<conductor-time-system :read-only="true" />
</div>
<conductor-inputs-fixed v-if="isFixed" :input-bounds="viewBounds" :read-only="true" />
<conductor-inputs-realtime v-else :input-bounds="viewBounds" :read-only="true" />
<conductor-axis
v-if="isFixed"
class="c-conductor__ticks"
:view-bounds="viewBounds"
:is-fixed="isFixed"
:alt-pressed="altPressed"
@endPan="endPan"
@endZoom="endZoom"
@panAxis="pan"
@zoomAxis="zoom"
/>
<div class="c-not-button c-not-button--compact c-compact-tc__gear icon-gear"></div>
<conductor-pop-up
v-if="showConductorPopup"
ref="conductorPopup"
:bottom="false"
:position-x="positionX"
:position-y="positionY"
:is-fixed="isFixed"
@popupLoaded="initializePopup"
@modeUpdated="saveMode"
@clockUpdated="saveClock"
@fixedBoundsUpdated="saveFixedBounds"
@clockOffsetsUpdated="saveClockOffsets"
@dismiss="clearPopup"
/>
</div>
</template>
<script>
import _ from 'lodash';
import ConductorMode from './ConductorMode.vue';
import ConductorTimeSystem from './ConductorTimeSystem.vue';
import {
FIXED_MODE_KEY,
MODES,
REALTIME_MODE_KEY,
TIME_CONTEXT_EVENTS
} from '../../api/time/constants';
import ConductorAxis from './ConductorAxis.vue';
import ConductorModeIcon from './ConductorModeIcon.vue';
import ConductorHistory from './ConductorHistory.vue';
import ConductorInputsFixed from './ConductorInputsFixed.vue';
import ConductorInputsRealtime from './ConductorInputsRealtime.vue';
import ConductorTimeSystem from './ConductorTimeSystem.vue';
import ConductorClock from './ConductorClock.vue';
import ConductorMode from './ConductorMode.vue';
import conductorPopUpManager from './conductorPopUpManager';
import ConductorPopUp from './ConductorPopUp.vue';
const DEFAULT_DURATION_FORMATTER = 'duration';
export default {
components: {
ConductorTimeSystem,
ConductorClock,
ConductorMode,
ConductorInputsRealtime,
ConductorInputsFixed,
ConductorMode,
ConductorTimeSystem,
ConductorAxis,
ConductorModeIcon,
ConductorHistory
ConductorPopUp
},
mixins: [conductorPopUpManager],
inject: ['openmct', 'configuration'],
data() {
let bounds = this.openmct.time.bounds();
let offsets = this.openmct.time.clockOffsets();
let timeSystem = this.openmct.time.timeSystem();
let timeFormatter = this.getFormatter(timeSystem.timeFormat);
let durationFormatter = this.getFormatter(
const isFixed = this.openmct.time.isFixed();
const bounds = this.openmct.time.getBounds();
const offsets = this.openmct.time.getClockOffsets();
const timeSystem = this.openmct.time.getTimeSystem();
const timeFormatter = this.getFormatter(timeSystem.timeFormat);
const durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
);
return {
timeSystem: timeSystem,
timeFormatter: timeFormatter,
durationFormatter: durationFormatter,
timeSystem,
timeFormatter,
durationFormatter,
offsets: {
start: offsets && durationFormatter.format(Math.abs(offsets.start)),
end: offsets && durationFormatter.format(Math.abs(offsets.end))
@ -114,37 +131,44 @@ export default {
start: bounds.start,
end: bounds.end
},
isFixed: this.openmct.time.clock() === undefined,
isFixed,
isUTCBased: timeSystem.isUTCBased,
showDatePicker: false,
showConductorPopup: false,
altPressed: false,
isPanning: false,
isZooming: false,
showTCInputStart: false,
showTCInputEnd: false
isZooming: false
};
},
computed: {
timeMode() {
return this.isFixed ? 'fixed' : 'realtime';
mode() {
return this.isFixed ? FIXED_MODE_KEY : REALTIME_MODE_KEY;
}
},
mounted() {
document.addEventListener('keydown', this.handleKeyDown);
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.openmct.time.on('timeSystem', this.setTimeSystem);
this.openmct.time.on('clock', this.setViewFromClock);
this.setTimeSystem(this.copy(this.openmct.time.getTimeSystem()));
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);
},
beforeDestroy() {
document.removeEventListener('keydown', this.handleKeyDown);
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);
},
methods: {
handleNewBounds(bounds) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
handleNewBounds(bounds, isTick) {
if (this.openmct.time.isRealTime() || !isTick) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
}
},
setBounds(bounds) {
this.bounds = bounds;
@ -166,7 +190,7 @@ export default {
endPan(bounds) {
this.isPanning = false;
if (bounds) {
this.openmct.time.bounds(bounds);
this.openmct.time.setBounds(bounds);
}
},
zoom(bounds) {
@ -181,7 +205,7 @@ export default {
endZoom(bounds) {
this.isZooming = false;
if (bounds) {
this.openmct.time.bounds(bounds);
this.openmct.time.setBounds(bounds);
} else {
this.setViewFromBounds(this.bounds);
}
@ -194,9 +218,8 @@ export default {
);
this.isUTCBased = timeSystem.isUTCBased;
},
setViewFromClock(clock) {
// this.clearAllValidation();
this.isFixed = clock === undefined;
setMode() {
this.isFixed = this.openmct.time.isFixed();
},
setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start);
@ -209,11 +232,24 @@ export default {
format: key
}).formatter;
},
saveClockOffsets(offsets) {
this.openmct.time.clockOffsets(offsets);
getBoundsForMode(mode) {
const isRealTime = mode === MODES.realtime;
return isRealTime ? this.openmct.time.getClockOffsets() : this.openmct.time.getBounds();
},
saveFixedOffsets(bounds) {
this.openmct.time.bounds(bounds);
saveFixedBounds(bounds) {
this.openmct.time.setBounds(bounds);
},
saveClockOffsets(offsets) {
this.openmct.time.setClockOffsets(offsets);
},
saveClock(clockOptions) {
this.openmct.time.setClock(clockOptions.clockKey);
},
saveMode(mode) {
this.openmct.time.setMode(mode, this.getBoundsForMode(mode));
},
copy(object) {
return JSON.parse(JSON.stringify(object));
}
}
};

View File

@ -30,6 +30,7 @@ import * as d3Selection from 'd3-selection';
import * as d3Axis from 'd3-axis';
import * as d3Scale from 'd3-scale';
import utcMultiTimeFormat from './utcMultiTimeFormat.js';
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants';
const PADDING = 1;
const DEFAULT_DURATION_FORMATTER = 'duration';
@ -83,13 +84,16 @@ export default {
// draw x axis with labels. CSS is used to position them.
this.axisElement = vis.append('g').attr('class', 'axis');
this.setViewFromTimeSystem(this.openmct.time.timeSystem());
this.setViewFromTimeSystem(this.openmct.time.getTimeSystem());
this.setAxisDimensions();
this.setScale();
//Respond to changes in conductor
this.openmct.time.on('timeSystem', this.setViewFromTimeSystem);
setInterval(this.resize, RESIZE_POLL_INTERVAL);
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setViewFromTimeSystem);
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
},
beforeDestroy() {
clearInterval(this.resizeTimer);
},
methods: {
setAxisDimensions() {
@ -104,7 +108,7 @@ export default {
return;
}
let timeSystem = this.openmct.time.timeSystem();
let timeSystem = this.openmct.time.getTimeSystem();
if (timeSystem.isUTCBased) {
this.xScale.domain([new Date(this.viewBounds.start), new Date(this.viewBounds.end)]);
@ -140,7 +144,7 @@ export default {
this.setScale();
},
getActiveFormatter() {
let timeSystem = this.openmct.time.timeSystem();
let timeSystem = this.openmct.time.getTimeSystem();
if (this.isFixed) {
return this.getFormatter(timeSystem.timeFormat);
@ -209,7 +213,7 @@ export default {
this.inPanMode = false;
},
getPanBounds() {
const bounds = this.openmct.time.bounds();
const bounds = this.openmct.time.getBounds();
const deltaTime = bounds.end - bounds.start;
const deltaX = this.dragX - this.dragStartX;
const percX = deltaX / this.width;
@ -272,7 +276,7 @@ export default {
};
},
scaleToBounds(value) {
const bounds = this.openmct.time.bounds();
const bounds = this.openmct.time.getBounds();
const timeDelta = bounds.end - bounds.start;
const valueDelta = value - this.left;
const offset = (valueDelta / this.width) * timeDelta;

View File

@ -0,0 +1,128 @@
/***************************************************************************** * 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="readOnly === false" 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 js-clock-button"
:class="[buttonCssClass, selectedClock.cssClass]"
@click.prevent.stop="showClocksMenu"
>
<span class="c-button__label">{{ selectedClock.name }}</span>
</button>
</div>
</div>
<div v-else class="c-compact-tc__setting-value__elem" :title="`Clock: ${selectedClock.name}`">
{{ selectedClock.name }}
</div>
</template>
<script>
import clockMixin from './clock-mixin';
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants';
export default {
mixins: [clockMixin],
inject: {
openmct: 'openmct',
configuration: {
from: 'configuration',
default: undefined
}
},
props: {
readOnly: {
type: Boolean,
default() {
return false;
}
}
},
data: function () {
const activeClock = this.getActiveClock();
return {
selectedClock: activeClock ? this.getClockMetadata(activeClock) : undefined,
clocks: []
};
},
mounted: function () {
this.loadClocks(this.configuration.menuOptions);
this.openmct.time.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
},
destroyed: function () {
this.openmct.time.off(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
},
methods: {
showClocksMenu() {
const elementBoundingClientRect = this.$refs.clockButton.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y;
const menuOptions = {
menuClass: 'c-conductor__clock-menu c-super-menu--sm',
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.setTimeSystem(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.selectedClock = this.getClockMetadata(clock);
}
}
};
</script>

View File

@ -25,6 +25,7 @@
<button
aria-label="Time Conductor History"
class="c-button--menu c-history-button icon-history"
:class="buttonCssClass"
@click.prevent.stop="showHistoryMenu"
>
<span class="c-button__label">History</span>
@ -41,29 +42,22 @@ const DEFAULT_RECORDS_LENGTH = 10;
import { millisecondsToDHMS } from 'utils/duration';
import UTCTimeFormat from '../utcTimeSystem/UTCTimeFormat.js';
import { REALTIME_MODE_KEY, TIME_CONTEXT_EVENTS } from '../../api/time/constants';
export default {
inject: ['openmct', 'configuration'],
props: {
bounds: {
type: Object,
required: true
},
offsets: {
type: Object,
required: false,
default: () => {}
},
timeSystem: {
type: Object,
required: true
},
mode: {
buttonCssClass: {
type: String,
required: true
required: false,
default() {
return '';
}
}
},
data() {
const mode = this.openmct.time.getMode();
return {
/**
* previous bounds entries available for easy re-use
@ -76,15 +70,15 @@ export default {
* @fixedHistory array of timespans
* @timespans {start, end} number representing timestamp
*/
mode,
currentHistory: mode + 'History',
fixedHistory: {},
presets: [],
isFixed: this.openmct.time.clock() === undefined
timeSystem: this.openmct.time.getTimeSystem(),
isFixed: this.openmct.time.isFixed()
};
},
computed: {
currentHistory() {
return this.mode + 'History';
},
historyForCurrentTimeSystem() {
const history = this[this.currentHistory][this.timeSystem.key];
@ -92,55 +86,29 @@ export default {
},
storageKey() {
let key = LOCAL_STORAGE_HISTORY_KEY_FIXED;
if (!this.isFixed) {
if (this.mode === REALTIME_MODE_KEY) {
key = LOCAL_STORAGE_HISTORY_KEY_REALTIME;
}
return key;
}
},
watch: {
bounds: {
handler() {
// only for fixed time since we track offsets for realtime
if (this.isFixed) {
this.updateMode();
this.addTimespan();
}
},
deep: true
},
offsets: {
handler() {
this.updateMode();
this.addTimespan();
},
deep: true
},
timeSystem: {
handler(ts) {
this.updateMode();
this.loadConfiguration();
this.addTimespan();
},
deep: true
},
mode: function () {
this.updateMode();
this.loadConfiguration();
}
},
mounted() {
this.updateMode();
this.getHistoryFromLocalStorage();
this.initializeHistoryIfNoHistory();
this.loadConfiguration();
this.openmct.time.on(TIME_CONTEXT_EVENTS.boundsChanged, this.addTimespan);
this.openmct.time.on(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.addTimespan);
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.updateTimeSystem);
this.openmct.time.on(TIME_CONTEXT_EVENTS.modeChanged, this.updateMode);
},
beforeDestroy() {
this.openmct.time.off(TIME_CONTEXT_EVENTS.boundsChanged, this.addTimespan);
this.openmct.time.off(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.addTimespan);
this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.updateTimeSystem);
this.openmct.time.off(TIME_CONTEXT_EVENTS.modeChanged, this.updateMode);
},
methods: {
updateMode() {
this.isFixed = this.openmct.time.clock() === undefined;
this.getHistoryFromLocalStorage();
this.initializeHistoryIfNoHistory();
},
getHistoryMenuItems() {
const descriptionDateFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
const history = this.historyForCurrentTimeSystem.map((timespan) => {
@ -151,7 +119,7 @@ export default {
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)}`;
} else {
name = description;
@ -189,22 +157,41 @@ export default {
const localStorageHistory = localStorage.getItem(this.storageKey);
const history = localStorageHistory ? JSON.parse(localStorageHistory) : undefined;
this[this.currentHistory] = history;
this.initializeHistoryIfNoHistory();
},
initializeHistoryIfNoHistory() {
if (!this[this.currentHistory]) {
this[this.currentHistory] = {};
this[this.currentHistory][this.timeSystem.key] = [];
this.persistHistoryToLocalStorage();
}
},
persistHistoryToLocalStorage() {
localStorage.setItem(this.storageKey, JSON.stringify(this[this.currentHistory]));
},
addTimespan() {
updateMode() {
this.mode = this.openmct.time.getMode();
this.currentHistory = this.mode + 'History';
this.loadConfiguration();
this.getHistoryFromLocalStorage();
},
updateTimeSystem(timeSystem) {
this.timeSystem = timeSystem;
this.loadConfiguration();
this.getHistoryFromLocalStorage();
},
addTimespan(bounds, isTick) {
if (isTick) {
return;
}
const key = this.timeSystem.key;
let [...currentHistory] = this[this.currentHistory][key] || [];
const isFixed = this.openmct.time.isFixed();
let [...currentHistory] = this.historyForCurrentTimeSystem || [];
const timespan = {
start: this.isFixed ? this.bounds.start : this.offsets.start,
end: this.isFixed ? this.bounds.end : this.offsets.end
start: isFixed ? bounds.start : this.openmct.time.getClockOffsets().start,
end: isFixed ? bounds.end : this.openmct.time.getClockOffsets().end
};
// no dupes
@ -221,10 +208,10 @@ export default {
this.persistHistoryToLocalStorage();
},
selectTimespan(timespan) {
if (this.isFixed) {
this.openmct.time.bounds(timespan);
if (this.openmct.time.isFixed()) {
this.openmct.time.setBounds(timespan);
} else {
this.openmct.time.clockOffsets(timespan);
this.openmct.time.setClockOffsets(timespan);
}
},
selectPresetBounds(bounds) {
@ -262,7 +249,7 @@ export default {
let format = this.timeSystem.timeFormat;
let isNegativeOffset = false;
if (!this.isFixed) {
if (!this.openmct.time.isFixed()) {
if (time < 0) {
isNegativeOffset = true;
}

View File

@ -20,76 +20,42 @@
at runtime from the About dialog for additional information.
-->
<template>
<form ref="fixedDeltaInput" class="c-conductor__inputs">
<div class="c-ctrl-wrapper c-conductor-input c-conductor__start-fixed">
<!-- Fixed start -->
<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"
/>
<time-popup-fixed
v-if="readOnly === false"
:input-bounds="bounds"
:input-time-system="timeSystem"
@focus.native="$event.target.select()"
@update="setBoundsFromView"
@dismiss="dismiss"
/>
<div v-else class="c-compact-tc__setting-wrapper">
<div
class="c-compact-tc__setting-value u-fade-truncate--lg --no-sep"
:title="`Start bounds: ${formattedBounds.start}`"
>
{{ formattedBounds.start }}
</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 class="c-compact-tc__bounds__start-end-sep icon-arrows-right-left"></div>
<div
class="c-compact-tc__setting-value u-fade-truncate--lg --no-sep"
:title="`End bounds: ${formattedBounds.end}`"
>
{{ formattedBounds.end }}
</div>
</form>
</div>
</template>
<script>
import DatePicker from './DatePicker.vue';
import TimePopupFixed from './timePopupFixed.vue';
import _ from 'lodash';
const DEFAULT_DURATION_FORMATTER = 'duration';
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants';
export default {
components: {
DatePicker
TimePopupFixed
},
inject: ['openmct'],
props: {
keyString: {
type: String,
default() {
return undefined;
}
},
inputBounds: {
type: Object,
default() {
@ -101,20 +67,27 @@ export default {
default() {
return [];
}
},
readOnly: {
type: Boolean,
default() {
return false;
}
},
compact: {
type: Boolean,
default() {
return false;
}
}
},
data() {
let timeSystem = this.openmct.time.timeSystem();
let durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
);
let timeFormatter = this.getFormatter(timeSystem.timeFormat);
let bounds = this.bounds || this.openmct.time.bounds();
const timeSystem = this.openmct.time.getTimeSystem();
const timeFormatter = this.getFormatter(timeSystem.timeFormat);
let bounds = this.inputBounds || this.openmct.time.getBounds();
return {
showTCInputStart: true,
showTCInputEnd: true,
durationFormatter,
timeSystem,
timeFormatter,
bounds: {
start: bounds.start,
@ -128,8 +101,15 @@ export default {
};
},
watch: {
keyString() {
this.setTimeContext();
objectPath: {
handler(newPath, oldPath) {
if (newPath === oldPath) {
return;
}
this.setTimeContext();
},
deep: true
},
inputBounds: {
handler(newBounds) {
@ -140,41 +120,31 @@ export default {
},
mounted() {
this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem())));
this.openmct.time.on('timeSystem', this.setTimeSystem);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.getTimeSystem())));
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.setTimeContext();
},
beforeDestroy() {
this.clearAllValidation();
this.openmct.time.off('timeSystem', this.setTimeSystem);
this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.stopFollowingTimeContext();
},
methods: {
setTimeContext() {
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.timeContext.on('bounds', this.handleNewBounds);
this.timeContext.on('clock', this.clearAllValidation);
this.handleNewBounds(this.timeContext.getBounds());
this.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.handleNewBounds);
this.timeContext.off('clock', this.clearAllValidation);
this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
}
},
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;
},
@ -185,9 +155,6 @@ export default {
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) {
@ -195,112 +162,14 @@ export default {
format: key
}).formatter;
},
setBoundsFromView($event) {
if (this.$refs.fixedDeltaInput.checkValidity()) {
let start = this.timeFormatter.parse(this.formattedBounds.start);
let end = this.timeFormatter.parse(this.formattedBounds.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);
setBoundsFromView(bounds) {
this.$emit('boundsUpdated', {
start: bounds.start,
end: bounds.end
});
},
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.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();
dismiss() {
this.$emit('dismissInputsFixed');
}
}
};

View File

@ -20,87 +20,53 @@
at runtime from the About dialog for additional information.
-->
<template>
<form ref="deltaInput" class="c-conductor__inputs">
<div class="c-ctrl-wrapper c-conductor-input c-conductor__start-delta">
<!-- RT start -->
<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>
<time-popup-realtime
v-if="readOnly === false"
:offsets="offsets"
@focus.native="$event.target.select()"
@update="timePopUpdate"
@dismiss="dismiss"
/>
<div v-else class="c-compact-tc__setting-wrapper">
<div
v-if="!compact"
class="c-compact-tc__setting-value icon-minus u-fade-truncate--lg --no-sep"
:title="`Start offset: ${offsets.start}`"
>
{{ offsets.start }}
</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 v-if="!compact" class="c-compact-tc__bounds__start-end-sep icon-arrows-right-left"></div>
<div
v-if="!compact"
class="c-compact-tc__setting-value icon-plus u-fade-truncate--lg"
:class="{ '--no-sep': compact }"
:title="`End offset: ${offsets.end}`"
>
{{ offsets.end }}
</div>
<div class="c-ctrl-wrapper c-conductor-input c-conductor__end-delta">
<!-- RT 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
class="c-compact-tc__setting-value icon-clock c-compact-tc__current-update u-fade-truncate--lg --no-sep"
title="Last update"
>
{{ formattedCurrentValue }}
</div>
</form>
<div class="u-flex-spreader"></div>
</div>
</template>
<script>
import timePopup from './timePopup.vue';
import TimePopupRealtime from './timePopupRealtime.vue';
import _ from 'lodash';
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants';
const DEFAULT_DURATION_FORMATTER = 'duration';
export default {
components: {
timePopup
TimePopupRealtime
},
inject: ['openmct'],
props: {
keyString: {
type: String,
default() {
return undefined;
}
},
objectPath: {
type: Array,
default() {
@ -112,17 +78,29 @@ export default {
default() {
return undefined;
}
},
readOnly: {
type: Boolean,
default() {
return false;
}
},
compact: {
type: Boolean,
default() {
return false;
}
}
},
data() {
let timeSystem = this.openmct.time.timeSystem();
let durationFormatter = this.getFormatter(
const timeSystem = this.openmct.time.getTimeSystem();
const durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
);
let timeFormatter = this.getFormatter(timeSystem.timeFormat);
let bounds = this.bounds || this.openmct.time.bounds();
let offsets = this.openmct.time.clockOffsets();
let currentValue = this.openmct.time.clock()?.currentValue();
const timeFormatter = this.getFormatter(timeSystem.timeFormat);
const bounds = this.bounds ?? this.openmct.time.getBounds();
const offsets = this.offsets ?? this.openmct.time.getClockOffsets();
const currentValue = this.openmct.time.getClock()?.currentValue();
return {
showTCInputStart: false,
@ -147,8 +125,15 @@ export default {
};
},
watch: {
keyString() {
this.setTimeContext();
objectPath: {
handler(newPath, oldPath) {
if (newPath === oldPath) {
return;
}
this.setTimeContext();
},
deep: true
},
inputBounds: {
handler(newBounds) {
@ -159,45 +144,54 @@ export default {
},
mounted() {
this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem())));
this.openmct.time.on('timeSystem', this.setTimeSystem);
this.setTimeSystem(this.copy(this.openmct.time.getTimeSystem()));
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.setTimeContext();
},
beforeDestroy() {
this.openmct.time.off('timeSystem', this.setTimeSystem);
this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.stopFollowingTime();
},
methods: {
followTime() {
this.handleNewBounds(this.timeContext.bounds());
this.setViewFromOffsets(this.timeContext.clockOffsets());
this.timeContext.on('bounds', this.handleNewBounds);
this.timeContext.on('clock', this.clearAllValidation);
this.timeContext.on('clockOffsets', this.setViewFromOffsets);
const bounds = this.timeContext
? this.timeContext.getBounds()
: this.openmct.time.getBounds();
const offsets = this.timeContext
? this.timeContext.getClockOffsets()
: this.openmct.time.getClockOffsets();
this.handleNewBounds(bounds);
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() {
if (this.timeContext) {
this.timeContext.off('bounds', this.handleNewBounds);
this.timeContext.off('clock', this.clearAllValidation);
this.timeContext.off('clockOffsets', this.setViewFromOffsets);
this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
this.timeContext.off(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.setViewFromOffsets);
} else {
this.openmct.time.off(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
this.openmct.time.off(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.setViewFromOffsets);
}
},
setTimeContext() {
this.stopFollowingTime();
this.timeContext = this.openmct.time.getContextForView(this.keyString ? this.objectPath : []);
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.followTime();
},
handleNewBounds(bounds) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
this.updateCurrentValue();
},
clearAllValidation() {
[this.$refs.startOffset, this.$refs.endOffset].forEach(this.clearValidationForInput);
},
clearValidationForInput(input) {
input.setCustomValidity('');
input.title = '';
handleNewBounds(bounds, isTick) {
if (this.timeContext.isRealTime() || !isTick) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
this.updateCurrentValue();
}
},
setViewFromOffsets(offsets) {
if (offsets) {
@ -213,7 +207,7 @@ export default {
this.formattedBounds.end = this.timeFormatter.format(bounds.end);
},
updateCurrentValue() {
const currentValue = this.openmct.time.clock()?.currentValue();
const currentValue = this.openmct.time.getClock()?.currentValue();
if (currentValue !== undefined) {
this.setCurrentValue(currentValue);
@ -236,85 +230,25 @@ export default {
format: key
}).formatter;
},
hideAllTimePopups() {
this.showTCInputStart = false;
this.showTCInputEnd = false;
},
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(':');
timePopUpdate({ start, end }) {
this.offsets.start = [start.hours, start.minutes, start.seconds].join(':');
this.offsets.end = [end.hours, end.minutes, end.seconds].join(':');
this.setOffsetsFromView();
this.hideAllTimePopups();
},
setOffsetsFromView($event) {
if (this.$refs.deltaInput.checkValidity()) {
let startOffset = 0 - this.durationFormatter.parse(this.offsets.start);
let endOffset = this.durationFormatter.parse(this.offsets.end);
setOffsetsFromView() {
let startOffset = 0 - this.durationFormatter.parse(this.offsets.start);
let endOffset = this.durationFormatter.parse(this.offsets.end);
this.$emit('updated', {
start: startOffset,
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);
this.$emit('offsetsUpdated', {
start: startOffset,
end: endOffset
});
},
handleValidationResults(input, validationResult) {
if (validationResult.valid !== true) {
input.setCustomValidity(validationResult.message);
input.title = validationResult.message;
} else {
input.setCustomValidity('');
input.title = '';
}
return validationResult.valid;
dismiss() {
this.$emit('dismissInputsRealtime');
},
copy(object) {
return JSON.parse(JSON.stringify(object));
}
}
};

View File

@ -20,42 +20,61 @@
at runtime from the About dialog for additional information.
-->
<template>
<div ref="modeButton" class="c-ctrl-wrapper c-ctrl-wrapper--menus-up">
<div v-if="readOnly === false" ref="modeButton" 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-mode-button" @click.prevent.stop="showModesMenu">
<button
class="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>
<div v-else class="c-compact-tc__setting-value__elem" :title="`Mode: ${selectedMode.name}`">
{{ selectedMode.name }}
</div>
</template>
<script>
import toggleMixin from '../../ui/mixins/toggle-mixin';
import modeMixin from './mode-mixin';
const TEST_IDS = true;
export default {
mixins: [toggleMixin],
mixins: [modeMixin],
inject: ['openmct', 'configuration'],
data: function () {
let activeClock = this.openmct.time.clock();
if (activeClock !== undefined) {
//Create copy of active clock so the time API does not get reactified.
activeClock = Object.create(activeClock);
props: {
mode: {
type: String,
default() {
return undefined;
}
},
readOnly: {
type: Boolean,
default() {
return false;
}
}
},
data: function () {
const mode = this.openmct.time.getMode();
return {
selectedMode: this.getModeOptionForClock(activeClock),
selectedTimeSystem: JSON.parse(JSON.stringify(this.openmct.time.timeSystem())),
modes: [],
hoveredMode: {}
selectedMode: this.getModeMetadata(mode, TEST_IDS),
modes: []
};
},
mounted: function () {
this.loadClocksFromConfiguration();
this.openmct.time.on('clock', this.setViewFromClock);
watch: {
mode: {
handler(newMode) {
this.setViewFromMode(newMode);
}
}
},
destroyed: function () {
this.openmct.time.off('clock', this.setViewFromClock);
mounted: function () {
this.loadModes();
},
methods: {
showModesMenu() {
@ -64,112 +83,19 @@ export default {
const y = elementBoundingClientRect.y;
const menuOptions = {
menuClass: 'c-conductor__mode-menu',
menuClass: 'c-conductor__mode-menu c-super-menu--sm',
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);
},
loadClocksFromConfiguration() {
let clocks = this.configuration.menuOptions
.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;
}
setViewFromMode(mode) {
this.selectedMode = this.getModeMetadata(mode, TEST_IDS);
},
setMode(mode) {
this.setViewFromMode(mode);
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);
this.$emit('modeUpdated', mode);
}
}
};

View File

@ -21,6 +21,12 @@
-->
<template>
<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-big"></div>
</div>

View File

@ -0,0 +1,241 @@
<template>
<div class="c-tc-input-popup" :class="popupClasses" :style="position">
<div class="c-tc-input-popup__options">
<IndependentMode
v-if="isIndependent"
class="c-conductor__mode-select"
title="Sets the Time Conductor's mode."
:mode="timeOptionMode"
@independentModeUpdated="saveIndependentMode"
/>
<ConductorMode
v-else
class="c-conductor__mode-select"
title="Sets the Time Conductor's mode."
:button-css-class="'c-icon-button'"
@modeUpdated="saveMode"
/>
<IndependentClock
v-if="isIndependent"
class="c-conductor__mode-select"
title="Sets the Time Conductor's clock."
:clock="timeOptionClock"
:button-css-class="'c-icon-button'"
@independentClockUpdated="saveIndependentClock"
/>
<ConductorClock
v-else
class="c-conductor__mode-select"
title="Sets the Time Conductor's clock."
: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"
title="Sets the Time Conductor's time system."
:button-css-class="'c-icon-button'"
/>
<ConductorHistory
v-if="!isIndependent"
class="c-conductor__history-select"
title="Select and apply previously entered time intervals."
:button-css-class="'c-icon-button'"
/>
</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 } 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;
},
popupClasses() {
const value = this.bottom ? 'c-tc-input-popup--bottom ' : '';
const mode = this.isFixed ? 'fixed-mode' : 'realtime-mode';
const independentClass = this.isIndependent ? 'itc-popout ' : '';
return `${independentClass}${value}c-tc-input-popup--${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.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);
},
setViewFromClock() {
this.bounds = this.isFixed
? this.timeContext.getBounds()
: this.openmct.time.getClockOffsets();
},
setBounds(bounds, isTick) {
if (this.isFixed || !isTick) {
this.bounds = bounds;
}
},
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

@ -21,38 +21,62 @@
-->
<template>
<div
v-if="selectedTimeSystem.name"
v-if="selectedTimeSystem.name && readOnly === false"
ref="timeSystemButton"
class="c-ctrl-wrapper c-ctrl-wrapper--menus-up"
>
<button
class="c-button--menu c-time-system-button"
:class="selectedTimeSystem.cssClass"
:class="[buttonCssClass]"
@click.prevent.stop="showTimeSystemMenu"
>
<span class="c-button__label">{{ selectedTimeSystem.name }}</span>
</button>
</div>
<div
v-else
class="c-compact-tc__setting-value__elem"
:title="`Time system: ${selectedTimeSystem.name}`"
>
{{ selectedTimeSystem.name }}
</div>
</template>
<script>
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants';
export default {
inject: ['openmct', 'configuration'],
props: {
buttonCssClass: {
type: String,
required: false,
default() {
return '';
}
},
readOnly: {
type: Boolean,
default() {
return false;
}
}
},
data: function () {
let activeClock = this.openmct.time.clock();
let activeClock = this.openmct.time.getClock();
return {
selectedTimeSystem: JSON.parse(JSON.stringify(this.openmct.time.timeSystem())),
selectedTimeSystem: JSON.parse(JSON.stringify(this.openmct.time.getTimeSystem())),
timeSystems: this.getValidTimesystemsForClock(activeClock)
};
},
mounted: function () {
this.openmct.time.on('timeSystem', this.setViewFromTimeSystem);
this.openmct.time.on('clock', this.setViewFromClock);
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSysteChanged, this.setViewFromTimeSystem);
this.openmct.time.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
},
destroyed: function () {
this.openmct.time.off('timeSystem', this.setViewFromTimeSystem);
this.openmct.time.on('clock', this.setViewFromClock);
this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setViewFromTimeSystem);
this.openmct.time.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
},
methods: {
showTimeSystemMenu() {
@ -80,7 +104,7 @@ export default {
},
setTimeSystemFromView(timeSystem) {
if (timeSystem.key !== this.selectedTimeSystem.key) {
let activeClock = this.openmct.time.clock();
let activeClock = this.openmct.time.getClock();
let configuration = this.getMatchingConfig({
clock: activeClock && activeClock.key,
timeSystem: timeSystem.key
@ -89,15 +113,15 @@ export default {
let bounds;
if (this.selectedTimeSystem.isUTCBased && timeSystem.isUTCBased) {
bounds = this.openmct.time.bounds();
bounds = this.openmct.time.getBounds();
} else {
bounds = configuration.bounds;
}
this.openmct.time.timeSystem(timeSystem.key, bounds);
this.openmct.time.setTimeSystem(timeSystem.key, bounds);
} else {
this.openmct.time.timeSystem(timeSystem.key);
this.openmct.time.clockOffsets(configuration.clockOffsets);
this.openmct.time.setTimeSystem(timeSystem.key);
this.openmct.time.setClockOffsets(configuration.clockOffsets);
}
}
},
@ -126,7 +150,7 @@ export default {
},
setViewFromClock(clock) {
let activeClock = this.openmct.time.clock();
let activeClock = this.openmct.time.getClock();
this.timeSystems = this.getValidTimesystemsForClock(activeClock);
}
}

View File

@ -0,0 +1,52 @@
export default {
props: {
buttonCssClass: {
type: String,
required: false,
default() {
return '';
}
}
},
methods: {
loadClocks(menuOptions) {
let clocks;
if (menuOptions) {
clocks = menuOptions
.map((menuOption) => menuOption.clock)
.filter(isDefinedAndUnique)
.map(this.getClock);
} else {
clocks = this.openmct.time.getAllClocks();
}
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);
},
getClockMetadata(clock) {
const key = clock.key;
const clockOptions = {
key,
name: clock.name,
description: 'Uses the system clock as the current time basis. ' + clock.description,
cssClass: clock.cssClass || 'icon-clock',
onItemClicked: () => this.setClock(key)
};
return clockOptions;
}
}
};

View File

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

View File

@ -86,22 +86,17 @@
}
.c-clock-symbol {
$c: $colorBtnBg; //$colorObjHdrIc;
$d: 18px;
$c: rgba($colorBodyFg, 0.5);
$d: 16px;
height: $d;
width: $d;
position: relative;
&:before {
font-family: symbolsfont;
color: $c;
content: $glyph-icon-brackets;
font-size: $d;
line-height: normal;
display: block;
&__outer {
// SVG brackets shape
width: 100%;
height: 100%;
z-index: 1;
fill: $c;
}
// Clock hands
@ -117,6 +112,7 @@
left: 50%;
top: 50%;
z-index: 2;
&:before {
background: $c;
content: '';
@ -125,18 +121,22 @@
width: 100%;
bottom: -1px;
}
&.hand-little {
z-index: 2;
animation-duration: 12s;
transform: translate(-50%, -50%) rotate(120deg);
&:before {
height: ceil($handH * 0.6);
}
}
&.hand-big {
z-index: 1;
animation-duration: 1s;
transform: translate(-50%, -50%);
&:before {
height: $handH;
}
@ -146,14 +146,35 @@
// Modes
.is-realtime-mode &,
.is-lad-mode & {
&:before {
$c: $colorTimeRealtimeFgSubtle;
.c-clock-symbol__outer {
// Brackets icon
color: $colorTime;
fill: $c;
}
div[class*='hand'] {
animation-name: clock-hands;
&:before {
background: $colorTime;
background: $c;
}
}
}
}
// Contexts
.c-so-view--no-frame {
.c-compact-tc:not(.is-expanded) {
.c-clock-symbol {
$c: $frameControlsColorFg;
&__outer {
fill: $c;
}
div[class*='hand']:before {
background: $c;
}
}
}

View File

@ -1,14 +0,0 @@
.c-conductor__mode-menu {
max-height: 80vh;
max-width: 500px;
min-height: 250px;
z-index: 70;
[class*='__icon'] {
filter: $colorKeyFilter;
}
[class*='__item-description'] {
min-width: 200px;
}
}

View File

@ -1,56 +1,356 @@
.c-input--submit {
// Can't use display: none because some browsers will pretend the input doesn't exist, and enter won't work
visibility: none;
height: 0;
width: 0;
padding: 0;
// Can't use display: none because some browsers will pretend the input doesn't exist, and enter won't work
visibility: hidden;
height: 0;
width: 0;
padding: 0;
}
/*********************************************** CONDUCTOR LAYOUT */
.c-conductor {
&__inputs {
display: contents;
}
&__inputs {
display: flex;
flex: 0 0 auto;
&__time-bounds {
display: grid;
grid-column-gap: $interiorMargin;
grid-row-gap: $interiorMargin;
align-items: center;
> * + * {
margin-left: $interiorMargin;
}
}
// Default: fixed mode, desktop
grid-template-rows: 1fr;
grid-template-columns: 20px auto 1fr auto;
grid-template-areas: 'tc-mode-icon tc-start tc-ticks tc-end';
}
&__ticks {
flex: 1 1 auto;
}
&__mode-icon {
grid-area: tc-mode-icon;
}
&__controls {
grid-area: tc-controls;
display: flex;
align-items: center;
&__start-fixed,
&__start-delta {
grid-area: tc-start;
display: flex;
}
> * + * {
margin-left: $interiorMargin;
}
}
&__end-fixed,
&__end-delta {
grid-area: tc-end;
display: flex;
justify-content: flex-end;
}
/************************************ FIXED MODE STYLING */
&.is-fixed-mode {
.c-conductor-axis {
&__zoom-indicator {
border: 1px solid transparent;
display: none; // Hidden by default
}
}
&__ticks {
grid-area: tc-ticks;
}
&:not(.is-panning),
&:not(.is-zooming) {
.c-conductor-axis {
&:hover,
&:active {
cursor: col-resize;
}
}
}
&__controls {
grid-area: tc-controls;
&.is-panning,
&.is-zooming {
.c-conductor-input input {
// Styles for inputs while zooming or panning
background: rgba($timeConductorActiveBg, 0.4);
}
}
&.alt-pressed {
.c-conductor-axis:hover {
// When alt is being pressed and user is hovering over the axis, set the cursor
@include cursorGrab();
}
}
&.is-panning {
.c-conductor-axis {
@include cursorGrab();
background-color: $timeConductorActivePanBg;
transition: $transIn;
svg text {
stroke: $timeConductorActivePanBg;
transition: $transIn;
}
}
}
&.is-zooming {
.c-conductor-axis__zoom-indicator {
display: block;
position: absolute;
background: rgba($timeConductorActiveBg, 0.4);
border-left-color: $timeConductorActiveBg;
border-right-color: $timeConductorActiveBg;
top: 0;
bottom: 0;
}
}
}
/************************************ REAL-TIME MODE STYLING */
&.is-realtime-mode {
.c-conductor__time-bounds {
grid-template-columns: 20px auto 1fr auto auto;
grid-template-areas: 'tc-mode-icon tc-start tc-ticks tc-updated tc-end';
}
.c-conductor__end-fixed {
grid-area: tc-updated;
}
}
}
.c-conductor-holder--compact {
flex: 0 1 auto;
overflow: hidden;
.c-conductor {
&__inputs,
&__time-bounds {
display: flex;
flex: 0 1 auto;
overflow: hidden;
}
&__inputs {
> * + * {
margin-left: $interiorMarginSm;
}
}
}
.is-realtime-mode .c-conductor__end-fixed {
display: none !important;
}
}
.c-conductor-input {
color: $colorInputFg;
display: flex;
align-items: center;
justify-content: flex-start;
> * + * {
margin-left: $interiorMargin;
margin-left: $interiorMarginSm;
}
&:before {
// Realtime-mode clock icon symbol
margin-right: $interiorMarginSm;
}
input:invalid {
background: rgba($colorFormInvalid, 0.5);
}
}
.is-realtime-mode {
.c-conductor__delta-button {
color: $colorTimeRealtimeFg;
}
.c-conductor-input {
&:before {
color: $colorTimeRealtimeFgSubtle;
}
}
.c-conductor__end-fixed {
// Displays last RT update
color: $colorTimeRealtimeFgSubtle;
input {
// Remove input look
background: none;
box-shadow: none;
color: $colorTimeRealtimeFgSubtle;
pointer-events: none;
&[disabled] {
opacity: 1 !important;
}
}
}
}
.pr-tc-input-menu--start,
.pr-tc-input-menu--end {
background: $colorBodyBg;
border-radius: $controlCr;
display: grid;
grid-template-columns: 1fr 1fr 2fr;
grid-column-gap: 3px;
grid-row-gap: 4px;
align-items: start;
box-shadow: $shdwMenu;
padding: $interiorMarginLg;
position: absolute;
left: 8px;
bottom: 24px;
z-index: 99;
&[class*='--bottom'] {
bottom: auto;
top: 24px;
}
}
.pr-tc-input-menu {
&__options {
display: flex;
> * + * {
margin-left: $interiorMargin;
}
}
&__input-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2fr;
grid-column-gap: 3px;
grid-row-gap: $interiorMargin;
align-items: start;
}
}
.l-shell__time-conductor .pr-tc-input-menu--end {
left: auto;
right: 0;
}
.pr-time-label {
font-size: 0.9em;
text-transform: uppercase;
&:before {
font-size: 0.8em;
margin-right: $interiorMarginSm;
}
}
.pr-time-input {
display: flex;
align-items: center;
white-space: nowrap;
> * + * {
margin-left: $interiorMarginSm;
}
input {
height: 22px;
line-height: 1em;
font-size: 1.25em;
}
&--date input {
width: 85px;
}
&--time input {
width: 70px;
}
&--buttons {
> * + * {
margin-left: $interiorMargin;
}
}
&__start-end-sep {
height: 100%;
}
&--input-and-button {
@include wrappedInput();
padding-right: unset;
}
}
/*********************************************** 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: 0 1 auto;
align-items: center;
padding: 2px 0;
&__setting-wrapper {
display: contents;
}
&__setting-value {
border-right: 1px solid rgba($colorTimeCommonFg, 0.3);
cursor: pointer;
color: $colorTimeCommonFg;
align-items: center;
display: flex;
flex: 0 1 auto;
overflow: hidden;
padding: 0 $fadeTruncateW;
position: relative;
max-width: max-content;
text-transform: uppercase;
white-space: nowrap;
> * + * {
margin-left: $interiorMarginSm;
&:before {
content: " - ";
display: inline-block;
opacity: 0.4;
}
}
&[class*="icon"] {
&:before {
font-size: 0.75em;
line-height: 80%;
margin-right: $interiorMarginSm;
}
}
}
.c-toggle-switch,
.c-clock-symbol,
.c-conductor__mode-icon {
// Used in independent Time Conductor
flex: 0 0 auto;
}
.c-toggle-switch {
margin-right: $interiorMarginSm;
}
.c-conductor__mode-icon {
margin-left: $interiorMargin;
}
.c-so-view & {
// Time Conductor in a Layout frame
padding: 3px 0;
.c-clock-symbol {
$h: 13px;
height: $h;
width: $h;
}
[class*='button'] {
$p: 0px;
padding: $p $p + 2;
}
}
@ -112,199 +412,211 @@
}
}
}
}
&.is-realtime-mode {
.c-conductor__time-bounds {
grid-template-columns: 20px auto 1fr auto auto;
grid-template-areas: 'tc-mode-icon tc-start tc-ticks tc-updated tc-end';
}
.c-conductor__end-fixed {
grid-area: tc-updated;
}
}
body.phone.portrait & {
.c-conductor__time-bounds {
grid-row-gap: $interiorMargin;
grid-template-rows: auto auto;
grid-template-columns: 20px auto auto;
}
.c-conductor__controls {
padding-left: 25px; // Line up visually with other controls
}
&__mode-icon {
grid-row: 1;
}
&__ticks,
&__zoom {
display: none;
}
&.is-fixed-mode {
[class*='__start-fixed'],
[class*='__end-fixed'] {
[class*='__label'] {
// Start and end are in separate columns; make the labels line up
width: 30px;
.u-fade-truncate,
.u-fade-truncate--lg {
.is-fixed-mode & {
&:after {
@include fadeTruncate($color: $colorTimeFixedBg);
}
}
[class*='__end-input'] {
justify-content: flex-start;
}
.c-conductor__time-bounds {
grid-template-areas:
'tc-mode-icon tc-start tc-start'
'tc-mode-icon tc-end tc-end';
}
}
&.is-realtime-mode {
.c-conductor__time-bounds {
grid-template-areas:
'tc-mode-icon tc-start tc-updated'
'tc-mode-icon tc-end tc-end';
}
.c-conductor__end-fixed {
justify-content: flex-end;
}
.is-realtime-mode & {
&:after {
@include fadeTruncate($color: $colorTimeRealtimeBg);
}
}
}
}
.c-conductor-holder--compact {
min-height: 22px;
.itc-popout.c-tc-input-popup {
&--fixed-mode {
background: $colorTimeFixedBg;
color: $colorTimeFixedFgSubtle;
.c-conductor {
&__inputs,
&__time-bounds {
display: flex;
em,
.pr-time-label:before {
color: $colorTimeFixedFg;
}
.c-toggle-switch {
// Used in independent Time Conductor
flex: 0 0 auto;
}
&__bounds__valuelue {
color: $colorTimeFixedFg;
}
&__time-value {
color: $colorTimeFixedFg;
}
[class*='c-button--'] {
color: $colorTimeFixedBtnFg;
[class*='label'] {
color: $colorTimeRealtimeFg;
}
}
}
}
.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--'] {
color: $colorTimeFixedBtnFg;
[class*='label'] {
color: $colorTimeRealtimeFg;
}
}
}
&__inputs {
> * + * {
margin-left: $interiorMarginSm;
}
&.c-compact-tc {
@include hover {
$c: $colorTimeFixedHov;
background: $c;
[class*='u-fade-truncate']:after {
@include fadeTruncate($color: $c);
}
}
}
}
.is-realtime-mode .c-conductor__end-fixed {
display: none !important;
}
}
.c-conductor-input {
color: $colorInputFg;
display: flex;
align-items: center;
justify-content: flex-start;
.itc-popout.c-tc-input-popup {
&--realtime-mode {
background: rgba($colorTimeRealtimeBg, 1);
color: $colorTimeRealtimeFgSubtle;
> * + * {
margin-left: $interiorMarginSm;
}
em,
.pr-time-label:before {
color: $colorTimeRealtimeFg;
}
&:before {
// Realtime-mode clock icon symbol
margin-right: $interiorMarginSm;
}
&__bounds__valuelue {
color: $colorTimeRealtimeFg;
}
.c-direction-indicator {
// Holds realtime-mode + and - symbols
font-size: 0.7em;
}
&__time-value {
color: $colorTimeRealtimeFg;
}
input:invalid {
background: rgba($colorFormInvalid, 0.5);
}
}
[class*='c-button--'] {
color: $colorTimeRealtimeBtnFg;
.is-realtime-mode {
.c-conductor__controls button,
.c-conductor__delta-button {
@include themedButton($colorTimeBg);
color: $colorTimeFg;
}
.c-conductor-input {
&:before {
color: $colorTime;
[class*='label'] {
color: $colorTimeRealtimeFg;
}
}
}
}
}
.c-conductor__end-fixed {
// Displays last RT udpate
color: $colorTime;
.is-realtime-mode.is-expanded {
&.c-compact-tc,
.c-tc-input-popup {
background: rgba($colorTimeRealtimeBg, 1);
color: $colorTimeRealtimeFgSubtle;
input {
// Remove input look
background: none;
box-shadow: none;
color: $colorTime;
pointer-events: none;
em,
.pr-time-label:before {
color: $colorTimeRealtimeFg;
}
&[disabled] {
opacity: 1 !important;
}
&__bounds__valuelue {
color: $colorTimeRealtimeFg;
}
&__time-value {
color: $colorTimeRealtimeFg;
}
[class*='c-button--'] {
color: $colorTimeRealtimeBtnFg;
[class*='label'] {
color: $colorTimeRealtimeFg;
}
}
}
}
}
[class^='pr-tc-input-menu'] {
// Uses ^= here to target both start and end menus
background: $colorBodyBg;
border-radius: $controlCr;
display: grid;
grid-template-columns: 1fr 1fr 2fr;
grid-column-gap: 3px;
grid-row-gap: 4px;
align-items: start;
box-shadow: $shdwMenu;
padding: $interiorMargin;
position: absolute;
left: 8px;
bottom: 24px;
z-index: 99;
&.c-compact-tc {
@include hover {
$c: $colorTimeRealtimeHov;
background: $c;
&[class*='--bottom'] {
bottom: auto;
top: 24px;
}
}
.l-shell__time-conductor .pr-tc-input-menu--end {
left: auto;
right: 0;
}
[class^='pr-time'] {
&[class*='label'] {
font-size: 0.8em;
opacity: 0.6;
text-transform: uppercase;
}
&[class*='controls'] {
display: flex;
align-items: center;
white-space: nowrap;
input {
height: 22px;
line-height: 22px;
margin-right: $interiorMarginSm;
font-size: 1.25em;
width: 42px;
[class*='u-fade-truncate']:after {
@include fadeTruncate($color: $c);
}
}
}
}
.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;
}
.c-button--menu {
padding: cButtonPadding($compact: true);
}
}
&--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,88 @@
/*****************************************************************************
* 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(clickEvent) {
const isAxis = clickEvent.target.closest('.c-conductor-axis') !== null;
if (isAxis || this.conductorPopup) {
return;
}
this.showConductorPopup = true;
},
positionBox() {
if (!this.conductorPopup) {
return;
}
const timeConductorOptionsBox = this.timeConductorOptionsHolder.getBoundingClientRect();
const offsetTop = this.conductorPopup.getBoundingClientRect().height;
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,122 @@
/***************************************************************************** * 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 clockMixin from '../clock-mixin';
import { TIME_CONTEXT_EVENTS } from '../../../api/time/constants';
export default {
mixins: [toggleMixin, clockMixin],
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.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 c-super-menu--sm',
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.getTimeSystem().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.selectedClock = this.getClockMetadata(clock);
}
}
};
</script>

View File

@ -0,0 +1,100 @@
<!--
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.
-->
<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';
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 c-super-menu--sm',
placement: this.openmct.menus.menuPlacement.BOTTOM_RIGHT
};
this.openmct.menus.showSuperMenu(x, y, this.modes, menuOptions);
},
setViewFromMode(mode) {
this.selectedMode = this.getModeMetadata(mode);
},
setMode(mode) {
this.setViewFromMode(mode);
this.$emit('independentModeUpdated', mode);
}
}
};
</script>

View File

@ -21,65 +21,88 @@
-->
<template>
<div
class="c-conductor"
ref="timeConductorOptionsHolder"
class="c-compact-tc"
: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
id="independentTCToggle"
:checked="independentTCEnabled"
:title="`${independentTCEnabled ? 'Disable' : 'Enable'} independent Time Conductor`"
@change="toggleIndependentTC"
/>
<toggle-switch
id="independentTCToggle"
class="c-toggle-switch--mini"
:checked="independentTCEnabled"
:title="toggleTitle"
@change="toggleIndependentTC"
/>
<ConductorModeIcon />
<ConductorModeIcon />
<div v-if="timeOptions && independentTCEnabled" class="c-conductor__controls">
<Mode
v-if="mode"
class="c-conductor__mode-select"
:key-string="domainObject.identifier.key"
:mode="timeOptions.mode"
:enabled="independentTCEnabled"
@modeChanged="saveMode"
/>
<conductor-inputs-fixed
v-if="showFixedInputs"
class="c-compact-tc__bounds--fixed"
:object-path="objectPath"
:read-only="true"
:compact="true"
/>
<conductor-inputs-fixed
v-if="isFixed"
:key-string="domainObject.identifier.key"
:object-path="objectPath"
@updated="saveFixedOffsets"
/>
<conductor-inputs-realtime
v-if="showRealtimeInputs"
class="c-compact-tc__bounds--real-time"
:object-path="objectPath"
:read-only="true"
:compact="true"
/>
<div
v-if="independentTCEnabled"
class="c-not-button c-not-button--compact c-compact-tc__gear icon-gear"
></div>
<conductor-inputs-realtime
v-else
:key-string="domainObject.identifier.key"
:object-path="objectPath"
@updated="saveClockOffsets"
/>
</div>
</div>
<conductor-pop-up
v-if="showConductorPopup"
ref="conductorPopup"
:object-path="objectPath"
:is-independent="true"
:time-options="timeOptions"
:is-fixed="isFixed"
:bottom="true"
:position-x="positionX"
:position-y="positionY"
@popupLoaded="initializePopup"
@independentModeUpdated="saveMode"
@independentClockUpdated="saveClock"
@fixedBoundsUpdated="saveFixedBounds"
@clockOffsetsUpdated="saveClockOffsets"
@dismiss="clearPopup"
/>
</div>
</template>
<script>
import { TIME_CONTEXT_EVENTS, FIXED_MODE_KEY } from '../../../api/time/constants';
import ConductorInputsFixed from '../ConductorInputsFixed.vue';
import ConductorInputsRealtime from '../ConductorInputsRealtime.vue';
import ConductorModeIcon from '@/plugins/timeConductor/ConductorModeIcon.vue';
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
import Mode from './Mode.vue';
import ConductorPopUp from '../ConductorPopUp.vue';
import independentTimeConductorPopUpManager from './independentTimeConductorPopUpManager';
export default {
components: {
Mode,
ConductorModeIcon,
ConductorInputsRealtime,
ConductorInputsFixed,
ConductorPopUp,
ToggleSwitch
},
inject: ['openmct'],
mixins: [independentTimeConductorPopUpManager],
inject: {
openmct: 'openmct',
configuration: {
from: 'configuration',
default: undefined
}
},
props: {
domainObject: {
type: Object,
@ -91,22 +114,44 @@ export default {
}
},
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 {
timeOptions: this.domainObject.configuration.timeOptions || {
clockOffsets: this.openmct.time.clockOffsets(),
fixedOffsets: this.openmct.time.bounds()
},
mode: undefined,
independentTCEnabled: this.domainObject.configuration.useIndependentTime === true
timeOptions,
isFixed,
independentTCEnabled: this.domainObject.configuration.useIndependentTime === true,
viewBounds: {
start: fixedOffsets.start,
end: fixedOffsets.end
}
};
},
computed: {
isFixed() {
if (!this.mode || !this.mode.key) {
return this.openmct.time.clock() === undefined;
} else {
return this.mode.key === 'fixed';
}
toggleTitle() {
return `${this.independentTCEnabled ? 'Disable' : 'Enable'} independent Time Conductor`;
},
showFixedInputs() {
return this.isFixed && this.independentTCEnabled;
},
showRealtimeInputs() {
return !this.isFixed && this.independentTCEnabled;
}
},
watch: {
@ -118,15 +163,33 @@ export default {
this.destroyIndependentTime();
this.independentTCEnabled = domainObject.configuration.useIndependentTime === true;
this.timeOptions = domainObject.configuration.timeOptions || {
clockOffsets: this.openmct.time.clockOffsets(),
fixedOffsets: this.openmct.time.bounds()
this.timeOptions = domainObject.configuration.timeOptions ?? {
clockOffsets: this.openmct.time.getClockOffsets(),
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();
}
},
deep: true
},
objectPath: {
handler(newPath, oldPath) {
//domain object or view has probably changed
this.setTimeContext();
},
deep: true
}
},
mounted() {
@ -141,120 +204,118 @@ export default {
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
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) {
this.registerIndependentTimeOffsets();
}
},
toggleIndependentTC() {
this.independentTCEnabled = !this.independentTCEnabled;
if (this.independentTCEnabled) {
this.registerIndependentTimeOffsets();
} else {
this.clearPopup();
this.destroyIndependentTime();
}
this.$emit('stateChanged', this.independentTCEnabled);
this.openmct.objects.mutate(
this.domainObject,
'configuration.useIndependentTime',
this.independentTCEnabled
);
},
setTimeContext() {
this.stopFollowingTimeContext();
if (this.timeContext) {
this.stopFollowingTimeContext();
}
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() {
if (this.timeContext) {
this.timeContext.off('clock', this.setTimeOptions);
}
this.timeContext.off(TIME_CONTEXT_EVENTS.clockChanged, this.setTimeOptionsClock);
this.timeContext.off(TIME_CONTEXT_EVENTS.modeChanged, this.setTimeOptionsMode);
},
setTimeOptions(clock) {
setTimeOptionsClock(clock) {
this.setTimeOptionsOffsets();
this.timeOptions.clock = clock.key;
},
setTimeOptionsMode(mode) {
this.setTimeOptionsOffsets();
this.timeOptions.mode = mode;
},
setTimeOptionsOffsets() {
this.timeOptions.clockOffsets =
this.timeOptions.clockOffsets || this.timeContext.clockOffsets();
this.timeOptions.fixedOffsets = this.timeOptions.fixedOffsets || this.timeContext.bounds();
if (!this.timeOptions.mode) {
this.mode =
this.timeContext.clock() === undefined
? { key: 'fixed' }
: { key: Object.create(this.timeContext.clock()).key };
this.registerIndependentTimeOffsets();
}
this.timeOptions.clockOffsets ?? this.timeContext.getClockOffsets();
this.timeOptions.fixedOffsets = this.timeOptions.fixedOffsets ?? this.timeContext.getBounds();
},
saveFixedOffsets(offsets) {
const newOptions = Object.assign({}, this.timeOptions, {
fixedOffsets: offsets
saveFixedBounds(bounds) {
const newOptions = this.updateTimeOptionProperty({
fixedOffsets: bounds
});
this.updateTimeOptions(newOptions);
},
saveClockOffsets(offsets) {
const newOptions = Object.assign({}, this.timeOptions, {
const newOptions = this.updateTimeOptionProperty({
clockOffsets: offsets
});
this.updateTimeOptions(newOptions);
},
saveMode(mode) {
this.mode = mode;
const newOptions = Object.assign({}, this.timeOptions, {
mode: this.mode
this.isFixed = mode === FIXED_MODE_KEY;
const newOptions = this.updateTimeOptionProperty({
mode: mode
});
this.updateTimeOptions(newOptions);
},
saveClock(clock) {
const newOptions = this.updateTimeOptionProperty({
clock
});
this.updateTimeOptions(newOptions);
},
updateTimeOptions(options) {
this.timeOptions = options;
if (!this.timeOptions.mode) {
this.timeOptions.mode = this.mode;
}
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() {
if (!this.timeOptions.mode) {
return;
}
const timeContext = this.openmct.time.getIndependentContext(this.keyString);
let offsets;
if (this.isFixed) {
offsets = this.timeOptions.fixedOffsets;
offsets = this.timeOptions.fixedOffsets ?? this.timeContext.getBounds();
} else {
if (this.timeOptions.clockOffsets === undefined) {
this.timeOptions.clockOffsets = this.openmct.time.clockOffsets();
}
offsets = this.timeOptions.clockOffsets;
offsets = this.timeOptions.clockOffsets ?? this.openmct.time.getClockOffsets();
}
const timeContext = this.openmct.time.getIndependentContext(this.keyString);
if (!timeContext.hasOwnContext()) {
this.unregisterIndependentTime = this.openmct.time.addIndependentContext(
this.keyString,
offsets,
this.isFixed ? undefined : this.mode.key
this.isFixed ? undefined : this.timeOptions.clock
);
} else {
if (this.isFixed) {
timeContext.stopClock();
timeContext.bounds(offsets);
} else {
timeContext.clock(this.mode.key, offsets);
if (!this.isFixed) {
timeContext.setClock(this.timeOptions.clock);
}
timeContext.setMode(this.timeOptions.mode, offsets);
}
},
destroyIndependentTime() {
if (this.unregisterIndependentTime) {
this.unregisterIndependentTime();
}
},
updateTimeOptionProperty(option) {
return Object.assign({}, this.timeOptions, option);
}
}
};

View File

@ -1,231 +0,0 @@
<!--
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.
-->
<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,121 @@
/*****************************************************************************
* 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: -10000, // prevents initial flash after appending to body element
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,
// something is emitting a dupe event with pointer id = -1, want to ignore those
// itc is currently enabled
if (
!this.conductorPopup &&
!isToggle &&
clickEvent.pointerId !== -1 &&
this.independentTCEnabled
) {
this.showConductorPopup = true;
}
},
handleClickAway(clickEvent) {
if (this.canClose(clickEvent)) {
this.clearPopup();
}
},
positionBox() {
if (!this.conductorPopup) {
return;
}
const timeConductorOptionsBox = this.timeConductorOptionsHolder.getBoundingClientRect();
const topHalf = timeConductorOptionsBox.top < window.innerHeight / 2;
const padding = 5;
const offsetTop = this.conductorPopup.getBoundingClientRect().height;
const popupRight = timeConductorOptionsBox.left + this.conductorPopup.clientWidth;
const offsetLeft = Math.min(window.innerWidth - popupRight, 0);
if (topHalf) {
this.positionY =
timeConductorOptionsBox.bottom + this.conductorPopup.clientHeight + padding;
} else {
this.positionY = timeConductorOptionsBox.top - padding;
}
this.positionX = timeConductorOptionsBox.left + 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;
this.positionX = -10000; // reset it off screan
document.removeEventListener('click', this.handleClickAway);
window.removeEventListener('resize', this.positionBox);
},
canClose(clickAwayEvent) {
const isChildMenu = clickAwayEvent.target.closest('.c-menu') !== null;
const isPopupOrChild = clickAwayEvent.target.closest('.c-tc-input-popup') !== null;
const isTimeConductor = this.timeConductorOptionsHolder.contains(clickAwayEvent.target);
const isToggle = clickAwayEvent.target.classList.contains('c-toggle-switch__slider');
return !isTimeConductor && !isChildMenu && !isToggle && !isPopupOrChild;
}
}
};

View File

@ -0,0 +1,51 @@
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);
},
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;
}
}
};

View File

@ -21,6 +21,7 @@
*****************************************************************************/
import Conductor from './Conductor.vue';
import { FIXED_MODE_KEY, REALTIME_MODE_KEY } from '../../api/time/constants';
function isTruthy(a) {
return Boolean(a);
@ -118,11 +119,34 @@ export default function (config) {
throwIfError(configResult);
const defaults = config.menuOptions[0];
if (defaults.clock) {
openmct.time.clock(defaults.clock, defaults.clockOffsets);
openmct.time.timeSystem(defaults.timeSystem, openmct.time.bounds());
const defaultClock = defaults.clock;
const defaultMode = defaultClock ? REALTIME_MODE_KEY : FIXED_MODE_KEY;
const defaultBounds = defaults?.bounds;
let clockOffsets = openmct.time.getClockOffsets();
if (defaultClock) {
openmct.time.setClock(defaults.clock);
clockOffsets = defaults.clockOffsets;
} 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);
clockOffsets = firstClock.clockOffsets;
}
}
openmct.time.setMode(defaultMode, defaultClock ? clockOffsets : defaultBounds);
openmct.time.setTimeSystem(defaults.timeSystem, defaultBounds);
//We are going to set the clockOffsets in fixed time mode since the conductor components down the line need these
if (clockOffsets && defaultMode === FIXED_MODE_KEY) {
openmct.time.setClockOffsets(clockOffsets);
}
//We are going to set the fixed time bounds in realtime time mode since the conductor components down the line need these
if (defaultBounds && defaultMode === REALTIME_MODE_KEY) {
openmct.time.setBounds(clockOffsets);
}
openmct.on('start', function () {

View File

@ -24,6 +24,7 @@ import { createMouseEvent, createOpenMct, resetApplicationState } from 'utils/te
import { millisecondsToDHMS, getPreciseDuration } from '../../utils/duration';
import ConductorPlugin from './plugin';
import Vue from 'vue';
import { FIXED_MODE_KEY } from '../../api/time/constants';
const THIRTY_SECONDS = 30 * 1000;
const ONE_MINUTE = THIRTY_SECONDS * 2;
@ -65,7 +66,6 @@ describe('time conductor', () => {
beforeEach((done) => {
openmct = createOpenMct();
openmct.install(new ConductorPlugin(config));
element = document.createElement('div');
element.style.width = '640px';
element.style.height = '480px';
@ -75,7 +75,7 @@ describe('time conductor', () => {
element.appendChild(child);
openmct.on('start', () => {
openmct.time.bounds({
openmct.time.setMode(FIXED_MODE_KEY, {
start: config.menuOptions[0].bounds.start,
end: config.menuOptions[0].bounds.end
});
@ -97,55 +97,63 @@ describe('time conductor', () => {
describe('in fixed time mode', () => {
it('shows delta inputs', () => {
const fixedModeEl = appHolder.querySelector('.is-fixed-mode');
const dateTimeInputs = fixedModeEl.querySelectorAll('.c-input--datetime');
expect(dateTimeInputs[0].value).toEqual('1978-01-19 23:30:00.000Z');
expect(dateTimeInputs[1].value).toEqual('1978-01-20 00:00:00.000Z');
expect(fixedModeEl.querySelector('.c-mode-button .c-button__label').innerHTML).toEqual(
'Fixed Timespan'
);
const dateTimeInputs = fixedModeEl.querySelectorAll('.c-compact-tc__setting-value__elem');
expect(dateTimeInputs[0].innerHTML.trim()).toEqual('Fixed Timespan');
expect(dateTimeInputs[1].innerHTML.trim()).toEqual('Local Clock');
expect(dateTimeInputs[2].innerHTML.trim()).toEqual('UTC');
const dateTimes = fixedModeEl.querySelectorAll('.c-compact-tc__setting-value');
expect(dateTimes[1].innerHTML.trim()).toEqual('1978-01-19 23:30:00.000Z');
expect(dateTimes[2].innerHTML.trim()).toEqual('1978-01-20 00:00:00.000Z');
});
});
describe('in realtime mode', () => {
beforeEach((done) => {
const switcher = appHolder.querySelector('.c-mode-button');
const switcher = appHolder.querySelector('.is-fixed-mode');
const clickEvent = createMouseEvent('click');
switcher.dispatchEvent(clickEvent);
Vue.nextTick(() => {
const clockItem = document.querySelectorAll('.c-conductor__mode-menu li')[1];
clockItem.dispatchEvent(clickEvent);
const modeButton = switcher.querySelector('.c-tc-input-popup .c-button--menu');
const clickEvent1 = createMouseEvent('click');
modeButton.dispatchEvent(clickEvent1);
Vue.nextTick(() => {
done();
const clockItem = document.querySelectorAll(
'.c-conductor__mode-menu .c-super-menu__menu li'
)[1];
const clickEvent2 = createMouseEvent('click');
clockItem.dispatchEvent(clickEvent2);
Vue.nextTick(() => {
done();
});
});
});
});
it('shows delta inputs', () => {
const realtimeModeEl = appHolder.querySelector('.is-realtime-mode');
const dateTimeInputs = realtimeModeEl.querySelectorAll('.c-conductor__delta-button');
const dateTimeInputs = realtimeModeEl.querySelectorAll('.c-compact-tc__setting-value__elem');
expect(dateTimeInputs[0].innerHTML.trim()).toEqual('Real-Time');
expect(dateTimeInputs[0].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:30:00');
expect(dateTimeInputs[1].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:00:30');
const dateTimes = realtimeModeEl.querySelectorAll('.c-compact-tc__setting-value');
expect(dateTimes[1].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:30:00');
expect(dateTimes[2].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:00:30');
});
it('shows clock options', () => {
const realtimeModeEl = appHolder.querySelector('.is-realtime-mode');
expect(realtimeModeEl.querySelector('.c-mode-button .c-button__label').innerHTML).toEqual(
'Local Clock'
);
const dateTimeInputs = realtimeModeEl.querySelectorAll('.c-compact-tc__setting-value__elem');
expect(dateTimeInputs[1].innerHTML.trim()).toEqual('Local Clock');
});
it('shows the current time', () => {
const realtimeModeEl = appHolder.querySelector('.is-realtime-mode');
const currentTimeEl = realtimeModeEl.querySelector('.c-input--datetime');
const currentTimeEl = realtimeModeEl.querySelector('.c-compact-tc__current-update');
const currentTime = openmct.time.clock().currentValue();
const { start, end } = openmct.time.bounds();
expect(currentTime).toBeGreaterThan(start);
expect(currentTime).toBeLessThanOrEqual(end);
expect(currentTimeEl.value.length).toBeGreaterThan(0);
expect(currentTimeEl.innerHTML.trim().length).toBeGreaterThan(0);
});
});
});

View File

@ -1,198 +0,0 @@
<!--
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.
-->
<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,315 @@
<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')"
/>
<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')"
/>
</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')"
/>
<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')"
/>
</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.getBounds();
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');
},
endDateSelected(date) {
this.formattedBounds.end = this.timeFormatter.format(date).split(' ')[0];
this.validateAllBounds('endDate');
},
hide($event) {
if ($event.target.className.indexOf('c-button icon-x') > -1) {
this.$emit('dismiss');
}
}
}
};
</script>

View File

@ -0,0 +1,254 @@
<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>