Compare commits

..

21 Commits

Author SHA1 Message Date
Michael Rogers
15bab81447 Merge branch 'master' into mct6555-v2 2023-06-16 20:03:03 -05:00
Michael Rogers
cf8db0a21d Cleanup 2023-06-16 18:19:48 -05:00
Michael Rogers
bda652b3bb Removed status.getStatusRoleForCurrentUser 2023-06-16 18:18:30 -05:00
Michael Rogers
e7f2f4b5f6 Remove default status role from example user plugin 2023-06-15 11:53:45 -05:00
Michael Rogers
18b5ee1340 Removed unused role param from status api call 2023-06-15 11:50:35 -05:00
Michael Rogers
9006c13eb4 Lint 2023-06-14 16:20:18 -05:00
Michael Rogers
d7b16a5a2c Added success notification and cleanup 2023-06-14 16:17:02 -05:00
Michael Rogers
a76701c23a Store status roles in an array instead of a singular value 2023-06-14 11:45:40 -05:00
Michael Rogers
bb52291ab6 Cleanup 2023-06-14 11:02:40 -05:00
Michael Rogers
87db357b5a Updates to status api canPRovideStatusForRole 2023-06-14 11:00:01 -05:00
Michael Rogers
587c27464b Reconnect channel on error and UserIndicator updates 2023-06-07 17:36:53 -05:00
Michael Rogers
e0bad2620e Moved prompt to UserIndicator 2023-06-07 11:20:48 -05:00
Michael Rogers
0222b77941 UserAPI role updates and UserIndicator improvement 2023-06-01 17:24:13 -05:00
Michael Rogers
e05d812219 Comment width 2023-06-01 10:04:45 -05:00
Michael Rogers
d51498752f Update comment 2023-06-01 10:03:16 -05:00
Michael Rogers
a3b1fa34e4 Updates to broadcast channel lifecycle 2023-06-01 09:57:16 -05:00
Michael Rogers
598ebddd29 Display role in user indicator 2023-05-31 17:30:30 -05:00
Michael Rogers
010e86bf83 Added selection dialog to overlays and implemented for operator status 2023-05-31 17:17:49 -05:00
Michael Rogers
e8ed10db78 Update example user provider 2023-05-26 16:54:25 -05:00
Michael Rogers
fd20d392c2 Add session storage and role to user indicator 2023-05-26 16:52:16 -05:00
Michael Rogers
841c999d16 Add additional test roles to example user 2023-05-08 14:08:37 -05:00
369 changed files with 5338 additions and 10055 deletions

View File

@@ -242,6 +242,10 @@ 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:

View File

@@ -28,8 +28,6 @@ module.exports = {
}
},
rules: {
'vue/no-v-for-template-key': 'off',
'vue/no-v-for-template-key-on-child': 'error',
'prettier/prettier': 'error',
'you-dont-need-lodash-underscore/omit': 'off',
'you-dont-need-lodash-underscore/throttle': 'off',

View File

@@ -1,43 +1,21 @@
name: 'e2e-couchdb'
on:
push:
branches: master
workflow_dispatch:
pull_request:
types:
- labeled
- opened
schedule:
- cron: '0 0 * * *'
jobs:
e2e-couchdb:
if: contains(github.event.pull_request.labels.*.name, 'pr:e2e:couchdb') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.action == 'opened'
if: ${{ github.event.label.name == 'pr:e2e:couchdb' }} || ${{ 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/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 }}
node-version: 'lts/gallium'
- 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)
@@ -45,32 +23,26 @@ 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: always()
if: ${{ contains(github.event.pull_request.labels.*.name, 'pr:e2e:couchdb') }}
uses: actions/github-script@v6
with:
script: |
@@ -84,5 +56,5 @@ jobs:
name: labelToRemove
});
} catch (error) {
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
core.warning(`Failed to remove 'pr:e2e:couchdb' label: ${error.message}`);
}

View File

@@ -1,41 +1,37 @@
name: 'e2e-pr'
on:
push:
branches: master
workflow_dispatch:
pull_request:
types:
- labeled
- opened
schedule:
- cron: '0 0 * * *'
jobs:
e2e-full:
if: contains(github.event.pull_request.labels.*.name, 'pr:e2e') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
if: ${{ github.event.label.name == 'pr:e2e' }}
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: '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-
node-version: '16'
- run: npx playwright@1.32.3 install
- run: npx playwright install chrome-beta
- run: npm install --cache ~/.npm --prefer-offline --no-audit --progress=false
- run: npm install
- run: npm run test:e2e:full -- --max-failures=40
- run: npm run cov:e2e:report || true
- shell: bash
@@ -48,9 +44,30 @@ 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: always()
if: ${{ contains(github.event.pull_request.labels.*.name, 'pr:e2e') }}
uses: actions/github-script@v6
with:
script: |
@@ -64,5 +81,5 @@ jobs:
name: labelToRemove
});
} catch (error) {
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
}
core.warning(`Failed to remove 'pr:e2e' label: ${error.message}`);
}

View File

@@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: lts/hydrogen
node-version: 16
- 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: lts/hydrogen
node-version: 16
registry-url: https://registry.npmjs.org/
- run: npm install
- run: npm publish --access=public --tag unstable

View File

@@ -1,19 +1,13 @@
name: 'pr-platform'
on:
push:
branches: master
workflow_dispatch:
pull_request:
types:
- labeled
- opened
schedule:
- cron: '0 0 * * *'
types: [labeled]
jobs:
pr-platform:
if: contains(github.event.pull_request.labels.*.name, 'pr:platform') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
e2e-full:
if: ${{ github.event.label.name == 'pr:platform' }}
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
@@ -22,49 +16,18 @@ jobs:
- macos-latest
- windows-latest
node_version:
- lts/gallium
- lts/hydrogen
- 16
- 18
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 }}
- 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 install
- 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}`);
}

View File

@@ -1,6 +1,5 @@
{
"trailingComma": "none",
"singleQuote": true,
"printWidth": 100,
"endOfLine": "auto"
"printWidth": 100
}

View File

@@ -67,8 +67,8 @@ const config = {
MCT: path.join(projectRootDir, 'src/MCT'),
testUtils: path.join(projectRootDir, 'src/utils/testUtils.js'),
objectUtils: path.join(projectRootDir, 'src/api/objects/object-utils.js'),
utils: path.join(projectRootDir, 'src/utils'),
vue: path.join(projectRootDir, 'node_modules/@vue/compat/dist/vue.esm-bundler.js'),
kdbush: path.join(projectRootDir, 'node_modules/kdbush/kdbush.min.js'),
utils: path.join(projectRootDir, 'src/utils')
}
},
plugins: [
@@ -122,15 +122,7 @@ const config = {
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
compilerOptions: {
whitespace: 'preserve',
compatConfig: {
MODE: 2
}
}
}
use: 'vue-loader'
},
{
test: /\.html$/,

View File

@@ -25,6 +25,11 @@ module.exports = merge(common, {
'**/.*' // dotfiles and dotfolders
]
},
resolve: {
alias: {
vue: path.join(projectRootDir, 'node_modules/vue/dist/vue.js')
}
},
plugins: [
new webpack.DefinePlugin({
__OPENMCT_ROOT_RELATIVE__: '"dist/"'

View File

@@ -13,6 +13,11 @@ const projectRootDir = path.resolve(__dirname, '..');
module.exports = merge(common, {
mode: 'production',
resolve: {
alias: {
vue: path.join(projectRootDir, 'node_modules/vue/dist/vue.min.js')
}
},
plugins: [
new webpack.DefinePlugin({
__OPENMCT_ROOT_RELATIVE__: '""'

6
API.md
View File

@@ -2,7 +2,7 @@
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**
- [Developing Applications With Open MCT](#developing-applications-with-open-mct)
- [Building 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,8 +873,6 @@ 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`.

View File

@@ -314,9 +314,7 @@ async function _isInEditMode(page, identifier) {
*/
async function setTimeConductorMode(page, isFixedTimespan = true) {
// Click 'mode' button
const timeConductorMode = await page.locator('.c-compact-tc');
await timeConductorMode.click();
await timeConductorMode.locator('.js-mode-button').click();
await page.locator('.c-mode-button').click();
// Switch time conductor mode
if (isFixedTimespan) {
@@ -355,23 +353,23 @@ async function setRealTimeMode(page) {
* @param {OffsetValues} offset
* @param {import('@playwright/test').Locator} offsetButton
*/
async function setTimeConductorOffset(page, { hours, mins, secs }) {
// await offsetButton.click();
async function setTimeConductorOffset(page, { hours, mins, secs }, offsetButton) {
await offsetButton.click();
if (hours) {
await page.fill('.pr-time-input__hrs', hours);
await page.fill('.pr-time-controls__hrs', hours);
}
if (mins) {
await page.fill('.pr-time-input__mins', mins);
await page.fill('.pr-time-controls__mins', mins);
}
if (secs) {
await page.fill('.pr-time-input__secs', secs);
await page.fill('.pr-time-controls__secs', secs);
}
// Click the check button
await page.locator('.pr-time-input--buttons .icon-check').click();
await page.locator('.pr-time__buttons .icon-check').click();
}
/**
@@ -380,10 +378,8 @@ async function setTimeConductorOffset(page, { hours, mins, secs }) {
* @param {OffsetValues} offset
*/
async function setStartOffset(page, offset) {
// Click 'mode' button
const timeConductorMode = await page.locator('.c-compact-tc');
await timeConductorMode.click();
await setTimeConductorOffset(page, offset);
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
await setTimeConductorOffset(page, offset, startOffsetButton);
}
/**
@@ -392,10 +388,8 @@ async function setStartOffset(page, offset) {
* @param {OffsetValues} offset
*/
async function setEndOffset(page, offset) {
// Click 'mode' button
const timeConductorMode = await page.locator('.c-compact-tc');
await timeConductorMode.click();
await setTimeConductorOffset(page, offset);
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
await setTimeConductorOffset(page, offset, endOffsetButton);
}
/**
@@ -407,7 +401,14 @@ async function setEndOffset(page, offset) {
async function selectInspectorTab(page, name) {
const inspectorTabs = page.getByRole('tablist');
const inspectorTab = inspectorTabs.getByTitle(name);
await inspectorTab.click();
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();
}
}
/**

View File

@@ -29,7 +29,7 @@
*/
const base = require('@playwright/test');
const { expect, request } = base;
const { expect } = base;
const fs = require('fs');
const path = require('path');
const { v4: uuid } = require('uuid');
@@ -179,5 +179,4 @@ exports.test = base.test.extend({
});
exports.expect = expect;
exports.request = request;
exports.waitForAnimations = waitForAnimations;

View File

@@ -77,6 +77,7 @@ const config = {
}
],
['junit', { outputFile: '../test-results/results.xml' }],
['github'],
['@deploysentinel/playwright']
]
};

View File

@@ -26,7 +26,7 @@
* and appActions. These fixtures should be generalized across all plugins.
*/
const { test, expect, request } = require('./baseFixtures');
const { test, expect } = require('./baseFixtures');
// const { createDomainObjectWithDefaults } = require('./appActions');
const path = require('path');
@@ -147,7 +147,6 @@ exports.test = test.extend({
}
});
exports.expect = expect;
exports.request = request;
/**
* Takes a readable stream and returns a string.

View File

@@ -5,18 +5,18 @@
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1689710399654,\"created\":1689710398656,\"persisted\":1689710399654},\"58f55f3a-46d9-4c37-a726-27b5d38b895a\":{\"identifier\":{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"},\"name\":\"Overlay Plot:b0ba67ab-e383-40c1-a181-82b174e8fdf0\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateVisualTestData.e2e.spec.js\\nGenerate Visual Test Data @localStorage\\nchrome\",\"modified\":1689710400878,\"location\":\"mine\",\"created\":1689710399651,\"persisted\":1689710400878},\"19f2e461-190e-4662-8d62-251e90bb7aac\":{\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"identifier\":{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\",\"infinityValues\":false,\"staleness\":false},\"modified\":1689710400433,\"location\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"created\":1689710400433,\"persisted\":1689710400433}}"
"name": "tcHistory",
"value": "{\"utc\":[{\"start\":1658617611983,\"end\":1658619411983}]}"
},
{
"name": "mct-recent-objects",
"value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"},\"name\":\"Overlay Plot:b0ba67ab-e383-40c1-a181-82b174e8fdf0\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateVisualTestData.e2e.spec.js\\nGenerate Visual Test Data @localStorage\\nchrome\",\"modified\":1689710400435,\"location\":\"mine\",\"created\":1689710399651,\"persisted\":1689710400436},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1689710399654,\"created\":1689710398656,\"persisted\":1689710399654},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"domainObject\":{\"identifier\":{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"},\"name\":\"Overlay Plot:b0ba67ab-e383-40c1-a181-82b174e8fdf0\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateVisualTestData.e2e.spec.js\\nGenerate Visual Test Data @localStorage\\nchrome\",\"modified\":1689710400435,\"location\":\"mine\",\"created\":1689710399651,\"persisted\":1689710400436}},{\"objectPath\":[{\"identifier\":{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"},\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\",\"infinityValues\":false,\"staleness\":false},\"modified\":1689710400433,\"location\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"created\":1689710400433,\"persisted\":1689710400433},{\"identifier\":{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"},\"name\":\"Overlay Plot:b0ba67ab-e383-40c1-a181-82b174e8fdf0\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateVisualTestData.e2e.spec.js\\nGenerate Visual Test Data @localStorage\\nchrome\",\"modified\":1689710400435,\"location\":\"mine\",\"created\":1689710399651,\"persisted\":1689710400436},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1689710399654,\"created\":1689710398656,\"persisted\":1689710399654},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/58f55f3a-46d9-4c37-a726-27b5d38b895a/19f2e461-190e-4662-8d62-251e90bb7aac\",\"domainObject\":{\"identifier\":{\"key\":\"19f2e461-190e-4662-8d62-251e90bb7aac\",\"namespace\":\"\"},\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\",\"infinityValues\":false,\"staleness\":false},\"modified\":1689710400433,\"location\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"created\":1689710400433,\"persisted\":1689710400433}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1689710399654,\"created\":1689710398656,\"persisted\":1689710399654},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine\",\"domainObject\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"58f55f3a-46d9-4c37-a726-27b5d38b895a\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1689710399654,\"created\":1689710398656,\"persisted\":1689710399654}}]"
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"7fa5749b-8969-494c-9d85-c272516d333c\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1658619412848,\"modified\":1658619412848},\"7fa5749b-8969-494c-9d85-c272516d333c\":{\"identifier\":{\"key\":\"7fa5749b-8969-494c-9d85-c272516d333c\",\"namespace\":\"\"},\"name\":\"Unnamed Overlay Plot\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\",\"namespace\":\"\"}}]},\"modified\":1658619413566,\"location\":\"mine\",\"persisted\":1658619413567},\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\":{\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"identifier\":{\"key\":\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\"},\"modified\":1658619413552,\"location\":\"7fa5749b-8969-494c-9d85-c272516d333c\",\"persisted\":1658619413552}}"
},
{
"name": "mct-tree-expanded",
"value": "[]"
"value": "[\"/browse/mine\"]"
}
]
}
]
}
}

View File

@@ -4,23 +4,19 @@
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1689710689554,\"modified\":1689710689554},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002},\"2d02a680-eb7e-4645-bba2-dd298f76efb8\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4291d80c-303c-4d8d-85e1-10f012b864fb\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1654538965702,\"location\":\"mine\",\"persisted\":1654538965702},\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2b6bf89f-877b-42b8-acc1-a9a575efdbe1\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658610682787,\"location\":\"mine\",\"persisted\":1658610682787},\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"b9a9c413-4b94-401d-b0c7-5e404f182616\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658618261112,\"location\":\"mine\",\"persisted\":1658618261112},\"3e294eae-6124-409b-a870-554d1bdcdd6f\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"108043b1-9c88-4e1d-8deb-fbf2cdb528f9\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658618890910,\"location\":\"mine\",\"persisted\":1658618890910},\"ec24d05d-5df5-4c96-9241-b73636cd19a9\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4062bd9b-b788-43dd-ab0a-8fa10a78d4b3\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658619295363,\"location\":\"mine\",\"persisted\":1658619295363},\"0ec517e8-6c11-4d98-89b5-c300fe61b304\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2f1585da-6f7e-4ccd-8a20-590fdf177b5d\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1689710689550,\"location\":\"mine\",\"created\":1689710689550,\"persisted\":1689710689550}}"
},
{
"name": "mct-tree-expanded",
"value": "[]"
},
{
"name": "tcHistory",
"value": "{\"utc\":[{\"start\":1658617494563,\"end\":1658619294563},{\"start\":1658617090044,\"end\":1658618890044},{\"start\":1658616460484,\"end\":1658618260484},{\"start\":1658608882159,\"end\":1658610682159},{\"start\":1654537164464,\"end\":1654538964464},{\"start\":1652301954635,\"end\":1652303754635}]}"
},
{
"name": "mct-recent-objects",
"value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2f1585da-6f7e-4ccd-8a20-590fdf177b5d\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1689710689550,\"location\":\"mine\",\"created\":1689710689550,\"persisted\":1689710689550},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1689710689554,\"modified\":1689710689554},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"domainObject\":{\"identifier\":{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"},\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2f1585da-6f7e-4ccd-8a20-590fdf177b5d\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1689710689550,\"location\":\"mine\",\"created\":1689710689550,\"persisted\":1689710689550}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1689710689554,\"modified\":1689710689554},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine\",\"domainObject\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},{\"key\":\"0ec517e8-6c11-4d98-89b5-c300fe61b304\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1689710689554,\"modified\":1689710689554}}]"
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1658619295366,\"modified\":1658619295366},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002},\"2d02a680-eb7e-4645-bba2-dd298f76efb8\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4291d80c-303c-4d8d-85e1-10f012b864fb\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1654538965702,\"location\":\"mine\",\"persisted\":1654538965702},\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"72a5f66b-39a7-4f62-8c40-4a99a33d6a8e\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"2b6bf89f-877b-42b8-acc1-a9a575efdbe1\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658610682787,\"location\":\"mine\",\"persisted\":1658610682787},\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"8e4d20f1-9a04-4de5-8db5-c7e08d27f70d\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"b9a9c413-4b94-401d-b0c7-5e404f182616\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658618261112,\"location\":\"mine\",\"persisted\":1658618261112},\"3e294eae-6124-409b-a870-554d1bdcdd6f\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"3e294eae-6124-409b-a870-554d1bdcdd6f\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"108043b1-9c88-4e1d-8deb-fbf2cdb528f9\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658618890910,\"location\":\"mine\",\"persisted\":1658618890910},\"ec24d05d-5df5-4c96-9241-b73636cd19a9\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"ec24d05d-5df5-4c96-9241-b73636cd19a9\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4062bd9b-b788-43dd-ab0a-8fa10a78d4b3\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1658619295363,\"location\":\"mine\",\"persisted\":1658619295363}}"
},
{
"name": "mct-tree-expanded",
"value": "[]"
}
]
}
]
}
}

View File

@@ -29,8 +29,7 @@ relates to how we've extended it (i.e. ./e2e/baseFixtures.js) and assumptions ma
const { test } = require('../../baseFixtures.js');
test.describe('baseFixtures tests', () => {
//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('Verify that tests fail if console.error is thrown', async ({ page }) => {
test.fail();
//Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });

View File

@@ -41,7 +41,7 @@ test.describe('Form Validation Behavior', () => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.click('button:has-text("Create")');
await page.getByRole('menuitem', { name: ' Folder' }).click();
await page.click(':nth-match(:text("Folder"), 2)');
// Fill in empty string into title and trigger validation with 'Tab'
await page.click('text=Properties Title Notes >> input[type="text"]');
@@ -192,12 +192,8 @@ test.describe('Persistence operations @couchdb', () => {
]);
//Slow down the test a bit
await expect(
page.getByRole('button', { name: `Expand ${myItemsFolderName} folder` })
).toBeVisible();
await expect(
page2.getByRole('button', { name: `Expand ${myItemsFolderName} folder` })
).toBeVisible();
await expect(page.getByRole('treeitem', { name: `  ${myItemsFolderName}` })).toBeVisible();
await expect(page2.getByRole('treeitem', { name: `  ${myItemsFolderName}` })).toBeVisible();
// Both pages: Click the Create button
await Promise.all([

View File

@@ -206,49 +206,6 @@ 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
}) => {

View File

@@ -158,46 +158,4 @@ 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();
});
});

View File

@@ -30,7 +30,6 @@ const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const backgroundImageSelector = '.c-imagery__main-image__background-image';
const panHotkey = process.platform === 'linux' ? ['Shift', 'Alt'] : ['Alt'];
const tagHotkey = ['Shift', 'Alt'];
const expectedAltText = process.platform === 'linux' ? 'Shift+Alt drag to pan' : 'Alt drag to pan';
const thumbnailUrlParamsRegexp = /\?w=100&h=100/;
@@ -45,7 +44,7 @@ test.describe('Example Imagery Object', () => {
// Verify that the created object is focused
await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);
await page.locator('.c-imagery__main-image__bg').hover({ trial: true });
await page.locator(backgroundImageSelector).hover({ trial: true });
});
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
@@ -70,60 +69,14 @@ test.describe('Example Imagery Object', () => {
await dragContrastSliderAndAssertFilterValues(page);
});
test('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.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').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 page.getByRole('button', { name: /Fixed Timespan/ }).click();
await page.getByTestId('conductor-modeOption-realtime').click();
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
await page.locator('.c-imagery__main-image__bg').hover({ trial: true });
await page.locator(backgroundImageSelector).hover({ trial: true });
// zoom in
await page.mouse.wheel(0, deltaYStep * 2);
await page.locator('.c-imagery__main-image__bg').hover({ trial: true });
await page.locator(backgroundImageSelector).hover({ trial: true });
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
@@ -178,36 +131,6 @@ test.describe('Example Imagery Object', () => {
expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
});
test('Can use alt+shift+drag to create a tag', async ({ page }) => {
const canvas = page.locator('canvas');
await canvas.hover({ trial: true });
const canvasBoundingBox = await canvas.boundingBox();
const canvasCenterX = canvasBoundingBox.x + canvasBoundingBox.width / 2;
const canvasCenterY = canvasBoundingBox.y + canvasBoundingBox.height / 2;
await Promise.all(tagHotkey.map((x) => page.keyboard.down(x)));
await page.mouse.down();
// steps not working for me here
await page.mouse.move(canvasCenterX - 20, canvasCenterY - 20);
await page.mouse.move(canvasCenterX - 100, canvasCenterY - 100);
await page.mouse.up();
await Promise.all(tagHotkey.map((x) => page.keyboard.up(x)));
//Wait for canvas to stablize.
await canvas.hover({ trial: true });
// add some tags
await page.getByText('Annotations').click();
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Driving').click();
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Science').click();
});
test('Can use + - buttons to zoom on the image @unstable', async ({ page }) => {
await buttonZoomOnImageAndAssert(page);
});
@@ -235,9 +158,11 @@ test.describe('Example Imagery Object', () => {
test('Using the zoom features does not pause telemetry', async ({ page }) => {
const pausePlayButton = page.locator('.c-button.pause-play');
// switch to realtime
await setRealTimeMode(page);
// open the time conductor drop down
await page.locator('.c-mode-button').click();
// Click local clock
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
// Zoom in via button
@@ -277,8 +202,11 @@ 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 setRealTimeMode(page);
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
// pause/play button
const pausePlayButton = await page.locator('.c-button.pause-play');
@@ -300,8 +228,11 @@ 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 setRealTimeMode(page);
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
// pause/play button
const pausePlayButton = await page.locator('.c-button.pause-play');
@@ -582,8 +513,11 @@ async function performImageryViewOperationsAndAssert(page) {
const nextImageButton = page.locator('.c-nav--next');
await nextImageButton.click();
// set realtime mode
await setRealTimeMode(page);
// 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();
// Zoom in on next image
await mouseZoomOnImageAndAssert(page, 2);
@@ -779,6 +713,7 @@ async function panZoomAndAssertImageProperties(page) {
async function mouseZoomOnImageAndAssert(page, factor = 2) {
// Zoom in
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
await page.locator(backgroundImageSelector).hover({ trial: true });
const deltaYStep = 100; // equivalent to 1x zoom
await page.mouse.wheel(0, deltaYStep * factor);
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
@@ -789,7 +724,7 @@ async function mouseZoomOnImageAndAssert(page, factor = 2) {
await page.mouse.move(imageCenterX, imageCenterY);
// Wait for zoom animation to finish
await page.locator('.c-imagery__main-image__bg').hover({ trial: true });
await page.locator(backgroundImageSelector).hover({ trial: true });
const imageMouseZoomed = await page.locator(backgroundImageSelector).boundingBox();
if (factor > 0) {
@@ -928,15 +863,3 @@ async function createImageryView(page) {
page.waitForSelector('.c-message-banner__message')
]);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function setRealTimeMode(page) {
await page.locator('.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();
}

View File

@@ -47,11 +47,6 @@ 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

View File

@@ -26,11 +26,7 @@ necessarily be used for reference when writing new tests in this area.
*/
const { test, expect } = require('../../../../pluginFixtures');
const {
createDomainObjectWithDefaults,
selectInspectorTab,
waitForPlotsToRender
} = require('../../../../appActions');
const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions');
test.describe('Stacked Plot', () => {
let stackedPlot;
@@ -231,45 +227,4 @@ 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);
}

View File

@@ -1,398 +0,0 @@
/*****************************************************************************
* 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);
// });
});

View File

@@ -174,42 +174,6 @@ 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);
});
});
/**

View File

@@ -63,24 +63,21 @@ const STATUSES = [
* @implements {StatusUserProvider}
*/
export default class ExampleUserProvider extends EventEmitter {
constructor(
openmct,
{ statusRoles } = {
statusRoles: []
}
) {
super();
constructor(openmct, {statusRoles} = {
statusRoles: []
}) {
super();
this.openmct = openmct;
this.user = undefined;
this.loggedIn = false;
this.autoLoginUser = undefined;
this.statusRoleValues = statusRoles.map((role) => ({
role: role,
status: STATUSES[0]
}));
this.pollQuestion = undefined;
this.statusRoles = statusRoles;
this.openmct = openmct;
this.user = undefined;
this.loggedIn = false;
this.autoLoginUser = undefined;
this.statusRoleValues = statusRoles.map(x => ({
role: x,
status: STATUSES[0]
}));
this.pollQuestion = undefined;
this.statusRoles = statusRoles;
this.ExampleUser = createExampleUser(this.openmct.user.User);
this.loginPromise = undefined;
@@ -102,13 +99,14 @@ export default class ExampleUserProvider extends EventEmitter {
return this.loginPromise;
}
canProvideStatusForRole(role) {
return this.statusRoles.includes(role);
canProvideStatusForRole() {
return Promise.resolve(true);
}
canSetPollQuestion() {
return Promise.resolve(true);
}
hasRole(roleId) {
if (!this.loggedIn) {
Promise.resolve(undefined);
@@ -117,18 +115,16 @@ export default class ExampleUserProvider extends EventEmitter {
return Promise.resolve(this.user.getRoles().includes(roleId));
}
getPossibleRoles() {
return this.user.getRoles();
getStatusRoleForCurrentUser() {
return Promise.resolve(this.defaultStatusRole);
}
getAllStatusRoles() {
return Promise.resolve(this.statusRoles);
return Promise.resolve([this.defaultStatusRole]);
}
getStatusForRole(role) {
const statusForRole = this.statusRoleValues.find((statusRole) => statusRole.role === role);
return Promise.resolve(statusForRole?.status);
return Promise.resolve(this.status);
}
async getDefaultStatusForRole(role) {
@@ -139,8 +135,7 @@ export default class ExampleUserProvider extends EventEmitter {
setStatusForRole(role, status) {
status.timestamp = Date.now();
const matchingIndex = this.statusRoleValues.findIndex((statusRole) => statusRole.role === role);
this.statusRoleValues[matchingIndex].status = status;
this.status = status;
this.emit('statusChange', {
role,
status
@@ -179,41 +174,126 @@ export default class ExampleUserProvider extends EventEmitter {
return Promise.resolve(STATUSES);
}
_login() {
const id = uuid();
// 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, ['flight', 'driver', 'observer']);
this.loggedIn = true;
canProvideStatusForRole(role) {
return this.statusRoles.includes(role);
}
return Promise.resolve();
}
const formStructure = {
title: 'Login',
sections: [
{
rows: [
{
key: 'username',
control: 'textfield',
name: 'Username',
pattern: '\\S+',
required: true,
cssClass: 'l-input-lg',
value: ''
}
]
}
],
buttons: {
submit: {
label: 'Login'
}
canSetPollQuestion() {
return Promise.resolve(true);
}
hasRole(roleId) {
if (!this.loggedIn) {
Promise.resolve(undefined);
}
};
return Promise.resolve(this.user.getRoles().includes(roleId));
}
getPossibleRoles() {
return this.user.getRoles();
}
getStatusRoleForCurrentUser(role) {
const matchedRole = this.statusRoleValues.find(x => x.role === role);
return Promise.resolve(matchedRole?.status);
}
getAllStatusRoles() {
return Promise.resolve(this.statusRoles);
}
getStatusForRole(role) {
const statusForRole = this.statusRoleValues.find(x => x.role === role);
return Promise.resolve(statusForRole?.status);
}
async getDefaultStatusForRole(role) {
const allRoles = await this.getPossibleStatuses();
return allRoles?.[0];
}
setStatusForRole(role, status) {
status.timestamp = Date.now();
const matchingIndex = this.statusRoleValues.findIndex(x => x.role === role);
this.statusRoleValues[matchingIndex].status = status;
this.emit('statusChange', {
role,
status
});
return true;
}
// eslint-disable-next-line require-await
async getPollQuestion() {
if (this.pollQuestion) {
return this.pollQuestion;
} else {
return undefined;
}
}
setPollQuestion(pollQuestion) {
if (!pollQuestion) {
// If the poll question is undefined, set it to a blank string.
// This behavior better reflects how other telemetry systems
// deal with undefined poll questions.
pollQuestion = '';
}
this.pollQuestion = {
question: pollQuestion,
timestamp: Date.now()
};
this.emit("pollQuestionChange", this.pollQuestion);
return true;
}
getPossibleStatuses() {
return Promise.resolve(STATUSES);
}
_login() {
const id = uuid();
// 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, ['test-role-1', 'test-role-2', 'test-role-3', 'test-role-4']);
this.loggedIn = true;
return Promise.resolve();
}
const formStructure = {
title: "Login",
sections: [
{
rows: [
{
key: "username",
control: "textfield",
name: "Username",
pattern: "\\S+",
required: true,
cssClass: "l-input-lg",
value: ''
}
]
}
],
buttons: {
submit: {
label: 'Login'
}
}
};
return this.openmct.forms.showForm(formStructure).then(
(info) => {

View File

@@ -21,19 +21,17 @@
*****************************************************************************/
import ExampleUserProvider from './ExampleUserProvider';
const AUTO_LOGIN_USER = 'mct-user';
const STATUS_ROLES = ['flight', 'driver'];
const AUTO_LOGIN_USER = 'guest';
const STATUS_ROLES = ['test-role-1', 'test-role-2', 'test-role-3'];
export default function ExampleUserPlugin(
{ autoLoginUser, statusRoles } = {
export default function ExampleUserPlugin({autoLoginUser, statusRoles} = {
autoLoginUser: AUTO_LOGIN_USER,
statusRoles: STATUS_ROLES
}
) {
return function install(openmct) {
const userProvider = new ExampleUserProvider(openmct, {
statusRoles
});
}) {
return function install(openmct) {
const userProvider = new ExampleUserProvider(openmct, {
statusRoles
});
if (autoLoginUser !== undefined) {
userProvider.autoLogin(autoLoginUser);

View File

@@ -156,9 +156,9 @@ export default function () {
key: 'thumbnail',
...formatThumbnail
});
openmct.telemetry.addProvider(getRealtimeProvider(openmct));
openmct.telemetry.addProvider(getHistoricalProvider(openmct));
openmct.telemetry.addProvider(getLadProvider(openmct));
openmct.telemetry.addProvider(getRealtimeProvider());
openmct.telemetry.addProvider(getHistoricalProvider());
openmct.telemetry.addProvider(getLadProvider());
};
}
@@ -207,14 +207,14 @@ function getImageLoadDelay(domainObject) {
return imageLoadDelay;
}
function getRealtimeProvider(openmct) {
function getRealtimeProvider() {
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(openmct.time.now(), domainObject.name, imageSamples, delay);
const datum = pointForTimestamp(Date.now(), domainObject.name, imageSamples, delay);
callback(datum);
}, delay);
@@ -225,7 +225,7 @@ function getRealtimeProvider(openmct) {
};
}
function getHistoricalProvider(openmct) {
function getHistoricalProvider() {
return {
supportsRequest: (domainObject, options) => {
return domainObject.type === 'example.imagery' && options.strategy !== 'latest';
@@ -233,12 +233,17 @@ function getHistoricalProvider(openmct) {
request: (domainObject, options) => {
const delay = getImageLoadDelay(domainObject);
let start = options.start;
const end = Math.min(options.end, openmct.time.now());
const end = Math.min(options.end, Date.now());
const data = [];
while (start <= end && data.length < delay) {
const imageSamples = getImageSamples(domainObject.configuration);
const generatedDataPoint = pointForTimestamp(start, domainObject.name, imageSamples, delay);
data.push(generatedDataPoint);
data.push(
pointForTimestamp(
start,
domainObject.name,
getImageSamples(domainObject.configuration),
delay
)
);
start += delay;
}
@@ -247,7 +252,7 @@ function getHistoricalProvider(openmct) {
};
}
function getLadProvider(openmct) {
function getLadProvider() {
return {
supportsRequest: (domainObject, options) => {
return domainObject.type === 'example.imagery' && options.strategy === 'latest';
@@ -255,7 +260,7 @@ function getLadProvider(openmct) {
request: (domainObject, options) => {
const delay = getImageLoadDelay(domainObject);
const datum = pointForTimestamp(
openmct.time.now(),
Date.now(),
domainObject.name,
getImageSamples(domainObject.configuration),
delay

View File

@@ -24,7 +24,7 @@ function SimpleVuePlugin() {
container.appendChild(vm.$mount().$el);
},
destroy: function (container) {
//vm.$destroy();
vm.$destroy();
}
};
}

View File

@@ -92,9 +92,7 @@
}
</style>
</head>
<body>
<div id="app"></div>
</body>
<body></body>
<script>
const THIRTY_SECONDS = 30 * 1000;
const ONE_MINUTE = THIRTY_SECONDS * 2;

View File

@@ -56,7 +56,6 @@ 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

View File

@@ -1,19 +1,17 @@
{
"name": "openmct",
"version": "3.0.0-SNAPSHOT",
"version": "2.2.5-SNAPSHOT",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.22.5",
"@babel/eslint-parser": "7.21.8",
"@braintree/sanitize-url": "6.0.2",
"@deploysentinel/playwright": "0.3.4",
"@percy/cli": "1.26.0",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.32.3",
"@types/eventemitter3": "1.2.0",
"@types/jasmine": "4.3.4",
"@types/jasmine": "4.3.1",
"@types/lodash": "4.14.192",
"@vue/compat": "^3.1.0",
"@vue/compiler-sfc": "^3.1.0",
"babel-loader": "9.1.0",
"babel-plugin-istanbul": "6.1.1",
"codecov": "3.8.3",
@@ -23,16 +21,15 @@
"d3-axis": "3.0.0",
"d3-scale": "3.3.0",
"d3-selection": "3.0.0",
"eslint": "8.43.0",
"eslint-config-prettier": "8.8.0",
"eslint": "8.42.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.15.0",
"eslint-plugin-vue": "9.14.1",
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
"eventemitter3": "1.2.0",
"file-saver": "2.0.5",
"flatbush": "4.2.0",
"git-rev-sync": "3.0.2",
"html2canvas": "1.4.1",
"imports-loader": "4.0.1",
@@ -47,6 +44,7 @@
"karma-sourcemap-loader": "0.4.0",
"karma-spec-reporter": "0.0.36",
"karma-webpack": "5.0.0",
"kdbush": "3.0.0",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"mini-css-extract-plugin": "2.7.6",
@@ -61,17 +59,18 @@
"prettier": "2.8.7",
"printj": "1.3.1",
"resolve-url-loader": "5.0.0",
"sanitize-html": "2.11.0",
"sass": "1.63.4",
"sass-loader": "13.3.2",
"sanitize-html": "2.10.0",
"sass": "1.63.3",
"sass-loader": "13.3.1",
"sinon": "15.1.0",
"style-loader": "3.3.3",
"typescript": "5.1.3",
"uuid": "9.0.0",
"vue": "^3.1.0",
"vue": "2.6.14",
"vue-eslint-parser": "9.3.1",
"webpack": "5.88.0",
"vue-loader": "^16.0.0",
"vue-loader": "15.9.8",
"vue-template-compiler": "2.6.14",
"webpack": "5.86.0",
"webpack-cli": "5.1.1",
"webpack-dev-server": "4.15.1",
"webpack-merge": "5.9.0"

View File

@@ -24,7 +24,6 @@ define([
'EventEmitter',
'./api/api',
'./api/overlays/OverlayAPI',
'./api/tooltips/ToolTipAPI',
'./selection/Selection',
'./plugins/plugins',
'./ui/registries/ViewRegistry',
@@ -49,7 +48,6 @@ define([
EventEmitter,
api,
OverlayAPI,
ToolTipAPI,
Selection,
plugins,
ViewRegistry,
@@ -96,7 +94,6 @@ define([
};
this.destroy = this.destroy.bind(this);
this.defaultClock = 'local';
[
/**
* Tracks current selection state of the application.
@@ -223,8 +220,6 @@ define([
['overlays', () => new OverlayAPI.default()],
['tooltips', () => new ToolTipAPI.default()],
['menus', () => new api.MenuAPI(this)],
['actions', () => new api.ActionsAPI(this)],
@@ -343,17 +338,7 @@ define([
* @param {HTMLElement} [domElement] the DOM element in which to run
* MCT; if undefined, MCT will be run in the body of the document
*/
MCT.prototype.start = function (
domElement = document.body.firstElementChild,
isHeadlessMode = false
) {
// Create element to mount Layout if it doesn't exist
if (domElement === null) {
domElement = document.createElement('div');
document.body.appendChild(domElement);
}
domElement.id = 'openmct-app';
MCT.prototype.start = function (domElement = document.body, isHeadlessMode = false) {
if (this.types.get('layout') === undefined) {
this.install(
this.plugins.DisplayLayout({
@@ -364,10 +349,6 @@ define([
this.element = domElement;
if (!this.time.getClock()) {
this.time.setClock(this.defaultClock);
}
this.router.route(/^\/$/, () => {
this.router.setPath('/browse/');
});
@@ -380,30 +361,25 @@ define([
*/
if (!isHeadlessMode) {
const appLayout = Vue.createApp({
const appLayout = new Vue({
components: {
Layout: Layout.default
},
provide: {
openmct: Vue.markRaw(this)
openmct: this
},
template: '<Layout ref="layout"></Layout>'
});
const component = appLayout.mount(domElement);
component.$nextTick(() => {
this.layout = component.$refs.layout;
this.app = appLayout;
Browse(this);
window.addEventListener('beforeunload', this.destroy);
this.router.start();
this.emit('start');
});
} else {
window.addEventListener('beforeunload', this.destroy);
domElement.appendChild(appLayout.$mount().$el);
this.router.start();
this.emit('start');
this.layout = appLayout.$refs.layout;
Browse(this);
}
window.addEventListener('beforeunload', this.destroy);
this.router.start();
this.emit('start');
};
MCT.prototype.startHeadless = function () {

View File

@@ -21,7 +21,6 @@
*****************************************************************************/
import objectUtils from '../objects/object-utils';
import CompositionProvider from './CompositionProvider';
import { toRaw } from 'vue';
/**
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
@@ -74,7 +73,7 @@ export default class DefaultCompositionProvider extends CompositionProvider {
* the Identifiers in this composition
*/
load(domainObject) {
return Promise.all(domainObject.composition.map(this.publicAPI.objects.parseKeyString));
return Promise.all(domainObject.composition);
}
/**
* Attach listeners for changes to the composition of a given domain object.
@@ -168,7 +167,7 @@ export default class DefaultCompositionProvider extends CompositionProvider {
*/
add(parent, childId) {
if (!this.includes(parent, childId)) {
const composition = structuredClone(toRaw(parent.composition));
const composition = structuredClone(parent.composition);
composition.push(childId);
this.publicAPI.objects.mutate(parent, 'composition', composition);
}

View File

@@ -10,7 +10,8 @@ import TextAreaField from './components/controls/TextAreaField.vue';
import TextField from './components/controls/TextField.vue';
import ToggleSwitchField from './components/controls/ToggleSwitchField.vue';
import mount from 'utils/mount';
import Vue from 'vue';
export const DEFAULT_CONTROLS_MAP = {
autocomplete: AutoCompleteField,
checkbox: CheckBoxField,
@@ -68,40 +69,31 @@ export default class FormControl {
*/
_getControlViewProvider(control) {
const self = this;
let _destroy = null;
let rowComponent;
return {
show(element, model, onChange) {
const { vNode, destroy } = mount(
{
el: element,
components: {
FormControlComponent: DEFAULT_CONTROLS_MAP[control]
},
provide: {
openmct: self.openmct
},
data() {
return {
model,
onChange
};
},
template: `<FormControlComponent :model="model" @onChange="onChange"></FormControlComponent>`
rowComponent = new Vue({
el: element,
components: {
FormControlComponent: DEFAULT_CONTROLS_MAP[control]
},
{
element,
app: self.openmct.app
}
);
_destroy = destroy;
provide: {
openmct: self.openmct
},
data() {
return {
model,
onChange
};
},
template: `<FormControlComponent :model="model" @onChange="onChange"></FormControlComponent>`
});
return vNode;
return rowComponent;
},
destroy() {
if (_destroy) {
_destroy();
}
rowComponent.$destroy();
}
};
}

View File

@@ -23,8 +23,8 @@
import FormController from './FormController';
import FormProperties from './components/FormProperties.vue';
import Vue from 'vue';
import _ from 'lodash';
import mount from 'utils/mount';
export default class FormsAPI {
constructor(openmct) {
@@ -156,28 +156,25 @@ export default class FormsAPI {
formCancel = onFormAction(reject);
});
const { destroy } = mount(
{
components: { FormProperties },
provide: {
openmct: self.openmct
},
data() {
return {
formStructure,
onChange: onFormPropertyChange,
onCancel: formCancel,
onSave: formSave
};
},
template:
'<FormProperties :model="formStructure" @onChange="onChange" @onCancel="onCancel" @onSave="onSave"></FormProperties>'
const vm = new Vue({
components: { FormProperties },
provide: {
openmct: self.openmct
},
{
element,
app: self.openmct.app
}
);
data() {
return {
formStructure,
onChange: onFormPropertyChange,
onCancel: formCancel,
onSave: formSave
};
},
template:
'<FormProperties :model="formStructure" @onChange="onChange" @onCancel="onCancel" @onSave="onSave"></FormProperties>'
}).$mount();
const formElement = vm.$el;
element.append(formElement);
function onFormPropertyChange(data) {
if (onChange) {
@@ -198,7 +195,8 @@ export default class FormsAPI {
function onFormAction(callback) {
return () => {
destroy();
formElement.remove();
vm.$destroy();
if (callback) {
callback(changes);

View File

@@ -141,7 +141,7 @@ export default {
},
methods: {
onChange(data) {
this.invalidProperties[data.model.key] = data.invalid;
this.$set(this.invalidProperties, data.model.key, data.invalid);
this.$emit('onChange', data);
},

View File

@@ -26,7 +26,9 @@
{{ row.name }}
</div>
<div class="c-form-row__state-indicator" :class="reqClass"></div>
<div v-if="row.control" ref="rowElement" class="c-form-row__controls"></div>
<div v-if="row.control" class="c-form-row__controls">
<div ref="rowElement"></div>
</div>
</div>
</template>
@@ -89,7 +91,7 @@ export default {
this.formControl.show(this.$refs.rowElement, this.row, this.onChange);
},
unmounted() {
destroyed() {
const destroy = this.formControl.destroy;
if (destroy) {
destroy();

View File

@@ -166,7 +166,7 @@ export default {
this.options = this.model.options;
}
},
unmounted() {
destroyed() {
document.body.removeEventListener('click', this.handleOutsideClick);
},
methods: {

View File

@@ -20,17 +20,17 @@
at runtime from the About dialog for additional information.
-->
<template>
<div class="c-menu" :class="options.menuClass" :style="styleObject">
<div class="c-menu" :class="options.menuClass">
<ul v-if="options.actions.length && options.actions[0].length" role="menu">
<template v-for="(actionGroups, index) in options.actions" :key="index">
<div role="group">
<template v-for="(actionGroups, index) in options.actions">
<div :key="index" role="group">
<li
v-for="action in actionGroups"
:key="action.name"
role="menuitem"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || null"
:data-testid="action.testId || false"
@click="action.onItemClicked"
>
{{ action.name }}
@@ -42,8 +42,8 @@
class="c-menu__section-separator"
></div>
<li v-if="actionGroups.length === 0" :key="index">No actions defined.</li>
</div>
</template>
</div></template
>
</ul>
<ul v-else role="menu">
@@ -53,7 +53,7 @@
role="menuitem"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || null"
:data-testid="action.testId || false"
@click="action.onItemClicked"
>
{{ action.name }}
@@ -64,9 +64,7 @@
</template>
<script>
import popupMenuMixin from '../mixins/popupMenuMixin';
export default {
mixins: [popupMenuMixin],
inject: ['options']
};
</script>

View File

@@ -20,21 +20,21 @@
at runtime from the About dialog for additional information.
-->
<template>
<div class="c-menu" :class="[options.menuClass, 'c-super-menu']" :style="styleObject">
<div class="c-menu" :class="[options.menuClass, 'c-super-menu']">
<ul
v-if="options.actions.length && options.actions[0].length"
role="menu"
class="c-super-menu__menu"
>
<template v-for="(actionGroups, index) in options.actions" :key="index">
<div role="group">
<template v-for="(actionGroups, index) in options.actions">
<div :key="index" role="group">
<li
v-for="action in actionGroups"
:key="action.name"
role="menuitem"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || null"
:data-testid="action.testId || false"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
@@ -59,7 +59,7 @@
role="menuitem"
:class="action.cssClass"
:title="action.description"
:data-testid="action.testId || null"
:data-testid="action.testId || false"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
@@ -80,10 +80,9 @@
</div>
</div>
</template>
<script>
import popupMenuMixin from '../mixins/popupMenuMixin';
export default {
mixins: [popupMenuMixin],
inject: ['options'],
data: function () {
return {

View File

@@ -22,8 +22,7 @@
import EventEmitter from 'EventEmitter';
import MenuComponent from './components/Menu.vue';
import SuperMenuComponent from './components/SuperMenu.vue';
import { h } from 'vue';
import mount from 'utils/mount';
import Vue from 'vue';
export const MENU_PLACEMENT = {
TOP: 'top',
@@ -53,67 +52,137 @@ class Menu extends EventEmitter {
dismiss() {
this.emit('destroy');
if (this.destroy) {
this.destroy();
this.destroy = null;
}
document.body.removeChild(this.component.$el);
document.removeEventListener('click', this.dismiss);
this.component.$destroy();
}
show() {
this.component.$mount();
document.body.appendChild(this.component.$el);
let position = this._calculatePopupPosition(this.component.$el);
this.component.$el.style.left = `${position.x}px`;
this.component.$el.style.top = `${position.y}px`;
document.addEventListener('click', this.dismiss);
}
showMenu() {
if (this.destroy) {
return;
}
const { vNode, destroy } = mount({
render() {
return h(MenuComponent);
this.component = new Vue({
components: {
MenuComponent
},
provide: {
options: this.options
},
// TODO: Remove this exception upon full migration to Vue 3
// https://v3-migration.vuejs.org/breaking-changes/render-function-api.html#render-function-argument
compatConfig: {
RENDER_FUNCTION: false
}
template: '<menu-component />'
});
this.el = vNode.el;
this.destroy = destroy;
this.show();
}
showSuperMenu() {
const { vNode, destroy } = mount({
data() {
return {
top: '0px',
left: '0px'
};
},
render() {
return h(SuperMenuComponent);
this.component = new Vue({
components: {
SuperMenuComponent
},
provide: {
options: this.options
},
// TODO: Remove this exception upon full migration to Vue 3
// https://v3-migration.vuejs.org/breaking-changes/render-function-api.html#render-function-argument
compatConfig: {
RENDER_FUNCTION: false
}
template: '<super-menu-component />'
});
this.el = vNode.el;
this.destroy = destroy;
this.show();
}
show() {
document.body.appendChild(this.el);
document.addEventListener('click', this.dismiss);
/**
* @private
*/
_calculatePopupPosition(menuElement) {
let menuDimensions = menuElement.getBoundingClientRect();
if (!this.options.placement) {
this.options.placement = MENU_PLACEMENT.BOTTOM_RIGHT;
}
const menuPosition = this._getMenuPositionBasedOnPlacement(menuDimensions);
return this._preventMenuOverflow(menuPosition, menuDimensions);
}
/**
* @private
*/
_getMenuPositionBasedOnPlacement(menuDimensions) {
let eventPosX = this.options.x;
let eventPosY = this.options.y;
// Adjust popup menu based on placement
switch (this.options.placement) {
case MENU_PLACEMENT.TOP:
eventPosX = this.options.x - Math.floor(menuDimensions.width / 2);
eventPosY = this.options.y - menuDimensions.height;
break;
case MENU_PLACEMENT.BOTTOM:
eventPosX = this.options.x - Math.floor(menuDimensions.width / 2);
break;
case MENU_PLACEMENT.LEFT:
eventPosX = this.options.x - menuDimensions.width;
eventPosY = this.options.y - Math.floor(menuDimensions.height / 2);
break;
case MENU_PLACEMENT.RIGHT:
eventPosY = this.options.y - Math.floor(menuDimensions.height / 2);
break;
case MENU_PLACEMENT.TOP_LEFT:
eventPosX = this.options.x - menuDimensions.width;
eventPosY = this.options.y - menuDimensions.height;
break;
case MENU_PLACEMENT.TOP_RIGHT:
eventPosY = this.options.y - menuDimensions.height;
break;
case MENU_PLACEMENT.BOTTOM_LEFT:
eventPosX = this.options.x - menuDimensions.width;
break;
case MENU_PLACEMENT.BOTTOM_RIGHT:
break;
}
return {
x: eventPosX,
y: eventPosY
};
}
/**
* @private
*/
_preventMenuOverflow(menuPosition, menuDimensions) {
let { x: eventPosX, y: eventPosY } = menuPosition;
let overflowX = eventPosX + menuDimensions.width - document.body.clientWidth;
let overflowY = eventPosY + menuDimensions.height - document.body.clientHeight;
if (overflowX > 0) {
eventPosX = eventPosX - overflowX;
}
if (overflowY > 0) {
eventPosY = eventPosY - overflowY;
}
if (eventPosX < 0) {
eventPosX = 0;
}
if (eventPosY < 0) {
eventPosY = 0;
}
return {
x: eventPosX,
y: eventPosY
};
}
}

View File

@@ -1,111 +0,0 @@
import { MENU_PLACEMENT } from '../menu';
export default {
methods: {
/**
* @private
*/
_calculatePopupPosition(menuElement) {
let menuDimensions = menuElement.getBoundingClientRect();
if (!this.options.placement) {
this.options.placement = MENU_PLACEMENT.BOTTOM_RIGHT;
}
const menuPosition = this._getMenuPositionBasedOnPlacement(menuDimensions);
return this._preventMenuOverflow(menuPosition, menuDimensions);
},
/**
* @private
*/
_getMenuPositionBasedOnPlacement(menuDimensions) {
let eventPosX = this.options.x;
let eventPosY = this.options.y;
// Adjust popup menu based on placement
switch (this.options.placement) {
case MENU_PLACEMENT.TOP:
eventPosX = this.options.x - Math.floor(menuDimensions.width / 2);
eventPosY = this.options.y - menuDimensions.height;
break;
case MENU_PLACEMENT.BOTTOM:
eventPosX = this.options.x - Math.floor(menuDimensions.width / 2);
break;
case MENU_PLACEMENT.LEFT:
eventPosX = this.options.x - menuDimensions.width;
eventPosY = this.options.y - Math.floor(menuDimensions.height / 2);
break;
case MENU_PLACEMENT.RIGHT:
eventPosY = this.options.y - Math.floor(menuDimensions.height / 2);
break;
case MENU_PLACEMENT.TOP_LEFT:
eventPosX = this.options.x - menuDimensions.width;
eventPosY = this.options.y - menuDimensions.height;
break;
case MENU_PLACEMENT.TOP_RIGHT:
eventPosY = this.options.y - menuDimensions.height;
break;
case MENU_PLACEMENT.BOTTOM_LEFT:
eventPosX = this.options.x - menuDimensions.width;
break;
case MENU_PLACEMENT.BOTTOM_RIGHT:
break;
}
return {
x: eventPosX,
y: eventPosY
};
},
/**
* @private
*/
_preventMenuOverflow(menuPosition, menuDimensions) {
let { x: eventPosX, y: eventPosY } = menuPosition;
let overflowX = eventPosX + menuDimensions.width - document.body.clientWidth;
let overflowY = eventPosY + menuDimensions.height - document.body.clientHeight;
if (overflowX > 0) {
eventPosX = eventPosX - overflowX;
}
if (overflowY > 0) {
eventPosY = eventPosY - overflowY;
}
if (eventPosX < 0) {
eventPosX = 0;
}
if (eventPosY < 0) {
eventPosY = 0;
}
return {
x: eventPosX,
y: eventPosY
};
}
},
mounted() {
this.$nextTick(() => {
const position = this._calculatePopupPosition(this.$el);
this.top = position.y;
this.left = position.x;
});
},
data() {
return {
top: '0px',
left: '0px'
};
},
computed: {
styleObject() {
return {
top: `${this.top}px`,
left: `${this.left}px`
};
}
}
};

View File

@@ -159,9 +159,6 @@ class InMemorySearchProvider {
return pendingQuery.promise;
}
/**
* @private
*/
#localQueryFallBack({ queryId, searchType, query, maxResults }) {
if (searchType === this.searchTypes.OBJECTS) {
return this.localSearchForObjects(queryId, query, maxResults);

View File

@@ -96,26 +96,12 @@ class MutableDomainObject {
//Emit events specific to properties affected
let parentPropertiesList = path.split('.');
for (let index = parentPropertiesList.length; index > 0; index--) {
let pathToThisProperty = parentPropertiesList.slice(0, index);
let parentPropertyPath = parentPropertiesList.slice(0, index).join('.');
this._globalEventEmitter.emit(
qualifiedEventName(this, parentPropertyPath),
_.get(this, parentPropertyPath),
_.get(oldModel, parentPropertyPath)
);
const lastPathElement = parentPropertiesList[index - 1];
// Also emit an event for the array whose element has changed so developers do not need to listen to every element of the array.
if (lastPathElement.endsWith(']')) {
const arrayPathElement = lastPathElement.substring(0, lastPathElement.lastIndexOf('['));
pathToThisProperty[index - 1] = arrayPathElement;
const pathToArrayString = pathToThisProperty.join('.');
this._globalEventEmitter.emit(
qualifiedEventName(this, pathToArrayString),
_.get(this, pathToArrayString),
_.get(oldModel, pathToArrayString)
);
}
}
//TODO: Emit events for listeners of child properties when parent changes.

View File

@@ -242,16 +242,11 @@ export default class ObjectAPI {
return domainObject;
})
.catch((error) => {
delete this.cache[keystring];
// suppress abort errors
if (error.name === 'AbortError') {
return;
}
console.warn(`Failed to retrieve ${keystring}:`, error);
delete this.cache[keystring];
const result = this.applyGetInterceptors(identifier);
return this.applyGetInterceptors(identifier);
return result;
});
this.cache[keystring] = objectPromise;
@@ -545,40 +540,6 @@ 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

View File

@@ -248,17 +248,10 @@ describe('The Object API', () => {
});
it('displays a notification in the event of an error', () => {
openmct.notifications.warn = jasmine.createSpy('warn');
mockProvider.get.and.returnValue(
Promise.reject({
name: 'Error',
status: 404,
statusText: 'Not Found'
})
);
mockProvider.get.and.returnValue(Promise.reject());
return objectAPI.get(mockDomainObject.identifier).catch(() => {
expect(openmct.notifications.warn).toHaveBeenCalledWith(
expect(openmct.notifications.error).toHaveBeenCalledWith(
`Failed to retrieve object ${TEST_NAMESPACE}:${TEST_KEY}`
);
});

View File

@@ -1,10 +1,10 @@
import DialogComponent from './components/DialogComponent.vue';
import Overlay from './Overlay';
import mount from 'utils/mount';
import Vue from 'vue';
class Dialog extends Overlay {
constructor({ iconClass, message, title, hint, timestamp, ...options }) {
const { vNode, destroy } = mount({
let component = new Vue({
components: {
DialogComponent: DialogComponent
},
@@ -16,17 +16,17 @@ class Dialog extends Overlay {
timestamp
},
template: '<dialog-component></dialog-component>'
});
}).$mount();
super({
element: vNode.el,
element: component.$el,
size: 'fit',
dismissable: false,
...options
});
this.once('destroy', () => {
destroy();
component.$destroy();
});
}
}

View File

@@ -1,6 +1,6 @@
import OverlayComponent from './components/OverlayComponent.vue';
import EventEmitter from 'EventEmitter';
import mount from 'utils/mount';
import Vue from 'vue';
const cssClasses = {
large: 'l-overlay-large',
@@ -28,25 +28,18 @@ class Overlay extends EventEmitter {
this.autoHide = autoHide;
this.dismissable = dismissable !== false;
const { destroy } = mount(
{
components: {
OverlayComponent: OverlayComponent
},
provide: {
dismiss: this.notifyAndDismiss.bind(this),
element,
buttons,
dismissable: this.dismissable
},
template: '<overlay-component></overlay-component>'
this.component = new Vue({
components: {
OverlayComponent: OverlayComponent
},
{
element: this.container
}
);
this.destroy = destroy;
provide: {
dismiss: this.notifyAndDismiss.bind(this),
element,
buttons,
dismissable: this.dismissable
},
template: '<overlay-component></overlay-component>'
});
if (onDestroy) {
this.once('destroy', onDestroy);
@@ -60,7 +53,7 @@ class Overlay extends EventEmitter {
dismiss() {
this.emit('destroy');
document.body.removeChild(this.container);
this.destroy();
this.component.$destroy();
}
//Ensures that any callers are notified that the overlay is dismissed
@@ -74,6 +67,7 @@ class Overlay extends EventEmitter {
**/
show() {
document.body.appendChild(this.container);
this.container.appendChild(this.component.$mount().$el);
}
}

View File

@@ -1,25 +1,3 @@
/*****************************************************************************
* 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';
@@ -150,15 +128,14 @@ class OverlayAPI {
let progressDialog = new ProgressDialog(options);
this.showOverlay(progressDialog);
return progressDialog;
}
selection(options) {
let selection = new Selection(options);
this.showOverlay(selection);
let selection = new Selection(options);
this.showOverlay(selection);
return selection;
return selection;
}
}

View File

@@ -1,8 +1,9 @@
import ProgressDialogComponent from './components/ProgressDialogComponent.vue';
import Overlay from './Overlay';
import mount from 'utils/mount';
import Vue from 'vue';
let component;
class ProgressDialog extends Overlay {
constructor({
progressPerc,
@@ -14,7 +15,7 @@ class ProgressDialog extends Overlay {
timestamp,
...options
}) {
const { vNode, destroy } = mount({
component = new Vue({
components: {
ProgressDialogComponent: ProgressDialogComponent
},
@@ -34,18 +35,17 @@ class ProgressDialog extends Overlay {
};
},
template: '<progress-dialog-component :model="model"></progress-dialog-component>'
});
component = vNode.componentInstance;
}).$mount();
super({
element: vNode.el,
element: component.$el,
size: 'fit',
dismissable: false,
...options
});
this.once('destroy', () => {
destroy();
component.$destroy();
});
}

View File

@@ -1,69 +1,38 @@
/*****************************************************************************
* 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 mount from 'utils/mount';
import Vue from 'vue';
class Selection extends Overlay {
constructor({
iconClass,
title,
message,
selectionOptions,
onChange,
currentSelection,
...options
}) {
const { vNode, destroy } = mount({
components: {
SelectionComponent: SelectionComponent
},
provide: {
iconClass,
title,
message,
selectionOptions,
onChange,
currentSelection
},
template: '<selection-component></selection-component>'
});
constructor({iconClass, title, message, selectionOptions, onChange, currentSelection, ...options}) {
const component = vNode.componentInstance;
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
});
super({
element: component.$el,
size: 'fit',
dismissable: false,
onChange,
currentSelection,
...options
});
this.once('destroy', () => {
destroy();
});
}
this.once('destroy', () => {
component.$destroy();
});
}
}
export default Selection;

View File

@@ -1,34 +1,45 @@
<template>
<div class="c-message">
<div class="c-message">
<!--Uses flex-row -->
<div class="c-message__icon" :class="['u-icon-bg-color-' + iconClass]"></div>
<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"
<!-- Uses flex-column -->
<div
v-if="title"
class="c-message__title"
>
{{ option.name }}
</option>
</select>
{{ title }}
</div>
<slot></slot>
<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>
</div>
</template>
<script>
export default {
inject: ['iconClass', 'title', 'message', 'selectionOptions', 'currentSelection', 'onChange']
inject: ['iconClass', 'title', 'message', 'selectionOptions', 'currentSelection', 'onChange']
};
</script>

View File

@@ -204,23 +204,27 @@ export default class TelemetryAPI {
*/
standardizeRequestOptions(options = {}) {
if (!Object.hasOwn(options, 'start')) {
if (options.timeContext?.getBounds()) {
options.start = options.timeContext.getBounds().start;
if (options.timeContext?.bounds()) {
options.start = options.timeContext.bounds().start;
} else {
options.start = this.openmct.time.getBounds().start;
options.start = this.openmct.time.bounds().start;
}
}
if (!Object.hasOwn(options, 'end')) {
if (options.timeContext?.getBounds()) {
options.end = options.timeContext.getBounds().end;
if (options.timeContext?.bounds()) {
options.end = options.timeContext.bounds().end;
} else {
options.end = this.openmct.time.getBounds().end;
options.end = this.openmct.time.bounds().end;
}
}
if (!Object.hasOwn(options, 'domain')) {
options.domain = this.openmct.time.getTimeSystem().key;
options.domain = this.openmct.time.timeSystem().key;
}
if (!Object.hasOwn(options, 'timeContext')) {
options.timeContext = this.openmct.time;
}
return options;
@@ -485,62 +489,6 @@ 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.
*
@@ -728,7 +676,7 @@ export default class TelemetryAPI {
*
* @param {module:openmct.DomainObject} domainObject the domain
* object for which to get limits
* @returns {LimitsResponseObject}
* @returns {module:openmct.TelemetryAPI~LimitEvaluator}
* @method limits
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/
@@ -775,8 +723,18 @@ export default class TelemetryAPI {
*
* @param {module:openmct.DomainObject} domainObject the domain
* object for which to display limits
* @returns {LimitsResponseObject}
* @method limits returns a limits object of type {LimitsResponseObject}
* @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 }
* }
* }
* supported colors are purple, red, orange, yellow and cyan
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/
@@ -808,7 +766,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 {LimitViolation} metadata about
* @returns {module:openmct.TelemetryAPI~LimitViolation} metadata about
* the limit violation, or undefined if a value is within limits
*/
@@ -819,42 +777,6 @@ 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
*/
/**

View File

@@ -29,20 +29,15 @@ describe('Telemetry API', () => {
beforeEach(() => {
openmct = {
time: jasmine.createSpyObj('timeAPI', ['timeSystem', 'getTimeSystem', 'bounds', 'getBounds']),
time: jasmine.createSpyObj('timeAPI', ['timeSystem', 'bounds']),
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);
});
@@ -266,14 +261,16 @@ describe('Telemetry API', () => {
signal,
start: 0,
end: 1,
domain: 'system'
domain: 'system',
timeContext: jasmine.any(Object)
});
expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), {
signal,
start: 0,
end: 1,
domain: 'system'
domain: 'system',
timeContext: jasmine.any(Object)
});
telemetryProvider.supportsRequest.calls.reset();
@@ -284,14 +281,16 @@ describe('Telemetry API', () => {
signal,
start: 0,
end: 1,
domain: 'system'
domain: 'system',
timeContext: jasmine.any(Object)
});
expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), {
signal,
start: 0,
end: 1,
domain: 'system'
domain: 'system',
timeContext: jasmine.any(Object)
});
});
@@ -310,14 +309,16 @@ describe('Telemetry API', () => {
start: 20,
end: 30,
domain: 'someDomain',
signal
signal,
timeContext: jasmine.any(Object)
});
expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), {
start: 20,
end: 30,
domain: 'someDomain',
signal
signal,
timeContext: jasmine.any(Object)
});
});
});

View File

@@ -23,7 +23,6 @@
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
@@ -61,11 +60,8 @@ 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;
@@ -82,11 +78,11 @@ export default class TelemetryCollection extends EventEmitter {
this._error(LOADED_ERROR);
}
this._setTimeSystem(this.options.timeContext.getTimeSystem());
this.lastBounds = this.options.timeContext.getBounds();
this._setTimeSystem(this.options.timeContext.timeSystem());
this.lastBounds = this.options.timeContext.bounds();
this._watchBounds();
this._watchTimeSystem();
this._watchTimeModeChange();
this._requestHistoricalTelemetry();
this._initiateSubscriptionTelemetry();
@@ -105,7 +101,6 @@ export default class TelemetryCollection extends EventEmitter {
this._unwatchBounds();
this._unwatchTimeSystem();
this._unwatchTimeModeChange();
if (this.unsubscribe) {
this.unsubscribe();
}
@@ -126,7 +121,7 @@ export default class TelemetryCollection extends EventEmitter {
* @private
*/
async _requestHistoricalTelemetry() {
let options = this.openmct.telemetry.standardizeRequestOptions({ ...this.options });
let options = { ...this.options };
const historicalProvider = this.openmct.telemetry.findRequestProvider(
this.domainObject,
options
@@ -438,10 +433,6 @@ export default class TelemetryCollection extends EventEmitter {
this._reset();
}
_timeModeChanged() {
this._reset();
}
/**
* Reset the telemetry data of the collection, and re-request
* historical telemetry
@@ -459,35 +450,19 @@ export default class TelemetryCollection extends EventEmitter {
}
/**
* adds the _bounds callback to the 'boundsChanged' timeAPI listener
* adds the _bounds callback to the 'bounds' timeAPI listener
* @private
*/
_watchBounds() {
this.options.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this._bounds, this);
this.options.timeContext.on('bounds', this._bounds, this);
}
/**
* removes the _bounds callback from the 'boundsChanged' timeAPI listener
* removes the _bounds callback from the 'bounds' timeAPI listener
* @private
*/
_unwatchBounds() {
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);
this.options.timeContext.off('bounds', this._bounds, this);
}
/**
@@ -495,11 +470,7 @@ export default class TelemetryCollection extends EventEmitter {
* @private
*/
_watchTimeSystem() {
this.options.timeContext.on(
TIME_CONTEXT_EVENTS.timeSystemChanged,
this._setTimeSystemAndFetchData,
this
);
this.options.timeContext.on('timeSystem', this._setTimeSystemAndFetchData, this);
}
/**
@@ -507,11 +478,7 @@ export default class TelemetryCollection extends EventEmitter {
* @private
*/
_unwatchTimeSystem() {
this.options.timeContext.off(
TIME_CONTEXT_EVENTS.timeSystemChanged,
this._setTimeSystemAndFetchData,
this
);
this.options.timeContext.off('timeSystem', this._setTimeSystemAndFetchData, this);
}
/**

View File

@@ -134,14 +134,6 @@ 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];

View File

@@ -20,8 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TimeContext from './TimeContext';
import { MODES, REALTIME_MODE_KEY, TIME_CONTEXT_EVENTS } from './constants';
import TimeContext, { TIME_CONTEXT_EVENTS } from './TimeContext';
/**
* The IndependentTimeContext handles getting and setting time of the openmct application in general.
@@ -47,7 +46,7 @@ class IndependentTimeContext extends TimeContext {
this.globalTimeContext.on('removeOwnContext', this.removeIndependentContext);
}
bounds() {
bounds(newBounds) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.bounds(...arguments);
} else {
@@ -55,23 +54,7 @@ class IndependentTimeContext extends TimeContext {
}
}
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() {
tick(timestamp) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.tick(...arguments);
} else {
@@ -79,7 +62,7 @@ class IndependentTimeContext extends TimeContext {
}
}
clockOffsets() {
clockOffsets(offsets) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.clockOffsets(...arguments);
} else {
@@ -87,19 +70,11 @@ class IndependentTimeContext extends TimeContext {
}
}
getClockOffsets() {
stopClock() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getClockOffsets();
this.upstreamTimeContext.stopClock();
} else {
return super.getClockOffsets();
}
}
setClockOffsets() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setClockOffsets(...arguments);
} else {
return super.setClockOffsets(...arguments);
super.stopClock();
}
}
@@ -111,19 +86,10 @@ 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.
* and ticking will begin. Offsets from 'now' must also be provided. A clock
* can be unset by calling {@link stopClock}.
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
@@ -160,19 +126,15 @@ class IndependentTimeContext extends TimeContext {
this.activeClock = clock;
/**
* The active clock has changed.
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @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);
}
@@ -183,122 +145,6 @@ 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.
@@ -306,7 +152,7 @@ class IndependentTimeContext extends TimeContext {
followTimeContext() {
this.stopFollowingTimeContext();
if (this.upstreamTimeContext) {
Object.values(TIME_CONTEXT_EVENTS).forEach((eventName) => {
TIME_CONTEXT_EVENTS.forEach((eventName) => {
const thisTimeContext = this;
this.upstreamTimeContext.on(eventName, passthrough);
this.unlisteners.push(() => {
@@ -351,7 +197,6 @@ 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() {
@@ -414,16 +259,11 @@ class IndependentTimeContext extends TimeContext {
this.followTimeContext();
// Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.getBounds());
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
this.emit('bounds', this.bounds());
// 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;

View File

@@ -22,7 +22,6 @@
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
@@ -135,15 +134,14 @@ 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.setClock(clockKey);
timeContext.setMode(REALTIME_MODE_KEY, value);
timeContext.clock(clockKey, value);
} else {
timeContext.setMode(FIXED_MODE_KEY, value);
timeContext.stopClock();
timeContext.bounds(value);
}
// Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context
@@ -187,7 +185,6 @@ 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);

View File

@@ -87,7 +87,7 @@ describe('The Time API', function () {
expect(function () {
api.timeSystem(timeSystem, bounds);
}).not.toThrow();
expect(api.timeSystem()).toEqual(timeSystem);
expect(api.timeSystem()).toBe(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()).toEqual(timeSystem);
expect(api.timeSystem()).toBe(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));
});
xit('Allows the active clock to be set and unset', function () {
it('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', () => {

View File

@@ -21,7 +21,8 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import { TIME_CONTEXT_EVENTS, MODES, REALTIME_MODE_KEY, FIXED_MODE_KEY } from './constants';
export const TIME_CONTEXT_EVENTS = ['bounds', 'clock', 'timeSystem', 'clockOffsets'];
class TimeContext extends EventEmitter {
constructor() {
@@ -41,7 +42,6 @@ class TimeContext extends EventEmitter {
this.activeClock = undefined;
this.offsets = undefined;
this.mode = undefined;
this.tick = this.tick.bind(this);
}
@@ -56,8 +56,6 @@ 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.');
@@ -93,7 +91,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 = this.#copy(timeSystem);
this.system = timeSystem;
/**
* The time system used by the time
@@ -104,10 +102,7 @@ class TimeContext extends EventEmitter {
* @property {TimeSystem} The value of the currently applied
* Time System
* */
const system = this.#copy(this.system);
this.emit('timeSystem', system);
this.emit(TIME_CONTEXT_EVENTS.timeSystemChanged, system);
this.emit('timeSystem', this.system);
if (bounds) {
this.bounds(bounds);
}
@@ -168,8 +163,6 @@ 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) {
@@ -177,7 +170,7 @@ class TimeContext extends EventEmitter {
}
//Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = this.#copy(newBounds);
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
/**
* The start time, end time, or both have been updated.
* @event bounds
@@ -187,11 +180,10 @@ 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 this.#copy(this.boundsVal);
return JSON.parse(JSON.stringify(this.boundsVal));
}
/**
@@ -256,8 +248,6 @@ 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) {
@@ -288,19 +278,20 @@ class TimeContext extends EventEmitter {
}
/**
* Stop following the currently active clock. This will
* Stop the currently active clock from ticking, and unset it. This will
* revert all views to showing a static time frame defined by the current
* bounds.
*/
stopClock() {
this.#warnMethodDeprecated('"stopClock"');
this.setMode(FIXED_MODE_KEY);
if (this.activeClock) {
this.clock(undefined, undefined);
}
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided.
* and ticking will begin. Offsets from 'now' must also be provided. A clock
* can be unset by calling {@link stopClock}.
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
@@ -310,8 +301,6 @@ 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;
@@ -335,19 +324,15 @@ class TimeContext extends EventEmitter {
this.activeClock = clock;
/**
* The active clock has changed.
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @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);
}
@@ -355,7 +340,7 @@ class TimeContext extends EventEmitter {
throw 'When setting the clock, clock offsets must also be provided';
}
return this.isRealTime() ? this.activeClock : undefined;
return this.activeClock;
}
/**
@@ -364,304 +349,29 @@ class TimeContext extends EventEmitter {
* using current offsets.
*/
tick(timestamp) {
// 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);
}
}
/**
* 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) {
if (!this.activeClock) {
return;
}
if (mode === MODES.realtime && this.activeClock === undefined) {
throw `Unknown clock. Has a clock been registered with 'addClock'?`;
}
const newBounds = {
start: timestamp + this.offsets.start,
end: timestamp + this.offsets.end
};
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);
}
}
this.boundsVal = newBounds;
this.emit('bounds', this.boundsVal, true);
}
/**
* Checks if this time context is in realtime mode or not.
* Checks if this time context is in real-time mode or not.
* @returns {boolean} true if this context is in real-time mode, false if not
*/
isRealTime() {
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);
if (this.clock()) {
return true;
}
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));
return false;
}
}

View File

@@ -1,22 +0,0 @@
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
};

View File

@@ -1,72 +0,0 @@
/*****************************************************************************
* 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 mount from 'utils/mount';
class Tooltip extends EventEmitter {
constructor(
{ toolTipText, toolTipLocation, parentElement } = {
tooltipText: '',
toolTipLocation: 'below',
parentElement: null
}
) {
super();
const { vNode, destroy } = mount({
components: {
TooltipComponent: TooltipComponent
},
provide: {
toolTipText,
toolTipLocation,
parentElement
},
template: '<tooltip-component toolTipText="toolTipText"></tooltip-component>'
});
this.component = vNode.componentInstance;
this._destroy = destroy;
this.isActive = null;
}
destroy() {
if (!this.isActive) {
return;
}
this._destroy();
this.isActive = false;
}
/**
* @private
**/
show() {
document.body.appendChild(this.component.$el);
this.isActive = true;
}
}
export default Tooltip;

View File

@@ -1,90 +0,0 @@
/*****************************************************************************
* 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;

View File

@@ -1,61 +0,0 @@
<!--
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>

View File

@@ -1,10 +0,0 @@
.c-tooltip-wrapper {
max-width: 200px;
height: auto;
width: auto;
padding: $interiorMargin;
}
.c-tooltip {
font-style: italic;
}

View File

@@ -1,72 +0,0 @@
/*****************************************************************************
* 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;

View File

@@ -1,37 +0,0 @@
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;

View File

@@ -0,0 +1,52 @@
import { BROADCAST_CHANNEL_NAME } from './constants';
class RoleChannel {
constructor(openmct, channelName = BROADCAST_CHANNEL_NAME) {
this.openmct = openmct;
this.channelName = channelName;
this.roleChannel = undefined;
}
createRoleChannel() {
this.roleChannel = new BroadcastChannel(this.channelName);
}
subscribeToRole(cb) {
this.roleChannel.onmessage = (event => {
const role = event.data;
this.openmct.user.setActiveRole(role);
if (cb) {
cb(role);
}
});
}
unsubscribeToRole() {
this.roleChannel.close();
}
reconnect() {
this.roleChannel.close();
this.createRoleChannel();
}
broadcastNewRole(role) {
if (!this.roleChannel.name) {
return false;
}
try {
this.roleChannel.postMessage(role);
} catch (e) {
this.reconnect();
this.broadcastNewRole(role);
/** FIXME: there doesn't seem to be a reliable way to test for open/closed
* status of a broadcast channel; channel.name exists even after the
* channel is closed. Failure to update the subscribed tabs, should
* not block the focused tab's selection and so it is caught here.
* An error will often be thrown if the dialog remains open during HMR.
**/
}
}
}
export default RoleChannel;

View File

@@ -20,18 +20,19 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { ACTIVE_ROLE_LOCAL_STORAGE_KEY } from './constants';
import { SESSION_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);
}
class SessionPersistance {
getActiveRole() {
return sessionStorage.getItem(SESSION_STORAGE_KEY);
}
setActiveRole(role) {
return sessionStorage.setItem(SESSION_STORAGE_KEY, role);
}
clearActiveRole() {
return sessionStorage.removeItem(SESSION_STORAGE_KEY);
}
}
export default new StoragePersistance();
export default new SessionPersistance();

View File

@@ -19,248 +19,249 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import EventEmitter from "EventEmitter";
export default class StatusAPI extends EventEmitter {
#userAPI;
#openmct;
#userAPI;
#openmct;
constructor(userAPI, openmct) {
super();
this.#userAPI = userAPI;
this.#openmct = openmct;
constructor(userAPI, openmct) {
super();
this.#userAPI = userAPI;
this.#openmct = openmct;
this.onProviderStatusChange = this.onProviderStatusChange.bind(this);
this.onProviderPollQuestionChange = this.onProviderPollQuestionChange.bind(this);
this.listenToStatusEvents = this.listenToStatusEvents.bind(this);
this.onProviderStatusChange = this.onProviderStatusChange.bind(this);
this.onProviderPollQuestionChange = this.onProviderPollQuestionChange.bind(this);
this.listenToStatusEvents = this.listenToStatusEvents.bind(this);
this.#openmct.once('destroy', () => {
const provider = this.#userAPI.getProvider();
this.#openmct.once('destroy', () => {
const provider = this.#userAPI.getProvider();
if (typeof provider?.off === 'function') {
provider.off('statusChange', this.onProviderStatusChange);
provider.off('pollQuestionChange', this.onProviderPollQuestionChange);
}
});
if (typeof provider?.off === 'function') {
provider.off('statusChange', this.onProviderStatusChange);
provider.off('pollQuestionChange', this.onProviderPollQuestionChange);
}
});
this.#userAPI.on('providerAdded', this.listenToStatusEvents);
}
/**
* Fetch the currently defined operator status poll question. When presented with a status poll question, all operators will reply with their current status.
* @returns {Promise<PollQuestion>}
*/
getPollQuestion() {
const provider = this.#userAPI.getProvider();
if (provider.getPollQuestion) {
return provider.getPollQuestion();
} else {
this.#userAPI.error('User provider does not support polling questions');
this.#userAPI.on('providerAdded', this.listenToStatusEvents);
}
}
/**
* Set a poll question for operators to respond to. When presented with a status poll question, all operators will reply with their current status.
* @param {String} questionText - The text of the question
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async setPollQuestion(questionText) {
const canSetPollQuestion = await this.canSetPollQuestion();
/**
* Fetch the currently defined operator status poll question. When presented with a status poll question, all operators will reply with their current status.
* @returns {Promise<PollQuestion>}
*/
getPollQuestion() {
const provider = this.#userAPI.getProvider();
if (canSetPollQuestion) {
const provider = this.#userAPI.getProvider();
const result = await provider.setPollQuestion(questionText);
try {
await this.resetAllStatuses();
} catch (error) {
console.warn('Poll question set but unable to clear operator statuses.');
console.error(error);
}
return result;
} else {
this.#userAPI.error('User provider does not support setting polling question');
if (provider.getPollQuestion) {
return provider.getPollQuestion();
} else {
this.#userAPI.error("User provider does not support polling questions");
}
}
}
/**
* Can the currently logged in user set the operator status poll question.
* @returns {Promise<Boolean>}
*/
canSetPollQuestion() {
const provider = this.#userAPI.getProvider();
/**
* Set a poll question for operators to respond to. When presented with a status poll question, all operators will reply with their current status.
* @param {String} questionText - The text of the question
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async setPollQuestion(questionText) {
const canSetPollQuestion = await this.canSetPollQuestion();
if (provider.canSetPollQuestion) {
return provider.canSetPollQuestion();
} else {
return Promise.resolve(false);
if (canSetPollQuestion) {
const provider = this.#userAPI.getProvider();
const result = await provider.setPollQuestion(questionText);
try {
await this.resetAllStatuses();
} catch (error) {
console.warn("Poll question set but unable to clear operator statuses.");
console.error(error);
}
return result;
} else {
this.#userAPI.error("User provider does not support setting polling question");
}
}
}
/**
* @returns {Promise<Array<Status>>} the complete list of possible states that an operator can reply to a poll question with.
*/
async getPossibleStatuses() {
const provider = this.#userAPI.getProvider();
/**
* Can the currently logged in user set the operator status poll question.
* @returns {Promise<Boolean>}
*/
canSetPollQuestion() {
const provider = this.#userAPI.getProvider();
if (provider.getPossibleStatuses) {
const possibleStatuses = (await provider.getPossibleStatuses()) || [];
return possibleStatuses.map((status) => status);
} else {
this.#userAPI.error('User provider cannot provide statuses');
if (provider.canSetPollQuestion) {
return provider.canSetPollQuestion();
} else {
return Promise.resolve(false);
}
}
}
/**
* @param {import("./UserAPI").Role} role The role to fetch the current status for.
* @returns {Promise<Status>} the current status of the provided role
*/
async getStatusForRole(role) {
const provider = this.#userAPI.getProvider();
/**
* @returns {Promise<Array<Status>>} the complete list of possible states that an operator can reply to a poll question with.
*/
async getPossibleStatuses() {
const provider = this.#userAPI.getProvider();
if (provider.getStatusForRole) {
const status = await provider.getStatusForRole(role);
if (provider.getPossibleStatuses) {
const possibleStatuses = await provider.getPossibleStatuses() || [];
return status;
} else {
this.#userAPI.error('User provider does not support role status');
return possibleStatuses.map(status => status);
} else {
this.#userAPI.error("User provider cannot provide statuses");
}
}
}
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the given role
* @see StatusUserProvider
*/
canProvideStatusForRole(role) {
const provider = this.#userAPI.getProvider();
/**
* @param {import("./UserAPI").Role} role The role to fetch the current status for.
* @returns {Promise<Status>} the current status of the provided role
*/
async getStatusForRole(role) {
const provider = this.#userAPI.getProvider();
if (provider.canProvideStatusForRole) {
return Promise.resolve(provider.canProvideStatusForRole(role));
} else {
return Promise.resolve(false);
if (provider.getStatusForRole) {
const status = await provider.getStatusForRole(role);
return status;
} else {
this.#userAPI.error("User provider does not support role status");
}
}
}
/**
* @param {import("./UserAPI").Role} role The role to set the status for.
* @param {Status} status The status to set for the provided role
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
setStatusForRole(status) {
const provider = this.#userAPI.getProvider();
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the given role
* @see StatusUserProvider
*/
canProvideStatusForRole(role) {
const provider = this.#userAPI.getProvider();
if (provider.setStatusForRole) {
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');
if (provider.canProvideStatusForRole) {
return Promise.resolve(provider.canProvideStatusForRole(role));
} else {
return Promise.resolve(false);
}
}
}
/**
* Resets the status of the provided role back to its default status.
* @param {import("./UserAPI").Role} role The role to set the status for.
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async resetStatusForRole(role) {
const provider = this.#userAPI.getProvider();
const defaultStatus = await this.getDefaultStatusForRole(role);
/**
* @param {import("./UserAPI").Role} role The role to set the status for.
* @param {Status} status The status to set for the provided role
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
setStatusForRole(status) {
const provider = this.#userAPI.getProvider();
if (provider.setStatusForRole) {
return provider.setStatusForRole(role, defaultStatus);
} else {
this.#userAPI.error('User provider does not support resetting role status');
if (provider.setStatusForRole) {
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");
}
}
}
/**
* Resets the status of all operators to their default status
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async resetAllStatuses() {
const allStatusRoles = await this.getAllStatusRoles();
/**
* Resets the status of the provided role back to its default status.
* @param {import("./UserAPI").Role} role The role to set the status for.
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async resetStatusForRole(role) {
const provider = this.#userAPI.getProvider();
const defaultStatus = await this.getDefaultStatusForRole(role);
return Promise.all(allStatusRoles.map((role) => this.resetStatusForRole(role)));
}
/**
* The default status. This is the status that will be used before the user has selected any status.
* @param {import("./UserAPI").Role} role
* @returns {Promise<Status>} the default operator status if no other has been set.
*/
async getDefaultStatusForRole(role) {
const provider = this.#userAPI.getProvider();
const defaultStatus = await provider.getDefaultStatusForRole(role);
return defaultStatus;
}
/**
* All possible status roles. A status role is a user role that can provide status. In some systems
* this may be all user roles, but there may be cases where some users are not are not polled
* for status if they do not have a real-time operational role.
*
* @returns {Promise<Array<import("./UserAPI").Role>>} the default operator status if no other has been set.
*/
getAllStatusRoles() {
const provider = this.#userAPI.getProvider();
if (provider.getAllStatusRoles) {
return provider.getAllStatusRoles();
} else {
this.#userAPI.error('User provider cannot provide all status roles');
if (provider.setStatusForRole) {
return provider.setStatusForRole(role, defaultStatus);
} else {
this.#userAPI.error("User provider does not support resetting role status");
}
}
}
/**
* @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the currently logged in user, false otherwise.
* @see StatusUserProvider
*/
async canProvideStatusForCurrentUser() {
const provider = this.#userAPI.getProvider();
/**
* Resets the status of all operators to their default status
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async resetAllStatuses() {
const allStatusRoles = await this.getAllStatusRoles();
if (!provider) {
return false;
return Promise.all(allStatusRoles.map(role => this.resetStatusForRole(role)));
}
const activeStatusRole = await this.#userAPI.getActiveRole();
const canProvideStatus = await this.canProvideStatusForRole(activeStatusRole);
return canProvideStatus;
}
/**
* The default status. This is the status that will be used before the user has selected any status.
* @param {import("./UserAPI").Role} role
* @returns {Promise<Status>} the default operator status if no other has been set.
*/
async getDefaultStatusForRole(role) {
const provider = this.#userAPI.getProvider();
const defaultStatus = await provider.getDefaultStatusForRole(role);
/**
* Private internal function that cannot be made #private because it needs to be registered as a callback to the user provider
* @private
*/
listenToStatusEvents(provider) {
if (typeof provider.on === 'function') {
provider.on('statusChange', this.onProviderStatusChange);
provider.on('pollQuestionChange', this.onProviderPollQuestionChange);
return defaultStatus;
}
}
/**
* @private
*/
onProviderStatusChange(newStatus) {
this.emit('statusChange', newStatus);
}
/**
* All possible status roles. A status role is a user role that can provide status. In some systems
* this may be all user roles, but there may be cases where some users are not are not polled
* for status if they do not have a real-time operational role.
*
* @returns {Promise<Array<import("./UserAPI").Role>>} the default operator status if no other has been set.
*/
getAllStatusRoles() {
const provider = this.#userAPI.getProvider();
/**
* @private
*/
onProviderPollQuestionChange(pollQuestion) {
this.emit('pollQuestionChange', pollQuestion);
}
if (provider.getAllStatusRoles) {
return provider.getAllStatusRoles();
} else {
this.#userAPI.error("User provider cannot provide all status roles");
}
}
/**
* @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the currently logged in user, false otherwise.
* @see StatusUserProvider
*/
async canProvideStatusForCurrentUser() {
const provider = this.#userAPI.getProvider();
if (provider.getStatusRoleForCurrentUser) {
const activeStatusRole = await this.#userAPI.getActiveRole();
const canProvideStatus = await this.canProvideStatusForRole(activeStatusRole);
return canProvideStatus;
} else {
return false;
}
}
/**
* Private internal function that cannot be made #private because it needs to be registered as a callback to the user provider
* @private
*/
listenToStatusEvents(provider) {
if (typeof provider.on === 'function') {
provider.on('statusChange', this.onProviderStatusChange);
provider.on('pollQuestionChange', this.onProviderPollQuestionChange);
}
}
/**
* @private
*/
onProviderStatusChange(newStatus) {
this.emit('statusChange', newStatus);
}
/**
* @private
*/
onProviderPollQuestionChange(pollQuestion) {
this.emit('pollQuestionChange', pollQuestion);
}
}
/**
@@ -282,4 +283,4 @@ export default class StatusAPI extends EventEmitter {
* @property {String} key - A unique identifier for this status
* @property {String} label - A human readable label for this status
* @property {Number} timestamp - The time that the status was set.
*/
*/

View File

@@ -22,59 +22,59 @@
import UserProvider from './UserProvider';
export default class StatusUserProvider extends UserProvider {
/**
* @param {('statusChange'|'pollQuestionChange')} event the name of the event to listen to
* @param {Function} callback a function to invoke when this event occurs
*/
on(event, callback) {}
/**
* @param {('statusChange'|'pollQuestionChange')} event the name of the event to stop listen to
* @param {Function} callback the callback function used to register the listener
*/
off(event, callback) {}
/**
* @returns {import("./StatusAPI").PollQuestion} the current status poll question
*/
async getPollQuestion() {}
/**
* @param {import("./StatusAPI").PollQuestion} pollQuestion a new poll question to set
* @returns {Promise<Boolean>} true if operation was successful, otherwise false
*/
async setPollQuestion(pollQuestion) {}
/**
* @returns {Promise<Boolean>} true if the current user can set the poll question, otherwise false
*/
async canSetPollQuestion() {}
/**
* @returns {Promise<Array<import("./StatusAPI").Status>>} a list of the possible statuses that an operator can be in
*/
async getPossibleStatuses() {}
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<import("./StatusAPI").Status}
*/
async getStatusForRole(role) {}
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<import("./StatusAPI").Status}
*/
async getDefaultStatusForRole(role) {}
/**
* @param {import("./UserAPI").Role} role
* @param {*} status
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async setStatusForRole(role, status) {}
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<Boolean} true if the user provider can provide status for the given role
*/
async canProvideStatusForRole(role) {}
/**
* @returns {Promise<Array<import("./UserAPI").Role>>} a list of all available status roles, if user permissions allow it.
*/
async getAllStatusRoles() {}
/**
* @returns {Promise<import("./UserAPI").Role>} the active status role for the currently logged in user
*/
/**
* @param {('statusChange'|'pollQuestionChange')} event the name of the event to listen to
* @param {Function} callback a function to invoke when this event occurs
*/
on(event, callback) {}
/**
* @param {('statusChange'|'pollQuestionChange')} event the name of the event to stop listen to
* @param {Function} callback the callback function used to register the listener
*/
off(event, callback) {}
/**
* @returns {import("./StatusAPI").PollQuestion} the current status poll question
*/
async getPollQuestion() {}
/**
* @param {import("./StatusAPI").PollQuestion} pollQuestion a new poll question to set
* @returns {Promise<Boolean>} true if operation was successful, otherwise false
*/
async setPollQuestion(pollQuestion) {}
/**
* @returns {Promise<Boolean>} true if the current user can set the poll question, otherwise false
*/
async canSetPollQuestion() {}
/**
* @returns {Promise<Array<import("./StatusAPI").Status>>} a list of the possible statuses that an operator can be in
*/
async getPossibleStatuses() {}
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<import("./StatusAPI").Status}
*/
async getStatusForRole(role) {}
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<import("./StatusAPI").Status}
*/
async getDefaultStatusForRole(role) {}
/**
* @param {import("./UserAPI").Role} role
* @param {*} status
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async setStatusForRole(role, status) {}
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<Boolean} true if the user provider can provide status for the given role
*/
async canProvideStatusForRole(role) {}
/**
* @returns {Promise<Array<import("./UserAPI").Role>>} a list of all available status roles, if user permissions allow it.
*/
async getAllStatusRoles() {}
/**
* @returns {Promise<import("./UserAPI").Role>} the active status role for the currently logged in user
*/
}

View File

@@ -21,178 +21,173 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import { MULTIPLE_PROVIDER_ERROR, NO_PROVIDER_ERROR } from './constants';
import {
MULTIPLE_PROVIDER_ERROR,
NO_PROVIDER_ERROR
} from './constants';
import StatusAPI from './StatusAPI';
import User from './User';
import StoragePersistance from './StoragePersistance';
import SessionPersistance from './SessionPersistance';
class UserAPI extends EventEmitter {
/**
* @param {OpenMCT} openmct
* @param {UserAPIConfiguration} config
*/
constructor(openmct, config) {
super();
/**
* @param {OpenMCT} openmct
* @param {UserAPIConfiguration} config
*/
constructor(openmct, config) {
super();
this._openmct = openmct;
this._provider = undefined;
this._openmct = openmct;
this._provider = undefined;
this.User = User;
this.status = new StatusAPI(this, openmct, config);
}
/**
* Set the user provider for the user API. This allows you
* to specifiy ONE user provider to be used with Open MCT.
* @method setProvider
* @memberof module:openmct.UserAPI#
* @param {module:openmct.UserAPI~UserProvider} provider the new
* user provider
*/
setProvider(provider) {
if (this.hasProvider()) {
this.error(MULTIPLE_PROVIDER_ERROR);
this.User = User;
this.status = new StatusAPI(this, openmct, config);
}
this._provider = provider;
this.emit('providerAdded', this._provider);
}
/**
* Set the user provider for the user API. This allows you
* to specifiy ONE user provider to be used with Open MCT.
* @method setProvider
* @memberof module:openmct.UserAPI#
* @param {module:openmct.UserAPI~UserProvider} provider the new
* user provider
*/
setProvider(provider) {
if (this.hasProvider()) {
this.error(MULTIPLE_PROVIDER_ERROR);
}
getProvider() {
return this._provider;
}
/**
* Return true if the user provider has been set.
*
* @memberof module:openmct.UserAPI#
* @returns {boolean} true if the user provider exists
*/
hasProvider() {
return this._provider !== undefined;
}
/**
* If a user provider is set, it will return a copy of a user object from
* the provider. If the user is not logged in, it will return undefined;
*
* @memberof module:openmct.UserAPI#
* @returns {Function|Promise} user provider 'getCurrentUser' method
* @throws Will throw an error if no user provider is set
*/
getCurrentUser() {
if (!this.hasProvider()) {
return Promise.resolve(undefined);
} else {
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;
this._provider = provider;
this.emit('providerAdded', this._provider);
}
// 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
* 'isLoggedIn' method
*
* @memberof module:openmct.UserAPI#
* @returns {Function|Boolean} user provider 'isLoggedIn' method
* @throws Will throw an error if no user provider is set
*/
isLoggedIn() {
if (!this.hasProvider()) {
return false;
getProvider() {
return this._provider;
}
return this._provider.isLoggedIn();
}
/**
* If a user provider is set, it will return a call to it's
* 'hasRole' method
*
* @memberof module:openmct.UserAPI#
* @returns {Function|Boolean} user provider 'isLoggedIn' method
* @param {string} roleId id of role to check for
* @throws Will throw an error if no user provider is set
*/
hasRole(roleId) {
this.noProviderCheck();
return this._provider.hasRole(roleId);
}
/**
* Checks if a provider is set and if not, will throw error
*
* @private
* @throws Will throw an error if no user provider is set
*/
noProviderCheck() {
if (!this.hasProvider()) {
this.error(NO_PROVIDER_ERROR);
/**
* Return true if the user provider has been set.
*
* @memberof module:openmct.UserAPI#
* @returns {boolean} true if the user provider exists
*/
hasProvider() {
return this._provider !== undefined;
}
}
/**
* Utility function for throwing errors
*
* @private
* @param {string} error description of error
* @throws Will throw error passed in
*/
error(error) {
throw new Error(error);
}
/**
* If a user provider is set, it will return a copy of a user object from
* the provider. If the user is not logged in, it will return undefined;
*
* @memberof module:openmct.UserAPI#
* @returns {Function|Promise} user provider 'getCurrentUser' method
* @throws Will throw an error if no user provider is set
*/
getCurrentUser() {
if (!this.hasProvider()) {
return Promise.resolve(undefined);
} else {
return this._provider.getCurrentUser();
}
}
getPossibleRoles() {
if (!this.hasProvider()) {
return Promise.resolve(undefined);
} else {
return this._provider.getPossibleRoles();
}
}
/**
* If a user provider is set, it will return the active role Id
* @returns object
*/
getActiveRole() {
if (!this.hasProvider()) {
return Promise.resolve(undefined);
}
// get from session storage
const sessionStorageValue = SessionPersistance.getActiveRole();
if (sessionStorageValue === 'undefined' || sessionStorageValue === undefined) {
return undefined;
}
return sessionStorageValue;
}
setActiveRole(role) {
SessionPersistance.setActiveRole(role);
}
/**
* Will return if a role can provide a operator status response
* @memberof module:openmct.UserApi#
* @returns {Boolean}
*/
canProvideStatusForRole() {
if (!this || !this.hasProvider()) {
return Promise.resolve(undefined);
}
const activeRole = this.getActiveRole();
return this._provider.canProvideStatusForRole?.(activeRole);
}
/**
* If a user provider is set, it will return the user provider's
* 'isLoggedIn' method
*
* @memberof module:openmct.UserAPI#
* @returns {Function|Boolean} user provider 'isLoggedIn' method
* @throws Will throw an error if no user provider is set
*/
isLoggedIn() {
if (!this.hasProvider()) {
return false;
}
return this._provider.isLoggedIn();
}
/**
* If a user provider is set, it will return a call to it's
* 'hasRole' method
*
* @memberof module:openmct.UserAPI#
* @returns {Function|Boolean} user provider 'isLoggedIn' method
* @param {string} roleId id of role to check for
* @throws Will throw an error if no user provider is set
*/
hasRole(roleId) {
this.noProviderCheck();
return this._provider.hasRole(roleId);
}
/**
* Checks if a provider is set and if not, will throw error
*
* @private
* @throws Will throw an error if no user provider is set
*/
noProviderCheck() {
if (!this.hasProvider()) {
this.error(NO_PROVIDER_ERROR);
}
}
/**
* Utility function for throwing errors
*
* @private
* @param {string} error description of error
* @throws Will throw error passed in
*/
error(error) {
throw new Error(error);
}
}
export default UserAPI;
@@ -212,4 +207,4 @@ export default UserAPI;
* @property {String} statusClass The class to apply to the indicator when this status is active eg. "s-status-error"
* @property {String} statusBgColor The background color to apply in the status summary section of the poll question popup for this status eg."#9900cc"
* @property {String} statusFgColor The foreground color to apply in the status summary section of the poll question popup for this status eg. "#fff"
*/
*/

View File

@@ -25,7 +25,7 @@ import { MULTIPLE_PROVIDER_ERROR } from './constants';
import ExampleUserProvider from '../../../example/exampleUser/ExampleUserProvider';
const USERNAME = 'Test User';
const EXAMPLE_ROLE = 'flight';
const EXAMPLE_ROLE = 'example-role';
describe('The User API', () => {
let openmct;

View File

@@ -23,5 +23,5 @@
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';
export const SESSION_STORAGE_KEY = 'USER_ROLE';
export const BROADCAST_CHANNEL_NAME = 'USER_ROLE';

View File

@@ -1,26 +1,21 @@
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: couchSearchName,
key: couchSearchName
namespace: 'couch-search',
key: 'couch-search'
});
openmct.objects.addProvider(couchSearchName, {
openmct.objects.addProvider('couch-search', {
get(identifier) {
if (identifier.key !== couchSearchName) {
if (identifier.key !== 'couch-search') {
return undefined;
} else {
return Promise.resolve({
identifier,
type: 'folder',
name: folderName || DEFAULT_NAME,
name: folderName || 'CouchDB Documents',
location: 'ROOT'
});
}
@@ -30,8 +25,8 @@ export default function (folderName, couchPlugin, searchFilter) {
openmct.composition.addProvider({
appliesTo(domainObject) {
return (
domainObject.identifier.namespace === couchSearchName &&
domainObject.identifier.key === couchSearchName
domainObject.identifier.namespace === 'couch-search' &&
domainObject.identifier.key === 'couch-search'
);
},
load() {

View File

@@ -25,8 +25,8 @@ import CouchDBSearchFolderPlugin from './plugin';
describe('the plugin', function () {
let identifier = {
namespace: 'couch-search-CouchDB-Documents',
key: 'couch-search-CouchDB-Documents'
namespace: 'couch-search',
key: 'couch-search'
};
let testPath = '/test/db';
let openmct;

View File

@@ -21,16 +21,13 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import { markRaw } from 'vue';
export default class LADTableConfiguration extends EventEmitter {
constructor(domainObject, openmct) {
super();
this.domainObject = domainObject;
// Prevent Vue from making this a Proxy, otherwise
// it cannot access any private methods (like #mutate()).
this.openmct = markRaw(openmct);
this.openmct = openmct;
this.objectMutated = this.objectMutated.bind(this);
this.unlistenFromMutation = openmct.objects.observe(

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
import LADTableConfigurationComponent from './components/LADTableConfiguration.vue';
import mount from 'utils/mount';
import Vue from 'vue';
export default function LADTableConfigurationViewProvider(openmct) {
return {
@@ -37,34 +37,28 @@ export default function LADTableConfigurationViewProvider(openmct) {
return object?.type === 'LadTable' || object?.type === 'LadTableSet';
},
view(selection) {
let _destroy = null;
let component;
return {
show(element) {
const { destroy } = mount(
{
el: element,
components: {
LADTableConfiguration: LADTableConfigurationComponent
},
provide: {
openmct
},
template: '<LADTableConfiguration />'
component = new Vue({
el: element,
components: {
LADTableConfiguration: LADTableConfigurationComponent
},
{
app: openmct.app,
element
}
);
_destroy = destroy;
provide: {
openmct
},
template: '<LADTableConfiguration />'
});
},
priority() {
return 1;
},
destroy() {
if (_destroy) {
_destroy();
if (component) {
component.$destroy();
component = undefined;
}
}
};

View File

@@ -22,47 +22,38 @@
import LadTable from './components/LADTable.vue';
import LADTableConfiguration from './LADTableConfiguration';
import mount from 'utils/mount';
import Vue from 'vue';
export default class LADTableView {
constructor(openmct, domainObject, objectPath) {
this.openmct = openmct;
this.domainObject = domainObject;
this.objectPath = objectPath;
this.component = null;
this._destroy = null;
this.component = undefined;
}
show(element) {
let ladTableConfiguration = new LADTableConfiguration(this.domainObject, this.openmct);
const { vNode, destroy } = mount(
{
el: element,
components: {
LadTable
},
provide: {
openmct: this.openmct,
currentView: this,
ladTableConfiguration
},
data: () => {
return {
domainObject: this.domainObject,
objectPath: this.objectPath
};
},
template:
'<lad-table ref="ladTable" :domain-object="domainObject" :object-path="objectPath"></lad-table>'
this.component = new Vue({
el: element,
components: {
LadTable
},
{
app: this.openmct.app,
element
}
);
this.component = vNode.componentInstance;
this._destroy = destroy;
provide: {
openmct: this.openmct,
currentView: this,
ladTableConfiguration
},
data: () => {
return {
domainObject: this.domainObject,
objectPath: this.objectPath
};
},
template:
'<lad-table ref="ladTable" :domain-object="domainObject" :object-path="objectPath"></lad-table>'
});
}
getViewContext() {
@@ -73,9 +64,8 @@ export default class LADTableView {
return this.component.$refs.ladTable.getViewContext();
}
destroy() {
if (this._destroy) {
this._destroy();
}
destroy(element) {
this.component.$destroy();
this.component = undefined;
}
}

View File

@@ -22,46 +22,37 @@
import LadTableSet from './components/LadTableSet.vue';
import LADTableConfiguration from './LADTableConfiguration';
import mount from 'utils/mount';
import Vue from 'vue';
export default class LadTableSetView {
constructor(openmct, domainObject, objectPath) {
this.openmct = openmct;
this.domainObject = domainObject;
this.objectPath = objectPath;
this._destroy = null;
this.component = null;
this.component = undefined;
}
show(element) {
let ladTableConfiguration = new LADTableConfiguration(this.domainObject, this.openmct);
const { vNode, destroy } = mount(
{
el: element,
components: {
LadTableSet
},
provide: {
openmct: this.openmct,
objectPath: this.objectPath,
currentView: this,
ladTableConfiguration
},
data: () => {
return {
domainObject: this.domainObject
};
},
template: '<lad-table-set ref="ladTableSet" :domain-object="domainObject"></lad-table-set>'
this.component = new Vue({
el: element,
components: {
LadTableSet
},
{
app: this.openmct.app,
element
}
);
this._destroy = destroy;
this.component = vNode.componentInstance;
provide: {
openmct: this.openmct,
objectPath: this.objectPath,
currentView: this,
ladTableConfiguration
},
data: () => {
return {
domainObject: this.domainObject
};
},
template: '<lad-table-set ref="ladTableSet" :domain-object="domainObject"></lad-table-set>'
});
}
getViewContext() {
@@ -72,9 +63,8 @@ export default class LadTableSetView {
return this.component.$refs.ladTableSet.getViewContext();
}
destroy() {
if (this._destroy) {
this._destroy();
}
destroy(element) {
this.component.$destroy();
this.component = undefined;
}
}

View File

@@ -26,14 +26,7 @@
@click="clickedRow"
@contextmenu.prevent="showContextMenu"
>
<td
ref="tableCell"
class="js-first-data"
@mouseover.ctrl="showToolTip"
@mouseleave="hideToolTip"
>
{{ domainObject.name }}
</td>
<td class="js-first-data">{{ 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">
@@ -49,10 +42,8 @@ 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: {
@@ -190,7 +181,7 @@ export default {
this.previewAction = new PreviewAction(this.openmct);
this.previewAction.on('isVisible', this.togglePreviewState);
},
unmounted() {
destroyed() {
this.openmct.time.off('timeSystem', this.updateTimeSystem);
this.telemetryCollection.off('add', this.setLatestValues);
this.telemetryCollection.off('clear', this.resetValues);
@@ -268,10 +259,6 @@ 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');
}
}
};

View File

@@ -49,7 +49,7 @@
</template>
<script>
import Vue, { toRaw } from 'vue';
import Vue from 'vue';
import LadRow from './LADRow.vue';
import StalenessUtils from '@/utils/staleness';
@@ -139,7 +139,7 @@ export default {
);
this.initializeViewActions();
},
unmounted() {
destroyed() {
this.ladTableConfiguration.off('change', this.handleConfigurationChange);
this.composition.off('add', this.addItem);
@@ -191,7 +191,7 @@ export default {
reorder(reorderPlan) {
const oldItems = this.items.slice();
reorderPlan.forEach((reorderEvent) => {
this.items[reorderEvent.newIndex] = oldItems[reorderEvent.oldIndex];
this.$set(this.items, reorderEvent.newIndex, oldItems[reorderEvent.oldIndex]);
});
},
metadataHasUnits(valueMetadatas) {
@@ -230,7 +230,7 @@ export default {
};
},
toggleFixedLayout() {
const config = structuredClone(toRaw(this.configuration));
const config = structuredClone(this.configuration);
config.isFixedLayout = !this.configuration.isFixedLayout;
this.ladTableConfiguration.updateConfiguration(config);

View File

@@ -87,7 +87,7 @@ export default {
this.composition.load();
},
unmounted() {
destroyed() {
this.ladTableConfiguration.destroy();
this.openmct.editor.off('isEditing', this.toggleEdit);
@@ -126,7 +126,7 @@ export default {
ladTable.domainObject = domainObject;
ladTable.key = this.openmct.objects.makeKeyString(domainObject.identifier);
this.ladTelemetryObjects[ladTable.key] = [];
this.$set(this.ladTelemetryObjects, ladTable.key, []);
this.ladTableObjects.push(ladTable);
const composition = this.openmct.composition.get(ladTable.domainObject);
@@ -165,7 +165,7 @@ export default {
const telemetryObjects = this.ladTelemetryObjects[ladTable.key];
telemetryObjects.push(telemetryObject);
this.ladTelemetryObjects[ladTable.key] = telemetryObjects;
this.$set(this.ladTelemetryObjects, ladTable.key, telemetryObjects);
this.shouldShowUnitsCheckbox();
};
@@ -179,7 +179,7 @@ export default {
);
telemetryObjects.splice(index, 1);
this.ladTelemetryObjects[ladTable.key] = telemetryObjects;
this.$set(this.ladTelemetryObjects, ladTable.key, telemetryObjects);
this.shouldShowUnitsCheckbox();
};
@@ -220,7 +220,7 @@ export default {
}
if (showUnitsCheckbox && this.headers.units === undefined) {
this.headers.units = 'Units';
this.$set(this.headers, 'units', 'Units');
}
if (!showUnitsCheckbox && this.headers?.units) {

View File

@@ -33,8 +33,8 @@
</tr>
</thead>
<tbody>
<template v-for="ladTable in ladTableObjects" :key="ladTable.key">
<tr class="c-table__group-header js-lad-table-set__table-headers">
<template v-for="ladTable in ladTableObjects">
<tr :key="ladTable.key" class="c-table__group-header js-lad-table-set__table-headers">
<td colspan="10">
{{ ladTable.domainObject.name }}
</td>
@@ -125,7 +125,7 @@ export default {
this.stalenessSubscription = {};
},
unmounted() {
destroyed() {
this.ladTableConfiguration.off('change', this.handleConfigurationChange);
this.composition.off('add', this.addLadTable);
this.composition.off('remove', this.removeLadTable);
@@ -147,7 +147,7 @@ export default {
ladTable.key = this.openmct.objects.makeKeyString(domainObject.identifier);
ladTable.objectPath = [domainObject, ...this.objectPath];
this.ladTelemetryObjects[ladTable.key] = [];
this.$set(this.ladTelemetryObjects, ladTable.key, []);
this.ladTableObjects.push(ladTable);
let composition = this.openmct.composition.get(ladTable.domainObject);
@@ -201,7 +201,7 @@ export default {
const telemetryObjects = this.ladTelemetryObjects[ladTable.key];
telemetryObjects.push(telemetryObject);
this.ladTelemetryObjects[ladTable.key] = telemetryObjects;
this.$set(this.ladTelemetryObjects, ladTable.key, telemetryObjects);
this.stalenessSubscription[combinedKey] = {};
this.stalenessSubscription[combinedKey].stalenessUtils = new StalenessUtils(
@@ -236,7 +236,7 @@ export default {
this.unwatchStaleness(combinedKey);
telemetryObjects.splice(index, 1);
this.ladTelemetryObjects[ladTable.key] = telemetryObjects;
this.$set(this.ladTelemetryObjects, ladTable.key, telemetryObjects);
};
},
unwatchStaleness(combinedKey) {

View File

@@ -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.trim();
const rowName = parent.querySelector(TABLE_BODY_FIRST_ROW_FIRST_DATA).innerText;
const expectedName = mockObj.telemetry.name;
expect(rowName).toBe(expectedName);

View File

@@ -20,20 +20,14 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { FIXED_MODE_KEY, REALTIME_MODE_KEY, TIME_CONTEXT_EVENTS } from '../../api/time/constants';
const TIME_EVENTS = ['timeSystem', 'clock', 'clockOffsets'];
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 TIME_EVENTS = [
TIME_CONTEXT_EVENTS.timeSystemChanged,
TIME_CONTEXT_EVENTS.modeChanged,
TIME_CONTEXT_EVENTS.clockChanged,
TIME_CONTEXT_EVENTS.clockOffsetsChanged
];
const MODE_FIXED = 'fixed';
export default class URLTimeSettingsSynchronizer {
constructor(openmct) {
@@ -73,7 +67,7 @@ export default class URLTimeSettingsSynchronizer {
}
updateTimeSettings() {
const timeParameters = this.parseParametersFromUrl();
let timeParameters = this.parseParametersFromUrl();
if (this.areTimeParametersValid(timeParameters)) {
this.setTimeApiFromUrl(timeParameters);
@@ -84,18 +78,21 @@ export default class URLTimeSettingsSynchronizer {
}
parseParametersFromUrl() {
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 = {
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 = {
start: startBound,
end: endBound
};
const startOffset = parseInt(searchParams.get(SEARCH_START_DELTA), 10);
const endOffset = parseInt(searchParams.get(SEARCH_END_DELTA), 10);
const clockOffsets = {
let startOffset = parseInt(searchParams.get(SEARCH_START_DELTA), 10);
let endOffset = parseInt(searchParams.get(SEARCH_END_DELTA), 10);
let clockOffsets = {
start: 0 - startOffset,
end: endOffset
};
@@ -109,35 +106,30 @@ export default class URLTimeSettingsSynchronizer {
}
setTimeApiFromUrl(timeParameters) {
const timeSystem = this.openmct.time.getTimeSystem();
if (timeParameters.mode === FIXED_MODE_KEY) {
// should update timesystem
if (timeSystem.key !== timeParameters.timeSystem) {
this.openmct.time.setTimeSystem(timeParameters.timeSystem, timeParameters.bounds);
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);
}
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);
if (this.openmct.time.clock()) {
this.openmct.time.stopClock();
}
} else {
const clock = this.openmct.time.getClock();
if (clock?.key !== timeParameters.mode) {
this.openmct.time.setClock(timeParameters.mode);
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);
}
if (
!this.areStartAndEndEqual(this.openmct.time.getClockOffsets(), timeParameters.clockOffsets)
!this.openmct.time.timeSystem() ||
this.openmct.time.timeSystem().key !== 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);
this.openmct.time.timeSystem(timeParameters.timeSystem);
}
}
}
@@ -149,14 +141,13 @@ export default class URLTimeSettingsSynchronizer {
}
setUrlFromTimeApi() {
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();
let searchParams = this.openmct.router.getAllSearchParams();
let clock = this.openmct.time.clock();
let bounds = this.openmct.time.bounds();
let clockOffsets = this.openmct.time.clockOffsets();
if (mode === FIXED_MODE_KEY) {
searchParams.set(SEARCH_MODE, FIXED_MODE_KEY);
if (clock === undefined) {
searchParams.set(SEARCH_MODE, MODE_FIXED);
searchParams.set(SEARCH_START_BOUND, bounds.start);
searchParams.set(SEARCH_END_BOUND, bounds.end);
@@ -177,8 +168,8 @@ export default class URLTimeSettingsSynchronizer {
searchParams.delete(SEARCH_END_BOUND);
}
searchParams.set(SEARCH_TIME_SYSTEM, this.openmct.time.getTimeSystem().key);
this.openmct.router.updateParams(searchParams);
searchParams.set(SEARCH_TIME_SYSTEM, this.openmct.time.timeSystem().key);
this.openmct.router.setAllSearchParams(searchParams);
}
areTimeParametersValid(timeParameters) {
@@ -188,7 +179,7 @@ export default class URLTimeSettingsSynchronizer {
this.isModeValid(timeParameters.mode) &&
this.isTimeSystemValid(timeParameters.timeSystem)
) {
if (timeParameters.mode === FIXED_MODE_KEY) {
if (timeParameters.mode === 'fixed') {
isValid = this.areStartAndEndValid(timeParameters.bounds);
} else {
isValid = this.areStartAndEndValid(timeParameters.clockOffsets);
@@ -212,9 +203,8 @@ export default class URLTimeSettingsSynchronizer {
isTimeSystemValid(timeSystem) {
let isValid = timeSystem !== undefined;
if (isValid) {
const timeSystemObject = this.openmct.time.timeSystems.get(timeSystem);
let timeSystemObject = this.openmct.time.timeSystems.get(timeSystem);
isValid = timeSystemObject !== undefined;
}
@@ -228,17 +218,18 @@ export default class URLTimeSettingsSynchronizer {
isValid = true;
}
if (
isValid &&
(mode.toLowerCase() === FIXED_MODE_KEY || this.openmct.time.clocks.get(mode) !== undefined)
) {
isValid = true;
if (isValid) {
if (mode.toLowerCase() === MODE_FIXED) {
isValid = true;
} else {
isValid = this.openmct.time.clocks.get(mode) !== undefined;
}
}
return isValid;
}
areStartAndEndEqual(firstBounds, secondBounds) {
return firstBounds?.start === secondBounds.start && firstBounds?.end === secondBounds.end;
return firstBounds.start === secondBounds.start && firstBounds.end === secondBounds.end;
}
}

View File

@@ -40,6 +40,7 @@ describe('The URLTimeSettingsSynchronizer', () => {
});
afterEach(() => {
openmct.time.stopClock();
openmct.router.removeListener('change:hash', resolveFunction);
appHolder = undefined;

View File

@@ -84,7 +84,7 @@ define([
rowCount: 'reflow'
},
template: autoflowTemplate,
unmounted: function () {
destroyed: function () {
controller.destroy();
if (interval) {
@@ -109,7 +109,7 @@ define([
});
}
AutoflowTabularView.prototype = Object.create(VueView.default.prototype);
AutoflowTabularView.prototype = Object.create(VueView.prototype);
return AutoflowTabularView;
});

View File

@@ -20,15 +20,15 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import mount from 'utils/mount';
export default function () {
return function VueView(options) {
const { vNode, destroy } = mount(options);
define(['vue'], function (Vue) {
function VueView(options) {
const vm = new Vue(options);
this.show = function (container) {
container.appendChild(vNode.el);
container.appendChild(vm.$mount().$el);
};
this.destroy = destroy;
};
}
this.destroy = vm.$destroy.bind(vm);
}
return VueView;
});

View File

@@ -93,7 +93,7 @@ export default {
});
this.registerListeners();
},
beforeUnmount() {
beforeDestroy() {
if (this.plotResizeObserver) {
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
clearTimeout(this.resizeTimer);

View File

@@ -83,7 +83,7 @@ export default {
this.refreshData
);
},
beforeUnmount() {
beforeDestroy() {
this.stopFollowingTimeContext();
this.removeAllSubscriptions();

View File

@@ -22,7 +22,7 @@
import BarGraphView from './BarGraphView.vue';
import { BAR_GRAPH_KEY, BAR_GRAPH_VIEW } from './BarGraphConstants';
import mount from 'utils/mount';
import Vue from 'vue';
export default function BarGraphViewProvider(openmct) {
function isCompactView(objectPath) {
@@ -44,43 +44,34 @@ export default function BarGraphViewProvider(openmct) {
},
view: function (domainObject, objectPath) {
let _destroy = null;
let component = null;
let component;
return {
show: function (element) {
let isCompact = isCompactView(objectPath);
const { vNode, destroy } = mount(
{
el: element,
components: {
BarGraphView
},
provide: {
openmct,
domainObject,
path: objectPath
},
data() {
return {
options: {
compact: isCompact
}
};
},
template: '<bar-graph-view ref="graphComponent" :options="options"></bar-graph-view>'
component = new Vue({
el: element,
components: {
BarGraphView
},
{
app: openmct.app,
element
}
);
_destroy = destroy;
component = vNode.componentInstance;
provide: {
openmct,
domainObject,
path: objectPath
},
data() {
return {
options: {
compact: isCompact
}
};
},
template: '<bar-graph-view ref="graphComponent" :options="options"></bar-graph-view>'
});
},
destroy: function () {
_destroy();
component.$destroy();
component = undefined;
},
onClearData() {
component.$refs.graphComponent.refreshData();

View File

@@ -1,6 +1,6 @@
import { BAR_GRAPH_INSPECTOR_KEY, BAR_GRAPH_KEY } from '../BarGraphConstants';
import Vue from 'vue';
import BarGraphOptions from './BarGraphOptions.vue';
import mount from 'utils/mount';
export default function BarGraphInspectorViewProvider(openmct) {
return {
@@ -16,35 +16,29 @@ export default function BarGraphInspectorViewProvider(openmct) {
return object && object.type === BAR_GRAPH_KEY;
},
view: function (selection) {
let _destroy = null;
let component;
return {
show: function (element) {
const { destroy } = mount(
{
el: element,
components: {
BarGraphOptions
},
provide: {
openmct,
domainObject: selection[0][0].context.item
},
template: '<bar-graph-options></bar-graph-options>'
component = new Vue({
el: element,
components: {
BarGraphOptions
},
{
app: openmct.app,
element
}
);
_destroy = destroy;
provide: {
openmct,
domainObject: selection[0][0].context.item
},
template: '<bar-graph-options></bar-graph-options>'
});
},
priority: function () {
return openmct.priority.HIGH + 1;
},
destroy: function () {
if (_destroy) {
_destroy();
if (component) {
component.$destroy();
component = undefined;
}
}
};

View File

@@ -167,7 +167,7 @@ export default {
this.registerListeners();
this.composition.load();
},
beforeUnmount() {
beforeDestroy() {
this.openmct.editor.off('isEditing', this.setEditState);
this.stopListening();
},
@@ -192,7 +192,7 @@ export default {
}
},
addSeries(series, index) {
this.plotSeries.push(series);
this.$set(this.plotSeries, this.plotSeries.length, series);
this.setupOptions();
},
removeSeries(seriesIdentifier) {

View File

@@ -115,7 +115,7 @@ export default {
this.initColorAndName
);
},
beforeUnmount() {
beforeDestroy() {
if (this.removeBarStylesListener) {
this.removeBarStylesListener();
}

View File

@@ -77,7 +77,7 @@ export default {
this.reloadTelemetry
);
},
beforeUnmount() {
beforeDestroy() {
this.stopFollowingTimeContext();
if (!this.composition) {

View File

@@ -22,7 +22,7 @@
import ScatterPlotView from './ScatterPlotView.vue';
import { SCATTER_PLOT_KEY, SCATTER_PLOT_VIEW, TIME_STRIP_KEY } from './scatterPlotConstants.js';
import mount from 'utils/mount';
import Vue from 'vue';
export default function ScatterPlotViewProvider(openmct) {
function isCompactView(objectPath) {
@@ -44,42 +44,34 @@ export default function ScatterPlotViewProvider(openmct) {
},
view: function (domainObject, objectPath) {
let _destroy = null;
let component;
return {
show: function (element) {
const isCompact = isCompactView(objectPath);
const { destroy } = mount(
{
el: element,
components: {
ScatterPlotView
},
provide: {
openmct,
domainObject,
path: objectPath
},
data() {
return {
options: {
compact: isCompact
}
};
},
template: '<scatter-plot-view :options="options"></scatter-plot-view>'
let isCompact = isCompactView(objectPath);
component = new Vue({
el: element,
components: {
ScatterPlotView
},
{
app: openmct.app,
element
}
);
_destroy = destroy;
provide: {
openmct,
domainObject,
path: objectPath
},
data() {
return {
options: {
compact: isCompact
}
};
},
template: '<scatter-plot-view :options="options"></scatter-plot-view>'
});
},
destroy: function () {
if (_destroy) {
_destroy();
}
component.$destroy();
component = undefined;
}
};
}

View File

@@ -87,10 +87,7 @@ export default {
watch: {
data: {
immediate: false,
handler() {
this.updateData();
},
deep: true
handler: 'updateData'
}
},
mounted() {
@@ -109,7 +106,7 @@ export default {
this.$refs.plot.on('plotly_relayout', this.zoom);
},
beforeUnmount() {
beforeDestroy() {
if (this.$refs.plot && this.$refs.plot.off) {
this.$refs.plot.off('plotly_relayout', this.zoom);
}

Some files were not shown because too many files have changed in this diff Show More