mirror of
https://github.com/nasa/openmct.git
synced 2024-12-20 13:43:09 +00:00
Merge branch 'master' into mission-status-situational-awareness
This commit is contained in:
commit
90d08982e0
@ -5,20 +5,20 @@ executors:
|
|||||||
- image: mcr.microsoft.com/playwright:v1.39.0-focal
|
- image: mcr.microsoft.com/playwright:v1.39.0-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
|
||||||
PERCY_LOGLEVEL: 'debug' # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742)
|
PERCY_LOGLEVEL: "debug" # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742)
|
||||||
ubuntu:
|
ubuntu:
|
||||||
machine:
|
machine:
|
||||||
image: ubuntu-2204:current
|
image: ubuntu-2204:current
|
||||||
docker_layer_caching: true
|
docker_layer_caching: true
|
||||||
parameters:
|
parameters:
|
||||||
BUST_CACHE:
|
BUST_CACHE:
|
||||||
description: 'Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!'
|
description: "Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!"
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
commands:
|
commands:
|
||||||
build_and_install:
|
build_and_install:
|
||||||
description: 'All steps used to build and install. Will use cache if found'
|
description: "All steps used to build and install. Will use cache if found"
|
||||||
parameters:
|
parameters:
|
||||||
node-version:
|
node-version:
|
||||||
type: string
|
type: string
|
||||||
@ -30,7 +30,7 @@ commands:
|
|||||||
node-version: << parameters.node-version >>
|
node-version: << parameters.node-version >>
|
||||||
- run: npm install --no-audit --progress=false
|
- run: npm install --no-audit --progress=false
|
||||||
restore_cache_cmd:
|
restore_cache_cmd:
|
||||||
description: 'Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache'
|
description: "Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache"
|
||||||
parameters:
|
parameters:
|
||||||
node-version:
|
node-version:
|
||||||
type: string
|
type: string
|
||||||
@ -42,7 +42,7 @@ commands:
|
|||||||
- restore_cache:
|
- restore_cache:
|
||||||
key: deps--{{ arch }}--{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
|
key: deps--{{ arch }}--{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
|
||||||
save_cache_cmd:
|
save_cache_cmd:
|
||||||
description: 'Custom command for saving cache.'
|
description: "Custom command for saving cache."
|
||||||
parameters:
|
parameters:
|
||||||
node-version:
|
node-version:
|
||||||
type: string
|
type: string
|
||||||
@ -53,7 +53,7 @@ commands:
|
|||||||
- ~/.npm
|
- ~/.npm
|
||||||
- node_modules
|
- node_modules
|
||||||
generate_and_store_version_and_filesystem_artifacts:
|
generate_and_store_version_and_filesystem_artifacts:
|
||||||
description: 'Track important packages and files'
|
description: "Track important packages and files"
|
||||||
steps:
|
steps:
|
||||||
- run: |
|
- run: |
|
||||||
[[ $EUID -ne 0 ]] && (sudo mkdir -p /tmp/artifacts && sudo chmod 777 /tmp/artifacts) || (mkdir -p /tmp/artifacts && chmod 777 /tmp/artifacts)
|
[[ $EUID -ne 0 ]] && (sudo mkdir -p /tmp/artifacts && sudo chmod 777 /tmp/artifacts) || (mkdir -p /tmp/artifacts && chmod 777 /tmp/artifacts)
|
||||||
@ -64,7 +64,7 @@ commands:
|
|||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: /tmp/artifacts/
|
path: /tmp/artifacts/
|
||||||
generate_e2e_code_cov_report:
|
generate_e2e_code_cov_report:
|
||||||
description: 'Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test'
|
description: "Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test"
|
||||||
parameters:
|
parameters:
|
||||||
suite:
|
suite:
|
||||||
type: string
|
type: string
|
||||||
@ -129,7 +129,7 @@ jobs:
|
|||||||
node-version: lts/hydrogen
|
node-version: lts/hydrogen
|
||||||
- when: #Only install chrome-beta when running the 'full' suite to save $$$
|
- when: #Only install chrome-beta when running the 'full' suite to save $$$
|
||||||
condition:
|
condition:
|
||||||
equal: ['full', <<parameters.suite>>]
|
equal: ["full", <<parameters.suite>>]
|
||||||
steps:
|
steps:
|
||||||
- run: npx playwright install chrome-beta
|
- run: npx playwright install chrome-beta
|
||||||
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
|
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
|
||||||
@ -251,8 +251,6 @@ workflows:
|
|||||||
- e2e-test:
|
- e2e-test:
|
||||||
name: e2e-stable
|
name: e2e-stable
|
||||||
suite: stable
|
suite: stable
|
||||||
- mem-test
|
|
||||||
- perf-test
|
|
||||||
- visual-a11y-tests:
|
- visual-a11y-tests:
|
||||||
name: visual-test-ci
|
name: visual-test-ci
|
||||||
suite: ci
|
suite: ci
|
||||||
@ -278,7 +276,7 @@ workflows:
|
|||||||
- e2e-couchdb
|
- e2e-couchdb
|
||||||
triggers:
|
triggers:
|
||||||
- schedule:
|
- schedule:
|
||||||
cron: '0 0 * * *'
|
cron: "0 0 * * *"
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
|
@ -493,6 +493,8 @@
|
|||||||
"WCAG",
|
"WCAG",
|
||||||
"stackedplot",
|
"stackedplot",
|
||||||
"Andale",
|
"Andale",
|
||||||
|
"checksnapshots",
|
||||||
|
"specced",
|
||||||
"composables"
|
"composables"
|
||||||
],
|
],
|
||||||
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US"],
|
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US"],
|
||||||
|
58
.github/workflows/e2e-perf.yml
vendored
Normal file
58
.github/workflows/e2e-perf.yml
vendored
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
name: 'e2e-perf'
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: master
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- labeled
|
||||||
|
- opened
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
jobs:
|
||||||
|
e2e-full:
|
||||||
|
if: contains(github.event.pull_request.labels.*.name, 'pr:e2e:perf') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 'lts/hydrogen'
|
||||||
|
|
||||||
|
- name: Cache NPM dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-node-
|
||||||
|
|
||||||
|
- run: npx playwright@1.39.0 install
|
||||||
|
- run: npm install --cache ~/.npm --no-audit --progress=false
|
||||||
|
- run: npm run test:perf:localhost
|
||||||
|
- run: npm run test:perf:contract
|
||||||
|
- run: npm run test:perf:memory
|
||||||
|
- name: Archive test results
|
||||||
|
if: success() || failure()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
path: test-results
|
||||||
|
|
||||||
|
- name: Remove pr:e2e:perf label (if present)
|
||||||
|
if: always()
|
||||||
|
uses: actions/github-script@v6
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const { owner, repo, number } = context.issue;
|
||||||
|
const labelToRemove = 'pr:e2e:perf';
|
||||||
|
try {
|
||||||
|
await github.rest.issues.removeLabel({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: number,
|
||||||
|
name: labelToRemove
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
|
||||||
|
}
|
@ -109,7 +109,7 @@ For those interested in the mechanics of snapshot testing with Playwright, you c
|
|||||||
// from our package.json or circleCI configuration file
|
// from our package.json or circleCI configuration file
|
||||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-focal /bin/bash
|
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-focal /bin/bash
|
||||||
npm install
|
npm install
|
||||||
npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot
|
npm run test:e2e:checksnapshots
|
||||||
```
|
```
|
||||||
|
|
||||||
### Updating Snapshots
|
### Updating Snapshots
|
||||||
@ -134,6 +134,12 @@ npm install
|
|||||||
npm run test:e2e:updatesnapshots
|
npm run test:e2e:updatesnapshots
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Once that's done, you'll need to run the following to verify that the changes do not cause more problems:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run test:e2e:checksnapshots
|
||||||
|
```
|
||||||
|
|
||||||
## Automated Accessibility (a11y) Testing
|
## Automated Accessibility (a11y) Testing
|
||||||
|
|
||||||
Open MCT incorporates accessibility testing through two primary methods to ensure its compliance with accessibility standards:
|
Open MCT incorporates accessibility testing through two primary methods to ensure its compliance with accessibility standards:
|
||||||
|
@ -6,7 +6,8 @@
|
|||||||
"end": 1660343797000,
|
"end": 1660343797000,
|
||||||
"type": "Group 1",
|
"type": "Group 1",
|
||||||
"color": "orange",
|
"color": "orange",
|
||||||
"textColor": "white"
|
"textColor": "white",
|
||||||
|
"id": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Past event 2",
|
"name": "Past event 2",
|
||||||
@ -14,7 +15,8 @@
|
|||||||
"end": 1660429160000,
|
"end": 1660429160000,
|
||||||
"type": "Group 1",
|
"type": "Group 1",
|
||||||
"color": "orange",
|
"color": "orange",
|
||||||
"textColor": "white"
|
"textColor": "white",
|
||||||
|
"id": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Past event 3",
|
"name": "Past event 3",
|
||||||
@ -22,7 +24,8 @@
|
|||||||
"end": 1660503981000,
|
"end": 1660503981000,
|
||||||
"type": "Group 1",
|
"type": "Group 1",
|
||||||
"color": "orange",
|
"color": "orange",
|
||||||
"textColor": "white"
|
"textColor": "white",
|
||||||
|
"id": 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Past event 4",
|
"name": "Past event 4",
|
||||||
@ -30,7 +33,8 @@
|
|||||||
"end": 1660624108000,
|
"end": 1660624108000,
|
||||||
"type": "Group 1",
|
"type": "Group 1",
|
||||||
"color": "orange",
|
"color": "orange",
|
||||||
"textColor": "white"
|
"textColor": "white",
|
||||||
|
"id": 4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Past event 5",
|
"name": "Past event 5",
|
||||||
@ -38,7 +42,8 @@
|
|||||||
"end": 1660681529000,
|
"end": 1660681529000,
|
||||||
"type": "Group 1",
|
"type": "Group 1",
|
||||||
"color": "orange",
|
"color": "orange",
|
||||||
"textColor": "white"
|
"textColor": "white",
|
||||||
|
"id": 5
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,8 @@
|
|||||||
"end": 1660343797000,
|
"end": 1660343797000,
|
||||||
"type": "Group 1",
|
"type": "Group 1",
|
||||||
"color": "orange",
|
"color": "orange",
|
||||||
"textColor": "white"
|
"textColor": "white",
|
||||||
|
"id": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Time until supper",
|
"name": "Time until supper",
|
||||||
@ -14,7 +15,8 @@
|
|||||||
"end": 1650420410000,
|
"end": 1650420410000,
|
||||||
"type": "Group 2",
|
"type": "Group 2",
|
||||||
"color": "blue",
|
"color": "blue",
|
||||||
"textColor": "white"
|
"textColor": "white",
|
||||||
|
"id": 2
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Group 2": [
|
"Group 2": [
|
||||||
@ -24,7 +26,8 @@
|
|||||||
"end": 1650320102001,
|
"end": 1650320102001,
|
||||||
"type": "Group 2",
|
"type": "Group 2",
|
||||||
"color": "green",
|
"color": "green",
|
||||||
"textColor": "white"
|
"textColor": "white",
|
||||||
|
"id": 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Time since last accident",
|
"name": "Time since last accident",
|
||||||
@ -32,7 +35,8 @@
|
|||||||
"end": 1650320102002,
|
"end": 1650320102002,
|
||||||
"type": "Group 1",
|
"type": "Group 1",
|
||||||
"color": "yellow",
|
"color": "yellow",
|
||||||
"textColor": "white"
|
"textColor": "white",
|
||||||
|
"id": 4
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ import {
|
|||||||
assertPlanActivities,
|
assertPlanActivities,
|
||||||
assertPlanOrderedSwimLanes
|
assertPlanOrderedSwimLanes
|
||||||
} from '../../../helper/planningUtils.js';
|
} from '../../../helper/planningUtils.js';
|
||||||
import { test } from '../../../pluginFixtures.js';
|
import { expect, test } from '../../../pluginFixtures.js';
|
||||||
|
|
||||||
const testPlan1 = JSON.parse(
|
const testPlan1 = JSON.parse(
|
||||||
fs.readFileSync(
|
fs.readFileSync(
|
||||||
@ -63,4 +63,47 @@ test.describe('Plan', () => {
|
|||||||
});
|
});
|
||||||
await assertPlanOrderedSwimLanes(page, testPlanWithOrderedLanes, planWithSwimLanes.url);
|
await assertPlanOrderedSwimLanes(page, testPlanWithOrderedLanes, planWithSwimLanes.url);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Allows setting the state of an activity when selected.', async ({ page }) => {
|
||||||
|
const groups = Object.keys(testPlan1);
|
||||||
|
const firstGroupKey = groups[0];
|
||||||
|
const firstGroupItems = testPlan1[firstGroupKey];
|
||||||
|
const firstActivity = firstGroupItems[0];
|
||||||
|
const lastActivity = firstGroupItems[firstGroupItems.length - 1];
|
||||||
|
const startBound = firstActivity.start;
|
||||||
|
// Set the endBound to the end time of the current activity
|
||||||
|
let endBound = lastActivity.end;
|
||||||
|
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||||
|
if (endBound === startBound) {
|
||||||
|
// Prevent oddities with setting start and end bound equal
|
||||||
|
// via URL params
|
||||||
|
endBound += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to fixed time mode with all plan events within the bounds
|
||||||
|
await page.goto(
|
||||||
|
`${plan.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=plan.view`
|
||||||
|
);
|
||||||
|
|
||||||
|
// select the first activity in the list
|
||||||
|
await page.getByText('Past event 1').click();
|
||||||
|
|
||||||
|
// Find the activity state section in the inspector
|
||||||
|
await page.getByRole('tab', { name: 'Activity' }).click();
|
||||||
|
|
||||||
|
// Check that activity state dropdown selection shows the `set status` option by default
|
||||||
|
await expect(page.getByLabel('Activity Status').locator("[aria-selected='true']")).toHaveText(
|
||||||
|
'Not started'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Change the selection of the activity status
|
||||||
|
await page.getByRole('combobox').selectOption({ label: 'Aborted' });
|
||||||
|
// select a different activity and back to the previous one
|
||||||
|
await page.getByText('Past event 2').click();
|
||||||
|
await page.getByText('Past event 1').click();
|
||||||
|
// Check that activity state dropdown selection shows the previously selected option by default
|
||||||
|
await expect(page.getByLabel('Activity Status').locator("[aria-selected='true']")).toHaveText(
|
||||||
|
'Aborted'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -30,6 +30,11 @@ const examplePlanSmall3 = JSON.parse(
|
|||||||
new URL('../../../test-data/examplePlans/ExamplePlan_Small3.json', import.meta.url)
|
new URL('../../../test-data/examplePlans/ExamplePlan_Small3.json', import.meta.url)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
const examplePlanSmall1 = JSON.parse(
|
||||||
|
fs.readFileSync(
|
||||||
|
new URL('../../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)
|
||||||
|
)
|
||||||
|
);
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const START_TIME_COLUMN = 0;
|
const START_TIME_COLUMN = 0;
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
@ -40,53 +45,8 @@ const ACTIVITY_COLUMN = 3;
|
|||||||
const HEADER_ROW = 0;
|
const HEADER_ROW = 0;
|
||||||
const NUM_COLUMNS = 4;
|
const NUM_COLUMNS = 4;
|
||||||
|
|
||||||
const testPlan = {
|
|
||||||
TEST_GROUP: [
|
|
||||||
{
|
|
||||||
name: 'Past event 1',
|
|
||||||
start: 1660320408000,
|
|
||||||
end: 1660343797000,
|
|
||||||
type: 'TEST-GROUP',
|
|
||||||
color: 'orange',
|
|
||||||
textColor: 'white'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Past event 2',
|
|
||||||
start: 1660406808000,
|
|
||||||
end: 1660429160000,
|
|
||||||
type: 'TEST-GROUP',
|
|
||||||
color: 'orange',
|
|
||||||
textColor: 'white'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Past event 3',
|
|
||||||
start: 1660493208000,
|
|
||||||
end: 1660503981000,
|
|
||||||
type: 'TEST-GROUP',
|
|
||||||
color: 'orange',
|
|
||||||
textColor: 'white'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Past event 4',
|
|
||||||
start: 1660579608000,
|
|
||||||
end: 1660624108000,
|
|
||||||
type: 'TEST-GROUP',
|
|
||||||
color: 'orange',
|
|
||||||
textColor: 'white'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Past event 5',
|
|
||||||
start: 1660666008000,
|
|
||||||
end: 1660681529000,
|
|
||||||
type: 'TEST-GROUP',
|
|
||||||
color: 'orange',
|
|
||||||
textColor: 'white'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
test.describe('Time List', () => {
|
test.describe('Time List', () => {
|
||||||
test('Create a Time List, add a single Plan to it and verify all the activities are displayed with no milliseconds', async ({
|
test("Create a Time List, add a single Plan to it, verify all the activities are displayed with no milliseconds and selecting an activity shows it's properties", async ({
|
||||||
page
|
page
|
||||||
}) => {
|
}) => {
|
||||||
// Goto baseURL
|
// Goto baseURL
|
||||||
@ -103,12 +63,16 @@ test.describe('Time List', () => {
|
|||||||
await test.step('Create a Plan and add it to the timelist', async () => {
|
await test.step('Create a Plan and add it to the timelist', async () => {
|
||||||
await createPlanFromJSON(page, {
|
await createPlanFromJSON(page, {
|
||||||
name: 'Test Plan',
|
name: 'Test Plan',
|
||||||
json: testPlan,
|
json: examplePlanSmall1,
|
||||||
parent: timelist.uuid
|
parent: timelist.uuid
|
||||||
});
|
});
|
||||||
|
const groups = Object.keys(examplePlanSmall1);
|
||||||
const startBound = testPlan.TEST_GROUP[0].start;
|
const firstGroupKey = groups[0];
|
||||||
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
|
const firstGroupItems = examplePlanSmall1[firstGroupKey];
|
||||||
|
const firstActivity = firstGroupItems[0];
|
||||||
|
const lastActivity = firstGroupItems[firstGroupItems.length - 1];
|
||||||
|
const startBound = firstActivity.start;
|
||||||
|
const endBound = lastActivity.end;
|
||||||
|
|
||||||
// Switch to fixed time mode with all plan events within the bounds
|
// Switch to fixed time mode with all plan events within the bounds
|
||||||
await page.goto(
|
await page.goto(
|
||||||
@ -118,7 +82,7 @@ test.describe('Time List', () => {
|
|||||||
// Verify all events are displayed
|
// Verify all events are displayed
|
||||||
const eventCount = await page.getByRole('row').count();
|
const eventCount = await page.getByRole('row').count();
|
||||||
// subtracting one for the header
|
// subtracting one for the header
|
||||||
await expect(eventCount - 1).toEqual(testPlan.TEST_GROUP.length);
|
await expect(eventCount - 1).toEqual(firstGroupItems.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('Does not show milliseconds in times', async () => {
|
await test.step('Does not show milliseconds in times', async () => {
|
||||||
@ -131,6 +95,17 @@ test.describe('Time List', () => {
|
|||||||
await expect(row.locator('.--end')).not.toContainText('.');
|
await expect(row.locator('.--end')).not.toContainText('.');
|
||||||
await expect(row.locator('.--duration')).not.toContainText('.');
|
await expect(row.locator('.--duration')).not.toContainText('.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await test.step('Shows activity properties when a row is selected', async () => {
|
||||||
|
await page.getByRole('row').nth(2).click();
|
||||||
|
|
||||||
|
// Find the activity state section in the inspector
|
||||||
|
await page.getByRole('tab', { name: 'Activity' }).click();
|
||||||
|
// Check that activity state label is displayed in the inspector.
|
||||||
|
await expect(page.getByLabel('Activity Status').locator("[aria-selected='true']")).toHaveText(
|
||||||
|
'Not started'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ import { expect, test } from '../../../../pluginFixtures.js';
|
|||||||
|
|
||||||
let conditionSetUrl;
|
let conditionSetUrl;
|
||||||
|
|
||||||
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
test.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () => {
|
||||||
test.beforeAll(async ({ browser }) => {
|
test.beforeAll(async ({ browser }) => {
|
||||||
//TODO: This needs to be refactored
|
//TODO: This needs to be refactored
|
||||||
const context = await browser.newContext();
|
const context = await browser.newContext();
|
||||||
@ -68,30 +68,35 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//Begin suite of tests again localStorage
|
//Begin suite of tests again localStorage
|
||||||
test('Condition set object properties persist in main view and inspector @localStorage', async ({
|
test.fixme(
|
||||||
page
|
'Condition set object properties persist in main view and inspector @localStorage',
|
||||||
}) => {
|
async ({ page }) => {
|
||||||
//Navigate to baseURL with injected localStorage
|
test.info().annotations.push({
|
||||||
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/7421'
|
||||||
|
});
|
||||||
|
//Navigate to baseURL with injected localStorage
|
||||||
|
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
|
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
|
||||||
await expect
|
await expect
|
||||||
.soft(page.locator('.l-browse-bar__object-name'))
|
.soft(page.locator('.l-browse-bar__object-name'))
|
||||||
.toContainText('Unnamed Condition Set');
|
.toContainText('Unnamed Condition Set');
|
||||||
|
|
||||||
//Assertions on loaded Condition Set in Inspector
|
//Assertions on loaded Condition Set in Inspector
|
||||||
expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
|
expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
|
||||||
|
|
||||||
//Reload Page
|
//Reload Page
|
||||||
await Promise.all([page.reload(), page.waitForLoadState('networkidle')]);
|
await Promise.all([page.reload(), page.waitForLoadState('networkidle')]);
|
||||||
|
|
||||||
//Re-verify after reload
|
//Re-verify after reload
|
||||||
await expect
|
await expect
|
||||||
.soft(page.locator('.l-browse-bar__object-name'))
|
.soft(page.locator('.l-browse-bar__object-name'))
|
||||||
.toContainText('Unnamed Condition Set');
|
.toContainText('Unnamed Condition Set');
|
||||||
//Assertions on loaded Condition Set in Inspector
|
//Assertions on loaded Condition Set in Inspector
|
||||||
expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
|
expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
test('condition set object can be modified on @localStorage', async ({ page, openmctConfig }) => {
|
test('condition set object can be modified on @localStorage', async ({ page, openmctConfig }) => {
|
||||||
const { myItemsFolderName } = openmctConfig;
|
const { myItemsFolderName } = openmctConfig;
|
||||||
|
|
||||||
|
@ -161,6 +161,13 @@ test.describe('Display Layout', () => {
|
|||||||
const trimmedDisplayValue = displayLayoutValue.trim();
|
const trimmedDisplayValue = displayLayoutValue.trim();
|
||||||
|
|
||||||
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
|
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
|
||||||
|
|
||||||
|
// ensure we can right click on the alpha-numeric widget and view historical data
|
||||||
|
await page.getByLabel('Sine', { exact: true }).click({
|
||||||
|
button: 'right'
|
||||||
|
});
|
||||||
|
await page.getByLabel('View Historical Data').click();
|
||||||
|
await expect(page.getByLabel('Plot Container Style Target')).toBeVisible();
|
||||||
});
|
});
|
||||||
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in fixed time', async ({
|
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in fixed time', async ({
|
||||||
page
|
page
|
||||||
|
@ -136,7 +136,11 @@ test.describe('Gauge', () => {
|
|||||||
// TODO: Verify changes in the UI
|
// TODO: Verify changes in the UI
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Gauge does not display NaN when data not available', async ({ page }) => {
|
test.fixme('Gauge does not display NaN when data not available', async ({ page }) => {
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/7421'
|
||||||
|
});
|
||||||
// Create a Gauge
|
// Create a Gauge
|
||||||
const gauge = await createDomainObjectWithDefaults(page, {
|
const gauge = await createDomainObjectWithDefaults(page, {
|
||||||
type: 'Gauge'
|
type: 'Gauge'
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 30 KiB |
Binary file not shown.
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 39 KiB |
@ -224,31 +224,37 @@ test.describe('Overlay Plot', () => {
|
|||||||
expect(yAxis3Group.getByRole('listitem').nth(0).getByText(swgB.name)).toBeTruthy();
|
expect(yAxis3Group.getByRole('listitem').nth(0).getByText(swgB.name)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Clicking on an item in the elements pool brings up the plot preview with data points', async ({
|
test.fixme(
|
||||||
page
|
'Clicking on an item in the elements pool brings up the plot preview with data points',
|
||||||
}) => {
|
async ({ page }) => {
|
||||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
test.info().annotations.push({
|
||||||
type: 'Overlay Plot'
|
type: 'issue',
|
||||||
});
|
description: 'https://github.com/nasa/openmct/issues/7421'
|
||||||
|
});
|
||||||
|
|
||||||
const swgA = await createDomainObjectWithDefaults(page, {
|
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||||
type: 'Sine Wave Generator',
|
type: 'Overlay Plot'
|
||||||
parent: overlayPlot.uuid
|
});
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(overlayPlot.url);
|
const swgA = await createDomainObjectWithDefaults(page, {
|
||||||
// Wait for plot series data to load and be drawn
|
type: 'Sine Wave Generator',
|
||||||
await waitForPlotsToRender(page);
|
parent: overlayPlot.uuid
|
||||||
await page.getByLabel('Edit Object').click();
|
});
|
||||||
|
|
||||||
await page.getByRole('tab', { name: 'Elements' }).click();
|
await page.goto(overlayPlot.url);
|
||||||
|
// Wait for plot series data to load and be drawn
|
||||||
|
await waitForPlotsToRender(page);
|
||||||
|
await page.getByLabel('Edit Object').click();
|
||||||
|
|
||||||
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
|
await page.getByRole('tab', { name: 'Elements' }).click();
|
||||||
|
|
||||||
const plotPixels = await getCanvasPixels(page, '.js-overlay canvas');
|
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
|
||||||
const plotPixelSize = plotPixels.length;
|
|
||||||
expect(plotPixelSize).toBeGreaterThan(0);
|
const plotPixels = await getCanvasPixels(page, '.js-overlay canvas');
|
||||||
});
|
const plotPixelSize = plotPixels.length;
|
||||||
|
expect(plotPixelSize).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -52,7 +52,11 @@ test.describe('Plot Rendering', () => {
|
|||||||
expect(createMineFolderRequests.length).toEqual(0);
|
expect(createMineFolderRequests.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Plot is rendered when infinity values exist', async ({ page }) => {
|
test.fixme('Plot is rendered when infinity values exist', async ({ page }) => {
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/7421'
|
||||||
|
});
|
||||||
// Edit Plot
|
// Edit Plot
|
||||||
await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject);
|
await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject);
|
||||||
|
|
||||||
|
33
e2e/tests/functional/plugins/preview/preview.e2e.spec.js
Normal file
33
e2e/tests/functional/plugins/preview/preview.e2e.spec.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 is dedicated to testing the preview plugin.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test } from '../../../../pluginFixtures.js';
|
||||||
|
|
||||||
|
test.describe('Preview mode', () => {
|
||||||
|
test.fixme('all context menu items are available for a telemetry table', async ({ page }) => {
|
||||||
|
// compare the context menu options when viewing a telemetry table directly
|
||||||
|
// vs when it is presented in preview mode (e.g. edit mode is enabled and the table is clicked on from the tree)
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,125 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2023, 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, expandEntireTree } from '../../../../appActions.js';
|
||||||
|
import { expect, test } from '../../../../pluginFixtures.js';
|
||||||
|
|
||||||
|
test.describe('Reload action', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
const displayLayout = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Display Layout'
|
||||||
|
});
|
||||||
|
|
||||||
|
const alphaTable = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Telemetry Table',
|
||||||
|
name: 'Alpha Table'
|
||||||
|
});
|
||||||
|
|
||||||
|
const betaTable = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Telemetry Table',
|
||||||
|
name: 'Beta Table'
|
||||||
|
});
|
||||||
|
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Sine Wave Generator',
|
||||||
|
parent: alphaTable.uuid,
|
||||||
|
customParameters: {
|
||||||
|
'[aria-label="Data Rate (hz)"]': '0.001'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Sine Wave Generator',
|
||||||
|
parent: betaTable.uuid,
|
||||||
|
customParameters: {
|
||||||
|
'[aria-label="Data Rate (hz)"]': '0.001'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(displayLayout.url);
|
||||||
|
|
||||||
|
// Expand all folders
|
||||||
|
await expandEntireTree(page);
|
||||||
|
|
||||||
|
await page.getByLabel('Edit Object', { exact: true }).click();
|
||||||
|
|
||||||
|
await page.dragAndDrop(`text='Alpha Table'`, '.l-layout__grid-holder', {
|
||||||
|
targetPosition: { x: 0, y: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.dragAndDrop(`text='Beta Table'`, '.l-layout__grid-holder', {
|
||||||
|
targetPosition: { x: 0, y: 250 }
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.locator('button[title="Save"]').click();
|
||||||
|
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can reload display layout and its children', async ({ page }) => {
|
||||||
|
const beforeReloadAlphaTelemetryValue = await page
|
||||||
|
.getByLabel('Alpha Table table content')
|
||||||
|
.getByLabel('wavelengths table cell')
|
||||||
|
.first()
|
||||||
|
.getAttribute('title');
|
||||||
|
const beforeReloadBetaTelemetryValue = await page
|
||||||
|
.getByLabel('Beta Table table content')
|
||||||
|
.getByLabel('wavelengths table cell')
|
||||||
|
.first()
|
||||||
|
.getAttribute('title');
|
||||||
|
// reload alpha
|
||||||
|
await page.getByTitle('View menu items').first().click();
|
||||||
|
await page.getByRole('menuitem', { name: /Reload/ }).click();
|
||||||
|
|
||||||
|
const afterReloadAlphaTelemetryValue = await page
|
||||||
|
.getByLabel('Alpha Table table content')
|
||||||
|
.getByLabel('wavelengths table cell')
|
||||||
|
.first()
|
||||||
|
.getAttribute('title');
|
||||||
|
const afterReloadBetaTelemetryValue = await page
|
||||||
|
.getByLabel('Beta Table table content')
|
||||||
|
.getByLabel('wavelengths table cell')
|
||||||
|
.first()
|
||||||
|
.getAttribute('title');
|
||||||
|
|
||||||
|
expect(beforeReloadAlphaTelemetryValue).not.toEqual(afterReloadAlphaTelemetryValue);
|
||||||
|
expect(beforeReloadBetaTelemetryValue).toEqual(afterReloadBetaTelemetryValue);
|
||||||
|
|
||||||
|
// now reload parent
|
||||||
|
await page.getByTitle('More actions').click();
|
||||||
|
await page.getByRole('menuitem', { name: /Reload/ }).click();
|
||||||
|
|
||||||
|
const fullReloadAlphaTelemetryValue = await page
|
||||||
|
.getByLabel('Alpha Table table content')
|
||||||
|
.getByLabel('wavelengths table cell')
|
||||||
|
.first()
|
||||||
|
.getAttribute('title');
|
||||||
|
const fullReloadBetaTelemetryValue = await page
|
||||||
|
.getByLabel('Beta Table table content')
|
||||||
|
.getByLabel('wavelengths table cell')
|
||||||
|
.first()
|
||||||
|
.getAttribute('title');
|
||||||
|
|
||||||
|
expect(fullReloadAlphaTelemetryValue).not.toEqual(afterReloadAlphaTelemetryValue);
|
||||||
|
expect(fullReloadBetaTelemetryValue).not.toEqual(afterReloadBetaTelemetryValue);
|
||||||
|
});
|
||||||
|
});
|
@ -24,13 +24,18 @@ import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
|||||||
import { expect, test } from '../../../../pluginFixtures.js';
|
import { expect, test } from '../../../../pluginFixtures.js';
|
||||||
|
|
||||||
test.describe('Tabs View', () => {
|
test.describe('Tabs View', () => {
|
||||||
test('Renders tabbed elements', async ({ page }) => {
|
let tabsView;
|
||||||
|
let table;
|
||||||
|
let notebook;
|
||||||
|
let sineWaveGenerator;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
const tabsView = await createDomainObjectWithDefaults(page, {
|
tabsView = await createDomainObjectWithDefaults(page, {
|
||||||
type: 'Tabs View'
|
type: 'Tabs View'
|
||||||
});
|
});
|
||||||
const table = await createDomainObjectWithDefaults(page, {
|
table = await createDomainObjectWithDefaults(page, {
|
||||||
type: 'Telemetry Table',
|
type: 'Telemetry Table',
|
||||||
parent: tabsView.uuid
|
parent: tabsView.uuid
|
||||||
});
|
});
|
||||||
@ -38,19 +43,21 @@ test.describe('Tabs View', () => {
|
|||||||
type: 'Event Message Generator',
|
type: 'Event Message Generator',
|
||||||
parent: table.uuid
|
parent: table.uuid
|
||||||
});
|
});
|
||||||
const notebook = await createDomainObjectWithDefaults(page, {
|
notebook = await createDomainObjectWithDefaults(page, {
|
||||||
type: 'Notebook',
|
type: 'Notebook',
|
||||||
parent: tabsView.uuid
|
parent: tabsView.uuid
|
||||||
});
|
});
|
||||||
const sineWaveGenerator = await createDomainObjectWithDefaults(page, {
|
sineWaveGenerator = await createDomainObjectWithDefaults(page, {
|
||||||
type: 'Sine Wave Generator',
|
type: 'Sine Wave Generator',
|
||||||
parent: tabsView.uuid
|
parent: tabsView.uuid
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
page.goto(tabsView.url);
|
test('Renders tabbed elements', async ({ page }) => {
|
||||||
|
await page.goto(tabsView.url);
|
||||||
|
|
||||||
// select first tab
|
// select first tab
|
||||||
await page.getByLabel(`${table.name} tab`).click();
|
await page.getByLabel(`${table.name} tab`, { 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();
|
||||||
|
|
||||||
@ -58,7 +65,7 @@ test.describe('Tabs View', () => {
|
|||||||
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
|
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
|
||||||
|
|
||||||
// select second tab
|
// select second tab
|
||||||
await page.getByLabel(`${notebook.name} tab`).click();
|
await page.getByLabel(`${notebook.name} tab`, { exact: true }).click();
|
||||||
|
|
||||||
// ensure notebook visible
|
// ensure notebook visible
|
||||||
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
|
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
|
||||||
@ -67,7 +74,7 @@ test.describe('Tabs View', () => {
|
|||||||
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
|
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
|
||||||
|
|
||||||
// select third tab
|
// select third tab
|
||||||
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
|
await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click();
|
||||||
|
|
||||||
// expect sine wave generator visible
|
// expect sine wave generator visible
|
||||||
await expect(page.locator('.c-plot')).toBeVisible();
|
await expect(page.locator('.c-plot')).toBeVisible();
|
||||||
@ -78,7 +85,7 @@ test.describe('Tabs View', () => {
|
|||||||
await expect(page.locator('canvas').nth(1)).toBeVisible();
|
await expect(page.locator('canvas').nth(1)).toBeVisible();
|
||||||
|
|
||||||
// now try to select the first tab again
|
// now try to select the first tab again
|
||||||
await page.getByLabel(`${table.name} tab`).click();
|
await page.getByLabel(`${table.name} tab`, { 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();
|
||||||
|
|
||||||
@ -86,3 +93,29 @@ test.describe('Tabs View', () => {
|
|||||||
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
|
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Tabs View CRUD', () => {
|
||||||
|
let tabsView;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
tabsView = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Tabs View'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Eager Load Tabs is the default and then can be toggled off', async ({ page }) => {
|
||||||
|
test.info().annotations.push({
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/nasa/openmct/issues/7198'
|
||||||
|
});
|
||||||
|
await page.goto(tabsView.url);
|
||||||
|
|
||||||
|
await page.getByLabel('Edit Object').click();
|
||||||
|
await page.getByLabel('More actions').click();
|
||||||
|
await page.getByLabel('Edit Properties...').click();
|
||||||
|
await expect(await page.getByLabel('Eager Load Tabs')).not.toBeChecked();
|
||||||
|
await page.getByLabel('Eager Load Tabs').setChecked(true);
|
||||||
|
await expect(await page.getByLabel('Eager Load Tabs')).toBeChecked();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -64,10 +64,9 @@ test.describe('Telemetry Table', () => {
|
|||||||
|
|
||||||
// Get the most recent telemetry date
|
// Get the most recent telemetry date
|
||||||
const latestTelemetryDate = await page
|
const latestTelemetryDate = await page
|
||||||
.locator('table.c-telemetry-table__body > tbody > tr')
|
.getByLabel('table content')
|
||||||
|
.getByLabel('utc table cell')
|
||||||
.last()
|
.last()
|
||||||
.locator('td')
|
|
||||||
.nth(1)
|
|
||||||
.getAttribute('title');
|
.getAttribute('title');
|
||||||
|
|
||||||
// Verify that it is <= our new end bound
|
// Verify that it is <= our new end bound
|
||||||
@ -91,7 +90,7 @@ test.describe('Telemetry Table', () => {
|
|||||||
await page.getByRole('searchbox', { name: 'message filter input' }).click();
|
await page.getByRole('searchbox', { name: 'message filter input' }).click();
|
||||||
await page.getByRole('searchbox', { name: 'message filter input' }).fill('Roger');
|
await page.getByRole('searchbox', { name: 'message filter input' }).fill('Roger');
|
||||||
|
|
||||||
let cells = await page.getByRole('cell', { name: /Roger/ }).all();
|
let cells = await page.getByRole('cell').getByText(/Roger/).all();
|
||||||
// ensure we've got more than one cell
|
// ensure we've got more than one cell
|
||||||
expect(cells.length).toBeGreaterThan(1);
|
expect(cells.length).toBeGreaterThan(1);
|
||||||
// ensure the text content of each cell contains the search term
|
// ensure the text content of each cell contains the search term
|
||||||
@ -103,7 +102,10 @@ test.describe('Telemetry Table', () => {
|
|||||||
await page.getByRole('searchbox', { name: 'message filter input' }).click();
|
await page.getByRole('searchbox', { name: 'message filter input' }).click();
|
||||||
await page.getByRole('searchbox', { name: 'message filter input' }).fill('Dodger');
|
await page.getByRole('searchbox', { name: 'message filter input' }).fill('Dodger');
|
||||||
|
|
||||||
cells = await page.getByRole('cell', { name: /Dodger/ }).all();
|
cells = await page
|
||||||
|
.getByRole('cell')
|
||||||
|
.getByText(/Dodger/)
|
||||||
|
.all();
|
||||||
// ensure we've got more than one cell
|
// ensure we've got more than one cell
|
||||||
expect(cells.length).toBe(0);
|
expect(cells.length).toBe(0);
|
||||||
// ensure the text content of each cell contains the search term
|
// ensure the text content of each cell contains the search term
|
||||||
@ -135,7 +137,7 @@ test.describe('Telemetry Table', () => {
|
|||||||
await page.getByRole('searchbox', { name: 'message filter input' }).click();
|
await page.getByRole('searchbox', { name: 'message filter input' }).click();
|
||||||
await page.getByRole('searchbox', { name: 'message filter input' }).fill('/[Rr]oger/');
|
await page.getByRole('searchbox', { name: 'message filter input' }).fill('/[Rr]oger/');
|
||||||
|
|
||||||
let cells = await page.getByRole('cell', { name: /Roger/ }).all();
|
let cells = await page.getByRole('cell').getByText(/Roger/).all();
|
||||||
// ensure we've got more than one cell
|
// ensure we've got more than one cell
|
||||||
expect(cells.length).toBeGreaterThan(1);
|
expect(cells.length).toBeGreaterThan(1);
|
||||||
// ensure the text content of each cell contains the search term
|
// ensure the text content of each cell contains the search term
|
||||||
@ -147,7 +149,10 @@ test.describe('Telemetry Table', () => {
|
|||||||
await page.getByRole('searchbox', { name: 'message filter input' }).click();
|
await page.getByRole('searchbox', { name: 'message filter input' }).click();
|
||||||
await page.getByRole('searchbox', { name: 'message filter input' }).fill('/[Dd]oger/');
|
await page.getByRole('searchbox', { name: 'message filter input' }).fill('/[Dd]oger/');
|
||||||
|
|
||||||
cells = await page.getByRole('cell', { name: /Dodger/ }).all();
|
cells = await page
|
||||||
|
.getByRole('cell')
|
||||||
|
.getByText(/Dodger/)
|
||||||
|
.all();
|
||||||
// ensure we've got more than one cell
|
// ensure we've got more than one cell
|
||||||
expect(cells.length).toBe(0);
|
expect(cells.length).toBe(0);
|
||||||
// ensure the text content of each cell contains the search term
|
// ensure the text content of each cell contains the search term
|
||||||
|
@ -24,7 +24,7 @@ import { createDomainObjectWithDefaults, waitForPlotsToRender } from '../../appA
|
|||||||
import { expect, test } from '../../pluginFixtures.js';
|
import { expect, test } from '../../pluginFixtures.js';
|
||||||
|
|
||||||
test.describe('Tabs View', () => {
|
test.describe('Tabs View', () => {
|
||||||
test('Renders tabbed elements nicely', async ({ page }) => {
|
test('Renders tabbed elements only when visible', async ({ page }) => {
|
||||||
// Code to hook into the requestAnimationFrame function and log each call
|
// Code to hook into the requestAnimationFrame function and log each call
|
||||||
let animationCalls = [];
|
let animationCalls = [];
|
||||||
await page.exposeFunction('logCall', (callCount) => {
|
await page.exposeFunction('logCall', (callCount) => {
|
||||||
@ -64,24 +64,24 @@ test.describe('Tabs View', () => {
|
|||||||
page.goto(tabsView.url);
|
page.goto(tabsView.url);
|
||||||
|
|
||||||
// select first tab
|
// select first tab
|
||||||
await page.getByLabel(`${table.name} tab`).click();
|
await page.getByLabel(`${table.name} tab`, { 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();
|
||||||
|
|
||||||
// select second tab
|
// select second tab
|
||||||
await page.getByLabel(`${notebook.name} tab`).click();
|
await page.getByLabel(`${notebook.name} tab`, { exact: true }).click();
|
||||||
|
|
||||||
// expect notebook visible
|
// expect notebook visible
|
||||||
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
|
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
|
||||||
|
|
||||||
// select third tab
|
// select third tab
|
||||||
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
|
await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click();
|
||||||
|
|
||||||
// ensure sine wave generator visible
|
// ensure sine wave generator visible
|
||||||
expect(await page.locator('.c-plot').isVisible()).toBe(true);
|
expect(await page.locator('.c-plot').isVisible()).toBe(true);
|
||||||
|
|
||||||
// now select notebook and clear animation calls
|
// now select notebook and clear animation calls
|
||||||
await page.getByLabel(`${notebook.name} tab`).click();
|
await page.getByLabel(`${notebook.name} tab`, { exact: true }).click();
|
||||||
animationCalls = [];
|
animationCalls = [];
|
||||||
// expect notebook visible
|
// expect notebook visible
|
||||||
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
|
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
|
||||||
@ -89,7 +89,7 @@ test.describe('Tabs View', () => {
|
|||||||
|
|
||||||
// select sine wave generator and clear animation calls
|
// select sine wave generator and clear animation calls
|
||||||
animationCalls = [];
|
animationCalls = [];
|
||||||
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
|
await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click();
|
||||||
|
|
||||||
// ensure sine wave generator visible
|
// ensure sine wave generator visible
|
||||||
await waitForPlotsToRender(page);
|
await waitForPlotsToRender(page);
|
93
e2e/tests/visual-a11y/imagery.visual.spec.js
Normal file
93
e2e/tests/visual-a11y/imagery.visual.spec.js
Normal file
@ -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.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import percySnapshot from '@percy/playwright';
|
||||||
|
|
||||||
|
import { createDomainObjectWithDefaults, setRealTimeMode } from '../../appActions.js';
|
||||||
|
import { VISUAL_URL } from '../../constants.js';
|
||||||
|
import { expect, test } from '../../pluginFixtures.js';
|
||||||
|
|
||||||
|
test.describe('Visual - Example Imagery', () => {
|
||||||
|
let exampleImagery;
|
||||||
|
let parentLayout;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
parentLayout = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Display Layout',
|
||||||
|
name: 'Parent Layout'
|
||||||
|
});
|
||||||
|
|
||||||
|
exampleImagery = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Example Imagery',
|
||||||
|
name: 'Example Imagery Test',
|
||||||
|
parent: parentLayout.uuid
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modify Example Imagery to create a really stable Example Imagery
|
||||||
|
await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' });
|
||||||
|
await page.getByRole('button', { name: 'More actions' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
|
||||||
|
await page
|
||||||
|
.locator('#imageLocation-textarea')
|
||||||
|
.fill(
|
||||||
|
'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-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-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-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-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-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-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-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-18731.jpg'
|
||||||
|
);
|
||||||
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||||
|
await page.getByTitle('Collapse Browse Pane').click();
|
||||||
|
await page.getByTitle('Collapse Inspect Pane').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Example Imagery in Fixed Time', async ({ page, theme }) => {
|
||||||
|
await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Image Wrapper')).toBeVisible();
|
||||||
|
|
||||||
|
await percySnapshot(page, `Example Imagery in Fixed Time (theme: ${theme})`);
|
||||||
|
|
||||||
|
await page.getByLabel('Image Wrapper').hover();
|
||||||
|
|
||||||
|
await percySnapshot(page, `Example Imagery Hover in Fixed Time (theme: ${theme})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Example Imagery in Real Time', async ({ page, theme }) => {
|
||||||
|
await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
await setRealTimeMode(page, true);
|
||||||
|
//Temporary to close the dialog
|
||||||
|
await page.getByLabel('Submit time offsets').click();
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Image Wrapper')).toBeVisible();
|
||||||
|
|
||||||
|
await percySnapshot(page, `Example Imagery in Real Time (theme: ${theme})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Example Imagery in Display Layout', async ({ page, theme }) => {
|
||||||
|
await page.goto(parentLayout.url, { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Image Wrapper')).toBeVisible();
|
||||||
|
|
||||||
|
await percySnapshot(page, `Example Imagery in Display Layout (theme: ${theme})`);
|
||||||
|
});
|
||||||
|
});
|
@ -92,6 +92,8 @@ GeneratorProvider.prototype.request = function (domainObject, request) {
|
|||||||
var workerRequest = this.makeWorkerRequest(domainObject, request);
|
var workerRequest = this.makeWorkerRequest(domainObject, request);
|
||||||
workerRequest.start = request.start;
|
workerRequest.start = request.start;
|
||||||
workerRequest.end = request.end;
|
workerRequest.end = request.end;
|
||||||
|
workerRequest.size = request.size;
|
||||||
|
workerRequest.strategy = request.strategy;
|
||||||
|
|
||||||
return this.workerInterface.request(workerRequest);
|
return this.workerInterface.request(workerRequest);
|
||||||
};
|
};
|
||||||
|
@ -130,48 +130,37 @@
|
|||||||
var now = Date.now();
|
var now = Date.now();
|
||||||
var start = request.start;
|
var start = request.start;
|
||||||
var end = request.end > now ? now : request.end;
|
var end = request.end > now ? now : request.end;
|
||||||
var amplitude = request.amplitude;
|
|
||||||
var period = request.period;
|
var period = request.period;
|
||||||
var offset = request.offset;
|
|
||||||
var dataRateInHz = request.dataRateInHz;
|
var dataRateInHz = request.dataRateInHz;
|
||||||
var phase = request.phase;
|
|
||||||
var randomness = request.randomness;
|
|
||||||
var loadDelay = Math.max(request.loadDelay, 0);
|
var loadDelay = Math.max(request.loadDelay, 0);
|
||||||
var infinityValues = request.infinityValues;
|
var size = request.size;
|
||||||
var exceedFloat32 = request.exceedFloat32;
|
var duration = end - start;
|
||||||
|
|
||||||
var step = 1000 / dataRateInHz;
|
var step = 1000 / dataRateInHz;
|
||||||
|
var maxPoints = Math.floor(duration / step);
|
||||||
var nextStep = start - (start % step) + step;
|
var nextStep = start - (start % step) + step;
|
||||||
|
|
||||||
var data = [];
|
var data = [];
|
||||||
|
|
||||||
for (; nextStep < end && data.length < 5000; nextStep += step) {
|
if (request.strategy === 'minmax' && size) {
|
||||||
data.push({
|
// Calculate the number of cycles to include based on size (2 points per cycle)
|
||||||
utc: nextStep,
|
var totalCycles = Math.min(Math.floor(size / 2), Math.floor(duration / period));
|
||||||
yesterday: nextStep - 60 * 60 * 24 * 1000,
|
|
||||||
sin: sin(
|
for (let cycle = 0; cycle < totalCycles; cycle++) {
|
||||||
nextStep,
|
// Distribute cycles evenly across the time range
|
||||||
period,
|
let cycleStart = start + (duration / totalCycles) * cycle;
|
||||||
amplitude,
|
let minPointTime = cycleStart; // Assuming min at the start of the cycle
|
||||||
offset,
|
let maxPointTime = cycleStart + period / 2; // Assuming max at the halfway of the cycle
|
||||||
phase,
|
|
||||||
randomness,
|
data.push(createDataPoint(minPointTime, request), createDataPoint(maxPointTime, request));
|
||||||
infinityValues,
|
}
|
||||||
exceedFloat32
|
} else {
|
||||||
),
|
for (let i = 0; i < maxPoints && nextStep < end; i++, nextStep += step) {
|
||||||
wavelengths: wavelengths(),
|
data.push(createDataPoint(nextStep, request));
|
||||||
intensities: intensities(),
|
}
|
||||||
cos: cos(
|
}
|
||||||
nextStep,
|
|
||||||
period,
|
if (request.strategy !== 'minmax' && size) {
|
||||||
amplitude,
|
data = data.slice(-size);
|
||||||
offset,
|
|
||||||
phase,
|
|
||||||
randomness,
|
|
||||||
infinityValues,
|
|
||||||
exceedFloat32
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadDelay === 0) {
|
if (loadDelay === 0) {
|
||||||
@ -181,6 +170,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createDataPoint(time, request) {
|
||||||
|
return {
|
||||||
|
utc: time,
|
||||||
|
yesterday: time - 60 * 60 * 24 * 1000,
|
||||||
|
sin: sin(
|
||||||
|
time,
|
||||||
|
request.period,
|
||||||
|
request.amplitude,
|
||||||
|
request.offset,
|
||||||
|
request.phase,
|
||||||
|
request.randomness,
|
||||||
|
request.infinityValues,
|
||||||
|
request.exceedFloat32
|
||||||
|
),
|
||||||
|
wavelengths: wavelengths(),
|
||||||
|
intensities: intensities(),
|
||||||
|
cos: cos(
|
||||||
|
time,
|
||||||
|
request.period,
|
||||||
|
request.amplitude,
|
||||||
|
request.offset,
|
||||||
|
request.phase,
|
||||||
|
request.randomness,
|
||||||
|
request.infinityValues,
|
||||||
|
request.exceedFloat32
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function postOnRequest(message, request, data) {
|
function postOnRequest(message, request, data) {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
id: message.id,
|
id: message.id,
|
||||||
|
@ -57,9 +57,9 @@
|
|||||||
"karma-webpack": "5.0.0",
|
"karma-webpack": "5.0.0",
|
||||||
"location-bar": "3.0.1",
|
"location-bar": "3.0.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"marked": "11.1.0",
|
"marked": "11.2.0",
|
||||||
"mini-css-extract-plugin": "2.7.6",
|
"mini-css-extract-plugin": "2.7.6",
|
||||||
"moment": "2.29.4",
|
"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",
|
||||||
"npm-run-all2": "6.1.1",
|
"npm-run-all2": "6.1.1",
|
||||||
@ -72,7 +72,7 @@
|
|||||||
"resolve-url-loader": "5.0.0",
|
"resolve-url-loader": "5.0.0",
|
||||||
"sanitize-html": "2.11.0",
|
"sanitize-html": "2.11.0",
|
||||||
"sass": "1.68.0",
|
"sass": "1.68.0",
|
||||||
"sass-loader": "13.3.2",
|
"sass-loader": "14.0.0",
|
||||||
"sinon": "17.0.0",
|
"sinon": "17.0.0",
|
||||||
"style-loader": "3.3.3",
|
"style-loader": "3.3.3",
|
||||||
"terser-webpack-plugin": "5.3.9",
|
"terser-webpack-plugin": "5.3.9",
|
||||||
@ -111,6 +111,7 @@
|
|||||||
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",
|
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",
|
||||||
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
|
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
|
||||||
"test:e2e:generatedata": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @generatedata",
|
"test:e2e:generatedata": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @generatedata",
|
||||||
|
"test:e2e:checksnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --retries=0",
|
||||||
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots",
|
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots",
|
||||||
"test:e2e:visual:ci": "percy exec --config ./e2e/.percy.ci.yml --partial -- npx playwright test --config=e2e/playwright-visual-a11y.config.js --project=chrome --grep-invert @unstable",
|
"test:e2e:visual:ci": "percy exec --config ./e2e/.percy.ci.yml --partial -- npx playwright test --config=e2e/playwright-visual-a11y.config.js --project=chrome --grep-invert @unstable",
|
||||||
"test:e2e:visual:full": "percy exec --config ./e2e/.percy.nightly.yml -- npx playwright test --config=e2e/playwright-visual-a11y.config.js --grep-invert @unstable",
|
"test:e2e:visual:full": "percy exec --config ./e2e/.percy.nightly.yml -- npx playwright test --config=e2e/playwright-visual-a11y.config.js --grep-invert @unstable",
|
||||||
|
@ -251,6 +251,7 @@ export class MCT extends EventEmitter {
|
|||||||
this.install(this.plugins.FlexibleLayout());
|
this.install(this.plugins.FlexibleLayout());
|
||||||
this.install(this.plugins.GoToOriginalAction());
|
this.install(this.plugins.GoToOriginalAction());
|
||||||
this.install(this.plugins.OpenInNewTabAction());
|
this.install(this.plugins.OpenInNewTabAction());
|
||||||
|
this.install(this.plugins.ReloadAction());
|
||||||
this.install(this.plugins.WebPage());
|
this.install(this.plugins.WebPage());
|
||||||
this.install(this.plugins.Condition());
|
this.install(this.plugins.Condition());
|
||||||
this.install(this.plugins.ConditionWidget());
|
this.install(this.plugins.ConditionWidget());
|
||||||
|
@ -28,7 +28,8 @@
|
|||||||
v-for="action in actionGroups"
|
v-for="action in actionGroups"
|
||||||
:key="action.name"
|
:key="action.name"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
:aria-disabled="action.isDisabled"
|
||||||
|
:class="action.cssClass"
|
||||||
:aria-label="action.name"
|
:aria-label="action.name"
|
||||||
:title="action.description"
|
:title="action.description"
|
||||||
@click="action.onItemClicked"
|
@click="action.onItemClicked"
|
||||||
@ -51,7 +52,8 @@
|
|||||||
v-for="action in options.actions"
|
v-for="action in options.actions"
|
||||||
:key="action.name"
|
:key="action.name"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
:aria-disabled="action.isDisabled"
|
||||||
|
:class="action.cssClass"
|
||||||
:aria-label="action.name"
|
:aria-label="action.name"
|
||||||
:title="action.description"
|
:title="action.description"
|
||||||
@click="action.onItemClicked"
|
@click="action.onItemClicked"
|
||||||
|
@ -37,7 +37,8 @@
|
|||||||
v-for="action in actionGroups"
|
v-for="action in actionGroups"
|
||||||
:key="action.name"
|
:key="action.name"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
:aria-disabled="action.isDisabled"
|
||||||
|
:class="action.cssClass"
|
||||||
:title="action.description"
|
:title="action.description"
|
||||||
@click="action.onItemClicked"
|
@click="action.onItemClicked"
|
||||||
@mouseover="toggleItemDescription(action)"
|
@mouseover="toggleItemDescription(action)"
|
||||||
|
@ -99,7 +99,13 @@ export default class ObjectAPI {
|
|||||||
this.cache = {};
|
this.cache = {};
|
||||||
this.interceptorRegistry = new InterceptorRegistry();
|
this.interceptorRegistry = new InterceptorRegistry();
|
||||||
|
|
||||||
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'restricted-notebook', 'plan', 'annotation'];
|
this.SYNCHRONIZED_OBJECT_TYPES = [
|
||||||
|
'notebook',
|
||||||
|
'restricted-notebook',
|
||||||
|
'plan',
|
||||||
|
'annotation',
|
||||||
|
'activity-states'
|
||||||
|
];
|
||||||
|
|
||||||
this.errors = {
|
this.errors = {
|
||||||
Conflict: ConflictError
|
Conflict: ConflictError
|
||||||
|
194
src/api/telemetry/BatchingWebSocket.js
Normal file
194
src/api/telemetry/BatchingWebSocket.js
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT Web, Copyright (c) 2014-2024, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT Web 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 Web 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 installWorker from './WebSocketWorker.js';
|
||||||
|
const DEFAULT_RATE_MS = 1000;
|
||||||
|
/**
|
||||||
|
* Describes the strategy to be used when batching WebSocket messages
|
||||||
|
*
|
||||||
|
* @typedef BatchingStrategy
|
||||||
|
* @property {Function} shouldBatchMessage a function that accepts a single
|
||||||
|
* argument - the raw message received from the websocket. Every message
|
||||||
|
* received will be evaluated against this function so it should be performant.
|
||||||
|
* Note also that this function is executed in a worker, so it must be
|
||||||
|
* completely self-contained with no external dependencies. The function
|
||||||
|
* should return `true` if the message should be batched, and `false` if not.
|
||||||
|
* @property {Function} getBatchIdFromMessage a function that accepts a
|
||||||
|
* single argument - the raw message received from the websocket. Only messages
|
||||||
|
* where `shouldBatchMessage` has evaluated to true will be passed into this
|
||||||
|
* function. The function should return a unique value on which to batch the
|
||||||
|
* messages. For example a telemetry, channel, or parameter identifier.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Provides a reliable and convenient WebSocket abstraction layer that handles
|
||||||
|
* a lot of boilerplate common to managing WebSocket connections such as:
|
||||||
|
* - Establishing a WebSocket connection to a server
|
||||||
|
* - Reconnecting on error, with a fallback strategy
|
||||||
|
* - Queuing messages so that clients can send messages without concern for the current
|
||||||
|
* connection state of the WebSocket.
|
||||||
|
*
|
||||||
|
* The WebSocket that it manages is based in a dedicated worker so that network
|
||||||
|
* concerns are not handled on the main event loop. This allows for performant receipt
|
||||||
|
* and batching of messages without blocking either the UI or server.
|
||||||
|
*
|
||||||
|
* @memberof module:openmct.telemetry
|
||||||
|
*/
|
||||||
|
class BatchingWebSocket extends EventTarget {
|
||||||
|
#worker;
|
||||||
|
#openmct;
|
||||||
|
#showingRateLimitNotification;
|
||||||
|
#rate;
|
||||||
|
|
||||||
|
constructor(openmct) {
|
||||||
|
super();
|
||||||
|
// Install worker, register listeners etc.
|
||||||
|
const workerFunction = `(${installWorker.toString()})()`;
|
||||||
|
const workerBlob = new Blob([workerFunction]);
|
||||||
|
const workerUrl = URL.createObjectURL(workerBlob, { type: 'application/javascript' });
|
||||||
|
this.#worker = new Worker(workerUrl);
|
||||||
|
this.#openmct = openmct;
|
||||||
|
this.#showingRateLimitNotification = false;
|
||||||
|
this.#rate = DEFAULT_RATE_MS;
|
||||||
|
|
||||||
|
const routeMessageToHandler = this.#routeMessageToHandler.bind(this);
|
||||||
|
this.#worker.addEventListener('message', routeMessageToHandler);
|
||||||
|
openmct.on(
|
||||||
|
'destroy',
|
||||||
|
() => {
|
||||||
|
this.disconnect();
|
||||||
|
URL.revokeObjectURL(workerUrl);
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will establish a WebSocket connection to the provided url
|
||||||
|
* @param {string} url The URL to connect to
|
||||||
|
*/
|
||||||
|
connect(url) {
|
||||||
|
this.#worker.postMessage({
|
||||||
|
type: 'connect',
|
||||||
|
url
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#readyForNextBatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
#readyForNextBatch() {
|
||||||
|
this.#worker.postMessage({
|
||||||
|
type: 'readyForNextBatch'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message to the WebSocket.
|
||||||
|
* @param {any} message The message to send. Can be any type supported by WebSockets.
|
||||||
|
* See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send#data
|
||||||
|
*/
|
||||||
|
sendMessage(message) {
|
||||||
|
this.#worker.postMessage({
|
||||||
|
type: 'message',
|
||||||
|
message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the strategy used to both decide which raw messages to batch, and how to group
|
||||||
|
* them.
|
||||||
|
* @param {BatchingStrategy} strategy The batching strategy to use when evaluating
|
||||||
|
* raw messages from the WebSocket.
|
||||||
|
*/
|
||||||
|
setBatchingStrategy(strategy) {
|
||||||
|
const serializedStrategy = {
|
||||||
|
shouldBatchMessage: strategy.shouldBatchMessage.toString(),
|
||||||
|
getBatchIdFromMessage: strategy.getBatchIdFromMessage.toString()
|
||||||
|
};
|
||||||
|
|
||||||
|
this.#worker.postMessage({
|
||||||
|
type: 'setBatchingStrategy',
|
||||||
|
serializedStrategy
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When using batching, sets the rate at which batches of messages are released.
|
||||||
|
* @param {Number} rate the amount of time to wait, in ms, between batches.
|
||||||
|
*/
|
||||||
|
setRate(rate) {
|
||||||
|
this.#rate = rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Number} maxBatchSize the maximum length of a batch of messages. For example,
|
||||||
|
* the maximum number of telemetry values to batch before dropping them
|
||||||
|
* Note that this is a fail-safe that is only invoked if performance drops to the
|
||||||
|
* point where Open MCT cannot keep up with the amount of telemetry it is receiving.
|
||||||
|
* In this event it will sacrifice the oldest telemetry in the batch in favor of the
|
||||||
|
* most recent telemetry. The user will be informed that telemetry has been dropped.
|
||||||
|
*
|
||||||
|
* This should be set appropriately for the expected data rate. eg. If telemetry
|
||||||
|
* is received at 10Hz for each telemetry point, then a minimal combination of batch
|
||||||
|
* size and rate is 10 and 1000 respectively. Ideally you would add some margin, so
|
||||||
|
* 15 would probably be a better batch size.
|
||||||
|
*/
|
||||||
|
setMaxBatchSize(maxBatchSize) {
|
||||||
|
this.#worker.postMessage({
|
||||||
|
type: 'setMaxBatchSize',
|
||||||
|
maxBatchSize
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect the associated WebSocket. Generally speaking there is no need to call
|
||||||
|
* this manually.
|
||||||
|
*/
|
||||||
|
disconnect() {
|
||||||
|
this.#worker.postMessage({
|
||||||
|
type: 'disconnect'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#routeMessageToHandler(message) {
|
||||||
|
if (message.data.type === 'batch') {
|
||||||
|
if (message.data.batch.dropped === true && !this.#showingRateLimitNotification) {
|
||||||
|
const notification = this.#openmct.notifications.alert(
|
||||||
|
'Telemetry dropped due to client rate limiting.',
|
||||||
|
{ hint: 'Refresh individual telemetry views to retrieve dropped telemetry if needed.' }
|
||||||
|
);
|
||||||
|
this.#showingRateLimitNotification = true;
|
||||||
|
notification.once('minimized', () => {
|
||||||
|
this.#showingRateLimitNotification = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.dispatchEvent(new CustomEvent('batch', { detail: message.data.batch }));
|
||||||
|
setTimeout(() => {
|
||||||
|
this.#readyForNextBatch();
|
||||||
|
}, this.#rate);
|
||||||
|
} else if (message.data.type === 'message') {
|
||||||
|
this.dispatchEvent(new CustomEvent('message', { detail: message.data.message }));
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown message type: ${message.data.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BatchingWebSocket;
|
@ -23,6 +23,7 @@
|
|||||||
import objectUtils from 'objectUtils';
|
import objectUtils from 'objectUtils';
|
||||||
|
|
||||||
import CustomStringFormatter from '../../plugins/displayLayout/CustomStringFormatter.js';
|
import CustomStringFormatter from '../../plugins/displayLayout/CustomStringFormatter.js';
|
||||||
|
import BatchingWebSocket from './BatchingWebSocket.js';
|
||||||
import DefaultMetadataProvider from './DefaultMetadataProvider.js';
|
import DefaultMetadataProvider from './DefaultMetadataProvider.js';
|
||||||
import TelemetryCollection from './TelemetryCollection.js';
|
import TelemetryCollection from './TelemetryCollection.js';
|
||||||
import TelemetryMetadataManager from './TelemetryMetadataManager.js';
|
import TelemetryMetadataManager from './TelemetryMetadataManager.js';
|
||||||
@ -54,6 +55,28 @@ import TelemetryValueFormatter from './TelemetryValueFormatter.js';
|
|||||||
* @memberof module:openmct.TelemetryAPI~
|
* @memberof module:openmct.TelemetryAPI~
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes and bounds requests for telemetry data.
|
||||||
|
*
|
||||||
|
* @typedef TelemetrySubscriptionOptions
|
||||||
|
* @property {String} [strategy] symbolic identifier directing providers on how
|
||||||
|
* to handle telemetry subscriptions. The default behavior is 'latest' which will
|
||||||
|
* always return a single telemetry value with each callback, and in the event
|
||||||
|
* of throttling will always prioritize the latest data, meaning intermediate
|
||||||
|
* data will be skipped. Alternatively, the `batch` strategy can be used, which
|
||||||
|
* will return all telemetry values since the last callback. This strategy is
|
||||||
|
* useful for cases where intermediate data is important, such as when
|
||||||
|
* rendering a telemetry plot or table. If `batch` is specified, the subscription
|
||||||
|
* callback will be invoked with an Array.
|
||||||
|
*
|
||||||
|
* @memberof module:openmct.TelemetryAPI~
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SUBSCRIBE_STRATEGY = {
|
||||||
|
LATEST: 'latest',
|
||||||
|
BATCH: 'batch'
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utilities for telemetry
|
* Utilities for telemetry
|
||||||
* @interface TelemetryAPI
|
* @interface TelemetryAPI
|
||||||
@ -61,6 +84,11 @@ import TelemetryValueFormatter from './TelemetryValueFormatter.js';
|
|||||||
*/
|
*/
|
||||||
export default class TelemetryAPI {
|
export default class TelemetryAPI {
|
||||||
#isGreedyLAD;
|
#isGreedyLAD;
|
||||||
|
#subscribeCache;
|
||||||
|
|
||||||
|
get SUBSCRIBE_STRATEGY() {
|
||||||
|
return SUBSCRIBE_STRATEGY;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(openmct) {
|
constructor(openmct) {
|
||||||
this.openmct = openmct;
|
this.openmct = openmct;
|
||||||
@ -78,6 +106,8 @@ export default class TelemetryAPI {
|
|||||||
this.valueFormatterCache = new WeakMap();
|
this.valueFormatterCache = new WeakMap();
|
||||||
this.requestInterceptorRegistry = new TelemetryRequestInterceptorRegistry();
|
this.requestInterceptorRegistry = new TelemetryRequestInterceptorRegistry();
|
||||||
this.#isGreedyLAD = true;
|
this.#isGreedyLAD = true;
|
||||||
|
this.BatchingWebSocket = BatchingWebSocket;
|
||||||
|
this.#subscribeCache = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
abortAllRequests() {
|
abortAllRequests() {
|
||||||
@ -378,54 +408,111 @@ export default class TelemetryAPI {
|
|||||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||||
* @param {module:openmct.DomainObject} domainObject the object
|
* @param {module:openmct.DomainObject} domainObject the object
|
||||||
* which has associated telemetry
|
* which has associated telemetry
|
||||||
* @param {TelemetryRequestOptions} options configuration items for subscription
|
* @param {TelemetrySubscriptionOptions} options configuration items for subscription
|
||||||
* @param {Function} callback the callback to invoke with new data, as
|
* @param {Function} callback the callback to invoke with new data, as
|
||||||
* it becomes available
|
* it becomes available
|
||||||
* @returns {Function} a function which may be called to terminate
|
* @returns {Function} a function which may be called to terminate
|
||||||
* the subscription
|
* the subscription
|
||||||
*/
|
*/
|
||||||
subscribe(domainObject, callback, options) {
|
subscribe(domainObject, callback, options = { strategy: SUBSCRIBE_STRATEGY.LATEST }) {
|
||||||
|
const requestedStrategy = options.strategy || SUBSCRIBE_STRATEGY.LATEST;
|
||||||
|
|
||||||
if (domainObject.type === 'unknown') {
|
if (domainObject.type === 'unknown') {
|
||||||
return () => {};
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const provider = this.findSubscriptionProvider(domainObject);
|
const provider = this.findSubscriptionProvider(domainObject, options);
|
||||||
|
const supportsBatching =
|
||||||
|
Boolean(provider?.supportsBatching) && provider?.supportsBatching(domainObject, options);
|
||||||
|
|
||||||
if (!this.subscribeCache) {
|
if (!this.#subscribeCache) {
|
||||||
this.subscribeCache = {};
|
this.#subscribeCache = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyString = objectUtils.makeKeyString(domainObject.identifier);
|
const keyString = objectUtils.makeKeyString(domainObject.identifier);
|
||||||
let subscriber = this.subscribeCache[keyString];
|
const supportedStrategy = supportsBatching ? requestedStrategy : SUBSCRIBE_STRATEGY.LATEST;
|
||||||
|
// Override the requested strategy with the strategy supported by the provider
|
||||||
|
const optionsWithSupportedStrategy = {
|
||||||
|
...options,
|
||||||
|
strategy: supportedStrategy
|
||||||
|
};
|
||||||
|
// If batching is supported, we need to cache a subscription for each strategy -
|
||||||
|
// latest and batched.
|
||||||
|
const cacheKey = `${keyString}:${supportedStrategy}`;
|
||||||
|
let subscriber = this.#subscribeCache[cacheKey];
|
||||||
|
|
||||||
if (!subscriber) {
|
if (!subscriber) {
|
||||||
subscriber = this.subscribeCache[keyString] = {
|
subscriber = this.#subscribeCache[cacheKey] = {
|
||||||
callbacks: [callback]
|
latestCallbacks: [],
|
||||||
|
batchCallbacks: []
|
||||||
};
|
};
|
||||||
if (provider) {
|
if (provider) {
|
||||||
subscriber.unsubscribe = provider.subscribe(
|
subscriber.unsubscribe = provider.subscribe(
|
||||||
domainObject,
|
domainObject,
|
||||||
function (value) {
|
invokeCallbackWithRequestedStrategy,
|
||||||
subscriber.callbacks.forEach(function (cb) {
|
optionsWithSupportedStrategy
|
||||||
cb(value);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
options
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
subscriber.unsubscribe = function () {};
|
subscriber.unsubscribe = function () {};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestedStrategy === SUBSCRIBE_STRATEGY.BATCH) {
|
||||||
|
subscriber.batchCallbacks.push(callback);
|
||||||
} else {
|
} else {
|
||||||
subscriber.callbacks.push(callback);
|
subscriber.latestCallbacks.push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guarantees that view receive telemetry in the expected form
|
||||||
|
function invokeCallbackWithRequestedStrategy(data) {
|
||||||
|
invokeCallbacksWithArray(data, subscriber.batchCallbacks);
|
||||||
|
invokeCallbacksWithSingleValue(data, subscriber.latestCallbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
function invokeCallbacksWithArray(data, batchCallbacks) {
|
||||||
|
//
|
||||||
|
if (data === undefined || data === null || data.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
'Attempt to invoke telemetry subscription callback with no telemetry datum'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
data = [data];
|
||||||
|
}
|
||||||
|
|
||||||
|
batchCallbacks.forEach((cb) => {
|
||||||
|
cb(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function invokeCallbacksWithSingleValue(data, latestCallbacks) {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
data = data[data.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data === undefined || data === null) {
|
||||||
|
throw new Error(
|
||||||
|
'Attempt to invoke telemetry subscription callback with no telemetry datum'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
latestCallbacks.forEach((cb) => {
|
||||||
|
cb(data);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return function unsubscribe() {
|
return function unsubscribe() {
|
||||||
subscriber.callbacks = subscriber.callbacks.filter(function (cb) {
|
subscriber.latestCallbacks = subscriber.latestCallbacks.filter(function (cb) {
|
||||||
return cb !== callback;
|
return cb !== callback;
|
||||||
});
|
});
|
||||||
if (subscriber.callbacks.length === 0) {
|
subscriber.batchCallbacks = subscriber.batchCallbacks.filter(function (cb) {
|
||||||
|
return cb !== callback;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (subscriber.latestCallbacks.length === 0 && subscriber.batchCallbacks.length === 0) {
|
||||||
subscriber.unsubscribe();
|
subscriber.unsubscribe();
|
||||||
delete this.subscribeCache[keyString];
|
delete this.#subscribeCache[cacheKey];
|
||||||
}
|
}
|
||||||
}.bind(this);
|
}.bind(this);
|
||||||
}
|
}
|
||||||
|
@ -90,7 +90,9 @@ describe('Telemetry API', () => {
|
|||||||
|
|
||||||
const callback = jasmine.createSpy('callback');
|
const callback = jasmine.createSpy('callback');
|
||||||
const unsubscribe = telemetryAPI.subscribe(domainObject, callback);
|
const unsubscribe = telemetryAPI.subscribe(domainObject, callback);
|
||||||
expect(telemetryProvider.supportsSubscribe).toHaveBeenCalledWith(domainObject);
|
expect(telemetryProvider.supportsSubscribe).toHaveBeenCalledWith(domainObject, {
|
||||||
|
strategy: 'latest'
|
||||||
|
});
|
||||||
expect(telemetryProvider.subscribe).not.toHaveBeenCalled();
|
expect(telemetryProvider.subscribe).not.toHaveBeenCalled();
|
||||||
expect(unsubscribe).toEqual(jasmine.any(Function));
|
expect(unsubscribe).toEqual(jasmine.any(Function));
|
||||||
|
|
||||||
@ -111,12 +113,16 @@ describe('Telemetry API', () => {
|
|||||||
const callback = jasmine.createSpy('callback');
|
const callback = jasmine.createSpy('callback');
|
||||||
const unsubscribe = telemetryAPI.subscribe(domainObject, callback);
|
const unsubscribe = telemetryAPI.subscribe(domainObject, callback);
|
||||||
expect(telemetryProvider.supportsSubscribe.calls.count()).toBe(1);
|
expect(telemetryProvider.supportsSubscribe.calls.count()).toBe(1);
|
||||||
expect(telemetryProvider.supportsSubscribe).toHaveBeenCalledWith(domainObject);
|
expect(telemetryProvider.supportsSubscribe).toHaveBeenCalledWith(domainObject, {
|
||||||
|
strategy: 'latest'
|
||||||
|
});
|
||||||
expect(telemetryProvider.subscribe.calls.count()).toBe(1);
|
expect(telemetryProvider.subscribe.calls.count()).toBe(1);
|
||||||
expect(telemetryProvider.subscribe).toHaveBeenCalledWith(
|
expect(telemetryProvider.subscribe).toHaveBeenCalledWith(
|
||||||
domainObject,
|
domainObject,
|
||||||
jasmine.any(Function),
|
jasmine.any(Function),
|
||||||
undefined
|
{
|
||||||
|
strategy: 'latest'
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const notify = telemetryProvider.subscribe.calls.mostRecent().args[1];
|
const notify = telemetryProvider.subscribe.calls.mostRecent().args[1];
|
||||||
@ -321,6 +327,126 @@ describe('Telemetry API', () => {
|
|||||||
signal
|
signal
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('telemetry batching support', () => {
|
||||||
|
let callbacks;
|
||||||
|
let unsubFunc;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
callbacks = [];
|
||||||
|
unsubFunc = jasmine.createSpy('unsubscribe');
|
||||||
|
telemetryProvider.supportsBatching = jasmine.createSpy('supportsBatching');
|
||||||
|
telemetryProvider.supportsBatching.and.returnValue(true);
|
||||||
|
telemetryProvider.supportsSubscribe.and.returnValue(true);
|
||||||
|
|
||||||
|
telemetryProvider.subscribe.and.callFake(function (obj, cb, options) {
|
||||||
|
callbacks.push(cb);
|
||||||
|
|
||||||
|
return unsubFunc;
|
||||||
|
});
|
||||||
|
|
||||||
|
telemetryAPI.addProvider(telemetryProvider);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('caches subscriptions for batched and latest telemetry subscriptions', () => {
|
||||||
|
const latestCallback1 = jasmine.createSpy('latestCallback1');
|
||||||
|
const unsubscribeFromLatest1 = telemetryAPI.subscribe(domainObject, latestCallback1, {
|
||||||
|
strategy: 'latest'
|
||||||
|
});
|
||||||
|
const latestCallback2 = jasmine.createSpy('latestCallback2');
|
||||||
|
const unsubscribeFromLatest2 = telemetryAPI.subscribe(domainObject, latestCallback2, {
|
||||||
|
strategy: 'latest'
|
||||||
|
});
|
||||||
|
|
||||||
|
//Expect a single cached subscription for latest telemetry
|
||||||
|
expect(telemetryProvider.subscribe.calls.count()).toBe(1);
|
||||||
|
|
||||||
|
const batchedCallback1 = jasmine.createSpy('batchedCallback1');
|
||||||
|
const unsubscribeFromBatched1 = telemetryAPI.subscribe(domainObject, batchedCallback1, {
|
||||||
|
strategy: 'batch'
|
||||||
|
});
|
||||||
|
|
||||||
|
const batchedCallback2 = jasmine.createSpy('batchedCallback2');
|
||||||
|
const unsubscribeFromBatched2 = telemetryAPI.subscribe(domainObject, batchedCallback2, {
|
||||||
|
strategy: 'batch'
|
||||||
|
});
|
||||||
|
|
||||||
|
//Expect a single cached subscription for each strategy telemetry
|
||||||
|
expect(telemetryProvider.subscribe.calls.count()).toBe(2);
|
||||||
|
|
||||||
|
unsubscribeFromLatest1();
|
||||||
|
unsubscribeFromLatest2();
|
||||||
|
unsubscribeFromBatched1();
|
||||||
|
unsubscribeFromBatched2();
|
||||||
|
|
||||||
|
expect(unsubFunc).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
it('subscriptions with the latest strategy are always invoked with a single value', () => {
|
||||||
|
const latestCallback = jasmine.createSpy('latestCallback1');
|
||||||
|
telemetryAPI.subscribe(domainObject, latestCallback, {
|
||||||
|
strategy: 'latest'
|
||||||
|
});
|
||||||
|
|
||||||
|
const batchedValues = [1, 2, 3];
|
||||||
|
callbacks.forEach((cb) => {
|
||||||
|
cb(batchedValues);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(latestCallback).toHaveBeenCalledWith(3);
|
||||||
|
|
||||||
|
const singleValue = 1;
|
||||||
|
callbacks.forEach((cb) => {
|
||||||
|
cb(singleValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(latestCallback).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('subscriptions with the batch strategy are always invoked with an array', () => {
|
||||||
|
const batchedCallback = jasmine.createSpy('batchedCallback1');
|
||||||
|
const latestCallback = jasmine.createSpy('latestCallback1');
|
||||||
|
telemetryAPI.subscribe(domainObject, batchedCallback, {
|
||||||
|
strategy: 'batch'
|
||||||
|
});
|
||||||
|
telemetryAPI.subscribe(domainObject, latestCallback, {
|
||||||
|
strategy: 'latest'
|
||||||
|
});
|
||||||
|
|
||||||
|
const batchedValues = [1, 2, 3];
|
||||||
|
callbacks.forEach((cb) => {
|
||||||
|
cb(batchedValues);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Callbacks for the 'batch' strategy are always called with an array of values
|
||||||
|
expect(batchedCallback).toHaveBeenCalledWith(batchedValues);
|
||||||
|
// Callbacks for the 'latest' strategy are always called with a single value
|
||||||
|
expect(latestCallback).toHaveBeenCalledWith(3);
|
||||||
|
|
||||||
|
callbacks.forEach((cb) => {
|
||||||
|
cb(1);
|
||||||
|
});
|
||||||
|
// Callbacks for the 'batch' strategy are always called with an array of values, even if there is only one value
|
||||||
|
expect(batchedCallback).toHaveBeenCalledWith([1]);
|
||||||
|
// Callbacks for the 'latest' strategy are always called with a single value
|
||||||
|
expect(latestCallback).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('legacy providers are left unchanged, with a single subscription', () => {
|
||||||
|
delete telemetryProvider.supportsBatching;
|
||||||
|
|
||||||
|
const batchCallback = jasmine.createSpy('batchCallback');
|
||||||
|
telemetryAPI.subscribe(domainObject, batchCallback, {
|
||||||
|
strategy: 'batch'
|
||||||
|
});
|
||||||
|
expect(telemetryProvider.subscribe.calls.mostRecent().args[2].strategy).toBe('latest');
|
||||||
|
|
||||||
|
const latestCallback = jasmine.createSpy('latestCallback');
|
||||||
|
telemetryAPI.subscribe(domainObject, latestCallback, {
|
||||||
|
strategy: 'latest'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(telemetryProvider.subscribe.calls.mostRecent().args[2].strategy).toBe('latest');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('metadata', () => {
|
describe('metadata', () => {
|
||||||
|
@ -180,11 +180,14 @@ export default class TelemetryCollection extends EventEmitter {
|
|||||||
if (this.unsubscribe) {
|
if (this.unsubscribe) {
|
||||||
this.unsubscribe();
|
this.unsubscribe();
|
||||||
}
|
}
|
||||||
|
const options = { ...this.options };
|
||||||
|
//We always want to receive all available values in telemetry tables.
|
||||||
|
options.strategy = this.openmct.telemetry.SUBSCRIBE_STRATEGY.BATCH;
|
||||||
|
|
||||||
this.unsubscribe = this.openmct.telemetry.subscribe(
|
this.unsubscribe = this.openmct.telemetry.subscribe(
|
||||||
this.domainObject,
|
this.domainObject,
|
||||||
(datum) => this._processNewTelemetry(datum),
|
(datum) => this._processNewTelemetry(datum),
|
||||||
this.options
|
options
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,6 +212,8 @@ export default class TelemetryCollection extends EventEmitter {
|
|||||||
let added = [];
|
let added = [];
|
||||||
let addedIndices = [];
|
let addedIndices = [];
|
||||||
let hasDataBeforeStartBound = false;
|
let hasDataBeforeStartBound = false;
|
||||||
|
let size = this.options.size;
|
||||||
|
let enforceSize = size !== undefined && this.options.enforceSize;
|
||||||
|
|
||||||
// loop through, sort and dedupe
|
// loop through, sort and dedupe
|
||||||
for (let datum of data) {
|
for (let datum of data) {
|
||||||
@ -271,6 +276,13 @@ export default class TelemetryCollection extends EventEmitter {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.emit('add', added, addedIndices);
|
this.emit('add', added, addedIndices);
|
||||||
|
|
||||||
|
if (enforceSize && this.boundedTelemetry.length > size) {
|
||||||
|
const removeCount = this.boundedTelemetry.length - size;
|
||||||
|
const removed = this.boundedTelemetry.splice(0, removeCount);
|
||||||
|
|
||||||
|
this.emit('remove', removed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
366
src/api/telemetry/WebSocketWorker.js
Normal file
366
src/api/telemetry/WebSocketWorker.js
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT Web, Copyright (c) 2014-2024, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT Web 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 Web 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.
|
||||||
|
*****************************************************************************/
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
export default function installWorker() {
|
||||||
|
const FALLBACK_AND_WAIT_MS = [1000, 5000, 5000, 10000, 10000, 30000];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('./BatchingWebSocket').BatchingStrategy} BatchingStrategy
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a WebSocket connection that is resilient to errors and dropouts.
|
||||||
|
* On an error or dropout, will automatically reconnect.
|
||||||
|
*
|
||||||
|
* Additionally, messages will be queued and sent only when WebSocket is
|
||||||
|
* connected meaning that client code does not need to check the state of
|
||||||
|
* the socket before sending.
|
||||||
|
*/
|
||||||
|
class ResilientWebSocket extends EventTarget {
|
||||||
|
#webSocket;
|
||||||
|
#isConnected = false;
|
||||||
|
#isConnecting = false;
|
||||||
|
#messageQueue = [];
|
||||||
|
#reconnectTimeoutHandle;
|
||||||
|
#currentWaitIndex = 0;
|
||||||
|
#messageCallbacks = [];
|
||||||
|
#wsUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Establish a new WebSocket connection to the given URL
|
||||||
|
* @param {String} url
|
||||||
|
*/
|
||||||
|
connect(url) {
|
||||||
|
this.#wsUrl = url;
|
||||||
|
if (this.#isConnected) {
|
||||||
|
throw new Error('WebSocket already connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#isConnecting) {
|
||||||
|
throw new Error('WebSocket connection in progress');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#isConnecting = true;
|
||||||
|
|
||||||
|
this.#webSocket = new WebSocket(url);
|
||||||
|
|
||||||
|
const boundConnected = this.#connected.bind(this);
|
||||||
|
this.#webSocket.addEventListener('open', boundConnected);
|
||||||
|
|
||||||
|
const boundCleanUpAndReconnect = this.#cleanUpAndReconnect.bind(this);
|
||||||
|
this.#webSocket.addEventListener('error', boundCleanUpAndReconnect);
|
||||||
|
this.#webSocket.addEventListener('close', boundCleanUpAndReconnect);
|
||||||
|
|
||||||
|
const boundMessage = this.#message.bind(this);
|
||||||
|
this.#webSocket.addEventListener('message', boundMessage);
|
||||||
|
|
||||||
|
this.addEventListener(
|
||||||
|
'disconnected',
|
||||||
|
() => {
|
||||||
|
this.#webSocket.removeEventListener('open', boundConnected);
|
||||||
|
this.#webSocket.removeEventListener('error', boundCleanUpAndReconnect);
|
||||||
|
this.#webSocket.removeEventListener('close', boundCleanUpAndReconnect);
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a callback to be invoked when a message is received on the WebSocket.
|
||||||
|
* This paradigm is used instead of the standard EventTarget or EventEmitter approach
|
||||||
|
* for performance reasons.
|
||||||
|
* @param {Function} callback The function to be invoked when a message is received
|
||||||
|
* @returns an unregister function
|
||||||
|
*/
|
||||||
|
registerMessageCallback(callback) {
|
||||||
|
this.#messageCallbacks.push(callback);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.#messageCallbacks = this.#messageCallbacks.filter((cb) => cb !== callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#connected() {
|
||||||
|
console.debug('Websocket connected.');
|
||||||
|
this.#isConnected = true;
|
||||||
|
this.#isConnecting = false;
|
||||||
|
this.#currentWaitIndex = 0;
|
||||||
|
|
||||||
|
this.dispatchEvent(new Event('connected'));
|
||||||
|
|
||||||
|
this.#flushQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
#cleanUpAndReconnect() {
|
||||||
|
console.warn('Websocket closed. Attempting to reconnect...');
|
||||||
|
this.disconnect();
|
||||||
|
this.#reconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
#message(event) {
|
||||||
|
this.#messageCallbacks.forEach((callback) => callback(event.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.#isConnected = false;
|
||||||
|
this.#isConnecting = false;
|
||||||
|
|
||||||
|
// On WebSocket error, both error callback and close callback are invoked, resulting in
|
||||||
|
// this function being called twice, and websocket being destroyed and deallocated.
|
||||||
|
if (this.#webSocket !== undefined && this.#webSocket !== null) {
|
||||||
|
this.#webSocket.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispatchEvent(new Event('disconnected'));
|
||||||
|
this.#webSocket = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
#reconnect() {
|
||||||
|
if (this.#reconnectTimeoutHandle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#reconnectTimeoutHandle = setTimeout(() => {
|
||||||
|
this.connect(this.#wsUrl);
|
||||||
|
|
||||||
|
this.#reconnectTimeoutHandle = undefined;
|
||||||
|
}, FALLBACK_AND_WAIT_MS[this.#currentWaitIndex]);
|
||||||
|
|
||||||
|
if (this.#currentWaitIndex < FALLBACK_AND_WAIT_MS.length - 1) {
|
||||||
|
this.#currentWaitIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enqueueMessage(message) {
|
||||||
|
this.#messageQueue.push(message);
|
||||||
|
this.#flushQueueIfReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
#flushQueueIfReady() {
|
||||||
|
if (this.#isConnected) {
|
||||||
|
this.#flushQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#flushQueue() {
|
||||||
|
while (this.#messageQueue.length > 0) {
|
||||||
|
if (!this.#isConnected) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = this.#messageQueue.shift();
|
||||||
|
this.#webSocket.send(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles messages over the worker interface, and
|
||||||
|
* sends corresponding WebSocket messages.
|
||||||
|
*/
|
||||||
|
class WorkerToWebSocketMessageBroker {
|
||||||
|
#websocket;
|
||||||
|
#messageBatcher;
|
||||||
|
|
||||||
|
constructor(websocket, messageBatcher) {
|
||||||
|
this.#websocket = websocket;
|
||||||
|
this.#messageBatcher = messageBatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
routeMessageToHandler(message) {
|
||||||
|
const { type } = message.data;
|
||||||
|
switch (type) {
|
||||||
|
case 'connect':
|
||||||
|
this.connect(message);
|
||||||
|
break;
|
||||||
|
case 'disconnect':
|
||||||
|
this.disconnect(message);
|
||||||
|
break;
|
||||||
|
case 'message':
|
||||||
|
this.#websocket.enqueueMessage(message.data.message);
|
||||||
|
break;
|
||||||
|
case 'setBatchingStrategy':
|
||||||
|
this.setBatchingStrategy(message);
|
||||||
|
break;
|
||||||
|
case 'readyForNextBatch':
|
||||||
|
this.#messageBatcher.readyForNextBatch();
|
||||||
|
break;
|
||||||
|
case 'setMaxBatchSize':
|
||||||
|
this.#messageBatcher.setMaxBatchSize(message.data.maxBatchSize);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown message type: ${type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connect(message) {
|
||||||
|
const { url } = message.data;
|
||||||
|
this.#websocket.connect(url);
|
||||||
|
}
|
||||||
|
disconnect() {
|
||||||
|
this.#websocket.disconnect();
|
||||||
|
}
|
||||||
|
setBatchingStrategy(message) {
|
||||||
|
const { serializedStrategy } = message.data;
|
||||||
|
const batchingStrategy = {
|
||||||
|
// eslint-disable-next-line no-new-func
|
||||||
|
shouldBatchMessage: new Function(`return ${serializedStrategy.shouldBatchMessage}`)(),
|
||||||
|
// eslint-disable-next-line no-new-func
|
||||||
|
getBatchIdFromMessage: new Function(`return ${serializedStrategy.getBatchIdFromMessage}`)()
|
||||||
|
// Will also include maximum batch length here
|
||||||
|
};
|
||||||
|
this.#messageBatcher.setBatchingStrategy(batchingStrategy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Received messages from the WebSocket, and passes them along to the
|
||||||
|
* Worker interface and back to the main thread.
|
||||||
|
*/
|
||||||
|
class WebSocketToWorkerMessageBroker {
|
||||||
|
#worker;
|
||||||
|
#messageBatcher;
|
||||||
|
|
||||||
|
constructor(messageBatcher, worker) {
|
||||||
|
this.#messageBatcher = messageBatcher;
|
||||||
|
this.#worker = worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
routeMessageToHandler(data) {
|
||||||
|
//Implement batching here
|
||||||
|
if (this.#messageBatcher.shouldBatchMessage(data)) {
|
||||||
|
this.#messageBatcher.addMessageToBatch(data);
|
||||||
|
} else {
|
||||||
|
this.#worker.postMessage({
|
||||||
|
type: 'message',
|
||||||
|
message: data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for batching messages according to the defined batching strategy.
|
||||||
|
*/
|
||||||
|
class MessageBatcher {
|
||||||
|
#batch;
|
||||||
|
#batchingStrategy;
|
||||||
|
#hasBatch = false;
|
||||||
|
#maxBatchSize;
|
||||||
|
#readyForNextBatch;
|
||||||
|
#worker;
|
||||||
|
|
||||||
|
constructor(worker) {
|
||||||
|
this.#maxBatchSize = 10;
|
||||||
|
this.#readyForNextBatch = false;
|
||||||
|
this.#worker = worker;
|
||||||
|
this.#resetBatch();
|
||||||
|
}
|
||||||
|
#resetBatch() {
|
||||||
|
this.#batch = {};
|
||||||
|
this.#hasBatch = false;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {BatchingStrategy} strategy
|
||||||
|
*/
|
||||||
|
setBatchingStrategy(strategy) {
|
||||||
|
this.#batchingStrategy = strategy;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Applies the `shouldBatchMessage` function from the supplied batching strategy
|
||||||
|
* to each message to determine if it should be added to a batch. If not batched,
|
||||||
|
* the message is immediately sent over the worker to the main thread.
|
||||||
|
* @param {any} message the message received from the WebSocket. See the WebSocket
|
||||||
|
* documentation for more details -
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/data
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
shouldBatchMessage(message) {
|
||||||
|
return (
|
||||||
|
this.#batchingStrategy.shouldBatchMessage &&
|
||||||
|
this.#batchingStrategy.shouldBatchMessage(message)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Adds the given message to a batch. The batch group that the message is added
|
||||||
|
* to will be determined by the value returned by `getBatchIdFromMessage`.
|
||||||
|
* @param {any} message the message received from the WebSocket. See the WebSocket
|
||||||
|
* documentation for more details -
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/data
|
||||||
|
*/
|
||||||
|
addMessageToBatch(message) {
|
||||||
|
const batchId = this.#batchingStrategy.getBatchIdFromMessage(message);
|
||||||
|
let batch = this.#batch[batchId];
|
||||||
|
if (batch === undefined) {
|
||||||
|
batch = this.#batch[batchId] = [message];
|
||||||
|
} else {
|
||||||
|
batch.push(message);
|
||||||
|
}
|
||||||
|
if (batch.length > this.#maxBatchSize) {
|
||||||
|
batch.shift();
|
||||||
|
this.#batch.dropped = this.#batch.dropped || true;
|
||||||
|
}
|
||||||
|
if (this.#readyForNextBatch) {
|
||||||
|
this.#sendNextBatch();
|
||||||
|
} else {
|
||||||
|
this.#hasBatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setMaxBatchSize(maxBatchSize) {
|
||||||
|
this.#maxBatchSize = maxBatchSize;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Indicates that client code is ready to receive the next batch of
|
||||||
|
* messages. If a batch is available, it will be immediately sent.
|
||||||
|
* Otherwise a flag will be set to send the next batch as soon as
|
||||||
|
* any new data is available.
|
||||||
|
*/
|
||||||
|
readyForNextBatch() {
|
||||||
|
if (this.#hasBatch) {
|
||||||
|
this.#sendNextBatch();
|
||||||
|
} else {
|
||||||
|
this.#readyForNextBatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#sendNextBatch() {
|
||||||
|
const batch = this.#batch;
|
||||||
|
this.#resetBatch();
|
||||||
|
this.#worker.postMessage({
|
||||||
|
type: 'batch',
|
||||||
|
batch
|
||||||
|
});
|
||||||
|
this.#readyForNextBatch = false;
|
||||||
|
this.#hasBatch = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const websocket = new ResilientWebSocket();
|
||||||
|
const messageBatcher = new MessageBatcher(self);
|
||||||
|
const workerBroker = new WorkerToWebSocketMessageBroker(websocket, messageBatcher);
|
||||||
|
const websocketBroker = new WebSocketToWorkerMessageBroker(messageBatcher, self);
|
||||||
|
|
||||||
|
self.addEventListener('message', (message) => {
|
||||||
|
workerBroker.routeMessageToHandler(message);
|
||||||
|
});
|
||||||
|
websocket.registerMessageCallback((data) => {
|
||||||
|
websocketBroker.routeMessageToHandler(data);
|
||||||
|
});
|
||||||
|
}
|
68
src/plugins/activityStates/activityStatesInterceptor.js
Normal file
68
src/plugins/activityStates/activityStatesInterceptor.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 { ACTIVITY_STATES_KEY } from './createActivityStatesIdentifier.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} ActivityStatesInterceptorOptions
|
||||||
|
* @property {import('../../api/objects/ObjectAPI').Identifier} identifier the {namespace, key} to use for the activity states object.
|
||||||
|
* @property {string} name The name of the activity states model.
|
||||||
|
* @property {number} priority the priority of the interceptor. By default, it is low.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an activity states object in the persistence store. This is used to save plan activity states.
|
||||||
|
* This will only get invoked when an attempt is made to save the state for an activity and no activity states object exists in the store.
|
||||||
|
* @param {import('../../../openmct').OpenMCT} openmct
|
||||||
|
* @param {ActivityStatesInterceptorOptions} options
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
const ACTIVITY_STATES_TYPE = 'activity-states';
|
||||||
|
|
||||||
|
function activityStatesInterceptor(openmct, options) {
|
||||||
|
const { identifier, name, priority = openmct.priority.LOW } = options;
|
||||||
|
const activityStatesModel = {
|
||||||
|
identifier,
|
||||||
|
name,
|
||||||
|
type: ACTIVITY_STATES_TYPE,
|
||||||
|
activities: {},
|
||||||
|
location: null
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
appliesTo: (identifierObject) => {
|
||||||
|
return identifierObject.key === ACTIVITY_STATES_KEY;
|
||||||
|
},
|
||||||
|
invoke: (identifierObject, object) => {
|
||||||
|
if (!object || openmct.objects.isMissing(object)) {
|
||||||
|
openmct.objects.save(activityStatesModel);
|
||||||
|
|
||||||
|
return activityStatesModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return object;
|
||||||
|
},
|
||||||
|
priority
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default activityStatesInterceptor;
|
30
src/plugins/activityStates/createActivityStatesIdentifier.js
Normal file
30
src/plugins/activityStates/createActivityStatesIdentifier.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 ACTIVITY_STATES_KEY = 'activity-states';
|
||||||
|
|
||||||
|
export function createActivityStatesIdentifier(namespace = '') {
|
||||||
|
return {
|
||||||
|
key: ACTIVITY_STATES_KEY,
|
||||||
|
namespace
|
||||||
|
};
|
||||||
|
}
|
89
src/plugins/activityStates/pluginSpec.js
Normal file
89
src/plugins/activityStates/pluginSpec.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 { createOpenMct, resetApplicationState } from 'utils/testing';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ACTIVITY_STATES_KEY,
|
||||||
|
createActivityStatesIdentifier
|
||||||
|
} from './createActivityStatesIdentifier.js';
|
||||||
|
|
||||||
|
const MISSING_NAME = `Missing: ${ACTIVITY_STATES_KEY}`;
|
||||||
|
const DEFAULT_NAME = 'Activity States';
|
||||||
|
const activityStatesIdentifier = createActivityStatesIdentifier();
|
||||||
|
|
||||||
|
describe('the plugin', () => {
|
||||||
|
let openmct;
|
||||||
|
let missingObj = {
|
||||||
|
identifier: activityStatesIdentifier,
|
||||||
|
type: 'unknown',
|
||||||
|
name: MISSING_NAME
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('with no arguments passed in', () => {
|
||||||
|
beforeEach((done) => {
|
||||||
|
openmct = createOpenMct();
|
||||||
|
openmct.install(openmct.plugins.PlanLayout());
|
||||||
|
|
||||||
|
openmct.on('start', done);
|
||||||
|
openmct.startHeadless();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
return resetApplicationState(openmct);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when installed, adds "Activity States"', async () => {
|
||||||
|
const activityStatesObject = await openmct.objects.get(activityStatesIdentifier);
|
||||||
|
expect(activityStatesObject.name).toBe(DEFAULT_NAME);
|
||||||
|
expect(activityStatesObject).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('adds an interceptor that returns a "Activity States" model for', () => {
|
||||||
|
let activityStatesObject;
|
||||||
|
let mockNotFoundProvider;
|
||||||
|
let activeProvider;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockNotFoundProvider = {
|
||||||
|
get: () => Promise.reject(new Error('Not found')),
|
||||||
|
create: () => Promise.resolve(missingObj),
|
||||||
|
update: () => Promise.resolve(missingObj)
|
||||||
|
};
|
||||||
|
|
||||||
|
activeProvider = mockNotFoundProvider;
|
||||||
|
spyOn(openmct.objects, 'getProvider').and.returnValue(activeProvider);
|
||||||
|
activityStatesObject = await openmct.objects.get(activityStatesIdentifier);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('missing objects', () => {
|
||||||
|
let idsMatch = openmct.objects.areIdsEqual(
|
||||||
|
activityStatesObject.identifier,
|
||||||
|
activityStatesIdentifier
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(activityStatesObject).toBeDefined();
|
||||||
|
expect(idsMatch).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -84,12 +84,7 @@ import LayoutFrame from './LayoutFrame.vue';
|
|||||||
|
|
||||||
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
|
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
|
||||||
const DEFAULT_POSITION = [1, 1];
|
const DEFAULT_POSITION = [1, 1];
|
||||||
const CONTEXT_MENU_ACTIONS = [
|
const CONTEXT_MENU_ACTIONS = ['copyToClipboard', 'copyToNotebook', 'viewHistoricalData'];
|
||||||
'copyToClipboard',
|
|
||||||
'copyToNotebook',
|
|
||||||
'viewHistoricalData',
|
|
||||||
'renderWhenVisible'
|
|
||||||
];
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
makeDefinition(openmct, gridSize, domainObject, position) {
|
makeDefinition(openmct, gridSize, domainObject, position) {
|
||||||
|
@ -145,7 +145,7 @@
|
|||||||
v-if="relatedTelemetry.hasRelatedTelemetry && isSpacecraftPositionFresh"
|
v-if="relatedTelemetry.hasRelatedTelemetry && isSpacecraftPositionFresh"
|
||||||
class="c-imagery__age icon-check c-imagery--new no-animation"
|
class="c-imagery__age icon-check c-imagery--new no-animation"
|
||||||
>
|
>
|
||||||
POS
|
ROV
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- camera position fresh -->
|
<!-- camera position fresh -->
|
||||||
|
@ -112,7 +112,7 @@ export default {
|
|||||||
},
|
},
|
||||||
renderPlot(plotObject) {
|
renderPlot(plotObject) {
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
const visibilityObserver = new VisibilityObserver(wrapper);
|
const visibilityObserver = new VisibilityObserver(wrapper, this.openmct.element);
|
||||||
|
|
||||||
const { destroy } = mount(
|
const { destroy } = mount(
|
||||||
{
|
{
|
||||||
|
@ -96,7 +96,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="selectedPage && !selectedPage.isLocked"
|
v-if="selectedPage && !selectedPage.isLocked"
|
||||||
:class="{ disabled: activeTransaction }"
|
:aria-disabled="activeTransaction"
|
||||||
class="c-notebook__drag-area icon-plus"
|
class="c-notebook__drag-area icon-plus"
|
||||||
@click="newEntry(null, $event)"
|
@click="newEntry(null, $event)"
|
||||||
@dragover="dragOver"
|
@dragover="dragOver"
|
||||||
|
@ -135,6 +135,7 @@ export default {
|
|||||||
default: 22
|
default: 22
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
emits: ['activity-selected'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
lineHeight: 10
|
lineHeight: 10
|
||||||
@ -142,30 +143,11 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setSelectionForActivity(activity, event) {
|
setSelectionForActivity(activity, event) {
|
||||||
const element = event.currentTarget;
|
|
||||||
const multiSelect = event.metaKey;
|
|
||||||
|
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
this.$emit('activity-selected', {
|
||||||
this.openmct.selection.select(
|
event,
|
||||||
[
|
selection: activity.selection
|
||||||
{
|
});
|
||||||
element: element,
|
|
||||||
context: {
|
|
||||||
type: 'activity',
|
|
||||||
activity: activity
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
element: this.openmct.layout.$refs.browseObject.$el,
|
|
||||||
context: {
|
|
||||||
item: this.domainObject,
|
|
||||||
supportsMultiSelect: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
multiSelect
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -47,6 +47,7 @@
|
|||||||
:width="group.width"
|
:width="group.width"
|
||||||
:is-nested="options.isChildObject"
|
:is-nested="options.isChildObject"
|
||||||
:status="status"
|
:status="status"
|
||||||
|
@activity-selected="selectActivity"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -134,7 +135,7 @@ export default {
|
|||||||
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
|
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
|
||||||
this.clipActivityNames = this.configuration.clipActivityNames;
|
this.clipActivityNames = this.configuration.clipActivityNames;
|
||||||
if (this.domainObject.type === 'plan') {
|
if (this.domainObject.type === 'plan') {
|
||||||
this.planData = getValidatedData(this.domainObject);
|
this.setPlanData(this.domainObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
@ -177,6 +178,9 @@ export default {
|
|||||||
this.planViewConfiguration.destroy();
|
this.planViewConfiguration.destroy();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setPlanData(domainObject) {
|
||||||
|
this.planData = getValidatedData(domainObject);
|
||||||
|
},
|
||||||
activityNameFitsRect(activityName, rectWidth) {
|
activityNameFitsRect(activityName, rectWidth) {
|
||||||
return this.getTextWidth(activityName) + TEXT_LEFT_PADDING < rectWidth;
|
return this.getTextWidth(activityName) + TEXT_LEFT_PADDING < rectWidth;
|
||||||
},
|
},
|
||||||
@ -215,9 +219,7 @@ export default {
|
|||||||
callback: () => {
|
callback: () => {
|
||||||
this.removeFromComposition(this.planObject);
|
this.removeFromComposition(this.planObject);
|
||||||
this.planObject = domainObject;
|
this.planObject = domainObject;
|
||||||
this.planData = getValidatedData(domainObject);
|
this.handleSelectFileChange();
|
||||||
this.setStatus(this.openmct.status.get(domainObject.identifier));
|
|
||||||
this.setScaleAndGenerateActivities();
|
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -237,9 +239,7 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.planObject = domainObject;
|
this.planObject = domainObject;
|
||||||
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
|
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
|
||||||
this.planData = getValidatedData(domainObject);
|
this.handleSelectFileChange(domainObject);
|
||||||
this.setStatus(this.openmct.status.get(domainObject.identifier));
|
|
||||||
this.setScaleAndGenerateActivities();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleConfigurationChange(newConfiguration) {
|
handleConfigurationChange(newConfiguration) {
|
||||||
@ -259,8 +259,10 @@ export default {
|
|||||||
|
|
||||||
this.setScaleAndGenerateActivities();
|
this.setScaleAndGenerateActivities();
|
||||||
},
|
},
|
||||||
handleSelectFileChange() {
|
handleSelectFileChange(domainObject) {
|
||||||
this.planData = getValidatedData(this.domainObject);
|
const planDomainObject = domainObject || this.domainObject;
|
||||||
|
this.setPlanData(planDomainObject);
|
||||||
|
this.setStatus(this.openmct.status.get(planDomainObject.identifier));
|
||||||
this.setScaleAndGenerateActivities();
|
this.setScaleAndGenerateActivities();
|
||||||
},
|
},
|
||||||
removeFromComposition(domainObject) {
|
removeFromComposition(domainObject) {
|
||||||
@ -434,7 +436,7 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
rawActivities.forEach((rawActivity) => {
|
rawActivities.forEach((rawActivity, index) => {
|
||||||
if (!this.isActivityInBounds(rawActivity)) {
|
if (!this.isActivityInBounds(rawActivity)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -481,13 +483,10 @@ export default {
|
|||||||
const activity = {
|
const activity = {
|
||||||
color: color,
|
color: color,
|
||||||
textColor: textColor,
|
textColor: textColor,
|
||||||
name: rawActivity.name,
|
|
||||||
exceeds: {
|
exceeds: {
|
||||||
start: this.xScale(this.viewBounds.start) > this.xScale(rawActivity.start),
|
start: this.xScale(this.viewBounds.start) > this.xScale(rawActivity.start),
|
||||||
end: this.xScale(this.viewBounds.end) < this.xScale(rawActivity.end)
|
end: this.xScale(this.viewBounds.end) < this.xScale(rawActivity.end)
|
||||||
},
|
},
|
||||||
start: rawActivity.start,
|
|
||||||
end: rawActivity.end,
|
|
||||||
row: currentRow,
|
row: currentRow,
|
||||||
textLines: textLines,
|
textLines: textLines,
|
||||||
textStart: textStart,
|
textStart: textStart,
|
||||||
@ -496,7 +495,11 @@ export default {
|
|||||||
rectStart: rectX1,
|
rectStart: rectX1,
|
||||||
rectEnd: showTextInsideRect ? rectX2 : textStart + textWidth,
|
rectEnd: showTextInsideRect ? rectX2 : textStart + textWidth,
|
||||||
rectWidth: rectWidth,
|
rectWidth: rectWidth,
|
||||||
clipPathId: this.getClipPathId(groupName, rawActivity, currentRow)
|
clipPathId: this.getClipPathId(groupName, rawActivity, currentRow),
|
||||||
|
selection: {
|
||||||
|
groupName,
|
||||||
|
index
|
||||||
|
}
|
||||||
};
|
};
|
||||||
activitiesByRow[currentRow].push(activity);
|
activitiesByRow[currentRow].push(activity);
|
||||||
});
|
});
|
||||||
@ -573,6 +576,31 @@ export default {
|
|||||||
const activityName = activity.name.toLowerCase().replace(/ /g, '-');
|
const activityName = activity.name.toLowerCase().replace(/ /g, '-');
|
||||||
|
|
||||||
return `${groupName}-${activityName}-${activity.start}-${activity.end}-${row}`;
|
return `${groupName}-${activityName}-${activity.start}-${activity.end}-${row}`;
|
||||||
|
},
|
||||||
|
selectActivity({ event, selection }) {
|
||||||
|
const element = event.currentTarget;
|
||||||
|
const multiSelect = event.metaKey;
|
||||||
|
const { groupName, index } = selection;
|
||||||
|
const rawActivity = this.planData[groupName][index];
|
||||||
|
this.openmct.selection.select(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
element: element,
|
||||||
|
context: {
|
||||||
|
type: 'activity',
|
||||||
|
activity: rawActivity
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: this.openmct.layout.$refs.browseObject.$el,
|
||||||
|
context: {
|
||||||
|
item: this.domainObject,
|
||||||
|
supportsMultiSelect: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
multiSelect
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -20,21 +20,35 @@
|
|||||||
at runtime from the About dialog for additional information.
|
at runtime from the About dialog for additional information.
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="c-inspector__properties c-inspect-properties">
|
<plan-activity-time-view
|
||||||
<plan-activity-view
|
v-for="activity in activities"
|
||||||
v-for="activity in activities"
|
:key="activity.key"
|
||||||
:key="activity.id"
|
:activity="activity"
|
||||||
:activity="activity"
|
:heading="heading"
|
||||||
:heading="heading"
|
/>
|
||||||
/>
|
<plan-activity-properties-view
|
||||||
</div>
|
v-for="activity in activities"
|
||||||
|
:key="activity.key"
|
||||||
|
heading="Properties"
|
||||||
|
:activity="activity"
|
||||||
|
/>
|
||||||
|
<plan-activity-status-view
|
||||||
|
v-if="canPersistState"
|
||||||
|
:key="activities[0].key"
|
||||||
|
:activity="activities[0]"
|
||||||
|
:execution-state="activityExecutionState"
|
||||||
|
heading="Activity Status"
|
||||||
|
@update-activity-state="persistActivityState"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getPreciseDuration } from 'utils/duration';
|
import { getPreciseDuration } from 'utils/duration';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
|
|
||||||
import PlanActivityView from './PlanActivityView.vue';
|
import { getDisplayProperties } from '../../util.js';
|
||||||
|
import PlanActivityPropertiesView from './PlanActivityPropertiesView.vue';
|
||||||
|
import PlanActivityStatusView from './PlanActivityStatusView.vue';
|
||||||
|
import PlanActivityTimeView from './PlanActivityTimeView.vue';
|
||||||
|
|
||||||
const propertyLabels = {
|
const propertyLabels = {
|
||||||
start: 'Start DateTime',
|
start: 'Start DateTime',
|
||||||
@ -44,23 +58,34 @@ const propertyLabels = {
|
|||||||
latestEnd: 'Latest End',
|
latestEnd: 'Latest End',
|
||||||
gap: 'Gap',
|
gap: 'Gap',
|
||||||
overlap: 'Overlap',
|
overlap: 'Overlap',
|
||||||
totalTime: 'Total Time'
|
totalTime: 'Total Time',
|
||||||
|
description: 'Description'
|
||||||
};
|
};
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
PlanActivityView
|
PlanActivityTimeView,
|
||||||
|
PlanActivityPropertiesView,
|
||||||
|
PlanActivityStatusView
|
||||||
},
|
},
|
||||||
inject: ['openmct', 'selection'],
|
inject: ['openmct', 'selection'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
name: '',
|
name: '',
|
||||||
activities: [],
|
activities: [],
|
||||||
|
selectedActivities: [],
|
||||||
|
activityExecutionState: undefined,
|
||||||
heading: ''
|
heading: ''
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
canPersistState() {
|
||||||
|
return this.selectedActivities.length === 1 && this.activities?.[0]?.id;
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.setFormatters();
|
this.setFormatters();
|
||||||
this.getPlanData(this.selection);
|
this.getPlanData(this.selection);
|
||||||
|
this.getActivityStates();
|
||||||
this.getActivities();
|
this.getActivities();
|
||||||
this.openmct.selection.on('change', this.updateSelection);
|
this.openmct.selection.on('change', this.updateSelection);
|
||||||
this.openmct.time.on('timeSystem', this.setFormatters);
|
this.openmct.time.on('timeSystem', this.setFormatters);
|
||||||
@ -68,8 +93,28 @@ export default {
|
|||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
this.openmct.selection.off('change', this.updateSelection);
|
this.openmct.selection.off('change', this.updateSelection);
|
||||||
this.openmct.time.off('timeSystem', this.setFormatters);
|
this.openmct.time.off('timeSystem', this.setFormatters);
|
||||||
|
if (this.stopObservingActivityStatesObject) {
|
||||||
|
this.stopObservingActivityStatesObject();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async getActivityStates() {
|
||||||
|
this.activityStatesObject = await this.openmct.objects.get('activity-states');
|
||||||
|
this.setActivityStates(this.activityStatesObject);
|
||||||
|
this.stopObservingActivityStatesObject = this.openmct.objects.observe(
|
||||||
|
this.activityStatesObject,
|
||||||
|
'*',
|
||||||
|
this.setActivityStates
|
||||||
|
);
|
||||||
|
},
|
||||||
|
setActivityStates(newActivitiesStateObject) {
|
||||||
|
if (this.activities.length) {
|
||||||
|
const id = this.activities[0].id;
|
||||||
|
this.activityExecutionState = newActivitiesStateObject.activities[id];
|
||||||
|
} else {
|
||||||
|
this.activityExecutionState = undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
setFormatters() {
|
setFormatters() {
|
||||||
let timeSystem = this.openmct.time.timeSystem();
|
let timeSystem = this.openmct.time.timeSystem();
|
||||||
this.timeFormatter = this.openmct.telemetry.getValueFormatter({
|
this.timeFormatter = this.openmct.telemetry.getValueFormatter({
|
||||||
@ -86,6 +131,7 @@ export default {
|
|||||||
if (selectionItem[0].context.type === 'activity') {
|
if (selectionItem[0].context.type === 'activity') {
|
||||||
const activity = selectionItem[0].context.activity;
|
const activity = selectionItem[0].context.activity;
|
||||||
if (activity) {
|
if (activity) {
|
||||||
|
activity.key = activity.id ?? activity.name;
|
||||||
this.selectedActivities.push(activity);
|
this.selectedActivities.push(activity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,20 +150,37 @@ export default {
|
|||||||
this.activities.splice(0);
|
this.activities.splice(0);
|
||||||
this.selectedActivities.forEach((selectedActivity, index) => {
|
this.selectedActivities.forEach((selectedActivity, index) => {
|
||||||
const activity = {
|
const activity = {
|
||||||
id: uuid(),
|
id: selectedActivity.id,
|
||||||
start: {
|
key: selectedActivity.key,
|
||||||
label: propertyLabels.start,
|
timeProperties: {
|
||||||
value: this.formatTime(selectedActivity.start)
|
start: {
|
||||||
},
|
label: propertyLabels.start,
|
||||||
end: {
|
value: this.formatTime(selectedActivity.start)
|
||||||
label: propertyLabels.end,
|
},
|
||||||
value: this.formatTime(selectedActivity.end)
|
end: {
|
||||||
},
|
label: propertyLabels.end,
|
||||||
duration: {
|
value: this.formatTime(selectedActivity.end)
|
||||||
label: propertyLabels.duration,
|
},
|
||||||
value: this.formatDuration(selectedActivity.end - selectedActivity.start)
|
duration: {
|
||||||
|
label: propertyLabels.duration,
|
||||||
|
value: this.formatDuration(selectedActivity.end - selectedActivity.start)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
activity.metadata = {};
|
||||||
|
if (selectedActivity.description) {
|
||||||
|
activity.metadata.description = {
|
||||||
|
label: propertyLabels.description,
|
||||||
|
value: selectedActivity.description
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayProperties = getDisplayProperties(selectedActivity);
|
||||||
|
activity.metadata = {
|
||||||
|
...activity.metadata,
|
||||||
|
...displayProperties
|
||||||
|
};
|
||||||
|
|
||||||
this.activities[index] = activity;
|
this.activities[index] = activity;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -141,6 +204,8 @@ export default {
|
|||||||
let latestEnd;
|
let latestEnd;
|
||||||
let gap;
|
let gap;
|
||||||
let overlap;
|
let overlap;
|
||||||
|
let id;
|
||||||
|
let key;
|
||||||
|
|
||||||
//Sort by start time
|
//Sort by start time
|
||||||
let selectedActivities = this.selectedActivities.sort(this.sortFn);
|
let selectedActivities = this.selectedActivities.sort(this.sortFn);
|
||||||
@ -159,6 +224,8 @@ export default {
|
|||||||
earliestStart = Math.min(earliestStart, selectedActivity.start);
|
earliestStart = Math.min(earliestStart, selectedActivity.start);
|
||||||
latestEnd = Math.max(latestEnd, selectedActivity.end);
|
latestEnd = Math.max(latestEnd, selectedActivity.end);
|
||||||
} else {
|
} else {
|
||||||
|
id = selectedActivity.id;
|
||||||
|
key = selectedActivity.id ?? selectedActivity.name;
|
||||||
earliestStart = selectedActivity.start;
|
earliestStart = selectedActivity.start;
|
||||||
latestEnd = selectedActivity.end;
|
latestEnd = selectedActivity.end;
|
||||||
}
|
}
|
||||||
@ -166,30 +233,33 @@ export default {
|
|||||||
let totalTime = latestEnd - earliestStart;
|
let totalTime = latestEnd - earliestStart;
|
||||||
|
|
||||||
const activity = {
|
const activity = {
|
||||||
id: uuid(),
|
id,
|
||||||
earliestStart: {
|
key,
|
||||||
label: propertyLabels.earliestStart,
|
timeProperties: {
|
||||||
value: this.formatTime(earliestStart)
|
earliestStart: {
|
||||||
},
|
label: propertyLabels.earliestStart,
|
||||||
latestEnd: {
|
value: this.formatTime(earliestStart)
|
||||||
label: propertyLabels.latestEnd,
|
},
|
||||||
value: this.formatTime(latestEnd)
|
latestEnd: {
|
||||||
|
label: propertyLabels.latestEnd,
|
||||||
|
value: this.formatTime(latestEnd)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (gap) {
|
if (gap) {
|
||||||
activity.gap = {
|
activity.timeProperties.gap = {
|
||||||
label: propertyLabels.gap,
|
label: propertyLabels.gap,
|
||||||
value: this.formatDuration(gap)
|
value: this.formatDuration(gap)
|
||||||
};
|
};
|
||||||
} else if (overlap) {
|
} else if (overlap) {
|
||||||
activity.overlap = {
|
activity.timeProperties.overlap = {
|
||||||
label: propertyLabels.overlap,
|
label: propertyLabels.overlap,
|
||||||
value: this.formatDuration(overlap)
|
value: this.formatDuration(overlap)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
activity.totalTime = {
|
activity.timeProperties.totalTime = {
|
||||||
label: propertyLabels.totalTime,
|
label: propertyLabels.totalTime,
|
||||||
value: this.formatDuration(totalTime)
|
value: this.formatDuration(totalTime)
|
||||||
};
|
};
|
||||||
@ -201,6 +271,11 @@ export default {
|
|||||||
},
|
},
|
||||||
formatTime(time) {
|
formatTime(time) {
|
||||||
return this.timeFormatter.format(time);
|
return this.timeFormatter.format(time);
|
||||||
|
},
|
||||||
|
persistActivityState(data) {
|
||||||
|
const { key, executionState } = data;
|
||||||
|
const activitiesPath = `activities.${key}`;
|
||||||
|
this.openmct.objects.mutate(this.activityStatesObject, activitiesPath, executionState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
<!--
|
||||||
|
Open MCT, Copyright (c) 2014-2023, 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-inspector__properties c-inspect-properties">
|
||||||
|
<div v-if="properties.length" class="u-contents">
|
||||||
|
<div class="c-inspect-properties__header">{{ heading }}</div>
|
||||||
|
<ul v-for="property in properties" :key="property.id" class="c-inspect-properties__section">
|
||||||
|
<activity-property :label="property.label" :value="property.value" />
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ActivityProperty from './ActivityProperty.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ActivityProperty
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
activity: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
properties: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.setProperties();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setProperties() {
|
||||||
|
if (!this.activity.metadata) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(this.activity.metadata).forEach((key) => {
|
||||||
|
if (this.activity.metadata[key].label) {
|
||||||
|
const label = this.activity.metadata[key].label;
|
||||||
|
const value = String(this.activity.metadata[key].value);
|
||||||
|
const id = this.activity.id;
|
||||||
|
|
||||||
|
this.properties[this.properties.length] = {
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
127
src/plugins/plan/inspector/components/PlanActivityStatusView.vue
Normal file
127
src/plugins/plan/inspector/components/PlanActivityStatusView.vue
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
<!--
|
||||||
|
Open MCT, Copyright (c) 2014-2023, 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-inspector__properties c-inspect-properties">
|
||||||
|
<div class="u-contents">
|
||||||
|
<div class="c-inspect-properties__header">{{ heading }}</div>
|
||||||
|
<div class="c-inspect-properties__row">
|
||||||
|
<div class="c-inspect-properties__label" title="Set Status">Set Status</div>
|
||||||
|
<div class="c-inspect-properties__value" aria-label="Activity Status Label">
|
||||||
|
<select
|
||||||
|
v-model="currentStatusKey"
|
||||||
|
name="setActivityStatus"
|
||||||
|
aria-label="Activity Status"
|
||||||
|
@change="changeActivityStatus"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="status in activityStates"
|
||||||
|
:key="status.key"
|
||||||
|
:value="status.key"
|
||||||
|
:aria-selected="currentStatusKey === status.key"
|
||||||
|
>
|
||||||
|
{{ status.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const activityStates = [
|
||||||
|
{
|
||||||
|
key: 'notStarted',
|
||||||
|
label: 'Not started'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'in-progress',
|
||||||
|
label: 'In progress'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'completed',
|
||||||
|
label: 'Completed'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'aborted',
|
||||||
|
label: 'Aborted'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'cancelled',
|
||||||
|
label: 'Cancelled'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
activity: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
executionState: {
|
||||||
|
type: String,
|
||||||
|
default() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['updateActivityState'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activityStates: activityStates,
|
||||||
|
currentStatusKey: activityStates[0].key
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
executionState() {
|
||||||
|
this.setActivityStatus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.setActivityStatus();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setActivityStatus() {
|
||||||
|
let statusKeyIndex = activityStates.findIndex((state) => state.key === this.executionState);
|
||||||
|
if (statusKeyIndex < 0) {
|
||||||
|
statusKeyIndex = 0;
|
||||||
|
}
|
||||||
|
this.currentStatusKey = this.activityStates[statusKeyIndex].key;
|
||||||
|
},
|
||||||
|
changeActivityStatus() {
|
||||||
|
if (this.currentStatusKey === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.activity.executionState = this.currentStatusKey;
|
||||||
|
this.$emit('updateActivityState', {
|
||||||
|
key: this.activity.id,
|
||||||
|
executionState: this.currentStatusKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -21,23 +21,23 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="timeProperties.length" class="u-contents">
|
<div class="c-inspector__properties c-inspect-properties">
|
||||||
<div class="c-inspect-properties__header">
|
<div v-if="timeProperties.length" class="u-contents">
|
||||||
{{ heading }}
|
<div class="c-inspect-properties__header">
|
||||||
|
{{ heading }}
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
v-for="timeProperty in timeProperties"
|
||||||
|
:key="timeProperty.id"
|
||||||
|
class="c-inspect-properties__section"
|
||||||
|
>
|
||||||
|
<activity-property :label="timeProperty.label" :value="timeProperty.value" />
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
|
||||||
v-for="timeProperty in timeProperties"
|
|
||||||
:key="timeProperty.id"
|
|
||||||
class="c-inspect-properties__section"
|
|
||||||
>
|
|
||||||
<activity-property :label="timeProperty.label" :value="timeProperty.value" />
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
|
|
||||||
import ActivityProperty from './ActivityProperty.vue';
|
import ActivityProperty from './ActivityProperty.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -64,13 +64,14 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setProperties() {
|
setProperties() {
|
||||||
Object.keys(this.activity).forEach((key) => {
|
Object.keys(this.activity.timeProperties).forEach((key) => {
|
||||||
if (this.activity[key].label) {
|
if (this.activity.timeProperties[key].label) {
|
||||||
const label = this.activity[key].label;
|
const label = this.activity.timeProperties[key].label;
|
||||||
const value = String(this.activity[key].value);
|
const value = String(this.activity.timeProperties[key].value);
|
||||||
|
const id = this.activity.id;
|
||||||
|
|
||||||
this.timeProperties[this.timeProperties.length] = {
|
this.timeProperties[this.timeProperties.length] = {
|
||||||
id: uuid(),
|
id,
|
||||||
label,
|
label,
|
||||||
value
|
value
|
||||||
};
|
};
|
@ -20,12 +20,28 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import activityStatesInterceptor from '../activityStates/activityStatesInterceptor.js';
|
||||||
|
import { createActivityStatesIdentifier } from '../activityStates/createActivityStatesIdentifier.js';
|
||||||
import ganttChartCompositionPolicy from './GanttChartCompositionPolicy.js';
|
import ganttChartCompositionPolicy from './GanttChartCompositionPolicy.js';
|
||||||
import ActivityInspectorViewProvider from './inspector/ActivityInspectorViewProvider.js';
|
import ActivityInspectorViewProvider from './inspector/ActivityInspectorViewProvider.js';
|
||||||
import GanttChartInspectorViewProvider from './inspector/GanttChartInspectorViewProvider.js';
|
import GanttChartInspectorViewProvider from './inspector/GanttChartInspectorViewProvider.js';
|
||||||
import { DEFAULT_CONFIGURATION } from './PlanViewConfiguration.js';
|
import { DEFAULT_CONFIGURATION } from './PlanViewConfiguration.js';
|
||||||
import PlanViewProvider from './PlanViewProvider.js';
|
import PlanViewProvider from './PlanViewProvider.js';
|
||||||
|
|
||||||
|
const ACTIVITY_STATES_DEFAULT_NAME = 'Activity States';
|
||||||
|
/**
|
||||||
|
* @typedef {object} PlanOptions
|
||||||
|
* @property {boolean} creatable true/false to allow creation of a plan via the Create menu.
|
||||||
|
* @property {string} name The name of the activity states model.
|
||||||
|
* @property {string} namespace the namespace to use for the activity states object.
|
||||||
|
* @property {Number} priority the priority of the interceptor. By default, it is low.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {PlanOptions} options
|
||||||
|
* @returns {*} (any)
|
||||||
|
*/
|
||||||
export default function (options = {}) {
|
export default function (options = {}) {
|
||||||
return function install(openmct) {
|
return function install(openmct) {
|
||||||
openmct.types.addType('plan', {
|
openmct.types.addType('plan', {
|
||||||
@ -70,5 +86,13 @@ export default function (options = {}) {
|
|||||||
openmct.inspectorViews.addProvider(new ActivityInspectorViewProvider(openmct));
|
openmct.inspectorViews.addProvider(new ActivityInspectorViewProvider(openmct));
|
||||||
openmct.inspectorViews.addProvider(new GanttChartInspectorViewProvider(openmct));
|
openmct.inspectorViews.addProvider(new GanttChartInspectorViewProvider(openmct));
|
||||||
openmct.composition.addPolicy(ganttChartCompositionPolicy(openmct));
|
openmct.composition.addPolicy(ganttChartCompositionPolicy(openmct));
|
||||||
|
|
||||||
|
//add activity states get interceptor
|
||||||
|
const { name = ACTIVITY_STATES_DEFAULT_NAME, namespace = '', priority } = options;
|
||||||
|
const identifier = createActivityStatesIdentifier(namespace);
|
||||||
|
|
||||||
|
openmct.objects.addGetInterceptor(
|
||||||
|
activityStatesInterceptor(openmct, { identifier, name, priority })
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,19 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The SourceMap allows mapping specific implementations of plan domain objects to those expected by Open MCT.
|
||||||
|
* @typedef {object} SourceMapOption
|
||||||
|
* @property {string} orderedGroups the property of the plan that lists groups/swim lanes specifying what order they will be displayed in Open MCT.
|
||||||
|
* @property {string} activities the property of the plan that has the list of activities to be displayed.
|
||||||
|
* @property {string} groupId the property of the activity that maps to the group/swim lane it should be displayed in.
|
||||||
|
* @property {string} start The start time property of the activity
|
||||||
|
* @property {string} end The end time property of the activity
|
||||||
|
* @property {string} id The unique id of the activity. This is required to allow setting activity states
|
||||||
|
* @property {object} displayProperties a list of key: value pairs that specifies which properties of the activity should be displayed when it is selected. Ex. {'location': 'Location', 'metadata.length_in_meters', 'Length (meters)'}
|
||||||
|
*/
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
export function getValidatedData(domainObject) {
|
export function getValidatedData(domainObject) {
|
||||||
const sourceMap = domainObject.sourceMap;
|
const sourceMap = domainObject.sourceMap;
|
||||||
const json = getObjectJson(domainObject);
|
const json = getObjectJson(domainObject);
|
||||||
@ -45,6 +58,24 @@ export function getValidatedData(domainObject) {
|
|||||||
groupActivity.end = activity[sourceMap.end];
|
groupActivity.end = activity[sourceMap.end];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(sourceMap.filterMetadata)) {
|
||||||
|
groupActivity.filterMetadataValues = [];
|
||||||
|
sourceMap.filterMetadata.forEach((property) => {
|
||||||
|
const value = _.get(activity, property);
|
||||||
|
groupActivity.filterMetadataValues.push({
|
||||||
|
value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceMap.id) {
|
||||||
|
groupActivity.id = activity[sourceMap.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceMap.displayProperties) {
|
||||||
|
groupActivity.displayProperties = sourceMap.displayProperties;
|
||||||
|
}
|
||||||
|
|
||||||
if (!mappedJson[groupIdKey]) {
|
if (!mappedJson[groupIdKey]) {
|
||||||
mappedJson[groupIdKey] = [];
|
mappedJson[groupIdKey] = [];
|
||||||
}
|
}
|
||||||
@ -92,7 +123,6 @@ export function getValidatedGroups(domainObject, planData) {
|
|||||||
orderedGroupNames = groups;
|
orderedGroupNames = groups;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (orderedGroupNames === undefined) {
|
if (orderedGroupNames === undefined) {
|
||||||
orderedGroupNames = Object.keys(planData);
|
orderedGroupNames = Object.keys(planData);
|
||||||
}
|
}
|
||||||
@ -100,6 +130,37 @@ export function getValidatedGroups(domainObject, planData) {
|
|||||||
return orderedGroupNames;
|
return orderedGroupNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDisplayProperties(activity) {
|
||||||
|
let displayProperties = {};
|
||||||
|
function extractProperties(properties, useKeyAsLabel = false) {
|
||||||
|
Object.keys(properties).forEach((key) => {
|
||||||
|
const label = useKeyAsLabel ? key : properties[key];
|
||||||
|
const value = _.get(activity, key);
|
||||||
|
if (value) {
|
||||||
|
displayProperties[key] = { label, value };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activity?.displayProperties) {
|
||||||
|
extractProperties(activity.displayProperties);
|
||||||
|
} else if (activity?.properties) {
|
||||||
|
extractProperties(activity.properties, true);
|
||||||
|
}
|
||||||
|
return displayProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFilteredValues(activity) {
|
||||||
|
let values = [];
|
||||||
|
if (Array.isArray(activity.filterMetadataValues)) {
|
||||||
|
values = activity.filterMetadataValues;
|
||||||
|
} else if (activity?.properties) {
|
||||||
|
values = Object.values(activity.properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
export function getContrastingColor(hexColor) {
|
export function getContrastingColor(hexColor) {
|
||||||
function cutHex(h, start, end) {
|
function cutHex(h, start, end) {
|
||||||
const hStr = h.charAt(0) === '#' ? h.substring(1, 7) : h;
|
const hStr = h.charAt(0) === '#' ? h.substring(1, 7) : h;
|
||||||
|
@ -200,7 +200,13 @@ export default {
|
|||||||
this.chartVisible = true;
|
this.chartVisible = true;
|
||||||
this.chartContainer = this.$refs.chart;
|
this.chartContainer = this.$refs.chart;
|
||||||
this.drawnOnce = false;
|
this.drawnOnce = false;
|
||||||
this.visibilityObserver = new IntersectionObserver(this.visibilityChanged);
|
const rootContainer = this.openmct.element;
|
||||||
|
const options = {
|
||||||
|
root: rootContainer,
|
||||||
|
rootMargin: '0px',
|
||||||
|
threshold: 1.0
|
||||||
|
};
|
||||||
|
this.visibilityObserver = new IntersectionObserver(this.visibilityChanged, options);
|
||||||
eventHelpers.extend(this);
|
eventHelpers.extend(this);
|
||||||
this.seriesModels = [];
|
this.seriesModels = [];
|
||||||
this.config = this.getConfig();
|
this.config = this.getConfig();
|
||||||
@ -276,6 +282,8 @@ export default {
|
|||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
visibilityChanged([entry]) {
|
visibilityChanged([entry]) {
|
||||||
|
// Per https://github.com/nasa/openmct/issues/7405, we only want to draw when the chart is visible.
|
||||||
|
// and we need to use the Open MCT root element as the root of the intersection observer.
|
||||||
if (entry.target === this.chartContainer) {
|
if (entry.target === this.chartContainer) {
|
||||||
const wasVisible = this.chartVisible;
|
const wasVisible = this.chartVisible;
|
||||||
this.chartVisible = entry.isIntersecting;
|
this.chartVisible = entry.isIntersecting;
|
||||||
|
@ -139,7 +139,9 @@ export default class Model extends EventEmitter {
|
|||||||
|
|
||||||
/** @typedef {any} TODO */
|
/** @typedef {any} TODO */
|
||||||
|
|
||||||
/** @typedef {TODO} OpenMCT */
|
/**
|
||||||
|
* @typedef {import('../../../../openmct.js').OpenMCT} OpenMCT
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@template {object} T
|
@template {object} T
|
||||||
|
@ -211,9 +211,16 @@ export default class PlotSeries extends Model {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!this.unsubscribe) {
|
if (!this.unsubscribe) {
|
||||||
this.unsubscribe = this.openmct.telemetry.subscribe(this.domainObject, this.add.bind(this), {
|
this.unsubscribe = this.openmct.telemetry.subscribe(
|
||||||
filters: this.filters
|
this.domainObject,
|
||||||
});
|
(data) => {
|
||||||
|
this.addAll(data, true);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filters: this.filters,
|
||||||
|
strategy: this.openmct.telemetry.SUBSCRIBE_STRATEGY.BATCH
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -302,9 +309,7 @@ export default class PlotSeries extends Model {
|
|||||||
this.resetStats();
|
this.resetStats();
|
||||||
this.emit('reset');
|
this.emit('reset');
|
||||||
if (newData) {
|
if (newData) {
|
||||||
newData.forEach(function (point) {
|
this.addAll(newData, true);
|
||||||
this.add(point, true);
|
|
||||||
}, this);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -416,14 +421,14 @@ export default class PlotSeries extends Model {
|
|||||||
* when adding an array of points that are already properly sorted.
|
* when adding an array of points that are already properly sorted.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @param {Object} point a telemetry datum.
|
* @param {Object} newData a telemetry datum.
|
||||||
* @param {Boolean} [appendOnly] default false, if true will append
|
* @param {Boolean} [sorted] default false, if true will append
|
||||||
* a point to the end without dupe checking.
|
* a point to the end without dupe checking.
|
||||||
*/
|
*/
|
||||||
add(point, appendOnly) {
|
add(newData, sorted = false) {
|
||||||
let data = this.getSeriesData();
|
let data = this.getSeriesData();
|
||||||
let insertIndex = data.length;
|
let insertIndex = data.length;
|
||||||
const currentYVal = this.getYVal(point);
|
const currentYVal = this.getYVal(newData);
|
||||||
const lastYVal = this.getYVal(data[insertIndex - 1]);
|
const lastYVal = this.getYVal(data[insertIndex - 1]);
|
||||||
|
|
||||||
if (this.isValueInvalid(currentYVal) && this.isValueInvalid(lastYVal)) {
|
if (this.isValueInvalid(currentYVal) && this.isValueInvalid(lastYVal)) {
|
||||||
@ -432,22 +437,28 @@ export default class PlotSeries extends Model {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!appendOnly) {
|
if (!sorted) {
|
||||||
insertIndex = this.sortedIndex(point);
|
insertIndex = this.sortedIndex(newData);
|
||||||
if (this.getXVal(data[insertIndex]) === this.getXVal(point)) {
|
if (this.getXVal(data[insertIndex]) === this.getXVal(newData)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.getXVal(data[insertIndex - 1]) === this.getXVal(point)) {
|
if (this.getXVal(data[insertIndex - 1]) === this.getXVal(newData)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateStats(point);
|
this.updateStats(newData);
|
||||||
point.mctLimitState = this.evaluate(point);
|
newData.mctLimitState = this.evaluate(newData);
|
||||||
data.splice(insertIndex, 0, point);
|
data.splice(insertIndex, 0, newData);
|
||||||
this.updateSeriesData(data);
|
this.updateSeriesData(data);
|
||||||
this.emit('add', point, insertIndex, this);
|
this.emit('add', newData, insertIndex, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
addAll(points, sorted = false) {
|
||||||
|
for (let i = 0; i < points.length; i++) {
|
||||||
|
this.add(points[i], sorted);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -65,6 +65,7 @@ import PerformanceIndicator from './performanceIndicator/plugin.js';
|
|||||||
import CouchDBPlugin from './persistence/couch/plugin.js';
|
import CouchDBPlugin from './persistence/couch/plugin.js';
|
||||||
import PlanLayout from './plan/plugin.js';
|
import PlanLayout from './plan/plugin.js';
|
||||||
import PlotPlugin from './plot/plugin.js';
|
import PlotPlugin from './plot/plugin.js';
|
||||||
|
import ReloadAction from './reloadAction/plugin.js';
|
||||||
import RemoteClock from './remoteClock/plugin.js';
|
import RemoteClock from './remoteClock/plugin.js';
|
||||||
import StaticRootPlugin from './staticRootPlugin/plugin.js';
|
import StaticRootPlugin from './staticRootPlugin/plugin.js';
|
||||||
import SummaryWidget from './summaryWidget/plugin.js';
|
import SummaryWidget from './summaryWidget/plugin.js';
|
||||||
@ -141,6 +142,7 @@ plugins.Filters = Filters;
|
|||||||
plugins.ObjectMigration = ObjectMigration;
|
plugins.ObjectMigration = ObjectMigration;
|
||||||
plugins.GoToOriginalAction = GoToOriginalAction;
|
plugins.GoToOriginalAction = GoToOriginalAction;
|
||||||
plugins.OpenInNewTabAction = OpenInNewTabAction;
|
plugins.OpenInNewTabAction = OpenInNewTabAction;
|
||||||
|
plugins.ReloadAction = ReloadAction;
|
||||||
plugins.ClearData = ClearData;
|
plugins.ClearData = ClearData;
|
||||||
plugins.WebPage = WebPagePlugin;
|
plugins.WebPage = WebPagePlugin;
|
||||||
plugins.Espresso = Espresso;
|
plugins.Espresso = Espresso;
|
||||||
|
37
src/plugins/reloadAction/ReloadAction.js
Normal file
37
src/plugins/reloadAction/ReloadAction.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
export default class ReloadAction {
|
||||||
|
constructor(openmct) {
|
||||||
|
this.name = 'Reload';
|
||||||
|
this.key = 'reload';
|
||||||
|
this.description = 'Reload this object and its children';
|
||||||
|
this.group = 'action';
|
||||||
|
this.priority = 10;
|
||||||
|
this.cssClass = 'icon-refresh';
|
||||||
|
|
||||||
|
this.openmct = openmct;
|
||||||
|
}
|
||||||
|
invoke(objectPath, view) {
|
||||||
|
const domainObject = objectPath[0];
|
||||||
|
this.openmct.objectViews.emit('reload', domainObject);
|
||||||
|
}
|
||||||
|
}
|
28
src/plugins/reloadAction/plugin.js
Normal file
28
src/plugins/reloadAction/plugin.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2023, 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 ReloadAction from './ReloadAction.js';
|
||||||
|
|
||||||
|
export default function plugin() {
|
||||||
|
return function install(openmct) {
|
||||||
|
openmct.actions.register(new ReloadAction(openmct));
|
||||||
|
};
|
||||||
|
}
|
@ -59,26 +59,21 @@ export default class RemoteClock extends DefaultClock {
|
|||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
this.openmct.objects
|
this.openmct.objects.get(this.identifier).then((domainObject) => {
|
||||||
.get(this.identifier)
|
// The start method is called when at least one listener registers with the clock.
|
||||||
.then((domainObject) => {
|
// When the clock is changed, listeners are unregistered from the clock and the stop method is called.
|
||||||
// The start method is called when at least one listener registers with the clock.
|
// Sometimes, the objects.get call above does not resolve before the stop method is called.
|
||||||
// When the clock is changed, listeners are unregistered from the clock and the stop method is called.
|
// So when we proceed with the clock subscription below, we first need to ensure that there is at least one listener for our clock.
|
||||||
// Sometimes, the objects.get call above does not resolve before the stop method is called.
|
if (this.eventNames().length === 0) {
|
||||||
// So when we proceed with the clock subscription below, we first need to ensure that there is at least one listener for our clock.
|
return;
|
||||||
if (this.eventNames().length === 0) {
|
}
|
||||||
return;
|
this.openmct.time.on('timeSystem', this._timeSystemChange);
|
||||||
}
|
this.timeTelemetryObject = domainObject;
|
||||||
this.openmct.time.on('timeSystem', this._timeSystemChange);
|
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
|
||||||
this.timeTelemetryObject = domainObject;
|
this._timeSystemChange();
|
||||||
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
|
this._requestLatest();
|
||||||
this._timeSystemChange();
|
this._subscribe();
|
||||||
this._requestLatest();
|
});
|
||||||
this._subscribe();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
throw new Error(error);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
|
@ -21,8 +21,10 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
import Tabs from './tabs.js';
|
import Tabs from './tabs.js';
|
||||||
|
|
||||||
export default function plugin() {
|
export default function plugin(options) {
|
||||||
return function install(openmct) {
|
return function install(openmct) {
|
||||||
|
const eagerLoad = options?.eagerLoad ?? false;
|
||||||
|
|
||||||
openmct.objectViews.addProvider(new Tabs(openmct));
|
openmct.objectViews.addProvider(new Tabs(openmct));
|
||||||
|
|
||||||
openmct.types.addType('tabs', {
|
openmct.types.addType('tabs', {
|
||||||
@ -32,13 +34,13 @@ export default function plugin() {
|
|||||||
cssClass: 'icon-tabs-view',
|
cssClass: 'icon-tabs-view',
|
||||||
initialize(domainObject) {
|
initialize(domainObject) {
|
||||||
domainObject.composition = [];
|
domainObject.composition = [];
|
||||||
domainObject.keep_alive = true;
|
domainObject.keep_alive = eagerLoad;
|
||||||
},
|
},
|
||||||
form: [
|
form: [
|
||||||
{
|
{
|
||||||
key: 'keep_alive',
|
key: 'keep_alive',
|
||||||
name: 'Eager Load Tabs',
|
name: 'Eager Load Tabs',
|
||||||
control: 'select',
|
control: 'toggleSwitch',
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
name: 'True',
|
name: 'True',
|
||||||
|
@ -30,7 +30,8 @@ describe('the plugin', function () {
|
|||||||
let element;
|
let element;
|
||||||
let child;
|
let child;
|
||||||
let openmct;
|
let openmct;
|
||||||
let tabsLayoutDefinition;
|
let tabsType;
|
||||||
|
|
||||||
const testViewObject = {
|
const testViewObject = {
|
||||||
identifier: {
|
identifier: {
|
||||||
key: 'mock-tabs-object',
|
key: 'mock-tabs-object',
|
||||||
@ -85,8 +86,7 @@ describe('the plugin', function () {
|
|||||||
|
|
||||||
beforeEach((done) => {
|
beforeEach((done) => {
|
||||||
openmct = createOpenMct();
|
openmct = createOpenMct();
|
||||||
openmct.install(new TabsLayout());
|
tabsType = openmct.types.get('tabs');
|
||||||
tabsLayoutDefinition = openmct.types.get('tabs');
|
|
||||||
|
|
||||||
element = document.createElement('div');
|
element = document.createElement('div');
|
||||||
child = document.createElement('div');
|
child = document.createElement('div');
|
||||||
@ -100,15 +100,56 @@ describe('the plugin', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
child = undefined;
|
||||||
|
element = undefined;
|
||||||
|
|
||||||
return resetApplicationState(openmct);
|
return resetApplicationState(openmct);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('defines a tabs object type with the correct key', () => {
|
it('is installed by default and provides a tabs object', () => {
|
||||||
expect(tabsLayoutDefinition.definition.name).toEqual('Tabs View');
|
expect(tabsType.definition.name).toEqual('Tabs View');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is creatable', () => {
|
it('the tabs object is creatable', () => {
|
||||||
expect(tabsLayoutDefinition.definition.creatable).toEqual(true);
|
expect(tabsType.definition.creatable).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets eager load to false by default', () => {
|
||||||
|
const tabsObject = {
|
||||||
|
identifier: {
|
||||||
|
key: 'some-tab-object',
|
||||||
|
namespace: ''
|
||||||
|
},
|
||||||
|
type: 'tabs'
|
||||||
|
};
|
||||||
|
|
||||||
|
tabsType.definition.initialize(tabsObject);
|
||||||
|
|
||||||
|
expect(tabsObject.keep_alive).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be installed with eager load defaulting to true', () => {
|
||||||
|
const options = {
|
||||||
|
eagerLoad: true
|
||||||
|
};
|
||||||
|
const openmct2 = createOpenMct();
|
||||||
|
openmct2.install(new TabsLayout(options));
|
||||||
|
openmct2.startHeadless();
|
||||||
|
|
||||||
|
const tabsObject = {
|
||||||
|
identifier: {
|
||||||
|
key: 'some-tab-object',
|
||||||
|
namespace: ''
|
||||||
|
},
|
||||||
|
type: 'tabs'
|
||||||
|
};
|
||||||
|
|
||||||
|
const overriddenTabsType = openmct2.types.get('tabs');
|
||||||
|
overriddenTabsType.definition.initialize(tabsObject);
|
||||||
|
|
||||||
|
expect(tabsObject.keep_alive).toBeTrue();
|
||||||
|
|
||||||
|
return resetApplicationState(openmct2);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('the view', function () {
|
describe('the view', function () {
|
||||||
|
@ -37,10 +37,11 @@ export default class TelemetryTable extends EventEmitter {
|
|||||||
|
|
||||||
this.domainObject = domainObject;
|
this.domainObject = domainObject;
|
||||||
this.openmct = openmct;
|
this.openmct = openmct;
|
||||||
this.rowCount = 100;
|
|
||||||
this.tableComposition = undefined;
|
this.tableComposition = undefined;
|
||||||
this.datumCache = [];
|
this.datumCache = [];
|
||||||
this.configuration = new TelemetryTableConfiguration(domainObject, openmct);
|
this.configuration = new TelemetryTableConfiguration(domainObject, openmct);
|
||||||
|
this.telemetryMode = this.configuration.getTelemetryMode();
|
||||||
|
this.rowLimit = this.configuration.getRowLimit();
|
||||||
this.paused = false;
|
this.paused = false;
|
||||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||||
|
|
||||||
@ -101,18 +102,40 @@ export default class TelemetryTable extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateTelemetryMode(mode) {
|
||||||
|
if (this.telemetryMode === mode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.telemetryMode = mode;
|
||||||
|
|
||||||
|
this.updateRowLimit();
|
||||||
|
|
||||||
|
this.clearAndResubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRowLimit() {
|
||||||
|
if (this.telemetryMode === 'performance') {
|
||||||
|
this.tableRows.setLimit(this.rowLimit);
|
||||||
|
} else {
|
||||||
|
this.tableRows.removeLimit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createTableRowCollections() {
|
createTableRowCollections() {
|
||||||
this.tableRows = new TableRowCollection();
|
this.tableRows = new TableRowCollection();
|
||||||
|
|
||||||
//Fetch any persisted default sort
|
//Fetch any persisted default sort
|
||||||
let sortOptions = this.configuration.getConfiguration().sortOptions;
|
let sortOptions = this.configuration.getConfiguration().sortOptions;
|
||||||
|
|
||||||
//If no persisted sort order, default to sorting by time system, ascending.
|
//If no persisted sort order, default to sorting by time system, descending.
|
||||||
sortOptions = sortOptions || {
|
sortOptions = sortOptions || {
|
||||||
key: this.openmct.time.timeSystem().key,
|
key: this.openmct.time.timeSystem().key,
|
||||||
direction: 'asc'
|
direction: 'desc'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.updateRowLimit();
|
||||||
|
|
||||||
this.tableRows.sortBy(sortOptions);
|
this.tableRows.sortBy(sortOptions);
|
||||||
this.tableRows.on('resetRowsFromAllData', this.resetRowsFromAllData);
|
this.tableRows.on('resetRowsFromAllData', this.resetRowsFromAllData);
|
||||||
}
|
}
|
||||||
@ -144,6 +167,11 @@ export default class TelemetryTable extends EventEmitter {
|
|||||||
|
|
||||||
this.removeTelemetryCollection(keyString);
|
this.removeTelemetryCollection(keyString);
|
||||||
|
|
||||||
|
if (this.telemetryMode === 'performance') {
|
||||||
|
requestOptions.size = this.rowLimit;
|
||||||
|
requestOptions.enforceSize = true;
|
||||||
|
}
|
||||||
|
|
||||||
this.telemetryCollections[keyString] = this.openmct.telemetry.requestCollection(
|
this.telemetryCollections[keyString] = this.openmct.telemetry.requestCollection(
|
||||||
telemetryObject,
|
telemetryObject,
|
||||||
requestOptions
|
requestOptions
|
||||||
|
@ -48,6 +48,10 @@ export default class TelemetryTableConfiguration extends EventEmitter {
|
|||||||
configuration.columnOrder = configuration.columnOrder || [];
|
configuration.columnOrder = configuration.columnOrder || [];
|
||||||
configuration.cellFormat = configuration.cellFormat || {};
|
configuration.cellFormat = configuration.cellFormat || {};
|
||||||
configuration.autosize = configuration.autosize === undefined ? true : configuration.autosize;
|
configuration.autosize = configuration.autosize === undefined ? true : configuration.autosize;
|
||||||
|
// anything that doesn't have a telemetryMode existed before the change and should stay as it was for consistency
|
||||||
|
configuration.telemetryMode = configuration.telemetryMode ?? 'unlimited';
|
||||||
|
configuration.persistModeChange = configuration.persistModeChange ?? true;
|
||||||
|
configuration.rowLimit = configuration.rowLimit ?? 50;
|
||||||
|
|
||||||
return configuration;
|
return configuration;
|
||||||
}
|
}
|
||||||
@ -137,6 +141,42 @@ export default class TelemetryTableConfiguration extends EventEmitter {
|
|||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTelemetryMode() {
|
||||||
|
let configuration = this.getConfiguration();
|
||||||
|
|
||||||
|
return configuration.telemetryMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTelemetryMode(mode) {
|
||||||
|
let configuration = this.getConfiguration();
|
||||||
|
configuration.telemetryMode = mode;
|
||||||
|
this.updateConfiguration(configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRowLimit() {
|
||||||
|
let configuration = this.getConfiguration();
|
||||||
|
|
||||||
|
return configuration.rowLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRowLimit(limit) {
|
||||||
|
let configuration = this.getConfiguration();
|
||||||
|
configuration.rowLimit = limit;
|
||||||
|
this.updateConfiguration(configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPersistModeChange() {
|
||||||
|
let configuration = this.getConfiguration();
|
||||||
|
|
||||||
|
return configuration.persistModeChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPersistModeChange(value) {
|
||||||
|
let configuration = this.getConfiguration();
|
||||||
|
configuration.persistModeChange = value;
|
||||||
|
this.updateConfiguration(configuration);
|
||||||
|
}
|
||||||
|
|
||||||
getColumnWidths() {
|
getColumnWidths() {
|
||||||
let configuration = this.getConfiguration();
|
let configuration = this.getConfiguration();
|
||||||
|
|
||||||
|
@ -20,17 +20,57 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
export default {
|
export default function getTelemetryTableType(options = {}) {
|
||||||
name: 'Telemetry Table',
|
const { telemetryMode = 'performance', persistModeChanges = true, rowLimit = 50 } = options;
|
||||||
description:
|
|
||||||
'Display values for one or more telemetry end points in a scrolling table. Each row is a time-stamped value.',
|
return {
|
||||||
creatable: true,
|
name: 'Telemetry Table',
|
||||||
cssClass: 'icon-tabular-scrolling',
|
description:
|
||||||
initialize(domainObject) {
|
'Display values for one or more telemetry end points in a scrolling table. Each row is a time-stamped value.',
|
||||||
domainObject.composition = [];
|
creatable: true,
|
||||||
domainObject.configuration = {
|
cssClass: 'icon-tabular-scrolling',
|
||||||
columnWidths: {},
|
form: [
|
||||||
hiddenColumns: {}
|
{
|
||||||
};
|
key: 'telemetryMode',
|
||||||
}
|
name: 'Telemetry Mode',
|
||||||
};
|
control: 'select',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: 'performance',
|
||||||
|
name: 'Performance Mode'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'unlimited',
|
||||||
|
name: 'Unlimited Mode'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
cssClass: 'l-inline',
|
||||||
|
property: ['configuration', 'telemetryMode']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Persist Telemetry Mode Changes',
|
||||||
|
control: 'toggleSwitch',
|
||||||
|
cssClass: 'l-input',
|
||||||
|
key: 'persistModeChanges',
|
||||||
|
property: ['configuration', 'persistModeChanges']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Performance Mode Row Limit',
|
||||||
|
control: 'toggleSwitch',
|
||||||
|
cssClass: 'l-input',
|
||||||
|
key: 'rowLimit',
|
||||||
|
property: ['configuration', 'rowLimit']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
initialize(domainObject) {
|
||||||
|
domainObject.composition = [];
|
||||||
|
domainObject.configuration = {
|
||||||
|
columnWidths: {},
|
||||||
|
hiddenColumns: {},
|
||||||
|
telemetryMode,
|
||||||
|
persistModeChanges,
|
||||||
|
rowLimit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -25,7 +25,7 @@ import TableComponent from './components/TableComponent.vue';
|
|||||||
import TelemetryTable from './TelemetryTable.js';
|
import TelemetryTable from './TelemetryTable.js';
|
||||||
|
|
||||||
export default class TelemetryTableView {
|
export default class TelemetryTableView {
|
||||||
constructor(openmct, domainObject, objectPath) {
|
constructor(openmct, domainObject, objectPath, options) {
|
||||||
this.openmct = openmct;
|
this.openmct = openmct;
|
||||||
this.domainObject = domainObject;
|
this.domainObject = domainObject;
|
||||||
this.objectPath = objectPath;
|
this.objectPath = objectPath;
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
import TelemetryTableView from './TelemetryTableView.js';
|
import TelemetryTableView from './TelemetryTableView.js';
|
||||||
|
|
||||||
export default function TelemetryTableViewProvider(openmct) {
|
export default function TelemetryTableViewProvider(openmct, options) {
|
||||||
function hasTelemetry(domainObject) {
|
function hasTelemetry(domainObject) {
|
||||||
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {
|
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {
|
||||||
return false;
|
return false;
|
||||||
@ -44,7 +44,7 @@ export default function TelemetryTableViewProvider(openmct) {
|
|||||||
return domainObject.type === 'table';
|
return domainObject.type === 'table';
|
||||||
},
|
},
|
||||||
view(domainObject, objectPath) {
|
view(domainObject, objectPath) {
|
||||||
return new TelemetryTableView(openmct, domainObject, objectPath);
|
return new TelemetryTableView(openmct, domainObject, objectPath, options);
|
||||||
},
|
},
|
||||||
priority() {
|
priority() {
|
||||||
return 1;
|
return 1;
|
||||||
|
@ -129,6 +129,15 @@ export default class TableRowCollection extends EventEmitter {
|
|||||||
this.rows[index] = foundRow;
|
this.rows[index] = foundRow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLimit(rowLimit) {
|
||||||
|
this.rowLimit = rowLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeLimit() {
|
||||||
|
this.rowLimit = null;
|
||||||
|
delete this.rowLimit;
|
||||||
|
}
|
||||||
|
|
||||||
sortCollection(rows) {
|
sortCollection(rows) {
|
||||||
const sortedRows = _.orderBy(
|
const sortedRows = _.orderBy(
|
||||||
rows,
|
rows,
|
||||||
@ -363,10 +372,22 @@ export default class TableRowCollection extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getRows() {
|
getRows() {
|
||||||
|
if (this.rowLimit && this.rows.length > this.rowLimit) {
|
||||||
|
if (this.sortOptions.direction === 'desc') {
|
||||||
|
return this.rows.slice(0, this.rowLimit);
|
||||||
|
} else {
|
||||||
|
return this.rows.slice(-this.rowLimit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return this.rows;
|
return this.rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRowsLength() {
|
getRowsLength() {
|
||||||
|
if (this.rowLimit && this.rows.length > this.rowLimit) {
|
||||||
|
return this.rowLimit;
|
||||||
|
}
|
||||||
|
|
||||||
return this.rows.length;
|
return this.rows.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,8 +22,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<td
|
<td
|
||||||
ref="tableCell"
|
ref="tableCell"
|
||||||
:aria-label="formattedValue"
|
|
||||||
:title="formattedValue"
|
:title="formattedValue"
|
||||||
|
:aria-label="`${columnKey} table cell ${formattedValue}`"
|
||||||
@click="selectCell($event.currentTarget, columnKey)"
|
@click="selectCell($event.currentTarget, columnKey)"
|
||||||
@mouseover.ctrl="showToolTip"
|
@mouseover.ctrl="showToolTip"
|
||||||
@mouseleave="hideToolTip"
|
@mouseleave="hideToolTip"
|
||||||
|
@ -222,6 +222,7 @@
|
|||||||
ref="contentTable"
|
ref="contentTable"
|
||||||
class="c-table__body c-telemetry-table__body js-telemetry-table__content"
|
class="c-table__body c-telemetry-table__body js-telemetry-table__content"
|
||||||
:style="{ height: totalHeight + 'px' }"
|
:style="{ height: totalHeight + 'px' }"
|
||||||
|
:aria-label="`${table.domainObject.name} table content`"
|
||||||
>
|
>
|
||||||
<tbody>
|
<tbody>
|
||||||
<telemetry-table-row
|
<telemetry-table-row
|
||||||
@ -233,7 +234,7 @@
|
|||||||
:object-path="objectPath"
|
:object-path="objectPath"
|
||||||
:row-offset="rowOffset"
|
:row-offset="rowOffset"
|
||||||
:row-height="rowHeight"
|
:row-height="rowHeight"
|
||||||
:row="row"
|
:row="getRow(rowIndex)"
|
||||||
:marked="row.marked"
|
:marked="row.marked"
|
||||||
@mark="markRow"
|
@mark="markRow"
|
||||||
@unmark="unmarkRow"
|
@unmark="unmarkRow"
|
||||||
@ -276,6 +277,8 @@
|
|||||||
class="c-telemetry-table__footer"
|
class="c-telemetry-table__footer"
|
||||||
:marked-rows="markedRows.length"
|
:marked-rows="markedRows.length"
|
||||||
:total-rows="totalNumberOfRows"
|
:total-rows="totalNumberOfRows"
|
||||||
|
:telemetry-mode="telemetryMode"
|
||||||
|
@telemetry-mode-change="updateTelemetryMode"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -292,6 +295,7 @@ import CSVExporter from '../../../exporters/CSVExporter.js';
|
|||||||
import ProgressBar from '../../../ui/components/ProgressBar.vue';
|
import ProgressBar from '../../../ui/components/ProgressBar.vue';
|
||||||
import Search from '../../../ui/components/SearchComponent.vue';
|
import Search from '../../../ui/components/SearchComponent.vue';
|
||||||
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
|
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
|
||||||
|
import throttle from '../../../utils/throttle';
|
||||||
import SizingRow from './SizingRow.vue';
|
import SizingRow from './SizingRow.vue';
|
||||||
import TableColumnHeader from './TableColumnHeader.vue';
|
import TableColumnHeader from './TableColumnHeader.vue';
|
||||||
import TableFooterIndicator from './TableFooterIndicator.vue';
|
import TableFooterIndicator from './TableFooterIndicator.vue';
|
||||||
@ -300,7 +304,7 @@ import TelemetryTableRow from './TableRow.vue';
|
|||||||
const VISIBLE_ROW_COUNT = 100;
|
const VISIBLE_ROW_COUNT = 100;
|
||||||
const ROW_HEIGHT = 17;
|
const ROW_HEIGHT = 17;
|
||||||
const RESIZE_POLL_INTERVAL = 200;
|
const RESIZE_POLL_INTERVAL = 200;
|
||||||
const AUTO_SCROLL_TRIGGER_HEIGHT = 100;
|
const AUTO_SCROLL_TRIGGER_HEIGHT = ROW_HEIGHT * 3;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -384,7 +388,9 @@ export default {
|
|||||||
enableRegexSearch: {},
|
enableRegexSearch: {},
|
||||||
hideHeaders: configuration.hideHeaders,
|
hideHeaders: configuration.hideHeaders,
|
||||||
totalNumberOfRows: 0,
|
totalNumberOfRows: 0,
|
||||||
rowContext: {}
|
rowContext: {},
|
||||||
|
telemetryMode: configuration.telemetryMode,
|
||||||
|
persistModeChanges: configuration.persistModeChanges
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -437,6 +443,12 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
loading: {
|
loading: {
|
||||||
handler(isLoading) {
|
handler(isLoading) {
|
||||||
|
if (isLoading) {
|
||||||
|
this.setLoadingPromise();
|
||||||
|
} else {
|
||||||
|
this.loadFinishResolve();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.viewActionsCollection) {
|
if (this.viewActionsCollection) {
|
||||||
let action = isLoading ? 'disable' : 'enable';
|
let action = isLoading ? 'disable' : 'enable';
|
||||||
this.viewActionsCollection[action](['export-csv-all']);
|
this.viewActionsCollection[action](['export-csv-all']);
|
||||||
@ -503,6 +515,8 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateVisibleRows = throttle(this.updateVisibleRows, 1000);
|
||||||
|
|
||||||
this.table.on('object-added', this.addObject);
|
this.table.on('object-added', this.addObject);
|
||||||
this.table.on('object-removed', this.removeObject);
|
this.table.on('object-removed', this.removeObject);
|
||||||
this.table.on('refresh', this.clearRowsAndRerender);
|
this.table.on('refresh', this.clearRowsAndRerender);
|
||||||
@ -555,6 +569,12 @@ export default {
|
|||||||
this.table.destroy();
|
this.table.destroy();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setLoadingPromise() {
|
||||||
|
this.loadFinishResolve = null;
|
||||||
|
this.isFinishedLoading = new Promise((resolve, reject) => {
|
||||||
|
this.loadFinishResolve = resolve;
|
||||||
|
});
|
||||||
|
},
|
||||||
updateVisibleRows() {
|
updateVisibleRows() {
|
||||||
if (!this.updatingView) {
|
if (!this.updatingView) {
|
||||||
this.updatingView = this.renderWhenVisible(() => {
|
this.updatingView = this.renderWhenVisible(() => {
|
||||||
@ -632,7 +652,21 @@ export default {
|
|||||||
|
|
||||||
this.calculateScrollbarWidth();
|
this.calculateScrollbarWidth();
|
||||||
},
|
},
|
||||||
|
getRow(rowIndex) {
|
||||||
|
return toRaw(this.visibleRows[rowIndex]);
|
||||||
|
},
|
||||||
sortBy(columnKey) {
|
sortBy(columnKey) {
|
||||||
|
let timeSystemKey = this.openmct.time.getTimeSystem().key;
|
||||||
|
|
||||||
|
if (this.telemetryMode === 'performance' && columnKey !== timeSystemKey) {
|
||||||
|
this.confirmUnlimitedMode('Switch to Unlimited Telemetry and Sort', () => {
|
||||||
|
this.initiateSort(columnKey);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.initiateSort(columnKey);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
initiateSort(columnKey) {
|
||||||
// If sorting by the same column, flip the sort direction.
|
// If sorting by the same column, flip the sort direction.
|
||||||
if (this.sortOptions.key === columnKey) {
|
if (this.sortOptions.key === columnKey) {
|
||||||
if (this.sortOptions.direction === 'asc') {
|
if (this.sortOptions.direction === 'asc') {
|
||||||
@ -643,7 +677,7 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.sortOptions = {
|
this.sortOptions = {
|
||||||
key: columnKey,
|
key: columnKey,
|
||||||
direction: 'asc'
|
direction: 'desc'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -653,7 +687,7 @@ export default {
|
|||||||
this.updateVisibleRows();
|
this.updateVisibleRows();
|
||||||
this.synchronizeScrollX();
|
this.synchronizeScrollX();
|
||||||
|
|
||||||
if (this.shouldSnapToBottom()) {
|
if (this.shouldAutoScroll()) {
|
||||||
this.autoScroll = true;
|
this.autoScroll = true;
|
||||||
} else {
|
} else {
|
||||||
// If user scrolls away from bottom, disable auto-scroll.
|
// If user scrolls away from bottom, disable auto-scroll.
|
||||||
@ -661,13 +695,17 @@ export default {
|
|||||||
this.autoScroll = false;
|
this.autoScroll = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
shouldSnapToBottom() {
|
shouldAutoScroll() {
|
||||||
|
if (this.sortOptions.direction === 'desc') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
this.scrollable.scrollTop >=
|
this.scrollable.scrollTop >=
|
||||||
this.scrollable.scrollHeight - this.scrollable.offsetHeight - AUTO_SCROLL_TRIGGER_HEIGHT
|
this.scrollable.scrollHeight - this.scrollable.offsetHeight - AUTO_SCROLL_TRIGGER_HEIGHT
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
scrollToBottom() {
|
initiateAutoScroll() {
|
||||||
this.scrollable.scrollTop = Number.MAX_SAFE_INTEGER;
|
this.scrollable.scrollTop = Number.MAX_SAFE_INTEGER;
|
||||||
},
|
},
|
||||||
synchronizeScrollX() {
|
synchronizeScrollX() {
|
||||||
@ -716,7 +754,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.autoScroll) {
|
if (this.autoScroll) {
|
||||||
this.scrollToBottom();
|
this.initiateAutoScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateVisibleRows();
|
this.updateVisibleRows();
|
||||||
@ -743,12 +781,25 @@ export default {
|
|||||||
headers: headerKeys
|
headers: headerKeys
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
exportAllDataAsCSV() {
|
getTableRowData() {
|
||||||
const justTheData = this.table.tableRows
|
const justTheData = this.table.tableRows
|
||||||
.getRows()
|
.getRows()
|
||||||
.map((row) => row.getFormattedDatum(this.headers));
|
.map((row) => row.getFormattedDatum(this.headers));
|
||||||
|
|
||||||
this.exportAsCSV(justTheData);
|
return justTheData;
|
||||||
|
},
|
||||||
|
exportAllDataAsCSV() {
|
||||||
|
if (this.telemetryMode === 'performance') {
|
||||||
|
this.confirmUnlimitedMode('Switch to Unlimited Telemetry and Export', () => {
|
||||||
|
const data = this.getTableRowData();
|
||||||
|
|
||||||
|
this.exportAsCSV(data);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const data = this.getTableRowData();
|
||||||
|
|
||||||
|
this.exportAsCSV(data);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
exportMarkedDataAsCSV() {
|
exportMarkedDataAsCSV() {
|
||||||
const data = this.table.tableRows
|
const data = this.table.tableRows
|
||||||
@ -842,7 +893,7 @@ export default {
|
|||||||
// On some resize events scrollTop is reset to 0. Possibly due to a transition we're using?
|
// On some resize events scrollTop is reset to 0. Possibly due to a transition we're using?
|
||||||
// Need to preserve scroll position in this case.
|
// Need to preserve scroll position in this case.
|
||||||
if (this.autoScroll) {
|
if (this.autoScroll) {
|
||||||
this.scrollToBottom();
|
this.initiateAutoScroll();
|
||||||
} else {
|
} else {
|
||||||
this.scrollable.scrollTop = scrollTop;
|
this.scrollable.scrollTop = scrollTop;
|
||||||
}
|
}
|
||||||
@ -1099,6 +1150,54 @@ export default {
|
|||||||
this.viewActionsCollection.hide(['expand-columns']);
|
this.viewActionsCollection.hide(['expand-columns']);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
confirmUnlimitedMode(
|
||||||
|
label,
|
||||||
|
callback,
|
||||||
|
message = 'A new data request for all telemetry values for all endpoints will be made which will take some time. Do you want to continue?'
|
||||||
|
) {
|
||||||
|
const dialog = this.openmct.overlays.dialog({
|
||||||
|
iconClass: 'alert',
|
||||||
|
message,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
label,
|
||||||
|
emphasis: true,
|
||||||
|
callback: async () => {
|
||||||
|
this.updateTelemetryMode();
|
||||||
|
await this.isFinishedLoading;
|
||||||
|
|
||||||
|
callback();
|
||||||
|
|
||||||
|
dialog.dismiss();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cancel',
|
||||||
|
callback: () => {
|
||||||
|
dialog.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateTelemetryMode() {
|
||||||
|
this.telemetryMode = this.telemetryMode === 'unlimited' ? 'performance' : 'unlimited';
|
||||||
|
|
||||||
|
if (this.persistModeChanges) {
|
||||||
|
this.table.configuration.setTelemetryMode(this.telemetryMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.table.updateTelemetryMode(this.telemetryMode);
|
||||||
|
|
||||||
|
const timeSystemKey = this.openmct.time.getTimeSystem().key;
|
||||||
|
|
||||||
|
if (this.telemetryMode === 'performance' && this.sortOptions.key !== timeSystemKey) {
|
||||||
|
this.openmct.notifications.info(
|
||||||
|
'Switched to Performance Mode: Table now sorted by time for optimized efficiency.'
|
||||||
|
);
|
||||||
|
this.initiateSort(timeSystemKey);
|
||||||
|
}
|
||||||
|
},
|
||||||
setRowHeight(height) {
|
setRowHeight(height) {
|
||||||
this.rowHeight = height;
|
this.rowHeight = height;
|
||||||
this.setHeight();
|
this.setHeight();
|
||||||
|
@ -36,11 +36,11 @@
|
|||||||
|
|
||||||
<div class="c-table-indicator__counts">
|
<div class="c-table-indicator__counts">
|
||||||
<span
|
<span
|
||||||
:aria-label="totalRows + ' rows visible after any filtering'"
|
:aria-label="rowCountTitle"
|
||||||
:title="totalRows + ' rows visible after any filtering'"
|
:title="rowCountTitle"
|
||||||
class="c-table-indicator__elem c-table-indicator__row-count"
|
class="c-table-indicator__elem c-table-indicator__row-count"
|
||||||
>
|
>
|
||||||
{{ totalRows }} Rows
|
{{ rowCount }} Rows
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
@ -51,6 +51,10 @@
|
|||||||
>
|
>
|
||||||
{{ markedRows }} Marked
|
{{ markedRows }} Marked
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<button :title="telemetryModeButtonTitle" class="c-button" @click="toggleTelemetryMode">
|
||||||
|
{{ telemetryModeButtonLabel }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -74,8 +78,13 @@ export default {
|
|||||||
totalRows: {
|
totalRows: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
|
},
|
||||||
|
telemetryMode: {
|
||||||
|
type: String,
|
||||||
|
default: 'performance'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
emits: ['telemetry-mode-change'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
filterNames: [],
|
filterNames: [],
|
||||||
@ -93,6 +102,9 @@ export default {
|
|||||||
return !_.isEqual(filtersToCompare, _.omit(filters, [USE_GLOBAL]));
|
return !_.isEqual(filtersToCompare, _.omit(filters, [USE_GLOBAL]));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
isUnlimitedMode() {
|
||||||
|
return this.telemetryMode === 'unlimited';
|
||||||
|
},
|
||||||
label() {
|
label() {
|
||||||
if (this.hasMixedFilters) {
|
if (this.hasMixedFilters) {
|
||||||
return FILTER_INDICATOR_LABEL_MIXED;
|
return FILTER_INDICATOR_LABEL_MIXED;
|
||||||
@ -100,6 +112,22 @@ export default {
|
|||||||
return FILTER_INDICATOR_LABEL;
|
return FILTER_INDICATOR_LABEL;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
rowCount() {
|
||||||
|
return this.isUnlimitedMode ? this.totalRows : 'LATEST 50';
|
||||||
|
},
|
||||||
|
rowCountTitle() {
|
||||||
|
return this.isUnlimitedMode
|
||||||
|
? this.totalRows + ' rows visible after any filtering'
|
||||||
|
: 'performance mode limited to 50 rows';
|
||||||
|
},
|
||||||
|
telemetryModeButtonLabel() {
|
||||||
|
return this.isUnlimitedMode ? 'SHOW LATEST 50' : 'SHOW ALL';
|
||||||
|
},
|
||||||
|
telemetryModeButtonTitle() {
|
||||||
|
return this.isUnlimitedMode
|
||||||
|
? 'Change to Performance mode (latest 50 values)'
|
||||||
|
: 'Change to show all values';
|
||||||
|
},
|
||||||
title() {
|
title() {
|
||||||
if (this.hasMixedFilters) {
|
if (this.hasMixedFilters) {
|
||||||
return FILTER_INDICATOR_TITLE_MIXED;
|
return FILTER_INDICATOR_TITLE_MIXED;
|
||||||
@ -117,6 +145,9 @@ export default {
|
|||||||
this.table.configuration.off('change', this.handleConfigurationChanges);
|
this.table.configuration.off('change', this.handleConfigurationChanges);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
toggleTelemetryMode() {
|
||||||
|
this.$emit('telemetry-mode-change');
|
||||||
|
},
|
||||||
setFilterNames() {
|
setFilterNames() {
|
||||||
let names = [];
|
let names = [];
|
||||||
let composition = this.openmct.composition.get(this.table.configuration.domainObject);
|
let composition = this.openmct.composition.get(this.table.configuration.domainObject);
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
&__counts {
|
&__counts {
|
||||||
//background: rgba(deeppink, 0.1);
|
//background: rgba(deeppink, 0.1);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -123,15 +123,6 @@
|
|||||||
.is-editing .l-layout__frame & {
|
.is-editing .l-layout__frame & {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-selected {
|
|
||||||
background-color: $colorSelectedBg !important;
|
|
||||||
color: $colorSelectedFg !important;
|
|
||||||
td {
|
|
||||||
background: none !important;
|
|
||||||
color: inherit !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
@ -169,30 +160,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__footer {
|
&__footer {
|
||||||
$pt: 2px;
|
|
||||||
border-top: 1px solid $colorInteriorBorder;
|
|
||||||
margin-top: $interiorMargin;
|
margin-top: $interiorMargin;
|
||||||
padding: $pt 0;
|
margin-bottom: $interiorMarginSm;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: all 250ms;
|
|
||||||
|
|
||||||
&:not(.is-filtering) {
|
.c-frame & {
|
||||||
.c-frame & {
|
.c-button {
|
||||||
height: 0;
|
padding: 2px 5px;
|
||||||
padding: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.c-frame & {
|
|
||||||
// target .c-frame .c-telemetry-table {}
|
|
||||||
$pt: 2px;
|
|
||||||
&:hover {
|
|
||||||
.c-telemetry-table__footer:not(.is-filtering) {
|
|
||||||
height: $pt + 16px;
|
|
||||||
padding: initial;
|
|
||||||
visibility: visible;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -203,6 +177,18 @@ td {
|
|||||||
@include isLimit();
|
@include isLimit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.c-table tr {
|
||||||
|
&[s-selected],
|
||||||
|
&.is-selected {
|
||||||
|
background-color: $colorSelectedBg !important;
|
||||||
|
color: $colorSelectedFg !important;
|
||||||
|
td {
|
||||||
|
background: none !important;
|
||||||
|
color: inherit !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/******************************* SPECIFIC CASE WRAPPERS */
|
/******************************* SPECIFIC CASE WRAPPERS */
|
||||||
.is-editing {
|
.is-editing {
|
||||||
.c-telemetry-table__headers__labels {
|
.c-telemetry-table__headers__labels {
|
||||||
|
@ -19,16 +19,17 @@
|
|||||||
* 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 TableConfigurationViewProvider from './TableConfigurationViewProvider.js';
|
import TableConfigurationViewProvider from './TableConfigurationViewProvider.js';
|
||||||
import TelemetryTableType from './TelemetryTableType.js';
|
import getTelemetryTableType from './TelemetryTableType.js';
|
||||||
import TelemetryTableViewProvider from './TelemetryTableViewProvider.js';
|
import TelemetryTableViewProvider from './TelemetryTableViewProvider.js';
|
||||||
import TelemetryTableViewActions from './ViewActions.js';
|
import TelemetryTableViewActions from './ViewActions.js';
|
||||||
|
|
||||||
export default function plugin() {
|
export default function plugin(options) {
|
||||||
return function install(openmct) {
|
return function install(openmct) {
|
||||||
openmct.objectViews.addProvider(new TelemetryTableViewProvider(openmct));
|
openmct.objectViews.addProvider(new TelemetryTableViewProvider(openmct, options));
|
||||||
openmct.inspectorViews.addProvider(new TableConfigurationViewProvider(openmct));
|
openmct.inspectorViews.addProvider(new TableConfigurationViewProvider(openmct));
|
||||||
openmct.types.addType('table', TelemetryTableType);
|
openmct.types.addType('table', getTelemetryTableType(options));
|
||||||
openmct.composition.addPolicy((parent, child) => {
|
openmct.composition.addPolicy((parent, child) => {
|
||||||
if (parent.type === 'table') {
|
if (parent.type === 'table') {
|
||||||
return Object.prototype.hasOwnProperty.call(child, 'telemetry');
|
return Object.prototype.hasOwnProperty.call(child, 'telemetry');
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
:header-items="headerItems"
|
:header-items="headerItems"
|
||||||
:default-sort="defaultSort"
|
:default-sort="defaultSort"
|
||||||
class="sticky"
|
class="sticky"
|
||||||
|
@item-selection-changed="setSelectionForActivity"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -38,7 +39,7 @@ import { v4 as uuid } from 'uuid';
|
|||||||
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';
|
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';
|
||||||
import ListView from '../../ui/components/List/ListView.vue';
|
import ListView from '../../ui/components/List/ListView.vue';
|
||||||
import { getPreciseDuration } from '../../utils/duration.js';
|
import { getPreciseDuration } from '../../utils/duration.js';
|
||||||
import { getValidatedData, getValidatedGroups } from '../plan/util.js';
|
import { getFilteredValues, getValidatedData, getValidatedGroups } from '../plan/util.js';
|
||||||
import { SORT_ORDER_OPTIONS } from './constants.js';
|
import { SORT_ORDER_OPTIONS } from './constants.js';
|
||||||
|
|
||||||
const SCROLL_TIMEOUT = 10000;
|
const SCROLL_TIMEOUT = 10000;
|
||||||
@ -208,22 +209,22 @@ export default {
|
|||||||
this.setViewFromConfig(mutatedObject.configuration);
|
this.setViewFromConfig(mutatedObject.configuration);
|
||||||
},
|
},
|
||||||
setViewFromConfig(configuration) {
|
setViewFromConfig(configuration) {
|
||||||
|
this.filterValue = configuration.filter || '';
|
||||||
|
this.filterMetadataValue = configuration.filterMetadata || '';
|
||||||
if (this.isEditing) {
|
if (this.isEditing) {
|
||||||
this.filterValue = configuration.filter;
|
|
||||||
this.hideAll = false;
|
this.hideAll = false;
|
||||||
this.listActivities();
|
|
||||||
} else {
|
} else {
|
||||||
this.filterValue = configuration.filter;
|
|
||||||
this.setSort();
|
this.setSort();
|
||||||
this.listActivities();
|
|
||||||
}
|
}
|
||||||
|
this.listActivities();
|
||||||
},
|
},
|
||||||
updateTimestamp(timestamp) {
|
updateTimestamp(timestamp) {
|
||||||
//The clock never stops ticking
|
//The clock never stops ticking
|
||||||
this.updateTimeStampAndListActivities(timestamp);
|
this.updateTimeStampAndListActivities(timestamp);
|
||||||
},
|
},
|
||||||
setFixedTime() {
|
setFixedTime() {
|
||||||
this.filterValue = this.domainObject.configuration.filter;
|
this.filterValue = this.domainObject.configuration.filter || '';
|
||||||
|
this.filterMetadataValue = this.domainObject.configuration.filterMetadata || '';
|
||||||
this.isFixedTime = !this.timeContext.isRealTime();
|
this.isFixedTime = !this.timeContext.isRealTime();
|
||||||
if (this.isFixedTime) {
|
if (this.isFixedTime) {
|
||||||
this.hideAll = false;
|
this.hideAll = false;
|
||||||
@ -326,7 +327,21 @@ export default {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasFilterMatch = this.filterByName(activity.name);
|
let hasNameMatch = false;
|
||||||
|
let hasMetadataMatch = false;
|
||||||
|
if (this.filterValue || this.filterMetadataValue) {
|
||||||
|
if (this.filterValue) {
|
||||||
|
hasNameMatch = this.filterByName(activity.name);
|
||||||
|
}
|
||||||
|
if (this.filterMetadataValue) {
|
||||||
|
hasMetadataMatch = this.filterByMetadata(activity);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hasNameMatch = true;
|
||||||
|
hasMetadataMatch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFilterMatch = hasNameMatch || hasMetadataMatch;
|
||||||
if (hasFilterMatch === false || this.hideAll === true) {
|
if (hasFilterMatch === false || this.hideAll === true) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -354,6 +369,17 @@ export default {
|
|||||||
return regex.test(name.toLowerCase());
|
return regex.test(name.toLowerCase());
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
filterByMetadata(activity) {
|
||||||
|
const filters = this.filterMetadataValue.split(',');
|
||||||
|
|
||||||
|
return filters.some((search) => {
|
||||||
|
const normalized = search.trim().toLowerCase();
|
||||||
|
const regex = new RegExp(normalized);
|
||||||
|
const activityValues = getFilteredValues(activity);
|
||||||
|
|
||||||
|
return regex.test(activityValues.join().toLowerCase());
|
||||||
|
});
|
||||||
|
},
|
||||||
// Add activity classes, increase activity counts by type,
|
// Add activity classes, increase activity counts by type,
|
||||||
// set indices of the first occurrences of current and future activities - used for scrolling
|
// set indices of the first occurrences of current and future activities - used for scrolling
|
||||||
styleActivity(activity, index) {
|
styleActivity(activity, index) {
|
||||||
@ -516,6 +542,29 @@ export default {
|
|||||||
setEditState(isEditing) {
|
setEditState(isEditing) {
|
||||||
this.isEditing = isEditing;
|
this.isEditing = isEditing;
|
||||||
this.setViewFromConfig(this.domainObject.configuration);
|
this.setViewFromConfig(this.domainObject.configuration);
|
||||||
|
},
|
||||||
|
setSelectionForActivity(activity, element) {
|
||||||
|
const multiSelect = false;
|
||||||
|
|
||||||
|
this.openmct.selection.select(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
element: element,
|
||||||
|
context: {
|
||||||
|
type: 'activity',
|
||||||
|
activity: activity
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: this.openmct.layout.$refs.browseObject.$el,
|
||||||
|
context: {
|
||||||
|
item: this.domainObject,
|
||||||
|
supportsMultiSelect: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
multiSelect
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -22,20 +22,57 @@
|
|||||||
<template>
|
<template>
|
||||||
<li class="c-inspect-properties__row">
|
<li class="c-inspect-properties__row">
|
||||||
<div v-if="canEdit" class="c-inspect-properties__hint span-all">
|
<div v-if="canEdit" class="c-inspect-properties__hint span-all">
|
||||||
Filter this view by comma-separated keywords.
|
Filter this view by comma-separated keywords. Filtering uses an 'OR' method.
|
||||||
</div>
|
</div>
|
||||||
<div class="c-inspect-properties__label" title="Filter by keyword.">Filters</div>
|
<div class="c-inspect-properties__label" aria-label="Activity Names" title="Filter by keyword.">
|
||||||
<div v-if="canEdit" class="c-inspect-properties__value" :class="{ 'form-error': hasError }">
|
Activity Names
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="canEdit"
|
||||||
|
class="c-inspect-properties__value"
|
||||||
|
:class="{ 'form-error': hasFilterError }"
|
||||||
|
>
|
||||||
<textarea
|
<textarea
|
||||||
v-model="filterValue"
|
v-model="filterValue"
|
||||||
class="c-input--flex"
|
class="c-input--flex"
|
||||||
type="text"
|
type="text"
|
||||||
@keydown.enter.exact.stop="forceBlur($event)"
|
@keydown.enter.exact.stop="forceBlur($event)"
|
||||||
@keyup="updateForm($event, 'filter')"
|
@keyup="updateNameFilter($event, 'filter')"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="c-inspect-properties__value">
|
<div v-else class="c-inspect-properties__value">
|
||||||
{{ filterValue }}
|
<template v-if="filterValue.length > 0">
|
||||||
|
{{ filterValue }}
|
||||||
|
</template>
|
||||||
|
<template v-else> No filters applied </template>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="c-inspect-properties__row">
|
||||||
|
<div
|
||||||
|
class="c-inspect-properties__label"
|
||||||
|
aria-label="Meta-data Properties"
|
||||||
|
title="Filter by keyword."
|
||||||
|
>
|
||||||
|
Meta-data Properties
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="canEdit"
|
||||||
|
class="c-inspect-properties__value"
|
||||||
|
:class="{ 'form-error': hasMetadataFilterError }"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
v-model="filterMetadataValue"
|
||||||
|
class="c-input--flex"
|
||||||
|
type="text"
|
||||||
|
@keydown.enter.exact.stop="forceBlur($event)"
|
||||||
|
@keyup="updateMetadataFilter($event, 'filterMetadata')"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div v-else class="c-inspect-properties__value">
|
||||||
|
<template v-if="filterMetadataValue.length > 0">
|
||||||
|
{{ filterMetadataValue }}
|
||||||
|
</template>
|
||||||
|
<template v-else> No filters applied </template>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
@ -48,7 +85,9 @@ export default {
|
|||||||
return {
|
return {
|
||||||
isEditing: this.openmct.editor.isEditing(),
|
isEditing: this.openmct.editor.isEditing(),
|
||||||
filterValue: this.domainObject.configuration.filter,
|
filterValue: this.domainObject.configuration.filter,
|
||||||
hasError: false
|
filterMetadataValue: this.domainObject.configuration.filterMetadata,
|
||||||
|
hasFilterError: false,
|
||||||
|
hasMetadataFilterError: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -65,37 +104,55 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
setEditState(isEditing) {
|
setEditState(isEditing) {
|
||||||
this.isEditing = isEditing;
|
this.isEditing = isEditing;
|
||||||
if (!this.isEditing && this.hasError) {
|
if (!this.isEditing) {
|
||||||
this.filterValue = this.domainObject.configuration.filter;
|
if (this.hasFilterError) {
|
||||||
this.hasError = false;
|
this.filterValue = this.domainObject.configuration.filter;
|
||||||
|
}
|
||||||
|
if (this.hasMetadataFilterError) {
|
||||||
|
this.filterMetadataValue = this.domainObject.configuration.filterMetadata;
|
||||||
|
}
|
||||||
|
this.hasFilterError = false;
|
||||||
|
this.hasMetadataFilterError = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
forceBlur(event) {
|
forceBlur(event) {
|
||||||
event.target.blur();
|
event.target.blur();
|
||||||
},
|
},
|
||||||
updateForm(event, property) {
|
updateNameFilter(event, property) {
|
||||||
if (!this.isValid()) {
|
if (!this.isValid(this.filterValue)) {
|
||||||
this.hasError = true;
|
this.hasFilterError = true;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.hasFilterError = false;
|
||||||
this.hasError = false;
|
|
||||||
|
|
||||||
this.$emit('updated', {
|
this.$emit('updated', {
|
||||||
property,
|
property,
|
||||||
value: this.filterValue.replace(/,(\s)*$/, '')
|
value: this.filterValue.replace(/,(\s)*$/, '')
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
isValid() {
|
updateMetadataFilter(event, property) {
|
||||||
|
if (!this.isValid(this.filterMetadataValue)) {
|
||||||
|
this.hasMetadataFilterError = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.hasMetadataFilterError = false;
|
||||||
|
|
||||||
|
this.$emit('updated', {
|
||||||
|
property,
|
||||||
|
value: this.filterMetadataValue.replace(/,(\s)*$/, '')
|
||||||
|
});
|
||||||
|
},
|
||||||
|
isValid(value) {
|
||||||
// Test for any word character, any whitespace character or comma
|
// Test for any word character, any whitespace character or comma
|
||||||
if (this.filterValue === '') {
|
if (value === '') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const regex = new RegExp(/^([a-zA-Z0-9_\-\s,])+$/g);
|
const regex = new RegExp(/^([a-zA-Z0-9_\-\s,])+$/g);
|
||||||
|
|
||||||
return regex.test(this.filterValue);
|
return regex.test(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -28,7 +28,7 @@ import TimelistPropertiesView from './TimelistPropertiesView.vue';
|
|||||||
export default function TimeListInspectorViewProvider(openmct) {
|
export default function TimeListInspectorViewProvider(openmct) {
|
||||||
return {
|
return {
|
||||||
key: 'timelist-inspector',
|
key: 'timelist-inspector',
|
||||||
name: 'Timelist Inspector View',
|
name: 'Config',
|
||||||
canView: function (selection) {
|
canView: function (selection) {
|
||||||
if (selection.length === 0 || selection[0].length === 0) {
|
if (selection.length === 0 || selection[0].length === 0) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -38,16 +38,10 @@ export default function () {
|
|||||||
initialize: function (domainObject) {
|
initialize: function (domainObject) {
|
||||||
domainObject.configuration = {
|
domainObject.configuration = {
|
||||||
sortOrderIndex: 0,
|
sortOrderIndex: 0,
|
||||||
futureEventsIndex: 1,
|
|
||||||
futureEventsDurationIndex: 0,
|
|
||||||
futureEventsDuration: 20,
|
|
||||||
currentEventsIndex: 1,
|
currentEventsIndex: 1,
|
||||||
currentEventsDurationIndex: 0,
|
filter: '',
|
||||||
currentEventsDuration: 20,
|
filterMetadata: '',
|
||||||
pastEventsIndex: 1,
|
isCompact: false
|
||||||
pastEventsDurationIndex: 0,
|
|
||||||
pastEventsDuration: 20,
|
|
||||||
filter: ''
|
|
||||||
};
|
};
|
||||||
domainObject.composition = [];
|
domainObject.composition = [];
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,10 @@ describe('the plugin', function () {
|
|||||||
end: twoHoursFuture,
|
end: twoHoursFuture,
|
||||||
type: 'TEST-GROUP',
|
type: 'TEST-GROUP',
|
||||||
color: 'fuchsia',
|
color: 'fuchsia',
|
||||||
textColor: 'black'
|
textColor: 'black',
|
||||||
|
properties: {
|
||||||
|
location: 'garden'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Sed ut perspiciatis two',
|
name: 'Sed ut perspiciatis two',
|
||||||
@ -79,7 +82,10 @@ describe('the plugin', function () {
|
|||||||
end: threeHoursFuture,
|
end: threeHoursFuture,
|
||||||
type: 'TEST-GROUP',
|
type: 'TEST-GROUP',
|
||||||
color: 'fuchsia',
|
color: 'fuchsia',
|
||||||
textColor: 'black'
|
textColor: 'black',
|
||||||
|
properties: {
|
||||||
|
location: 'hallway'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
@ -305,7 +311,7 @@ describe('the plugin', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('filters', () => {
|
describe('filters by name', () => {
|
||||||
let timelistDomainObject;
|
let timelistDomainObject;
|
||||||
let timelistView;
|
let timelistView;
|
||||||
|
|
||||||
@ -379,6 +385,129 @@ describe('the plugin', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('filters by metadata', () => {
|
||||||
|
let timelistDomainObject;
|
||||||
|
let timelistView;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
timelistDomainObject = {
|
||||||
|
identifier: {
|
||||||
|
key: 'test-object',
|
||||||
|
namespace: ''
|
||||||
|
},
|
||||||
|
type: TIMELIST_TYPE,
|
||||||
|
id: 'test-object',
|
||||||
|
configuration: {
|
||||||
|
sortOrderIndex: 2,
|
||||||
|
futureEventsIndex: 1,
|
||||||
|
futureEventsDurationIndex: 0,
|
||||||
|
futureEventsDuration: 0,
|
||||||
|
currentEventsIndex: 1,
|
||||||
|
currentEventsDurationIndex: 0,
|
||||||
|
currentEventsDuration: 0,
|
||||||
|
pastEventsIndex: 1,
|
||||||
|
pastEventsDurationIndex: 0,
|
||||||
|
pastEventsDuration: 0,
|
||||||
|
filter: '',
|
||||||
|
filterMetadata: 'hallway,garden'
|
||||||
|
},
|
||||||
|
composition: [
|
||||||
|
{
|
||||||
|
identifier: {
|
||||||
|
key: 'test-plan-object',
|
||||||
|
namespace: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
openmct.router.path = [timelistDomainObject];
|
||||||
|
|
||||||
|
const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]);
|
||||||
|
timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view');
|
||||||
|
let view = timelistView.view(timelistDomainObject, [timelistDomainObject]);
|
||||||
|
view.show(child, true);
|
||||||
|
|
||||||
|
return nextTick();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('activities and sorts them correctly', () => {
|
||||||
|
mockComposition.emit('add', planObject);
|
||||||
|
|
||||||
|
return nextTick(() => {
|
||||||
|
const timeFormat = openmct.time.timeSystem().timeFormat;
|
||||||
|
const timeFormatter = openmct.telemetry.getValueFormatter({ format: timeFormat }).formatter;
|
||||||
|
|
||||||
|
const items = element.querySelectorAll(LIST_ITEM_CLASS);
|
||||||
|
expect(items.length).toEqual(2);
|
||||||
|
|
||||||
|
const itemValues = items[1].querySelectorAll(LIST_ITEM_VALUE_CLASS);
|
||||||
|
expect(itemValues[0].innerHTML.trim()).toEqual(timeFormatter.format(now, TIME_FORMAT));
|
||||||
|
expect(itemValues[1].innerHTML.trim()).toEqual(
|
||||||
|
timeFormatter.format(threeHoursFuture, TIME_FORMAT)
|
||||||
|
);
|
||||||
|
expect(itemValues[3].innerHTML.trim()).toEqual('Sed ut perspiciatis two');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filters by name and metadata', () => {
|
||||||
|
let timelistDomainObject;
|
||||||
|
let timelistView;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
timelistDomainObject = {
|
||||||
|
identifier: {
|
||||||
|
key: 'test-object',
|
||||||
|
namespace: ''
|
||||||
|
},
|
||||||
|
type: TIMELIST_TYPE,
|
||||||
|
id: 'test-object',
|
||||||
|
configuration: {
|
||||||
|
sortOrderIndex: 2,
|
||||||
|
currentEventsIndex: 1,
|
||||||
|
filter: 'two',
|
||||||
|
filterMetadata: 'garden'
|
||||||
|
},
|
||||||
|
composition: [
|
||||||
|
{
|
||||||
|
identifier: {
|
||||||
|
key: 'test-plan-object',
|
||||||
|
namespace: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
openmct.router.path = [timelistDomainObject];
|
||||||
|
|
||||||
|
const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]);
|
||||||
|
timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view');
|
||||||
|
let view = timelistView.view(timelistDomainObject, [timelistDomainObject]);
|
||||||
|
view.show(child, true);
|
||||||
|
|
||||||
|
return nextTick();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('activities and sorts them correctly', () => {
|
||||||
|
mockComposition.emit('add', planObject);
|
||||||
|
|
||||||
|
return nextTick(() => {
|
||||||
|
const timeFormat = openmct.time.timeSystem().timeFormat;
|
||||||
|
const timeFormatter = openmct.telemetry.getValueFormatter({ format: timeFormat }).formatter;
|
||||||
|
|
||||||
|
const items = element.querySelectorAll(LIST_ITEM_CLASS);
|
||||||
|
expect(items.length).toEqual(2);
|
||||||
|
|
||||||
|
const itemValues = items[0].querySelectorAll(LIST_ITEM_VALUE_CLASS);
|
||||||
|
expect(itemValues[0].innerHTML.trim()).toEqual(timeFormatter.format(now, TIME_FORMAT));
|
||||||
|
expect(itemValues[1].innerHTML.trim()).toEqual(
|
||||||
|
timeFormatter.format(twoHoursFuture, TIME_FORMAT)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('time filtering - past', () => {
|
describe('time filtering - past', () => {
|
||||||
let timelistDomainObject;
|
let timelistDomainObject;
|
||||||
let timelistView;
|
let timelistView;
|
||||||
|
@ -361,11 +361,12 @@ body.desktop .has-local-controls {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[aria-disabled = 'true'],
|
||||||
*[disabled],
|
*[disabled],
|
||||||
.disabled {
|
.disabled {
|
||||||
opacity: $controlDisabledOpacity;
|
opacity: $controlDisabledOpacity;
|
||||||
|
cursor: not-allowed !important;
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
cursor: default !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/******************************************************** RESPONSIVE CONTAINERS */
|
/******************************************************** RESPONSIVE CONTAINERS */
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
:key="item.key"
|
:key="item.key"
|
||||||
:item="item"
|
:item="item"
|
||||||
:item-properties="itemProperties"
|
:item-properties="itemProperties"
|
||||||
|
@click.stop="itemSelected(item, $event)"
|
||||||
/>
|
/>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -86,6 +87,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
emits: ['item-selection-changed'],
|
||||||
data() {
|
data() {
|
||||||
let sortBy = this.defaultSort.property;
|
let sortBy = this.defaultSort.property;
|
||||||
let ascending = this.defaultSort.defaultDirection;
|
let ascending = this.defaultSort.defaultDirection;
|
||||||
@ -156,6 +158,9 @@ export default {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
itemSelected(item, event) {
|
||||||
|
this.$emit('item-selection-changed', item, event.currentTarget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -33,6 +33,7 @@ import StyleRuleManager from '@/plugins/condition/StyleRuleManager';
|
|||||||
import { STYLE_CONSTANTS } from '@/plugins/condition/utils/constants';
|
import { STYLE_CONSTANTS } from '@/plugins/condition/utils/constants';
|
||||||
import stalenessMixin from '@/ui/mixins/staleness-mixin';
|
import stalenessMixin from '@/ui/mixins/staleness-mixin';
|
||||||
|
|
||||||
|
import objectUtils from '../../api/objects/object-utils.js';
|
||||||
import VisibilityObserver from '../../utils/visibility/VisibilityObserver.js';
|
import VisibilityObserver from '../../utils/visibility/VisibilityObserver.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -130,7 +131,10 @@ export default {
|
|||||||
this.debounceUpdateView = _.debounce(this.updateView, 10);
|
this.debounceUpdateView = _.debounce(this.updateView, 10);
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.visibilityObserver = new VisibilityObserver(this.$refs.objectViewWrapper);
|
this.visibilityObserver = new VisibilityObserver(
|
||||||
|
this.$refs.objectViewWrapper,
|
||||||
|
this.openmct.element
|
||||||
|
);
|
||||||
this.updateView();
|
this.updateView();
|
||||||
this.$refs.objectViewWrapper.addEventListener('dragover', this.onDragOver, {
|
this.$refs.objectViewWrapper.addEventListener('dragover', this.onDragOver, {
|
||||||
capture: true
|
capture: true
|
||||||
@ -184,6 +188,7 @@ export default {
|
|||||||
this.triggerUnsubscribeFromStaleness(this.domainObject);
|
this.triggerUnsubscribeFromStaleness(this.domainObject);
|
||||||
|
|
||||||
this.openmct.objectViews.off('clearData', this.clearData);
|
this.openmct.objectViews.off('clearData', this.clearData);
|
||||||
|
this.openmct.objectViews.off('reload', this.reload);
|
||||||
if (this.contextActionEvent) {
|
if (this.contextActionEvent) {
|
||||||
this.openmct.objectViews.off(this.contextActionEvent, this.performContextAction);
|
this.openmct.objectViews.off(this.contextActionEvent, this.performContextAction);
|
||||||
}
|
}
|
||||||
@ -218,6 +223,13 @@ export default {
|
|||||||
this.clear();
|
this.clear();
|
||||||
this.updateView(true);
|
this.updateView(true);
|
||||||
},
|
},
|
||||||
|
reload(domainObjectToReload) {
|
||||||
|
if (objectUtils.equals(domainObjectToReload, this.domainObject)) {
|
||||||
|
this.updateView(true);
|
||||||
|
this.initObjectStyles();
|
||||||
|
this.triggerStalenessSubscribe(this.domainObject);
|
||||||
|
}
|
||||||
|
},
|
||||||
triggerStalenessSubscribe(object) {
|
triggerStalenessSubscribe(object) {
|
||||||
if (this.openmct.telemetry.isTelemetryObject(object)) {
|
if (this.openmct.telemetry.isTelemetryObject(object)) {
|
||||||
this.subscribeToStaleness(object);
|
this.subscribeToStaleness(object);
|
||||||
@ -316,6 +328,7 @@ export default {
|
|||||||
this.domainObject.identifier
|
this.domainObject.identifier
|
||||||
)}`;
|
)}`;
|
||||||
this.openmct.objectViews.on('clearData', this.clearData);
|
this.openmct.objectViews.on('clearData', this.clearData);
|
||||||
|
this.openmct.objectViews.on('reload', this.reload);
|
||||||
this.openmct.objectViews.on(this.contextActionEvent, this.performContextAction);
|
this.openmct.objectViews.on(this.contextActionEvent, this.performContextAction);
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
<div ref="createButton" class="c-create-button--w">
|
<div ref="createButton" class="c-create-button--w">
|
||||||
<button
|
<button
|
||||||
class="c-create-button c-button--menu c-button--major icon-plus"
|
class="c-create-button c-button--menu c-button--major icon-plus"
|
||||||
|
:aria-disabled="isEditing"
|
||||||
@click.prevent.stop="showCreateMenu"
|
@click.prevent.stop="showCreateMenu"
|
||||||
>
|
>
|
||||||
<span class="c-button__label">Create</span>
|
<span class="c-button__label">Create</span>
|
||||||
@ -38,6 +39,7 @@ export default {
|
|||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
menuItems: {},
|
menuItems: {},
|
||||||
|
isEditing: this.openmct.editor.isEditing(),
|
||||||
selectedMenuItem: {},
|
selectedMenuItem: {},
|
||||||
opened: false
|
opened: false
|
||||||
};
|
};
|
||||||
@ -57,6 +59,12 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this.openmct.editor.on('isEditing', this.toggleEdit);
|
||||||
|
},
|
||||||
|
unmounted() {
|
||||||
|
this.openmct.editor.off('isEditing', this.toggleEdit);
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getItems() {
|
getItems() {
|
||||||
let keys = this.openmct.types.listKeys();
|
let keys = this.openmct.types.listKeys();
|
||||||
@ -89,6 +97,9 @@ export default {
|
|||||||
|
|
||||||
this.openmct.menus.showSuperMenu(x, y, this.sortedItems, menuOptions);
|
this.openmct.menus.showSuperMenu(x, y, this.sortedItems, menuOptions);
|
||||||
},
|
},
|
||||||
|
toggleEdit(isEditing) {
|
||||||
|
this.isEditing = isEditing;
|
||||||
|
},
|
||||||
create(key) {
|
create(key) {
|
||||||
const createAction = new CreateAction(this.openmct, key, this.openmct.router.path[0]);
|
const createAction = new CreateAction(this.openmct, key, this.openmct.router.path[0]);
|
||||||
|
|
||||||
|
@ -4,10 +4,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.c-create-button {
|
.c-create-button {
|
||||||
.is-editing & {
|
|
||||||
@include disabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
.c-button__label {
|
.c-button__label {
|
||||||
text-transform: $createBtnTextTransform;
|
text-transform: $createBtnTextTransform;
|
||||||
}
|
}
|
||||||
|
@ -203,8 +203,6 @@
|
|||||||
|
|
||||||
.is-editing .is-navigated-object {
|
.is-editing .is-navigated-object {
|
||||||
a[class*='__item__label'] {
|
a[class*='__item__label'] {
|
||||||
opacity: 0.4;
|
|
||||||
|
|
||||||
[class*='__name'] {
|
[class*='__name'] {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
@ -108,6 +108,11 @@ export default {
|
|||||||
this.addExistingViewBackToParent();
|
this.addExistingViewBackToParent();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updated() {
|
||||||
|
// FIXME: fixes a problem where the some context menu items are not available when in Preview Mode
|
||||||
|
// see https://github.com/nasa/openmct/issues/7158
|
||||||
|
this.getActionsCollection(this.view);
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
clear() {
|
clear() {
|
||||||
if (this.view) {
|
if (this.view) {
|
||||||
|
@ -32,17 +32,22 @@ export default class VisibilityObserver {
|
|||||||
* Constructs a VisibilityObserver instance to manage visibility-based requestAnimationFrame calls.
|
* Constructs a VisibilityObserver instance to manage visibility-based requestAnimationFrame calls.
|
||||||
*
|
*
|
||||||
* @param {HTMLElement} element - The DOM element to observe for visibility changes.
|
* @param {HTMLElement} element - The DOM element to observe for visibility changes.
|
||||||
|
* @param {HTMLElement} rootContainer - The DOM element that is the root of the viewport.
|
||||||
* @throws {Error} If element is not provided.
|
* @throws {Error} If element is not provided.
|
||||||
*/
|
*/
|
||||||
constructor(element) {
|
constructor(element, rootContainer) {
|
||||||
if (!element) {
|
if (!element || !rootContainer) {
|
||||||
throw new Error(`VisibilityObserver must be created with an element`);
|
throw new Error(`VisibilityObserver must be created with an element and a rootContainer.`);
|
||||||
}
|
}
|
||||||
this.#element = element;
|
this.#element = element;
|
||||||
this.isIntersecting = true;
|
this.isIntersecting = true;
|
||||||
this.calledOnce = false;
|
this.calledOnce = false;
|
||||||
|
const options = {
|
||||||
this.#observer = new IntersectionObserver(this.#observerCallback);
|
root: rootContainer,
|
||||||
|
rootMargin: '0px',
|
||||||
|
threshold: 1.0
|
||||||
|
};
|
||||||
|
this.#observer = new IntersectionObserver(this.#observerCallback, options);
|
||||||
this.lastUnfiredFunc = null;
|
this.lastUnfiredFunc = null;
|
||||||
this.renderWhenVisible = this.renderWhenVisible.bind(this);
|
this.renderWhenVisible = this.renderWhenVisible.bind(this);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user