Compare commits
	
		
			21 Commits
		
	
	
		
			fix-tree-n
			...
			mct6555-v2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 15bab81447 | ||
|   | cf8db0a21d | ||
|   | bda652b3bb | ||
|   | e7f2f4b5f6 | ||
|   | 18b5ee1340 | ||
|   | 9006c13eb4 | ||
|   | d7b16a5a2c | ||
|   | a76701c23a | ||
|   | bb52291ab6 | ||
|   | 87db357b5a | ||
|   | 587c27464b | ||
|   | e0bad2620e | ||
|   | 0222b77941 | ||
|   | e05d812219 | ||
|   | d51498752f | ||
|   | a3b1fa34e4 | ||
|   | 598ebddd29 | ||
|   | 010e86bf83 | ||
|   | e8ed10db78 | ||
|   | fd20d392c2 | ||
|   | 841c999d16 | 
| @@ -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: | ||||
|   | ||||
| @@ -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', | ||||
|   | ||||
							
								
								
									
										38
									
								
								.github/workflows/e2e-couchdb.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										38
									
								
								.github/workflows/e2e-couchdb.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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}`); | ||||
|             } | ||||
|   | ||||
							
								
								
									
										59
									
								
								.github/workflows/e2e-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										59
									
								
								.github/workflows/e2e-pr.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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}`); | ||||
|             } | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/npm-prerelease.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/npm-prerelease.yml
									
									
									
									
										vendored
									
									
								
							| @@ -14,7 +14,7 @@ jobs: | ||||
|       - uses: actions/checkout@v3 | ||||
|       - uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: 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 | ||||
|   | ||||
							
								
								
									
										51
									
								
								.github/workflows/pr-platform.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										51
									
								
								.github/workflows/pr-platform.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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}`); | ||||
|             } | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| { | ||||
|   "trailingComma": "none", | ||||
|   "singleQuote": true, | ||||
|   "printWidth": 100, | ||||
|   "endOfLine": "auto" | ||||
|   "printWidth": 100 | ||||
| } | ||||
|   | ||||
| @@ -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$/, | ||||
|   | ||||
| @@ -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/"' | ||||
|   | ||||
| @@ -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
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								API.md
									
									
									
									
									
								
							| @@ -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`. | ||||
|  | ||||
|   | ||||
| @@ -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(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -77,6 +77,7 @@ const config = { | ||||
|       } | ||||
|     ], | ||||
|     ['junit', { outputFile: '../test-results/results.xml' }], | ||||
|     ['github'], | ||||
|     ['@deploysentinel/playwright'] | ||||
|   ] | ||||
| }; | ||||
|   | ||||
| @@ -26,7 +26,7 @@ | ||||
|  * and appActions. These fixtures should be generalized across all plugins. | ||||
|  */ | ||||
|  | ||||
| const { test, expect, 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. | ||||
|   | ||||
| @@ -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\"]" | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -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": "[]" | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -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' }); | ||||
|   | ||||
| @@ -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([ | ||||
|   | ||||
| @@ -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 | ||||
|   }) => { | ||||
|   | ||||
| @@ -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(); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -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(); | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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); | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
|   // }); | ||||
| }); | ||||
| @@ -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); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -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) => { | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -24,7 +24,7 @@ function SimpleVuePlugin() { | ||||
|             container.appendChild(vm.$mount().$el); | ||||
|           }, | ||||
|           destroy: function (container) { | ||||
|             //vm.$destroy(); | ||||
|             vm.$destroy(); | ||||
|           } | ||||
|         }; | ||||
|       } | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										29
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								package.json
									
									
									
									
									
								
							| @@ -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" | ||||
|   | ||||
							
								
								
									
										46
									
								
								src/MCT.js
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								src/MCT.js
									
									
									
									
									
								
							| @@ -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 () { | ||||
|   | ||||
| @@ -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); | ||||
|     } | ||||
|   | ||||
| @@ -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(); | ||||
|       } | ||||
|     }; | ||||
|   } | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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); | ||||
|     }, | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -166,7 +166,7 @@ export default { | ||||
|       this.options = this.model.options; | ||||
|     } | ||||
|   }, | ||||
|   unmounted() { | ||||
|   destroyed() { | ||||
|     document.body.removeEventListener('click', this.handleOutsideClick); | ||||
|   }, | ||||
|   methods: { | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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 | ||||
|     }; | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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` | ||||
|       }; | ||||
|     } | ||||
|   } | ||||
| }; | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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}` | ||||
|           ); | ||||
|         }); | ||||
|   | ||||
| @@ -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(); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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 | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -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) | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|   | ||||
| @@ -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); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|   | ||||
| @@ -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]; | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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', () => { | ||||
|   | ||||
| @@ -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; | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| }; | ||||
| @@ -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; | ||||
| @@ -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; | ||||
| @@ -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> | ||||
| @@ -1,10 +0,0 @@ | ||||
| .c-tooltip-wrapper { | ||||
|   max-width: 200px; | ||||
|   height: auto; | ||||
|   width: auto; | ||||
|   padding: $interiorMargin; | ||||
| } | ||||
|  | ||||
| .c-tooltip { | ||||
|   font-style: italic; | ||||
| } | ||||
| @@ -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; | ||||
| @@ -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; | ||||
							
								
								
									
										52
									
								
								src/api/user/RoleChannel.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/api/user/RoleChannel.js
									
									
									
									
									
										Normal 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; | ||||
|  | ||||
| @@ -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(); | ||||
| 
 | ||||
| @@ -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. | ||||
|  */ | ||||
|  */ | ||||
| @@ -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 | ||||
|      */ | ||||
| } | ||||
|   | ||||
| @@ -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" | ||||
|  */ | ||||
|  */ | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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'; | ||||
|   | ||||
| @@ -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() { | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
| @@ -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; | ||||
|           } | ||||
|         } | ||||
|       }; | ||||
|   | ||||
| @@ -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; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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'); | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -40,6 +40,7 @@ describe('The URLTimeSettingsSynchronizer', () => { | ||||
|   }); | ||||
|  | ||||
|   afterEach(() => { | ||||
|     openmct.time.stopClock(); | ||||
|     openmct.router.removeListener('change:hash', resolveFunction); | ||||
|  | ||||
|     appHolder = undefined; | ||||
|   | ||||
| @@ -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; | ||||
| }); | ||||
|   | ||||
| @@ -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; | ||||
| }); | ||||
|   | ||||
| @@ -93,7 +93,7 @@ export default { | ||||
|     }); | ||||
|     this.registerListeners(); | ||||
|   }, | ||||
|   beforeUnmount() { | ||||
|   beforeDestroy() { | ||||
|     if (this.plotResizeObserver) { | ||||
|       this.plotResizeObserver.unobserve(this.$refs.plotWrapper); | ||||
|       clearTimeout(this.resizeTimer); | ||||
|   | ||||
| @@ -83,7 +83,7 @@ export default { | ||||
|       this.refreshData | ||||
|     ); | ||||
|   }, | ||||
|   beforeUnmount() { | ||||
|   beforeDestroy() { | ||||
|     this.stopFollowingTimeContext(); | ||||
|  | ||||
|     this.removeAllSubscriptions(); | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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; | ||||
|           } | ||||
|         } | ||||
|       }; | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -115,7 +115,7 @@ export default { | ||||
|       this.initColorAndName | ||||
|     ); | ||||
|   }, | ||||
|   beforeUnmount() { | ||||
|   beforeDestroy() { | ||||
|     if (this.removeBarStylesListener) { | ||||
|       this.removeBarStylesListener(); | ||||
|     } | ||||
|   | ||||
| @@ -77,7 +77,7 @@ export default { | ||||
|       this.reloadTelemetry | ||||
|     ); | ||||
|   }, | ||||
|   beforeUnmount() { | ||||
|   beforeDestroy() { | ||||
|     this.stopFollowingTimeContext(); | ||||
|  | ||||
|     if (!this.composition) { | ||||
|   | ||||
| @@ -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; | ||||
|         } | ||||
|       }; | ||||
|     } | ||||
|   | ||||
| @@ -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
		Reference in New Issue
	
	Block a user