mirror of
https://github.com/nasa/openmct.git
synced 2025-06-26 19:12:02 +00:00
Compare commits
20 Commits
telemetry-
...
master
Author | SHA1 | Date | |
---|---|---|---|
f4637b8ac7 | |||
826c7134b5 | |||
fa1a45b6cd | |||
10bc8eb55d | |||
2e7fb94dd5 | |||
573bbb041e | |||
6a450a0e89 | |||
e5631c9f6c | |||
15b5d1405d | |||
0377788533 | |||
28dcff7e89 | |||
b191eb9d64 | |||
f8e4aba922 | |||
28f6987dd7 | |||
f3047093d6 | |||
34e57ef300 | |||
88e6557782 | |||
dbd4abebae | |||
50ca27e54f | |||
2955092c86 |
@ -484,7 +484,8 @@
|
|||||||
"darkmatter",
|
"darkmatter",
|
||||||
"Undeletes",
|
"Undeletes",
|
||||||
"SSSZ",
|
"SSSZ",
|
||||||
"pageerror"
|
"pageerror",
|
||||||
|
"annotatable"
|
||||||
],
|
],
|
||||||
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],
|
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],
|
||||||
"ignorePaths": [
|
"ignorePaths": [
|
||||||
|
2
.github/workflows/e2e-couchdb.yml
vendored
2
.github/workflows/e2e-couchdb.yml
vendored
@ -56,7 +56,7 @@ jobs:
|
|||||||
run: npm run cov:e2e:report
|
run: npm run cov:e2e:report
|
||||||
|
|
||||||
- name: Publish Results to Codecov.io
|
- name: Publish Results to Codecov.io
|
||||||
uses: codecov/codecov-action@v4
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
files: ./coverage/e2e/lcov.info
|
files: ./coverage/e2e/lcov.info
|
||||||
|
12
API.md
12
API.md
@ -31,6 +31,10 @@
|
|||||||
- [`latest` request strategy](#latest-request-strategy)
|
- [`latest` request strategy](#latest-request-strategy)
|
||||||
- [`minmax` request strategy](#minmax-request-strategy)
|
- [`minmax` request strategy](#minmax-request-strategy)
|
||||||
- [Telemetry Formats](#telemetry-formats)
|
- [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)
|
- [Registering Formats](#registering-formats)
|
||||||
- [Telemetry Data](#telemetry-data)
|
- [Telemetry Data](#telemetry-data)
|
||||||
- [Telemetry Datums](#telemetry-datums)
|
- [Telemetry Datums](#telemetry-datums)
|
||||||
@ -59,6 +63,12 @@
|
|||||||
- [Custom Indicators](#custom-indicators)
|
- [Custom Indicators](#custom-indicators)
|
||||||
- [Priority API](#priority-api)
|
- [Priority API](#priority-api)
|
||||||
- [Priority Types](#priority-types)
|
- [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 -->
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
|
|
||||||
@ -1301,9 +1311,11 @@ 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):
|
Currently, the Open MCT Priority API provides (type: numeric value):
|
||||||
|
|
||||||
|
- HIGHEST: Infinity
|
||||||
- HIGH: 1000
|
- HIGH: 1000
|
||||||
- Default: 0
|
- Default: 0
|
||||||
- LOW: -1000
|
- LOW: -1000
|
||||||
|
- LOWEST: -Infinity
|
||||||
|
|
||||||
View provider Example:
|
View provider Example:
|
||||||
|
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
codecov:
|
codecov:
|
||||||
require_ci_to_pass: false #This setting will update the bot regardless of whether or not tests pass
|
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:
|
coverage:
|
||||||
status:
|
status:
|
||||||
project:
|
project:
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: ['plugin:playwright/recommended'],
|
extends: ['plugin:playwright/recommended'],
|
||||||
rules: {
|
rules: {
|
||||||
'playwright/max-nested-describe': ['error', { max: 1 }],
|
|
||||||
'playwright/expect-expect': 'off'
|
'playwright/expect-expect': 'off'
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
|
@ -68,7 +68,11 @@ 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
|
* @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.
|
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
|
||||||
*/
|
*/
|
||||||
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
|
async function createDomainObjectWithDefaults(
|
||||||
|
page,
|
||||||
|
{ type, name, parent = 'mine' },
|
||||||
|
additionalOptions = {}
|
||||||
|
) {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
name = `${type}:${genUuid()}`;
|
name = `${type}:${genUuid()}`;
|
||||||
}
|
}
|
||||||
@ -89,6 +93,13 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
|
|||||||
await page.getByLabel('Title', { exact: true }).fill('');
|
await page.getByLabel('Title', { exact: true }).fill('');
|
||||||
await page.getByLabel('Title', { exact: true }).fill(name);
|
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) {
|
if (page.testNotes) {
|
||||||
// Fill the "Notes" section with information about the
|
// Fill the "Notes" section with information about the
|
||||||
// currently running test and its project.
|
// currently running test and its project.
|
||||||
@ -105,7 +116,7 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
|
|||||||
|
|
||||||
if (await _isInEditMode(page, uuid)) {
|
if (await _isInEditMode(page, uuid)) {
|
||||||
// Save (exit edit mode)
|
// Save (exit edit mode)
|
||||||
await page.getByRole('button', { name: 'Save' }).click();
|
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,25 +103,40 @@ const extendedTest = test.extend({
|
|||||||
* Default: `true`
|
* Default: `true`
|
||||||
*/
|
*/
|
||||||
failOnConsoleError: [true, { option: true }],
|
failOnConsoleError: [true, { option: true }],
|
||||||
|
ignore404s: [[], { option: true }],
|
||||||
/**
|
/**
|
||||||
* Extends the base page class to enable console log error detection.
|
* Extends the base page class to enable console log error detection.
|
||||||
* @see {@link https://github.com/microsoft/playwright/discussions/11690 Github Discussion}
|
* @see {@link https://github.com/microsoft/playwright/discussions/11690 Github Discussion}
|
||||||
*/
|
*/
|
||||||
page: async ({ page, failOnConsoleError }, use) => {
|
page: async ({ page, failOnConsoleError, ignore404s }, use) => {
|
||||||
// Capture any console errors during test execution
|
// Capture any console errors during test execution
|
||||||
const messages = [];
|
let messages = [];
|
||||||
page.on('console', (msg) => messages.push(msg));
|
page.on('console', (msg) => messages.push(msg));
|
||||||
|
|
||||||
await use(page);
|
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
|
// Assert against console errors during teardown
|
||||||
if (failOnConsoleError) {
|
if (failOnConsoleError) {
|
||||||
messages.forEach((msg) =>
|
messages.forEach((msg) => {
|
||||||
// eslint-disable-next-line playwright/no-standalone-expect
|
// eslint-disable-next-line playwright/no-standalone-expect
|
||||||
expect
|
expect
|
||||||
.soft(msg.type(), `Console error detected: ${_consoleMessageToString(msg)}`)
|
.soft(msg.type(), `Console error detected: ${_consoleMessageToString(msg)}`)
|
||||||
.not.toEqual('error')
|
.not.toEqual('error');
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -224,7 +224,7 @@ export async function createTimelistWithPlanAndSetActivityInProgress(page, planJ
|
|||||||
await page.getByRole('button', { name: 'Edit Object' }).click();
|
await page.getByRole('button', { name: 'Edit Object' }).click();
|
||||||
|
|
||||||
// Find the display properties section in the inspector
|
// Find the display properties section in the inspector
|
||||||
await page.getByRole('tab', { name: 'View Properties' }).click();
|
await page.getByRole('tab', { name: 'Config' }).click();
|
||||||
// Switch to expanded view and save the setting
|
// Switch to expanded view and save the setting
|
||||||
await page.getByLabel('Display Style').selectOption({ label: 'Expanded' });
|
await page.getByLabel('Display Style').selectOption({ label: 'Expanded' });
|
||||||
|
|
||||||
|
@ -76,6 +76,7 @@ export async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {
|
|||||||
export async function testTelemetryItem(page, telemetryItem) {
|
export async function testTelemetryItem(page, telemetryItem) {
|
||||||
// Check that telemetry item also received the tag
|
// Check that telemetry item also received the tag
|
||||||
await page.goto(telemetryItem.url);
|
await page.goto(telemetryItem.url);
|
||||||
|
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||||
|
|
||||||
await expect(page.getByText('No tags to display for this item')).toBeVisible();
|
await expect(page.getByText('No tags to display for this item')).toBeVisible();
|
||||||
|
|
||||||
@ -93,6 +94,7 @@ export async function testTelemetryItem(page, telemetryItem) {
|
|||||||
y: 100
|
y: 100
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||||
|
|
||||||
await expect(page.getByText('Science')).toBeVisible();
|
await expect(page.getByText('Science')).toBeVisible();
|
||||||
await expect(page.getByText('Driving')).toBeHidden();
|
await expect(page.getByText('Driving')).toBeHidden();
|
||||||
@ -107,6 +109,8 @@ export async function basicTagsTests(page) {
|
|||||||
// Search for Driving
|
// Search for Driving
|
||||||
await page.getByRole('searchbox', { name: 'Search Input' }).click();
|
await page.getByRole('searchbox', { name: 'Search Input' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||||
|
|
||||||
// Clicking elsewhere should cause annotation selection to be cleared
|
// Clicking elsewhere should cause annotation selection to be cleared
|
||||||
await expect(page.getByText('No tags to display for this item')).toBeVisible();
|
await expect(page.getByText('No tags to display for this item')).toBeVisible();
|
||||||
//
|
//
|
||||||
@ -119,6 +123,8 @@ export async function basicTagsTests(page) {
|
|||||||
.first()
|
.first()
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||||
|
|
||||||
// Delete Driving Tag
|
// Delete Driving Tag
|
||||||
await page.hover('[aria-label="Tag"]:has-text("Driving")');
|
await page.hover('[aria-label="Tag"]:has-text("Driving")');
|
||||||
await page.locator('[aria-label="Remove tag Driving"]').click();
|
await page.locator('[aria-label="Remove tag Driving"]').click();
|
||||||
@ -155,6 +161,8 @@ export async function basicTagsTests(page) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||||
|
|
||||||
//Expect Science to be visible but Driving to be hidden
|
//Expect Science to be visible but Driving to be hidden
|
||||||
await expect(page.getByText('Science')).toBeVisible();
|
await expect(page.getByText('Science')).toBeVisible();
|
||||||
await expect(page.getByText('Driving')).toBeHidden();
|
await expect(page.getByText('Driving')).toBeHidden();
|
||||||
@ -170,7 +178,7 @@ export async function basicTagsTests(page) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add Driving Tag again
|
// Add Driving Tag again
|
||||||
await page.getByText('Annotations').click();
|
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||||
await page.getByRole('button', { name: /Add Tag/ }).click();
|
await page.getByRole('button', { name: /Add Tag/ }).click();
|
||||||
await page.getByPlaceholder('Type to select tag').click();
|
await page.getByPlaceholder('Type to select tag').click();
|
||||||
await page.getByText('Driving').click();
|
await page.getByText('Driving').click();
|
||||||
|
@ -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();
|
await page.getByRole('button', { name: 'Edit Object' }).click();
|
||||||
|
|
||||||
// Find the display properties section in the inspector
|
// Find the display properties section in the inspector
|
||||||
await page.getByRole('tab', { name: 'View Properties' }).click();
|
await page.getByRole('tab', { name: 'Config' }).click();
|
||||||
// Switch to expanded view and save the setting
|
// Switch to expanded view and save the setting
|
||||||
await page.getByLabel('Display Style').selectOption({ label: 'Expanded' });
|
await page.getByLabel('Display Style').selectOption({ label: 'Expanded' });
|
||||||
|
|
||||||
|
@ -54,8 +54,7 @@ const examplePlanSmall1 = JSON.parse(
|
|||||||
const TIME_TO_FROM_COLUMN = 2;
|
const TIME_TO_FROM_COLUMN = 2;
|
||||||
const HEADER_ROW = 0;
|
const HEADER_ROW = 0;
|
||||||
const NUM_COLUMNS = 5;
|
const NUM_COLUMNS = 5;
|
||||||
const FULL_CIRCLE_PATH =
|
const FULL_CIRCLE_PATH = 'M0,-50A50,50,0,1,1,0,50A50,50,0,1,1,0,-50Z';
|
||||||
'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.
|
* The regular expression used to parse the countdown string.
|
||||||
|
@ -27,7 +27,8 @@ demonstrate some playwright for test developers. This pattern should not be re-u
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
createDomainObjectWithDefaults,
|
createDomainObjectWithDefaults,
|
||||||
createExampleTelemetryObject
|
createExampleTelemetryObject,
|
||||||
|
setRealTimeMode
|
||||||
} from '../../../../appActions.js';
|
} from '../../../../appActions.js';
|
||||||
import { expect, test } from '../../../../pluginFixtures.js';
|
import { expect, test } from '../../../../pluginFixtures.js';
|
||||||
|
|
||||||
@ -116,7 +117,7 @@ test.describe('Basic Condition Set Use', () => {
|
|||||||
await page.getByLabel('Conditions View').click();
|
await page.getByLabel('Conditions View').click();
|
||||||
await expect(page.getByText('Current Output')).toBeVisible();
|
await expect(page.getByText('Current Output')).toBeVisible();
|
||||||
});
|
});
|
||||||
test('ConditionSet has correct outputs when telemetry is and is not available', async ({
|
test('ConditionSet produces an output when telemetry is available, and does not when it is not', async ({
|
||||||
page
|
page
|
||||||
}) => {
|
}) => {
|
||||||
const exampleTelemetry = await createExampleTelemetryObject(page);
|
const exampleTelemetry = await createExampleTelemetryObject(page);
|
||||||
@ -281,6 +282,101 @@ test.describe('Basic Condition Set Use', () => {
|
|||||||
await page.goto(exampleTelemetry.url);
|
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.fixme('Ensure condition sets work with telemetry like operator status', ({ page }) => {
|
||||||
test.info().annotations.push({
|
test.info().annotations.push({
|
||||||
type: 'issue',
|
type: 'issue',
|
||||||
|
@ -236,7 +236,7 @@ test.describe('Display Layout', () => {
|
|||||||
name: new RegExp(sineWaveObject.name)
|
name: new RegExp(sineWaveObject.name)
|
||||||
});
|
});
|
||||||
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
|
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
|
||||||
await page.locator('button[title="Save"]').click();
|
await page.getByLabel('Save', { exact: true }).click();
|
||||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||||
|
|
||||||
// Subscribe to the Sine Wave Generator data
|
// Subscribe to the Sine Wave Generator data
|
||||||
@ -278,7 +278,7 @@ test.describe('Display Layout', () => {
|
|||||||
name: new RegExp(sineWaveObject.name)
|
name: new RegExp(sineWaveObject.name)
|
||||||
});
|
});
|
||||||
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
|
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
|
||||||
await page.locator('button[title="Save"]').click();
|
await page.getByLabel('Save', { exact: true }).click();
|
||||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||||
|
|
||||||
// Subscribe to the Sine Wave Generator data
|
// Subscribe to the Sine Wave Generator data
|
||||||
@ -317,7 +317,7 @@ test.describe('Display Layout', () => {
|
|||||||
name: new RegExp(sineWaveObject.name)
|
name: new RegExp(sineWaveObject.name)
|
||||||
});
|
});
|
||||||
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
|
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
|
||||||
await page.locator('button[title="Save"]').click();
|
await page.getByLabel('Save', { exact: true }).click();
|
||||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||||
|
|
||||||
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
|
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)
|
name: new RegExp(sineWaveObject.name)
|
||||||
});
|
});
|
||||||
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
|
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
|
||||||
await page.locator('button[title="Save"]').click();
|
await page.getByLabel('Save', { exact: true }).click();
|
||||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||||
|
|
||||||
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
|
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').click();
|
||||||
await page.locator('div[title="Resize object width"] > input').fill('70');
|
await page.locator('div[title="Resize object width"] > input').fill('70');
|
||||||
|
|
||||||
await page.locator('button[title="Save"]').click();
|
await page.getByLabel('Save', { exact: true }).click();
|
||||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||||
|
|
||||||
const startDate = '2021-12-30 01:01:00.000Z';
|
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('View type').click();
|
||||||
await page.getByText('Overlay Plot').click();
|
await page.getByText('Overlay Plot').click();
|
||||||
|
|
||||||
await page.getByLabel('Save').click();
|
await page.getByLabel('Save', { exact: true }).click();
|
||||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||||
|
|
||||||
// Time to inspect some network traffic
|
// Time to inspect some network traffic
|
||||||
@ -531,7 +531,7 @@ test.describe('Display Layout', () => {
|
|||||||
await stateGeneratorTreeItem.click({ button: 'right' });
|
await stateGeneratorTreeItem.click({ button: 'right' });
|
||||||
await page.getByLabel('Edit Properties...').click();
|
await page.getByLabel('Edit Properties...').click();
|
||||||
await page.getByLabel('State Duration (seconds)', { exact: true }).fill('0.1');
|
await page.getByLabel('State Duration (seconds)', { exact: true }).fill('0.1');
|
||||||
await page.getByLabel('Save').click();
|
await page.getByLabel('Save', { exact: true }).click();
|
||||||
|
|
||||||
// Create a Table for filtering ON values
|
// Create a Table for filtering ON values
|
||||||
const tableFilterOnValue = await createDomainObjectWithDefaults(page, {
|
const tableFilterOnValue = await createDomainObjectWithDefaults(page, {
|
||||||
@ -555,14 +555,14 @@ test.describe('Display Layout', () => {
|
|||||||
await page.goto(tableFilterOnValue.url);
|
await page.goto(tableFilterOnValue.url);
|
||||||
await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));
|
await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));
|
||||||
await selectFilterOption(page, '1');
|
await selectFilterOption(page, '1');
|
||||||
await page.getByLabel('Save').click();
|
await page.getByLabel('Save', { exact: true }).click();
|
||||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||||
|
|
||||||
// Navigate to OFF filtering table and add state generator and setup filters
|
// Navigate to OFF filtering table and add state generator and setup filters
|
||||||
await page.goto(tableFilterOffValue.url);
|
await page.goto(tableFilterOffValue.url);
|
||||||
await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));
|
await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));
|
||||||
await selectFilterOption(page, '0');
|
await selectFilterOption(page, '0');
|
||||||
await page.getByLabel('Save').click();
|
await page.getByLabel('Save', { exact: true }).click();
|
||||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||||
|
|
||||||
// Navigate to the display layout and edit it
|
// Navigate to the display layout and edit it
|
||||||
@ -586,7 +586,7 @@ test.describe('Display Layout', () => {
|
|||||||
// eslint-disable-next-line playwright/no-force-option
|
// eslint-disable-next-line playwright/no-force-option
|
||||||
force: true
|
force: true
|
||||||
});
|
});
|
||||||
await page.getByLabel('Save').click();
|
await page.getByLabel('Save', { exact: true }).click();
|
||||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||||
|
|
||||||
// Get the tables so we can verify filtering is working as expected
|
// Get the tables so we can verify filtering is working as expected
|
||||||
|
@ -54,8 +54,8 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
|
|||||||
|
|
||||||
await page.goto(exampleDataVisualizationSource.url);
|
await page.goto(exampleDataVisualizationSource.url);
|
||||||
|
|
||||||
await page.getByRole('tab', { name: 'Data Visualization' }).click();
|
|
||||||
await page.getByRole('cell', { name: /First Sine Wave Generator/ }).click();
|
await page.getByRole('cell', { name: /First Sine Wave Generator/ }).click();
|
||||||
|
await page.getByRole('tab', { name: 'Data Visualization' }).click();
|
||||||
await expect(page.getByText('Numeric Data')).toBeVisible();
|
await expect(page.getByText('Numeric Data')).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('span.plot-series-name', { hasText: 'First Sine Wave Generator Hz' })
|
page.locator('span.plot-series-name', { hasText: 'First Sine Wave Generator Hz' })
|
||||||
@ -63,6 +63,7 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
|
|||||||
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
|
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('cell', { name: /Second Sine Wave Generator/ }).click();
|
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.getByText('Numeric Data')).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('span.plot-series-name', { hasText: 'Second Sine Wave Generator Hz' })
|
page.locator('span.plot-series-name', { hasText: 'Second Sine Wave Generator Hz' })
|
||||||
@ -77,6 +78,8 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
|
|||||||
// ensure our new tab's title is correct
|
// ensure our new tab's title is correct
|
||||||
const newPage = await pagePromise;
|
const newPage = await pagePromise;
|
||||||
await newPage.waitForLoadState();
|
await newPage.waitForLoadState();
|
||||||
|
await page.getByRole('tab', { name: 'Data Visualization' }).click();
|
||||||
|
|
||||||
// expect new tab title to contain 'Second Sine Wave Generator'
|
// expect new tab title to contain 'Second Sine Wave Generator'
|
||||||
await expect(newPage).toHaveTitle('Second Sine Wave Generator');
|
await expect(newPage).toHaveTitle('Second Sine Wave Generator');
|
||||||
|
|
||||||
|
@ -53,7 +53,6 @@ test.describe('Testing LAD table configuration', () => {
|
|||||||
test('in edit mode, LAD Tables provide ability to hide columns', async ({ page }) => {
|
test('in edit mode, LAD Tables provide ability to hide columns', async ({ page }) => {
|
||||||
// Edit LAD table
|
// Edit LAD table
|
||||||
await page.getByLabel('Edit Object').click();
|
await page.getByLabel('Edit Object').click();
|
||||||
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
|
|
||||||
|
|
||||||
// make sure headers are visible initially
|
// make sure headers are visible initially
|
||||||
await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();
|
await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();
|
||||||
@ -114,7 +113,6 @@ test.describe('Testing LAD table configuration', () => {
|
|||||||
|
|
||||||
// Edit LAD table
|
// Edit LAD table
|
||||||
await page.getByLabel('Edit Object').click();
|
await page.getByLabel('Edit Object').click();
|
||||||
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
|
|
||||||
|
|
||||||
// show timestamp column
|
// show timestamp column
|
||||||
await page.getByLabel('Timestamp', { exact: true }).check();
|
await page.getByLabel('Timestamp', { exact: true }).check();
|
||||||
@ -142,7 +140,6 @@ test.describe('Testing LAD table configuration', () => {
|
|||||||
|
|
||||||
// Edit LAD table
|
// Edit LAD table
|
||||||
await page.getByLabel('Edit Object').click();
|
await page.getByLabel('Edit Object').click();
|
||||||
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
|
|
||||||
|
|
||||||
// show units, type, and WATCH columns
|
// show units, type, and WATCH columns
|
||||||
await page.getByLabel('Units').check();
|
await page.getByLabel('Units').check();
|
||||||
@ -182,7 +179,6 @@ test.describe('Testing LAD table configuration', () => {
|
|||||||
|
|
||||||
// Edit LAD table
|
// Edit LAD table
|
||||||
await page.getByLabel('Edit Object').click();
|
await page.getByLabel('Edit Object').click();
|
||||||
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
|
|
||||||
|
|
||||||
// make sure Sine Wave headers are visible initially too
|
// make sure Sine Wave headers are visible initially too
|
||||||
await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();
|
await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();
|
||||||
|
@ -65,9 +65,11 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
|||||||
});
|
});
|
||||||
test('Can add tags with blank entry', async ({ page }) => {
|
test('Can add tags with blank entry', async ({ page }) => {
|
||||||
await createDomainObjectWithDefaults(page, { type: 'Notebook' });
|
await createDomainObjectWithDefaults(page, { type: 'Notebook' });
|
||||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
|
||||||
|
|
||||||
await enterTextEntry(page, '');
|
await enterTextEntry(page, '');
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||||
|
|
||||||
await page.hover(`button:has-text("Add Tag")`);
|
await page.hover(`button:has-text("Add Tag")`);
|
||||||
await page.locator(`button:has-text("Add Tag")`).click();
|
await page.locator(`button:has-text("Add Tag")`).click();
|
||||||
|
|
||||||
|
@ -47,8 +47,6 @@ test.describe('Notebook Tests with CouchDB @couchdb @network', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Inspect Notebook Entry Network Requests', async ({ page }) => {
|
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
|
// Expand sidebar
|
||||||
await page.locator('.c-notebook__toggle-nav-button').click();
|
await page.locator('.c-notebook__toggle-nav-button').click();
|
||||||
|
|
||||||
@ -86,6 +84,9 @@ test.describe('Notebook Tests with CouchDB @couchdb @network', () => {
|
|||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
expect(notebookElementsRequests.length).toBeLessThanOrEqual(2);
|
expect(notebookElementsRequests.length).toBeLessThanOrEqual(2);
|
||||||
|
|
||||||
|
//Ensure we're on the annotations Tab in the inspector
|
||||||
|
await page.getByText('Annotations').click();
|
||||||
|
|
||||||
// Add some tags
|
// Add some tags
|
||||||
// Network Requests are for each tag creation are:
|
// Network Requests are for each tag creation are:
|
||||||
// 1) Getting the original path of the parent object
|
// 1) Getting the original path of the parent object
|
||||||
@ -180,8 +181,8 @@ test.describe('Notebook Tests with CouchDB @couchdb @network', () => {
|
|||||||
type: 'issue',
|
type: 'issue',
|
||||||
description: 'https://github.com/akhenry/openmct-yamcs/issues/69'
|
description: 'https://github.com/akhenry/openmct-yamcs/issues/69'
|
||||||
});
|
});
|
||||||
await page.getByText('Annotations').click();
|
|
||||||
await nbUtils.enterTextEntry(page, 'First Entry');
|
await nbUtils.enterTextEntry(page, 'First Entry');
|
||||||
|
await page.getByText('Annotations').click();
|
||||||
|
|
||||||
// Add three tags
|
// Add three tags
|
||||||
await addTagAndAwaitNetwork(page, 'Science');
|
await addTagAndAwaitNetwork(page, 'Science');
|
||||||
|
@ -100,6 +100,9 @@ test.describe('Overlay Plot', () => {
|
|||||||
await page.getByLabel('Expand By Default').check();
|
await page.getByLabel('Expand By Default').check();
|
||||||
await page.getByLabel('Save').click();
|
await page.getByLabel('Save').click();
|
||||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).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
|
// Assert that the legend is now open
|
||||||
await expect(page.getByLabel('Plot Legend Collapsed')).toBeHidden();
|
await expect(page.getByLabel('Plot Legend Collapsed')).toBeHidden();
|
||||||
await expect(page.getByLabel('Plot Legend Expanded')).toBeVisible();
|
await expect(page.getByLabel('Plot Legend Expanded')).toBeVisible();
|
||||||
@ -111,6 +114,9 @@ test.describe('Overlay Plot', () => {
|
|||||||
|
|
||||||
// Assert that the legend is expanded on page load
|
// Assert that the legend is expanded on page load
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Config' }).click();
|
||||||
|
|
||||||
await expect(page.getByLabel('Plot Legend Collapsed')).toBeHidden();
|
await expect(page.getByLabel('Plot Legend Collapsed')).toBeHidden();
|
||||||
await expect(page.getByLabel('Plot Legend Expanded')).toBeVisible();
|
await expect(page.getByLabel('Plot Legend Expanded')).toBeVisible();
|
||||||
await expect(page.getByRole('cell', { name: 'Name' })).toBeVisible();
|
await expect(page.getByRole('cell', { name: 'Name' })).toBeVisible();
|
||||||
|
244
e2e/tests/functional/plugins/plot/plotViewActions.e2e.spec.js
Normal file
244
e2e/tests/functional/plugins/plot/plotViewActions.e2e.spec.js
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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`);
|
||||||
|
});
|
||||||
|
});
|
@ -50,7 +50,7 @@ test.describe('Plots work in Previews', () => {
|
|||||||
});
|
});
|
||||||
const layoutGridHolder = page.getByLabel('Test Display Layout Layout Grid');
|
const layoutGridHolder = page.getByLabel('Test Display Layout Layout Grid');
|
||||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
|
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
|
||||||
await page.getByLabel('Save').click();
|
await page.getByLabel('Save', { exact: true }).click();
|
||||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||||
|
|
||||||
// right click on the plot and select view large
|
// 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.getByLabel('Move Sub-object Frame').click();
|
||||||
await page.getByText('View type').click();
|
await page.getByText('View type').click();
|
||||||
await page.getByText('Overlay Plot').click();
|
await page.getByText('Overlay Plot').click();
|
||||||
await page.getByLabel('Save').click();
|
await page.getByLabel('Save', { exact: true }).click();
|
||||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByLabel('Test Display Layout Layout', { exact: true }).getByLabel('Plot Canvas')
|
page.getByLabel('Test Display Layout Layout', { exact: true }).getByLabel('Plot Canvas')
|
||||||
|
@ -152,14 +152,14 @@ test.describe('Stacked Plot', () => {
|
|||||||
}) => {
|
}) => {
|
||||||
await page.goto(stackedPlot.url);
|
await page.goto(stackedPlot.url);
|
||||||
|
|
||||||
await page.getByRole('tab', { name: 'Config' }).click();
|
|
||||||
|
|
||||||
// Click on the 1st plot
|
// Click on the 1st plot
|
||||||
await page
|
await page
|
||||||
.getByLabel('Stacked Plot Item Sine Wave Generator A')
|
.getByLabel('Stacked Plot Item Sine Wave Generator A')
|
||||||
.getByLabel('Plot Canvas')
|
.getByLabel('Plot Canvas')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Config' }).click();
|
||||||
|
|
||||||
// Assert that the inspector shows the Y Axis properties for swgA
|
// 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: 'Plot Series' })).toBeVisible();
|
||||||
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
|
||||||
@ -172,6 +172,9 @@ test.describe('Stacked Plot', () => {
|
|||||||
.getByLabel('Stacked Plot Item Sine Wave Generator B')
|
.getByLabel('Stacked Plot Item Sine Wave Generator B')
|
||||||
.getByLabel('Plot Canvas')
|
.getByLabel('Plot Canvas')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Config' }).click();
|
||||||
|
|
||||||
// Assert that the inspector shows the Y Axis properties for swgB
|
// 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: 'Plot Series' })).toBeVisible();
|
||||||
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
|
||||||
@ -184,6 +187,9 @@ test.describe('Stacked Plot', () => {
|
|||||||
.getByLabel('Stacked Plot Item Sine Wave Generator C')
|
.getByLabel('Stacked Plot Item Sine Wave Generator C')
|
||||||
.getByLabel('Plot Canvas')
|
.getByLabel('Plot Canvas')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Config' }).click();
|
||||||
|
|
||||||
// Assert that the inspector shows the Y Axis properties for swgB
|
// 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: 'Plot Series' })).toBeVisible();
|
||||||
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
|
||||||
@ -194,7 +200,7 @@ test.describe('Stacked Plot', () => {
|
|||||||
// Go into edit mode
|
// Go into edit mode
|
||||||
await page.getByLabel('Edit Object').click();
|
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
|
// Click on the 1st plot
|
||||||
await page.getByLabel('Stacked Plot Item Sine Wave Generator A').click();
|
await page.getByLabel('Stacked Plot Item Sine Wave Generator A').click();
|
||||||
@ -233,11 +239,11 @@ test.describe('Stacked Plot', () => {
|
|||||||
// Go into edit mode
|
// Go into edit mode
|
||||||
await page.getByLabel('Edit Object').click();
|
await page.getByLabel('Edit Object').click();
|
||||||
|
|
||||||
await page.getByRole('tab', { name: 'Config' }).click();
|
|
||||||
|
|
||||||
// Click on canvas for the 1st plot
|
// Click on canvas for the 1st plot
|
||||||
await page.getByLabel(`Stacked Plot Item ${swgA.name}`).click();
|
await page.getByLabel(`Stacked Plot Item ${swgA.name}`).click();
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Config' }).click();
|
||||||
|
|
||||||
// Expand config for the series
|
// Expand config for the series
|
||||||
await page.getByLabel('Expand Sine Wave Generator A Plot Series Options').click();
|
await page.getByLabel('Expand Sine Wave Generator A Plot Series Options').click();
|
||||||
|
|
||||||
@ -255,6 +261,8 @@ test.describe('Stacked Plot', () => {
|
|||||||
// Click on canvas for the 1st plot
|
// Click on canvas for the 1st plot
|
||||||
await page.getByLabel(`Stacked Plot Item ${swgA.name}`).click();
|
await page.getByLabel(`Stacked Plot Item ${swgA.name}`).click();
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Config' }).click();
|
||||||
|
|
||||||
// Expand config for the series
|
// Expand config for the series
|
||||||
await page.getByLabel('Expand Sine Wave Generator A Plot Series Options').click();
|
await page.getByLabel('Expand Sine Wave Generator A Plot Series Options').click();
|
||||||
|
|
||||||
|
@ -45,6 +45,8 @@ const setFontFamily = '"Andale Mono", sans-serif';
|
|||||||
|
|
||||||
test.describe('Stacked Plot styling', () => {
|
test.describe('Stacked Plot styling', () => {
|
||||||
let stackedPlot;
|
let stackedPlot;
|
||||||
|
let overlayPlot1;
|
||||||
|
let overlayPlot2;
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
@ -54,17 +56,30 @@ test.describe('Stacked Plot styling', () => {
|
|||||||
name: 'StackedPlot1'
|
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
|
// Create two SWGs and attach them to the Stacked Plot
|
||||||
await createDomainObjectWithDefaults(page, {
|
await createDomainObjectWithDefaults(page, {
|
||||||
type: 'Sine Wave Generator',
|
type: 'Sine Wave Generator',
|
||||||
name: 'Sine Wave Generator 1',
|
name: 'Sine Wave Generator 1',
|
||||||
parent: stackedPlot.uuid
|
parent: overlayPlot1.uuid
|
||||||
});
|
});
|
||||||
|
|
||||||
await createDomainObjectWithDefaults(page, {
|
await createDomainObjectWithDefaults(page, {
|
||||||
type: 'Sine Wave Generator',
|
type: 'Sine Wave Generator',
|
||||||
name: 'Sine Wave Generator 2',
|
name: 'Sine Wave Generator 2',
|
||||||
parent: stackedPlot.uuid
|
parent: overlayPlot2.uuid
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -138,21 +153,21 @@ test.describe('Stacked Plot styling', () => {
|
|||||||
NO_STYLE_RGBA,
|
NO_STYLE_RGBA,
|
||||||
NO_STYLE_RGBA,
|
NO_STYLE_RGBA,
|
||||||
hexToRGB(setTextColor),
|
hexToRGB(setTextColor),
|
||||||
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
|
page.getByLabel('Stacked Plot Item Overlay Plot 1')
|
||||||
);
|
);
|
||||||
|
|
||||||
await checkStyles(
|
await checkStyles(
|
||||||
NO_STYLE_RGBA,
|
NO_STYLE_RGBA,
|
||||||
NO_STYLE_RGBA,
|
NO_STYLE_RGBA,
|
||||||
hexToRGB(setTextColor),
|
hexToRGB(setTextColor),
|
||||||
page.getByLabel('Stacked Plot Item Sine Wave Generator 2')
|
page.getByLabel('Stacked Plot Item Overlay Plot 2')
|
||||||
);
|
);
|
||||||
|
|
||||||
await checkFontStyles(
|
await checkFontStyles(
|
||||||
setFontSize,
|
setFontSize,
|
||||||
setFontWeight,
|
setFontWeight,
|
||||||
setFontFamily,
|
setFontFamily,
|
||||||
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
|
page.getByLabel('Stacked Plot Item Overlay Plot 1')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -169,19 +184,19 @@ test.describe('Stacked Plot styling', () => {
|
|||||||
|
|
||||||
await page.getByRole('tab', { name: 'Styles' }).click();
|
await page.getByRole('tab', { name: 'Styles' }).click();
|
||||||
|
|
||||||
//Check default styles for SWG1 and SWG2
|
//Check default styles for overlayPlot1 and overlayPlot2
|
||||||
await checkStyles(
|
await checkStyles(
|
||||||
NO_STYLE_RGBA,
|
NO_STYLE_RGBA,
|
||||||
NO_STYLE_RGBA,
|
NO_STYLE_RGBA,
|
||||||
hexToRGB(defaultTextColor),
|
hexToRGB(defaultTextColor),
|
||||||
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
|
page.getByLabel('Stacked Plot Item Overlay Plot 1')
|
||||||
);
|
);
|
||||||
|
|
||||||
await checkStyles(
|
await checkStyles(
|
||||||
NO_STYLE_RGBA,
|
NO_STYLE_RGBA,
|
||||||
NO_STYLE_RGBA,
|
NO_STYLE_RGBA,
|
||||||
hexToRGB(defaultTextColor),
|
hexToRGB(defaultTextColor),
|
||||||
page.getByLabel('Stacked Plot Item Sine Wave Generator 2')
|
page.getByLabel('Stacked Plot Item Overlay Plot 2')
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set styles using setStyles function on StackedPlot1 but not StackedPlot2
|
// Set styles using setStyles function on StackedPlot1 but not StackedPlot2
|
||||||
@ -190,11 +205,11 @@ test.describe('Stacked Plot styling', () => {
|
|||||||
setBorderColor,
|
setBorderColor,
|
||||||
setBackgroundColor,
|
setBackgroundColor,
|
||||||
setTextColor,
|
setTextColor,
|
||||||
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
|
page.getByLabel('Stacked Plot Item Overlay Plot 1')
|
||||||
);
|
);
|
||||||
|
|
||||||
//Set Font Styles on SWG1 but not SWG2
|
//Set Font Styles on SWG1 but not SWG2
|
||||||
await page.getByLabel('Stacked Plot Item Sine Wave Generator 1').click();
|
await page.getByLabel('Stacked Plot Item Overlay Plot 1').click();
|
||||||
//Set Font Size to 72
|
//Set Font Size to 72
|
||||||
await page.getByLabel('Set Font Size').click();
|
await page.getByLabel('Set Font Size').click();
|
||||||
await page.getByRole('menuitem', { name: '72px' }).click();
|
await page.getByRole('menuitem', { name: '72px' }).click();
|
||||||
|
@ -31,6 +31,8 @@ import { expect, test } from '../../pluginFixtures.js';
|
|||||||
test.describe('Grand Search', () => {
|
test.describe('Grand Search', () => {
|
||||||
let grandSearchInput;
|
let grandSearchInput;
|
||||||
|
|
||||||
|
test.use({ ignore404s: [/_design\/object_names\/_view\/object_names$/] });
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
grandSearchInput = page
|
grandSearchInput = page
|
||||||
.getByLabel('OpenMCT Search')
|
.getByLabel('OpenMCT Search')
|
||||||
@ -191,7 +193,88 @@ test.describe('Grand Search', () => {
|
|||||||
await expect(searchResults).toContainText(folderName);
|
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 }) => {
|
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({
|
test.info().annotations.push({
|
||||||
type: 'issue',
|
type: 'issue',
|
||||||
description: 'https://github.com/nasa/openmct/issues/6179'
|
description: 'https://github.com/nasa/openmct/issues/6179'
|
||||||
@ -199,11 +282,17 @@ test.describe('Grand Search', () => {
|
|||||||
await createObjectsForSearch(page);
|
await createObjectsForSearch(page);
|
||||||
|
|
||||||
let networkRequests = [];
|
let networkRequests = [];
|
||||||
|
|
||||||
page.on('request', (request) => {
|
page.on('request', (request) => {
|
||||||
const searchRequest =
|
const isSearchRequest =
|
||||||
request.url().endsWith('_find') || request.url().includes('by_keystring');
|
request.url().endsWith('object_names') ||
|
||||||
const fetchRequest = request.resourceType() === 'fetch';
|
request.url().endsWith('_find') ||
|
||||||
if (searchRequest && fetchRequest) {
|
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) {
|
||||||
networkRequests.push(request);
|
networkRequests.push(request);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* eslint-disable playwright/no-conditional-in-test */
|
||||||
|
/* eslint-disable playwright/no-conditional-expect */
|
||||||
/*****************************************************************************
|
/*****************************************************************************
|
||||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
@ -31,6 +33,104 @@ 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.
|
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.describe('Inspector tests', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
@ -72,4 +172,49 @@ test.describe('Inspector tests', () => {
|
|||||||
|
|
||||||
await expect(lastInspectorPropertyValue).toBeInViewport();
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -213,7 +213,6 @@ test.describe('Navigation memory leak is not detected in', () => {
|
|||||||
page,
|
page,
|
||||||
'example-imagery-memory-leak-test'
|
'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.
|
// 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);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
@ -317,6 +316,12 @@ test.describe('Navigation memory leak is not detected in', () => {
|
|||||||
|
|
||||||
// Manually invoke the garbage collector once all references are removed.
|
// Manually invoke the garbage collector once all references are removed.
|
||||||
window.gc();
|
window.gc();
|
||||||
|
window.gc();
|
||||||
|
window.gc();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
window.gc();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
return gcPromise;
|
return gcPromise;
|
||||||
});
|
});
|
||||||
|
@ -40,6 +40,9 @@ test.describe('Visual - Inspector @ally @clock', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Inspector from overlay_plot_with_delay_storage @localStorage', async ({ page, theme }) => {
|
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
|
//Expand the Inspector Pane
|
||||||
await page.getByRole('button', { name: 'Inspect' }).click();
|
await page.getByRole('button', { name: 'Inspect' }).click();
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ test.describe('Grand Search @a11y', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Save and finish editing the Display Layout
|
// Save and finish editing the Display Layout
|
||||||
await page.getByRole('button', { name: 'Save' }).click();
|
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||||
|
|
||||||
// Search for the object
|
// Search for the object
|
||||||
|
@ -100,9 +100,12 @@ test.describe('Flexible Layout styling @a11y', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Save Flexible Layout
|
// Save Flexible Layout
|
||||||
await page.getByRole('button', { name: 'Save' }).click();
|
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||||
|
|
||||||
|
// Select styles tab
|
||||||
|
await page.getByRole('tab', { name: 'Styles' }).click();
|
||||||
|
|
||||||
await percySnapshot(
|
await percySnapshot(
|
||||||
page,
|
page,
|
||||||
`Saved Styled Flex Layout with Styled StackedPlot (theme: '${theme}')`
|
`Saved Styled Flex Layout with Styled StackedPlot (theme: '${theme}')`
|
||||||
@ -124,17 +127,30 @@ test.describe('Stacked Plot styling @a11y', () => {
|
|||||||
name: 'StackedPlot1'
|
name: 'StackedPlot1'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create two SWGs and attach them to the Stacked Plot
|
// 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
|
||||||
await createDomainObjectWithDefaults(page, {
|
await createDomainObjectWithDefaults(page, {
|
||||||
type: 'Sine Wave Generator',
|
type: 'Sine Wave Generator',
|
||||||
name: 'Sine Wave Generator 1',
|
name: 'Sine Wave Generator 1',
|
||||||
parent: stackedPlot.uuid
|
parent: overlayPlot1.uuid
|
||||||
});
|
});
|
||||||
|
|
||||||
await createDomainObjectWithDefaults(page, {
|
await createDomainObjectWithDefaults(page, {
|
||||||
type: 'Sine Wave Generator',
|
type: 'Sine Wave Generator',
|
||||||
name: 'Sine Wave Generator 2',
|
name: 'Sine Wave Generator 2',
|
||||||
parent: stackedPlot.uuid
|
parent: overlayPlot2.uuid
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -177,7 +193,7 @@ test.describe('Stacked Plot styling @a11y', () => {
|
|||||||
setBorderColor,
|
setBorderColor,
|
||||||
setBackgroundColor,
|
setBackgroundColor,
|
||||||
setTextColor,
|
setTextColor,
|
||||||
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
|
page.getByLabel('Stacked Plot Item Overlay Plot 1')
|
||||||
);
|
);
|
||||||
|
|
||||||
await percySnapshot(page, `Edit Mode StackedPlot with Styled SWG (theme: '${theme}')`);
|
await percySnapshot(page, `Edit Mode StackedPlot with Styled SWG (theme: '${theme}')`);
|
||||||
|
1260
package-lock.json
generated
1260
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@ -16,25 +16,25 @@
|
|||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/eslint-parser": "7.23.3",
|
"@babel/eslint-parser": "7.23.3",
|
||||||
"@braintree/sanitize-url": "6.0.4",
|
"@braintree/sanitize-url": "7.1.1",
|
||||||
"@types/d3-axis": "3.0.6",
|
"@types/d3-axis": "3.0.6",
|
||||||
"@types/d3-scale": "4.0.8",
|
"@types/d3-scale": "4.0.8",
|
||||||
"@types/d3-selection": "3.0.10",
|
"@types/d3-selection": "3.0.10",
|
||||||
"@types/d3-shape": "3.0.0",
|
"@types/d3-shape": "3.1.7",
|
||||||
"@types/eventemitter3": "1.2.0",
|
"@types/eventemitter3": "1.2.0",
|
||||||
"@types/jasmine": "5.1.2",
|
"@types/jasmine": "5.1.2",
|
||||||
"@types/lodash": "4.17.0",
|
"@types/lodash": "4.17.0",
|
||||||
"@vue/compiler-sfc": "3.4.3",
|
"@vue/compiler-sfc": "3.4.3",
|
||||||
"babel-loader": "9.1.0",
|
"babel-loader": "9.1.0",
|
||||||
"babel-plugin-istanbul": "6.1.1",
|
"babel-plugin-istanbul": "7.0.0",
|
||||||
"comma-separated-values": "3.6.4",
|
"comma-separated-values": "3.6.4",
|
||||||
"copy-webpack-plugin": "12.0.2",
|
"copy-webpack-plugin": "13.0.0",
|
||||||
"cspell": "7.3.8",
|
"cspell": "7.3.8",
|
||||||
"css-loader": "6.10.0",
|
"css-loader": "6.10.0",
|
||||||
"d3-axis": "3.0.0",
|
"d3-axis": "3.0.0",
|
||||||
"d3-scale": "4.0.2",
|
"d3-scale": "4.0.2",
|
||||||
"d3-selection": "3.0.0",
|
"d3-selection": "3.0.0",
|
||||||
"d3-shape": "3.0.0",
|
"d3-shape": "3.2.0",
|
||||||
"eslint": "8.56.0",
|
"eslint": "8.56.0",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
"eslint-plugin-compat": "4.2.0",
|
"eslint-plugin-compat": "4.2.0",
|
||||||
@ -51,7 +51,7 @@
|
|||||||
"git-rev-sync": "3.0.2",
|
"git-rev-sync": "3.0.2",
|
||||||
"html2canvas": "1.4.1",
|
"html2canvas": "1.4.1",
|
||||||
"imports-loader": "5.0.0",
|
"imports-loader": "5.0.0",
|
||||||
"jasmine-core": "5.1.1",
|
"jasmine-core": "5.6.0",
|
||||||
"karma": "6.4.2",
|
"karma": "6.4.2",
|
||||||
"karma-chrome-launcher": "3.2.0",
|
"karma-chrome-launcher": "3.2.0",
|
||||||
"karma-cli": "2.0.0",
|
"karma-cli": "2.0.0",
|
||||||
@ -64,14 +64,14 @@
|
|||||||
"karma-webpack": "5.0.1",
|
"karma-webpack": "5.0.1",
|
||||||
"location-bar": "3.0.1",
|
"location-bar": "3.0.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"marked": "12.0.0",
|
"marked": "15.0.7",
|
||||||
"mini-css-extract-plugin": "2.7.6",
|
"mini-css-extract-plugin": "2.9.2",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"moment-duration-format": "2.3.2",
|
"moment-duration-format": "2.3.2",
|
||||||
"moment-timezone": "0.5.41",
|
"moment-timezone": "0.5.41",
|
||||||
"nano": "10.1.4",
|
"nano": "10.1.4",
|
||||||
"npm-run-all2": "6.1.2",
|
"npm-run-all2": "7.0.2",
|
||||||
"nyc": "15.1.0",
|
"nyc": "17.1.0",
|
||||||
"painterro": "1.2.87",
|
"painterro": "1.2.87",
|
||||||
"plotly.js-basic-dist-min": "2.29.1",
|
"plotly.js-basic-dist-min": "2.29.1",
|
||||||
"plotly.js-gl2d-dist-min": "2.20.0",
|
"plotly.js-gl2d-dist-min": "2.20.0",
|
||||||
@ -79,21 +79,21 @@
|
|||||||
"prettier-eslint": "16.3.0",
|
"prettier-eslint": "16.3.0",
|
||||||
"printj": "1.3.1",
|
"printj": "1.3.1",
|
||||||
"resolve-url-loader": "5.0.0",
|
"resolve-url-loader": "5.0.0",
|
||||||
"sanitize-html": "2.12.1",
|
"sanitize-html": "2.15.0",
|
||||||
"sass": "1.71.1",
|
"sass": "1.71.1",
|
||||||
"sass-loader": "14.1.1",
|
"sass-loader": "14.1.1",
|
||||||
"style-loader": "3.3.3",
|
"style-loader": "4.0.0",
|
||||||
"terser-webpack-plugin": "5.3.9",
|
"terser-webpack-plugin": "5.3.9",
|
||||||
"tiny-emitter": "2.1.0",
|
"tiny-emitter": "2.1.0",
|
||||||
"typescript": "5.3.3",
|
"typescript": "5.3.3",
|
||||||
"uuid": "9.0.1",
|
"uuid": "11.1.0",
|
||||||
"vue": "3.4.24",
|
"vue": "3.4.24",
|
||||||
"vue-eslint-parser": "9.4.2",
|
"vue-eslint-parser": "9.4.2",
|
||||||
"vue-loader": "16.8.3",
|
"vue-loader": "16.8.3",
|
||||||
"webpack": "5.90.3",
|
"webpack": "5.98.0",
|
||||||
"webpack-cli": "5.1.1",
|
"webpack-cli": "5.1.1",
|
||||||
"webpack-dev-server": "5.0.2",
|
"webpack-dev-server": "5.0.2",
|
||||||
"webpack-merge": "5.10.0"
|
"webpack-merge": "6.0.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rm -rf ./dist ./node_modules ./coverage ./html-test-results ./e2e/test-results ./.nyc_output ./e2e/.nyc_output",
|
"clean": "rm -rf ./dist ./node_modules ./coverage ./html-test-results ./e2e/test-results ./.nyc_output ./e2e/.nyc_output",
|
||||||
|
@ -582,4 +582,15 @@ export default class AnnotationAPI extends EventEmitter {
|
|||||||
_.isEqual(targets, otherTargets)
|
_.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,8 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import { isIdentifier } from '../objects/object-utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('openmct').DomainObject} DomainObject
|
* @typedef {import('openmct').DomainObject} DomainObject
|
||||||
*/
|
*/
|
||||||
@ -209,9 +211,15 @@ export default class CompositionCollection {
|
|||||||
this.#cleanUpMutables();
|
this.#cleanUpMutables();
|
||||||
const children = await this.#provider.load(this.domainObject);
|
const children = await this.#provider.load(this.domainObject);
|
||||||
const childObjects = await Promise.all(
|
const childObjects = await Promise.all(
|
||||||
children.map((c) => this.#publicAPI.objects.get(c, abortSignal))
|
children.map((child) => {
|
||||||
|
if (isIdentifier(child)) {
|
||||||
|
return this.#publicAPI.objects.get(child, abortSignal);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve(child);
|
||||||
|
}
|
||||||
|
})
|
||||||
);
|
);
|
||||||
childObjects.forEach((c) => this.add(c, true));
|
childObjects.forEach((child) => this.add(child, true));
|
||||||
this.#emit('load');
|
this.#emit('load');
|
||||||
|
|
||||||
return childObjects;
|
return childObjects;
|
||||||
|
@ -96,8 +96,9 @@ export default class CompositionProvider {
|
|||||||
* object.
|
* object.
|
||||||
* @param {DomainObject} domainObject the domain object
|
* @param {DomainObject} domainObject the domain object
|
||||||
* for which to load composition
|
* for which to load composition
|
||||||
* @returns {Promise<Identifier[]>} a promise for
|
* @returns {Promise<Identifier[] | DomainObject[]>} a promise for
|
||||||
* the Identifiers in this composition
|
* the Identifiers or Domain Objects in this composition. If Identifiers are returned,
|
||||||
|
* they will be automatically resolved to domain objects by the API.
|
||||||
*/
|
*/
|
||||||
load(domainObject) {
|
load(domainObject) {
|
||||||
throw new Error('This method must be implemented by a subclass.');
|
throw new Error('This method must be implemented by a subclass.');
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
import { toRaw } from 'vue';
|
import { toRaw } from 'vue';
|
||||||
|
|
||||||
import { makeKeyString } from '../objects/object-utils.js';
|
import { makeKeyString, parseKeyString } from '../objects/object-utils.js';
|
||||||
import CompositionProvider from './CompositionProvider.js';
|
import CompositionProvider from './CompositionProvider.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -75,7 +75,11 @@ export default class DefaultCompositionProvider extends CompositionProvider {
|
|||||||
* the Identifiers in this composition
|
* the Identifiers in this composition
|
||||||
*/
|
*/
|
||||||
load(domainObject) {
|
load(domainObject) {
|
||||||
return Promise.all(domainObject.composition);
|
const identifiers = domainObject.composition
|
||||||
|
.filter((idOrKeystring) => idOrKeystring !== null && idOrKeystring !== undefined)
|
||||||
|
.map((idOrKeystring) => parseKeyString(idOrKeystring));
|
||||||
|
|
||||||
|
return Promise.all(identifiers);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Attach listeners for changes to the composition of a given domain object.
|
* Attach listeners for changes to the composition of a given domain object.
|
||||||
|
@ -27,6 +27,7 @@ import ConflictError from './ConflictError.js';
|
|||||||
import InMemorySearchProvider from './InMemorySearchProvider.js';
|
import InMemorySearchProvider from './InMemorySearchProvider.js';
|
||||||
import InterceptorRegistry from './InterceptorRegistry.js';
|
import InterceptorRegistry from './InterceptorRegistry.js';
|
||||||
import MutableDomainObject from './MutableDomainObject.js';
|
import MutableDomainObject from './MutableDomainObject.js';
|
||||||
|
import { isIdentifier, isKeyString } from './object-utils.js';
|
||||||
import RootObjectProvider from './RootObjectProvider.js';
|
import RootObjectProvider from './RootObjectProvider.js';
|
||||||
import RootRegistry from './RootRegistry.js';
|
import RootRegistry from './RootRegistry.js';
|
||||||
import Transaction from './Transaction.js';
|
import Transaction from './Transaction.js';
|
||||||
@ -742,11 +743,19 @@ export default class ObjectAPI {
|
|||||||
* @param {AbortSignal} abortSignal (optional) signal to abort fetch requests
|
* @param {AbortSignal} abortSignal (optional) signal to abort fetch requests
|
||||||
* @returns {Promise<Array<DomainObject>>} a promise containing an array of domain objects
|
* @returns {Promise<Array<DomainObject>>} a promise containing an array of domain objects
|
||||||
*/
|
*/
|
||||||
async getOriginalPath(identifier, path = [], abortSignal = null) {
|
async getOriginalPath(identifierOrObject, path = [], abortSignal = null) {
|
||||||
const domainObject = await this.get(identifier, abortSignal);
|
let domainObject;
|
||||||
|
|
||||||
|
if (isKeyString(identifierOrObject) || isIdentifier(identifierOrObject)) {
|
||||||
|
domainObject = await this.get(identifierOrObject, abortSignal);
|
||||||
|
} else {
|
||||||
|
domainObject = identifierOrObject;
|
||||||
|
}
|
||||||
|
|
||||||
if (!domainObject) {
|
if (!domainObject) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
path.push(domainObject);
|
path.push(domainObject);
|
||||||
const { location } = domainObject;
|
const { location } = domainObject;
|
||||||
if (location && !this.#pathContainsDomainObject(location, path)) {
|
if (location && !this.#pathContainsDomainObject(location, path)) {
|
||||||
|
@ -21,8 +21,11 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
const PRIORITIES = Object.freeze({
|
const PRIORITIES = Object.freeze({
|
||||||
|
HIGHEST: Infinity,
|
||||||
HIGH: 1000,
|
HIGH: 1000,
|
||||||
DEFAULT: 0,
|
DEFAULT: 0,
|
||||||
LOW: -1000
|
LOW: -1000,
|
||||||
|
LOWEST: -Infinity
|
||||||
});
|
});
|
||||||
|
|
||||||
export default PRIORITIES;
|
export default PRIORITIES;
|
||||||
|
@ -284,6 +284,33 @@ export default class TelemetryAPI {
|
|||||||
return value;
|
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
|
* Generates a numeric hash value for an options object. The hash is consistent
|
||||||
* for equivalent option objects regardless of property order.
|
* for equivalent option objects regardless of property order.
|
||||||
|
@ -89,6 +89,17 @@ export default class TypeRegistry {
|
|||||||
get(typeKey) {
|
get(typeKey) {
|
||||||
return this.types[typeKey] || UNKNOWN_TYPE;
|
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) {
|
importLegacyTypes(types) {
|
||||||
types
|
types
|
||||||
.filter((t) => this.get(t.key) === UNKNOWN_TYPE)
|
.filter((t) => this.get(t.key) === UNKNOWN_TYPE)
|
||||||
|
@ -25,10 +25,14 @@
|
|||||||
* Originally created by hudsonfoo on 09/02/16
|
* Originally created by hudsonfoo on 09/02/16
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function replaceDotsWithUnderscores(filename) {
|
function sanitizeFilename(filename) {
|
||||||
const regex = /\./gi;
|
const replacedPeriods = filename.replace(/\./g, '_');
|
||||||
|
const safeFilename = replacedPeriods.replace(/[^a-zA-Z0-9_\-.\s]/g, '');
|
||||||
|
|
||||||
return filename.replace(regex, '_');
|
// Handle leading/trailing spaces and periods
|
||||||
|
const trimmedFilename = safeFilename.trim().replace(/^\.+|\.+$/g, '');
|
||||||
|
|
||||||
|
return trimmedFilename;
|
||||||
}
|
}
|
||||||
|
|
||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
@ -150,7 +154,7 @@ class ImageExporter {
|
|||||||
* @returns {promise}
|
* @returns {promise}
|
||||||
*/
|
*/
|
||||||
async exportJPG(element, filename, className) {
|
async exportJPG(element, filename, className) {
|
||||||
const processedFilename = replaceDotsWithUnderscores(filename);
|
const processedFilename = sanitizeFilename(filename);
|
||||||
|
|
||||||
const img = await this.renderElement(element, {
|
const img = await this.renderElement(element, {
|
||||||
imageType: 'jpg',
|
imageType: 'jpg',
|
||||||
@ -167,7 +171,7 @@ class ImageExporter {
|
|||||||
* @returns {promise}
|
* @returns {promise}
|
||||||
*/
|
*/
|
||||||
async exportPNG(element, filename, className) {
|
async exportPNG(element, filename, className) {
|
||||||
const processedFilename = replaceDotsWithUnderscores(filename);
|
const processedFilename = sanitizeFilename(filename);
|
||||||
|
|
||||||
const img = await this.renderElement(element, {
|
const img = await this.renderElement(element, {
|
||||||
imageType: 'png',
|
imageType: 'png',
|
||||||
|
@ -24,6 +24,9 @@ export default function (folderName, couchPlugin, searchFilter) {
|
|||||||
location: 'ROOT'
|
location: 'ROOT'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
search() {
|
||||||
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -35,9 +38,17 @@ export default function (folderName, couchPlugin, searchFilter) {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
load() {
|
load() {
|
||||||
return couchProvider.getObjectsByFilter(searchFilter).then((objects) => {
|
let searchResults;
|
||||||
return objects.map((object) => object.identifier);
|
|
||||||
});
|
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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -41,9 +41,10 @@ export default class LADTableConfiguration extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getConfiguration() {
|
getConfiguration() {
|
||||||
const configuration = this.domainObject.configuration || {};
|
const configuration = this.domainObject.configuration ?? {};
|
||||||
configuration.hiddenColumns = configuration.hiddenColumns || {};
|
configuration.hiddenColumns = configuration.hiddenColumns ?? {};
|
||||||
configuration.isFixedLayout = configuration.isFixedLayout ?? true;
|
configuration.isFixedLayout = configuration.isFixedLayout ?? true;
|
||||||
|
configuration.objectStyles = configuration.objectStyles ?? {};
|
||||||
|
|
||||||
return configuration;
|
return configuration;
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ import LadTableConfiguration from './components/LadTableConfiguration.vue';
|
|||||||
export default function LADTableConfigurationViewProvider(openmct) {
|
export default function LADTableConfigurationViewProvider(openmct) {
|
||||||
return {
|
return {
|
||||||
key: 'lad-table-configuration',
|
key: 'lad-table-configuration',
|
||||||
name: 'LAD Table Configuration',
|
name: 'Config',
|
||||||
canView(selection) {
|
canView(selection) {
|
||||||
if (selection.length !== 1 || selection[0].length === 0) {
|
if (selection.length !== 1 || selection[0].length === 0) {
|
||||||
return false;
|
return false;
|
||||||
@ -61,7 +61,7 @@ export default function LADTableConfigurationViewProvider(openmct) {
|
|||||||
_destroy = destroy;
|
_destroy = destroy;
|
||||||
},
|
},
|
||||||
priority() {
|
priority() {
|
||||||
return 1;
|
return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;
|
||||||
},
|
},
|
||||||
destroy() {
|
destroy() {
|
||||||
if (_destroy) {
|
if (_destroy) {
|
||||||
|
@ -22,28 +22,24 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="c-inspect-properties">
|
<div class="c-inspect-properties">
|
||||||
<template v-if="isEditing">
|
<div class="c-inspect-properties__header">Table Column Visibility</div>
|
||||||
<div class="c-inspect-properties__header">Table Column Visibility</div>
|
<ul class="c-inspect-properties__section">
|
||||||
<ul class="c-inspect-properties__section">
|
<li v-for="(title, key) in headers" :key="key" class="c-inspect-properties__row">
|
||||||
<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">
|
||||||
<div class="c-inspect-properties__label" title="Show or hide column">
|
<label :for="key + 'ColumnControl'">{{ title }}</label>
|
||||||
<label :for="key + 'ColumnControl'">{{ title }}</label>
|
</div>
|
||||||
</div>
|
<div class="c-inspect-properties__value">
|
||||||
<div class="c-inspect-properties__value">
|
<input
|
||||||
<input
|
v-if="isEditing"
|
||||||
:id="key + 'ColumnControl'"
|
:id="key + 'ColumnControl'"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="configuration.hiddenColumns[key] !== true"
|
:checked="configuration.hiddenColumns[key] !== true"
|
||||||
@change="toggleColumn(key)"
|
@change="toggleColumn(key)"
|
||||||
/>
|
/>
|
||||||
</div>
|
<span v-if="!isEditing && configuration.hiddenColumns[key] !== true">Visible</span>
|
||||||
</li>
|
</div>
|
||||||
</ul>
|
</li>
|
||||||
</template>
|
</ul>
|
||||||
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -62,7 +58,8 @@ export default {
|
|||||||
isEditing: this.openmct.editor.isEditing(),
|
isEditing: this.openmct.editor.isEditing(),
|
||||||
configuration: ladTableConfiguration.getConfiguration(),
|
configuration: ladTableConfiguration.getConfiguration(),
|
||||||
items: [],
|
items: [],
|
||||||
ladTableObjects: []
|
ladTableObjects: [],
|
||||||
|
ladTelemetryObjects: {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -150,11 +147,14 @@ export default {
|
|||||||
this.ladTableObjects.push(ladTable);
|
this.ladTableObjects.push(ladTable);
|
||||||
|
|
||||||
const composition = this.openmct.composition.get(ladTable.domainObject);
|
const composition = this.openmct.composition.get(ladTable.domainObject);
|
||||||
|
composition.on('add', this.addItem);
|
||||||
|
composition.on('remove', this.removeItem);
|
||||||
composition.load();
|
composition.load();
|
||||||
|
|
||||||
this.compositions.push({
|
this.compositions.push({
|
||||||
composition
|
composition,
|
||||||
|
addCallback: this.addItem,
|
||||||
|
removeCallback: this.removeItem
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
removeLadTable(identifier) {
|
removeLadTable(identifier) {
|
||||||
|
@ -39,6 +39,9 @@ export default function plugin() {
|
|||||||
cssClass: 'icon-tabular-lad',
|
cssClass: 'icon-tabular-lad',
|
||||||
initialize(domainObject) {
|
initialize(domainObject) {
|
||||||
domainObject.composition = [];
|
domainObject.composition = [];
|
||||||
|
domainObject.configuration = {
|
||||||
|
objectStyles: {}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ export default function BarGraphInspectorViewProvider(openmct) {
|
|||||||
_destroy = destroy;
|
_destroy = destroy;
|
||||||
},
|
},
|
||||||
priority: function () {
|
priority: function () {
|
||||||
return openmct.priority.HIGH + 1;
|
return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;
|
||||||
},
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
if (_destroy) {
|
if (_destroy) {
|
||||||
|
@ -40,7 +40,7 @@ export default function ScatterPlotInspectorViewProvider(openmct) {
|
|||||||
_destroy = destroy;
|
_destroy = destroy;
|
||||||
},
|
},
|
||||||
priority: function () {
|
priority: function () {
|
||||||
return openmct.priority.HIGH + 1;
|
return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;
|
||||||
},
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
if (_destroy) {
|
if (_destroy) {
|
||||||
|
@ -44,48 +44,57 @@ import { getLatestTimestamp } from './utils/time.js';
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
export default class Condition extends EventEmitter {
|
export default class Condition extends EventEmitter {
|
||||||
|
#definition;
|
||||||
/**
|
/**
|
||||||
* Manages criteria and emits the result of - true or false - based on criteria evaluated.
|
* Manages criteria and emits the result of - true or false - based on criteria evaluated.
|
||||||
* @constructor
|
* @constructor
|
||||||
* @param conditionConfiguration: {id: uuid,trigger: enum, criteria: Array of {id: uuid, operation: enum, input: Array, metaDataKey: string, key: {domainObject.identifier} }
|
* @param definition: {id: uuid,trigger: enum, criteria: Array of {id: uuid, operation: enum, input: Array, metaDataKey: string, key: {domainObject.identifier} }
|
||||||
* @param openmct
|
* @param openmct
|
||||||
* @param conditionManager
|
* @param conditionManager
|
||||||
*/
|
*/
|
||||||
constructor(conditionConfiguration, openmct, conditionManager) {
|
constructor(definition, openmct, conditionManager) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.openmct = openmct;
|
this.openmct = openmct;
|
||||||
this.conditionManager = conditionManager;
|
this.conditionManager = conditionManager;
|
||||||
this.id = conditionConfiguration.id;
|
|
||||||
this.criteria = [];
|
this.criteria = [];
|
||||||
this.result = undefined;
|
this.result = undefined;
|
||||||
this.timeSystems = this.openmct.time.getAllTimeSystems();
|
this.timeSystems = this.openmct.time.getAllTimeSystems();
|
||||||
if (conditionConfiguration.configuration.criteria) {
|
this.#definition = definition;
|
||||||
this.createCriteria(conditionConfiguration.configuration.criteria);
|
|
||||||
|
if (definition.configuration.criteria) {
|
||||||
|
this.createCriteria(definition.configuration.criteria);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.trigger = conditionConfiguration.configuration.trigger;
|
this.trigger = definition.configuration.trigger;
|
||||||
this.summary = '';
|
this.summary = '';
|
||||||
this.handleCriterionUpdated = this.handleCriterionUpdated.bind(this);
|
this.handleCriterionUpdated = this.handleCriterionUpdated.bind(this);
|
||||||
this.handleOldTelemetryCriterion = this.handleOldTelemetryCriterion.bind(this);
|
this.handleOldTelemetryCriterion = this.handleOldTelemetryCriterion.bind(this);
|
||||||
this.handleTelemetryStaleness = this.handleTelemetryStaleness.bind(this);
|
this.handleTelemetryStaleness = this.handleTelemetryStaleness.bind(this);
|
||||||
}
|
}
|
||||||
|
get id() {
|
||||||
|
return this.#definition.id;
|
||||||
|
}
|
||||||
|
get configuration() {
|
||||||
|
return this.#definition.configuration;
|
||||||
|
}
|
||||||
|
|
||||||
updateResult(datum) {
|
updateResult(latestDataTable, telemetryIdThatChanged) {
|
||||||
if (!datum || !datum.id) {
|
if (!latestDataTable) {
|
||||||
console.log('no data received');
|
console.log('no data received');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if all the criteria in this condition have no telemetry, we want to force the condition result to evaluate
|
// if all the criteria in this condition have no telemetry, we want to force the condition result to evaluate
|
||||||
if (this.hasNoTelemetry() || this.isTelemetryUsed(datum.id)) {
|
if (this.hasNoTelemetry() || this.isTelemetryUsed(telemetryIdThatChanged)) {
|
||||||
|
const currentTimeSystemKey = this.openmct.time.getTimeSystem().key;
|
||||||
this.criteria.forEach((criterion) => {
|
this.criteria.forEach((criterion) => {
|
||||||
if (this.isAnyOrAllTelemetry(criterion)) {
|
if (this.isAnyOrAllTelemetry(criterion)) {
|
||||||
criterion.updateResult(datum, this.conditionManager.telemetryObjects);
|
criterion.updateResult(latestDataTable, this.conditionManager.telemetryObjects);
|
||||||
} else {
|
} else {
|
||||||
if (criterion.usesTelemetry(datum.id)) {
|
const relevantDatum = latestDataTable.get(criterion.telemetryObjectIdAsString);
|
||||||
criterion.updateResult(datum);
|
if (criterion.shouldUpdateResult(relevantDatum, currentTimeSystemKey)) {
|
||||||
|
criterion.updateResult(relevantDatum, currentTimeSystemKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -102,9 +111,11 @@ export default class Condition extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hasNoTelemetry() {
|
hasNoTelemetry() {
|
||||||
return this.criteria.every((criterion) => {
|
const usesSomeTelemetry = this.criteria.some((criterion) => {
|
||||||
return !this.isAnyOrAllTelemetry(criterion) && criterion.telemetry === '';
|
return this.isAnyOrAllTelemetry(criterion) || criterion.telemetry !== '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return !usesSomeTelemetry;
|
||||||
}
|
}
|
||||||
|
|
||||||
isTelemetryUsed(id) {
|
isTelemetryUsed(id) {
|
||||||
@ -182,7 +193,7 @@ export default class Condition extends EventEmitter {
|
|||||||
findCriterion(id) {
|
findCriterion(id) {
|
||||||
let criterion;
|
let criterion;
|
||||||
|
|
||||||
for (let i = 0, ii = this.criteria.length; i < ii; i++) {
|
for (let i = 0; i < this.criteria.length; i++) {
|
||||||
if (this.criteria[i].id === id) {
|
if (this.criteria[i].id === id) {
|
||||||
criterion = {
|
criterion = {
|
||||||
item: this.criteria[i],
|
item: this.criteria[i],
|
||||||
@ -247,7 +258,7 @@ export default class Condition extends EventEmitter {
|
|||||||
this.timeSystems,
|
this.timeSystems,
|
||||||
this.openmct.time.getTimeSystem()
|
this.openmct.time.getTimeSystem()
|
||||||
);
|
);
|
||||||
this.conditionManager.updateCurrentCondition(latestTimestamp);
|
this.conditionManager.updateCurrentCondition(latestTimestamp, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTelemetryStaleness() {
|
handleTelemetryStaleness() {
|
||||||
|
@ -27,6 +27,12 @@ import Condition from './Condition.js';
|
|||||||
import { getLatestTimestamp } from './utils/time.js';
|
import { getLatestTimestamp } from './utils/time.js';
|
||||||
|
|
||||||
export default class ConditionManager extends EventEmitter {
|
export default class ConditionManager extends EventEmitter {
|
||||||
|
#latestDataTable = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('openmct.js').DomainObject} conditionSetDomainObject
|
||||||
|
* @param {import('openmct.js').OpenMCT} openmct
|
||||||
|
*/
|
||||||
constructor(conditionSetDomainObject, openmct) {
|
constructor(conditionSetDomainObject, openmct) {
|
||||||
super();
|
super();
|
||||||
this.openmct = openmct;
|
this.openmct = openmct;
|
||||||
@ -304,22 +310,6 @@ export default class ConditionManager extends EventEmitter {
|
|||||||
this.persistConditions();
|
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) {
|
getCurrentConditionLAD(conditionResults) {
|
||||||
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
|
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
|
||||||
let currentCondition = conditionCollection[conditionCollection.length - 1];
|
let currentCondition = conditionCollection[conditionCollection.length - 1];
|
||||||
@ -410,26 +400,34 @@ export default class ConditionManager extends EventEmitter {
|
|||||||
|
|
||||||
const normalizedDatum = this.createNormalizedDatum(datum, endpoint);
|
const normalizedDatum = this.createNormalizedDatum(datum, endpoint);
|
||||||
const timeSystemKey = this.openmct.time.getTimeSystem().key;
|
const timeSystemKey = this.openmct.time.getTimeSystem().key;
|
||||||
let timestamp = {};
|
|
||||||
const currentTimestamp = normalizedDatum[timeSystemKey];
|
const currentTimestamp = normalizedDatum[timeSystemKey];
|
||||||
|
const timestamp = {};
|
||||||
|
|
||||||
timestamp[timeSystemKey] = currentTimestamp;
|
timestamp[timeSystemKey] = currentTimestamp;
|
||||||
|
this.#latestDataTable.set(normalizedDatum.id, normalizedDatum);
|
||||||
|
|
||||||
if (this.shouldEvaluateNewTelemetry(currentTimestamp)) {
|
if (this.shouldEvaluateNewTelemetry(currentTimestamp)) {
|
||||||
this.updateConditionResults(normalizedDatum);
|
const matchingCondition = this.updateConditionResults(normalizedDatum.id);
|
||||||
this.updateCurrentCondition(timestamp);
|
this.updateCurrentCondition(timestamp, matchingCondition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateConditionResults(normalizedDatum) {
|
updateConditionResults(keyStringForUpdatedTelemetryObject) {
|
||||||
//We want to stop when the first condition evaluates to true.
|
//We want to stop when the first condition evaluates to true.
|
||||||
this.conditions.some((condition) => {
|
const matchingCondition = this.conditions.find((condition) => {
|
||||||
condition.updateResult(normalizedDatum);
|
condition.updateResult(this.#latestDataTable, keyStringForUpdatedTelemetryObject);
|
||||||
|
|
||||||
return condition.result === true;
|
return condition.result === true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return matchingCondition;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCurrentCondition(timestamp) {
|
updateCurrentCondition(timestamp, matchingCondition) {
|
||||||
const currentCondition = this.getCurrentCondition();
|
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
|
||||||
|
const defaultCondition = conditionCollection[conditionCollection.length - 1];
|
||||||
|
|
||||||
|
const currentCondition = matchingCondition || defaultCondition;
|
||||||
|
|
||||||
this.emit(
|
this.emit(
|
||||||
'conditionSetResultUpdated',
|
'conditionSetResultUpdated',
|
||||||
@ -444,11 +442,13 @@ export default class ConditionManager extends EventEmitter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTestData(metadatum) {
|
getTestData(metadatum, identifier) {
|
||||||
let data = undefined;
|
let data = undefined;
|
||||||
if (this.testData.applied) {
|
if (this.testData.applied) {
|
||||||
const found = this.testData.conditionTestInputs.find(
|
const found = this.testData.conditionTestInputs.find(
|
||||||
(testInput) => testInput.metadata === metadatum.source
|
(testInput) =>
|
||||||
|
testInput.metadata === metadatum.source &&
|
||||||
|
this.openmct.objects.areIdsEqual(testInput.telemetry, identifier)
|
||||||
);
|
);
|
||||||
if (found) {
|
if (found) {
|
||||||
data = found.value;
|
data = found.value;
|
||||||
@ -463,7 +463,7 @@ export default class ConditionManager extends EventEmitter {
|
|||||||
const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas;
|
const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas;
|
||||||
|
|
||||||
const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => {
|
const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => {
|
||||||
const testValue = this.getTestData(metadatum);
|
const testValue = this.getTestData(metadatum, endpoint.identifier);
|
||||||
const formatter = this.openmct.telemetry.getValueFormatter(metadatum);
|
const formatter = this.openmct.telemetry.getValueFormatter(metadatum);
|
||||||
datum[metadatum.key] =
|
datum[metadatum.key] =
|
||||||
testValue !== undefined
|
testValue !== undefined
|
||||||
@ -480,7 +480,7 @@ export default class ConditionManager extends EventEmitter {
|
|||||||
|
|
||||||
updateTestData(testData) {
|
updateTestData(testData) {
|
||||||
if (!_.isEqual(testData, this.testData)) {
|
if (!_.isEqual(testData, this.testData)) {
|
||||||
this.testData = testData;
|
this.testData = JSON.parse(JSON.stringify(testData));
|
||||||
this.openmct.objects.mutate(
|
this.openmct.objects.mutate(
|
||||||
this.conditionSetDomainObject,
|
this.conditionSetDomainObject,
|
||||||
'configuration.conditionTestData',
|
'configuration.conditionTestData',
|
||||||
|
@ -53,6 +53,7 @@ describe('The condition', function () {
|
|||||||
valueMetadatas: [
|
valueMetadatas: [
|
||||||
{
|
{
|
||||||
key: 'some-key',
|
key: 'some-key',
|
||||||
|
source: 'some-key',
|
||||||
name: 'Some attribute',
|
name: 'Some attribute',
|
||||||
hints: {
|
hints: {
|
||||||
range: 2
|
range: 2
|
||||||
@ -60,6 +61,7 @@ describe('The condition', function () {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'utc',
|
key: 'utc',
|
||||||
|
source: 'utc',
|
||||||
name: 'Time',
|
name: 'Time',
|
||||||
format: 'utc',
|
format: 'utc',
|
||||||
hints: {
|
hints: {
|
||||||
@ -88,17 +90,32 @@ describe('The condition', function () {
|
|||||||
openmct.telemetry = jasmine.createSpyObj('telemetry', [
|
openmct.telemetry = jasmine.createSpyObj('telemetry', [
|
||||||
'isTelemetryObject',
|
'isTelemetryObject',
|
||||||
'subscribe',
|
'subscribe',
|
||||||
'getMetadata'
|
'getMetadata',
|
||||||
|
'getValueFormatter'
|
||||||
]);
|
]);
|
||||||
openmct.telemetry.isTelemetryObject.and.returnValue(true);
|
openmct.telemetry.isTelemetryObject.and.returnValue(true);
|
||||||
openmct.telemetry.subscribe.and.returnValue(function () {});
|
openmct.telemetry.subscribe.and.returnValue(function () {});
|
||||||
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);
|
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);
|
||||||
|
openmct.telemetry.getValueFormatter.and.callFake((metadatum) => {
|
||||||
|
return {
|
||||||
|
parse(input) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
mockTimeSystems = {
|
mockTimeSystems = {
|
||||||
key: 'utc'
|
key: 'utc'
|
||||||
};
|
};
|
||||||
openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems', 'on', 'off']);
|
openmct.time = jasmine.createSpyObj('time', [
|
||||||
|
'getTimeSystem',
|
||||||
|
'getAllTimeSystems',
|
||||||
|
'on',
|
||||||
|
'off'
|
||||||
|
]);
|
||||||
|
openmct.time.getTimeSystem.and.returnValue({ key: 'utc' });
|
||||||
openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]);
|
openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]);
|
||||||
|
//openmct.time.getTimeSystem.and.returnValue();
|
||||||
openmct.time.on.and.returnValue(() => {});
|
openmct.time.on.and.returnValue(() => {});
|
||||||
openmct.time.off.and.returnValue(() => {});
|
openmct.time.off.and.returnValue(() => {});
|
||||||
|
|
||||||
@ -113,7 +130,7 @@ describe('The condition', function () {
|
|||||||
id: '1234-5678-9999-0000',
|
id: '1234-5678-9999-0000',
|
||||||
operation: 'equalTo',
|
operation: 'equalTo',
|
||||||
input: ['0'],
|
input: ['0'],
|
||||||
metadata: 'value',
|
metadata: 'testSource',
|
||||||
telemetry: testTelemetryObject.identifier
|
telemetry: testTelemetryObject.identifier
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -156,37 +173,24 @@ describe('The condition', function () {
|
|||||||
expect(conditionObj.criteria.length).toEqual(0);
|
expect(conditionObj.criteria.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('gets the result of a condition when new telemetry data is received', function () {
|
|
||||||
conditionObj.updateResult({
|
|
||||||
value: '0',
|
|
||||||
utc: 'Hi',
|
|
||||||
id: 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 () {
|
it('keeps the old result new telemetry data is not used by it', function () {
|
||||||
conditionObj.updateResult({
|
const latestDataTable = new Map();
|
||||||
|
latestDataTable.set(testTelemetryObject.identifier.key, {
|
||||||
value: '0',
|
value: '0',
|
||||||
utc: 'Hi',
|
utc: 'Hi',
|
||||||
id: testTelemetryObject.identifier.key
|
id: testTelemetryObject.identifier.key
|
||||||
});
|
});
|
||||||
|
conditionObj.updateResult(latestDataTable, testTelemetryObject.identifier.key);
|
||||||
|
|
||||||
expect(conditionObj.result).toBeTrue();
|
expect(conditionObj.result).toBeTrue();
|
||||||
|
|
||||||
conditionObj.updateResult({
|
latestDataTable.set('1234', {
|
||||||
value: '1',
|
value: '1',
|
||||||
utc: 'Hi',
|
utc: 'Hi',
|
||||||
id: '1234'
|
id: '1234'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
conditionObj.updateResult(latestDataTable, '1234');
|
||||||
expect(conditionObj.result).toBeTrue();
|
expect(conditionObj.result).toBeTrue();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
<div
|
<div
|
||||||
class="c-condition-h"
|
class="c-condition-h"
|
||||||
:class="{ 'is-drag-target': draggingOver }"
|
:class="{ 'is-drag-target': draggingOver }"
|
||||||
aria-label="Condition Set Condition"
|
:aria-label="conditionSetLabel"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@drop.prevent="dropCondition($event, conditionIndex)"
|
@drop.prevent="dropCondition($event, conditionIndex)"
|
||||||
@dragenter="dragEnter($event, conditionIndex)"
|
@dragenter="dragEnter($event, conditionIndex)"
|
||||||
@ -53,7 +53,9 @@
|
|||||||
@click="expanded = !expanded"
|
@click="expanded = !expanded"
|
||||||
></span>
|
></span>
|
||||||
|
|
||||||
<span class="c-condition__name">{{ condition.configuration.name }}</span>
|
<span class="c-condition__name" aria-label="Condition Name Label">{{
|
||||||
|
condition.configuration.name
|
||||||
|
}}</span>
|
||||||
<span class="c-condition__summary">
|
<span class="c-condition__summary">
|
||||||
<template v-if="!condition.isDefault && !canEvaluateCriteria"> Define criteria </template>
|
<template v-if="!condition.isDefault && !canEvaluateCriteria"> Define criteria </template>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
@ -259,6 +261,17 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
conditionSetLabel() {
|
||||||
|
let label;
|
||||||
|
|
||||||
|
if (this.condition.id === this.currentConditionId) {
|
||||||
|
label = 'Active Condition Set Condition';
|
||||||
|
} else {
|
||||||
|
label = 'Condition Set Condition';
|
||||||
|
}
|
||||||
|
|
||||||
|
return label;
|
||||||
|
},
|
||||||
triggers() {
|
triggers() {
|
||||||
const keys = Object.keys(TRIGGER);
|
const keys = Object.keys(TRIGGER);
|
||||||
const triggerOptions = [];
|
const triggerOptions = [];
|
||||||
|
@ -114,7 +114,7 @@
|
|||||||
class="c-button c-button--major icon-plus labeled"
|
class="c-button c-button--major icon-plus labeled"
|
||||||
@click="addTestInput"
|
@click="addTestInput"
|
||||||
>
|
>
|
||||||
<span class="c-cs-button__label">Add Test Datum</span>
|
<span class="c-cs-button__label" aria-label="Add Test Datum">Add Test Datum</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -181,13 +181,20 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
|
|||||||
|
|
||||||
if (validatedData && !this.isStalenessCheck()) {
|
if (validatedData && !this.isStalenessCheck()) {
|
||||||
if (this.isOldCheck()) {
|
if (this.isOldCheck()) {
|
||||||
if (this.ageCheck?.[validatedData.id]) {
|
Object.keys(this.telemetryDataCache).forEach((objectIdKeystring) => {
|
||||||
this.ageCheck[validatedData.id].update(validatedData);
|
if (this.ageCheck?.[objectIdKeystring]) {
|
||||||
}
|
this.ageCheck[objectIdKeystring].update(validatedData[objectIdKeystring]);
|
||||||
|
}
|
||||||
|
|
||||||
this.telemetryDataCache[validatedData.id] = false;
|
this.telemetryDataCache[objectIdKeystring] = false;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.telemetryDataCache[validatedData.id] = this.computeResult(validatedData);
|
Object.keys(this.telemetryDataCache).forEach((objectIdKeystring) => {
|
||||||
|
const telemetryObject = telemetryObjects[objectIdKeystring];
|
||||||
|
this.telemetryDataCache[objectIdKeystring] = this.computeResult(
|
||||||
|
this.createNormalizedDatum(validatedData[objectIdKeystring], telemetryObject)
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,21 +29,29 @@ import { getOperatorText, OPERATIONS } from '../utils/operations.js';
|
|||||||
import { checkIfOld } from '../utils/time.js';
|
import { checkIfOld } from '../utils/time.js';
|
||||||
|
|
||||||
export default class TelemetryCriterion extends EventEmitter {
|
export default class TelemetryCriterion extends EventEmitter {
|
||||||
|
#lastUpdated;
|
||||||
|
#lastTimeSystem;
|
||||||
|
#comparator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribes/Unsubscribes to telemetry and emits the result
|
* Subscribes/Unsubscribes to telemetry and emits the result
|
||||||
* of operations performed on the telemetry data returned and a given input value.
|
* of operations performed on the telemetry data returned and a given input value.
|
||||||
* @constructor
|
* @constructor
|
||||||
* @param telemetryDomainObjectDefinition {id: uuid, operation: enum, input: Array, metadata: string, key: {domainObject.identifier} }
|
* @param telemetryDomainObjectDefinition {id: uuid, operation: enum, input: Array, metadata: string, key: {domainObject.identifier} }
|
||||||
* @param openmct
|
* @param {import('../../../MCT.js').OpenMCT} openmct
|
||||||
*/
|
*/
|
||||||
constructor(telemetryDomainObjectDefinition, openmct) {
|
constructor(telemetryDomainObjectDefinition, openmct) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import('../../../MCT.js').MCT}
|
||||||
|
*/
|
||||||
this.openmct = openmct;
|
this.openmct = openmct;
|
||||||
this.telemetryDomainObjectDefinition = telemetryDomainObjectDefinition;
|
this.telemetryDomainObjectDefinition = telemetryDomainObjectDefinition;
|
||||||
this.id = telemetryDomainObjectDefinition.id;
|
this.id = telemetryDomainObjectDefinition.id;
|
||||||
this.telemetry = telemetryDomainObjectDefinition.telemetry;
|
this.telemetry = telemetryDomainObjectDefinition.telemetry;
|
||||||
this.operation = telemetryDomainObjectDefinition.operation;
|
this.operation = telemetryDomainObjectDefinition.operation;
|
||||||
|
this.#comparator = this.#findOperation(this.operation);
|
||||||
this.input = telemetryDomainObjectDefinition.input;
|
this.input = telemetryDomainObjectDefinition.input;
|
||||||
this.metadata = telemetryDomainObjectDefinition.metadata;
|
this.metadata = telemetryDomainObjectDefinition.metadata;
|
||||||
this.result = undefined;
|
this.result = undefined;
|
||||||
@ -83,7 +91,6 @@ export default class TelemetryCriterion extends EventEmitter {
|
|||||||
if (this.ageCheck) {
|
if (this.ageCheck) {
|
||||||
this.ageCheck.clear();
|
this.ageCheck.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ageCheck = checkIfOld(this.handleOldTelemetry.bind(this), this.input[0] * 1000);
|
this.ageCheck = checkIfOld(this.handleOldTelemetry.bind(this), this.input[0] * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,7 +160,6 @@ export default class TelemetryCriterion extends EventEmitter {
|
|||||||
createNormalizedDatum(telemetryDatum, endpoint) {
|
createNormalizedDatum(telemetryDatum, endpoint) {
|
||||||
const id = this.openmct.objects.makeKeyString(endpoint.identifier);
|
const id = this.openmct.objects.makeKeyString(endpoint.identifier);
|
||||||
const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas;
|
const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas;
|
||||||
|
|
||||||
const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => {
|
const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => {
|
||||||
const formatter = this.openmct.telemetry.getValueFormatter(metadatum);
|
const formatter = this.openmct.telemetry.getValueFormatter(metadatum);
|
||||||
datum[metadatum.key] = formatter.parse(telemetryDatum[metadatum.source]);
|
datum[metadatum.key] = formatter.parse(telemetryDatum[metadatum.source]);
|
||||||
@ -179,9 +185,18 @@ export default class TelemetryCriterion extends EventEmitter {
|
|||||||
|
|
||||||
return datum;
|
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;
|
||||||
|
|
||||||
updateResult(data) {
|
return dataIsDefined && (hasTimeSystemChanged || isCacheStale);
|
||||||
const validatedData = this.isValid() ? data : {};
|
}
|
||||||
|
updateResult(data, currentTimeSystemKey) {
|
||||||
|
const validatedData = this.isValid()
|
||||||
|
? this.createNormalizedDatum(data, this.telemetryObject)
|
||||||
|
: {};
|
||||||
|
|
||||||
if (!this.isStalenessCheck()) {
|
if (!this.isStalenessCheck()) {
|
||||||
if (this.isOldCheck()) {
|
if (this.isOldCheck()) {
|
||||||
@ -193,6 +208,8 @@ export default class TelemetryCriterion extends EventEmitter {
|
|||||||
} else {
|
} else {
|
||||||
this.result = this.computeResult(validatedData);
|
this.result = this.computeResult(validatedData);
|
||||||
}
|
}
|
||||||
|
this.#lastUpdated = data[currentTimeSystemKey];
|
||||||
|
this.#lastTimeSystem = currentTimeSystemKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,8 +253,8 @@ export default class TelemetryCriterion extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
findOperation(operation) {
|
#findOperation(operation) {
|
||||||
for (let i = 0, ii = OPERATIONS.length; i < ii; i++) {
|
for (let i = 0; i < OPERATIONS.length; i++) {
|
||||||
if (operation === OPERATIONS[i].name) {
|
if (operation === OPERATIONS[i].name) {
|
||||||
return OPERATIONS[i].operation;
|
return OPERATIONS[i].operation;
|
||||||
}
|
}
|
||||||
@ -249,15 +266,14 @@ export default class TelemetryCriterion extends EventEmitter {
|
|||||||
computeResult(data) {
|
computeResult(data) {
|
||||||
let result = false;
|
let result = false;
|
||||||
if (data) {
|
if (data) {
|
||||||
let comparator = this.findOperation(this.operation);
|
|
||||||
let params = [];
|
let params = [];
|
||||||
params.push(data[this.metadata]);
|
params.push(data[this.metadata]);
|
||||||
if (this.isValidInput()) {
|
if (this.isValidInput()) {
|
||||||
this.input.forEach((input) => params.push(input));
|
this.input.forEach((input) => params.push(input));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof comparator === 'function') {
|
if (typeof this.#comparator === 'function') {
|
||||||
result = Boolean(comparator(params));
|
result = Boolean(this.#comparator(params));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,7 +106,7 @@ describe('The telemetry criterion', function () {
|
|||||||
id: 'test-criterion-id',
|
id: 'test-criterion-id',
|
||||||
telemetry: openmct.objects.makeKeyString(testTelemetryObject.identifier),
|
telemetry: openmct.objects.makeKeyString(testTelemetryObject.identifier),
|
||||||
operation: 'textContains',
|
operation: 'textContains',
|
||||||
metadata: 'value',
|
metadata: 'testSource',
|
||||||
input: ['Hell'],
|
input: ['Hell'],
|
||||||
telemetryObjects: { [testTelemetryObject.identifier.key]: testTelemetryObject }
|
telemetryObjects: { [testTelemetryObject.identifier.key]: testTelemetryObject }
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -20,11 +20,13 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import conditionWidgetStylesInterceptor from './conditionWidgetStylesInterceptor.js';
|
||||||
import ConditionWidgetViewProvider from './ConditionWidgetViewProvider.js';
|
import ConditionWidgetViewProvider from './ConditionWidgetViewProvider.js';
|
||||||
|
|
||||||
export default function plugin() {
|
export default function plugin() {
|
||||||
return function install(openmct) {
|
return function install(openmct) {
|
||||||
openmct.objectViews.addProvider(new ConditionWidgetViewProvider(openmct));
|
openmct.objectViews.addProvider(new ConditionWidgetViewProvider(openmct));
|
||||||
|
openmct.objects.addGetInterceptor(conditionWidgetStylesInterceptor(openmct));
|
||||||
|
|
||||||
openmct.types.addType('conditionWidget', {
|
openmct.types.addType('conditionWidget', {
|
||||||
key: 'conditionWidget',
|
key: 'conditionWidget',
|
||||||
@ -34,7 +36,9 @@ export default function plugin() {
|
|||||||
creatable: true,
|
creatable: true,
|
||||||
cssClass: 'icon-condition-widget',
|
cssClass: 'icon-condition-widget',
|
||||||
initialize(domainObject) {
|
initialize(domainObject) {
|
||||||
domainObject.configuration = {};
|
domainObject.configuration = {
|
||||||
|
objectStyles: {}
|
||||||
|
};
|
||||||
domainObject.label = 'Condition Widget';
|
domainObject.label = 'Condition Widget';
|
||||||
domainObject.conditionalLabel = '';
|
domainObject.conditionalLabel = '';
|
||||||
domainObject.url = '';
|
domainObject.url = '';
|
||||||
|
@ -65,7 +65,9 @@ class AlphanumericFormatView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
priority() {
|
priority() {
|
||||||
return 1;
|
return this.openmct.editor.isEditing()
|
||||||
|
? this.openmct.priority.DEFAULT
|
||||||
|
: this.openmct.priority.LOW;
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
@ -31,7 +31,8 @@ export default function DisplayLayoutType() {
|
|||||||
domainObject.composition = [];
|
domainObject.composition = [];
|
||||||
domainObject.configuration = {
|
domainObject.configuration = {
|
||||||
items: [],
|
items: [],
|
||||||
layoutGrid: [10, 10]
|
layoutGrid: [10, 10],
|
||||||
|
objectStyles: {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
form: [
|
form: [
|
||||||
|
@ -32,13 +32,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="c-inspect-properties__value">
|
<div class="c-inspect-properties__value">
|
||||||
<input
|
<input
|
||||||
|
v-if="isEditing"
|
||||||
id="telemetryPrintfFormat"
|
id="telemetryPrintfFormat"
|
||||||
type="text"
|
type="text"
|
||||||
:disabled="!isEditing"
|
|
||||||
:value="telemetryFormat"
|
:value="telemetryFormat"
|
||||||
:placeholder="nonMixedFormat ? '' : 'Mixed'"
|
:placeholder="nonMixedFormat ? '' : 'Mixed'"
|
||||||
@change="formatTelemetry"
|
@change="formatTelemetry"
|
||||||
/>
|
/>
|
||||||
|
<template v-if="!isEditing && telemetryFormat?.length">
|
||||||
|
{{ telemetryFormat }}
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
40
src/plugins/displayLayout/displayLayoutStylesInterceptor.js
Normal file
40
src/plugins/displayLayout/displayLayoutStylesInterceptor.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -25,6 +25,7 @@ import mount from 'utils/mount';
|
|||||||
import CopyToClipboardAction from './actions/CopyToClipboardAction.js';
|
import CopyToClipboardAction from './actions/CopyToClipboardAction.js';
|
||||||
import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js';
|
import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js';
|
||||||
import DisplayLayout from './components/DisplayLayout.vue';
|
import DisplayLayout from './components/DisplayLayout.vue';
|
||||||
|
import displayLayoutStylesInterceptor from './displayLayoutStylesInterceptor.js';
|
||||||
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
|
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
|
||||||
import DisplayLayoutType from './DisplayLayoutType.js';
|
import DisplayLayoutType from './DisplayLayoutType.js';
|
||||||
import DisplayLayoutDrawingObjectTypes from './DrawingObjectTypes.js';
|
import DisplayLayoutDrawingObjectTypes from './DrawingObjectTypes.js';
|
||||||
@ -123,6 +124,7 @@ export default function DisplayLayoutPlugin(options) {
|
|||||||
return 100;
|
return 100;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
openmct.objects.addGetInterceptor(displayLayoutStylesInterceptor(openmct));
|
||||||
openmct.types.addType('layout', DisplayLayoutType());
|
openmct.types.addType('layout', DisplayLayoutType());
|
||||||
openmct.toolbars.addProvider(new DisplayLayoutToolbar(openmct));
|
openmct.toolbars.addProvider(new DisplayLayoutToolbar(openmct));
|
||||||
openmct.inspectorViews.addProvider(new AlphaNumericFormatViewProvider(openmct, options));
|
openmct.inspectorViews.addProvider(new AlphaNumericFormatViewProvider(openmct, options));
|
||||||
|
@ -63,7 +63,7 @@ export default function FaultManagementInspectorViewProvider(openmct) {
|
|||||||
_destroy = destroy;
|
_destroy = destroy;
|
||||||
},
|
},
|
||||||
priority: function () {
|
priority: function () {
|
||||||
return openmct.priority.HIGH + 1;
|
return openmct.priority.HIGH;
|
||||||
},
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
if (_destroy) {
|
if (_destroy) {
|
||||||
|
@ -43,8 +43,6 @@ export default class FiltersInspectorViewProvider {
|
|||||||
let openmct = this.openmct;
|
let openmct = this.openmct;
|
||||||
let _destroy = null;
|
let _destroy = null;
|
||||||
|
|
||||||
const domainObject = selection?.[0]?.[0]?.context?.item;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
show: function (element) {
|
show: function (element) {
|
||||||
const { destroy } = mount(
|
const { destroy } = mount(
|
||||||
@ -69,13 +67,6 @@ export default class FiltersInspectorViewProvider {
|
|||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const metadata = openmct.telemetry.getMetadata(domainObject);
|
|
||||||
const metadataWithFilters = metadata
|
|
||||||
? metadata.valueMetadatas.filter((value) => value.filters)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return metadataWithFilters.length;
|
|
||||||
},
|
},
|
||||||
priority: function () {
|
priority: function () {
|
||||||
return openmct.priority.DEFAULT;
|
return openmct.priority.DEFAULT;
|
||||||
|
@ -38,6 +38,9 @@
|
|||||||
@update-filters="persistFilters"
|
@update-filters="persistFilters"
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
|
<span v-else>
|
||||||
|
This view doesn't include any parameters that have configured filter criteria.
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -158,6 +158,7 @@ export default {
|
|||||||
this.composition.on('remove', this.removeChildObject);
|
this.composition.on('remove', this.removeChildObject);
|
||||||
this.composition.on('add', this.addFrame);
|
this.composition.on('add', this.addFrame);
|
||||||
this.composition.load();
|
this.composition.load();
|
||||||
|
|
||||||
this.unObserveContainers = this.openmct.objects.observe(
|
this.unObserveContainers = this.openmct.objects.observe(
|
||||||
this.domainObject,
|
this.domainObject,
|
||||||
'configuration.containers',
|
'configuration.containers',
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -20,6 +20,7 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import flexibleLayoutStylesInterceptor from './flexibleLayoutStylesInterceptor.js';
|
||||||
import FlexibleLayoutViewProvider from './flexibleLayoutViewProvider.js';
|
import FlexibleLayoutViewProvider from './flexibleLayoutViewProvider.js';
|
||||||
import ToolBarProvider from './toolbarProvider.js';
|
import ToolBarProvider from './toolbarProvider.js';
|
||||||
import Container from './utils/container.js';
|
import Container from './utils/container.js';
|
||||||
@ -37,11 +38,13 @@ export default function plugin() {
|
|||||||
initialize: function (domainObject) {
|
initialize: function (domainObject) {
|
||||||
domainObject.configuration = {
|
domainObject.configuration = {
|
||||||
containers: [new Container(50), new Container(50)],
|
containers: [new Container(50), new Container(50)],
|
||||||
rowsLayout: false
|
rowsLayout: false,
|
||||||
|
objectStyles: {}
|
||||||
};
|
};
|
||||||
domainObject.composition = [];
|
domainObject.composition = [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
openmct.objects.addGetInterceptor(flexibleLayoutStylesInterceptor(openmct));
|
||||||
|
|
||||||
let toolbar = ToolBarProvider(openmct);
|
let toolbar = ToolBarProvider(openmct);
|
||||||
|
|
||||||
|
@ -21,28 +21,10 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
export default function GaugeCompositionPolicy(openmct) {
|
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 {
|
return {
|
||||||
allow: function (parent, child) {
|
allow: function (parent, child) {
|
||||||
if (parent.type === 'gauge') {
|
if (parent.type === 'gauge') {
|
||||||
return hasNumericTelemetry(child);
|
return openmct.telemetry.hasNumericTelemetry(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -24,6 +24,7 @@ import mount from 'utils/mount';
|
|||||||
|
|
||||||
import GaugeFormController from './components/GaugeFormController.vue';
|
import GaugeFormController from './components/GaugeFormController.vue';
|
||||||
import GaugeCompositionPolicy from './GaugeCompositionPolicy.js';
|
import GaugeCompositionPolicy from './GaugeCompositionPolicy.js';
|
||||||
|
import gaugeStylesInterceptor from './gaugeStylesInterceptor.js';
|
||||||
import GaugeViewProvider from './GaugeViewProvider.js';
|
import GaugeViewProvider from './GaugeViewProvider.js';
|
||||||
|
|
||||||
export const GAUGE_TYPES = [
|
export const GAUGE_TYPES = [
|
||||||
@ -37,7 +38,7 @@ export const GAUGE_TYPES = [
|
|||||||
export default function () {
|
export default function () {
|
||||||
return function install(openmct) {
|
return function install(openmct) {
|
||||||
openmct.objectViews.addProvider(new GaugeViewProvider(openmct));
|
openmct.objectViews.addProvider(new GaugeViewProvider(openmct));
|
||||||
|
openmct.objects.addGetInterceptor(gaugeStylesInterceptor(openmct));
|
||||||
openmct.forms.addNewFormControl('gauge-controller', getGaugeFormController(openmct));
|
openmct.forms.addNewFormControl('gauge-controller', getGaugeFormController(openmct));
|
||||||
openmct.types.addType('gauge', {
|
openmct.types.addType('gauge', {
|
||||||
name: 'Gauge',
|
name: 'Gauge',
|
||||||
@ -59,7 +60,8 @@ export default function () {
|
|||||||
max: 100,
|
max: 100,
|
||||||
min: 0,
|
min: 0,
|
||||||
precision: 2
|
precision: 2
|
||||||
}
|
},
|
||||||
|
objectStyles: {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
form: [
|
form: [
|
||||||
|
40
src/plugins/gauge/gaugeStylesInterceptor.js
Normal file
40
src/plugins/gauge/gaugeStylesInterceptor.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 gaugeStylesInterceptor(openmct) {
|
||||||
|
return {
|
||||||
|
appliesTo: (identifier, domainObject) => {
|
||||||
|
return domainObject?.type === 'gauge';
|
||||||
|
},
|
||||||
|
invoke: (identifier, domainObject) => {
|
||||||
|
if (!domainObject.configuration) {
|
||||||
|
domainObject.configuration = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!domainObject.configuration.objectStyles) {
|
||||||
|
domainObject.configuration.objectStyles = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return domainObject;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -30,17 +30,30 @@ export default function AnnotationsViewProvider(openmct) {
|
|||||||
name: 'Annotations',
|
name: 'Annotations',
|
||||||
canView: function (selection) {
|
canView: function (selection) {
|
||||||
const availableTags = openmct.annotation.getAvailableTags();
|
const availableTags = openmct.annotation.getAvailableTags();
|
||||||
|
const selectionContext = selection?.[0]?.[0]?.context;
|
||||||
|
const domainObject = selectionContext?.item;
|
||||||
|
const isLayoutItem = selectionContext?.layoutItem;
|
||||||
|
|
||||||
if (availableTags.length < 1) {
|
if (availableTags.length < 1 || isLayoutItem || !domainObject || openmct.editor.isEditing()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return selection.length;
|
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;
|
||||||
},
|
},
|
||||||
view: function (selection) {
|
view: function (selection) {
|
||||||
let _destroy = null;
|
let _destroy = null;
|
||||||
|
|
||||||
const domainObject = selection?.[0]?.[0]?.context?.item;
|
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';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
show: function (element) {
|
show: function (element) {
|
||||||
@ -64,6 +77,14 @@ export default function AnnotationsViewProvider(openmct) {
|
|||||||
_destroy = destroy;
|
_destroy = destroy;
|
||||||
},
|
},
|
||||||
priority: function () {
|
priority: function () {
|
||||||
|
if (isNotebookEntry || isImageSelection) {
|
||||||
|
return openmct.priority.HIGHEST;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isConditionSet) {
|
||||||
|
return openmct.priority.LOW;
|
||||||
|
}
|
||||||
|
|
||||||
return openmct.priority.DEFAULT;
|
return openmct.priority.DEFAULT;
|
||||||
},
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<li
|
<li
|
||||||
|
v-if="allowDrag"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
:aria-label="`${elementObject.name} Element Item`"
|
:aria-label="`${elementObject.name} Element Item`"
|
||||||
:aria-grabbed="hover"
|
:aria-grabbed="hover"
|
||||||
@ -47,6 +48,22 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -74,6 +91,9 @@ export default {
|
|||||||
},
|
},
|
||||||
allowDrop: {
|
allowDrop: {
|
||||||
type: Boolean
|
type: Boolean
|
||||||
|
},
|
||||||
|
allowDrag: {
|
||||||
|
type: Boolean
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['drop-custom', 'dragstart-custom'],
|
emits: ['drop-custom', 'dragstart-custom'],
|
||||||
|
@ -39,6 +39,7 @@
|
|||||||
:key="element.identifier.key"
|
:key="element.identifier.key"
|
||||||
:index="index"
|
:index="index"
|
||||||
:element-object="element"
|
:element-object="element"
|
||||||
|
:allow-drag="isEditing"
|
||||||
:allow-drop="allowDrop"
|
:allow-drop="allowDrop"
|
||||||
@dragstart-custom="moveFrom(index)"
|
@dragstart-custom="moveFrom(index)"
|
||||||
@drop-custom="moveTo(index)"
|
@drop-custom="moveTo(index)"
|
||||||
|
@ -31,8 +31,9 @@ export default function ElementsViewProvider(openmct) {
|
|||||||
canView: function (selection) {
|
canView: function (selection) {
|
||||||
const hasValidSelection = selection?.length;
|
const hasValidSelection = selection?.length;
|
||||||
const isOverlayPlot = selection?.[0]?.[0]?.context?.item?.type === 'telemetry.plot.overlay';
|
const isOverlayPlot = selection?.[0]?.[0]?.context?.item?.type === 'telemetry.plot.overlay';
|
||||||
|
const isFolder = selection?.[0]?.[0]?.context?.item?.type === 'folder';
|
||||||
|
|
||||||
return hasValidSelection && !isOverlayPlot;
|
return hasValidSelection && !isOverlayPlot && !isFolder;
|
||||||
},
|
},
|
||||||
view: function (selection) {
|
view: function (selection) {
|
||||||
let _destroy = null;
|
let _destroy = null;
|
||||||
@ -62,10 +63,10 @@ export default function ElementsViewProvider(openmct) {
|
|||||||
showTab: function (isEditing) {
|
showTab: function (isEditing) {
|
||||||
const hasComposition = Boolean(domainObject && openmct.composition.get(domainObject));
|
const hasComposition = Boolean(domainObject && openmct.composition.get(domainObject));
|
||||||
|
|
||||||
return hasComposition && isEditing;
|
return hasComposition;
|
||||||
},
|
},
|
||||||
priority: function () {
|
priority: function () {
|
||||||
return openmct.priority.DEFAULT;
|
return openmct.editor.isEditing() ? openmct.priority.DEFAULT : openmct.priority.LOW;
|
||||||
},
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
if (_destroy) {
|
if (_destroy) {
|
||||||
|
@ -30,7 +30,9 @@ export default function PropertiesViewProvider(openmct) {
|
|||||||
name: 'Properties',
|
name: 'Properties',
|
||||||
glyph: 'icon-info',
|
glyph: 'icon-info',
|
||||||
canView: function (selection) {
|
canView: function (selection) {
|
||||||
return selection.length > 0;
|
const domainObject = selection?.[0]?.[0]?.context?.item;
|
||||||
|
|
||||||
|
return domainObject && selection.length > 0;
|
||||||
},
|
},
|
||||||
view: function (selection) {
|
view: function (selection) {
|
||||||
let _destroy = null;
|
let _destroy = null;
|
||||||
@ -56,7 +58,7 @@ export default function PropertiesViewProvider(openmct) {
|
|||||||
_destroy = destroy;
|
_destroy = destroy;
|
||||||
},
|
},
|
||||||
priority: function () {
|
priority: function () {
|
||||||
return openmct.priority.DEFAULT;
|
return openmct.editor.isEditing() ? openmct.priority.LOW : openmct.priority.HIGH;
|
||||||
},
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
if (_destroy) {
|
if (_destroy) {
|
||||||
|
@ -25,7 +25,24 @@ import mount from 'utils/mount';
|
|||||||
import StylesInspectorView from './StylesInspectorView.vue';
|
import StylesInspectorView from './StylesInspectorView.vue';
|
||||||
import stylesManager from './StylesManager.js';
|
import stylesManager from './StylesManager.js';
|
||||||
|
|
||||||
const NON_STYLABLE_TYPES = ['folder', 'webPage', 'conditionSet', 'summary-widget', 'hyperlink'];
|
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'
|
||||||
|
];
|
||||||
|
|
||||||
function isLayoutObject(selection, objectType) {
|
function isLayoutObject(selection, objectType) {
|
||||||
//we allow conditionSets to be styled if they're part of a layout
|
//we allow conditionSets to be styled if they're part of a layout
|
||||||
@ -35,8 +52,8 @@ function isLayoutObject(selection, objectType) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCreatableObject(object, type) {
|
function isCreatableObject(object, typeObject) {
|
||||||
return NON_STYLABLE_TYPES.indexOf(object.type) < 0 && type.definition.creatable;
|
return NON_STYLABLE_TYPES.indexOf(object.type) < 0 && typeObject.definition.creatable;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function StylesInspectorViewProvider(openmct) {
|
export default function StylesInspectorViewProvider(openmct) {
|
||||||
@ -47,12 +64,13 @@ export default function StylesInspectorViewProvider(openmct) {
|
|||||||
canView: function (selection) {
|
canView: function (selection) {
|
||||||
const objectSelection = selection?.[0];
|
const objectSelection = selection?.[0];
|
||||||
const objectContext = objectSelection?.[0]?.context;
|
const objectContext = objectSelection?.[0]?.context;
|
||||||
const layoutItem = objectContext?.layoutItem;
|
|
||||||
const domainObject = objectContext?.item;
|
const domainObject = objectContext?.item;
|
||||||
|
const hasStyles = domainObject?.configuration?.objectStyles;
|
||||||
const isFlexibleLayoutContainer =
|
const isFlexibleLayoutContainer =
|
||||||
domainObject?.type === 'flexible-layout' && objectContext.type === 'container';
|
domainObject?.type === 'flexible-layout' && objectContext.type === 'container';
|
||||||
|
const isLayoutItem = objectContext?.layoutItem;
|
||||||
|
|
||||||
if (layoutItem) {
|
if ((isLayoutItem || hasStyles) && !isFlexibleLayoutContainer) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,10 +78,11 @@ export default function StylesInspectorViewProvider(openmct) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const type = openmct.types.get(domainObject.type);
|
const typeObject = openmct.types.get(domainObject.type);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isLayoutObject(objectSelection, domainObject.type) || isCreatableObject(domainObject, type)
|
isLayoutObject(objectSelection, domainObject.type) ||
|
||||||
|
isCreatableObject(domainObject, typeObject)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
view: function (selection) {
|
view: function (selection) {
|
||||||
@ -91,8 +110,11 @@ export default function StylesInspectorViewProvider(openmct) {
|
|||||||
);
|
);
|
||||||
_destroy = destroy;
|
_destroy = destroy;
|
||||||
},
|
},
|
||||||
|
showTab: function (isEditing) {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
priority: function () {
|
priority: function () {
|
||||||
return openmct.priority.DEFAULT;
|
return openmct.editor.isEditing() ? openmct.priority.DEFAULT : openmct.priority.LOW;
|
||||||
},
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
if (_destroy) {
|
if (_destroy) {
|
||||||
|
@ -38,6 +38,7 @@ export default function MissingObjectInterceptor(openmct) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return object;
|
return object;
|
||||||
}
|
},
|
||||||
|
priority: openmct.priority.HIGH
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ function myItemsInterceptor({ openmct, identifierObject, name }) {
|
|||||||
|
|
||||||
return object;
|
return object;
|
||||||
},
|
},
|
||||||
priority: openmct.priority.HIGH
|
priority: openmct.priority.HIGHEST
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -344,12 +344,19 @@ export default {
|
|||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
this.marked = new Marked();
|
this.marked = new Marked();
|
||||||
this.renderer = new this.marked.Renderer();
|
this.marked.use({
|
||||||
|
breaks: true,
|
||||||
|
extensions: [
|
||||||
|
{
|
||||||
|
name: 'link',
|
||||||
|
renderer: (options) => {
|
||||||
|
return this.validateLink(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const originalLinkRenderer = this.renderer.link;
|
|
||||||
this.renderer.link = this.validateLink.bind(this, originalLinkRenderer);
|
|
||||||
|
|
||||||
this.manageEmbedLayout = _.debounce(this.manageEmbedLayout, 400);
|
this.manageEmbedLayout = _.debounce(this.manageEmbedLayout, 400);
|
||||||
|
|
||||||
if (this.$refs.embedsWrapper) {
|
if (this.$refs.embedsWrapper) {
|
||||||
@ -437,10 +444,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
convertMarkDownToHtml(text = '') {
|
convertMarkDownToHtml(text = '') {
|
||||||
let markDownHtml = this.marked.parse(text, {
|
let markDownHtml = this.marked.parse(text);
|
||||||
breaks: true,
|
|
||||||
renderer: this.renderer
|
|
||||||
});
|
|
||||||
markDownHtml = sanitizeHtml(markDownHtml, SANITIZATION_SCHEMA);
|
markDownHtml = sanitizeHtml(markDownHtml, SANITIZATION_SCHEMA);
|
||||||
return markDownHtml;
|
return markDownHtml;
|
||||||
},
|
},
|
||||||
@ -451,21 +455,19 @@ export default {
|
|||||||
this.$refs.entryInput.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
this.$refs.entryInput.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
validateLink(originalLinkRenderer, href, title, text) {
|
validateLink(options) {
|
||||||
|
const { href, text } = options;
|
||||||
try {
|
try {
|
||||||
const domain = new URL(href).hostname;
|
const domain = new URL(href).hostname;
|
||||||
const urlIsWhitelisted = this.urlWhitelist.some((partialDomain) => {
|
const urlIsWhitelisted = this.urlWhitelist.some((partialDomain) => {
|
||||||
return domain.endsWith(partialDomain);
|
return domain.endsWith(partialDomain);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!urlIsWhitelisted) {
|
if (!urlIsWhitelisted) {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
const linkHtml = originalLinkRenderer.call(this.renderer, href, title, text);
|
|
||||||
const linkHtmlWithTarget = linkHtml.replace(
|
return `<a class="c-hyperlink" target="_blank" href="${href}">${text}</a>`;
|
||||||
/^<a /,
|
|
||||||
'<a class="c-hyperlink" target="_blank"'
|
|
||||||
);
|
|
||||||
return linkHtmlWithTarget;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// had error parsing this URL, just return the text
|
// had error parsing this URL, just return the text
|
||||||
return text;
|
return text;
|
||||||
|
@ -434,16 +434,69 @@ class CouchObjectProvider {
|
|||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getObjectsByView({ designDoc, viewName, keysToSearch }, abortSignal) {
|
async isViewDefined(designDoc, viewName) {
|
||||||
const stringifiedKeys = JSON.stringify(keysToSearch);
|
const url = `${this.url}/_design/${designDoc}/_view/${viewName}`;
|
||||||
const url = `${this.url}/_design/${designDoc}/_view/${viewName}?keys=${stringifiedKeys}&include_docs=true`;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
let objectModels = [];
|
let objectModels = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
signal: abortSignal
|
signal: abortSignal,
|
||||||
|
body: requestBodyString
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@ -454,13 +507,21 @@ class CouchObjectProvider {
|
|||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
const couchRows = result.rows;
|
const couchRows = result.rows;
|
||||||
couchRows.forEach((couchRow) => {
|
if (objectIdField !== undefined) {
|
||||||
const couchDoc = couchRow.doc;
|
const objectIdsToResolve = [];
|
||||||
const objectModel = this.#getModel(couchDoc);
|
couchRows.forEach((couchRow) => {
|
||||||
if (objectModel) {
|
objectIdsToResolve.push(couchRow[objectIdField]);
|
||||||
objectModels.push(objectModel);
|
});
|
||||||
}
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,11 @@ class CouchSearchProvider {
|
|||||||
#bulkPromise;
|
#bulkPromise;
|
||||||
#batchIds;
|
#batchIds;
|
||||||
#lastAbortSignal;
|
#lastAbortSignal;
|
||||||
|
#isSearchByNameViewDefined;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('./CouchObjectProvider').default} couchObjectProvider
|
||||||
|
*/
|
||||||
constructor(couchObjectProvider) {
|
constructor(couchObjectProvider) {
|
||||||
this.couchObjectProvider = couchObjectProvider;
|
this.couchObjectProvider = couchObjectProvider;
|
||||||
this.searchTypes = couchObjectProvider.openmct.objects.SEARCH_TYPES;
|
this.searchTypes = couchObjectProvider.openmct.objects.SEARCH_TYPES;
|
||||||
@ -67,18 +71,47 @@ class CouchSearchProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
searchForObjects(query, abortSignal) {
|
#isOptimizedSearchByNameSupported() {
|
||||||
const filter = {
|
let isOptimizedSearchAvailable;
|
||||||
selector: {
|
|
||||||
model: {
|
if (this.#isSearchByNameViewDefined === undefined) {
|
||||||
name: {
|
isOptimizedSearchAvailable = this.#isSearchByNameViewDefined =
|
||||||
$regex: `(?i)${query}`
|
this.couchObjectProvider.isViewDefined('object_names', 'object_names');
|
||||||
|
} else {
|
||||||
|
isOptimizedSearchAvailable = this.#isSearchByNameViewDefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isOptimizedSearchAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchForObjects(query, abortSignal) {
|
||||||
|
const preparedQuery = query.toLowerCase().trim();
|
||||||
|
const supportsOptimizedSearchByName = await this.#isOptimizedSearchByNameSupported();
|
||||||
|
|
||||||
|
if (supportsOptimizedSearchByName) {
|
||||||
|
return this.couchObjectProvider.getObjectsByView(
|
||||||
|
{
|
||||||
|
designDoc: 'object_names',
|
||||||
|
viewName: 'object_names',
|
||||||
|
startKey: preparedQuery,
|
||||||
|
endKey: preparedQuery + `\ufff0`,
|
||||||
|
objectIdField: 'value',
|
||||||
|
limit: 1000
|
||||||
|
},
|
||||||
|
abortSignal
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const filter = {
|
||||||
|
selector: {
|
||||||
|
model: {
|
||||||
|
name: {
|
||||||
|
$regex: `(?i)${query}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
return this.couchObjectProvider.getObjectsByFilter(filter);
|
||||||
|
}
|
||||||
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async #deferBatchAnnotationSearch() {
|
async #deferBatchAnnotationSearch() {
|
||||||
|
@ -373,44 +373,6 @@ describe('the plugin', () => {
|
|||||||
expect(requestMethod).toEqual('PUT');
|
expect(requestMethod).toEqual('PUT');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('implements server-side search', () => {
|
|
||||||
let mockPromise;
|
|
||||||
beforeEach(() => {
|
|
||||||
mockPromise = Promise.resolve({
|
|
||||||
body: {
|
|
||||||
getReader() {
|
|
||||||
return {
|
|
||||||
read() {
|
|
||||||
return Promise.resolve({
|
|
||||||
done: true,
|
|
||||||
value: undefined
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
fetch.and.returnValue(mockPromise);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("using Couch's 'find' endpoint", async () => {
|
|
||||||
await Promise.all(openmct.objects.search('test'));
|
|
||||||
const requestUrl = fetch.calls.mostRecent().args[0];
|
|
||||||
|
|
||||||
// we only want one call to fetch, not 2!
|
|
||||||
// see https://github.com/nasa/openmct/issues/4667
|
|
||||||
expect(fetch).toHaveBeenCalledTimes(1);
|
|
||||||
expect(requestUrl.endsWith('_find')).toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('and supports search by object name', async () => {
|
|
||||||
await Promise.all(openmct.objects.search('test'));
|
|
||||||
const requestPayload = JSON.parse(fetch.calls.mostRecent().args[1].body);
|
|
||||||
|
|
||||||
expect(requestPayload).toBeDefined();
|
|
||||||
expect(requestPayload.selector.model.name.$regex).toEqual('(?i)test');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('the view', () => {
|
describe('the view', () => {
|
||||||
|
@ -99,6 +99,48 @@ create_replicator_table() {
|
|||||||
add_index_and_views() {
|
add_index_and_views() {
|
||||||
echo "Adding index and views to $OPENMCT_DATABASE_NAME database"
|
echo "Adding index and views to $OPENMCT_DATABASE_NAME database"
|
||||||
|
|
||||||
|
# Add object names search index
|
||||||
|
response=$(curl --silent --user "${CURL_USERPASS_ARG}" --request PUT "$COUCH_BASE_LOCAL"/"$OPENMCT_DATABASE_NAME"/_design/object_names/\
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"_id":"_design/object_names",
|
||||||
|
"views":{
|
||||||
|
"object_names":{
|
||||||
|
"map":"function(doc) { if (doc.model && doc.model.name) { const name = doc.model.name.toLowerCase().trim(); if (name.length > 0) { emit(name, doc._id); const tokens = name.split(/[^a-zA-Z0-9]/); tokens.forEach((token) => { if (token.length > 0) { emit(token, doc._id); } }); } } }"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}');
|
||||||
|
|
||||||
|
if [[ $response =~ "\"ok\":true" ]]; then
|
||||||
|
echo "Successfully created object_names"
|
||||||
|
elif [[ $response =~ "\"error\":\"conflict\"" ]]; then
|
||||||
|
echo "object_names already exists, skipping creation"
|
||||||
|
else
|
||||||
|
echo "Unable to create object_names"
|
||||||
|
echo $response
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add object types search index
|
||||||
|
response=$(curl --silent --user "${CURL_USERPASS_ARG}" --request PUT "$COUCH_BASE_LOCAL"/"$OPENMCT_DATABASE_NAME"/_design/object_types/\
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"_id":"_design/object_types",
|
||||||
|
"views":{
|
||||||
|
"object_types":{
|
||||||
|
"map":"function(doc) { if (doc.model && doc.model.type) { const type = doc.model.type.toLowerCase().trim(); if (type.length > 0) { emit(type, null); } } }"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}')
|
||||||
|
|
||||||
|
if [[ $response =~ "\"ok\":true" ]]; then
|
||||||
|
echo "Successfully created object_types"
|
||||||
|
elif [[ $response =~ "\"error\":\"conflict\"" ]]; then
|
||||||
|
echo "object_types already exists, skipping creation"
|
||||||
|
else
|
||||||
|
echo "Unable to create object_types"
|
||||||
|
echo $response
|
||||||
|
fi
|
||||||
|
|
||||||
# Add type_tags_index
|
# Add type_tags_index
|
||||||
response=$(curl --silent --user "${CURL_USERPASS_ARG}" --request POST "$COUCH_BASE_LOCAL"/"$OPENMCT_DATABASE_NAME"/_index/\
|
response=$(curl --silent --user "${CURL_USERPASS_ARG}" --request POST "$COUCH_BASE_LOCAL"/"$OPENMCT_DATABASE_NAME"/_index/\
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
|
@ -62,7 +62,7 @@ export default function ActivityInspectorViewProvider(openmct) {
|
|||||||
_destroy = destroy;
|
_destroy = destroy;
|
||||||
},
|
},
|
||||||
priority: function () {
|
priority: function () {
|
||||||
return openmct.priority.HIGH + 1;
|
return openmct.priority.HIGH;
|
||||||
},
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
if (_destroy) {
|
if (_destroy) {
|
||||||
|
@ -62,7 +62,7 @@ export default function GanttChartInspectorViewProvider(openmct) {
|
|||||||
_destroy = destroy;
|
_destroy = destroy;
|
||||||
},
|
},
|
||||||
priority: function () {
|
priority: function () {
|
||||||
return openmct.priority.HIGH + 1;
|
return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;
|
||||||
},
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
if (_destroy) {
|
if (_destroy) {
|
||||||
|
@ -202,13 +202,17 @@ export default {
|
|||||||
this.imageExporter = null;
|
this.imageExporter = null;
|
||||||
this.stopListening();
|
this.stopListening();
|
||||||
},
|
},
|
||||||
exportJPG() {
|
exportJPG(filename) {
|
||||||
const plotElement = this.$refs.plotContainer;
|
const plotElement = this.$refs.plotContainer;
|
||||||
this.imageExporter.exportJPG(plotElement, 'plot.jpg', 'export-plot');
|
filename = filename ?? `${this.domainObject.name} - plot`;
|
||||||
|
|
||||||
|
this.imageExporter.exportJPG(plotElement, filename, 'export-plot');
|
||||||
},
|
},
|
||||||
exportPNG() {
|
exportPNG(filename) {
|
||||||
const plotElement = this.$refs.plotContainer;
|
const plotElement = this.$refs.plotContainer;
|
||||||
this.imageExporter.exportPNG(plotElement, 'plot.png', 'export-plot');
|
filename = filename ?? `${this.domainObject.name} - plot`;
|
||||||
|
|
||||||
|
this.imageExporter.exportPNG(plotElement, filename, 'export-plot');
|
||||||
},
|
},
|
||||||
setStatus(status) {
|
setStatus(status) {
|
||||||
this.status = status;
|
this.status = status;
|
||||||
|
@ -25,27 +25,6 @@ import mount from 'utils/mount';
|
|||||||
import Plot from './PlotView.vue';
|
import Plot from './PlotView.vue';
|
||||||
|
|
||||||
export default function PlotViewProvider(openmct) {
|
export default function PlotViewProvider(openmct) {
|
||||||
function hasNumericTelemetry(domainObject) {
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let metadata = openmct.telemetry.getMetadata(domainObject);
|
|
||||||
|
|
||||||
return metadata.values().length > 0 && hasDomainAndNumericRange(metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasDomainAndNumericRange(metadata) {
|
|
||||||
const rangeValues = metadata.valuesForHints(['range']);
|
|
||||||
const domains = metadata.valuesForHints(['domain']);
|
|
||||||
|
|
||||||
return (
|
|
||||||
domains.length > 0 &&
|
|
||||||
rangeValues.length > 0 &&
|
|
||||||
!rangeValues.every((value) => value.format === 'string')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCompactView(objectPath) {
|
function isCompactView(objectPath) {
|
||||||
let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip');
|
let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip');
|
||||||
|
|
||||||
@ -57,7 +36,7 @@ export default function PlotViewProvider(openmct) {
|
|||||||
name: 'Plot',
|
name: 'Plot',
|
||||||
cssClass: 'icon-telemetry',
|
cssClass: 'icon-telemetry',
|
||||||
canView(domainObject, objectPath) {
|
canView(domainObject, objectPath) {
|
||||||
return hasNumericTelemetry(domainObject);
|
return openmct.telemetry.hasNumericTelemetry(domainObject);
|
||||||
},
|
},
|
||||||
|
|
||||||
view: function (domainObject, objectPath) {
|
view: function (domainObject, objectPath) {
|
||||||
|
@ -27,8 +27,8 @@ const exportPNG = {
|
|||||||
description: "Export This View's Data as PNG",
|
description: "Export This View's Data as PNG",
|
||||||
cssClass: 'icon-download',
|
cssClass: 'icon-download',
|
||||||
group: 'view',
|
group: 'view',
|
||||||
invoke(objectPath, view) {
|
invoke(objectPath, view, filename) {
|
||||||
view.getViewContext().exportPNG();
|
view.getViewContext().exportPNG(filename);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -38,8 +38,8 @@ const exportJPG = {
|
|||||||
description: "Export This View's Data as JPG",
|
description: "Export This View's Data as JPG",
|
||||||
cssClass: 'icon-download',
|
cssClass: 'icon-download',
|
||||||
group: 'view',
|
group: 'view',
|
||||||
invoke(objectPath, view) {
|
invoke(objectPath, view, filename) {
|
||||||
view.getViewContext().exportJPG();
|
view.getViewContext().exportJPG(filename);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ export default function PlotsInspectorViewProvider(openmct) {
|
|||||||
_destroy = destroy;
|
_destroy = destroy;
|
||||||
},
|
},
|
||||||
priority: function () {
|
priority: function () {
|
||||||
return openmct.priority.HIGH + 1;
|
return openmct.priority.DEFAULT;
|
||||||
},
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
if (_destroy) {
|
if (_destroy) {
|
||||||
|
@ -50,7 +50,7 @@ export default function StackedPlotsInspectorViewProvider(openmct) {
|
|||||||
_destroy = destroy;
|
_destroy = destroy;
|
||||||
},
|
},
|
||||||
priority: function () {
|
priority: function () {
|
||||||
return openmct.priority.HIGH + 1;
|
return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;
|
||||||
},
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
if (_destroy) {
|
if (_destroy) {
|
||||||
|
@ -1,25 +1,10 @@
|
|||||||
export default function OverlayPlotCompositionPolicy(openmct) {
|
export default function OverlayPlotCompositionPolicy(openmct) {
|
||||||
function hasNumericTelemetry(domainObject) {
|
|
||||||
const hasTelemetry = openmct.telemetry.isTelemetryObject(domainObject);
|
|
||||||
if (!hasTelemetry) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let 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 {
|
return {
|
||||||
allow: function (parent, child) {
|
allow: function (parent, child) {
|
||||||
if (parent.type === 'telemetry.plot.overlay' && hasNumericTelemetry(child) === false) {
|
if (
|
||||||
|
parent.type === 'telemetry.plot.overlay' &&
|
||||||
|
!openmct.telemetry.hasNumericTelemetry(child)
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
41
src/plugins/plot/overlayPlot/overlayPlotStylesInterceptor.js
Normal file
41
src/plugins/plot/overlayPlot/overlayPlotStylesInterceptor.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 overlayPlotStylesInterceptor(openmct) {
|
||||||
|
return {
|
||||||
|
appliesTo: (identifier, domainObject) => {
|
||||||
|
return (
|
||||||
|
domainObject?.type === 'telemetry.plot.overlay' &&
|
||||||
|
!domainObject?.configuration?.objectStyles
|
||||||
|
);
|
||||||
|
},
|
||||||
|
invoke: (identifier, domainObject) => {
|
||||||
|
if (!domainObject.configuration) {
|
||||||
|
domainObject.configuration = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
domainObject.configuration.objectStyles = {};
|
||||||
|
|
||||||
|
return domainObject;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -23,6 +23,7 @@ import PlotViewActions from './actions/ViewActions.js';
|
|||||||
import PlotsInspectorViewProvider from './inspector/PlotsInspectorViewProvider.js';
|
import PlotsInspectorViewProvider from './inspector/PlotsInspectorViewProvider.js';
|
||||||
import StackedPlotsInspectorViewProvider from './inspector/StackedPlotsInspectorViewProvider.js';
|
import StackedPlotsInspectorViewProvider from './inspector/StackedPlotsInspectorViewProvider.js';
|
||||||
import OverlayPlotCompositionPolicy from './overlayPlot/OverlayPlotCompositionPolicy.js';
|
import OverlayPlotCompositionPolicy from './overlayPlot/OverlayPlotCompositionPolicy.js';
|
||||||
|
import overlayPlotStylesInterceptor from './overlayPlot/overlayPlotStylesInterceptor.js';
|
||||||
import OverlayPlotViewProvider from './overlayPlot/OverlayPlotViewProvider.js';
|
import OverlayPlotViewProvider from './overlayPlot/OverlayPlotViewProvider.js';
|
||||||
import PlotViewProvider from './PlotViewProvider.js';
|
import PlotViewProvider from './PlotViewProvider.js';
|
||||||
import StackedPlotCompositionPolicy from './stackedPlot/StackedPlotCompositionPolicy.js';
|
import StackedPlotCompositionPolicy from './stackedPlot/StackedPlotCompositionPolicy.js';
|
||||||
@ -38,16 +39,20 @@ export default function () {
|
|||||||
description:
|
description:
|
||||||
'Combine multiple telemetry elements and view them together as a plot with common X and Y axes. Can be added to Display Layouts.',
|
'Combine multiple telemetry elements and view them together as a plot with common X and Y axes. Can be added to Display Layouts.',
|
||||||
creatable: true,
|
creatable: true,
|
||||||
|
annotatable: true,
|
||||||
initialize: function (domainObject) {
|
initialize: function (domainObject) {
|
||||||
domainObject.composition = [];
|
domainObject.composition = [];
|
||||||
domainObject.configuration = {
|
domainObject.configuration = {
|
||||||
//series is an array of objects of type: {identifier, series: {color...}, yAxis:{}}
|
//series is an array of objects of type: {identifier, series: {color...}, yAxis:{}}
|
||||||
series: []
|
series: [],
|
||||||
|
objectStyles: {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
priority: 891
|
priority: 891
|
||||||
});
|
});
|
||||||
|
|
||||||
|
openmct.objects.addGetInterceptor(overlayPlotStylesInterceptor(openmct));
|
||||||
|
|
||||||
openmct.types.addType('telemetry.plot.stacked', {
|
openmct.types.addType('telemetry.plot.stacked', {
|
||||||
key: 'telemetry.plot.stacked',
|
key: 'telemetry.plot.stacked',
|
||||||
name: 'Stacked Plot',
|
name: 'Stacked Plot',
|
||||||
@ -55,12 +60,14 @@ export default function () {
|
|||||||
description:
|
description:
|
||||||
'Combine multiple telemetry elements and view them together as a plot with a common X axis and individual Y axes. Can be added to Display Layouts.',
|
'Combine multiple telemetry elements and view them together as a plot with a common X axis and individual Y axes. Can be added to Display Layouts.',
|
||||||
creatable: true,
|
creatable: true,
|
||||||
|
annotatable: true,
|
||||||
initialize: function (domainObject) {
|
initialize: function (domainObject) {
|
||||||
domainObject.composition = [];
|
domainObject.composition = [];
|
||||||
domainObject.configuration = {
|
domainObject.configuration = {
|
||||||
series: [],
|
series: [],
|
||||||
yAxis: {},
|
yAxis: {},
|
||||||
xAxis: {}
|
xAxis: {},
|
||||||
|
objectStyles: {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
priority: 890
|
priority: 890
|
||||||
|
@ -271,23 +271,23 @@ export default {
|
|||||||
this.compositionObjects = [];
|
this.compositionObjects = [];
|
||||||
},
|
},
|
||||||
|
|
||||||
exportJPG() {
|
exportJPG(filename) {
|
||||||
this.hideExportButtons = true;
|
this.hideExportButtons = true;
|
||||||
const plotElement = this.$el;
|
const plotElement = this.$el;
|
||||||
|
filename = filename ?? `${this.domainObject.name} - stacked-plot`;
|
||||||
|
|
||||||
this.imageExporter.exportJPG(plotElement, 'stacked-plot.jpg', 'export-plot').finally(
|
this.imageExporter.exportJPG(plotElement, filename, 'export-plot').finally(
|
||||||
function () {
|
function () {
|
||||||
this.hideExportButtons = false;
|
this.hideExportButtons = false;
|
||||||
}.bind(this)
|
}.bind(this)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
exportPNG() {
|
exportPNG(filename) {
|
||||||
this.hideExportButtons = true;
|
this.hideExportButtons = true;
|
||||||
|
|
||||||
const plotElement = this.$el;
|
const plotElement = this.$el;
|
||||||
|
filename = filename ?? `${this.domainObject.name} - stacked-plot`;
|
||||||
this.imageExporter.exportPNG(plotElement, 'stacked-plot.png', 'export-plot').finally(
|
this.imageExporter.exportPNG(plotElement, filename, 'export-plot').finally(
|
||||||
function () {
|
function () {
|
||||||
this.hideExportButtons = false;
|
this.hideExportButtons = false;
|
||||||
}.bind(this)
|
}.bind(this)
|
||||||
|
@ -23,14 +23,25 @@
|
|||||||
export default function stackedPlotConfigurationInterceptor(openmct) {
|
export default function stackedPlotConfigurationInterceptor(openmct) {
|
||||||
openmct.objects.addGetInterceptor({
|
openmct.objects.addGetInterceptor({
|
||||||
appliesTo: (identifier, domainObject) => {
|
appliesTo: (identifier, domainObject) => {
|
||||||
return domainObject && domainObject.type === 'telemetry.plot.stacked';
|
return (
|
||||||
|
domainObject?.type === 'telemetry.plot.stacked' &&
|
||||||
|
(!domainObject.configuration?.series || !domainObject.configuration?.objectStyles)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
invoke: (identifier, object) => {
|
invoke: (identifier, domainObject) => {
|
||||||
if (object && object.configuration && object.configuration.series === undefined) {
|
if (!domainObject.configuration) {
|
||||||
object.configuration.series = [];
|
domainObject.configuration = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return object;
|
if (!domainObject.configuration.series) {
|
||||||
|
domainObject.configuration.series = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!domainObject.configuration.objectStyles) {
|
||||||
|
domainObject.configuration.objectStyles = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return domainObject;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -66,10 +66,10 @@ export default function TableConfigurationViewProvider(openmct, options) {
|
|||||||
_destroy = destroy;
|
_destroy = destroy;
|
||||||
},
|
},
|
||||||
showTab: function (isEditing) {
|
showTab: function (isEditing) {
|
||||||
return isEditing;
|
return true;
|
||||||
},
|
},
|
||||||
priority: function () {
|
priority: function () {
|
||||||
return 1;
|
return openmct.editor.isEditing() ? openmct.priority.HIGH : openmct.priority.DEFAULT;
|
||||||
},
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
if (_destroy) {
|
if (_destroy) {
|
||||||
|
@ -71,7 +71,8 @@ export default function getTelemetryTableType(options) {
|
|||||||
hiddenColumns: {},
|
hiddenColumns: {},
|
||||||
telemetryMode,
|
telemetryMode,
|
||||||
persistModeChange,
|
persistModeChange,
|
||||||
rowLimit
|
rowLimit,
|
||||||
|
objectStyles: {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -21,53 +21,57 @@
|
|||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="c-inspect-properties">
|
<div class="c-inspect-properties">
|
||||||
<template v-if="isEditing">
|
<div class="c-inspect-properties__header">Layout</div>
|
||||||
<div class="c-inspect-properties__header">Layout</div>
|
<ul class="c-inspect-properties__section">
|
||||||
<ul class="c-inspect-properties__section">
|
<li class="c-inspect-properties__row">
|
||||||
<li class="c-inspect-properties__row">
|
<div class="c-inspect-properties__label" title="Auto-size table">
|
||||||
<div class="c-inspect-properties__label" title="Auto-size table">
|
<label for="AutoSizeControl">Auto-size</label>
|
||||||
<label for="AutoSizeControl">Auto-size</label>
|
</div>
|
||||||
</div>
|
<div class="c-inspect-properties__value">
|
||||||
<div class="c-inspect-properties__value">
|
<input
|
||||||
<input
|
v-if="isEditing"
|
||||||
id="AutoSizeControl"
|
id="AutoSizeControl"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="configuration.autosize !== false"
|
:checked="configuration.autosize !== false"
|
||||||
@change="toggleAutosize()"
|
@change="toggleAutosize()"
|
||||||
/>
|
/>
|
||||||
</div>
|
<span v-if="!isEditing && configuration.autosize !== false">Enabled</span>
|
||||||
</li>
|
</div>
|
||||||
<li class="c-inspect-properties__row">
|
</li>
|
||||||
<div class="c-inspect-properties__label" title="Show or hide headers">
|
<li class="c-inspect-properties__row">
|
||||||
<label for="header-visibility">Hide Header</label>
|
<div class="c-inspect-properties__label" title="Show or hide headers">
|
||||||
</div>
|
<label for="header-visibility">Hide Header</label>
|
||||||
<div class="c-inspect-properties__value">
|
</div>
|
||||||
<input
|
<div class="c-inspect-properties__value">
|
||||||
id="header-visibility"
|
<input
|
||||||
type="checkbox"
|
v-if="isEditing"
|
||||||
:checked="configuration.hideHeaders === true"
|
id="header-visibility"
|
||||||
@change="toggleHeaderVisibility"
|
type="checkbox"
|
||||||
/>
|
:checked="configuration.hideHeaders === true"
|
||||||
</div>
|
@change="toggleHeaderVisibility"
|
||||||
</li>
|
/>
|
||||||
</ul>
|
<span v-if="!isEditing && configuration.hideHeaders === true">True</span>
|
||||||
<div class="c-inspect-properties__header">Columns</div>
|
</div>
|
||||||
<ul class="c-inspect-properties__section">
|
</li>
|
||||||
<li v-for="(title, key) in headers" :key="key" class="c-inspect-properties__row">
|
</ul>
|
||||||
<div class="c-inspect-properties__label" title="Show or hide column">
|
<div class="c-inspect-properties__header">Columns</div>
|
||||||
<label :for="key + 'ColumnControl'">{{ title }}</label>
|
<ul class="c-inspect-properties__section">
|
||||||
</div>
|
<li v-for="(title, key) in headers" :key="key" class="c-inspect-properties__row">
|
||||||
<div class="c-inspect-properties__value">
|
<div class="c-inspect-properties__label" title="Show or hide column">
|
||||||
<input
|
<label :for="key + 'ColumnControl'">{{ title }}</label>
|
||||||
:id="key + 'ColumnControl'"
|
</div>
|
||||||
type="checkbox"
|
<div class="c-inspect-properties__value">
|
||||||
:checked="configuration.hiddenColumns[key] !== true"
|
<input
|
||||||
@change="toggleColumn(key)"
|
v-if="isEditing"
|
||||||
/>
|
:id="key + 'ColumnControl'"
|
||||||
</div>
|
type="checkbox"
|
||||||
</li>
|
:checked="configuration.hiddenColumns[key] !== true"
|
||||||
</ul>
|
@change="toggleColumn(key)"
|
||||||
</template>
|
/>
|
||||||
|
<span v-if="!isEditing && configuration.hiddenColumns[key] !== true">Visible</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -112,6 +116,15 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateHeaders(headers) {
|
updateHeaders(headers) {
|
||||||
|
// add name column if it doesn't exist,
|
||||||
|
// it's always the first column when it's manually added
|
||||||
|
if (!headers.name) {
|
||||||
|
headers = {
|
||||||
|
name: 'Name',
|
||||||
|
...headers
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
this.headers = headers;
|
this.headers = headers;
|
||||||
},
|
},
|
||||||
toggleColumn(key) {
|
toggleColumn(key) {
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
import { MODE } from './constants.js';
|
import { MODE } from './constants.js';
|
||||||
import TableConfigurationViewProvider from './TableConfigurationViewProvider.js';
|
import TableConfigurationViewProvider from './TableConfigurationViewProvider.js';
|
||||||
|
import telemetryTableStylesInterceptor from './telemetryTableStylesInterceptor.js';
|
||||||
import getTelemetryTableType from './TelemetryTableType.js';
|
import getTelemetryTableType from './TelemetryTableType.js';
|
||||||
import TelemetryTableViewProvider from './TelemetryTableViewProvider.js';
|
import TelemetryTableViewProvider from './TelemetryTableViewProvider.js';
|
||||||
import TelemetryTableViewActions from './ViewActions.js';
|
import TelemetryTableViewActions from './ViewActions.js';
|
||||||
@ -33,6 +34,7 @@ export default function plugin(
|
|||||||
openmct.objectViews.addProvider(new TelemetryTableViewProvider(openmct, options));
|
openmct.objectViews.addProvider(new TelemetryTableViewProvider(openmct, options));
|
||||||
openmct.inspectorViews.addProvider(new TableConfigurationViewProvider(openmct, options));
|
openmct.inspectorViews.addProvider(new TableConfigurationViewProvider(openmct, options));
|
||||||
openmct.types.addType('table', getTelemetryTableType(options));
|
openmct.types.addType('table', getTelemetryTableType(options));
|
||||||
|
openmct.objects.addGetInterceptor(telemetryTableStylesInterceptor(openmct));
|
||||||
openmct.composition.addPolicy((parent, child) => {
|
openmct.composition.addPolicy((parent, child) => {
|
||||||
if (parent.type === 'table') {
|
if (parent.type === 'table') {
|
||||||
return Object.prototype.hasOwnProperty.call(child, 'telemetry');
|
return Object.prototype.hasOwnProperty.call(child, 'telemetry');
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 telemetryTableStylesInterceptor(openmct) {
|
||||||
|
return {
|
||||||
|
appliesTo: (identifier, domainObject) => {
|
||||||
|
return (
|
||||||
|
domainObject?.type === 'table' &&
|
||||||
|
domainObject.configuration && // only applies to tables with existing configuration
|
||||||
|
!domainObject.configuration.objectStyles
|
||||||
|
);
|
||||||
|
},
|
||||||
|
invoke: (identifier, domainObject) => {
|
||||||
|
domainObject.configuration.objectStyles = {};
|
||||||
|
|
||||||
|
return domainObject;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user