Merge branch 'master' of https://github.com/nasa/openmct into 7936-add-discrete-event-visualization

This commit is contained in:
Shefali 2025-03-05 16:11:21 -08:00
commit 74a5d7e1f3
13 changed files with 252 additions and 58 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

@ -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

@ -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) {