Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nasa/openmct.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Tsay <3614296+davetsay@users.noreply.github.com>2020-05-14 00:54:09 +0300
committerGitHub <noreply@github.com>2020-05-14 00:54:09 +0300
commit480e327c63b99bd039932e7fbc329e0495042576 (patch)
tree4de4526c6c1f0cc27b90c1580426c960f625958a
parente8b10f0193e170b4e285afced77c19443d6f085b (diff)
[Time Conductor] add history and select range features to vista-r4.4.1 (#3015)V4.4.1-RC1
* linting * linting * more linting * linting * change realtime end bound to 30 secondes * add max duration validation * Tweaks to Time Conductor History menu - Enhanced styles for `.c-menu`; - Added hint messaging and separator; - Reversed displayed history array so that latest entry is always first; * refactor to use browser mouse events instead of d3brush * move zoom/pan styling up to conductor * WIP almost there * fix zoom * move altPressed up to parent * handle no drag on pan * rename inMode vars for clarity * Styling for Time Conductor zoom and pan - Minor fix to hover cursor for alt-pressed panning; * add configurable bounds limit to time conductor * add presets and records * fixes for history * remove lodash * add default configurables for examples * do not install local time system * cleanup * fix merge conflict * fix merge conflict * add styles from missing picks * styling fixes * local time system is utc based * fix indentation remove logging * remove comments * section-hint without section-separator styling * provide reasonable defaults for conductor configuration * specify input to check validation on * improve validation * first check both inputs for valid formats * clear each valid input on new entry * tear down listeners * add user instructions * allow preset bounds to be declared as callback function Co-authored-by: charlesh88 <charlesh88@gmail.com>
-rw-r--r--index.html40
-rw-r--r--src/plugins/localTimeSystem/LocalTimeSystem.js2
-rw-r--r--src/plugins/timeConductor/Conductor.vue176
-rw-r--r--src/plugins/timeConductor/ConductorAxis.vue183
-rw-r--r--src/plugins/timeConductor/ConductorHistory.vue200
-rw-r--r--src/plugins/timeConductor/ConductorMode.vue2
-rw-r--r--src/plugins/timeConductor/conductor-axis.scss14
-rw-r--r--src/plugins/timeConductor/conductor.scss59
-rw-r--r--src/styles/_constants-espresso.scss3
-rw-r--r--src/styles/_constants-maelstrom.scss3
-rw-r--r--src/styles/_constants-snow.scss5
-rw-r--r--src/styles/_controls.scss27
12 files changed, 616 insertions, 98 deletions
diff --git a/index.html b/index.html
index 095e80b59..c67afaa51 100644
--- a/index.html
+++ b/index.html
@@ -34,8 +34,8 @@
<body>
</body>
<script>
- const FIVE_MINUTES = 5 * 60 * 1000;
- const THIRTY_MINUTES = 30 * 60 * 1000;
+ const THIRTY_SECONDS = 30 * 1000;
+ const THIRTY_MINUTES = THIRTY_SECONDS * 60;
[
'example/eventGenerator'
@@ -63,7 +63,39 @@
bounds: {
start: Date.now() - THIRTY_MINUTES,
end: Date.now()
- }
+ },
+ // commonly used bounds can be stored in history
+ // bounds (start and end) can accept either a milliseconds number
+ // or a callback function returning a milliseconds number
+ // a function is useful for invoking Date.now() at exact moment of preset selection
+ presets: [
+ {
+ label: 'Last Day',
+ bounds: {
+ start: () => Date.now() - 1000 * 60 * 60 * 24,
+ end: () => Date.now()
+ }
+ },
+ {
+ label: 'Last 2 hours',
+ bounds: {
+ start: () => Date.now() - 1000 * 60 * 60 * 2,
+ end: () => Date.now()
+ }
+ },
+ {
+ label: 'Last hour',
+ bounds: {
+ start: () => Date.now() - 1000 * 60 * 60,
+ end: () => Date.now()
+ }
+ }
+ ],
+ // maximum recent bounds to retain in conductor history
+ records: 10,
+ // maximum duration between start and end bounds
+ // for utc-based time systems this is in milliseconds
+ limit: 1000 * 60 * 60 * 24
},
{
name: "Realtime",
@@ -71,7 +103,7 @@
clock: 'local',
clockOffsets: {
start: - THIRTY_MINUTES,
- end: FIVE_MINUTES
+ end: THIRTY_SECONDS
}
}
]
diff --git a/src/plugins/localTimeSystem/LocalTimeSystem.js b/src/plugins/localTimeSystem/LocalTimeSystem.js
index fd37b334d..3f08d07fe 100644
--- a/src/plugins/localTimeSystem/LocalTimeSystem.js
+++ b/src/plugins/localTimeSystem/LocalTimeSystem.js
@@ -41,7 +41,7 @@ define([], function () {
this.timeFormat = 'local-format';
this.durationFormat = 'duration';
- this.isUTCBased = false;
+ this.isUTCBased = true;
}
return LocalTimeSystem;
diff --git a/src/plugins/timeConductor/Conductor.vue b/src/plugins/timeConductor/Conductor.vue
index 63c3ced73..eaa9ce3d9 100644
--- a/src/plugins/timeConductor/Conductor.vue
+++ b/src/plugins/timeConductor/Conductor.vue
@@ -22,7 +22,12 @@
<template>
<div
class="c-conductor"
- :class="[isFixed ? 'is-fixed-mode' : 'is-realtime-mode']"
+ :class="[
+ { 'is-zooming': isZooming },
+ { 'is-panning': isPanning },
+ { 'alt-pressed': altPressed },
+ isFixed ? 'is-fixed-mode' : 'is-realtime-mode'
+ ]"
>
<form
ref="conductorForm"
@@ -52,7 +57,7 @@
type="text"
autocorrect="off"
spellcheck="false"
- @change="validateAllBounds(); submitForm()"
+ @change="validateAllBounds('startDate'); submitForm()"
>
<date-picker
v-if="isFixed && isUTCBased"
@@ -92,7 +97,7 @@
autocorrect="off"
spellcheck="false"
:disabled="!isFixed"
- @change="validateAllBounds(); submitForm()"
+ @change="validateAllBounds('endDate'); submitForm()"
>
<date-picker
v-if="isFixed && isUTCBased"
@@ -122,14 +127,25 @@
<conductor-axis
class="c-conductor__ticks"
- :bounds="rawBounds"
- @panAxis="setViewFromBounds"
+ :view-bounds="viewBounds"
+ :is-fixed="isFixed"
+ :alt-pressed="altPressed"
+ @endPan="endPan"
+ @endZoom="endZoom"
+ @panAxis="pan"
+ @zoomAxis="zoom"
/>
+
</div>
<div class="c-conductor__controls">
- <!-- Mode, time system menu buttons and duration slider -->
<ConductorMode class="c-conductor__mode-select" />
<ConductorTimeSystem class="c-conductor__time-system-select" />
+ <ConductorHistory
+ v-if="isFixed"
+ class="c-conductor__history-select"
+ :bounds="openmct.time.bounds()"
+ :time-system="timeSystem"
+ />
</div>
<input
type="submit"
@@ -145,6 +161,7 @@ import ConductorTimeSystem from './ConductorTimeSystem.vue';
import DatePicker from './DatePicker.vue';
import ConductorAxis from './ConductorAxis.vue';
import ConductorModeIcon from './ConductorModeIcon.vue';
+import ConductorHistory from './ConductorHistory.vue'
const DEFAULT_DURATION_FORMATTER = 'duration';
@@ -155,7 +172,8 @@ export default {
ConductorTimeSystem,
DatePicker,
ConductorAxis,
- ConductorModeIcon
+ ConductorModeIcon,
+ ConductorHistory
},
data() {
let bounds = this.openmct.time.bounds();
@@ -165,6 +183,7 @@ export default {
let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
return {
+ timeSystem: timeSystem,
timeFormatter: timeFormatter,
durationFormatter: durationFormatter,
offsets: {
@@ -175,29 +194,68 @@ export default {
start: timeFormatter.format(bounds.start),
end: timeFormatter.format(bounds.end)
},
- rawBounds: {
+ viewBounds: {
start: bounds.start,
end: bounds.end
},
isFixed: this.openmct.time.clock() === undefined,
isUTCBased: timeSystem.isUTCBased,
- showDatePicker: false
+ showDatePicker: false,
+ altPressed: false,
+ isPanning: false,
+ isZooming: false
}
},
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', this.setViewFromBounds);
this.openmct.time.on('timeSystem', this.setTimeSystem);
this.openmct.time.on('clock', this.setViewFromClock);
this.openmct.time.on('clockOffsets', this.setViewFromOffsets)
},
+ beforeDestroy() {
+ document.removeEventListener('keydown', this.handleKeyDown);
+ document.removeEventListener('keyup', this.handleKeyUp);
+ },
methods: {
+ 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) {
+ this.isZooming = true;
+ this.formattedBounds.start = this.timeFormatter.format(bounds.start);
+ this.formattedBounds.end = this.timeFormatter.format(bounds.end);
+ },
+ endZoom(bounds) {
+ const _bounds = bounds ? bounds : this.openmct.time.bounds();
+ this.isZooming = false;
+
+ this.openmct.time.bounds(_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;
},
setOffsetsFromView($event) {
@@ -237,8 +295,8 @@ export default {
setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start);
this.formattedBounds.end = this.timeFormatter.format(bounds.end);
- this.rawBounds.start = bounds.start;
- this.rawBounds.end = bounds.end;
+ this.viewBounds.start = bounds.start;
+ this.viewBounds.end = bounds.end;
},
setViewFromOffsets(offsets) {
this.offsets.start = this.durationFormatter.format(Math.abs(offsets.start));
@@ -251,6 +309,15 @@ export default {
this.setOffsetsFromView();
}
},
+ 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;
+ },
clearAllValidation() {
if (this.isFixed) {
[this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput);
@@ -262,36 +329,52 @@ export default {
input.setCustomValidity('');
input.title = '';
},
- validateAllBounds() {
+ validateAllBounds(ref) {
+ if (!this.areBoundsFormatsValid()) {
+ return false;
+ }
+
+ let validationResult = true;
+ const currentInput = this.$refs[ref];
+
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
- let validationResult = true;
- let formattedDate;
+ let boundsValues = {
+ start: this.timeFormatter.parse(this.formattedBounds.start),
+ end: this.timeFormatter.parse(this.formattedBounds.end)
+ };
+ const limit = this.getBoundsLimit();
- if (input === this.$refs.startDate) {
- formattedDate = this.formattedBounds.start;
+ if (
+ this.timeSystem.isUTCBased
+ && limit
+ && boundsValues.end - boundsValues.start > limit
+ ) {
+ if (input === currentInput) {
+ validationResult = "Start and end difference exceeds allowable limit";
+ }
} else {
- formattedDate = this.formattedBounds.end;
+ if (input === currentInput) {
+ validationResult = this.openmct.time.validateBounds(boundsValues);
+ }
}
+ return this.handleValidationResults(input, validationResult);
+ });
+ },
+ areBoundsFormatsValid() {
+ let validationResult = 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 = 'Invalid date';
- } else {
- let boundsValues = {
- start: this.timeFormatter.parse(this.formattedBounds.start),
- end: this.timeFormatter.parse(this.formattedBounds.end)
- };
- validationResult = this.openmct.time.validateBounds(boundsValues);
}
- if (validationResult !== true) {
- input.setCustomValidity(validationResult);
- input.title = validationResult;
- return false;
- } else {
- input.setCustomValidity('');
- input.title = '';
- return true;
- }
+ return this.handleValidationResults(input, validationResult);
});
},
validateAllOffsets(event) {
@@ -315,17 +398,20 @@ export default {
validationResult = this.openmct.time.validateOffsets(offsetValues);
}
- if (validationResult !== true) {
- input.setCustomValidity(validationResult);
- input.title = validationResult;
- return false;
- } else {
- input.setCustomValidity('');
- input.title = '';
- return true;
- }
+ return this.handleValidationResults(input, validationResult);
});
},
+ handleValidationResults(input, validationResult) {
+ if (validationResult !== true) {
+ input.setCustomValidity(validationResult);
+ input.title = validationResult;
+ return false;
+ } else {
+ input.setCustomValidity('');
+ input.title = '';
+ return true;
+ }
+ },
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)
@@ -338,12 +424,12 @@ export default {
},
startDateSelected(date) {
this.formattedBounds.start = this.timeFormatter.format(date);
- this.validateAllBounds();
+ this.validateAllBounds('startDate');
this.submitForm();
},
endDateSelected(date) {
this.formattedBounds.end = this.timeFormatter.format(date);
- this.validateAllBounds();
+ this.validateAllBounds('endDate');
this.submitForm();
}
}
diff --git a/src/plugins/timeConductor/ConductorAxis.vue b/src/plugins/timeConductor/ConductorAxis.vue
index da7871b04..2877cb730 100644
--- a/src/plugins/timeConductor/ConductorAxis.vue
+++ b/src/plugins/timeConductor/ConductorAxis.vue
@@ -24,7 +24,12 @@
ref="axisHolder"
class="c-conductor-axis"
@mousedown="dragStart($event)"
-></div>
+>
+ <div
+ class="c-conductor-axis__zoom-indicator"
+ :style="zoomStyle"
+ ></div>
+</div>
</template>
<script>
@@ -43,14 +48,35 @@ const PIXELS_PER_TICK_WIDE = 200;
export default {
inject: ['openmct'],
props: {
- bounds: {
+ viewBounds: {
type: Object,
required: true
+ },
+ 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: {
- bounds: {
- handler(bounds) {
+ viewBounds: {
+ handler() {
this.setScale();
},
deep: true
@@ -58,18 +84,23 @@ export default {
},
mounted() {
let axisHolder = this.$refs.axisHolder;
- let height = axisHolder.offsetHeight;
+ this.height = axisHolder.offsetHeight;
+ this.width = axisHolder.clientWidth;
+ const rect = axisHolder.getBoundingClientRect();
+ this.left = Math.round(rect.left);
+
let vis = d3Selection.select(axisHolder)
.append("svg:svg")
.attr("width", "100%")
- .attr("height", height);
+ .attr("height", this.height);
+
- this.width = this.$refs.axisHolder.clientWidth;
this.xAxis = d3Axis.axisTop();
this.dragging = false;
// draw x axis with labels. CSS is used to position them.
- this.axisElement = vis.append("g");
+ this.axisElement = vis.append("g")
+ .attr("class", "axis");
this.setViewFromTimeSystem(this.openmct.time.timeSystem());
this.setScale();
@@ -83,12 +114,15 @@ export default {
methods: {
setScale() {
let timeSystem = this.openmct.time.timeSystem();
- let bounds = this.bounds;
if (timeSystem.isUTCBased) {
- this.xScale.domain([new Date(bounds.start), new Date(bounds.end)]);
+ this.xScale.domain(
+ [new Date(this.viewBounds.start), new Date(this.viewBounds.end)]
+ );
} else {
- this.xScale.domain([bounds.start, bounds.end]);
+ this.xScale.domain(
+ [this.viewBounds.start, this.viewBounds.end]
+ );
}
this.xAxis.scale(this.xScale);
@@ -102,7 +136,7 @@ export default {
this.xAxis.ticks(this.width / PIXELS_PER_TICK);
}
- this.msPerPixel = (bounds.end - bounds.start) / this.width;
+ 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
@@ -120,9 +154,8 @@ export default {
},
getActiveFormatter() {
let timeSystem = this.openmct.time.timeSystem();
- let isFixed = this.openmct.time.clock() === undefined;
- if (isFixed) {
+ if (this.isFixed) {
return this.getFormatter(timeSystem.timeFormat);
} else {
return this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
@@ -134,42 +167,128 @@ export default {
}).formatter;
},
dragStart($event) {
- let isFixed = this.openmct.time.clock() === undefined;
- if (isFixed) {
+ 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(()=>{
- let deltaX = $event.clientX - this.dragStartX;
- let percX = deltaX / this.width;
- let bounds = this.openmct.time.bounds();
- let deltaTime = bounds.end - bounds.start;
- let newStart = bounds.start - percX * deltaTime;
- this.$emit('panAxis',{
- start: newStart,
- end: newStart + deltaTime
- });
+
+ requestAnimationFrame(() => {
+ this.dragX = $event.clientX;
+ this.inPanMode ? this.pan() : this.zoom();
this.dragging = false;
- })
- } else {
- console.log('Rejected drag due to RAF cap');
+ });
}
},
dragEnd() {
+ this.inPanMode ? this.endPan() : this.endZoom();
+
document.removeEventListener('mousemove', this.drag);
- this.openmct.time.bounds({
- start: this.bounds.start,
- end: this.bounds.end
+ this.dragStartX = undefined;
+ this.dragX = undefined;
+ },
+ pan() {
+ const panBounds = this.getPanBounds();
+ this.$emit('panAxis', panBounds);
+ },
+ endPan() {
+ const panBounds = this.dragStartX && this.dragX && this.dragStartX !== this.dragX
+ ? 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: panStart,
+ end: panStart + deltaTime
+ };
+ },
+ 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() {
+ const zoomRange = this.dragStartX && this.dragX && this.dragStartX !== this.dragX
+ ? this.getZoomRange()
+ : undefined;
+
+ const zoomBounds = zoomRange
+ ? {
+ start: this.scaleToBounds(zoomRange.start),
+ end: this.scaleToBounds(zoomRange.end)
+ }
+ : this.openmct.time.bounds();
+
+ 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 bounds.start + offset;
+ },
resize() {
if (this.$refs.axisHolder.clientWidth !== this.width) {
this.width = this.$refs.axisHolder.clientWidth;
diff --git a/src/plugins/timeConductor/ConductorHistory.vue b/src/plugins/timeConductor/ConductorHistory.vue
new file mode 100644
index 000000000..bcebf9dd5
--- /dev/null
+++ b/src/plugins/timeConductor/ConductorHistory.vue
@@ -0,0 +1,200 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2018, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+<template>
+<div class="c-ctrl-wrapper c-ctrl-wrapper--menus-up">
+ <button class="c-button--menu c-history-button icon-history"
+ @click.prevent="toggle"
+ >
+ <span class="c-button__label">History</span>
+ </button>
+ <div v-if="open"
+ class="c-menu c-conductor__history-menu"
+ >
+ <ul v-if="hasHistoryPresets">
+ <li
+ v-for="preset in presets"
+ :key="preset.label"
+ class="icon-clock"
+ @click="selectPresetBounds(preset.bounds)"
+ >
+ {{ preset.label }}
+ </li>
+ </ul>
+
+ <div
+ v-if="hasHistoryPresets"
+ class="c-menu__section-separator"
+ ></div>
+
+ <div class="c-menu__section-hint">
+ Past timeframes, ordered by latest first
+ </div>
+
+ <ul>
+ <li
+ v-for="(timespan, index) in historyForCurrentTimeSystem"
+ :key="index"
+ class="icon-history"
+ @click="selectTimespan(timespan)"
+ >
+ {{ formatTime(timespan.start) }} - {{ formatTime(timespan.end) }}
+ </li>
+ </ul>
+ </div>
+</div>
+</template>
+
+<script>
+import toggleMixin from '../../ui/mixins/toggle-mixin';
+
+const LOCAL_STORAGE_HISTORY_KEY = 'tcHistory';
+const DEFAULT_RECORDS = 10;
+
+export default {
+ inject: ['openmct', 'configuration'],
+ mixins: [toggleMixin],
+ props: {
+ bounds: {
+ type: Object,
+ required: true
+ },
+ timeSystem: {
+ type: Object,
+ required: true
+ }
+ },
+ data() {
+ return {
+ history: {}, // contains arrays of timespans {start, end}, array key is time system key
+ presets: []
+ }
+ },
+ computed: {
+ hasHistoryPresets() {
+ return this.timeSystem.isUTCBased && this.presets.length;
+ },
+ historyForCurrentTimeSystem() {
+ const history = this.history[this.timeSystem.key];
+
+ return history;
+ }
+ },
+ watch: {
+ bounds: {
+ handler() {
+ this.addTimespan();
+ },
+ deep: true
+ },
+ timeSystem: {
+ handler() {
+ this.loadConfiguration();
+ this.addTimespan();
+ },
+ deep: true
+ },
+ history: {
+ handler() {
+ this.persistHistoryToLocalStorage();
+ },
+ deep: true
+ }
+ },
+ mounted() {
+ this.getHistoryFromLocalStorage();
+ },
+ methods: {
+ getHistoryFromLocalStorage() {
+ if (localStorage.getItem(LOCAL_STORAGE_HISTORY_KEY)) {
+ this.history = JSON.parse(localStorage.getItem(LOCAL_STORAGE_HISTORY_KEY))
+ } else {
+ this.history = {};
+ this.persistHistoryToLocalStorage();
+ }
+ },
+ persistHistoryToLocalStorage() {
+ localStorage.setItem(LOCAL_STORAGE_HISTORY_KEY, JSON.stringify(this.history));
+ },
+ addTimespan() {
+ const key = this.timeSystem.key;
+ let [...currentHistory] = this.history[key] || [];
+ const timespan = {
+ start: this.bounds.start,
+ end: this.bounds.end
+ };
+
+ const isNotEqual = function (entry) {
+ const start = entry.start !== this.start;
+ const end = entry.end !== this.end;
+
+ return start || end;
+ };
+ currentHistory = currentHistory.filter(isNotEqual, timespan);
+
+ while (currentHistory.length >= this.records) {
+ currentHistory.pop();
+ }
+
+ currentHistory.unshift(timespan);
+ this.history[key] = currentHistory;
+ },
+ selectTimespan(timespan) {
+ this.openmct.time.bounds(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: start,
+ end: end
+ });
+ },
+ loadConfiguration() {
+ const configurations = this.configuration.menuOptions
+ .filter(option => option.timeSystem === this.timeSystem.key);
+
+ this.presets = this.loadPresets(configurations);
+ this.records = this.loadRecords(configurations);
+ },
+ loadPresets(configurations) {
+ const configuration = configurations.find(option => option.presets);
+ const presets = configuration ? configuration.presets : [];
+
+ return presets;
+ },
+ loadRecords(configurations) {
+ const configuration = configurations.find(option => option.records);
+ const records = configuration ? configuration.records : DEFAULT_RECORDS;
+
+ return records;
+ },
+ formatTime(time) {
+ const formatter = this.openmct.telemetry.getValueFormatter({
+ format: this.timeSystem.timeFormat
+ }).formatter;
+
+ return formatter.format(time);
+ }
+ }
+}
+</script>
diff --git a/src/plugins/timeConductor/ConductorMode.vue b/src/plugins/timeConductor/ConductorMode.vue
index 070a711cc..20d8d30c5 100644
--- a/src/plugins/timeConductor/ConductorMode.vue
+++ b/src/plugins/timeConductor/ConductorMode.vue
@@ -110,7 +110,7 @@ export default {
if (clock === undefined) {
return {
key: 'fixed',
- name: 'Fixed Timespan Mode',
+ name: 'Fixed Timespan',
description: 'Query and explore data that falls between two fixed datetimes.',
cssClass: 'icon-tabular'
}
diff --git a/src/plugins/timeConductor/conductor-axis.scss b/src/plugins/timeConductor/conductor-axis.scss
index 23ded63ea..b16a58f71 100644
--- a/src/plugins/timeConductor/conductor-axis.scss
+++ b/src/plugins/timeConductor/conductor-axis.scss
@@ -13,7 +13,7 @@
text-rendering: geometricPrecision;
width: 100%;
height: 100%;
- > g {
+ > g.axis {
// Overall Tick holder
transform: translateY($tickYPos);
path {
@@ -44,7 +44,6 @@
}
body.desktop .is-fixed-mode & {
- @include cursorGrab();
background-size: 3px 30%;
background-color: $colorBodyBgSubtle;
box-shadow: inset rgba(black, 0.4) 0 1px 1px;
@@ -55,17 +54,6 @@
stroke: $colorBodyBgSubtle;
transition: $transOut;
}
-
- &:hover,
- &:active {
- $c: $colorKeySubtle;
- background-color: $c;
- transition: $transIn;
- svg text {
- stroke: $c;
- transition: $transIn;
- }
- }
}
.is-realtime-mode & {
diff --git a/src/plugins/timeConductor/conductor.scss b/src/plugins/timeConductor/conductor.scss
index dc5c30847..4ecba9e3f 100644
--- a/src/plugins/timeConductor/conductor.scss
+++ b/src/plugins/timeConductor/conductor.scss
@@ -57,6 +57,65 @@
}
}
+ &.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;
+ filter: $timeConductorAxisHoverFilter;
+ }
+ }
+ }
+
+ &.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;
diff --git a/src/styles/_constants-espresso.scss b/src/styles/_constants-espresso.scss
index 1ea130b89..3e3bbaf2d 100644
--- a/src/styles/_constants-espresso.scss
+++ b/src/styles/_constants-espresso.scss
@@ -142,6 +142,9 @@ $colorTimeHov: pullForward($colorTime, 10%);
$colorTimeSubtle: pushBack($colorTime, 20%);
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov
+$timeConductorAxisHoverFilter: brightness(1.2);
+$timeConductorActiveBg: $colorKey;
+$timeConductorActivePanBg: #226074;
/************************************************** BROWSING */
$browseFrameColor: pullForward($colorBodyBg, 10%);
diff --git a/src/styles/_constants-maelstrom.scss b/src/styles/_constants-maelstrom.scss
index 81a9cf3ea..d72abb7f8 100644
--- a/src/styles/_constants-maelstrom.scss
+++ b/src/styles/_constants-maelstrom.scss
@@ -146,6 +146,9 @@ $colorTimeHov: pullForward($colorTime, 10%);
$colorTimeSubtle: pushBack($colorTime, 20%);
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov
+$timeConductorAxisHoverFilter: brightness(1.2);
+$timeConductorActiveBg: $colorKey;
+$timeConductorActivePanBg: #226074;
/************************************************** BROWSING */
$browseFrameColor: pullForward($colorBodyBg, 10%);
diff --git a/src/styles/_constants-snow.scss b/src/styles/_constants-snow.scss
index dcc4998f1..7f4412801 100644
--- a/src/styles/_constants-snow.scss
+++ b/src/styles/_constants-snow.scss
@@ -132,7 +132,7 @@ $colorPausedFg: #fff;
// Base variations
$colorBodyBgSubtle: pullForward($colorBodyBg, 5%);
$colorBodyBgSubtleHov: pushBack($colorKey, 50%);
-$colorKeySubtle: pushBack($colorKey, 10%);
+$colorKeySubtle: pushBack($colorKey, 20%);
// Time Colors
$colorTime: #618cff;
@@ -142,6 +142,9 @@ $colorTimeHov: pushBack($colorTime, 5%);
$colorTimeSubtle: pushBack($colorTime, 20%);
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov
+$timeConductorAxisHoverFilter: brightness(0.8);
+$timeConductorActiveBg: $colorKey;
+$timeConductorActivePanBg: #A0CDE1;
/************************************************** BROWSING */
$browseFrameColor: pullForward($colorBodyBg, 10%);
diff --git a/src/styles/_controls.scss b/src/styles/_controls.scss
index 9e1aa22c5..dd190d16f 100644
--- a/src/styles/_controls.scss
+++ b/src/styles/_controls.scss
@@ -462,9 +462,17 @@ select {
text-shadow: $shdwMenuText;
padding: $interiorMarginSm;
box-shadow: $shdwMenu;
- display: block;
+ display: flex;
+ flex-direction: column;
position: absolute;
z-index: 100;
+
+ > * {
+ flex: 0 0 auto;
+ //+ * {
+ // margin-top: $interiorMarginSm;
+ //}
+ }
}
@mixin menuInner() {
@@ -502,6 +510,23 @@ select {
.c-menu {
@include menuOuter();
@include menuInner();
+
+ &__section-hint {
+ $m: $interiorMargin;
+ margin: $m 0;
+ padding: $m nth($menuItemPad, 2) 0 nth($menuItemPad, 2);
+
+ opacity: 0.6;
+ font-size: 0.9em;
+ font-style: italic;
+ }
+
+ &__section-separator {
+ $m: $interiorMargin;
+ border-top: 1px solid $colorInteriorBorder;
+ margin: $m 0;
+ padding: $m nth($menuItemPad, 2) 0 nth($menuItemPad, 2);
+ }
}
.c-super-menu {