mirror of
https://github.com/nasa/openmct.git
synced 2025-03-15 00:36:33 +00:00
Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
28b5d7c41c | ||
|
ecd120387c | ||
|
a6517bb33e | ||
|
1fde0d9e38 | ||
|
5be103ea72 | ||
|
d74e1b19b6 | ||
|
5bb6a18cd4 | ||
|
14b947c101 | ||
|
61b982ab99 | ||
|
ba4d8a428b | ||
|
ea9947cab5 | ||
|
2010f2e377 | ||
|
3241e9ba57 |
@ -5,7 +5,7 @@ orbs:
|
|||||||
executors:
|
executors:
|
||||||
pw-focal-development:
|
pw-focal-development:
|
||||||
docker:
|
docker:
|
||||||
- image: mcr.microsoft.com/playwright:v1.47.2-focal
|
- image: mcr.microsoft.com/playwright:v1.48.1-focal
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
|
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
|
||||||
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
|
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
|
||||||
@ -198,7 +198,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- build_and_install:
|
- build_and_install:
|
||||||
node-version: lts/hydrogen
|
node-version: lts/hydrogen
|
||||||
- run: npx playwright@1.47.2 install #Necessary for bare ubuntu machine
|
- run: npx playwright@1.48.1 install #Necessary for bare ubuntu machine
|
||||||
- run: |
|
- run: |
|
||||||
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
|
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
|
||||||
docker compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
|
docker compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
|
||||||
|
@ -483,7 +483,8 @@
|
|||||||
"countup",
|
"countup",
|
||||||
"darkmatter",
|
"darkmatter",
|
||||||
"Undeletes",
|
"Undeletes",
|
||||||
"SSSZ"
|
"SSSZ",
|
||||||
|
"pageerror"
|
||||||
],
|
],
|
||||||
"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": [
|
||||||
|
10
.github/workflows/e2e-couchdb.yml
vendored
10
.github/workflows/e2e-couchdb.yml
vendored
@ -37,7 +37,7 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- run: npx playwright@1.47.2 install
|
- run: npx playwright@1.48.1 install
|
||||||
|
|
||||||
- name: Start CouchDB Docker Container and Init with Setup Scripts
|
- name: Start CouchDB Docker Container and Init with Setup Scripts
|
||||||
run: |
|
run: |
|
||||||
@ -66,15 +66,19 @@ jobs:
|
|||||||
|
|
||||||
- name: Archive test results
|
- name: Archive test results
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
name: e2e-couchdb-test-results
|
||||||
path: test-results
|
path: test-results
|
||||||
|
overwrite: true
|
||||||
|
|
||||||
- name: Archive html test results
|
- name: Archive html test results
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
name: e2e-couchdb-html-test-results
|
||||||
path: html-test-results
|
path: html-test-results
|
||||||
|
overwrite: true
|
||||||
|
|
||||||
- name: Remove pr:e2e:couchdb label (if present)
|
- name: Remove pr:e2e:couchdb label (if present)
|
||||||
if: always()
|
if: always()
|
||||||
|
6
.github/workflows/e2e-flakefinder.yml
vendored
6
.github/workflows/e2e-flakefinder.yml
vendored
@ -30,7 +30,7 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-node-
|
${{ runner.os }}-node-
|
||||||
|
|
||||||
- run: npx playwright@1.47.2 install
|
- run: npx playwright@1.48.1 install
|
||||||
- run: npm ci --no-audit --progress=false
|
- run: npm ci --no-audit --progress=false
|
||||||
|
|
||||||
- name: Run E2E Tests (Repeated 10 Times)
|
- name: Run E2E Tests (Repeated 10 Times)
|
||||||
@ -38,9 +38,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Archive test results
|
- name: Archive test results
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
name: e2e-flakefinder-test-results
|
||||||
path: test-results
|
path: test-results
|
||||||
|
overwrite: true
|
||||||
|
|
||||||
- name: Remove pr:e2e:flakefinder label (if present)
|
- name: Remove pr:e2e:flakefinder label (if present)
|
||||||
if: always()
|
if: always()
|
||||||
|
6
.github/workflows/e2e-perf.yml
vendored
6
.github/workflows/e2e-perf.yml
vendored
@ -28,16 +28,18 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-node-
|
${{ runner.os }}-node-
|
||||||
|
|
||||||
- run: npx playwright@1.47.2 install
|
- run: npx playwright@1.48.1 install
|
||||||
- run: npm ci --no-audit --progress=false
|
- run: npm ci --no-audit --progress=false
|
||||||
- run: npm run test:perf:localhost
|
- run: npm run test:perf:localhost
|
||||||
- run: npm run test:perf:contract
|
- run: npm run test:perf:contract
|
||||||
- run: npm run test:perf:memory
|
- run: npm run test:perf:memory
|
||||||
- name: Archive test results
|
- name: Archive test results
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
name: e2e-perf-test-results
|
||||||
path: test-results
|
path: test-results
|
||||||
|
overwrite: true
|
||||||
|
|
||||||
- name: Remove pr:e2e:perf label (if present)
|
- name: Remove pr:e2e:perf label (if present)
|
||||||
if: always()
|
if: always()
|
||||||
|
4
.github/workflows/e2e-pr.yml
vendored
4
.github/workflows/e2e-pr.yml
vendored
@ -45,9 +45,11 @@ jobs:
|
|||||||
npm run cov:e2e:full:publish
|
npm run cov:e2e:full:publish
|
||||||
- name: Archive test results
|
- name: Archive test results
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
name: e2e-pr-test-results
|
||||||
path: test-results
|
path: test-results
|
||||||
|
overwrite: true
|
||||||
|
|
||||||
- name: Remove pr:e2e label (if present)
|
- name: Remove pr:e2e label (if present)
|
||||||
if: always()
|
if: always()
|
||||||
|
116
.github/workflows/release.yml
vendored
116
.github/workflows/release.yml
vendored
@ -1,116 +0,0 @@
|
|||||||
# GitHub Actions Workflow for Automated Releases
|
|
||||||
|
|
||||||
name: Automated Release Workflow
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
# Nightly builds at 6 PM PST every day
|
|
||||||
- cron: '0 2 * * *'
|
|
||||||
release:
|
|
||||||
types:
|
|
||||||
- created
|
|
||||||
- published
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
nightly-build:
|
|
||||||
if: github.event_name == 'schedule'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Nightly Build and Release
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set Up Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 'lts/iron' # Specify your Node.js version
|
|
||||||
registry-url: 'https://registry.npmjs.org/'
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Bump Version for Nightly
|
|
||||||
id: bump_version
|
|
||||||
run: |
|
|
||||||
PACKAGE_VERSION=$(node -p "require('./package.json').version")
|
|
||||||
DATE=$(date +%Y%m%d)
|
|
||||||
NIGHTLY_VERSION=$(echo $PACKAGE_VERSION | awk -F. -v OFS=. '{$NF+=1; print}')-nightly-$DATE
|
|
||||||
echo "NIGHTLY_VERSION=${NIGHTLY_VERSION}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Update package.json
|
|
||||||
run: |
|
|
||||||
npm version $NIGHTLY_VERSION --no-git-tag-version
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git add package.json
|
|
||||||
git commit -m "chore: bump version to $NIGHTLY_VERSION for nightly build"
|
|
||||||
|
|
||||||
- name: Push Changes
|
|
||||||
uses: ad-m/github-push-action@v0.6.0
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
branch: ${{ github.ref }}
|
|
||||||
|
|
||||||
- name: Build Project
|
|
||||||
run: npm run build:prod
|
|
||||||
|
|
||||||
- name: Publish Nightly to NPM
|
|
||||||
run: |
|
|
||||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
|
||||||
npm publish --access public --tag nightly
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
|
|
||||||
prerelease-build:
|
|
||||||
if: github.event.release.prerelease == true
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Pre-release (Beta) Build and Publish
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set Up Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '16' # Specify your Node.js version
|
|
||||||
registry-url: 'https://registry.npmjs.org/'
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Build Project
|
|
||||||
run: npm run build:prod
|
|
||||||
|
|
||||||
- name: Publish Beta to NPM
|
|
||||||
run: |
|
|
||||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
|
||||||
npm publish --access public --tag beta
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
|
|
||||||
stable-release-build:
|
|
||||||
if: github.event.release.prerelease == false
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Stable Release Build and Publish
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set Up Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '16' # Specify your Node.js version
|
|
||||||
registry-url: 'https://registry.npmjs.org/'
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Build Project
|
|
||||||
run: npm run build:prod
|
|
||||||
|
|
||||||
- name: Publish to NPM
|
|
||||||
run: |
|
|
||||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
|
||||||
npm publish --access public
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@ -682,6 +682,21 @@ async function linkParameterToObject(page, parameterName, objectName) {
|
|||||||
await page.getByLabel('Save').click();
|
await page.getByLabel('Save').click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename the currently viewed `domainObject` from the browse bar.
|
||||||
|
*
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {string} newName
|
||||||
|
*/
|
||||||
|
async function renameCurrentObjectFromBrowseBar(page, newName) {
|
||||||
|
const nameInput = page.getByLabel('Browse bar object name');
|
||||||
|
await nameInput.click();
|
||||||
|
await nameInput.fill('');
|
||||||
|
await nameInput.fill(newName);
|
||||||
|
// Click the browse bar container to save changes
|
||||||
|
await page.getByLabel('Browse bar', { exact: true }).click();
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
createDomainObjectWithDefaults,
|
createDomainObjectWithDefaults,
|
||||||
createExampleTelemetryObject,
|
createExampleTelemetryObject,
|
||||||
@ -693,6 +708,7 @@ export {
|
|||||||
linkParameterToObject,
|
linkParameterToObject,
|
||||||
navigateToObjectWithFixedTimeBounds,
|
navigateToObjectWithFixedTimeBounds,
|
||||||
navigateToObjectWithRealTime,
|
navigateToObjectWithRealTime,
|
||||||
|
renameCurrentObjectFromBrowseBar,
|
||||||
setEndOffset,
|
setEndOffset,
|
||||||
setFixedIndependentTimeConductorBounds,
|
setFixedIndependentTimeConductorBounds,
|
||||||
setFixedTimeMode,
|
setFixedTimeMode,
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@percy/cli": "1.27.4",
|
"@percy/cli": "1.27.4",
|
||||||
"@percy/playwright": "1.0.4",
|
"@percy/playwright": "1.0.4",
|
||||||
"@playwright/test": "1.47.2",
|
"@playwright/test": "1.48.1",
|
||||||
"@axe-core/playwright": "4.8.5"
|
"@axe-core/playwright": "4.8.5"
|
||||||
},
|
},
|
||||||
"author": {
|
"author": {
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import { createDomainObjectWithDefaults, setRealTimeMode } from '../../../appActions.js';
|
||||||
|
import { MISSION_TIME } from '../../../constants.js';
|
||||||
|
import { expect, test } from '../../../pluginFixtures.js';
|
||||||
|
|
||||||
|
const TELEMETRY_RATE = 2500;
|
||||||
|
|
||||||
|
test.describe('Example Event Generator Acknowledge with Controlled Clock @clock', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.clock.install({ time: MISSION_TIME });
|
||||||
|
await page.clock.resume();
|
||||||
|
|
||||||
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
await setRealTimeMode(page);
|
||||||
|
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Event Message Generator with Acknowledge'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Rows are updatable in place', async ({ page }) => {
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/7938'
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('First telemetry datum gets added as new row', async () => {
|
||||||
|
await page.clock.fastForward(TELEMETRY_RATE);
|
||||||
|
const rows = page.getByLabel('table content').getByLabel('Table Row');
|
||||||
|
const acknowledgeCell = rows.first().getByLabel('acknowledge table cell');
|
||||||
|
|
||||||
|
await expect(rows).toHaveCount(1);
|
||||||
|
await expect(acknowledgeCell).not.toHaveAttribute('title', 'OK');
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Incoming Telemetry datum matching an existing rows in place update key has data merged to existing row', async () => {
|
||||||
|
await page.clock.fastForward(TELEMETRY_RATE * 2);
|
||||||
|
const rows = page.getByLabel('table content').getByLabel('Table Row');
|
||||||
|
const acknowledgeCell = rows.first().getByLabel('acknowledge table cell');
|
||||||
|
|
||||||
|
await expect(rows).toHaveCount(1);
|
||||||
|
await expect(acknowledgeCell).toHaveAttribute('title', 'OK');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -24,7 +24,9 @@ import {
|
|||||||
createDomainObjectWithDefaults,
|
createDomainObjectWithDefaults,
|
||||||
createPlanFromJSON,
|
createPlanFromJSON,
|
||||||
navigateToObjectWithFixedTimeBounds,
|
navigateToObjectWithFixedTimeBounds,
|
||||||
setFixedIndependentTimeConductorBounds
|
setFixedIndependentTimeConductorBounds,
|
||||||
|
setFixedTimeMode,
|
||||||
|
setTimeConductorBounds
|
||||||
} from '../../../appActions.js';
|
} from '../../../appActions.js';
|
||||||
import { expect, test } from '../../../pluginFixtures.js';
|
import { expect, test } from '../../../pluginFixtures.js';
|
||||||
|
|
||||||
@ -74,21 +76,14 @@ const testPlan = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
test.describe('Time Strip', () => {
|
test.describe('Time Strip', () => {
|
||||||
test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts', async ({
|
let timestrip;
|
||||||
page
|
let plan;
|
||||||
}) => {
|
|
||||||
test.info().annotations.push({
|
|
||||||
type: 'issue',
|
|
||||||
description: 'https://github.com/nasa/openmct/issues/5627'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Constant locators
|
|
||||||
const activityBounds = page.locator('.activity-bounds');
|
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
// Goto baseURL
|
// Goto baseURL
|
||||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
const timestrip = await test.step('Create a Time Strip', async () => {
|
timestrip = await test.step('Create a Time Strip', async () => {
|
||||||
const createdTimeStrip = await createDomainObjectWithDefaults(page, { type: 'Time Strip' });
|
const createdTimeStrip = await createDomainObjectWithDefaults(page, { type: 'Time Strip' });
|
||||||
const objectName = await page.locator('.l-browse-bar__object-name').innerText();
|
const objectName = await page.locator('.l-browse-bar__object-name').innerText();
|
||||||
expect(objectName).toBe(createdTimeStrip.name);
|
expect(objectName).toBe(createdTimeStrip.name);
|
||||||
@ -96,7 +91,7 @@ test.describe('Time Strip', () => {
|
|||||||
return createdTimeStrip;
|
return createdTimeStrip;
|
||||||
});
|
});
|
||||||
|
|
||||||
const plan = await test.step('Create a Plan and add it to the timestrip', async () => {
|
plan = await test.step('Create a Plan and add it to the timestrip', async () => {
|
||||||
const createdPlan = await createPlanFromJSON(page, {
|
const createdPlan = await createPlanFromJSON(page, {
|
||||||
name: 'Test Plan',
|
name: 'Test Plan',
|
||||||
json: testPlan
|
json: testPlan
|
||||||
@ -110,6 +105,22 @@ test.describe('Time Strip', () => {
|
|||||||
.dragTo(page.getByLabel('Object View'));
|
.dragTo(page.getByLabel('Object View'));
|
||||||
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();
|
||||||
|
|
||||||
|
return createdPlan;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/5627'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Constant locators
|
||||||
|
const activityBounds = page.locator('.activity-bounds');
|
||||||
|
|
||||||
|
await test.step('Set time strip to fixed timespan mode and verify activities', async () => {
|
||||||
const startBound = testPlan.TEST_GROUP[0].start;
|
const startBound = testPlan.TEST_GROUP[0].start;
|
||||||
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
|
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
|
||||||
|
|
||||||
@ -119,8 +130,6 @@ test.describe('Time Strip', () => {
|
|||||||
// Verify all events are displayed
|
// Verify all events are displayed
|
||||||
const eventCount = await page.locator('.activity-bounds').count();
|
const eventCount = await page.locator('.activity-bounds').count();
|
||||||
expect(eventCount).toEqual(testPlan.TEST_GROUP.length);
|
expect(eventCount).toEqual(testPlan.TEST_GROUP.length);
|
||||||
|
|
||||||
return createdPlan;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('TimeStrip can use the Independent Time Conductor', async () => {
|
await test.step('TimeStrip can use the Independent Time Conductor', async () => {
|
||||||
@ -177,4 +186,48 @@ test.describe('Time Strip', () => {
|
|||||||
expect(await activityBounds.count()).toEqual(1);
|
expect(await activityBounds.count()).toEqual(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Time strip now line', async ({ page }) => {
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/7817'
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Is displayed in realtime mode', async () => {
|
||||||
|
await expect(page.getByLabel('Now Marker')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Is hidden when out of bounds of the time axis', async () => {
|
||||||
|
// Switch to fixed timespan mode
|
||||||
|
await setFixedTimeMode(page);
|
||||||
|
// Get the end bounds
|
||||||
|
const endBounds = await page.getByLabel('End bounds').textContent();
|
||||||
|
|
||||||
|
// Add 2 minutes to end bound datetime and use it as the new end time
|
||||||
|
let endTimeStamp = new Date(endBounds);
|
||||||
|
endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() + 2);
|
||||||
|
const endDate = endTimeStamp.toISOString().split('T')[0];
|
||||||
|
const milliseconds = endTimeStamp.getMilliseconds();
|
||||||
|
const endTime = endTimeStamp.toISOString().split('T')[1].replace(`.${milliseconds}Z`, '');
|
||||||
|
|
||||||
|
// Subtract 1 minute from the end bound and use it as the new start time
|
||||||
|
let startTimeStamp = new Date(endBounds);
|
||||||
|
startTimeStamp.setUTCMinutes(startTimeStamp.getUTCMinutes() + 1);
|
||||||
|
const startDate = startTimeStamp.toISOString().split('T')[0];
|
||||||
|
const startMilliseconds = startTimeStamp.getMilliseconds();
|
||||||
|
const startTime = startTimeStamp
|
||||||
|
.toISOString()
|
||||||
|
.split('T')[1]
|
||||||
|
.replace(`.${startMilliseconds}Z`, '');
|
||||||
|
// Set fixed timespan mode to the future so that "now" is out of bounds.
|
||||||
|
await setTimeConductorBounds(page, {
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
startTime,
|
||||||
|
endTime
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Now Marker')).toBeHidden();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -287,6 +287,41 @@ test.describe('Basic Condition Set Use', () => {
|
|||||||
description: 'https://github.com/nasa/openmct/issues/7484'
|
description: 'https://github.com/nasa/openmct/issues/7484'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('ConditionSet has add criteria button enabled/disabled when composition is and is not available', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
const exampleTelemetry = await createExampleTelemetryObject(page);
|
||||||
|
|
||||||
|
await page.getByLabel('Show selected item in tree').click();
|
||||||
|
await page.goto(conditionSet.url);
|
||||||
|
// Change the object to edit mode
|
||||||
|
await page.getByLabel('Edit Object').click();
|
||||||
|
|
||||||
|
// Create a condition
|
||||||
|
await page.locator('#addCondition').click();
|
||||||
|
await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');
|
||||||
|
|
||||||
|
// Validate that the add criteria button is disabled
|
||||||
|
await expect(page.getByLabel('Add Criteria - Disabled')).toHaveAttribute('disabled');
|
||||||
|
|
||||||
|
// Add Telemetry to ConditionSet
|
||||||
|
const sineWaveGeneratorTreeItem = page
|
||||||
|
.getByRole('tree', {
|
||||||
|
name: 'Main Tree'
|
||||||
|
})
|
||||||
|
.getByRole('treeitem', {
|
||||||
|
name: exampleTelemetry.name
|
||||||
|
});
|
||||||
|
const conditionCollection = page.locator('#conditionCollection');
|
||||||
|
await sineWaveGeneratorTreeItem.dragTo(conditionCollection);
|
||||||
|
|
||||||
|
// Validate that the add criteria button is enabled and adds a new criterion
|
||||||
|
await expect(page.getByLabel('Add Criteria - Enabled')).not.toHaveAttribute('disabled');
|
||||||
|
await page.getByLabel('Add Criteria - Enabled').click();
|
||||||
|
const numOfUnnamedCriteria = await page.getByLabel('Criterion Telemetry Selection').count();
|
||||||
|
expect(numOfUnnamedCriteria).toEqual(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Condition Set Composition', () => {
|
test.describe('Condition Set Composition', () => {
|
||||||
|
@ -507,8 +507,140 @@ test.describe('Display Layout', () => {
|
|||||||
// In real time mode, we don't fetch annotations at all
|
// In real time mode, we don't fetch annotations at all
|
||||||
await expect.poll(() => networkRequests, { timeout: 10000 }).toHaveLength(0);
|
await expect.poll(() => networkRequests, { timeout: 10000 }).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Same objects with different request options have unique subscriptions', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
// Expand My Items
|
||||||
|
await page.getByLabel('Expand My Items folder').click();
|
||||||
|
|
||||||
|
// Create a Display Layout
|
||||||
|
const displayLayout = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Display Layout',
|
||||||
|
name: 'Test Display'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create a State Generator, set to higher frequency updates
|
||||||
|
const stateGenerator = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'State Generator',
|
||||||
|
name: 'State Generator'
|
||||||
|
});
|
||||||
|
const stateGeneratorTreeItem = page.getByRole('treeitem', {
|
||||||
|
name: stateGenerator.name
|
||||||
|
});
|
||||||
|
await stateGeneratorTreeItem.click({ button: 'right' });
|
||||||
|
await page.getByLabel('Edit Properties...').click();
|
||||||
|
await page.getByLabel('State Duration (seconds)', { exact: true }).fill('0.1');
|
||||||
|
await page.getByLabel('Save').click();
|
||||||
|
|
||||||
|
// Create a Table for filtering ON values
|
||||||
|
const tableFilterOnValue = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Telemetry Table',
|
||||||
|
name: 'Table Filter On Value'
|
||||||
|
});
|
||||||
|
const tableFilterOnTreeItem = page.getByRole('treeitem', {
|
||||||
|
name: tableFilterOnValue.name
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a Table for filtering OFF values
|
||||||
|
const tableFilterOffValue = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Telemetry Table',
|
||||||
|
name: 'Table Filter Off Value'
|
||||||
|
});
|
||||||
|
const tableFilterOffTreeItem = page.getByRole('treeitem', {
|
||||||
|
name: tableFilterOffValue.name
|
||||||
|
});
|
||||||
|
|
||||||
|
// Navigate to ON filtering table and add state generator and setup filters
|
||||||
|
await page.goto(tableFilterOnValue.url);
|
||||||
|
await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));
|
||||||
|
await selectFilterOption(page, '1');
|
||||||
|
await page.getByLabel('Save').click();
|
||||||
|
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||||
|
|
||||||
|
// Navigate to OFF filtering table and add state generator and setup filters
|
||||||
|
await page.goto(tableFilterOffValue.url);
|
||||||
|
await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));
|
||||||
|
await selectFilterOption(page, '0');
|
||||||
|
await page.getByLabel('Save').click();
|
||||||
|
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||||
|
|
||||||
|
// Navigate to the display layout and edit it
|
||||||
|
await page.goto(displayLayout.url);
|
||||||
|
|
||||||
|
// Add the tables to the display layout
|
||||||
|
await page.getByLabel('Edit Object').click();
|
||||||
|
await tableFilterOffTreeItem.dragTo(page.getByLabel('Layout Grid'), {
|
||||||
|
targetPosition: { x: 10, y: 300 }
|
||||||
|
});
|
||||||
|
await page.locator('.c-frame-edit > div:nth-child(4)').dragTo(page.getByLabel('Layout Grid'), {
|
||||||
|
targetPosition: { x: 400, y: 500 },
|
||||||
|
// eslint-disable-next-line playwright/no-force-option
|
||||||
|
force: true
|
||||||
|
});
|
||||||
|
await tableFilterOnTreeItem.dragTo(page.getByLabel('Layout Grid'), {
|
||||||
|
targetPosition: { x: 10, y: 100 }
|
||||||
|
});
|
||||||
|
await page.locator('.c-frame-edit > div:nth-child(4)').dragTo(page.getByLabel('Layout Grid'), {
|
||||||
|
targetPosition: { x: 400, y: 300 },
|
||||||
|
// eslint-disable-next-line playwright/no-force-option
|
||||||
|
force: true
|
||||||
|
});
|
||||||
|
await page.getByLabel('Save').click();
|
||||||
|
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||||
|
|
||||||
|
// Get the tables so we can verify filtering is working as expected
|
||||||
|
const tableFilterOn = page.getByLabel(`${tableFilterOnValue.name} Frame`, {
|
||||||
|
exact: true
|
||||||
|
});
|
||||||
|
const tableFilterOff = page.getByLabel(`${tableFilterOffValue.name} Frame`, {
|
||||||
|
exact: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify filtering is working correctly
|
||||||
|
|
||||||
|
// Check that no filtered values appear for at least 2 seconds
|
||||||
|
const VERIFICATION_TIME = 2000; // 2 seconds
|
||||||
|
const CHECK_INTERVAL = 100; // Check every 100ms
|
||||||
|
|
||||||
|
// Create a promise that will check for filtered values periodically
|
||||||
|
const checkForCorrectValues = new Promise((resolve, reject) => {
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
const offCount = await tableFilterOn.locator('td[title="OFF"]').count();
|
||||||
|
const onCount = await tableFilterOff.locator('td[title="ON"]').count();
|
||||||
|
if (offCount > 0 || onCount > 0) {
|
||||||
|
clearInterval(interval);
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Found ${offCount} OFF and ${onCount} ON values when expecting 0 OFF and 0 ON`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, CHECK_INTERVAL);
|
||||||
|
|
||||||
|
// After VERIFICATION_TIME, if no filtered values were found, resolve successfully
|
||||||
|
setTimeout(() => {
|
||||||
|
clearInterval(interval);
|
||||||
|
resolve();
|
||||||
|
}, VERIFICATION_TIME);
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(checkForCorrectValues).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function selectFilterOption(page, filterOption) {
|
||||||
|
await page.getByRole('tab', { name: 'Filters' }).click();
|
||||||
|
await page
|
||||||
|
.getByLabel('Inspector Views')
|
||||||
|
.locator('li')
|
||||||
|
.filter({ hasText: 'State Generator' })
|
||||||
|
.locator('span')
|
||||||
|
.click();
|
||||||
|
await page.getByRole('switch').click();
|
||||||
|
await page.selectOption('select[name="setSelectionThreshold"]', filterOption);
|
||||||
|
}
|
||||||
|
|
||||||
async function addAndRemoveDrawingObjectAndAssert(page, layoutObject, DISPLAY_LAYOUT_NAME) {
|
async function addAndRemoveDrawingObjectAndAssert(page, layoutObject, DISPLAY_LAYOUT_NAME) {
|
||||||
await expect(page.getByLabel(layoutObject, { exact: true })).toHaveCount(0);
|
await expect(page.getByLabel(layoutObject, { exact: true })).toHaveCount(0);
|
||||||
await addLayoutObject(page, DISPLAY_LAYOUT_NAME, layoutObject);
|
await addLayoutObject(page, DISPLAY_LAYOUT_NAME, layoutObject);
|
||||||
|
@ -28,7 +28,9 @@ import { v4 as uuid } from 'uuid';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
createDomainObjectWithDefaults,
|
createDomainObjectWithDefaults,
|
||||||
createExampleTelemetryObject
|
createExampleTelemetryObject,
|
||||||
|
setRealTimeMode,
|
||||||
|
setStartOffset
|
||||||
} from '../../../../appActions.js';
|
} from '../../../../appActions.js';
|
||||||
import { expect, test } from '../../../../pluginFixtures.js';
|
import { expect, test } from '../../../../pluginFixtures.js';
|
||||||
|
|
||||||
@ -166,6 +168,57 @@ test.describe('Gauge', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Gauge does not break when an object is missing', async ({ page }) => {
|
||||||
|
// Set up error listeners
|
||||||
|
const pageErrors = [];
|
||||||
|
|
||||||
|
// Listen for uncaught exceptions
|
||||||
|
page.on('pageerror', (err) => {
|
||||||
|
pageErrors.push(err.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
await setRealTimeMode(page);
|
||||||
|
|
||||||
|
// Create a Gauge
|
||||||
|
const gauge = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Gauge',
|
||||||
|
name: 'Gauge with missing object'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a Sine Wave Generator in the Gauge with a loading delay
|
||||||
|
const missingSWG = await createExampleTelemetryObject(page, gauge.uuid);
|
||||||
|
|
||||||
|
// Remove the object from local storage
|
||||||
|
await page.evaluate(
|
||||||
|
([missingObject]) => {
|
||||||
|
const mct = localStorage.getItem('mct');
|
||||||
|
const mctObjects = JSON.parse(mct);
|
||||||
|
delete mctObjects[missingObject.uuid];
|
||||||
|
localStorage.setItem('mct', JSON.stringify(mctObjects));
|
||||||
|
},
|
||||||
|
[missingSWG]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify start bounds
|
||||||
|
await expect(page.getByLabel('Start offset: 00:30:00')).toBeVisible();
|
||||||
|
|
||||||
|
// Nav to the Gauge
|
||||||
|
await page.goto(gauge.url, { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
// adjust time bounds and ensure they are updated
|
||||||
|
await setStartOffset(page, {
|
||||||
|
startHours: '00',
|
||||||
|
startMins: '45',
|
||||||
|
startSecs: '00'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify start bounds changed
|
||||||
|
await expect(page.getByLabel('Start offset: 00:45:00')).toBeVisible();
|
||||||
|
|
||||||
|
// // Verify no errors were thrown
|
||||||
|
expect(pageErrors).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
test('Gauge enforces composition policy', async ({ page }) => {
|
test('Gauge enforces composition policy', async ({ page }) => {
|
||||||
// Create a Gauge
|
// Create a Gauge
|
||||||
await createDomainObjectWithDefaults(page, {
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
@ -26,7 +26,10 @@ This test suite is dedicated to tests which verify the basic operations surround
|
|||||||
|
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
import {
|
||||||
|
createDomainObjectWithDefaults,
|
||||||
|
renameCurrentObjectFromBrowseBar
|
||||||
|
} from '../../../../appActions.js';
|
||||||
import { copy, paste, selectAll } from '../../../../helper/hotkeys/hotkeys.js';
|
import { copy, paste, selectAll } from '../../../../helper/hotkeys/hotkeys.js';
|
||||||
import * as nbUtils from '../../../../helper/notebookUtils.js';
|
import * as nbUtils from '../../../../helper/notebookUtils.js';
|
||||||
import { expect, streamToString, test } from '../../../../pluginFixtures.js';
|
import { expect, streamToString, test } from '../../../../pluginFixtures.js';
|
||||||
@ -596,4 +599,61 @@ test.describe('Notebook entry tests', () => {
|
|||||||
await expect(await page.locator(`text="${TEST_TEXT.repeat(1)}"`).count()).toEqual(1);
|
await expect(await page.locator(`text="${TEST_TEXT.repeat(1)}"`).count()).toEqual(1);
|
||||||
await expect(await page.locator(`text="${TEST_TEXT.repeat(2)}"`).count()).toEqual(0);
|
await expect(await page.locator(`text="${TEST_TEXT.repeat(2)}"`).count()).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('When changing the name of a notebook in the browse bar, new notebook changes are not lost', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
const TEST_TEXT = 'Do not lose me!';
|
||||||
|
const FIRST_NEW_NAME = 'New Name';
|
||||||
|
const SECOND_NEW_NAME = 'Second New Name';
|
||||||
|
|
||||||
|
await page.goto(notebookObject.url);
|
||||||
|
|
||||||
|
await page.getByLabel('Expand My Items folder').click();
|
||||||
|
|
||||||
|
await renameCurrentObjectFromBrowseBar(page, FIRST_NEW_NAME);
|
||||||
|
|
||||||
|
// verify the name change in tree and browse bar
|
||||||
|
await verifyNameChange(page, FIRST_NEW_NAME);
|
||||||
|
|
||||||
|
// enter one entry
|
||||||
|
await enterAndCommitTextEntry(page, TEST_TEXT);
|
||||||
|
|
||||||
|
// verify the entry is present
|
||||||
|
await expect(await page.locator(`text="${TEST_TEXT}"`).count()).toEqual(1);
|
||||||
|
|
||||||
|
// change the name
|
||||||
|
await renameCurrentObjectFromBrowseBar(page, SECOND_NEW_NAME);
|
||||||
|
|
||||||
|
// verify the name change in tree and browse bar
|
||||||
|
await verifyNameChange(page, SECOND_NEW_NAME);
|
||||||
|
|
||||||
|
// verify the entry is still present
|
||||||
|
await expect(await page.locator(`text="${TEST_TEXT}"`).count()).toEqual(1);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enter text into the last notebook entry and commit it.
|
||||||
|
*
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {string} text
|
||||||
|
*/
|
||||||
|
async function enterAndCommitTextEntry(page, text) {
|
||||||
|
await nbUtils.addNotebookEntry(page);
|
||||||
|
await nbUtils.enterTextInLastEntry(page, text);
|
||||||
|
await nbUtils.commitEntry(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify the name change in the tree and browse bar.
|
||||||
|
*
|
||||||
|
* @param {import('@playwright/test').Page} page
|
||||||
|
* @param {string} newName
|
||||||
|
*/
|
||||||
|
async function verifyNameChange(page, newName) {
|
||||||
|
await expect(
|
||||||
|
page.getByRole('treeitem').locator('.is-navigated-object .c-tree__item__name')
|
||||||
|
).toHaveText(newName);
|
||||||
|
await expect(page.getByLabel('Browse bar object name')).toHaveText(newName);
|
||||||
|
}
|
||||||
|
@ -108,4 +108,42 @@ test.describe('Plot Controls', () => {
|
|||||||
// Expect before and after plot points to match
|
// Expect before and after plot points to match
|
||||||
await expect(plotPixelSizeAtPause).toEqual(plotPixelSizeAfterWait);
|
await expect(plotPixelSizeAtPause).toEqual(plotPixelSizeAfterWait);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Test to verify that switching a plot's time context from global to
|
||||||
|
its own independent time context and then back to global context works correctly.
|
||||||
|
|
||||||
|
After switching from fixed time mode (ITC) to real time mode (global context),
|
||||||
|
the pause control for the plot should be available, indicating that it is following the right context.
|
||||||
|
*/
|
||||||
|
test('Plots follow the right time context', async ({ page }) => {
|
||||||
|
// Set global time conductor to real-time mode
|
||||||
|
await setRealTimeMode(page);
|
||||||
|
|
||||||
|
// hover over plot for plot controls
|
||||||
|
await page.getByLabel('Plot Canvas').hover();
|
||||||
|
// Ensure pause control is visible since global time conductor is in Real time mode.
|
||||||
|
await expect(page.getByTitle('Pause incoming real-time data')).toBeVisible();
|
||||||
|
|
||||||
|
// Toggle independent time conductor ON
|
||||||
|
await page.getByLabel('Enable Independent Time Conductor').click();
|
||||||
|
|
||||||
|
// Bring up the independent time conductor popup and switch to fixed time mode
|
||||||
|
await page.getByLabel('Independent Time Conductor Settings').click();
|
||||||
|
await page.getByLabel('Independent Time Conductor Mode Menu').click();
|
||||||
|
await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click();
|
||||||
|
|
||||||
|
// hover over plot for plot controls
|
||||||
|
await page.getByLabel('Plot Canvas').hover();
|
||||||
|
// Ensure pause control is no longer visible since the plot is following the independent time context
|
||||||
|
await expect(page.getByTitle('Pause incoming real-time data')).toBeHidden();
|
||||||
|
|
||||||
|
// Toggle independent time conductor OFF - Note that the global time conductor is still in Real time mode
|
||||||
|
await page.getByLabel('Disable Independent Time Conductor').click();
|
||||||
|
|
||||||
|
// hover over plot for plot controls
|
||||||
|
await page.getByLabel('Plot Canvas').hover();
|
||||||
|
// Ensure pause control is visible since the global time conductor is in real time mode
|
||||||
|
await expect(page.getByTitle('Pause incoming real-time data')).toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,58 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2025, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This test suite is dedicated to testing the rendering and interaction of plots.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||||
|
import { expect, test } from '../../../../pluginFixtures.js';
|
||||||
|
|
||||||
|
test.describe('Plot Controls in compact mode', () => {
|
||||||
|
let timeStrip;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||||
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
timeStrip = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Time Strip'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create an overlay plot with a sine wave generator
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Sine Wave Generator',
|
||||||
|
parent: timeStrip.uuid
|
||||||
|
});
|
||||||
|
await page.goto(`${timeStrip.url}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Plots show cursor guides', async ({ page }) => {
|
||||||
|
// hover over plot for plot controls
|
||||||
|
await page.getByLabel('Plot Canvas').hover();
|
||||||
|
// click on cursor guides control
|
||||||
|
await page.getByTitle('Toggle cursor guides').click();
|
||||||
|
await page.getByLabel('Plot Canvas').hover();
|
||||||
|
await expect(page.getByLabel('Vertical cursor guide')).toBeVisible();
|
||||||
|
await expect(page.getByLabel('Horizontal cursor guide')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
@ -46,6 +46,24 @@ class EventMetadataProvider {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const inPlaceUpdateMetadataValue = {
|
||||||
|
key: 'messageId',
|
||||||
|
name: 'row identifier',
|
||||||
|
format: 'string',
|
||||||
|
useToUpdateInPlace: true
|
||||||
|
};
|
||||||
|
const eventAcknowledgeMetadataValue = {
|
||||||
|
key: 'acknowledge',
|
||||||
|
name: 'Acknowledge',
|
||||||
|
format: 'string'
|
||||||
|
};
|
||||||
|
|
||||||
|
const eventGeneratorWithAcknowledge = structuredClone(this.METADATA_BY_TYPE.eventGenerator);
|
||||||
|
eventGeneratorWithAcknowledge.values.push(inPlaceUpdateMetadataValue);
|
||||||
|
eventGeneratorWithAcknowledge.values.push(eventAcknowledgeMetadataValue);
|
||||||
|
|
||||||
|
this.METADATA_BY_TYPE.eventGeneratorWithAcknowledge = eventGeneratorWithAcknowledge;
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsMetadata(domainObject) {
|
supportsMetadata(domainObject) {
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module defining EventTelemetryProvider. Created by chacskaylo on 06/18/2015.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import EventTelemetryProvider from './EventTelemetryProvider.js';
|
||||||
|
|
||||||
|
class EventWithAcknowledgeTelemetryProvider extends EventTelemetryProvider {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.unAcknowledgedData = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateData(firstObservedTime, count, startTime, duration, name) {
|
||||||
|
if (this.unAcknowledgedData === undefined) {
|
||||||
|
const unAcknowledgedData = super.generateData(
|
||||||
|
firstObservedTime,
|
||||||
|
count,
|
||||||
|
startTime,
|
||||||
|
duration,
|
||||||
|
name
|
||||||
|
);
|
||||||
|
unAcknowledgedData.messageId = unAcknowledgedData.message;
|
||||||
|
this.unAcknowledgedData = unAcknowledgedData;
|
||||||
|
|
||||||
|
return this.unAcknowledgedData;
|
||||||
|
} else {
|
||||||
|
const acknowledgedData = {
|
||||||
|
...this.unAcknowledgedData,
|
||||||
|
acknowledge: 'OK'
|
||||||
|
};
|
||||||
|
|
||||||
|
this.unAcknowledgedData = undefined;
|
||||||
|
|
||||||
|
return acknowledgedData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
supportsRequest(domainObject) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
supportsSubscribe(domainObject) {
|
||||||
|
return domainObject.type === 'eventGeneratorWithAcknowledge';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventWithAcknowledgeTelemetryProvider;
|
@ -21,6 +21,7 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
import EventMetadataProvider from './EventMetadataProvider.js';
|
import EventMetadataProvider from './EventMetadataProvider.js';
|
||||||
import EventTelemetryProvider from './EventTelemetryProvider.js';
|
import EventTelemetryProvider from './EventTelemetryProvider.js';
|
||||||
|
import EventWithAcknowledgeTelemetryProvider from './EventWithAcknowledgeTelemetryProvider.js';
|
||||||
|
|
||||||
export default function EventGeneratorPlugin(options) {
|
export default function EventGeneratorPlugin(options) {
|
||||||
return function install(openmct) {
|
return function install(openmct) {
|
||||||
@ -38,5 +39,20 @@ export default function EventGeneratorPlugin(options) {
|
|||||||
});
|
});
|
||||||
openmct.telemetry.addProvider(new EventTelemetryProvider());
|
openmct.telemetry.addProvider(new EventTelemetryProvider());
|
||||||
openmct.telemetry.addProvider(new EventMetadataProvider());
|
openmct.telemetry.addProvider(new EventMetadataProvider());
|
||||||
|
|
||||||
|
openmct.types.addType('eventGeneratorWithAcknowledge', {
|
||||||
|
name: 'Event Message Generator with Acknowledge',
|
||||||
|
description:
|
||||||
|
'For development use. Creates sample event message data stream and updates the event row with an acknowledgement.',
|
||||||
|
cssClass: 'icon-generator-events',
|
||||||
|
creatable: true,
|
||||||
|
initialize: function (object) {
|
||||||
|
object.telemetry = {
|
||||||
|
duration: 2.5
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
openmct.telemetry.addProvider(new EventWithAcknowledgeTelemetryProvider());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -108,6 +108,16 @@ const METADATA_BY_TYPE = {
|
|||||||
string: 'ON'
|
string: 'ON'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
singleSelectionThreshold: true,
|
||||||
|
comparator: 'equals',
|
||||||
|
possibleValues: [
|
||||||
|
{ label: 'OFF', value: 0 },
|
||||||
|
{ label: 'ON', value: 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
hints: {
|
hints: {
|
||||||
range: 1
|
range: 1
|
||||||
}
|
}
|
||||||
|
@ -34,14 +34,16 @@ StateGeneratorProvider.prototype.supportsSubscribe = function (domainObject) {
|
|||||||
return domainObject.type === 'example.state-generator';
|
return domainObject.type === 'example.state-generator';
|
||||||
};
|
};
|
||||||
|
|
||||||
StateGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
|
StateGeneratorProvider.prototype.subscribe = function (domainObject, callback, options) {
|
||||||
var duration = domainObject.telemetry.duration * 1000;
|
var duration = domainObject.telemetry.duration * 1000;
|
||||||
|
|
||||||
var interval = setInterval(function () {
|
var interval = setInterval(() => {
|
||||||
var now = Date.now();
|
var now = Date.now();
|
||||||
var datum = pointForTimestamp(now, duration, domainObject.name);
|
var datum = pointForTimestamp(now, duration, domainObject.name);
|
||||||
|
if (!this.shouldBeFiltered(datum, options)) {
|
||||||
datum.value = String(datum.value);
|
datum.value = String(datum.value);
|
||||||
callback(datum);
|
callback(datum);
|
||||||
|
}
|
||||||
}, duration);
|
}, duration);
|
||||||
|
|
||||||
return function () {
|
return function () {
|
||||||
@ -63,9 +65,25 @@ StateGeneratorProvider.prototype.request = function (domainObject, options) {
|
|||||||
|
|
||||||
var data = [];
|
var data = [];
|
||||||
while (start <= end && data.length < 5000) {
|
while (start <= end && data.length < 5000) {
|
||||||
data.push(pointForTimestamp(start, duration, domainObject.name));
|
const point = pointForTimestamp(start, duration, domainObject.name);
|
||||||
|
|
||||||
|
if (!this.shouldBeFiltered(point, options)) {
|
||||||
|
data.push(point);
|
||||||
|
}
|
||||||
start += duration;
|
start += duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(data);
|
return Promise.resolve(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
StateGeneratorProvider.prototype.shouldBeFiltered = function (point, options) {
|
||||||
|
const valueToFilter = options?.filters?.state?.equals?.[0];
|
||||||
|
|
||||||
|
if (!valueToFilter) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { value } = point;
|
||||||
|
|
||||||
|
return value !== Number(valueToFilter);
|
||||||
|
};
|
||||||
|
24
package-lock.json
generated
24
package-lock.json
generated
@ -104,7 +104,7 @@
|
|||||||
"@axe-core/playwright": "4.8.5",
|
"@axe-core/playwright": "4.8.5",
|
||||||
"@percy/cli": "1.27.4",
|
"@percy/cli": "1.27.4",
|
||||||
"@percy/playwright": "1.0.4",
|
"@percy/playwright": "1.0.4",
|
||||||
"@playwright/test": "1.47.2"
|
"@playwright/test": "1.48.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"e2e/node_modules/@percy/cli": {
|
"e2e/node_modules/@percy/cli": {
|
||||||
@ -1548,13 +1548,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/test": {
|
"node_modules/@playwright/test": {
|
||||||
"version": "1.47.2",
|
"version": "1.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.2.tgz",
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.1.tgz",
|
||||||
"integrity": "sha512-jTXRsoSPONAs8Za9QEQdyjFn+0ZQFjCiIztAIF6bi1HqhBzG9Ma7g1WotyiGqFSBRZjIEqMdT8RUlbk1QVhzCQ==",
|
"integrity": "sha512-s9RtWoxkOLmRJdw3oFvhFbs9OJS0BzrLUc8Hf6l2UdCNd1rqeEyD4BhCJkvzeEoD1FsK4mirsWwGerhVmYKtZg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.47.2"
|
"playwright": "1.48.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
@ -8750,13 +8750,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright": {
|
"node_modules/playwright": {
|
||||||
"version": "1.47.2",
|
"version": "1.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.2.tgz",
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.1.tgz",
|
||||||
"integrity": "sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==",
|
"integrity": "sha512-j8CiHW/V6HxmbntOfyB4+T/uk08tBy6ph0MpBXwuoofkSnLmlfdYNNkFTYD6ofzzlSqLA1fwH4vwvVFvJgLN0w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.47.2"
|
"playwright-core": "1.48.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
@ -8769,9 +8769,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright-core": {
|
"node_modules/playwright-core": {
|
||||||
"version": "1.47.2",
|
"version": "1.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.2.tgz",
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.1.tgz",
|
||||||
"integrity": "sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==",
|
"integrity": "sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -250,6 +250,90 @@ export default class TelemetryAPI {
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitizes objects for consistent serialization by:
|
||||||
|
* 1. Removing non-plain objects (class instances) and functions
|
||||||
|
* 2. Sorting object keys alphabetically to ensure consistent ordering
|
||||||
|
*/
|
||||||
|
sanitizeForSerialization(key, value) {
|
||||||
|
// Handle null and primitives directly
|
||||||
|
if (value === null || typeof value !== 'object') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove functions and non-plain objects (except arrays)
|
||||||
|
if (
|
||||||
|
typeof value === 'function' ||
|
||||||
|
(Object.getPrototypeOf(value) !== Object.prototype && !Array.isArray(value))
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For plain objects, just sort the keys
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
const sortedObject = {};
|
||||||
|
const sortedKeys = Object.keys(value).sort();
|
||||||
|
|
||||||
|
sortedKeys.forEach((objectKey) => {
|
||||||
|
sortedObject[objectKey] = value[objectKey];
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortedObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a numeric hash value for an options object. The hash is consistent
|
||||||
|
* for equivalent option objects regardless of property order.
|
||||||
|
*
|
||||||
|
* This is used to create compact, unique cache keys for telemetry subscriptions with
|
||||||
|
* different options configurations. The hash function ensures that identical options
|
||||||
|
* objects will always generate the same hash value, while different options objects
|
||||||
|
* (even with small differences) will generate different hash values.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {Object} options The options object to hash
|
||||||
|
* @returns {number} A positive integer hash of the options object
|
||||||
|
*/
|
||||||
|
#hashOptions(options) {
|
||||||
|
const sanitizedOptionsString = JSON.stringify(
|
||||||
|
options,
|
||||||
|
this.sanitizeForSerialization.bind(this)
|
||||||
|
);
|
||||||
|
|
||||||
|
let hash = 0;
|
||||||
|
const prime = 31;
|
||||||
|
const modulus = 1e9 + 9; // Large prime number
|
||||||
|
|
||||||
|
for (let i = 0; i < sanitizedOptionsString.length; i++) {
|
||||||
|
const char = sanitizedOptionsString.charCodeAt(i);
|
||||||
|
// Calculate new hash value while keeping numbers manageable
|
||||||
|
hash = Math.floor((hash * prime + char) % modulus);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.abs(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a unique cache key for a telemetry subscription based on the
|
||||||
|
* domain object identifier and options (which includes strategy).
|
||||||
|
*
|
||||||
|
* Uses a hash of the options object to create compact cache keys while still
|
||||||
|
* ensuring unique keys for different subscription configurations.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {import('openmct').DomainObject} domainObject The domain object being subscribed to
|
||||||
|
* @param {Object} options The subscription options object (including strategy)
|
||||||
|
* @returns {string} A unique key string for caching the subscription
|
||||||
|
*/
|
||||||
|
#getSubscriptionCacheKey(domainObject, options) {
|
||||||
|
const keyString = makeKeyString(domainObject.identifier);
|
||||||
|
|
||||||
|
return `${keyString}:${this.#hashOptions(options)}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a request interceptor that transforms a request via module:openmct.TelemetryAPI.request
|
* Register a request interceptor that transforms a request via module:openmct.TelemetryAPI.request
|
||||||
* The request will be modified when it is received and will be returned in it's modified state
|
* The request will be modified when it is received and will be returned in it's modified state
|
||||||
@ -418,16 +502,14 @@ export default class TelemetryAPI {
|
|||||||
this.#subscribeCache = {};
|
this.#subscribeCache = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyString = makeKeyString(domainObject.identifier);
|
|
||||||
const supportedStrategy = supportsBatching ? requestedStrategy : SUBSCRIBE_STRATEGY.LATEST;
|
const supportedStrategy = supportsBatching ? requestedStrategy : SUBSCRIBE_STRATEGY.LATEST;
|
||||||
// Override the requested strategy with the strategy supported by the provider
|
// Override the requested strategy with the strategy supported by the provider
|
||||||
const optionsWithSupportedStrategy = {
|
const optionsWithSupportedStrategy = {
|
||||||
...options,
|
...options,
|
||||||
strategy: supportedStrategy
|
strategy: supportedStrategy
|
||||||
};
|
};
|
||||||
// If batching is supported, we need to cache a subscription for each strategy -
|
|
||||||
// latest and batched.
|
const cacheKey = this.#getSubscriptionCacheKey(domainObject, optionsWithSupportedStrategy);
|
||||||
const cacheKey = `${keyString}:${supportedStrategy}`;
|
|
||||||
let subscriber = this.#subscribeCache[cacheKey];
|
let subscriber = this.#subscribeCache[cacheKey];
|
||||||
|
|
||||||
if (!subscriber) {
|
if (!subscriber) {
|
||||||
|
@ -359,6 +359,18 @@ class IndependentTimeContext extends TimeContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {boolean}
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
isFixed() {
|
||||||
|
if (this.upstreamTimeContext) {
|
||||||
|
return this.upstreamTimeContext.isFixed(...arguments);
|
||||||
|
} else {
|
||||||
|
return super.isFixed(...arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
* @override
|
* @override
|
||||||
@ -400,7 +412,7 @@ class IndependentTimeContext extends TimeContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the time context to the global time context
|
* Reset the time context from the global time context
|
||||||
*/
|
*/
|
||||||
resetContext() {
|
resetContext() {
|
||||||
if (this.upstreamTimeContext) {
|
if (this.upstreamTimeContext) {
|
||||||
@ -428,6 +440,10 @@ class IndependentTimeContext extends TimeContext {
|
|||||||
// Emit bounds so that views that are changing context get the upstream bounds
|
// Emit bounds so that views that are changing context get the upstream bounds
|
||||||
this.emit('bounds', this.getBounds());
|
this.emit('bounds', this.getBounds());
|
||||||
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
|
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
|
||||||
|
// Also emit the mode in case it's different from previous time context
|
||||||
|
if (this.getMode()) {
|
||||||
|
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.getMode()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -502,6 +518,10 @@ class IndependentTimeContext extends TimeContext {
|
|||||||
// Emit bounds so that views that are changing context get the upstream bounds
|
// Emit bounds so that views that are changing context get the upstream bounds
|
||||||
this.emit('bounds', this.getBounds());
|
this.emit('bounds', this.getBounds());
|
||||||
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
|
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
|
||||||
|
// Also emit the mode in case it's different from the global time context
|
||||||
|
if (this.getMode()) {
|
||||||
|
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.getMode()));
|
||||||
|
}
|
||||||
// now that the view's context is set, tell others to check theirs in case they were following this view's context.
|
// now that the view's context is set, tell others to check theirs in case they were following this view's context.
|
||||||
this.globalTimeContext.emit('refreshContext', viewKey);
|
this.globalTimeContext.emit('refreshContext', viewKey);
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
import { FIXED_MODE_KEY, REALTIME_MODE_KEY } from '@/api/time/constants';
|
import { FIXED_MODE_KEY, REALTIME_MODE_KEY } from '@/api/time/constants';
|
||||||
import IndependentTimeContext from '@/api/time/IndependentTimeContext';
|
import IndependentTimeContext from '@/api/time/IndependentTimeContext';
|
||||||
|
|
||||||
|
import { TIME_CONTEXT_EVENTS } from './constants';
|
||||||
import GlobalTimeContext from './GlobalTimeContext.js';
|
import GlobalTimeContext from './GlobalTimeContext.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -142,7 +143,7 @@ class TimeAPI extends GlobalTimeContext {
|
|||||||
addIndependentContext(key, value, clockKey) {
|
addIndependentContext(key, value, clockKey) {
|
||||||
let timeContext = this.getIndependentContext(key);
|
let timeContext = this.getIndependentContext(key);
|
||||||
|
|
||||||
//stop following upstream time context since the view has it's own
|
//stop following upstream time context since the view has its own
|
||||||
timeContext.resetContext();
|
timeContext.resetContext();
|
||||||
|
|
||||||
if (clockKey) {
|
if (clockKey) {
|
||||||
@ -152,6 +153,9 @@ class TimeAPI extends GlobalTimeContext {
|
|||||||
timeContext.setMode(FIXED_MODE_KEY, value);
|
timeContext.setMode(FIXED_MODE_KEY, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also emit the mode in case it's different from the previous time context
|
||||||
|
timeContext.emit(TIME_CONTEXT_EVENTS.modeChanged, structuredClone(timeContext.getMode()));
|
||||||
|
|
||||||
// Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context
|
// Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context
|
||||||
this.emit('refreshContext', key);
|
this.emit('refreshContext', key);
|
||||||
|
|
||||||
|
@ -160,8 +160,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="c-cdef__separator c-row-separator"></div>
|
<div class="c-cdef__separator c-row-separator"></div>
|
||||||
<div class="c-cdef__controls" :disabled="!telemetry.length">
|
<div class="c-cdef__controls">
|
||||||
<button
|
<button
|
||||||
|
:disabled="!telemetry.length"
|
||||||
|
:aria-label="`Add Criteria - ${!telemetry.length ? 'Disabled' : 'Enabled'}`"
|
||||||
class="c-cdef__add-criteria-button c-button c-button--labeled icon-plus"
|
class="c-cdef__add-criteria-button c-button c-button--labeled icon-plus"
|
||||||
@click="addCriteria"
|
@click="addCriteria"
|
||||||
>
|
>
|
||||||
|
@ -27,9 +27,9 @@ To define a filter, you'll need to add a new `filter` property to the domain obj
|
|||||||
singleSelectionThreshold: true,
|
singleSelectionThreshold: true,
|
||||||
comparator: 'equals',
|
comparator: 'equals',
|
||||||
possibleValues: [
|
possibleValues: [
|
||||||
{ name: 'Apple', value: 'apple' },
|
{ label: 'Apple', value: 'apple' },
|
||||||
{ name: 'Banana', value: 'banana' },
|
{ label: 'Banana', value: 'banana' },
|
||||||
{ name: 'Orange', value: 'orange' }
|
{ label: 'Orange', value: 'orange' }
|
||||||
]
|
]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
@ -649,6 +649,11 @@ export default {
|
|||||||
},
|
},
|
||||||
request(domainObject = this.telemetryObject) {
|
request(domainObject = this.telemetryObject) {
|
||||||
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
|
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
|
||||||
|
|
||||||
|
if (!this.metadata) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
|
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
|
||||||
const LimitEvaluator = this.openmct.telemetry.getLimits(domainObject);
|
const LimitEvaluator = this.openmct.telemetry.getLimits(domainObject);
|
||||||
LimitEvaluator.limits().then(this.updateLimits);
|
LimitEvaluator.limits().then(this.updateLimits);
|
||||||
|
@ -164,11 +164,13 @@
|
|||||||
<div
|
<div
|
||||||
v-show="cursorGuide"
|
v-show="cursorGuide"
|
||||||
ref="cursorGuideVertical"
|
ref="cursorGuideVertical"
|
||||||
|
aria-label="Vertical cursor guide"
|
||||||
class="c-cursor-guide--v js-cursor-guide--v"
|
class="c-cursor-guide--v js-cursor-guide--v"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
v-show="cursorGuide"
|
v-show="cursorGuide"
|
||||||
ref="cursorGuideHorizontal"
|
ref="cursorGuideHorizontal"
|
||||||
|
aria-label="Horizontal cursor guide"
|
||||||
class="c-cursor-guide--h js-cursor-guide--h"
|
class="c-cursor-guide--h js-cursor-guide--h"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
@ -537,6 +539,7 @@ export default {
|
|||||||
this.followTimeContext();
|
this.followTimeContext();
|
||||||
},
|
},
|
||||||
followTimeContext() {
|
followTimeContext() {
|
||||||
|
this.updateMode();
|
||||||
this.updateDisplayBounds(this.timeContext.getBounds());
|
this.updateDisplayBounds(this.timeContext.getBounds());
|
||||||
this.timeContext.on('modeChanged', this.updateMode);
|
this.timeContext.on('modeChanged', this.updateMode);
|
||||||
this.timeContext.on('boundsChanged', this.updateDisplayBounds);
|
this.timeContext.on('boundsChanged', this.updateDisplayBounds);
|
||||||
@ -854,13 +857,11 @@ export default {
|
|||||||
|
|
||||||
this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1];
|
this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1];
|
||||||
|
|
||||||
if (!this.options.compact) {
|
|
||||||
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
|
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
|
||||||
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
|
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
|
||||||
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
|
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
|
||||||
this.listenTo(this.canvas, 'click', this.selectNearbyAnnotations, this);
|
this.listenTo(this.canvas, 'click', this.selectNearbyAnnotations, this);
|
||||||
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
|
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
marqueeAnnotations(annotationsToSelect) {
|
marqueeAnnotations(annotationsToSelect) {
|
||||||
@ -1115,6 +1116,7 @@ export default {
|
|||||||
this.listenTo(window, 'mouseup', this.onMouseUp, this);
|
this.listenTo(window, 'mouseup', this.onMouseUp, this);
|
||||||
this.listenTo(window, 'mousemove', this.trackMousePosition, this);
|
this.listenTo(window, 'mousemove', this.trackMousePosition, this);
|
||||||
|
|
||||||
|
if (!this.options.compact) {
|
||||||
// track frozen state on mouseDown to be read on mouseUp
|
// track frozen state on mouseDown to be read on mouseUp
|
||||||
const isFrozen =
|
const isFrozen =
|
||||||
this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
|
this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
|
||||||
@ -1129,6 +1131,7 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
return this.startMarquee(event, false);
|
return this.startMarquee(event, false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onMouseUp(event) {
|
onMouseUp(event) {
|
||||||
@ -1158,11 +1161,15 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
isMouseClick() {
|
isMouseClick() {
|
||||||
if (!this.marquee) {
|
// We may not have a marquee if we've disabled pan/zoom, but we still need to know if it's a mouse click for highlights and lock points.
|
||||||
|
if (!this.marquee && !this.positionOverPlot) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { start, end } = this.marquee;
|
const { start, end } = this.marquee ?? {
|
||||||
|
start: this.positionOverPlot,
|
||||||
|
end: this.positionOverPlot
|
||||||
|
};
|
||||||
const someYPositionOverPlot = start.y.some((y) => y);
|
const someYPositionOverPlot = start.y.some((y) => y);
|
||||||
|
|
||||||
return start.x === end.x && someYPositionOverPlot;
|
return start.x === end.x && someYPositionOverPlot;
|
||||||
|
@ -162,14 +162,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
gridLines(newGridLines) {
|
|
||||||
this.gridLines = newGridLines;
|
|
||||||
},
|
|
||||||
cursorGuide(newCursorGuide) {
|
|
||||||
this.cursorGuide = newCursorGuide;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
created() {
|
||||||
eventHelpers.extend(this);
|
eventHelpers.extend(this);
|
||||||
this.imageExporter = new ImageExporter(this.openmct);
|
this.imageExporter = new ImageExporter(this.openmct);
|
||||||
|
@ -91,15 +91,19 @@ export default class TelemetryTableRow {
|
|||||||
return [VIEW_DATUM_ACTION_KEY, VIEW_HISTORICAL_DATA_ACTION_KEY];
|
return [VIEW_DATUM_ACTION_KEY, VIEW_HISTORICAL_DATA_ACTION_KEY];
|
||||||
}
|
}
|
||||||
|
|
||||||
updateWithDatum(updatesToDatum) {
|
/**
|
||||||
const normalizedUpdatesToDatum = createNormalizedDatum(updatesToDatum, this.columns);
|
* Merges the row parameter's datum with the current row datum
|
||||||
|
* @param {TelemetryTableRow} row
|
||||||
|
*/
|
||||||
|
updateWithDatum(row) {
|
||||||
this.datum = {
|
this.datum = {
|
||||||
...this.datum,
|
...this.datum,
|
||||||
...normalizedUpdatesToDatum
|
...row.datum
|
||||||
};
|
};
|
||||||
|
|
||||||
this.fullDatum = {
|
this.fullDatum = {
|
||||||
...this.fullDatum,
|
...this.fullDatum,
|
||||||
...updatesToDatum
|
...row.fullDatum
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,11 @@ import { EventEmitter } from 'eventemitter3';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import { ORDER } from '../constants.js';
|
import { ORDER } from '../constants.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('.TelemetryTableRow.js').default} TelemetryTableRow
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
@ -124,10 +129,22 @@ export default class TableRowCollection extends EventEmitter {
|
|||||||
return foundIndex;
|
return foundIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRowInPlace(row, index) {
|
/**
|
||||||
const foundRow = this.rows[index];
|
* `incomingRow` exists in the collection,
|
||||||
foundRow.updateWithDatum(row.datum);
|
* so merge existing and incoming row properties
|
||||||
this.rows[index] = foundRow;
|
*
|
||||||
|
* Do to reactivity of Vue, we want to replace the existing row with the updated row
|
||||||
|
* @param {TelemetryTableRow} incomingRow to update
|
||||||
|
* @param {number} index of the existing row in the collection to update
|
||||||
|
*/
|
||||||
|
updateRowInPlace(incomingRow, index) {
|
||||||
|
// Update the incoming row, not the existing row
|
||||||
|
const existingRow = this.rows[index];
|
||||||
|
incomingRow.updateWithDatum(existingRow);
|
||||||
|
|
||||||
|
// Replacing the existing row with the updated, incoming row will trigger Vue reactivity
|
||||||
|
// because the reference to the row has changed
|
||||||
|
this.rows.splice(index, 1, incomingRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
setLimit(rowLimit) {
|
setLimit(rowLimit) {
|
||||||
|
@ -373,7 +373,6 @@ export default {
|
|||||||
configuredColumnWidths: configuration.columnWidths,
|
configuredColumnWidths: configuration.columnWidths,
|
||||||
sizingRows: {},
|
sizingRows: {},
|
||||||
rowHeight: ROW_HEIGHT,
|
rowHeight: ROW_HEIGHT,
|
||||||
scrollOffset: 0,
|
|
||||||
totalHeight: 0,
|
totalHeight: 0,
|
||||||
totalWidth: 0,
|
totalWidth: 0,
|
||||||
rowOffset: 0,
|
rowOffset: 0,
|
||||||
@ -552,6 +551,7 @@ export default {
|
|||||||
//Default sort
|
//Default sort
|
||||||
this.sortOptions = this.table.tableRows.sortBy();
|
this.sortOptions = this.table.tableRows.sortBy();
|
||||||
this.scrollable = this.$refs.scrollable;
|
this.scrollable = this.$refs.scrollable;
|
||||||
|
this.lastScrollLeft = this.scrollable.scrollLeft;
|
||||||
this.contentTable = this.$refs.contentTable;
|
this.contentTable = this.$refs.contentTable;
|
||||||
this.sizingTable = this.$refs.sizingTable;
|
this.sizingTable = this.$refs.sizingTable;
|
||||||
this.headersHolderEl = this.$refs.headersHolderEl;
|
this.headersHolderEl = this.$refs.headersHolderEl;
|
||||||
@ -740,7 +740,9 @@ export default {
|
|||||||
this.table.sortBy(this.sortOptions);
|
this.table.sortBy(this.sortOptions);
|
||||||
},
|
},
|
||||||
scroll() {
|
scroll() {
|
||||||
|
if (this.lastScrollLeft === this.scrollable.scrollLeft) {
|
||||||
this.throttledUpdateVisibleRows();
|
this.throttledUpdateVisibleRows();
|
||||||
|
}
|
||||||
this.synchronizeScrollX();
|
this.synchronizeScrollX();
|
||||||
|
|
||||||
if (this.shouldAutoScroll()) {
|
if (this.shouldAutoScroll()) {
|
||||||
@ -765,6 +767,8 @@ export default {
|
|||||||
this.scrollable.scrollTop = Number.MAX_SAFE_INTEGER;
|
this.scrollable.scrollTop = Number.MAX_SAFE_INTEGER;
|
||||||
},
|
},
|
||||||
synchronizeScrollX() {
|
synchronizeScrollX() {
|
||||||
|
this.lastScrollLeft = this.scrollable.scrollLeft;
|
||||||
|
|
||||||
if (this.$refs.headersHolderEl && this.scrollable) {
|
if (this.$refs.headersHolderEl && this.scrollable) {
|
||||||
this.headersHolderEl.scrollLeft = this.scrollable.scrollLeft;
|
this.headersHolderEl.scrollLeft = this.scrollable.scrollLeft;
|
||||||
}
|
}
|
||||||
|
@ -243,12 +243,20 @@ export default {
|
|||||||
this.timeContext.off(TIME_CONTEXT_EVENTS.modeChanged, this.setTimeOptionsMode);
|
this.timeContext.off(TIME_CONTEXT_EVENTS.modeChanged, this.setTimeOptionsMode);
|
||||||
},
|
},
|
||||||
setTimeOptionsClock(clock) {
|
setTimeOptionsClock(clock) {
|
||||||
|
// If the user has persisted any time options, then don't override them with global settings.
|
||||||
|
if (this.independentTCEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.setTimeOptionsOffsets();
|
this.setTimeOptionsOffsets();
|
||||||
this.timeOptions.clock = clock.key;
|
this.timeOptions.clock = clock.key;
|
||||||
},
|
},
|
||||||
setTimeOptionsMode(mode) {
|
setTimeOptionsMode(mode) {
|
||||||
|
// If the user has persisted any time options, then don't override them with global settings.
|
||||||
|
if (this.independentTCEnabled) {
|
||||||
this.setTimeOptionsOffsets();
|
this.setTimeOptionsOffsets();
|
||||||
this.timeOptions.mode = mode;
|
this.timeOptions.mode = mode;
|
||||||
|
this.isFixed = this.timeOptions.mode === FIXED_MODE_KEY;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
setTimeOptionsOffsets() {
|
setTimeOptionsOffsets() {
|
||||||
this.timeOptions.clockOffsets =
|
this.timeOptions.clockOffsets =
|
||||||
|
@ -436,6 +436,9 @@ export default {
|
|||||||
|
|
||||||
return startInBounds || endInBounds || middleInBounds;
|
return startInBounds || endInBounds || middleInBounds;
|
||||||
},
|
},
|
||||||
|
isActivityInProgress(activity) {
|
||||||
|
return this.persistedActivityStates[activity.id] === 'in-progress';
|
||||||
|
},
|
||||||
filterActivities(activity) {
|
filterActivities(activity) {
|
||||||
if (this.isEditing) {
|
if (this.isEditing) {
|
||||||
return true;
|
return true;
|
||||||
@ -460,7 +463,8 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isActivityInBounds(activity)) {
|
// An activity may be out of bounds, but if it is in-progress, we show it.
|
||||||
|
if (!this.isActivityInBounds(activity) && !this.isActivityInProgress(activity)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
//current event or future start event or past end event
|
//current event or future start event or past end event
|
||||||
|
@ -21,7 +21,9 @@
|
|||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div ref="axisHolder" class="c-timesystem-axis">
|
<div ref="axisHolder" class="c-timesystem-axis">
|
||||||
<div class="nowMarker" :style="nowMarkerStyle"><span class="icon-arrow-down"></span></div>
|
<div class="nowMarker" :style="nowMarkerStyle" aria-label="Now Marker">
|
||||||
|
<span class="icon-arrow-down"></span>
|
||||||
|
</div>
|
||||||
<svg :width="svgWidth" :height="svgHeight">
|
<svg :width="svgWidth" :height="svgHeight">
|
||||||
<g class="axis" font-size="1.3em" :transform="axisTransform"></g>
|
<g class="axis" font-size="1.3em" :transform="axisTransform"></g>
|
||||||
</svg>
|
</svg>
|
||||||
@ -116,8 +118,10 @@ export default {
|
|||||||
this.axisTransform = `translate(${this.alignmentData.leftWidth + leftOffset}, 20)`;
|
this.axisTransform = `translate(${this.alignmentData.leftWidth + leftOffset}, 20)`;
|
||||||
|
|
||||||
const rightOffset = this.alignmentData.rightWidth ? AXES_PADDING : 0;
|
const rightOffset = this.alignmentData.rightWidth ? AXES_PADDING : 0;
|
||||||
|
|
||||||
|
this.leftAlignmentOffset = this.alignmentData.leftWidth + leftOffset;
|
||||||
this.alignmentOffset =
|
this.alignmentOffset =
|
||||||
this.alignmentData.leftWidth + leftOffset + this.alignmentData.rightWidth + rightOffset;
|
this.leftAlignmentOffset + this.alignmentData.rightWidth + rightOffset;
|
||||||
this.refresh();
|
this.refresh();
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
@ -175,8 +179,8 @@ export default {
|
|||||||
this.nowMarkerStyle.height = this.contentHeight + 'px';
|
this.nowMarkerStyle.height = this.contentHeight + 'px';
|
||||||
const nowTimeStamp = this.openmct.time.now();
|
const nowTimeStamp = this.openmct.time.now();
|
||||||
const now = this.xScale(nowTimeStamp);
|
const now = this.xScale(nowTimeStamp);
|
||||||
this.nowMarkerStyle.left = `${now + this.alignmentOffset}px`;
|
this.nowMarkerStyle.left = `${now + this.leftAlignmentOffset}px`;
|
||||||
if (now > this.width) {
|
if (now < 0 || now > this.width) {
|
||||||
nowMarker.classList.add('hidden');
|
nowMarker.classList.add('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
at runtime from the About dialog for additional information.
|
at runtime from the About dialog for additional information.
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="l-browse-bar">
|
<div class="l-browse-bar" aria-label="Browse bar">
|
||||||
<div class="l-browse-bar__start">
|
<div class="l-browse-bar__start">
|
||||||
<button
|
<button
|
||||||
v-if="hasParent"
|
v-if="hasParent"
|
||||||
@ -35,6 +35,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
ref="objectName"
|
ref="objectName"
|
||||||
|
aria-label="Browse bar object name"
|
||||||
class="l-browse-bar__object-name c-object-label__name"
|
class="l-browse-bar__object-name c-object-label__name"
|
||||||
:class="{ 'c-input-inline': isPersistable }"
|
:class="{ 'c-input-inline': isPersistable }"
|
||||||
:contenteditable="isNameEditable"
|
:contenteditable="isNameEditable"
|
||||||
|
@ -80,13 +80,11 @@ class Browse {
|
|||||||
this.#openmct.layout.$refs.browseBar.viewKey = viewProvider.key;
|
this.#openmct.layout.$refs.browseBar.viewKey = viewProvider.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
#updateDocumentTitleOnNameMutation(newName) {
|
#handleBrowseObjectUpdate(newObject) {
|
||||||
if (typeof newName === 'string' && newName !== document.title) {
|
this.#openmct.layout.$refs.browseBar.domainObject = newObject;
|
||||||
document.title = newName;
|
|
||||||
this.#openmct.layout.$refs.browseBar.domainObject = {
|
if (typeof newObject.name === 'string' && newObject.name !== document.title) {
|
||||||
...this.#openmct.layout.$refs.browseBar.domainObject,
|
document.title = newObject.name;
|
||||||
name: newName
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,8 +118,8 @@ class Browse {
|
|||||||
document.title = this.#browseObject.name; //change document title to current object in main view
|
document.title = this.#browseObject.name; //change document title to current object in main view
|
||||||
this.#unobserve = this.#openmct.objects.observe(
|
this.#unobserve = this.#openmct.objects.observe(
|
||||||
this.#browseObject,
|
this.#browseObject,
|
||||||
'name',
|
'*',
|
||||||
this.#updateDocumentTitleOnNameMutation.bind(this)
|
this.#handleBrowseObjectUpdate.bind(this)
|
||||||
);
|
);
|
||||||
const currentProvider = this.#openmct.objectViews.getByProviderKey(currentViewKey);
|
const currentProvider = this.#openmct.objectViews.getByProviderKey(currentViewKey);
|
||||||
if (currentProvider && currentProvider.canView(this.#browseObject, this.#openmct.router.path)) {
|
if (currentProvider && currentProvider.canView(this.#browseObject, this.#openmct.router.path)) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user