Merge branch '7936-add-discrete-event-visualization' into issue/7957-adjustable-swimlane-size

This commit is contained in:
David Tsay 2025-03-05 17:06:51 -08:00
commit f943519666
23 changed files with 368 additions and 287 deletions

View File

@ -51,7 +51,7 @@ jobs:
env:
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha }}
run: npm run test:e2e:couchdb
- name: Generate Code Coverage Report
run: npm run cov:e2e:report
@ -66,15 +66,19 @@ jobs:
- name: Archive test results
if: success() || failure()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: e2e-couchdb-test-results
path: test-results
overwrite: true
- name: Archive html test results
if: success() || failure()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: e2e-couchdb-html-test-results
path: html-test-results
overwrite: true
- name: Remove pr:e2e:couchdb label (if present)
if: always()

View File

@ -38,9 +38,11 @@ jobs:
- name: Archive test results
if: success() || failure()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: e2e-flakefinder-test-results
path: test-results
overwrite: true
- name: Remove pr:e2e:flakefinder label (if present)
if: always()

View File

@ -35,9 +35,11 @@ jobs:
- run: npm run test:perf:memory
- name: Archive test results
if: success() || failure()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: e2e-perf-test-results
path: test-results
overwrite: true
- name: Remove pr:e2e:perf label (if present)
if: always()

View File

@ -45,9 +45,11 @@ jobs:
npm run cov:e2e:full:publish
- name: Archive test results
if: success() || failure()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: e2e-pr-test-results
path: test-results
overwrite: true
- name: Remove pr:e2e label (if present)
if: always()

View File

@ -24,7 +24,9 @@ import {
createDomainObjectWithDefaults,
createPlanFromJSON,
navigateToObjectWithFixedTimeBounds,
setFixedIndependentTimeConductorBounds
setFixedIndependentTimeConductorBounds,
setFixedTimeMode,
setTimeConductorBounds
} from '../../../appActions.js';
import { expect, test } from '../../../pluginFixtures.js';
@ -74,21 +76,14 @@ const testPlan = {
};
test.describe('Time Strip', () => {
test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts', async ({
page
}) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5627'
});
// Constant locators
const activityBounds = page.locator('.activity-bounds');
let timestrip;
let plan;
test.beforeEach(async ({ page }) => {
// Goto baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
const timestrip = await test.step('Create a Time Strip', async () => {
timestrip = await test.step('Create a Time Strip', async () => {
const createdTimeStrip = await createDomainObjectWithDefaults(page, { type: 'Time Strip' });
const objectName = await page.locator('.l-browse-bar__object-name').innerText();
expect(objectName).toBe(createdTimeStrip.name);
@ -96,7 +91,7 @@ test.describe('Time Strip', () => {
return createdTimeStrip;
});
const plan = await test.step('Create a Plan and add it to the timestrip', async () => {
plan = await test.step('Create a Plan and add it to the timestrip', async () => {
const createdPlan = await createPlanFromJSON(page, {
name: 'Test Plan',
json: testPlan
@ -110,6 +105,22 @@ test.describe('Time Strip', () => {
.dragTo(page.getByLabel('Object View'));
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
return createdPlan;
});
});
test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts', async ({
page
}) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5627'
});
// Constant locators
const activityBounds = page.locator('.activity-bounds');
await test.step('Set time strip to fixed timespan mode and verify activities', async () => {
const startBound = testPlan.TEST_GROUP[0].start;
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
@ -119,8 +130,6 @@ test.describe('Time Strip', () => {
// Verify all events are displayed
const eventCount = await page.locator('.activity-bounds').count();
expect(eventCount).toEqual(testPlan.TEST_GROUP.length);
return createdPlan;
});
await test.step('TimeStrip can use the Independent Time Conductor', async () => {
@ -177,4 +186,48 @@ test.describe('Time Strip', () => {
expect(await activityBounds.count()).toEqual(1);
});
});
test('Time strip now line', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7817'
});
await test.step('Is displayed in realtime mode', async () => {
await expect(page.getByLabel('Now Marker')).toBeVisible();
});
await test.step('Is hidden when out of bounds of the time axis', async () => {
// Switch to fixed timespan mode
await setFixedTimeMode(page);
// Get the end bounds
const endBounds = await page.getByLabel('End bounds').textContent();
// Add 2 minutes to end bound datetime and use it as the new end time
let endTimeStamp = new Date(endBounds);
endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() + 2);
const endDate = endTimeStamp.toISOString().split('T')[0];
const milliseconds = endTimeStamp.getMilliseconds();
const endTime = endTimeStamp.toISOString().split('T')[1].replace(`.${milliseconds}Z`, '');
// Subtract 1 minute from the end bound and use it as the new start time
let startTimeStamp = new Date(endBounds);
startTimeStamp.setUTCMinutes(startTimeStamp.getUTCMinutes() + 1);
const startDate = startTimeStamp.toISOString().split('T')[0];
const startMilliseconds = startTimeStamp.getMilliseconds();
const startTime = startTimeStamp
.toISOString()
.split('T')[1]
.replace(`.${startMilliseconds}Z`, '');
// Set fixed timespan mode to the future so that "now" is out of bounds.
await setTimeConductorBounds(page, {
startDate,
endDate,
startTime,
endTime
});
await expect(page.getByLabel('Now Marker')).toBeHidden();
});
});
});

View File

@ -108,4 +108,42 @@ test.describe('Plot Controls', () => {
// Expect before and after plot points to match
await expect(plotPixelSizeAtPause).toEqual(plotPixelSizeAfterWait);
});
/*
Test to verify that switching a plot's time context from global to
its own independent time context and then back to global context works correctly.
After switching from fixed time mode (ITC) to real time mode (global context),
the pause control for the plot should be available, indicating that it is following the right context.
*/
test('Plots follow the right time context', async ({ page }) => {
// Set global time conductor to real-time mode
await setRealTimeMode(page);
// hover over plot for plot controls
await page.getByLabel('Plot Canvas').hover();
// Ensure pause control is visible since global time conductor is in Real time mode.
await expect(page.getByTitle('Pause incoming real-time data')).toBeVisible();
// Toggle independent time conductor ON
await page.getByLabel('Enable Independent Time Conductor').click();
// Bring up the independent time conductor popup and switch to fixed time mode
await page.getByLabel('Independent Time Conductor Settings').click();
await page.getByLabel('Independent Time Conductor Mode Menu').click();
await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click();
// hover over plot for plot controls
await page.getByLabel('Plot Canvas').hover();
// Ensure pause control is no longer visible since the plot is following the independent time context
await expect(page.getByTitle('Pause incoming real-time data')).toBeHidden();
// Toggle independent time conductor OFF - Note that the global time conductor is still in Real time mode
await page.getByLabel('Disable Independent Time Conductor').click();
// hover over plot for plot controls
await page.getByLabel('Plot Canvas').hover();
// Ensure pause control is visible since the global time conductor is in real time mode
await expect(page.getByTitle('Pause incoming real-time data')).toBeVisible();
});
});

View File

@ -0,0 +1,58 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2025, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
* This test suite is dedicated to testing the rendering and interaction of plots.
*
*/
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Plot Controls in compact mode', () => {
let timeStrip;
test.beforeEach(async ({ page }) => {
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'domcontentloaded' });
timeStrip = await createDomainObjectWithDefaults(page, {
type: 'Time Strip'
});
// Create an overlay plot with a sine wave generator
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: timeStrip.uuid
});
await page.goto(`${timeStrip.url}`);
});
test('Plots show cursor guides', async ({ page }) => {
// hover over plot for plot controls
await page.getByLabel('Plot Canvas').hover();
// click on cursor guides control
await page.getByTitle('Toggle cursor guides').click();
await page.getByLabel('Plot Canvas').hover();
await expect(page.getByLabel('Vertical cursor guide')).toBeVisible();
await expect(page.getByLabel('Horizontal cursor guide')).toBeVisible();
});
});

View File

@ -359,6 +359,18 @@ class IndependentTimeContext extends TimeContext {
}
}
/**
* @returns {boolean}
* @override
*/
isFixed() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.isFixed(...arguments);
} else {
return super.isFixed(...arguments);
}
}
/**
* @returns {number}
* @override
@ -400,7 +412,7 @@ class IndependentTimeContext extends TimeContext {
}
/**
* Reset the time context to the global time context
* Reset the time context from the global time context
*/
resetContext() {
if (this.upstreamTimeContext) {
@ -428,6 +440,10 @@ class IndependentTimeContext extends TimeContext {
// Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.getBounds());
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
// Also emit the mode in case it's different from previous time context
if (this.getMode()) {
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.getMode()));
}
}
/**
@ -502,6 +518,10 @@ class IndependentTimeContext extends TimeContext {
// Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.getBounds());
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
// Also emit the mode in case it's different from the global time context
if (this.getMode()) {
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.getMode()));
}
// now that the view's context is set, tell others to check theirs in case they were following this view's context.
this.globalTimeContext.emit('refreshContext', viewKey);
}

View File

@ -23,6 +23,7 @@
import { FIXED_MODE_KEY, REALTIME_MODE_KEY } from '@/api/time/constants';
import IndependentTimeContext from '@/api/time/IndependentTimeContext';
import { TIME_CONTEXT_EVENTS } from './constants';
import GlobalTimeContext from './GlobalTimeContext.js';
/**
@ -142,7 +143,7 @@ class TimeAPI extends GlobalTimeContext {
addIndependentContext(key, value, clockKey) {
let timeContext = this.getIndependentContext(key);
//stop following upstream time context since the view has it's own
//stop following upstream time context since the view has its own
timeContext.resetContext();
if (clockKey) {
@ -152,6 +153,9 @@ class TimeAPI extends GlobalTimeContext {
timeContext.setMode(FIXED_MODE_KEY, value);
}
// Also emit the mode in case it's different from the previous time context
timeContext.emit(TIME_CONTEXT_EVENTS.modeChanged, structuredClone(timeContext.getMode()));
// Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context
this.emit('refreshContext', key);

View File

@ -80,6 +80,7 @@ class TooltipAPI {
* @property {string} tooltipText text to show in the tooltip
* @property {TOOLTIP_LOCATIONS} tooltipLocation location to show the tooltip relative to the parentElement
* @property {HTMLElement} parentElement reference to the DOM node we're adding the tooltip to
* @property {Array} cssClasses css classes to use with the tool tip element
*/
/**

View File

@ -48,7 +48,7 @@ const tooltipHelpers = {
.reverse()
.join(' / ');
},
buildToolTip(tooltipText, tooltipLocation, elementRef) {
buildToolTip(tooltipText, tooltipLocation, elementRef, cssClasses) {
if (!tooltipText || tooltipText.length < 1) {
return;
}
@ -59,7 +59,8 @@ const tooltipHelpers = {
this.tooltip = this.openmct.tooltips.tooltip({
toolTipText: tooltipText,
toolTipLocation: tooltipLocation,
parentElement: parentElement
parentElement: parentElement,
cssClasses
});
},
hideToolTip() {

View File

@ -34,7 +34,10 @@ export default function EventTimestripViewProvider(openmct, extendedLinesBus) {
const hasDomain = metadata.valuesForHints(['domain']).length > 0;
const hasNoRange = !metadata.valuesForHints(['range'])?.length;
return hasDomain && hasNoRange;
// for the moment, let's also exclude telemetry with images
const hasNoImages = !metadata.valuesForHints(['image']).length;
return hasDomain && hasNoRange && hasNoImages;
}
return {
@ -42,7 +45,8 @@ export default function EventTimestripViewProvider(openmct, extendedLinesBus) {
name: 'Event Timeline View',
cssClass: 'icon-event',
priority: function () {
return 6000; // big number!
// We want this to be higher priority than the TelemetryTableView
return openmct.priority.HIGH;
},
canView: function (domainObject, objectPath) {
const isChildOfTimeStrip = objectPath.some((object) => object.type === 'time-strip');

View File

@ -21,29 +21,47 @@
-->
<template>
<div ref="events" class="c-events-tsv js-events-tsv" :style="alignmentStyle" />
<div ref="events" class="c-events-tsv js-events-tsv" :style="alignmentStyle">
<SwimLane v-if="eventItems.length" :is-nested="true" :hide-label="true">
<template #object>
<div ref="eventsContainer" class="c-events-tsv__container">
<div
v-for="event in eventItems"
:id="`wrapper-${event.time}`"
:ref="`wrapper-${event.time}`"
:key="event.id"
:aria-label="titleKey ? `${event[titleKey]}` : ''"
class="c-events-tsv__event-line"
:class="event.limitClass || ''"
:style="`left: ${event.left}px`"
@mouseover="showToolTip(event)"
@mouseleave="dismissToolTip()"
@click.stop="createSelectionForInspector(event)"
></div>
</div>
</template>
</SwimLane>
<div v-else class="c-timeline__no-items">No events within timeframe</div>
</div>
</template>
<script>
import { scaleLinear, scaleUtc } from 'd3-scale';
import _ from 'lodash';
import mount from 'utils/mount';
import { inject } from 'vue';
import SwimLane from '@/ui/components/swim-lane/SwimLane.vue';
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
import { useAlignment } from '../../../ui/composables/alignmentContext.js';
import eventData from '../mixins/eventData.js';
const PADDING = 1;
const CONTAINER_CLASS = 'c-events-tsv__container';
const NO_ITEMS_CLASS = 'c-timeline__no-items';
const EVENT_WRAPPER_CLASS = 'c-events-tsv__event-line';
const ID_PREFIX = 'wrapper-';
const AXES_PADDING = 20;
export default {
mixins: [eventData],
components: { SwimLane },
mixins: [eventData, tooltipHelpers],
inject: ['openmct', 'domainObject', 'objectPath', 'extendedLinesBus'],
setup() {
const domainObject = inject('domainObject');
@ -53,19 +71,10 @@ export default {
return { alignmentData };
},
data() {
const timeSystem = this.openmct.time.getTimeSystem();
this.valueMetadata = {};
this.requestCount = 0;
return {
viewBounds: null,
height: 0,
eventItems: [],
eventHistory: [],
timeSystem: timeSystem,
extendLines: false,
titleKey: null,
tooltip: null,
selectedEvent: null
titleKey: null
};
},
computed: {
@ -82,24 +91,26 @@ export default {
},
watch: {
eventHistory: {
handler(newHistory, oldHistory) {
this.updatePlotEvents();
handler() {
this.updateEventItems();
},
deep: true
},
alignmentData: {
handler() {
this.updateViewBounds();
this.setScaleAndPlotEvents(this.timeSystem);
},
deep: true
}
},
created() {
this.valueMetadata = {};
this.height = 0;
this.timeSystem = this.openmct.time.getTimeSystem();
this.extendLines = false;
},
mounted() {
this.setDimensions();
this.setScaleAndPlotEvents = this.setScaleAndPlotEvents.bind(this);
this.updateViewBounds = this.updateViewBounds.bind(this);
this.setTimeContext = this.setTimeContext.bind(this);
this.setTimeContext();
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);
@ -175,7 +186,7 @@ export default {
const clientWidth = this.getClientWidth();
if (clientWidth !== this.width) {
this.setDimensions();
this.updateViewBounds();
this.setScaleAndPlotEvents(this.timeSystem);
}
},
getClientWidth() {
@ -200,14 +211,14 @@ export default {
this.setScaleAndPlotEvents(this.timeSystem, !isTick);
},
setScaleAndPlotEvents(timeSystem, clearAllEvents) {
setScaleAndPlotEvents(timeSystem) {
if (timeSystem) {
this.timeSystem = timeSystem;
this.timeFormatter = this.getFormatter(this.timeSystem.key);
}
this.setScale(this.timeSystem);
this.updatePlotEvents(clearAllEvents);
this.updateEventItems();
},
getFormatter(key) {
const metadata = this.openmct.telemetry.getMetadata(this.domainObject);
@ -217,38 +228,19 @@ export default {
return valueFormatter;
},
updatePlotEvents(clearAllEvents) {
this.clearPreviousEvents(clearAllEvents);
updateEventItems() {
if (this.xScale) {
this.drawEvents();
}
},
clearPreviousEvents(clearAllEvents) {
//TODO: Only clear items that are out of bounds
let noItemsEl = this.$el.querySelectorAll(`.${NO_ITEMS_CLASS}`);
noItemsEl.forEach((item) => {
item.remove();
});
const events = this.$el.querySelectorAll(`.${EVENT_WRAPPER_CLASS}`);
events.forEach((eventElm) => {
if (clearAllEvents) {
eventElm.remove();
} else {
const id = eventElm.getAttributeNS(null, 'id');
if (id) {
const timestamp = id.replace(ID_PREFIX, '');
if (
!this.isEventInBounds({
time: timestamp
})
) {
eventElm.remove();
}
}
this.eventItems = this.eventHistory.map((eventHistoryItem) => {
const limitClass = this.getLimitClass(eventHistoryItem);
return {
...eventHistoryItem,
left: this.xScale(eventHistoryItem.time),
limitClass
};
});
if (this.extendLines) {
this.emitExtendedLines();
}
});
if (this.extendLines) {
this.emitExtendedLines();
}
},
setDimensions() {
@ -276,92 +268,6 @@ export default {
this.xScale.range([PADDING, this.width - PADDING * 2]);
},
isEventInBounds(evenObj) {
return evenObj.time <= this.viewBounds.end && evenObj.time >= this.viewBounds.start;
},
getEventsContainer() {
let eventContainer;
let existingContainer = this.$el.querySelector(`.${CONTAINER_CLASS}`);
if (existingContainer) {
eventContainer = existingContainer;
} else {
if (this.destroyEventsContainer) {
this.destroyEventsContainer();
}
const { vNode, destroy } = mount(
{
components: {
SwimLane
},
provide: {
openmct: this.openmct
},
data() {
return {
isNested: true
};
},
template: `<swim-lane :is-nested="isNested" :hide-label="true">
<template v-slot:object>
<div class="c-events-tsv__container"/>
</template>
</swim-lane>`
},
{
app: this.openmct.app
}
);
this.destroyEventsContainer = destroy;
const component = vNode.componentInstance;
this.$refs.events.appendChild(component.$el);
eventContainer = component.$el.querySelector(`.${CONTAINER_CLASS}`);
}
return eventContainer;
},
drawEvents() {
let eventContainer = this.getEventsContainer();
if (this.eventHistory.length) {
this.eventHistory.forEach((currentEventObject) => {
if (this.isEventInBounds(currentEventObject)) {
this.plotEvents(currentEventObject, eventContainer);
}
});
} else {
this.plotNoItems(eventContainer);
}
},
plotNoItems(containerElement) {
const textElement = document.createElement('div');
textElement.classList.add(NO_ITEMS_CLASS);
textElement.innerHTML = 'No events within timeframe';
containerElement.appendChild(textElement);
},
getEventWrapper(item) {
const id = `${ID_PREFIX}${item.time}`;
return this.$el.querySelector(`.js-events-tsv div[id=${id}]`);
},
plotEvents(item, containerElement) {
const existingEventWrapper = this.getEventWrapper(item);
if (existingEventWrapper) {
this.updateExistingEventWrapper(existingEventWrapper, item);
} else {
const eventWrapper = this.createEventWrapper(item);
containerElement.appendChild(eventWrapper);
}
if (this.extendLines) {
this.emitExtendedLines();
}
},
updateExistingEventWrapper(existingEventWrapper, event) {
existingEventWrapper.style.left = `${this.xScale(event.time)}px`;
},
createPathSelection(eventWrapper) {
const selection = [];
selection.unshift({
@ -381,7 +287,18 @@ export default {
return selection;
},
createSelectionForInspector(event, eventWrapper) {
setSelection() {
let childContext = {};
childContext.item = this.childObject;
this.context = childContext;
if (this.removeSelectable) {
this.removeSelectable();
}
this.removeSelectable = this.openmct.selection.selectable(this.$el, this.context);
},
createSelectionForInspector(event) {
const eventWrapper = this.$refs[`wrapper-${event.time}`][0];
const eventContext = {
type: 'time-strip-event-selection',
event
@ -414,70 +331,38 @@ export default {
const limitEvaluation = this.limitEvaluator.evaluate(event, this.valueMetadata);
return limitEvaluation?.cssClass;
},
createEventWrapper(event) {
const id = `${ID_PREFIX}${event.time}`;
const eventWrapper = document.createElement('div');
eventWrapper.setAttribute('id', id);
eventWrapper.classList.add(EVENT_WRAPPER_CLASS);
eventWrapper.style.left = `${this.xScale(event.time)}px`;
if (this.titleKey) {
const textToShow = event[this.titleKey];
eventWrapper.ariaLabel = textToShow;
eventWrapper.addEventListener('mouseover', () => {
this.showToolTip(textToShow, eventWrapper, event);
this.extendedLinesBus.updateHoverExtendEventLine(this.keyString, event.time);
});
eventWrapper.addEventListener('mouseleave', () => {
this.tooltip?.destroy();
this.extendedLinesBus.updateHoverExtendEventLine(this.keyString, null);
});
}
const limitClass = this.getLimitClass(event);
if (limitClass) {
eventWrapper.classList.add(limitClass);
event.limitClass = limitClass;
}
eventWrapper.addEventListener('click', (mouseEvent) => {
mouseEvent.stopPropagation();
this.createSelectionForInspector(event, eventWrapper);
});
return eventWrapper;
},
emitExtendedLines() {
if (this.extendLines) {
const lines = this.eventHistory
.filter((e) => this.isEventInBounds(e))
.map((e) => ({ x: this.xScale(e.time), limitClass: e.limitClass, id: e.time }));
this.extendedLinesBus.emit('update-extended-lines', {
lines,
keyString: this.keyString
});
} else {
this.extendedLinesBus.emit('update-extended-lines', {
lines: [],
keyString: this.keyString
});
}
},
showToolTip(textToShow, referenceElement, event) {
showToolTip(event) {
const aClasses = ['c-events-tooltip'];
const limitClass = this.getLimitClass(event);
if (limitClass) {
aClasses.push(limitClass);
if (event.limitClass) {
aClasses.push(event.limitClass);
}
const showToLeft = false; // Temp, stubbed in
if (showToLeft) {
aClasses.push('--left');
}
this.tooltip = this.openmct.tooltips.tooltip({
toolTipText: textToShow,
toolTipLocation: this.openmct.tooltips.TOOLTIP_LOCATIONS.RIGHT,
parentElement: referenceElement,
cssClasses: [aClasses.join(' ')]
});
this.buildToolTip(
this.titleKey ? `${event[this.titleKey]}` : '',
this.openmct.tooltips.TOOLTIP_LOCATIONS.RIGHT,
`wrapper-${event.time}`,
[aClasses.join(' ')]
);
this.extendedLinesBus.updateHoverExtendEventLine(this.keyString, event.time);
},
dismissToolTip() {
this.hideToolTip();
this.extendedLinesBus.updateHoverExtendEventLine(this.keyString, null);
},
emitExtendedLines() {
let lines = [];
if (this.extendLines) {
lines = this.eventItems.map((e) => ({
x: e.left,
limitClass: e.limitClass,
id: e.time
}));
}
this.extendedLinesBus.updateExtendedLines(this.keyString, lines);
}
}
};

View File

@ -39,9 +39,6 @@ export default function ImageryTimestripViewProvider(openmct) {
key: type,
name: 'Imagery Timestrip View',
cssClass: 'icon-image',
priority: function () {
return 7000; // big number!
},
canView: function (domainObject, objectPath) {
let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip');

View File

@ -260,7 +260,6 @@ export default {
let existingContainer = this.$el.querySelector(`.${CONTAINER_CLASS}`);
if (existingContainer) {
imageryContainer = existingContainer;
// imageryContainer.style.maxWidth = `${containerWidth}px`;
} else {
if (this.destroyImageryContainer) {
this.destroyImageryContainer();
@ -290,7 +289,6 @@ export default {
this.$refs.imageryHolder.appendChild(component.$el);
imageryContainer = component.$el.querySelector(`.${CONTAINER_CLASS}`);
// imageryContainer.style.maxWidth = `${containerWidth}px`;
}
return imageryContainer;

View File

@ -164,11 +164,13 @@
<div
v-show="cursorGuide"
ref="cursorGuideVertical"
aria-label="Vertical cursor guide"
class="c-cursor-guide--v js-cursor-guide--v"
></div>
<div
v-show="cursorGuide"
ref="cursorGuideHorizontal"
aria-label="Horizontal cursor guide"
class="c-cursor-guide--h js-cursor-guide--h"
></div>
</div>
@ -537,6 +539,7 @@ export default {
this.followTimeContext();
},
followTimeContext() {
this.updateMode();
this.updateDisplayBounds(this.timeContext.getBounds());
this.timeContext.on('modeChanged', this.updateMode);
this.timeContext.on('boundsChanged', this.updateDisplayBounds);
@ -854,13 +857,11 @@ export default {
this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1];
if (!this.options.compact) {
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
this.listenTo(this.canvas, 'click', this.selectNearbyAnnotations, this);
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
}
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
this.listenTo(this.canvas, 'click', this.selectNearbyAnnotations, this);
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
},
marqueeAnnotations(annotationsToSelect) {
@ -1115,19 +1116,21 @@ export default {
this.listenTo(window, 'mouseup', this.onMouseUp, this);
this.listenTo(window, 'mousemove', this.trackMousePosition, this);
// track frozen state on mouseDown to be read on mouseUp
const isFrozen =
this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
this.isFrozenOnMouseDown = isFrozen;
if (!this.options.compact) {
// track frozen state on mouseDown to be read on mouseUp
const isFrozen =
this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
this.isFrozenOnMouseDown = isFrozen;
if (event.altKey && !event.shiftKey) {
return this.startPan(event);
} else if (event.altKey && event.shiftKey) {
this.freeze();
if (event.altKey && !event.shiftKey) {
return this.startPan(event);
} else if (event.altKey && event.shiftKey) {
this.freeze();
return this.startMarquee(event, true);
} else {
return this.startMarquee(event, false);
return this.startMarquee(event, true);
} else {
return this.startMarquee(event, false);
}
}
},
@ -1158,11 +1161,15 @@ export default {
},
isMouseClick() {
if (!this.marquee) {
// We may not have a marquee if we've disabled pan/zoom, but we still need to know if it's a mouse click for highlights and lock points.
if (!this.marquee && !this.positionOverPlot) {
return false;
}
const { start, end } = this.marquee;
const { start, end } = this.marquee ?? {
start: this.positionOverPlot,
end: this.positionOverPlot
};
const someYPositionOverPlot = start.y.some((y) => y);
return start.x === end.x && someYPositionOverPlot;

View File

@ -162,14 +162,6 @@ export default {
}
}
},
watch: {
gridLines(newGridLines) {
this.gridLines = newGridLines;
},
cursorGuide(newCursorGuide) {
this.cursorGuide = newCursorGuide;
}
},
created() {
eventHelpers.extend(this);
this.imageExporter = new ImageExporter(this.openmct);

View File

@ -243,12 +243,20 @@ export default {
this.timeContext.off(TIME_CONTEXT_EVENTS.modeChanged, this.setTimeOptionsMode);
},
setTimeOptionsClock(clock) {
// If the user has persisted any time options, then don't override them with global settings.
if (this.independentTCEnabled) {
return;
}
this.setTimeOptionsOffsets();
this.timeOptions.clock = clock.key;
},
setTimeOptionsMode(mode) {
this.setTimeOptionsOffsets();
this.timeOptions.mode = mode;
// If the user has persisted any time options, then don't override them with global settings.
if (this.independentTCEnabled) {
this.setTimeOptionsOffsets();
this.timeOptions.mode = mode;
this.isFixed = this.timeOptions.mode === FIXED_MODE_KEY;
}
},
setTimeOptionsOffsets() {
this.timeOptions.clockOffsets =

View File

@ -22,8 +22,8 @@
import { EventEmitter } from 'eventemitter3';
export default class ExtendedLinesBus extends EventEmitter {
updateExtendedLines(keyString, lineData) {
this.emit('update-extended-lines', { lineData, keyString });
updateExtendedLines(keyString, lines) {
this.emit('update-extended-lines', { lines, keyString });
}
disableExtendEventLines(keyString) {
this.emit('disable-extended-lines', keyString);

View File

@ -29,7 +29,7 @@
<div
v-for="(line, index) in lines"
:id="line.id"
:key="index"
:key="`${index - line.id}`"
class="c-timeline__event-line--extended"
:class="[
line.limitClass,

View File

@ -29,7 +29,7 @@
:domain-object="item.domainObject"
:button-title="`Toggle extended event lines overlay for ${item.domainObject.name}`"
button-icon="icon-arrows-up-down"
:hide-button="!hasEventTelemetry()"
:hide-button="!item.isEventTelemetry"
:button-click-on="enableExtendEventLines"
:button-click-off="disableExtendEventLines"
:style="[{ 'flex-basis': sizeString }]"
@ -156,18 +156,6 @@ export default {
const keyString = this.openmct.objects.makeKeyString(this.item.domainObject.identifier);
this.extendedLinesBus.disableExtendEventLines(keyString);
},
hasEventTelemetry() {
const metadata = this.openmct.telemetry.getMetadata(this.item.domainObject);
if (!metadata) {
return false;
}
const hasDomain = metadata.valuesForHints(['domain']).length > 0;
const hasNoRange = !metadata.valuesForHints(['range'])?.length;
// for the moment, let's also exclude telemetry with images
const hasNoImages = !metadata.valuesForHints(['image']).length;
return hasDomain && hasNoRange && hasNoImages;
},
setActionCollection(actionCollection) {
this.openmct.menus.actionsToMenuItems(
actionCollection.getVisibleActions(),

View File

@ -260,7 +260,7 @@ export default {
} else if (domainObject.type === 'gantt-chart') {
rowCount = Object.keys(domainObject.configuration.swimlaneVisibility).length;
}
const isEventTelemetry = this.hasEventTelemetry(domainObject);
let height =
domainObject.type === 'telemetry.plot.stacked'
? `${domainObject.composition.length * PLOT_ITEM_H_PX}px`
@ -271,7 +271,8 @@ export default {
type,
keyString,
rowCount,
height
height,
isEventTelemetry
};
this.items.push(item);
@ -288,6 +289,18 @@ export default {
this.addContainer(container);
}
},
hasEventTelemetry(domainObject) {
const metadata = this.openmct.telemetry.getMetadata(domainObject);
if (!metadata) {
return false;
}
const hasDomain = metadata.valuesForHints(['domain']).length > 0;
const hasNoRange = !metadata.valuesForHints(['range'])?.length;
// for the moment, let's also exclude telemetry with images
const hasNoImages = !metadata.valuesForHints(['image']).length;
return hasDomain && hasNoRange && hasNoImages;
},
removeItem(identifier) {
let index = this.items.findIndex((item) =>
this.openmct.objects.areIdsEqual(identifier, item.domainObject.identifier)

View File

@ -20,8 +20,8 @@
at runtime from the About dialog for additional information.
-->
<template>
<div ref="axisHolder" class="c-timesystem-axis" :style="alignmentStyle">
<div class="c-timesystem-axis__mb-line" :style="nowMarkerStyle"></div>
<div ref="axisHolder" class="c-timesystem-axis">
<div class="c-timesystem-axis__mb-line" :style="nowMarkerStyle" aria-label="Now Marker"></div>
<svg :width="svgWidth" :height="svgHeight">
<g class="axis" :transform="axisTransform"></g>
</svg>
@ -117,8 +117,12 @@ export default {
if (this.alignmentData.leftWidth) {
leftOffset = this.alignmentData.multiple ? 2 * AXES_PADDING : AXES_PADDING;
}
this.axisTransform = `translate(${this.alignmentData.leftWidth + leftOffset}, 20)`;
this.leftAlignmentOffset = this.alignmentData.leftWidth + leftOffset;
this.alignmentOffset =
this.leftAlignmentOffset + this.alignmentData.rightWidth + rightOffset;
this.alignmentStyle = {
margin: `0 ${this.alignmentData.rightWidth + rightOffset}px 0 ${this.alignmentData.leftWidth + leftOffset}px`
margin: `0 ${this.leftAlignmentOffset + this.alignmentData.rightWidth + rightOffset}px 0 ${this.alignmentData.leftWidth + leftOffset}px`
};
this.refresh();
},
@ -178,14 +182,14 @@ export default {
this.nowMarkerStyle.top = TIME_AXIS_LINE_Y + 'px';
const nowTimeStamp = this.openmct.time.now();
const now = this.xScale(nowTimeStamp);
this.nowMarkerStyle.left = `${now}px`;
this.nowMarkerStyle.left = `${now + this.leftAlignmentOffset}px`;
if (now < 0 || now > this.width) {
nowMarker.classList.add('hidden');
}
}
},
setDimensions() {
this.width = this.axisHolder.clientWidth;
this.width = this.axisHolder.clientWidth - (this.alignmentOffset ?? 0);
this.height = Math.round(this.axisHolder.getBoundingClientRect().height);
if (this.useSVG) {