chore: add prettier (2/3): apply formatting, re-enable lint ci step (#6682)

* style: apply prettier formatting

* fix: re-enable lint ci check
This commit is contained in:
Jesse Mazzella
2023-05-18 14:54:46 -07:00
committed by GitHub
parent 172e0b23fd
commit caa7bc6fae
976 changed files with 115922 additions and 114693 deletions

View File

@ -20,50 +20,46 @@
at runtime from the About dialog for additional information.
-->
<template>
<div
<div
class="c-conductor"
:class="[
{ 'is-zooming': isZooming },
{ 'is-panning': isPanning },
{ 'alt-pressed': altPressed },
isFixed ? 'is-fixed-mode' : 'is-realtime-mode'
{ 'is-zooming': isZooming },
{ 'is-panning': isPanning },
{ 'alt-pressed': altPressed },
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"
/>
<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"
/>
<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"
/>
</div>
</div>
</div>
</template>
<script>
@ -73,149 +69,152 @@ import ConductorTimeSystem from './ConductorTimeSystem.vue';
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 ConductorInputsFixed from './ConductorInputsFixed.vue';
import ConductorInputsRealtime from './ConductorInputsRealtime.vue';
const DEFAULT_DURATION_FORMATTER = 'duration';
export default {
components: {
ConductorInputsRealtime,
ConductorInputsFixed,
ConductorMode,
ConductorTimeSystem,
ConductorAxis,
ConductorModeIcon,
ConductorHistory
},
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(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
components: {
ConductorInputsRealtime,
ConductorInputsFixed,
ConductorMode,
ConductorTimeSystem,
ConductorAxis,
ConductorModeIcon,
ConductorHistory
},
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(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
);
return {
timeSystem: timeSystem,
timeFormatter: timeFormatter,
durationFormatter: durationFormatter,
offsets: {
start: offsets && durationFormatter.format(Math.abs(offsets.start)),
end: offsets && durationFormatter.format(Math.abs(offsets.end))
},
bounds: {
start: bounds.start,
end: bounds.end
},
formattedBounds: {
start: timeFormatter.format(bounds.start),
end: timeFormatter.format(bounds.end)
},
viewBounds: {
start: bounds.start,
end: bounds.end
},
isFixed: this.openmct.time.clock() === undefined,
isUTCBased: timeSystem.isUTCBased,
showDatePicker: false,
altPressed: false,
isPanning: false,
isZooming: false,
showTCInputStart: false,
showTCInputEnd: false
};
},
computed: {
timeMode() {
return this.isFixed ? 'fixed' : 'realtime';
}
},
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);
},
beforeDestroy() {
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp);
},
methods: {
handleNewBounds(bounds) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
},
setBounds(bounds) {
this.bounds = bounds;
},
handleKeyDown(event) {
if (event.key === 'Alt') {
this.altPressed = true;
}
},
handleKeyUp(event) {
if (event.key === 'Alt') {
this.altPressed = false;
}
},
pan(bounds) {
this.isPanning = true;
this.setViewFromBounds(bounds);
},
endPan(bounds) {
this.isPanning = false;
if (bounds) {
this.openmct.time.bounds(bounds);
}
},
zoom(bounds) {
if (isNaN(bounds.start) || isNaN(bounds.end)) {
this.isZooming = false;
} else {
this.isZooming = true;
this.formattedBounds.start = this.timeFormatter.format(bounds.start);
this.formattedBounds.end = this.timeFormatter.format(bounds.end);
}
},
endZoom(bounds) {
this.isZooming = false;
if (bounds) {
this.openmct.time.bounds(bounds);
} else {
this.setViewFromBounds(this.bounds);
}
},
setTimeSystem(timeSystem) {
this.timeSystem = timeSystem;
this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
this.durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.isUTCBased = timeSystem.isUTCBased;
},
setViewFromClock(clock) {
// this.clearAllValidation();
this.isFixed = clock === undefined;
},
setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start);
this.formattedBounds.end = this.timeFormatter.format(bounds.end);
this.viewBounds.start = bounds.start;
this.viewBounds.end = bounds.end;
},
getFormatter(key) {
return this.openmct.telemetry.getValueFormatter({
format: key
}).formatter;
},
saveClockOffsets(offsets) {
this.openmct.time.clockOffsets(offsets);
},
saveFixedOffsets(bounds) {
this.openmct.time.bounds(bounds);
}
return {
timeSystem: timeSystem,
timeFormatter: timeFormatter,
durationFormatter: durationFormatter,
offsets: {
start: offsets && durationFormatter.format(Math.abs(offsets.start)),
end: offsets && durationFormatter.format(Math.abs(offsets.end))
},
bounds: {
start: bounds.start,
end: bounds.end
},
formattedBounds: {
start: timeFormatter.format(bounds.start),
end: timeFormatter.format(bounds.end)
},
viewBounds: {
start: bounds.start,
end: bounds.end
},
isFixed: this.openmct.time.clock() === undefined,
isUTCBased: timeSystem.isUTCBased,
showDatePicker: false,
altPressed: false,
isPanning: false,
isZooming: false,
showTCInputStart: false,
showTCInputEnd: false
};
},
computed: {
timeMode() {
return this.isFixed ? 'fixed' : 'realtime';
}
},
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);
},
beforeDestroy() {
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp);
},
methods: {
handleNewBounds(bounds) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
},
setBounds(bounds) {
this.bounds = bounds;
},
handleKeyDown(event) {
if (event.key === 'Alt') {
this.altPressed = true;
}
},
handleKeyUp(event) {
if (event.key === 'Alt') {
this.altPressed = false;
}
},
pan(bounds) {
this.isPanning = true;
this.setViewFromBounds(bounds);
},
endPan(bounds) {
this.isPanning = false;
if (bounds) {
this.openmct.time.bounds(bounds);
}
},
zoom(bounds) {
if (isNaN(bounds.start) || isNaN(bounds.end)) {
this.isZooming = false;
} else {
this.isZooming = true;
this.formattedBounds.start = this.timeFormatter.format(bounds.start);
this.formattedBounds.end = this.timeFormatter.format(bounds.end);
}
},
endZoom(bounds) {
this.isZooming = false;
if (bounds) {
this.openmct.time.bounds(bounds);
} else {
this.setViewFromBounds(this.bounds);
}
},
setTimeSystem(timeSystem) {
this.timeSystem = timeSystem;
this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
this.durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
);
this.isUTCBased = timeSystem.isUTCBased;
},
setViewFromClock(clock) {
// this.clearAllValidation();
this.isFixed = clock === undefined;
},
setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start);
this.formattedBounds.end = this.timeFormatter.format(bounds.end);
this.viewBounds.start = bounds.start;
this.viewBounds.end = bounds.end;
},
getFormatter(key) {
return this.openmct.telemetry.getValueFormatter({
format: key
}).formatter;
},
saveClockOffsets(offsets) {
this.openmct.time.clockOffsets(offsets);
},
saveFixedOffsets(bounds) {
this.openmct.time.bounds(bounds);
}
}
};
</script>

View File

@ -20,20 +20,12 @@
at runtime from the About dialog for additional information.
-->
<template>
<div
ref="axisHolder"
class="c-conductor-axis"
@mousedown="dragStart($event)"
>
<div
class="c-conductor-axis__zoom-indicator"
:style="zoomStyle"
></div>
</div>
<div ref="axisHolder" class="c-conductor-axis" @mousedown="dragStart($event)">
<div class="c-conductor-axis__zoom-indicator" :style="zoomStyle"></div>
</div>
</template>
<script>
import * as d3Selection from 'd3-selection';
import * as d3Axis from 'd3-axis';
import * as d3Scale from 'd3-scale';
@ -46,268 +38,256 @@ const PIXELS_PER_TICK = 100;
const PIXELS_PER_TICK_WIDE = 200;
export default {
inject: ['openmct'],
props: {
viewBounds: {
type: Object,
required: true
},
isFixed: {
type: Boolean,
required: true
},
altPressed: {
type: Boolean,
required: true
}
inject: ['openmct'],
props: {
viewBounds: {
type: Object,
required: true
},
data() {
return {
inPanMode: false,
dragStartX: undefined,
dragX: undefined,
zoomStyle: {}
isFixed: {
type: Boolean,
required: true
},
altPressed: {
type: Boolean,
required: true
}
},
data() {
return {
inPanMode: false,
dragStartX: undefined,
dragX: undefined,
zoomStyle: {}
};
},
computed: {
inZoomMode() {
return !this.inPanMode;
}
},
watch: {
viewBounds: {
handler() {
this.setScale();
},
deep: true
}
},
mounted() {
let vis = d3Selection.select(this.$refs.axisHolder).append('svg:svg');
this.xAxis = d3Axis.axisTop();
this.dragging = false;
// 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.setAxisDimensions();
this.setScale();
//Respond to changes in conductor
this.openmct.time.on('timeSystem', this.setViewFromTimeSystem);
setInterval(this.resize, RESIZE_POLL_INTERVAL);
},
methods: {
setAxisDimensions() {
const axisHolder = this.$refs.axisHolder;
const rect = axisHolder.getBoundingClientRect();
this.left = Math.round(rect.left);
this.width = axisHolder.clientWidth;
},
setScale() {
if (!this.width) {
return;
}
let timeSystem = this.openmct.time.timeSystem();
if (timeSystem.isUTCBased) {
this.xScale.domain([new Date(this.viewBounds.start), new Date(this.viewBounds.end)]);
} else {
this.xScale.domain([this.viewBounds.start, this.viewBounds.end]);
}
this.xAxis.scale(this.xScale);
this.xScale.range([PADDING, this.width - PADDING * 2]);
this.axisElement.call(this.xAxis);
if (this.width > 1800) {
this.xAxis.ticks(this.width / PIXELS_PER_TICK_WIDE);
} else {
this.xAxis.ticks(this.width / PIXELS_PER_TICK);
}
this.msPerPixel = (this.viewBounds.end - this.viewBounds.start) / this.width;
},
setViewFromTimeSystem(timeSystem) {
//The D3 scale used depends on the type of time system as d3
// supports UTC out of the box.
if (timeSystem.isUTCBased) {
this.xScale = d3Scale.scaleUtc();
} else {
this.xScale = d3Scale.scaleLinear();
}
this.xAxis.scale(this.xScale);
this.xAxis.tickFormat(utcMultiTimeFormat);
this.axisElement.call(this.xAxis);
this.setScale();
},
getActiveFormatter() {
let timeSystem = this.openmct.time.timeSystem();
if (this.isFixed) {
return this.getFormatter(timeSystem.timeFormat);
} else {
return this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
}
},
getFormatter(key) {
return this.openmct.telemetry.getValueFormatter({
format: key
}).formatter;
},
dragStart($event) {
if (this.isFixed) {
this.dragStartX = $event.clientX;
if (this.altPressed) {
this.inPanMode = true;
}
document.addEventListener('mousemove', this.drag);
document.addEventListener('mouseup', this.dragEnd, {
once: true
});
if (this.inZoomMode) {
this.startZoom();
}
}
},
drag($event) {
if (!this.dragging) {
this.dragging = true;
requestAnimationFrame(() => {
this.dragX = $event.clientX;
if (this.inPanMode) {
this.pan();
} else {
this.zoom();
}
this.dragging = false;
});
}
},
dragEnd() {
if (this.inPanMode) {
this.endPan();
} else {
this.endZoom();
}
document.removeEventListener('mousemove', this.drag);
this.dragStartX = undefined;
this.dragX = undefined;
},
pan() {
const panBounds = this.getPanBounds();
this.$emit('panAxis', panBounds);
},
endPan() {
const panBounds = this.isChangingViewBounds() ? this.getPanBounds() : undefined;
this.$emit('endPan', panBounds);
this.inPanMode = false;
},
getPanBounds() {
const bounds = this.openmct.time.bounds();
const deltaTime = bounds.end - bounds.start;
const deltaX = this.dragX - this.dragStartX;
const percX = deltaX / this.width;
const panStart = bounds.start - percX * deltaTime;
return {
start: parseInt(panStart, 10),
end: parseInt(panStart + deltaTime, 10)
};
},
startZoom() {
const x = this.scaleToBounds(this.dragStartX);
this.zoomStyle = {
left: `${this.dragStartX - this.left}px`
};
this.$emit('zoomAxis', {
start: x,
end: x
});
},
zoom() {
const zoomRange = this.getZoomRange();
this.zoomStyle = {
left: `${zoomRange.start - this.left}px`,
width: `${zoomRange.end - zoomRange.start}px`
};
this.$emit('zoomAxis', {
start: this.scaleToBounds(zoomRange.start),
end: this.scaleToBounds(zoomRange.end)
});
},
endZoom() {
let zoomBounds;
if (this.isChangingViewBounds()) {
const zoomRange = this.getZoomRange();
zoomBounds = {
start: this.scaleToBounds(zoomRange.start),
end: this.scaleToBounds(zoomRange.end)
};
},
computed: {
inZoomMode() {
return !this.inPanMode;
}
},
watch: {
viewBounds: {
handler() {
this.setScale();
},
deep: true
}
},
mounted() {
let vis = d3Selection.select(this.$refs.axisHolder).append("svg:svg");
}
this.xAxis = d3Axis.axisTop();
this.dragging = false;
this.zoomStyle = {};
this.$emit('endZoom', zoomBounds);
},
getZoomRange() {
const leftBound = this.left;
const rightBound = this.left + this.width;
// draw x axis with labels. CSS is used to position them.
this.axisElement = vis.append("g")
.attr("class", "axis");
const zoomStart = this.dragX < leftBound ? leftBound : Math.min(this.dragX, this.dragStartX);
this.setViewFromTimeSystem(this.openmct.time.timeSystem());
const zoomEnd = this.dragX > rightBound ? rightBound : Math.max(this.dragX, this.dragStartX);
return {
start: zoomStart,
end: zoomEnd
};
},
scaleToBounds(value) {
const bounds = this.openmct.time.bounds();
const timeDelta = bounds.end - bounds.start;
const valueDelta = value - this.left;
const offset = (valueDelta / this.width) * timeDelta;
return parseInt(bounds.start + offset, 10);
},
isChangingViewBounds() {
return this.dragStartX && this.dragX && this.dragStartX !== this.dragX;
},
resize() {
if (this.$refs.axisHolder.clientWidth !== this.width) {
this.setAxisDimensions();
this.setScale();
//Respond to changes in conductor
this.openmct.time.on("timeSystem", this.setViewFromTimeSystem);
setInterval(this.resize, RESIZE_POLL_INTERVAL);
},
methods: {
setAxisDimensions() {
const axisHolder = this.$refs.axisHolder;
const rect = axisHolder.getBoundingClientRect();
this.left = Math.round(rect.left);
this.width = axisHolder.clientWidth;
},
setScale() {
if (!this.width) {
return;
}
let timeSystem = this.openmct.time.timeSystem();
if (timeSystem.isUTCBased) {
this.xScale.domain(
[new Date(this.viewBounds.start), new Date(this.viewBounds.end)]
);
} else {
this.xScale.domain(
[this.viewBounds.start, this.viewBounds.end]
);
}
this.xAxis.scale(this.xScale);
this.xScale.range([PADDING, this.width - PADDING * 2]);
this.axisElement.call(this.xAxis);
if (this.width > 1800) {
this.xAxis.ticks(this.width / PIXELS_PER_TICK_WIDE);
} else {
this.xAxis.ticks(this.width / PIXELS_PER_TICK);
}
this.msPerPixel = (this.viewBounds.end - this.viewBounds.start) / this.width;
},
setViewFromTimeSystem(timeSystem) {
//The D3 scale used depends on the type of time system as d3
// supports UTC out of the box.
if (timeSystem.isUTCBased) {
this.xScale = d3Scale.scaleUtc();
} else {
this.xScale = d3Scale.scaleLinear();
}
this.xAxis.scale(this.xScale);
this.xAxis.tickFormat(utcMultiTimeFormat);
this.axisElement.call(this.xAxis);
this.setScale();
},
getActiveFormatter() {
let timeSystem = this.openmct.time.timeSystem();
if (this.isFixed) {
return this.getFormatter(timeSystem.timeFormat);
} else {
return this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
}
},
getFormatter(key) {
return this.openmct.telemetry.getValueFormatter({
format: key
}).formatter;
},
dragStart($event) {
if (this.isFixed) {
this.dragStartX = $event.clientX;
if (this.altPressed) {
this.inPanMode = true;
}
document.addEventListener('mousemove', this.drag);
document.addEventListener('mouseup', this.dragEnd, {
once: true
});
if (this.inZoomMode) {
this.startZoom();
}
}
},
drag($event) {
if (!this.dragging) {
this.dragging = true;
requestAnimationFrame(() => {
this.dragX = $event.clientX;
if (this.inPanMode) {
this.pan();
} else {
this.zoom();
}
this.dragging = false;
});
}
},
dragEnd() {
if (this.inPanMode) {
this.endPan();
} else {
this.endZoom();
}
document.removeEventListener('mousemove', this.drag);
this.dragStartX = undefined;
this.dragX = undefined;
},
pan() {
const panBounds = this.getPanBounds();
this.$emit('panAxis', panBounds);
},
endPan() {
const panBounds = this.isChangingViewBounds()
? this.getPanBounds()
: undefined;
this.$emit('endPan', panBounds);
this.inPanMode = false;
},
getPanBounds() {
const bounds = this.openmct.time.bounds();
const deltaTime = bounds.end - bounds.start;
const deltaX = this.dragX - this.dragStartX;
const percX = deltaX / this.width;
const panStart = bounds.start - percX * deltaTime;
return {
start: parseInt(panStart, 10),
end: parseInt(panStart + deltaTime, 10)
};
},
startZoom() {
const x = this.scaleToBounds(this.dragStartX);
this.zoomStyle = {
left: `${this.dragStartX - this.left}px`
};
this.$emit('zoomAxis', {
start: x,
end: x
});
},
zoom() {
const zoomRange = this.getZoomRange();
this.zoomStyle = {
left: `${zoomRange.start - this.left}px`,
width: `${zoomRange.end - zoomRange.start}px`
};
this.$emit('zoomAxis', {
start: this.scaleToBounds(zoomRange.start),
end: this.scaleToBounds(zoomRange.end)
});
},
endZoom() {
let zoomBounds;
if (this.isChangingViewBounds()) {
const zoomRange = this.getZoomRange();
zoomBounds = {
start: this.scaleToBounds(zoomRange.start),
end: this.scaleToBounds(zoomRange.end)
};
}
this.zoomStyle = {};
this.$emit('endZoom', zoomBounds);
},
getZoomRange() {
const leftBound = this.left;
const rightBound = this.left + this.width;
const zoomStart = this.dragX < leftBound
? leftBound
: Math.min(this.dragX, this.dragStartX);
const zoomEnd = this.dragX > rightBound
? rightBound
: Math.max(this.dragX, this.dragStartX);
return {
start: zoomStart,
end: zoomEnd
};
},
scaleToBounds(value) {
const bounds = this.openmct.time.bounds();
const timeDelta = bounds.end - bounds.start;
const valueDelta = value - this.left;
const offset = valueDelta / this.width * timeDelta;
return parseInt(bounds.start + offset, 10);
},
isChangingViewBounds() {
return this.dragStartX && this.dragX && this.dragStartX !== this.dragX;
},
resize() {
if (this.$refs.axisHolder.clientWidth !== this.width) {
this.setAxisDimensions();
this.setScale();
}
}
}
}
}
};
</script>

View File

@ -20,20 +20,17 @@
at runtime from the About dialog for additional information.
-->
<template>
<div
ref="historyButton"
class="c-ctrl-wrapper c-ctrl-wrapper--menus-up"
>
<div ref="historyButton" class="c-ctrl-wrapper c-ctrl-wrapper--menus-up">
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
aria-label="Time Conductor History"
class="c-button--menu c-history-button icon-history"
@click.prevent.stop="showHistoryMenu"
>
<span class="c-button__label">History</span>
</button>
<button
aria-label="Time Conductor History"
class="c-button--menu c-history-button icon-history"
@click.prevent.stop="showHistoryMenu"
>
<span class="c-button__label">History</span>
</button>
</div>
</div>
</div>
</template>
<script>
@ -42,270 +39,278 @@ const LOCAL_STORAGE_HISTORY_KEY_FIXED = 'tcHistory';
const LOCAL_STORAGE_HISTORY_KEY_REALTIME = 'tcHistoryRealtime';
const DEFAULT_RECORDS_LENGTH = 10;
import { millisecondsToDHMS } from "utils/duration";
import UTCTimeFormat from "../utcTimeSystem/UTCTimeFormat.js";
import { millisecondsToDHMS } from 'utils/duration';
import UTCTimeFormat from '../utcTimeSystem/UTCTimeFormat.js';
export default {
inject: ['openmct', 'configuration'],
props: {
bounds: {
type: Object,
required: true
},
offsets: {
type: Object,
required: false,
default: () => {}
},
timeSystem: {
type: Object,
required: true
},
mode: {
type: String,
required: true
}
inject: ['openmct', 'configuration'],
props: {
bounds: {
type: Object,
required: true
},
data() {
return {
/**
* previous bounds entries available for easy re-use
* @realtimeHistory array of timespans
* @timespans {start, end} number representing timestamp
*/
realtimeHistory: {},
/**
* previous bounds entries available for easy re-use
* @fixedHistory array of timespans
* @timespans {start, end} number representing timestamp
*/
fixedHistory: {},
presets: [],
isFixed: this.openmct.time.clock() === undefined
};
offsets: {
type: Object,
required: false,
default: () => {}
},
computed: {
currentHistory() {
return this.mode + 'History';
},
historyForCurrentTimeSystem() {
const history = this[this.currentHistory][this.timeSystem.key];
return history;
},
storageKey() {
let key = LOCAL_STORAGE_HISTORY_KEY_FIXED;
if (!this.isFixed) {
key = LOCAL_STORAGE_HISTORY_KEY_REALTIME;
}
return key;
}
timeSystem: {
type: Object,
required: true
},
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();
},
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 => {
let name;
const startTime = this.formatTime(timespan.start);
const description = `${this.formatTime(timespan.start, descriptionDateFormat)} - ${this.formatTime(timespan.end, descriptionDateFormat)}`;
if (this.timeSystem.isUTCBased && !this.openmct.time.clock()) {
name = `${startTime} ${millisecondsToDHMS(timespan.end - timespan.start)}`;
} else {
name = description;
}
return {
cssClass: 'icon-history',
name,
description,
onItemClicked: () => this.selectTimespan(timespan)
};
});
history.unshift({
cssClass: 'c-menu__section-hint',
description: 'Past timeframes, ordered by latest first',
isDisabled: true,
name: 'Past timeframes, ordered by latest first',
onItemClicked: () => {}
});
return history;
},
getPresetMenuItems() {
return this.presets.map(preset => {
return {
cssClass: 'icon-clock',
name: preset.label,
description: preset.label,
onItemClicked: () => this.selectPresetBounds(preset.bounds)
};
});
},
getHistoryFromLocalStorage() {
const localStorageHistory = localStorage.getItem(this.storageKey);
const history = localStorageHistory ? JSON.parse(localStorageHistory) : undefined;
this[this.currentHistory] = history;
},
initializeHistoryIfNoHistory() {
if (!this[this.currentHistory]) {
this[this.currentHistory] = {};
this.persistHistoryToLocalStorage();
}
},
persistHistoryToLocalStorage() {
localStorage.setItem(this.storageKey, JSON.stringify(this[this.currentHistory]));
},
addTimespan() {
const key = this.timeSystem.key;
let [...currentHistory] = this[this.currentHistory][key] || [];
const timespan = {
start: this.isFixed ? this.bounds.start : this.offsets.start,
end: this.isFixed ? this.bounds.end : this.offsets.end
};
// no dupes
currentHistory = currentHistory.filter(ts => !(ts.start === timespan.start && ts.end === timespan.end));
currentHistory.unshift(timespan); // add to front
if (currentHistory.length > this.MAX_RECORDS_LENGTH) {
currentHistory.length = this.MAX_RECORDS_LENGTH;
}
this.$set(this[this.currentHistory], key, currentHistory);
this.persistHistoryToLocalStorage();
},
selectTimespan(timespan) {
if (this.isFixed) {
this.openmct.time.bounds(timespan);
} else {
this.openmct.time.clockOffsets(timespan);
}
},
selectPresetBounds(bounds) {
const start = typeof bounds.start === 'function' ? bounds.start() : bounds.start;
const end = typeof bounds.end === 'function' ? bounds.end() : bounds.end;
this.selectTimespan({
start,
end
});
},
loadConfiguration() {
const configurations = this.configuration.menuOptions
.filter(option => option.timeSystem === this.timeSystem.key);
this.presets = this.loadPresets(configurations);
this.MAX_RECORDS_LENGTH = this.loadRecords(configurations);
},
loadPresets(configurations) {
const configuration = configurations.find(option => {
return option.presets && option.name.toLowerCase() === this.mode;
});
const presets = configuration ? configuration.presets : [];
return presets;
},
loadRecords(configurations) {
const configuration = configurations.find(option => option.records);
const maxRecordsLength = configuration ? configuration.records : DEFAULT_RECORDS_LENGTH;
return maxRecordsLength;
},
formatTime(time, utcDateFormat) {
let format = this.timeSystem.timeFormat;
let isNegativeOffset = false;
if (!this.isFixed) {
if (time < 0) {
isNegativeOffset = true;
}
time = Math.abs(time);
format = this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER;
}
const formatter = this.openmct.telemetry.getValueFormatter({
format: format
}).formatter;
let formattedDate;
if (formatter instanceof UTCTimeFormat) {
const formatString = formatter.isValidFormatString(utcDateFormat) ? utcDateFormat : formatter.DATE_FORMATS.PRECISION_SECONDS;
formattedDate = formatter.format(time, formatString);
} else {
formattedDate = formatter.format(time);
}
return (isNegativeOffset ? '-' : '') + formattedDate;
},
showHistoryMenu() {
const elementBoundingClientRect = this.$refs.historyButton.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y;
const menuOptions = {
menuClass: 'c-conductor__history-menu',
placement: this.openmct.menus.menuPlacement.TOP_RIGHT
};
const menuActions = [];
const presets = this.getPresetMenuItems();
if (presets.length) {
menuActions.push(presets);
}
const history = this.getHistoryMenuItems();
menuActions.push(history);
this.openmct.menus.showMenu(x, y, menuActions, menuOptions);
}
mode: {
type: String,
required: true
}
},
data() {
return {
/**
* previous bounds entries available for easy re-use
* @realtimeHistory array of timespans
* @timespans {start, end} number representing timestamp
*/
realtimeHistory: {},
/**
* previous bounds entries available for easy re-use
* @fixedHistory array of timespans
* @timespans {start, end} number representing timestamp
*/
fixedHistory: {},
presets: [],
isFixed: this.openmct.time.clock() === undefined
};
},
computed: {
currentHistory() {
return this.mode + 'History';
},
historyForCurrentTimeSystem() {
const history = this[this.currentHistory][this.timeSystem.key];
return history;
},
storageKey() {
let key = LOCAL_STORAGE_HISTORY_KEY_FIXED;
if (!this.isFixed) {
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();
},
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) => {
let name;
const startTime = this.formatTime(timespan.start);
const description = `${this.formatTime(
timespan.start,
descriptionDateFormat
)} - ${this.formatTime(timespan.end, descriptionDateFormat)}`;
if (this.timeSystem.isUTCBased && !this.openmct.time.clock()) {
name = `${startTime} ${millisecondsToDHMS(timespan.end - timespan.start)}`;
} else {
name = description;
}
return {
cssClass: 'icon-history',
name,
description,
onItemClicked: () => this.selectTimespan(timespan)
};
});
history.unshift({
cssClass: 'c-menu__section-hint',
description: 'Past timeframes, ordered by latest first',
isDisabled: true,
name: 'Past timeframes, ordered by latest first',
onItemClicked: () => {}
});
return history;
},
getPresetMenuItems() {
return this.presets.map((preset) => {
return {
cssClass: 'icon-clock',
name: preset.label,
description: preset.label,
onItemClicked: () => this.selectPresetBounds(preset.bounds)
};
});
},
getHistoryFromLocalStorage() {
const localStorageHistory = localStorage.getItem(this.storageKey);
const history = localStorageHistory ? JSON.parse(localStorageHistory) : undefined;
this[this.currentHistory] = history;
},
initializeHistoryIfNoHistory() {
if (!this[this.currentHistory]) {
this[this.currentHistory] = {};
this.persistHistoryToLocalStorage();
}
},
persistHistoryToLocalStorage() {
localStorage.setItem(this.storageKey, JSON.stringify(this[this.currentHistory]));
},
addTimespan() {
const key = this.timeSystem.key;
let [...currentHistory] = this[this.currentHistory][key] || [];
const timespan = {
start: this.isFixed ? this.bounds.start : this.offsets.start,
end: this.isFixed ? this.bounds.end : this.offsets.end
};
// no dupes
currentHistory = currentHistory.filter(
(ts) => !(ts.start === timespan.start && ts.end === timespan.end)
);
currentHistory.unshift(timespan); // add to front
if (currentHistory.length > this.MAX_RECORDS_LENGTH) {
currentHistory.length = this.MAX_RECORDS_LENGTH;
}
this.$set(this[this.currentHistory], key, currentHistory);
this.persistHistoryToLocalStorage();
},
selectTimespan(timespan) {
if (this.isFixed) {
this.openmct.time.bounds(timespan);
} else {
this.openmct.time.clockOffsets(timespan);
}
},
selectPresetBounds(bounds) {
const start = typeof bounds.start === 'function' ? bounds.start() : bounds.start;
const end = typeof bounds.end === 'function' ? bounds.end() : bounds.end;
this.selectTimespan({
start,
end
});
},
loadConfiguration() {
const configurations = this.configuration.menuOptions.filter(
(option) => option.timeSystem === this.timeSystem.key
);
this.presets = this.loadPresets(configurations);
this.MAX_RECORDS_LENGTH = this.loadRecords(configurations);
},
loadPresets(configurations) {
const configuration = configurations.find((option) => {
return option.presets && option.name.toLowerCase() === this.mode;
});
const presets = configuration ? configuration.presets : [];
return presets;
},
loadRecords(configurations) {
const configuration = configurations.find((option) => option.records);
const maxRecordsLength = configuration ? configuration.records : DEFAULT_RECORDS_LENGTH;
return maxRecordsLength;
},
formatTime(time, utcDateFormat) {
let format = this.timeSystem.timeFormat;
let isNegativeOffset = false;
if (!this.isFixed) {
if (time < 0) {
isNegativeOffset = true;
}
time = Math.abs(time);
format = this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER;
}
const formatter = this.openmct.telemetry.getValueFormatter({
format: format
}).formatter;
let formattedDate;
if (formatter instanceof UTCTimeFormat) {
const formatString = formatter.isValidFormatString(utcDateFormat)
? utcDateFormat
: formatter.DATE_FORMATS.PRECISION_SECONDS;
formattedDate = formatter.format(time, formatString);
} else {
formattedDate = formatter.format(time);
}
return (isNegativeOffset ? '-' : '') + formattedDate;
},
showHistoryMenu() {
const elementBoundingClientRect = this.$refs.historyButton.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y;
const menuOptions = {
menuClass: 'c-conductor__history-menu',
placement: this.openmct.menus.menuPlacement.TOP_RIGHT
};
const menuActions = [];
const presets = this.getPresetMenuItems();
if (presets.length) {
menuActions.push(presets);
}
const history = this.getHistoryMenuItems();
menuActions.push(history);
this.openmct.menus.showMenu(x, y, menuActions, menuOptions);
}
}
};
</script>

View File

@ -20,293 +20,288 @@
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"
/>
<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"
/>
</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"
/>
<!-- Fixed end and RT 'last update' display -->
<div class="c-conductor__end-fixed__label">End</div>
<input
ref="endDate"
v-model="formattedBounds.end"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="
validateAllBounds('endDate');
submitForm();
"
/>
<date-picker
v-if="isUTCBased"
class="c-ctrl-wrapper--menus-left"
:bottom="keyString !== undefined"
:default-date-time="formattedBounds.end"
:formatter="timeFormatter"
@date-selected="endDateSelected"
/>
</div>
</form>
</form>
</template>
<script>
import DatePicker from "./DatePicker.vue";
import _ from "lodash";
import DatePicker from './DatePicker.vue';
import _ from 'lodash';
const DEFAULT_DURATION_FORMATTER = 'duration';
export default {
components: {
DatePicker
components: {
DatePicker
},
inject: ['openmct'],
props: {
keyString: {
type: String,
default() {
return undefined;
}
},
inject: ['openmct'],
props: {
keyString: {
type: String,
default() {
return undefined;
}
},
inputBounds: {
type: Object,
default() {
return undefined;
}
},
objectPath: {
type: Array,
default() {
return [];
}
}
inputBounds: {
type: Object,
default() {
return undefined;
}
},
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();
return {
showTCInputStart: true,
showTCInputEnd: true,
durationFormatter,
timeFormatter,
bounds: {
start: bounds.start,
end: bounds.end
},
formattedBounds: {
start: timeFormatter.format(bounds.start),
end: timeFormatter.format(bounds.end)
},
isUTCBased: timeSystem.isUTCBased
};
},
watch: {
keyString() {
this.setTimeContext();
},
inputBounds: {
handler(newBounds) {
this.handleNewBounds(newBounds);
},
deep: true
}
},
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.setTimeContext();
},
beforeDestroy() {
this.clearAllValidation();
this.openmct.time.off('timeSystem', this.setTimeSystem);
this.stopFollowingTimeContext();
},
methods: {
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.keyString ? this.objectPath : []);
this.handleNewBounds(this.timeContext.bounds());
this.timeContext.on('bounds', this.handleNewBounds);
this.timeContext.on('clock', this.clearAllValidation);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.handleNewBounds);
this.timeContext.off('clock', this.clearAllValidation);
}
},
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);
this.formattedBounds.end = this.timeFormatter.format(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($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);
});
},
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();
}
objectPath: {
type: Array,
default() {
return [];
}
}
},
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();
return {
showTCInputStart: true,
showTCInputEnd: true,
durationFormatter,
timeFormatter,
bounds: {
start: bounds.start,
end: bounds.end
},
formattedBounds: {
start: timeFormatter.format(bounds.start),
end: timeFormatter.format(bounds.end)
},
isUTCBased: timeSystem.isUTCBased
};
},
watch: {
keyString() {
this.setTimeContext();
},
inputBounds: {
handler(newBounds) {
this.handleNewBounds(newBounds);
},
deep: true
}
},
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.setTimeContext();
},
beforeDestroy() {
this.clearAllValidation();
this.openmct.time.off('timeSystem', this.setTimeSystem);
this.stopFollowingTimeContext();
},
methods: {
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.keyString ? this.objectPath : []);
this.handleNewBounds(this.timeContext.bounds());
this.timeContext.on('bounds', this.handleNewBounds);
this.timeContext.on('clock', this.clearAllValidation);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.handleNewBounds);
this.timeContext.off('clock', this.clearAllValidation);
}
},
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);
this.formattedBounds.end = this.timeFormatter.format(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($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);
});
},
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();
}
}
};
</script>

View File

@ -20,310 +20,302 @@
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>
<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>
</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"
>
<!-- RT 'last update' display -->
<div class="c-conductor__end-fixed__label">Current</div>
<input
ref="endDate"
v-model="formattedCurrentValue"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
:disabled="true"
/>
</div>
<div
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-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>
</form>
</form>
</template>
<script>
import timePopup from "./timePopup.vue";
import _ from "lodash";
import timePopup from './timePopup.vue';
import _ from 'lodash';
const DEFAULT_DURATION_FORMATTER = 'duration';
export default {
components: {
timePopup
components: {
timePopup
},
inject: ['openmct'],
props: {
keyString: {
type: String,
default() {
return undefined;
}
},
inject: ['openmct'],
props: {
keyString: {
type: String,
default() {
return undefined;
}
},
objectPath: {
type: Array,
default() {
return [];
}
},
inputBounds: {
type: Object,
default() {
return undefined;
}
}
objectPath: {
type: Array,
default() {
return [];
}
},
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();
let offsets = this.openmct.time.clockOffsets();
let currentValue = this.openmct.time.clock()?.currentValue();
return {
showTCInputStart: false,
showTCInputEnd: false,
durationFormatter,
timeFormatter,
bounds: {
start: bounds.start,
end: bounds.end
},
offsets: {
start: offsets && durationFormatter.format(Math.abs(offsets.start)),
end: offsets && durationFormatter.format(Math.abs(offsets.end))
},
formattedBounds: {
start: timeFormatter.format(bounds.start),
end: timeFormatter.format(bounds.end)
},
currentValue,
formattedCurrentValue: timeFormatter.format(currentValue),
isUTCBased: timeSystem.isUTCBased
};
},
watch: {
keyString() {
this.setTimeContext();
},
inputBounds: {
handler(newBounds) {
this.handleNewBounds(newBounds);
},
deep: true
}
},
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.setTimeContext();
},
beforeDestroy() {
this.openmct.time.off('timeSystem', 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);
},
stopFollowingTime() {
if (this.timeContext) {
this.timeContext.off('bounds', this.handleNewBounds);
this.timeContext.off('clock', this.clearAllValidation);
this.timeContext.off('clockOffsets', this.setViewFromOffsets);
}
},
setTimeContext() {
this.stopFollowingTime();
this.timeContext = this.openmct.time.getContextForView(this.keyString ? 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 = '';
},
setViewFromOffsets(offsets) {
if (offsets) {
this.offsets.start = this.durationFormatter.format(Math.abs(offsets.start));
this.offsets.end = this.durationFormatter.format(Math.abs(offsets.end));
}
},
setBounds(bounds) {
this.bounds = bounds;
},
setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start);
this.formattedBounds.end = this.timeFormatter.format(bounds.end);
},
updateCurrentValue() {
const currentValue = this.openmct.time.clock()?.currentValue();
if (currentValue !== undefined) {
this.setCurrentValue(currentValue);
}
},
setCurrentValue(value) {
this.currentValue = value;
this.formattedCurrentValue = this.timeFormatter.format(value);
},
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;
},
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(':');
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);
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);
});
},
handleValidationResults(input, validationResult) {
if (validationResult.valid !== true) {
input.setCustomValidity(validationResult.message);
input.title = validationResult.message;
} else {
input.setCustomValidity('');
input.title = '';
}
return validationResult.valid;
}
inputBounds: {
type: Object,
default() {
return undefined;
}
}
},
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();
let offsets = this.openmct.time.clockOffsets();
let currentValue = this.openmct.time.clock()?.currentValue();
return {
showTCInputStart: false,
showTCInputEnd: false,
durationFormatter,
timeFormatter,
bounds: {
start: bounds.start,
end: bounds.end
},
offsets: {
start: offsets && durationFormatter.format(Math.abs(offsets.start)),
end: offsets && durationFormatter.format(Math.abs(offsets.end))
},
formattedBounds: {
start: timeFormatter.format(bounds.start),
end: timeFormatter.format(bounds.end)
},
currentValue,
formattedCurrentValue: timeFormatter.format(currentValue),
isUTCBased: timeSystem.isUTCBased
};
},
watch: {
keyString() {
this.setTimeContext();
},
inputBounds: {
handler(newBounds) {
this.handleNewBounds(newBounds);
},
deep: true
}
},
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.setTimeContext();
},
beforeDestroy() {
this.openmct.time.off('timeSystem', 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);
},
stopFollowingTime() {
if (this.timeContext) {
this.timeContext.off('bounds', this.handleNewBounds);
this.timeContext.off('clock', this.clearAllValidation);
this.timeContext.off('clockOffsets', this.setViewFromOffsets);
}
},
setTimeContext() {
this.stopFollowingTime();
this.timeContext = this.openmct.time.getContextForView(this.keyString ? 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 = '';
},
setViewFromOffsets(offsets) {
if (offsets) {
this.offsets.start = this.durationFormatter.format(Math.abs(offsets.start));
this.offsets.end = this.durationFormatter.format(Math.abs(offsets.end));
}
},
setBounds(bounds) {
this.bounds = bounds;
},
setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start);
this.formattedBounds.end = this.timeFormatter.format(bounds.end);
},
updateCurrentValue() {
const currentValue = this.openmct.time.clock()?.currentValue();
if (currentValue !== undefined) {
this.setCurrentValue(currentValue);
}
},
setCurrentValue(value) {
this.currentValue = value;
this.formattedCurrentValue = this.timeFormatter.format(value);
},
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;
},
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(':');
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);
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);
});
},
handleValidationResults(input, validationResult) {
if (validationResult.valid !== true) {
input.setCustomValidity(validationResult.message);
input.title = validationResult.message;
} else {
input.setCustomValidity('');
input.title = '';
}
return validationResult.valid;
}
}
};
</script>

View File

@ -20,163 +20,157 @@
at runtime from the About dialog for additional information.
-->
<template>
<div
ref="modeButton"
class="c-ctrl-wrapper c-ctrl-wrapper--menus-up"
>
<div ref="modeButton" 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-button--menu c-mode-button"
@click.prevent.stop="showModesMenu"
>
<span class="c-button__label">{{ selectedMode.name }}</span>
</button>
<button class="c-button--menu c-mode-button" @click.prevent.stop="showModesMenu">
<span class="c-button__label">{{ selectedMode.name }}</span>
</button>
</div>
</div>
</div>
</template>
<script>
import toggleMixin from '../../ui/mixins/toggle-mixin';
export default {
mixins: [toggleMixin],
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);
}
mixins: [toggleMixin],
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);
}
return {
selectedMode: this.getModeOptionForClock(activeClock),
selectedTimeSystem: JSON.parse(JSON.stringify(this.openmct.time.timeSystem())),
modes: [],
hoveredMode: {}
};
},
mounted: function () {
this.loadClocksFromConfiguration();
this.openmct.time.on('clock', this.setViewFromClock);
},
destroyed: function () {
this.openmct.time.off('clock', this.setViewFromClock);
},
methods: {
showModesMenu() {
const elementBoundingClientRect = this.$refs.modeButton.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y;
const menuOptions = {
menuClass: 'c-conductor__mode-menu',
placement: this.openmct.menus.menuPlacement.TOP_RIGHT
};
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;
}
},
getModeOptionForClock(clock) {
if (clock === undefined) {
const key = 'fixed';
return {
selectedMode: this.getModeOptionForClock(activeClock),
selectedTimeSystem: JSON.parse(JSON.stringify(this.openmct.time.timeSystem())),
modes: [],
hoveredMode: {}
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)
};
}
},
mounted: function () {
this.loadClocksFromConfiguration();
this.openmct.time.on('clock', this.setViewFromClock);
getClock(key) {
return this.openmct.time.getAllClocks().filter(function (clock) {
return clock.key === key;
})[0];
},
destroyed: function () {
this.openmct.time.off('clock', this.setViewFromClock);
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);
}
},
methods: {
showModesMenu() {
const elementBoundingClientRect = this.$refs.modeButton.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y;
const menuOptions = {
menuClass: 'c-conductor__mode-menu',
placement: this.openmct.menus.menuPlacement.TOP_RIGHT
};
this.openmct.menus.showSuperMenu(x, y, this.modes, menuOptions);
getMatchingConfig(options) {
const matchers = {
clock(config) {
return options.clock === config.clock;
},
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;
}
},
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);
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);
}
}
};
</script>

View File

@ -20,8 +20,8 @@
at runtime from the About dialog for additional information.
-->
<template>
<div class="c-clock-symbol">
<div class="c-clock-symbol">
<div class="hand-little"></div>
<div class="hand-big"></div>
</div>
</div>
</template>

View File

@ -20,114 +20,115 @@
at runtime from the About dialog for additional information.
-->
<template>
<div
<div
v-if="selectedTimeSystem.name"
ref="timeSystemButton"
class="c-ctrl-wrapper c-ctrl-wrapper--menus-up"
>
>
<button
class="c-button--menu c-time-system-button"
:class="selectedTimeSystem.cssClass"
@click.prevent.stop="showTimeSystemMenu"
class="c-button--menu c-time-system-button"
:class="selectedTimeSystem.cssClass"
@click.prevent.stop="showTimeSystemMenu"
>
<span class="c-button__label">{{ selectedTimeSystem.name }}</span>
<span class="c-button__label">{{ selectedTimeSystem.name }}</span>
</button>
</div>
</div>
</template>
<script>
export default {
inject: ['openmct', 'configuration'],
data: function () {
inject: ['openmct', 'configuration'],
data: function () {
let activeClock = this.openmct.time.clock();
return {
selectedTimeSystem: JSON.parse(JSON.stringify(this.openmct.time.timeSystem())),
timeSystems: this.getValidTimesystemsForClock(activeClock)
};
},
mounted: function () {
this.openmct.time.on('timeSystem', this.setViewFromTimeSystem);
this.openmct.time.on('clock', this.setViewFromClock);
},
destroyed: function () {
this.openmct.time.off('timeSystem', this.setViewFromTimeSystem);
this.openmct.time.on('clock', this.setViewFromClock);
},
methods: {
showTimeSystemMenu() {
const elementBoundingClientRect = this.$refs.timeSystemButton.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y;
const menuOptions = {
placement: this.openmct.menus.menuPlacement.TOP_RIGHT
};
this.openmct.menus.showMenu(x, y, this.timeSystems, menuOptions);
},
getValidTimesystemsForClock(clock) {
return this.configuration.menuOptions
.filter((menuOption) => menuOption.clock === (clock && clock.key))
.map((menuOption) => {
const timeSystem = JSON.parse(
JSON.stringify(this.openmct.time.timeSystems.get(menuOption.timeSystem))
);
timeSystem.onItemClicked = () => this.setTimeSystemFromView(timeSystem);
return timeSystem;
});
},
setTimeSystemFromView(timeSystem) {
if (timeSystem.key !== this.selectedTimeSystem.key) {
let activeClock = this.openmct.time.clock();
let configuration = this.getMatchingConfig({
clock: activeClock && activeClock.key,
timeSystem: timeSystem.key
});
if (activeClock === undefined) {
let bounds;
return {
selectedTimeSystem: JSON.parse(JSON.stringify(this.openmct.time.timeSystem())),
timeSystems: this.getValidTimesystemsForClock(activeClock)
};
},
mounted: function () {
this.openmct.time.on('timeSystem', this.setViewFromTimeSystem);
this.openmct.time.on('clock', this.setViewFromClock);
},
destroyed: function () {
this.openmct.time.off('timeSystem', this.setViewFromTimeSystem);
this.openmct.time.on('clock', this.setViewFromClock);
},
methods: {
showTimeSystemMenu() {
const elementBoundingClientRect = this.$refs.timeSystemButton.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y;
if (this.selectedTimeSystem.isUTCBased && timeSystem.isUTCBased) {
bounds = this.openmct.time.bounds();
} else {
bounds = configuration.bounds;
}
const menuOptions = {
placement: this.openmct.menus.menuPlacement.TOP_RIGHT
};
this.openmct.menus.showMenu(x, y, this.timeSystems, menuOptions);
},
getValidTimesystemsForClock(clock) {
return this.configuration.menuOptions
.filter(menuOption => menuOption.clock === (clock && clock.key))
.map(menuOption => {
const timeSystem = JSON.parse(JSON.stringify(this.openmct.time.timeSystems.get(menuOption.timeSystem)));
timeSystem.onItemClicked = () => this.setTimeSystemFromView(timeSystem);
return timeSystem;
});
},
setTimeSystemFromView(timeSystem) {
if (timeSystem.key !== this.selectedTimeSystem.key) {
let activeClock = this.openmct.time.clock();
let configuration = this.getMatchingConfig({
clock: activeClock && activeClock.key,
timeSystem: timeSystem.key
});
if (activeClock === undefined) {
let bounds;
if (this.selectedTimeSystem.isUTCBased && timeSystem.isUTCBased) {
bounds = this.openmct.time.bounds();
} else {
bounds = configuration.bounds;
}
this.openmct.time.timeSystem(timeSystem.key, bounds);
} else {
this.openmct.time.timeSystem(timeSystem.key);
this.openmct.time.clockOffsets(configuration.clockOffsets);
}
}
},
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];
},
setViewFromTimeSystem(timeSystem) {
this.selectedTimeSystem = timeSystem;
},
setViewFromClock(clock) {
let activeClock = this.openmct.time.clock();
this.timeSystems = this.getValidTimesystemsForClock(activeClock);
this.openmct.time.timeSystem(timeSystem.key, bounds);
} else {
this.openmct.time.timeSystem(timeSystem.key);
this.openmct.time.clockOffsets(configuration.clockOffsets);
}
}
}
},
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];
},
setViewFromTimeSystem(timeSystem) {
this.selectedTimeSystem = timeSystem;
},
setViewFromClock(clock) {
let activeClock = this.openmct.time.clock();
this.timeSystems = this.getValidTimesystemsForClock(activeClock);
}
}
};
</script>

View File

@ -20,69 +20,54 @@
at runtime from the About dialog for additional information.
-->
<template>
<div
<div
ref="calendarHolder"
class="c-ctrl-wrapper c-datetime-picker__wrapper"
:class="{'c-ctrl-wrapper--menus-up': bottom !== true, 'c-ctrl-wrapper--menus-down': bottom === true}"
>
<a
class="c-icon-button icon-calendar"
@click="toggle"
></a>
<div
v-if="open"
class="c-menu c-menu--mobile-modal c-datetime-picker"
>
<div class="c-datetime-picker__close-button">
<button
class="c-click-icon icon-x-in-circle"
@click="toggle"
></button>
</div>
<div class="c-datetime-picker__pager c-pager l-month-year-pager">
<div
class="c-pager__prev c-icon-button icon-arrow-left"
@click.stop="changeMonth(-1)"
></div>
<div class="c-pager__month-year">
{{ model.month }} {{ model.year }}
:class="{
'c-ctrl-wrapper--menus-up': bottom !== true,
'c-ctrl-wrapper--menus-down': bottom === true
}"
>
<a class="c-icon-button icon-calendar" @click="toggle"></a>
<div v-if="open" class="c-menu c-menu--mobile-modal c-datetime-picker">
<div class="c-datetime-picker__close-button">
<button class="c-click-icon icon-x-in-circle" @click="toggle"></button>
</div>
<div class="c-datetime-picker__pager c-pager l-month-year-pager">
<div
class="c-pager__prev c-icon-button icon-arrow-left"
@click.stop="changeMonth(-1)"
></div>
<div class="c-pager__month-year">{{ model.month }} {{ model.year }}</div>
<div
class="c-pager__next c-icon-button icon-arrow-right"
@click.stop="changeMonth(1)"
></div>
</div>
<div class="c-datetime-picker__calendar c-calendar">
<ul class="c-calendar__row--header l-cal-row">
<li v-for="day in ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']" :key="day">
{{ day }}
</li>
</ul>
<ul v-for="(row, tableIndex) in table" :key="tableIndex" class="c-calendar__row--body">
<li
v-for="(cell, rowIndex) in row"
:key="rowIndex"
:class="{ 'is-in-month': isInCurrentMonth(cell), selected: isSelected(cell) }"
@click="select(cell)"
>
<div class="c-calendar__day--prime">
{{ cell.day }}
</div>
<div
class="c-pager__next c-icon-button icon-arrow-right"
@click.stop="changeMonth(1)"
></div>
</div>
<div class="c-datetime-picker__calendar c-calendar">
<ul class="c-calendar__row--header l-cal-row">
<li
v-for="day in ['Su','Mo','Tu','We','Th','Fr','Sa']"
:key="day"
>
{{ day }}
</li>
</ul>
<ul
v-for="(row, tableIndex) in table"
:key="tableIndex"
class="c-calendar__row--body"
>
<li
v-for="(cell, rowIndex) in row"
:key="rowIndex"
:class="{ 'is-in-month': isInCurrentMonth(cell), selected: isSelected(cell) }"
@click="select(cell)"
>
<div class="c-calendar__day--prime">
{{ cell.day }}
</div>
<div class="c-calendar__day--sub">
{{ cell.dayOfYear }}
</div>
</li>
</ul>
</div>
<div class="c-calendar__day--sub">
{{ cell.dayOfYear }}
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
@ -90,184 +75,182 @@ import moment from 'moment';
import toggleMixin from '../../ui/mixins/toggle-mixin';
const TIME_NAMES = {
'hours': "Hour",
'minutes': "Minute",
'seconds': "Second"
hours: 'Hour',
minutes: 'Minute',
seconds: 'Second'
};
const MONTHS = moment.months();
const TIME_OPTIONS = (function makeRanges() {
let arr = [];
while (arr.length < 60) {
arr.push(arr.length);
}
let arr = [];
while (arr.length < 60) {
arr.push(arr.length);
}
return {
hours: arr.slice(0, 24),
minutes: arr,
seconds: arr
};
}());
return {
hours: arr.slice(0, 24),
minutes: arr,
seconds: arr
};
})();
export default {
mixins: [toggleMixin],
inject: ['openmct'],
props: {
defaultDateTime: {
type: String,
default: undefined
},
formatter: {
type: Object,
required: true
},
bottom: {
type: Boolean,
default() {
return false;
}
}
mixins: [toggleMixin],
inject: ['openmct'],
props: {
defaultDateTime: {
type: String,
default: undefined
},
data: function () {
return {
picker: {
year: undefined,
month: undefined,
interacted: false
},
model: {
year: undefined,
month: undefined
},
table: undefined,
date: undefined,
time: undefined
};
formatter: {
type: Object,
required: true
},
watch: {
defaultDateTime() {
this.updateFromModel(this.defaultDateTime);
}
},
mounted: function () {
this.updateFromModel(this.defaultDateTime);
this.updateViewForMonth();
},
methods: {
generateTable() {
let m = moment.utc({
year: this.picker.year,
month: this.picker.month
}).day(0);
let table = [];
let row;
let col;
for (row = 0; row < 6; row += 1) {
table.push([]);
for (col = 0; col < 7; col += 1) {
table[row].push({
year: m.year(),
month: m.month(),
day: m.date(),
dayOfYear: m.dayOfYear()
});
m.add(1, 'days'); // Next day!
}
}
return table;
},
updateViewForMonth() {
this.model.month = MONTHS[this.picker.month];
this.model.year = this.picker.year;
this.table = this.generateTable();
},
updateFromModel(defaultDateTime) {
let m = moment.utc(defaultDateTime);
this.date = {
year: m.year(),
month: m.month(),
day: m.date()
};
this.time = {
hours: m.hour(),
minutes: m.minute(),
seconds: m.second()
};
// Zoom to that date in the picker, but
// only if the user hasn't interacted with it yet.
if (!this.picker.interacted) {
this.picker.year = m.year();
this.picker.month = m.month();
this.updateViewForMonth();
}
},
updateFromView() {
let m = moment.utc({
year: this.date.year,
month: this.date.month,
day: this.date.day,
hour: this.time.hours,
minute: this.time.minutes,
second: this.time.seconds
});
this.$emit('date-selected', m.valueOf());
},
isInCurrentMonth(cell) {
return cell.month === this.picker.month;
},
isSelected(cell) {
let date = this.date || {};
return cell.day === date.day
&& cell.month === date.month
&& cell.year === date.year;
},
select(cell) {
this.date = this.date || {};
this.date.month = cell.month;
this.date.year = cell.year;
this.date.day = cell.day;
this.updateFromView();
},
dateEquals(d1, d2) {
return d1.year === d2.year
&& d1.month === d2.month
&& d1.day === d2.day;
},
changeMonth(delta) {
this.picker.month += delta;
if (this.picker.month > 11) {
this.picker.month = 0;
this.picker.year += 1;
}
if (this.picker.month < 0) {
this.picker.month = 11;
this.picker.year -= 1;
}
this.picker.interacted = true;
this.updateViewForMonth();
},
nameFor(key) {
return TIME_NAMES[key];
},
optionsFor(key) {
return TIME_OPTIONS[key];
}
bottom: {
type: Boolean,
default() {
return false;
}
}
},
data: function () {
return {
picker: {
year: undefined,
month: undefined,
interacted: false
},
model: {
year: undefined,
month: undefined
},
table: undefined,
date: undefined,
time: undefined
};
},
watch: {
defaultDateTime() {
this.updateFromModel(this.defaultDateTime);
}
},
mounted: function () {
this.updateFromModel(this.defaultDateTime);
this.updateViewForMonth();
},
methods: {
generateTable() {
let m = moment
.utc({
year: this.picker.year,
month: this.picker.month
})
.day(0);
let table = [];
let row;
let col;
for (row = 0; row < 6; row += 1) {
table.push([]);
for (col = 0; col < 7; col += 1) {
table[row].push({
year: m.year(),
month: m.month(),
day: m.date(),
dayOfYear: m.dayOfYear()
});
m.add(1, 'days'); // Next day!
}
}
return table;
},
updateViewForMonth() {
this.model.month = MONTHS[this.picker.month];
this.model.year = this.picker.year;
this.table = this.generateTable();
},
updateFromModel(defaultDateTime) {
let m = moment.utc(defaultDateTime);
this.date = {
year: m.year(),
month: m.month(),
day: m.date()
};
this.time = {
hours: m.hour(),
minutes: m.minute(),
seconds: m.second()
};
// Zoom to that date in the picker, but
// only if the user hasn't interacted with it yet.
if (!this.picker.interacted) {
this.picker.year = m.year();
this.picker.month = m.month();
this.updateViewForMonth();
}
},
updateFromView() {
let m = moment.utc({
year: this.date.year,
month: this.date.month,
day: this.date.day,
hour: this.time.hours,
minute: this.time.minutes,
second: this.time.seconds
});
this.$emit('date-selected', m.valueOf());
},
isInCurrentMonth(cell) {
return cell.month === this.picker.month;
},
isSelected(cell) {
let date = this.date || {};
return cell.day === date.day && cell.month === date.month && cell.year === date.year;
},
select(cell) {
this.date = this.date || {};
this.date.month = cell.month;
this.date.year = cell.year;
this.date.day = cell.day;
this.updateFromView();
},
dateEquals(d1, d2) {
return d1.year === d2.year && d1.month === d2.month && d1.day === d2.day;
},
changeMonth(delta) {
this.picker.month += delta;
if (this.picker.month > 11) {
this.picker.month = 0;
this.picker.year += 1;
}
if (this.picker.month < 0) {
this.picker.month = 11;
this.picker.year -= 1;
}
this.picker.interacted = true;
this.updateViewForMonth();
},
nameFor(key) {
return TIME_NAMES[key];
},
optionsFor(key) {
return TIME_OPTIONS[key];
}
}
};
</script>

View File

@ -1,67 +1,67 @@
@use 'sass:math';
.c-conductor-axis {
$h: 18px;
$tickYPos: math.div($h, 2) + 12px;
$h: 18px;
$tickYPos: math.div($h, 2) + 12px;
@include userSelectNone();
@include bgTicks($c: rgba($colorBodyFg, 0.4));
background-position: 0 50%;
background-size: 5px 2px;
border-radius: $controlCr;
height: $h;
@include userSelectNone();
@include bgTicks($c: rgba($colorBodyFg, 0.4));
background-position: 0 50%;
background-size: 5px 2px;
border-radius: $controlCr;
height: $h;
svg {
text-rendering: geometricPrecision;
width: 100%;
height: 100%;
> g.axis {
// Overall Tick holder
transform: translateY($tickYPos);
path {
// Domain line
display: none;
}
svg {
text-rendering: geometricPrecision;
width: 100%;
height: 100%;
> g.axis {
// Overall Tick holder
transform: translateY($tickYPos);
path {
// Domain line
display: none;
}
g {
// Each tick. These move on drag.
line {
// Line beneath ticks
display: none;
}
}
}
text {
// Tick labels
fill: $colorBodyFg;
font-size: 1em;
paint-order: stroke;
font-weight: bold;
stroke: $colorBodyBg;
stroke-linecap: butt;
stroke-linejoin: bevel;
stroke-width: 6px;
g {
// Each tick. These move on drag.
line {
// Line beneath ticks
display: none;
}
}
}
body.desktop .is-fixed-mode & {
background-size: 3px 30%;
background-color: $colorBodyBgSubtle;
box-shadow: inset rgba(black, 0.4) 0 1px 1px;
svg text {
fill: $colorBodyFg;
stroke: $colorBodyBgSubtle;
}
text {
// Tick labels
fill: $colorBodyFg;
font-size: 1em;
paint-order: stroke;
font-weight: bold;
stroke: $colorBodyBg;
stroke-linecap: butt;
stroke-linejoin: bevel;
stroke-width: 6px;
}
}
.is-realtime-mode & {
$c: 1px solid rgba($colorTime, 0.7);
border-left: $c;
border-right: $c;
svg text {
fill: $colorTime;
}
body.desktop .is-fixed-mode & {
background-size: 3px 30%;
background-color: $colorBodyBgSubtle;
box-shadow: inset rgba(black, 0.4) 0 1px 1px;
svg text {
fill: $colorBodyFg;
stroke: $colorBodyBgSubtle;
}
}
.is-realtime-mode & {
$c: 1px solid rgba($colorTime, 0.7);
border-left: $c;
border-right: $c;
svg text {
fill: $colorTime;
}
}
}

View File

@ -1,107 +1,160 @@
@keyframes clock-hands {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
@keyframes clock-hands-sticky {
0% { transform: translate(-50%, -50%) rotate(0deg); }
7% { transform: translate(-50%, -50%) rotate(0deg); }
8% { transform: translate(-50%, -50%) rotate(30deg); }
15% { transform: translate(-50%, -50%) rotate(30deg); }
16% { transform: translate(-50%, -50%) rotate(60deg); }
24% { transform: translate(-50%, -50%) rotate(60deg); }
25% { transform: translate(-50%, -50%) rotate(90deg); }
32% { transform: translate(-50%, -50%) rotate(90deg); }
33% { transform: translate(-50%, -50%) rotate(120deg); }
40% { transform: translate(-50%, -50%) rotate(120deg); }
41% { transform: translate(-50%, -50%) rotate(150deg); }
49% { transform: translate(-50%, -50%) rotate(150deg); }
50% { transform: translate(-50%, -50%) rotate(180deg); }
57% { transform: translate(-50%, -50%) rotate(180deg); }
58% { transform: translate(-50%, -50%) rotate(210deg); }
65% { transform: translate(-50%, -50%) rotate(210deg); }
66% { transform: translate(-50%, -50%) rotate(240deg); }
74% { transform: translate(-50%, -50%) rotate(240deg); }
75% { transform: translate(-50%, -50%) rotate(270deg); }
82% { transform: translate(-50%, -50%) rotate(270deg); }
83% { transform: translate(-50%, -50%) rotate(300deg); }
90% { transform: translate(-50%, -50%) rotate(300deg); }
91% { transform: translate(-50%, -50%) rotate(330deg); }
99% { transform: translate(-50%, -50%) rotate(330deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
7% {
transform: translate(-50%, -50%) rotate(0deg);
}
8% {
transform: translate(-50%, -50%) rotate(30deg);
}
15% {
transform: translate(-50%, -50%) rotate(30deg);
}
16% {
transform: translate(-50%, -50%) rotate(60deg);
}
24% {
transform: translate(-50%, -50%) rotate(60deg);
}
25% {
transform: translate(-50%, -50%) rotate(90deg);
}
32% {
transform: translate(-50%, -50%) rotate(90deg);
}
33% {
transform: translate(-50%, -50%) rotate(120deg);
}
40% {
transform: translate(-50%, -50%) rotate(120deg);
}
41% {
transform: translate(-50%, -50%) rotate(150deg);
}
49% {
transform: translate(-50%, -50%) rotate(150deg);
}
50% {
transform: translate(-50%, -50%) rotate(180deg);
}
57% {
transform: translate(-50%, -50%) rotate(180deg);
}
58% {
transform: translate(-50%, -50%) rotate(210deg);
}
65% {
transform: translate(-50%, -50%) rotate(210deg);
}
66% {
transform: translate(-50%, -50%) rotate(240deg);
}
74% {
transform: translate(-50%, -50%) rotate(240deg);
}
75% {
transform: translate(-50%, -50%) rotate(270deg);
}
82% {
transform: translate(-50%, -50%) rotate(270deg);
}
83% {
transform: translate(-50%, -50%) rotate(300deg);
}
90% {
transform: translate(-50%, -50%) rotate(300deg);
}
91% {
transform: translate(-50%, -50%) rotate(330deg);
}
99% {
transform: translate(-50%, -50%) rotate(330deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
.c-clock-symbol {
$c: $colorBtnBg; //$colorObjHdrIc;
$d: 18px;
height: $d;
width: $d;
position: relative;
$c: $colorBtnBg; //$colorObjHdrIc;
$d: 18px;
height: $d;
width: $d;
position: relative;
&:before {
font-family: symbolsfont;
color: $c;
content: $glyph-icon-brackets;
font-size: $d;
line-height: normal;
display: block;
width: 100%;
height: 100%;
z-index: 1;
}
// Clock hands
div[class*='hand'] {
$handW: 2px;
$handH: $d * 0.4;
animation-iteration-count: infinite;
animation-timing-function: steps(12);
transform-origin: bottom;
position: absolute;
height: $handW;
width: $handW;
left: 50%;
top: 50%;
z-index: 2;
&:before {
font-family: symbolsfont;
color: $c;
content: $glyph-icon-brackets;
font-size: $d;
line-height: normal;
display: block;
width: 100%;
height: 100%;
z-index: 1;
background: $c;
content: '';
display: block;
position: absolute;
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;
}
}
}
// Clock hands
div[class*="hand"] {
$handW: 2px;
$handH: $d * 0.4;
animation-iteration-count: infinite;
animation-timing-function: steps(12);
transform-origin: bottom;
position: absolute;
height: $handW;
width: $handW;
left: 50%;
top: 50%;
z-index: 2;
&:before {
background: $c;
content: '';
display: block;
position: absolute;
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;
}
}
// Modes
.is-realtime-mode &,
.is-lad-mode & {
&:before {
// Brackets icon
color: $colorTime;
}
// Modes
.is-realtime-mode &,
.is-lad-mode & {
&:before {
// Brackets icon
color: $colorTime;
}
div[class*="hand"] {
animation-name: clock-hands;
&:before {
background: $colorTime;
}
}
div[class*='hand'] {
animation-name: clock-hands;
&:before {
background: $colorTime;
}
}
}
}

View File

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

View File

@ -1,310 +1,310 @@
.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: none;
height: 0;
width: 0;
padding: 0;
}
/*********************************************** CONDUCTOR LAYOUT */
.c-conductor {
&__inputs {
display: contents;
}
&__inputs {
display: contents;
}
&__time-bounds {
display: grid;
grid-column-gap: $interiorMargin;
grid-row-gap: $interiorMargin;
align-items: center;
&__time-bounds {
display: grid;
grid-column-gap: $interiorMargin;
grid-row-gap: $interiorMargin;
align-items: center;
// 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";
}
// 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';
}
&__mode-icon {
grid-area: tc-mode-icon;
}
&__mode-icon {
grid-area: tc-mode-icon;
}
&__start-fixed,
&__start-delta {
grid-area: tc-start;
display: flex;
}
&__start-fixed,
&__start-delta {
grid-area: tc-start;
display: flex;
}
&__end-fixed,
&__end-delta {
grid-area: tc-end;
display: flex;
justify-content: flex-end;
}
&__end-fixed,
&__end-delta {
grid-area: tc-end;
display: flex;
justify-content: flex-end;
}
&__ticks {
grid-area: tc-ticks;
}
&__ticks {
grid-area: tc-ticks;
}
&__controls {
grid-area: tc-controls;
display: flex;
align-items: center;
> * + * {
margin-left: $interiorMargin;
}
}
&.is-fixed-mode {
.c-conductor-axis {
&__zoom-indicator {
border: 1px solid transparent;
display: none; // Hidden by default
}
}
&:not(.is-panning),
&:not(.is-zooming) {
.c-conductor-axis {
&:hover,
&:active {
cursor: col-resize;
}
}
}
&.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;
}
}
}
&.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;
}
}
[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;
}
}
}
}
.c-conductor-holder--compact {
min-height: 22px;
.c-conductor {
&__inputs,
&__time-bounds {
display: flex;
.c-toggle-switch {
// Used in independent Time Conductor
flex: 0 0 auto;
}
}
&__inputs {
> * + * {
margin-left: $interiorMarginSm;
}
}
}
.is-realtime-mode .c-conductor__end-fixed {
display: none !important;
}
}
.c-conductor-input {
color: $colorInputFg;
&__controls {
grid-area: tc-controls;
display: flex;
align-items: center;
justify-content: flex-start;
> * + * {
margin-left: $interiorMarginSm;
margin-left: $interiorMargin;
}
}
&.is-fixed-mode {
.c-conductor-axis {
&__zoom-indicator {
border: 1px solid transparent;
display: none; // Hidden by default
}
}
&:before {
// Realtime-mode clock icon symbol
margin-right: $interiorMarginSm;
}
.c-direction-indicator {
// Holds realtime-mode + and - symbols
font-size: 0.7em;
}
input:invalid {
background: rgba($colorFormInvalid, 0.5);
}
}
.is-realtime-mode {
.c-conductor__controls button,
.c-conductor__delta-button {
@include themedButton($colorTimeBg);
color: $colorTimeFg;
}
.c-conductor-input {
&:before {
color: $colorTime;
&:not(.is-panning),
&:not(.is-zooming) {
.c-conductor-axis {
&:hover,
&:active {
cursor: col-resize;
}
}
}
&.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;
}
}
}
&.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 {
// Displays last RT udpate
color: $colorTime;
input {
// Remove input look
background: none;
box-shadow: none;
color: $colorTime;
pointer-events: none;
&[disabled] {
opacity: 1 !important;
}
}
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;
}
}
[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;
}
}
}
}
.c-conductor-holder--compact {
min-height: 22px;
.c-conductor {
&__inputs,
&__time-bounds {
display: flex;
.c-toggle-switch {
// Used in independent Time Conductor
flex: 0 0 auto;
}
}
&__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: $interiorMarginSm;
}
&:before {
// Realtime-mode clock icon symbol
margin-right: $interiorMarginSm;
}
.c-direction-indicator {
// Holds realtime-mode + and - symbols
font-size: 0.7em;
}
input:invalid {
background: rgba($colorFormInvalid, 0.5);
}
}
.is-realtime-mode {
.c-conductor__controls button,
.c-conductor__delta-button {
@include themedButton($colorTimeBg);
color: $colorTimeFg;
}
.c-conductor-input {
&:before {
color: $colorTime;
}
}
.c-conductor__end-fixed {
// Displays last RT udpate
color: $colorTime;
input {
// Remove input look
background: none;
box-shadow: none;
color: $colorTime;
pointer-events: none;
&[disabled] {
opacity: 1 !important;
}
}
}
}
[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;
// 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;
&[class*='--bottom'] {
bottom: auto;
top: 24px;
}
&[class*='--bottom'] {
bottom: auto;
top: 24px;
}
}
.l-shell__time-conductor .pr-tc-input-menu--end {
left: auto;
right: 0;
left: auto;
right: 0;
}
[class^='pr-time'] {
&[class*='label'] {
font-size: 0.8em;
opacity: 0.6;
text-transform: uppercase;
}
&[class*='label'] {
font-size: 0.8em;
opacity: 0.6;
text-transform: uppercase;
}
&[class*='controls'] {
display: flex;
align-items: center;
white-space: nowrap;
&[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;
}
input {
height: 22px;
line-height: 22px;
margin-right: $interiorMarginSm;
font-size: 1.25em;
width: 42px;
}
}
}

View File

@ -1,101 +1,101 @@
/******************************************************** PICKER */
.c-datetime-picker {
@include userSelectNone();
padding: $interiorMarginLg !important;
display: flex !important; // Override .c-menu display: block;
flex-direction: column;
> * + * {
margin-top: $interiorMargin;
}
@include userSelectNone();
padding: $interiorMarginLg !important;
display: flex !important; // Override .c-menu display: block;
flex-direction: column;
> * + * {
margin-top: $interiorMargin;
}
&__close-button {
display: none; // Only show when body.phone, see below.
}
&__close-button {
display: none; // Only show when body.phone, see below.
}
&__pager {
flex: 0 0 auto;
}
&__pager {
flex: 0 0 auto;
}
&__calendar {
border-top: 1px solid $colorInteriorBorder;
flex: 1 1 auto;
}
&__calendar {
border-top: 1px solid $colorInteriorBorder;
flex: 1 1 auto;
}
}
.c-pager {
display: grid;
grid-column-gap: $interiorMargin;
grid-template-rows: 1fr;
grid-template-columns: auto 1fr auto;
align-items: center;
display: grid;
grid-column-gap: $interiorMargin;
grid-template-rows: 1fr;
grid-template-columns: auto 1fr auto;
align-items: center;
.c-icon-button {
font-size: 0.8em;
}
.c-icon-button {
font-size: 0.8em;
}
&__month-year {
text-align: center;
}
&__month-year {
text-align: center;
}
}
/******************************************************** CALENDAR */
.c-calendar {
display: grid;
grid-template-columns: repeat(7, min-content);
grid-template-rows: auto;
grid-gap: 1px;
height: 100%;
display: grid;
grid-template-columns: repeat(7, min-content);
grid-template-rows: auto;
grid-gap: 1px;
height: 100%;
$mutedOpacity: 0.5;
$mutedOpacity: 0.5;
ul {
display: contents;
&[class*='--header'] {
pointer-events: none;
li {
opacity: $mutedOpacity;
}
}
ul {
display: contents;
&[class*='--header'] {
pointer-events: none;
li {
opacity: $mutedOpacity;
}
}
}
li {
display: flex;
flex-direction: column;
justify-content: center !important;
padding: $interiorMargin;
&.is-in-month {
background: $colorMenuElementHilite;
}
li {
display: flex;
flex-direction: column;
justify-content: center !important;
padding: $interiorMargin;
&.is-in-month {
background: $colorMenuElementHilite;
}
&.selected {
background: $colorKey;
color: $colorKeyFg;
}
&.selected {
background: $colorKey;
color: $colorKeyFg;
}
}
&__day {
&--sub {
opacity: $mutedOpacity;
font-size: 0.8em;
}
&__day {
&--sub {
opacity: $mutedOpacity;
font-size: 0.8em;
}
}
}
/******************************************************** MOBILE */
body.phone {
.c-datetime-picker {
&.c-menu {
@include modalFullScreen();
}
&__close-button {
display: flex;
justify-content: flex-end;
}
.c-datetime-picker {
&.c-menu {
@include modalFullScreen();
}
.c-calendar {
grid-template-columns: repeat(7, auto);
&__close-button {
display: flex;
justify-content: flex-end;
}
}
.c-calendar {
grid-template-columns: repeat(7, auto);
}
}

View File

@ -20,237 +20,242 @@
at runtime from the About dialog for additional information.
-->
<template>
<div
<div
class="c-conductor"
:class="[
isFixed ? 'is-fixed-mode' : independentTCEnabled ? 'is-realtime-mode' : 'is-fixed-mode'
isFixed ? 'is-fixed-mode' : independentTCEnabled ? 'is-realtime-mode' : 'is-fixed-mode'
]"
>
>
<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"
:checked="independentTCEnabled"
:title="`${independentTCEnabled ? 'Disable' : 'Enable'} independent Time Conductor`"
@change="toggleIndependentTC"
/>
<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"
/>
<ConductorModeIcon />
<conductor-inputs-fixed
v-if="isFixed"
:key-string="domainObject.identifier.key"
:object-path="objectPath"
@updated="saveFixedOffsets"
/>
<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="isFixed"
:key-string="domainObject.identifier.key"
:object-path="objectPath"
@updated="saveFixedOffsets"
/>
<conductor-inputs-realtime
v-else
:key-string="domainObject.identifier.key"
:object-path="objectPath"
@updated="saveClockOffsets"
/>
</div>
<conductor-inputs-realtime
v-else
:key-string="domainObject.identifier.key"
:object-path="objectPath"
@updated="saveClockOffsets"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import ConductorInputsFixed from "../ConductorInputsFixed.vue";
import ConductorInputsRealtime from "../ConductorInputsRealtime.vue";
import ConductorModeIcon from "@/plugins/timeConductor/ConductorModeIcon.vue";
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 Mode from './Mode.vue';
export default {
components: {
Mode,
ConductorModeIcon,
ConductorInputsRealtime,
ConductorInputsFixed,
ToggleSwitch
components: {
Mode,
ConductorModeIcon,
ConductorInputsRealtime,
ConductorInputsFixed,
ToggleSwitch
},
inject: ['openmct'],
props: {
domainObject: {
type: Object,
required: true
},
inject: ['openmct'],
props: {
domainObject: {
type: Object,
required: true
},
objectPath: {
type: Array,
required: true
}
},
data() {
return {
timeOptions: this.domainObject.configuration.timeOptions || {
clockOffsets: this.openmct.time.clockOffsets(),
fixedOffsets: this.openmct.time.bounds()
},
mode: undefined,
independentTCEnabled: this.domainObject.configuration.useIndependentTime === true
};
},
computed: {
isFixed() {
if (!this.mode || !this.mode.key) {
return this.openmct.time.clock() === undefined;
} else {
return this.mode.key === 'fixed';
}
}
},
watch: {
domainObject: {
handler(domainObject) {
const key = this.openmct.objects.makeKeyString(domainObject.identifier);
if (key !== this.keyString) {
//domain object has changed
this.destroyIndependentTime();
this.independentTCEnabled = domainObject.configuration.useIndependentTime === true;
this.timeOptions = domainObject.configuration.timeOptions || {
clockOffsets: this.openmct.time.clockOffsets(),
fixedOffsets: this.openmct.time.bounds()
};
this.initialize();
}
},
deep: true
}
},
mounted() {
this.initialize();
},
beforeDestroy() {
this.stopFollowingTimeContext();
this.destroyIndependentTime();
},
methods: {
initialize() {
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.destroyIndependentTime();
}
this.$emit('stateChanged', this.independentTCEnabled);
},
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on('clock', this.setTimeOptions);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('clock', this.setTimeOptions);
}
},
setTimeOptions(clock) {
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();
}
},
saveFixedOffsets(offsets) {
const newOptions = Object.assign({}, this.timeOptions, {
fixedOffsets: offsets
});
this.updateTimeOptions(newOptions);
},
saveClockOffsets(offsets) {
const newOptions = Object.assign({}, this.timeOptions, {
clockOffsets: offsets
});
this.updateTimeOptions(newOptions);
},
saveMode(mode) {
this.mode = mode;
const newOptions = Object.assign({}, this.timeOptions, {
mode: this.mode
});
this.updateTimeOptions(newOptions);
},
updateTimeOptions(options) {
this.timeOptions = options;
if (!this.timeOptions.mode) {
this.timeOptions.mode = this.mode;
}
this.registerIndependentTimeOffsets();
this.$emit('updated', this.timeOptions);
},
registerIndependentTimeOffsets() {
if (!this.timeOptions.mode) {
return;
}
let offsets;
if (this.isFixed) {
offsets = this.timeOptions.fixedOffsets;
} else {
if (this.timeOptions.clockOffsets === undefined) {
this.timeOptions.clockOffsets = this.openmct.time.clockOffsets();
}
offsets = this.timeOptions.clockOffsets;
}
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);
} else {
if (this.isFixed) {
timeContext.stopClock();
timeContext.bounds(offsets);
} else {
timeContext.clock(this.mode.key, offsets);
}
}
},
destroyIndependentTime() {
if (this.unregisterIndependentTime) {
this.unregisterIndependentTime();
}
}
objectPath: {
type: Array,
required: true
}
},
data() {
return {
timeOptions: this.domainObject.configuration.timeOptions || {
clockOffsets: this.openmct.time.clockOffsets(),
fixedOffsets: this.openmct.time.bounds()
},
mode: undefined,
independentTCEnabled: this.domainObject.configuration.useIndependentTime === true
};
},
computed: {
isFixed() {
if (!this.mode || !this.mode.key) {
return this.openmct.time.clock() === undefined;
} else {
return this.mode.key === 'fixed';
}
}
},
watch: {
domainObject: {
handler(domainObject) {
const key = this.openmct.objects.makeKeyString(domainObject.identifier);
if (key !== this.keyString) {
//domain object has changed
this.destroyIndependentTime();
this.independentTCEnabled = domainObject.configuration.useIndependentTime === true;
this.timeOptions = domainObject.configuration.timeOptions || {
clockOffsets: this.openmct.time.clockOffsets(),
fixedOffsets: this.openmct.time.bounds()
};
this.initialize();
}
},
deep: true
}
},
mounted() {
this.initialize();
},
beforeDestroy() {
this.stopFollowingTimeContext();
this.destroyIndependentTime();
},
methods: {
initialize() {
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.destroyIndependentTime();
}
this.$emit('stateChanged', this.independentTCEnabled);
},
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on('clock', this.setTimeOptions);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('clock', this.setTimeOptions);
}
},
setTimeOptions(clock) {
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();
}
},
saveFixedOffsets(offsets) {
const newOptions = Object.assign({}, this.timeOptions, {
fixedOffsets: offsets
});
this.updateTimeOptions(newOptions);
},
saveClockOffsets(offsets) {
const newOptions = Object.assign({}, this.timeOptions, {
clockOffsets: offsets
});
this.updateTimeOptions(newOptions);
},
saveMode(mode) {
this.mode = mode;
const newOptions = Object.assign({}, this.timeOptions, {
mode: this.mode
});
this.updateTimeOptions(newOptions);
},
updateTimeOptions(options) {
this.timeOptions = options;
if (!this.timeOptions.mode) {
this.timeOptions.mode = this.mode;
}
this.registerIndependentTimeOffsets();
this.$emit('updated', this.timeOptions);
},
registerIndependentTimeOffsets() {
if (!this.timeOptions.mode) {
return;
}
let offsets;
if (this.isFixed) {
offsets = this.timeOptions.fixedOffsets;
} else {
if (this.timeOptions.clockOffsets === undefined) {
this.timeOptions.clockOffsets = this.openmct.time.clockOffsets();
}
offsets = this.timeOptions.clockOffsets;
}
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
);
} else {
if (this.isFixed) {
timeContext.stopClock();
timeContext.bounds(offsets);
} else {
timeContext.clock(this.mode.key, offsets);
}
}
},
destroyIndependentTime() {
if (this.unregisterIndependentTime) {
this.unregisterIndependentTime();
}
}
}
};
</script>

View File

@ -20,206 +20,212 @@
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 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>
<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>
</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;
}
}
mixins: [toggleMixin],
inject: ['openmct'],
props: {
mode: {
type: Object,
default() {
return undefined;
}
},
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();
}
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);
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 {
selectedMode: this.getModeOptionForClock(clock),
modes: []
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)
};
}
},
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);
}
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

@ -23,101 +23,110 @@
import Conductor from './Conductor.vue';
function isTruthy(a) {
return Boolean(a);
return Boolean(a);
}
function validateMenuOption(menuOption, index) {
if (menuOption.clock && !menuOption.clockOffsets) {
return `Conductor menu option is missing required property 'clockOffsets'. This field is required when configuring a menu option with a clock.\r\n${JSON.stringify(menuOption)}`;
}
if (menuOption.clock && !menuOption.clockOffsets) {
return `Conductor menu option is missing required property 'clockOffsets'. This field is required when configuring a menu option with a clock.\r\n${JSON.stringify(
menuOption
)}`;
}
if (!menuOption.timeSystem) {
return `Conductor menu option is missing required property 'timeSystem'\r\n${JSON.stringify(menuOption)}`;
}
if (!menuOption.timeSystem) {
return `Conductor menu option is missing required property 'timeSystem'\r\n${JSON.stringify(
menuOption
)}`;
}
if (!menuOption.bounds && !menuOption.clock) {
return `Conductor menu option is missing required property 'bounds'. This field is required when configuring a menu option with fixed bounds.\r\n${JSON.stringify(menuOption)}`;
}
if (!menuOption.bounds && !menuOption.clock) {
return `Conductor menu option is missing required property 'bounds'. This field is required when configuring a menu option with fixed bounds.\r\n${JSON.stringify(
menuOption
)}`;
}
}
function hasRequiredOptions(config) {
if (config === undefined
|| config.menuOptions === undefined
|| config.menuOptions.length === 0) {
return "You must specify one or more 'menuOptions'.";
}
if (config === undefined || config.menuOptions === undefined || config.menuOptions.length === 0) {
return "You must specify one or more 'menuOptions'.";
}
if (config.menuOptions.some(validateMenuOption)) {
return config.menuOptions.map(validateMenuOption)
.filter(isTruthy)
.join('\n');
}
if (config.menuOptions.some(validateMenuOption)) {
return config.menuOptions.map(validateMenuOption).filter(isTruthy).join('\n');
}
return undefined;
return undefined;
}
function validateConfiguration(config, openmct) {
const systems = openmct.time.getAllTimeSystems()
.reduce(function (m, ts) {
m[ts.key] = ts;
const systems = openmct.time.getAllTimeSystems().reduce(function (m, ts) {
m[ts.key] = ts;
return m;
}, {});
const clocks = openmct.time.getAllClocks()
.reduce(function (m, c) {
m[c.key] = c;
return m;
}, {});
const clocks = openmct.time.getAllClocks().reduce(function (m, c) {
m[c.key] = c;
return m;
}, {});
return m;
}, {});
return config.menuOptions.map(function (menuOption) {
let message = '';
if (menuOption.timeSystem && !systems[menuOption.timeSystem]) {
message = `Time system '${menuOption.timeSystem}' has not been registered: \r\n ${JSON.stringify(menuOption)}`;
}
return config.menuOptions
.map(function (menuOption) {
let message = '';
if (menuOption.timeSystem && !systems[menuOption.timeSystem]) {
message = `Time system '${
menuOption.timeSystem
}' has not been registered: \r\n ${JSON.stringify(menuOption)}`;
}
if (menuOption.clock && !clocks[menuOption.clock]) {
message = `Clock '${menuOption.clock}' has not been registered: \r\n ${JSON.stringify(menuOption)}`;
}
if (menuOption.clock && !clocks[menuOption.clock]) {
message = `Clock '${menuOption.clock}' has not been registered: \r\n ${JSON.stringify(
menuOption
)}`;
}
return message;
}).filter(isTruthy).join('\n');
return message;
})
.filter(isTruthy)
.join('\n');
}
function throwIfError(configResult) {
if (configResult) {
throw new Error(`Invalid Time Conductor Configuration. ${configResult} \r\n https://github.com/nasa/openmct/blob/master/API.md#the-time-conductor`);
}
if (configResult) {
throw new Error(
`Invalid Time Conductor Configuration. ${configResult} \r\n https://github.com/nasa/openmct/blob/master/API.md#the-time-conductor`
);
}
}
function mountComponent(openmct, configuration) {
openmct.layout.conductorComponent = Object.create({
components: {
Conductor
},
template: "<conductor></conductor>",
provide: {
openmct: openmct,
configuration: configuration
}
});
openmct.layout.conductorComponent = Object.create({
components: {
Conductor
},
template: '<conductor></conductor>',
provide: {
openmct: openmct,
configuration: configuration
}
});
}
export default function (config) {
return function (openmct) {
let configResult = hasRequiredOptions(config) || validateConfiguration(config, openmct);
throwIfError(configResult);
return function (openmct) {
let configResult = hasRequiredOptions(config) || validateConfiguration(config, openmct);
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());
} else {
openmct.time.timeSystem(defaults.timeSystem, defaults.bounds);
}
const defaults = config.menuOptions[0];
if (defaults.clock) {
openmct.time.clock(defaults.clock, defaults.clockOffsets);
openmct.time.timeSystem(defaults.timeSystem, openmct.time.bounds());
} else {
openmct.time.timeSystem(defaults.timeSystem, defaults.bounds);
}
openmct.on('start', function () {
mountComponent(openmct, config);
});
};
openmct.on('start', function () {
mountComponent(openmct, config);
});
};
}

View File

@ -20,9 +20,9 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {createMouseEvent, createOpenMct, resetApplicationState} from "utils/testing";
import {millisecondsToDHMS, getPreciseDuration} from "../../utils/duration";
import ConductorPlugin from "./plugin";
import { createMouseEvent, createOpenMct, resetApplicationState } from 'utils/testing';
import { millisecondsToDHMS, getPreciseDuration } from '../../utils/duration';
import ConductorPlugin from './plugin';
import Vue from 'vue';
const THIRTY_SECONDS = 30 * 1000;
@ -33,132 +33,149 @@ const THIRTY_MINUTES = FIFTEEN_MINUTES * 2;
const date = new Date(Date.UTC(78, 0, 20, 0, 0, 0)).getTime();
describe('time conductor', () => {
let element;
let child;
let appHolder;
let openmct;
let config = {
menuOptions: [
{
name: "FixedTimeRange",
timeSystem: 'utc',
bounds: {
start: date - THIRTY_MINUTES,
end: date
},
presets: [],
records: 2
},
{
name: "LocalClock",
timeSystem: 'utc',
clock: 'local',
clockOffsets: {
start: -THIRTY_MINUTES,
end: THIRTY_SECONDS
},
presets: []
}
]
};
let element;
let child;
let appHolder;
let openmct;
let config = {
menuOptions: [
{
name: 'FixedTimeRange',
timeSystem: 'utc',
bounds: {
start: date - THIRTY_MINUTES,
end: date
},
presets: [],
records: 2
},
{
name: 'LocalClock',
timeSystem: 'utc',
clock: 'local',
clockOffsets: {
start: -THIRTY_MINUTES,
end: THIRTY_SECONDS
},
presets: []
}
]
};
beforeEach((done) => {
openmct = createOpenMct();
openmct.install(new ConductorPlugin(config));
element = document.createElement('div');
element.style.width = '640px';
element.style.height = '480px';
child = document.createElement('div');
child.style.width = '640px';
child.style.height = '480px';
element.appendChild(child);
openmct.on('start', () => {
openmct.time.bounds({
start: config.menuOptions[0].bounds.start,
end: config.menuOptions[0].bounds.end
});
Vue.nextTick(() => {
done();
});
});
appHolder = document.createElement('div');
openmct.start(appHolder);
});
afterEach(() => {
appHolder = undefined;
openmct = undefined;
return resetApplicationState(openmct);
});
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'
);
});
});
describe('in realtime mode', () => {
beforeEach((done) => {
openmct = createOpenMct();
openmct.install(new ConductorPlugin(config));
const switcher = appHolder.querySelector('.c-mode-button');
const clickEvent = createMouseEvent('click');
element = document.createElement('div');
element.style.width = '640px';
element.style.height = '480px';
child = document.createElement('div');
child.style.width = '640px';
child.style.height = '480px';
element.appendChild(child);
openmct.on('start', () => {
openmct.time.bounds({
start: config.menuOptions[0].bounds.start,
end: config.menuOptions[0].bounds.end
});
Vue.nextTick(() => {
done();
});
switcher.dispatchEvent(clickEvent);
Vue.nextTick(() => {
const clockItem = document.querySelectorAll('.c-conductor__mode-menu li')[1];
clockItem.dispatchEvent(clickEvent);
Vue.nextTick(() => {
done();
});
appHolder = document.createElement("div");
openmct.start(appHolder);
});
});
afterEach(() => {
appHolder = undefined;
openmct = undefined;
it('shows delta inputs', () => {
const realtimeModeEl = appHolder.querySelector('.is-realtime-mode');
const dateTimeInputs = realtimeModeEl.querySelectorAll('.c-conductor__delta-button');
return resetApplicationState(openmct);
expect(dateTimeInputs[0].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:30:00');
expect(dateTimeInputs[1].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:00:30');
});
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');
});
it('shows clock options', () => {
const realtimeModeEl = appHolder.querySelector('.is-realtime-mode');
expect(realtimeModeEl.querySelector('.c-mode-button .c-button__label').innerHTML).toEqual(
'Local Clock'
);
});
describe('in realtime mode', () => {
beforeEach((done) => {
const switcher = appHolder.querySelector('.c-mode-button');
const clickEvent = createMouseEvent("click");
it('shows the current time', () => {
const realtimeModeEl = appHolder.querySelector('.is-realtime-mode');
const currentTimeEl = realtimeModeEl.querySelector('.c-input--datetime');
const currentTime = openmct.time.clock().currentValue();
const { start, end } = openmct.time.bounds();
switcher.dispatchEvent(clickEvent);
Vue.nextTick(() => {
const clockItem = document.querySelectorAll('.c-conductor__mode-menu li')[1];
clockItem.dispatchEvent(clickEvent);
Vue.nextTick(() => {
done();
});
});
});
it('shows delta inputs', () => {
const realtimeModeEl = appHolder.querySelector('.is-realtime-mode');
const dateTimeInputs = realtimeModeEl.querySelectorAll('.c-conductor__delta-button');
expect(dateTimeInputs[0].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:30:00');
expect(dateTimeInputs[1].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');
});
it('shows the current time', () => {
const realtimeModeEl = appHolder.querySelector('.is-realtime-mode');
const currentTimeEl = realtimeModeEl.querySelector('.c-input--datetime');
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(currentTime).toBeGreaterThan(start);
expect(currentTime).toBeLessThanOrEqual(end);
expect(currentTimeEl.value.length).toBeGreaterThan(0);
});
});
});
describe('duration functions', () => {
it('should transform milliseconds to DHMS', () => {
const functionResults = [millisecondsToDHMS(0), millisecondsToDHMS(86400000),
millisecondsToDHMS(129600000), millisecondsToDHMS(661824000), millisecondsToDHMS(213927028)];
const validResults = [' ', '+ 1d', '+ 1d 12h', '+ 7d 15h 50m 24s', '+ 2d 11h 25m 27s 28ms'];
expect(validResults).toEqual(functionResults);
});
it('should transform milliseconds to DHMS', () => {
const functionResults = [
millisecondsToDHMS(0),
millisecondsToDHMS(86400000),
millisecondsToDHMS(129600000),
millisecondsToDHMS(661824000),
millisecondsToDHMS(213927028)
];
const validResults = [' ', '+ 1d', '+ 1d 12h', '+ 7d 15h 50m 24s', '+ 2d 11h 25m 27s 28ms'];
expect(validResults).toEqual(functionResults);
});
it('should get precise duration', () => {
const functionResults = [getPreciseDuration(0), getPreciseDuration(643680000),
getPreciseDuration(1605312000), getPreciseDuration(213927028)];
const validResults = ['00:00:00:00:000', '07:10:48:00:000', '18:13:55:12:000', '02:11:25:27:028'];
expect(validResults).toEqual(functionResults);
});
it('should get precise duration', () => {
const functionResults = [
getPreciseDuration(0),
getPreciseDuration(643680000),
getPreciseDuration(1605312000),
getPreciseDuration(213927028)
];
const validResults = [
'00:00:00:00:000',
'07:10:48:00:000',
'18:13:55:12:000',
'02:11:25:27:028'
];
expect(validResults).toEqual(functionResults);
});
});

View File

@ -20,183 +20,179 @@
at runtime from the About dialog for additional information.
-->
<template>
<div
<div
class="pr-tc-input-menu"
:class="{'pr-tc-input-menu--bottom' : bottom === true}"
: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')"
>
:
<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')"
>
:
<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>
<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>
</div>
</template>
<script>
export default {
props: {
type: {
type: String,
required: true
},
offset: {
type: String,
required: true
},
bottom: {
type: Boolean,
default() {
return false;
}
}
props: {
type: {
type: String,
required: true
},
data() {
return {
inputHrs: '00',
inputMins: '00',
inputSecs: '00',
isDisabled: false
};
offset: {
type: String,
required: true
},
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();
}
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

@ -23,44 +23,67 @@
import moment from 'moment';
export default function multiFormat(date) {
const momentified = moment.utc(date);
/**
* Uses logic from d3 Time-Scales, v3 of the API. See
* https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Scales.md
*
* Licensed
*/
const format = [
[".SSS", function (m) {
return m.milliseconds();
}],
[":ss", function (m) {
return m.seconds();
}],
["HH:mm", function (m) {
return m.minutes();
}],
["HH:mm", function (m) {
return m.hours();
}],
["ddd DD", function (m) {
return m.days()
&& m.date() !== 1;
}],
["MMM DD", function (m) {
return m.date() !== 1;
}],
["MMMM", function (m) {
return m.month();
}],
["YYYY", function () {
return true;
}]
].filter(function (row) {
return row[1](momentified);
})[0][0];
const momentified = moment.utc(date);
/**
* Uses logic from d3 Time-Scales, v3 of the API. See
* https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Scales.md
*
* Licensed
*/
const format = [
[
'.SSS',
function (m) {
return m.milliseconds();
}
],
[
':ss',
function (m) {
return m.seconds();
}
],
[
'HH:mm',
function (m) {
return m.minutes();
}
],
[
'HH:mm',
function (m) {
return m.hours();
}
],
[
'ddd DD',
function (m) {
return m.days() && m.date() !== 1;
}
],
[
'MMM DD',
function (m) {
return m.date() !== 1;
}
],
[
'MMMM',
function (m) {
return m.month();
}
],
[
'YYYY',
function () {
return true;
}
]
].filter(function (row) {
return row[1](momentified);
})[0][0];
if (format !== undefined) {
return moment.utc(date).format(format);
}
if (format !== undefined) {
return moment.utc(date).format(format);
}
}