Compare commits

...

66 Commits

Author SHA1 Message Date
1aee416194 Merge branch 'master' of https://github.com/nasa/openmct into 7936-add-discrete-event-visualization-refactor 2025-03-05 16:07:57 -08:00
aa5fa468b5 Use the tooltips mixin 2025-03-05 15:23:53 -08:00
3ad64f08c5 Refactor code to
1) reduce call to instance method
2) Use existing event line bus functionality
3) move non reactive properties to the created lifecycle hook
2025-03-05 12:33:17 -08:00
1ced12d22f Remove the priority for imagerytimestripviewprovider and reduce the priority for eventtimelineviewprovider to HIGH.
Also add a condition to the eventtimelineview to reject objects that have imagery (this is to promote the imagerytimestripview to handle those objets)
2025-03-05 12:21:26 -08:00
9e68514b27 Removed commented out code 2025-03-05 11:32:17 -08:00
32a0e15691 handle case where we only have events in timeline 2025-01-06 10:40:51 +01:00
0e940b2883 lint and simplify playwright locator 2025-01-06 10:09:39 +01:00
15b674f3d1 Closes #7936
- Fix left and right `alignmentData` offsets that were not being applied to the correct element.
2024-12-19 19:17:28 -08:00
0933d27ce6 Closes #7936
- Fix left and right `alignmentData` offsets in
EventTimelineView.vue, ImageryTimeView.vue and ActivityTimeline.vue.
2024-12-19 18:09:17 -08:00
f163034e18 Closes #7936
- Sanding and shimming on imagery and events TS look and feel.
- Fixed scrollbar issue in imagery TS view when thumb goes beyond the right edge of the time frame.
2024-12-19 17:34:31 -08:00
e6cb940ee7 Closes #7936
- WIP prepping activities view for adjustable swimlane height.
- Refactored ActivityTimeline.vue to not draw SVG if no activities in timeframe.
- ActivityTimeline.vue `leftOffset` now uses absolute position `left` instead of `left-margin`.
2024-12-19 15:25:30 -08:00
cfa2129660 Closes #7936
- Consolidate `__no-items` message style into timeline.scss.
2024-12-19 15:06:22 -08:00
6cafa7a22d Closes #7936
- Add in alignmentData to set the left edge of the imagery-tsv element properly.
2024-12-19 12:45:18 -08:00
9522040929 Closes #7936
- Significant improvements for Time Strip imagery view ahead of user-settable swimlane heights.
- Imagery container height, image height and width no longer set in code.
- Imagery swimlane now uses styles and hover behavior consistent with events.
2024-12-19 12:35:20 -08:00
5b28086f95 Closes #7936
- CSS cleanups.
2024-12-19 12:30:45 -08:00
bb4fea78f5 Closes #7936
- Swimlane style refinements.
- New theme constants for swimlane colors.
- Time Strip label column buttons aligned right.
2024-12-19 09:56:28 -08:00
5312458776 Closes #7936
- Fixed swimlane button markup.
- CSS cleanup.
2024-12-18 17:26:18 -08:00
3c24205b67 Merge remote-tracking branch 'origin/7936-add-discrete-event-visualization' into 7936-add-discrete-event-visualization 2024-12-18 16:39:08 -08:00
65b1f0256d Closes #7936
- Fixed previous change that broke grid layout of Stacked Plots in Time Strip.
- Re-enabled code that sets min-height for Stacked Plots in Time Strip based
on the number of children.
2024-12-18 16:38:57 -08:00
8c72e4a062 Closes #7936
- Remove `c-menu` from Tooltip.
- Tooltip component tweaks.
2024-12-18 11:46:47 -08:00
601fc33e75 trigger off selection for extended line hilight 2024-12-18 20:39:55 +01:00
638b03c17d spelling 2024-12-18 16:23:23 +01:00
531ef3ef1b good job tests 2024-12-18 15:54:51 +01:00
68fc3172a0 Merge branch '7936-add-discrete-event-visualization' of github.com:nasa/openmct into 7936-add-discrete-event-visualization 2024-12-18 09:19:57 +01:00
51d96544ec fix selection issue 2024-12-18 09:19:50 +01:00
546714b3dc Closes #7936
- Styling added to tooltip for event severity.
2024-12-17 23:44:58 -08:00
099153ba4e Closes #7936
- Changed `--hovered` to `--hilite`.
2024-12-17 16:42:30 -08:00
27af030566 Mods to Event Generator and limit provider
- Changed SEVERE to use `is-event--purple` style.
- Mods to EventTelemetryProvider.js:
  - Adds a more random start time to each event.
  - Reduces frequency at which a severity is applied to events.
2024-12-17 16:27:45 -08:00
b865d8c038 Closes #7936
- Moved all event line styling into events-view.scss.
- Refactor `*__event-wrapper` to `*__event-line`.
- Event line color styling for hover and `s-select`.
- New theme constants for `$colorEvent<color>Line`.
- Removed `--no-style` CSS class; created unnecessary need to override.
2024-12-17 16:23:44 -08:00
2ae1fe1579 resolve conflicts 2024-12-17 20:08:30 +01:00
cba7c7f8ed remove is selected, add hover event for extended liens 2024-12-17 20:04:16 +01:00
49a106b79e Closes #7936
- Remove `element-handle`.
2024-12-17 09:56:41 -08:00
f4ec532357 add tests 2024-12-17 13:44:49 +01:00
72ff0bced6 start e2e testing 2024-12-17 12:31:35 +01:00
36d31973fe bump priority for our timeline view 2024-12-17 11:46:57 +01:00
3af9083f89 add a random severity 2024-12-17 10:50:16 +01:00
2ba6bc9c73 ensure metadata exists on events 2024-12-17 10:21:48 +01:00
aaa2e43796 Closes #7936
- Removed event handle again.
2024-12-16 17:43:16 -08:00
6bda108e95 Closes #7936
- Layout converted to set `min-height` on top-most `c-swimlane` element.
Interior containers now use 100% height or absolute positioning.
- Removed `c-timeline-holder` from `c-events-tsv` in EventTimelineView.vue;
Refactored `c-events-tsv__contents` to be `js-events-tsv` as that was being used as a reference.
- New theme constant `eventLineW` sets event lines to be 1px wide for more precision.
2024-12-16 16:14:38 -08:00
20426fe359 add tooltip class and only offset swimlane 2024-12-16 20:30:16 +01:00
20247bbd87 only add left margin to container 2024-12-16 20:19:52 +01:00
62b4975d57 add selection class 2024-12-16 13:19:43 +01:00
d048af108e add tooltip 2024-12-16 12:39:23 +01:00
cda7cc9d06 fix extended lines 2024-12-16 12:15:59 +01:00
d97f7c347b resolve conflicts 2024-12-16 11:50:26 +01:00
781d83410a add hovered effect for extended lines 2024-12-16 11:39:34 +01:00
64bd625d0b remove debugging code and extraneous classes 2024-12-16 10:31:47 +01:00
3d3f093c7e Closes #7960
- Removed bad `}` in TimeSystemAxis.vue.
- Removed `.u-contents` from line 129 of ganttChart.e2e.spec.js.
- Removed `event-handle` element; not needed.
- Changed `__event-wrapper` to not set height explicitly; uses absolute positioning.
- Added :before element to event-wrapper for better hit area.
- Improved hover styling.
- $colorEvent* style constants added to theme constant SCSS files.
2024-12-13 17:42:46 -08:00
38292953fc Closes #7960
- Removed in-page `style` defs from ExtendedLinesOverlay.vue; CSS actually located in timeline.scss.
- Improved sizing and style for Marcus Bains ("now") line.
- Removed extraneous padding at bottom of plot view when in Time Strip.
- Added missing header info to timeline.scss.
- CSS refinements.
2024-12-13 14:43:55 -08:00
96d8870f22 watch for left offset changes 2024-12-13 15:47:44 +01:00
aaec052783 add title 2024-12-13 12:36:02 +01:00
6f26add740 works per swimlane now 2024-12-13 11:39:58 +01:00
052129ba87 ensure colored lines work 2024-12-13 10:35:18 +01:00
d046ad13ff extended events 2024-12-12 16:06:36 +01:00
e9f120a480 button works 2024-12-12 14:55:31 +01:00
0db301dea8 add facility to send action to mounted component regarding extending lines 2024-12-12 14:05:14 +01:00
3b236cc33b pass event bus 2024-12-11 17:20:08 +01:00
8b5e2f4595 remove undefined 2024-12-11 11:55:22 +01:00
5b006b69b7 inspector and colors work 2024-12-11 11:34:07 +01:00
2776cc8ac9 inspector works 2024-12-11 11:06:02 +01:00
55bed6a525 Merge remote-tracking branch 'origin/master' into 7936-add-discrete-event-visualization 2024-12-11 09:32:09 +01:00
944634d759 adding inspector 2024-12-10 18:21:38 +01:00
7af3996d29 fix events not being removed 2024-12-10 12:01:27 +01:00
7b22cf3371 going to try with YAMCS data 2024-12-10 11:16:47 +01:00
680b0953b2 more scaffolding 2024-12-09 16:46:56 +01:00
3159de08b1 initial structure 2024-12-09 10:32:42 +01:00
42 changed files with 1794 additions and 195 deletions

View File

@ -126,7 +126,7 @@ test.describe('Gantt Chart', () => {
await page.goto(ganttChart.url);
// Assert that the Plan's status is displayed as draft
expect(await page.locator('.u-contents.c-swimlane.is-status--draft').count()).toBe(
expect(await page.locator('.c-swimlane.is-status--draft').count()).toBe(
Object.keys(testPlan1).length
);
});

View File

@ -0,0 +1,110 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createDomainObjectWithDefaults, setTimeConductorBounds } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Event Timeline View', () => {
let eventTimelineView;
let eventGenerator1;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
eventTimelineView = await createDomainObjectWithDefaults(page, {
type: 'Time Strip'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: eventTimelineView.uuid
});
eventGenerator1 = await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator',
parent: eventTimelineView.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Event Message Generator with Acknowledge',
parent: eventTimelineView.uuid
});
await setTimeConductorBounds(page, {
startDate: '2024-01-01',
endDate: '2024-01-01',
startTime: '01:01:00',
endTime: '01:04:00'
});
});
test('Ensure we can build a Time Strip with event', async ({ page }) => {
await page.goto(eventTimelineView.url);
// click on an event
await page
.getByLabel(eventTimelineView.name)
.getByLabel(/PROGRAM ALARM/)
.click();
// click on the event inspector tab
await page.getByRole('tab', { name: 'Event' }).click();
// ensure the event inspector has the the same event
await expect(page.getByText(/PROGRAM ALARM/)).toBeVisible();
// count the event lines
const eventWrappersContainer = page.locator('.c-events-tsv__container');
const eventWrappers = eventWrappersContainer.locator('.c-events-tsv__event-line');
const expectedEventWrappersCount = 25;
await expect(eventWrappers).toHaveCount(expectedEventWrappersCount);
// click on another event
await page
.getByLabel(eventTimelineView.name)
.getByLabel(/pegged/)
.click();
// ensure the tooltip shows up
await expect(
page.getByRole('tooltip').getByText(/pegged on horizontal velocity/)
).toBeVisible();
// and that event appears in the inspector
await expect(
page.getByLabel('Inspector Views').getByText(/pegged on horizontal velocity/)
).toBeVisible();
// turn on extended lines
await page
.getByRole('button', {
name: `Toggle extended event lines overlay for ${eventGenerator1.name}`
})
.click();
// count the extended lines
const overlayLinesContainer = page.locator('.c-timeline__overlay-lines');
const extendedLines = overlayLinesContainer.locator('.c-timeline__event-line--extended');
const expectedCount = 25;
await expect(extendedLines).toHaveCount(expectedCount);
});
});

View File

@ -0,0 +1,88 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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.
*****************************************************************************/
export const SEVERITY_CSS = {
WATCH: 'is-event--yellow',
WARNING: 'is-event--yellow',
DISTRESS: 'is-event--red',
CRITICAL: 'is-event--red',
SEVERE: 'is-event--purple'
};
const NOMINAL_SEVERITY = {
cssClass: 'is-event--no-style',
name: 'NOMINAL'
};
/**
* @typedef {Object} EvaluationResult
* @property {string} cssClass CSS class information
* @property {string} name a severity name
*/
export default class EventLimitProvider {
constructor(openmct) {
this.openmct = openmct;
}
getLimitEvaluator(domainObject) {
const self = this;
return {
/**
* Evaluates a telemetry datum for severity.
*
* @param {Datum} datum the telemetry datum from the historical or realtime plugin ({@link Datum})
* @param {object} valueMetadata metadata about the telemetry datum
*
* @returns {EvaluationResult} ({@link EvaluationResult})
*/
evaluate: function (datum, valueMetadata) {
// prevent applying the class to the tr, only to td
if (!valueMetadata) {
return;
}
if (datum.severity in SEVERITY_CSS) {
return self.getSeverity(datum, valueMetadata);
}
return NOMINAL_SEVERITY;
}
};
}
getSeverity(datum, valueMetadata) {
if (!valueMetadata) {
return;
}
const severityValue = datum.severity;
return {
cssClass: SEVERITY_CSS[severityValue],
name: severityValue
};
}
supportsLimits(domainObject) {
return domainObject.type === 'eventGenerator';
}
}

View File

@ -41,7 +41,10 @@ class EventMetadataProvider {
{
key: 'message',
name: 'Message',
format: 'string'
format: 'string',
hints: {
label: 0
}
}
]
}

View File

@ -24,8 +24,11 @@
* Module defining EventTelemetryProvider. Created by chacskaylo on 06/18/2015.
*/
import { SEVERITY_CSS } from './EventLimitProvider.js';
import messages from './transcript.json';
const DUR_MIN = 1000;
const DUR_MAX = 10000;
class EventTelemetryProvider {
constructor() {
this.defaultSize = 25;
@ -33,14 +36,23 @@ class EventTelemetryProvider {
generateData(firstObservedTime, count, startTime, duration, name) {
const millisecondsSinceStart = startTime - firstObservedTime;
const utc = startTime + count * duration;
const randStartDelay = Math.max(DUR_MIN, Math.random() * DUR_MAX);
const utc = startTime + randStartDelay + count * duration;
const ind = count % messages.length;
const message = messages[ind] + ' - [' + millisecondsSinceStart + ']';
// pick a random severity level + 1 for an undefined level so we can do nominal
const severity =
Math.random() > 0.4
? Object.keys(SEVERITY_CSS)[
Math.floor(Math.random() * Object.keys(SEVERITY_CSS).length + 1)
]
: undefined;
return {
name,
utc,
message
message,
severity
};
}
@ -53,7 +65,7 @@ class EventTelemetryProvider {
}
subscribe(domainObject, callback) {
const duration = domainObject.telemetry.duration * 1000;
const duration = domainObject.telemetry.duration * DUR_MIN;
const firstObservedTime = Date.now();
let count = 0;
@ -78,7 +90,7 @@ class EventTelemetryProvider {
request(domainObject, options) {
let start = options.start;
const end = Math.min(Date.now(), options.end); // no future values
const duration = domainObject.telemetry.duration * 1000;
const duration = domainObject.telemetry.duration * DUR_MIN;
const size = options.size ? options.size : this.defaultSize;
const data = [];
const firstObservedTime = options.start;

View File

@ -19,6 +19,7 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventLimitProvider from './EventLimitProvider.js';
import EventMetadataProvider from './EventMetadataProvider.js';
import EventTelemetryProvider from './EventTelemetryProvider.js';
import EventWithAcknowledgeTelemetryProvider from './EventWithAcknowledgeTelemetryProvider.js';
@ -54,5 +55,7 @@ export default function EventGeneratorPlugin(options) {
});
openmct.telemetry.addProvider(new EventWithAcknowledgeTelemetryProvider());
openmct.telemetry.addProvider(new EventLimitProvider(openmct));
};
}

View File

@ -113,7 +113,8 @@
creatable: true
})
);
openmct.install(openmct.plugins.Timeline());
const timeLinePlugin = openmct.plugins.Timeline();
openmct.install(timeLinePlugin);
openmct.install(openmct.plugins.Hyperlink());
openmct.install(openmct.plugins.UTCTimeSystem());
openmct.install(
@ -234,6 +235,7 @@
openmct.install(openmct.plugins.Timelist());
openmct.install(openmct.plugins.BarChart());
openmct.install(openmct.plugins.ScatterPlot());
openmct.install(openmct.plugins.EventTimestripPlugin(timeLinePlugin.extendedLinesBus));
document.addEventListener('DOMContentLoaded', function () {
openmct.start();
});

View File

@ -27,10 +27,11 @@ import TooltipComponent from './components/TooltipComponent.vue';
class Tooltip extends EventEmitter {
constructor(
{ toolTipText, toolTipLocation, parentElement } = {
{ toolTipText, toolTipLocation, parentElement, cssClasses } = {
tooltipText: '',
toolTipLocation: 'below',
parentElement: null
parentElement: null,
cssClasses: []
}
) {
super();
@ -42,7 +43,8 @@ class Tooltip extends EventEmitter {
provide: {
toolTipText,
toolTipLocation,
parentElement
parentElement,
cssClasses
},
template: '<tooltip-component toolTipText="toolTipText"></tooltip-component>'
});

View File

@ -80,10 +80,11 @@ 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
*/
/**
* Tooltips take an options object that consists of the string, tooltipLocation, and parentElement
* Tooltips take an options object that consists of the string, tooltipLocation, a parentElement, and an array of cssClasses
* @param {TooltipOptions} options
*/
tooltip(options) {

View File

@ -22,7 +22,8 @@ at runtime from the About dialog for additional information.
<template>
<div
ref="tooltip-wrapper"
class="c-menu c-tooltip-wrapper"
class="c-tooltip-wrapper"
:class="cssClasses"
:style="toolTipLocationStyle"
role="tooltip"
aria-labelledby="tooltip-text"
@ -36,7 +37,7 @@ at runtime from the About dialog for additional information.
<script>
export default {
inject: ['toolTipText', 'toolTipLocation', 'parentElement'],
inject: ['toolTipText', 'toolTipLocation', 'parentElement', 'cssClasses'],
computed: {
toolTipCoordinates() {
return this.parentElement.getBoundingClientRect();

View File

@ -1,9 +1,13 @@
.c-tooltip-wrapper {
@include menuOuter();
max-width: 200px;
height: auto;
width: auto;
padding: $interiorMargin;
padding: $interiorMargin $interiorMarginLg;
overflow-wrap: break-word;
pointer-events: none;
position: absolute;
z-index: 100;
}
.c-tooltip {

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

@ -0,0 +1,55 @@
import mount from 'utils/mount';
import EventInspectorView from './components/EventInspectorView.vue';
export default function EventInspectorViewProvider(openmct) {
const TIMELINE_VIEW = 'time-strip.event.inspector';
return {
key: TIMELINE_VIEW,
name: 'Event',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 0) {
return false;
}
const selectionType = selection[0][0].context?.type;
const event = selection[0][0].context?.event;
return selectionType === 'time-strip-event-selection' && event;
},
view: function (selection) {
let _destroy = null;
return {
show: function (element) {
const { destroy } = mount(
{
el: element,
components: {
EventInspectorView
},
provide: {
openmct,
domainObject: selection[0][0].context.item,
event: selection[0][0].context.event
},
template: '<event-inspector-view></event-inspector-view>'
},
{
app: openmct.app,
element
}
);
_destroy = destroy;
},
priority: function () {
return openmct.priority.HIGH;
},
destroy: function () {
if (_destroy) {
_destroy();
}
}
};
}
};
}

View File

@ -0,0 +1,101 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import mount from 'utils/mount';
import EventTimelineView from './components/EventTimelineView.vue';
export default function EventTimestripViewProvider(openmct, extendedLinesBus) {
const type = 'event.time-line.view';
function hasEventTelemetry(domainObject) {
const metadata = 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;
}
return {
key: type,
name: 'Event Timeline View',
cssClass: 'icon-event',
priority: function () {
// 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');
return (
hasEventTelemetry(domainObject) &&
isChildOfTimeStrip &&
!openmct.router.isNavigatedObject(objectPath)
);
},
view: function (domainObject, objectPath) {
let _destroy = null;
let component = null;
return {
show: function (element) {
const { vNode, destroy } = mount(
{
el: element,
components: {
EventTimelineView
},
provide: {
openmct: openmct,
domainObject: domainObject,
objectPath: objectPath,
extendedLinesBus
},
template: '<event-timeline-view ref="root"></event-timeline-view>'
},
{
app: openmct.app,
element
}
);
_destroy = destroy;
component = vNode.componentInstance;
},
destroy: function () {
if (_destroy) {
_destroy();
}
},
getComponent() {
return component;
}
};
}
};
}

View File

@ -0,0 +1,45 @@
<!--
Open MCT, Copyright (c) 2014-2024, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div class="c-timelist-properties">
<div class="c-inspect-properties">
<ul class="c-inspect-properties__section">
<div class="c-inspect-properties_header" title="'Details'">Details</div>
<li
v-for="[key, value] in Object.entries(event)"
:key="key"
class="c-inspect-properties__row"
>
<span class="c-inspect-properties__label">{{ key }}</span>
<span class="c-inspect-properties__value">{{ value }}</span>
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
inject: ['openmct', 'domainObject', 'event']
};
</script>

View File

@ -0,0 +1,370 @@
<!--
Open MCT, Copyright (c) 2014-2024, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div ref="events" class="c-events-tsv js-events-tsv" :style="alignmentStyle">
<swim-lane v-if="eventItems.length" :is-nested="true" :hide-label="true">
<template v-slot: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>
</swim-lane>
<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 { inject } from 'vue';
import SwimLane from '@/ui/components/swim-lane/SwimLane.vue';
import { useAlignment } from '../../../ui/composables/alignmentContext.js';
import eventData from '../mixins/eventData.js';
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
const PADDING = 1;
const AXES_PADDING = 20;
export default {
components: { SwimLane },
mixins: [eventData, tooltipHelpers],
inject: ['openmct', 'domainObject', 'objectPath', 'extendedLinesBus'],
setup() {
const domainObject = inject('domainObject');
const objectPath = inject('objectPath');
const openmct = inject('openmct');
const { alignment: alignmentData } = useAlignment(domainObject, objectPath, openmct);
return { alignmentData };
},
data() {
return {
eventItems: [],
eventHistory: [],
titleKey: null
};
},
computed: {
alignmentStyle() {
let leftOffset = 0;
const rightOffset = this.alignmentData.rightWidth ? AXES_PADDING : 0;
if (this.alignmentData.leftWidth) {
leftOffset = this.alignmentData.multiple ? 2 * AXES_PADDING : AXES_PADDING;
}
return {
margin: `0 ${this.alignmentData.rightWidth + rightOffset}px 0 ${this.alignmentData.leftWidth + leftOffset}px`
};
}
},
watch: {
eventHistory: {
handler() {
this.updateEventItems();
},
deep: true
},
alignmentData: {
handler() {
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.setTimeContext();
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
const metadata = this.openmct.telemetry.getMetadata(this.domainObject);
if (metadata) {
this.valueMetadata =
metadata.valuesForHints(['range'])[0] || this.firstNonDomainAttribute(metadata);
}
// title is in the metadata, and is either a "hint" with a "label", or failing that, the first string type we find
this.titleKey =
metadata.valuesForHints(['label'])?.[0]?.key ||
metadata.values().find((metadatum) => metadatum.format === 'string')?.key;
this.updateViewBounds();
this.resize = _.debounce(this.resize, 400);
this.eventStripResizeObserver = new ResizeObserver(this.resize);
this.eventStripResizeObserver.observe(this.$refs.events);
this.extendedLinesBus.on('disable-extended-lines', this.disableExtendEventLines);
this.extendedLinesBus.on('enable-extended-lines', this.enableExtendEventLines);
},
beforeUnmount() {
if (this.eventStripResizeObserver) {
this.eventStripResizeObserver.disconnect();
}
this.stopFollowingTimeContext();
if (this.unlisten) {
this.unlisten();
}
if (this.destroyEventContainer) {
this.destroyEventContainer();
}
this.extendedLinesBus.off('disable-extended-lines', this.disableExtendEventLines);
this.extendedLinesBus.off('enable-extended-lines', this.enableExtendEventLines);
this.extendedLinesBus.off('event-hovered', this.checkIfOurEvent);
},
methods: {
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on('timeSystem', this.setScaleAndPlotEvents);
this.timeContext.on('boundsChanged', this.updateViewBounds);
},
enableExtendEventLines(keyStringToEnable) {
if (this.keyString === keyStringToEnable) {
this.extendLines = true;
this.emitExtendedLines();
}
},
disableExtendEventLines(keyStringToDisable) {
if (this.keyString === keyStringToDisable) {
this.extendLines = false;
// emit an empty array to clear the lines
this.emitExtendedLines();
}
},
firstNonDomainAttribute(metadata) {
return metadata
.values()
.find((metadatum) => !metadatum.hints.domain && metadatum.key !== 'name');
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('timeSystem', this.setScaleAndPlotEvents);
this.timeContext.off('boundsChanged', this.updateViewBounds);
}
},
resize() {
const clientWidth = this.getClientWidth();
if (clientWidth !== this.width) {
this.setDimensions();
this.setScaleAndPlotEvents(this.timeSystem);
}
},
getClientWidth() {
let clientWidth = this.$refs.events.clientWidth;
if (!clientWidth) {
//this is a hack - need a better way to find the parent of this component
const parent = this.openmct.layout.$refs.browseObject.$el;
if (parent) {
clientWidth = parent.getBoundingClientRect().width;
}
}
return clientWidth;
},
updateViewBounds(bounds, isTick) {
this.viewBounds = this.timeContext.getBounds();
if (!this.timeSystem) {
this.timeSystem = this.timeContext.getTimeSystem();
}
this.setScaleAndPlotEvents(this.timeSystem, !isTick);
},
setScaleAndPlotEvents(timeSystem) {
if (timeSystem) {
this.timeSystem = timeSystem;
this.timeFormatter = this.getFormatter(this.timeSystem.key);
}
this.setScale(this.timeSystem);
this.updateEventItems();
},
getFormatter(key) {
const metadata = this.openmct.telemetry.getMetadata(this.domainObject);
const metadataValue = metadata.value(key) || { format: key };
const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
return valueFormatter;
},
updateEventItems() {
if (this.xScale) {
this.eventItems = this.eventHistory.map((eventHistoryItem) => {
const limitClass = this.getLimitClass(eventHistoryItem);
return {
...eventHistoryItem,
left: this.xScale(eventHistoryItem.time),
limitClass
};
});
if (this.extendLines) {
this.emitExtendedLines();
}
}
},
setDimensions() {
const eventsHolder = this.$refs.events;
this.width = this.getClientWidth();
this.height = Math.round(eventsHolder.getBoundingClientRect().height);
},
setScale(timeSystem) {
if (!this.width) {
return;
}
if (!timeSystem) {
timeSystem = this.timeContext.getTimeSystem();
}
if (timeSystem.isUTCBased) {
this.xScale = scaleUtc();
this.xScale.domain([new Date(this.viewBounds.start), new Date(this.viewBounds.end)]);
} else {
this.xScale = scaleLinear();
this.xScale.domain([this.viewBounds.start, this.viewBounds.end]);
}
this.xScale.range([PADDING, this.width - PADDING * 2]);
},
createPathSelection(eventWrapper) {
const selection = [];
selection.unshift({
element: eventWrapper,
context: {
item: this.domainObject
}
});
this.objectPath.forEach((pathObject) => {
selection.push({
element: this.openmct.layout.$refs.browseObject.$el,
context: {
item: pathObject
}
});
});
return selection;
},
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
};
const selection = this.createPathSelection(eventWrapper);
if (
selection.length &&
this.openmct.objects.areIdsEqual(
selection[0].context.item.identifier,
this.domainObject.identifier
)
) {
selection[0].context = {
...selection[0].context,
...eventContext
};
} else {
selection.unshift({
element: eventWrapper,
context: {
item: this.domainObject,
...eventContext
}
});
}
this.openmct.selection.select(selection, true);
},
getLimitClass(event) {
const limitEvaluation = this.limitEvaluator.evaluate(event, this.valueMetadata);
return limitEvaluation?.cssClass;
},
showToolTip(event) {
const aClasses = ['c-events-tooltip'];
if (event.limitClass) {
aClasses.push(event.limitClass);
}
const showToLeft = false; // Temp, stubbed in
if (showToLeft) {
aClasses.push('--left');
}
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);
}
}
};
</script>

View File

@ -0,0 +1,111 @@
@mixin styleEventLine($colorConst) {
background-color: $colorConst !important;
transition: box-shadow 250ms ease-out;
&:hover,
&[s-selected] {
box-shadow: rgba($colorConst, 0.5) 0 0 0px 4px;
transition: none;
z-index: 2;
}
}
@mixin styleEventLineExtended($colorConst) {
background-color: $colorConst !important;
}
.c-events-tsv {
$m: $interiorMargin;
overflow: hidden;
@include abs();
&__container {
// Holds event lines
background-color: $colorPlotBg;
//box-shadow: inset $colorPlotAreaBorder 0 0 0 1px; // Using box-shadow instead of border to not affect box size
position: absolute;
top: $m; right: 0; bottom: $m; left: 0;
}
&__event-line {
// Wraps an individual event line
// Also holds the hover flyout element
$c: $colorEventLine;
$lineW: $eventLineW;
$hitAreaW: 7px;
$m: $interiorMarginSm;
cursor: pointer;
position: absolute;
display: flex;
top: $m; bottom: $m;
width: $lineW;
z-index: 1;
@include styleEventLine($colorEventLine);
&.is-event {
&--purple {
@include styleEventLine($colorEventPurpleLine);
}
&--red {
@include styleEventLine($colorEventRedLine);
}
&--orange {
@include styleEventLine($colorEventOrangeLine);
}
&--yellow {
@include styleEventLine($colorEventYellowLine);
}
}
&:before {
// Extend hit area
content: '';
display: block;
position: absolute;
top: 0; bottom: 0;
z-index: 0;
width: $hitAreaW;
transform: translateX(($hitAreaW - $lineW) * -0.5);
}
}
}
.c-events-canvas {
pointer-events: auto;
position: absolute;
left: 0;
top: 0;
z-index: 2;
}
// Extended event lines
.c-timeline__event-line--extended {
@include abs();
width: $eventLineW;
opacity: 0.4;
&.--hilite {
opacity: 0.8;
transition: none;
}
@include styleEventLineExtended($colorEventLine);
&.is-event {
&--purple {
@include styleEventLineExtended($colorEventPurpleLine);
}
&--red {
@include styleEventLineExtended($colorEventRedLine);
}
&--orange {
@include styleEventLineExtended($colorEventOrangeLine);
}
&--yellow {
@include styleEventLineExtended($colorEventYellowLine);
}
}
}
.c-events-tooltip {
// Default to right of event line
border-radius: 0 !important;
//transform: translate(0, $interiorMargin);
}

View File

@ -0,0 +1,183 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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.
*****************************************************************************/
const DEFAULT_DURATION_FORMATTER = 'duration';
import { TIME_CONTEXT_EVENTS } from '../../../api/time/constants.js';
export default {
inject: ['openmct', 'domainObject', 'objectPath'],
mounted() {
// listen
this.boundsChanged = this.boundsChanged.bind(this);
this.timeSystemChanged = this.timeSystemChanged.bind(this);
this.setDataTimeContext = this.setDataTimeContext.bind(this);
this.openmct.objectViews.on('clearData', this.dataCleared);
// Get metadata and formatters
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.durationFormatter = this.getFormatter(
this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
);
// initialize
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
this.setDataTimeContext();
this.loadTelemetry();
},
beforeUnmount() {
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
}
this.stopFollowingDataTimeContext();
this.openmct.objectViews.off('clearData', this.dataCleared);
this.telemetryCollection.off('add', this.dataAdded);
this.telemetryCollection.off('remove', this.dataRemoved);
this.telemetryCollection.off('clear', this.dataCleared);
this.telemetryCollection.destroy();
},
methods: {
dataAdded(addedItems, addedItemIndices) {
const normalizedDataToAdd = addedItems.map((datum) => this.normalizeDatum(datum));
let newEventHistory = this.eventHistory.slice();
normalizedDataToAdd.forEach((datum, index) => {
newEventHistory.splice(addedItemIndices[index] ?? -1, 0, datum);
});
//Assign just once so eventHistory watchers don't get called too often
this.eventHistory = newEventHistory;
},
dataCleared() {
this.eventHistory = [];
},
dataRemoved(removed) {
const removedTimestamps = {};
removed.forEach((_removed) => {
const removedTimestamp = this.parseTime(_removed);
removedTimestamps[removedTimestamp] = true;
});
this.eventHistory = this.eventHistory.filter((event) => {
const eventTimestamp = this.parseTime(event);
return !removedTimestamps[eventTimestamp];
});
},
setDataTimeContext() {
this.stopFollowingDataTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsChanged);
this.timeContext.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.timeSystemChanged);
},
stopFollowingDataTimeContext() {
if (this.timeContext) {
this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsChanged);
this.timeContext.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.timeSystemChanged);
}
},
formatEventUrl(datum) {
if (!datum) {
return;
}
return this.eventFormatter.format(datum);
},
formatEventThumbnailUrl(datum) {
if (!datum || !this.eventThumbnailFormatter) {
return;
}
return this.eventThumbnailFormatter.format(datum);
},
formatTime(datum) {
if (!datum) {
return;
}
const dateTimeStr = this.timeFormatter.format(datum);
// Replace ISO "T" with a space to allow wrapping
return dateTimeStr.replace('T', ' ');
},
getEventDownloadName(datum) {
let eventDownloadName = '';
if (datum) {
const key = this.eventDownloadNameMetadataValue.key;
eventDownloadName = datum[key];
}
return eventDownloadName;
},
parseTime(datum) {
if (!datum) {
return;
}
return this.timeFormatter.parse(datum);
},
loadTelemetry() {
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
timeContext: this.timeContext
});
this.telemetryCollection.on('add', this.dataAdded);
this.telemetryCollection.on('remove', this.dataRemoved);
this.telemetryCollection.on('clear', this.dataCleared);
this.telemetryCollection.load();
},
boundsChanged(bounds, isTick) {
if (isTick) {
return;
}
this.bounds = bounds;
},
timeSystemChanged() {
this.timeSystem = this.timeContext.getTimeSystem();
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
this.durationFormatter = this.getFormatter(
this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
);
},
normalizeDatum(datum) {
const formattedTime = this.formatTime(datum);
const time = this.parseTime(formattedTime);
return {
...datum,
formattedTime,
time
};
},
getFormatter(key) {
const metadataValue = this.metadata.value(key) || { format: key };
const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
return valueFormatter;
}
}
};

View File

@ -0,0 +1,31 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventInspectorViewProvider from './EventInspectorViewProvider.js';
import EventTimelineViewProvider from './EventTimelineViewProvider.js';
export default function plugin(extendedLinesBus) {
return function install(openmct) {
openmct.objectViews.addProvider(new EventTimelineViewProvider(openmct, extendedLinesBus));
openmct.inspectorViews.addProvider(new EventInspectorViewProvider(openmct));
};
}

View File

@ -21,7 +21,7 @@
-->
<template>
<div ref="imagery" class="c-imagery-tsv c-timeline-holder">
<div ref="imagery" class="c-imagery-tsv js-imagery-tsv" :style="alignmentStyle">
<div ref="imageryHolder" class="c-imagery-tsv__contents u-contents"></div>
</div>
</template>
@ -30,24 +30,32 @@
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 { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';
import { useAlignment } from '../../../ui/composables/alignmentContext';
import imageryData from '../../imagery/mixins/imageryData.js';
const AXES_PADDING = 20;
const PADDING = 1;
const ROW_HEIGHT = 100;
const IMAGE_SIZE = 85;
const IMAGE_WIDTH_THRESHOLD = 25;
const CONTAINER_CLASS = 'c-imagery-tsv-container';
const NO_ITEMS_CLASS = 'c-imagery-tsv__no-items';
const NO_ITEMS_CLASS = 'c-timeline__no-items';
const IMAGE_WRAPPER_CLASS = 'c-imagery-tsv__image-wrapper';
const ID_PREFIX = 'wrapper-';
export default {
mixins: [imageryData],
inject: ['openmct', 'domainObject', 'objectPath'],
setup() {
const domainObject = inject('domainObject');
const objectPath = inject('objectPath');
const openmct = inject('openmct');
const { alignment: alignmentData } = useAlignment(domainObject, objectPath, openmct);
return { alignmentData };
},
data() {
let timeSystem = this.openmct.time.getTimeSystem();
this.metadata = {};
@ -62,6 +70,18 @@ export default {
keyString: undefined
};
},
computed: {
alignmentStyle() {
let leftOffset = 0;
const rightOffset = this.alignmentData.rightWidth ? AXES_PADDING : 0;
if (this.alignmentData.leftWidth) {
leftOffset = this.alignmentData.multiple ? 2 * AXES_PADDING : AXES_PADDING;
}
return {
margin: `0 ${this.alignmentData.rightWidth + rightOffset}px 0 ${this.alignmentData.leftWidth + leftOffset}px`
};
}
},
watch: {
imageHistory: {
handler(newHistory, oldHistory) {
@ -73,9 +93,11 @@ export default {
mounted() {
this.previewAction = this.openmct.actions.getAction(PREVIEW_ACTION_KEY);
this.canvas = this.$refs.imagery.appendChild(document.createElement('canvas'));
this.canvas.height = 0;
this.canvasContext = this.canvas.getContext('2d');
// Why are we doing this? This element causes scroll problems in the swimlane.
// this.canvas = this.$refs.imagery.appendChild(document.createElement('canvas'));
// this.canvas.height = 0;
// this.canvas.width = 10;
// this.canvasContext = this.canvas.getContext('2d');
this.setDimensions();
this.setScaleAndPlotImagery = this.setScaleAndPlotImagery.bind(this);
@ -207,8 +229,8 @@ export default {
setDimensions() {
const imageryHolder = this.$refs.imagery;
this.width = this.getClientWidth();
this.height = Math.round(imageryHolder.getBoundingClientRect().height);
this.imageHeight = this.height - 10;
},
setScale(timeSystem) {
if (!this.width) {
@ -233,14 +255,11 @@ export default {
return imageObj.time <= this.viewBounds.end && imageObj.time >= this.viewBounds.start;
},
getImageryContainer() {
let containerHeight = 100;
let containerWidth = this.imageHistory.length ? this.width : 200;
let imageryContainer;
let existingContainer = this.$el.querySelector(`.${CONTAINER_CLASS}`);
if (existingContainer) {
imageryContainer = existingContainer;
imageryContainer.style.maxWidth = `${containerWidth}px`;
} else {
if (this.destroyImageryContainer) {
this.destroyImageryContainer();
@ -270,8 +289,6 @@ export default {
this.$refs.imageryHolder.appendChild(component.$el);
imageryContainer = component.$el.querySelector(`.${CONTAINER_CLASS}`);
imageryContainer.style.maxWidth = `${containerWidth}px`;
imageryContainer.style.height = `${containerHeight}px`;
}
return imageryContainer;
@ -307,7 +324,7 @@ export default {
}
},
plotNoItems(containerElement) {
let textElement = document.createElement('text');
let textElement = document.createElement('div');
textElement.classList.add(NO_ITEMS_CLASS);
textElement.innerHTML = 'No images within timeframe';
@ -380,15 +397,11 @@ export default {
//create image vertical tick indicator
let imageTickElement = document.createElement('div');
imageTickElement.classList.add('c-imagery-tsv__image-handle');
imageTickElement.style.width = '2px';
imageTickElement.style.height = `${String(ROW_HEIGHT - 10)}px`;
imageWrapper.appendChild(imageTickElement);
//create placeholder - this will also hold the actual image
let imagePlaceholder = document.createElement('div');
imagePlaceholder.classList.add('c-imagery-tsv__image-placeholder');
imagePlaceholder.style.width = `${IMAGE_SIZE}px`;
imagePlaceholder.style.height = `${IMAGE_SIZE}px`;
imageWrapper.appendChild(imagePlaceholder);
//create image element
@ -396,8 +409,6 @@ export default {
this.setNSAttributesForElement(imageElement, {
src: image.thumbnailUrl || image.url
});
imageElement.style.width = `${IMAGE_SIZE}px`;
imageElement.style.height = `${IMAGE_SIZE}px`;
this.setImageDisplay(imageElement, showImagePlaceholders);
//handle mousedown event to show the image in a large view

View File

@ -509,45 +509,92 @@
/*************************************** IMAGERY IN TIMESTRIP VIEWS */
.c-imagery-tsv {
div.c-imagery-tsv__image-wrapper {
$m: $interiorMargin;
@include abs();
// We need overflow: hidden this because an image thumb can extend to the right past the time frame edge
overflow: hidden;
&-container {
background: $colorPlotBg;
//box-shadow: inset $colorPlotAreaBorder 0 0 0 1px; // Using box-shadow instead of border to not affect box size
position: absolute;
top: $m; right: 0; bottom: $m; left: 0;
}
.c-imagery-tsv__image-wrapper {
$m: $interiorMarginSm;
cursor: pointer;
position: absolute;
top: 0;
top: $m; bottom: $m;
display: flex;
z-index: 1;
margin-top: 5px;
img {
align-self: flex-end;
}
&:hover {
z-index: 2;
[class*='__image-handle'] {
background-color: $colorBodyFg;
.c-imagery-tsv {
&__image-handle {
box-shadow: rgba($colorEventLine, 0.5) 0 0 0px 4px;
transition: none;
}
&__image-placeholder img {
filter: none;
}
}
img {
// img can be `display: none` when there's not enough space between tick lines
display: block !important;
}
}
}
&__image-placeholder {
background-color: deeppink; //pushBack($colorBodyBg, 0.3);
$m: $interiorMargin;
display: block;
position: absolute;
top: $m; right: auto; bottom: $m; left: 0;
img {
filter: brightness(0.8);
height: 100%;
}
}
&__image-handle {
$lineW: $eventLineW;
$hitAreaW: 7px;
background-color: $colorEventLine;
transition: box-shadow 250ms ease-out;
top: 0; bottom: 0;
width: $lineW;
z-index: 3;
&:before {
// Extend hit area
content: '';
display: block;
position: absolute;
top: 0; bottom: 0;
z-index: 0;
width: $hitAreaW;
transform: translateX(($hitAreaW - $lineW) * -0.5);
}
}
&__no-items {
fill: $colorBodyFg !important;
}
&__image-handle {
background-color: rgba($colorBodyFg, 0.5);
}
&__image-placeholder {
background-color: pushBack($colorBodyBg, 0.3);
display: block;
align-self: flex-end;
}
}
// DON'T THINK THIS IS BEING USED
.c-image-canvas {
pointer-events: auto; // This allows the image element to receive a browser-level context click
position: absolute;

View File

@ -25,68 +25,63 @@
{{ heading }}
</template>
<template #object>
<svg :height="height" :width="svgWidth" :style="alignmentStyle">
<symbol id="activity-bar-bg" :height="rowHeight" width="2" preserveAspectRatio="none">
<rect x="0" y="0" width="100%" height="100%" fill="currentColor" />
<line
x1="100%"
y1="0"
x2="100%"
y2="100%"
stroke="black"
stroke-width="1"
opacity="0.3"
transform="translate(-0.5, 0)"
/>
</symbol>
<template v-for="(activity, index) in activities" :key="`g-${activity.clipPathId}`">
<template v-if="clipActivityNames === true">
<clipPath :id="activity.clipPathId" :key="activity.clipPathId">
<rect
<div class="c-plan-av" :style="alignmentStyle">
<svg v-if="activities.length > 0" class="c-plan-av__svg" :height="height">
<symbol id="activity-bar-bg" :height="rowHeight" width="2" preserveAspectRatio="none">
<rect x="0" y="0" width="100%" height="100%" fill="currentColor" />
<line
x1="100%"
y1="0"
x2="100%"
y2="100%"
stroke="black"
stroke-width="1"
opacity="0.3"
transform="translate(-0.5, 0)"
/>
</symbol>
<template v-for="(activity, index) in activities" :key="`g-${activity.clipPathId}`">
<template v-if="clipActivityNames === true">
<clipPath :id="activity.clipPathId" :key="activity.clipPathId">
<rect
:x="activity.rectStart"
:y="activity.row"
:width="activity.rectWidth - 1"
:height="rowHeight"
/>
</clipPath>
</template>
<g
class="c-plan__activity activity-bounds"
@click="setSelectionForActivity(activity, $event)"
>
<title>{{ activity.name }}</title>
<use
:key="`rect-${index}`"
href="#activity-bar-bg"
:x="activity.rectStart"
:y="activity.row"
:width="activity.rectWidth - 1"
:width="activity.rectWidth"
:height="rowHeight"
:class="activity.class"
:color="activity.color"
/>
</clipPath>
<text
v-for="(textLine, textIndex) in activity.textLines"
:key="`text-${index}-${textIndex}`"
:class="`c-plan__activity-label ${activity.textClass}`"
:x="activity.textStart"
:y="activity.textY + textIndex * lineHeight"
:fill="activity.textColor"
:clip-path="clipActivityNames === true ? `url(#${activity.clipPathId})` : ''"
>
{{ textLine }}
</text>
</g>
</template>
<g
class="c-plan__activity activity-bounds"
@click="setSelectionForActivity(activity, $event)"
>
<title>{{ activity.name }}</title>
<use
:key="`rect-${index}`"
href="#activity-bar-bg"
:x="activity.rectStart"
:y="activity.row"
:width="activity.rectWidth"
:height="rowHeight"
:class="activity.class"
:color="activity.color"
/>
<text
v-for="(textLine, textIndex) in activity.textLines"
:key="`text-${index}-${textIndex}`"
:class="`c-plan__activity-label ${activity.textClass}`"
:x="activity.textStart"
:y="activity.textY + textIndex * lineHeight"
:fill="activity.textColor"
:clip-path="clipActivityNames === true ? `url(#${activity.clipPathId})` : ''"
>
{{ textLine }}
</text>
</g>
</template>
<text
v-if="activities.length === 0"
x="10"
y="20"
class="c-plan__activity-label--outside-rect"
>
No activities within current timeframe
</text>
</svg>
</svg>
<div v-else class="c-timeline__no-items">No activities within timeframe</div>
</div>
</template>
</SwimLane>
</template>
@ -162,24 +157,27 @@ export default {
computed: {
alignmentStyle() {
let leftOffset = 0;
const rightOffset = this.alignmentData.rightWidth ? AXES_PADDING : 0;
if (this.alignmentData.leftWidth) {
leftOffset = this.alignmentData.multiple ? 2 * AXES_PADDING : AXES_PADDING;
}
return {
marginLeft: `${this.alignmentData.leftWidth + leftOffset}px`
margin: `0 ${this.alignmentData.rightWidth + rightOffset}px 0 ${this.alignmentData.leftWidth + leftOffset}px`
};
},
svgWidth() {
// Reduce the width by left axis width, then take off the right yaxis width as well
return '100%'; // TEMP!
/*
let leftOffset = 0;
if (this.alignmentData.leftWidth) {
leftOffset = this.alignmentData.multiple ? 2 * AXES_PADDING : AXES_PADDING;
}
const rightOffset = this.alignmentData.rightWidth ? AXES_PADDING : 0;
return (
return (
this.width -
(this.alignmentData.leftWidth + leftOffset + this.alignmentData.rightWidth + rightOffset)
);
);*/
}
},
methods: {

View File

@ -27,6 +27,10 @@
text {
stroke: none;
}
.c-swimlane__lane-object {
display: flex;
}
}
&__activity {
@ -53,3 +57,14 @@
display: none;
}
}
.c-plan-av {
// Activities view
background-color: $colorPlotBg;
flex: 1 1 auto;
height: 100%;
&__svg {
width: 100%;
}
}

View File

@ -38,6 +38,7 @@ import CouchDBSearchFolder from './CouchDBSearchFolder/plugin.js';
import DefaultRootName from './defaultRootName/plugin.js';
import DeviceClassifier from './DeviceClassifier/plugin.js';
import DisplayLayoutPlugin from './displayLayout/plugin.js';
import EventTimestripPlugin from './events/plugin.js';
import FaultManagementPlugin from './faultManagement/FaultManagementPlugin.js';
import Filters from './filters/plugin.js';
import FlexibleLayout from './flexibleLayout/plugin.js';
@ -176,5 +177,6 @@ plugins.Gauge = GaugePlugin;
plugins.Timelist = TimeList;
plugins.InspectorViews = InspectorViews;
plugins.InspectorDataVisualization = InspectorDataVisualization;
plugins.EventTimestripPlugin = EventTimestripPlugin;
export default plugins;

View File

@ -0,0 +1,37 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { EventEmitter } from 'eventemitter3';
export default class ExtendedLinesBus extends EventEmitter {
updateExtendedLines(keyString, lines) {
this.emit('update-extended-lines', { lines, keyString });
}
disableExtendEventLines(keyString) {
this.emit('disable-extended-lines', keyString);
}
enableExtendEventLines(keyString) {
this.emit('enable-extended-lines', keyString);
}
updateHoverExtendEventLine(keyString, id) {
this.emit('update-extended-hover', { id, keyString });
}
}

View File

@ -0,0 +1,88 @@
<!--
Open MCT, Copyright (c) 2014-2024, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div class="c-timeline__overlay-lines">
<div
v-for="(lines, key) in extendedLinesPerKey"
:key="key"
class="c-timeline__event-line--extended-container"
>
<div
v-for="(line, index) in lines"
:id="line.id"
:key="`${index - line.id}`"
class="c-timeline__event-line--extended"
:class="[
line.limitClass,
{
'--hilite':
(hoveredLineId && hoveredKeyString === key && line.id === hoveredLineId) ||
(selectedLineId && selectedKeyString === key && line.id === selectedLineId)
}
]"
:style="{ left: `${line.x + leftOffset}px`, height: `${height}px` }"
></div>
</div>
</div>
</template>
<script>
export default {
name: 'ExtendedLinesOverlay',
props: {
extendedLinesPerKey: {
type: Object,
required: true
},
height: {
type: Number,
required: true
},
leftOffset: {
type: Number,
default: 0
},
extendedLineHover: {
type: Object,
required: true
},
extendedLineSelection: {
type: Object,
required: true
}
},
computed: {
hoveredLineId() {
return this.extendedLineHover.id || null;
},
hoveredKeyString() {
return this.extendedLineHover.keyString || null;
},
selectedLineId() {
return this.extendedLineSelection.id || null;
},
selectedKeyString() {
return this.extendedLineSelection.keyString || null;
}
}
};
</script>

View File

@ -33,10 +33,7 @@ export default function TimelineCompositionPolicy(openmct) {
}
function hasDomainAndRange(metadata) {
return (
metadata.valuesForHints(['range']).length > 0 &&
metadata.valuesForHints(['domain']).length > 0
);
return metadata.valuesForHints(['domain']).length > 0;
}
function hasImageTelemetry(domainObject, metadata) {

View File

@ -28,6 +28,11 @@
:show-ucontents="isPlanLikeObject(item.domainObject)"
:span-rows-count="item.rowCount"
:domain-object="item.domainObject"
:button-title="`Toggle extended event lines overlay for ${item.domainObject.name}`"
button-icon="icon-arrows-up-down"
:hide-button="!item.isEventTelemetry"
:button-click-on="enableExtendEventLines"
:button-click-off="disableExtendEventLines"
>
<template #label>
{{ item.domainObject.name }}
@ -58,12 +63,16 @@ export default {
item: {
type: Object,
required: true
},
extendedLinesBus: {
type: Object,
required: true
}
},
data() {
return {
domainObject: undefined,
mutablePromise: undefined,
domainObject: null,
mutablePromise: null,
status: ''
};
},
@ -103,33 +112,40 @@ export default {
}
},
methods: {
setObject(domainObject) {
async setObject(domainObject) {
this.domainObject = domainObject;
this.mutablePromise = undefined;
this.$nextTick(() => {
let reference = this.$refs.objectView;
this.mutablePromise = null;
await this.$nextTick();
let reference = this.$refs.objectView;
if (reference) {
let childContext = this.$refs.objectView.getSelectionContext();
childContext.item = domainObject;
this.context = childContext;
if (this.removeSelectable) {
this.removeSelectable();
}
this.removeSelectable = this.openmct.selection.selectable(this.$el, this.context);
if (reference) {
let childContext = this.$refs.objectView.getSelectionContext();
childContext.item = domainObject;
this.context = childContext;
if (this.removeSelectable) {
this.removeSelectable();
}
if (this.removeStatusListener) {
this.removeStatusListener();
}
this.removeSelectable = this.openmct.selection.selectable(this.$el, this.context);
}
this.removeStatusListener = this.openmct.status.observe(
this.domainObject.identifier,
this.setStatus
);
this.status = this.openmct.status.get(this.domainObject.identifier);
});
if (this.removeStatusListener) {
this.removeStatusListener();
}
this.removeStatusListener = this.openmct.status.observe(
this.domainObject.identifier,
this.setStatus
);
this.status = this.openmct.status.get(this.domainObject.identifier);
},
enableExtendEventLines() {
const keyString = this.openmct.objects.makeKeyString(this.item.domainObject.identifier);
this.extendedLinesBus.enableExtendEventLines(keyString);
},
disableExtendEventLines() {
const keyString = this.openmct.objects.makeKeyString(this.item.domainObject.identifier);
this.extendedLinesBus.disableExtendEventLines(keyString);
},
setActionCollection(actionCollection) {
this.openmct.menus.actionsToMenuItems(

View File

@ -42,8 +42,17 @@
:key="item.keyString"
class="c-timeline__content js-timeline__content"
:item="item"
:extended-lines-bus
/>
</div>
<ExtendedLinesOverlay
:extended-lines-per-key="extendedLinesPerKey"
:height="height"
:left-offset="extendedLinesLeftOffset"
:extended-line-hover="extendedLineHover"
:extended-line-selection="extendedLineSelection"
/>
</div>
</template>
@ -56,6 +65,7 @@ import SwimLane from '@/ui/components/swim-lane/SwimLane.vue';
import TimelineAxis from '../../ui/components/TimeSystemAxis.vue';
import { useAlignment } from '../../ui/composables/alignmentContext.js';
import { getValidatedData, getValidatedGroups } from '../plan/util.js';
import ExtendedLinesOverlay from './ExtendedLinesOverlay.vue';
import TimelineObjectView from './TimelineObjectView.vue';
const unknownObjectType = {
@ -65,13 +75,17 @@ const unknownObjectType = {
}
};
const AXES_PADDING = 20;
const PLOT_ITEM_H_PX = 100;
export default {
components: {
TimelineObjectView,
TimelineAxis,
SwimLane
SwimLane,
ExtendedLinesOverlay
},
inject: ['openmct', 'domainObject', 'path', 'composition'],
inject: ['openmct', 'domainObject', 'path', 'composition', 'extendedLinesBus'],
setup() {
const domainObject = inject('domainObject');
const path = inject('path');
@ -90,9 +104,21 @@ export default {
timeSystems: [],
height: 0,
useIndependentTime: this.domainObject.configuration.useIndependentTime === true,
timeOptions: this.domainObject.configuration.timeOptions
timeOptions: this.domainObject.configuration.timeOptions,
extendedLinesPerKey: {},
extendedLineHover: {},
extendedLineSelection: {},
extendedLinesLeftOffset: 0
};
},
watch: {
alignmentData: {
handler() {
this.calculateExtendedLinesLeftOffset();
},
deep: true
}
},
beforeUnmount() {
this.resetAlignment();
this.composition.off('add', this.addItem);
@ -101,11 +127,18 @@ export default {
this.stopFollowingTimeContext();
this.handleContentResize.cancel();
this.contentResizeObserver.disconnect();
this.extendedLinesBus.off('update-extended-lines', this.updateExtendedLines);
this.extendedLinesBus.off('update-extended-hover', this.updateExtendedHover);
this.openmct.selection.off('change', this.checkForLineSelection);
},
mounted() {
this.items = [];
this.setTimeContext();
this.extendedLinesBus.on('update-extended-lines', this.updateExtendedLines);
this.extendedLinesBus.on('update-extended-hover', this.updateExtendedHover);
this.openmct.selection.on('change', this.checkForLineSelection);
if (this.composition) {
this.composition.on('add', this.addItem);
this.composition.on('remove', this.removeItem);
@ -129,27 +162,41 @@ 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 * 100}px`
: '100px';
? `${domainObject.composition.length * PLOT_ITEM_H_PX}px`
: 'auto';
let item = {
domainObject,
objectPath,
type,
keyString,
rowCount,
height
height,
isEventTelemetry
};
this.items.push(item);
},
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)
);
this.items.splice(index, 1);
delete this.extendedLinesPerKey[this.openmct.objects.makeKeyString(identifier)];
},
reorder(reorderPlan) {
let oldItems = this.items.slice();
@ -165,6 +212,7 @@ export default {
if (this.height !== clientHeight) {
this.height = clientHeight;
}
this.calculateExtendedLinesLeftOffset();
},
getClientHeight() {
let clientHeight = this.$refs.timelineHolder.getBoundingClientRect().height;
@ -222,6 +270,41 @@ export default {
this.timeContext.off('boundsChanged', this.updateViewBounds);
this.timeContext.off('clockChanged', this.updateViewBounds);
}
},
updateExtendedLines({ keyString, lines }) {
this.extendedLinesPerKey[keyString] = lines;
},
updateExtendedHover({ keyString, id }) {
this.extendedLineHover = { keyString, id };
},
checkForLineSelection(selection) {
const selectionContext = selection?.[0]?.[0]?.context;
const eventType = selectionContext?.type;
if (eventType === 'time-strip-event-selection') {
const event = selectionContext.event;
const selectedObject = selectionContext.item;
const keyString = this.openmct.objects.makeKeyString(selectedObject.identifier);
this.extendedLineSelection = { keyString, id: event?.time };
} else {
this.extendedLineSelection = {};
}
},
calculateExtendedLinesLeftOffset() {
const swimLaneOffset = this.calculateSwimlaneOffset();
this.extendedLinesLeftOffset = this.alignmentData.leftWidth + swimLaneOffset;
},
calculateSwimlaneOffset() {
const firstSwimLane = this.$el.querySelector('.c-swimlane__lane-object');
if (firstSwimLane) {
const timelineHolderRect = this.$refs.timelineHolder.getBoundingClientRect();
const laneObjectRect = firstSwimLane.getBoundingClientRect();
const offset = laneObjectRect.left - timelineHolderRect.left;
const hasAxes = this.alignmentData.axes && Object.keys(this.alignmentData.axes).length > 0;
const swimLaneOffset = hasAxes ? offset + AXES_PADDING : offset;
return swimLaneOffset;
} else {
return 0;
}
}
}
};

View File

@ -24,7 +24,7 @@ import mount from 'utils/mount';
import TimelineViewLayout from './TimelineViewLayout.vue';
export default function TimelineViewProvider(openmct) {
export default function TimelineViewProvider(openmct, extendedLinesBus) {
return {
key: 'time-strip.view',
name: 'TimeStrip',
@ -52,7 +52,8 @@ export default function TimelineViewProvider(openmct) {
openmct,
domainObject,
path: objectPath,
composition: openmct.composition.get(domainObject)
composition: openmct.composition.get(domainObject),
extendedLinesBus
},
template: '<timeline-view-layout></timeline-view-layout>'
},

View File

@ -20,12 +20,17 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ExtendedLinesBus from './ExtendedLinesBus.js';
import TimelineCompositionPolicy from './TimelineCompositionPolicy.js';
import timelineInterceptor from './timelineInterceptor.js';
import TimelineViewProvider from './TimelineViewProvider.js';
const extendedLinesBus = new ExtendedLinesBus();
export { extendedLinesBus };
export default function () {
return function install(openmct) {
function install(openmct) {
openmct.types.addType('time-strip', {
name: 'Time Strip',
key: 'time-strip',
@ -43,6 +48,10 @@ export default function () {
timelineInterceptor(openmct);
openmct.composition.addPolicy(new TimelineCompositionPolicy(openmct).allow);
openmct.objectViews.addProvider(new TimelineViewProvider(openmct));
};
openmct.objectViews.addProvider(new TimelineViewProvider(openmct, extendedLinesBus));
}
install.extendedLinesBus = extendedLinesBus;
return install;
}

View File

@ -1,7 +1,62 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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.
*****************************************************************************/
/********************************************* TIME STRIP */
.c-timeline-holder {
overflow: hidden;
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
gap: 1px;
// Plot view overrides
.gl-plot-display-area,
.gl-plot-axis-area.gl-plot-y {
bottom: $interiorMargin !important;
}
}
.c-timeline__objects {
display: contents;
.c-timeline {
&__objects {
display: contents;
.c-swimlane {
min-height: 100px; // TEMP!! Will be replaced when heights are set by user
}
}
&__overlay-lines {
//background: rgba(deeppink, 0.2);
@include abs();
top: 20px; // Offset down to line up with time axis ticks line
pointer-events: none; // Allows clicks to pass through
z-index: 10; // Ensure it sits atop swimlanes
}
&__no-items {
font-style: italic;
position: absolute;
left: $interiorMargin;
top: 50%;
transform: translateY(-50%);
}
}

View File

@ -443,6 +443,10 @@ $colorEventPurpleBg: #31204a;
$colorEventRedBg: #3c1616;
$colorEventOrangeBg: #3e2a13;
$colorEventYellowBg: #3e3316;
$colorEventPurpleLine: #9e36ff;
$colorEventRedLine: #ff2525;
$colorEventOrangeLine: #ff8800;
$colorEventYellowLine: #fdce22;
// Bubble colors
$colorInfoBubbleBg: #dddddd;
@ -517,6 +521,11 @@ $colorInProgressBg: $colorTimeRealtimeBg;
$colorInProgressFg: $colorTimeRealtimeFgSubtle;
$colorInProgressFgEm: $colorTimeRealtimeFg;
$colorGanttSelectedBorder: rgba(#fff, 0.3);
$colorEventLine: $colorBodyFg;
$colorEventLineExtended: rgba($colorEventLine, 0.3);
$colorTimeStripDraftBg: rgba(#a57748, 0.2);
$colorTimeStripLabelBg: rgba($colorBodyFg, 0.15);
$eventLineW: 1px;
// Tree
$colorTreeBg: transparent;

View File

@ -412,6 +412,10 @@ $colorEventPurpleBg: #31204a;
$colorEventRedBg: #3c1616;
$colorEventOrangeBg: #3e2a13;
$colorEventYellowBg: #3e3316;
$colorEventPurpleLine: #9e36ff;
$colorEventRedLine: #ff2525;
$colorEventOrangeLine: #ff8800;
$colorEventYellowLine: #fdce22;
// Bubble colors
$colorInfoBubbleBg: #dddddd;
@ -482,6 +486,11 @@ $colorInProgressBg: $colorTimeRealtimeBg;
$colorInProgressFg: $colorTimeRealtimeFgSubtle;
$colorInProgressFgEm: $colorTimeRealtimeFg;
$colorGanttSelectedBorder: rgba(#fff, 0.3);
$colorEventLine: $colorBodyFg;
$colorEventLineExtended: rgba($colorEventLine, 0.3);
$colorTimeStripDraftBg: rgba(#a57748, 0.2);
$colorTimeStripLabelBg: rgba($colorBodyFg, 0.15);
$eventLineW: 1px;
// Tree
$colorTreeBg: transparent;

View File

@ -428,6 +428,10 @@ $colorEventPurpleBg: #31204a;
$colorEventRedBg: #3c1616;
$colorEventOrangeBg: #3e2a13;
$colorEventYellowBg: #3e3316;
$colorEventPurpleLine: #9e36ff;
$colorEventRedLine: #ff2525;
$colorEventOrangeLine: #ff8800;
$colorEventYellowLine: #fdce22;
// Bubble colors
$colorInfoBubbleBg: #dddddd;
@ -498,6 +502,11 @@ $colorInProgressBg: $colorTimeRealtimeBg;
$colorInProgressFg: $colorTimeRealtimeFgSubtle;
$colorInProgressFgEm: $colorTimeRealtimeFg;
$colorGanttSelectedBorder: rgba(#fff, 0.3);
$colorEventLine: $colorBodyFg;
$colorEventLineExtended: rgba($colorEventLine, 0.3);
$colorTimeStripDraftBg: rgba(#a57748, 0.2);
$colorTimeStripLabelBg: rgba($colorBodyFg, 0.15);
$eventLineW: 1px;
// Tree
$colorTreeBg: transparent;

View File

@ -403,14 +403,18 @@ $colorLimitCyanFg: #d3faff;
$colorLimitCyanIc: #1795c0;
// Events
$colorEventPurpleFg: #6433ff;
$colorEventPurpleFg: #6f07ed;
$colorEventRedFg: #aa0000;
$colorEventOrangeFg: #b84900;
$colorEventYellowFg: #867109;
$colorEventYellowFg: #a98c04;
$colorEventPurpleBg: #ebe7fb;
$colorEventRedBg: #fcefef;
$colorEventOrangeBg: #ffece3;
$colorEventYellowBg: #fdf8eb;
$colorEventPurpleLine: $colorEventPurpleFg;
$colorEventRedLine: $colorEventRedFg;
$colorEventOrangeLine: $colorEventOrangeFg;
$colorEventYellowLine: $colorEventYellowFg;
// Bubble colors
$colorInfoBubbleBg: $colorMenuBg;
@ -481,6 +485,11 @@ $colorInProgressBg: #b1e8ff;
$colorInProgressFg: $colorCurrentFg;
$colorInProgressFgEm: $colorCurrentFgEm;
$colorGanttSelectedBorder: #fff;
$colorEventLine: $colorBodyFg;
$colorEventLineExtended: rgba($colorEventLine, 0.3);
$colorTimeStripDraftBg: rgba(#a57748, 0.2);
$colorTimeStripLabelBg: rgba($colorBodyFg, 0.15);
$eventLineW: 1px;
// Tree
$colorTreeBg: transparent;

View File

@ -252,8 +252,6 @@ tr {
background-color: $colorEventYellowBg !important;
color: $colorEventYellowFg !important;
}
&--no-style {
background-color: $colorBodyBg !important;
color: $colorBodyFg !important;
}
}

View File

@ -11,6 +11,7 @@
@import '../plugins/displayLayout/components/layout-frame.scss';
@import '../plugins/displayLayout/components/telemetry-view.scss';
@import '../plugins/displayLayout/components/text-view.scss';
@import '../plugins/events/components/events-view.scss';
@import '../plugins/filters/components/filters-view.scss';
@import '../plugins/filters/components/global-filters.scss';
@import '../plugins/flexibleLayout/components/flexible-layout.scss';

View File

@ -21,11 +21,9 @@
-->
<template>
<div ref="axisHolder" class="c-timesystem-axis">
<div class="nowMarker" :style="nowMarkerStyle" aria-label="Now Marker">
<span class="icon-arrow-down"></span>
</div>
<div class="c-timesystem-axis__mb-line" :style="nowMarkerStyle" aria-label="Now Marker"></div>
<svg :width="svgWidth" :height="svgHeight">
<g class="axis" font-size="1.3em" :transform="axisTransform"></g>
<g class="axis" :transform="axisTransform"></g>
</svg>
</div>
</template>
@ -46,6 +44,7 @@ import { useResizeObserver } from '../composables/resize';
const PADDING = 1;
const PIXELS_PER_TICK = 100;
const PIXELS_PER_TICK_WIDE = 200;
const TIME_AXIS_LINE_Y = 20;
export default {
inject: ['openmct', 'domainObject', 'path'],
@ -80,8 +79,9 @@ export default {
const { size: containerSize, startObserving } = useResizeObserver();
const svgWidth = ref(0);
const svgHeight = ref(0);
const axisTransform = ref('translate(0,20)');
const axisTransform = ref(`translate(0,${TIME_AXIS_LINE_Y})`);
const alignmentOffset = ref(0);
const alignmentStyle = ref({ margin: `0 0 0 0` });
const nowMarkerStyle = reactive({
height: '0px',
left: '0px'
@ -104,6 +104,7 @@ export default {
svgHeight,
axisTransform,
alignmentOffset,
alignmentStyle,
nowMarkerStyle,
openmct
};
@ -112,16 +113,17 @@ export default {
alignmentData: {
handler() {
let leftOffset = 0;
const rightOffset = this.alignmentData.rightWidth ? AXES_PADDING : 0;
if (this.alignmentData.leftWidth) {
leftOffset = this.alignmentData.multiple ? 2 * AXES_PADDING : AXES_PADDING;
}
this.axisTransform = `translate(${this.alignmentData.leftWidth + leftOffset}, 20)`;
const rightOffset = this.alignmentData.rightWidth ? AXES_PADDING : 0;
this.leftAlignmentOffset = this.alignmentData.leftWidth + leftOffset;
this.alignmentOffset =
this.leftAlignmentOffset + this.alignmentData.rightWidth + rightOffset;
this.alignmentStyle = {
margin: `0 ${this.leftAlignmentOffset + this.alignmentData.rightWidth + rightOffset}px 0 ${this.alignmentData.leftWidth + leftOffset}px`
};
this.refresh();
},
deep: true
@ -173,10 +175,11 @@ export default {
this.updateNowMarker();
},
updateNowMarker() {
const nowMarker = this.$el.querySelector('.nowMarker');
const nowMarker = this.$el.querySelector('.c-timesystem-axis__mb-line');
if (nowMarker) {
nowMarker.classList.remove('hidden');
this.nowMarkerStyle.height = this.contentHeight + 'px';
this.nowMarkerStyle.height = this.contentHeight - TIME_AXIS_LINE_Y + 'px';
this.nowMarkerStyle.top = TIME_AXIS_LINE_Y + 'px';
const nowTimeStamp = this.openmct.time.now();
const now = this.xScale(nowTimeStamp);
this.nowMarkerStyle.left = `${now + this.leftAlignmentOffset}px`;

View File

@ -42,10 +42,19 @@
:title="`This item is ${status}`"
></span>
</div>
<div class="c-object-label__name">
<slot name="label"></slot>
</div>
<div class="c-swimlane__lane-label-button-h">
<button
v-if="!hideButton"
class="c-button"
:class="[buttonIcon, buttonPressed ? 'is-active' : '']"
:title="buttonTitle"
:aria-label="buttonTitle"
@click="pressOnButton"
/>
</div>
</div>
<div
class="c-swimlane__lane-object"
@ -115,8 +124,43 @@ export default {
domainObject: {
type: Object,
default: undefined
},
hideButton: {
type: Boolean,
default() {
return true;
}
},
buttonTitle: {
type: String,
default() {
return null;
}
},
buttonIcon: {
type: String,
default() {
return null;
}
},
buttonClickOn: {
type: Function,
default() {
return () => {};
}
},
buttonClickOff: {
type: Function,
default() {
return () => {};
}
}
},
data() {
return {
buttonPressed: false
};
},
computed: {
gridRowSpan() {
if (this.spanRowsCount) {
@ -128,7 +172,7 @@ export default {
swimlaneClass() {
if (!this.spanRowsCount && !this.isNested) {
return 'c-swimlane__lane-label--span-cols';
return 'c-swimlane__lane-label --span-cols';
}
return '';
@ -142,6 +186,14 @@ export default {
async showToolTip() {
const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;
this.buildToolTip(await this.getObjectPath(), BELOW, 'swimLane');
},
pressOnButton() {
this.buttonPressed = !this.buttonPressed;
if (this.buttonPressed) {
this.buttonClickOn();
} else {
this.buttonClickOff();
}
}
}
};

View File

@ -24,26 +24,22 @@
display: grid;
grid-template-columns: 100px 100px 1fr;
grid-column-gap: 1px;
grid-row-gap: 1px;
margin-bottom: 1px;
grid-row-gap: 1px; // Used for grid within a swimlane for Plan views
width: 100%;
&.is-status--draft {
background: rgba($colorAlert, 0.2);
background: $colorTimeStripDraftBg;
}
[class*='__lane-label'] {
background: rgba($colorBodyFg, 0.2);
&__lane-label {
background: $colorTimeStripLabelBg;
color: $colorBodyFg;
padding: $interiorMarginSm;
}
[class*='--span-cols'] {
grid-column: span 2;
padding: $interiorMarginSm $interiorMargin;
}
&__lane-object {
background: rgba(black, 0.1);
height: 100%;
.c-plan {
display: contents;
@ -52,10 +48,18 @@
@include smallerControlButtons;
}
// Yet more brittle special case selecting...
.is-object-type-plan {
display: contents;
&__lane-label-button-h {
// Holds swimlane button(s)
flex: 1 1 auto;
text-align: right;
}
.--span-cols {
grid-column: span 2;
}
// Yet more brittle special case selecting...
.is-object-type-plan,
.is-object-type-gantt-chart {
display: contents;
}

View File

@ -1,9 +1,11 @@
@use 'sass:math';
.c-timesystem-axis {
$h: 30px;
height: $h;
svg {
$lineC: rgba($colorBodyFg, 0.3) !important;
$lineC: $colorInteriorBorder; //rgba($colorBodyFg, 0.3) !important;
text-rendering: geometricPrecision;
width: 100%;
height: 100%;
@ -26,21 +28,42 @@
}
}
.nowMarker {
width: 2px;
&__mb-line {
$c: $colorTimeRealtimeBtnBgMajor;
$w: 13px;
$wHalf: math.floor(math.div($w, 2));
//$h: 5px;
//$hHalf: math.floor(math.div($h, 2));
$transform: translateX(($wHalf - 1) * -1);
border-right: 2px dashed $c;
pointer-events: none;
width: 1px;
position: absolute;
z-index: 10;
background: gray;
&:before,
&:after {
//background: $c;
content: '';
display: block;
position: absolute;
width: 0;
height: 0;
transform: $transform;
border-left: $wHalf solid transparent;
border-right: $wHalf solid transparent;
border-top: $wHalf solid $c;
}
&:after {
bottom: 0;
transform: $transform rotate(180deg);
}
&.hidden {
display: none;
}
& .icon-arrow-down {
font-size: large;
position: absolute;
top: -8px;
left: -8px;
}
}
}