mirror of
https://github.com/nasa/openmct.git
synced 2025-07-04 05:53:23 +00:00
Compare commits
109 Commits
telemetry-
...
event-visu
Author | SHA1 | Date | |
---|---|---|---|
6f97117c42 | |||
2c61739542 | |||
c1f7243263 | |||
5a364cea00 | |||
1ea2cf9ce5 | |||
25ca7d4321 | |||
39ce53f561 | |||
2e35212c32 | |||
8545ddeaff | |||
55d15caa63 | |||
bbae9c6048 | |||
af6f3274cb | |||
b171f82e94 | |||
88b43314d9 | |||
14a5c47efa | |||
8be969fb7a | |||
157bde841e | |||
74a5d7e1f3 | |||
aa5fa468b5 | |||
3ad64f08c5 | |||
1ced12d22f | |||
9e68514b27 | |||
28b5d7c41c | |||
ecd120387c | |||
a6517bb33e | |||
1fde0d9e38 | |||
32a0e15691 | |||
0e940b2883 | |||
15b674f3d1 | |||
0933d27ce6 | |||
f163034e18 | |||
e6cb940ee7 | |||
cfa2129660 | |||
6cafa7a22d | |||
9522040929 | |||
5b28086f95 | |||
bb4fea78f5 | |||
5312458776 | |||
3c24205b67 | |||
65b1f0256d | |||
8c72e4a062 | |||
601fc33e75 | |||
638b03c17d | |||
531ef3ef1b | |||
68fc3172a0 | |||
51d96544ec | |||
546714b3dc | |||
099153ba4e | |||
27af030566 | |||
b865d8c038 | |||
2ae1fe1579 | |||
cba7c7f8ed | |||
49a106b79e | |||
f4ec532357 | |||
72ff0bced6 | |||
36d31973fe | |||
3af9083f89 | |||
2ba6bc9c73 | |||
aaa2e43796 | |||
6bda108e95 | |||
20426fe359 | |||
20247bbd87 | |||
62b4975d57 | |||
d048af108e | |||
cda7cc9d06 | |||
d97f7c347b | |||
781d83410a | |||
64bd625d0b | |||
3d3f093c7e | |||
38292953fc | |||
96d8870f22 | |||
aaec052783 | |||
6f26add740 | |||
052129ba87 | |||
d046ad13ff | |||
e9f120a480 | |||
0db301dea8 | |||
3b236cc33b | |||
8b5e2f4595 | |||
5b006b69b7 | |||
2776cc8ac9 | |||
55bed6a525 | |||
944634d759 | |||
7af3996d29 | |||
7b22cf3371 | |||
5be103ea72 | |||
680b0953b2 | |||
3159de08b1 | |||
d74e1b19b6 | |||
5bb6a18cd4 | |||
14b947c101 | |||
61b982ab99 | |||
ba4d8a428b | |||
ea9947cab5 | |||
2010f2e377 | |||
3241e9ba57 | |||
057a5f997c | |||
078cd341a5 | |||
518b55cf0f | |||
3e23dceb64 | |||
7f8b5e09e5 | |||
7c2bb16bfd | |||
890ddcac4e | |||
d8c5095ebb | |||
ccf7ed91af | |||
2b8673941a | |||
703186adf1 | |||
c43ef64733 | |||
f4cf9c756b |
@ -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
|
||||||
@ -286,8 +286,8 @@ workflows:
|
|||||||
overall-circleci-commit-status: #These jobs run on every commit
|
overall-circleci-commit-status: #These jobs run on every commit
|
||||||
jobs:
|
jobs:
|
||||||
- lint:
|
- lint:
|
||||||
name: node20-lint
|
name: node22-lint
|
||||||
node-version: lts/iron
|
node-version: '22'
|
||||||
- unit-test:
|
- unit-test:
|
||||||
name: node18-chrome
|
name: node18-chrome
|
||||||
node-version: lts/hydrogen
|
node-version: lts/hydrogen
|
||||||
@ -304,8 +304,8 @@ workflows:
|
|||||||
the-nightly: #These jobs do not run on PRs, but against master at night
|
the-nightly: #These jobs do not run on PRs, but against master at night
|
||||||
jobs:
|
jobs:
|
||||||
- unit-test:
|
- unit-test:
|
||||||
name: node20-chrome-nightly
|
name: node22-chrome-nightly
|
||||||
node-version: lts/iron
|
node-version: '22'
|
||||||
- unit-test:
|
- unit-test:
|
||||||
name: node18-chrome
|
name: node18-chrome
|
||||||
node-version: lts/hydrogen
|
node-version: lts/hydrogen
|
||||||
|
@ -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 }}
|
|
@ -510,6 +510,10 @@ async function setTimeConductorBounds(page, { submitChanges = true, ...bounds })
|
|||||||
// Open the time conductor popup
|
// Open the time conductor popup
|
||||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||||
|
|
||||||
|
// FIXME: https://github.com/nasa/openmct/pull/7818
|
||||||
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
if (startDate) {
|
if (startDate) {
|
||||||
await page.getByLabel('Start date').fill(startDate);
|
await page.getByLabel('Start date').fill(startDate);
|
||||||
}
|
}
|
||||||
@ -678,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,
|
||||||
@ -689,6 +708,7 @@ export {
|
|||||||
linkParameterToObject,
|
linkParameterToObject,
|
||||||
navigateToObjectWithFixedTimeBounds,
|
navigateToObjectWithFixedTimeBounds,
|
||||||
navigateToObjectWithRealTime,
|
navigateToObjectWithRealTime,
|
||||||
|
renameCurrentObjectFromBrowseBar,
|
||||||
setEndOffset,
|
setEndOffset,
|
||||||
setFixedIndependentTimeConductorBounds,
|
setFixedIndependentTimeConductorBounds,
|
||||||
setFixedTimeMode,
|
setFixedTimeMode,
|
||||||
|
@ -129,6 +129,7 @@ export async function setBoundsToSpanAllActivities(page, planJson, planObjectUrl
|
|||||||
*/
|
*/
|
||||||
export function getEarliestStartTime(planJson) {
|
export function getEarliestStartTime(planJson) {
|
||||||
const activities = Object.values(planJson).flat();
|
const activities = Object.values(planJson).flat();
|
||||||
|
|
||||||
return Math.min(...activities.map((activity) => activity.start));
|
return Math.min(...activities.map((activity) => activity.start));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,6 +140,7 @@ export function getEarliestStartTime(planJson) {
|
|||||||
*/
|
*/
|
||||||
export function getLatestEndTime(planJson) {
|
export function getLatestEndTime(planJson) {
|
||||||
const activities = Object.values(planJson).flat();
|
const activities = Object.values(planJson).flat();
|
||||||
|
|
||||||
return Math.max(...activities.map((activity) => activity.end));
|
return Math.max(...activities.map((activity) => activity.end));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,6 +153,7 @@ export function getFirstActivity(planJson) {
|
|||||||
const groups = Object.keys(planJson);
|
const groups = Object.keys(planJson);
|
||||||
const firstGroupKey = groups[0];
|
const firstGroupKey = groups[0];
|
||||||
const firstGroupItems = planJson[firstGroupKey];
|
const firstGroupItems = planJson[firstGroupKey];
|
||||||
|
|
||||||
return firstGroupItems[0];
|
return firstGroupItems[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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": {
|
||||||
|
BIN
e2e/test-data/rick space roll.jpg
Normal file
BIN
e2e/test-data/rick space roll.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -126,7 +126,7 @@ test.describe('Gantt Chart', () => {
|
|||||||
await page.goto(ganttChart.url);
|
await page.goto(ganttChart.url);
|
||||||
|
|
||||||
// Assert that the Plan's status is displayed as draft
|
// Assert that the Plan's status is displayed as draft
|
||||||
expect(await page.locator('.u-contents.c-swimlane.is-status--draft').count()).toBe(
|
expect(await page.locator('.c-swimlane.is-status--draft').count()).toBe(
|
||||||
Object.keys(testPlan1).length
|
Object.keys(testPlan1).length
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
|
110
e2e/tests/functional/plugins/event/eventTimelineView.e2e.spec.js
Normal file
110
e2e/tests/functional/plugins/event/eventTimelineView.e2e.spec.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import { createDomainObjectWithDefaults, setTimeConductorBounds } from '../../../../appActions.js';
|
||||||
|
import { expect, test } from '../../../../pluginFixtures.js';
|
||||||
|
|
||||||
|
test.describe('Event Timeline View', () => {
|
||||||
|
let eventTimelineView;
|
||||||
|
let eventGenerator1;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
eventTimelineView = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Time Strip'
|
||||||
|
});
|
||||||
|
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Sine Wave Generator',
|
||||||
|
parent: eventTimelineView.uuid
|
||||||
|
});
|
||||||
|
|
||||||
|
eventGenerator1 = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Event Message Generator',
|
||||||
|
parent: eventTimelineView.uuid
|
||||||
|
});
|
||||||
|
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Event Message Generator with Acknowledge',
|
||||||
|
parent: eventTimelineView.uuid
|
||||||
|
});
|
||||||
|
|
||||||
|
await setTimeConductorBounds(page, {
|
||||||
|
startDate: '2024-01-01',
|
||||||
|
endDate: '2024-01-01',
|
||||||
|
startTime: '01:01:00',
|
||||||
|
endTime: '01:04:00'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Ensure we can build a Time Strip with event', async ({ page }) => {
|
||||||
|
await page.goto(eventTimelineView.url);
|
||||||
|
|
||||||
|
// click on an event
|
||||||
|
await page
|
||||||
|
.getByLabel(eventTimelineView.name)
|
||||||
|
.getByLabel(/PROGRAM ALARM/)
|
||||||
|
.click();
|
||||||
|
|
||||||
|
// click on the event inspector tab
|
||||||
|
await page.getByRole('tab', { name: 'Event' }).click();
|
||||||
|
|
||||||
|
// ensure the event inspector has the the same event
|
||||||
|
await expect(page.getByText(/PROGRAM ALARM/)).toBeVisible();
|
||||||
|
|
||||||
|
// count the event lines
|
||||||
|
const eventWrappersContainer = page.locator('.c-events-tsv__container');
|
||||||
|
const eventWrappers = eventWrappersContainer.locator('.c-events-tsv__event-line');
|
||||||
|
const expectedEventWrappersCount = 25;
|
||||||
|
await expect(eventWrappers).toHaveCount(expectedEventWrappersCount);
|
||||||
|
|
||||||
|
// click on another event
|
||||||
|
await page
|
||||||
|
.getByLabel(eventTimelineView.name)
|
||||||
|
.getByLabel(/pegged/)
|
||||||
|
.click();
|
||||||
|
|
||||||
|
// ensure the tooltip shows up
|
||||||
|
await expect(
|
||||||
|
page.getByRole('tooltip').getByText(/pegged on horizontal velocity/)
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// and that event appears in the inspector
|
||||||
|
await expect(
|
||||||
|
page.getByLabel('Inspector Views').getByText(/pegged on horizontal velocity/)
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// turn on extended lines
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: `Toggle extended event lines overlay for ${eventGenerator1.name}`
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
|
||||||
|
// count the extended lines
|
||||||
|
const overlayLinesContainer = page.locator('.c-timeline__overlay-lines');
|
||||||
|
const extendedLines = overlayLinesContainer.locator('.c-timeline__event-line--extended');
|
||||||
|
const expectedCount = 25;
|
||||||
|
await expect(extendedLines).toHaveCount(expectedCount);
|
||||||
|
});
|
||||||
|
});
|
@ -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, {
|
||||||
|
@ -96,9 +96,6 @@ test.describe('Example Imagery Object', () => {
|
|||||||
expect(newPage.url()).toContain('.jpg');
|
expect(newPage.url()).toContain('.jpg');
|
||||||
});
|
});
|
||||||
|
|
||||||
// this requires CORS to be enabled in some fashion
|
|
||||||
test.fixme('Can right click on image and save it as a file', async ({ page }) => {});
|
|
||||||
|
|
||||||
test('Can adjust image brightness/contrast by dragging the sliders', async ({
|
test('Can adjust image brightness/contrast by dragging the sliders', async ({
|
||||||
page,
|
page,
|
||||||
browserName
|
browserName
|
||||||
|
@ -0,0 +1,93 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This test suite verifies modifying the image location of the example imagery object.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||||
|
import { expect, test } from '../../../../pluginFixtures.js';
|
||||||
|
|
||||||
|
test.describe('Example Imagery Object Custom Images', () => {
|
||||||
|
let exampleImagery;
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
//Go to baseURL
|
||||||
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
// Create a default 'Example Imagery' object
|
||||||
|
exampleImagery = await createDomainObjectWithDefaults(page, {
|
||||||
|
name: 'Example Imagery',
|
||||||
|
type: 'Example Imagery'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify that the created object is focused
|
||||||
|
await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);
|
||||||
|
await page.getByLabel('Focused Image Element').hover({ trial: true });
|
||||||
|
|
||||||
|
// Wait for image thumbnail auto-scroll to complete
|
||||||
|
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||||
|
});
|
||||||
|
// this requires CORS to be enabled in some fashion
|
||||||
|
test.fixme('Can right click on image and save it as a file', async ({ page }) => {});
|
||||||
|
test('Can provide a custom image location for the example imagery object', async ({ page }) => {
|
||||||
|
// Modify Example Imagery to create a really stable image which will never let us down
|
||||||
|
await page.getByRole('button', { name: 'More actions' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
|
||||||
|
await page
|
||||||
|
.locator('#imageLocation-textarea')
|
||||||
|
.fill(
|
||||||
|
'https://raw.githubusercontent.com/nasa/openmct/554f77c42fec81cf0f63e62b278012cb08d82af9/e2e/test-data/rick.jpg,https://raw.githubusercontent.com/nasa/openmct/554f77c42fec81cf0f63e62b278012cb08d82af9/e2e/test-data/rick.jpg'
|
||||||
|
);
|
||||||
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
// Wait for the thumbnails to finish their scroll animation
|
||||||
|
// (Wait until the rightmost thumbnail is in view)
|
||||||
|
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Image Wrapper')).toBeVisible();
|
||||||
|
});
|
||||||
|
test.fixme('Can provide a custom image with spaces in name', async ({ page }) => {
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/7903'
|
||||||
|
});
|
||||||
|
await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
// Modify Example Imagery to create a really stable image which will never let us down
|
||||||
|
await page.getByRole('button', { name: 'More actions' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
|
||||||
|
await page
|
||||||
|
.locator('#imageLocation-textarea')
|
||||||
|
.fill(
|
||||||
|
'https://raw.githubusercontent.com/nasa/openmct/d8c64f183400afb70137221fc1a035e091bea912/e2e/test-data/rick%20space%20roll.jpg'
|
||||||
|
);
|
||||||
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
// Wait for the thumbnails to finish their scroll animation
|
||||||
|
// (Wait until the rightmost thumbnail is in view)
|
||||||
|
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Image Wrapper')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
@ -57,7 +57,7 @@ test.describe('Tabs View', () => {
|
|||||||
await page.goto(tabsView.url);
|
await page.goto(tabsView.url);
|
||||||
|
|
||||||
// select first tab
|
// select first tab
|
||||||
await page.getByLabel(`${table.name} tab`, { exact: true }).click();
|
await page.getByLabel(`${table.name} tab - selected`, { exact: true }).click();
|
||||||
// ensure table header visible
|
// ensure table header visible
|
||||||
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
|
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
|
||||||
|
|
||||||
@ -92,6 +92,38 @@ test.describe('Tabs View', () => {
|
|||||||
// no canvas (i.e., sine wave generator) in the document should be visible
|
// no canvas (i.e., sine wave generator) in the document should be visible
|
||||||
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
|
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Changing the displayed tab should not be persisted if the view is locked', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
await page.goto(tabsView.url);
|
||||||
|
//lock the view
|
||||||
|
await page.getByLabel('Unlocked for editing, click to lock.', { exact: true }).click();
|
||||||
|
// get the initial tab index
|
||||||
|
const initialTab = page.getByLabel(/- selected/);
|
||||||
|
// switch to a different tab in the view
|
||||||
|
const swgTab = page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true });
|
||||||
|
await swgTab.click();
|
||||||
|
await page.getByLabel(`${sineWaveGenerator.name} Object View`).isVisible();
|
||||||
|
// navigate away from the tabbed view and back
|
||||||
|
await page.getByRole('treeitem', { name: 'My Items' }).click();
|
||||||
|
await page.goto(tabsView.url);
|
||||||
|
// check that the initial tab is displayed
|
||||||
|
const lockedSelectedTab = page.getByLabel(/- selected/);
|
||||||
|
await expect(lockedSelectedTab).toHaveText(await initialTab.textContent());
|
||||||
|
|
||||||
|
//unlock the view
|
||||||
|
await page.getByLabel('Locked for editing. Click to unlock.', { exact: true }).click();
|
||||||
|
// switch to a different tab in the view
|
||||||
|
await swgTab.click();
|
||||||
|
await page.getByLabel(`${sineWaveGenerator.name} Object View`).isVisible();
|
||||||
|
// navigate away from the tabbed view and back
|
||||||
|
await page.getByRole('treeitem', { name: 'My Items' }).click();
|
||||||
|
await page.goto(tabsView.url);
|
||||||
|
// check that the newly selected tab is displayed
|
||||||
|
const unlockedSelectedTab = page.getByLabel(/- selected/);
|
||||||
|
await expect(unlockedSelectedTab).toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Tabs View CRUD', () => {
|
test.describe('Tabs View CRUD', () => {
|
||||||
|
@ -117,7 +117,8 @@ test.describe('Telemetry Table', () => {
|
|||||||
|
|
||||||
endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() - 5);
|
endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() - 5);
|
||||||
const endDate = endTimeStamp.toISOString().split('T')[0];
|
const endDate = endTimeStamp.toISOString().split('T')[0];
|
||||||
const endTime = endTimeStamp.toISOString().split('T')[1];
|
const milliseconds = endTimeStamp.getMilliseconds();
|
||||||
|
const endTime = endTimeStamp.toISOString().split('T')[1].replace(`.${milliseconds}Z`, '');
|
||||||
|
|
||||||
await setTimeConductorBounds(page, { endDate, endTime });
|
await setTimeConductorBounds(page, { endDate, endTime });
|
||||||
|
|
||||||
|
@ -24,65 +24,210 @@ import {
|
|||||||
setEndOffset,
|
setEndOffset,
|
||||||
setFixedTimeMode,
|
setFixedTimeMode,
|
||||||
setRealTimeMode,
|
setRealTimeMode,
|
||||||
setStartOffset,
|
setStartOffset
|
||||||
setTimeConductorBounds
|
|
||||||
} from '../../../../appActions.js';
|
} from '../../../../appActions.js';
|
||||||
import { expect, test } from '../../../../pluginFixtures.js';
|
import { expect, test } from '../../../../pluginFixtures.js';
|
||||||
|
|
||||||
test.describe('Time conductor operations', () => {
|
test.describe('Time conductor operations', () => {
|
||||||
test('validate start time does not exceed end time', async ({ page }) => {
|
const DAY = '2024-01-01';
|
||||||
|
const DAY_AFTER = '2024-01-02';
|
||||||
|
const ONE_O_CLOCK = '01:00:00';
|
||||||
|
const TWO_O_CLOCK = '02:00:00';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
// Go to baseURL
|
// Go to baseURL
|
||||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
const year = new Date().getFullYear();
|
});
|
||||||
|
|
||||||
// Set initial valid time bounds
|
test('validate date and time inputs are validated on input event', async ({ page }) => {
|
||||||
const startDate = `${year}-01-01`;
|
const submitButtonLocator = page.getByLabel('Submit time bounds');
|
||||||
const startTime = '01:00:00';
|
|
||||||
const endDate = `${year}-01-01`;
|
|
||||||
const endTime = '02:00:00';
|
|
||||||
await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime });
|
|
||||||
|
|
||||||
// Open the time conductor popup
|
// Open the time conductor popup
|
||||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||||
|
|
||||||
// Test invalid start date
|
await test.step('invalid start date disables submit button', async () => {
|
||||||
const invalidStartDate = `${year}-01-02`;
|
const initialStartDate = await page.getByLabel('Start date').inputValue();
|
||||||
|
const invalidStartDate = `${initialStartDate.substring(0, 5)}${initialStartDate.substring(6)}`;
|
||||||
|
|
||||||
await page.getByLabel('Start date').fill(invalidStartDate);
|
await page.getByLabel('Start date').fill(invalidStartDate);
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
await expect(submitButtonLocator).toBeDisabled();
|
||||||
await page.getByLabel('Start date').fill(startDate);
|
await page.getByLabel('Start date').fill(initialStartDate);
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
await expect(submitButtonLocator).toBeEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
// Test invalid end date
|
await test.step('invalid start time disables submit button', async () => {
|
||||||
const invalidEndDate = `${year - 1}-12-31`;
|
const initialStartTime = await page.getByLabel('Start time').inputValue();
|
||||||
await page.getByLabel('End date').fill(invalidEndDate);
|
const invalidStartTime = `${initialStartTime.substring(0, 5)}${initialStartTime.substring(6)}`;
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
|
||||||
await page.getByLabel('End date').fill(endDate);
|
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
|
||||||
|
|
||||||
// Test invalid start time
|
|
||||||
const invalidStartTime = '42:00:00';
|
|
||||||
await page.getByLabel('Start time').fill(invalidStartTime);
|
await page.getByLabel('Start time').fill(invalidStartTime);
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
await expect(submitButtonLocator).toBeDisabled();
|
||||||
await page.getByLabel('Start time').fill(startTime);
|
await page.getByLabel('Start time').fill(initialStartTime);
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
await expect(submitButtonLocator).toBeEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('disable/enable submit button also works with multiple invalid inputs', async () => {
|
||||||
|
const initialEndDate = await page.getByLabel('End date').inputValue();
|
||||||
|
const invalidEndDate = `${initialEndDate.substring(0, 5)}${initialEndDate.substring(6)}`;
|
||||||
|
const initialStartTime = await page.getByLabel('Start time').inputValue();
|
||||||
|
const invalidStartTime = `${initialStartTime.substring(0, 5)}${initialStartTime.substring(6)}`;
|
||||||
|
|
||||||
|
await page.getByLabel('Start time').fill(invalidStartTime);
|
||||||
|
await expect(submitButtonLocator).toBeDisabled();
|
||||||
|
await page.getByLabel('End date').fill(invalidEndDate);
|
||||||
|
await expect(submitButtonLocator).toBeDisabled();
|
||||||
|
await page.getByLabel('End date').fill(initialEndDate);
|
||||||
|
await expect(submitButtonLocator).toBeDisabled();
|
||||||
|
await page.getByLabel('Start time').fill(initialStartTime);
|
||||||
|
await expect(submitButtonLocator).toBeEnabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validate date and time inputs validation is reported on change event', async ({ page }) => {
|
||||||
|
// Open the time conductor popup
|
||||||
|
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||||
|
|
||||||
|
await test.step('invalid start date is reported on change event, not on input event', async () => {
|
||||||
|
const initialStartDate = await page.getByLabel('Start date').inputValue();
|
||||||
|
const invalidStartDate = `${initialStartDate.substring(0, 5)}${initialStartDate.substring(6)}`;
|
||||||
|
|
||||||
|
await page.getByLabel('Start date').fill(invalidStartDate);
|
||||||
|
await expect(page.getByLabel('Start date')).not.toHaveAttribute('title', 'Invalid Date');
|
||||||
|
await page.getByLabel('Start date').press('Tab');
|
||||||
|
await expect(page.getByLabel('Start date')).toHaveAttribute('title', 'Invalid Date');
|
||||||
|
await page.getByLabel('Start date').fill(initialStartDate);
|
||||||
|
await expect(page.getByLabel('Start date')).not.toHaveAttribute('title', 'Invalid Date');
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('invalid start time is reported on change event, not on input event', async () => {
|
||||||
|
const initialStartTime = await page.getByLabel('Start time').inputValue();
|
||||||
|
const invalidStartTime = `${initialStartTime.substring(0, 5)}${initialStartTime.substring(6)}`;
|
||||||
|
|
||||||
|
await page.getByLabel('Start time').fill(invalidStartTime);
|
||||||
|
await expect(page.getByLabel('Start time')).not.toHaveAttribute('title', 'Invalid Time');
|
||||||
|
await page.getByLabel('Start time').press('Tab');
|
||||||
|
await expect(page.getByLabel('Start time')).toHaveAttribute('title', 'Invalid Time');
|
||||||
|
await page.getByLabel('Start time').fill(initialStartTime);
|
||||||
|
await expect(page.getByLabel('Start time')).not.toHaveAttribute('title', 'Invalid Time');
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('invalid end date is reported on change event, not on input event', async () => {
|
||||||
|
const initialEndDate = await page.getByLabel('End date').inputValue();
|
||||||
|
const invalidEndDate = `${initialEndDate.substring(0, 5)}${initialEndDate.substring(6)}`;
|
||||||
|
|
||||||
|
await page.getByLabel('End date').fill(invalidEndDate);
|
||||||
|
await expect(page.getByLabel('End date')).not.toHaveAttribute('title', 'Invalid Date');
|
||||||
|
await page.getByLabel('End date').press('Tab');
|
||||||
|
await expect(page.getByLabel('End date')).toHaveAttribute('title', 'Invalid Date');
|
||||||
|
await page.getByLabel('End date').fill(initialEndDate);
|
||||||
|
await expect(page.getByLabel('End date')).not.toHaveAttribute('title', 'Invalid Date');
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('invalid end time is reported on change event, not on input event', async () => {
|
||||||
|
const initialEndTime = await page.getByLabel('End time').inputValue();
|
||||||
|
const invalidEndTime = `${initialEndTime.substring(0, 5)}${initialEndTime.substring(6)}`;
|
||||||
|
|
||||||
// Test invalid end time
|
|
||||||
const invalidEndTime = '43:00:00';
|
|
||||||
await page.getByLabel('End time').fill(invalidEndTime);
|
await page.getByLabel('End time').fill(invalidEndTime);
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
await expect(page.getByLabel('End time')).not.toHaveAttribute('title', 'Invalid Time');
|
||||||
await page.getByLabel('End time').fill(endTime);
|
await page.getByLabel('End time').press('Tab');
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
await expect(page.getByLabel('End time')).toHaveAttribute('title', 'Invalid Time');
|
||||||
|
await page.getByLabel('End time').fill(initialEndTime);
|
||||||
|
await expect(page.getByLabel('End time')).not.toHaveAttribute('title', 'Invalid Time');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Submit valid time bounds
|
test('validate start time does not exceed end time on submit', async ({ page }) => {
|
||||||
|
// Open the time conductor popup
|
||||||
|
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||||
|
|
||||||
|
// FIXME: https://github.com/nasa/openmct/pull/7818
|
||||||
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
await page.getByLabel('Start date').fill(DAY);
|
||||||
|
await page.getByLabel('Start time').fill(TWO_O_CLOCK);
|
||||||
|
await page.getByLabel('End date').fill(DAY);
|
||||||
|
await page.getByLabel('End time').fill(ONE_O_CLOCK);
|
||||||
await page.getByLabel('Submit time bounds').click();
|
await page.getByLabel('Submit time bounds').click();
|
||||||
|
|
||||||
// Verify the submitted time bounds
|
await expect(page.getByLabel('Start date')).toHaveAttribute(
|
||||||
await expect(page.getByLabel('Start bounds')).toHaveText(
|
'title',
|
||||||
new RegExp(`${startDate} ${startTime}.000Z`)
|
'Specified start date exceeds end bound'
|
||||||
);
|
);
|
||||||
await expect(page.getByLabel('End bounds')).toHaveText(
|
await expect(page.getByLabel('Start bounds')).not.toHaveText(`${DAY} ${TWO_O_CLOCK}.000Z`);
|
||||||
new RegExp(`${endDate} ${endTime}.000Z`)
|
await expect(page.getByLabel('End bounds')).not.toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
|
||||||
|
|
||||||
|
await page.getByLabel('Start date').fill(DAY);
|
||||||
|
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
|
||||||
|
await page.getByLabel('End date').fill(DAY);
|
||||||
|
await page.getByLabel('End time').fill(TWO_O_CLOCK);
|
||||||
|
await page.getByLabel('Submit time bounds').click();
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Start bounds')).toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
|
||||||
|
await expect(page.getByLabel('End bounds')).toHaveText(`${DAY} ${TWO_O_CLOCK}.000Z`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validate start datetime does not exceed end datetime on submit', async ({ page }) => {
|
||||||
|
// Open the time conductor popup
|
||||||
|
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||||
|
|
||||||
|
// FIXME: https://github.com/nasa/openmct/pull/7818
|
||||||
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
await page.getByLabel('Start date').fill(DAY_AFTER);
|
||||||
|
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
|
||||||
|
await page.getByLabel('End date').fill(DAY);
|
||||||
|
await page.getByLabel('End time').fill(ONE_O_CLOCK);
|
||||||
|
await page.getByLabel('Submit time bounds').click();
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Start date')).toHaveAttribute(
|
||||||
|
'title',
|
||||||
|
'Specified start date exceeds end bound'
|
||||||
);
|
);
|
||||||
|
await expect(page.getByLabel('Start bounds')).not.toHaveText(
|
||||||
|
`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`
|
||||||
|
);
|
||||||
|
await expect(page.getByLabel('End bounds')).not.toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
|
||||||
|
|
||||||
|
await page.getByLabel('Start date').fill(DAY);
|
||||||
|
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
|
||||||
|
await page.getByLabel('End date').fill(DAY_AFTER);
|
||||||
|
await page.getByLabel('End time').fill(ONE_O_CLOCK);
|
||||||
|
await page.getByLabel('Submit time bounds').click();
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Start bounds')).toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
|
||||||
|
await expect(page.getByLabel('End bounds')).toHaveText(`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cancelling form does not set bounds', async ({ page }) => {
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/7791'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open the time conductor popup
|
||||||
|
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||||
|
|
||||||
|
await page.getByLabel('Start date').fill(DAY);
|
||||||
|
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
|
||||||
|
await page.getByLabel('End date').fill(DAY_AFTER);
|
||||||
|
await page.getByLabel('End time').fill(ONE_O_CLOCK);
|
||||||
|
await page.getByLabel('Discard changes and close time popup').click();
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Start bounds')).not.toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
|
||||||
|
await expect(page.getByLabel('End bounds')).not.toHaveText(`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`);
|
||||||
|
|
||||||
|
// Open the time conductor popup
|
||||||
|
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||||
|
|
||||||
|
await page.getByLabel('Start date').fill(DAY);
|
||||||
|
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
|
||||||
|
await page.getByLabel('End date').fill(DAY_AFTER);
|
||||||
|
await page.getByLabel('End time').fill(ONE_O_CLOCK);
|
||||||
|
await page.getByLabel('Submit time bounds').click();
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Start bounds')).toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
|
||||||
|
await expect(page.getByLabel('End bounds')).toHaveText(`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -131,77 +276,6 @@ test.describe('Global Time Conductor', () => {
|
|||||||
await expect(page.getByLabel('End offset: 01:30:31')).toBeVisible();
|
await expect(page.getByLabel('End offset: 01:30:31')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Input field validation: fixed time mode', async ({ page }) => {
|
|
||||||
test.info().annotations.push({
|
|
||||||
type: 'issue',
|
|
||||||
description: 'https://github.com/nasa/openmct/issues/7791'
|
|
||||||
});
|
|
||||||
// Switch to fixed time mode
|
|
||||||
await setFixedTimeMode(page);
|
|
||||||
|
|
||||||
// Define valid time bounds for testing
|
|
||||||
const validBounds = {
|
|
||||||
startDate: '2024-04-20',
|
|
||||||
startTime: '00:04:20',
|
|
||||||
endDate: '2024-04-20',
|
|
||||||
endTime: '16:04:20'
|
|
||||||
};
|
|
||||||
// Set valid time conductor bounds ✌️
|
|
||||||
await setTimeConductorBounds(page, validBounds);
|
|
||||||
|
|
||||||
// Verify that the time bounds are set correctly
|
|
||||||
await expect(page.getByLabel(`Start bounds: 2024-04-20 00:04:20.000Z`)).toBeVisible();
|
|
||||||
await expect(page.getByLabel(`End bounds: 2024-04-20 16:04:20.000Z`)).toBeVisible();
|
|
||||||
|
|
||||||
// Open the Time Conductor Mode popup
|
|
||||||
await page.getByLabel('Time Conductor Mode').click();
|
|
||||||
|
|
||||||
// Test invalid start date
|
|
||||||
const invalidStartDate = '2024-04-21';
|
|
||||||
await page.getByLabel('Start date').fill(invalidStartDate);
|
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
|
||||||
await page.getByLabel('Start date').fill(validBounds.startDate);
|
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
|
||||||
|
|
||||||
// Test invalid end date
|
|
||||||
const invalidEndDate = '2024-04-19';
|
|
||||||
await page.getByLabel('End date').fill(invalidEndDate);
|
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
|
||||||
await page.getByLabel('End date').fill(validBounds.endDate);
|
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
|
||||||
|
|
||||||
// Test invalid start time
|
|
||||||
const invalidStartTime = '16:04:21';
|
|
||||||
await page.getByLabel('Start time').fill(invalidStartTime);
|
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
|
||||||
await page.getByLabel('Start time').fill(validBounds.startTime);
|
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
|
||||||
|
|
||||||
// Test invalid end time
|
|
||||||
const invalidEndTime = '00:04:19';
|
|
||||||
await page.getByLabel('End time').fill(invalidEndTime);
|
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
|
|
||||||
await page.getByLabel('End time').fill(validBounds.endTime);
|
|
||||||
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
|
|
||||||
|
|
||||||
// Verify that the time bounds remain unchanged after invalid inputs
|
|
||||||
await expect(page.getByLabel(`Start bounds: 2024-04-20 00:04:20.000Z`)).toBeVisible();
|
|
||||||
await expect(page.getByLabel(`End bounds: 2024-04-20 16:04:20.000Z`)).toBeVisible();
|
|
||||||
|
|
||||||
// Discard changes and verify that bounds remain unchanged
|
|
||||||
await setTimeConductorBounds(page, {
|
|
||||||
startDate: validBounds.startDate,
|
|
||||||
startTime: '04:20:00',
|
|
||||||
endDate: validBounds.endDate,
|
|
||||||
endTime: '04:20:20',
|
|
||||||
submitChanges: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify that the original time bounds are still displayed after discarding changes
|
|
||||||
await expect(page.getByLabel(`Start bounds: 2024-04-20 00:04:20.000Z`)).toBeVisible();
|
|
||||||
await expect(page.getByLabel(`End bounds: 2024-04-20 16:04:20.000Z`)).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify that offsets and url params are preserved when switching
|
* Verify that offsets and url params are preserved when switching
|
||||||
* between fixed timespan and real-time mode.
|
* between fixed timespan and real-time mode.
|
||||||
|
@ -64,7 +64,7 @@ test.describe('Tabs View', () => {
|
|||||||
page.goto(tabsView.url);
|
page.goto(tabsView.url);
|
||||||
|
|
||||||
// select first tab
|
// select first tab
|
||||||
await page.getByLabel(`${table.name} tab`, { exact: true }).click();
|
await page.getByLabel(`${table.name} tab - selected`, { exact: true }).click();
|
||||||
// ensure table header visible
|
// ensure table header visible
|
||||||
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
|
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
|
||||||
|
|
||||||
|
@ -26,14 +26,25 @@ import fs from 'fs';
|
|||||||
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appActions.js';
|
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appActions.js';
|
||||||
import { scanForA11yViolations, test } from '../../avpFixtures.js';
|
import { scanForA11yViolations, test } from '../../avpFixtures.js';
|
||||||
import { VISUAL_FIXED_URL } from '../../constants.js';
|
import { VISUAL_FIXED_URL } from '../../constants.js';
|
||||||
import { setBoundsToSpanAllActivities, setDraftStatusForPlan } from '../../helper/planningUtils.js';
|
import {
|
||||||
|
getFirstActivity,
|
||||||
|
setBoundsToSpanAllActivities,
|
||||||
|
setDraftStatusForPlan
|
||||||
|
} from '../../helper/planningUtils.js';
|
||||||
|
|
||||||
const examplePlanSmall2 = JSON.parse(
|
const examplePlanSmall2 = JSON.parse(
|
||||||
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))
|
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const FIRST_ACTIVITY_SMALL_2 = getFirstActivity(examplePlanSmall2);
|
||||||
|
|
||||||
test.describe('Visual - Gantt Chart @a11y', () => {
|
test.describe('Visual - Gantt Chart @a11y', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// Set the clock to the end of the first activity in the plan
|
||||||
|
// This is so we can see the "now" line in the plan view
|
||||||
|
await page.clock.install({ time: FIRST_ACTIVITY_SMALL_2.end + 10000 });
|
||||||
|
await page.clock.resume();
|
||||||
|
|
||||||
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
|
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
|
||||||
});
|
});
|
||||||
test('Gantt Chart View', async ({ page, theme }) => {
|
test('Gantt Chart View', async ({ page, theme }) => {
|
||||||
|
@ -27,14 +27,21 @@ import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appAct
|
|||||||
import { scanForA11yViolations, test } from '../../avpFixtures.js';
|
import { scanForA11yViolations, test } from '../../avpFixtures.js';
|
||||||
import { waitForAnimations } from '../../baseFixtures.js';
|
import { waitForAnimations } from '../../baseFixtures.js';
|
||||||
import { VISUAL_FIXED_URL } from '../../constants.js';
|
import { VISUAL_FIXED_URL } from '../../constants.js';
|
||||||
import { setBoundsToSpanAllActivities } from '../../helper/planningUtils.js';
|
import { getFirstActivity, setBoundsToSpanAllActivities } from '../../helper/planningUtils.js';
|
||||||
|
|
||||||
const examplePlanSmall2 = JSON.parse(
|
const examplePlanSmall2 = JSON.parse(
|
||||||
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))
|
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const FIRST_ACTIVITY_SMALL_2 = getFirstActivity(examplePlanSmall2);
|
||||||
|
|
||||||
test.describe('Visual - Time Strip @a11y', () => {
|
test.describe('Visual - Time Strip @a11y', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// Set the clock to the end of the first activity in the plan
|
||||||
|
// This is so we can see the "now" line in the plan view
|
||||||
|
await page.clock.install({ time: FIRST_ACTIVITY_SMALL_2.end + 10000 });
|
||||||
|
await page.clock.resume();
|
||||||
|
|
||||||
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
|
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
|
||||||
});
|
});
|
||||||
test('Time Strip View', async ({ page, theme }) => {
|
test('Time Strip View', async ({ page, theme }) => {
|
||||||
|
@ -42,6 +42,7 @@ const examplePlanSmall2 = JSON.parse(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const FIRST_ACTIVITY_SMALL_1 = getFirstActivity(examplePlanSmall1);
|
const FIRST_ACTIVITY_SMALL_1 = getFirstActivity(examplePlanSmall1);
|
||||||
|
const FIRST_ACTIVITY_SMALL_2 = getFirstActivity(examplePlanSmall2);
|
||||||
|
|
||||||
test.describe('Visual - Timelist progress bar @clock @a11y', () => {
|
test.describe('Visual - Timelist progress bar @clock @a11y', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
@ -59,6 +60,11 @@ test.describe('Visual - Timelist progress bar @clock @a11y', () => {
|
|||||||
|
|
||||||
test.describe('Visual - Plan View @a11y', () => {
|
test.describe('Visual - Plan View @a11y', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// Set the clock to the end of the first activity in the plan
|
||||||
|
// This is so we can see the "now" line in the plan view
|
||||||
|
await page.clock.install({ time: FIRST_ACTIVITY_SMALL_2.end + 10000 });
|
||||||
|
await page.clock.resume();
|
||||||
|
|
||||||
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
|
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
88
example/eventGenerator/EventLimitProvider.js
Normal file
88
example/eventGenerator/EventLimitProvider.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
export const SEVERITY_CSS = {
|
||||||
|
WATCH: 'is-event--yellow',
|
||||||
|
WARNING: 'is-event--yellow',
|
||||||
|
DISTRESS: 'is-event--red',
|
||||||
|
CRITICAL: 'is-event--red',
|
||||||
|
SEVERE: 'is-event--purple'
|
||||||
|
};
|
||||||
|
|
||||||
|
const NOMINAL_SEVERITY = {
|
||||||
|
cssClass: 'is-event--no-style',
|
||||||
|
name: 'NOMINAL'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} EvaluationResult
|
||||||
|
* @property {string} cssClass CSS class information
|
||||||
|
* @property {string} name a severity name
|
||||||
|
*/
|
||||||
|
export default class EventLimitProvider {
|
||||||
|
constructor(openmct) {
|
||||||
|
this.openmct = openmct;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLimitEvaluator(domainObject) {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Evaluates a telemetry datum for severity.
|
||||||
|
*
|
||||||
|
* @param {Datum} datum the telemetry datum from the historical or realtime plugin ({@link Datum})
|
||||||
|
* @param {object} valueMetadata metadata about the telemetry datum
|
||||||
|
*
|
||||||
|
* @returns {EvaluationResult} ({@link EvaluationResult})
|
||||||
|
*/
|
||||||
|
evaluate: function (datum, valueMetadata) {
|
||||||
|
// prevent applying the class to the tr, only to td
|
||||||
|
if (!valueMetadata) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (datum.severity in SEVERITY_CSS) {
|
||||||
|
return self.getSeverity(datum, valueMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NOMINAL_SEVERITY;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
getSeverity(datum, valueMetadata) {
|
||||||
|
if (!valueMetadata) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const severityValue = datum.severity;
|
||||||
|
|
||||||
|
return {
|
||||||
|
cssClass: SEVERITY_CSS[severityValue],
|
||||||
|
name: severityValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
supportsLimits(domainObject) {
|
||||||
|
return domainObject.type === 'eventGenerator';
|
||||||
|
}
|
||||||
|
}
|
@ -41,11 +41,34 @@ class EventMetadataProvider {
|
|||||||
{
|
{
|
||||||
key: 'message',
|
key: 'message',
|
||||||
name: 'Message',
|
name: 'Message',
|
||||||
format: 'string'
|
format: 'string',
|
||||||
|
hints: {
|
||||||
|
// this is used in the EventTimelineView to provide a title for the event
|
||||||
|
// label can be changed to other properties for the title (e.g., the `name` property)
|
||||||
|
label: 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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) {
|
||||||
|
@ -24,8 +24,11 @@
|
|||||||
* Module defining EventTelemetryProvider. Created by chacskaylo on 06/18/2015.
|
* Module defining EventTelemetryProvider. Created by chacskaylo on 06/18/2015.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { SEVERITY_CSS } from './EventLimitProvider.js';
|
||||||
import messages from './transcript.json';
|
import messages from './transcript.json';
|
||||||
|
|
||||||
|
const DUR_MIN = 1000;
|
||||||
|
const DUR_MAX = 10000;
|
||||||
class EventTelemetryProvider {
|
class EventTelemetryProvider {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.defaultSize = 25;
|
this.defaultSize = 25;
|
||||||
@ -33,14 +36,23 @@ class EventTelemetryProvider {
|
|||||||
|
|
||||||
generateData(firstObservedTime, count, startTime, duration, name) {
|
generateData(firstObservedTime, count, startTime, duration, name) {
|
||||||
const millisecondsSinceStart = startTime - firstObservedTime;
|
const millisecondsSinceStart = startTime - firstObservedTime;
|
||||||
const utc = startTime + count * duration;
|
const randStartDelay = Math.max(DUR_MIN, Math.random() * DUR_MAX);
|
||||||
|
const utc = startTime + randStartDelay + count * duration;
|
||||||
const ind = count % messages.length;
|
const ind = count % messages.length;
|
||||||
const message = messages[ind] + ' - [' + millisecondsSinceStart + ']';
|
const message = messages[ind] + ' - [' + millisecondsSinceStart + ']';
|
||||||
|
// pick a random severity level + 1 for an undefined level so we can do nominal
|
||||||
|
const severity =
|
||||||
|
Math.random() > 0.4
|
||||||
|
? Object.keys(SEVERITY_CSS)[
|
||||||
|
Math.floor(Math.random() * Object.keys(SEVERITY_CSS).length + 1)
|
||||||
|
]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
utc,
|
utc,
|
||||||
message
|
message,
|
||||||
|
severity
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +65,7 @@ class EventTelemetryProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
subscribe(domainObject, callback) {
|
subscribe(domainObject, callback) {
|
||||||
const duration = domainObject.telemetry.duration * 1000;
|
const duration = domainObject.telemetry.duration * DUR_MIN;
|
||||||
const firstObservedTime = Date.now();
|
const firstObservedTime = Date.now();
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
@ -78,7 +90,7 @@ class EventTelemetryProvider {
|
|||||||
request(domainObject, options) {
|
request(domainObject, options) {
|
||||||
let start = options.start;
|
let start = options.start;
|
||||||
const end = Math.min(Date.now(), options.end); // no future values
|
const end = Math.min(Date.now(), options.end); // no future values
|
||||||
const duration = domainObject.telemetry.duration * 1000;
|
const duration = domainObject.telemetry.duration * DUR_MIN;
|
||||||
const size = options.size ? options.size : this.defaultSize;
|
const size = options.size ? options.size : this.defaultSize;
|
||||||
const data = [];
|
const data = [];
|
||||||
const firstObservedTime = options.start;
|
const firstObservedTime = options.start;
|
||||||
|
@ -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;
|
@ -19,8 +19,10 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
import EventLimitProvider from './EventLimitProvider.js';
|
||||||
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 +40,22 @@ 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());
|
||||||
|
|
||||||
|
openmct.telemetry.addProvider(new EventLimitProvider(openmct));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
};
|
||||||
|
@ -20,6 +20,8 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import { seededRandom } from 'utils/random.js';
|
||||||
|
|
||||||
const DEFAULT_IMAGE_SAMPLES = [
|
const DEFAULT_IMAGE_SAMPLES = [
|
||||||
'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg',
|
'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg',
|
||||||
'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18732.jpg',
|
'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18732.jpg',
|
||||||
@ -162,8 +164,8 @@ export default function () {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCompassValues(min, max) {
|
function getCompassValues(min, max, timestamp) {
|
||||||
return min + Math.random() * (max - min);
|
return min + seededRandom(timestamp) * (max - min);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getImageSamples(configuration) {
|
function getImageSamples(configuration) {
|
||||||
@ -283,9 +285,9 @@ function pointForTimestamp(timestamp, name, imageSamples, delay) {
|
|||||||
utc: Math.floor(timestamp / delay) * delay,
|
utc: Math.floor(timestamp / delay) * delay,
|
||||||
local: Math.floor(timestamp / delay) * delay,
|
local: Math.floor(timestamp / delay) * delay,
|
||||||
url,
|
url,
|
||||||
sunOrientation: getCompassValues(0, 360),
|
sunOrientation: getCompassValues(0, 360, timestamp),
|
||||||
cameraAzimuth: getCompassValues(0, 360),
|
cameraAzimuth: getCompassValues(0, 360, timestamp),
|
||||||
heading: getCompassValues(0, 360),
|
heading: getCompassValues(0, 360, timestamp),
|
||||||
transformations: navCamTransformations,
|
transformations: navCamTransformations,
|
||||||
imageDownloadName
|
imageDownloadName
|
||||||
};
|
};
|
||||||
|
@ -234,6 +234,7 @@
|
|||||||
openmct.install(openmct.plugins.Timelist());
|
openmct.install(openmct.plugins.Timelist());
|
||||||
openmct.install(openmct.plugins.BarChart());
|
openmct.install(openmct.plugins.BarChart());
|
||||||
openmct.install(openmct.plugins.ScatterPlot());
|
openmct.install(openmct.plugins.ScatterPlot());
|
||||||
|
openmct.install(openmct.plugins.EventTimestripPlugin());
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
openmct.start();
|
openmct.start();
|
||||||
});
|
});
|
||||||
|
120
package-lock.json
generated
120
package-lock.json
generated
@ -66,6 +66,7 @@
|
|||||||
"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",
|
||||||
"npm-run-all2": "6.1.2",
|
"npm-run-all2": "6.1.2",
|
||||||
"nyc": "15.1.0",
|
"nyc": "15.1.0",
|
||||||
"painterro": "1.2.87",
|
"painterro": "1.2.87",
|
||||||
@ -92,7 +93,7 @@
|
|||||||
"webpack-merge": "5.10.0"
|
"webpack-merge": "5.10.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.14.2 <22"
|
"node": ">=18.14.2 <23"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"e2e": {
|
"e2e": {
|
||||||
@ -103,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": {
|
||||||
@ -1547,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"
|
||||||
@ -2463,6 +2464,12 @@
|
|||||||
"@mdn/browser-compat-data": "^5.2.34"
|
"@mdn/browser-compat-data": "^5.2.34"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/axe-core": {
|
"node_modules/axe-core": {
|
||||||
"version": "4.8.4",
|
"version": "4.8.4",
|
||||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.8.4.tgz",
|
||||||
@ -2472,6 +2479,17 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.7.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
||||||
|
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/babel-loader": {
|
"node_modules/babel-loader": {
|
||||||
"version": "9.1.0",
|
"version": "9.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.0.tgz",
|
||||||
@ -3012,6 +3030,18 @@
|
|||||||
"node": ">=0.1.90"
|
"node": ">=0.1.90"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/comma-separated-values": {
|
"node_modules/comma-separated-values": {
|
||||||
"version": "3.6.4",
|
"version": "3.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/comma-separated-values/-/comma-separated-values-3.6.4.tgz",
|
"resolved": "https://registry.npmjs.org/comma-separated-values/-/comma-separated-values-3.6.4.tgz",
|
||||||
@ -4102,6 +4132,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@ -5755,6 +5794,20 @@
|
|||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@ -7896,6 +7949,35 @@
|
|||||||
"multicast-dns": "cli.js"
|
"multicast-dns": "cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nano": {
|
||||||
|
"version": "10.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/nano/-/nano-10.1.4.tgz",
|
||||||
|
"integrity": "sha512-bJOFIPLExIbF6mljnfExXX9Cub4W0puhDjVMp+qV40xl/DBvgKao7St4+6/GB6EoHZap7eFnrnx4mnp5KYgwJA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.7.4",
|
||||||
|
"node-abort-controller": "^3.1.1",
|
||||||
|
"qs": "^6.13.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nano/node_modules/qs": {
|
||||||
|
"version": "6.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||||
|
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.0.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.7",
|
"version": "3.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||||
@ -7935,6 +8017,12 @@
|
|||||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/node-abort-controller": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/node-fetch": {
|
"node_modules/node-fetch": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
@ -8662,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"
|
||||||
@ -8681,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": {
|
||||||
@ -9185,6 +9273,12 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/pump": {
|
"node_modules/pump": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||||
|
@ -69,6 +69,7 @@
|
|||||||
"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",
|
||||||
"npm-run-all2": "6.1.2",
|
"npm-run-all2": "6.1.2",
|
||||||
"nyc": "15.1.0",
|
"nyc": "15.1.0",
|
||||||
"painterro": "1.2.87",
|
"painterro": "1.2.87",
|
||||||
@ -138,7 +139,7 @@
|
|||||||
"url": "git+https://github.com/nasa/openmct.git"
|
"url": "git+https://github.com/nasa/openmct.git"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.14.2 <22"
|
"node": ">=18.14.2 <23"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"Firefox ESR",
|
"Firefox ESR",
|
||||||
|
@ -35,7 +35,7 @@ export const DEFAULT_SHELVE_DURATIONS = [
|
|||||||
value: 900000
|
value: 900000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Indefinite',
|
name: 'Unlimited',
|
||||||
value: null
|
value: null
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@ -136,17 +136,21 @@ export default class FaultManagementAPI {
|
|||||||
/**
|
/**
|
||||||
* Retrieves the available shelve durations from the provider, or the default durations if the
|
* Retrieves the available shelve durations from the provider, or the default durations if the
|
||||||
* provider does not provide any.
|
* provider does not provide any.
|
||||||
* @returns {ShelveDuration[]}
|
* @returns {ShelveDuration[] | undefined}
|
||||||
*/
|
*/
|
||||||
getShelveDurations() {
|
getShelveDurations() {
|
||||||
return this.provider?.getShelveDurations() ?? DEFAULT_SHELVE_DURATIONS;
|
if (!this.provider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.provider.getShelveDurations?.() ?? DEFAULT_SHELVE_DURATIONS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} ShelveDuration
|
* @typedef {Object} ShelveDuration
|
||||||
* @property {string} name - The name of the shelve duration
|
* @property {string} name - The name of the shelve duration
|
||||||
* @property {number|null} value - The value of the shelve duration in milliseconds, or null for indefinite
|
* @property {number|null} value - The value of the shelve duration in milliseconds, or null for unlimited
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -27,10 +27,11 @@ import TooltipComponent from './components/TooltipComponent.vue';
|
|||||||
|
|
||||||
class Tooltip extends EventEmitter {
|
class Tooltip extends EventEmitter {
|
||||||
constructor(
|
constructor(
|
||||||
{ toolTipText, toolTipLocation, parentElement } = {
|
{ toolTipText, toolTipLocation, parentElement, cssClasses } = {
|
||||||
tooltipText: '',
|
tooltipText: '',
|
||||||
toolTipLocation: 'below',
|
toolTipLocation: 'below',
|
||||||
parentElement: null
|
parentElement: null,
|
||||||
|
cssClasses: []
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
@ -42,7 +43,8 @@ class Tooltip extends EventEmitter {
|
|||||||
provide: {
|
provide: {
|
||||||
toolTipText,
|
toolTipText,
|
||||||
toolTipLocation,
|
toolTipLocation,
|
||||||
parentElement
|
parentElement,
|
||||||
|
cssClasses
|
||||||
},
|
},
|
||||||
template: '<tooltip-component toolTipText="toolTipText"></tooltip-component>'
|
template: '<tooltip-component toolTipText="toolTipText"></tooltip-component>'
|
||||||
});
|
});
|
||||||
|
@ -80,10 +80,11 @@ class TooltipAPI {
|
|||||||
* @property {string} tooltipText text to show in the tooltip
|
* @property {string} tooltipText text to show in the tooltip
|
||||||
* @property {TOOLTIP_LOCATIONS} tooltipLocation location to show the tooltip relative to the parentElement
|
* @property {TOOLTIP_LOCATIONS} tooltipLocation location to show the tooltip relative to the parentElement
|
||||||
* @property {HTMLElement} parentElement reference to the DOM node we're adding the tooltip to
|
* @property {HTMLElement} parentElement reference to the DOM node we're adding the tooltip to
|
||||||
|
* @property {Array} cssClasses css classes to use with the tool tip element
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tooltips take an options object that consists of the string, tooltipLocation, and parentElement
|
* Tooltips take an options object that consists of the string, tooltipLocation, a parentElement, and an array of cssClasses
|
||||||
* @param {TooltipOptions} options
|
* @param {TooltipOptions} options
|
||||||
*/
|
*/
|
||||||
tooltip(options) {
|
tooltip(options) {
|
||||||
|
@ -22,7 +22,8 @@ at runtime from the About dialog for additional information.
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="tooltip-wrapper"
|
ref="tooltip-wrapper"
|
||||||
class="c-menu c-tooltip-wrapper"
|
class="c-tooltip-wrapper"
|
||||||
|
:class="cssClasses"
|
||||||
:style="toolTipLocationStyle"
|
:style="toolTipLocationStyle"
|
||||||
role="tooltip"
|
role="tooltip"
|
||||||
aria-labelledby="tooltip-text"
|
aria-labelledby="tooltip-text"
|
||||||
@ -36,7 +37,7 @@ at runtime from the About dialog for additional information.
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
inject: ['toolTipText', 'toolTipLocation', 'parentElement'],
|
inject: ['toolTipText', 'toolTipLocation', 'parentElement', 'cssClasses'],
|
||||||
computed: {
|
computed: {
|
||||||
toolTipCoordinates() {
|
toolTipCoordinates() {
|
||||||
return this.parentElement.getBoundingClientRect();
|
return this.parentElement.getBoundingClientRect();
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
.c-tooltip-wrapper {
|
.c-tooltip-wrapper {
|
||||||
|
@include menuOuter();
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
height: auto;
|
height: auto;
|
||||||
width: auto;
|
width: auto;
|
||||||
padding: $interiorMargin;
|
padding: $interiorMargin $interiorMarginLg;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-tooltip {
|
.c-tooltip {
|
||||||
|
@ -48,7 +48,7 @@ const tooltipHelpers = {
|
|||||||
.reverse()
|
.reverse()
|
||||||
.join(' / ');
|
.join(' / ');
|
||||||
},
|
},
|
||||||
buildToolTip(tooltipText, tooltipLocation, elementRef) {
|
buildToolTip(tooltipText, tooltipLocation, elementRef, cssClasses) {
|
||||||
if (!tooltipText || tooltipText.length < 1) {
|
if (!tooltipText || tooltipText.length < 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -59,7 +59,8 @@ const tooltipHelpers = {
|
|||||||
this.tooltip = this.openmct.tooltips.tooltip({
|
this.tooltip = this.openmct.tooltips.tooltip({
|
||||||
toolTipText: tooltipText,
|
toolTipText: tooltipText,
|
||||||
toolTipLocation: tooltipLocation,
|
toolTipLocation: tooltipLocation,
|
||||||
parentElement: parentElement
|
parentElement: parentElement,
|
||||||
|
cssClasses
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
hideToolTip() {
|
hideToolTip() {
|
||||||
|
@ -332,7 +332,11 @@ export default {
|
|||||||
this.domainObject.configuration.axes.xKey === undefined ||
|
this.domainObject.configuration.axes.xKey === undefined ||
|
||||||
this.domainObject.configuration.axes.yKey === undefined
|
this.domainObject.configuration.axes.yKey === undefined
|
||||||
) {
|
) {
|
||||||
return;
|
const { xKey, yKey } = this.identifyAxesKeys(axisMetadata);
|
||||||
|
this.openmct.objects.mutate(this.domainObject, 'configuration.axes', {
|
||||||
|
xKey,
|
||||||
|
yKey
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let xValues = [];
|
let xValues = [];
|
||||||
@ -431,6 +435,30 @@ export default {
|
|||||||
subscribeToAll() {
|
subscribeToAll() {
|
||||||
const telemetryObjects = Object.values(this.telemetryObjects);
|
const telemetryObjects = Object.values(this.telemetryObjects);
|
||||||
telemetryObjects.forEach(this.subscribeToObject);
|
telemetryObjects.forEach(this.subscribeToObject);
|
||||||
|
},
|
||||||
|
identifyAxesKeys(metadata) {
|
||||||
|
const { xAxisMetadata, yAxisMetadata } = metadata;
|
||||||
|
|
||||||
|
let xKey;
|
||||||
|
let yKey;
|
||||||
|
|
||||||
|
// If xAxisMetadata contains array values, use the first one for xKey
|
||||||
|
const arrayValues = xAxisMetadata.filter((metaDatum) => metaDatum.isArrayValue);
|
||||||
|
const nonArrayValues = xAxisMetadata.filter((metaDatum) => !metaDatum.isArrayValue);
|
||||||
|
|
||||||
|
if (arrayValues.length > 0) {
|
||||||
|
xKey = arrayValues[0].key;
|
||||||
|
yKey = arrayValues.length > 1 ? arrayValues[1].key : yAxisMetadata.key;
|
||||||
|
} else if (nonArrayValues.length > 0) {
|
||||||
|
xKey = nonArrayValues[0].key;
|
||||||
|
yKey = 'none';
|
||||||
|
} else {
|
||||||
|
// Fallback if no valid xKey or yKey is found
|
||||||
|
xKey = 'none';
|
||||||
|
yKey = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
return { xKey, yKey };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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"
|
||||||
>
|
>
|
||||||
|
@ -28,11 +28,7 @@
|
|||||||
{ 'is-style-invisible': styleItem.style && styleItem.style.isStyleInvisible },
|
{ 'is-style-invisible': styleItem.style && styleItem.style.isStyleInvisible },
|
||||||
{ 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 }
|
{ 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 }
|
||||||
]"
|
]"
|
||||||
:style="[
|
:style="[encodedImageUrl ? { backgroundImage: 'url(' + encodedImageUrl + ')' } : itemStyle]"
|
||||||
styleItem.style.imageUrl
|
|
||||||
? { backgroundImage: 'url(' + styleItem.style.imageUrl + ')' }
|
|
||||||
: itemStyle
|
|
||||||
]"
|
|
||||||
class="c-style-thumb"
|
class="c-style-thumb"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@ -62,7 +58,7 @@
|
|||||||
@change="updateStyleValue"
|
@change="updateStyleValue"
|
||||||
/>
|
/>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
v-if="hasProperty(styleItem.style.imageUrl)"
|
v-if="hasProperty(encodedImageUrl)"
|
||||||
class="c-style__toolbar-button--image-url"
|
class="c-style__toolbar-button--image-url"
|
||||||
:options="imageUrlOption"
|
:options="imageUrlOption"
|
||||||
@change="updateStyleValue"
|
@change="updateStyleValue"
|
||||||
@ -93,6 +89,8 @@ import ToolbarButton from '@/ui/toolbar/components/ToolbarButton.vue';
|
|||||||
import ToolbarColorPicker from '@/ui/toolbar/components/ToolbarColorPicker.vue';
|
import ToolbarColorPicker from '@/ui/toolbar/components/ToolbarColorPicker.vue';
|
||||||
import ToolbarToggleButton from '@/ui/toolbar/components/ToolbarToggleButton.vue';
|
import ToolbarToggleButton from '@/ui/toolbar/components/ToolbarToggleButton.vue';
|
||||||
|
|
||||||
|
import { encode_url } from '../../../../utils/encoding';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'StyleEditor',
|
name: 'StyleEditor',
|
||||||
components: {
|
components: {
|
||||||
@ -183,11 +181,14 @@ export default {
|
|||||||
},
|
},
|
||||||
property: 'imageUrl',
|
property: 'imageUrl',
|
||||||
formKeys: ['url'],
|
formKeys: ['url'],
|
||||||
value: { url: this.styleItem.style.imageUrl },
|
value: { url: this.encodedImageUrl },
|
||||||
isEditing: this.isEditing,
|
isEditing: this.isEditing,
|
||||||
nonSpecific: this.mixedStyles.indexOf('imageUrl') > -1
|
nonSpecific: this.mixedStyles.indexOf('imageUrl') > -1
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
encodedImageUrl() {
|
||||||
|
return encode_url(this.styleItem.style.imageUrl);
|
||||||
|
},
|
||||||
isStyleInvisibleOption() {
|
isStyleInvisibleOption() {
|
||||||
return {
|
return {
|
||||||
value: this.styleItem.style.isStyleInvisible,
|
value: this.styleItem.style.isStyleInvisible,
|
||||||
|
@ -35,6 +35,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { encode_url } from '../../../utils/encoding';
|
||||||
import conditionalStylesMixin from '../mixins/objectStyles-mixin.js';
|
import conditionalStylesMixin from '../mixins/objectStyles-mixin.js';
|
||||||
import LayoutFrame from './LayoutFrame.vue';
|
import LayoutFrame from './LayoutFrame.vue';
|
||||||
|
|
||||||
@ -80,12 +81,12 @@ export default {
|
|||||||
return this.isEditing || !this.itemStyle?.isStyleInvisible;
|
return this.isEditing || !this.itemStyle?.isStyleInvisible;
|
||||||
},
|
},
|
||||||
style() {
|
style() {
|
||||||
let backgroundImage = 'url(' + this.item.url + ')';
|
let backgroundImage = `url('${encode_url(this.item.url)}')`;
|
||||||
let border = '1px solid ' + this.item.stroke;
|
let border = '1px solid ' + this.item.stroke;
|
||||||
|
|
||||||
if (this.itemStyle) {
|
if (this.itemStyle) {
|
||||||
if (this.itemStyle.imageUrl !== undefined) {
|
if (this.itemStyle.imageUrl !== undefined) {
|
||||||
backgroundImage = 'url(' + this.itemStyle.imageUrl + ')';
|
backgroundImage = `url('${encode_url(this.itemStyle.imageUrl)}')`;
|
||||||
}
|
}
|
||||||
|
|
||||||
border = this.itemStyle.border;
|
border = this.itemStyle.border;
|
||||||
|
@ -109,8 +109,9 @@ class DuplicateAction {
|
|||||||
let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
|
let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
|
||||||
let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
|
let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
|
||||||
let objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
|
let objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
|
||||||
|
const isLocked = parentCandidate.locked === true;
|
||||||
|
|
||||||
if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {
|
if (isLocked || !this.openmct.objects.isPersistable(parentCandidate.identifier)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,10 +140,9 @@ class DuplicateAction {
|
|||||||
const parentType = parent && this.openmct.types.get(parent.type);
|
const parentType = parent && this.openmct.types.get(parent.type);
|
||||||
const child = objectPath[0];
|
const child = objectPath[0];
|
||||||
const childType = child && this.openmct.types.get(child.type);
|
const childType = child && this.openmct.types.get(child.type);
|
||||||
const locked = child.locked ? child.locked : parent && parent.locked;
|
|
||||||
const isPersistable = this.openmct.objects.isPersistable(child.identifier);
|
const isPersistable = this.openmct.objects.isPersistable(child.identifier);
|
||||||
|
|
||||||
if (locked || !isPersistable) {
|
if (!isPersistable) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
55
src/plugins/events/EventInspectorViewProvider.js
Normal file
55
src/plugins/events/EventInspectorViewProvider.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import mount from 'utils/mount';
|
||||||
|
|
||||||
|
import EventInspectorView from './components/EventInspectorView.vue';
|
||||||
|
|
||||||
|
export default function EventInspectorViewProvider(openmct) {
|
||||||
|
const TIMELINE_VIEW = 'time-strip.event.inspector';
|
||||||
|
return {
|
||||||
|
key: TIMELINE_VIEW,
|
||||||
|
name: 'Event',
|
||||||
|
canView: function (selection) {
|
||||||
|
if (selection.length === 0 || selection[0].length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectionType = selection[0][0].context?.type;
|
||||||
|
const event = selection[0][0].context?.event;
|
||||||
|
return selectionType === 'time-strip-event-selection' && event;
|
||||||
|
},
|
||||||
|
view: function (selection) {
|
||||||
|
let _destroy = null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
show: function (element) {
|
||||||
|
const { destroy } = mount(
|
||||||
|
{
|
||||||
|
el: element,
|
||||||
|
components: {
|
||||||
|
EventInspectorView
|
||||||
|
},
|
||||||
|
provide: {
|
||||||
|
openmct,
|
||||||
|
domainObject: selection[0][0].context.item,
|
||||||
|
event: selection[0][0].context.event
|
||||||
|
},
|
||||||
|
template: '<event-inspector-view></event-inspector-view>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
app: openmct.app,
|
||||||
|
element
|
||||||
|
}
|
||||||
|
);
|
||||||
|
_destroy = destroy;
|
||||||
|
},
|
||||||
|
priority: function () {
|
||||||
|
return openmct.priority.HIGH;
|
||||||
|
},
|
||||||
|
destroy: function () {
|
||||||
|
if (_destroy) {
|
||||||
|
_destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
100
src/plugins/events/EventTimelineViewProvider.js
Normal file
100
src/plugins/events/EventTimelineViewProvider.js
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
import mount from 'utils/mount';
|
||||||
|
|
||||||
|
import EventTimelineView from './components/EventTimelineView.vue';
|
||||||
|
|
||||||
|
export default function EventTimestripViewProvider(openmct) {
|
||||||
|
const type = 'event.time-line.view';
|
||||||
|
|
||||||
|
function hasEventTelemetry(domainObject) {
|
||||||
|
const metadata = openmct.telemetry.getMetadata(domainObject);
|
||||||
|
if (!metadata) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasDomain = metadata.valuesForHints(['domain']).length > 0;
|
||||||
|
const hasNoRange = !metadata.valuesForHints(['range'])?.length;
|
||||||
|
|
||||||
|
// for the moment, let's also exclude telemetry with images
|
||||||
|
const hasNoImages = !metadata.valuesForHints(['image']).length;
|
||||||
|
|
||||||
|
return hasDomain && hasNoRange && hasNoImages;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: type,
|
||||||
|
name: 'Event Timeline View',
|
||||||
|
cssClass: 'icon-event',
|
||||||
|
priority: function () {
|
||||||
|
// We want this to be higher priority than the TelemetryTableView
|
||||||
|
return openmct.priority.HIGH + 1;
|
||||||
|
},
|
||||||
|
canView: function (domainObject, objectPath) {
|
||||||
|
const isChildOfTimeStrip = objectPath.some((object) => object.type === 'time-strip');
|
||||||
|
|
||||||
|
return (
|
||||||
|
hasEventTelemetry(domainObject) &&
|
||||||
|
isChildOfTimeStrip &&
|
||||||
|
!openmct.router.isNavigatedObject(objectPath)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
view: function (domainObject, objectPath) {
|
||||||
|
let _destroy = null;
|
||||||
|
let component = null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
show: function (element) {
|
||||||
|
const { vNode, destroy } = mount(
|
||||||
|
{
|
||||||
|
el: element,
|
||||||
|
components: {
|
||||||
|
EventTimelineView
|
||||||
|
},
|
||||||
|
provide: {
|
||||||
|
openmct: openmct,
|
||||||
|
domainObject: domainObject,
|
||||||
|
objectPath: objectPath
|
||||||
|
},
|
||||||
|
template: '<event-timeline-view ref="root"></event-timeline-view>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
app: openmct.app,
|
||||||
|
element
|
||||||
|
}
|
||||||
|
);
|
||||||
|
_destroy = destroy;
|
||||||
|
component = vNode.componentInstance;
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy: function () {
|
||||||
|
if (_destroy) {
|
||||||
|
_destroy();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getComponent() {
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
43
src/plugins/events/components/EventInspectorView.vue
Normal file
43
src/plugins/events/components/EventInspectorView.vue
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<!--
|
||||||
|
Open MCT, Copyright (c) 2014-2024, United States Government
|
||||||
|
as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
Administration. All rights reserved.
|
||||||
|
|
||||||
|
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
License for the specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
Open MCT includes source code licensed under additional open source
|
||||||
|
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
this source code distribution or the Licensing information page available
|
||||||
|
at runtime from the About dialog for additional information.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="c-inspect-properties">
|
||||||
|
<ul class="c-inspect-properties__section">
|
||||||
|
<div class="c-inspect-properties_header" title="'Details'">Details</div>
|
||||||
|
<li
|
||||||
|
v-for="[key, value] in Object.entries(event)"
|
||||||
|
:key="key"
|
||||||
|
class="c-inspect-properties__row"
|
||||||
|
>
|
||||||
|
<span class="c-inspect-properties__label">{{ key }}</span>
|
||||||
|
<span class="c-inspect-properties__value">{{ value }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
inject: ['openmct', 'domainObject', 'event']
|
||||||
|
};
|
||||||
|
</script>
|
386
src/plugins/events/components/EventTimelineView.vue
Normal file
386
src/plugins/events/components/EventTimelineView.vue
Normal file
@ -0,0 +1,386 @@
|
|||||||
|
<!--
|
||||||
|
Open MCT, Copyright (c) 2014-2024, United States Government
|
||||||
|
as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
Administration. All rights reserved.
|
||||||
|
|
||||||
|
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
License for the specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
Open MCT includes source code licensed under additional open source
|
||||||
|
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
this source code distribution or the Licensing information page available
|
||||||
|
at runtime from the About dialog for additional information.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="events" class="c-events-tsv js-events-tsv" :style="alignmentStyle">
|
||||||
|
<SwimLane v-if="eventItems.length" :is-nested="true" :hide-label="true">
|
||||||
|
<template #object>
|
||||||
|
<div ref="eventsContainer" class="c-events-tsv__container">
|
||||||
|
<div
|
||||||
|
v-for="event in eventItems"
|
||||||
|
:id="`wrapper-${event.time}`"
|
||||||
|
:ref="`wrapper-${event.time}`"
|
||||||
|
:key="event.id"
|
||||||
|
:aria-label="titleKey ? `${event[titleKey]}` : ''"
|
||||||
|
class="c-events-tsv__event-line"
|
||||||
|
:class="event.limitClass || ''"
|
||||||
|
:style="`left: ${event.left}px`"
|
||||||
|
@mouseenter="showToolTip(event)"
|
||||||
|
@mouseleave="dismissToolTip(event)"
|
||||||
|
@click.stop="createSelectionForInspector(event)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</SwimLane>
|
||||||
|
<div v-else class="c-timeline__no-items">No events within timeframe</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { scaleLinear, scaleUtc } from 'd3-scale';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
import SwimLane from '@/ui/components/swim-lane/SwimLane.vue';
|
||||||
|
|
||||||
|
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
|
||||||
|
import { useAlignment } from '../../../ui/composables/alignmentContext.js';
|
||||||
|
import { useExtendedLines } from '../../../ui/composables/extendedLines';
|
||||||
|
import eventData from '../mixins/eventData.js';
|
||||||
|
|
||||||
|
const PADDING = 1;
|
||||||
|
const AXES_PADDING = 20;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { SwimLane },
|
||||||
|
mixins: [eventData, tooltipHelpers],
|
||||||
|
inject: ['openmct', 'domainObject', 'objectPath'],
|
||||||
|
setup() {
|
||||||
|
const domainObject = inject('domainObject');
|
||||||
|
const objectPath = inject('objectPath');
|
||||||
|
const openmct = inject('openmct');
|
||||||
|
const { alignment: alignmentData } = useAlignment(domainObject, objectPath, openmct);
|
||||||
|
|
||||||
|
const {
|
||||||
|
extendedLines: extendedLines,
|
||||||
|
update: updateExtendedLines,
|
||||||
|
updateLineHover: updateExtendedLineHover,
|
||||||
|
remove: removeLinesForObject
|
||||||
|
} = useExtendedLines(domainObject, objectPath, openmct);
|
||||||
|
|
||||||
|
return {
|
||||||
|
alignmentData,
|
||||||
|
extendedLines,
|
||||||
|
updateExtendedLineHover,
|
||||||
|
updateExtendedLines,
|
||||||
|
removeLinesForObject
|
||||||
|
};
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
eventItems: [],
|
||||||
|
eventHistory: [],
|
||||||
|
titleKey: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
alignmentStyle() {
|
||||||
|
let leftMargin = 0;
|
||||||
|
let rightMargin = 0;
|
||||||
|
if (this.alignmentData.leftWidth) {
|
||||||
|
const leftOffset = this.alignmentData.multiple ? 2 * AXES_PADDING : AXES_PADDING;
|
||||||
|
leftMargin = `${this.alignmentData.leftWidth + leftOffset}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.alignmentData.rightWidth) {
|
||||||
|
rightMargin = `${this.alignmentData.rightWidth + AXES_PADDING}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
margin: `0 ${rightMargin} 0 ${leftMargin}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
eventHistory: {
|
||||||
|
handler() {
|
||||||
|
this.updateEventItems();
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
|
alignmentData: {
|
||||||
|
handler() {
|
||||||
|
this.setScaleAndPlotEvents(this.timeSystem);
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
|
extendedLines: {
|
||||||
|
handler() {
|
||||||
|
const { enabled = false } = this.extendedLines[this.keyString];
|
||||||
|
if (this.extendedLinesEnabled !== enabled) {
|
||||||
|
this.extendedLinesEnabled = enabled === true;
|
||||||
|
this.updateLines();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.valueMetadata = {};
|
||||||
|
this.height = 0;
|
||||||
|
this.timeSystem = this.openmct.time.getTimeSystem();
|
||||||
|
this.extendedLinesEnabled = false;
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.setDimensions();
|
||||||
|
this.setTimeContext();
|
||||||
|
|
||||||
|
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);
|
||||||
|
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||||
|
const metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||||
|
if (metadata) {
|
||||||
|
this.valueMetadata =
|
||||||
|
metadata.valuesForHints(['range'])[0] || this.firstNonDomainAttribute(metadata);
|
||||||
|
}
|
||||||
|
// title is in the metadata, and is either a "hint" with a "label", or failing that, the first string type we find
|
||||||
|
this.titleKey =
|
||||||
|
metadata.valuesForHints(['label'])?.[0]?.key ||
|
||||||
|
metadata.values().find((metadatum) => metadatum.format === 'string')?.key;
|
||||||
|
|
||||||
|
this.updateViewBounds();
|
||||||
|
|
||||||
|
this.resize = _.debounce(this.resize, 400);
|
||||||
|
this.eventStripResizeObserver = new ResizeObserver(this.resize);
|
||||||
|
this.eventStripResizeObserver.observe(this.$refs.events);
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if (this.eventStripResizeObserver) {
|
||||||
|
this.eventStripResizeObserver.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stopFollowingTimeContext();
|
||||||
|
if (this.unlisten) {
|
||||||
|
this.unlisten();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setTimeContext() {
|
||||||
|
this.stopFollowingTimeContext();
|
||||||
|
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||||
|
this.timeContext.on('timeSystem', this.setScaleAndPlotEvents);
|
||||||
|
this.timeContext.on('boundsChanged', this.updateViewBounds);
|
||||||
|
},
|
||||||
|
firstNonDomainAttribute(metadata) {
|
||||||
|
return metadata
|
||||||
|
.values()
|
||||||
|
.find((metadatum) => !metadatum.hints.domain && metadatum.key !== 'name');
|
||||||
|
},
|
||||||
|
stopFollowingTimeContext() {
|
||||||
|
if (this.timeContext) {
|
||||||
|
this.timeContext.off('timeSystem', this.setScaleAndPlotEvents);
|
||||||
|
this.timeContext.off('boundsChanged', this.updateViewBounds);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resize() {
|
||||||
|
const clientWidth = this.getClientWidth();
|
||||||
|
if (clientWidth !== this.width) {
|
||||||
|
this.setDimensions();
|
||||||
|
this.setScaleAndPlotEvents(this.timeSystem);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getClientWidth() {
|
||||||
|
// Try to use the component’s own element first
|
||||||
|
let clientWidth = this.$refs.events?.clientWidth;
|
||||||
|
if (!clientWidth) {
|
||||||
|
// Fallback: use the actual container element (the immediate parent)
|
||||||
|
const parent = this.$el.parentElement;
|
||||||
|
if (parent) {
|
||||||
|
clientWidth = parent.getBoundingClientRect().width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clientWidth;
|
||||||
|
},
|
||||||
|
updateViewBounds(bounds, isTick) {
|
||||||
|
this.viewBounds = this.timeContext.getBounds();
|
||||||
|
|
||||||
|
if (!this.timeSystem) {
|
||||||
|
this.timeSystem = this.timeContext.getTimeSystem();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setScaleAndPlotEvents(this.timeSystem, !isTick);
|
||||||
|
},
|
||||||
|
setScaleAndPlotEvents(timeSystem) {
|
||||||
|
if (timeSystem) {
|
||||||
|
this.timeSystem = timeSystem;
|
||||||
|
this.timeFormatter = this.getFormatter(this.timeSystem.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setScale(this.timeSystem);
|
||||||
|
this.updateEventItems();
|
||||||
|
},
|
||||||
|
getFormatter(key) {
|
||||||
|
const metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||||
|
|
||||||
|
const metadataValue = metadata.value(key) || { format: key };
|
||||||
|
const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||||
|
|
||||||
|
return valueFormatter;
|
||||||
|
},
|
||||||
|
updateEventItems() {
|
||||||
|
if (this.xScale) {
|
||||||
|
this.eventItems = this.eventHistory.map((eventHistoryItem) => {
|
||||||
|
const limitClass = this.getLimitClass(eventHistoryItem);
|
||||||
|
return {
|
||||||
|
...eventHistoryItem,
|
||||||
|
left: this.xScale(eventHistoryItem.time),
|
||||||
|
limitClass,
|
||||||
|
hoverEnabled: false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this.updateLines();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setDimensions() {
|
||||||
|
const eventsHolder = this.$refs.events;
|
||||||
|
this.width = this.getClientWidth();
|
||||||
|
|
||||||
|
this.height = Math.round(eventsHolder.getBoundingClientRect().height);
|
||||||
|
},
|
||||||
|
setScale(timeSystem) {
|
||||||
|
if (!this.width) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!timeSystem) {
|
||||||
|
timeSystem = this.timeContext.getTimeSystem();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeSystem.isUTCBased) {
|
||||||
|
this.xScale = scaleUtc();
|
||||||
|
this.xScale.domain([new Date(this.viewBounds.start), new Date(this.viewBounds.end)]);
|
||||||
|
} else {
|
||||||
|
this.xScale = scaleLinear();
|
||||||
|
this.xScale.domain([this.viewBounds.start, this.viewBounds.end]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.xScale.range([PADDING, this.width - PADDING * 2]);
|
||||||
|
},
|
||||||
|
createPathSelection(eventWrapper) {
|
||||||
|
const selection = [];
|
||||||
|
selection.unshift({
|
||||||
|
element: eventWrapper,
|
||||||
|
context: {
|
||||||
|
item: this.domainObject
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.objectPath.forEach((pathObject) => {
|
||||||
|
selection.push({
|
||||||
|
element: this.openmct.layout.$refs.browseObject.$el,
|
||||||
|
context: {
|
||||||
|
item: pathObject
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return selection;
|
||||||
|
},
|
||||||
|
setSelection() {
|
||||||
|
let childContext = {};
|
||||||
|
childContext.item = this.childObject;
|
||||||
|
this.context = childContext;
|
||||||
|
if (this.removeSelectable) {
|
||||||
|
this.removeSelectable();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.removeSelectable = this.openmct.selection.selectable(this.$el, this.context);
|
||||||
|
},
|
||||||
|
createSelectionForInspector(event) {
|
||||||
|
const eventWrapper = this.$refs[`wrapper-${event.time}`][0];
|
||||||
|
const eventContext = {
|
||||||
|
type: 'time-strip-event-selection',
|
||||||
|
event
|
||||||
|
};
|
||||||
|
|
||||||
|
const selection = this.createPathSelection(eventWrapper);
|
||||||
|
if (
|
||||||
|
selection.length &&
|
||||||
|
this.openmct.objects.areIdsEqual(
|
||||||
|
selection[0].context.item.identifier,
|
||||||
|
this.domainObject.identifier
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
selection[0].context = {
|
||||||
|
...selection[0].context,
|
||||||
|
...eventContext
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
selection.unshift({
|
||||||
|
element: eventWrapper,
|
||||||
|
context: {
|
||||||
|
item: this.domainObject,
|
||||||
|
...eventContext
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.openmct.selection.select(selection, true);
|
||||||
|
},
|
||||||
|
getLimitClass(event) {
|
||||||
|
const limitEvaluation = this.limitEvaluator.evaluate(event, this.valueMetadata);
|
||||||
|
return limitEvaluation?.cssClass;
|
||||||
|
},
|
||||||
|
showToolTip(event) {
|
||||||
|
const aClasses = ['c-events-tooltip'];
|
||||||
|
if (event.limitClass) {
|
||||||
|
aClasses.push(event.limitClass);
|
||||||
|
}
|
||||||
|
const showToLeft = false; // Temp, stubbed in
|
||||||
|
if (showToLeft) {
|
||||||
|
aClasses.push('--left');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.buildToolTip(
|
||||||
|
this.titleKey ? `${event[this.titleKey]}` : '',
|
||||||
|
this.openmct.tooltips.TOOLTIP_LOCATIONS.RIGHT,
|
||||||
|
`wrapper-${event.time}`,
|
||||||
|
[aClasses.join(' ')]
|
||||||
|
);
|
||||||
|
this.updateExtendedLineHover({
|
||||||
|
keyString: this.keyString,
|
||||||
|
id: event.time,
|
||||||
|
hoverEnabled: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
dismissToolTip(event) {
|
||||||
|
this.hideToolTip();
|
||||||
|
this.updateExtendedLineHover({
|
||||||
|
keyString: this.keyString,
|
||||||
|
id: event.time
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateLines() {
|
||||||
|
let lines = [];
|
||||||
|
if (this.extendedLinesEnabled) {
|
||||||
|
lines = this.eventItems.map((e) => ({
|
||||||
|
x: e.left,
|
||||||
|
limitClass: e.limitClass,
|
||||||
|
id: e.time,
|
||||||
|
hoverEnabled: false
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
this.updateExtendedLines({
|
||||||
|
lines,
|
||||||
|
keyString: this.keyString
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
111
src/plugins/events/components/events-view.scss
Normal file
111
src/plugins/events/components/events-view.scss
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
@mixin styleEventLine($colorConst) {
|
||||||
|
background-color: $colorConst !important;
|
||||||
|
transition: box-shadow 250ms ease-out;
|
||||||
|
&:hover,
|
||||||
|
&[s-selected] {
|
||||||
|
box-shadow: rgba($colorConst, 0.5) 0 0 0px 4px;
|
||||||
|
transition: none;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@mixin styleEventLineExtended($colorConst) {
|
||||||
|
background-color: $colorConst !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-events-tsv {
|
||||||
|
$m: $interiorMargin;
|
||||||
|
overflow: hidden;
|
||||||
|
@include abs();
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
// Holds event lines
|
||||||
|
background-color: $colorPlotBg;
|
||||||
|
//box-shadow: inset $colorPlotAreaBorder 0 0 0 1px; // Using box-shadow instead of border to not affect box size
|
||||||
|
position: absolute;
|
||||||
|
top: $m; right: 0; bottom: $m; left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__event-line {
|
||||||
|
// Wraps an individual event line
|
||||||
|
// Also holds the hover flyout element
|
||||||
|
$c: $colorEventLine;
|
||||||
|
$lineW: $eventLineW;
|
||||||
|
$hitAreaW: 7px;
|
||||||
|
$m: $interiorMarginSm;
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
top: $m; bottom: $m;
|
||||||
|
width: $lineW;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
@include styleEventLine($colorEventLine);
|
||||||
|
&.is-event {
|
||||||
|
&--purple {
|
||||||
|
@include styleEventLine($colorEventPurpleLine);
|
||||||
|
}
|
||||||
|
&--red {
|
||||||
|
@include styleEventLine($colorEventRedLine);
|
||||||
|
}
|
||||||
|
&--orange {
|
||||||
|
@include styleEventLine($colorEventOrangeLine);
|
||||||
|
}
|
||||||
|
&--yellow {
|
||||||
|
@include styleEventLine($colorEventYellowLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
// Extend hit area
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0; bottom: 0;
|
||||||
|
z-index: 0;
|
||||||
|
width: $hitAreaW;
|
||||||
|
transform: translateX(($hitAreaW - $lineW) * -0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-events-canvas {
|
||||||
|
pointer-events: auto;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extended event lines
|
||||||
|
.c-timeline__event-line--extended {
|
||||||
|
@include abs();
|
||||||
|
width: $eventLineW;
|
||||||
|
opacity: 0.4;
|
||||||
|
|
||||||
|
&.--hilite {
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include styleEventLineExtended($colorEventLine);
|
||||||
|
&.is-event {
|
||||||
|
&--purple {
|
||||||
|
@include styleEventLineExtended($colorEventPurpleLine);
|
||||||
|
}
|
||||||
|
&--red {
|
||||||
|
@include styleEventLineExtended($colorEventRedLine);
|
||||||
|
}
|
||||||
|
&--orange {
|
||||||
|
@include styleEventLineExtended($colorEventOrangeLine);
|
||||||
|
}
|
||||||
|
&--yellow {
|
||||||
|
@include styleEventLineExtended($colorEventYellowLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-events-tooltip {
|
||||||
|
// Default to right of event line
|
||||||
|
border-radius: 0 !important;
|
||||||
|
//transform: translate(0, $interiorMargin);
|
||||||
|
}
|
183
src/plugins/events/mixins/eventData.js
Normal file
183
src/plugins/events/mixins/eventData.js
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||||
|
import { TIME_CONTEXT_EVENTS } from '../../../api/time/constants.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
inject: ['openmct', 'domainObject', 'objectPath'],
|
||||||
|
mounted() {
|
||||||
|
// listen
|
||||||
|
this.boundsChanged = this.boundsChanged.bind(this);
|
||||||
|
this.timeSystemChanged = this.timeSystemChanged.bind(this);
|
||||||
|
this.setDataTimeContext = this.setDataTimeContext.bind(this);
|
||||||
|
this.openmct.objectViews.on('clearData', this.dataCleared);
|
||||||
|
|
||||||
|
// Get metadata and formatters
|
||||||
|
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||||
|
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||||
|
|
||||||
|
this.durationFormatter = this.getFormatter(
|
||||||
|
this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
|
||||||
|
);
|
||||||
|
|
||||||
|
// initialize
|
||||||
|
this.timeKey = this.timeSystem.key;
|
||||||
|
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||||
|
this.setDataTimeContext();
|
||||||
|
this.loadTelemetry();
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if (this.unsubscribe) {
|
||||||
|
this.unsubscribe();
|
||||||
|
delete this.unsubscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stopFollowingDataTimeContext();
|
||||||
|
this.openmct.objectViews.off('clearData', this.dataCleared);
|
||||||
|
|
||||||
|
this.telemetryCollection.off('add', this.dataAdded);
|
||||||
|
this.telemetryCollection.off('remove', this.dataRemoved);
|
||||||
|
this.telemetryCollection.off('clear', this.dataCleared);
|
||||||
|
|
||||||
|
this.telemetryCollection.destroy();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
dataAdded(addedItems, addedItemIndices) {
|
||||||
|
const normalizedDataToAdd = addedItems.map((datum) => this.normalizeDatum(datum));
|
||||||
|
let newEventHistory = this.eventHistory.slice();
|
||||||
|
normalizedDataToAdd.forEach((datum, index) => {
|
||||||
|
newEventHistory.splice(addedItemIndices[index] ?? -1, 0, datum);
|
||||||
|
});
|
||||||
|
//Assign just once so eventHistory watchers don't get called too often
|
||||||
|
this.eventHistory = newEventHistory;
|
||||||
|
},
|
||||||
|
dataCleared() {
|
||||||
|
this.eventHistory = [];
|
||||||
|
},
|
||||||
|
dataRemoved(removed) {
|
||||||
|
const removedTimestamps = {};
|
||||||
|
removed.forEach((_removed) => {
|
||||||
|
const removedTimestamp = this.parseTime(_removed);
|
||||||
|
removedTimestamps[removedTimestamp] = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventHistory = this.eventHistory.filter((event) => {
|
||||||
|
const eventTimestamp = this.parseTime(event);
|
||||||
|
|
||||||
|
return !removedTimestamps[eventTimestamp];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setDataTimeContext() {
|
||||||
|
this.stopFollowingDataTimeContext();
|
||||||
|
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||||
|
this.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsChanged);
|
||||||
|
this.timeContext.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.timeSystemChanged);
|
||||||
|
},
|
||||||
|
stopFollowingDataTimeContext() {
|
||||||
|
if (this.timeContext) {
|
||||||
|
this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsChanged);
|
||||||
|
this.timeContext.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.timeSystemChanged);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatEventUrl(datum) {
|
||||||
|
if (!datum) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.eventFormatter.format(datum);
|
||||||
|
},
|
||||||
|
formatEventThumbnailUrl(datum) {
|
||||||
|
if (!datum || !this.eventThumbnailFormatter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.eventThumbnailFormatter.format(datum);
|
||||||
|
},
|
||||||
|
formatTime(datum) {
|
||||||
|
if (!datum) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateTimeStr = this.timeFormatter.format(datum);
|
||||||
|
|
||||||
|
// Replace ISO "T" with a space to allow wrapping
|
||||||
|
return dateTimeStr.replace('T', ' ');
|
||||||
|
},
|
||||||
|
getEventDownloadName(datum) {
|
||||||
|
let eventDownloadName = '';
|
||||||
|
if (datum) {
|
||||||
|
const key = this.eventDownloadNameMetadataValue.key;
|
||||||
|
eventDownloadName = datum[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventDownloadName;
|
||||||
|
},
|
||||||
|
parseTime(datum) {
|
||||||
|
if (!datum) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.timeFormatter.parse(datum);
|
||||||
|
},
|
||||||
|
loadTelemetry() {
|
||||||
|
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
|
||||||
|
timeContext: this.timeContext
|
||||||
|
});
|
||||||
|
this.telemetryCollection.on('add', this.dataAdded);
|
||||||
|
this.telemetryCollection.on('remove', this.dataRemoved);
|
||||||
|
this.telemetryCollection.on('clear', this.dataCleared);
|
||||||
|
this.telemetryCollection.load();
|
||||||
|
},
|
||||||
|
boundsChanged(bounds, isTick) {
|
||||||
|
if (isTick) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bounds = bounds;
|
||||||
|
},
|
||||||
|
timeSystemChanged() {
|
||||||
|
this.timeSystem = this.timeContext.getTimeSystem();
|
||||||
|
this.timeKey = this.timeSystem.key;
|
||||||
|
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||||
|
this.durationFormatter = this.getFormatter(
|
||||||
|
this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
|
||||||
|
);
|
||||||
|
},
|
||||||
|
normalizeDatum(datum) {
|
||||||
|
const formattedTime = this.formatTime(datum);
|
||||||
|
const time = this.parseTime(formattedTime);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...datum,
|
||||||
|
formattedTime,
|
||||||
|
time
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getFormatter(key) {
|
||||||
|
const metadataValue = this.metadata.value(key) || { format: key };
|
||||||
|
const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||||
|
|
||||||
|
return valueFormatter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
31
src/plugins/events/plugin.js
Normal file
31
src/plugins/events/plugin.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import EventInspectorViewProvider from './EventInspectorViewProvider.js';
|
||||||
|
import EventTimelineViewProvider from './EventTimelineViewProvider.js';
|
||||||
|
|
||||||
|
export default function plugin() {
|
||||||
|
return function install(openmct) {
|
||||||
|
openmct.objectViews.addProvider(new EventTimelineViewProvider(openmct));
|
||||||
|
openmct.inspectorViews.addProvider(new EventInspectorViewProvider(openmct));
|
||||||
|
};
|
||||||
|
}
|
@ -330,7 +330,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
shelveData.comment = data.comment || '';
|
shelveData.comment = data.comment || '';
|
||||||
shelveData.shelveDuration = data.shelveDuration ?? this.shelveDurations[0].value;
|
shelveData.shelveDuration =
|
||||||
|
data.shelveDuration === undefined ? this.shelveDurations[0].value : data.shelveDuration;
|
||||||
} else {
|
} else {
|
||||||
shelveData = {
|
shelveData = {
|
||||||
shelved: false
|
shelved: false
|
||||||
|
@ -42,24 +42,6 @@ export const FAULT_MANAGEMENT_TYPE = 'faultManagement';
|
|||||||
export const FAULT_MANAGEMENT_INSPECTOR = 'faultManagementInspector';
|
export const FAULT_MANAGEMENT_INSPECTOR = 'faultManagementInspector';
|
||||||
export const FAULT_MANAGEMENT_ALARMS = 'alarms';
|
export const FAULT_MANAGEMENT_ALARMS = 'alarms';
|
||||||
export const FAULT_MANAGEMENT_GLOBAL_ALARMS = 'global-alarm-status';
|
export const FAULT_MANAGEMENT_GLOBAL_ALARMS = 'global-alarm-status';
|
||||||
export const FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS = [
|
|
||||||
{
|
|
||||||
name: '5 Minutes',
|
|
||||||
value: 300000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '10 Minutes',
|
|
||||||
value: 600000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '15 Minutes',
|
|
||||||
value: 900000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Indefinite',
|
|
||||||
value: 0
|
|
||||||
}
|
|
||||||
];
|
|
||||||
export const FAULT_MANAGEMENT_VIEW = 'faultManagement.view';
|
export const FAULT_MANAGEMENT_VIEW = 'faultManagement.view';
|
||||||
export const FAULT_MANAGEMENT_NAMESPACE = 'faults.taxonomy';
|
export const FAULT_MANAGEMENT_NAMESPACE = 'faults.taxonomy';
|
||||||
export const FILTER_ITEMS = ['Standard View', 'Acknowledged', 'Unacknowledged', 'Shelved'];
|
export const FILTER_ITEMS = ['Standard View', 'Acknowledged', 'Unacknowledged', 'Shelved'];
|
||||||
|
@ -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' }
|
||||||
]
|
]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ class EditPropertiesAction extends PropertiesAction {
|
|||||||
const definition = this._getTypeDefinition(object.type);
|
const definition = this._getTypeDefinition(object.type);
|
||||||
const persistable = this.openmct.objects.isPersistable(object.identifier);
|
const persistable = this.openmct.objects.isPersistable(object.identifier);
|
||||||
|
|
||||||
return persistable && definition && definition.creatable;
|
return persistable && definition && definition.creatable && !object.locked;
|
||||||
}
|
}
|
||||||
|
|
||||||
invoke(objectPath) {
|
invoke(objectPath) {
|
||||||
|
@ -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);
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
<img
|
<img
|
||||||
ref="img"
|
ref="img"
|
||||||
class="c-thumb__image"
|
class="c-thumb__image"
|
||||||
:src="`${image.thumbnailUrl || image.url}`"
|
:src="imageSrc"
|
||||||
fetchpriority="low"
|
fetchpriority="low"
|
||||||
@load="imageLoadCompleted"
|
@load="imageLoadCompleted"
|
||||||
/>
|
/>
|
||||||
@ -54,6 +54,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { encode_url } from '../../../utils/encoding';
|
||||||
|
|
||||||
const THUMB_PADDING = 4;
|
const THUMB_PADDING = 4;
|
||||||
const BORDER_WIDTH = 2;
|
const BORDER_WIDTH = 2;
|
||||||
|
|
||||||
@ -96,6 +98,9 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
imageSrc() {
|
||||||
|
return `${encode_url(this.image.thumbnailUrl) || encode_url(this.image.url)}`;
|
||||||
|
},
|
||||||
ariaLabel() {
|
ariaLabel() {
|
||||||
return `Image thumbnail from ${this.image.formattedTime}${this.showAnnotationIndicator ? ', has annotations' : ''}`;
|
return `Image thumbnail from ${this.image.formattedTime}${this.showAnnotationIndicator ? ', has annotations' : ''}`;
|
||||||
},
|
},
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="imagery" class="c-imagery-tsv c-timeline-holder">
|
<div ref="imagery" class="c-imagery-tsv js-imagery-tsv" :style="alignmentStyle">
|
||||||
<div ref="imageryHolder" class="c-imagery-tsv__contents u-contents"></div>
|
<div ref="imageryHolder" class="c-imagery-tsv__contents u-contents"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -30,24 +30,32 @@
|
|||||||
import { scaleLinear, scaleUtc } from 'd3-scale';
|
import { scaleLinear, scaleUtc } from 'd3-scale';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import mount from 'utils/mount';
|
import mount from 'utils/mount';
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
import SwimLane from '@/ui/components/swim-lane/SwimLane.vue';
|
import SwimLane from '@/ui/components/swim-lane/SwimLane.vue';
|
||||||
import { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';
|
import { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';
|
||||||
|
|
||||||
|
import { useAlignment } from '../../../ui/composables/alignmentContext';
|
||||||
import imageryData from '../../imagery/mixins/imageryData.js';
|
import imageryData from '../../imagery/mixins/imageryData.js';
|
||||||
|
|
||||||
|
const AXES_PADDING = 20;
|
||||||
const PADDING = 1;
|
const PADDING = 1;
|
||||||
const ROW_HEIGHT = 100;
|
|
||||||
const IMAGE_SIZE = 85;
|
|
||||||
const IMAGE_WIDTH_THRESHOLD = 25;
|
const IMAGE_WIDTH_THRESHOLD = 25;
|
||||||
const CONTAINER_CLASS = 'c-imagery-tsv-container';
|
const CONTAINER_CLASS = 'c-imagery-tsv-container';
|
||||||
const NO_ITEMS_CLASS = 'c-imagery-tsv__no-items';
|
const NO_ITEMS_CLASS = 'c-timeline__no-items';
|
||||||
const IMAGE_WRAPPER_CLASS = 'c-imagery-tsv__image-wrapper';
|
const IMAGE_WRAPPER_CLASS = 'c-imagery-tsv__image-wrapper';
|
||||||
const ID_PREFIX = 'wrapper-';
|
const ID_PREFIX = 'wrapper-';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [imageryData],
|
mixins: [imageryData],
|
||||||
inject: ['openmct', 'domainObject', 'objectPath'],
|
inject: ['openmct', 'domainObject', 'objectPath'],
|
||||||
|
setup() {
|
||||||
|
const domainObject = inject('domainObject');
|
||||||
|
const objectPath = inject('objectPath');
|
||||||
|
const openmct = inject('openmct');
|
||||||
|
const { alignment: alignmentData } = useAlignment(domainObject, objectPath, openmct);
|
||||||
|
return { alignmentData };
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
let timeSystem = this.openmct.time.getTimeSystem();
|
let timeSystem = this.openmct.time.getTimeSystem();
|
||||||
this.metadata = {};
|
this.metadata = {};
|
||||||
@ -62,6 +70,18 @@ export default {
|
|||||||
keyString: undefined
|
keyString: undefined
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
alignmentStyle() {
|
||||||
|
let leftOffset = 0;
|
||||||
|
const rightOffset = this.alignmentData.rightWidth ? AXES_PADDING : 0;
|
||||||
|
if (this.alignmentData.leftWidth) {
|
||||||
|
leftOffset = this.alignmentData.multiple ? 2 * AXES_PADDING : AXES_PADDING;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
margin: `0 ${this.alignmentData.rightWidth + rightOffset}px 0 ${this.alignmentData.leftWidth + leftOffset}px`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
imageHistory: {
|
imageHistory: {
|
||||||
handler(newHistory, oldHistory) {
|
handler(newHistory, oldHistory) {
|
||||||
@ -73,9 +93,11 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.previewAction = this.openmct.actions.getAction(PREVIEW_ACTION_KEY);
|
this.previewAction = this.openmct.actions.getAction(PREVIEW_ACTION_KEY);
|
||||||
|
|
||||||
this.canvas = this.$refs.imagery.appendChild(document.createElement('canvas'));
|
// Why are we doing this? This element causes scroll problems in the swimlane.
|
||||||
this.canvas.height = 0;
|
// this.canvas = this.$refs.imagery.appendChild(document.createElement('canvas'));
|
||||||
this.canvasContext = this.canvas.getContext('2d');
|
// this.canvas.height = 0;
|
||||||
|
// this.canvas.width = 10;
|
||||||
|
// this.canvasContext = this.canvas.getContext('2d');
|
||||||
this.setDimensions();
|
this.setDimensions();
|
||||||
|
|
||||||
this.setScaleAndPlotImagery = this.setScaleAndPlotImagery.bind(this);
|
this.setScaleAndPlotImagery = this.setScaleAndPlotImagery.bind(this);
|
||||||
@ -207,8 +229,8 @@ export default {
|
|||||||
setDimensions() {
|
setDimensions() {
|
||||||
const imageryHolder = this.$refs.imagery;
|
const imageryHolder = this.$refs.imagery;
|
||||||
this.width = this.getClientWidth();
|
this.width = this.getClientWidth();
|
||||||
|
|
||||||
this.height = Math.round(imageryHolder.getBoundingClientRect().height);
|
this.height = Math.round(imageryHolder.getBoundingClientRect().height);
|
||||||
|
this.imageHeight = this.height - 10;
|
||||||
},
|
},
|
||||||
setScale(timeSystem) {
|
setScale(timeSystem) {
|
||||||
if (!this.width) {
|
if (!this.width) {
|
||||||
@ -233,14 +255,11 @@ export default {
|
|||||||
return imageObj.time <= this.viewBounds.end && imageObj.time >= this.viewBounds.start;
|
return imageObj.time <= this.viewBounds.end && imageObj.time >= this.viewBounds.start;
|
||||||
},
|
},
|
||||||
getImageryContainer() {
|
getImageryContainer() {
|
||||||
let containerHeight = 100;
|
|
||||||
let containerWidth = this.imageHistory.length ? this.width : 200;
|
|
||||||
let imageryContainer;
|
let imageryContainer;
|
||||||
|
|
||||||
let existingContainer = this.$el.querySelector(`.${CONTAINER_CLASS}`);
|
let existingContainer = this.$el.querySelector(`.${CONTAINER_CLASS}`);
|
||||||
if (existingContainer) {
|
if (existingContainer) {
|
||||||
imageryContainer = existingContainer;
|
imageryContainer = existingContainer;
|
||||||
imageryContainer.style.maxWidth = `${containerWidth}px`;
|
|
||||||
} else {
|
} else {
|
||||||
if (this.destroyImageryContainer) {
|
if (this.destroyImageryContainer) {
|
||||||
this.destroyImageryContainer();
|
this.destroyImageryContainer();
|
||||||
@ -270,8 +289,6 @@ export default {
|
|||||||
this.$refs.imageryHolder.appendChild(component.$el);
|
this.$refs.imageryHolder.appendChild(component.$el);
|
||||||
|
|
||||||
imageryContainer = component.$el.querySelector(`.${CONTAINER_CLASS}`);
|
imageryContainer = component.$el.querySelector(`.${CONTAINER_CLASS}`);
|
||||||
imageryContainer.style.maxWidth = `${containerWidth}px`;
|
|
||||||
imageryContainer.style.height = `${containerHeight}px`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return imageryContainer;
|
return imageryContainer;
|
||||||
@ -307,7 +324,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
plotNoItems(containerElement) {
|
plotNoItems(containerElement) {
|
||||||
let textElement = document.createElement('text');
|
let textElement = document.createElement('div');
|
||||||
textElement.classList.add(NO_ITEMS_CLASS);
|
textElement.classList.add(NO_ITEMS_CLASS);
|
||||||
textElement.innerHTML = 'No images within timeframe';
|
textElement.innerHTML = 'No images within timeframe';
|
||||||
|
|
||||||
@ -380,15 +397,11 @@ export default {
|
|||||||
//create image vertical tick indicator
|
//create image vertical tick indicator
|
||||||
let imageTickElement = document.createElement('div');
|
let imageTickElement = document.createElement('div');
|
||||||
imageTickElement.classList.add('c-imagery-tsv__image-handle');
|
imageTickElement.classList.add('c-imagery-tsv__image-handle');
|
||||||
imageTickElement.style.width = '2px';
|
|
||||||
imageTickElement.style.height = `${String(ROW_HEIGHT - 10)}px`;
|
|
||||||
imageWrapper.appendChild(imageTickElement);
|
imageWrapper.appendChild(imageTickElement);
|
||||||
|
|
||||||
//create placeholder - this will also hold the actual image
|
//create placeholder - this will also hold the actual image
|
||||||
let imagePlaceholder = document.createElement('div');
|
let imagePlaceholder = document.createElement('div');
|
||||||
imagePlaceholder.classList.add('c-imagery-tsv__image-placeholder');
|
imagePlaceholder.classList.add('c-imagery-tsv__image-placeholder');
|
||||||
imagePlaceholder.style.width = `${IMAGE_SIZE}px`;
|
|
||||||
imagePlaceholder.style.height = `${IMAGE_SIZE}px`;
|
|
||||||
imageWrapper.appendChild(imagePlaceholder);
|
imageWrapper.appendChild(imagePlaceholder);
|
||||||
|
|
||||||
//create image element
|
//create image element
|
||||||
@ -396,8 +409,6 @@ export default {
|
|||||||
this.setNSAttributesForElement(imageElement, {
|
this.setNSAttributesForElement(imageElement, {
|
||||||
src: image.thumbnailUrl || image.url
|
src: image.thumbnailUrl || image.url
|
||||||
});
|
});
|
||||||
imageElement.style.width = `${IMAGE_SIZE}px`;
|
|
||||||
imageElement.style.height = `${IMAGE_SIZE}px`;
|
|
||||||
this.setImageDisplay(imageElement, showImagePlaceholders);
|
this.setImageDisplay(imageElement, showImagePlaceholders);
|
||||||
|
|
||||||
//handle mousedown event to show the image in a large view
|
//handle mousedown event to show the image in a large view
|
||||||
|
@ -222,6 +222,7 @@ import { TIME_CONTEXT_EVENTS } from '@/api/time/constants.js';
|
|||||||
import imageryData from '@/plugins/imagery/mixins/imageryData.js';
|
import imageryData from '@/plugins/imagery/mixins/imageryData.js';
|
||||||
import { VIEW_LARGE_ACTION_KEY } from '@/plugins/viewLargeAction/viewLargeAction.js';
|
import { VIEW_LARGE_ACTION_KEY } from '@/plugins/viewLargeAction/viewLargeAction.js';
|
||||||
|
|
||||||
|
import { encode_url } from '../../../utils/encoding';
|
||||||
import eventHelpers from '../lib/eventHelpers.js';
|
import eventHelpers from '../lib/eventHelpers.js';
|
||||||
import AnnotationsCanvas from './AnnotationsCanvas.vue';
|
import AnnotationsCanvas from './AnnotationsCanvas.vue';
|
||||||
import Compass from './Compass/CompassComponent.vue';
|
import Compass from './Compass/CompassComponent.vue';
|
||||||
@ -364,7 +365,7 @@ export default {
|
|||||||
filter: `brightness(${this.filters.brightness}%) contrast(${this.filters.contrast}%)`,
|
filter: `brightness(${this.filters.brightness}%) contrast(${this.filters.contrast}%)`,
|
||||||
backgroundImage: `${
|
backgroundImage: `${
|
||||||
this.imageUrl
|
this.imageUrl
|
||||||
? `url(${this.imageUrl}),
|
? `url(${encode_url(this.imageUrl)}),
|
||||||
repeating-linear-gradient(
|
repeating-linear-gradient(
|
||||||
45deg,
|
45deg,
|
||||||
transparent,
|
transparent,
|
||||||
@ -789,7 +790,7 @@ export default {
|
|||||||
},
|
},
|
||||||
getVisibleLayerStyles(layer) {
|
getVisibleLayerStyles(layer) {
|
||||||
return {
|
return {
|
||||||
backgroundImage: `url(${layer.source})`,
|
backgroundImage: `url(${encode_url(layer.source)})`,
|
||||||
transform: `scale(${this.zoomFactor}) translate(${this.imageTranslateX / 2}px, ${
|
transform: `scale(${this.zoomFactor}) translate(${this.imageTranslateX / 2}px, ${
|
||||||
this.imageTranslateY / 2
|
this.imageTranslateY / 2
|
||||||
}px)`,
|
}px)`,
|
||||||
|
@ -509,45 +509,92 @@
|
|||||||
|
|
||||||
/*************************************** IMAGERY IN TIMESTRIP VIEWS */
|
/*************************************** IMAGERY IN TIMESTRIP VIEWS */
|
||||||
.c-imagery-tsv {
|
.c-imagery-tsv {
|
||||||
div.c-imagery-tsv__image-wrapper {
|
$m: $interiorMargin;
|
||||||
|
@include abs();
|
||||||
|
// We need overflow: hidden this because an image thumb can extend to the right past the time frame edge
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&-container {
|
||||||
|
background: $colorPlotBg;
|
||||||
|
//box-shadow: inset $colorPlotAreaBorder 0 0 0 1px; // Using box-shadow instead of border to not affect box size
|
||||||
|
position: absolute;
|
||||||
|
top: $m; right: 0; bottom: $m; left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-imagery-tsv__image-wrapper {
|
||||||
|
$m: $interiorMarginSm;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: $m; bottom: $m;
|
||||||
display: flex;
|
display: flex;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
margin-top: 5px;
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
[class*='__image-handle'] {
|
.c-imagery-tsv {
|
||||||
background-color: $colorBodyFg;
|
&__image-handle {
|
||||||
|
box-shadow: rgba($colorEventLine, 0.5) 0 0 0px 4px;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__image-placeholder img {
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
// img can be `display: none` when there's not enough space between tick lines
|
||||||
display: block !important;
|
display: block !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__image-placeholder {
|
||||||
|
background-color: deeppink; //pushBack($colorBodyBg, 0.3);
|
||||||
|
$m: $interiorMargin;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: $m; right: auto; bottom: $m; left: 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
filter: brightness(0.8);
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
&__image-handle {
|
||||||
|
$lineW: $eventLineW;
|
||||||
|
$hitAreaW: 7px;
|
||||||
|
background-color: $colorEventLine;
|
||||||
|
transition: box-shadow 250ms ease-out;
|
||||||
|
top: 0; bottom: 0;
|
||||||
|
width: $lineW;
|
||||||
|
z-index: 3;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
// Extend hit area
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0; bottom: 0;
|
||||||
|
z-index: 0;
|
||||||
|
width: $hitAreaW;
|
||||||
|
transform: translateX(($hitAreaW - $lineW) * -0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__no-items {
|
&__no-items {
|
||||||
fill: $colorBodyFg !important;
|
fill: $colorBodyFg !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__image-handle {
|
|
||||||
background-color: rgba($colorBodyFg, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__image-placeholder {
|
|
||||||
background-color: pushBack($colorBodyBg, 0.3);
|
|
||||||
display: block;
|
|
||||||
align-self: flex-end;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DON'T THINK THIS IS BEING USED
|
||||||
.c-image-canvas {
|
.c-image-canvas {
|
||||||
pointer-events: auto; // This allows the image element to receive a browser-level context click
|
pointer-events: auto; // This allows the image element to receive a browser-level context click
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -96,6 +96,8 @@ export default {
|
|||||||
const createdTimestamp = this.domainObject.created;
|
const createdTimestamp = this.domainObject.created;
|
||||||
const createdBy = this.domainObject.createdBy ? this.domainObject.createdBy : UNKNOWN_USER;
|
const createdBy = this.domainObject.createdBy ? this.domainObject.createdBy : UNKNOWN_USER;
|
||||||
const modifiedBy = this.domainObject.modifiedBy ? this.domainObject.modifiedBy : UNKNOWN_USER;
|
const modifiedBy = this.domainObject.modifiedBy ? this.domainObject.modifiedBy : UNKNOWN_USER;
|
||||||
|
const locked = this.domainObject.locked;
|
||||||
|
const lockedBy = this.domainObject.lockedBy ?? UNKNOWN_USER;
|
||||||
const modifiedTimestamp = this.domainObject.modified
|
const modifiedTimestamp = this.domainObject.modified
|
||||||
? this.domainObject.modified
|
? this.domainObject.modified
|
||||||
: this.domainObject.created;
|
: this.domainObject.created;
|
||||||
@ -148,6 +150,13 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (locked === true) {
|
||||||
|
details.push({
|
||||||
|
name: 'Locked By',
|
||||||
|
value: lockedBy
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (version) {
|
if (version) {
|
||||||
details.push({
|
details.push({
|
||||||
name: 'Version',
|
name: 'Version',
|
||||||
|
191
src/plugins/persistence/couch/scripts/lockObjects.mjs
Normal file
191
src/plugins/persistence/couch/scripts/lockObjects.mjs
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import http from 'http';
|
||||||
|
import nano from 'nano';
|
||||||
|
import { parseArgs } from 'util';
|
||||||
|
|
||||||
|
const COUCH_URL = process.env.OPENMCT_COUCH_URL || 'http://127.0.0.1:5984';
|
||||||
|
const COUCH_DB_NAME = process.env.OPENMCT_DATABASE_NAME || 'openmct';
|
||||||
|
|
||||||
|
const {
|
||||||
|
values: { couchUrl, database, lock, unlock, startObjectKeystring, user, pass }
|
||||||
|
} = parseArgs({
|
||||||
|
options: {
|
||||||
|
couchUrl: {
|
||||||
|
type: 'string',
|
||||||
|
default: COUCH_URL
|
||||||
|
},
|
||||||
|
database: {
|
||||||
|
type: 'string',
|
||||||
|
short: 'd',
|
||||||
|
default: COUCH_DB_NAME
|
||||||
|
},
|
||||||
|
lock: {
|
||||||
|
type: 'boolean',
|
||||||
|
short: 'l'
|
||||||
|
},
|
||||||
|
unlock: {
|
||||||
|
type: 'boolean',
|
||||||
|
short: 'u'
|
||||||
|
},
|
||||||
|
startObjectKeystring: {
|
||||||
|
type: 'string',
|
||||||
|
short: 'o',
|
||||||
|
default: 'mine'
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'string'
|
||||||
|
},
|
||||||
|
pass: {
|
||||||
|
type: 'string'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const BATCH_SIZE = 100;
|
||||||
|
const SOCKET_POOL_SIZE = 100;
|
||||||
|
|
||||||
|
const locked = lock === true;
|
||||||
|
console.info(`Connecting to ${couchUrl}/${database}`);
|
||||||
|
console.info(`${locked ? 'Locking' : 'Unlocking'} all children of ${startObjectKeystring}`);
|
||||||
|
|
||||||
|
const poolingAgent = new http.Agent({
|
||||||
|
keepAlive: true,
|
||||||
|
maxSockets: SOCKET_POOL_SIZE
|
||||||
|
});
|
||||||
|
|
||||||
|
const db = nano({
|
||||||
|
url: couchUrl,
|
||||||
|
requestDefaults: {
|
||||||
|
agent: poolingAgent
|
||||||
|
}
|
||||||
|
}).use(database);
|
||||||
|
db.auth(user, pass);
|
||||||
|
|
||||||
|
if (!unlock && !lock) {
|
||||||
|
throw new Error('Either -l or -u option is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const startObjectIdentifier = keystringToIdentifier(startObjectKeystring);
|
||||||
|
const documentBatch = [];
|
||||||
|
const alreadySeen = new Set();
|
||||||
|
let updatedDocumentCount = 0;
|
||||||
|
|
||||||
|
await processObjectTreeFrom(startObjectIdentifier);
|
||||||
|
//Persist final batch
|
||||||
|
await persistBatch();
|
||||||
|
console.log(`Processed ${updatedDocumentCount} documents`);
|
||||||
|
|
||||||
|
function processObjectTreeFrom(parentObjectIdentifier) {
|
||||||
|
//1. Fetch document for identifier;
|
||||||
|
return fetchDocument(parentObjectIdentifier)
|
||||||
|
.then(async (document) => {
|
||||||
|
if (document !== undefined) {
|
||||||
|
if (!alreadySeen.has(document._id)) {
|
||||||
|
alreadySeen.add(document._id);
|
||||||
|
//2. Lock or unlock object
|
||||||
|
document.model.locked = locked;
|
||||||
|
document.model.disallowUnlock = locked;
|
||||||
|
|
||||||
|
if (locked) {
|
||||||
|
document.model.lockedBy = 'script';
|
||||||
|
} else {
|
||||||
|
delete document.model.lockedBy;
|
||||||
|
}
|
||||||
|
//3. Push document to a batch
|
||||||
|
documentBatch.push(document);
|
||||||
|
//4. Persist batch if necessary, reporting failures
|
||||||
|
await persistBatchIfNeeded();
|
||||||
|
//5. Repeat for each composee
|
||||||
|
const composition = document.model.composition || [];
|
||||||
|
return Promise.all(
|
||||||
|
composition.map((composee) => {
|
||||||
|
return processObjectTreeFrom(composee);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(`Error ${error}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchDocument(identifierOrKeystring) {
|
||||||
|
let keystring;
|
||||||
|
if (typeof identifierOrKeystring === 'object') {
|
||||||
|
keystring = identifierToKeystring(identifierOrKeystring);
|
||||||
|
} else {
|
||||||
|
keystring = identifierOrKeystring;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const document = await db.get(keystring);
|
||||||
|
|
||||||
|
return document;
|
||||||
|
} catch (error) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistBatchIfNeeded() {
|
||||||
|
if (documentBatch.length >= BATCH_SIZE) {
|
||||||
|
return persistBatch();
|
||||||
|
} else {
|
||||||
|
//Noop - batch is not big enough yet
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistBatch() {
|
||||||
|
try {
|
||||||
|
const localBatch = [].concat(documentBatch);
|
||||||
|
|
||||||
|
//Immediately clear the shared batch array. This asynchronous process is non-blocking, and
|
||||||
|
//we don't want to try and persist the same batch multiple times while we are waiting for
|
||||||
|
//the subsequent bulk operation to complete.
|
||||||
|
updatedDocumentCount += documentBatch.length;
|
||||||
|
|
||||||
|
documentBatch.splice(0, documentBatch.length);
|
||||||
|
const response = await db.bulk({ docs: localBatch });
|
||||||
|
|
||||||
|
if (response instanceof Array) {
|
||||||
|
response.forEach((r) => {
|
||||||
|
console.info(JSON.stringify(r));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.info(JSON.stringify(response));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Array) {
|
||||||
|
error.forEach((e) => console.error(JSON.stringify(e)));
|
||||||
|
} else {
|
||||||
|
console.error(`${error.statusCode} - ${error.reason}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function keystringToIdentifier(keystring) {
|
||||||
|
const tokens = keystring.split(':');
|
||||||
|
if (tokens.length === 2) {
|
||||||
|
return {
|
||||||
|
namespace: tokens[0],
|
||||||
|
key: tokens[1]
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
namespace: '',
|
||||||
|
key: tokens[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function identifierToKeystring(identifier) {
|
||||||
|
if (typeof identifier === 'string') {
|
||||||
|
return identifier;
|
||||||
|
} else if (typeof identifier === 'object') {
|
||||||
|
if (identifier.namespace) {
|
||||||
|
return `${identifier.namespace}:${identifier.key}`;
|
||||||
|
} else {
|
||||||
|
return identifier.key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -160,6 +160,24 @@ add_index_and_views() {
|
|||||||
echo "Unable to create annotation_keystring_index"
|
echo "Unable to create annotation_keystring_index"
|
||||||
echo $response
|
echo $response
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Add auth database for locked objects
|
||||||
|
response=$(curl --silent --user "${CURL_USERPASS_ARG}" --request PUT "$COUCH_BASE_LOCAL"/"$OPENMCT_DATABASE_NAME"/_design/auth \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"_id": "_design/auth",
|
||||||
|
"language": "javascript",
|
||||||
|
"validate_doc_update": "function (newDoc, oldDoc, userCtx) { if (userCtx.roles.indexOf('\''_admin'\'') !== -1) { return; } else if (oldDoc === null) { return; } else if (oldDoc.model.type === '\''timer'\'' || oldDoc.model.type === '\''notebook'\'' || oldDoc.model.type === '\''restricted-notebook'\'') { if (oldDoc.model.name !== newDoc.model.name) { throw ({ forbidden: '\''Read-only object'\'' }); } else { return; } } else if (oldDoc.model.locked === true && oldDoc.model.disallowUnlock === true) { throw ({ forbidden: '\''Read-only object'\'' }); } else { return; }}"
|
||||||
|
}')
|
||||||
|
|
||||||
|
if [[ $response =~ "\"ok\":true" ]]; then
|
||||||
|
echo "Successfully created _design/auth design document for locked objects"
|
||||||
|
elif [[ $response =~ "\"error\":\"conflict\"" ]]; then
|
||||||
|
echo "_design/auth already exists, skipping creation"
|
||||||
|
else
|
||||||
|
echo "Unable to create _design/auth"
|
||||||
|
echo $response
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Main script execution
|
# Main script execution
|
||||||
|
@ -25,7 +25,8 @@
|
|||||||
{{ heading }}
|
{{ heading }}
|
||||||
</template>
|
</template>
|
||||||
<template #object>
|
<template #object>
|
||||||
<svg :height="height" :width="svgWidth" :style="alignmentStyle">
|
<div class="c-plan-av" :style="alignmentStyle">
|
||||||
|
<svg v-if="activities.length > 0" class="c-plan-av__svg" :height="height">
|
||||||
<symbol id="activity-bar-bg" :height="rowHeight" width="2" preserveAspectRatio="none">
|
<symbol id="activity-bar-bg" :height="rowHeight" width="2" preserveAspectRatio="none">
|
||||||
<rect x="0" y="0" width="100%" height="100%" fill="currentColor" />
|
<rect x="0" y="0" width="100%" height="100%" fill="currentColor" />
|
||||||
<line
|
<line
|
||||||
@ -78,15 +79,9 @@
|
|||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
</template>
|
</template>
|
||||||
<text
|
|
||||||
v-if="activities.length === 0"
|
|
||||||
x="10"
|
|
||||||
y="20"
|
|
||||||
class="c-plan__activity-label--outside-rect"
|
|
||||||
>
|
|
||||||
No activities within current timeframe
|
|
||||||
</text>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
<div v-else class="c-timeline__no-items">No activities within timeframe</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</SwimLane>
|
</SwimLane>
|
||||||
</template>
|
</template>
|
||||||
@ -162,24 +157,13 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
alignmentStyle() {
|
alignmentStyle() {
|
||||||
let leftOffset = 0;
|
let leftOffset = 0;
|
||||||
|
const rightOffset = this.alignmentData.rightWidth ? AXES_PADDING : 0;
|
||||||
if (this.alignmentData.leftWidth) {
|
if (this.alignmentData.leftWidth) {
|
||||||
leftOffset = this.alignmentData.multiple ? 2 * AXES_PADDING : AXES_PADDING;
|
leftOffset = this.alignmentData.multiple ? 2 * AXES_PADDING : AXES_PADDING;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
marginLeft: `${this.alignmentData.leftWidth + leftOffset}px`
|
margin: `0 ${this.alignmentData.rightWidth + rightOffset}px 0 ${this.alignmentData.leftWidth + leftOffset}px`
|
||||||
};
|
};
|
||||||
},
|
|
||||||
svgWidth() {
|
|
||||||
// Reduce the width by left axis width, then take off the right yaxis width as well
|
|
||||||
let leftOffset = 0;
|
|
||||||
if (this.alignmentData.leftWidth) {
|
|
||||||
leftOffset = this.alignmentData.multiple ? 2 * AXES_PADDING : AXES_PADDING;
|
|
||||||
}
|
|
||||||
const rightOffset = this.alignmentData.rightWidth ? AXES_PADDING : 0;
|
|
||||||
return (
|
|
||||||
this.width -
|
|
||||||
(this.alignmentData.leftWidth + leftOffset + this.alignmentData.rightWidth + rightOffset)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -243,11 +243,12 @@ export default {
|
|||||||
if (this.planObject) {
|
if (this.planObject) {
|
||||||
this.showReplacePlanDialog(domainObject);
|
this.showReplacePlanDialog(domainObject);
|
||||||
} else {
|
} else {
|
||||||
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
|
|
||||||
this.setupPlan(domainObject);
|
this.setupPlan(domainObject);
|
||||||
|
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleConfigurationChange(newConfiguration) {
|
handleConfigurationChange(newConfiguration) {
|
||||||
|
this.configuration = this.planViewConfiguration.getConfiguration();
|
||||||
Object.keys(newConfiguration).forEach((key) => {
|
Object.keys(newConfiguration).forEach((key) => {
|
||||||
this[key] = newConfiguration[key];
|
this[key] = newConfiguration[key];
|
||||||
});
|
});
|
||||||
@ -423,7 +424,10 @@ export default {
|
|||||||
return currentRow || SWIMLANE_PADDING;
|
return currentRow || SWIMLANE_PADDING;
|
||||||
},
|
},
|
||||||
generateActivities() {
|
generateActivities() {
|
||||||
const groupNames = getValidatedGroups(this.domainObject, this.planData);
|
if (!this.planObject) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const groupNames = getValidatedGroups(this.planObject, this.planData);
|
||||||
|
|
||||||
if (!groupNames.length) {
|
if (!groupNames.length) {
|
||||||
return;
|
return;
|
||||||
|
@ -27,6 +27,10 @@
|
|||||||
text {
|
text {
|
||||||
stroke: none;
|
stroke: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.c-swimlane__lane-object {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__activity {
|
&__activity {
|
||||||
@ -53,3 +57,14 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.c-plan-av {
|
||||||
|
// Activities view
|
||||||
|
background-color: $colorPlotBg;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&__svg {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -38,6 +38,7 @@ import CouchDBSearchFolder from './CouchDBSearchFolder/plugin.js';
|
|||||||
import DefaultRootName from './defaultRootName/plugin.js';
|
import DefaultRootName from './defaultRootName/plugin.js';
|
||||||
import DeviceClassifier from './DeviceClassifier/plugin.js';
|
import DeviceClassifier from './DeviceClassifier/plugin.js';
|
||||||
import DisplayLayoutPlugin from './displayLayout/plugin.js';
|
import DisplayLayoutPlugin from './displayLayout/plugin.js';
|
||||||
|
import EventTimestripPlugin from './events/plugin.js';
|
||||||
import FaultManagementPlugin from './faultManagement/FaultManagementPlugin.js';
|
import FaultManagementPlugin from './faultManagement/FaultManagementPlugin.js';
|
||||||
import Filters from './filters/plugin.js';
|
import Filters from './filters/plugin.js';
|
||||||
import FlexibleLayout from './flexibleLayout/plugin.js';
|
import FlexibleLayout from './flexibleLayout/plugin.js';
|
||||||
@ -176,5 +177,6 @@ plugins.Gauge = GaugePlugin;
|
|||||||
plugins.Timelist = TimeList;
|
plugins.Timelist = TimeList;
|
||||||
plugins.InspectorViews = InspectorViews;
|
plugins.InspectorViews = InspectorViews;
|
||||||
plugins.InspectorDataVisualization = InspectorDataVisualization;
|
plugins.InspectorDataVisualization = InspectorDataVisualization;
|
||||||
|
plugins.EventTimestripPlugin = EventTimestripPlugin;
|
||||||
|
|
||||||
export default plugins;
|
export default plugins;
|
||||||
|
@ -38,11 +38,11 @@
|
|||||||
v-for="(tab, index) in tabsList"
|
v-for="(tab, index) in tabsList"
|
||||||
:ref="tab.keyString"
|
:ref="tab.keyString"
|
||||||
:key="tab.keyString"
|
:key="tab.keyString"
|
||||||
:aria-label="`${tab.domainObject.name} tab`"
|
:aria-label="`${tab.domainObject.name} tab${tab.keyString === currentTab.keyString ? ' - selected' : ''}`"
|
||||||
class="c-tab c-tabs-view__tab js-tab"
|
class="c-tab c-tabs-view__tab js-tab"
|
||||||
role="tab"
|
role="tab"
|
||||||
:class="{
|
:class="{
|
||||||
'is-current': isCurrent(tab)
|
'is-current': tab.keyString === currentTab.keyString
|
||||||
}"
|
}"
|
||||||
@click="showTab(tab, index)"
|
@click="showTab(tab, index)"
|
||||||
@mouseover.ctrl="showToolTip(tab)"
|
@mouseover.ctrl="showToolTip(tab)"
|
||||||
@ -74,7 +74,7 @@
|
|||||||
:key="tab.keyString"
|
:key="tab.keyString"
|
||||||
:style="getTabStyles(tab)"
|
:style="getTabStyles(tab)"
|
||||||
class="c-tabs-view__object-holder"
|
class="c-tabs-view__object-holder"
|
||||||
:class="{ 'c-tabs-view__object-holder--hidden': !isCurrent(tab) }"
|
:class="{ 'c-tabs-view__object-holder--hidden': tab.keyString !== currentTab.keyString }"
|
||||||
>
|
>
|
||||||
<ObjectView
|
<ObjectView
|
||||||
v-if="shouldLoadTab(tab)"
|
v-if="shouldLoadTab(tab)"
|
||||||
@ -353,7 +353,10 @@ export default {
|
|||||||
this.internalDomainObject = domainObject;
|
this.internalDomainObject = domainObject;
|
||||||
},
|
},
|
||||||
persistCurrentTabIndex(index) {
|
persistCurrentTabIndex(index) {
|
||||||
|
//only persist if the domain object is not locked. The object mutate API will deal with whether the object is persistable or not.
|
||||||
|
if (!this.internalDomainObject.locked) {
|
||||||
this.openmct.objects.mutate(this.internalDomainObject, 'currentTabIndex', index);
|
this.openmct.objects.mutate(this.internalDomainObject, 'currentTabIndex', index);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
storeCurrentTabIndexInURL(index) {
|
storeCurrentTabIndexInURL(index) {
|
||||||
let currentTabIndexInURL = this.openmct.router.getSearchParam(this.searchTabKey);
|
let currentTabIndexInURL = this.openmct.router.getSearchParam(this.searchTabKey);
|
||||||
|
@ -25,7 +25,7 @@ import _ from 'lodash';
|
|||||||
|
|
||||||
import StalenessUtils from '../../utils/staleness.js';
|
import StalenessUtils from '../../utils/staleness.js';
|
||||||
import TableRowCollection from './collections/TableRowCollection.js';
|
import TableRowCollection from './collections/TableRowCollection.js';
|
||||||
import { MODE, ORDER } from './constants.js';
|
import { MODE } from './constants.js';
|
||||||
import TelemetryTableColumn from './TelemetryTableColumn.js';
|
import TelemetryTableColumn from './TelemetryTableColumn.js';
|
||||||
import TelemetryTableConfiguration from './TelemetryTableConfiguration.js';
|
import TelemetryTableConfiguration from './TelemetryTableConfiguration.js';
|
||||||
import TelemetryTableNameColumn from './TelemetryTableNameColumn.js';
|
import TelemetryTableNameColumn from './TelemetryTableNameColumn.js';
|
||||||
@ -130,14 +130,7 @@ export default class TelemetryTable extends EventEmitter {
|
|||||||
createTableRowCollections() {
|
createTableRowCollections() {
|
||||||
this.tableRows = new TableRowCollection();
|
this.tableRows = new TableRowCollection();
|
||||||
|
|
||||||
//Fetch any persisted default sort
|
const sortOptions = this.configuration.getSortOptions();
|
||||||
let sortOptions = this.configuration.getConfiguration().sortOptions;
|
|
||||||
|
|
||||||
//If no persisted sort order, default to sorting by time system, descending.
|
|
||||||
sortOptions = sortOptions || {
|
|
||||||
key: this.openmct.time.getTimeSystem().key,
|
|
||||||
direction: ORDER.DESCENDING
|
|
||||||
};
|
|
||||||
|
|
||||||
this.updateRowLimit();
|
this.updateRowLimit();
|
||||||
|
|
||||||
@ -172,8 +165,8 @@ export default class TelemetryTable extends EventEmitter {
|
|||||||
|
|
||||||
this.removeTelemetryCollection(keyString);
|
this.removeTelemetryCollection(keyString);
|
||||||
|
|
||||||
let sortOptions = this.configuration.getConfiguration().sortOptions;
|
let sortOptions = this.configuration.getSortOptions();
|
||||||
requestOptions.order = sortOptions?.direction ?? ORDER.DESCENDING; // default to descending
|
requestOptions.order = sortOptions.direction;
|
||||||
|
|
||||||
if (this.telemetryMode === MODE.PERFORMANCE) {
|
if (this.telemetryMode === MODE.PERFORMANCE) {
|
||||||
requestOptions.size = this.rowLimit;
|
requestOptions.size = this.rowLimit;
|
||||||
@ -442,12 +435,13 @@ export default class TelemetryTable extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sortBy(sortOptions) {
|
sortBy(sortOptions) {
|
||||||
this.tableRows.sortBy(sortOptions);
|
this.configuration.setSortOptions(sortOptions);
|
||||||
|
|
||||||
if (this.openmct.editor.isEditing()) {
|
if (this.telemetryMode === MODE.PERFORMANCE) {
|
||||||
let configuration = this.configuration.getConfiguration();
|
this.tableRows.setSortOptions(sortOptions);
|
||||||
configuration.sortOptions = sortOptions;
|
this.clearAndResubscribe();
|
||||||
this.configuration.updateConfiguration(configuration);
|
} else {
|
||||||
|
this.tableRows.sortBy(sortOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +23,11 @@
|
|||||||
import { EventEmitter } from 'eventemitter3';
|
import { EventEmitter } from 'eventemitter3';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { ORDER } from './constants';
|
||||||
|
|
||||||
export default class TelemetryTableConfiguration extends EventEmitter {
|
export default class TelemetryTableConfiguration extends EventEmitter {
|
||||||
|
#sortOptions;
|
||||||
|
|
||||||
constructor(domainObject, openmct, options) {
|
constructor(domainObject, openmct, options) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@ -44,6 +48,26 @@ export default class TelemetryTableConfiguration extends EventEmitter {
|
|||||||
this.notPersistable = !this.openmct.objects.isPersistable(this.domainObject.identifier);
|
this.notPersistable = !this.openmct.objects.isPersistable(this.domainObject.identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSortOptions() {
|
||||||
|
return (
|
||||||
|
this.#sortOptions ||
|
||||||
|
this.getConfiguration().sortOptions || {
|
||||||
|
key: this.openmct.time.getTimeSystem().key,
|
||||||
|
direction: ORDER.DESCENDING
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSortOptions(sortOptions) {
|
||||||
|
this.#sortOptions = sortOptions;
|
||||||
|
|
||||||
|
if (this.openmct.editor.isEditing()) {
|
||||||
|
let configuration = this.getConfiguration();
|
||||||
|
configuration.sortOptions = sortOptions;
|
||||||
|
this.updateConfiguration(configuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getConfiguration() {
|
getConfiguration() {
|
||||||
let configuration = this.domainObject.configuration || {};
|
let configuration = this.domainObject.configuration || {};
|
||||||
configuration.hiddenColumns = configuration.hiddenColumns || {};
|
configuration.hiddenColumns = configuration.hiddenColumns || {};
|
||||||
|
@ -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) {
|
||||||
@ -273,7 +290,7 @@ export default class TableRowCollection extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
sortBy(sortOptions) {
|
sortBy(sortOptions) {
|
||||||
if (arguments.length > 0) {
|
if (arguments.length > 0) {
|
||||||
this.sortOptions = sortOptions;
|
this.setSortOptions(sortOptions);
|
||||||
this.rows = _.orderBy(
|
this.rows = _.orderBy(
|
||||||
this.rows,
|
this.rows,
|
||||||
(row) => row.getParsedValue(sortOptions.key),
|
(row) => row.getParsedValue(sortOptions.key),
|
||||||
@ -286,6 +303,10 @@ export default class TableRowCollection extends EventEmitter {
|
|||||||
return Object.assign({}, this.sortOptions);
|
return Object.assign({}, this.sortOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSortOptions(sortOptions) {
|
||||||
|
this.sortOptions = sortOptions;
|
||||||
|
}
|
||||||
|
|
||||||
setColumnFilter(columnKey, filter) {
|
setColumnFilter(columnKey, filter) {
|
||||||
filter = filter.trim().toLowerCase();
|
filter = filter.trim().toLowerCase();
|
||||||
let wasBlank = this.columnFilters[columnKey] === undefined;
|
let wasBlank = this.columnFilters[columnKey] === undefined;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -11,18 +11,19 @@
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
ref="startDate"
|
ref="startDate"
|
||||||
v-model="formattedBounds.start"
|
v-model="formattedBounds.startDate"
|
||||||
class="c-input--datetime"
|
class="c-input--datetime"
|
||||||
type="text"
|
type="text"
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
aria-label="Start date"
|
aria-label="Start date"
|
||||||
@input="validateAllBounds('startDate')"
|
@input="validateInput('startDate')"
|
||||||
|
@change="reportValidity('startDate')"
|
||||||
/>
|
/>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
v-if="isUTCBased"
|
v-if="isUTCBased"
|
||||||
class="c-ctrl-wrapper--menus-right"
|
class="c-ctrl-wrapper--menus-right"
|
||||||
:default-date-time="formattedBounds.start"
|
:default-date-time="formattedBounds.startDate"
|
||||||
:formatter="timeFormatter"
|
:formatter="timeFormatter"
|
||||||
@date-selected="startDateSelected"
|
@date-selected="startDateSelected"
|
||||||
/>
|
/>
|
||||||
@ -37,7 +38,8 @@
|
|||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
aria-label="Start time"
|
aria-label="Start time"
|
||||||
@input="validateAllBounds('startDate')"
|
@input="validateInput('startTime')"
|
||||||
|
@change="reportValidity('startTime')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -48,18 +50,19 @@
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
ref="endDate"
|
ref="endDate"
|
||||||
v-model="formattedBounds.end"
|
v-model="formattedBounds.endDate"
|
||||||
class="c-input--datetime"
|
class="c-input--datetime"
|
||||||
type="text"
|
type="text"
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
aria-label="End date"
|
aria-label="End date"
|
||||||
@input="validateAllBounds('endDate')"
|
@input="validateInput('endDate')"
|
||||||
|
@change="reportValidity('endDate')"
|
||||||
/>
|
/>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
v-if="isUTCBased"
|
v-if="isUTCBased"
|
||||||
class="c-ctrl-wrapper--menus-left"
|
class="c-ctrl-wrapper--menus-left"
|
||||||
:default-date-time="formattedBounds.end"
|
:default-date-time="formattedBounds.endDate"
|
||||||
:formatter="timeFormatter"
|
:formatter="timeFormatter"
|
||||||
@date-selected="endDateSelected"
|
@date-selected="endDateSelected"
|
||||||
/>
|
/>
|
||||||
@ -74,14 +77,15 @@
|
|||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
aria-label="End time"
|
aria-label="End time"
|
||||||
@input="validateAllBounds('endDate')"
|
@input="validateInput('endTime')"
|
||||||
|
@change="reportValidity('endTime')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pr-time-input pr-time-input--buttons">
|
<div class="pr-time-input pr-time-input--buttons">
|
||||||
<button
|
<button
|
||||||
class="c-button c-button--major icon-check"
|
class="c-button c-button--major icon-check"
|
||||||
:disabled="isDisabled"
|
:disabled="hasInputValidityError"
|
||||||
aria-label="Submit time bounds"
|
aria-label="Submit time bounds"
|
||||||
@click.prevent="handleFormSubmission(true)"
|
@click.prevent="handleFormSubmission(true)"
|
||||||
></button>
|
></button>
|
||||||
@ -125,6 +129,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
timeFormatter: this.getFormatter(timeSystem.timeFormat),
|
timeFormatter: this.getFormatter(timeSystem.timeFormat),
|
||||||
durationFormatter: this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER),
|
durationFormatter: this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER),
|
||||||
|
timeSystemKey: timeSystem.key,
|
||||||
bounds: {
|
bounds: {
|
||||||
start: bounds.start,
|
start: bounds.start,
|
||||||
end: bounds.end
|
end: bounds.end
|
||||||
@ -136,9 +141,29 @@ export default {
|
|||||||
endTime: ''
|
endTime: ''
|
||||||
},
|
},
|
||||||
isUTCBased: timeSystem.isUTCBased,
|
isUTCBased: timeSystem.isUTCBased,
|
||||||
isDisabled: false
|
inputValidityMap: {
|
||||||
|
startDate: { valid: true },
|
||||||
|
startTime: { valid: true },
|
||||||
|
endDate: { valid: true },
|
||||||
|
endTime: { valid: true }
|
||||||
|
},
|
||||||
|
logicalValidityMap: {
|
||||||
|
limit: { valid: true },
|
||||||
|
bounds: { valid: true }
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
hasInputValidityError() {
|
||||||
|
return Object.values(this.inputValidityMap).some((isValid) => !isValid.valid);
|
||||||
|
},
|
||||||
|
hasLogicalValidationErrors() {
|
||||||
|
return Object.values(this.logicalValidityMap).some((isValid) => !isValid.valid);
|
||||||
|
},
|
||||||
|
isValid() {
|
||||||
|
return !this.hasInputValidityError && !this.hasLogicalValidationErrors;
|
||||||
|
}
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
inputBounds: {
|
inputBounds: {
|
||||||
handler(newBounds) {
|
handler(newBounds) {
|
||||||
@ -168,25 +193,17 @@ export default {
|
|||||||
this.setBounds(bounds);
|
this.setBounds(bounds);
|
||||||
this.setViewFromBounds(bounds);
|
this.setViewFromBounds(bounds);
|
||||||
},
|
},
|
||||||
clearAllValidation() {
|
|
||||||
[this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput);
|
|
||||||
},
|
|
||||||
clearValidationForInput(input) {
|
|
||||||
if (input) {
|
|
||||||
input.setCustomValidity('');
|
|
||||||
input.title = '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setBounds(bounds) {
|
setBounds(bounds) {
|
||||||
this.bounds = bounds;
|
this.bounds = bounds;
|
||||||
},
|
},
|
||||||
setViewFromBounds(bounds) {
|
setViewFromBounds(bounds) {
|
||||||
this.formattedBounds.start = this.timeFormatter.format(bounds.start).split(' ')[0];
|
this.formattedBounds.startDate = this.timeFormatter.format(bounds.start).split(' ')[0];
|
||||||
this.formattedBounds.end = this.timeFormatter.format(bounds.end).split(' ')[0];
|
this.formattedBounds.endDate = this.timeFormatter.format(bounds.end).split(' ')[0];
|
||||||
this.formattedBounds.startTime = this.durationFormatter.format(Math.abs(bounds.start));
|
this.formattedBounds.startTime = this.durationFormatter.format(Math.abs(bounds.start));
|
||||||
this.formattedBounds.endTime = this.durationFormatter.format(Math.abs(bounds.end));
|
this.formattedBounds.endTime = this.durationFormatter.format(Math.abs(bounds.end));
|
||||||
},
|
},
|
||||||
setTimeSystem(timeSystem) {
|
setTimeSystem(timeSystem) {
|
||||||
|
this.timeSystemKey = timeSystem.key;
|
||||||
this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
|
this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
|
||||||
this.durationFormatter = this.getFormatter(
|
this.durationFormatter = this.getFormatter(
|
||||||
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
|
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
|
||||||
@ -201,10 +218,10 @@ export default {
|
|||||||
setBoundsFromView(dismiss) {
|
setBoundsFromView(dismiss) {
|
||||||
if (this.$refs.fixedDeltaInput.checkValidity()) {
|
if (this.$refs.fixedDeltaInput.checkValidity()) {
|
||||||
let start = this.timeFormatter.parse(
|
let start = this.timeFormatter.parse(
|
||||||
`${this.formattedBounds.start} ${this.formattedBounds.startTime}`
|
`${this.formattedBounds.startDate} ${this.formattedBounds.startTime}`
|
||||||
);
|
);
|
||||||
let end = this.timeFormatter.parse(
|
let end = this.timeFormatter.parse(
|
||||||
`${this.formattedBounds.end} ${this.formattedBounds.endTime}`
|
`${this.formattedBounds.endDate} ${this.formattedBounds.endTime}`
|
||||||
);
|
);
|
||||||
|
|
||||||
this.$emit('update', { start, end });
|
this.$emit('update', { start, end });
|
||||||
@ -215,96 +232,93 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleFormSubmission(shouldDismiss) {
|
clearAllValidation() {
|
||||||
this.validateAllBounds('startDate');
|
Object.keys(this.inputValidityMap).forEach(this.clearValidation);
|
||||||
this.validateAllBounds('endDate');
|
},
|
||||||
|
clearValidation(refName) {
|
||||||
|
const input = this.getInput(refName);
|
||||||
|
|
||||||
if (!this.isDisabled) {
|
input.setCustomValidity('');
|
||||||
|
input.title = '';
|
||||||
|
},
|
||||||
|
handleFormSubmission(shouldDismiss) {
|
||||||
|
this.validateLimit();
|
||||||
|
this.reportValidity('limit');
|
||||||
|
this.validateBounds();
|
||||||
|
this.reportValidity('bounds');
|
||||||
|
|
||||||
|
if (this.isValid) {
|
||||||
this.setBoundsFromView(shouldDismiss);
|
this.setBoundsFromView(shouldDismiss);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
validateAllBounds(ref) {
|
validateInput(refName) {
|
||||||
this.isDisabled = false;
|
this.clearAllValidation();
|
||||||
|
|
||||||
if (!this.areBoundsFormatsValid()) {
|
const inputType = refName.includes('Date') ? 'Date' : 'Time';
|
||||||
this.isDisabled = true;
|
const formatter = inputType === 'Date' ? this.timeFormatter : this.durationFormatter;
|
||||||
return false;
|
const validationResult = formatter.validate(this.formattedBounds[refName])
|
||||||
}
|
? { valid: true }
|
||||||
|
: { valid: false, message: `Invalid ${inputType}` };
|
||||||
|
|
||||||
let validationResult = { valid: true };
|
this.inputValidityMap[refName] = validationResult;
|
||||||
const currentInput = this.$refs[ref];
|
},
|
||||||
|
validateBounds() {
|
||||||
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
|
const bounds = {
|
||||||
let boundsValues = {
|
|
||||||
start: this.timeFormatter.parse(
|
start: this.timeFormatter.parse(
|
||||||
`${this.formattedBounds.start} ${this.formattedBounds.startTime}`
|
`${this.formattedBounds.startDate} ${this.formattedBounds.startTime}`
|
||||||
),
|
),
|
||||||
end: this.timeFormatter.parse(
|
end: this.timeFormatter.parse(
|
||||||
`${this.formattedBounds.end} ${this.formattedBounds.endTime}`
|
`${this.formattedBounds.endDate} ${this.formattedBounds.endTime}`
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
//TODO: Do we need limits here? We have conductor limits disabled right now
|
|
||||||
// const limit = this.getBoundsLimit();
|
|
||||||
const limit = false;
|
|
||||||
|
|
||||||
if (this.isUTCBased && limit && boundsValues.end - boundsValues.start > limit) {
|
this.logicalValidityMap.bounds = this.openmct.time.validateBounds(bounds);
|
||||||
if (input === currentInput) {
|
},
|
||||||
validationResult = {
|
validateLimit(bounds) {
|
||||||
|
const limit = this.configuration?.menuOptions
|
||||||
|
?.filter((option) => option.timeSystem === this.timeSystemKey)
|
||||||
|
?.find((option) => option.limit)?.limit;
|
||||||
|
|
||||||
|
if (this.isUTCBased && limit && bounds.end - bounds.start > limit) {
|
||||||
|
this.logicalValidityMap.limit = {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: 'Start and end difference exceeds allowable limit'
|
message: 'Start and end difference exceeds allowable limit'
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
this.logicalValidityMap.limit = { valid: true };
|
||||||
}
|
}
|
||||||
} else if (input === currentInput) {
|
|
||||||
validationResult = this.openmct.time.validateBounds(boundsValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.handleValidationResults(input, validationResult);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
areBoundsFormatsValid() {
|
reportValidity(refName) {
|
||||||
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
|
const input = this.getInput(refName);
|
||||||
const formattedDate =
|
const validationResult = this.inputValidityMap[refName] ?? this.logicalValidityMap[refName];
|
||||||
input === this.$refs.startDate
|
|
||||||
? `${this.formattedBounds.start} ${this.formattedBounds.startTime}`
|
|
||||||
: `${this.formattedBounds.end} ${this.formattedBounds.endTime}`;
|
|
||||||
|
|
||||||
const validationResult = this.timeFormatter.validate(formattedDate)
|
|
||||||
? { valid: true }
|
|
||||||
: { valid: false, message: 'Invalid date' };
|
|
||||||
|
|
||||||
return this.handleValidationResults(input, validationResult);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getBoundsLimit() {
|
|
||||||
const configuration = this.configuration.menuOptions
|
|
||||||
.filter((option) => option.timeSystem === this.timeSystem.key)
|
|
||||||
.find((option) => option.limit);
|
|
||||||
|
|
||||||
const limit = configuration ? configuration.limit : undefined;
|
|
||||||
|
|
||||||
return limit;
|
|
||||||
},
|
|
||||||
handleValidationResults(input, validationResult) {
|
|
||||||
if (validationResult.valid !== true) {
|
if (validationResult.valid !== true) {
|
||||||
input.setCustomValidity(validationResult.message);
|
input.setCustomValidity(validationResult.message);
|
||||||
input.title = validationResult.message;
|
input.title = validationResult.message;
|
||||||
this.isDisabled = true;
|
this.hasLogicalValidationErrors = true;
|
||||||
} else {
|
} else {
|
||||||
input.setCustomValidity('');
|
input.setCustomValidity('');
|
||||||
input.title = '';
|
input.title = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$refs.fixedDeltaInput.reportValidity();
|
this.$refs.fixedDeltaInput.reportValidity();
|
||||||
|
},
|
||||||
|
getInput(refName) {
|
||||||
|
if (Object.keys(this.inputValidityMap).includes(refName)) {
|
||||||
|
return this.$refs[refName];
|
||||||
|
}
|
||||||
|
|
||||||
return validationResult.valid;
|
return this.$refs.startDate;
|
||||||
},
|
},
|
||||||
startDateSelected(date) {
|
startDateSelected(date) {
|
||||||
this.formattedBounds.start = this.timeFormatter.format(date).split(' ')[0];
|
this.formattedBounds.startDate = this.timeFormatter.format(date).split(' ')[0];
|
||||||
this.validateAllBounds('startDate');
|
this.validateInput('startDate');
|
||||||
|
this.reportValidity('startDate');
|
||||||
},
|
},
|
||||||
endDateSelected(date) {
|
endDateSelected(date) {
|
||||||
this.formattedBounds.end = this.timeFormatter.format(date).split(' ')[0];
|
this.formattedBounds.endDate = this.timeFormatter.format(date).split(' ')[0];
|
||||||
this.validateAllBounds('endDate');
|
this.validateInput('endDate');
|
||||||
|
this.reportValidity('endDate');
|
||||||
},
|
},
|
||||||
hide($event) {
|
hide($event) {
|
||||||
if ($event.target.className.indexOf('c-button icon-x') > -1) {
|
if ($event.target.className.indexOf('c-button icon-x') > -1) {
|
||||||
|
@ -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 =
|
||||||
|
88
src/plugins/timeline/ExtendedLinesOverlay.vue
Normal file
88
src/plugins/timeline/ExtendedLinesOverlay.vue
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<!--
|
||||||
|
Open MCT, Copyright (c) 2014-2024, United States Government
|
||||||
|
as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
Administration. All rights reserved.
|
||||||
|
|
||||||
|
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
License for the specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
Open MCT includes source code licensed under additional open source
|
||||||
|
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
this source code distribution or the Licensing information page available
|
||||||
|
at runtime from the About dialog for additional information.
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div class="c-timeline__overlay-lines">
|
||||||
|
<div
|
||||||
|
v-for="(lineData, key) in extendedLines"
|
||||||
|
:key="key"
|
||||||
|
class="c-timeline__event-line--extended-container"
|
||||||
|
>
|
||||||
|
<template v-if="lineData.enabled">
|
||||||
|
<div
|
||||||
|
v-for="(line, index) in lineData.lines"
|
||||||
|
:id="line.id"
|
||||||
|
:key="`${index - line.id}`"
|
||||||
|
class="c-timeline__event-line--extended"
|
||||||
|
:class="[
|
||||||
|
line.limitClass,
|
||||||
|
{
|
||||||
|
'--hilite':
|
||||||
|
line.hoverEnabled === true ||
|
||||||
|
(selectedLineId && selectedKeyString === key && line.id === selectedLineId)
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
:style="{ left: `${line.x + leftOffset}px`, height: `${height}px` }"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
import { useExtendedLines } from '../../ui/composables/extendedLines';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ExtendedLinesOverlay',
|
||||||
|
props: {
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
leftOffset: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
extendedLineSelection: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const domainObject = inject('domainObject');
|
||||||
|
const path = inject('path');
|
||||||
|
const openmct = inject('openmct');
|
||||||
|
const { extendedLines: extendedLines } = useExtendedLines(domainObject, path, openmct);
|
||||||
|
|
||||||
|
return { extendedLines };
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selectedLineId() {
|
||||||
|
return this.extendedLineSelection.id || null;
|
||||||
|
},
|
||||||
|
selectedKeyString() {
|
||||||
|
return this.extendedLineSelection.keyString || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -33,10 +33,7 @@ export default function TimelineCompositionPolicy(openmct) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hasDomainAndRange(metadata) {
|
function hasDomainAndRange(metadata) {
|
||||||
return (
|
return metadata.valuesForHints(['domain']).length > 0;
|
||||||
metadata.valuesForHints(['range']).length > 0 &&
|
|
||||||
metadata.valuesForHints(['domain']).length > 0
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasImageTelemetry(domainObject, metadata) {
|
function hasImageTelemetry(domainObject, metadata) {
|
||||||
|
@ -28,6 +28,11 @@
|
|||||||
:show-ucontents="isPlanLikeObject(item.domainObject)"
|
:show-ucontents="isPlanLikeObject(item.domainObject)"
|
||||||
:span-rows-count="item.rowCount"
|
:span-rows-count="item.rowCount"
|
||||||
:domain-object="item.domainObject"
|
:domain-object="item.domainObject"
|
||||||
|
:button-title="`Toggle extended event lines overlay for ${item.domainObject.name}`"
|
||||||
|
button-icon="icon-arrows-up-down"
|
||||||
|
:hide-button="!item.isEventTelemetry"
|
||||||
|
:button-click-on="enableExtendEventLines"
|
||||||
|
:button-click-off="disableExtendEventLines"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
{{ item.domainObject.name }}
|
{{ item.domainObject.name }}
|
||||||
@ -45,9 +50,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
import ObjectView from '@/ui/components/ObjectView.vue';
|
import ObjectView from '@/ui/components/ObjectView.vue';
|
||||||
import SwimLane from '@/ui/components/swim-lane/SwimLane.vue';
|
import SwimLane from '@/ui/components/swim-lane/SwimLane.vue';
|
||||||
|
|
||||||
|
import { useExtendedLines } from '../../ui/composables/extendedLines';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ObjectView,
|
ObjectView,
|
||||||
@ -60,10 +69,20 @@ export default {
|
|||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
const domainObject = inject('domainObject');
|
||||||
|
const objectPath = inject('path');
|
||||||
|
const openmct = inject('openmct');
|
||||||
|
|
||||||
|
const { disable: disableExtendedLinesForObject, enable: enableExtendedLinesForObject } =
|
||||||
|
useExtendedLines(domainObject, objectPath, openmct);
|
||||||
|
|
||||||
|
return { disableExtendedLinesForObject, enableExtendedLinesForObject };
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
domainObject: undefined,
|
domainObject: null,
|
||||||
mutablePromise: undefined,
|
mutablePromise: null,
|
||||||
status: ''
|
status: ''
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -90,6 +109,8 @@ export default {
|
|||||||
this.removeSelectable();
|
this.removeSelectable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.disableExtendEventLines();
|
||||||
|
|
||||||
if (this.removeStatusListener) {
|
if (this.removeStatusListener) {
|
||||||
this.removeStatusListener();
|
this.removeStatusListener();
|
||||||
}
|
}
|
||||||
@ -103,10 +124,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setObject(domainObject) {
|
async setObject(domainObject) {
|
||||||
this.domainObject = domainObject;
|
this.domainObject = domainObject;
|
||||||
this.mutablePromise = undefined;
|
this.mutablePromise = null;
|
||||||
this.$nextTick(() => {
|
await this.$nextTick();
|
||||||
let reference = this.$refs.objectView;
|
let reference = this.$refs.objectView;
|
||||||
|
|
||||||
if (reference) {
|
if (reference) {
|
||||||
@ -129,7 +150,14 @@ export default {
|
|||||||
this.setStatus
|
this.setStatus
|
||||||
);
|
);
|
||||||
this.status = this.openmct.status.get(this.domainObject.identifier);
|
this.status = this.openmct.status.get(this.domainObject.identifier);
|
||||||
});
|
},
|
||||||
|
enableExtendEventLines() {
|
||||||
|
const keyString = this.openmct.objects.makeKeyString(this.item.domainObject.identifier);
|
||||||
|
this.enableExtendedLinesForObject(keyString);
|
||||||
|
},
|
||||||
|
disableExtendEventLines() {
|
||||||
|
const keyString = this.openmct.objects.makeKeyString(this.item.domainObject.identifier);
|
||||||
|
this.disableExtendedLinesForObject(keyString);
|
||||||
},
|
},
|
||||||
setActionCollection(actionCollection) {
|
setActionCollection(actionCollection) {
|
||||||
this.openmct.menus.actionsToMenuItems(
|
this.openmct.menus.actionsToMenuItems(
|
||||||
|
@ -42,8 +42,15 @@
|
|||||||
:key="item.keyString"
|
:key="item.keyString"
|
||||||
class="c-timeline__content js-timeline__content"
|
class="c-timeline__content js-timeline__content"
|
||||||
:item="item"
|
:item="item"
|
||||||
|
:extended-lines-bus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ExtendedLinesOverlay
|
||||||
|
:height="height"
|
||||||
|
:left-offset="extendedLinesLeftOffset"
|
||||||
|
:extended-line-selection="extendedLineSelection"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -56,6 +63,7 @@ import SwimLane from '@/ui/components/swim-lane/SwimLane.vue';
|
|||||||
import TimelineAxis from '../../ui/components/TimeSystemAxis.vue';
|
import TimelineAxis from '../../ui/components/TimeSystemAxis.vue';
|
||||||
import { useAlignment } from '../../ui/composables/alignmentContext.js';
|
import { useAlignment } from '../../ui/composables/alignmentContext.js';
|
||||||
import { getValidatedData, getValidatedGroups } from '../plan/util.js';
|
import { getValidatedData, getValidatedGroups } from '../plan/util.js';
|
||||||
|
import ExtendedLinesOverlay from './ExtendedLinesOverlay.vue';
|
||||||
import TimelineObjectView from './TimelineObjectView.vue';
|
import TimelineObjectView from './TimelineObjectView.vue';
|
||||||
|
|
||||||
const unknownObjectType = {
|
const unknownObjectType = {
|
||||||
@ -65,11 +73,15 @@ const unknownObjectType = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AXES_PADDING = 20;
|
||||||
|
const PLOT_ITEM_H_PX = 100;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
TimelineObjectView,
|
TimelineObjectView,
|
||||||
TimelineAxis,
|
TimelineAxis,
|
||||||
SwimLane
|
SwimLane,
|
||||||
|
ExtendedLinesOverlay
|
||||||
},
|
},
|
||||||
inject: ['openmct', 'domainObject', 'path', 'composition'],
|
inject: ['openmct', 'domainObject', 'path', 'composition'],
|
||||||
setup() {
|
setup() {
|
||||||
@ -90,9 +102,19 @@ export default {
|
|||||||
timeSystems: [],
|
timeSystems: [],
|
||||||
height: 0,
|
height: 0,
|
||||||
useIndependentTime: this.domainObject.configuration.useIndependentTime === true,
|
useIndependentTime: this.domainObject.configuration.useIndependentTime === true,
|
||||||
timeOptions: this.domainObject.configuration.timeOptions
|
timeOptions: this.domainObject.configuration.timeOptions,
|
||||||
|
extendedLineSelection: {},
|
||||||
|
extendedLinesLeftOffset: 0
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
alignmentData: {
|
||||||
|
handler() {
|
||||||
|
this.calculateExtendedLinesLeftOffset();
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
this.resetAlignment();
|
this.resetAlignment();
|
||||||
this.composition.off('add', this.addItem);
|
this.composition.off('add', this.addItem);
|
||||||
@ -101,11 +123,14 @@ export default {
|
|||||||
this.stopFollowingTimeContext();
|
this.stopFollowingTimeContext();
|
||||||
this.handleContentResize.cancel();
|
this.handleContentResize.cancel();
|
||||||
this.contentResizeObserver.disconnect();
|
this.contentResizeObserver.disconnect();
|
||||||
|
this.openmct.selection.off('change', this.checkForLineSelection);
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.items = [];
|
this.items = [];
|
||||||
this.setTimeContext();
|
this.setTimeContext();
|
||||||
|
|
||||||
|
this.openmct.selection.on('change', this.checkForLineSelection);
|
||||||
|
|
||||||
if (this.composition) {
|
if (this.composition) {
|
||||||
this.composition.on('add', this.addItem);
|
this.composition.on('add', this.addItem);
|
||||||
this.composition.on('remove', this.removeItem);
|
this.composition.on('remove', this.removeItem);
|
||||||
@ -129,27 +154,41 @@ export default {
|
|||||||
} else if (domainObject.type === 'gantt-chart') {
|
} else if (domainObject.type === 'gantt-chart') {
|
||||||
rowCount = Object.keys(domainObject.configuration.swimlaneVisibility).length;
|
rowCount = Object.keys(domainObject.configuration.swimlaneVisibility).length;
|
||||||
}
|
}
|
||||||
|
const isEventTelemetry = this.hasEventTelemetry(domainObject);
|
||||||
let height =
|
let height =
|
||||||
domainObject.type === 'telemetry.plot.stacked'
|
domainObject.type === 'telemetry.plot.stacked'
|
||||||
? `${domainObject.composition.length * 100}px`
|
? `${domainObject.composition.length * PLOT_ITEM_H_PX}px`
|
||||||
: '100px';
|
: 'auto';
|
||||||
let item = {
|
let item = {
|
||||||
domainObject,
|
domainObject,
|
||||||
objectPath,
|
objectPath,
|
||||||
type,
|
type,
|
||||||
keyString,
|
keyString,
|
||||||
rowCount,
|
rowCount,
|
||||||
height
|
height,
|
||||||
|
isEventTelemetry
|
||||||
};
|
};
|
||||||
|
|
||||||
this.items.push(item);
|
this.items.push(item);
|
||||||
},
|
},
|
||||||
|
hasEventTelemetry(domainObject) {
|
||||||
|
const metadata = this.openmct.telemetry.getMetadata(domainObject);
|
||||||
|
if (!metadata) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hasDomain = metadata.valuesForHints(['domain']).length > 0;
|
||||||
|
const hasNoRange = !metadata.valuesForHints(['range'])?.length;
|
||||||
|
// for the moment, let's also exclude telemetry with images
|
||||||
|
const hasNoImages = !metadata.valuesForHints(['image']).length;
|
||||||
|
|
||||||
|
return hasDomain && hasNoRange && hasNoImages;
|
||||||
|
},
|
||||||
removeItem(identifier) {
|
removeItem(identifier) {
|
||||||
let index = this.items.findIndex((item) =>
|
let index = this.items.findIndex((item) =>
|
||||||
this.openmct.objects.areIdsEqual(identifier, item.domainObject.identifier)
|
this.openmct.objects.areIdsEqual(identifier, item.domainObject.identifier)
|
||||||
);
|
);
|
||||||
this.items.splice(index, 1);
|
this.items.splice(index, 1);
|
||||||
|
delete this.extendedLinesPerKey[this.openmct.objects.makeKeyString(identifier)];
|
||||||
},
|
},
|
||||||
reorder(reorderPlan) {
|
reorder(reorderPlan) {
|
||||||
let oldItems = this.items.slice();
|
let oldItems = this.items.slice();
|
||||||
@ -165,6 +204,7 @@ export default {
|
|||||||
if (this.height !== clientHeight) {
|
if (this.height !== clientHeight) {
|
||||||
this.height = clientHeight;
|
this.height = clientHeight;
|
||||||
}
|
}
|
||||||
|
this.calculateExtendedLinesLeftOffset();
|
||||||
},
|
},
|
||||||
getClientHeight() {
|
getClientHeight() {
|
||||||
let clientHeight = this.$refs.timelineHolder.getBoundingClientRect().height;
|
let clientHeight = this.$refs.timelineHolder.getBoundingClientRect().height;
|
||||||
@ -222,6 +262,39 @@ export default {
|
|||||||
this.timeContext.off('boundsChanged', this.updateViewBounds);
|
this.timeContext.off('boundsChanged', this.updateViewBounds);
|
||||||
this.timeContext.off('clockChanged', this.updateViewBounds);
|
this.timeContext.off('clockChanged', this.updateViewBounds);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
updateExtendedLines(event) {
|
||||||
|
const { keyString, lines } = event.detail;
|
||||||
|
this.extendedLinesPerKey[keyString] = lines;
|
||||||
|
},
|
||||||
|
checkForLineSelection(selection) {
|
||||||
|
const selectionContext = selection?.[0]?.[0]?.context;
|
||||||
|
const eventType = selectionContext?.type;
|
||||||
|
if (eventType === 'time-strip-event-selection') {
|
||||||
|
const event = selectionContext.event;
|
||||||
|
const selectedObject = selectionContext.item;
|
||||||
|
const keyString = this.openmct.objects.makeKeyString(selectedObject.identifier);
|
||||||
|
this.extendedLineSelection = { keyString, id: event?.time };
|
||||||
|
} else {
|
||||||
|
this.extendedLineSelection = {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
calculateExtendedLinesLeftOffset() {
|
||||||
|
const swimLaneOffset = this.calculateSwimlaneOffset();
|
||||||
|
this.extendedLinesLeftOffset = this.alignmentData.leftWidth + swimLaneOffset;
|
||||||
|
},
|
||||||
|
calculateSwimlaneOffset() {
|
||||||
|
const firstSwimLane = this.$el.querySelector('.c-swimlane__lane-object');
|
||||||
|
if (firstSwimLane) {
|
||||||
|
const timelineHolderRect = this.$refs.timelineHolder.getBoundingClientRect();
|
||||||
|
const laneObjectRect = firstSwimLane.getBoundingClientRect();
|
||||||
|
const offset = laneObjectRect.left - timelineHolderRect.left;
|
||||||
|
const hasAxes = this.alignmentData.axes && Object.keys(this.alignmentData.axes).length > 0;
|
||||||
|
const swimLaneOffset = hasAxes ? offset + AXES_PADDING : offset;
|
||||||
|
return swimLaneOffset;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -25,7 +25,7 @@ import timelineInterceptor from './timelineInterceptor.js';
|
|||||||
import TimelineViewProvider from './TimelineViewProvider.js';
|
import TimelineViewProvider from './TimelineViewProvider.js';
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
return function install(openmct) {
|
function install(openmct) {
|
||||||
openmct.types.addType('time-strip', {
|
openmct.types.addType('time-strip', {
|
||||||
name: 'Time Strip',
|
name: 'Time Strip',
|
||||||
key: 'time-strip',
|
key: 'time-strip',
|
||||||
@ -44,5 +44,7 @@ export default function () {
|
|||||||
openmct.composition.addPolicy(new TimelineCompositionPolicy(openmct).allow);
|
openmct.composition.addPolicy(new TimelineCompositionPolicy(openmct).allow);
|
||||||
|
|
||||||
openmct.objectViews.addProvider(new TimelineViewProvider(openmct));
|
openmct.objectViews.addProvider(new TimelineViewProvider(openmct));
|
||||||
};
|
}
|
||||||
|
|
||||||
|
return install;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,62 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
/********************************************* TIME STRIP */
|
||||||
.c-timeline-holder {
|
.c-timeline-holder {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
|
||||||
|
// Plot view overrides
|
||||||
|
.gl-plot-display-area,
|
||||||
|
.gl-plot-axis-area.gl-plot-y {
|
||||||
|
bottom: $interiorMargin !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-timeline__objects {
|
.c-timeline {
|
||||||
|
&__objects {
|
||||||
display: contents;
|
display: contents;
|
||||||
|
|
||||||
|
.c-swimlane {
|
||||||
|
min-height: 100px; // TEMP!! Will be replaced when heights are set by user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__overlay-lines {
|
||||||
|
//background: rgba(deeppink, 0.2);
|
||||||
|
@include abs();
|
||||||
|
top: 20px; // Offset down to line up with time axis ticks line
|
||||||
|
pointer-events: none; // Allows clicks to pass through
|
||||||
|
z-index: 10; // Ensure it sits atop swimlanes
|
||||||
|
}
|
||||||
|
|
||||||
|
&__no-items {
|
||||||
|
font-style: italic;
|
||||||
|
position: absolute;
|
||||||
|
left: $interiorMargin;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -443,6 +443,10 @@ $colorEventPurpleBg: #31204a;
|
|||||||
$colorEventRedBg: #3c1616;
|
$colorEventRedBg: #3c1616;
|
||||||
$colorEventOrangeBg: #3e2a13;
|
$colorEventOrangeBg: #3e2a13;
|
||||||
$colorEventYellowBg: #3e3316;
|
$colorEventYellowBg: #3e3316;
|
||||||
|
$colorEventPurpleLine: #9e36ff;
|
||||||
|
$colorEventRedLine: #ff2525;
|
||||||
|
$colorEventOrangeLine: #ff8800;
|
||||||
|
$colorEventYellowLine: #fdce22;
|
||||||
|
|
||||||
// Bubble colors
|
// Bubble colors
|
||||||
$colorInfoBubbleBg: #dddddd;
|
$colorInfoBubbleBg: #dddddd;
|
||||||
@ -517,6 +521,11 @@ $colorInProgressBg: $colorTimeRealtimeBg;
|
|||||||
$colorInProgressFg: $colorTimeRealtimeFgSubtle;
|
$colorInProgressFg: $colorTimeRealtimeFgSubtle;
|
||||||
$colorInProgressFgEm: $colorTimeRealtimeFg;
|
$colorInProgressFgEm: $colorTimeRealtimeFg;
|
||||||
$colorGanttSelectedBorder: rgba(#fff, 0.3);
|
$colorGanttSelectedBorder: rgba(#fff, 0.3);
|
||||||
|
$colorEventLine: $colorBodyFg;
|
||||||
|
$colorEventLineExtended: rgba($colorEventLine, 0.3);
|
||||||
|
$colorTimeStripDraftBg: rgba(#a57748, 0.2);
|
||||||
|
$colorTimeStripLabelBg: rgba($colorBodyFg, 0.15);
|
||||||
|
$eventLineW: 1px;
|
||||||
|
|
||||||
// Tree
|
// Tree
|
||||||
$colorTreeBg: transparent;
|
$colorTreeBg: transparent;
|
||||||
|
@ -412,6 +412,10 @@ $colorEventPurpleBg: #31204a;
|
|||||||
$colorEventRedBg: #3c1616;
|
$colorEventRedBg: #3c1616;
|
||||||
$colorEventOrangeBg: #3e2a13;
|
$colorEventOrangeBg: #3e2a13;
|
||||||
$colorEventYellowBg: #3e3316;
|
$colorEventYellowBg: #3e3316;
|
||||||
|
$colorEventPurpleLine: #9e36ff;
|
||||||
|
$colorEventRedLine: #ff2525;
|
||||||
|
$colorEventOrangeLine: #ff8800;
|
||||||
|
$colorEventYellowLine: #fdce22;
|
||||||
|
|
||||||
// Bubble colors
|
// Bubble colors
|
||||||
$colorInfoBubbleBg: #dddddd;
|
$colorInfoBubbleBg: #dddddd;
|
||||||
@ -482,6 +486,11 @@ $colorInProgressBg: $colorTimeRealtimeBg;
|
|||||||
$colorInProgressFg: $colorTimeRealtimeFgSubtle;
|
$colorInProgressFg: $colorTimeRealtimeFgSubtle;
|
||||||
$colorInProgressFgEm: $colorTimeRealtimeFg;
|
$colorInProgressFgEm: $colorTimeRealtimeFg;
|
||||||
$colorGanttSelectedBorder: rgba(#fff, 0.3);
|
$colorGanttSelectedBorder: rgba(#fff, 0.3);
|
||||||
|
$colorEventLine: $colorBodyFg;
|
||||||
|
$colorEventLineExtended: rgba($colorEventLine, 0.3);
|
||||||
|
$colorTimeStripDraftBg: rgba(#a57748, 0.2);
|
||||||
|
$colorTimeStripLabelBg: rgba($colorBodyFg, 0.15);
|
||||||
|
$eventLineW: 1px;
|
||||||
|
|
||||||
// Tree
|
// Tree
|
||||||
$colorTreeBg: transparent;
|
$colorTreeBg: transparent;
|
||||||
|
@ -428,6 +428,10 @@ $colorEventPurpleBg: #31204a;
|
|||||||
$colorEventRedBg: #3c1616;
|
$colorEventRedBg: #3c1616;
|
||||||
$colorEventOrangeBg: #3e2a13;
|
$colorEventOrangeBg: #3e2a13;
|
||||||
$colorEventYellowBg: #3e3316;
|
$colorEventYellowBg: #3e3316;
|
||||||
|
$colorEventPurpleLine: #9e36ff;
|
||||||
|
$colorEventRedLine: #ff2525;
|
||||||
|
$colorEventOrangeLine: #ff8800;
|
||||||
|
$colorEventYellowLine: #fdce22;
|
||||||
|
|
||||||
// Bubble colors
|
// Bubble colors
|
||||||
$colorInfoBubbleBg: #dddddd;
|
$colorInfoBubbleBg: #dddddd;
|
||||||
@ -498,6 +502,11 @@ $colorInProgressBg: $colorTimeRealtimeBg;
|
|||||||
$colorInProgressFg: $colorTimeRealtimeFgSubtle;
|
$colorInProgressFg: $colorTimeRealtimeFgSubtle;
|
||||||
$colorInProgressFgEm: $colorTimeRealtimeFg;
|
$colorInProgressFgEm: $colorTimeRealtimeFg;
|
||||||
$colorGanttSelectedBorder: rgba(#fff, 0.3);
|
$colorGanttSelectedBorder: rgba(#fff, 0.3);
|
||||||
|
$colorEventLine: $colorBodyFg;
|
||||||
|
$colorEventLineExtended: rgba($colorEventLine, 0.3);
|
||||||
|
$colorTimeStripDraftBg: rgba(#a57748, 0.2);
|
||||||
|
$colorTimeStripLabelBg: rgba($colorBodyFg, 0.15);
|
||||||
|
$eventLineW: 1px;
|
||||||
|
|
||||||
// Tree
|
// Tree
|
||||||
$colorTreeBg: transparent;
|
$colorTreeBg: transparent;
|
||||||
|
@ -403,14 +403,18 @@ $colorLimitCyanFg: #d3faff;
|
|||||||
$colorLimitCyanIc: #1795c0;
|
$colorLimitCyanIc: #1795c0;
|
||||||
|
|
||||||
// Events
|
// Events
|
||||||
$colorEventPurpleFg: #6433ff;
|
$colorEventPurpleFg: #6f07ed;
|
||||||
$colorEventRedFg: #aa0000;
|
$colorEventRedFg: #aa0000;
|
||||||
$colorEventOrangeFg: #b84900;
|
$colorEventOrangeFg: #b84900;
|
||||||
$colorEventYellowFg: #867109;
|
$colorEventYellowFg: #a98c04;
|
||||||
$colorEventPurpleBg: #ebe7fb;
|
$colorEventPurpleBg: #ebe7fb;
|
||||||
$colorEventRedBg: #fcefef;
|
$colorEventRedBg: #fcefef;
|
||||||
$colorEventOrangeBg: #ffece3;
|
$colorEventOrangeBg: #ffece3;
|
||||||
$colorEventYellowBg: #fdf8eb;
|
$colorEventYellowBg: #fdf8eb;
|
||||||
|
$colorEventPurpleLine: $colorEventPurpleFg;
|
||||||
|
$colorEventRedLine: $colorEventRedFg;
|
||||||
|
$colorEventOrangeLine: $colorEventOrangeFg;
|
||||||
|
$colorEventYellowLine: $colorEventYellowFg;
|
||||||
|
|
||||||
// Bubble colors
|
// Bubble colors
|
||||||
$colorInfoBubbleBg: $colorMenuBg;
|
$colorInfoBubbleBg: $colorMenuBg;
|
||||||
@ -481,6 +485,11 @@ $colorInProgressBg: #b1e8ff;
|
|||||||
$colorInProgressFg: $colorCurrentFg;
|
$colorInProgressFg: $colorCurrentFg;
|
||||||
$colorInProgressFgEm: $colorCurrentFgEm;
|
$colorInProgressFgEm: $colorCurrentFgEm;
|
||||||
$colorGanttSelectedBorder: #fff;
|
$colorGanttSelectedBorder: #fff;
|
||||||
|
$colorEventLine: $colorBodyFg;
|
||||||
|
$colorEventLineExtended: rgba($colorEventLine, 0.3);
|
||||||
|
$colorTimeStripDraftBg: rgba(#a57748, 0.2);
|
||||||
|
$colorTimeStripLabelBg: rgba($colorBodyFg, 0.15);
|
||||||
|
$eventLineW: 1px;
|
||||||
|
|
||||||
// Tree
|
// Tree
|
||||||
$colorTreeBg: transparent;
|
$colorTreeBg: transparent;
|
||||||
|
@ -252,8 +252,6 @@ tr {
|
|||||||
background-color: $colorEventYellowBg !important;
|
background-color: $colorEventYellowBg !important;
|
||||||
color: $colorEventYellowFg !important;
|
color: $colorEventYellowFg !important;
|
||||||
}
|
}
|
||||||
&--no-style {
|
|
||||||
background-color: $colorBodyBg !important;
|
|
||||||
color: $colorBodyFg !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user