Compare commits

..

83 Commits

Author SHA1 Message Date
6f97117c42 Update src/ui/composables/extendedLines.js
Co-authored-by: Even Stensberg <evenstensberg@gmail.com>
2025-04-23 09:50:05 -07:00
2c61739542 Merge branch '7936-add-discrete-event-visualization' of https://github.com/nasa/openmct into event-visualization-composable 2025-04-23 09:49:34 -07:00
c1f7243263 Remove commented obsolete code. 2025-04-21 12:09:15 -07:00
5a364cea00 Remove timelinePlugin argument to EventTimeline plugin 2025-03-19 11:05:28 -07:00
1ea2cf9ce5 Disable extended lines on unmount 2025-03-19 10:45:51 -07:00
25ca7d4321 Remove references to ExtendedLinesBus 2025-03-18 11:39:41 -07:00
39ce53f561 Use composable instead of using event emitters for extended lines overlays 2025-03-18 11:32:09 -07:00
2e35212c32 Decode object path 2025-03-07 07:33:45 -08:00
8545ddeaff remove eventemitter and use native custom events 2025-03-07 12:01:26 +01:00
55d15caa63 add documentation as to why we need the label hint for EventTimelineView 2025-03-07 11:24:13 +01:00
bbae9c6048 use this.el instead of hardcoded layout ref 2025-03-07 11:17:52 +01:00
af6f3274cb remove extraneous class 2025-03-07 10:49:28 +01:00
b171f82e94 Ensure now marker shows up correctly even when there are no plots to send tick updates 2025-03-06 16:35:48 -08:00
88b43314d9 Handle right offset alignment for event timeline view 2025-03-06 16:35:23 -08:00
14a5c47efa Increase the priority of EventtimelineView 2025-03-05 17:10:29 -08:00
8be969fb7a Increase priority of event timeline view provider 2025-03-05 16:50:54 -08:00
157bde841e Fix linting issues 2025-03-05 16:17:13 -08:00
74a5d7e1f3 Merge branch 'master' of https://github.com/nasa/openmct into 7936-add-discrete-event-visualization 2025-03-05 16:11:21 -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
151 changed files with 3003 additions and 2713 deletions

View File

@ -484,8 +484,7 @@
"darkmatter",
"Undeletes",
"SSSZ",
"pageerror",
"annotatable"
"pageerror"
],
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],
"ignorePaths": [

View File

@ -56,7 +56,7 @@ jobs:
run: npm run cov:e2e:report
- name: Publish Results to Codecov.io
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/e2e/lcov.info

12
API.md
View File

@ -31,10 +31,6 @@
- [`latest` request strategy](#latest-request-strategy)
- [`minmax` request strategy](#minmax-request-strategy)
- [Telemetry Formats](#telemetry-formats)
- [Built-in Formats](#built-in-formats)
- [**Number Format (default):**](#number-format-default)
- [**String Format**](#string-format)
- [**Enum Format**](#enum-format)
- [Registering Formats](#registering-formats)
- [Telemetry Data](#telemetry-data)
- [Telemetry Datums](#telemetry-datums)
@ -63,12 +59,6 @@
- [Custom Indicators](#custom-indicators)
- [Priority API](#priority-api)
- [Priority Types](#priority-types)
- [User API](#user-api)
- [Example](#example)
- [Visibility-Based Rendering in View Providers](#visibility-based-rendering-in-view-providers)
- [Overview](#overview)
- [Implementing Visibility-Based Rendering](#implementing-visibility-based-rendering)
- [Example](#example-1)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
@ -1311,11 +1301,9 @@ Open MCT provides some built-in priority values that can be used in the applicat
Currently, the Open MCT Priority API provides (type: numeric value):
- HIGHEST: Infinity
- HIGH: 1000
- Default: 0
- LOW: -1000
- LOWEST: -Infinity
View provider Example:

View File

@ -1,10 +1,6 @@
codecov:
require_ci_to_pass: false #This setting will update the bot regardless of whether or not tests pass
# Disabling annotations for now. They are incorrectly labelling lines as lacking coverage when they are in fact covered by tests.
github_checks:
annotations: false
coverage:
status:
project:

View File

@ -2,6 +2,7 @@
module.exports = {
extends: ['plugin:playwright/recommended'],
rules: {
'playwright/max-nested-describe': ['error', { max: 1 }],
'playwright/expect-expect': 'off'
},
overrides: [

View File

@ -68,11 +68,7 @@ import { v4 as genUuid } from 'uuid';
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} [options.parent='mine'] - The Identifier or uuid of the parent object. Defaults to 'mine' folder
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
*/
async function createDomainObjectWithDefaults(
page,
{ type, name, parent = 'mine' },
additionalOptions = {}
) {
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
if (!name) {
name = `${type}:${genUuid()}`;
}
@ -93,13 +89,6 @@ async function createDomainObjectWithDefaults(
await page.getByLabel('Title', { exact: true }).fill('');
await page.getByLabel('Title', { exact: true }).fill(name);
if (additionalOptions) {
for (const [key, value] of Object.entries(additionalOptions)) {
// eslint-disable-next-line playwright/no-raw-locators
await page.locator(`#form-${key}`).fill(value);
}
}
if (page.testNotes) {
// Fill the "Notes" section with information about the
// currently running test and its project.
@ -116,7 +105,7 @@ async function createDomainObjectWithDefaults(
if (await _isInEditMode(page, uuid)) {
// Save (exit edit mode)
await page.getByRole('button', { name: 'Save', exact: true }).click();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
}

View File

@ -103,40 +103,25 @@ const extendedTest = test.extend({
* Default: `true`
*/
failOnConsoleError: [true, { option: true }],
ignore404s: [[], { option: true }],
/**
* Extends the base page class to enable console log error detection.
* @see {@link https://github.com/microsoft/playwright/discussions/11690 Github Discussion}
*/
page: async ({ page, failOnConsoleError, ignore404s }, use) => {
page: async ({ page, failOnConsoleError }, use) => {
// Capture any console errors during test execution
let messages = [];
const messages = [];
page.on('console', (msg) => messages.push(msg));
await use(page);
if (ignore404s.length > 0) {
messages = messages.filter((msg) => {
let keep = true;
if (msg.text().match(/404 \((Object )?Not Found\)/) !== null) {
keep = ignore404s.every((ignoreRule) => {
return msg.location().url.match(ignoreRule) === null;
});
}
return keep;
});
}
// Assert against console errors during teardown
if (failOnConsoleError) {
messages.forEach((msg) => {
messages.forEach((msg) =>
// eslint-disable-next-line playwright/no-standalone-expect
expect
.soft(msg.type(), `Console error detected: ${_consoleMessageToString(msg)}`)
.not.toEqual('error');
});
.not.toEqual('error')
);
}
}
});

View File

@ -224,7 +224,7 @@ export async function createTimelistWithPlanAndSetActivityInProgress(page, planJ
await page.getByRole('button', { name: 'Edit Object' }).click();
// Find the display properties section in the inspector
await page.getByRole('tab', { name: 'Config' }).click();
await page.getByRole('tab', { name: 'View Properties' }).click();
// Switch to expanded view and save the setting
await page.getByLabel('Display Style').selectOption({ label: 'Expanded' });

View File

@ -76,7 +76,6 @@ export async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {
export async function testTelemetryItem(page, telemetryItem) {
// Check that telemetry item also received the tag
await page.goto(telemetryItem.url);
await page.getByRole('tab', { name: 'Annotations' }).click();
await expect(page.getByText('No tags to display for this item')).toBeVisible();
@ -94,7 +93,6 @@ export async function testTelemetryItem(page, telemetryItem) {
y: 100
}
});
await page.getByRole('tab', { name: 'Annotations' }).click();
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeHidden();
@ -109,8 +107,6 @@ export async function basicTagsTests(page) {
// Search for Driving
await page.getByRole('searchbox', { name: 'Search Input' }).click();
await page.getByRole('tab', { name: 'Annotations' }).click();
// Clicking elsewhere should cause annotation selection to be cleared
await expect(page.getByText('No tags to display for this item')).toBeVisible();
//
@ -123,8 +119,6 @@ export async function basicTagsTests(page) {
.first()
.click();
await page.getByRole('tab', { name: 'Annotations' }).click();
// Delete Driving Tag
await page.hover('[aria-label="Tag"]:has-text("Driving")');
await page.locator('[aria-label="Remove tag Driving"]').click();
@ -161,8 +155,6 @@ export async function basicTagsTests(page) {
}
});
await page.getByRole('tab', { name: 'Annotations' }).click();
//Expect Science to be visible but Driving to be hidden
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeHidden();
@ -178,7 +170,7 @@ export async function basicTagsTests(page) {
});
// Add Driving Tag again
await page.getByRole('tab', { name: 'Annotations' }).click();
await page.getByText('Annotations').click();
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Driving').click();

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

@ -132,7 +132,7 @@ test("View a timelist in expanded view, verify all the activities are displayed
await page.getByRole('button', { name: 'Edit Object' }).click();
// Find the display properties section in the inspector
await page.getByRole('tab', { name: 'Config' }).click();
await page.getByRole('tab', { name: 'View Properties' }).click();
// Switch to expanded view and save the setting
await page.getByLabel('Display Style').selectOption({ label: 'Expanded' });

View File

@ -54,7 +54,8 @@ const examplePlanSmall1 = JSON.parse(
const TIME_TO_FROM_COLUMN = 2;
const HEADER_ROW = 0;
const NUM_COLUMNS = 5;
const FULL_CIRCLE_PATH = 'M0,-50A50,50,0,1,1,0,50A50,50,0,1,1,0,-50Z';
const FULL_CIRCLE_PATH =
'M3.061616997868383e-15,-50A50,50,0,1,1,-3.061616997868383e-15,50A50,50,0,1,1,3.061616997868383e-15,-50Z';
/**
* The regular expression used to parse the countdown string.

View File

@ -27,8 +27,7 @@ demonstrate some playwright for test developers. This pattern should not be re-u
import {
createDomainObjectWithDefaults,
createExampleTelemetryObject,
setRealTimeMode
createExampleTelemetryObject
} from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
@ -117,7 +116,7 @@ test.describe('Basic Condition Set Use', () => {
await page.getByLabel('Conditions View').click();
await expect(page.getByText('Current Output')).toBeVisible();
});
test('ConditionSet produces an output when telemetry is available, and does not when it is not', async ({
test('ConditionSet has correct outputs when telemetry is and is not available', async ({
page
}) => {
const exampleTelemetry = await createExampleTelemetryObject(page);
@ -282,101 +281,6 @@ test.describe('Basic Condition Set Use', () => {
await page.goto(exampleTelemetry.url);
});
test('Short circuit evaluation does not cause incorrect evaluation https://github.com/nasa/openmct/issues/7992', async ({
page
}) => {
await setRealTimeMode(page);
await page.getByLabel('Create', { exact: true }).click();
await page.getByLabel('State Generator').click();
await page.getByLabel('Title', { exact: true }).fill('P1');
await page.getByLabel('State Duration (seconds)').fill('1');
await page.getByLabel('Save').click();
await page.getByLabel('Create', { exact: true }).click();
await page.getByLabel('State Generator').click();
await page.getByLabel('Title', { exact: true }).fill('P2');
await page.getByLabel('State Duration (seconds)', { exact: true }).fill('1');
await page.getByRole('treeitem', { name: 'Test Condition Set' }).click();
await page.getByLabel('Save').click();
await page.getByLabel('Expand My Items folder').click();
await page.getByRole('treeitem', { name: 'Test Condition Set' }).click();
await page.getByLabel('Edit Object').click();
await page.getByLabel('Add Condition').click();
await page.getByLabel('Condition Name Input').first().fill('P1 IS ON AND P2 IS ON');
await page.getByLabel('Criterion Telemetry Selection').selectOption({ label: 'P1' });
await page.getByLabel('Criterion Metadata Selection').selectOption('value');
await page.getByLabel('Criterion Comparison Selection').selectOption('equalTo');
await page.getByLabel('Criterion Input').fill('1');
await page.getByLabel('Add Criteria - Enabled').click();
await page.getByLabel('Criterion Telemetry Selection').nth(1).selectOption({ label: 'P2' });
await page.getByLabel('Criterion Metadata Selection').nth(1).selectOption('value');
await page.getByLabel('Criterion Comparison Selection').nth(1).selectOption('equalTo');
await page.getByLabel('Criterion Input').nth(1).fill('1');
await page.getByLabel('Add Condition').click();
await page.getByLabel('Condition Name Input').first().fill('P1 IS OFF OR P2 IS OFF');
await page.getByLabel('Condition Trigger').first().selectOption('any');
await page.getByLabel('Criterion Telemetry Selection').first().selectOption({ label: 'P1' });
await page.getByLabel('Criterion Metadata Selection').first().selectOption('value');
await page.getByLabel('Criterion Comparison Selection').first().selectOption('equalTo');
await page.getByLabel('Criterion Input').first().fill('0');
await page.getByLabel('Add Criteria - Enabled').first().click();
await page.getByLabel('Criterion Telemetry Selection').nth(1).selectOption({ label: 'P2' });
await page.getByLabel('Criterion Metadata Selection').nth(1).selectOption('value');
await page.getByLabel('Criterion Comparison Selection').nth(1).selectOption('equalTo');
await page.getByLabel('Criterion Input').nth(1).fill('0');
await page.getByLabel('Condition Name Input').first().dblclick();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.getByLabel('Edit Object').click();
/**
* Create default conditions for test. Start with invalid values to put condition set into
* "default" state
*/
await page.getByLabel('Test Data Telemetry Selection').selectOption({ label: 'P1' });
await page.getByLabel('Test Data Metadata Selection').selectOption({ label: 'Value' });
await page.getByLabel('Test Data Input').fill('3');
await page.getByLabel('Add Test Datum').click();
await page.getByLabel('Test Data Telemetry Selection').nth(1).selectOption({ label: 'P2' });
await page.getByLabel('Test Data Metadata Selection').nth(1).selectOption({ label: 'Value' });
await page.getByLabel('Test Data Input').nth(1).fill('3');
await page.getByLabel('Apply Test Data').nth(1).click();
let activeCondition = page.getByLabel('Active Condition Set Condition');
let activeConditionName = activeCondition.getByLabel('Condition Name Label');
await expect(activeConditionName).toHaveText('Default');
/**
* Set P1 to 0
*/
await page.getByLabel('Test Data Input').nth(0).fill('0');
activeCondition = page.getByLabel('Active Condition Set Condition');
activeConditionName = activeCondition.getByLabel('Condition Name Label');
await expect(activeConditionName).toHaveText('P1 IS OFF OR P2 IS OFF');
/**
* Set P2 to 1
*/
await page.getByLabel('Test Data Input').nth(1).fill('1');
activeCondition = page.getByLabel('Active Condition Set Condition');
activeConditionName = activeCondition.getByLabel('Condition Name Label');
await expect(activeConditionName).toHaveText('P1 IS OFF OR P2 IS OFF');
/**
* Set P1 to 1
*/
await page.getByLabel('Test Data Input').nth(0).fill('1');
activeCondition = page.getByLabel('Active Condition Set Condition');
activeConditionName = activeCondition.getByLabel('Condition Name Label');
await expect(activeConditionName).toHaveText('P1 IS ON AND P2 IS ON');
});
test.fixme('Ensure condition sets work with telemetry like operator status', ({ page }) => {
test.info().annotations.push({
type: 'issue',

View File

@ -236,7 +236,7 @@ test.describe('Display Layout', () => {
name: new RegExp(sineWaveObject.name)
});
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
await page.getByLabel('Save', { exact: true }).click();
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Subscribe to the Sine Wave Generator data
@ -278,7 +278,7 @@ test.describe('Display Layout', () => {
name: new RegExp(sineWaveObject.name)
});
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
await page.getByLabel('Save', { exact: true }).click();
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Subscribe to the Sine Wave Generator data
@ -317,7 +317,7 @@ test.describe('Display Layout', () => {
name: new RegExp(sineWaveObject.name)
});
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
await page.getByLabel('Save', { exact: true }).click();
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
@ -358,7 +358,7 @@ test.describe('Display Layout', () => {
name: new RegExp(sineWaveObject.name)
});
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
await page.getByLabel('Save', { exact: true }).click();
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
@ -413,7 +413,7 @@ test.describe('Display Layout', () => {
await page.locator('div[title="Resize object width"] > input').click();
await page.locator('div[title="Resize object width"] > input').fill('70');
await page.getByLabel('Save', { exact: true }).click();
await page.locator('button[title="Save"]').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
const startDate = '2021-12-30 01:01:00.000Z';
@ -473,7 +473,7 @@ test.describe('Display Layout', () => {
await page.getByText('View type').click();
await page.getByText('Overlay Plot').click();
await page.getByLabel('Save', { exact: true }).click();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Time to inspect some network traffic
@ -531,7 +531,7 @@ test.describe('Display Layout', () => {
await stateGeneratorTreeItem.click({ button: 'right' });
await page.getByLabel('Edit Properties...').click();
await page.getByLabel('State Duration (seconds)', { exact: true }).fill('0.1');
await page.getByLabel('Save', { exact: true }).click();
await page.getByLabel('Save').click();
// Create a Table for filtering ON values
const tableFilterOnValue = await createDomainObjectWithDefaults(page, {
@ -555,14 +555,14 @@ test.describe('Display Layout', () => {
await page.goto(tableFilterOnValue.url);
await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));
await selectFilterOption(page, '1');
await page.getByLabel('Save', { exact: true }).click();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Navigate to OFF filtering table and add state generator and setup filters
await page.goto(tableFilterOffValue.url);
await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));
await selectFilterOption(page, '0');
await page.getByLabel('Save', { exact: true }).click();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Navigate to the display layout and edit it
@ -586,7 +586,7 @@ test.describe('Display Layout', () => {
// eslint-disable-next-line playwright/no-force-option
force: true
});
await page.getByLabel('Save', { exact: true }).click();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Get the tables so we can verify filtering is working as expected

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

@ -54,8 +54,8 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
await page.goto(exampleDataVisualizationSource.url);
await page.getByRole('cell', { name: /First Sine Wave Generator/ }).click();
await page.getByRole('tab', { name: 'Data Visualization' }).click();
await page.getByRole('cell', { name: /First Sine Wave Generator/ }).click();
await expect(page.getByText('Numeric Data')).toBeVisible();
await expect(
page.locator('span.plot-series-name', { hasText: 'First Sine Wave Generator Hz' })
@ -63,7 +63,6 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
await page.getByRole('cell', { name: /Second Sine Wave Generator/ }).click();
await page.getByRole('tab', { name: 'Data Visualization' }).click();
await expect(page.getByText('Numeric Data')).toBeVisible();
await expect(
page.locator('span.plot-series-name', { hasText: 'Second Sine Wave Generator Hz' })
@ -78,8 +77,6 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
// ensure our new tab's title is correct
const newPage = await pagePromise;
await newPage.waitForLoadState();
await page.getByRole('tab', { name: 'Data Visualization' }).click();
// expect new tab title to contain 'Second Sine Wave Generator'
await expect(newPage).toHaveTitle('Second Sine Wave Generator');

View File

@ -53,6 +53,7 @@ test.describe('Testing LAD table configuration', () => {
test('in edit mode, LAD Tables provide ability to hide columns', async ({ page }) => {
// Edit LAD table
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
// make sure headers are visible initially
await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();
@ -113,6 +114,7 @@ test.describe('Testing LAD table configuration', () => {
// Edit LAD table
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
// show timestamp column
await page.getByLabel('Timestamp', { exact: true }).check();
@ -140,6 +142,7 @@ test.describe('Testing LAD table configuration', () => {
// Edit LAD table
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
// show units, type, and WATCH columns
await page.getByLabel('Units').check();
@ -179,6 +182,7 @@ test.describe('Testing LAD table configuration', () => {
// Edit LAD table
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
// make sure Sine Wave headers are visible initially too
await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();

View File

@ -65,11 +65,9 @@ test.describe('Tagging in Notebooks @addInit', () => {
});
test('Can add tags with blank entry', async ({ page }) => {
await createDomainObjectWithDefaults(page, { type: 'Notebook' });
await enterTextEntry(page, '');
await page.getByRole('tab', { name: 'Annotations' }).click();
await enterTextEntry(page, '');
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();

View File

@ -47,6 +47,8 @@ test.describe('Notebook Tests with CouchDB @couchdb @network', () => {
});
test('Inspect Notebook Entry Network Requests', async ({ page }) => {
//Ensure we're on the annotations Tab in the inspector
await page.getByText('Annotations').click();
// Expand sidebar
await page.locator('.c-notebook__toggle-nav-button').click();
@ -84,9 +86,6 @@ test.describe('Notebook Tests with CouchDB @couchdb @network', () => {
await page.waitForLoadState('networkidle');
expect(notebookElementsRequests.length).toBeLessThanOrEqual(2);
//Ensure we're on the annotations Tab in the inspector
await page.getByText('Annotations').click();
// Add some tags
// Network Requests are for each tag creation are:
// 1) Getting the original path of the parent object
@ -181,8 +180,8 @@ test.describe('Notebook Tests with CouchDB @couchdb @network', () => {
type: 'issue',
description: 'https://github.com/akhenry/openmct-yamcs/issues/69'
});
await nbUtils.enterTextEntry(page, 'First Entry');
await page.getByText('Annotations').click();
await nbUtils.enterTextEntry(page, 'First Entry');
// Add three tags
await addTagAndAwaitNetwork(page, 'Science');

View File

@ -100,9 +100,6 @@ test.describe('Overlay Plot', () => {
await page.getByLabel('Expand By Default').check();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await page.getByRole('tab', { name: 'Config' }).click();
// Assert that the legend is now open
await expect(page.getByLabel('Plot Legend Collapsed')).toBeHidden();
await expect(page.getByLabel('Plot Legend Expanded')).toBeVisible();
@ -114,9 +111,6 @@ test.describe('Overlay Plot', () => {
// Assert that the legend is expanded on page load
await page.reload();
await page.getByRole('tab', { name: 'Config' }).click();
await expect(page.getByLabel('Plot Legend Collapsed')).toBeHidden();
await expect(page.getByLabel('Plot Legend Expanded')).toBeVisible();
await expect(page.getByRole('cell', { name: 'Name' })).toBeVisible();

View File

@ -1,244 +0,0 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
Tests to verify log plot functionality when objects are missing
*/
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
const SWG_NAME = 'Sine Wave Generator';
const OVERLAY_PLOT_NAME = 'Overlay Plot';
const STACKED_PLOT_NAME = 'Stacked Plot';
test.describe('For a default Plot View, Plot View Action:', () => {
let download;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const plot = await createDomainObjectWithDefaults(page, {
type: SWG_NAME,
name: SWG_NAME
});
await page.goto(plot.url);
// Set up dialog handler before clicking the export button
await page.getByLabel('More actions').click();
});
test.afterEach(async ({ page }) => {
if (download) {
await download.cancel();
}
});
test('Export as PNG, will suggest the correct default filename', async ({ page }) => {
// Start waiting for download before clicking. Note no await.
const downloadPromise = page.waitForEvent('download');
// trigger the download
await page.getByLabel('Export as PNG').click();
download = await downloadPromise;
// Verify the filename contains the expected pattern
expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.png`);
});
test('Export as JPG, will suggest the correct default filename', async ({ page }) => {
// Start waiting for download before clicking. Note no await.
const downloadPromise = page.waitForEvent('download');
// trigger the download
await page.getByLabel('Export as JPG').click();
download = await downloadPromise;
// Verify the filename contains the expected pattern
expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.jpeg`);
});
});
test.describe('For an Overlay Plot View, Plot View Action:', () => {
let download;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: OVERLAY_PLOT_NAME,
name: OVERLAY_PLOT_NAME
});
await createDomainObjectWithDefaults(page, {
type: SWG_NAME,
name: SWG_NAME,
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
// Set up dialog handler before clicking the export button
await page.getByLabel('More actions').click();
});
test.afterEach(async ({ page }) => {
if (download) {
await download.cancel();
}
});
test('Export as PNG, will suggest the correct default filename', async ({ page }) => {
// Start waiting for download before clicking. Note no await.
const downloadPromise = page.waitForEvent('download');
// trigger the download
await page.getByLabel('Export as PNG').click();
download = await downloadPromise;
// Verify the filename contains the expected pattern
expect(download.suggestedFilename()).toBe(`${OVERLAY_PLOT_NAME} - plot.png`);
});
test('Export as JPG, will suggest the correct default filename', async ({ page }) => {
// Start waiting for download before clicking. Note no await.
const downloadPromise = page.waitForEvent('download');
// trigger the download
await page.getByLabel('Export as JPG').click();
download = await downloadPromise;
// Verify the filename contains the expected pattern
expect(download.suggestedFilename()).toBe(`${OVERLAY_PLOT_NAME} - plot.jpeg`);
});
});
test.describe('For a Stacked Plot View, Plot View Action:', () => {
let download;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const stackedPlot = await createDomainObjectWithDefaults(page, {
type: STACKED_PLOT_NAME,
name: STACKED_PLOT_NAME
});
await createDomainObjectWithDefaults(page, {
type: SWG_NAME,
name: SWG_NAME,
parent: stackedPlot.uuid
});
await page.goto(stackedPlot.url);
// Set up dialog handler before clicking the export button
await page.getByLabel('More actions').click();
});
test.afterEach(async ({ page }) => {
if (download) {
await download.cancel();
}
});
test('Export as PNG, will suggest the correct default filename', async ({ page }) => {
// Start waiting for download before clicking. Note no await.
const downloadPromise = page.waitForEvent('download');
// trigger the download
await page.getByLabel('Export as PNG').click();
download = await downloadPromise;
// Verify the filename contains the expected pattern
expect(download.suggestedFilename()).toBe(`${STACKED_PLOT_NAME} - stacked-plot.png`);
});
test('Export as JPG, will suggest the correct default filename', async ({ page }) => {
// Start waiting for download before clicking. Note no await.
const downloadPromise = page.waitForEvent('download');
// trigger the download
await page.getByLabel('Export as JPG').click();
download = await downloadPromise;
// Verify the filename contains the expected pattern
expect(download.suggestedFilename()).toBe(`${STACKED_PLOT_NAME} - stacked-plot.jpeg`);
});
});
test.describe('Plot View Action:', () => {
let download;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
const plot = await createDomainObjectWithDefaults(page, {
type: SWG_NAME,
name: `!@#${SWG_NAME}!@#><`
});
await page.goto(plot.url);
// Set up dialog handler before clicking the export button
await page.getByLabel('More actions').click();
});
test.afterEach(async ({ page }) => {
if (download) {
await download.cancel();
}
});
test('Export as PNG saved filenames will not include invalid characters', async ({ page }) => {
// Start waiting for download before clicking. Note no await.
const downloadPromise = page.waitForEvent('download');
// trigger the download
await page.getByLabel('Export as PNG').click();
download = await downloadPromise;
// Verify the filename contains the expected pattern
expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.png`);
});
test('Export as JPG saved filenames will not include invalid characters', async ({ page }) => {
// Start waiting for download before clicking. Note no await.
const downloadPromise = page.waitForEvent('download');
// trigger the download
await page.getByLabel('Export as JPG').click();
download = await downloadPromise;
// Verify the filename contains the expected pattern
expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.jpeg`);
});
});

View File

@ -50,7 +50,7 @@ test.describe('Plots work in Previews', () => {
});
const layoutGridHolder = page.getByLabel('Test Display Layout Layout Grid');
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
await page.getByLabel('Save', { exact: true }).click();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// right click on the plot and select view large
@ -67,7 +67,7 @@ test.describe('Plots work in Previews', () => {
await page.getByLabel('Move Sub-object Frame').click();
await page.getByText('View type').click();
await page.getByText('Overlay Plot').click();
await page.getByLabel('Save', { exact: true }).click();
await page.getByLabel('Save').click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
await expect(
page.getByLabel('Test Display Layout Layout', { exact: true }).getByLabel('Plot Canvas')

View File

@ -152,14 +152,14 @@ test.describe('Stacked Plot', () => {
}) => {
await page.goto(stackedPlot.url);
await page.getByRole('tab', { name: 'Config' }).click();
// Click on the 1st plot
await page
.getByLabel('Stacked Plot Item Sine Wave Generator A')
.getByLabel('Plot Canvas')
.click();
await page.getByRole('tab', { name: 'Config' }).click();
// Assert that the inspector shows the Y Axis properties for swgA
await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
@ -172,9 +172,6 @@ test.describe('Stacked Plot', () => {
.getByLabel('Stacked Plot Item Sine Wave Generator B')
.getByLabel('Plot Canvas')
.click();
await page.getByRole('tab', { name: 'Config' }).click();
// Assert that the inspector shows the Y Axis properties for swgB
await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
@ -187,9 +184,6 @@ test.describe('Stacked Plot', () => {
.getByLabel('Stacked Plot Item Sine Wave Generator C')
.getByLabel('Plot Canvas')
.click();
await page.getByRole('tab', { name: 'Config' }).click();
// Assert that the inspector shows the Y Axis properties for swgB
await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
@ -200,7 +194,7 @@ test.describe('Stacked Plot', () => {
// Go into edit mode
await page.getByLabel('Edit Object').click();
// await page.getByRole('tab', { name: 'Config' }).click();
await page.getByRole('tab', { name: 'Config' }).click();
// Click on the 1st plot
await page.getByLabel('Stacked Plot Item Sine Wave Generator A').click();
@ -239,11 +233,11 @@ test.describe('Stacked Plot', () => {
// Go into edit mode
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'Config' }).click();
// Click on canvas for the 1st plot
await page.getByLabel(`Stacked Plot Item ${swgA.name}`).click();
await page.getByRole('tab', { name: 'Config' }).click();
// Expand config for the series
await page.getByLabel('Expand Sine Wave Generator A Plot Series Options').click();
@ -261,8 +255,6 @@ test.describe('Stacked Plot', () => {
// Click on canvas for the 1st plot
await page.getByLabel(`Stacked Plot Item ${swgA.name}`).click();
await page.getByRole('tab', { name: 'Config' }).click();
// Expand config for the series
await page.getByLabel('Expand Sine Wave Generator A Plot Series Options').click();

View File

@ -45,8 +45,6 @@ const setFontFamily = '"Andale Mono", sans-serif';
test.describe('Stacked Plot styling', () => {
let stackedPlot;
let overlayPlot1;
let overlayPlot2;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
@ -56,30 +54,17 @@ test.describe('Stacked Plot styling', () => {
name: 'StackedPlot1'
});
// create two overlay plots
overlayPlot1 = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: 'Overlay Plot 1',
parent: stackedPlot.uuid
});
overlayPlot2 = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: 'Overlay Plot 2',
parent: stackedPlot.uuid
});
// Create two SWGs and attach them to the Stacked Plot
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Sine Wave Generator 1',
parent: overlayPlot1.uuid
parent: stackedPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Sine Wave Generator 2',
parent: overlayPlot2.uuid
parent: stackedPlot.uuid
});
});
@ -153,21 +138,21 @@ test.describe('Stacked Plot styling', () => {
NO_STYLE_RGBA,
NO_STYLE_RGBA,
hexToRGB(setTextColor),
page.getByLabel('Stacked Plot Item Overlay Plot 1')
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
);
await checkStyles(
NO_STYLE_RGBA,
NO_STYLE_RGBA,
hexToRGB(setTextColor),
page.getByLabel('Stacked Plot Item Overlay Plot 2')
page.getByLabel('Stacked Plot Item Sine Wave Generator 2')
);
await checkFontStyles(
setFontSize,
setFontWeight,
setFontFamily,
page.getByLabel('Stacked Plot Item Overlay Plot 1')
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
);
});
@ -184,19 +169,19 @@ test.describe('Stacked Plot styling', () => {
await page.getByRole('tab', { name: 'Styles' }).click();
//Check default styles for overlayPlot1 and overlayPlot2
//Check default styles for SWG1 and SWG2
await checkStyles(
NO_STYLE_RGBA,
NO_STYLE_RGBA,
hexToRGB(defaultTextColor),
page.getByLabel('Stacked Plot Item Overlay Plot 1')
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
);
await checkStyles(
NO_STYLE_RGBA,
NO_STYLE_RGBA,
hexToRGB(defaultTextColor),
page.getByLabel('Stacked Plot Item Overlay Plot 2')
page.getByLabel('Stacked Plot Item Sine Wave Generator 2')
);
// Set styles using setStyles function on StackedPlot1 but not StackedPlot2
@ -205,11 +190,11 @@ test.describe('Stacked Plot styling', () => {
setBorderColor,
setBackgroundColor,
setTextColor,
page.getByLabel('Stacked Plot Item Overlay Plot 1')
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
);
//Set Font Styles on SWG1 but not SWG2
await page.getByLabel('Stacked Plot Item Overlay Plot 1').click();
await page.getByLabel('Stacked Plot Item Sine Wave Generator 1').click();
//Set Font Size to 72
await page.getByLabel('Set Font Size').click();
await page.getByRole('menuitem', { name: '72px' }).click();

View File

@ -31,8 +31,6 @@ import { expect, test } from '../../pluginFixtures.js';
test.describe('Grand Search', () => {
let grandSearchInput;
test.use({ ignore404s: [/_design\/object_names\/_view\/object_names$/] });
test.beforeEach(async ({ page }) => {
grandSearchInput = page
.getByLabel('OpenMCT Search')
@ -193,88 +191,7 @@ test.describe('Grand Search', () => {
await expect(searchResults).toContainText(folderName);
});
test.describe('Search will test for the presence of the object_names index, and', () => {
test('use index if available @couchdb @network', async ({ page }) => {
await createObjectsForSearch(page);
let isObjectNamesViewAvailable = false;
let isObjectNamesUsedForSearch = false;
page.on('request', async (request) => {
const isObjectNamesRequest = request.url().endsWith('_view/object_names');
const isHeadRequest = request.method().toLowerCase() === 'head';
if (isObjectNamesRequest && isHeadRequest) {
const response = await request.response();
isObjectNamesViewAvailable = response.status() === 200;
}
});
page.on('request', (request) => {
const isObjectNamesRequest = request.url().endsWith('_view/object_names');
const isPostRequest = request.method().toLowerCase() === 'post';
if (isObjectNamesRequest && isPostRequest) {
isObjectNamesUsedForSearch = true;
}
});
// Full search for object
await grandSearchInput.pressSequentially('Clock', { delay: 100 });
// Wait for search to finish
await waitForSearchCompletion(page);
expect(isObjectNamesViewAvailable).toBe(true);
expect(isObjectNamesUsedForSearch).toBe(true);
});
test('fall-back on base index if index not available @couchdb @network', async ({ page }) => {
await page.route('**/_view/object_names', (route) => {
route.fulfill({
status: 404
});
});
await createObjectsForSearch(page);
let isObjectNamesViewAvailable = false;
let isFindUsedForSearch = false;
page.on('request', async (request) => {
const isObjectNamesRequest = request.url().endsWith('_view/object_names');
const isHeadRequest = request.method().toLowerCase() === 'head';
if (isObjectNamesRequest && isHeadRequest) {
const response = await request.response();
isObjectNamesViewAvailable = response.status() === 200;
}
});
page.on('request', (request) => {
const isFindRequest = request.url().endsWith('_find');
const isPostRequest = request.method().toLowerCase() === 'post';
if (isFindRequest && isPostRequest) {
isFindUsedForSearch = true;
}
});
// Full search for object
await grandSearchInput.pressSequentially('Clock', { delay: 100 });
// Wait for search to finish
await waitForSearchCompletion(page);
console.info(
`isObjectNamesViewAvailable: ${isObjectNamesViewAvailable} | isFindUsedForSearch: ${isFindUsedForSearch}`
);
expect(isObjectNamesViewAvailable).toBe(false);
expect(isFindUsedForSearch).toBe(true);
});
});
test('Search results are debounced @couchdb @network', async ({ page }) => {
// Unfortunately 404s are always logged to the JavaScript console and can't be suppressed
// A 404 is now thrown when we test for the presence of the object names view used by search.
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6179'
@ -282,17 +199,11 @@ test.describe('Grand Search', () => {
await createObjectsForSearch(page);
let networkRequests = [];
page.on('request', (request) => {
const isSearchRequest =
request.url().endsWith('object_names') ||
request.url().endsWith('_find') ||
request.url().includes('by_keystring');
const isFetchRequest = request.resourceType() === 'fetch';
// CouchDB search results in a one-time head request to test for the presence of an index.
const isHeadRequest = request.method().toLowerCase() === 'head';
if (isSearchRequest && isFetchRequest && !isHeadRequest) {
const searchRequest =
request.url().endsWith('_find') || request.url().includes('by_keystring');
const fetchRequest = request.resourceType() === 'fetch';
if (searchRequest && fetchRequest) {
networkRequests.push(request);
}
});

View File

@ -1,5 +1,3 @@
/* eslint-disable playwright/no-conditional-in-test */
/* eslint-disable playwright/no-conditional-expect */
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
@ -33,104 +31,6 @@ Enim nec dui nunc mattis. Cursus turpis massa tincidunt dui ut. Donec adipiscing
Proin libero nunc consequat interdum varius sit amet mattis vulputate. Metus dictum at tempor commodo ullamcorper a lacus vestibulum sed. Quisque non tellus orci ac auctor augue mauris. Id ornare arcu odio ut. Rhoncus est pellentesque elit ullamcorper dignissim. Senectus et netus et malesuada fames ac turpis egestas. Volutpat ac tincidunt vitae semper quis lectus nulla. Adipiscing elit duis tristique sollicitudin. Ipsum faucibus vitae aliquet nec ullamcorper sit. Gravida neque convallis a cras semper auctor neque vitae tempus. Porttitor leo a diam sollicitudin tempor id. Dictum non consectetur a erat nam at lectus. At volutpat diam ut venenatis tellus in. Morbi enim nunc faucibus a pellentesque sit amet. Cursus in hac habitasse platea. Sed augue lacus viverra vitae.
`;
const viewsTabsMatrix = {
Clock: {
Browse: ['Properties']
},
'Condition Set': {
Browse: ['Properties', 'Elements', 'Annotations'],
Edit: ['Elements', 'Properties']
},
'Condition Widget': {
Browse: ['Properties', 'Styles'],
Edit: ['Styles', 'Properties']
},
'Display Layout': {
Browse: ['Properties', 'Elements', 'Styles'],
Edit: ['Elements', 'Styles', 'Properties']
},
'Event Message Generator': {
Browse: ['Properties']
},
'Event Message Generator with Acknowledge': {
Browse: ['Properties']
},
'Example Imagery': {
Browse: ['Properties', 'Annotations']
},
'Flexible Layout': {
Browse: ['Properties', 'Elements', 'Styles'],
Edit: ['Elements', 'Styles', 'Properties']
},
Folder: {
Browse: ['Properties']
},
'Gantt Chart': {
Browse: ['Properties', 'Config', 'Elements'],
Edit: ['Config', 'Elements', 'Properties']
},
Gauge: {
Browse: ['Properties', 'Elements', 'Styles'],
Edit: ['Elements', 'Styles', 'Properties']
},
Graph: {
Browse: ['Properties', 'Config', 'Elements', 'Styles'],
Edit: ['Config', 'Elements', 'Styles', 'Properties']
},
Hyperlink: {
Browse: ['Properties'],
required: {
url: 'https://www.google.com',
displayText: 'Google'
}
},
'LAD Table': {
Browse: ['Properties', 'Config', 'Elements', 'Styles'],
Edit: ['Config', 'Elements', 'Styles', 'Properties']
},
'LAD Table Set': {
Browse: ['Properties', 'Config', 'Elements', 'Styles'],
Edit: ['Config', 'Elements', 'Styles', 'Properties']
},
Notebook: {
Browse: ['Properties']
},
'Overlay Plot': {
Browse: ['Properties', 'Config', 'Annotations', 'Styles'],
Edit: ['Config', 'Elements', 'Styles', 'Filters', 'Properties']
},
'Scatter Plot': {
Browse: ['Properties', 'Config', 'Elements', 'Styles'],
Edit: ['Config', 'Elements', 'Styles', 'Properties']
},
'Sine Wave Generator': {
Browse: ['Properties', 'Annotations']
},
'Stacked Plot': {
Browse: ['Properties', 'Config', 'Annotations', 'Elements', 'Styles'],
Edit: ['Config', 'Elements', 'Styles', 'Properties']
},
'Tabs View': {
Browse: ['Properties', 'Elements', 'Styles'],
Edit: ['Elements', 'Styles', 'Properties']
},
'Telemetry Table': {
Browse: ['Properties', 'Config', 'Elements', 'Styles'],
Edit: ['Config', 'Elements', 'Styles', 'Filters', 'Properties']
},
'Time List': {
Browse: ['Properties', 'Config', 'Elements'],
Edit: ['Config', 'Elements', 'Properties']
},
'Time Strip': {
Browse: ['Properties', 'Elements'],
Edit: ['Elements', 'Properties']
},
Timer: {
Browse: ['Properties']
}
};
test.describe('Inspector tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
@ -172,49 +72,4 @@ test.describe('Inspector tests', () => {
await expect(lastInspectorPropertyValue).toBeInViewport();
});
test(`Inspector tabs show the correct tabs per view and mode`, async ({ page }) => {
// loop through each view type
for (const view of Object.keys(viewsTabsMatrix)) {
const viewConfig = viewsTabsMatrix[view];
const createOptions = {
type: view,
name: view
};
// create and navigate to view;
const objectInfo = await createDomainObjectWithDefaults(
page,
createOptions,
viewConfig.required ? viewConfig.required : {}
);
await page.goto(objectInfo.url);
// verify correct number of tabs for browse mode
expect(await page.getByRole('tab').count()).toBe(Object.keys(viewConfig.Browse).length);
// verify correct order of tabs for browse mode
for (const [index, value] of Object.entries(viewConfig.Browse)) {
const tab = page.getByRole('tab').nth(index);
await expect(tab).toHaveText(value);
}
// enter Edit if necessary
if (viewConfig.Edit) {
await page.getByLabel('Edit Object').click();
// verify correct number of tabs for edit mode
expect(await page.getByRole('tab').count()).toBe(Object.keys(viewConfig.Edit).length);
// verify correct order of tabs for edit mode
for (const [index, value] of Object.entries(viewConfig.Edit)) {
const tab = page.getByRole('tab').nth(index);
await expect(tab).toHaveText(value);
}
await page.getByLabel('Save').first().click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
}
}
});
});

View File

@ -213,6 +213,7 @@ test.describe('Navigation memory leak is not detected in', () => {
page,
'example-imagery-memory-leak-test'
);
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
@ -316,12 +317,6 @@ test.describe('Navigation memory leak is not detected in', () => {
// Manually invoke the garbage collector once all references are removed.
window.gc();
window.gc();
window.gc();
setTimeout(() => {
window.gc();
}, 1000);
return gcPromise;
});

View File

@ -40,9 +40,6 @@ test.describe('Visual - Inspector @ally @clock', () => {
});
test('Inspector from overlay_plot_with_delay_storage @localStorage', async ({ page, theme }) => {
// navigate to the plot
await page.getByRole('gridcell', { name: 'Overlay Plot with 5s Delay' }).click();
//Expand the Inspector Pane
await page.getByRole('button', { name: 'Inspect' }).click();

View File

@ -83,7 +83,7 @@ test.describe('Grand Search @a11y', () => {
);
// Save and finish editing the Display Layout
await page.getByRole('button', { name: 'Save', exact: true }).click();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Search for the object

View File

@ -100,12 +100,9 @@ test.describe('Flexible Layout styling @a11y', () => {
);
// Save Flexible Layout
await page.getByRole('button', { name: 'Save', exact: true }).click();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// Select styles tab
await page.getByRole('tab', { name: 'Styles' }).click();
await percySnapshot(
page,
`Saved Styled Flex Layout with Styled StackedPlot (theme: '${theme}')`
@ -127,30 +124,17 @@ test.describe('Stacked Plot styling @a11y', () => {
name: 'StackedPlot1'
});
// Create an overlay plots to hold the SWGs
const overlayPlot1 = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: 'Overlay Plot 1',
parent: stackedPlot.uuid
});
const overlayPlot2 = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: 'Overlay Plot 2',
parent: stackedPlot.uuid
});
// Create two SWGs and attach them to the overlay plots
// Create two SWGs and attach them to the Stacked Plot
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Sine Wave Generator 1',
parent: overlayPlot1.uuid
parent: stackedPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Sine Wave Generator 2',
parent: overlayPlot2.uuid
parent: stackedPlot.uuid
});
});
@ -193,7 +177,7 @@ test.describe('Stacked Plot styling @a11y', () => {
setBorderColor,
setBackgroundColor,
setTextColor,
page.getByLabel('Stacked Plot Item Overlay Plot 1')
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
);
await percySnapshot(page, `Edit Mode StackedPlot with Styled SWG (theme: '${theme}')`);

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,12 @@ class EventMetadataProvider {
{
key: 'message',
name: 'Message',
format: 'string'
format: 'string',
hints: {
// this is used in the EventTimelineView to provide a title for the event
// label can be changed to other properties for the title (e.g., the `name` property)
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

@ -234,6 +234,7 @@
openmct.install(openmct.plugins.Timelist());
openmct.install(openmct.plugins.BarChart());
openmct.install(openmct.plugins.ScatterPlot());
openmct.install(openmct.plugins.EventTimestripPlugin());
document.addEventListener('DOMContentLoaded', function () {
openmct.start();
});

1260
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,25 +16,25 @@
],
"devDependencies": {
"@babel/eslint-parser": "7.23.3",
"@braintree/sanitize-url": "7.1.1",
"@braintree/sanitize-url": "6.0.4",
"@types/d3-axis": "3.0.6",
"@types/d3-scale": "4.0.8",
"@types/d3-selection": "3.0.10",
"@types/d3-shape": "3.1.7",
"@types/d3-shape": "3.0.0",
"@types/eventemitter3": "1.2.0",
"@types/jasmine": "5.1.2",
"@types/lodash": "4.17.0",
"@vue/compiler-sfc": "3.4.3",
"babel-loader": "9.1.0",
"babel-plugin-istanbul": "7.0.0",
"babel-plugin-istanbul": "6.1.1",
"comma-separated-values": "3.6.4",
"copy-webpack-plugin": "13.0.0",
"copy-webpack-plugin": "12.0.2",
"cspell": "7.3.8",
"css-loader": "6.10.0",
"d3-axis": "3.0.0",
"d3-scale": "4.0.2",
"d3-selection": "3.0.0",
"d3-shape": "3.2.0",
"d3-shape": "3.0.0",
"eslint": "8.56.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-compat": "4.2.0",
@ -51,7 +51,7 @@
"git-rev-sync": "3.0.2",
"html2canvas": "1.4.1",
"imports-loader": "5.0.0",
"jasmine-core": "5.6.0",
"jasmine-core": "5.1.1",
"karma": "6.4.2",
"karma-chrome-launcher": "3.2.0",
"karma-cli": "2.0.0",
@ -64,14 +64,14 @@
"karma-webpack": "5.0.1",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"marked": "15.0.7",
"mini-css-extract-plugin": "2.9.2",
"marked": "12.0.0",
"mini-css-extract-plugin": "2.7.6",
"moment": "2.30.1",
"moment-duration-format": "2.3.2",
"moment-timezone": "0.5.41",
"nano": "10.1.4",
"npm-run-all2": "7.0.2",
"nyc": "17.1.0",
"npm-run-all2": "6.1.2",
"nyc": "15.1.0",
"painterro": "1.2.87",
"plotly.js-basic-dist-min": "2.29.1",
"plotly.js-gl2d-dist-min": "2.20.0",
@ -79,21 +79,21 @@
"prettier-eslint": "16.3.0",
"printj": "1.3.1",
"resolve-url-loader": "5.0.0",
"sanitize-html": "2.15.0",
"sanitize-html": "2.12.1",
"sass": "1.71.1",
"sass-loader": "14.1.1",
"style-loader": "4.0.0",
"style-loader": "3.3.3",
"terser-webpack-plugin": "5.3.9",
"tiny-emitter": "2.1.0",
"typescript": "5.3.3",
"uuid": "11.1.0",
"uuid": "9.0.1",
"vue": "3.4.24",
"vue-eslint-parser": "9.4.2",
"vue-loader": "16.8.3",
"webpack": "5.98.0",
"webpack": "5.90.3",
"webpack-cli": "5.1.1",
"webpack-dev-server": "5.0.2",
"webpack-merge": "6.0.1"
"webpack-merge": "5.10.0"
},
"scripts": {
"clean": "rm -rf ./dist ./node_modules ./coverage ./html-test-results ./e2e/test-results ./.nyc_output ./e2e/.nyc_output",

View File

@ -582,15 +582,4 @@ export default class AnnotationAPI extends EventEmitter {
_.isEqual(targets, otherTargets)
);
}
/**
* Checks if the given type is annotatable
* @param {string} type The type to check
* @returns {boolean} Returns true if the type is annotatable
*/
isAnnotatableType(type) {
const types = this.openmct.types.getAllTypes();
return types[type]?.definition?.annotatable;
}
}

View File

@ -20,8 +20,6 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { isIdentifier } from '../objects/object-utils';
/**
* @typedef {import('openmct').DomainObject} DomainObject
*/
@ -211,15 +209,9 @@ export default class CompositionCollection {
this.#cleanUpMutables();
const children = await this.#provider.load(this.domainObject);
const childObjects = await Promise.all(
children.map((child) => {
if (isIdentifier(child)) {
return this.#publicAPI.objects.get(child, abortSignal);
} else {
return Promise.resolve(child);
}
})
children.map((c) => this.#publicAPI.objects.get(c, abortSignal))
);
childObjects.forEach((child) => this.add(child, true));
childObjects.forEach((c) => this.add(c, true));
this.#emit('load');
return childObjects;

View File

@ -96,9 +96,8 @@ export default class CompositionProvider {
* object.
* @param {DomainObject} domainObject the domain object
* for which to load composition
* @returns {Promise<Identifier[] | DomainObject[]>} a promise for
* the Identifiers or Domain Objects in this composition. If Identifiers are returned,
* they will be automatically resolved to domain objects by the API.
* @returns {Promise<Identifier[]>} a promise for
* the Identifiers in this composition
*/
load(domainObject) {
throw new Error('This method must be implemented by a subclass.');

View File

@ -21,7 +21,7 @@
*****************************************************************************/
import { toRaw } from 'vue';
import { makeKeyString, parseKeyString } from '../objects/object-utils.js';
import { makeKeyString } from '../objects/object-utils.js';
import CompositionProvider from './CompositionProvider.js';
/**
@ -75,11 +75,7 @@ export default class DefaultCompositionProvider extends CompositionProvider {
* the Identifiers in this composition
*/
load(domainObject) {
const identifiers = domainObject.composition
.filter((idOrKeystring) => idOrKeystring !== null && idOrKeystring !== undefined)
.map((idOrKeystring) => parseKeyString(idOrKeystring));
return Promise.all(identifiers);
return Promise.all(domainObject.composition);
}
/**
* Attach listeners for changes to the composition of a given domain object.

View File

@ -27,7 +27,6 @@ import ConflictError from './ConflictError.js';
import InMemorySearchProvider from './InMemorySearchProvider.js';
import InterceptorRegistry from './InterceptorRegistry.js';
import MutableDomainObject from './MutableDomainObject.js';
import { isIdentifier, isKeyString } from './object-utils.js';
import RootObjectProvider from './RootObjectProvider.js';
import RootRegistry from './RootRegistry.js';
import Transaction from './Transaction.js';
@ -743,19 +742,11 @@ export default class ObjectAPI {
* @param {AbortSignal} abortSignal (optional) signal to abort fetch requests
* @returns {Promise<Array<DomainObject>>} a promise containing an array of domain objects
*/
async getOriginalPath(identifierOrObject, path = [], abortSignal = null) {
let domainObject;
if (isKeyString(identifierOrObject) || isIdentifier(identifierOrObject)) {
domainObject = await this.get(identifierOrObject, abortSignal);
} else {
domainObject = identifierOrObject;
}
async getOriginalPath(identifier, path = [], abortSignal = null) {
const domainObject = await this.get(identifier, abortSignal);
if (!domainObject) {
return [];
}
path.push(domainObject);
const { location } = domainObject;
if (location && !this.#pathContainsDomainObject(location, path)) {

View File

@ -21,11 +21,8 @@
*****************************************************************************/
const PRIORITIES = Object.freeze({
HIGHEST: Infinity,
HIGH: 1000,
DEFAULT: 0,
LOW: -1000,
LOWEST: -Infinity
LOW: -1000
});
export default PRIORITIES;

View File

@ -284,33 +284,6 @@ export default class TelemetryAPI {
return value;
}
/**
* Determines whether a domain object has numeric telemetry data.
* A domain object has numeric telemetry if it:
* 1. Has a telemetry property
* 2. Has telemetry metadata with domain values (like timestamps)
* 3. Has range values (measurements) where at least one is numeric
*
* @method hasNumericTelemetry
* @param {import('openmct').DomainObject} domainObject The domain object to check
* @returns {boolean} True if the object has numeric telemetry, false otherwise
*/
hasNumericTelemetry(domainObject) {
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {
return false;
}
const metadata = this.openmct.telemetry.getMetadata(domainObject);
const rangeValues = metadata.valuesForHints(['range']);
const domains = metadata.valuesForHints(['domain']);
return (
domains.length > 0 &&
rangeValues.length > 0 &&
!rangeValues.every((value) => value.format === 'string')
);
}
/**
* Generates a numeric hash value for an options object. The hash is consistent
* for equivalent option objects regardless of property order.

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

@ -89,17 +89,6 @@ export default class TypeRegistry {
get(typeKey) {
return this.types[typeKey] || UNKNOWN_TYPE;
}
/**
* List all registered types.
* @returns {Type[]} all registered types
*/
getAllTypes() {
return this.types;
}
/**
* Import legacy types.
* @param {TypeDefinition[]} types the types to import
*/
importLegacyTypes(types) {
types
.filter((t) => this.get(t.key) === UNKNOWN_TYPE)

View File

@ -25,14 +25,10 @@
* Originally created by hudsonfoo on 09/02/16
*/
function sanitizeFilename(filename) {
const replacedPeriods = filename.replace(/\./g, '_');
const safeFilename = replacedPeriods.replace(/[^a-zA-Z0-9_\-.\s]/g, '');
function replaceDotsWithUnderscores(filename) {
const regex = /\./gi;
// Handle leading/trailing spaces and periods
const trimmedFilename = safeFilename.trim().replace(/^\.+|\.+$/g, '');
return trimmedFilename;
return filename.replace(regex, '_');
}
import { saveAs } from 'file-saver';
@ -154,7 +150,7 @@ class ImageExporter {
* @returns {promise}
*/
async exportJPG(element, filename, className) {
const processedFilename = sanitizeFilename(filename);
const processedFilename = replaceDotsWithUnderscores(filename);
const img = await this.renderElement(element, {
imageType: 'jpg',
@ -171,7 +167,7 @@ class ImageExporter {
* @returns {promise}
*/
async exportPNG(element, filename, className) {
const processedFilename = sanitizeFilename(filename);
const processedFilename = replaceDotsWithUnderscores(filename);
const img = await this.renderElement(element, {
imageType: 'png',

View File

@ -24,9 +24,6 @@ export default function (folderName, couchPlugin, searchFilter) {
location: 'ROOT'
});
}
},
search() {
return Promise.resolve([]);
}
});
@ -38,17 +35,9 @@ export default function (folderName, couchPlugin, searchFilter) {
);
},
load() {
let searchResults;
if (searchFilter.viewName !== undefined) {
// Use a view to search, instead of an _all_docs find
searchResults = couchProvider.getObjectsByView(searchFilter);
} else {
// Use the _find endpoint to search _all_docs
searchResults = couchProvider.getObjectsByFilter(searchFilter);
}
return searchResults;
return couchProvider.getObjectsByFilter(searchFilter).then((objects) => {
return objects.map((object) => object.identifier);
});
}
});
};

View File

@ -41,10 +41,9 @@ export default class LADTableConfiguration extends EventEmitter {
}
getConfiguration() {
const configuration = this.domainObject.configuration ?? {};
configuration.hiddenColumns = configuration.hiddenColumns ?? {};
const configuration = this.domainObject.configuration || {};
configuration.hiddenColumns = configuration.hiddenColumns || {};
configuration.isFixedLayout = configuration.isFixedLayout ?? true;
configuration.objectStyles = configuration.objectStyles ?? {};
return configuration;
}

View File

@ -27,7 +27,7 @@ import LadTableConfiguration from './components/LadTableConfiguration.vue';
export default function LADTableConfigurationViewProvider(openmct) {
return {
key: 'lad-table-configuration',
name: 'Config',
name: 'LAD Table Configuration',
canView(selection) {
if (selection.length !== 1 || selection[0].length === 0) {
return false;
@ -61,7 +61,7 @@ export default function LADTableConfigurationViewProvider(openmct) {
_destroy = destroy;
},
priority() {
return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;
return 1;
},
destroy() {
if (_destroy) {

View File

@ -22,24 +22,28 @@
<template>
<div class="c-inspect-properties">
<div class="c-inspect-properties__header">Table Column Visibility</div>
<ul class="c-inspect-properties__section">
<li v-for="(title, key) in headers" :key="key" class="c-inspect-properties__row">
<div class="c-inspect-properties__label" title="Show or hide column">
<label :for="key + 'ColumnControl'">{{ title }}</label>
</div>
<div class="c-inspect-properties__value">
<input
v-if="isEditing"
:id="key + 'ColumnControl'"
type="checkbox"
:checked="configuration.hiddenColumns[key] !== true"
@change="toggleColumn(key)"
/>
<span v-if="!isEditing && configuration.hiddenColumns[key] !== true">Visible</span>
</div>
</li>
</ul>
<template v-if="isEditing">
<div class="c-inspect-properties__header">Table Column Visibility</div>
<ul class="c-inspect-properties__section">
<li v-for="(title, key) in headers" :key="key" class="c-inspect-properties__row">
<div class="c-inspect-properties__label" title="Show or hide column">
<label :for="key + 'ColumnControl'">{{ title }}</label>
</div>
<div class="c-inspect-properties__value">
<input
:id="key + 'ColumnControl'"
type="checkbox"
:checked="configuration.hiddenColumns[key] !== true"
@change="toggleColumn(key)"
/>
</div>
</li>
</ul>
</template>
<template v-else>
<div class="c-inspect-properties__header">LAD Table Configuration</div>
<div class="c-inspect-properties__row--span-all">Only available in edit mode.</div>
</template>
</div>
</template>
@ -58,8 +62,7 @@ export default {
isEditing: this.openmct.editor.isEditing(),
configuration: ladTableConfiguration.getConfiguration(),
items: [],
ladTableObjects: [],
ladTelemetryObjects: {}
ladTableObjects: []
};
},
computed: {
@ -147,14 +150,11 @@ export default {
this.ladTableObjects.push(ladTable);
const composition = this.openmct.composition.get(ladTable.domainObject);
composition.on('add', this.addItem);
composition.on('remove', this.removeItem);
composition.load();
this.compositions.push({
composition,
addCallback: this.addItem,
removeCallback: this.removeItem
composition
});
},
removeLadTable(identifier) {

View File

@ -39,9 +39,6 @@ export default function plugin() {
cssClass: 'icon-tabular-lad',
initialize(domainObject) {
domainObject.composition = [];
domainObject.configuration = {
objectStyles: {}
};
}
});

View File

@ -41,7 +41,7 @@ export default function BarGraphInspectorViewProvider(openmct) {
_destroy = destroy;
},
priority: function () {
return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;
return openmct.priority.HIGH + 1;
},
destroy: function () {
if (_destroy) {

View File

@ -40,7 +40,7 @@ export default function ScatterPlotInspectorViewProvider(openmct) {
_destroy = destroy;
},
priority: function () {
return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;
return openmct.priority.HIGH + 1;
},
destroy: function () {
if (_destroy) {

View File

@ -44,57 +44,48 @@ import { getLatestTimestamp } from './utils/time.js';
* }
*/
export default class Condition extends EventEmitter {
#definition;
/**
* Manages criteria and emits the result of - true or false - based on criteria evaluated.
* @constructor
* @param definition: {id: uuid,trigger: enum, criteria: Array of {id: uuid, operation: enum, input: Array, metaDataKey: string, key: {domainObject.identifier} }
* @param conditionConfiguration: {id: uuid,trigger: enum, criteria: Array of {id: uuid, operation: enum, input: Array, metaDataKey: string, key: {domainObject.identifier} }
* @param openmct
* @param conditionManager
*/
constructor(definition, openmct, conditionManager) {
constructor(conditionConfiguration, openmct, conditionManager) {
super();
this.openmct = openmct;
this.conditionManager = conditionManager;
this.id = conditionConfiguration.id;
this.criteria = [];
this.result = undefined;
this.timeSystems = this.openmct.time.getAllTimeSystems();
this.#definition = definition;
if (definition.configuration.criteria) {
this.createCriteria(definition.configuration.criteria);
if (conditionConfiguration.configuration.criteria) {
this.createCriteria(conditionConfiguration.configuration.criteria);
}
this.trigger = definition.configuration.trigger;
this.trigger = conditionConfiguration.configuration.trigger;
this.summary = '';
this.handleCriterionUpdated = this.handleCriterionUpdated.bind(this);
this.handleOldTelemetryCriterion = this.handleOldTelemetryCriterion.bind(this);
this.handleTelemetryStaleness = this.handleTelemetryStaleness.bind(this);
}
get id() {
return this.#definition.id;
}
get configuration() {
return this.#definition.configuration;
}
updateResult(latestDataTable, telemetryIdThatChanged) {
if (!latestDataTable) {
updateResult(datum) {
if (!datum || !datum.id) {
console.log('no data received');
return;
}
// if all the criteria in this condition have no telemetry, we want to force the condition result to evaluate
if (this.hasNoTelemetry() || this.isTelemetryUsed(telemetryIdThatChanged)) {
const currentTimeSystemKey = this.openmct.time.getTimeSystem().key;
if (this.hasNoTelemetry() || this.isTelemetryUsed(datum.id)) {
this.criteria.forEach((criterion) => {
if (this.isAnyOrAllTelemetry(criterion)) {
criterion.updateResult(latestDataTable, this.conditionManager.telemetryObjects);
criterion.updateResult(datum, this.conditionManager.telemetryObjects);
} else {
const relevantDatum = latestDataTable.get(criterion.telemetryObjectIdAsString);
if (criterion.shouldUpdateResult(relevantDatum, currentTimeSystemKey)) {
criterion.updateResult(relevantDatum, currentTimeSystemKey);
if (criterion.usesTelemetry(datum.id)) {
criterion.updateResult(datum);
}
}
});
@ -111,11 +102,9 @@ export default class Condition extends EventEmitter {
}
hasNoTelemetry() {
const usesSomeTelemetry = this.criteria.some((criterion) => {
return this.isAnyOrAllTelemetry(criterion) || criterion.telemetry !== '';
return this.criteria.every((criterion) => {
return !this.isAnyOrAllTelemetry(criterion) && criterion.telemetry === '';
});
return !usesSomeTelemetry;
}
isTelemetryUsed(id) {
@ -193,7 +182,7 @@ export default class Condition extends EventEmitter {
findCriterion(id) {
let criterion;
for (let i = 0; i < this.criteria.length; i++) {
for (let i = 0, ii = this.criteria.length; i < ii; i++) {
if (this.criteria[i].id === id) {
criterion = {
item: this.criteria[i],
@ -258,7 +247,7 @@ export default class Condition extends EventEmitter {
this.timeSystems,
this.openmct.time.getTimeSystem()
);
this.conditionManager.updateCurrentCondition(latestTimestamp, this);
this.conditionManager.updateCurrentCondition(latestTimestamp);
}
handleTelemetryStaleness() {

View File

@ -27,12 +27,6 @@ import Condition from './Condition.js';
import { getLatestTimestamp } from './utils/time.js';
export default class ConditionManager extends EventEmitter {
#latestDataTable = new Map();
/**
* @param {import('openmct.js').DomainObject} conditionSetDomainObject
* @param {import('openmct.js').OpenMCT} openmct
*/
constructor(conditionSetDomainObject, openmct) {
super();
this.openmct = openmct;
@ -310,6 +304,22 @@ export default class ConditionManager extends EventEmitter {
this.persistConditions();
}
getCurrentCondition() {
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
let currentCondition = conditionCollection[conditionCollection.length - 1];
for (let i = 0; i < conditionCollection.length - 1; i++) {
const condition = this.findConditionById(conditionCollection[i].id);
if (condition.result) {
//first condition to be true wins
currentCondition = conditionCollection[i];
break;
}
}
return currentCondition;
}
getCurrentConditionLAD(conditionResults) {
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
let currentCondition = conditionCollection[conditionCollection.length - 1];
@ -400,34 +410,26 @@ export default class ConditionManager extends EventEmitter {
const normalizedDatum = this.createNormalizedDatum(datum, endpoint);
const timeSystemKey = this.openmct.time.getTimeSystem().key;
let timestamp = {};
const currentTimestamp = normalizedDatum[timeSystemKey];
const timestamp = {};
timestamp[timeSystemKey] = currentTimestamp;
this.#latestDataTable.set(normalizedDatum.id, normalizedDatum);
if (this.shouldEvaluateNewTelemetry(currentTimestamp)) {
const matchingCondition = this.updateConditionResults(normalizedDatum.id);
this.updateCurrentCondition(timestamp, matchingCondition);
this.updateConditionResults(normalizedDatum);
this.updateCurrentCondition(timestamp);
}
}
updateConditionResults(keyStringForUpdatedTelemetryObject) {
updateConditionResults(normalizedDatum) {
//We want to stop when the first condition evaluates to true.
const matchingCondition = this.conditions.find((condition) => {
condition.updateResult(this.#latestDataTable, keyStringForUpdatedTelemetryObject);
this.conditions.some((condition) => {
condition.updateResult(normalizedDatum);
return condition.result === true;
});
return matchingCondition;
}
updateCurrentCondition(timestamp, matchingCondition) {
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
const defaultCondition = conditionCollection[conditionCollection.length - 1];
const currentCondition = matchingCondition || defaultCondition;
updateCurrentCondition(timestamp) {
const currentCondition = this.getCurrentCondition();
this.emit(
'conditionSetResultUpdated',
@ -442,13 +444,11 @@ export default class ConditionManager extends EventEmitter {
);
}
getTestData(metadatum, identifier) {
getTestData(metadatum) {
let data = undefined;
if (this.testData.applied) {
const found = this.testData.conditionTestInputs.find(
(testInput) =>
testInput.metadata === metadatum.source &&
this.openmct.objects.areIdsEqual(testInput.telemetry, identifier)
(testInput) => testInput.metadata === metadatum.source
);
if (found) {
data = found.value;
@ -463,7 +463,7 @@ export default class ConditionManager extends EventEmitter {
const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas;
const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => {
const testValue = this.getTestData(metadatum, endpoint.identifier);
const testValue = this.getTestData(metadatum);
const formatter = this.openmct.telemetry.getValueFormatter(metadatum);
datum[metadatum.key] =
testValue !== undefined
@ -480,7 +480,7 @@ export default class ConditionManager extends EventEmitter {
updateTestData(testData) {
if (!_.isEqual(testData, this.testData)) {
this.testData = JSON.parse(JSON.stringify(testData));
this.testData = testData;
this.openmct.objects.mutate(
this.conditionSetDomainObject,
'configuration.conditionTestData',

View File

@ -53,7 +53,6 @@ describe('The condition', function () {
valueMetadatas: [
{
key: 'some-key',
source: 'some-key',
name: 'Some attribute',
hints: {
range: 2
@ -61,7 +60,6 @@ describe('The condition', function () {
},
{
key: 'utc',
source: 'utc',
name: 'Time',
format: 'utc',
hints: {
@ -90,32 +88,17 @@ describe('The condition', function () {
openmct.telemetry = jasmine.createSpyObj('telemetry', [
'isTelemetryObject',
'subscribe',
'getMetadata',
'getValueFormatter'
'getMetadata'
]);
openmct.telemetry.isTelemetryObject.and.returnValue(true);
openmct.telemetry.subscribe.and.returnValue(function () {});
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);
openmct.telemetry.getValueFormatter.and.callFake((metadatum) => {
return {
parse(input) {
return input;
}
};
});
mockTimeSystems = {
key: 'utc'
};
openmct.time = jasmine.createSpyObj('time', [
'getTimeSystem',
'getAllTimeSystems',
'on',
'off'
]);
openmct.time.getTimeSystem.and.returnValue({ key: 'utc' });
openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems', 'on', 'off']);
openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]);
//openmct.time.getTimeSystem.and.returnValue();
openmct.time.on.and.returnValue(() => {});
openmct.time.off.and.returnValue(() => {});
@ -130,7 +113,7 @@ describe('The condition', function () {
id: '1234-5678-9999-0000',
operation: 'equalTo',
input: ['0'],
metadata: 'testSource',
metadata: 'value',
telemetry: testTelemetryObject.identifier
}
]
@ -173,24 +156,37 @@ describe('The condition', function () {
expect(conditionObj.criteria.length).toEqual(0);
});
it('keeps the old result new telemetry data is not used by it', function () {
const latestDataTable = new Map();
latestDataTable.set(testTelemetryObject.identifier.key, {
it('gets the result of a condition when new telemetry data is received', function () {
conditionObj.updateResult({
value: '0',
utc: 'Hi',
id: testTelemetryObject.identifier.key
});
conditionObj.updateResult(latestDataTable, testTelemetryObject.identifier.key);
expect(conditionObj.result).toBeTrue();
});
it('gets the result of a condition when new telemetry data is received', function () {
conditionObj.updateResult({
value: '1',
utc: 'Hi',
id: testTelemetryObject.identifier.key
});
expect(conditionObj.result).toBeFalse();
});
it('keeps the old result new telemetry data is not used by it', function () {
conditionObj.updateResult({
value: '0',
utc: 'Hi',
id: testTelemetryObject.identifier.key
});
expect(conditionObj.result).toBeTrue();
latestDataTable.set('1234', {
conditionObj.updateResult({
value: '1',
utc: 'Hi',
id: '1234'
});
conditionObj.updateResult(latestDataTable, '1234');
expect(conditionObj.result).toBeTrue();
});
});

View File

@ -24,7 +24,7 @@
<div
class="c-condition-h"
:class="{ 'is-drag-target': draggingOver }"
:aria-label="conditionSetLabel"
aria-label="Condition Set Condition"
@dragover.prevent
@drop.prevent="dropCondition($event, conditionIndex)"
@dragenter="dragEnter($event, conditionIndex)"
@ -53,9 +53,7 @@
@click="expanded = !expanded"
></span>
<span class="c-condition__name" aria-label="Condition Name Label">{{
condition.configuration.name
}}</span>
<span class="c-condition__name">{{ condition.configuration.name }}</span>
<span class="c-condition__summary">
<template v-if="!condition.isDefault && !canEvaluateCriteria"> Define criteria </template>
<span v-else>
@ -261,17 +259,6 @@ export default {
};
},
computed: {
conditionSetLabel() {
let label;
if (this.condition.id === this.currentConditionId) {
label = 'Active Condition Set Condition';
} else {
label = 'Condition Set Condition';
}
return label;
},
triggers() {
const keys = Object.keys(TRIGGER);
const triggerOptions = [];

View File

@ -114,7 +114,7 @@
class="c-button c-button--major icon-plus labeled"
@click="addTestInput"
>
<span class="c-cs-button__label" aria-label="Add Test Datum">Add Test Datum</span>
<span class="c-cs-button__label">Add Test Datum</span>
</button>
</div>
</section>

View File

@ -181,20 +181,13 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
if (validatedData && !this.isStalenessCheck()) {
if (this.isOldCheck()) {
Object.keys(this.telemetryDataCache).forEach((objectIdKeystring) => {
if (this.ageCheck?.[objectIdKeystring]) {
this.ageCheck[objectIdKeystring].update(validatedData[objectIdKeystring]);
}
if (this.ageCheck?.[validatedData.id]) {
this.ageCheck[validatedData.id].update(validatedData);
}
this.telemetryDataCache[objectIdKeystring] = false;
});
this.telemetryDataCache[validatedData.id] = false;
} else {
Object.keys(this.telemetryDataCache).forEach((objectIdKeystring) => {
const telemetryObject = telemetryObjects[objectIdKeystring];
this.telemetryDataCache[objectIdKeystring] = this.computeResult(
this.createNormalizedDatum(validatedData[objectIdKeystring], telemetryObject)
);
});
this.telemetryDataCache[validatedData.id] = this.computeResult(validatedData);
}
}

View File

@ -29,29 +29,21 @@ import { getOperatorText, OPERATIONS } from '../utils/operations.js';
import { checkIfOld } from '../utils/time.js';
export default class TelemetryCriterion extends EventEmitter {
#lastUpdated;
#lastTimeSystem;
#comparator;
/**
* Subscribes/Unsubscribes to telemetry and emits the result
* of operations performed on the telemetry data returned and a given input value.
* @constructor
* @param telemetryDomainObjectDefinition {id: uuid, operation: enum, input: Array, metadata: string, key: {domainObject.identifier} }
* @param {import('../../../MCT.js').OpenMCT} openmct
* @param openmct
*/
constructor(telemetryDomainObjectDefinition, openmct) {
super();
/**
* @type {import('../../../MCT.js').MCT}
*/
this.openmct = openmct;
this.telemetryDomainObjectDefinition = telemetryDomainObjectDefinition;
this.id = telemetryDomainObjectDefinition.id;
this.telemetry = telemetryDomainObjectDefinition.telemetry;
this.operation = telemetryDomainObjectDefinition.operation;
this.#comparator = this.#findOperation(this.operation);
this.input = telemetryDomainObjectDefinition.input;
this.metadata = telemetryDomainObjectDefinition.metadata;
this.result = undefined;
@ -91,6 +83,7 @@ export default class TelemetryCriterion extends EventEmitter {
if (this.ageCheck) {
this.ageCheck.clear();
}
this.ageCheck = checkIfOld(this.handleOldTelemetry.bind(this), this.input[0] * 1000);
}
@ -160,6 +153,7 @@ export default class TelemetryCriterion extends EventEmitter {
createNormalizedDatum(telemetryDatum, endpoint) {
const id = this.openmct.objects.makeKeyString(endpoint.identifier);
const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas;
const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => {
const formatter = this.openmct.telemetry.getValueFormatter(metadatum);
datum[metadatum.key] = formatter.parse(telemetryDatum[metadatum.source]);
@ -185,18 +179,9 @@ export default class TelemetryCriterion extends EventEmitter {
return datum;
}
shouldUpdateResult(datum, timesystem) {
const dataIsDefined = datum !== undefined;
const hasTimeSystemChanged =
this.#lastTimeSystem === undefined || this.#lastTimeSystem !== timesystem;
const isCacheStale = this.#lastUpdated === undefined || datum[timesystem] > this.#lastUpdated;
return dataIsDefined && (hasTimeSystemChanged || isCacheStale);
}
updateResult(data, currentTimeSystemKey) {
const validatedData = this.isValid()
? this.createNormalizedDatum(data, this.telemetryObject)
: {};
updateResult(data) {
const validatedData = this.isValid() ? data : {};
if (!this.isStalenessCheck()) {
if (this.isOldCheck()) {
@ -208,8 +193,6 @@ export default class TelemetryCriterion extends EventEmitter {
} else {
this.result = this.computeResult(validatedData);
}
this.#lastUpdated = data[currentTimeSystemKey];
this.#lastTimeSystem = currentTimeSystemKey;
}
}
@ -253,8 +236,8 @@ export default class TelemetryCriterion extends EventEmitter {
});
}
#findOperation(operation) {
for (let i = 0; i < OPERATIONS.length; i++) {
findOperation(operation) {
for (let i = 0, ii = OPERATIONS.length; i < ii; i++) {
if (operation === OPERATIONS[i].name) {
return OPERATIONS[i].operation;
}
@ -266,14 +249,15 @@ export default class TelemetryCriterion extends EventEmitter {
computeResult(data) {
let result = false;
if (data) {
let comparator = this.findOperation(this.operation);
let params = [];
params.push(data[this.metadata]);
if (this.isValidInput()) {
this.input.forEach((input) => params.push(input));
}
if (typeof this.#comparator === 'function') {
result = Boolean(this.#comparator(params));
if (typeof comparator === 'function') {
result = Boolean(comparator(params));
}
}

View File

@ -106,7 +106,7 @@ describe('The telemetry criterion', function () {
id: 'test-criterion-id',
telemetry: openmct.objects.makeKeyString(testTelemetryObject.identifier),
operation: 'textContains',
metadata: 'testSource',
metadata: 'value',
input: ['Hell'],
telemetryObjects: { [testTelemetryObject.identifier.key]: testTelemetryObject }
};

View File

@ -1,38 +0,0 @@
/*****************************************************************************
* 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 default function conditionWidgetStylesInterceptor(openmct) {
return {
appliesTo: (identifier, domainObject) => {
return domainObject?.type === 'conditionWidget' && !domainObject.configuration?.objectStyles;
},
invoke: (identifier, domainObject) => {
if (!domainObject.configuration) {
domainObject.configuration = {};
}
domainObject.configuration.objectStyles = {};
return domainObject;
}
};
}

View File

@ -20,13 +20,11 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import conditionWidgetStylesInterceptor from './conditionWidgetStylesInterceptor.js';
import ConditionWidgetViewProvider from './ConditionWidgetViewProvider.js';
export default function plugin() {
return function install(openmct) {
openmct.objectViews.addProvider(new ConditionWidgetViewProvider(openmct));
openmct.objects.addGetInterceptor(conditionWidgetStylesInterceptor(openmct));
openmct.types.addType('conditionWidget', {
key: 'conditionWidget',
@ -36,9 +34,7 @@ export default function plugin() {
creatable: true,
cssClass: 'icon-condition-widget',
initialize(domainObject) {
domainObject.configuration = {
objectStyles: {}
};
domainObject.configuration = {};
domainObject.label = 'Condition Widget';
domainObject.conditionalLabel = '';
domainObject.url = '';

View File

@ -65,9 +65,7 @@ class AlphanumericFormatView {
}
priority() {
return this.openmct.editor.isEditing()
? this.openmct.priority.DEFAULT
: this.openmct.priority.LOW;
return 1;
}
destroy() {

View File

@ -31,8 +31,7 @@ export default function DisplayLayoutType() {
domainObject.composition = [];
domainObject.configuration = {
items: [],
layoutGrid: [10, 10],
objectStyles: {}
layoutGrid: [10, 10]
};
},
form: [

View File

@ -32,16 +32,13 @@
</div>
<div class="c-inspect-properties__value">
<input
v-if="isEditing"
id="telemetryPrintfFormat"
type="text"
:disabled="!isEditing"
:value="telemetryFormat"
:placeholder="nonMixedFormat ? '' : 'Mixed'"
@change="formatTelemetry"
/>
<template v-if="!isEditing && telemetryFormat?.length">
{{ telemetryFormat }}
</template>
</div>
</li>
</ul>

View File

@ -1,40 +0,0 @@
/*****************************************************************************
* 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 default function displayLayoutStylesInterceptor(openmct) {
return {
appliesTo: (identifier, domainObject) => {
return domainObject?.type === 'layout';
},
invoke: (identifier, domainObject) => {
if (!domainObject.configuration) {
domainObject.configuration = {};
}
if (!domainObject.configuration.objectStyles) {
domainObject.configuration.objectStyles = {};
}
return domainObject;
}
};
}

View File

@ -25,7 +25,6 @@ import mount from 'utils/mount';
import CopyToClipboardAction from './actions/CopyToClipboardAction.js';
import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js';
import DisplayLayout from './components/DisplayLayout.vue';
import displayLayoutStylesInterceptor from './displayLayoutStylesInterceptor.js';
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
import DisplayLayoutType from './DisplayLayoutType.js';
import DisplayLayoutDrawingObjectTypes from './DrawingObjectTypes.js';
@ -124,7 +123,6 @@ export default function DisplayLayoutPlugin(options) {
return 100;
}
});
openmct.objects.addGetInterceptor(displayLayoutStylesInterceptor(openmct));
openmct.types.addType('layout', DisplayLayoutType());
openmct.toolbars.addProvider(new DisplayLayoutToolbar(openmct));
openmct.inspectorViews.addProvider(new AlphaNumericFormatViewProvider(openmct, options));

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,100 @@
/*****************************************************************************
* 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) {
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 + 1;
},
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
},
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,43 @@
<!--
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-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>
</template>
<script>
export default {
inject: ['openmct', 'domainObject', 'event']
};
</script>

View File

@ -0,0 +1,386 @@
<!--
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">
<SwimLane v-if="eventItems.length" :is-nested="true" :hide-label="true">
<template #object>
<div ref="eventsContainer" class="c-events-tsv__container">
<div
v-for="event in eventItems"
:id="`wrapper-${event.time}`"
:ref="`wrapper-${event.time}`"
:key="event.id"
:aria-label="titleKey ? `${event[titleKey]}` : ''"
class="c-events-tsv__event-line"
:class="event.limitClass || ''"
:style="`left: ${event.left}px`"
@mouseenter="showToolTip(event)"
@mouseleave="dismissToolTip(event)"
@click.stop="createSelectionForInspector(event)"
></div>
</div>
</template>
</SwimLane>
<div v-else class="c-timeline__no-items">No events within timeframe</div>
</div>
</template>
<script>
import { scaleLinear, scaleUtc } from 'd3-scale';
import _ from 'lodash';
import { inject } from 'vue';
import SwimLane from '@/ui/components/swim-lane/SwimLane.vue';
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
import { useAlignment } from '../../../ui/composables/alignmentContext.js';
import { useExtendedLines } from '../../../ui/composables/extendedLines';
import eventData from '../mixins/eventData.js';
const PADDING = 1;
const AXES_PADDING = 20;
export default {
components: { SwimLane },
mixins: [eventData, tooltipHelpers],
inject: ['openmct', 'domainObject', 'objectPath'],
setup() {
const domainObject = inject('domainObject');
const objectPath = inject('objectPath');
const openmct = inject('openmct');
const { alignment: alignmentData } = useAlignment(domainObject, objectPath, openmct);
const {
extendedLines: extendedLines,
update: updateExtendedLines,
updateLineHover: updateExtendedLineHover,
remove: removeLinesForObject
} = useExtendedLines(domainObject, objectPath, openmct);
return {
alignmentData,
extendedLines,
updateExtendedLineHover,
updateExtendedLines,
removeLinesForObject
};
},
data() {
return {
eventItems: [],
eventHistory: [],
titleKey: null
};
},
computed: {
alignmentStyle() {
let leftMargin = 0;
let rightMargin = 0;
if (this.alignmentData.leftWidth) {
const leftOffset = this.alignmentData.multiple ? 2 * AXES_PADDING : AXES_PADDING;
leftMargin = `${this.alignmentData.leftWidth + leftOffset}px`;
}
if (this.alignmentData.rightWidth) {
rightMargin = `${this.alignmentData.rightWidth + AXES_PADDING}px`;
}
return {
margin: `0 ${rightMargin} 0 ${leftMargin}`
};
}
},
watch: {
eventHistory: {
handler() {
this.updateEventItems();
},
deep: true
},
alignmentData: {
handler() {
this.setScaleAndPlotEvents(this.timeSystem);
},
deep: true
},
extendedLines: {
handler() {
const { enabled = false } = this.extendedLines[this.keyString];
if (this.extendedLinesEnabled !== enabled) {
this.extendedLinesEnabled = enabled === true;
this.updateLines();
}
},
deep: true
}
},
created() {
this.valueMetadata = {};
this.height = 0;
this.timeSystem = this.openmct.time.getTimeSystem();
this.extendedLinesEnabled = 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);
},
beforeUnmount() {
if (this.eventStripResizeObserver) {
this.eventStripResizeObserver.disconnect();
}
this.stopFollowingTimeContext();
if (this.unlisten) {
this.unlisten();
}
},
methods: {
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on('timeSystem', this.setScaleAndPlotEvents);
this.timeContext.on('boundsChanged', this.updateViewBounds);
},
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() {
// Try to use the components own element first
let clientWidth = this.$refs.events?.clientWidth;
if (!clientWidth) {
// Fallback: use the actual container element (the immediate parent)
const parent = this.$el.parentElement;
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,
hoverEnabled: false
};
});
this.updateLines();
}
},
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.updateExtendedLineHover({
keyString: this.keyString,
id: event.time,
hoverEnabled: true
});
},
dismissToolTip(event) {
this.hideToolTip();
this.updateExtendedLineHover({
keyString: this.keyString,
id: event.time
});
},
updateLines() {
let lines = [];
if (this.extendedLinesEnabled) {
lines = this.eventItems.map((e) => ({
x: e.left,
limitClass: e.limitClass,
id: e.time,
hoverEnabled: false
}));
}
this.updateExtendedLines({
lines,
keyString: this.keyString
});
}
}
};
</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

@ -20,21 +20,12 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default function gaugeStylesInterceptor(openmct) {
return {
appliesTo: (identifier, domainObject) => {
return domainObject?.type === 'gauge';
},
invoke: (identifier, domainObject) => {
if (!domainObject.configuration) {
domainObject.configuration = {};
}
import EventInspectorViewProvider from './EventInspectorViewProvider.js';
import EventTimelineViewProvider from './EventTimelineViewProvider.js';
if (!domainObject.configuration.objectStyles) {
domainObject.configuration.objectStyles = {};
}
return domainObject;
}
export default function plugin() {
return function install(openmct) {
openmct.objectViews.addProvider(new EventTimelineViewProvider(openmct));
openmct.inspectorViews.addProvider(new EventInspectorViewProvider(openmct));
};
}

View File

@ -63,7 +63,7 @@ export default function FaultManagementInspectorViewProvider(openmct) {
_destroy = destroy;
},
priority: function () {
return openmct.priority.HIGH;
return openmct.priority.HIGH + 1;
},
destroy: function () {
if (_destroy) {

View File

@ -43,6 +43,8 @@ export default class FiltersInspectorViewProvider {
let openmct = this.openmct;
let _destroy = null;
const domainObject = selection?.[0]?.[0]?.context?.item;
return {
show: function (element) {
const { destroy } = mount(
@ -67,6 +69,13 @@ export default class FiltersInspectorViewProvider {
if (isEditing) {
return true;
}
const metadata = openmct.telemetry.getMetadata(domainObject);
const metadataWithFilters = metadata
? metadata.valueMetadatas.filter((value) => value.filters)
: [];
return metadataWithFilters.length;
},
priority: function () {
return openmct.priority.DEFAULT;

View File

@ -38,9 +38,6 @@
@update-filters="persistFilters"
/>
</ul>
<span v-else>
This view doesn't include any parameters that have configured filter criteria.
</span>
</template>
<script>

View File

@ -158,7 +158,6 @@ export default {
this.composition.on('remove', this.removeChildObject);
this.composition.on('add', this.addFrame);
this.composition.load();
this.unObserveContainers = this.openmct.objects.observe(
this.domainObject,
'configuration.containers',

View File

@ -1,40 +0,0 @@
/*****************************************************************************
* 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 default function flexibleLayoutStylesInterceptor(openmct) {
return {
appliesTo: (identifier, domainObject) => {
return domainObject?.type === 'flexible-layout';
},
invoke: (identifier, domainObject) => {
if (!domainObject.configuration) {
domainObject.configuration = {};
}
if (!domainObject.configuration.objectStyles) {
domainObject.configuration.objectStyles = {};
}
return domainObject;
}
};
}

View File

@ -20,7 +20,6 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import flexibleLayoutStylesInterceptor from './flexibleLayoutStylesInterceptor.js';
import FlexibleLayoutViewProvider from './flexibleLayoutViewProvider.js';
import ToolBarProvider from './toolbarProvider.js';
import Container from './utils/container.js';
@ -38,13 +37,11 @@ export default function plugin() {
initialize: function (domainObject) {
domainObject.configuration = {
containers: [new Container(50), new Container(50)],
rowsLayout: false,
objectStyles: {}
rowsLayout: false
};
domainObject.composition = [];
}
});
openmct.objects.addGetInterceptor(flexibleLayoutStylesInterceptor(openmct));
let toolbar = ToolBarProvider(openmct);

View File

@ -21,10 +21,28 @@
*****************************************************************************/
export default function GaugeCompositionPolicy(openmct) {
function hasNumericTelemetry(domainObject) {
const hasTelemetry = openmct.telemetry.isTelemetryObject(domainObject);
if (!hasTelemetry) {
return false;
}
const metadata = openmct.telemetry.getMetadata(domainObject);
return metadata.values().length > 0 && hasDomainAndRange(metadata);
}
function hasDomainAndRange(metadata) {
return (
metadata.valuesForHints(['range']).length > 0 &&
metadata.valuesForHints(['domain']).length > 0
);
}
return {
allow: function (parent, child) {
if (parent.type === 'gauge') {
return openmct.telemetry.hasNumericTelemetry(child);
return hasNumericTelemetry(child);
}
return true;

View File

@ -24,7 +24,6 @@ import mount from 'utils/mount';
import GaugeFormController from './components/GaugeFormController.vue';
import GaugeCompositionPolicy from './GaugeCompositionPolicy.js';
import gaugeStylesInterceptor from './gaugeStylesInterceptor.js';
import GaugeViewProvider from './GaugeViewProvider.js';
export const GAUGE_TYPES = [
@ -38,7 +37,7 @@ export const GAUGE_TYPES = [
export default function () {
return function install(openmct) {
openmct.objectViews.addProvider(new GaugeViewProvider(openmct));
openmct.objects.addGetInterceptor(gaugeStylesInterceptor(openmct));
openmct.forms.addNewFormControl('gauge-controller', getGaugeFormController(openmct));
openmct.types.addType('gauge', {
name: 'Gauge',
@ -60,8 +59,7 @@ export default function () {
max: 100,
min: 0,
precision: 2
},
objectStyles: {}
}
};
},
form: [

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

@ -30,30 +30,17 @@ export default function AnnotationsViewProvider(openmct) {
name: 'Annotations',
canView: function (selection) {
const availableTags = openmct.annotation.getAvailableTags();
const selectionContext = selection?.[0]?.[0]?.context;
const domainObject = selectionContext?.item;
const isLayoutItem = selectionContext?.layoutItem;
if (availableTags.length < 1 || isLayoutItem || !domainObject || openmct.editor.isEditing()) {
if (availableTags.length < 1) {
return false;
}
const isAnnotatableType = openmct.annotation.isAnnotatableType(domainObject.type);
const metadata = openmct.telemetry.getMetadata(domainObject);
const hasImagery = metadata?.valuesForHints(['image']).length > 0;
const isNotebookEntry = selectionContext?.type === 'notebook-entry-selection';
const hasNumericTelemetry = openmct.telemetry.hasNumericTelemetry(domainObject);
return isAnnotatableType || hasImagery || hasNumericTelemetry || isNotebookEntry;
return selection.length;
},
view: function (selection) {
let _destroy = null;
const selectionContext = selection?.[0]?.[0]?.context;
const isImageSelection = selectionContext?.type === 'clicked-on-image-selection';
const domainObject = selectionContext?.item;
const isNotebookEntry = selectionContext?.type === 'notebook-entry-selection';
const isConditionSet = domainObject?.type === 'conditionSet';
const domainObject = selection?.[0]?.[0]?.context?.item;
return {
show: function (element) {
@ -77,14 +64,6 @@ export default function AnnotationsViewProvider(openmct) {
_destroy = destroy;
},
priority: function () {
if (isNotebookEntry || isImageSelection) {
return openmct.priority.HIGHEST;
}
if (isConditionSet) {
return openmct.priority.LOW;
}
return openmct.priority.DEFAULT;
},
destroy: function () {

View File

@ -22,7 +22,6 @@
<template>
<li
v-if="allowDrag"
draggable="true"
:aria-label="`${elementObject.name} Element Item`"
:aria-grabbed="hover"
@ -48,22 +47,6 @@
/>
</div>
</li>
<li v-else :aria-label="`${elementObject.name} Element Item`">
<div
class="c-tree__item c-elements-pool__item js-elements-pool__item"
:class="{
'is-context-clicked': contextClickActive,
hover: hover,
'is-alias': isAlias
}"
>
<ObjectLabel
:domain-object="elementObject"
:object-path="[elementObject, domainObject]"
@context-click-active="setContextClickState"
/>
</div>
</li>
</template>
<script>
@ -91,9 +74,6 @@ export default {
},
allowDrop: {
type: Boolean
},
allowDrag: {
type: Boolean
}
},
emits: ['drop-custom', 'dragstart-custom'],

View File

@ -39,7 +39,6 @@
:key="element.identifier.key"
:index="index"
:element-object="element"
:allow-drag="isEditing"
:allow-drop="allowDrop"
@dragstart-custom="moveFrom(index)"
@drop-custom="moveTo(index)"

View File

@ -31,9 +31,8 @@ export default function ElementsViewProvider(openmct) {
canView: function (selection) {
const hasValidSelection = selection?.length;
const isOverlayPlot = selection?.[0]?.[0]?.context?.item?.type === 'telemetry.plot.overlay';
const isFolder = selection?.[0]?.[0]?.context?.item?.type === 'folder';
return hasValidSelection && !isOverlayPlot && !isFolder;
return hasValidSelection && !isOverlayPlot;
},
view: function (selection) {
let _destroy = null;
@ -63,10 +62,10 @@ export default function ElementsViewProvider(openmct) {
showTab: function (isEditing) {
const hasComposition = Boolean(domainObject && openmct.composition.get(domainObject));
return hasComposition;
return hasComposition && isEditing;
},
priority: function () {
return openmct.editor.isEditing() ? openmct.priority.DEFAULT : openmct.priority.LOW;
return openmct.priority.DEFAULT;
},
destroy: function () {
if (_destroy) {

View File

@ -30,9 +30,7 @@ export default function PropertiesViewProvider(openmct) {
name: 'Properties',
glyph: 'icon-info',
canView: function (selection) {
const domainObject = selection?.[0]?.[0]?.context?.item;
return domainObject && selection.length > 0;
return selection.length > 0;
},
view: function (selection) {
let _destroy = null;
@ -58,7 +56,7 @@ export default function PropertiesViewProvider(openmct) {
_destroy = destroy;
},
priority: function () {
return openmct.editor.isEditing() ? openmct.priority.LOW : openmct.priority.HIGH;
return openmct.priority.DEFAULT;
},
destroy: function () {
if (_destroy) {

View File

@ -25,24 +25,7 @@ import mount from 'utils/mount';
import StylesInspectorView from './StylesInspectorView.vue';
import stylesManager from './StylesManager.js';
const NON_STYLABLE_TYPES = [
'clock',
'conditionSet',
'eventGenerator',
'eventGeneratorWithAcknowledge',
'example.imagery',
'folder',
'gantt-chart',
'generator',
'hyperlink',
'notebook',
'restricted-notebook',
'summary-widget',
'time-strip',
'timelist',
'timer',
'webPage'
];
const NON_STYLABLE_TYPES = ['folder', 'webPage', 'conditionSet', 'summary-widget', 'hyperlink'];
function isLayoutObject(selection, objectType) {
//we allow conditionSets to be styled if they're part of a layout
@ -52,8 +35,8 @@ function isLayoutObject(selection, objectType) {
);
}
function isCreatableObject(object, typeObject) {
return NON_STYLABLE_TYPES.indexOf(object.type) < 0 && typeObject.definition.creatable;
function isCreatableObject(object, type) {
return NON_STYLABLE_TYPES.indexOf(object.type) < 0 && type.definition.creatable;
}
export default function StylesInspectorViewProvider(openmct) {
@ -64,13 +47,12 @@ export default function StylesInspectorViewProvider(openmct) {
canView: function (selection) {
const objectSelection = selection?.[0];
const objectContext = objectSelection?.[0]?.context;
const layoutItem = objectContext?.layoutItem;
const domainObject = objectContext?.item;
const hasStyles = domainObject?.configuration?.objectStyles;
const isFlexibleLayoutContainer =
domainObject?.type === 'flexible-layout' && objectContext.type === 'container';
const isLayoutItem = objectContext?.layoutItem;
if ((isLayoutItem || hasStyles) && !isFlexibleLayoutContainer) {
if (layoutItem) {
return true;
}
@ -78,11 +60,10 @@ export default function StylesInspectorViewProvider(openmct) {
return false;
}
const typeObject = openmct.types.get(domainObject.type);
const type = openmct.types.get(domainObject.type);
return (
isLayoutObject(objectSelection, domainObject.type) ||
isCreatableObject(domainObject, typeObject)
isLayoutObject(objectSelection, domainObject.type) || isCreatableObject(domainObject, type)
);
},
view: function (selection) {
@ -110,11 +91,8 @@ export default function StylesInspectorViewProvider(openmct) {
);
_destroy = destroy;
},
showTab: function (isEditing) {
return true;
},
priority: function () {
return openmct.editor.isEditing() ? openmct.priority.DEFAULT : openmct.priority.LOW;
return openmct.priority.DEFAULT;
},
destroy: function () {
if (_destroy) {

View File

@ -38,7 +38,6 @@ export default function MissingObjectInterceptor(openmct) {
}
return object;
},
priority: openmct.priority.HIGH
}
});
}

View File

@ -45,7 +45,7 @@ function myItemsInterceptor({ openmct, identifierObject, name }) {
return object;
},
priority: openmct.priority.HIGHEST
priority: openmct.priority.HIGH
};
}

View File

@ -344,19 +344,12 @@ export default {
},
beforeMount() {
this.marked = new Marked();
this.marked.use({
breaks: true,
extensions: [
{
name: 'link',
renderer: (options) => {
return this.validateLink(options);
}
}
]
});
this.renderer = new this.marked.Renderer();
},
mounted() {
const originalLinkRenderer = this.renderer.link;
this.renderer.link = this.validateLink.bind(this, originalLinkRenderer);
this.manageEmbedLayout = _.debounce(this.manageEmbedLayout, 400);
if (this.$refs.embedsWrapper) {
@ -444,7 +437,10 @@ export default {
}
},
convertMarkDownToHtml(text = '') {
let markDownHtml = this.marked.parse(text);
let markDownHtml = this.marked.parse(text, {
breaks: true,
renderer: this.renderer
});
markDownHtml = sanitizeHtml(markDownHtml, SANITIZATION_SCHEMA);
return markDownHtml;
},
@ -455,19 +451,21 @@ export default {
this.$refs.entryInput.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
},
validateLink(options) {
const { href, text } = options;
validateLink(originalLinkRenderer, href, title, text) {
try {
const domain = new URL(href).hostname;
const urlIsWhitelisted = this.urlWhitelist.some((partialDomain) => {
return domain.endsWith(partialDomain);
});
if (!urlIsWhitelisted) {
return text;
}
return `<a class="c-hyperlink" target="_blank" href="${href}">${text}</a>`;
const linkHtml = originalLinkRenderer.call(this.renderer, href, title, text);
const linkHtmlWithTarget = linkHtml.replace(
/^<a /,
'<a class="c-hyperlink" target="_blank"'
);
return linkHtmlWithTarget;
} catch (error) {
// had error parsing this URL, just return the text
return text;

View File

@ -434,69 +434,16 @@ class CouchObjectProvider {
return Promise.resolve([]);
}
async isViewDefined(designDoc, viewName) {
const url = `${this.url}/_design/${designDoc}/_view/${viewName}`;
const response = await fetch(url, {
method: 'HEAD'
});
return response.ok;
}
/**
* @typedef GetObjectByViewOptions
* @property {String} designDoc the name of the design document that the view belongs to
* @property {String} viewName
* @property {Array.<String>} [keysToSearch] a list of discrete view keys to search for. View keys are not object identifiers.
* @property {String} [startKey] limit the search to a range of keys starting with the provided `startKey`. One of `keysToSearch` OR `startKey` AND `endKey` must be provided
* @property {String} [endKey] limit the search to a range of keys ending with the provided `endKey`. One of `keysToSearch` OR `startKey` AND `endKey` must be provided
* @property {Number} [limit] limit the number of results returned
* @property {String} [objectIdField] The field (either key or value) to treat as an object key. If provided, include_docs will be set to false in the request, and the field will be used as an object identifier. A bulk request will be used to resolve objects from identifiers
*/
/**
* Return objects based on a call to a view. See https://docs.couchdb.org/en/stable/api/ddoc/views.html.
* @param {GetObjectByViewOptions} options
* @param {AbortSignal} abortSignal
* @returns {Promise<Array.<import('openmct.js').DomainObject>>}
*/
async getObjectsByView(
{ designDoc, viewName, keysToSearch, startKey, endKey, limit, objectIdField },
abortSignal
) {
let stringifiedKeys = JSON.stringify(keysToSearch);
const url = `${this.url}/_design/${designDoc}/_view/${viewName}`;
const requestBody = {};
let requestBodyString;
if (objectIdField === undefined) {
requestBody.include_docs = true;
}
if (limit !== undefined) {
requestBody.limit = limit;
}
if (startKey !== undefined && endKey !== undefined) {
/* spell-checker: disable */
requestBody.startkey = startKey;
requestBody.endkey = endKey;
requestBodyString = JSON.stringify(requestBody);
requestBodyString = requestBodyString.replace('$START_KEY', startKey);
requestBodyString = requestBodyString.replace('$END_KEY', endKey);
/* spell-checker: enable */
} else {
requestBody.keys = stringifiedKeys;
requestBodyString = JSON.stringify(requestBody);
}
async getObjectsByView({ designDoc, viewName, keysToSearch }, abortSignal) {
const stringifiedKeys = JSON.stringify(keysToSearch);
const url = `${this.url}/_design/${designDoc}/_view/${viewName}?keys=${stringifiedKeys}&include_docs=true`;
let objectModels = [];
try {
const response = await fetch(url, {
method: 'POST',
method: 'GET',
headers: { 'Content-Type': 'application/json' },
signal: abortSignal,
body: requestBodyString
signal: abortSignal
});
if (!response.ok) {
@ -507,21 +454,13 @@ class CouchObjectProvider {
const result = await response.json();
const couchRows = result.rows;
if (objectIdField !== undefined) {
const objectIdsToResolve = [];
couchRows.forEach((couchRow) => {
objectIdsToResolve.push(couchRow[objectIdField]);
});
objectModels = Object.values(await this.#bulkGet(objectIdsToResolve), abortSignal);
} else {
couchRows.forEach((couchRow) => {
const couchDoc = couchRow.doc;
const objectModel = this.#getModel(couchDoc);
if (objectModel) {
objectModels.push(objectModel);
}
});
}
couchRows.forEach((couchRow) => {
const couchDoc = couchRow.doc;
const objectModel = this.#getModel(couchDoc);
if (objectModel) {
objectModels.push(objectModel);
}
});
} catch (error) {
// do nothing
}

Some files were not shown because too many files have changed in this diff Show More