mirror of
https://github.com/nasa/openmct.git
synced 2024-12-19 21:27:52 +00:00
Merge branch 'master' into dependabot/npm_and_yarn/eslint-plugin-unicorn-51.0.1
This commit is contained in:
commit
427404d908
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
@ -5,7 +5,6 @@
|
||||
// List of extensions which should be recommended for users of this workspace.
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"Vue.vscode-typescript-vue-plugin",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"rvest.vs-code-prettier-eslint"
|
||||
],
|
||||
|
@ -5,11 +5,8 @@ information to pull requests.
|
||||
*/
|
||||
|
||||
import config from './webpack.dev.js';
|
||||
// eslint-disable-next-line no-undef
|
||||
const CI = process.env.CI === 'true';
|
||||
|
||||
config.devtool = CI ? false : undefined;
|
||||
|
||||
config.devtool = 'source-map';
|
||||
config.devServer.hot = false;
|
||||
|
||||
config.module.rules.push({
|
||||
|
@ -505,15 +505,14 @@ async function setTimeConductorBounds(page, startDate, endDate) {
|
||||
* @param {string} startDate
|
||||
* @param {string} endDate
|
||||
*/
|
||||
async function setIndependentTimeConductorBounds(page, startDate, endDate) {
|
||||
// Activate Independent Time Conductor in Fixed Time Mode
|
||||
await page.getByRole('switch').click();
|
||||
async function setIndependentTimeConductorBounds(page, { start, end }) {
|
||||
// Activate Independent Time Conductor
|
||||
await page.getByLabel('Enable Independent Time Conductor').click();
|
||||
|
||||
// Bring up the time conductor popup
|
||||
await page.click('.c-conductor-holder--compact .c-compact-tc');
|
||||
await page.getByLabel('Independent Time Conductor Settings').click();
|
||||
await expect(page.locator('.itc-popout')).toBeInViewport();
|
||||
|
||||
await setTimeBounds(page, startDate, endDate);
|
||||
await setTimeBounds(page, start, end);
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
|
26
e2e/test-data/display_layout_with_child_overlay_plot.json
Normal file
26
e2e/test-data/display_layout_with_child_overlay_plot.json
Normal file
File diff suppressed because one or more lines are too long
@ -174,6 +174,6 @@ test.describe('AppActions', () => {
|
||||
type: 'Folder'
|
||||
});
|
||||
await openObjectTreeContextMenu(page, folder.url);
|
||||
await expect(page.getByLabel('Menu')).toBeVisible();
|
||||
await expect(page.getByLabel(`${folder.name} Context Menu`)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
@ -33,7 +33,12 @@
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { createDomainObjectWithDefaults, createExampleTelemetryObject } from '../../appActions.js';
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject,
|
||||
setIndependentTimeConductorBounds,
|
||||
setTimeConductorBounds
|
||||
} from '../../appActions.js';
|
||||
import { MISSION_TIME } from '../../constants.js';
|
||||
import { expect, test } from '../../pluginFixtures.js';
|
||||
|
||||
@ -89,6 +94,53 @@ test.describe('Generate Visual Test Data @localStorage @generatedata @clock', ()
|
||||
});
|
||||
});
|
||||
|
||||
test('Generate display layout with 1 child overlay plot', async ({ page, context }) => {
|
||||
const parent = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: 'Parent Display Layout'
|
||||
});
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot',
|
||||
name: 'Child Overlay Plot 1',
|
||||
parent: parent.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Child SWG 1',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
await page.goto(parent.url, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await setIndependentTimeConductorBounds(page, {
|
||||
start: '2024-11-12 19:11:11.000Z',
|
||||
end: '2024-11-12 20:11:11.000Z'
|
||||
});
|
||||
|
||||
const NEW_GLOBAL_START_BOUNDS = '2024-11-11 19:11:11.000Z';
|
||||
const NEW_GLOBAL_END_BOUNDS = '2024-11-11 20:11:11.000Z';
|
||||
|
||||
await setTimeConductorBounds(page, NEW_GLOBAL_START_BOUNDS, NEW_GLOBAL_END_BOUNDS);
|
||||
|
||||
// Verify that the global time conductor bounds have been updated
|
||||
expect(
|
||||
await page.getByLabel('Global Time Conductor').getByLabel('Start bounds').textContent()
|
||||
).toEqual(NEW_GLOBAL_START_BOUNDS);
|
||||
expect(
|
||||
await page.getByLabel('Global Time Conductor').getByLabel('End bounds').textContent()
|
||||
).toEqual(NEW_GLOBAL_END_BOUNDS);
|
||||
|
||||
//Save localStorage for future test execution
|
||||
await context.storageState({
|
||||
path: fileURLToPath(
|
||||
new URL(
|
||||
'../../../e2e/test-data/display_layout_with_child_overlay_plot.json',
|
||||
import.meta.url
|
||||
)
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
test('Generate flexible layout with 2 child display layouts', async ({ page, context }) => {
|
||||
// Create Display Layout
|
||||
const parent = await createDomainObjectWithDefaults(page, {
|
||||
|
@ -131,7 +131,10 @@ test.describe('Time Strip', () => {
|
||||
const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
|
||||
const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
|
||||
|
||||
await setIndependentTimeConductorBounds(page, startBoundString, endBoundString);
|
||||
await setIndependentTimeConductorBounds(page, {
|
||||
start: startBoundString,
|
||||
end: endBoundString
|
||||
});
|
||||
expect(await activityBounds.count()).toEqual(1);
|
||||
});
|
||||
|
||||
@ -160,7 +163,10 @@ test.describe('Time Strip', () => {
|
||||
const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
|
||||
const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
|
||||
|
||||
await setIndependentTimeConductorBounds(page, startBoundString, endBoundString);
|
||||
await setIndependentTimeConductorBounds(page, {
|
||||
start: startBoundString,
|
||||
end: endBoundString
|
||||
});
|
||||
|
||||
// Verify that two events are displayed
|
||||
expect(await activityBounds.count()).toEqual(2);
|
||||
|
@ -286,7 +286,7 @@ test.describe('Basic Condition Set Use', () => {
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
await page.click('button[title="Change the current view"]');
|
||||
await page.getByLabel('Open the View Switcher Menu').click();
|
||||
|
||||
await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden();
|
||||
await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible();
|
||||
|
@ -23,6 +23,7 @@ import { fileURLToPath } from 'url';
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
navigateToObjectWithFixedTimeBounds,
|
||||
setFixedTimeMode,
|
||||
setIndependentTimeConductorBounds,
|
||||
setRealTimeMode,
|
||||
@ -30,12 +31,120 @@ import {
|
||||
} from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
const LOCALSTORAGE_PATH = fileURLToPath(
|
||||
const CHILD_LAYOUT_STORAGE_STATE_PATH = fileURLToPath(
|
||||
new URL('../../../../test-data/display_layout_with_child_layouts.json', import.meta.url)
|
||||
);
|
||||
const CHILD_PLOT_STORAGE_STATE_PATH = fileURLToPath(
|
||||
new URL('../../../../test-data/display_layout_with_child_overlay_plot.json', import.meta.url)
|
||||
);
|
||||
const TINY_IMAGE_BASE64 =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII';
|
||||
|
||||
test.describe('Display Layout Sub-object Actions @localStorage', () => {
|
||||
const INIT_ITC_START_BOUNDS = '2024-11-12 19:11:11.000Z';
|
||||
const INIT_ITC_END_BOUNDS = '2024-11-12 20:11:11.000Z';
|
||||
const NEW_GLOBAL_START_BOUNDS = '2024-11-11 19:11:11.000Z';
|
||||
const NEW_GLOBAL_END_BOUNDS = '2024-11-11 20:11:11.000Z';
|
||||
|
||||
test.use({
|
||||
storageState: CHILD_PLOT_STORAGE_STATE_PATH
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await page.getByLabel('Expand My Items folder').click();
|
||||
const waitForMyItemsNavigation = page.waitForURL(`**/mine/?*`);
|
||||
await page
|
||||
.getByLabel('Main Tree')
|
||||
.getByLabel('Navigate to Parent Display Layout layout Object')
|
||||
.click();
|
||||
// Wait for the URL to change to the display layout
|
||||
await waitForMyItemsNavigation;
|
||||
});
|
||||
test('Open in New Tab action preserves time bounds @2p', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7524'
|
||||
});
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/6982'
|
||||
});
|
||||
|
||||
const TEST_FIXED_START_TIME = 1731352271000; // 2024-11-11 19:11:11.000Z
|
||||
const TEST_FIXED_END_TIME = TEST_FIXED_START_TIME + 3600000; // 2024-11-11 20:11:11.000Z
|
||||
|
||||
// Verify the ITC has the expected initial bounds
|
||||
expect(
|
||||
await page
|
||||
.getByLabel('Child Overlay Plot 1 Frame Controls')
|
||||
.getByLabel('Start bounds')
|
||||
.textContent()
|
||||
).toEqual(INIT_ITC_START_BOUNDS);
|
||||
expect(
|
||||
await page
|
||||
.getByLabel('Child Overlay Plot 1 Frame Controls')
|
||||
.getByLabel('End bounds')
|
||||
.textContent()
|
||||
).toEqual(INIT_ITC_END_BOUNDS);
|
||||
|
||||
// Update the global fixed bounds to 2024-11-11 19:11:11.000Z / 2024-11-11 20:11:11.000Z
|
||||
const url = page.url().split('?')[0];
|
||||
await navigateToObjectWithFixedTimeBounds(
|
||||
page,
|
||||
url,
|
||||
TEST_FIXED_START_TIME,
|
||||
TEST_FIXED_END_TIME
|
||||
);
|
||||
|
||||
// ITC bounds should still match the initial ITC bounds
|
||||
expect(
|
||||
await page
|
||||
.getByLabel('Child Overlay Plot 1 Frame Controls')
|
||||
.getByLabel('Start bounds')
|
||||
.textContent()
|
||||
).toEqual(INIT_ITC_START_BOUNDS);
|
||||
expect(
|
||||
await page
|
||||
.getByLabel('Child Overlay Plot 1 Frame Controls')
|
||||
.getByLabel('End bounds')
|
||||
.textContent()
|
||||
).toEqual(INIT_ITC_END_BOUNDS);
|
||||
|
||||
// Open the Child Overlay Plot 1 in a new tab
|
||||
await page.getByLabel('View menu items').click();
|
||||
const pagePromise = page.context().waitForEvent('page');
|
||||
await page.getByLabel('Open In New Tab').click();
|
||||
|
||||
const newPage = await pagePromise;
|
||||
await newPage.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Verify that the global time conductor bounds in the new page match the updated global bounds
|
||||
expect(
|
||||
await newPage.getByLabel('Global Time Conductor').getByLabel('Start bounds').textContent()
|
||||
).toEqual(NEW_GLOBAL_START_BOUNDS);
|
||||
expect(
|
||||
await newPage.getByLabel('Global Time Conductor').getByLabel('End bounds').textContent()
|
||||
).toEqual(NEW_GLOBAL_END_BOUNDS);
|
||||
|
||||
// Verify that the ITC is enabled in the new page
|
||||
await expect(newPage.getByLabel('Disable Independent Time Conductor')).toBeVisible();
|
||||
// Verify that the ITC bounds in the new page match the original ITC bounds
|
||||
expect(
|
||||
await newPage
|
||||
.getByLabel('Independent Time Conductor Panel')
|
||||
.getByLabel('Start bounds')
|
||||
.textContent()
|
||||
).toEqual(INIT_ITC_START_BOUNDS);
|
||||
expect(
|
||||
await newPage
|
||||
.getByLabel('Independent Time Conductor Panel')
|
||||
.getByLabel('End bounds')
|
||||
.textContent()
|
||||
).toEqual(INIT_ITC_END_BOUNDS);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Display Layout Toolbar Actions @localStorage', () => {
|
||||
const PARENT_DISPLAY_LAYOUT_NAME = 'Parent Display Layout';
|
||||
const CHILD_DISPLAY_LAYOUT_NAME1 = 'Child Layout 1';
|
||||
@ -50,7 +159,7 @@ test.describe('Display Layout Toolbar Actions @localStorage', () => {
|
||||
await page.getByLabel('Edit Object').click();
|
||||
});
|
||||
test.use({
|
||||
storageState: LOCALSTORAGE_PATH
|
||||
storageState: CHILD_LAYOUT_STORAGE_STATE_PATH
|
||||
});
|
||||
|
||||
test('can add/remove Text element to a single layout', async ({ page }) => {
|
||||
@ -163,7 +272,7 @@ test.describe('Display Layout', () => {
|
||||
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
|
||||
|
||||
// ensure we can right click on the alpha-numeric widget and view historical data
|
||||
await page.getByLabel('Sine', { exact: true }).click({
|
||||
await page.getByLabel(/Alpha-numeric telemetry value of.*/).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.getByLabel('View Historical Data').click();
|
||||
@ -336,7 +445,7 @@ test.describe('Display Layout', () => {
|
||||
|
||||
const startDate = '2021-12-30 01:01:00.000Z';
|
||||
const endDate = '2021-12-30 01:11:00.000Z';
|
||||
await setIndependentTimeConductorBounds(page, startDate, endDate);
|
||||
await setIndependentTimeConductorBounds(page, { start: startDate, end: endDate });
|
||||
|
||||
// check image date
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
|
||||
|
@ -248,11 +248,10 @@ test.describe('Flexible Layout', () => {
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// flip on independent time conductor
|
||||
await setIndependentTimeConductorBounds(
|
||||
page,
|
||||
'2021-12-30 01:01:00.000Z',
|
||||
'2021-12-30 01:11:00.000Z'
|
||||
);
|
||||
await setIndependentTimeConductorBounds(page, {
|
||||
start: '2021-12-30 01:01:00.000Z',
|
||||
end: '2021-12-30 01:11:00.000Z'
|
||||
});
|
||||
|
||||
// check image date
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
|
||||
|
@ -175,13 +175,13 @@ test.describe('Gauge', () => {
|
||||
});
|
||||
|
||||
// Try to create a Folder into the Gauge. Should be disallowed.
|
||||
await page.getByRole('button', { name: /Create/ }).click();
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
await page.getByRole('menuitem', { name: /Folder/ }).click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.getByLabel('Cancel').click();
|
||||
|
||||
// Try to create a Display Layout into the Gauge. Should be disallowed.
|
||||
await page.getByRole('button', { name: /Create/ }).click();
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
await page.getByRole('menuitem', { name: /Display Layout/ }).click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
});
|
||||
|
@ -37,6 +37,8 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
|
||||
});
|
||||
|
||||
test('Can click on telemetry and see data in inspector @2p', async ({ page, context }) => {
|
||||
const initStartBounds = await page.getByLabel('Start bounds').textContent();
|
||||
const initEndBounds = await page.getByLabel('End bounds').textContent();
|
||||
const exampleDataVisualizationSource = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Example Data Visualization Source'
|
||||
});
|
||||
@ -78,5 +80,9 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
|
||||
await newPage.waitForLoadState();
|
||||
// expect new tab title to contain 'Second Sine Wave Generator'
|
||||
await expect(newPage).toHaveTitle('Second Sine Wave Generator');
|
||||
|
||||
// Verify that "Open in New Tab" preserves the time bounds
|
||||
expect(initStartBounds).toEqual(await newPage.getByLabel('Start bounds').textContent());
|
||||
expect(initEndBounds).toEqual(await newPage.getByLabel('End bounds').textContent());
|
||||
});
|
||||
});
|
||||
|
@ -83,7 +83,7 @@ test.describe('Snapshot Container tests', () => {
|
||||
// name: "Dropped Overlay Plot"
|
||||
// });
|
||||
|
||||
await page.getByLabel('Take a Notebook Snapshot').click();
|
||||
await page.getByLabel('Open the Notebook Snapshot Menu').click();
|
||||
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
|
||||
await page.getByLabel('Show Snapshots').click();
|
||||
});
|
||||
|
@ -54,7 +54,9 @@ test.describe('Plots work in Previews', () => {
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// right click on the plot and select view large
|
||||
await page.getByLabel('Sine', { exact: true }).click({ button: 'right' });
|
||||
await page.getByLabel(/Alpha-numeric telemetry value of.*/).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.getByLabel('View Historical Data').click();
|
||||
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Close' }).click();
|
||||
|
@ -114,7 +114,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultFrameBorderColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2. Note: https://github.com/nasa/openmct/issues/7337
|
||||
@ -122,7 +124,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultFrameBorderColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
});
|
||||
|
||||
@ -143,7 +147,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(defaultTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2
|
||||
@ -151,7 +157,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(defaultTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Set styles using setStyles function on StackedPlot1 but not StackedPlot2
|
||||
@ -160,7 +168,7 @@ test.describe('Flexible Layout styling', () => {
|
||||
setBorderColor,
|
||||
setBackgroundColor,
|
||||
setTextColor,
|
||||
page.getByLabel('StackedPlot1 Frame')
|
||||
page.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot1
|
||||
@ -168,7 +176,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2
|
||||
@ -176,7 +186,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(defaultTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Save Flexible Layout
|
||||
@ -191,7 +203,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2
|
||||
@ -199,7 +213,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(defaultTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
});
|
||||
|
||||
@ -241,7 +257,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2 to verify they are the default
|
||||
@ -249,7 +267,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(defaultTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Set styles using setStyles function on StackedPlot2
|
||||
@ -258,7 +278,7 @@ test.describe('Flexible Layout styling', () => {
|
||||
setBorderColor,
|
||||
setBackgroundColor,
|
||||
setTextColor,
|
||||
page.getByLabel('StackedPlot2 Frame')
|
||||
page.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2
|
||||
@ -266,7 +286,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Save Flexible Layout
|
||||
@ -281,7 +303,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2
|
||||
@ -289,7 +313,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Directly navigate to the flexible layout
|
||||
@ -326,7 +352,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2 matches previous set colors
|
||||
@ -334,7 +362,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
});
|
||||
|
||||
@ -356,7 +386,7 @@ test.describe('Flexible Layout styling', () => {
|
||||
setBorderColor,
|
||||
setBackgroundColor,
|
||||
setTextColor,
|
||||
page.getByLabel('StackedPlot1 Frame')
|
||||
page.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
);
|
||||
|
||||
// Check styles using checkStyles function
|
||||
@ -364,7 +394,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Save Flexible Layout
|
||||
@ -386,7 +418,7 @@ test.describe('Flexible Layout styling', () => {
|
||||
'No Style',
|
||||
'No Style',
|
||||
'No Style',
|
||||
page.getByLabel('StackedPlot1 Frame')
|
||||
page.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
);
|
||||
|
||||
// Check styles using checkStyles function
|
||||
@ -394,7 +426,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(inheritedColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
// Save Flexible Layout
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
@ -408,7 +442,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(inheritedColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -67,7 +67,7 @@ test.describe('Style Inspector Options', () => {
|
||||
await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible();
|
||||
|
||||
// Select Stacked Layout Column
|
||||
await page.getByLabel('Stacked Plot Frame').click();
|
||||
await page.getByRole('group', { name: 'Stacked Plot Frame' }).click();
|
||||
|
||||
// The overall Flex Layout or Stacked Plot itself MUST be style-able.
|
||||
await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible();
|
||||
|
@ -109,7 +109,7 @@ test.describe('Verify tooltips', () => {
|
||||
|
||||
async function getToolTip(object) {
|
||||
await page.locator('.c-create-button').hover();
|
||||
await page.getByRole('cell', { name: object.name }).hover();
|
||||
await page.getByLabel('lad name').getByText(object.name).hover();
|
||||
let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
return tooltipText.replace('\n', '').trim();
|
||||
}
|
||||
|
@ -178,7 +178,7 @@ test.describe('Performance tests', () => {
|
||||
console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming));
|
||||
|
||||
// Click Close Icon
|
||||
await page.locator('[aria-label="Close"]').click();
|
||||
await page.getByRole('button', { name: 'Close' }).click();
|
||||
await page.evaluate(() => window.performance.mark('view-large-close-button'));
|
||||
|
||||
//await client.send('HeapProfiler.enable');
|
||||
|
@ -69,14 +69,14 @@ test.describe('Visual - Header @a11y', () => {
|
||||
});
|
||||
|
||||
test('show snapshot button', async ({ page, theme }) => {
|
||||
await page.getByLabel('Take a Notebook Snapshot').click();
|
||||
await page.getByLabel('Open the Notebook Snapshot Menu').click();
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
|
||||
|
||||
await percySnapshot(page, `Notebook Snapshot Show button (theme: '${theme}')`, {
|
||||
scope: header
|
||||
});
|
||||
await expect(await page.getByLabel('Show Snapshots')).toBeVisible();
|
||||
await expect(page.getByLabel('Show Snapshots')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -91,7 +91,7 @@ test.describe('Flexible Layout styling @a11y', () => {
|
||||
setBorderColor,
|
||||
setBackgroundColor,
|
||||
setTextColor,
|
||||
page.getByLabel('StackedPlot1 Frame')
|
||||
page.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
);
|
||||
|
||||
await percySnapshot(
|
||||
|
@ -53,11 +53,11 @@ test.describe('Visual - Telemetry Views', () => {
|
||||
await page.goto(telemetry.url, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
//Click this button to see telemetry display options
|
||||
await page.getByRole('button', { name: 'Plot' }).click();
|
||||
await page.getByLabel('Open the View Switcher Menu').click();
|
||||
await page.getByLabel('Telemetry Table').click();
|
||||
|
||||
//Get Table View in place
|
||||
expect(await page.getByLabel('Expand Columns')).toBeInViewport();
|
||||
await expect(page.getByLabel('Expand Columns')).toBeInViewport();
|
||||
|
||||
await percySnapshot(page, `Default Telemetry Table View (theme: ${theme})`);
|
||||
|
||||
|
@ -55,6 +55,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const ONE_HOUR = 60 * 60 * 1000;
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
@ -77,6 +78,10 @@ export default {
|
||||
selectItem(item, event) {
|
||||
event.stopPropagation();
|
||||
const bounds = this.openmct.time.getBounds();
|
||||
const otherBounds = {
|
||||
start: bounds.start - ONE_HOUR,
|
||||
end: bounds.end + ONE_HOUR
|
||||
};
|
||||
const selection = [
|
||||
{
|
||||
element: this.$el,
|
||||
@ -88,6 +93,9 @@ export default {
|
||||
icon: item.type.cssClass
|
||||
},
|
||||
dataRanges: [
|
||||
{
|
||||
bounds: otherBounds
|
||||
},
|
||||
{
|
||||
bounds
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
10
package.json
10
package.json
@ -23,7 +23,7 @@
|
||||
"babel-plugin-istanbul": "6.1.1",
|
||||
"codecov": "3.8.3",
|
||||
"comma-separated-values": "3.6.4",
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
"copy-webpack-plugin": "12.0.2",
|
||||
"cspell": "7.3.8",
|
||||
"css-loader": "6.10.0",
|
||||
"d3-axis": "3.0.0",
|
||||
@ -45,7 +45,7 @@
|
||||
"flatbush": "4.2.0",
|
||||
"git-rev-sync": "3.0.2",
|
||||
"html2canvas": "1.4.1",
|
||||
"imports-loader": "4.0.1",
|
||||
"imports-loader": "5.0.0",
|
||||
"jasmine-core": "5.1.1",
|
||||
"karma": "6.4.2",
|
||||
"karma-chrome-launcher": "3.2.0",
|
||||
@ -56,7 +56,7 @@
|
||||
"karma-junit-reporter": "2.0.1",
|
||||
"karma-sourcemap-loader": "0.4.0",
|
||||
"karma-spec-reporter": "0.0.36",
|
||||
"karma-webpack": "5.0.0",
|
||||
"karma-webpack": "5.0.1",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"marked": "12.0.0",
|
||||
@ -64,7 +64,7 @@
|
||||
"moment": "2.30.1",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"moment-timezone": "0.5.41",
|
||||
"npm-run-all2": "6.1.1",
|
||||
"npm-run-all2": "6.1.2",
|
||||
"nyc": "15.1.0",
|
||||
"painterro": "1.2.87",
|
||||
"plotly.js-basic-dist-min": "2.29.1",
|
||||
@ -74,7 +74,7 @@
|
||||
"printj": "1.3.1",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sanitize-html": "2.12.1",
|
||||
"sass": "1.68.0",
|
||||
"sass": "1.71.1",
|
||||
"sass-loader": "14.1.1",
|
||||
"sinon": "17.0.0",
|
||||
"style-loader": "3.3.3",
|
||||
|
@ -29,10 +29,13 @@
|
||||
:key="action.name"
|
||||
role="menuitem"
|
||||
:aria-disabled="action.isDisabled"
|
||||
:class="action.cssClass"
|
||||
:aria-label="action.name"
|
||||
aria-describedby="item-description"
|
||||
:class="action.cssClass"
|
||||
:title="action.description"
|
||||
@click="action.onItemClicked"
|
||||
@mouseover="toggleItem(action)"
|
||||
@mouseleave="toggleItem()"
|
||||
>
|
||||
{{ action.name }}
|
||||
</li>
|
||||
@ -52,16 +55,23 @@
|
||||
v-for="action in options.actions"
|
||||
:key="action.name"
|
||||
role="menuitem"
|
||||
aria-describedby="item-description"
|
||||
:aria-disabled="action.isDisabled"
|
||||
:class="action.cssClass"
|
||||
:aria-label="action.name"
|
||||
:title="action.description"
|
||||
@click="action.onItemClicked"
|
||||
@mouseover="toggleItem(action)"
|
||||
@mouseleave="toggleItem()"
|
||||
>
|
||||
{{ action.name }}
|
||||
</li>
|
||||
<li v-if="options.actions.length === 0">No actions defined.</li>
|
||||
</ul>
|
||||
<div v-if="hoveredItem" id="item-description" class="visually-hidden" aria-live="polite">
|
||||
<span v-if="hoveredItem.name">{{ hoveredItem.name }}</span>
|
||||
<span v-if="hoveredItem.description">: {{ hoveredItem.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -70,11 +80,21 @@ import popupMenuMixin from '../mixins/popupMenuMixin.js';
|
||||
export default {
|
||||
mixins: [popupMenuMixin],
|
||||
inject: ['options'],
|
||||
data() {
|
||||
return {
|
||||
hoveredItem: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
optionsLabel() {
|
||||
const label = this.options.label ? `${this.options.label} Menu` : 'Menu';
|
||||
const label = this.options.label ? `${this.options.label} Context Menu` : 'Context Menu';
|
||||
return label;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleItem(action) {
|
||||
this.hoveredItem = action ?? null;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -38,8 +38,8 @@
|
||||
:key="action.name"
|
||||
role="menuitem"
|
||||
:aria-disabled="action.isDisabled"
|
||||
aria-describedby="item-description"
|
||||
:class="action.cssClass"
|
||||
:title="action.description"
|
||||
@click="action.onItemClicked"
|
||||
@mouseover="toggleItemDescription(action)"
|
||||
@mouseleave="toggleItemDescription()"
|
||||
@ -64,7 +64,7 @@
|
||||
role="menuitem"
|
||||
:class="action.cssClass"
|
||||
:aria-label="action.name"
|
||||
:title="action.description"
|
||||
aria-describedby="item-description"
|
||||
@click="action.onItemClicked"
|
||||
@mouseover="toggleItemDescription(action)"
|
||||
@mouseleave="toggleItemDescription()"
|
||||
@ -74,13 +74,13 @@
|
||||
<li v-if="options.actions.length === 0">No actions defined.</li>
|
||||
</ul>
|
||||
|
||||
<div class="c-super-menu__item-description">
|
||||
<div :class="['l-item-description__icon', 'bg-' + hoveredItem.cssClass]"></div>
|
||||
<div aria-live="polite" class="c-super-menu__item-description">
|
||||
<div :class="itemDescriptionIconClass"></div>
|
||||
<div class="l-item-description__name">
|
||||
{{ hoveredItem.name }}
|
||||
{{ hoveredItemName }}
|
||||
</div>
|
||||
<div class="l-item-description__description">
|
||||
{{ hoveredItem.description }}
|
||||
<div id="item-description" class="l-item-description__description">
|
||||
{{ hoveredItemDescription }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -90,26 +90,39 @@ import popupMenuMixin from '../mixins/popupMenuMixin.js';
|
||||
export default {
|
||||
mixins: [popupMenuMixin],
|
||||
inject: ['options'],
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
hoveredItem: {}
|
||||
hoveredItem: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
optionsLabel() {
|
||||
const label = this.options.label ? `${this.options.label} Super Menu` : 'Super Menu';
|
||||
return label;
|
||||
},
|
||||
itemDescriptionIconClass() {
|
||||
const iconClass = ['l-item-description__icon'];
|
||||
if (this.hoveredItem) {
|
||||
iconClass.push('bg-' + this.hoveredItem.cssClass);
|
||||
}
|
||||
return iconClass;
|
||||
},
|
||||
hoveredItemName() {
|
||||
return this.hoveredItem?.name ?? '';
|
||||
},
|
||||
hoveredItemDescription() {
|
||||
return this.hoveredItem?.description ?? '';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleItemDescription(action = {}) {
|
||||
toggleItemDescription(action = null) {
|
||||
const hoveredItem = {
|
||||
name: action.name,
|
||||
description: action.description,
|
||||
cssClass: action.cssClass
|
||||
name: action?.name,
|
||||
description: action?.description,
|
||||
cssClass: action?.cssClass
|
||||
};
|
||||
|
||||
this.hoveredItem = Object.assign({}, this.hoveredItem, hoveredItem);
|
||||
this.hoveredItem = hoveredItem;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -20,7 +20,6 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import installWorker from './WebSocketWorker.js';
|
||||
const DEFAULT_RATE_MS = 1000;
|
||||
/**
|
||||
* Describes the strategy to be used when batching WebSocket messages
|
||||
*
|
||||
@ -51,11 +50,21 @@ const DEFAULT_RATE_MS = 1000;
|
||||
*
|
||||
* @memberof module:openmct.telemetry
|
||||
*/
|
||||
// Shim for Internet Explorer, I mean Safari. It doesn't support requestIdleCallback, but it's in a tech preview, so it will be dropping soon.
|
||||
const requestIdleCallback =
|
||||
// eslint-disable-next-line compat/compat
|
||||
window.requestIdleCallback ?? ((fn, { timeout }) => setTimeout(fn, timeout));
|
||||
const ONE_SECOND = 1000;
|
||||
const FIVE_SECONDS = 5 * ONE_SECOND;
|
||||
|
||||
class BatchingWebSocket extends EventTarget {
|
||||
#worker;
|
||||
#openmct;
|
||||
#showingRateLimitNotification;
|
||||
#rate;
|
||||
#maxBatchSize;
|
||||
#applicationIsInitializing;
|
||||
#maxBatchWait;
|
||||
#firstBatchReceived;
|
||||
|
||||
constructor(openmct) {
|
||||
super();
|
||||
@ -66,7 +75,10 @@ class BatchingWebSocket extends EventTarget {
|
||||
this.#worker = new Worker(workerUrl);
|
||||
this.#openmct = openmct;
|
||||
this.#showingRateLimitNotification = false;
|
||||
this.#rate = DEFAULT_RATE_MS;
|
||||
this.#maxBatchSize = Number.POSITIVE_INFINITY;
|
||||
this.#maxBatchWait = ONE_SECOND;
|
||||
this.#applicationIsInitializing = true;
|
||||
this.#firstBatchReceived = false;
|
||||
|
||||
const routeMessageToHandler = this.#routeMessageToHandler.bind(this);
|
||||
this.#worker.addEventListener('message', routeMessageToHandler);
|
||||
@ -78,6 +90,20 @@ class BatchingWebSocket extends EventTarget {
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
|
||||
openmct.once('start', () => {
|
||||
// An idle callback is a pretty good indication that a complex display is done loading. At that point set the batch size more conservatively.
|
||||
// Force it after 5 seconds if it hasn't happened yet.
|
||||
requestIdleCallback(
|
||||
() => {
|
||||
this.#applicationIsInitializing = false;
|
||||
this.setMaxBatchSize(this.#maxBatchSize);
|
||||
},
|
||||
{
|
||||
timeout: FIVE_SECONDS
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -129,14 +155,6 @@ class BatchingWebSocket extends EventTarget {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* When using batching, sets the rate at which batches of messages are released.
|
||||
* @param {Number} rate the amount of time to wait, in ms, between batches.
|
||||
*/
|
||||
setRate(rate) {
|
||||
this.#rate = rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} maxBatchSize the maximum length of a batch of messages. For example,
|
||||
* the maximum number of telemetry values to batch before dropping them
|
||||
@ -151,12 +169,29 @@ class BatchingWebSocket extends EventTarget {
|
||||
* 15 would probably be a better batch size.
|
||||
*/
|
||||
setMaxBatchSize(maxBatchSize) {
|
||||
this.#maxBatchSize = maxBatchSize;
|
||||
if (!this.#applicationIsInitializing) {
|
||||
this.#sendMaxBatchSizeToWorker(this.#maxBatchSize);
|
||||
}
|
||||
}
|
||||
setMaxBatchWait(wait) {
|
||||
this.#maxBatchWait = wait;
|
||||
this.#sendBatchWaitToWorker(this.#maxBatchWait);
|
||||
}
|
||||
#sendMaxBatchSizeToWorker(maxBatchSize) {
|
||||
this.#worker.postMessage({
|
||||
type: 'setMaxBatchSize',
|
||||
maxBatchSize
|
||||
});
|
||||
}
|
||||
|
||||
#sendBatchWaitToWorker(maxBatchWait) {
|
||||
this.#worker.postMessage({
|
||||
type: 'setMaxBatchWait',
|
||||
maxBatchWait
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the associated WebSocket. Generally speaking there is no need to call
|
||||
* this manually.
|
||||
@ -169,7 +204,9 @@ class BatchingWebSocket extends EventTarget {
|
||||
|
||||
#routeMessageToHandler(message) {
|
||||
if (message.data.type === 'batch') {
|
||||
if (message.data.batch.dropped === true && !this.#showingRateLimitNotification) {
|
||||
this.start = Date.now();
|
||||
const batch = message.data.batch;
|
||||
if (batch.dropped === true && !this.#showingRateLimitNotification) {
|
||||
const notification = this.#openmct.notifications.alert(
|
||||
'Telemetry dropped due to client rate limiting.',
|
||||
{ hint: 'Refresh individual telemetry views to retrieve dropped telemetry if needed.' }
|
||||
@ -179,16 +216,45 @@ class BatchingWebSocket extends EventTarget {
|
||||
this.#showingRateLimitNotification = false;
|
||||
});
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent('batch', { detail: message.data.batch }));
|
||||
setTimeout(() => {
|
||||
this.#readyForNextBatch();
|
||||
}, this.#rate);
|
||||
|
||||
this.dispatchEvent(new CustomEvent('batch', { detail: batch }));
|
||||
this.#waitUntilIdleAndRequestNextBatch(batch);
|
||||
} else if (message.data.type === 'message') {
|
||||
this.dispatchEvent(new CustomEvent('message', { detail: message.data.message }));
|
||||
} else if (message.data.type === 'reconnected') {
|
||||
this.dispatchEvent(new CustomEvent('reconnected'));
|
||||
} else {
|
||||
throw new Error(`Unknown message type: ${message.data.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
#waitUntilIdleAndRequestNextBatch(batch) {
|
||||
requestIdleCallback(
|
||||
(state) => {
|
||||
if (this.#firstBatchReceived === false) {
|
||||
this.#firstBatchReceived = true;
|
||||
}
|
||||
const now = Date.now();
|
||||
const waitedFor = now - this.start;
|
||||
if (state.didTimeout === true) {
|
||||
if (document.visibilityState === 'visible') {
|
||||
console.warn(`Event loop is too busy to process batch.`);
|
||||
this.#waitUntilIdleAndRequestNextBatch(batch);
|
||||
} else {
|
||||
// After ingesting a telemetry batch, wait until the event loop is idle again before
|
||||
// informing the worker we are ready for another batch.
|
||||
this.#readyForNextBatch();
|
||||
}
|
||||
} else {
|
||||
if (waitedFor > ONE_SECOND) {
|
||||
console.warn(`Warning, batch processing took ${waitedFor}ms`);
|
||||
}
|
||||
this.#readyForNextBatch();
|
||||
}
|
||||
},
|
||||
{ timeout: ONE_SECOND }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default BatchingWebSocket;
|
||||
|
@ -85,6 +85,7 @@ const SUBSCRIBE_STRATEGY = {
|
||||
export default class TelemetryAPI {
|
||||
#isGreedyLAD;
|
||||
#subscribeCache;
|
||||
#hasReturnedFirstData;
|
||||
|
||||
get SUBSCRIBE_STRATEGY() {
|
||||
return SUBSCRIBE_STRATEGY;
|
||||
@ -108,6 +109,7 @@ export default class TelemetryAPI {
|
||||
this.#isGreedyLAD = true;
|
||||
this.BatchingWebSocket = BatchingWebSocket;
|
||||
this.#subscribeCache = {};
|
||||
this.#hasReturnedFirstData = false;
|
||||
}
|
||||
|
||||
abortAllRequests() {
|
||||
@ -383,7 +385,10 @@ export default class TelemetryAPI {
|
||||
arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]);
|
||||
try {
|
||||
const telemetry = await provider.request(...arguments);
|
||||
|
||||
if (!this.#hasReturnedFirstData) {
|
||||
this.#hasReturnedFirstData = true;
|
||||
performance.mark('firstHistoricalDataReturned');
|
||||
}
|
||||
return telemetry;
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
|
@ -21,6 +21,7 @@
|
||||
*****************************************************************************/
|
||||
/* eslint-disable max-classes-per-file */
|
||||
export default function installWorker() {
|
||||
const ONE_SECOND = 1000;
|
||||
const FALLBACK_AND_WAIT_MS = [1000, 5000, 5000, 10000, 10000, 30000];
|
||||
|
||||
/**
|
||||
@ -44,6 +45,13 @@ export default function installWorker() {
|
||||
#currentWaitIndex = 0;
|
||||
#messageCallbacks = [];
|
||||
#wsUrl;
|
||||
#reconnecting = false;
|
||||
#worker;
|
||||
|
||||
constructor(worker) {
|
||||
super();
|
||||
this.#worker = worker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish a new WebSocket connection to the given URL
|
||||
@ -62,6 +70,9 @@ export default function installWorker() {
|
||||
this.#isConnecting = true;
|
||||
|
||||
this.#webSocket = new WebSocket(url);
|
||||
//Exposed to e2e tests so that the websocket can be manipulated during tests. Cannot find any other way to do this.
|
||||
// Playwright does not support forcing websocket state changes.
|
||||
this.#worker.currentWebSocket = this.#webSocket;
|
||||
|
||||
const boundConnected = this.#connected.bind(this);
|
||||
this.#webSocket.addEventListener('open', boundConnected);
|
||||
@ -100,12 +111,17 @@ export default function installWorker() {
|
||||
}
|
||||
|
||||
#connected() {
|
||||
console.debug('Websocket connected.');
|
||||
console.info('Websocket connected.');
|
||||
this.#isConnected = true;
|
||||
this.#isConnecting = false;
|
||||
this.#currentWaitIndex = 0;
|
||||
|
||||
this.dispatchEvent(new Event('connected'));
|
||||
if (this.#reconnecting) {
|
||||
this.#worker.postMessage({
|
||||
type: 'reconnected'
|
||||
});
|
||||
this.#reconnecting = false;
|
||||
}
|
||||
|
||||
this.#flushQueue();
|
||||
}
|
||||
@ -138,6 +154,7 @@ export default function installWorker() {
|
||||
if (this.#reconnectTimeoutHandle) {
|
||||
return;
|
||||
}
|
||||
this.#reconnecting = true;
|
||||
|
||||
this.#reconnectTimeoutHandle = setTimeout(() => {
|
||||
this.connect(this.#wsUrl);
|
||||
@ -207,6 +224,9 @@ export default function installWorker() {
|
||||
case 'setMaxBatchSize':
|
||||
this.#messageBatcher.setMaxBatchSize(message.data.maxBatchSize);
|
||||
break;
|
||||
case 'setMaxBatchWait':
|
||||
this.#messageBatcher.setMaxBatchWait(message.data.maxBatchWait);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown message type: ${type}`);
|
||||
}
|
||||
@ -245,7 +265,6 @@ export default function installWorker() {
|
||||
}
|
||||
|
||||
routeMessageToHandler(data) {
|
||||
//Implement batching here
|
||||
if (this.#messageBatcher.shouldBatchMessage(data)) {
|
||||
this.#messageBatcher.addMessageToBatch(data);
|
||||
} else {
|
||||
@ -267,12 +286,15 @@ export default function installWorker() {
|
||||
#maxBatchSize;
|
||||
#readyForNextBatch;
|
||||
#worker;
|
||||
#throttledSendNextBatch;
|
||||
|
||||
constructor(worker) {
|
||||
this.#maxBatchSize = 10;
|
||||
// No dropping telemetry unless we're explicitly told to.
|
||||
this.#maxBatchSize = Number.POSITIVE_INFINITY;
|
||||
this.#readyForNextBatch = false;
|
||||
this.#worker = worker;
|
||||
this.#resetBatch();
|
||||
this.setMaxBatchWait(ONE_SECOND);
|
||||
}
|
||||
#resetBatch() {
|
||||
this.#batch = {};
|
||||
@ -310,23 +332,29 @@ export default function installWorker() {
|
||||
const batchId = this.#batchingStrategy.getBatchIdFromMessage(message);
|
||||
let batch = this.#batch[batchId];
|
||||
if (batch === undefined) {
|
||||
this.#hasBatch = true;
|
||||
batch = this.#batch[batchId] = [message];
|
||||
} else {
|
||||
batch.push(message);
|
||||
}
|
||||
if (batch.length > this.#maxBatchSize) {
|
||||
console.warn(
|
||||
`Exceeded max batch size of ${this.#maxBatchSize} for ${batchId}. Dropping value.`
|
||||
);
|
||||
batch.shift();
|
||||
this.#batch.dropped = this.#batch.dropped || true;
|
||||
this.#batch.dropped = true;
|
||||
}
|
||||
|
||||
if (this.#readyForNextBatch) {
|
||||
this.#sendNextBatch();
|
||||
} else {
|
||||
this.#hasBatch = true;
|
||||
this.#throttledSendNextBatch();
|
||||
}
|
||||
}
|
||||
setMaxBatchSize(maxBatchSize) {
|
||||
this.#maxBatchSize = maxBatchSize;
|
||||
}
|
||||
setMaxBatchWait(maxBatchWait) {
|
||||
this.#throttledSendNextBatch = throttle(this.#sendNextBatch.bind(this), maxBatchWait);
|
||||
}
|
||||
/**
|
||||
* Indicates that client code is ready to receive the next batch of
|
||||
* messages. If a batch is available, it will be immediately sent.
|
||||
@ -335,7 +363,7 @@ export default function installWorker() {
|
||||
*/
|
||||
readyForNextBatch() {
|
||||
if (this.#hasBatch) {
|
||||
this.#sendNextBatch();
|
||||
this.#throttledSendNextBatch();
|
||||
} else {
|
||||
this.#readyForNextBatch = true;
|
||||
}
|
||||
@ -352,7 +380,34 @@ export default function installWorker() {
|
||||
}
|
||||
}
|
||||
|
||||
const websocket = new ResilientWebSocket();
|
||||
function throttle(callback, wait) {
|
||||
let last = 0;
|
||||
let throttling = false;
|
||||
|
||||
return function (...args) {
|
||||
if (throttling) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = performance.now();
|
||||
const timeSinceLast = now - last;
|
||||
|
||||
if (timeSinceLast >= wait) {
|
||||
last = now;
|
||||
callback(...args);
|
||||
} else if (!throttling) {
|
||||
throttling = true;
|
||||
|
||||
setTimeout(() => {
|
||||
last = performance.now();
|
||||
throttling = false;
|
||||
callback(...args);
|
||||
}, wait - timeSinceLast);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const websocket = new ResilientWebSocket(self);
|
||||
const messageBatcher = new MessageBatcher(self);
|
||||
const workerBroker = new WorkerToWebSocketMessageBroker(websocket, messageBatcher);
|
||||
const websocketBroker = new WebSocketToWorkerMessageBroker(messageBatcher, self);
|
||||
@ -363,4 +418,6 @@ export default function installWorker() {
|
||||
websocket.registerMessageCallback((data) => {
|
||||
websocketBroker.routeMessageToHandler(data);
|
||||
});
|
||||
|
||||
self.websocketInstance = websocket;
|
||||
}
|
||||
|
@ -24,11 +24,13 @@
|
||||
<tr
|
||||
ref="tableRow"
|
||||
class="js-lad-table__body__row c-table__selectable-row"
|
||||
aria-label="lad row"
|
||||
@click="clickedRow"
|
||||
@contextmenu.prevent="showContextMenu"
|
||||
>
|
||||
<td
|
||||
ref="tableCell"
|
||||
aria-label="lad name"
|
||||
class="js-first-data"
|
||||
@mouseover.ctrl="showToolTip"
|
||||
@mouseleave="hideToolTip"
|
||||
@ -58,7 +60,7 @@
|
||||
const CONTEXT_MENU_ACTIONS = ['viewDatumAction', 'viewHistoricalData', 'remove'];
|
||||
const BLANK_VALUE = '---';
|
||||
|
||||
import identifierToString from '/src/tools/url.js';
|
||||
import { objectPathToUrl } from '/src/tools/url.js';
|
||||
import PreviewAction from '@/ui/preview/PreviewAction.js';
|
||||
|
||||
import tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';
|
||||
@ -260,7 +262,7 @@ export default {
|
||||
event.preventDefault();
|
||||
this.preview(this.objectPath);
|
||||
} else {
|
||||
const resultUrl = identifierToString(this.openmct, this.objectPath);
|
||||
const resultUrl = objectPathToUrl(this.openmct, this.objectPath);
|
||||
this.openmct.router.navigate(resultUrl);
|
||||
}
|
||||
},
|
||||
|
@ -22,7 +22,7 @@
|
||||
|
||||
<template>
|
||||
<div class="c-lad-table-wrapper u-style-receiver js-style-receiver" :class="staleClass">
|
||||
<table class="c-table c-lad-table" :class="applyLayoutClass">
|
||||
<table aria-label="lad table" class="c-table c-lad-table" :class="applyLayoutClass">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
|
@ -25,6 +25,7 @@
|
||||
aria-label="Clock Indicator"
|
||||
class="c-indicator t-indicator-clock icon-clock no-minify c-indicator--not-clickable"
|
||||
role="complementary"
|
||||
aria-live="off"
|
||||
>
|
||||
<span class="label c-indicator__label">
|
||||
{{ timeTextValue }}
|
||||
|
@ -22,6 +22,7 @@
|
||||
|
||||
<template>
|
||||
<div
|
||||
aria-label="sub object frame"
|
||||
class="l-layout__frame c-frame"
|
||||
:class="{
|
||||
'no-frame': !item.hasFrame,
|
||||
|
@ -37,24 +37,24 @@
|
||||
:style="styleObject"
|
||||
:data-font-size="item.fontSize"
|
||||
:data-font="item.font"
|
||||
aria-label="Alpha-numeric telemetry"
|
||||
@contextmenu.prevent="showContextMenu"
|
||||
@mouseover.ctrl="showToolTip"
|
||||
@mouseleave="hideToolTip"
|
||||
>
|
||||
<div
|
||||
class="is-status__indicator"
|
||||
:aria-label="`This item is ${status}`"
|
||||
:title="`This item is ${status}`"
|
||||
></div>
|
||||
<div class="is-status__indicator"></div>
|
||||
<div v-if="showLabel" class="c-telemetry-view__label">
|
||||
<div class="c-telemetry-view__label-text">
|
||||
<div
|
||||
class="c-telemetry-view__label-text"
|
||||
:aria-label="`Alpha-numeric telemetry name for ${domainObject.name}`"
|
||||
>
|
||||
{{ domainObject.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="showValue"
|
||||
:aria-label="fieldName"
|
||||
:aria-label="`Alpha-numeric telemetry value of ${telemetryValue}`"
|
||||
:title="fieldName"
|
||||
class="c-telemetry-view__value"
|
||||
:class="[telemetryClass]"
|
||||
|
@ -220,6 +220,7 @@
|
||||
lengthAdjust="spacing"
|
||||
text-anchor="middle"
|
||||
dominant-baseline="middle"
|
||||
:aria-label="`gauge value of ${curVal}`"
|
||||
x="50%"
|
||||
y="50%"
|
||||
>
|
||||
|
@ -25,7 +25,18 @@
|
||||
<div class="c-inspect-properties">
|
||||
<div class="c-inspect-properties__header">Numeric Data</div>
|
||||
</div>
|
||||
<div ref="numericDataView"></div>
|
||||
<div ref="numericDataView">
|
||||
<TelemetryFrame
|
||||
v-for="plotObject of plotObjects"
|
||||
:key="plotObject.identifier.key"
|
||||
:bounds="bounds"
|
||||
:telemetry-object="plotObject"
|
||||
:path="[plotObject]"
|
||||
:render-when-visible="plotObject.renderWhenVisible"
|
||||
>
|
||||
<Plot />
|
||||
</TelemetryFrame>
|
||||
</div>
|
||||
|
||||
<div v-if="!hasNumericData">
|
||||
{{ noNumericDataText }}
|
||||
@ -33,13 +44,15 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import mount from 'utils/mount';
|
||||
|
||||
import VisibilityObserver from '../../utils/visibility/VisibilityObserver.js';
|
||||
import Plot from '../plot/PlotView.vue';
|
||||
import TelemetryFrame from './TelemetryFrame.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TelemetryFrame,
|
||||
Plot
|
||||
},
|
||||
inject: ['openmct', 'domainObject', 'timeFormatter'],
|
||||
props: {
|
||||
bounds: {
|
||||
@ -90,16 +103,19 @@ export default {
|
||||
this.clearPlots();
|
||||
|
||||
this.unregisterTimeContextList = [];
|
||||
this.componentsList = [];
|
||||
this.elementsList = [];
|
||||
this.visibilityObservers = [];
|
||||
|
||||
this.telemetryKeys.forEach(async (telemetryKey) => {
|
||||
const plotObject = await this.openmct.objects.get(telemetryKey);
|
||||
const visibilityObserver = new VisibilityObserver(
|
||||
this.$refs.numericDataView,
|
||||
this.openmct.element
|
||||
);
|
||||
plotObject.renderWhenVisible = visibilityObserver.renderWhenVisible;
|
||||
|
||||
this.visibilityObservers.push(visibilityObserver);
|
||||
this.plotObjects.push(plotObject);
|
||||
this.unregisterTimeContextList.push(this.setIndependentTimeContextForComponent(plotObject));
|
||||
this.renderPlot(plotObject);
|
||||
});
|
||||
},
|
||||
setIndependentTimeContextForComponent(plotObject) {
|
||||
@ -110,63 +126,14 @@ export default {
|
||||
// set the time context of the object to the selected time range
|
||||
return this.openmct.time.addIndependentContext(keyString, this.bounds);
|
||||
},
|
||||
renderPlot(plotObject) {
|
||||
const wrapper = document.createElement('div');
|
||||
const visibilityObserver = new VisibilityObserver(wrapper, this.openmct.element);
|
||||
|
||||
const { destroy } = mount(
|
||||
{
|
||||
components: {
|
||||
TelemetryFrame,
|
||||
Plot
|
||||
},
|
||||
provide: {
|
||||
openmct: this.openmct,
|
||||
path: [plotObject],
|
||||
renderWhenVisible: visibilityObserver.renderWhenVisible
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
plotObject,
|
||||
bounds: this.bounds
|
||||
};
|
||||
},
|
||||
template: `<TelemetryFrame
|
||||
:bounds="bounds"
|
||||
:telemetry-object="plotObject"
|
||||
>
|
||||
<Plot />
|
||||
</TelemetryFrame>`
|
||||
},
|
||||
{
|
||||
app: this.openmct.app,
|
||||
element: wrapper
|
||||
}
|
||||
);
|
||||
|
||||
this.componentsList.push(destroy);
|
||||
this.elementsList.push(wrapper);
|
||||
this.visibilityObservers.push(visibilityObserver);
|
||||
this.$refs.numericDataView.append(wrapper);
|
||||
},
|
||||
clearPlots() {
|
||||
if (this.componentsList?.length) {
|
||||
this.componentsList.forEach((destroy) => destroy());
|
||||
delete this.componentsList;
|
||||
}
|
||||
|
||||
if (this.elementsList?.length) {
|
||||
this.elementsList.forEach((element) => element.remove());
|
||||
delete this.elementsList;
|
||||
}
|
||||
|
||||
if (this.visibilityObservers?.length) {
|
||||
this.visibilityObservers.forEach((visibilityObserver) => visibilityObserver.destroy());
|
||||
delete this.visibilityObservers;
|
||||
}
|
||||
|
||||
if (this.plotObjects?.length) {
|
||||
this.plotObjects = [];
|
||||
this.plotObjects.splice(0, this.plotObjects.length);
|
||||
}
|
||||
|
||||
if (this.unregisterTimeContextList?.length) {
|
||||
|
@ -70,7 +70,9 @@ export default {
|
||||
inject: ['openmct'],
|
||||
provide() {
|
||||
return {
|
||||
domainObject: this.telemetryObject
|
||||
domainObject: this.telemetryObject,
|
||||
path: this.path,
|
||||
renderWhenVisible: this.renderWhenVisible
|
||||
};
|
||||
},
|
||||
props: {
|
||||
@ -81,6 +83,14 @@ export default {
|
||||
telemetryObject: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
path: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
renderWhenVisible: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@ -110,7 +120,10 @@ export default {
|
||||
'tc.mode': 'fixed'
|
||||
};
|
||||
const newTabAction = this.openmct.actions.getAction('newTab');
|
||||
newTabAction.invoke([sourceTelemObject], urlParams);
|
||||
// No view context needed, so pass undefined.
|
||||
// The urlParams arg will override the global time bounds with the data visualization
|
||||
// plot bounds.
|
||||
newTabAction.invoke([sourceTelemObject], undefined, urlParams);
|
||||
this.showMenu = false;
|
||||
},
|
||||
previewTelemetry() {
|
||||
|
@ -52,7 +52,7 @@
|
||||
import Moment from 'moment';
|
||||
import mount from 'utils/mount';
|
||||
|
||||
import objectPathToUrl from '@/tools/url';
|
||||
import { objectPathToUrl } from '@/tools/url';
|
||||
|
||||
import tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';
|
||||
import ImageExporter from '../../../exporters/ImageExporter.js';
|
||||
|
@ -23,11 +23,11 @@
|
||||
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
|
||||
<button
|
||||
class="c-icon-button c-button--menu icon-camera"
|
||||
aria-label="Take a Notebook Snapshot"
|
||||
title="Take a Notebook Snapshot"
|
||||
:aria-label="snapshotMenuLabel"
|
||||
:title="snapshotMenuLabel"
|
||||
@click.stop.prevent="showMenu"
|
||||
>
|
||||
<span title="Take Notebook Snapshot" class="c-icon-button__label"> Snapshot </span>
|
||||
<span class="c-icon-button__label">Snapshot</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@ -72,6 +72,11 @@ export default {
|
||||
notebookTypes: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
snapshotMenuLabel() {
|
||||
return 'Open the Notebook Snapshot Menu';
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
validateNotebookStorageObject();
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import objectPathToUrl from '/src/tools/url.js';
|
||||
import { objectPathToUrl } from '/src/tools/url.js';
|
||||
export default class OpenInNewTab {
|
||||
constructor(openmct) {
|
||||
this.name = 'Open In New Tab';
|
||||
@ -31,8 +31,26 @@ export default class OpenInNewTab {
|
||||
|
||||
this._openmct = openmct;
|
||||
}
|
||||
invoke(objectPath, urlParams = undefined) {
|
||||
let url = objectPathToUrl(this._openmct, objectPath, urlParams);
|
||||
window.open(url);
|
||||
|
||||
/**
|
||||
* Invokes the "Open in New Tab" action. This will open the object in a new
|
||||
* browser tab. The URL for the new tab is determined by the current object
|
||||
* path and any custom time bounds.
|
||||
*
|
||||
* @param {import('@/api/objects/ObjectAPI').DomainObject[]} objectPath The current object path
|
||||
* @param {ViewContext} _view The view context for the object being opened (unused)
|
||||
* @param {Object<string, string | number>} customUrlParams Provides the ability to override
|
||||
* the global time conductor bounds. It is an object with the following key/value pairs:
|
||||
* ```
|
||||
* {
|
||||
* 'tc.start': <number>,
|
||||
* 'tc.end': <number>,
|
||||
* 'tc.mode': 'fixed' | 'local' | <string>
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
invoke(objectPath, _view, customUrlParams) {
|
||||
const url = objectPathToUrl(this._openmct, objectPath, customUrlParams);
|
||||
window.open(url, undefined, 'noopener');
|
||||
}
|
||||
}
|
||||
|
@ -287,7 +287,7 @@
|
||||
|
||||
<script>
|
||||
import _ from 'lodash';
|
||||
import { toRaw } from 'vue';
|
||||
import { onMounted, ref, toRaw } from 'vue';
|
||||
|
||||
import stalenessMixin from '@/ui/mixins/staleness-mixin';
|
||||
|
||||
@ -295,7 +295,7 @@ import CSVExporter from '../../../exporters/CSVExporter.js';
|
||||
import ProgressBar from '../../../ui/components/ProgressBar.vue';
|
||||
import Search from '../../../ui/components/SearchComponent.vue';
|
||||
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
|
||||
import throttle from '../../../utils/throttle';
|
||||
import { useResizeObserver } from '../../../ui/composables/resize.js';
|
||||
import SizingRow from './SizingRow.vue';
|
||||
import TableColumnHeader from './TableColumnHeader.vue';
|
||||
import TableFooterIndicator from './TableFooterIndicator.vue';
|
||||
@ -303,7 +303,6 @@ import TelemetryTableRow from './TableRow.vue';
|
||||
|
||||
const VISIBLE_ROW_COUNT = 100;
|
||||
const ROW_HEIGHT = 17;
|
||||
const RESIZE_POLL_INTERVAL = 200;
|
||||
const AUTO_SCROLL_TRIGGER_HEIGHT = ROW_HEIGHT * 3;
|
||||
|
||||
export default {
|
||||
@ -354,6 +353,15 @@ export default {
|
||||
}
|
||||
},
|
||||
emits: ['marked-rows-updated', 'filter'],
|
||||
setup() {
|
||||
const root = ref(null);
|
||||
const { size: containerSize, startObserving } = useResizeObserver();
|
||||
onMounted(() => {
|
||||
startObserving(root.value);
|
||||
});
|
||||
|
||||
return { containerSize, root };
|
||||
},
|
||||
data() {
|
||||
let configuration = this.table.configuration.getConfiguration();
|
||||
|
||||
@ -441,6 +449,13 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
//This should be refactored so that it doesn't require an explicit watch. Should be doable.
|
||||
containerSize: {
|
||||
handler() {
|
||||
this.debouncedRescaleToContainer();
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
loading: {
|
||||
handler(isLoading) {
|
||||
if (isLoading) {
|
||||
@ -500,9 +515,10 @@ export default {
|
||||
this.filterTelemetry = _.debounce(this.filterTelemetry, 500);
|
||||
},
|
||||
mounted() {
|
||||
this.throttledUpdateVisibleRows = _.throttle(this.updateVisibleRows, 1000, { leading: true });
|
||||
this.debouncedRescaleToContainer = _.debounce(this.rescaleToContainer, 300);
|
||||
|
||||
this.csvExporter = new CSVExporter();
|
||||
this.rowsAdded = _.throttle(this.rowsAdded, 200);
|
||||
this.rowsRemoved = _.throttle(this.rowsRemoved, 200);
|
||||
this.scroll = _.throttle(this.scroll, 100);
|
||||
|
||||
if (!this.marking.useAlternateControlBar && !this.enableLegacyToolbar) {
|
||||
@ -515,8 +531,6 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
this.updateVisibleRows = throttle(this.updateVisibleRows, 1000);
|
||||
|
||||
this.table.on('object-added', this.addObject);
|
||||
this.table.on('object-removed', this.removeObject);
|
||||
this.table.on('refresh', this.clearRowsAndRerender);
|
||||
@ -526,8 +540,8 @@ export default {
|
||||
|
||||
this.table.tableRows.on('add', this.rowsAdded);
|
||||
this.table.tableRows.on('remove', this.rowsRemoved);
|
||||
this.table.tableRows.on('sort', this.updateVisibleRows);
|
||||
this.table.tableRows.on('filter', this.updateVisibleRows);
|
||||
this.table.tableRows.on('sort', this.throttledUpdateVisibleRows);
|
||||
this.table.tableRows.on('filter', this.throttledUpdateVisibleRows);
|
||||
|
||||
this.openmct.time.on('bounds', this.boundsChanged);
|
||||
|
||||
@ -540,10 +554,10 @@ export default {
|
||||
this.table.configuration.on('change', this.updateConfiguration);
|
||||
|
||||
this.calculateTableSize();
|
||||
this.pollForResize();
|
||||
this.calculateScrollbarWidth();
|
||||
|
||||
this.table.initialize();
|
||||
this.rescaleToContainer();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.table.off('object-added', this.addObject);
|
||||
@ -555,15 +569,13 @@ export default {
|
||||
|
||||
this.table.tableRows.off('add', this.rowsAdded);
|
||||
this.table.tableRows.off('remove', this.rowsRemoved);
|
||||
this.table.tableRows.off('sort', this.updateVisibleRows);
|
||||
this.table.tableRows.off('filter', this.updateVisibleRows);
|
||||
this.table.tableRows.off('sort', this.throttledUpdateVisibleRows);
|
||||
this.table.tableRows.off('filter', this.throttledUpdateVisibleRows);
|
||||
|
||||
this.table.configuration.off('change', this.updateConfiguration);
|
||||
|
||||
this.openmct.time.off('bounds', this.boundsChanged);
|
||||
|
||||
clearInterval(this.resizePollHandle);
|
||||
|
||||
this.table.configuration.destroy();
|
||||
|
||||
this.table.destroy();
|
||||
@ -684,7 +696,7 @@ export default {
|
||||
this.table.sortBy(this.sortOptions);
|
||||
},
|
||||
scroll() {
|
||||
this.updateVisibleRows();
|
||||
this.throttledUpdateVisibleRows();
|
||||
this.synchronizeScrollX();
|
||||
|
||||
if (this.shouldAutoScroll()) {
|
||||
@ -757,11 +769,11 @@ export default {
|
||||
this.initiateAutoScroll();
|
||||
}
|
||||
|
||||
this.updateVisibleRows();
|
||||
this.throttledUpdateVisibleRows();
|
||||
},
|
||||
rowsRemoved(rows) {
|
||||
this.setHeight();
|
||||
this.updateVisibleRows();
|
||||
this.throttledUpdateVisibleRows();
|
||||
},
|
||||
/**
|
||||
* Calculates height based on total number of rows, and sets table height.
|
||||
@ -880,35 +892,27 @@ export default {
|
||||
dropTargetActive(isActive) {
|
||||
this.isDropTargetActive = isActive;
|
||||
},
|
||||
pollForResize() {
|
||||
let el = this.$refs.root;
|
||||
let width = el.clientWidth;
|
||||
let height = el.clientHeight;
|
||||
rescaleToContainer() {
|
||||
let scrollTop = this.scrollable.scrollTop;
|
||||
|
||||
this.resizePollHandle = setInterval(() => {
|
||||
this.renderWhenVisible(() => {
|
||||
if ((el.clientWidth !== width || el.clientHeight !== height) && this.isAutosizeEnabled) {
|
||||
this.calculateTableSize();
|
||||
// On some resize events scrollTop is reset to 0. Possibly due to a transition we're using?
|
||||
// Need to preserve scroll position in this case.
|
||||
if (this.autoScroll) {
|
||||
this.initiateAutoScroll();
|
||||
} else {
|
||||
this.scrollable.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
width = el.clientWidth;
|
||||
height = el.clientHeight;
|
||||
this.renderWhenVisible(() => {
|
||||
if (this.isAutosizeEnabled) {
|
||||
this.calculateTableSize();
|
||||
// On some resize events scrollTop is reset to 0. Possibly due to a transition we're using?
|
||||
// Need to preserve scroll position in this case.
|
||||
if (this.autoScroll) {
|
||||
this.initiateAutoScroll();
|
||||
} else {
|
||||
this.scrollable.scrollTop = scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
scrollTop = this.scrollable.scrollTop;
|
||||
});
|
||||
}, RESIZE_POLL_INTERVAL);
|
||||
scrollTop = this.scrollable.scrollTop;
|
||||
});
|
||||
},
|
||||
clearRowsAndRerender() {
|
||||
this.visibleRows = [];
|
||||
this.$nextTick().then(this.updateVisibleRows);
|
||||
this.$nextTick().then(this.throttledUpdateVisibleRows);
|
||||
},
|
||||
pause(byButton) {
|
||||
if (byButton) {
|
||||
|
@ -35,7 +35,6 @@ import utcMultiTimeFormat from './utcMultiTimeFormat.js';
|
||||
|
||||
const PADDING = 1;
|
||||
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||
const RESIZE_POLL_INTERVAL = 200;
|
||||
const PIXELS_PER_TICK = 100;
|
||||
const PIXELS_PER_TICK_WIDE = 200;
|
||||
|
||||
@ -92,7 +91,6 @@ export default {
|
||||
|
||||
//Respond to changes in conductor
|
||||
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setViewFromTimeSystem);
|
||||
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
|
||||
},
|
||||
beforeUnmount() {
|
||||
clearInterval(this.resizeTimer);
|
||||
|
@ -148,7 +148,10 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
|
||||
this.handleNewBounds = _.throttle(this.handleNewBounds, 300, {
|
||||
leading: true,
|
||||
trailing: false
|
||||
});
|
||||
this.setTimeSystem(this.copy(this.openmct.time.getTimeSystem()));
|
||||
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
|
||||
this.setTimeContext();
|
||||
|
@ -27,6 +27,7 @@
|
||||
isFixed ? 'is-fixed-mode' : independentTCEnabled ? 'is-realtime-mode' : 'is-fixed-mode',
|
||||
{ 'is-expanded': independentTCEnabled }
|
||||
]"
|
||||
aria-label="Independent Time Conductor Panel"
|
||||
>
|
||||
<ToggleSwitch
|
||||
id="independentTCToggle"
|
||||
|
@ -68,6 +68,18 @@ div {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
// Provides a way to add accessible text to elements
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/******************************************************** BROWSER ELEMENTS */
|
||||
body.desktop {
|
||||
::-webkit-scrollbar {
|
||||
@ -361,7 +373,7 @@ body.desktop .has-local-controls {
|
||||
}
|
||||
}
|
||||
|
||||
[aria-disabled = 'true'],
|
||||
[aria-disabled='true'],
|
||||
*[disabled],
|
||||
.disabled {
|
||||
opacity: $controlDisabledOpacity;
|
||||
@ -397,7 +409,7 @@ body.desktop .has-local-controls {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
content: "";
|
||||
content: '';
|
||||
right: 0;
|
||||
width: $fadeTruncateW * 1.5;
|
||||
z-index: 2;
|
||||
|
@ -24,10 +24,17 @@
|
||||
* Module defining url handling.
|
||||
*/
|
||||
|
||||
function getUrlParams(openmct, customUrlParams = {}) {
|
||||
/**
|
||||
* Convert the current URL parameters to an array of strings.
|
||||
* @param {import('../../openmct').OpenMCT} openmct
|
||||
* @returns {Array<string>} newTabParams
|
||||
*/
|
||||
export function paramsToArray(openmct, customUrlParams = {}) {
|
||||
let urlParams = openmct.router.getParams();
|
||||
Object.entries(customUrlParams).forEach((urlParam) => {
|
||||
const [key, value] = urlParam;
|
||||
|
||||
// Merge the custom URL parameters with the current URL parameters.
|
||||
Object.entries(customUrlParams).forEach((param) => {
|
||||
const [key, value] = param;
|
||||
urlParams[key] = value;
|
||||
});
|
||||
|
||||
@ -39,21 +46,7 @@ function getUrlParams(openmct, customUrlParams = {}) {
|
||||
delete urlParams['tc.endBound'];
|
||||
}
|
||||
|
||||
return urlParams;
|
||||
}
|
||||
|
||||
export function paramsToArray(openmct, customUrlParams = {}) {
|
||||
// parse urlParams from an object to an array.
|
||||
let urlParams = getUrlParams(openmct, customUrlParams);
|
||||
let newTabParams = [];
|
||||
for (let key in urlParams) {
|
||||
if ({}.hasOwnProperty.call(urlParams, key)) {
|
||||
let param = `${key}=${urlParams[key]}`;
|
||||
newTabParams.push(param);
|
||||
}
|
||||
}
|
||||
|
||||
return newTabParams;
|
||||
return Object.entries(urlParams).map(([key, value]) => `${key}=${value}`);
|
||||
}
|
||||
|
||||
export function identifierToString(openmct, objectPath) {
|
||||
@ -66,7 +59,7 @@ export function identifierToString(openmct, objectPath) {
|
||||
* @param {any} customUrlParams
|
||||
* @returns {string} url
|
||||
*/
|
||||
export default function objectPathToUrl(openmct, objectPath, customUrlParams = {}) {
|
||||
export function objectPathToUrl(openmct, objectPath, customUrlParams = {}) {
|
||||
let url = identifierToString(openmct, objectPath);
|
||||
|
||||
let urlParams = paramsToArray(openmct, customUrlParams);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { createOpenMct, resetApplicationState } from '../utils/testing.js';
|
||||
import { default as objectPathToUrl, identifierToString, paramsToArray } from './url.js';
|
||||
import { identifierToString, objectPathToUrl, paramsToArray } from './url.js';
|
||||
|
||||
describe('the url tool', function () {
|
||||
let openmct;
|
||||
|
@ -32,6 +32,7 @@
|
||||
'has-complex-content': complexContent
|
||||
}
|
||||
]"
|
||||
:aria-label="ariaLabel"
|
||||
>
|
||||
<div class="c-so-view__header">
|
||||
<div class="c-object-label" :class="[statusClass]">
|
||||
@ -45,6 +46,8 @@
|
||||
<div
|
||||
ref="objectName"
|
||||
class="c-object-label__name"
|
||||
aria-label="object name"
|
||||
:title="domainObject && domainObject.name"
|
||||
@mouseover.ctrl="showToolTip"
|
||||
@mouseleave="hideToolTip"
|
||||
>
|
||||
@ -58,6 +61,7 @@
|
||||
'c-so-view__frame-controls--no-frame': !hasFrame,
|
||||
'has-complex-content': complexContent
|
||||
}"
|
||||
:aria-label="`${ariaLabel} Controls`"
|
||||
>
|
||||
<div v-if="supportsIndependentTime" class="c-conductor-holder--compact">
|
||||
<independent-time-conductor :domain-object="domainObject" :object-path="objectPath" />
|
||||
@ -163,6 +167,9 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
ariaLabel() {
|
||||
return `${this.domainObject.name} Frame`;
|
||||
},
|
||||
statusClass() {
|
||||
return this.status ? `is-status--${this.status}` : '';
|
||||
}
|
||||
|
@ -105,7 +105,9 @@ export default {
|
||||
return this.status ? `is-status--${this.status}` : '';
|
||||
},
|
||||
ariaLabel() {
|
||||
return `${this.isEditing ? 'Preview' : 'Navigate to'} ${this.domainObject.name} ${this.domainObject.type} Object`;
|
||||
return `${this.isEditing ? 'Preview' : 'Navigate to'} ${this.domainObject.name} ${
|
||||
this.domainObject.type
|
||||
} Object`;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
@ -29,12 +29,14 @@
|
||||
import { axisTop } from 'd3-axis';
|
||||
import { scaleLinear, scaleUtc } from 'd3-scale';
|
||||
import { select } from 'd3-selection';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import utcMultiTimeFormat from '@/plugins/timeConductor/utcMultiTimeFormat';
|
||||
|
||||
import { useResizeObserver } from '../composables/resize';
|
||||
|
||||
//TODO: UI direction needed for the following property values
|
||||
const PADDING = 1;
|
||||
const RESIZE_POLL_INTERVAL = 200;
|
||||
const PIXELS_PER_TICK = 100;
|
||||
const PIXELS_PER_TICK_WIDE = 200;
|
||||
//This offset needs to be re-considered
|
||||
@ -73,6 +75,16 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const axisHolder = ref(null);
|
||||
const { size, startObserving } = useResizeObserver();
|
||||
onMounted(() => {
|
||||
startObserving(axisHolder.value);
|
||||
});
|
||||
return {
|
||||
containerSize: size
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
bounds(newBounds) {
|
||||
this.drawAxis(newBounds, this.timeSystem);
|
||||
@ -82,6 +94,9 @@ export default {
|
||||
},
|
||||
contentHeight() {
|
||||
this.updateNowMarker();
|
||||
},
|
||||
containerSize() {
|
||||
this.resize();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@ -100,7 +115,7 @@ export default {
|
||||
|
||||
this.setDimensions();
|
||||
this.drawAxis(this.bounds, this.timeSystem);
|
||||
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
|
||||
this.resize();
|
||||
},
|
||||
unmounted() {
|
||||
clearInterval(this.resizeTimer);
|
||||
|
@ -145,7 +145,11 @@
|
||||
:show-edit-view="true"
|
||||
@change-action-collection="setActionCollection"
|
||||
/>
|
||||
<component :is="conductorComponent" class="l-shell__time-conductor" />
|
||||
<component
|
||||
:is="conductorComponent"
|
||||
class="l-shell__time-conductor"
|
||||
aria-label="Global Time Conductor"
|
||||
/>
|
||||
</pane>
|
||||
<pane
|
||||
class="l-shell__pane-inspector l-pane--holds-multipane"
|
||||
|
@ -24,9 +24,10 @@
|
||||
<button
|
||||
class="c-create-button c-button--menu c-button--major icon-plus"
|
||||
:aria-disabled="isEditing"
|
||||
aria-labelledby="create-button-label"
|
||||
@click.prevent.stop="showCreateMenu"
|
||||
>
|
||||
<span class="c-button__label">Create</span>
|
||||
<span id="create-button-label" class="c-button__label">Create</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -27,7 +27,8 @@
|
||||
<button
|
||||
class="c-icon-button c-button--menu"
|
||||
:class="currentView.cssClass"
|
||||
title="Change the current view"
|
||||
:title="viewSwitcherLabel"
|
||||
:aria-label="viewSwitcherLabel"
|
||||
@click.prevent.stop="showMenu"
|
||||
>
|
||||
<span class="c-icon-button__label">
|
||||
@ -51,6 +52,11 @@ export default {
|
||||
}
|
||||
},
|
||||
emits: ['set-view'],
|
||||
computed: {
|
||||
viewSwitcherLabel() {
|
||||
return 'Open the View Switcher Menu';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setView(view) {
|
||||
this.$emit('set-view', view);
|
||||
|
@ -55,7 +55,7 @@
|
||||
|
||||
<script>
|
||||
import tooltipHelpers from '../../../api/tooltips/tooltipMixins.js';
|
||||
import identifierToString from '../../../tools/url.js';
|
||||
import { objectPathToUrl } from '../../../tools/url.js';
|
||||
import ObjectPath from '../../components/ObjectPath.vue';
|
||||
import PreviewAction from '../../preview/PreviewAction.js';
|
||||
|
||||
@ -101,7 +101,7 @@ export default {
|
||||
event.preventDefault();
|
||||
this.preview(objectPath);
|
||||
} else {
|
||||
let resultUrl = identifierToString(this.openmct, objectPath);
|
||||
let resultUrl = objectPathToUrl(this.openmct, objectPath);
|
||||
|
||||
// Remove the vestigial 'ROOT' identifier from url if it exists
|
||||
if (resultUrl.includes('/ROOT')) {
|
||||
|
@ -61,7 +61,8 @@ export default {
|
||||
let sortedActions = this.openmct.actions._groupAndSortActions(actions);
|
||||
|
||||
const menuOptions = {
|
||||
onDestroy: this.onContextMenuDestroyed
|
||||
onDestroy: this.onContextMenuDestroyed,
|
||||
label: this.objectPath[0].name
|
||||
};
|
||||
|
||||
const menuItems = this.openmct.menus.actionsToMenuItems(
|
||||
|
@ -1,4 +1,4 @@
|
||||
import objectPathToUrl from '../../tools/url.js';
|
||||
import { objectPathToUrl } from '../../tools/url.js';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
|
Loading…
Reference in New Issue
Block a user