Compare commits
138 Commits
release/2.
...
tc-e2e-tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7590da46ac | ||
|
|
7645dc910d | ||
|
|
78c0f8308d | ||
|
|
ae364e2272 | ||
|
|
5ce77544d4 | ||
|
|
5de350e371 | ||
|
|
da1ae385e3 | ||
|
|
249f8201c1 | ||
|
|
7fa554b94f | ||
|
|
d9c8a78fb3 | ||
|
|
85974fc5f1 | ||
|
|
761d4ce7e4 | ||
|
|
df6a3e40f2 | ||
|
|
1eb28102be | ||
|
|
f898aa356e | ||
|
|
19d5bb0fe5 | ||
|
|
c1ee1e0a70 | ||
|
|
6adc14e3ef | ||
|
|
ed8b75d7ad | ||
|
|
5b1298f221 | ||
|
|
0f2b73e35c | ||
|
|
60c260d28a | ||
|
|
0a2de2ac8c | ||
|
|
662d14354c | ||
|
|
db33539185 | ||
|
|
e386036dbf | ||
|
|
47dce7bab3 | ||
|
|
6e79e5e2b0 | ||
|
|
32529ff6b2 | ||
|
|
a17566c13d | ||
|
|
92329b3d8e | ||
|
|
cde8fbbb0d | ||
|
|
6d1b2ef6fb | ||
|
|
1de96e60a6 | ||
|
|
ca0d411faf | ||
|
|
94043843ef | ||
|
|
3cb2fefc21 | ||
|
|
4d28763ef9 | ||
|
|
4e788f783b | ||
|
|
40e53e0f08 | ||
|
|
43602d92cc | ||
|
|
795d7a7ec7 | ||
|
|
5031010a00 | ||
|
|
e8926da912 | ||
|
|
b5a8a2b3e9 | ||
|
|
be793dc183 | ||
|
|
bef67c882d | ||
|
|
39a3b4939d | ||
|
|
f90f6d6773 | ||
|
|
dac4c52cdf | ||
|
|
ee8bc16b3d | ||
|
|
ac22bebe76 | ||
|
|
045b6fd369 | ||
|
|
22fb33b330 | ||
|
|
2c7b27d0c8 | ||
|
|
1d72a15328 | ||
|
|
d08ea62932 | ||
|
|
b8ab9a6fad | ||
|
|
293f25df19 | ||
|
|
fa5de7c7cc | ||
|
|
0947794422 | ||
|
|
80cae8434b | ||
|
|
c8595eef94 | ||
|
|
6edbb67cce | ||
|
|
b909207755 | ||
|
|
c55f27e746 | ||
|
|
2762f5d6fd | ||
|
|
9c22bcfb3e | ||
|
|
959b4ee6e3 | ||
|
|
e9f806e3bc | ||
|
|
02c30c5953 | ||
|
|
903540dcba | ||
|
|
7ed5b42b0e | ||
|
|
e9f479391d | ||
|
|
240322841a | ||
|
|
e16f4fc3d5 | ||
|
|
1e97049b7f | ||
|
|
956c9a524f | ||
|
|
7169ca9812 | ||
|
|
94c1cc6429 | ||
|
|
fc453eea9a | ||
|
|
fdf405ccbe | ||
|
|
62d14ed0af | ||
|
|
6a45e2e3da | ||
|
|
3923d02fea | ||
|
|
4a3747596c | ||
|
|
2585c80807 | ||
|
|
55342a0258 | ||
|
|
0e9542c900 | ||
|
|
546662abed | ||
|
|
5aca274bb8 | ||
|
|
a24df424fe | ||
|
|
d464ded633 | ||
|
|
727eaa8e4d | ||
|
|
f1a89e0dc3 | ||
|
|
dbeb7c4573 | ||
|
|
e4497f55da | ||
|
|
b4454ad0b5 | ||
|
|
05c0042947 | ||
|
|
7578d33a78 | ||
|
|
668e88a805 | ||
|
|
d847b2648c | ||
|
|
2179bc4e56 | ||
|
|
6f5e3d2469 | ||
|
|
2bbe713949 | ||
|
|
84882be936 | ||
|
|
f1398cf746 | ||
|
|
3c86c43ba7 | ||
|
|
36a0fe91b3 | ||
|
|
db04cf1c8f | ||
|
|
9d256ac18e | ||
|
|
995a10b0d5 | ||
|
|
baf41fb8f0 | ||
|
|
170686a7c1 | ||
|
|
485ff91aa1 | ||
|
|
b7445940ef | ||
|
|
e743e09b7b | ||
|
|
5db595dca1 | ||
|
|
65801de534 | ||
|
|
7f974416fb | ||
|
|
7247a42cb2 | ||
|
|
5fa48983de | ||
|
|
26c592f8ec | ||
|
|
bc20d89e98 | ||
|
|
3e7e9d5eab | ||
|
|
c61fa04bc9 | ||
|
|
e86c74107b | ||
|
|
3b0e05ed14 | ||
|
|
ff7f55574d | ||
|
|
58f869b21b | ||
|
|
834a19f996 | ||
|
|
1d7cd64652 | ||
|
|
68ed7bf0e5 | ||
|
|
4b39ef3235 | ||
|
|
b685b9582e | ||
|
|
d8ac209a96 | ||
|
|
f254d4f078 | ||
|
|
c75a82dca5 |
@@ -242,10 +242,6 @@ workflows:
|
||||
name: e2e-stable
|
||||
node-version: lts/hydrogen
|
||||
suite: stable
|
||||
- perf-test:
|
||||
node-version: lts/hydrogen
|
||||
- visual-test:
|
||||
node-version: lts/hydrogen
|
||||
|
||||
the-nightly: #These jobs do not run on PRs, but against master at night
|
||||
jobs:
|
||||
|
||||
38
.github/workflows/e2e-couchdb.yml
vendored
38
.github/workflows/e2e-couchdb.yml
vendored
@@ -1,21 +1,43 @@
|
||||
name: 'e2e-couchdb'
|
||||
on:
|
||||
push:
|
||||
branches: master
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- opened
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
jobs:
|
||||
e2e-couchdb:
|
||||
if: ${{ github.event.label.name == 'pr:e2e:couchdb' }} || ${{ github.event.action == 'opened' }}
|
||||
if: contains(github.event.pull_request.labels.*.name, 'pr:e2e:couchdb') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.action == 'opened'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 'lts/gallium'
|
||||
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: npm install --cache ~/.npm --prefer-offline --no-audit --progress=false
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- run: npx playwright@1.32.3 install
|
||||
- run: npm install
|
||||
|
||||
- name: Start CouchDB Docker Container and Init with Setup Scripts
|
||||
run: |
|
||||
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
|
||||
@@ -23,26 +45,32 @@ jobs:
|
||||
sleep 3
|
||||
bash src/plugins/persistence/couch/setup-couchdb.sh
|
||||
bash src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
|
||||
|
||||
- name: Run CouchDB Tests and publish to deploysentinel
|
||||
env:
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha }}
|
||||
run: npm run test:e2e:couchdb
|
||||
|
||||
- name: Publish Results to Codecov.io
|
||||
env:
|
||||
SUPER_SECRET: ${{ secrets.CODECOV_TOKEN }}
|
||||
run: npm run cov:e2e:full:publish
|
||||
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: test-results
|
||||
|
||||
- name: Archive html test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: html-test-results
|
||||
|
||||
- name: Remove pr:e2e:couchdb label (if present)
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'pr:e2e:couchdb') }}
|
||||
if: always()
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
@@ -56,5 +84,5 @@ jobs:
|
||||
name: labelToRemove
|
||||
});
|
||||
} catch (error) {
|
||||
core.warning(`Failed to remove 'pr:e2e:couchdb' label: ${error.message}`);
|
||||
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
|
||||
}
|
||||
|
||||
59
.github/workflows/e2e-pr.yml
vendored
59
.github/workflows/e2e-pr.yml
vendored
@@ -1,37 +1,41 @@
|
||||
name: 'e2e-pr'
|
||||
on:
|
||||
push:
|
||||
branches: master
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- opened
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
jobs:
|
||||
e2e-full:
|
||||
if: ${{ github.event.label.name == 'pr:e2e' }}
|
||||
if: contains(github.event.pull_request.labels.*.name, 'pr:e2e') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
steps:
|
||||
- name: Trigger Success
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: "nasa",
|
||||
repo: "openmct",
|
||||
body: 'Started e2e Run. Follow along: https://github.com/nasa/openmct/actions/runs/' + context.runId
|
||||
})
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
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.32.3 install
|
||||
- run: npx playwright install chrome-beta
|
||||
- run: npm install
|
||||
- run: npm install --cache ~/.npm --prefer-offline --no-audit --progress=false
|
||||
- run: npm run test:e2e:full -- --max-failures=40
|
||||
- run: npm run cov:e2e:report || true
|
||||
- shell: bash
|
||||
@@ -44,30 +48,9 @@ jobs:
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: test-results
|
||||
- name: Test success
|
||||
if: ${{ success() }}
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: "nasa",
|
||||
repo: "openmct",
|
||||
body: 'Success ✅ ! Build artifacts are here: https://github.com/nasa/openmct/actions/runs/' + context.runId
|
||||
})
|
||||
- name: Test failure
|
||||
if: ${{ failure() }}
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: "nasa",
|
||||
repo: "openmct",
|
||||
body: 'Failure ❌ ! Build artifacts are here: https://github.com/nasa/openmct/actions/runs/' + context.runId
|
||||
})
|
||||
|
||||
- name: Remove pr:e2e label (if present)
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'pr:e2e') }}
|
||||
if: always()
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
@@ -81,5 +64,5 @@ jobs:
|
||||
name: labelToRemove
|
||||
});
|
||||
} catch (error) {
|
||||
core.warning(`Failed to remove 'pr:e2e' label: ${error.message}`);
|
||||
}
|
||||
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
|
||||
}
|
||||
4
.github/workflows/npm-prerelease.yml
vendored
4
.github/workflows/npm-prerelease.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: lts/hydrogen
|
||||
- run: npm install
|
||||
- run: |
|
||||
echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" >> ~/.npmrc
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: lts/hydrogen
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- run: npm install
|
||||
- run: npm publish --access=public --tag unstable
|
||||
|
||||
51
.github/workflows/pr-platform.yml
vendored
51
.github/workflows/pr-platform.yml
vendored
@@ -1,13 +1,19 @@
|
||||
name: 'pr-platform'
|
||||
on:
|
||||
push:
|
||||
branches: master
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
|
||||
types:
|
||||
- labeled
|
||||
- opened
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
jobs:
|
||||
e2e-full:
|
||||
if: ${{ github.event.label.name == 'pr:platform' }}
|
||||
pr-platform:
|
||||
if: contains(github.event.pull_request.labels.*.name, 'pr:platform') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -16,18 +22,49 @@ jobs:
|
||||
- macos-latest
|
||||
- windows-latest
|
||||
node_version:
|
||||
- 16
|
||||
- 18
|
||||
- lts/gallium
|
||||
- lts/hydrogen
|
||||
architecture:
|
||||
- x64
|
||||
|
||||
name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node_version }}
|
||||
architecture: ${{ matrix.architecture }}
|
||||
- run: npm install
|
||||
|
||||
- name: Cache NPM dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-${{ matrix.node_version }}-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ matrix.node_version }}-
|
||||
|
||||
- run: npm install --cache ~/.npm --prefer-offline --no-audit --progress=false
|
||||
|
||||
- run: npm test
|
||||
|
||||
- run: npm run lint -- --quiet
|
||||
|
||||
- name: Remove pr:platform label (if present)
|
||||
if: always()
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo, number } = context.issue;
|
||||
const labelToRemove = 'pr:platform';
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: number,
|
||||
name: labelToRemove
|
||||
});
|
||||
} catch (error) {
|
||||
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
"printWidth": 100,
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
|
||||
6
API.md
6
API.md
@@ -2,7 +2,7 @@
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
**Table of Contents**
|
||||
|
||||
- [Building Applications With Open MCT](#developing-applications-with-open-mct)
|
||||
- [Developing Applications With Open MCT](#developing-applications-with-open-mct)
|
||||
- [Scope and purpose of this document](#scope-and-purpose-of-this-document)
|
||||
- [Building From Source](#building-from-source)
|
||||
- [Starting an Open MCT application](#starting-an-open-mct-application)
|
||||
@@ -26,7 +26,7 @@
|
||||
- [Value Hints](#value-hints)
|
||||
- [The Time Conductor and Telemetry](#the-time-conductor-and-telemetry)
|
||||
- [Telemetry Providers](#telemetry-providers)
|
||||
- [Telemetry Requests and Responses.](#telemetry-requests-and-responses)
|
||||
- [Telemetry Requests and Responses](#telemetry-requests-and-responses)
|
||||
- [Request Strategies **draft**](#request-strategies-draft)
|
||||
- [`latest` request strategy](#latest-request-strategy)
|
||||
- [`minmax` request strategy](#minmax-request-strategy)
|
||||
@@ -873,6 +873,8 @@ function without any arguments.
|
||||
|
||||
#### Stopping an active clock
|
||||
|
||||
_As of July 2023, this method will be deprecated. Open MCT will always have a ticking clock._
|
||||
|
||||
The `stopClock` method can be used to stop an active clock, and to clear it. It
|
||||
will stop the clock from ticking, and set the active clock to `undefined`.
|
||||
|
||||
|
||||
@@ -401,14 +401,7 @@ async function setEndOffset(page, offset) {
|
||||
async function selectInspectorTab(page, name) {
|
||||
const inspectorTabs = page.getByRole('tablist');
|
||||
const inspectorTab = inspectorTabs.getByTitle(name);
|
||||
const inspectorTabClass = await inspectorTab.getAttribute('class');
|
||||
const isSelectedInspectorTab = inspectorTabClass.includes('is-current');
|
||||
|
||||
// do not click a tab that is already selected or it will timeout your test
|
||||
// do to a { pointer-events: none; } on selected tabs
|
||||
if (!isSelectedInspectorTab) {
|
||||
await inspectorTab.click();
|
||||
}
|
||||
await inspectorTab.click();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
*/
|
||||
|
||||
const base = require('@playwright/test');
|
||||
const { expect } = base;
|
||||
const { expect, request } = base;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { v4: uuid } = require('uuid');
|
||||
@@ -179,4 +179,5 @@ exports.test = base.test.extend({
|
||||
});
|
||||
|
||||
exports.expect = expect;
|
||||
exports.request = request;
|
||||
exports.waitForAnimations = waitForAnimations;
|
||||
|
||||
@@ -77,7 +77,6 @@ const config = {
|
||||
}
|
||||
],
|
||||
['junit', { outputFile: '../test-results/results.xml' }],
|
||||
['github'],
|
||||
['@deploysentinel/playwright']
|
||||
]
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
* and appActions. These fixtures should be generalized across all plugins.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('./baseFixtures');
|
||||
const { test, expect, request } = require('./baseFixtures');
|
||||
// const { createDomainObjectWithDefaults } = require('./appActions');
|
||||
const path = require('path');
|
||||
|
||||
@@ -147,6 +147,7 @@ exports.test = test.extend({
|
||||
}
|
||||
});
|
||||
exports.expect = expect;
|
||||
exports.request = request;
|
||||
|
||||
/**
|
||||
* Takes a readable stream and returns a string.
|
||||
|
||||
@@ -29,7 +29,8 @@ relates to how we've extended it (i.e. ./e2e/baseFixtures.js) and assumptions ma
|
||||
const { test } = require('../../baseFixtures.js');
|
||||
|
||||
test.describe('baseFixtures tests', () => {
|
||||
test('Verify that tests fail if console.error is thrown', async ({ page }) => {
|
||||
//Skip this test for now https://github.com/nasa/openmct/issues/6785
|
||||
test.fixme('Verify that tests fail if console.error is thrown', async ({ page }) => {
|
||||
test.fail();
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
@@ -192,8 +192,12 @@ test.describe('Persistence operations @couchdb', () => {
|
||||
]);
|
||||
|
||||
//Slow down the test a bit
|
||||
await expect(page.getByRole('treeitem', { name: ` ${myItemsFolderName}` })).toBeVisible();
|
||||
await expect(page2.getByRole('treeitem', { name: ` ${myItemsFolderName}` })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: `Expand ${myItemsFolderName} folder` })
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page2.getByRole('button', { name: `Expand ${myItemsFolderName} folder` })
|
||||
).toBeVisible();
|
||||
|
||||
// Both pages: Click the Create button
|
||||
await Promise.all([
|
||||
|
||||
@@ -206,6 +206,49 @@ test.describe('Display Layout', () => {
|
||||
expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
|
||||
});
|
||||
|
||||
test('independent time works with display layouts and its children', async ({ page }) => {
|
||||
await setFixedTimeMode(page);
|
||||
// Create Example Imagery
|
||||
const exampleImageryObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Example Imagery'
|
||||
});
|
||||
// Create a Display Layout
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout'
|
||||
});
|
||||
// Edit Display Layout
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||
// Add the Sine Wave Generator to the Display Layout and save changes
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const exampleImageryTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(exampleImageryObject.name)
|
||||
});
|
||||
let layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
await exampleImageryTreeItem.dragTo(layoutGridHolder);
|
||||
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// flip on independent time conductor
|
||||
await page.getByTitle('Enable independent Time Conductor').first().locator('label').click();
|
||||
await page.getByRole('textbox').nth(1).fill('2021-12-30 01:11:00.000Z');
|
||||
await page.getByRole('textbox').nth(0).fill('2021-12-30 01:01:00.000Z');
|
||||
await page.getByRole('textbox').nth(1).click();
|
||||
|
||||
// check image date
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
|
||||
|
||||
// flip it off
|
||||
await page.getByTitle('Disable independent Time Conductor').first().locator('label').click();
|
||||
// timestamp shouldn't be in the past anymore
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
|
||||
});
|
||||
|
||||
test('When multiple plots are contained in a layout, we only ask for annotations once @couchdb', async ({
|
||||
page
|
||||
}) => {
|
||||
|
||||
@@ -158,4 +158,46 @@ test.describe('Flexible Layout', () => {
|
||||
// Verify that the item has been removed from the layout
|
||||
expect(await page.locator('.c-fl-container__frame').count()).toEqual(0);
|
||||
});
|
||||
|
||||
test('independent time works with flexible layouts and its children', async ({ page }) => {
|
||||
// Create Example Imagery
|
||||
const exampleImageryObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Example Imagery'
|
||||
});
|
||||
// Create a Flexible Layout
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Flexible Layout'
|
||||
});
|
||||
// Edit Display Layout
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||
// Add the Sine Wave Generator to the Flexible Layout and save changes
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const exampleImageryTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(exampleImageryObject.name)
|
||||
});
|
||||
// Add the Sine Wave Generator to the Flexible Layout and save changes
|
||||
await exampleImageryTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
|
||||
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// flip on independent time conductor
|
||||
await page.getByTitle('Enable independent Time Conductor').first().locator('label').click();
|
||||
await page.getByRole('textbox').nth(1).fill('2021-12-30 01:11:00.000Z');
|
||||
await page.getByRole('textbox').nth(0).fill('2021-12-30 01:01:00.000Z');
|
||||
await page.getByRole('textbox').nth(1).click();
|
||||
|
||||
// check image date
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
|
||||
|
||||
// flip it off
|
||||
await page.getByTitle('Disable independent Time Conductor').first().locator('label').click();
|
||||
// timestamp shouldn't be in the past anymore
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,6 +70,56 @@ test.describe('Example Imagery Object', () => {
|
||||
await dragContrastSliderAndAssertFilterValues(page);
|
||||
});
|
||||
|
||||
test.only('Can use independent time conductor to change time', async ({ page }) => {
|
||||
// Test independent fixed time with global fixed time
|
||||
// flip on independent time conductor
|
||||
await page.getByTitle('Enable independent Time Conductor').locator('label').click();
|
||||
await page.pause();
|
||||
await page.locator('.c-compact-tc').first().click();
|
||||
await page.getByRole('textbox').nth(3).fill('01:11:00');
|
||||
await page.getByRole('textbox').nth(2).fill('2021-12-30');
|
||||
await page.getByRole('textbox').nth(1).fill('01:01:00');
|
||||
await page.getByRole('textbox').nth(0).fill('2021-12-30');
|
||||
await page.getByRole('button', { name: 'Submit Fixed Inputs' }).click();
|
||||
|
||||
// check image date
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
|
||||
|
||||
// flip it off
|
||||
await page.getByTitle('Disable independent Time Conductor').locator('label').click();
|
||||
// timestamp shouldn't be in the past anymore
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
|
||||
|
||||
// Test independent fixed time with global realtime
|
||||
await setGlobalRealTimeMode(page);
|
||||
await page.pause();
|
||||
await page.getByTitle('Enable independent Time Conductor').locator('label').click();
|
||||
// check image date to be in the past
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
|
||||
// flip it off
|
||||
await page.getByTitle('Disable independent Time Conductor').locator('label').click();
|
||||
// timestamp shouldn't be in the past anymore
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
|
||||
|
||||
// Test independent realtime with global realtime
|
||||
await page.getByTitle('Enable independent Time Conductor').locator('label').click();
|
||||
// check image date
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
|
||||
// change independent time to realtime
|
||||
await page.getByRole('button', { name: /Fixed Timespan/ }).click();
|
||||
await page.getByRole('menuitem', { name: /Local Clock/ }).click();
|
||||
// timestamp shouldn't be in the past anymore
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
|
||||
// back to the past
|
||||
await page
|
||||
.getByRole('button', { name: /Local Clock/ })
|
||||
.first()
|
||||
.click();
|
||||
await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click();
|
||||
// check image date to be in the past
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
|
||||
const deltaYStep = 100; //equivalent to 1x zoom
|
||||
|
||||
@@ -189,11 +239,9 @@ test.describe('Example Imagery Object', () => {
|
||||
test('Using the zoom features does not pause telemetry', async ({ page }) => {
|
||||
const pausePlayButton = page.locator('.c-button.pause-play');
|
||||
|
||||
// open the time conductor drop down
|
||||
await page.locator('.c-mode-button').click();
|
||||
// switch to realtime
|
||||
await setGlobalRealTimeMode(page);
|
||||
|
||||
// Click local clock
|
||||
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
|
||||
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
|
||||
|
||||
// Zoom in via button
|
||||
@@ -233,11 +281,8 @@ test.describe('Example Imagery in Display Layout', () => {
|
||||
description: 'https://github.com/nasa/openmct/issues/3647'
|
||||
});
|
||||
|
||||
// Click time conductor mode button
|
||||
await page.locator('.c-mode-button').click();
|
||||
|
||||
// set realtime mode
|
||||
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
|
||||
await setGlobalRealTimeMode(page);
|
||||
|
||||
// pause/play button
|
||||
const pausePlayButton = await page.locator('.c-button.pause-play');
|
||||
@@ -259,11 +304,8 @@ test.describe('Example Imagery in Display Layout', () => {
|
||||
description: 'https://github.com/nasa/openmct/issues/3647'
|
||||
});
|
||||
|
||||
// Click time conductor mode button
|
||||
await page.locator('.c-mode-button').click();
|
||||
|
||||
// set realtime mode
|
||||
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
|
||||
await setGlobalRealTimeMode(page);
|
||||
|
||||
// pause/play button
|
||||
const pausePlayButton = await page.locator('.c-button.pause-play');
|
||||
@@ -544,11 +586,8 @@ async function performImageryViewOperationsAndAssert(page) {
|
||||
const nextImageButton = page.locator('.c-nav--next');
|
||||
await nextImageButton.click();
|
||||
|
||||
// Click time conductor mode button
|
||||
await page.locator('.c-mode-button').click();
|
||||
|
||||
// Select local clock mode
|
||||
await page.locator('[data-testid=conductor-modeOption-realtime]').click();
|
||||
// set realtime mode
|
||||
await setGlobalRealTimeMode(page);
|
||||
|
||||
// Zoom in on next image
|
||||
await mouseZoomOnImageAndAssert(page, 2);
|
||||
@@ -893,3 +932,15 @@ async function createImageryView(page) {
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function setGlobalRealTimeMode(page) {
|
||||
await page.locator('.l-shell__time-conductor .c-compact-tc').click();
|
||||
await page.waitForSelector('.c-tc-input-popup', { state: 'visible' });
|
||||
// Click mode dropdown
|
||||
await page.getByRole('button', { name: ' Fixed Timespan ' }).click();
|
||||
// Click realtime
|
||||
await page.getByTestId('conductor-modeOption-realtime').click();
|
||||
}
|
||||
|
||||
@@ -47,6 +47,11 @@ test.describe('Operator Status', () => {
|
||||
path: path.join(__dirname, '../../../../helper/', 'addInitOperatorStatus.js')
|
||||
});
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.getByText('Select Role')).toBeVisible();
|
||||
// set role
|
||||
await page.getByRole('button', { name: 'Select' }).click();
|
||||
// dismiss role confirmation popup
|
||||
await page.getByRole('button', { name: 'Dismiss' }).click();
|
||||
});
|
||||
|
||||
// verify that operator status is visible
|
||||
|
||||
@@ -26,7 +26,11 @@ necessarily be used for reference when writing new tests in this area.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions');
|
||||
const {
|
||||
createDomainObjectWithDefaults,
|
||||
selectInspectorTab,
|
||||
waitForPlotsToRender
|
||||
} = require('../../../../appActions');
|
||||
|
||||
test.describe('Stacked Plot', () => {
|
||||
let stackedPlot;
|
||||
@@ -227,4 +231,45 @@ test.describe('Stacked Plot', () => {
|
||||
page.locator('[aria-label="Plot Series Properties"] .c-object-label')
|
||||
).toContainText(swgC.name);
|
||||
});
|
||||
|
||||
test('the legend toggles between aggregate and per child', async ({ page }) => {
|
||||
await page.goto(stackedPlot.url);
|
||||
|
||||
// Go into edit mode
|
||||
await page.click('button[title="Edit"]');
|
||||
|
||||
await selectInspectorTab(page, 'Config');
|
||||
|
||||
let legendProperties = await page.locator('[aria-label="Legend Properties"]');
|
||||
await legendProperties.locator('[title="Display legends per sub plot."]~div input').uncheck();
|
||||
|
||||
await assertAggregateLegendIsVisible(page);
|
||||
|
||||
// Save (exit edit mode)
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('li[title="Save and Finish Editing"]').click();
|
||||
|
||||
await assertAggregateLegendIsVisible(page);
|
||||
|
||||
await page.reload();
|
||||
|
||||
await assertAggregateLegendIsVisible(page);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Asserts that aggregate stacked plot legend is visible
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function assertAggregateLegendIsVisible(page) {
|
||||
// Wait for plot series data to load
|
||||
await waitForPlotsToRender(page);
|
||||
// Wait for plot legend to be shown
|
||||
await page.waitForSelector('.js-stacked-plot-legend', { state: 'attached' });
|
||||
// There should be 3 legend items
|
||||
expect(
|
||||
await page
|
||||
.locator('.js-stacked-plot-legend .c-plot-legend__wrapper div.plot-legend-item')
|
||||
.count()
|
||||
).toBe(3);
|
||||
}
|
||||
|
||||
398
e2e/tests/functional/tooltips.e2e.spec.js
Normal file
398
e2e/tests/functional/tooltips.e2e.spec.js
Normal file
@@ -0,0 +1,398 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which can quickly verify that any openmct installation is
|
||||
operable and that any type of testing can proceed.
|
||||
|
||||
Ideally, smoke tests should make zero assumptions about how and where they are run. This makes them
|
||||
more resilient to change and therefor a better indicator of failure. Smoke tests will also run quickly
|
||||
as they cover a very "thin surface" of functionality.
|
||||
|
||||
When deciding between authoring new smoke tests or functional tests, ask yourself "would I feel
|
||||
comfortable running this test during a live mission?" Avoid creating or deleting Domain Objects.
|
||||
Make no assumptions about the order that elements appear in the DOM.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults, expandEntireTree } = require('../../appActions');
|
||||
|
||||
test.describe('Verify tooltips', () => {
|
||||
let folder1;
|
||||
let folder2;
|
||||
let folder3;
|
||||
let sineWaveObject1;
|
||||
let sineWaveObject2;
|
||||
let sineWaveObject3;
|
||||
|
||||
const swg1Path = 'My Items / Folder Foo / SWG 1';
|
||||
const swg2Path = 'My Items / Folder Foo / Folder Bar / SWG 2';
|
||||
const swg3Path = 'My Items / Folder Foo / Folder Bar / Folder Baz / SWG 3';
|
||||
|
||||
test.beforeEach(async ({ page, openmctConfig }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
folder1 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Folder Foo'
|
||||
});
|
||||
folder2 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Folder Bar',
|
||||
parent: folder1.uuid
|
||||
});
|
||||
folder3 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Folder Baz',
|
||||
parent: folder2.uuid
|
||||
});
|
||||
// Create Sine Wave Generator
|
||||
sineWaveObject1 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'SWG 1',
|
||||
parent: folder1.uuid
|
||||
});
|
||||
sineWaveObject1.path = swg1Path;
|
||||
sineWaveObject2 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'SWG 2',
|
||||
parent: folder2.uuid
|
||||
});
|
||||
sineWaveObject2.path = swg2Path;
|
||||
sineWaveObject3 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'SWG 3',
|
||||
parent: folder3.uuid
|
||||
});
|
||||
sineWaveObject3.path = swg3Path;
|
||||
|
||||
// Expand all folders
|
||||
await expandEntireTree(page);
|
||||
});
|
||||
|
||||
// LAD Tables - DONE
|
||||
// Expanded collapsed plot legend - DONE
|
||||
// Object Labels - DONE
|
||||
// Display Layout headers - DONE
|
||||
// Flexible Layout headers - DONE
|
||||
// Tab View layout headers - DONE
|
||||
// Search - DONE
|
||||
// Gauge -
|
||||
// Notebook Embed - DONE
|
||||
// Telemetry Table -
|
||||
// Timeline Objects
|
||||
// Tree - DONE
|
||||
// Recent Objects
|
||||
|
||||
test('display correct paths for LAD tables', async ({ page, openmctConfig }) => {
|
||||
// Create LAD table
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'LAD Table',
|
||||
name: 'Test LAD Table'
|
||||
});
|
||||
// Edit LAD table
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
// Add the Sine Wave Generator to the LAD table and save changes
|
||||
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-lad-table-wrapper');
|
||||
await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.c-lad-table-wrapper');
|
||||
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-lad-table-wrapper');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
await page.keyboard.down('Control');
|
||||
|
||||
async function getToolTip(object) {
|
||||
await page.locator('.c-create-button').hover();
|
||||
await page.getByRole('cell', { name: object.name }).hover();
|
||||
let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
return tooltipText.replace('\n', '').trim();
|
||||
}
|
||||
|
||||
expect(await getToolTip(sineWaveObject1)).toBe(sineWaveObject1.path);
|
||||
expect(await getToolTip(sineWaveObject2)).toBe(sineWaveObject2.path);
|
||||
expect(await getToolTip(sineWaveObject3)).toBe(sineWaveObject3.path);
|
||||
});
|
||||
|
||||
test('display correct paths for expanded and collapsed plot legend items', async ({ page }) => {
|
||||
// Create Overlay Plot
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot',
|
||||
name: 'Test Overlay Plots'
|
||||
});
|
||||
// Edit Overlay Plot
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
// Add the Sine Wave Generator to the LAD table and save changes
|
||||
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.gl-plot');
|
||||
await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.gl-plot');
|
||||
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.gl-plot');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
await page.keyboard.down('Control');
|
||||
|
||||
async function getCollapsedLegendToolTip(object) {
|
||||
await page.locator('.c-create-button').hover();
|
||||
await page
|
||||
.locator('.plot-series-name', { has: page.locator(`text="${object.name} Hz"`) })
|
||||
.hover();
|
||||
let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
return tooltipText.replace('\n', '').trim();
|
||||
}
|
||||
|
||||
async function getExpandedLegendToolTip(object) {
|
||||
await page.locator('.c-create-button').hover();
|
||||
await page
|
||||
.locator('.plot-series-name', { has: page.locator(`text="${object.name}"`) })
|
||||
.hover();
|
||||
let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
return tooltipText.replace('\n', '').trim();
|
||||
}
|
||||
|
||||
expect(await getCollapsedLegendToolTip(sineWaveObject1)).toBe(sineWaveObject1.path);
|
||||
expect(await getCollapsedLegendToolTip(sineWaveObject2)).toBe(sineWaveObject2.path);
|
||||
expect(await getCollapsedLegendToolTip(sineWaveObject3)).toBe(sineWaveObject3.path);
|
||||
|
||||
await page.keyboard.up('Control');
|
||||
await page.locator('.gl-plot-legend__view-control.c-disclosure-triangle').click();
|
||||
await page.keyboard.down('Control');
|
||||
|
||||
expect(await getExpandedLegendToolTip(sineWaveObject1)).toBe(sineWaveObject1.path);
|
||||
expect(await getExpandedLegendToolTip(sineWaveObject2)).toBe(sineWaveObject2.path);
|
||||
expect(await getExpandedLegendToolTip(sineWaveObject3)).toBe(sineWaveObject3.path);
|
||||
});
|
||||
|
||||
test('display correct paths when hovering over object labels', async ({ page }) => {
|
||||
async function getObjectLabelTooltip(object) {
|
||||
await page
|
||||
.locator('.c-tree__item__name.c-object-label__name', {
|
||||
has: page.locator(`text="${object.name}"`)
|
||||
})
|
||||
.click();
|
||||
await page.keyboard.down('Control');
|
||||
await page
|
||||
.locator('.l-browse-bar__object-name.c-object-label__name', {
|
||||
has: page.locator(`text="${object.name}"`)
|
||||
})
|
||||
.hover();
|
||||
const tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
await page.keyboard.up('Control');
|
||||
return tooltipText.replace('\n', '').trim();
|
||||
}
|
||||
|
||||
expect(await getObjectLabelTooltip(sineWaveObject1)).toBe(sineWaveObject1.path);
|
||||
expect(await getObjectLabelTooltip(sineWaveObject3)).toBe(sineWaveObject3.path);
|
||||
});
|
||||
|
||||
test('display correct paths when hovering over display layout pane headers', async ({ page }) => {
|
||||
// Create Overlay Plot
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot',
|
||||
name: 'Test Overlay Plot'
|
||||
});
|
||||
// Edit Overlay Plot
|
||||
await page.locator('[title="Edit"]').click();
|
||||
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.gl-plot');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Create Stacked Plot
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Stacked Plot',
|
||||
name: 'Test Stacked Plot'
|
||||
});
|
||||
// Edit Stacked Plot
|
||||
await page.locator('[title="Edit"]').click();
|
||||
await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.c-plot--stacked.holder');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Create Display Layout
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: 'Test Display Layout'
|
||||
});
|
||||
// Edit Display Layout
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
await page.dragAndDrop("text='Test Overlay Plot'", '.l-layout__grid-holder', {
|
||||
targetPosition: { x: 0, y: 0 }
|
||||
});
|
||||
await page.dragAndDrop("text='Test Stacked Plot'", '.l-layout__grid-holder', {
|
||||
targetPosition: { x: 0, y: 250 }
|
||||
});
|
||||
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.l-layout__grid-holder', {
|
||||
targetPosition: { x: 500, y: 200 }
|
||||
});
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
await page.keyboard.down('Control');
|
||||
|
||||
await page.getByText('Test Overlay Plot').nth(2).hover();
|
||||
let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
tooltipText = tooltipText.replace('\n', '').trim();
|
||||
expect(tooltipText).toBe('My Items / Test Overlay Plot');
|
||||
|
||||
// await page.keyboard.up('Control');
|
||||
// await page.locator('.c-plot-legend__view-control >> nth=0').click();
|
||||
// await page.keyboard.down('Control');
|
||||
// await page.locator('.plot-wrapper-expanded-legend .plot-series-name').first().hover();
|
||||
// tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
// tooltipText = tooltipText.replace('\n', '').trim();
|
||||
// expect(tooltipText).toBe(sineWaveObject1.path);
|
||||
|
||||
await page.getByText('Test Stacked Plot').nth(2).hover();
|
||||
tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
tooltipText = tooltipText.replace('\n', '').trim();
|
||||
expect(tooltipText).toBe('My Items / Test Stacked Plot');
|
||||
|
||||
await page.getByText('SWG 3').nth(2).hover();
|
||||
tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
tooltipText = tooltipText.replace('\n', '').trim();
|
||||
expect(sineWaveObject3.path).toBe(tooltipText);
|
||||
});
|
||||
|
||||
test('display correct paths when hovering over flexible object labels', async ({ page }) => {
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Flexible Layout',
|
||||
name: 'Test Flexible Layout'
|
||||
});
|
||||
|
||||
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-fl__container >> nth=0');
|
||||
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-fl__container >> nth=1');
|
||||
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
await page.keyboard.down('Control');
|
||||
await page.getByText('SWG 1').nth(2).hover();
|
||||
let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
tooltipText = tooltipText.replace('\n', '').trim();
|
||||
expect(tooltipText).toBe(sineWaveObject1.path);
|
||||
|
||||
await page.getByText('SWG 3').nth(2).hover();
|
||||
tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
tooltipText = tooltipText.replace('\n', '').trim();
|
||||
expect(tooltipText).toBe(sineWaveObject3.path);
|
||||
});
|
||||
|
||||
test('display correct paths when hovering over tab view labels', async ({ page }) => {
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Tabs View',
|
||||
name: 'Test Tabs View'
|
||||
});
|
||||
|
||||
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-tabs-view__tabs-holder');
|
||||
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-tabs-view__tabs-holder');
|
||||
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
await page.keyboard.down('Control');
|
||||
await page.getByText('SWG 1').nth(2).hover();
|
||||
let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
tooltipText = tooltipText.replace('\n', '').trim();
|
||||
expect(tooltipText).toBe(sineWaveObject1.path);
|
||||
|
||||
await page.getByText('SWG 3').nth(2).hover();
|
||||
tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
tooltipText = tooltipText.replace('\n', '').trim();
|
||||
expect(tooltipText).toBe(sineWaveObject3.path);
|
||||
});
|
||||
|
||||
test('display correct paths when hovering tree items', async ({ page }) => {
|
||||
await page.keyboard.down('Control');
|
||||
await page.getByText('SWG 1').nth(0).hover();
|
||||
let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
tooltipText = tooltipText.replace('\n', '').trim();
|
||||
expect(tooltipText).toBe(sineWaveObject1.path);
|
||||
|
||||
await page.getByText('SWG 3').nth(0).hover();
|
||||
tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
tooltipText = tooltipText.replace('\n', '').trim();
|
||||
expect(tooltipText).toBe(sineWaveObject3.path);
|
||||
});
|
||||
|
||||
test('display correct paths when hovering search items', async ({ page }) => {
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).click();
|
||||
await page.fill('.c-search__input', 'SWG 3');
|
||||
|
||||
await page.keyboard.down('Control');
|
||||
await page.locator('.c-gsearch-result__title').hover();
|
||||
let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
tooltipText = tooltipText.replace('\n', '').trim();
|
||||
expect(tooltipText).toBe(sineWaveObject3.path);
|
||||
});
|
||||
|
||||
test('display path for source telemetry when hovering over gauge', ({ page }) => {
|
||||
expect(true).toBe(true);
|
||||
// await createDomainObjectWithDefaults(page, {
|
||||
// type: 'Gauge',
|
||||
// name: 'Test Gauge'
|
||||
// });
|
||||
// await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-gauge__wrapper');
|
||||
// await page.keyboard.down('Control');
|
||||
// await page.locator('.c-gauge__current-value-text-wrapper').hover();
|
||||
// let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
// tooltipText = tooltipText.replace('\n', '').trim();
|
||||
// expect(tooltipText).toBe(sineWaveObject3.path);
|
||||
});
|
||||
|
||||
test('display tooltip path for notebook embeds', async ({ page }) => {
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Notebook',
|
||||
name: 'Test Notebook'
|
||||
});
|
||||
|
||||
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-notebook__drag-area');
|
||||
await page.keyboard.down('Control');
|
||||
await page.locator('.c-ne__embed').hover();
|
||||
let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
tooltipText = tooltipText.replace('\n', '').trim();
|
||||
expect(tooltipText).toBe(sineWaveObject3.path);
|
||||
});
|
||||
|
||||
// test('display tooltip path for telemetry table names', async ({ page }) => {
|
||||
// await setEndOffset(page, { secs: '10' });
|
||||
// await createDomainObjectWithDefaults(page, {
|
||||
// type: 'Telemetry Table',
|
||||
// name: 'Test Telemetry Table'
|
||||
// });
|
||||
|
||||
// await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-telemetry-table');
|
||||
// await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-telemetry-table');
|
||||
|
||||
// await page.locator('button[title="Save"]').click();
|
||||
// await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// // .c-telemetry-table__body
|
||||
|
||||
// await page.keyboard.down('Control');
|
||||
|
||||
// await page.locator('.noselect > [title="SWG 3"]').first().hover();
|
||||
// let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
// tooltipText = tooltipText.replace('\n', '').trim();
|
||||
// expect(tooltipText).toBe(sineWaveObject3.path);
|
||||
// });
|
||||
});
|
||||
@@ -174,6 +174,42 @@ test.describe('Main Tree', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
test('Opening and closing an item before the request has been fulfilled will abort the request @couchdb', async ({
|
||||
page,
|
||||
openmctConfig
|
||||
}) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
let requestWasAborted = false;
|
||||
|
||||
page.on('requestfailed', (request) => {
|
||||
// check if the request was aborted
|
||||
if (request.failure().errorText === 'net::ERR_ABORTED') {
|
||||
requestWasAborted = true;
|
||||
}
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Foo'
|
||||
});
|
||||
|
||||
// Intercept and delay request
|
||||
const delayInMs = 500;
|
||||
|
||||
await page.route('**', async (route, request) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, delayInMs));
|
||||
route.continue();
|
||||
});
|
||||
|
||||
// Quickly Expand/close the root folder
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: `Expand ${myItemsFolderName} folder`
|
||||
})
|
||||
.dblclick({ delay: 400 });
|
||||
|
||||
expect(requestWasAborted).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -63,16 +63,24 @@ const STATUSES = [
|
||||
* @implements {StatusUserProvider}
|
||||
*/
|
||||
export default class ExampleUserProvider extends EventEmitter {
|
||||
constructor(openmct, { defaultStatusRole } = { defaultStatusRole: undefined }) {
|
||||
constructor(
|
||||
openmct,
|
||||
{ statusRoles } = {
|
||||
statusRoles: []
|
||||
}
|
||||
) {
|
||||
super();
|
||||
|
||||
this.openmct = openmct;
|
||||
this.user = undefined;
|
||||
this.loggedIn = false;
|
||||
this.autoLoginUser = undefined;
|
||||
this.status = STATUSES[0];
|
||||
this.statusRoleValues = statusRoles.map((role) => ({
|
||||
role: role,
|
||||
status: STATUSES[0]
|
||||
}));
|
||||
this.pollQuestion = undefined;
|
||||
this.defaultStatusRole = defaultStatusRole;
|
||||
this.statusRoles = statusRoles;
|
||||
|
||||
this.ExampleUser = createExampleUser(this.openmct.user.User);
|
||||
this.loginPromise = undefined;
|
||||
@@ -94,14 +102,13 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
return this.loginPromise;
|
||||
}
|
||||
|
||||
canProvideStatusForRole() {
|
||||
return Promise.resolve(true);
|
||||
canProvideStatusForRole(role) {
|
||||
return this.statusRoles.includes(role);
|
||||
}
|
||||
|
||||
canSetPollQuestion() {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
hasRole(roleId) {
|
||||
if (!this.loggedIn) {
|
||||
Promise.resolve(undefined);
|
||||
@@ -110,16 +117,18 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
return Promise.resolve(this.user.getRoles().includes(roleId));
|
||||
}
|
||||
|
||||
getStatusRoleForCurrentUser() {
|
||||
return Promise.resolve(this.defaultStatusRole);
|
||||
getPossibleRoles() {
|
||||
return this.user.getRoles();
|
||||
}
|
||||
|
||||
getAllStatusRoles() {
|
||||
return Promise.resolve([this.defaultStatusRole]);
|
||||
return Promise.resolve(this.statusRoles);
|
||||
}
|
||||
|
||||
getStatusForRole(role) {
|
||||
return Promise.resolve(this.status);
|
||||
const statusForRole = this.statusRoleValues.find((statusRole) => statusRole.role === role);
|
||||
|
||||
return Promise.resolve(statusForRole?.status);
|
||||
}
|
||||
|
||||
async getDefaultStatusForRole(role) {
|
||||
@@ -130,7 +139,8 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
|
||||
setStatusForRole(role, status) {
|
||||
status.timestamp = Date.now();
|
||||
this.status = status;
|
||||
const matchingIndex = this.statusRoleValues.findIndex((statusRole) => statusRole.role === role);
|
||||
this.statusRoleValues[matchingIndex].status = status;
|
||||
this.emit('statusChange', {
|
||||
role,
|
||||
status
|
||||
@@ -175,7 +185,7 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
// for testing purposes, this will skip the form, this wouldn't be used in
|
||||
// a normal authentication process
|
||||
if (this.autoLoginUser) {
|
||||
this.user = new this.ExampleUser(id, this.autoLoginUser, ['example-role']);
|
||||
this.user = new this.ExampleUser(id, this.autoLoginUser, ['flight', 'driver', 'observer']);
|
||||
this.loggedIn = true;
|
||||
|
||||
return Promise.resolve();
|
||||
|
||||
@@ -21,16 +21,18 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import ExampleUserProvider from './ExampleUserProvider';
|
||||
const AUTO_LOGIN_USER = 'mct-user';
|
||||
const STATUS_ROLES = ['flight', 'driver'];
|
||||
|
||||
export default function ExampleUserPlugin(
|
||||
{ autoLoginUser, defaultStatusRole } = {
|
||||
autoLoginUser: 'guest',
|
||||
defaultStatusRole: 'test-role'
|
||||
{ autoLoginUser, statusRoles } = {
|
||||
autoLoginUser: AUTO_LOGIN_USER,
|
||||
statusRoles: STATUS_ROLES
|
||||
}
|
||||
) {
|
||||
return function install(openmct) {
|
||||
const userProvider = new ExampleUserProvider(openmct, {
|
||||
defaultStatusRole
|
||||
statusRoles
|
||||
});
|
||||
|
||||
if (autoLoginUser !== undefined) {
|
||||
|
||||
@@ -156,9 +156,9 @@ export default function () {
|
||||
key: 'thumbnail',
|
||||
...formatThumbnail
|
||||
});
|
||||
openmct.telemetry.addProvider(getRealtimeProvider());
|
||||
openmct.telemetry.addProvider(getHistoricalProvider());
|
||||
openmct.telemetry.addProvider(getLadProvider());
|
||||
openmct.telemetry.addProvider(getRealtimeProvider(openmct));
|
||||
openmct.telemetry.addProvider(getHistoricalProvider(openmct));
|
||||
openmct.telemetry.addProvider(getLadProvider(openmct));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -207,14 +207,14 @@ function getImageLoadDelay(domainObject) {
|
||||
return imageLoadDelay;
|
||||
}
|
||||
|
||||
function getRealtimeProvider() {
|
||||
function getRealtimeProvider(openmct) {
|
||||
return {
|
||||
supportsSubscribe: (domainObject) => domainObject.type === 'example.imagery',
|
||||
subscribe: (domainObject, callback) => {
|
||||
const delay = getImageLoadDelay(domainObject);
|
||||
const interval = setInterval(() => {
|
||||
const imageSamples = getImageSamples(domainObject.configuration);
|
||||
const datum = pointForTimestamp(Date.now(), domainObject.name, imageSamples, delay);
|
||||
const datum = pointForTimestamp(openmct.time.now(), domainObject.name, imageSamples, delay);
|
||||
callback(datum);
|
||||
}, delay);
|
||||
|
||||
@@ -225,7 +225,7 @@ function getRealtimeProvider() {
|
||||
};
|
||||
}
|
||||
|
||||
function getHistoricalProvider() {
|
||||
function getHistoricalProvider(openmct) {
|
||||
return {
|
||||
supportsRequest: (domainObject, options) => {
|
||||
return domainObject.type === 'example.imagery' && options.strategy !== 'latest';
|
||||
@@ -233,17 +233,12 @@ function getHistoricalProvider() {
|
||||
request: (domainObject, options) => {
|
||||
const delay = getImageLoadDelay(domainObject);
|
||||
let start = options.start;
|
||||
const end = Math.min(options.end, Date.now());
|
||||
const end = Math.min(options.end, openmct.time.now());
|
||||
const data = [];
|
||||
while (start <= end && data.length < delay) {
|
||||
data.push(
|
||||
pointForTimestamp(
|
||||
start,
|
||||
domainObject.name,
|
||||
getImageSamples(domainObject.configuration),
|
||||
delay
|
||||
)
|
||||
);
|
||||
const imageSamples = getImageSamples(domainObject.configuration);
|
||||
const generatedDataPoint = pointForTimestamp(start, domainObject.name, imageSamples, delay);
|
||||
data.push(generatedDataPoint);
|
||||
start += delay;
|
||||
}
|
||||
|
||||
@@ -252,7 +247,7 @@ function getHistoricalProvider() {
|
||||
};
|
||||
}
|
||||
|
||||
function getLadProvider() {
|
||||
function getLadProvider(openmct) {
|
||||
return {
|
||||
supportsRequest: (domainObject, options) => {
|
||||
return domainObject.type === 'example.imagery' && options.strategy === 'latest';
|
||||
@@ -260,7 +255,7 @@ function getLadProvider() {
|
||||
request: (domainObject, options) => {
|
||||
const delay = getImageLoadDelay(domainObject);
|
||||
const datum = pointForTimestamp(
|
||||
Date.now(),
|
||||
openmct.time.now(),
|
||||
domainObject.name,
|
||||
getImageSamples(domainObject.configuration),
|
||||
delay
|
||||
|
||||
@@ -56,6 +56,7 @@ if (document.currentScript) {
|
||||
* @property {import('./src/api/notifications/NotificationAPI').default} notifications
|
||||
* @property {import('./src/api/Editor').default} editor
|
||||
* @property {import('./src/api/overlays/OverlayAPI')} overlays
|
||||
* @property {import('./src/api/tooltips/ToolTipAPI')} tooltips
|
||||
* @property {import('./src/api/menu/MenuAPI').default} menus
|
||||
* @property {import('./src/api/actions/ActionsAPI').default} actions
|
||||
* @property {import('./src/api/status/StatusAPI').default} status
|
||||
|
||||
16
package.json
16
package.json
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.2.5-SNAPSHOT",
|
||||
"version": "3.0.0-SNAPSHOT",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.21.8",
|
||||
"@babel/eslint-parser": "7.22.5",
|
||||
"@braintree/sanitize-url": "6.0.2",
|
||||
"@deploysentinel/playwright": "0.3.4",
|
||||
"@percy/cli": "1.26.0",
|
||||
@@ -21,16 +21,16 @@
|
||||
"d3-axis": "3.0.0",
|
||||
"d3-scale": "3.3.0",
|
||||
"d3-selection": "3.0.0",
|
||||
"eslint": "8.42.0",
|
||||
"eslint": "8.43.0",
|
||||
"eslint-plugin-compat": "4.1.4",
|
||||
"eslint-config-prettier": "8.8.0",
|
||||
"eslint-plugin-playwright": "0.12.0",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-vue": "9.14.1",
|
||||
"eslint-plugin-vue": "9.15.0",
|
||||
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
|
||||
"eventemitter3": "1.2.0",
|
||||
"file-saver": "2.0.5",
|
||||
"flatbush": "4.1.0",
|
||||
"flatbush": "4.2.0",
|
||||
"git-rev-sync": "3.0.2",
|
||||
"html2canvas": "1.4.1",
|
||||
"imports-loader": "4.0.1",
|
||||
@@ -59,8 +59,8 @@
|
||||
"prettier": "2.8.7",
|
||||
"printj": "1.3.1",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sanitize-html": "2.10.0",
|
||||
"sass": "1.63.3",
|
||||
"sanitize-html": "2.11.0",
|
||||
"sass": "1.63.4",
|
||||
"sass-loader": "13.3.2",
|
||||
"sinon": "15.1.0",
|
||||
"style-loader": "3.3.3",
|
||||
@@ -70,7 +70,7 @@
|
||||
"vue-eslint-parser": "9.3.1",
|
||||
"vue-loader": "15.9.8",
|
||||
"vue-template-compiler": "2.6.14",
|
||||
"webpack": "5.86.0",
|
||||
"webpack": "5.88.0",
|
||||
"webpack-cli": "5.1.1",
|
||||
"webpack-dev-server": "4.15.1",
|
||||
"webpack-merge": "5.9.0"
|
||||
|
||||
@@ -24,6 +24,7 @@ define([
|
||||
'EventEmitter',
|
||||
'./api/api',
|
||||
'./api/overlays/OverlayAPI',
|
||||
'./api/tooltips/ToolTipAPI',
|
||||
'./selection/Selection',
|
||||
'./plugins/plugins',
|
||||
'./ui/registries/ViewRegistry',
|
||||
@@ -48,6 +49,7 @@ define([
|
||||
EventEmitter,
|
||||
api,
|
||||
OverlayAPI,
|
||||
ToolTipAPI,
|
||||
Selection,
|
||||
plugins,
|
||||
ViewRegistry,
|
||||
@@ -220,6 +222,8 @@ define([
|
||||
|
||||
['overlays', () => new OverlayAPI.default()],
|
||||
|
||||
['tooltips', () => new ToolTipAPI.default()],
|
||||
|
||||
['menus', () => new api.MenuAPI(this)],
|
||||
|
||||
['actions', () => new api.ActionsAPI(this)],
|
||||
|
||||
@@ -58,7 +58,6 @@
|
||||
:key="action.name"
|
||||
role="menuitem"
|
||||
:class="action.cssClass"
|
||||
:title="action.description"
|
||||
:data-testid="action.testId || false"
|
||||
@click="action.onItemClicked"
|
||||
@mouseover="toggleItemDescription(action)"
|
||||
|
||||
@@ -242,11 +242,16 @@ export default class ObjectAPI {
|
||||
return domainObject;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn(`Failed to retrieve ${keystring}:`, error);
|
||||
delete this.cache[keystring];
|
||||
const result = this.applyGetInterceptors(identifier);
|
||||
|
||||
return result;
|
||||
// suppress abort errors
|
||||
if (error.name === 'AbortError') {
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(`Failed to retrieve ${keystring}:`, error);
|
||||
|
||||
return this.applyGetInterceptors(identifier);
|
||||
});
|
||||
|
||||
this.cache[keystring] = objectPromise;
|
||||
@@ -540,6 +545,40 @@ export default class ObjectAPI {
|
||||
.join('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return path of telemetry objects in the object composition
|
||||
* @param {object} identifier the identifier for the domain object to query for
|
||||
* @param {object} [telemetryIdentifier] the specific identifier for the telemetry
|
||||
* to look for in the composition, uses first object in composition otherwise
|
||||
* @returns {Array} path of telemetry object in object composition
|
||||
*/
|
||||
async getTelemetryPath(identifier, telemetryIdentifier) {
|
||||
const objectDetails = await this.get(identifier);
|
||||
const telemetryPath = [];
|
||||
if (objectDetails.composition && !['folder'].includes(objectDetails.type)) {
|
||||
let sourceTelemetry = objectDetails.composition[0];
|
||||
if (telemetryIdentifier) {
|
||||
sourceTelemetry = objectDetails.composition.find(
|
||||
(telemetrySource) =>
|
||||
this.makeKeyString(telemetrySource) === this.makeKeyString(telemetryIdentifier)
|
||||
);
|
||||
}
|
||||
const compositionElement = await this.get(sourceTelemetry);
|
||||
if (!['yamcs.telemetry', 'generator'].includes(compositionElement.type)) {
|
||||
return telemetryPath;
|
||||
}
|
||||
const telemetryKey = compositionElement.identifier.key;
|
||||
const telemetryPathObjects = await this.getOriginalPath(telemetryKey);
|
||||
telemetryPathObjects.forEach((pathObject) => {
|
||||
if (pathObject.type === 'root') {
|
||||
return;
|
||||
}
|
||||
telemetryPath.unshift(pathObject.name);
|
||||
});
|
||||
}
|
||||
return telemetryPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a domain object. Internal to ObjectAPI, won't call save after.
|
||||
* @private
|
||||
|
||||
@@ -248,10 +248,17 @@ describe('The Object API', () => {
|
||||
});
|
||||
|
||||
it('displays a notification in the event of an error', () => {
|
||||
mockProvider.get.and.returnValue(Promise.reject());
|
||||
openmct.notifications.warn = jasmine.createSpy('warn');
|
||||
mockProvider.get.and.returnValue(
|
||||
Promise.reject({
|
||||
name: 'Error',
|
||||
status: 404,
|
||||
statusText: 'Not Found'
|
||||
})
|
||||
);
|
||||
|
||||
return objectAPI.get(mockDomainObject.identifier).catch(() => {
|
||||
expect(openmct.notifications.error).toHaveBeenCalledWith(
|
||||
expect(openmct.notifications.warn).toHaveBeenCalledWith(
|
||||
`Failed to retrieve object ${TEST_NAMESPACE}:${TEST_KEY}`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
/*****************************************************************************
|
||||
* 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 Overlay from './Overlay';
|
||||
import Dialog from './Dialog';
|
||||
import ProgressDialog from './ProgressDialog';
|
||||
import Selection from './Selection';
|
||||
|
||||
/**
|
||||
* The OverlayAPI is responsible for pre-pending templates to
|
||||
@@ -130,6 +153,13 @@ class OverlayAPI {
|
||||
|
||||
return progressDialog;
|
||||
}
|
||||
|
||||
selection(options) {
|
||||
let selection = new Selection(options);
|
||||
this.showOverlay(selection);
|
||||
|
||||
return selection;
|
||||
}
|
||||
}
|
||||
|
||||
export default OverlayAPI;
|
||||
|
||||
67
src/api/overlays/Selection.js
Normal file
67
src/api/overlays/Selection.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/*****************************************************************************
|
||||
* 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 SelectionComponent from './components/SelectionComponent.vue';
|
||||
import Overlay from './Overlay';
|
||||
import Vue from 'vue';
|
||||
|
||||
class Selection extends Overlay {
|
||||
constructor({
|
||||
iconClass,
|
||||
title,
|
||||
message,
|
||||
selectionOptions,
|
||||
onChange,
|
||||
currentSelection,
|
||||
...options
|
||||
}) {
|
||||
let component = new Vue({
|
||||
components: {
|
||||
SelectionComponent: SelectionComponent
|
||||
},
|
||||
provide: {
|
||||
iconClass,
|
||||
title,
|
||||
message,
|
||||
selectionOptions,
|
||||
onChange,
|
||||
currentSelection
|
||||
},
|
||||
template: '<selection-component></selection-component>'
|
||||
}).$mount();
|
||||
|
||||
super({
|
||||
element: component.$el,
|
||||
size: 'fit',
|
||||
dismissable: false,
|
||||
onChange,
|
||||
currentSelection,
|
||||
...options
|
||||
});
|
||||
|
||||
this.once('destroy', () => {
|
||||
component.$destroy();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Selection;
|
||||
34
src/api/overlays/components/SelectionComponent.vue
Normal file
34
src/api/overlays/components/SelectionComponent.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="c-message">
|
||||
<!--Uses flex-row -->
|
||||
<div class="c-message__icon" :class="['u-icon-bg-color-' + iconClass]"></div>
|
||||
<div class="c-message__text">
|
||||
<!-- Uses flex-column -->
|
||||
<div v-if="title" class="c-message__title">
|
||||
{{ title }}
|
||||
</div>
|
||||
|
||||
<div v-if="message" class="c-message__action-text">
|
||||
{{ message }}
|
||||
</div>
|
||||
<select @change="onChange">
|
||||
<option
|
||||
v-for="option in selectionOptions"
|
||||
:key="option.key"
|
||||
:value="option.key"
|
||||
:selected="option.key === currentSelection"
|
||||
>
|
||||
{{ option.name }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inject: ['iconClass', 'title', 'message', 'selectionOptions', 'currentSelection', 'onChange']
|
||||
};
|
||||
</script>
|
||||
@@ -204,27 +204,23 @@ export default class TelemetryAPI {
|
||||
*/
|
||||
standardizeRequestOptions(options = {}) {
|
||||
if (!Object.hasOwn(options, 'start')) {
|
||||
if (options.timeContext?.bounds()) {
|
||||
options.start = options.timeContext.bounds().start;
|
||||
if (options.timeContext?.getBounds()) {
|
||||
options.start = options.timeContext.getBounds().start;
|
||||
} else {
|
||||
options.start = this.openmct.time.bounds().start;
|
||||
options.start = this.openmct.time.getBounds().start;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Object.hasOwn(options, 'end')) {
|
||||
if (options.timeContext?.bounds()) {
|
||||
options.end = options.timeContext.bounds().end;
|
||||
if (options.timeContext?.getBounds()) {
|
||||
options.end = options.timeContext.getBounds().end;
|
||||
} else {
|
||||
options.end = this.openmct.time.bounds().end;
|
||||
options.end = this.openmct.time.getBounds().end;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Object.hasOwn(options, 'domain')) {
|
||||
options.domain = this.openmct.time.timeSystem().key;
|
||||
}
|
||||
|
||||
if (!Object.hasOwn(options, 'timeContext')) {
|
||||
options.timeContext = this.openmct.time;
|
||||
options.domain = this.openmct.time.getTimeSystem().key;
|
||||
}
|
||||
|
||||
return options;
|
||||
@@ -489,6 +485,62 @@ export default class TelemetryAPI {
|
||||
}.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to run-time changes in configured telemetry limits for a specific domain object.
|
||||
* The callback will be called whenever data is received from a
|
||||
* limit provider.
|
||||
*
|
||||
* @method subscribeToLimits
|
||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||
* @param {module:openmct.DomainObject} domainObject the object
|
||||
* which has associated limits
|
||||
* @param {Function} callback the callback to invoke with new data, as
|
||||
* it becomes available
|
||||
* @returns {Function} a function which may be called to terminate
|
||||
* the subscription
|
||||
*/
|
||||
subscribeToLimits(domainObject, callback) {
|
||||
if (domainObject.type === 'unknown') {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const provider = this.#findLimitEvaluator(domainObject);
|
||||
|
||||
if (!this.limitsSubscribeCache) {
|
||||
this.limitsSubscribeCache = {};
|
||||
}
|
||||
|
||||
const keyString = objectUtils.makeKeyString(domainObject.identifier);
|
||||
let subscriber = this.limitsSubscribeCache[keyString];
|
||||
|
||||
if (!subscriber) {
|
||||
subscriber = this.limitsSubscribeCache[keyString] = {
|
||||
callbacks: [callback]
|
||||
};
|
||||
if (provider && provider.subscribeToLimits) {
|
||||
subscriber.unsubscribe = provider.subscribeToLimits(domainObject, function (value) {
|
||||
subscriber.callbacks.forEach(function (cb) {
|
||||
cb(value);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
subscriber.unsubscribe = function () {};
|
||||
}
|
||||
} else {
|
||||
subscriber.callbacks.push(callback);
|
||||
}
|
||||
|
||||
return function unsubscribe() {
|
||||
subscriber.callbacks = subscriber.callbacks.filter(function (cb) {
|
||||
return cb !== callback;
|
||||
});
|
||||
if (subscriber.callbacks.length === 0) {
|
||||
subscriber.unsubscribe();
|
||||
delete this.limitsSubscribeCache[keyString];
|
||||
}
|
||||
}.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request telemetry staleness for a domain object.
|
||||
*
|
||||
@@ -676,7 +728,7 @@ export default class TelemetryAPI {
|
||||
*
|
||||
* @param {module:openmct.DomainObject} domainObject the domain
|
||||
* object for which to get limits
|
||||
* @returns {module:openmct.TelemetryAPI~LimitEvaluator}
|
||||
* @returns {LimitsResponseObject}
|
||||
* @method limits
|
||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||
*/
|
||||
@@ -723,18 +775,8 @@ export default class TelemetryAPI {
|
||||
*
|
||||
* @param {module:openmct.DomainObject} domainObject the domain
|
||||
* object for which to display limits
|
||||
* @returns {module:openmct.TelemetryAPI~LimitEvaluator}
|
||||
* @method limits returns a limits object of
|
||||
* type {
|
||||
* level1: {
|
||||
* low: { key1: value1, key2: value2, color: <supportedColor> },
|
||||
* high: { key1: value1, key2: value2, color: <supportedColor> }
|
||||
* },
|
||||
* level2: {
|
||||
* low: { key1: value1, key2: value2 },
|
||||
* high: { key1: value1, key2: value2 }
|
||||
* }
|
||||
* }
|
||||
* @returns {LimitsResponseObject}
|
||||
* @method limits returns a limits object of type {LimitsResponseObject}
|
||||
* supported colors are purple, red, orange, yellow and cyan
|
||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||
*/
|
||||
@@ -766,7 +808,7 @@ export default class TelemetryAPI {
|
||||
* @param {*} datum the telemetry datum to evaluate
|
||||
* @param {TelemetryProperty} the property to check for limit violations
|
||||
* @memberof module:openmct.TelemetryAPI~LimitEvaluator
|
||||
* @returns {module:openmct.TelemetryAPI~LimitViolation} metadata about
|
||||
* @returns {LimitViolation} metadata about
|
||||
* the limit violation, or undefined if a value is within limits
|
||||
*/
|
||||
|
||||
@@ -777,6 +819,42 @@ export default class TelemetryAPI {
|
||||
* @property {string} cssClass the class (or space-separated classes) to
|
||||
* apply to display elements for values which violate this limit
|
||||
* @property {string} name the human-readable name for the limit violation
|
||||
* @property {number} low a lower limit for violation
|
||||
* @property {number} high a higher limit violation
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} LimitsResponseObject
|
||||
* @memberof {module:openmct.TelemetryAPI~}
|
||||
* @property {LimitDefinition} limitLevel the level name and it's limit definition
|
||||
* @example {
|
||||
* [limitLevel]: {
|
||||
* low: {
|
||||
* color: lowColor,
|
||||
* value: lowValue
|
||||
* },
|
||||
* high: {
|
||||
* color: highColor,
|
||||
* value: highValue
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* Limit defined for a telemetry property.
|
||||
* @typedef LimitDefinition
|
||||
* @memberof {module:openmct.TelemetryAPI~}
|
||||
* @property {LimitDefinitionValue} low a lower limit
|
||||
* @property {LimitDefinitionValue} high a higher limit
|
||||
*/
|
||||
|
||||
/**
|
||||
* Limit definition for a Limit of a telemetry property.
|
||||
* @typedef LimitDefinitionValue
|
||||
* @memberof {module:openmct.TelemetryAPI~}
|
||||
* @property {string} color color to represent this limit
|
||||
* @property {Number} value the limit value
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,15 +29,20 @@ describe('Telemetry API', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
openmct = {
|
||||
time: jasmine.createSpyObj('timeAPI', ['timeSystem', 'bounds']),
|
||||
time: jasmine.createSpyObj('timeAPI', ['timeSystem', 'getTimeSystem', 'bounds', 'getBounds']),
|
||||
types: jasmine.createSpyObj('typeRegistry', ['get'])
|
||||
};
|
||||
|
||||
openmct.time.timeSystem.and.returnValue({ key: 'system' });
|
||||
openmct.time.getTimeSystem.and.returnValue({ key: 'system' });
|
||||
openmct.time.bounds.and.returnValue({
|
||||
start: 0,
|
||||
end: 1
|
||||
});
|
||||
openmct.time.getBounds.and.returnValue({
|
||||
start: 0,
|
||||
end: 1
|
||||
});
|
||||
telemetryAPI = new TelemetryAPI(openmct);
|
||||
});
|
||||
|
||||
@@ -261,16 +266,14 @@ describe('Telemetry API', () => {
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system',
|
||||
timeContext: jasmine.any(Object)
|
||||
domain: 'system'
|
||||
});
|
||||
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), {
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system',
|
||||
timeContext: jasmine.any(Object)
|
||||
domain: 'system'
|
||||
});
|
||||
|
||||
telemetryProvider.supportsRequest.calls.reset();
|
||||
@@ -281,16 +284,14 @@ describe('Telemetry API', () => {
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system',
|
||||
timeContext: jasmine.any(Object)
|
||||
domain: 'system'
|
||||
});
|
||||
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), {
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system',
|
||||
timeContext: jasmine.any(Object)
|
||||
domain: 'system'
|
||||
});
|
||||
});
|
||||
|
||||
@@ -309,16 +310,14 @@ describe('Telemetry API', () => {
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain',
|
||||
signal,
|
||||
timeContext: jasmine.any(Object)
|
||||
signal
|
||||
});
|
||||
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), {
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain',
|
||||
signal,
|
||||
timeContext: jasmine.any(Object)
|
||||
signal
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import _ from 'lodash';
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import { LOADED_ERROR, TIMESYSTEM_KEY_NOTIFICATION, TIMESYSTEM_KEY_WARNING } from './constants';
|
||||
import { TIME_CONTEXT_EVENTS } from '../time/constants';
|
||||
|
||||
/**
|
||||
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
|
||||
@@ -60,8 +61,11 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
this.futureBuffer = [];
|
||||
this.parseTime = undefined;
|
||||
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
|
||||
if (!Object.hasOwn(options, 'timeContext')) {
|
||||
options.timeContext = this.openmct.time;
|
||||
}
|
||||
this.options = options;
|
||||
this.unsubscribe = undefined;
|
||||
this.options = this.openmct.telemetry.standardizeRequestOptions(options);
|
||||
this.pageState = undefined;
|
||||
this.lastBounds = undefined;
|
||||
this.requestAbort = undefined;
|
||||
@@ -78,11 +82,11 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
this._error(LOADED_ERROR);
|
||||
}
|
||||
|
||||
this._setTimeSystem(this.options.timeContext.timeSystem());
|
||||
this.lastBounds = this.options.timeContext.bounds();
|
||||
|
||||
this._setTimeSystem(this.options.timeContext.getTimeSystem());
|
||||
this.lastBounds = this.options.timeContext.getBounds();
|
||||
this._watchBounds();
|
||||
this._watchTimeSystem();
|
||||
this._watchTimeModeChange();
|
||||
|
||||
this._requestHistoricalTelemetry();
|
||||
this._initiateSubscriptionTelemetry();
|
||||
@@ -101,6 +105,7 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
|
||||
this._unwatchBounds();
|
||||
this._unwatchTimeSystem();
|
||||
this._unwatchTimeModeChange();
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
@@ -121,7 +126,7 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
* @private
|
||||
*/
|
||||
async _requestHistoricalTelemetry() {
|
||||
let options = { ...this.options };
|
||||
let options = this.openmct.telemetry.standardizeRequestOptions({ ...this.options });
|
||||
const historicalProvider = this.openmct.telemetry.findRequestProvider(
|
||||
this.domainObject,
|
||||
options
|
||||
@@ -433,6 +438,10 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
this._reset();
|
||||
}
|
||||
|
||||
_timeModeChanged() {
|
||||
this._reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the telemetry data of the collection, and re-request
|
||||
* historical telemetry
|
||||
@@ -450,19 +459,35 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* adds the _bounds callback to the 'bounds' timeAPI listener
|
||||
* adds the _bounds callback to the 'boundsChanged' timeAPI listener
|
||||
* @private
|
||||
*/
|
||||
_watchBounds() {
|
||||
this.options.timeContext.on('bounds', this._bounds, this);
|
||||
this.options.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this._bounds, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* removes the _bounds callback from the 'bounds' timeAPI listener
|
||||
* removes the _bounds callback from the 'boundsChanged' timeAPI listener
|
||||
* @private
|
||||
*/
|
||||
_unwatchBounds() {
|
||||
this.options.timeContext.off('bounds', this._bounds, this);
|
||||
this.options.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this._bounds, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* adds the _timeModeChanged callback to the 'modeChanged' timeAPI listener
|
||||
* @private
|
||||
*/
|
||||
_watchTimeModeChange() {
|
||||
this.options.timeContext.on(TIME_CONTEXT_EVENTS.modeChanged, this._timeModeChanged, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* removes the _timeModeChanged callback from the 'modeChanged' timeAPI listener
|
||||
* @private
|
||||
*/
|
||||
_unwatchTimeModeChange() {
|
||||
this.options.timeContext.off(TIME_CONTEXT_EVENTS.modeChanged, this._timeModeChanged, this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -470,7 +495,11 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
* @private
|
||||
*/
|
||||
_watchTimeSystem() {
|
||||
this.options.timeContext.on('timeSystem', this._setTimeSystemAndFetchData, this);
|
||||
this.options.timeContext.on(
|
||||
TIME_CONTEXT_EVENTS.timeSystemChanged,
|
||||
this._setTimeSystemAndFetchData,
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -478,7 +507,11 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
* @private
|
||||
*/
|
||||
_unwatchTimeSystem() {
|
||||
this.options.timeContext.off('timeSystem', this._setTimeSystemAndFetchData, this);
|
||||
this.options.timeContext.off(
|
||||
TIME_CONTEXT_EVENTS.timeSystemChanged,
|
||||
this._setTimeSystemAndFetchData,
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -134,6 +134,14 @@ define(['lodash'], function (_) {
|
||||
);
|
||||
};
|
||||
|
||||
TelemetryMetadataManager.prototype.getUseToUpdateInPlaceValue = function () {
|
||||
return this.valueMetadatas.find(this.isInPlaceUpdateValue);
|
||||
};
|
||||
|
||||
TelemetryMetadataManager.prototype.isInPlaceUpdateValue = function (metadatum) {
|
||||
return metadatum.useToUpdateInPlace === true;
|
||||
};
|
||||
|
||||
TelemetryMetadataManager.prototype.getDefaultDisplayValue = function () {
|
||||
let valueMetadata = this.valuesForHints(['range'])[0];
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import TimeContext, { TIME_CONTEXT_EVENTS } from './TimeContext';
|
||||
import TimeContext from './TimeContext';
|
||||
import { MODES, REALTIME_MODE_KEY, TIME_CONTEXT_EVENTS } from './constants';
|
||||
|
||||
/**
|
||||
* The IndependentTimeContext handles getting and setting time of the openmct application in general.
|
||||
@@ -46,7 +47,7 @@ class IndependentTimeContext extends TimeContext {
|
||||
this.globalTimeContext.on('removeOwnContext', this.removeIndependentContext);
|
||||
}
|
||||
|
||||
bounds(newBounds) {
|
||||
bounds() {
|
||||
if (this.upstreamTimeContext) {
|
||||
return this.upstreamTimeContext.bounds(...arguments);
|
||||
} else {
|
||||
@@ -54,7 +55,23 @@ class IndependentTimeContext extends TimeContext {
|
||||
}
|
||||
}
|
||||
|
||||
tick(timestamp) {
|
||||
getBounds() {
|
||||
if (this.upstreamTimeContext) {
|
||||
return this.upstreamTimeContext.getBounds();
|
||||
} else {
|
||||
return super.getBounds();
|
||||
}
|
||||
}
|
||||
|
||||
setBounds() {
|
||||
if (this.upstreamTimeContext) {
|
||||
return this.upstreamTimeContext.setBounds(...arguments);
|
||||
} else {
|
||||
return super.setBounds(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (this.upstreamTimeContext) {
|
||||
return this.upstreamTimeContext.tick(...arguments);
|
||||
} else {
|
||||
@@ -62,7 +79,7 @@ class IndependentTimeContext extends TimeContext {
|
||||
}
|
||||
}
|
||||
|
||||
clockOffsets(offsets) {
|
||||
clockOffsets() {
|
||||
if (this.upstreamTimeContext) {
|
||||
return this.upstreamTimeContext.clockOffsets(...arguments);
|
||||
} else {
|
||||
@@ -70,11 +87,19 @@ class IndependentTimeContext extends TimeContext {
|
||||
}
|
||||
}
|
||||
|
||||
stopClock() {
|
||||
getClockOffsets() {
|
||||
if (this.upstreamTimeContext) {
|
||||
this.upstreamTimeContext.stopClock();
|
||||
return this.upstreamTimeContext.getClockOffsets();
|
||||
} else {
|
||||
super.stopClock();
|
||||
return super.getClockOffsets();
|
||||
}
|
||||
}
|
||||
|
||||
setClockOffsets() {
|
||||
if (this.upstreamTimeContext) {
|
||||
return this.upstreamTimeContext.setClockOffsets(...arguments);
|
||||
} else {
|
||||
return super.setClockOffsets(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,10 +111,19 @@ class IndependentTimeContext extends TimeContext {
|
||||
return this.globalTimeContext.timeSystem(...arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time system of the TimeAPI.
|
||||
* @returns {TimeSystem} The currently applied time system
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method getTimeSystem
|
||||
*/
|
||||
getTimeSystem() {
|
||||
return this.globalTimeContext.getTimeSystem();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active clock. Tick source will be immediately subscribed to
|
||||
* and ticking will begin. Offsets from 'now' must also be provided. A clock
|
||||
* can be unset by calling {@link stopClock}.
|
||||
* and ticking will begin. Offsets from 'now' must also be provided.
|
||||
*
|
||||
* @param {Clock || string} keyOrClock The clock to activate, or its key
|
||||
* @param {ClockOffsets} offsets on each tick these will be used to calculate
|
||||
@@ -126,15 +160,19 @@ class IndependentTimeContext extends TimeContext {
|
||||
this.activeClock = clock;
|
||||
|
||||
/**
|
||||
* The active clock has changed. Clock can be unset by calling {@link stopClock}
|
||||
* The active clock has changed.
|
||||
* @event clock
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
* @property {Clock} clock The newly activated clock, or undefined
|
||||
* if the system is no longer following a clock source
|
||||
*/
|
||||
this.emit('clock', this.activeClock);
|
||||
this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);
|
||||
|
||||
if (this.activeClock !== undefined) {
|
||||
//set the mode here or isRealtime will be false even if we're in clock mode
|
||||
this.setMode(REALTIME_MODE_KEY);
|
||||
|
||||
this.clockOffsets(offsets);
|
||||
this.activeClock.on('tick', this.tick);
|
||||
}
|
||||
@@ -145,6 +183,122 @@ class IndependentTimeContext extends TimeContext {
|
||||
return this.activeClock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active clock.
|
||||
* @return {Clock} the currently active clock;
|
||||
*/
|
||||
getClock() {
|
||||
if (this.upstreamTimeContext) {
|
||||
return this.upstreamTimeContext.getClock();
|
||||
}
|
||||
|
||||
return this.activeClock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active clock. Tick source will be immediately subscribed to
|
||||
* and the currently ticking will begin.
|
||||
* Offsets from 'now', if provided, will be used to set realtime mode offsets
|
||||
*
|
||||
* @param {Clock || string} keyOrClock The clock to activate, or its key
|
||||
* @fires module:openmct.TimeAPI~clock
|
||||
* @return {Clock} the currently active clock;
|
||||
*/
|
||||
setClock(keyOrClock) {
|
||||
if (this.upstreamTimeContext) {
|
||||
return this.upstreamTimeContext.setClock(...arguments);
|
||||
}
|
||||
|
||||
let clock;
|
||||
|
||||
if (typeof keyOrClock === 'string') {
|
||||
clock = this.globalTimeContext.clocks.get(keyOrClock);
|
||||
if (clock === undefined) {
|
||||
throw `Unknown clock ${keyOrClock}. Has it been registered with 'addClock'?`;
|
||||
}
|
||||
} else if (typeof keyOrClock === 'object') {
|
||||
clock = keyOrClock;
|
||||
if (!this.globalTimeContext.clocks.has(clock.key)) {
|
||||
throw `Unknown clock ${keyOrClock.key}. Has it been registered with 'addClock'?`;
|
||||
}
|
||||
}
|
||||
|
||||
const previousClock = this.activeClock;
|
||||
if (previousClock) {
|
||||
previousClock.off('tick', this.tick);
|
||||
}
|
||||
|
||||
this.activeClock = clock;
|
||||
this.activeClock.on('tick', this.tick);
|
||||
|
||||
/**
|
||||
* The active clock has changed.
|
||||
* @event clock
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
* @property {Clock} clock The newly activated clock, or undefined
|
||||
* if the system is no longer following a clock source
|
||||
*/
|
||||
this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);
|
||||
|
||||
return this.activeClock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current mode.
|
||||
* @return {Mode} the current mode;
|
||||
*/
|
||||
getMode() {
|
||||
if (this.upstreamTimeContext) {
|
||||
return this.upstreamTimeContext.getMode();
|
||||
}
|
||||
|
||||
return this.mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the mode to either fixed or realtime.
|
||||
*
|
||||
* @param {Mode} mode The mode to activate
|
||||
* @param {TimeBounds | ClockOffsets} offsetsOrBounds A time window of a fixed width
|
||||
* @fires module:openmct.TimeAPI~clock
|
||||
* @return {Mode} the currently active mode;
|
||||
*/
|
||||
setMode(mode, offsetsOrBounds) {
|
||||
if (!mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.upstreamTimeContext) {
|
||||
return this.upstreamTimeContext.setMode(...arguments);
|
||||
}
|
||||
|
||||
if (mode === MODES.realtime && this.activeClock === undefined) {
|
||||
throw `Unknown clock. Has a clock been registered with 'addClock'?`;
|
||||
}
|
||||
|
||||
if (mode !== this.mode) {
|
||||
this.mode = mode;
|
||||
/**
|
||||
* The active mode has changed.
|
||||
* @event modeChanged
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
* @property {Mode} mode The newly activated mode
|
||||
*/
|
||||
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.mode));
|
||||
}
|
||||
|
||||
//We are also going to set bounds here
|
||||
if (offsetsOrBounds !== undefined) {
|
||||
if (this.mode === REALTIME_MODE_KEY) {
|
||||
this.setClockOffsets(offsetsOrBounds);
|
||||
} else {
|
||||
this.setBounds(offsetsOrBounds);
|
||||
}
|
||||
}
|
||||
|
||||
return this.mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes this time context to follow another time context (either the global context, or another upstream time context)
|
||||
* This allows views to have their own time context which points to the appropriate upstream context as necessary, achieving nesting.
|
||||
@@ -152,7 +306,7 @@ class IndependentTimeContext extends TimeContext {
|
||||
followTimeContext() {
|
||||
this.stopFollowingTimeContext();
|
||||
if (this.upstreamTimeContext) {
|
||||
TIME_CONTEXT_EVENTS.forEach((eventName) => {
|
||||
Object.values(TIME_CONTEXT_EVENTS).forEach((eventName) => {
|
||||
const thisTimeContext = this;
|
||||
this.upstreamTimeContext.on(eventName, passthrough);
|
||||
this.unlisteners.push(() => {
|
||||
@@ -197,6 +351,7 @@ class IndependentTimeContext extends TimeContext {
|
||||
|
||||
// Emit bounds so that views that are changing context get the upstream bounds
|
||||
this.emit('bounds', this.bounds());
|
||||
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
|
||||
}
|
||||
|
||||
hasOwnContext() {
|
||||
@@ -259,11 +414,16 @@ class IndependentTimeContext extends TimeContext {
|
||||
this.followTimeContext();
|
||||
|
||||
// Emit bounds so that views that are changing context get the upstream bounds
|
||||
this.emit('bounds', this.bounds());
|
||||
this.emit('bounds', this.getBounds());
|
||||
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
|
||||
// now that the view's context is set, tell others to check theirs in case they were following this view's context.
|
||||
this.globalTimeContext.emit('refreshContext', viewKey);
|
||||
}
|
||||
}
|
||||
|
||||
#copy(object) {
|
||||
return JSON.parse(JSON.stringify(object));
|
||||
}
|
||||
}
|
||||
|
||||
export default IndependentTimeContext;
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
import GlobalTimeContext from './GlobalTimeContext';
|
||||
import IndependentTimeContext from '@/api/time/IndependentTimeContext';
|
||||
import { FIXED_MODE_KEY, REALTIME_MODE_KEY } from '@/api/time/constants';
|
||||
|
||||
/**
|
||||
* The public API for setting and querying the temporal state of the
|
||||
@@ -134,14 +135,15 @@ class TimeAPI extends GlobalTimeContext {
|
||||
*/
|
||||
addIndependentContext(key, value, clockKey) {
|
||||
let timeContext = this.getIndependentContext(key);
|
||||
|
||||
//stop following upstream time context since the view has it's own
|
||||
timeContext.resetContext();
|
||||
|
||||
if (clockKey) {
|
||||
timeContext.clock(clockKey, value);
|
||||
timeContext.setClock(clockKey);
|
||||
timeContext.setMode(REALTIME_MODE_KEY, value);
|
||||
} else {
|
||||
timeContext.stopClock();
|
||||
timeContext.bounds(value);
|
||||
timeContext.setMode(FIXED_MODE_KEY, value);
|
||||
}
|
||||
|
||||
// Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context
|
||||
@@ -185,6 +187,7 @@ class TimeAPI extends GlobalTimeContext {
|
||||
}
|
||||
|
||||
let viewTimeContext = this.getIndependentContext(viewKey);
|
||||
|
||||
if (!viewTimeContext) {
|
||||
// If the context doesn't exist yet, create it.
|
||||
viewTimeContext = new IndependentTimeContext(this.openmct, this, objectPath);
|
||||
|
||||
@@ -87,7 +87,7 @@ describe('The Time API', function () {
|
||||
expect(function () {
|
||||
api.timeSystem(timeSystem, bounds);
|
||||
}).not.toThrow();
|
||||
expect(api.timeSystem()).toBe(timeSystem);
|
||||
expect(api.timeSystem()).toEqual(timeSystem);
|
||||
});
|
||||
|
||||
it('Disallows setting of time system without bounds', function () {
|
||||
@@ -110,7 +110,7 @@ describe('The Time API', function () {
|
||||
expect(function () {
|
||||
api.timeSystem(timeSystemKey);
|
||||
}).not.toThrow();
|
||||
expect(api.timeSystem()).toBe(timeSystem);
|
||||
expect(api.timeSystem()).toEqual(timeSystem);
|
||||
});
|
||||
|
||||
it('Emits an event when time system changes', function () {
|
||||
@@ -202,12 +202,12 @@ describe('The Time API', function () {
|
||||
expect(mockTickSource.off).toHaveBeenCalledWith('tick', jasmine.any(Function));
|
||||
});
|
||||
|
||||
it('Allows the active clock to be set and unset', function () {
|
||||
xit('Allows the active clock to be set and unset', function () {
|
||||
expect(api.clock()).toBeUndefined();
|
||||
api.clock('mts', mockOffsets);
|
||||
expect(api.clock()).toBeDefined();
|
||||
api.stopClock();
|
||||
expect(api.clock()).toBeUndefined();
|
||||
// api.stopClock();
|
||||
// expect(api.clock()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Provides a default time context', () => {
|
||||
|
||||
@@ -21,8 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
|
||||
export const TIME_CONTEXT_EVENTS = ['bounds', 'clock', 'timeSystem', 'clockOffsets'];
|
||||
import { TIME_CONTEXT_EVENTS, MODES, REALTIME_MODE_KEY, FIXED_MODE_KEY } from './constants';
|
||||
|
||||
class TimeContext extends EventEmitter {
|
||||
constructor() {
|
||||
@@ -42,6 +41,7 @@ class TimeContext extends EventEmitter {
|
||||
|
||||
this.activeClock = undefined;
|
||||
this.offsets = undefined;
|
||||
this.mode = undefined;
|
||||
|
||||
this.tick = this.tick.bind(this);
|
||||
}
|
||||
@@ -56,6 +56,8 @@ class TimeContext extends EventEmitter {
|
||||
* @method timeSystem
|
||||
*/
|
||||
timeSystem(timeSystemOrKey, bounds) {
|
||||
this.#warnMethodDeprecated('"timeSystem"', '"getTimeSystem" and "setTimeSystem"');
|
||||
|
||||
if (arguments.length >= 1) {
|
||||
if (arguments.length === 1 && !this.activeClock) {
|
||||
throw new Error('Must specify bounds when changing time system without an active clock.');
|
||||
@@ -91,7 +93,7 @@ class TimeContext extends EventEmitter {
|
||||
throw 'Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key';
|
||||
}
|
||||
|
||||
this.system = timeSystem;
|
||||
this.system = this.#copy(timeSystem);
|
||||
|
||||
/**
|
||||
* The time system used by the time
|
||||
@@ -102,7 +104,10 @@ class TimeContext extends EventEmitter {
|
||||
* @property {TimeSystem} The value of the currently applied
|
||||
* Time System
|
||||
* */
|
||||
this.emit('timeSystem', this.system);
|
||||
const system = this.#copy(this.system);
|
||||
this.emit('timeSystem', system);
|
||||
this.emit(TIME_CONTEXT_EVENTS.timeSystemChanged, system);
|
||||
|
||||
if (bounds) {
|
||||
this.bounds(bounds);
|
||||
}
|
||||
@@ -163,6 +168,8 @@ class TimeContext extends EventEmitter {
|
||||
* @method bounds
|
||||
*/
|
||||
bounds(newBounds) {
|
||||
this.#warnMethodDeprecated('"bounds"', '"getBounds" and "setBounds"');
|
||||
|
||||
if (arguments.length > 0) {
|
||||
const validationResult = this.validateBounds(newBounds);
|
||||
if (validationResult.valid !== true) {
|
||||
@@ -170,7 +177,7 @@ class TimeContext extends EventEmitter {
|
||||
}
|
||||
|
||||
//Create a copy to avoid direct mutation of conductor bounds
|
||||
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
|
||||
this.boundsVal = this.#copy(newBounds);
|
||||
/**
|
||||
* The start time, end time, or both have been updated.
|
||||
* @event bounds
|
||||
@@ -180,10 +187,11 @@ class TimeContext extends EventEmitter {
|
||||
* a "tick" event (ie. was an automatic update), false otherwise.
|
||||
*/
|
||||
this.emit('bounds', this.boundsVal, false);
|
||||
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsVal, false);
|
||||
}
|
||||
|
||||
//Return a copy to prevent direct mutation of time conductor bounds.
|
||||
return JSON.parse(JSON.stringify(this.boundsVal));
|
||||
return this.#copy(this.boundsVal);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -248,6 +256,8 @@ class TimeContext extends EventEmitter {
|
||||
* @returns {ClockOffsets}
|
||||
*/
|
||||
clockOffsets(offsets) {
|
||||
this.#warnMethodDeprecated('"clockOffsets"', '"getClockOffsets" and "setClockOffsets"');
|
||||
|
||||
if (arguments.length > 0) {
|
||||
const validationResult = this.validateOffsets(offsets);
|
||||
if (validationResult.valid !== true) {
|
||||
@@ -278,20 +288,19 @@ class TimeContext extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the currently active clock from ticking, and unset it. This will
|
||||
* Stop following the currently active clock. This will
|
||||
* revert all views to showing a static time frame defined by the current
|
||||
* bounds.
|
||||
*/
|
||||
stopClock() {
|
||||
if (this.activeClock) {
|
||||
this.clock(undefined, undefined);
|
||||
}
|
||||
this.#warnMethodDeprecated('"stopClock"');
|
||||
|
||||
this.setMode(FIXED_MODE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active clock. Tick source will be immediately subscribed to
|
||||
* and ticking will begin. Offsets from 'now' must also be provided. A clock
|
||||
* can be unset by calling {@link stopClock}.
|
||||
* and ticking will begin. Offsets from 'now' must also be provided.
|
||||
*
|
||||
* @param {Clock || string} keyOrClock The clock to activate, or its key
|
||||
* @param {ClockOffsets} offsets on each tick these will be used to calculate
|
||||
@@ -301,6 +310,8 @@ class TimeContext extends EventEmitter {
|
||||
* @return {Clock} the currently active clock;
|
||||
*/
|
||||
clock(keyOrClock, offsets) {
|
||||
this.#warnMethodDeprecated('"clock"', '"getClock" and "setClock"');
|
||||
|
||||
if (arguments.length === 2) {
|
||||
let clock;
|
||||
|
||||
@@ -324,15 +335,19 @@ class TimeContext extends EventEmitter {
|
||||
this.activeClock = clock;
|
||||
|
||||
/**
|
||||
* The active clock has changed. Clock can be unset by calling {@link stopClock}
|
||||
* The active clock has changed.
|
||||
* @event clock
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
* @property {Clock} clock The newly activated clock, or undefined
|
||||
* if the system is no longer following a clock source
|
||||
*/
|
||||
this.emit('clock', this.activeClock);
|
||||
this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);
|
||||
|
||||
if (this.activeClock !== undefined) {
|
||||
//set the mode or isRealtime will be false even though we're in clock mode
|
||||
this.setMode(REALTIME_MODE_KEY);
|
||||
|
||||
this.clockOffsets(offsets);
|
||||
this.activeClock.on('tick', this.tick);
|
||||
}
|
||||
@@ -349,29 +364,304 @@ class TimeContext extends EventEmitter {
|
||||
* using current offsets.
|
||||
*/
|
||||
tick(timestamp) {
|
||||
if (!this.activeClock) {
|
||||
return;
|
||||
// always emit the timestamp
|
||||
this.emit('tick', timestamp);
|
||||
|
||||
if (this.mode === REALTIME_MODE_KEY) {
|
||||
const newBounds = {
|
||||
start: timestamp + this.offsets.start,
|
||||
end: timestamp + this.offsets.end
|
||||
};
|
||||
|
||||
this.boundsVal = newBounds;
|
||||
// "bounds" will be deprecated in a future release
|
||||
this.emit('bounds', this.boundsVal, true);
|
||||
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsVal, true);
|
||||
}
|
||||
|
||||
const newBounds = {
|
||||
start: timestamp + this.offsets.start,
|
||||
end: timestamp + this.offsets.end
|
||||
};
|
||||
|
||||
this.boundsVal = newBounds;
|
||||
this.emit('bounds', this.boundsVal, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this time context is in real-time mode or not.
|
||||
* Get the timestamp of the current clock
|
||||
* @returns {number} current timestamp of current clock regardless of mode
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method now
|
||||
*/
|
||||
|
||||
now() {
|
||||
return this.activeClock.currentValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time system of the TimeAPI.
|
||||
* @returns {TimeSystem} The currently applied time system
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method getTimeSystem
|
||||
*/
|
||||
getTimeSystem() {
|
||||
return this.system;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time system of the TimeAPI.
|
||||
* @param {TimeSystem | string} timeSystemOrKey
|
||||
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
|
||||
* @fires module:openmct.TimeAPI~timeSystem
|
||||
* @returns {TimeSystem} The currently applied time system
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method setTimeSystem
|
||||
*/
|
||||
setTimeSystem(timeSystemOrKey, bounds) {
|
||||
if (timeSystemOrKey === undefined) {
|
||||
throw 'Please provide a time system';
|
||||
}
|
||||
|
||||
let timeSystem;
|
||||
|
||||
if (typeof timeSystemOrKey === 'string') {
|
||||
timeSystem = this.timeSystems.get(timeSystemOrKey);
|
||||
|
||||
if (timeSystem === undefined) {
|
||||
throw `Unknown time system ${timeSystemOrKey}. Has it been registered with 'addTimeSystem'?`;
|
||||
}
|
||||
} else if (typeof timeSystemOrKey === 'object') {
|
||||
timeSystem = timeSystemOrKey;
|
||||
|
||||
if (!this.timeSystems.has(timeSystem.key)) {
|
||||
throw `Unknown time system ${timeSystemOrKey.key}. Has it been registered with 'addTimeSystem'?`;
|
||||
}
|
||||
} else {
|
||||
throw 'Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key';
|
||||
}
|
||||
|
||||
this.system = this.#copy(timeSystem);
|
||||
/**
|
||||
* The time system used by the time
|
||||
* conductor has changed. A change in Time System will always be
|
||||
* followed by a bounds event specifying new query bounds.
|
||||
*
|
||||
* @event module:openmct.TimeAPI~timeSystem
|
||||
* @property {TimeSystem} The value of the currently applied
|
||||
* Time System
|
||||
* */
|
||||
this.emit(TIME_CONTEXT_EVENTS.timeSystemChanged, this.#copy(this.system));
|
||||
this.emit('timeSystem', this.#copy(this.system));
|
||||
|
||||
if (bounds) {
|
||||
this.setBounds(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the start and end time of the time conductor. Basic validation
|
||||
* of bounds is performed.
|
||||
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method bounds
|
||||
*/
|
||||
getBounds() {
|
||||
//Return a copy to prevent direct mutation of time conductor bounds.
|
||||
return this.#copy(this.boundsVal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the start and end time of the time conductor. Basic validation
|
||||
* of bounds is performed.
|
||||
*
|
||||
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
|
||||
* @throws {Error} Validation error
|
||||
* @fires module:openmct.TimeAPI~bounds
|
||||
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
|
||||
* @memberof module:openmct.TimeAPI#
|
||||
* @method bounds
|
||||
*/
|
||||
setBounds(newBounds) {
|
||||
const validationResult = this.validateBounds(newBounds);
|
||||
if (validationResult.valid !== true) {
|
||||
throw new Error(validationResult.message);
|
||||
}
|
||||
|
||||
//Create a copy to avoid direct mutation of conductor bounds
|
||||
this.boundsVal = this.#copy(newBounds);
|
||||
/**
|
||||
* The start time, end time, or both have been updated.
|
||||
* @event bounds
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
* @property {TimeConductorBounds} bounds The newly updated bounds
|
||||
* @property {boolean} [tick] `true` if the bounds update was due to
|
||||
* a "tick" event (i.e. was an automatic update), false otherwise.
|
||||
*/
|
||||
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsVal, false);
|
||||
this.emit('bounds', this.boundsVal, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active clock.
|
||||
* @return {Clock} the currently active clock;
|
||||
*/
|
||||
getClock() {
|
||||
return this.activeClock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active clock. Tick source will be immediately subscribed to
|
||||
* and the currently ticking will begin.
|
||||
* Offsets from 'now', if provided, will be used to set realtime mode offsets
|
||||
*
|
||||
* @param {Clock || string} keyOrClock The clock to activate, or its key
|
||||
* @fires module:openmct.TimeAPI~clock
|
||||
* @return {Clock} the currently active clock;
|
||||
*/
|
||||
setClock(keyOrClock) {
|
||||
let clock;
|
||||
|
||||
if (typeof keyOrClock === 'string') {
|
||||
clock = this.clocks.get(keyOrClock);
|
||||
if (clock === undefined) {
|
||||
throw `Unknown clock ${keyOrClock}. Has it been registered with 'addClock'?`;
|
||||
}
|
||||
} else if (typeof keyOrClock === 'object') {
|
||||
clock = keyOrClock;
|
||||
if (!this.clocks.has(clock.key)) {
|
||||
throw `Unknown clock ${keyOrClock.key}. Has it been registered with 'addClock'?`;
|
||||
}
|
||||
}
|
||||
|
||||
const previousClock = this.activeClock;
|
||||
if (previousClock) {
|
||||
previousClock.off('tick', this.tick);
|
||||
}
|
||||
|
||||
this.activeClock = clock;
|
||||
this.activeClock.on('tick', this.tick);
|
||||
|
||||
/**
|
||||
* The active clock has changed.
|
||||
* @event clock
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
* @property {Clock} clock The newly activated clock, or undefined
|
||||
* if the system is no longer following a clock source
|
||||
*/
|
||||
this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);
|
||||
this.emit('clock', this.activeClock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current mode.
|
||||
* @return {Mode} the current mode;
|
||||
*/
|
||||
getMode() {
|
||||
return this.mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the mode to either fixed or realtime.
|
||||
*
|
||||
* @param {Mode} mode The mode to activate
|
||||
* @param {TimeBounds | ClockOffsets} offsetsOrBounds A time window of a fixed width
|
||||
* @fires module:openmct.TimeAPI~clock
|
||||
* @return {Mode} the currently active mode;
|
||||
*/
|
||||
setMode(mode, offsetsOrBounds) {
|
||||
if (!mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === MODES.realtime && this.activeClock === undefined) {
|
||||
throw `Unknown clock. Has a clock been registered with 'addClock'?`;
|
||||
}
|
||||
|
||||
if (mode !== this.mode) {
|
||||
this.mode = mode;
|
||||
/**
|
||||
* The active mode has changed.
|
||||
* @event modeChanged
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
* @property {Mode} mode The newly activated mode
|
||||
*/
|
||||
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.mode));
|
||||
}
|
||||
|
||||
if (offsetsOrBounds !== undefined) {
|
||||
if (this.isRealTime()) {
|
||||
this.setClockOffsets(offsetsOrBounds);
|
||||
} else {
|
||||
this.setBounds(offsetsOrBounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this time context is in realtime mode or not.
|
||||
* @returns {boolean} true if this context is in real-time mode, false if not
|
||||
*/
|
||||
isRealTime() {
|
||||
if (this.clock()) {
|
||||
return true;
|
||||
return this.mode === MODES.realtime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this time context is in fixed mode or not.
|
||||
* @returns {boolean} true if this context is in fixed mode, false if not
|
||||
*/
|
||||
isFixed() {
|
||||
return this.mode === MODES.fixed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently applied clock offsets.
|
||||
* @returns {ClockOffsets}
|
||||
*/
|
||||
getClockOffsets() {
|
||||
return this.offsets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the currently applied clock offsets. If no parameter is provided,
|
||||
* the current value will be returned. If provided, the new value will be
|
||||
* used as the new clock offsets.
|
||||
* @param {ClockOffsets} offsets
|
||||
* @returns {ClockOffsets}
|
||||
*/
|
||||
setClockOffsets(offsets) {
|
||||
const validationResult = this.validateOffsets(offsets);
|
||||
if (validationResult.valid !== true) {
|
||||
throw new Error(validationResult.message);
|
||||
}
|
||||
|
||||
return false;
|
||||
this.offsets = this.#copy(offsets);
|
||||
|
||||
const currentValue = this.activeClock.currentValue();
|
||||
const newBounds = {
|
||||
start: currentValue + offsets.start,
|
||||
end: currentValue + offsets.end
|
||||
};
|
||||
|
||||
this.setBounds(newBounds);
|
||||
|
||||
/**
|
||||
* Event that is triggered when clock offsets change.
|
||||
* @event clockOffsets
|
||||
* @memberof module:openmct.TimeAPI~
|
||||
* @property {ClockOffsets} clockOffsets The newly activated clock
|
||||
* offsets.
|
||||
*/
|
||||
this.emit(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.#copy(offsets));
|
||||
}
|
||||
|
||||
#warnMethodDeprecated(method, newMethod) {
|
||||
let message = `[DEPRECATION WARNING]: The ${method} API method is deprecated and will be removed in a future version of Open MCT.`;
|
||||
|
||||
if (newMethod) {
|
||||
message += ` Please use the ${newMethod} API method(s) instead.`;
|
||||
}
|
||||
|
||||
// TODO: add docs and point to them in warning.
|
||||
// For more information and migration instructions, visit [link to documentation or migration guide].
|
||||
|
||||
console.warn(message);
|
||||
}
|
||||
|
||||
#copy(object) {
|
||||
return JSON.parse(JSON.stringify(object));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
22
src/api/time/constants.js
Normal file
22
src/api/time/constants.js
Normal file
@@ -0,0 +1,22 @@
|
||||
export const TIME_CONTEXT_EVENTS = {
|
||||
//old API events - to be deprecated
|
||||
bounds: 'bounds',
|
||||
clock: 'clock',
|
||||
timeSystem: 'timeSystem',
|
||||
clockOffsets: 'clockOffsets',
|
||||
//new API events
|
||||
tick: 'tick',
|
||||
modeChanged: 'modeChanged',
|
||||
boundsChanged: 'boundsChanged',
|
||||
clockChanged: 'clockChanged',
|
||||
timeSystemChanged: 'timeSystemChanged',
|
||||
clockOffsetsChanged: 'clockOffsetsChanged'
|
||||
};
|
||||
|
||||
export const REALTIME_MODE_KEY = 'realtime';
|
||||
export const FIXED_MODE_KEY = 'fixed';
|
||||
|
||||
export const MODES = {
|
||||
[FIXED_MODE_KEY]: FIXED_MODE_KEY,
|
||||
[REALTIME_MODE_KEY]: REALTIME_MODE_KEY
|
||||
};
|
||||
73
src/api/tooltips/ToolTip.js
Normal file
73
src/api/tooltips/ToolTip.js
Normal file
@@ -0,0 +1,73 @@
|
||||
/*****************************************************************************
|
||||
* 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 TooltipComponent from './components/TooltipComponent.vue';
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import Vue from 'vue';
|
||||
|
||||
class Tooltip extends EventEmitter {
|
||||
constructor(
|
||||
{ toolTipText, toolTipLocation, parentElement } = {
|
||||
tooltipText: '',
|
||||
toolTipLocation: 'below',
|
||||
parentElement: null
|
||||
}
|
||||
) {
|
||||
super();
|
||||
|
||||
this.container = document.createElement('div');
|
||||
|
||||
this.component = new Vue({
|
||||
components: {
|
||||
TooltipComponent: TooltipComponent
|
||||
},
|
||||
provide: {
|
||||
toolTipText,
|
||||
toolTipLocation,
|
||||
parentElement
|
||||
},
|
||||
template: '<tooltip-component toolTipText="toolTipText"></tooltip-component>'
|
||||
});
|
||||
|
||||
this.isActive = null;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (!this.isActive) {
|
||||
return;
|
||||
}
|
||||
document.body.removeChild(this.container);
|
||||
this.component.$destroy();
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
**/
|
||||
show() {
|
||||
document.body.appendChild(this.container);
|
||||
this.container.appendChild(this.component.$mount().$el);
|
||||
this.isActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
export default Tooltip;
|
||||
90
src/api/tooltips/ToolTipAPI.js
Normal file
90
src/api/tooltips/ToolTipAPI.js
Normal file
@@ -0,0 +1,90 @@
|
||||
/*****************************************************************************
|
||||
* 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 Tooltip from './ToolTip';
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
* @enum {String} TooltipLocation
|
||||
* @property {String} ABOVE The string for locating tooltips above an element
|
||||
* @property {String} BELOW The string for locating tooltips below an element
|
||||
* @property {String} RIGHT The pixel-spatial annotation type
|
||||
* @property {String} LEFT The temporal annotation type
|
||||
* @property {String} CENTER The plot-spatial annotation type
|
||||
*/
|
||||
const TOOLTIP_LOCATIONS = Object.freeze({
|
||||
ABOVE: 'above',
|
||||
BELOW: 'below',
|
||||
RIGHT: 'right',
|
||||
LEFT: 'left',
|
||||
CENTER: 'center'
|
||||
});
|
||||
|
||||
/**
|
||||
* The TooltipAPI is responsible for adding custom tooltips to
|
||||
* the desired elements on the screen
|
||||
*
|
||||
* @memberof api/tooltips
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
class TooltipAPI {
|
||||
constructor() {
|
||||
this.activeToolTips = [];
|
||||
this.TOOLTIP_LOCATIONS = TOOLTIP_LOCATIONS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private for platform-internal use
|
||||
*/
|
||||
showTooltip(tooltip) {
|
||||
for (let i = this.activeToolTips.length - 1; i > -1; i--) {
|
||||
this.activeToolTips[i].destroy();
|
||||
this.activeToolTips.splice(i, 1);
|
||||
}
|
||||
this.activeToolTips.push(tooltip);
|
||||
|
||||
tooltip.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* A description of option properties that can be passed into the tooltip
|
||||
* @typedef {Object} TooltipOptions
|
||||
* @property {string} tooltipText text to show in the tooltip
|
||||
* @property {TOOLTIP_LOCATIONS} tooltipLocation location to show the tooltip relative to the parentElement
|
||||
* @property {HTMLElement} parentElement reference to the DOM node we're adding the tooltip to
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tooltips take an options object that consists of the string, tooltipLocation, and parentElement
|
||||
* @param {TooltipOptions} options
|
||||
*/
|
||||
tooltip(options) {
|
||||
let tooltip = new Tooltip(options);
|
||||
|
||||
this.showTooltip(tooltip);
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
}
|
||||
|
||||
export default TooltipAPI;
|
||||
61
src/api/tooltips/components/TooltipComponent.vue
Normal file
61
src/api/tooltips/components/TooltipComponent.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<!--
|
||||
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 ref="tooltip-wrapper" class="c-menu c-tooltip-wrapper" :style="toolTipLocationStyle">
|
||||
<div class="c-tooltip">
|
||||
{{ toolTipText }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inject: ['toolTipText', 'toolTipLocation', 'parentElement'],
|
||||
computed: {
|
||||
toolTipCoordinates() {
|
||||
return this.parentElement.getBoundingClientRect();
|
||||
},
|
||||
toolTipLocationStyle() {
|
||||
const { top, left, height, width } = this.toolTipCoordinates;
|
||||
let toolTipLocationStyle = {};
|
||||
|
||||
if (this.toolTipLocation === 'above') {
|
||||
toolTipLocationStyle = { top: `${top - 5}px`, left: `${left}px` };
|
||||
}
|
||||
if (this.toolTipLocation === 'below') {
|
||||
toolTipLocationStyle = { top: `${top + height}px`, left: `${left}px` };
|
||||
}
|
||||
if (this.toolTipLocation === 'right') {
|
||||
toolTipLocationStyle = { top: `${top}px`, left: `${left + width}px` };
|
||||
}
|
||||
if (this.toolTipLocation === 'left') {
|
||||
toolTipLocationStyle = { top: `${top}px`, left: `${left - width}px` };
|
||||
}
|
||||
if (this.toolTipLocation === 'center') {
|
||||
toolTipLocationStyle = { top: `${top + height / 2}px`, left: `${left + width / 2}px` };
|
||||
}
|
||||
|
||||
return toolTipLocationStyle;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
8
src/api/tooltips/components/tooltip-component.scss
Normal file
8
src/api/tooltips/components/tooltip-component.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
.c-tooltip-wrapper {
|
||||
max-width: 200px;
|
||||
padding: $interiorMargin;
|
||||
}
|
||||
|
||||
.c-tooltip {
|
||||
font-style: italic;
|
||||
}
|
||||
72
src/api/tooltips/tooltipMixins.js
Normal file
72
src/api/tooltips/tooltipMixins.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
const tooltipHelpers = {
|
||||
methods: {
|
||||
async getTelemetryPathString(telemetryIdentifier) {
|
||||
let telemetryPathString = '';
|
||||
if (!this.domainObject?.identifier) {
|
||||
return;
|
||||
}
|
||||
const telemetryPath = await this.openmct.objects.getTelemetryPath(
|
||||
this.domainObject.identifier,
|
||||
telemetryIdentifier
|
||||
);
|
||||
if (telemetryPath.length) {
|
||||
telemetryPathString = telemetryPath.join(' / ');
|
||||
}
|
||||
return telemetryPathString;
|
||||
},
|
||||
async getObjectPath(objectIdentifier) {
|
||||
if (!objectIdentifier && !this.domainObject) {
|
||||
return;
|
||||
}
|
||||
const domainObjectIdentifier = objectIdentifier || this.domainObject.identifier;
|
||||
const objectPathList = await this.openmct.objects.getOriginalPath(domainObjectIdentifier);
|
||||
objectPathList.pop();
|
||||
return objectPathList
|
||||
.map((pathItem) => pathItem.name)
|
||||
.reverse()
|
||||
.join(' / ');
|
||||
},
|
||||
buildToolTip(tooltipText, tooltipLocation, elementRef) {
|
||||
if (!tooltipText || tooltipText.length < 1) {
|
||||
return;
|
||||
}
|
||||
let parentElement = this.$refs[elementRef];
|
||||
if (Array.isArray(parentElement)) {
|
||||
parentElement = parentElement[0];
|
||||
}
|
||||
this.tooltip = this.openmct.tooltips.tooltip({
|
||||
toolTipText: tooltipText,
|
||||
toolTipLocation: tooltipLocation,
|
||||
parentElement: parentElement
|
||||
});
|
||||
},
|
||||
hideToolTip() {
|
||||
this.tooltip?.destroy();
|
||||
this.tooltip = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default tooltipHelpers;
|
||||
37
src/api/user/ActiveRoleSynchronizer.js
Normal file
37
src/api/user/ActiveRoleSynchronizer.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ACTIVE_ROLE_BROADCAST_CHANNEL_NAME } from './constants';
|
||||
|
||||
class ActiveRoleSynchronizer {
|
||||
#roleChannel;
|
||||
|
||||
constructor(openmct) {
|
||||
this.openmct = openmct;
|
||||
this.#roleChannel = new BroadcastChannel(ACTIVE_ROLE_BROADCAST_CHANNEL_NAME);
|
||||
this.setActiveRoleFromChannelMessage = this.setActiveRoleFromChannelMessage.bind(this);
|
||||
|
||||
this.subscribeToRoleChanges(this.setActiveRoleFromChannelMessage);
|
||||
}
|
||||
subscribeToRoleChanges(callback) {
|
||||
this.#roleChannel.addEventListener('message', callback);
|
||||
}
|
||||
unsubscribeFromRoleChanges(callback) {
|
||||
this.#roleChannel.removeEventListener('message', callback);
|
||||
}
|
||||
|
||||
setActiveRoleFromChannelMessage(event) {
|
||||
const role = event.data;
|
||||
this.openmct.user.setActiveRole(role);
|
||||
}
|
||||
broadcastNewRole(role) {
|
||||
if (!this.#roleChannel.name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.#roleChannel.postMessage(role);
|
||||
}
|
||||
destroy() {
|
||||
this.unsubscribeFromRoleChanges(this.setActiveRoleFromChannelMessage);
|
||||
this.#roleChannel.close();
|
||||
}
|
||||
}
|
||||
|
||||
export default ActiveRoleSynchronizer;
|
||||
@@ -140,9 +140,9 @@ export default class StatusAPI extends EventEmitter {
|
||||
const provider = this.#userAPI.getProvider();
|
||||
|
||||
if (provider.canProvideStatusForRole) {
|
||||
return provider.canProvideStatusForRole(role);
|
||||
return Promise.resolve(provider.canProvideStatusForRole(role));
|
||||
} else {
|
||||
return false;
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,11 +151,16 @@ export default class StatusAPI extends EventEmitter {
|
||||
* @param {Status} status The status to set for the provided role
|
||||
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
|
||||
*/
|
||||
setStatusForRole(role, status) {
|
||||
setStatusForRole(status) {
|
||||
const provider = this.#userAPI.getProvider();
|
||||
|
||||
if (provider.setStatusForRole) {
|
||||
return provider.setStatusForRole(role, status);
|
||||
const activeRole = this.#userAPI.getActiveRole();
|
||||
if (!provider.canProvideStatusForRole(activeRole)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return provider.setStatusForRole(activeRole, status);
|
||||
} else {
|
||||
this.#userAPI.error('User provider does not support setting role status');
|
||||
}
|
||||
@@ -216,21 +221,6 @@ export default class StatusAPI extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The status role of the current user. A user may have multiple roles, but will only have one role
|
||||
* that provides status at any time.
|
||||
* @returns {Promise<import("./UserAPI").Role>} the role for which the current user can provide status.
|
||||
*/
|
||||
getStatusRoleForCurrentUser() {
|
||||
const provider = this.#userAPI.getProvider();
|
||||
|
||||
if (provider.getStatusRoleForCurrentUser) {
|
||||
return provider.getStatusRoleForCurrentUser();
|
||||
} else {
|
||||
this.#userAPI.error('User provider cannot provide role status for this user');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the currently logged in user, false otherwise.
|
||||
* @see StatusUserProvider
|
||||
@@ -238,14 +228,13 @@ export default class StatusAPI extends EventEmitter {
|
||||
async canProvideStatusForCurrentUser() {
|
||||
const provider = this.#userAPI.getProvider();
|
||||
|
||||
if (provider.getStatusRoleForCurrentUser) {
|
||||
const activeStatusRole = await this.#userAPI.getProvider().getStatusRoleForCurrentUser();
|
||||
const canProvideStatus = await this.canProvideStatusForRole(activeStatusRole);
|
||||
|
||||
return canProvideStatus;
|
||||
} else {
|
||||
if (!provider) {
|
||||
return false;
|
||||
}
|
||||
const activeStatusRole = await this.#userAPI.getActiveRole();
|
||||
const canProvideStatus = await this.canProvideStatusForRole(activeStatusRole);
|
||||
|
||||
return canProvideStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -77,5 +77,4 @@ export default class StatusUserProvider extends UserProvider {
|
||||
/**
|
||||
* @returns {Promise<import("./UserAPI").Role>} the active status role for the currently logged in user
|
||||
*/
|
||||
async getStatusRoleForCurrentUser() {}
|
||||
}
|
||||
|
||||
37
src/api/user/StoragePersistance.js
Normal file
37
src/api/user/StoragePersistance.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.
|
||||
*****************************************************************************/
|
||||
|
||||
import { ACTIVE_ROLE_LOCAL_STORAGE_KEY } from './constants';
|
||||
|
||||
class StoragePersistance {
|
||||
getActiveRole() {
|
||||
return localStorage.getItem(ACTIVE_ROLE_LOCAL_STORAGE_KEY);
|
||||
}
|
||||
setActiveRole(role) {
|
||||
return localStorage.setItem(ACTIVE_ROLE_LOCAL_STORAGE_KEY, role);
|
||||
}
|
||||
clearActiveRole() {
|
||||
return localStorage.removeItem(ACTIVE_ROLE_LOCAL_STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
export default new StoragePersistance();
|
||||
@@ -24,6 +24,7 @@ import EventEmitter from 'EventEmitter';
|
||||
import { MULTIPLE_PROVIDER_ERROR, NO_PROVIDER_ERROR } from './constants';
|
||||
import StatusAPI from './StatusAPI';
|
||||
import User from './User';
|
||||
import StoragePersistance from './StoragePersistance';
|
||||
|
||||
class UserAPI extends EventEmitter {
|
||||
/**
|
||||
@@ -86,6 +87,58 @@ class UserAPI extends EventEmitter {
|
||||
return this._provider.getCurrentUser();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* If a user provider is set, it will return an array of possible roles
|
||||
* that can be selected by the current user
|
||||
* @memberof module:openmct.UserAPI#
|
||||
* @returns {Array}
|
||||
* @throws Will throw an error if no user provider is set
|
||||
*/
|
||||
|
||||
getPossibleRoles() {
|
||||
if (!this.hasProvider()) {
|
||||
this.error(NO_PROVIDER_ERROR);
|
||||
}
|
||||
return this._provider.getPossibleRoles();
|
||||
}
|
||||
/**
|
||||
* If a user provider is set, it will return the active role or null
|
||||
* @memberof module:openmct.UserAPI#
|
||||
* @returns {string|null}
|
||||
*/
|
||||
getActiveRole() {
|
||||
if (!this.hasProvider()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// get from session storage
|
||||
const sessionStorageValue = StoragePersistance.getActiveRole();
|
||||
|
||||
return sessionStorageValue;
|
||||
}
|
||||
/**
|
||||
* Set the active role in session storage
|
||||
* @memberof module:openmct.UserAPI#
|
||||
* @returns {undefined}
|
||||
*/
|
||||
setActiveRole(role) {
|
||||
StoragePersistance.setActiveRole(role);
|
||||
this.emit('roleChanged', role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Will return if a role can provide a operator status response
|
||||
* @memberof module:openmct.UserApi#
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
canProvideStatusForRole() {
|
||||
if (!this.hasProvider()) {
|
||||
return null;
|
||||
}
|
||||
const activeRole = this.getActiveRole();
|
||||
|
||||
return this._provider.canProvideStatusForRole?.(activeRole);
|
||||
}
|
||||
|
||||
/**
|
||||
* If a user provider is set, it will return the user provider's
|
||||
|
||||
@@ -25,7 +25,7 @@ import { MULTIPLE_PROVIDER_ERROR } from './constants';
|
||||
import ExampleUserProvider from '../../../example/exampleUser/ExampleUserProvider';
|
||||
|
||||
const USERNAME = 'Test User';
|
||||
const EXAMPLE_ROLE = 'example-role';
|
||||
const EXAMPLE_ROLE = 'flight';
|
||||
|
||||
describe('The User API', () => {
|
||||
let openmct;
|
||||
|
||||
@@ -22,3 +22,6 @@
|
||||
|
||||
export const MULTIPLE_PROVIDER_ERROR = 'Only one user provider may be set at a time.';
|
||||
export const NO_PROVIDER_ERROR = 'No user provider has been set.';
|
||||
|
||||
export const ACTIVE_ROLE_LOCAL_STORAGE_KEY = 'ACTIVE_USER_ROLE';
|
||||
export const ACTIVE_ROLE_BROADCAST_CHANNEL_NAME = 'ActiveRoleChannel';
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
export default function (folderName, couchPlugin, searchFilter) {
|
||||
const DEFAULT_NAME = 'CouchDB Documents';
|
||||
|
||||
return function install(openmct) {
|
||||
const couchProvider = couchPlugin.couchProvider;
|
||||
//replace any non-letter/non-number with a hyphen
|
||||
const couchSearchId = (folderName || DEFAULT_NAME).replace(/[^a-zA-Z0-9]/g, '-');
|
||||
const couchSearchName = `couch-search-${couchSearchId}`;
|
||||
|
||||
openmct.objects.addRoot({
|
||||
namespace: 'couch-search',
|
||||
key: 'couch-search'
|
||||
namespace: couchSearchName,
|
||||
key: couchSearchName
|
||||
});
|
||||
|
||||
openmct.objects.addProvider('couch-search', {
|
||||
openmct.objects.addProvider(couchSearchName, {
|
||||
get(identifier) {
|
||||
if (identifier.key !== 'couch-search') {
|
||||
if (identifier.key !== couchSearchName) {
|
||||
return undefined;
|
||||
} else {
|
||||
return Promise.resolve({
|
||||
identifier,
|
||||
type: 'folder',
|
||||
name: folderName || 'CouchDB Documents',
|
||||
name: folderName || DEFAULT_NAME,
|
||||
location: 'ROOT'
|
||||
});
|
||||
}
|
||||
@@ -25,8 +30,8 @@ export default function (folderName, couchPlugin, searchFilter) {
|
||||
openmct.composition.addProvider({
|
||||
appliesTo(domainObject) {
|
||||
return (
|
||||
domainObject.identifier.namespace === 'couch-search' &&
|
||||
domainObject.identifier.key === 'couch-search'
|
||||
domainObject.identifier.namespace === couchSearchName &&
|
||||
domainObject.identifier.key === couchSearchName
|
||||
);
|
||||
},
|
||||
load() {
|
||||
|
||||
@@ -25,8 +25,8 @@ import CouchDBSearchFolderPlugin from './plugin';
|
||||
|
||||
describe('the plugin', function () {
|
||||
let identifier = {
|
||||
namespace: 'couch-search',
|
||||
key: 'couch-search'
|
||||
namespace: 'couch-search-CouchDB-Documents',
|
||||
key: 'couch-search-CouchDB-Documents'
|
||||
};
|
||||
let testPath = '/test/db';
|
||||
let openmct;
|
||||
|
||||
@@ -26,7 +26,14 @@
|
||||
@click="clickedRow"
|
||||
@contextmenu.prevent="showContextMenu"
|
||||
>
|
||||
<td class="js-first-data">{{ domainObject.name }}</td>
|
||||
<td
|
||||
ref="tableCell"
|
||||
class="js-first-data"
|
||||
@mouseover.ctrl="showToolTip"
|
||||
@mouseleave="hideToolTip"
|
||||
>
|
||||
{{ domainObject.name }}
|
||||
</td>
|
||||
<td v-if="showTimestamp" class="js-second-data">{{ formattedTimestamp }}</td>
|
||||
<td class="js-third-data" :class="valueClasses">{{ value }}</td>
|
||||
<td v-if="hasUnits" class="js-units">
|
||||
@@ -42,8 +49,10 @@ const BLANK_VALUE = '---';
|
||||
|
||||
import identifierToString from '/src/tools/url';
|
||||
import PreviewAction from '@/ui/preview/PreviewAction.js';
|
||||
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
|
||||
|
||||
export default {
|
||||
mixins: [tooltipHelpers],
|
||||
inject: ['openmct', 'currentView'],
|
||||
props: {
|
||||
domainObject: {
|
||||
@@ -259,6 +268,10 @@ export default {
|
||||
return metadata
|
||||
.values()
|
||||
.find((metadatum) => metadatum.hints.domain === undefined && metadatum.key !== 'name');
|
||||
},
|
||||
async showToolTip() {
|
||||
const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;
|
||||
this.buildToolTip(await this.getObjectPath(), BELOW, 'tableCell');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -264,7 +264,7 @@ describe('The LAD Table', () => {
|
||||
});
|
||||
|
||||
it('should show the name provided for the the telemetry producing object', () => {
|
||||
const rowName = parent.querySelector(TABLE_BODY_FIRST_ROW_FIRST_DATA).innerText;
|
||||
const rowName = parent.querySelector(TABLE_BODY_FIRST_ROW_FIRST_DATA).innerText.trim();
|
||||
|
||||
const expectedName = mockObj.telemetry.name;
|
||||
expect(rowName).toBe(expectedName);
|
||||
|
||||
@@ -20,14 +20,20 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const TIME_EVENTS = ['timeSystem', 'clock', 'clockOffsets'];
|
||||
import { FIXED_MODE_KEY, REALTIME_MODE_KEY, TIME_CONTEXT_EVENTS } from '../../api/time/constants';
|
||||
|
||||
const SEARCH_MODE = 'tc.mode';
|
||||
const SEARCH_TIME_SYSTEM = 'tc.timeSystem';
|
||||
const SEARCH_START_BOUND = 'tc.startBound';
|
||||
const SEARCH_END_BOUND = 'tc.endBound';
|
||||
const SEARCH_START_DELTA = 'tc.startDelta';
|
||||
const SEARCH_END_DELTA = 'tc.endDelta';
|
||||
const MODE_FIXED = 'fixed';
|
||||
const TIME_EVENTS = [
|
||||
TIME_CONTEXT_EVENTS.timeSystemChanged,
|
||||
TIME_CONTEXT_EVENTS.modeChanged,
|
||||
TIME_CONTEXT_EVENTS.clockChanged,
|
||||
TIME_CONTEXT_EVENTS.clockOffsetsChanged
|
||||
];
|
||||
|
||||
export default class URLTimeSettingsSynchronizer {
|
||||
constructor(openmct) {
|
||||
@@ -67,7 +73,7 @@ export default class URLTimeSettingsSynchronizer {
|
||||
}
|
||||
|
||||
updateTimeSettings() {
|
||||
let timeParameters = this.parseParametersFromUrl();
|
||||
const timeParameters = this.parseParametersFromUrl();
|
||||
|
||||
if (this.areTimeParametersValid(timeParameters)) {
|
||||
this.setTimeApiFromUrl(timeParameters);
|
||||
@@ -78,21 +84,18 @@ export default class URLTimeSettingsSynchronizer {
|
||||
}
|
||||
|
||||
parseParametersFromUrl() {
|
||||
let searchParams = this.openmct.router.getAllSearchParams();
|
||||
|
||||
let mode = searchParams.get(SEARCH_MODE);
|
||||
let timeSystem = searchParams.get(SEARCH_TIME_SYSTEM);
|
||||
|
||||
let startBound = parseInt(searchParams.get(SEARCH_START_BOUND), 10);
|
||||
let endBound = parseInt(searchParams.get(SEARCH_END_BOUND), 10);
|
||||
let bounds = {
|
||||
const searchParams = this.openmct.router.getAllSearchParams();
|
||||
const mode = searchParams.get(SEARCH_MODE);
|
||||
const timeSystem = searchParams.get(SEARCH_TIME_SYSTEM);
|
||||
const startBound = parseInt(searchParams.get(SEARCH_START_BOUND), 10);
|
||||
const endBound = parseInt(searchParams.get(SEARCH_END_BOUND), 10);
|
||||
const bounds = {
|
||||
start: startBound,
|
||||
end: endBound
|
||||
};
|
||||
|
||||
let startOffset = parseInt(searchParams.get(SEARCH_START_DELTA), 10);
|
||||
let endOffset = parseInt(searchParams.get(SEARCH_END_DELTA), 10);
|
||||
let clockOffsets = {
|
||||
const startOffset = parseInt(searchParams.get(SEARCH_START_DELTA), 10);
|
||||
const endOffset = parseInt(searchParams.get(SEARCH_END_DELTA), 10);
|
||||
const clockOffsets = {
|
||||
start: 0 - startOffset,
|
||||
end: endOffset
|
||||
};
|
||||
@@ -106,30 +109,35 @@ export default class URLTimeSettingsSynchronizer {
|
||||
}
|
||||
|
||||
setTimeApiFromUrl(timeParameters) {
|
||||
if (timeParameters.mode === 'fixed') {
|
||||
if (this.openmct.time.timeSystem().key !== timeParameters.timeSystem) {
|
||||
this.openmct.time.timeSystem(timeParameters.timeSystem, timeParameters.bounds);
|
||||
} else if (!this.areStartAndEndEqual(this.openmct.time.bounds(), timeParameters.bounds)) {
|
||||
this.openmct.time.bounds(timeParameters.bounds);
|
||||
}
|
||||
const timeSystem = this.openmct.time.getTimeSystem();
|
||||
|
||||
if (this.openmct.time.clock()) {
|
||||
this.openmct.time.stopClock();
|
||||
if (timeParameters.mode === FIXED_MODE_KEY) {
|
||||
// should update timesystem
|
||||
if (timeSystem.key !== timeParameters.timeSystem) {
|
||||
this.openmct.time.setTimeSystem(timeParameters.timeSystem, timeParameters.bounds);
|
||||
}
|
||||
if (!this.areStartAndEndEqual(this.openmct.time.getBounds(), timeParameters.bounds)) {
|
||||
this.openmct.time.setMode(FIXED_MODE_KEY, timeParameters.bounds);
|
||||
} else {
|
||||
this.openmct.time.setMode(FIXED_MODE_KEY);
|
||||
}
|
||||
} else {
|
||||
if (!this.openmct.time.clock() || this.openmct.time.clock().key !== timeParameters.mode) {
|
||||
this.openmct.time.clock(timeParameters.mode, timeParameters.clockOffsets);
|
||||
} else if (
|
||||
!this.areStartAndEndEqual(this.openmct.time.clockOffsets(), timeParameters.clockOffsets)
|
||||
) {
|
||||
this.openmct.time.clockOffsets(timeParameters.clockOffsets);
|
||||
const clock = this.openmct.time.getClock();
|
||||
|
||||
if (clock?.key !== timeParameters.mode) {
|
||||
this.openmct.time.setClock(timeParameters.mode);
|
||||
}
|
||||
|
||||
if (
|
||||
!this.openmct.time.timeSystem() ||
|
||||
this.openmct.time.timeSystem().key !== timeParameters.timeSystem
|
||||
!this.areStartAndEndEqual(this.openmct.time.getClockOffsets(), timeParameters.clockOffsets)
|
||||
) {
|
||||
this.openmct.time.timeSystem(timeParameters.timeSystem);
|
||||
this.openmct.time.setMode(REALTIME_MODE_KEY, timeParameters.clockOffsets);
|
||||
} else {
|
||||
this.openmct.time.setMode(REALTIME_MODE_KEY);
|
||||
}
|
||||
|
||||
if (timeSystem?.key !== timeParameters.timeSystem) {
|
||||
this.openmct.time.setTimeSystem(timeParameters.timeSystem);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,13 +149,14 @@ export default class URLTimeSettingsSynchronizer {
|
||||
}
|
||||
|
||||
setUrlFromTimeApi() {
|
||||
let searchParams = this.openmct.router.getAllSearchParams();
|
||||
let clock = this.openmct.time.clock();
|
||||
let bounds = this.openmct.time.bounds();
|
||||
let clockOffsets = this.openmct.time.clockOffsets();
|
||||
const searchParams = this.openmct.router.getAllSearchParams();
|
||||
const clock = this.openmct.time.getClock();
|
||||
const mode = this.openmct.time.getMode();
|
||||
const bounds = this.openmct.time.getBounds();
|
||||
const clockOffsets = this.openmct.time.getClockOffsets();
|
||||
|
||||
if (clock === undefined) {
|
||||
searchParams.set(SEARCH_MODE, MODE_FIXED);
|
||||
if (mode === FIXED_MODE_KEY) {
|
||||
searchParams.set(SEARCH_MODE, FIXED_MODE_KEY);
|
||||
searchParams.set(SEARCH_START_BOUND, bounds.start);
|
||||
searchParams.set(SEARCH_END_BOUND, bounds.end);
|
||||
|
||||
@@ -168,8 +177,8 @@ export default class URLTimeSettingsSynchronizer {
|
||||
searchParams.delete(SEARCH_END_BOUND);
|
||||
}
|
||||
|
||||
searchParams.set(SEARCH_TIME_SYSTEM, this.openmct.time.timeSystem().key);
|
||||
this.openmct.router.setAllSearchParams(searchParams);
|
||||
searchParams.set(SEARCH_TIME_SYSTEM, this.openmct.time.getTimeSystem().key);
|
||||
this.openmct.router.updateParams(searchParams);
|
||||
}
|
||||
|
||||
areTimeParametersValid(timeParameters) {
|
||||
@@ -179,7 +188,7 @@ export default class URLTimeSettingsSynchronizer {
|
||||
this.isModeValid(timeParameters.mode) &&
|
||||
this.isTimeSystemValid(timeParameters.timeSystem)
|
||||
) {
|
||||
if (timeParameters.mode === 'fixed') {
|
||||
if (timeParameters.mode === FIXED_MODE_KEY) {
|
||||
isValid = this.areStartAndEndValid(timeParameters.bounds);
|
||||
} else {
|
||||
isValid = this.areStartAndEndValid(timeParameters.clockOffsets);
|
||||
@@ -203,8 +212,9 @@ export default class URLTimeSettingsSynchronizer {
|
||||
|
||||
isTimeSystemValid(timeSystem) {
|
||||
let isValid = timeSystem !== undefined;
|
||||
|
||||
if (isValid) {
|
||||
let timeSystemObject = this.openmct.time.timeSystems.get(timeSystem);
|
||||
const timeSystemObject = this.openmct.time.timeSystems.get(timeSystem);
|
||||
isValid = timeSystemObject !== undefined;
|
||||
}
|
||||
|
||||
@@ -218,18 +228,17 @@ export default class URLTimeSettingsSynchronizer {
|
||||
isValid = true;
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
if (mode.toLowerCase() === MODE_FIXED) {
|
||||
isValid = true;
|
||||
} else {
|
||||
isValid = this.openmct.time.clocks.get(mode) !== undefined;
|
||||
}
|
||||
if (
|
||||
isValid &&
|
||||
(mode.toLowerCase() === FIXED_MODE_KEY || this.openmct.time.clocks.get(mode) !== undefined)
|
||||
) {
|
||||
isValid = true;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
areStartAndEndEqual(firstBounds, secondBounds) {
|
||||
return firstBounds.start === secondBounds.start && firstBounds.end === secondBounds.end;
|
||||
return firstBounds?.start === secondBounds.start && firstBounds?.end === secondBounds.end;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ describe('The URLTimeSettingsSynchronizer', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
openmct.time.stopClock();
|
||||
openmct.router.removeListener('change:hash', resolveFunction);
|
||||
|
||||
appHolder = undefined;
|
||||
|
||||
@@ -41,13 +41,13 @@
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import momentTimezone from 'moment-timezone';
|
||||
import ticker from 'utils/clock/Ticker';
|
||||
import raf from 'utils/raf';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
return {
|
||||
lastTimestamp: null
|
||||
lastTimestamp: this.openmct.time.now()
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -85,12 +85,11 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.unlisten = ticker.listen(this.tick);
|
||||
this.tick = raf(this.tick);
|
||||
this.openmct.time.on('tick', this.tick);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
this.openmct.time.off('tick', this.tick);
|
||||
},
|
||||
methods: {
|
||||
tick(timestamp) {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import ticker from 'utils/clock/Ticker';
|
||||
import raf from 'utils/raf';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
@@ -42,20 +42,22 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
timeTextValue: null
|
||||
timeTextValue: this.openmct.time.now()
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.unlisten = ticker.listen(this.tick);
|
||||
this.tick = raf(this.tick);
|
||||
this.openmct.time.on('tick', this.tick);
|
||||
this.tick(this.timeTextValue);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
this.openmct.time.off('tick', this.tick);
|
||||
},
|
||||
methods: {
|
||||
tick(timestamp) {
|
||||
this.timeTextValue = `${moment.utc(timestamp).format(this.indicatorFormat)} UTC`;
|
||||
this.timeTextValue = `${moment.utc(timestamp).format(this.indicatorFormat)} ${
|
||||
this.openmct.time.getTimeSystem().name
|
||||
}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -98,6 +98,7 @@ describe('Clock plugin:', () => {
|
||||
clockView.show(child);
|
||||
|
||||
await Vue.nextTick();
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -222,10 +223,12 @@ describe('Clock plugin:', () => {
|
||||
it('contains text', async () => {
|
||||
await setupClock(true);
|
||||
|
||||
await Vue.nextTick();
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
|
||||
clockIndicator = openmct.indicators.indicatorObjects.find(
|
||||
(indicator) => indicator.key === 'clock-indicator'
|
||||
).element;
|
||||
|
||||
const clockIndicatorText = clockIndicator.textContent.trim();
|
||||
const textIncludesUTC = clockIndicatorText.includes('UTC');
|
||||
|
||||
|
||||
@@ -21,7 +21,12 @@
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div ref="conditionWidgetElement" class="c-condition-widget u-style-receiver js-style-receiver">
|
||||
<div
|
||||
ref="conditionWidgetElement"
|
||||
class="c-condition-widget u-style-receiver js-style-receiver"
|
||||
@mouseover.ctrl="showToolTip"
|
||||
@mouseleave="hideToolTip"
|
||||
>
|
||||
<component :is="urlDefined ? 'a' : 'div'" class="c-condition-widget__label-wrapper" :href="url">
|
||||
<div class="c-condition-widget__label">{{ label }}</div>
|
||||
</component>
|
||||
@@ -29,9 +34,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
|
||||
const sanitizeUrl = require('@braintree/sanitize-url').sanitizeUrl;
|
||||
|
||||
export default {
|
||||
mixins: [tooltipHelpers],
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data: function () {
|
||||
return {
|
||||
@@ -116,6 +123,10 @@ export default {
|
||||
}
|
||||
|
||||
this.conditionalLabel = latestDatum.output || '';
|
||||
},
|
||||
async showToolTip() {
|
||||
const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;
|
||||
this.buildToolTip(await this.getObjectPath(), BELOW, 'conditionWidgetElement');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
<layout-frame
|
||||
:item="item"
|
||||
:grid-size="gridSize"
|
||||
:title="domainObject && domainObject.name"
|
||||
:is-editing="isEditing"
|
||||
@move="(gridDelta) => $emit('move', gridDelta)"
|
||||
@endMove="() => $emit('endMove')"
|
||||
|
||||
@@ -30,12 +30,15 @@
|
||||
>
|
||||
<div
|
||||
v-if="domainObject"
|
||||
ref="telemetryViewWrapper"
|
||||
class="c-telemetry-view u-style-receiver"
|
||||
:class="[itemClasses]"
|
||||
:style="styleObject"
|
||||
:data-font-size="item.fontSize"
|
||||
:data-font="item.font"
|
||||
@contextmenu.prevent="showContextMenu"
|
||||
@mouseover.ctrl="showToolTip"
|
||||
@mouseleave="hideToolTip"
|
||||
>
|
||||
<div class="is-status__indicator" :title="`This item is ${status}`"></div>
|
||||
<div v-if="showLabel" class="c-telemetry-view__label">
|
||||
@@ -69,6 +72,7 @@ import {
|
||||
getDefaultNotebook,
|
||||
getNotebookSectionAndPage
|
||||
} from '@/plugins/notebook/utils/notebook-storage.js';
|
||||
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
|
||||
|
||||
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
|
||||
const DEFAULT_POSITION = [1, 1];
|
||||
@@ -97,7 +101,7 @@ export default {
|
||||
components: {
|
||||
LayoutFrame
|
||||
},
|
||||
mixins: [conditionalStylesMixin, stalenessMixin],
|
||||
mixins: [conditionalStylesMixin, stalenessMixin, tooltipHelpers],
|
||||
inject: ['openmct', 'objectPath', 'currentView'],
|
||||
props: {
|
||||
item: {
|
||||
@@ -379,6 +383,10 @@ export default {
|
||||
},
|
||||
setStatus(status) {
|
||||
this.status = status;
|
||||
},
|
||||
async showToolTip() {
|
||||
const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;
|
||||
this.buildToolTip(await this.getObjectPath(), BELOW, 'telemetryViewWrapper');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22,7 +22,13 @@
|
||||
<template>
|
||||
<div class="c-gauge__wrapper js-gauge-wrapper" :class="gaugeClasses" :title="gaugeTitle">
|
||||
<template v-if="typeDial">
|
||||
<svg class="c-gauge c-dial" viewBox="0 0 10 10">
|
||||
<svg
|
||||
ref="gauge"
|
||||
class="c-gauge c-dial"
|
||||
viewBox="0 0 10 10"
|
||||
@mouseover.ctrl="showToolTip"
|
||||
@mouseleave="hideToolTip"
|
||||
>
|
||||
<g class="c-dial__masks">
|
||||
<mask id="gaugeValueMask">
|
||||
<path
|
||||
@@ -325,13 +331,14 @@
|
||||
<script>
|
||||
import { DIAL_VALUE_DEG_OFFSET, getLimitDegree } from '../gauge-limit-util';
|
||||
import stalenessMixin from '@/ui/mixins/staleness-mixin';
|
||||
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
|
||||
|
||||
const LIMIT_PADDING_IN_PERCENT = 10;
|
||||
const DEFAULT_CURRENT_VALUE = '--';
|
||||
|
||||
export default {
|
||||
name: 'Gauge',
|
||||
mixins: [stalenessMixin],
|
||||
mixins: [stalenessMixin, tooltipHelpers],
|
||||
inject: ['openmct', 'domainObject', 'composition'],
|
||||
data() {
|
||||
let gaugeController = this.domainObject.configuration.gaugeController;
|
||||
@@ -730,6 +737,10 @@ export default {
|
||||
},
|
||||
valToPercentMeter(vValue) {
|
||||
return this.round(((this.rangeHigh - vValue) / (this.rangeHigh - this.rangeLow)) * 100, 2);
|
||||
},
|
||||
async showToolTip() {
|
||||
const { CENTER } = this.openmct.tooltips.TOOLTIP_LOCATIONS;
|
||||
this.buildToolTip(await this.getTelemetryPathString(), CENTER, 'gauge');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -89,6 +89,11 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
imageryAnnotations() {
|
||||
this.drawAnnotations();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.canvas = this.$refs.canvas;
|
||||
this.context = this.canvas.getContext('2d');
|
||||
|
||||
@@ -255,7 +255,7 @@ export default {
|
||||
}
|
||||
},
|
||||
data() {
|
||||
let timeSystem = this.openmct.time.timeSystem();
|
||||
let timeSystem = this.openmct.time.getTimeSystem();
|
||||
this.metadata = {};
|
||||
this.requestCount = 0;
|
||||
|
||||
@@ -375,14 +375,11 @@ export default {
|
||||
return age < cutoff && !this.refreshCSS;
|
||||
},
|
||||
canTrackDuration() {
|
||||
let hasClock;
|
||||
if (this.timeContext) {
|
||||
hasClock = this.timeContext.clock();
|
||||
return this.timeContext.isRealTime();
|
||||
} else {
|
||||
hasClock = this.openmct.time.clock();
|
||||
return this.openmct.time.isRealTime();
|
||||
}
|
||||
|
||||
return hasClock && this.timeSystem.isUTCBased;
|
||||
},
|
||||
isNextDisabled() {
|
||||
let disabled = false;
|
||||
@@ -531,14 +528,11 @@ export default {
|
||||
return isFresh;
|
||||
},
|
||||
isFixed() {
|
||||
let clock;
|
||||
if (this.timeContext) {
|
||||
clock = this.timeContext.clock();
|
||||
return this.timeContext.isFixed();
|
||||
} else {
|
||||
clock = this.openmct.time.clock();
|
||||
return this.openmct.time.isFixed();
|
||||
}
|
||||
|
||||
return clock === undefined;
|
||||
},
|
||||
isSelectable() {
|
||||
return true;
|
||||
@@ -1111,7 +1105,7 @@ export default {
|
||||
window.clearInterval(this.durationTracker);
|
||||
},
|
||||
updateDuration() {
|
||||
let currentTime = this.timeContext.clock() && this.timeContext.clock().currentValue();
|
||||
let currentTime = this.timeContext.getClock().currentValue();
|
||||
if (currentTime === undefined) {
|
||||
this.numericDuration = currentTime;
|
||||
} else if (Number.isInteger(this.parsedSelectedTime)) {
|
||||
|
||||
@@ -112,7 +112,6 @@ export default class RelatedTelemetry {
|
||||
start: this._openmct.time.bounds().start,
|
||||
end: this._parseTime(datum)
|
||||
};
|
||||
ephemeralContext.stopClock();
|
||||
ephemeralContext.bounds(newBounds);
|
||||
|
||||
const options = {
|
||||
|
||||
@@ -24,15 +24,15 @@ const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||
const IMAGE_HINT_KEY = 'image';
|
||||
const IMAGE_THUMBNAIL_HINT_KEY = 'thumbnail';
|
||||
const IMAGE_DOWNLOAD_NAME_HINT_KEY = 'imageDownloadName';
|
||||
import { TIME_CONTEXT_EVENTS } from '../../../api/time/constants';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject', 'objectPath'],
|
||||
mounted() {
|
||||
// listen
|
||||
this.boundsChange = this.boundsChange.bind(this);
|
||||
this.timeSystemChange = this.timeSystemChange.bind(this);
|
||||
this.boundsChanged = this.boundsChanged.bind(this);
|
||||
this.timeSystemChanged = this.timeSystemChanged.bind(this);
|
||||
this.setDataTimeContext = this.setDataTimeContext.bind(this);
|
||||
this.setDataTimeContext();
|
||||
this.openmct.objectViews.on('clearData', this.dataCleared);
|
||||
|
||||
// Get metadata and formatters
|
||||
@@ -59,14 +59,8 @@ export default {
|
||||
// initialize
|
||||
this.timeKey = this.timeSystem.key;
|
||||
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||
|
||||
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
|
||||
timeContext: this.timeContext
|
||||
});
|
||||
this.telemetryCollection.on('add', this.dataAdded);
|
||||
this.telemetryCollection.on('remove', this.dataRemoved);
|
||||
this.telemetryCollection.on('clear', this.dataCleared);
|
||||
this.telemetryCollection.load();
|
||||
this.setDataTimeContext();
|
||||
this.loadTelemetry();
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.unsubscribe) {
|
||||
@@ -111,14 +105,13 @@ export default {
|
||||
setDataTimeContext() {
|
||||
this.stopFollowingDataTimeContext();
|
||||
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||
this.timeContext.on('bounds', this.boundsChange);
|
||||
this.boundsChange(this.timeContext.bounds());
|
||||
this.timeContext.on('timeSystem', this.timeSystemChange);
|
||||
this.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsChanged);
|
||||
this.timeContext.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.timeSystemChanged);
|
||||
},
|
||||
stopFollowingDataTimeContext() {
|
||||
if (this.timeContext) {
|
||||
this.timeContext.off('bounds', this.boundsChange);
|
||||
this.timeContext.off('timeSystem', this.timeSystemChange);
|
||||
this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsChanged);
|
||||
this.timeContext.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.timeSystemChanged);
|
||||
}
|
||||
},
|
||||
formatImageUrl(datum) {
|
||||
@@ -161,14 +154,23 @@ export default {
|
||||
|
||||
return this.timeFormatter.parse(datum);
|
||||
},
|
||||
boundsChange(bounds, isTick) {
|
||||
loadTelemetry() {
|
||||
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
|
||||
timeContext: this.timeContext
|
||||
});
|
||||
this.telemetryCollection.on('add', this.dataAdded);
|
||||
this.telemetryCollection.on('remove', this.dataRemoved);
|
||||
this.telemetryCollection.on('clear', this.dataCleared);
|
||||
this.telemetryCollection.load();
|
||||
},
|
||||
boundsChanged(bounds, isTick) {
|
||||
if (isTick) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.bounds = bounds; // setting bounds for ImageryView watcher
|
||||
},
|
||||
timeSystemChange() {
|
||||
timeSystemChanged() {
|
||||
this.timeSystem = this.timeContext.timeSystem();
|
||||
this.timeKey = this.timeSystem.key;
|
||||
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||
|
||||
@@ -684,6 +684,10 @@ describe('The Imagery View Layouts', () => {
|
||||
return Vue.nextTick();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
openmct.time.setClock('local');
|
||||
});
|
||||
|
||||
it('on mount should show imagery within the given bounds', (done) => {
|
||||
Vue.nextTick(() => {
|
||||
const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper');
|
||||
|
||||
@@ -105,14 +105,16 @@ export default class ExportNotebookAsTextAction {
|
||||
if (changes.exportMetaData) {
|
||||
const createdTimestamp = entry.createdOn;
|
||||
const createdBy = this.getUserName(entry.createdBy);
|
||||
const createdByRole = entry.createdByRole;
|
||||
const modifiedBy = this.getUserName(entry.modifiedBy);
|
||||
const modifiedByRole = entry.modifiedByRole;
|
||||
const modifiedTimestamp = entry.modified ?? entry.created;
|
||||
notebookAsText += `Created on ${this.formatTimeStamp(
|
||||
createdTimestamp
|
||||
)} by user ${createdBy}\n\n`;
|
||||
)} by user ${createdBy}${createdByRole ? `: ${createdByRole}` : ''}\n\n`;
|
||||
notebookAsText += `Updated on ${this.formatTimeStamp(
|
||||
modifiedTimestamp
|
||||
)} by user ${modifiedBy}\n\n`;
|
||||
)} by user ${modifiedBy}${modifiedByRole ? `: ${modifiedByRole}` : ''}\n\n`;
|
||||
}
|
||||
|
||||
if (changes.exportTags) {
|
||||
|
||||
@@ -422,7 +422,7 @@ export default {
|
||||
});
|
||||
},
|
||||
filterAndSortEntries() {
|
||||
const filterTime = Date.now();
|
||||
const filterTime = this.openmct.time.now();
|
||||
const pageEntries =
|
||||
getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage) || [];
|
||||
|
||||
|
||||
@@ -20,7 +20,12 @@
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<template>
|
||||
<div class="c-snapshot c-ne__embed">
|
||||
<div
|
||||
ref="notebookEmbed"
|
||||
class="c-snapshot c-ne__embed"
|
||||
@mouseover.ctrl="showToolTip"
|
||||
@mouseleave="hideToolTip"
|
||||
>
|
||||
<div v-if="embed.snapshot" class="c-ne__embed__snap-thumb" @click="openSnapshot()">
|
||||
<img :src="thumbnailImage" />
|
||||
</div>
|
||||
@@ -49,6 +54,7 @@ import RemoveDialog from '../utils/removeDialog';
|
||||
import PainterroInstance from '../utils/painterroInstance';
|
||||
import SnapshotTemplate from './snapshot-template.html';
|
||||
import objectPathToUrl from '@/tools/url';
|
||||
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
|
||||
|
||||
import { updateNotebookImageDomainObject } from '../utils/notebook-image';
|
||||
import ImageExporter from '../../../exporters/ImageExporter';
|
||||
@@ -56,6 +62,7 @@ import ImageExporter from '../../../exporters/ImageExporter';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default {
|
||||
mixins: [tooltipHelpers],
|
||||
inject: ['openmct', 'snapshotContainer'],
|
||||
props: {
|
||||
embed: {
|
||||
@@ -193,7 +200,7 @@ export default {
|
||||
template: '<div id="snap-annotation"></div>'
|
||||
}).$mount();
|
||||
|
||||
const painterroInstance = new PainterroInstance(annotateVue.$el);
|
||||
const painterroInstance = new PainterroInstance(annotateVue.$el, this.openmct);
|
||||
const annotateOverlay = this.openmct.overlays.overlay({
|
||||
element: annotateVue.$el,
|
||||
size: 'large',
|
||||
@@ -258,7 +265,6 @@ export default {
|
||||
this.embed.bounds.start !== bounds.start || this.embed.bounds.end !== bounds.end;
|
||||
const isFixedTimespanMode = !this.openmct.time.clock();
|
||||
|
||||
this.openmct.time.stopClock();
|
||||
let message = '';
|
||||
if (isTimeBoundChanged) {
|
||||
this.openmct.time.bounds({
|
||||
@@ -404,6 +410,14 @@ export default {
|
||||
snapshotObject.fullSizeImage
|
||||
);
|
||||
}
|
||||
},
|
||||
async showToolTip() {
|
||||
const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;
|
||||
this.buildToolTip(
|
||||
await this.getObjectPath(this.embed.domainObject.identifier),
|
||||
BELOW,
|
||||
'notebookEmbed'
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -37,7 +37,10 @@
|
||||
<span class="c-ne__created-date">{{ createdOnDate }}</span>
|
||||
<span class="c-ne__created-time">{{ createdOnTime }}</span>
|
||||
<span v-if="entry.createdBy" class="c-ne__creator">
|
||||
<span class="icon-person"></span> {{ entry.createdBy }}
|
||||
<span class="icon-person"></span>
|
||||
{{
|
||||
entry.createdByRole ? `${entry.createdBy}: ${entry.createdByRole}` : entry.createdBy
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<span v-if="!readOnly && !isLocked" class="c-ne__local-controls--hidden">
|
||||
@@ -433,13 +436,20 @@ export default {
|
||||
this.timestampAndUpdate();
|
||||
},
|
||||
async timestampAndUpdate() {
|
||||
const user = await this.openmct.user.getCurrentUser();
|
||||
|
||||
const [user, activeRole] = await Promise.all([
|
||||
this.openmct.user.getCurrentUser(),
|
||||
this.openmct.user.getActiveRole?.()
|
||||
]);
|
||||
if (user === undefined) {
|
||||
this.entry.modifiedBy = UNKNOWN_USER;
|
||||
} else {
|
||||
this.entry.modifiedBy = user.getName();
|
||||
if (activeRole) {
|
||||
this.entry.modifiedByRole = activeRole;
|
||||
}
|
||||
}
|
||||
|
||||
this.entry.modified = Date.now();
|
||||
this.entry.modified = this.openmct.time.now();
|
||||
|
||||
this.$emit('updateEntry', this.entry);
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<div class="c-snapshots-h">
|
||||
<div class="l-browse-bar">
|
||||
<div class="l-browse-bar__start">
|
||||
<div class="l-browse-bar__object-name--w">
|
||||
<div class="l-browse-bar__object-name--w c-snapshots-h__title">
|
||||
<div class="l-browse-bar__object-name c-object-label">
|
||||
<div class="c-object-label__type-icon icon-camera"></div>
|
||||
<div class="c-object-label__name">Notebook Snapshots</div>
|
||||
|
||||
@@ -12,6 +12,15 @@ async function getUsername(openmct) {
|
||||
return username;
|
||||
}
|
||||
|
||||
async function getActiveRole(openmct) {
|
||||
let role = null;
|
||||
if (openmct.user.hasProvider()) {
|
||||
role = await openmct.user.getActiveRole?.();
|
||||
}
|
||||
|
||||
return role;
|
||||
}
|
||||
|
||||
export const DEFAULT_CLASS = 'notebook-default';
|
||||
const TIME_BOUNDS = {
|
||||
START_BOUND: 'tc.startBound',
|
||||
@@ -114,7 +123,7 @@ export async function createNewEmbed(snapshotMeta, snapshot = '') {
|
||||
domainObjectType && domainObjectType.definition
|
||||
? domainObjectType.definition.cssClass
|
||||
: 'icon-object-unknown';
|
||||
const date = Date.now();
|
||||
const date = openmct.time.now();
|
||||
const historicLink = link
|
||||
? getHistoricLinkInFixedMode(openmct, bounds, link)
|
||||
: objectLink.computed.objectLink.call({
|
||||
@@ -150,17 +159,21 @@ export async function addNotebookEntry(
|
||||
return;
|
||||
}
|
||||
|
||||
const date = Date.now();
|
||||
const date = openmct.time.now();
|
||||
const configuration = domainObject.configuration;
|
||||
const entries = configuration.entries || {};
|
||||
const embeds = embed ? [embed] : [];
|
||||
|
||||
const id = `entry-${uuid()}`;
|
||||
const createdBy = await getUsername(openmct);
|
||||
const [createdBy, createdByRole] = await Promise.all([
|
||||
getUsername(openmct),
|
||||
getActiveRole(openmct)
|
||||
]);
|
||||
const entry = {
|
||||
id,
|
||||
createdOn: date,
|
||||
createdBy,
|
||||
createdByRole,
|
||||
text: entryText,
|
||||
embeds
|
||||
};
|
||||
|
||||
@@ -26,11 +26,12 @@ const DEFAULT_CONFIG = {
|
||||
};
|
||||
|
||||
export default class PainterroInstance {
|
||||
constructor(element) {
|
||||
constructor(element, openmct) {
|
||||
this.elementId = element.id;
|
||||
this.isSave = false;
|
||||
this.painterroInstance = undefined;
|
||||
this.saveCallback = undefined;
|
||||
this.openmct = openmct;
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
@@ -67,11 +68,11 @@ export default class PainterroInstance {
|
||||
src: fullSizeImageURL,
|
||||
type: url.type,
|
||||
size: url.size,
|
||||
modified: Date.now()
|
||||
modified: this.openmct.time.now()
|
||||
},
|
||||
thumbnailImage: {
|
||||
src: thumbnailURL,
|
||||
modified: Date.now()
|
||||
modified: this.openmct.time.now()
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
|
||||
<script>
|
||||
const DEFAULT_POLL_QUESTION = 'NO POLL QUESTION';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'indicator', 'configuration'],
|
||||
props: {
|
||||
@@ -63,7 +62,6 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
allRoles: [],
|
||||
role: '--',
|
||||
pollQuestionUpdated: '--',
|
||||
currentPollQuestion: DEFAULT_POLL_QUESTION,
|
||||
@@ -78,26 +76,27 @@ export default {
|
||||
left: `${this.positionX}px`,
|
||||
top: `${this.positionY}px`
|
||||
};
|
||||
},
|
||||
canProvideStatusForRole() {
|
||||
return this.openmct.user.canProvideStatusForRole(this.role);
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.openmct.user.status.off('statusChange', this.setStatus);
|
||||
this.openmct.user.status.off('pollQuestionChange', this.setPollQuestion);
|
||||
this.openmct.user.off('roleChanged', this.fetchMyStatus);
|
||||
},
|
||||
async mounted() {
|
||||
this.unsubscribe = [];
|
||||
await this.fetchUser();
|
||||
await this.findFirstApplicableRole();
|
||||
this.fetchPossibleStatusesForUser();
|
||||
this.fetchCurrentPoll();
|
||||
this.fetchMyStatus();
|
||||
await this.fetchMyStatus();
|
||||
this.subscribeToMyStatus();
|
||||
this.subscribeToPollQuestion();
|
||||
this.subscribeToRoleChange();
|
||||
},
|
||||
methods: {
|
||||
async findFirstApplicableRole() {
|
||||
this.role = await this.openmct.user.status.getStatusRoleForCurrentUser();
|
||||
},
|
||||
async fetchUser() {
|
||||
this.user = await this.openmct.user.getCurrentUser();
|
||||
},
|
||||
@@ -117,9 +116,22 @@ export default {
|
||||
this.indicator.text(pollQuestion?.question || '');
|
||||
},
|
||||
async fetchMyStatus() {
|
||||
const activeStatusRole = await this.openmct.user.status.getStatusRoleForCurrentUser();
|
||||
const status = await this.openmct.user.status.getStatusForRole(activeStatusRole);
|
||||
// hide indicator for observer
|
||||
const isStatusCapable = await this.openmct.user.canProvideStatusForRole();
|
||||
if (!isStatusCapable) {
|
||||
this.indicator.text('');
|
||||
this.indicator.statusClass('hidden');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const activeRole = await this.openmct.user.getActiveRole();
|
||||
if (!activeRole) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.role = activeRole;
|
||||
const status = await this.openmct.user.status.getStatusForRole(activeRole);
|
||||
if (status !== undefined) {
|
||||
this.setStatus({ status });
|
||||
}
|
||||
@@ -130,7 +142,10 @@ export default {
|
||||
subscribeToPollQuestion() {
|
||||
this.openmct.user.status.on('pollQuestionChange', this.setPollQuestion);
|
||||
},
|
||||
setStatus({ role, status }) {
|
||||
subscribeToRoleChange() {
|
||||
this.openmct.user.on('roleChanged', this.fetchMyStatus);
|
||||
},
|
||||
setStatus({ status }) {
|
||||
status = this.applyStyling(status);
|
||||
this.selectedStatus = status.key;
|
||||
this.indicator.iconClass(status.iconClassPoll);
|
||||
@@ -148,11 +163,16 @@ export default {
|
||||
return this.allStatuses.find((possibleMatch) => possibleMatch.key === statusKey);
|
||||
},
|
||||
async changeStatus() {
|
||||
if (!this.openmct.user.canProvideStatusForRole()) {
|
||||
this.openmct.notifications.error('Selected role is ineligible to provide operator status');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.selectedStatus !== undefined) {
|
||||
const statusObject = this.findStatusByKey(this.selectedStatus);
|
||||
|
||||
const result = await this.openmct.user.status.setStatusForRole(this.role, statusObject);
|
||||
|
||||
const result = await this.openmct.user.status.setStatusForRole(statusObject);
|
||||
if (result === true) {
|
||||
this.openmct.notifications.info('Successfully set operator status');
|
||||
} else {
|
||||
|
||||
@@ -29,13 +29,8 @@ import PollQuestionIndicator from './pollQuestion/PollQuestionIndicator';
|
||||
export default function operatorStatusPlugin(configuration) {
|
||||
return function install(openmct) {
|
||||
if (openmct.user.hasProvider()) {
|
||||
openmct.user.status.canProvideStatusForCurrentUser().then((canProvideStatus) => {
|
||||
if (canProvideStatus) {
|
||||
const operatorStatusIndicator = new OperatorStatusIndicator(openmct, configuration);
|
||||
|
||||
operatorStatusIndicator.install();
|
||||
}
|
||||
});
|
||||
const operatorStatusIndicator = new OperatorStatusIndicator(openmct, configuration);
|
||||
operatorStatusIndicator.install();
|
||||
|
||||
openmct.user.status.canSetPollQuestion().then((canSetPollQuestion) => {
|
||||
if (canSetPollQuestion) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import CouchDocument from './CouchDocument';
|
||||
import CouchObjectQueue from './CouchObjectQueue';
|
||||
import { PENDING, CONNECTED, DISCONNECTED, UNKNOWN } from './CouchStatusIndicator';
|
||||
import { isNotebookOrAnnotationType } from '../../notebook/notebook-constants.js';
|
||||
import _ from 'lodash';
|
||||
|
||||
const REV = '_rev';
|
||||
const ID = '_id';
|
||||
@@ -42,6 +43,8 @@ class CouchObjectProvider {
|
||||
this.batchIds = [];
|
||||
this.onEventMessage = this.onEventMessage.bind(this);
|
||||
this.onEventError = this.onEventError.bind(this);
|
||||
this.flushPersistenceQueue = _.debounce(this.flushPersistenceQueue.bind(this));
|
||||
this.persistenceQueue = [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -220,10 +223,16 @@ class CouchObjectProvider {
|
||||
|
||||
return json;
|
||||
} catch (error) {
|
||||
// abort errors are expected
|
||||
if (error.name === 'AbortError') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Network error, CouchDB unreachable.
|
||||
if (response === null) {
|
||||
this.indicator.setIndicatorToState(DISCONNECTED);
|
||||
console.error(error.message);
|
||||
|
||||
throw new Error(`CouchDB Error - No response"`);
|
||||
} else {
|
||||
if (body?.model && isNotebookOrAnnotationType(body.model)) {
|
||||
@@ -668,9 +677,12 @@ class CouchObjectProvider {
|
||||
if (!this.objectQueue[key].pending) {
|
||||
this.objectQueue[key].pending = true;
|
||||
const queued = this.objectQueue[key].dequeue();
|
||||
let document = new CouchDocument(key, queued.model);
|
||||
document.metadata.created = Date.now();
|
||||
this.request(key, 'PUT', document)
|
||||
let couchDocument = new CouchDocument(key, queued.model);
|
||||
couchDocument.metadata.created = Date.now();
|
||||
this.#enqueueForPersistence({
|
||||
key,
|
||||
document: couchDocument
|
||||
})
|
||||
.then((response) => {
|
||||
this.#checkResponse(response, queued.intermediateResponse, key);
|
||||
})
|
||||
@@ -683,6 +695,42 @@ class CouchObjectProvider {
|
||||
return intermediateResponse.promise;
|
||||
}
|
||||
|
||||
#enqueueForPersistence({ key, document }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.persistenceQueue.push({
|
||||
key,
|
||||
document,
|
||||
resolve,
|
||||
reject
|
||||
});
|
||||
this.flushPersistenceQueue();
|
||||
});
|
||||
}
|
||||
|
||||
async flushPersistenceQueue() {
|
||||
if (this.persistenceQueue.length > 1) {
|
||||
const batch = {
|
||||
docs: this.persistenceQueue.map((queued) => queued.document)
|
||||
};
|
||||
const response = await this.request('_bulk_docs', 'POST', batch);
|
||||
response.forEach((responseMetadatum) => {
|
||||
const queued = this.persistenceQueue.find(
|
||||
(queuedMetadatum) => queuedMetadatum.key === responseMetadatum.id
|
||||
);
|
||||
if (responseMetadatum.ok) {
|
||||
queued.resolve(responseMetadatum);
|
||||
} else {
|
||||
queued.reject(responseMetadatum);
|
||||
}
|
||||
});
|
||||
} else if (this.persistenceQueue.length === 1) {
|
||||
const { key, document, resolve, reject } = this.persistenceQueue[0];
|
||||
|
||||
this.request(key, 'PUT', document).then(resolve).catch(reject);
|
||||
}
|
||||
this.persistenceQueue = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
|
||||
@@ -243,6 +243,135 @@ describe('the plugin', () => {
|
||||
expect(requestMethod).toEqual('GET');
|
||||
});
|
||||
});
|
||||
describe('batches persistence', () => {
|
||||
let successfulMockPromise;
|
||||
let partialFailureMockPromise;
|
||||
let objectsToPersist;
|
||||
|
||||
beforeEach(() => {
|
||||
successfulMockPromise = Promise.resolve({
|
||||
json: () => {
|
||||
return [
|
||||
{
|
||||
id: 'object-1',
|
||||
ok: true
|
||||
},
|
||||
{
|
||||
id: 'object-2',
|
||||
ok: true
|
||||
},
|
||||
{
|
||||
id: 'object-3',
|
||||
ok: true
|
||||
}
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
partialFailureMockPromise = Promise.resolve({
|
||||
json: () => {
|
||||
return [
|
||||
{
|
||||
id: 'object-1',
|
||||
ok: true
|
||||
},
|
||||
{
|
||||
id: 'object-2',
|
||||
ok: false
|
||||
},
|
||||
{
|
||||
id: 'object-3',
|
||||
ok: true
|
||||
}
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
objectsToPersist = [
|
||||
{
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'object-1'
|
||||
},
|
||||
name: 'object-1',
|
||||
type: 'folder',
|
||||
modified: 0
|
||||
},
|
||||
{
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'object-2'
|
||||
},
|
||||
name: 'object-2',
|
||||
type: 'folder',
|
||||
modified: 0
|
||||
},
|
||||
{
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'object-3'
|
||||
},
|
||||
name: 'object-3',
|
||||
type: 'folder',
|
||||
modified: 0
|
||||
}
|
||||
];
|
||||
});
|
||||
it('for multiple simultaneous successful saves', async () => {
|
||||
fetch.and.returnValue(successfulMockPromise);
|
||||
|
||||
await Promise.all(
|
||||
objectsToPersist.map((objectToPersist) => openmct.objects.save(objectToPersist))
|
||||
);
|
||||
|
||||
const requestUrl = fetch.calls.mostRecent().args[0];
|
||||
const requestMethod = fetch.calls.mostRecent().args[1].method;
|
||||
const requestBody = JSON.parse(fetch.calls.mostRecent().args[1].body);
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(requestUrl.includes('_bulk_docs')).toBeTrue();
|
||||
expect(requestMethod).toEqual('POST');
|
||||
expect(
|
||||
objectsToPersist.every(
|
||||
(object, index) => object.identifier.key === requestBody.docs[index]._id
|
||||
)
|
||||
).toBeTrue();
|
||||
});
|
||||
it('for multiple simultaneous saves with partial failure', async () => {
|
||||
fetch.and.returnValue(partialFailureMockPromise);
|
||||
|
||||
let saveResults = await Promise.all(
|
||||
objectsToPersist.map((objectToPersist) =>
|
||||
openmct.objects
|
||||
.save(objectToPersist)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
)
|
||||
);
|
||||
expect(saveResults[0]).toBeTrue();
|
||||
expect(saveResults[1]).toBeFalse();
|
||||
expect(saveResults[2]).toBeTrue();
|
||||
});
|
||||
it('except for a single save', async () => {
|
||||
fetch.and.returnValue({
|
||||
json: () => {
|
||||
return {
|
||||
id: 'object-1',
|
||||
ok: true
|
||||
};
|
||||
}
|
||||
});
|
||||
await openmct.objects.save(objectsToPersist[0]);
|
||||
|
||||
const requestUrl = fetch.calls.mostRecent().args[0];
|
||||
const requestMethod = fetch.calls.mostRecent().args[1].method;
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(requestUrl.includes('_bulk_docs')).toBeFalse();
|
||||
expect(requestUrl.endsWith('object-1')).toBeTrue();
|
||||
expect(requestMethod).toEqual('PUT');
|
||||
});
|
||||
});
|
||||
describe('implements server-side search', () => {
|
||||
let mockPromise;
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
<mct-chart
|
||||
:rectangles="rectangles"
|
||||
:highlights="highlights"
|
||||
:show-limit-line-labels="limitLineLabels"
|
||||
:annotated-points="annotatedPoints"
|
||||
:annotation-selections="annotationSelections"
|
||||
:hidden-y-axis-ids="hiddenYAxisIds"
|
||||
@@ -231,7 +232,7 @@ export default {
|
||||
limitLineLabels: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
colorPalette: {
|
||||
@@ -257,7 +258,7 @@ export default {
|
||||
seriesModels: [],
|
||||
legend: {},
|
||||
pending: 0,
|
||||
isRealTime: this.openmct.time.clock() !== undefined,
|
||||
isRealTime: this.openmct.time.isRealTime(),
|
||||
loaded: false,
|
||||
isTimeOutOfSync: false,
|
||||
isFrozenOnMouseDown: false,
|
||||
@@ -349,7 +350,7 @@ export default {
|
||||
document.addEventListener('keydown', this.handleKeyDown);
|
||||
document.addEventListener('keyup', this.handleKeyUp);
|
||||
eventHelpers.extend(this);
|
||||
this.updateRealTime = this.updateRealTime.bind(this);
|
||||
this.updateMode = this.updateMode.bind(this);
|
||||
this.updateDisplayBounds = this.updateDisplayBounds.bind(this);
|
||||
this.setTimeContext = this.setTimeContext.bind(this);
|
||||
|
||||
@@ -521,20 +522,19 @@ export default {
|
||||
},
|
||||
setTimeContext() {
|
||||
this.stopFollowingTimeContext();
|
||||
|
||||
this.timeContext = this.openmct.time.getContextForView(this.path);
|
||||
this.followTimeContext();
|
||||
},
|
||||
followTimeContext() {
|
||||
this.updateDisplayBounds(this.timeContext.bounds());
|
||||
this.timeContext.on('clock', this.updateRealTime);
|
||||
this.timeContext.on('bounds', this.updateDisplayBounds);
|
||||
this.updateDisplayBounds(this.timeContext.getBounds());
|
||||
this.timeContext.on('modeChanged', this.updateMode);
|
||||
this.timeContext.on('boundsChanged', this.updateDisplayBounds);
|
||||
this.synchronized(true);
|
||||
},
|
||||
stopFollowingTimeContext() {
|
||||
if (this.timeContext) {
|
||||
this.timeContext.off('clock', this.updateRealTime);
|
||||
this.timeContext.off('bounds', this.updateDisplayBounds);
|
||||
this.timeContext.off('modeChanged', this.updateMode);
|
||||
this.timeContext.off('boundsChanged', this.updateDisplayBounds);
|
||||
}
|
||||
},
|
||||
getConfig() {
|
||||
@@ -773,8 +773,8 @@ export default {
|
||||
const displayRange = series.getDisplayRange(xKey);
|
||||
this.config.xAxis.set('range', displayRange);
|
||||
},
|
||||
updateRealTime(clock) {
|
||||
this.isRealTime = clock !== undefined;
|
||||
updateMode() {
|
||||
this.isRealTime = this.timeContext.isRealTime();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -835,13 +835,13 @@ export default {
|
||||
* displays can update accordingly.
|
||||
*/
|
||||
synchronized(value) {
|
||||
const isLocalClock = this.timeContext.clock();
|
||||
const isRealTime = this.timeContext.isRealTime();
|
||||
|
||||
if (typeof value !== 'undefined') {
|
||||
this._synchronized = value;
|
||||
this.isTimeOutOfSync = value !== true;
|
||||
|
||||
const isUnsynced = isLocalClock && !value;
|
||||
const isUnsynced = isRealTime && !value;
|
||||
this.setStatus(isUnsynced);
|
||||
}
|
||||
|
||||
@@ -1866,7 +1866,6 @@ export default {
|
||||
},
|
||||
|
||||
synchronizeTimeConductor() {
|
||||
this.timeContext.stopClock();
|
||||
const range = this.config.xAxis.get('displayRange');
|
||||
this.timeContext.bounds({
|
||||
start: range.min,
|
||||
|
||||
@@ -33,18 +33,23 @@
|
||||
/>
|
||||
<mct-plot
|
||||
:class="[plotLegendExpandedStateClass, plotLegendPositionClass]"
|
||||
:init-grid-lines="gridLines"
|
||||
:init-grid-lines="gridLinesProp"
|
||||
:init-cursor-guide="cursorGuide"
|
||||
:options="options"
|
||||
:limit-line-labels="limitLineLabels"
|
||||
:limit-line-labels="limitLineLabelsProp"
|
||||
:parent-y-tick-width="parentYTickWidth"
|
||||
:color-palette="colorPalette"
|
||||
@loadingUpdated="loadingUpdated"
|
||||
@statusUpdated="setStatus"
|
||||
@configLoaded="updateReady"
|
||||
@lockHighlightPoint="lockHighlightPointUpdated"
|
||||
@highlights="highlightsUpdated"
|
||||
@plotYTickWidth="onYTickWidthChange"
|
||||
@cursorGuide="onCursorGuideChange"
|
||||
@gridLines="onGridLinesChange"
|
||||
>
|
||||
<plot-legend
|
||||
v-if="configReady"
|
||||
v-if="configReady && hideLegend === false"
|
||||
:cursor-locked="lockHighlightPoint"
|
||||
:highlights="highlights"
|
||||
@legendHoverChanged="legendHoverChanged"
|
||||
@@ -79,14 +84,50 @@ export default {
|
||||
compact: false
|
||||
};
|
||||
}
|
||||
},
|
||||
gridLines: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
cursorGuide: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
parentLimitLineLabels: {
|
||||
type: Object,
|
||||
default() {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
colorPalette: {
|
||||
type: Object,
|
||||
default() {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
parentYTickWidth: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
leftTickWidth: 0,
|
||||
rightTickWidth: 0,
|
||||
hasMultipleLeftAxes: false
|
||||
};
|
||||
}
|
||||
},
|
||||
hideLegend: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
//Don't think we need this as it appears to be stacked plot specific
|
||||
// hideExportButtons: false
|
||||
cursorGuide: false,
|
||||
gridLines: !this.options.compact,
|
||||
loading: false,
|
||||
status: '',
|
||||
staleObjects: [],
|
||||
@@ -99,6 +140,12 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
limitLineLabelsProp() {
|
||||
return this.parentLimitLineLabels ?? this.limitLineLabels;
|
||||
},
|
||||
gridLinesProp() {
|
||||
return this.gridLines ?? !this.options.compact;
|
||||
},
|
||||
staleClass() {
|
||||
if (this.staleObjects.length !== 0) {
|
||||
return 'is-stale';
|
||||
@@ -117,6 +164,14 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
gridLines(newGridLines) {
|
||||
this.gridLines = newGridLines;
|
||||
},
|
||||
cursorGuide(newCursorGuide) {
|
||||
this.cursorGuide = newCursorGuide;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
eventHelpers.extend(this);
|
||||
this.imageExporter = new ImageExporter(this.openmct);
|
||||
@@ -188,6 +243,7 @@ export default {
|
||||
},
|
||||
loadingUpdated(loading) {
|
||||
this.loading = loading;
|
||||
this.$emit('loadingUpdated', ...arguments);
|
||||
},
|
||||
destroy() {
|
||||
if (this.stalenessSubscription) {
|
||||
@@ -223,9 +279,11 @@ export default {
|
||||
},
|
||||
lockHighlightPointUpdated(data) {
|
||||
this.lockHighlightPoint = data;
|
||||
this.$emit('lockHighlightPoint', ...arguments);
|
||||
},
|
||||
highlightsUpdated(data) {
|
||||
this.highlights = data;
|
||||
this.$emit('highlights', ...arguments);
|
||||
},
|
||||
legendHoverChanged(data) {
|
||||
this.limitLineLabels = data;
|
||||
@@ -238,6 +296,16 @@ export default {
|
||||
},
|
||||
updateReady(ready) {
|
||||
this.configReady = ready;
|
||||
this.$emit('configLoaded', ...arguments);
|
||||
},
|
||||
onYTickWidthChange() {
|
||||
this.$emit('plotYTickWidth', ...arguments);
|
||||
},
|
||||
onCursorGuideChange() {
|
||||
this.$emit('cursorGuide', ...arguments);
|
||||
},
|
||||
onGridLinesChange() {
|
||||
this.$emit('gridLines', ...arguments);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -73,17 +73,17 @@ export default {
|
||||
this.xAxis = this.getXAxisFromConfig();
|
||||
this.loaded = true;
|
||||
this.setUpXAxisOptions();
|
||||
this.openmct.time.on('timeSystem', this.syncXAxisToTimeSystem);
|
||||
this.openmct.time.on('timeSystemChanged', this.syncXAxisToTimeSystem);
|
||||
this.listenTo(this.xAxis, 'change', this.setUpXAxisOptions);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.openmct.time.off('timeSystem', this.syncXAxisToTimeSystem);
|
||||
this.openmct.time.off('timeSystemChanged', this.syncXAxisToTimeSystem);
|
||||
},
|
||||
methods: {
|
||||
isEnabledXKeyToggle() {
|
||||
const isSinglePlot = this.xKeyOptions && this.xKeyOptions.length > 1 && this.seriesModel;
|
||||
const isFrozen = this.xAxis.get('frozen');
|
||||
const inRealTimeMode = this.openmct.time.clock();
|
||||
const inRealTimeMode = this.openmct.time.isRealTime();
|
||||
|
||||
return isSinglePlot && !isFrozen && !inRealTimeMode;
|
||||
},
|
||||
|
||||
@@ -114,7 +114,7 @@ export default {
|
||||
showLimitLineLabels: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
hiddenYAxisIds: {
|
||||
@@ -725,7 +725,7 @@ export default {
|
||||
});
|
||||
},
|
||||
showLabels(seriesKey) {
|
||||
return this.showLimitLineLabels.seriesKey && this.showLimitLineLabels.seriesKey === seriesKey;
|
||||
return this.showLimitLineLabels?.seriesKey === seriesKey;
|
||||
},
|
||||
getLimitElement(limit) {
|
||||
let point = {
|
||||
|
||||
@@ -55,7 +55,8 @@ export default class LegendModel extends Model {
|
||||
showValueWhenExpanded: true,
|
||||
showMaximumWhenExpanded: true,
|
||||
showMinimumWhenExpanded: true,
|
||||
showUnitsWhenExpanded: true
|
||||
showUnitsWhenExpanded: true,
|
||||
showLegendsForChildren: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +141,10 @@ export default class PlotSeries extends Model {
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
if (this.unsubscribeLimits) {
|
||||
this.unsubscribeLimits();
|
||||
}
|
||||
|
||||
if (this.removeMutationListener) {
|
||||
this.removeMutationListener();
|
||||
}
|
||||
@@ -320,10 +324,26 @@ export default class PlotSeries extends Model {
|
||||
async load(options) {
|
||||
await this.fetch(options);
|
||||
this.emit('load');
|
||||
this.loadLimits();
|
||||
}
|
||||
|
||||
async loadLimits() {
|
||||
const limitsResponse = await this.limitDefinition.limits();
|
||||
this.limits = [];
|
||||
this.limits = {};
|
||||
if (!this.unsubscribeLimits) {
|
||||
this.unsubscribeLimits = this.openmct.telemetry.subscribeToLimits(
|
||||
this.domainObject,
|
||||
this.limitsUpdated.bind(this)
|
||||
);
|
||||
}
|
||||
this.limitsUpdated(limitsResponse);
|
||||
}
|
||||
|
||||
limitsUpdated(limitsResponse) {
|
||||
if (limitsResponse) {
|
||||
this.limits = limitsResponse;
|
||||
} else {
|
||||
this.limits = {};
|
||||
}
|
||||
|
||||
this.emit('limits', this);
|
||||
|
||||
@@ -73,6 +73,12 @@
|
||||
<div v-if="isStackedPlotObject || !isNestedWithinAStackedPlot" class="grid-properties">
|
||||
<ul class="l-inspector-part js-legend-properties">
|
||||
<h2 class="--first" title="Legend settings for this object">Legend</h2>
|
||||
<li v-if="isStackedPlotObject" class="grid-row">
|
||||
<div class="grid-cell label" title="Display legends per sub plot.">
|
||||
Show legend per plot
|
||||
</div>
|
||||
<div class="grid-cell value">{{ showLegendsForChildren ? 'Yes' : 'No' }}</div>
|
||||
</li>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
class="grid-cell label"
|
||||
@@ -139,6 +145,7 @@ export default {
|
||||
showMinimumWhenExpanded: '',
|
||||
showMaximumWhenExpanded: '',
|
||||
showUnitsWhenExpanded: '',
|
||||
showLegendsForChildren: '',
|
||||
loaded: false,
|
||||
plotSeries: [],
|
||||
yAxes: []
|
||||
@@ -218,6 +225,7 @@ export default {
|
||||
this.showMinimumWhenExpanded = this.config.legend.get('showMinimumWhenExpanded');
|
||||
this.showMaximumWhenExpanded = this.config.legend.get('showMaximumWhenExpanded');
|
||||
this.showUnitsWhenExpanded = this.config.legend.get('showUnitsWhenExpanded');
|
||||
this.showLegendsForChildren = this.config.legend.get('showLegendsForChildren');
|
||||
}
|
||||
},
|
||||
getConfig() {
|
||||
|
||||
@@ -35,7 +35,11 @@
|
||||
:y-axis="config.yAxis"
|
||||
@seriesUpdated="updateSeriesConfigForObject"
|
||||
/>
|
||||
<ul v-if="isStackedPlotObject || !isStackedPlotNestedObject" class="l-inspector-part">
|
||||
<ul
|
||||
v-if="isStackedPlotObject || !isStackedPlotNestedObject"
|
||||
class="l-inspector-part"
|
||||
aria-label="Legend Properties"
|
||||
>
|
||||
<h2 class="--first" title="Legend options">Legend</h2>
|
||||
<legend-form class="grid-properties" :legend="config.legend" />
|
||||
</ul>
|
||||
|
||||
@@ -21,6 +21,16 @@
|
||||
-->
|
||||
<template>
|
||||
<div>
|
||||
<li v-if="isStackedPlotObject" class="grid-row">
|
||||
<div class="grid-cell label" title="Display legends per sub plot.">Show legend per plot</div>
|
||||
<div class="grid-cell value">
|
||||
<input
|
||||
v-model="showLegendsForChildren"
|
||||
type="checkbox"
|
||||
@change="updateForm('showLegendsForChildren')"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
class="grid-cell label"
|
||||
@@ -128,7 +138,7 @@ import { coerce, objectPath, validate } from './formUtil';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
inject: ['openmct', 'domainObject', 'path'],
|
||||
props: {
|
||||
legend: {
|
||||
type: Object,
|
||||
@@ -148,9 +158,18 @@ export default {
|
||||
showMinimumWhenExpanded: '',
|
||||
showMaximumWhenExpanded: '',
|
||||
showUnitsWhenExpanded: '',
|
||||
showLegendsForChildren: '',
|
||||
validation: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isStackedPlotObject() {
|
||||
return this.path.find(
|
||||
(pathObject, pathObjIndex) =>
|
||||
pathObjIndex === 0 && pathObject?.type === 'telemetry.plot.stacked'
|
||||
);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initialize();
|
||||
this.initFormValues();
|
||||
@@ -200,6 +219,11 @@ export default {
|
||||
modelProp: 'showUnitsWhenExpanded',
|
||||
coerce: Boolean,
|
||||
objectPath: 'configuration.legend.showUnitsWhenExpanded'
|
||||
},
|
||||
{
|
||||
modelProp: 'showLegendsForChildren',
|
||||
coerce: Boolean,
|
||||
objectPath: 'configuration.legend.showLegendsForChildren'
|
||||
}
|
||||
];
|
||||
},
|
||||
@@ -213,6 +237,7 @@ export default {
|
||||
this.showMinimumWhenExpanded = this.legend.get('showMinimumWhenExpanded');
|
||||
this.showMaximumWhenExpanded = this.legend.get('showMaximumWhenExpanded');
|
||||
this.showUnitsWhenExpanded = this.legend.get('showUnitsWhenExpanded');
|
||||
this.showLegendsForChildren = this.legend.get('showLegendsForChildren');
|
||||
},
|
||||
updateForm(formKey) {
|
||||
const newVal = this[formKey];
|
||||
|
||||
@@ -29,7 +29,12 @@
|
||||
@mouseover="toggleHover(true)"
|
||||
@mouseleave="toggleHover(false)"
|
||||
>
|
||||
<div class="plot-series-swatch-and-name">
|
||||
<div
|
||||
ref="series"
|
||||
class="plot-series-swatch-and-name"
|
||||
@mouseover.ctrl="showToolTip"
|
||||
@mouseleave="hideToolTip"
|
||||
>
|
||||
<span class="plot-series-color-swatch" :style="{ 'background-color': colorAsHexString }">
|
||||
</span>
|
||||
<span class="is-status__indicator" title="This item is missing or suspect"></span>
|
||||
@@ -59,9 +64,10 @@ import { getLimitClass } from '@/plugins/plot/chart/limitUtil';
|
||||
import eventHelpers from '../lib/eventHelpers';
|
||||
import stalenessMixin from '@/ui/mixins/staleness-mixin';
|
||||
import configStore from '../configuration/ConfigStore';
|
||||
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
|
||||
|
||||
export default {
|
||||
mixins: [stalenessMixin],
|
||||
mixins: [stalenessMixin, tooltipHelpers],
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
seriesObject: {
|
||||
@@ -181,9 +187,22 @@ export default {
|
||||
},
|
||||
toggleHover(hover) {
|
||||
this.hover = hover;
|
||||
this.$emit('legendHoverChanged', {
|
||||
seriesKey: this.hover ? this.seriesObject.keyString : ''
|
||||
});
|
||||
this.$emit(
|
||||
'legendHoverChanged',
|
||||
this.hover
|
||||
? {
|
||||
seriesKey: this.seriesObject.keyString
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
},
|
||||
async showToolTip() {
|
||||
const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;
|
||||
this.buildToolTip(
|
||||
await this.getTelemetryPathString(this.seriesObject.domainObject.identifier),
|
||||
BELOW,
|
||||
'series'
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -33,7 +33,14 @@
|
||||
<span class="plot-series-color-swatch" :style="{ 'background-color': colorAsHexString }">
|
||||
</span>
|
||||
<span class="is-status__indicator" title="This item is missing or suspect"></span>
|
||||
<span class="plot-series-name">{{ name }}</span>
|
||||
<span
|
||||
ref="seriesName"
|
||||
class="plot-series-name"
|
||||
@mouseover.ctrl="showToolTip"
|
||||
@mouseleave="hideToolTip"
|
||||
>
|
||||
{{ name }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td v-if="showTimestampWhenExpanded">
|
||||
@@ -72,9 +79,10 @@ import { getLimitClass } from '@/plugins/plot/chart/limitUtil';
|
||||
import eventHelpers from '@/plugins/plot/lib/eventHelpers';
|
||||
import stalenessMixin from '@/ui/mixins/staleness-mixin';
|
||||
import configStore from '../configuration/ConfigStore';
|
||||
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
|
||||
|
||||
export default {
|
||||
mixins: [stalenessMixin],
|
||||
mixins: [stalenessMixin, tooltipHelpers],
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
seriesObject: {
|
||||
@@ -205,6 +213,14 @@ export default {
|
||||
this.$emit('legendHoverChanged', {
|
||||
seriesKey: this.hover ? this.seriesObject.keyString : ''
|
||||
});
|
||||
},
|
||||
async showToolTip() {
|
||||
const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;
|
||||
this.buildToolTip(
|
||||
await this.getTelemetryPathString(this.seriesObject.domainObject.identifier),
|
||||
BELOW,
|
||||
'seriesName'
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -27,9 +27,10 @@
|
||||
:class="[plotLegendExpandedStateClass, plotLegendPositionClass]"
|
||||
>
|
||||
<plot-legend
|
||||
v-if="compositionObjectsConfigLoaded"
|
||||
v-if="compositionObjectsConfigLoaded && showLegendsForChildren === false"
|
||||
:cursor-locked="!!lockHighlightPoint"
|
||||
:highlights="highlights"
|
||||
class="js-stacked-plot-legend"
|
||||
@legendHoverChanged="legendHoverChanged"
|
||||
@expanded="updateExpanded"
|
||||
@position="updatePosition"
|
||||
@@ -46,6 +47,7 @@
|
||||
:cursor-guide="cursorGuide"
|
||||
:show-limit-line-labels="showLimitLineLabels"
|
||||
:parent-y-tick-width="maxTickWidth"
|
||||
:hide-legend="showLegendsForChildren === false"
|
||||
@plotYTickWidth="onYTickWidthChange"
|
||||
@loadingUpdated="loadingUpdated"
|
||||
@cursorGuide="onCursorGuideChange"
|
||||
@@ -66,6 +68,7 @@ import ColorPalette from '@/ui/color/ColorPalette';
|
||||
import PlotLegend from '../legend/PlotLegend.vue';
|
||||
import StackedPlotItem from './StackedPlotItem.vue';
|
||||
import ImageExporter from '../../../exporters/ImageExporter';
|
||||
import eventHelpers from '../lib/eventHelpers';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -96,19 +99,28 @@ export default {
|
||||
colorPalette: new ColorPalette(),
|
||||
compositionObjectsConfigLoaded: false,
|
||||
position: 'top',
|
||||
showLegendsForChildren: true,
|
||||
expanded: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
plotLegendPositionClass() {
|
||||
if (this.showLegendsForChildren) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `plot-legend-${this.position}`;
|
||||
},
|
||||
plotLegendExpandedStateClass() {
|
||||
if (this.expanded) {
|
||||
return 'plot-legend-expanded';
|
||||
} else {
|
||||
return 'plot-legend-collapsed';
|
||||
let legendExpandedStateClass = '';
|
||||
|
||||
if (this.showLegendsForChildren !== true && this.expanded) {
|
||||
legendExpandedStateClass = 'plot-legend-expanded';
|
||||
} else if (this.showLegendsForChildren !== true && !this.expanded) {
|
||||
legendExpandedStateClass = 'plot-legend-collapsed';
|
||||
}
|
||||
|
||||
return legendExpandedStateClass;
|
||||
},
|
||||
/**
|
||||
* Returns the maximum width of the left and right y axes ticks of this stacked plots children
|
||||
@@ -137,9 +149,11 @@ export default {
|
||||
this.destroy();
|
||||
},
|
||||
mounted() {
|
||||
eventHelpers.extend(this);
|
||||
//We only need to initialize the stacked plot config for legend properties
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.config = this.getConfig(configId);
|
||||
this.showLegendsForChildren = this.config.legend.get('showLegendsForChildren');
|
||||
|
||||
this.loaded = true;
|
||||
this.imageExporter = new ImageExporter(this.openmct);
|
||||
@@ -183,11 +197,21 @@ export default {
|
||||
|
||||
return this.configLoaded[id] === true;
|
||||
});
|
||||
if (this.compositionObjectsConfigLoaded) {
|
||||
this.listenTo(
|
||||
this.config.legend,
|
||||
'change:showLegendsForChildren',
|
||||
this.updateShowLegendsForChildren,
|
||||
this
|
||||
);
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
this.composition.off('add', this.addChild);
|
||||
this.composition.off('remove', this.removeChild);
|
||||
this.composition.off('reorder', this.compositionReorder);
|
||||
|
||||
this.stopListening();
|
||||
},
|
||||
|
||||
addChild(child) {
|
||||
@@ -305,6 +329,9 @@ export default {
|
||||
updatePosition(position) {
|
||||
this.position = position;
|
||||
},
|
||||
updateShowLegendsForChildren(showLegendsForChildren) {
|
||||
this.showLegendsForChildren = showLegendsForChildren;
|
||||
},
|
||||
updateReady(ready) {
|
||||
this.configReady = ready;
|
||||
},
|
||||
|
||||
@@ -23,14 +23,13 @@
|
||||
<div :aria-label="`Stacked Plot Item ${childObject.name}`"></div>
|
||||
</template>
|
||||
<script>
|
||||
import MctPlot from '../MctPlot.vue';
|
||||
import Vue from 'vue';
|
||||
import conditionalStylesMixin from './mixins/objectStyles-mixin';
|
||||
import stalenessMixin from '@/ui/mixins/staleness-mixin';
|
||||
import StalenessUtils from '@/utils/staleness';
|
||||
import configStore from '@/plugins/plot/configuration/ConfigStore';
|
||||
import PlotConfigurationModel from '@/plugins/plot/configuration/PlotConfigurationModel';
|
||||
import ProgressBar from '../../../ui/components/ProgressBar.vue';
|
||||
import Plot from '../Plot.vue';
|
||||
|
||||
export default {
|
||||
mixins: [conditionalStylesMixin, stalenessMixin],
|
||||
@@ -63,7 +62,7 @@ export default {
|
||||
showLimitLineLabels: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
colorPalette: {
|
||||
@@ -81,6 +80,12 @@ export default {
|
||||
hasMultipleLeftAxes: false
|
||||
};
|
||||
}
|
||||
},
|
||||
hideLegend: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -104,6 +109,9 @@ export default {
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
hideLegend(newHideLegend) {
|
||||
this.updateComponentProp('hideLegend', newHideLegend);
|
||||
},
|
||||
staleObjects() {
|
||||
this.isStale = this.staleObjects.length > 0;
|
||||
this.updateComponentProp('isStale', this.isStale);
|
||||
@@ -163,7 +171,6 @@ export default {
|
||||
const onConfigLoaded = this.onConfigLoaded;
|
||||
const onCursorGuideChange = this.onCursorGuideChange;
|
||||
const onGridLinesChange = this.onGridLinesChange;
|
||||
const setStatus = this.setStatus;
|
||||
|
||||
const openmct = this.openmct;
|
||||
const path = this.path;
|
||||
@@ -192,8 +199,7 @@ export default {
|
||||
this.component = new Vue({
|
||||
el: viewContainer,
|
||||
components: {
|
||||
MctPlot,
|
||||
ProgressBar
|
||||
Plot
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
@@ -209,7 +215,6 @@ export default {
|
||||
onConfigLoaded,
|
||||
onCursorGuideChange,
|
||||
onGridLinesChange,
|
||||
setStatus,
|
||||
isMissing,
|
||||
loading: false
|
||||
};
|
||||
@@ -220,29 +225,22 @@ export default {
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div v-if="!isMissing" ref="plotWrapper"
|
||||
class="l-view-section u-style-receiver js-style-receiver"
|
||||
:class="{'s-status-timeconductor-unsynced': status && status === 'timeconductor-unsynced', 'is-stale': isStale}">
|
||||
<progress-bar
|
||||
v-show="loading !== false"
|
||||
class="c-telemetry-table__progress-bar"
|
||||
:model="{progressPerc: undefined}" />
|
||||
<mct-plot
|
||||
:init-grid-lines="gridLines"
|
||||
:init-cursor-guide="cursorGuide"
|
||||
:parent-y-tick-width="parentYTickWidth"
|
||||
:limit-line-labels="limitLineLabels"
|
||||
:color-palette="colorPalette"
|
||||
:options="options"
|
||||
@plotYTickWidth="onYTickWidthChange"
|
||||
@lockHighlightPoint="onLockHighlightPointUpdated"
|
||||
@highlights="onHighlightsUpdated"
|
||||
@configLoaded="onConfigLoaded"
|
||||
@cursorGuide="onCursorGuideChange"
|
||||
@gridLines="onGridLinesChange"
|
||||
@statusUpdated="setStatus"
|
||||
@loadingUpdated="loadingUpdated"/>
|
||||
</div>`
|
||||
<Plot ref="plotComponent" v-if="!isMissing"
|
||||
:class="{'is-stale': isStale}"
|
||||
:grid-lines="gridLines"
|
||||
:hide-legend="hideLegend"
|
||||
:cursor-guide="cursorGuide"
|
||||
:parent-limit-line-labels="limitLineLabels"
|
||||
:options="options"
|
||||
:parent-y-tick-width="parentYTickWidth"
|
||||
:color-palette="colorPalette"
|
||||
@loadingUpdated="loadingUpdated"
|
||||
@configLoaded="onConfigLoaded"
|
||||
@lockHighlightPoint="onLockHighlightPointUpdated"
|
||||
@highlights="onHighlightsUpdated"
|
||||
@plotYTickWidth="onYTickWidthChange"
|
||||
@cursorGuide="onCursorGuideChange"
|
||||
@gridLines="onGridLinesChange"/>`
|
||||
});
|
||||
|
||||
if (this.isEditing) {
|
||||
@@ -315,10 +313,6 @@ export default {
|
||||
onGridLinesChange() {
|
||||
this.$emit('gridLines', ...arguments);
|
||||
},
|
||||
setStatus(status) {
|
||||
this.status = status;
|
||||
this.updateComponentProp('status', status);
|
||||
},
|
||||
setSelection() {
|
||||
let childContext = {};
|
||||
childContext.item = this.childObject;
|
||||
@@ -331,12 +325,12 @@ export default {
|
||||
},
|
||||
getProps() {
|
||||
return {
|
||||
hideLegend: this.hideLegend,
|
||||
limitLineLabels: this.showLimitLineLabels,
|
||||
gridLines: this.gridLines,
|
||||
cursorGuide: this.cursorGuide,
|
||||
parentYTickWidth: this.parentYTickWidth,
|
||||
options: this.options,
|
||||
status: this.status,
|
||||
colorPalette: this.colorPalette,
|
||||
isStale: this.isStale
|
||||
};
|
||||
|
||||
@@ -490,12 +490,12 @@ describe('the plugin', function () {
|
||||
max: 10
|
||||
});
|
||||
Vue.nextTick(() => {
|
||||
expect(plotViewComponentObject.$children[0].component.$children[1].xScale.domain()).toEqual(
|
||||
{
|
||||
min: 0,
|
||||
max: 10
|
||||
}
|
||||
);
|
||||
expect(
|
||||
plotViewComponentObject.$children[0].component.$children[0].$children[1].xScale.domain()
|
||||
).toEqual({
|
||||
min: 0,
|
||||
max: 10
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -509,7 +509,8 @@ describe('the plugin', function () {
|
||||
});
|
||||
});
|
||||
Vue.nextTick(() => {
|
||||
const yAxesScales = plotViewComponentObject.$children[0].component.$children[1].yScale;
|
||||
const yAxesScales =
|
||||
plotViewComponentObject.$children[0].component.$children[0].$children[1].yScale;
|
||||
yAxesScales.forEach((yAxisScale) => {
|
||||
expect(yAxisScale.scale.domain()).toEqual({
|
||||
min: 10,
|
||||
|
||||
@@ -115,6 +115,10 @@ describe('the RemoteClock plugin', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
openmct.time.setClock('local');
|
||||
});
|
||||
|
||||
it('Does not throw error if time system is changed before remote clock initialized', () => {
|
||||
expect(() => openmct.time.timeSystem('utc')).not.toThrow();
|
||||
});
|
||||
|
||||
@@ -35,12 +35,15 @@
|
||||
</div>
|
||||
<div
|
||||
v-for="(tab, index) in tabsList"
|
||||
:ref="tab.keyString"
|
||||
:key="tab.keyString"
|
||||
class="c-tab c-tabs-view__tab js-tab"
|
||||
:class="{
|
||||
'is-current': isCurrent(tab)
|
||||
}"
|
||||
@click="showTab(tab, index)"
|
||||
@mouseover.ctrl="showToolTip(tab)"
|
||||
@mouseleave="hideToolTip"
|
||||
>
|
||||
<div
|
||||
ref="tabsLabel"
|
||||
@@ -79,6 +82,7 @@
|
||||
<script>
|
||||
import ObjectView from '../../../ui/components/ObjectView.vue';
|
||||
import RemoveAction from '../../remove/RemoveAction.js';
|
||||
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
|
||||
import _ from 'lodash';
|
||||
|
||||
const unknownObjectType = {
|
||||
@@ -92,6 +96,7 @@ export default {
|
||||
components: {
|
||||
ObjectView
|
||||
},
|
||||
mixins: [tooltipHelpers],
|
||||
inject: ['openmct', 'domainObject', 'composition', 'objectPath'],
|
||||
props: {
|
||||
isEditing: {
|
||||
@@ -389,6 +394,11 @@ export default {
|
||||
|
||||
this.tabWidth = this.$refs.tabs.offsetWidth + 'px';
|
||||
this.tabHeight = this.$refs.tabsHolder.offsetHeight - this.$refs.tabs.offsetHeight + 'px';
|
||||
},
|
||||
async showToolTip(tab) {
|
||||
const identifier = tab.domainObject.identifier;
|
||||
const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;
|
||||
this.buildToolTip(await this.getObjectPath(identifier), BELOW, tab.keyString);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -215,8 +215,13 @@ define([
|
||||
return;
|
||||
}
|
||||
|
||||
const metadataValue = this.openmct.telemetry
|
||||
.getMetadata(this.telemetryObjects[keyString].telemetryObject)
|
||||
.getUseToUpdateInPlaceValue();
|
||||
|
||||
let telemetryRows = telemetry.map(
|
||||
(datum) => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator)
|
||||
(datum) =>
|
||||
new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator, metadataValue?.key)
|
||||
);
|
||||
|
||||
if (this.paused) {
|
||||
@@ -268,8 +273,14 @@ define([
|
||||
Object.keys(this.telemetryCollections).forEach((keyString) => {
|
||||
let { columnMap, limitEvaluator } = this.telemetryObjects[keyString];
|
||||
|
||||
const metadataValue = this.openmct.telemetry
|
||||
.getMetadata(this.telemetryObjects[keyString].telemetryObject)
|
||||
.getUseToUpdateInPlaceValue();
|
||||
|
||||
this.telemetryCollections[keyString].getAll().forEach((datum) => {
|
||||
allRows.push(new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator));
|
||||
allRows.push(
|
||||
new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator, metadataValue?.key)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -321,11 +332,12 @@ define([
|
||||
}
|
||||
|
||||
addColumnsForObject(telemetryObject) {
|
||||
let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values();
|
||||
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||
let metadataValues = metadata.values();
|
||||
|
||||
this.addNameColumn(telemetryObject, metadataValues);
|
||||
metadataValues.forEach((metadatum) => {
|
||||
if (metadatum.key === 'name') {
|
||||
if (metadatum.key === 'name' || metadata.isInPlaceUpdateValue(metadatum)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,13 +22,14 @@
|
||||
|
||||
define([], function () {
|
||||
class TelemetryTableRow {
|
||||
constructor(datum, columns, objectKeyString, limitEvaluator) {
|
||||
constructor(datum, columns, objectKeyString, limitEvaluator, inPlaceUpdateKey) {
|
||||
this.columns = columns;
|
||||
|
||||
this.datum = createNormalizedDatum(datum, columns);
|
||||
this.fullDatum = datum;
|
||||
this.limitEvaluator = limitEvaluator;
|
||||
this.objectKeyString = objectKeyString;
|
||||
this.inPlaceUpdateKey = inPlaceUpdateKey;
|
||||
}
|
||||
|
||||
getFormattedDatum(headers) {
|
||||
@@ -88,6 +89,18 @@ define([], function () {
|
||||
getContextMenuActions() {
|
||||
return ['viewDatumAction', 'viewHistoricalData'];
|
||||
}
|
||||
|
||||
updateWithDatum(updatesToDatum) {
|
||||
const normalizedUpdatesToDatum = createNormalizedDatum(updatesToDatum, this.columns);
|
||||
this.datum = {
|
||||
...this.datum,
|
||||
...normalizedUpdatesToDatum
|
||||
};
|
||||
this.fullDatum = {
|
||||
...this.fullDatum,
|
||||
...updatesToDatum
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,7 +114,10 @@ define([], function () {
|
||||
const normalizedDatum = JSON.parse(JSON.stringify(datum));
|
||||
|
||||
Object.values(columns).forEach((column) => {
|
||||
normalizedDatum[column.getKey()] = column.getRawValue(datum);
|
||||
const rawValue = column.getRawValue(datum);
|
||||
if (rawValue !== undefined) {
|
||||
normalizedDatum[column.getKey()] = rawValue;
|
||||
}
|
||||
});
|
||||
|
||||
return normalizedDatum;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user