mirror of
https://github.com/nasa/openmct.git
synced 2024-12-18 20:57:53 +00:00
Memory leak fixes for several views (#7057)
* Change the mount utility to use Vue's createApp and defineComponent methods * Fix display layout memory leaks caused by `getSelectionContext` * fix some display layout leaks due to use of slots * Fix imagery memory leak (removed span tag). NOTE: CompassRose svg leaks memory - must test on firefox to see if this is a Chrome leak. * Fix ActionsAPI action collection and applicable actions leak. * Fix flexible layout memory leaks - remove listeners on unmount. NOTE: One type of overlay plot (Rover Yaw) is still leaking. * pass in the el on mount * e2e test config and spec changes * Remove mounting of limit lines. Use components directly * test: remove `.only()` * Fix display layout memory leaks * Enable passing tests * e2e README and appActions should be what master has. * lint: add word to cspell list * lint: fixes * lint:fix * fix: revert `el` change * fix: remove empty span * fix: creating shapes in displayLayout * fix: avoid `splice` as it loses reactivity * test: reduce timeout time * quick fixes * add prod mode and convert the test config to select the correct mode * Fix webpack prod config * Add launch flag for exposing window.gc * never worked * explicit naming * rename * We don't need to destroy view providers * test: increase timeout time * test: unskip all mem tests * fix(vue-loader): disable static hoisting * chore: run `test:perf:memory` * Don't destroy view providers * Move context menu once listener to beforeUnmount instead. * Disconnect all resize observers on unmount * Delete Test vue component * Use beforeUnmount and remove splice(0) in favor of [] for emptying arrays * re-structure * fix: unregister listener in pane.vue * test: tweak timeouts * chore: lint:fix * test: unskip perf tests * fix: unregister events properly * fix: unregister listener * fix: unregister listener * fix: unregister listener * fix: use `unmounted()` * fix: unregister listeners * fix: unregister listener properly * chore: lint:fix * test: fix imagery layer toggle test * test: increase timeout * Don't use anonymous functions for listeners * Destroy objects and event listeners properly * Delete config stores that are created by components * Use the right unmount hook. Destroy mounted view on unmount. * Use unmounted, not beforeUnmounted * Lint fixes * Fix time strip memory leak * Undo unneeded change for memory leaks. * chore: combine common webpack configs --------- Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov> Co-authored-by: John Hill <john.c.hill@nasa.gov>
This commit is contained in:
parent
61e7050391
commit
b8949db767
@ -197,7 +197,9 @@ jobs:
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- run: npm run test:perf
|
||||
- run: npm run test:perf:memory
|
||||
- run: npm run test:perf:localhost
|
||||
- run: npm run test:perf:contract
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
|
@ -480,7 +480,10 @@
|
||||
"sinonjs",
|
||||
"generatedata",
|
||||
"grandsearch",
|
||||
"websockets"
|
||||
"websockets",
|
||||
"swgs",
|
||||
"memlab",
|
||||
"devmode"
|
||||
],
|
||||
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US"],
|
||||
"ignorePaths": [
|
||||
|
@ -33,6 +33,16 @@ const projectRootDir = path.resolve(__dirname, '..');
|
||||
/** @type {import('webpack').Configuration} */
|
||||
const config = {
|
||||
context: projectRootDir,
|
||||
devServer: {
|
||||
client: {
|
||||
progress: true,
|
||||
overlay: {
|
||||
// Disable overlay for runtime errors.
|
||||
// See: https://github.com/webpack/webpack-dev-server/issues/4771
|
||||
runtimeErrors: false
|
||||
}
|
||||
}
|
||||
},
|
||||
entry: {
|
||||
openmct: './openmct.js',
|
||||
generatorWorker: './example/generator/generatorWorker.js',
|
||||
@ -125,6 +135,7 @@ const config = {
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
compilerOptions: {
|
||||
hoistStatic: false,
|
||||
whitespace: 'preserve',
|
||||
compatConfig: {
|
||||
MODE: 2
|
||||
|
@ -45,14 +45,6 @@ module.exports = merge(common, {
|
||||
directory: path.join(__dirname, '..', '/dist'),
|
||||
publicPath: '/dist',
|
||||
watch: false
|
||||
},
|
||||
client: {
|
||||
progress: true,
|
||||
overlay: {
|
||||
// Disable overlay for runtime errors.
|
||||
// See: https://github.com/webpack/webpack-dev-server/issues/4771
|
||||
runtimeErrors: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -134,11 +134,11 @@ npm run test:e2e:updatesnapshots
|
||||
|
||||
## Performance Testing
|
||||
|
||||
The open source performance tests function mostly as a contract for the locator logic, functionality, and assumptions will work in our downstream, closed source test suites.
|
||||
The open source performance tests function in three ways which match their naming and folder structure:
|
||||
|
||||
They're found under `./e2e/tests/performance` and are to be executed with the following npm script:
|
||||
|
||||
`npm run test:perf`
|
||||
`./e2e/tests/performance` - The tests at the root of this folder path detect functional changes which are mostly apparent with large performance regressions like [this](https://github.com/nasa/openmct/issues/6879). These tests run against openmct webpack in `production-mode` with the `npm run test:perf:localhost` script.
|
||||
`./e2e/tests/performance/contract/` - These tests serve as [contracts](https://martinfowler.com/bliki/ContractTest.html) for the locator logic, functionality, and assumptions will work in our downstream, closed source test suites. These tests run against openmct webpack in `dev-mode` with the `npm run test:perf:contract` script.
|
||||
`./e2e/tests/performance/memory/` - These tests execute memory leak detection checks in various ways. This is expected to evolve as we move to the `memlab` project. These tests run against openmct webpack in `production-mode` with the `npm run test:perf:memory` script.
|
||||
|
||||
These tests are expected to become blocking and gating with assertions as we extend the capabilities of Playwright.
|
||||
|
||||
@ -158,8 +158,11 @@ Our file structure follows the type of type of testing being excercised at the e
|
||||
|`./tests/functional/example/` | Tests which specifically verify the example plugins (e.g.: Sine Wave Generator).|
|
||||
|`./tests/functional/plugins/` | Tests which loosely test each plugin. This folder is the most likely to change. Note: some `@snapshot` tests are still contained within this structure.|
|
||||
|`./tests/framework/` | Tests which verify that our testing framework's functionality and assumptions will continue to work based on further refactoring or Playwright version changes (e.g.: verifying custom fixtures and appActions).|
|
||||
|`./tests/performance/` | Performance tests.|
|
||||
|`./tests/performance/` | Performance tests which should be run on every commit.|
|
||||
|`./tests/performance/contract/` | A subset of performance tests which are designed to provide a contract between the open source tests which are run on every commit and the downstream tests which are run post merge and with other frameworks.|
|
||||
|`./tests/performance/memory` | A subset of performance tests which are designed to test for memory leaks.|
|
||||
|`./tests/visual/` | Visual tests.|
|
||||
|`./tests/visual/component/` | Visual tests which are only run against a single component.|
|
||||
|`./appActions.js` | Contains common methods which can be leveraged by test case authors to quickly move through the application when writing new tests.|
|
||||
|`./baseFixture.js` | Contains base fixtures which only extend default `@playwright/test` functionality. The expectation is that these fixtures will be removed as the native Playwright API improves|
|
||||
|
||||
@ -176,6 +179,7 @@ Open MCT is leveraging the [config file](https://playwright.dev/docs/test-config
|
||||
|`./playwright-ci.config.js` | Used when running in CI or to debug CI issues locally|
|
||||
|`./playwright-local.config.js` | Used when running locally|
|
||||
|`./playwright-performance.config.js` | Used when running performance tests in CI or locally|
|
||||
|`./playwright-performance-devmode.config.js` | Used when running performance tests in CI or locally|
|
||||
|`./playwright-visual.config.js` | Used to run the visual tests in CI or locally|
|
||||
|
||||
#### Test Tags
|
||||
|
@ -2,25 +2,24 @@
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
const CI = process.env.CI === 'true';
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 1, //Only for debugging purposes for trace: 'on-first-retry'
|
||||
testDir: 'tests/performance/',
|
||||
testMatch: '*.contract.perf.spec.js', //Run everything except contract tests which require marks in dev mode
|
||||
timeout: 60 * 1000,
|
||||
workers: 1, //Only run in serial with 1 worker
|
||||
webServer: {
|
||||
command: 'npm run start', //coverage not generated
|
||||
command: 'npm run start', //need development mode for performance.marks and others
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: !CI
|
||||
reuseExistingServer: false
|
||||
},
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: CI, //Only if running locally
|
||||
ignoreHTTPSErrors: true,
|
||||
headless: true,
|
||||
ignoreHTTPSErrors: false, //HTTP performance varies!
|
||||
screenshot: 'off',
|
||||
trace: 'on-first-retry',
|
||||
video: 'off'
|
||||
@ -28,6 +27,7 @@ const config = {
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome',
|
||||
testIgnore: '*.memory.perf.spec.js', //Do not run memory tests without proper flags. Shouldn't get here
|
||||
use: {
|
||||
browserName: 'chromium'
|
||||
}
|
60
e2e/playwright-performance-prod.config.js
Normal file
60
e2e/playwright-performance-prod.config.js
Normal file
@ -0,0 +1,60 @@
|
||||
/* eslint-disable no-undef */
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 0, //Only for debugging purposes for trace: 'on-first-retry'
|
||||
testDir: 'tests/performance/',
|
||||
testIgnore: '*.contract.perf.spec.js', //Run everything except contract tests which require marks in dev mode
|
||||
timeout: 60 * 1000,
|
||||
workers: 1, //Only run in serial with 1 worker
|
||||
webServer: {
|
||||
command: 'npm run start:prod', //Production mode
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: false //Must be run with this option to prevent dev mode
|
||||
},
|
||||
use: {
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: true,
|
||||
ignoreHTTPSErrors: false, //HTTP performance varies!
|
||||
screenshot: 'off',
|
||||
trace: 'on-first-retry',
|
||||
video: 'off'
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome-memory',
|
||||
testMatch: '*.memory.perf.spec.js', //Only run memory tests
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
launchOptions: {
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-notifications',
|
||||
'--use-fake-ui-for-media-stream',
|
||||
'--use-fake-device-for-media-stream',
|
||||
'--js-flags=--no-move-object-start --expose-gc',
|
||||
'--enable-precise-memory-info',
|
||||
'--display=:100'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'chrome',
|
||||
testIgnore: '*.memory.perf.spec.js', //Do not run memory tests without proper flags
|
||||
use: {
|
||||
browserName: 'chromium'
|
||||
}
|
||||
}
|
||||
],
|
||||
reporter: [
|
||||
['list'],
|
||||
['junit', { outputFile: '../test-results/results.xml' }],
|
||||
['json', { outputFile: '../test-results/results.json' }]
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = config;
|
1
e2e/test-data/memory-leak-detection.json
Normal file
1
e2e/test-data/memory-leak-detection.json
Normal file
File diff suppressed because one or more lines are too long
@ -1,121 +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 an initial example for memory leak testing using performance. This configuration and execution must
|
||||
be kept separate from the traditional performance measurements to avoid any "observer" effects associated with tracing
|
||||
or profiling playwright and/or the browser.
|
||||
|
||||
Based on a pattern identified in https://github.com/trentmwillis/devtools-protocol-demos/blob/master/testing-demos/memory-leak-by-heap.js
|
||||
and https://github.com/paulirish/automated-chrome-profiling/issues/3
|
||||
|
||||
Best path forward: https://github.com/cowchimp/headless-devtools/blob/master/src/Memory/example.js
|
||||
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
|
||||
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.describe.skip('Memory Performance tests', () => {
|
||||
test.beforeEach(async ({ page, browser }, testInfo) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click a:has-text("My Items")
|
||||
await page.locator('a:has-text("My Items")').click({
|
||||
button: 'right'
|
||||
});
|
||||
|
||||
// Click text=Import from JSON
|
||||
await page.locator('text=Import from JSON').click();
|
||||
|
||||
// Upload Performance Display Layout.json
|
||||
await page.setInputFiles('#fileElem', filePath);
|
||||
|
||||
// Click text=OK
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
await expect(
|
||||
page.locator('a:has-text("Performance Display Layout Display Layout")')
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// To to Search Available after Launch
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill Search input
|
||||
await page
|
||||
.locator('[aria-label="OpenMCT Search"] input[type="search"]')
|
||||
.fill('Performance Display Layout');
|
||||
//Search Result Appears and is clicked
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('a:has-text("Performance Display Layout")').first().click()
|
||||
]);
|
||||
|
||||
//Time to Example Imagery Frame loads within Display Layout
|
||||
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
|
||||
//Time to Example Imagery object loads
|
||||
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
|
||||
|
||||
const client = await page.context().newCDPSession(page);
|
||||
await client.send('HeapProfiler.enable');
|
||||
await client.send('HeapProfiler.startSampling');
|
||||
// await client.send('HeapProfiler.collectGarbage');
|
||||
await client.send('Performance.enable');
|
||||
|
||||
let performanceMetricsBefore = await client.send('Performance.getMetrics');
|
||||
console.log(performanceMetricsBefore.metrics);
|
||||
|
||||
//await client.send('Performance.disable');
|
||||
|
||||
//Open Large view
|
||||
await page.locator('button:has-text("Large View")').click();
|
||||
await client.send('HeapProfiler.takeHeapSnapshot');
|
||||
|
||||
//Time to Imagery Rendered in Large Frame
|
||||
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
|
||||
|
||||
//Time to Example Imagery object loads
|
||||
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
|
||||
|
||||
// Click Close Icon
|
||||
await page.locator('.c-click-icon').click();
|
||||
|
||||
//Time to Example Imagery Frame loads within Display Layout
|
||||
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' });
|
||||
//Time to Example Imagery object loads
|
||||
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' });
|
||||
|
||||
await client.send('HeapProfiler.collectGarbage');
|
||||
//await client.send('Performance.enable');
|
||||
|
||||
let performanceMetricsAfter = await client.send('Performance.getMetrics');
|
||||
console.log(performanceMetricsAfter.metrics);
|
||||
|
||||
//await client.send('Performance.disable');
|
||||
});
|
||||
});
|
299
e2e/tests/performance/memory/navigation.memory.perf.spec.js
Normal file
299
e2e/tests/performance/memory/navigation.memory.perf.spec.js
Normal file
@ -0,0 +1,299 @@
|
||||
/*****************************************************************************
|
||||
* 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 { test, expect } = require('@playwright/test');
|
||||
|
||||
const memoryLeakFilePath = 'e2e/test-data/memory-leak-detection.json';
|
||||
/**
|
||||
* Executes tests to verify that views are not leaking memory on navigation away. This sort of
|
||||
* memory leak is generally caused by a failure to clean up registered listeners.
|
||||
*
|
||||
* These tests are executed on a set of pre-built displays loaded from ../test-data/memory-leak-detection.json.
|
||||
*
|
||||
* In order to modify the test data set:
|
||||
* 1. Run Open MCT locally (npm start)
|
||||
* 2. Right click on a folder in the tree, and select "Import From JSON"
|
||||
* 3. In the subsequent dialog, select the file ../test-data/memory-leak-detection.json
|
||||
* 4. Click "OK"
|
||||
* 5. Modify test objects as desired
|
||||
* 6. Right click on the "Memory Leak Detection" folder, and select "Export to JSON"
|
||||
* 7. Copy the exported file to ../test-data/memory-leak-detection.json
|
||||
*
|
||||
*/
|
||||
|
||||
const NAV_LEAK_TIMEOUT = 10 * 1000; // 10s
|
||||
test.describe('Navigation memory leak is not detected in', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await page.locator('a:has-text("My Items")').click({
|
||||
button: 'right'
|
||||
});
|
||||
|
||||
await page.locator('text=Import from JSON').click();
|
||||
|
||||
// Upload memory-leak-detection.json
|
||||
await page.setInputFiles('#fileElem', memoryLeakFilePath);
|
||||
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
await expect(page.locator('a:has-text("Memory Leak Detection")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('plot view', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(page, 'overlay-plot-single-1hz-swg', {
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
});
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('stacked plot view', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(page, 'stacked-plot-single-1hz-swg', {
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
});
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('LAD table view', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-single-1hz-swg', {
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
});
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('LAD table set', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(page, 'lad-table-set-single-1hz-swg', {
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
});
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
//TODO: Figure out why using the `table-row` component inside the `table` component leaks TelemetryTableRow objects
|
||||
test('telemetry table view', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'telemetry-table-single-1hz-swg',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
}
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
//TODO: Figure out why using the `SideBar` component inside the leaks Notebook objects
|
||||
test('notebook view', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'notebook-memory-leak-detection-test',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
}
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('display layout of a single SWG alphanumeric', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'display-layout-single-1hz-swg',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
}
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('display layout of a single SWG plot', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'display-layout-single-overlay-plot',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
}
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
//TODO: Figure out why `svg` in the CompassRose component leaks imagery
|
||||
test('example imagery view', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'example-imagery-memory-leak-test',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
|
||||
}
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('display layout of example imagery views', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'display-layout-images-memory-leak-test',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
|
||||
}
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('display layout with plots of swgs, alphanumerics, and condition sets, ', async ({
|
||||
page
|
||||
}) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'display-layout-simple-telemetry',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
|
||||
}
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('flexible layout with plots of swgs', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'flexible-layout-plots-memory-leak-test',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT
|
||||
}
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('flexible layout of example imagery views', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'flexible-layout-images-memory-leak-test',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
|
||||
}
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('tabbed view of display layouts and time strips', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'tab-view-simple-memory-leak-test',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT * 6 * 2 // 2 min
|
||||
}
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('time strip view of telemetry', async ({ page }) => {
|
||||
const result = await navigateToObjectAndDetectMemoryLeak(
|
||||
page,
|
||||
'time-strip-telemetry-memory-leak-test',
|
||||
{
|
||||
timeout: NAV_LEAK_TIMEOUT * 6 // 1 min
|
||||
}
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {*} objectName
|
||||
* @returns
|
||||
*/
|
||||
async function navigateToObjectAndDetectMemoryLeak(page, objectName) {
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill Search input
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill(objectName);
|
||||
|
||||
//Search Result Appears and is clicked
|
||||
await Promise.all([
|
||||
page.locator(`div.c-gsearch-result__title:has-text("${objectName}")`).first().click(),
|
||||
page.waitForNavigation()
|
||||
]);
|
||||
|
||||
// Register a finalization listener on the root node for the view. This tends to be the last thing to be
|
||||
// garbage collected since it has either direct or indirect references to all resources used by the view. Therefore it's a pretty good proxy
|
||||
// for detecting memory leaks.
|
||||
await page.evaluate(() => {
|
||||
window.gcPromise = new Promise((resolve) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
window.fr = new FinalizationRegistry(resolve);
|
||||
window.fr.register(
|
||||
window.openmct.layout.$refs.browseObject.$refs.objectViewWrapper.firstChild,
|
||||
'navigatedObject',
|
||||
window.openmct.layout.$refs.browseObject.$refs.objectViewWrapper.firstChild
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Nav back to folder
|
||||
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
|
||||
await page.waitForNavigation();
|
||||
|
||||
// This next code block blocks until the finalization listener is called and the gcPromise resolved. This means that the root node for the view has been garbage collected.
|
||||
// In the event that the root node is not garbage collected, the gcPromise will never resolve and the test will time out.
|
||||
await page.evaluate(() => {
|
||||
const gcPromise = window.gcPromise;
|
||||
window.gcPromise = null;
|
||||
|
||||
// Manually invoke the garbage collector once all references are removed.
|
||||
window.gc();
|
||||
|
||||
return gcPromise;
|
||||
});
|
||||
|
||||
// Clean up the finalization registry since we don't need it any more.
|
||||
await page.evaluate(() => {
|
||||
window.fr = null;
|
||||
});
|
||||
|
||||
// If we get here without timing out, it means the garbage collection promise resolved and the test passed.
|
||||
return true;
|
||||
}
|
||||
});
|
@ -82,6 +82,7 @@
|
||||
"scripts": {
|
||||
"clean": "rm -rf ./dist ./node_modules ./package-lock.json ./coverage ./html-test-results ./test-results ./.nyc_output ",
|
||||
"start": "npx webpack serve --config ./.webpack/webpack.dev.js",
|
||||
"start:prod": "npx webpack serve --config ./.webpack/webpack.prod.js",
|
||||
"start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.js",
|
||||
"lint:js": "eslint example src e2e --ext .js openmct.js --max-warnings=0",
|
||||
"lint:vue": "eslint example src --ext .vue",
|
||||
@ -106,7 +107,9 @@
|
||||
"test:e2e:visual:full": "percy exec --config ./e2e/.percy.nightly.yml -- npx playwright test --config=e2e/playwright-visual.config.js --grep-invert @unstable",
|
||||
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js --grep-invert @couchdb",
|
||||
"test:e2e:watch": "npx playwright test --ui --config=e2e/playwright-ci.config.js",
|
||||
"test:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
|
||||
"test:perf:contract": "npx playwright test --config=e2e/playwright-performance-dev.config.js",
|
||||
"test:perf:localhost": "npx playwright test --config=e2e/playwright-performance-prod.config.js --project=chrome",
|
||||
"test:perf:memory": "npx playwright test --config=e2e/playwright-performance-prod.config.js --project=chrome-memory",
|
||||
"update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2023/gm' ./src/ui/layout/AboutDialog.vue",
|
||||
"update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2023/gm'",
|
||||
"cov:e2e:report": "nyc report --reporter=lcovonly --report-dir=./coverage/e2e",
|
||||
|
@ -93,7 +93,7 @@ class ActionsAPI extends EventEmitter {
|
||||
if (this._actionCollections.has(key)) {
|
||||
let actionCollection = this._actionCollections.get(key);
|
||||
actionCollection.off('destroy', this._updateCachedActionCollections);
|
||||
|
||||
delete actionCollection.applicableActions;
|
||||
this._actionCollections.delete(key);
|
||||
}
|
||||
}
|
||||
|
@ -186,9 +186,10 @@ export default class CompositionProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#publicAPI.objects.eventEmitter.on('mutation', this.#onMutation.bind(this));
|
||||
const onMutation = this.#onMutation.bind(this);
|
||||
this.#publicAPI.objects.eventEmitter.on('mutation', onMutation);
|
||||
this.topicListener = () => {
|
||||
this.#publicAPI.objects.eventEmitter.off('mutation', this.#onMutation.bind(this));
|
||||
this.#publicAPI.objects.eventEmitter.off('mutation', onMutation);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -98,7 +98,7 @@ export default {
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.plotResizeObserver) {
|
||||
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
|
||||
this.plotResizeObserver.disconnect();
|
||||
clearTimeout(this.resizeTimer);
|
||||
}
|
||||
|
||||
|
@ -115,7 +115,7 @@ export default {
|
||||
}
|
||||
|
||||
if (this.plotResizeObserver) {
|
||||
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
|
||||
this.plotResizeObserver.disconnect();
|
||||
clearTimeout(this.resizeTimer);
|
||||
}
|
||||
|
||||
|
@ -66,6 +66,9 @@ export default class Condition extends EventEmitter {
|
||||
|
||||
this.trigger = conditionConfiguration.configuration.trigger;
|
||||
this.summary = '';
|
||||
this.handleCriterionUpdated = this.handleCriterionUpdated.bind(this);
|
||||
this.handleOldTelemetryCriterion = this.handleOldTelemetryCriterion.bind(this);
|
||||
this.handleTelemetryStaleness = this.handleTelemetryStaleness.bind(this);
|
||||
}
|
||||
|
||||
updateResult(datum) {
|
||||
@ -196,15 +199,15 @@ export default class Condition extends EventEmitter {
|
||||
if (found) {
|
||||
const newCriterionConfiguration = this.generateCriterion(criterionConfiguration);
|
||||
let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct);
|
||||
newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
newCriterion.on('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
|
||||
newCriterion.on('telemetryStaleness', () => this.handleTelemetryStaleness());
|
||||
newCriterion.on('criterionUpdated', this.handleCriterionUpdated);
|
||||
newCriterion.on('telemetryIsOld', this.handleOldTelemetryCriterion);
|
||||
newCriterion.on('telemetryStaleness', this.handleTelemetryStaleness);
|
||||
|
||||
let criterion = found.item;
|
||||
criterion.unsubscribe();
|
||||
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
criterion.off('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
|
||||
newCriterion.off('telemetryStaleness', () => this.handleTelemetryStaleness());
|
||||
criterion.off('criterionUpdated', this.handleCriterionUpdated);
|
||||
criterion.off('telemetryIsOld', this.handleOldTelemetryCriterion);
|
||||
newCriterion.off('telemetryStaleness', this.handleTelemetryStaleness);
|
||||
this.criteria.splice(found.index, 1, newCriterion);
|
||||
}
|
||||
}
|
||||
@ -213,9 +216,9 @@ export default class Condition extends EventEmitter {
|
||||
let found = this.findCriterion(id);
|
||||
if (found) {
|
||||
let criterion = found.item;
|
||||
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
criterion.off('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
|
||||
criterion.off('telemetryStaleness', () => this.handleTelemetryStaleness());
|
||||
criterion.off('criterionUpdated', this.handleCriterionUpdated);
|
||||
criterion.off('telemetryIsOld', this.handleOldTelemetryCriterion);
|
||||
criterion.off('telemetryStaleness', this.handleTelemetryStaleness);
|
||||
criterion.destroy();
|
||||
this.criteria.splice(found.index, 1);
|
||||
|
||||
|
@ -128,6 +128,7 @@ export default {
|
||||
this.composition.off('remove', this.removeTelemetryObject);
|
||||
if (this.conditionManager) {
|
||||
this.conditionManager.off('conditionSetResultUpdated', this.handleConditionSetResultUpdated);
|
||||
this.conditionManager.off('noTelemetryObjects', this.emitNoTelemetryObjectEvent);
|
||||
this.conditionManager.destroy();
|
||||
}
|
||||
|
||||
|
@ -81,7 +81,7 @@ export default {
|
||||
this.listenToConditionSetChanges();
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
unmounted() {
|
||||
this.stopListeningToConditionSetChanges();
|
||||
},
|
||||
methods: {
|
||||
|
@ -170,7 +170,7 @@ define(['lodash'], function (_) {
|
||||
if (form) {
|
||||
showForm(form, name, selectionPath);
|
||||
} else {
|
||||
selectionPath[0].context.addElement(name);
|
||||
openmct.objectViews.emit('contextAction', 'addElement', name);
|
||||
}
|
||||
},
|
||||
key: 'add',
|
||||
@ -236,7 +236,6 @@ define(['lodash'], function (_) {
|
||||
icon: 'icon-trash',
|
||||
title: 'Delete the selected object',
|
||||
method: function () {
|
||||
let removeItem = selectionPath[1].context.removeItem;
|
||||
let prompt = openmct.overlays.dialog({
|
||||
iconClass: 'alert',
|
||||
message: `Warning! This action will remove this item from the Display Layout. Do you want to continue?`,
|
||||
@ -245,7 +244,11 @@ define(['lodash'], function (_) {
|
||||
label: 'OK',
|
||||
emphasis: 'true',
|
||||
callback: function () {
|
||||
removeItem(getAllTypes(selection));
|
||||
openmct.objectViews.emit(
|
||||
'contextAction',
|
||||
'removeItem',
|
||||
getAllTypes(selection)
|
||||
);
|
||||
prompt.dismiss();
|
||||
}
|
||||
},
|
||||
@ -290,7 +293,12 @@ define(['lodash'], function (_) {
|
||||
}
|
||||
],
|
||||
method: function (option) {
|
||||
selectionPath[1].context.orderItem(option.value, getAllTypes(selectedObjects));
|
||||
openmct.objectViews.emit(
|
||||
'contextAction',
|
||||
'orderItem',
|
||||
option.value,
|
||||
getAllTypes(selectedObjects)
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -474,9 +482,7 @@ define(['lodash'], function (_) {
|
||||
icon: 'icon-duplicate',
|
||||
title: 'Duplicate the selected object',
|
||||
method: function () {
|
||||
let duplicateItem = selectionPath[1].context.duplicateItem;
|
||||
|
||||
duplicateItem(selection);
|
||||
openmct.objectViews.emit('contextAction', 'duplicateItem', selection);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -555,6 +561,7 @@ define(['lodash'], function (_) {
|
||||
|
||||
function getViewSwitcherMenu(selectedParent, selectionPath, selection) {
|
||||
if (selection.length === 1) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let displayLayoutContext = selectionPath[1].context;
|
||||
let selectedItemContext = selectionPath[0].context;
|
||||
let selectedItemType = selectedItemContext.item.type;
|
||||
@ -574,14 +581,18 @@ define(['lodash'], function (_) {
|
||||
label: 'View type',
|
||||
options: viewOptions,
|
||||
method: function (option) {
|
||||
displayLayoutContext.switchViewType(selectedItemContext, option.value, selection);
|
||||
openmct.objectViews.emit(
|
||||
'contextAction',
|
||||
'switchViewType',
|
||||
selectedItemContext,
|
||||
option.value,
|
||||
selection
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
} else if (selection.length > 1) {
|
||||
if (areAllViews('telemetry-view', 'layoutItem.type', selection)) {
|
||||
let displayLayoutContext = selectionPath[1].context;
|
||||
|
||||
return {
|
||||
control: 'menu',
|
||||
domainObject: selectedParent,
|
||||
@ -590,12 +601,15 @@ define(['lodash'], function (_) {
|
||||
label: 'View type',
|
||||
options: APPLICABLE_VIEWS['telemetry-view-multi'],
|
||||
method: function (option) {
|
||||
displayLayoutContext.mergeMultipleTelemetryViews(selection, option.value);
|
||||
openmct.objectViews.emit(
|
||||
'contextAction',
|
||||
'mergeMultipleTelemetryViews',
|
||||
selection,
|
||||
option.value
|
||||
);
|
||||
}
|
||||
};
|
||||
} else if (areAllViews('telemetry.plot.overlay', 'item.type', selection)) {
|
||||
let displayLayoutContext = selectionPath[1].context;
|
||||
|
||||
return {
|
||||
control: 'menu',
|
||||
domainObject: selectedParent,
|
||||
@ -603,7 +617,12 @@ define(['lodash'], function (_) {
|
||||
title: 'Merge into a stacked plot',
|
||||
options: APPLICABLE_VIEWS['telemetry.plot.overlay-multi'],
|
||||
method: function (option) {
|
||||
displayLayoutContext.mergeMultipleOverlayPlots(selection, option.value);
|
||||
openmct.objectViews.emit(
|
||||
'contextAction',
|
||||
'mergeMultipleOverlayPlots',
|
||||
selection,
|
||||
option.value
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -627,7 +646,7 @@ define(['lodash'], function (_) {
|
||||
domainObject: displayLayoutContext.item,
|
||||
icon: ICON_GRID_SHOW,
|
||||
method: function () {
|
||||
displayLayoutContext.toggleGrid();
|
||||
openmct.objectViews.emit('contextAction', 'toggleGrid');
|
||||
|
||||
this.icon = this.icon === ICON_GRID_SHOW ? ICON_GRID_HIDE : ICON_GRID_SHOW;
|
||||
},
|
||||
@ -653,7 +672,7 @@ define(['lodash'], function (_) {
|
||||
|
||||
function showForm(formStructure, name, selectionPath) {
|
||||
openmct.forms.showForm(formStructure).then((changes) => {
|
||||
selectionPath[0].context.addElement(name, changes);
|
||||
openmct.objectViews.emit('contextAction', 'addElement', name, changes);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -25,14 +25,16 @@
|
||||
:item="item"
|
||||
:grid-size="gridSize"
|
||||
:is-editing="isEditing"
|
||||
@move="(gridDelta) => $emit('move', gridDelta)"
|
||||
@endMove="() => $emit('endMove')"
|
||||
@move="move"
|
||||
@endMove="endMove"
|
||||
>
|
||||
<div
|
||||
class="c-box-view u-style-receiver js-style-receiver"
|
||||
:class="[styleClass]"
|
||||
:style="style"
|
||||
></div>
|
||||
<template #content>
|
||||
<div
|
||||
class="c-box-view u-style-receiver js-style-receiver"
|
||||
:class="[styleClass]"
|
||||
:style="style"
|
||||
></div>
|
||||
</template>
|
||||
</layout-frame>
|
||||
</template>
|
||||
|
||||
@ -115,10 +117,18 @@ export default {
|
||||
this.initSelect
|
||||
);
|
||||
},
|
||||
unmounted() {
|
||||
beforeUnmount() {
|
||||
if (this.removeSelectable) {
|
||||
this.removeSelectable();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
move(gridDelta) {
|
||||
this.$emit('move', gridDelta);
|
||||
},
|
||||
endMove() {
|
||||
this.$emit('endMove');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -145,7 +145,7 @@ function getItemDefinition(itemType, ...options) {
|
||||
|
||||
export default {
|
||||
components: components,
|
||||
inject: ['openmct', 'objectPath', 'options', 'objectUtils', 'currentView'],
|
||||
inject: ['openmct', 'objectPath', 'options', 'currentView'],
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
@ -222,13 +222,21 @@ export default {
|
||||
this.composition.load();
|
||||
this.gridDimensions = [this.$el.offsetWidth, this.$el.scrollHeight];
|
||||
|
||||
this.openmct.objects.observe(this.domainObject, 'configuration.items', (items) => {
|
||||
this.layoutItems = items;
|
||||
});
|
||||
this.unObserveItems = this.openmct.objects.observe(
|
||||
this.domainObject,
|
||||
'configuration.items',
|
||||
(items) => {
|
||||
this.layoutItems = [...items];
|
||||
}
|
||||
);
|
||||
|
||||
this.watchDisplayResize();
|
||||
},
|
||||
unmounted() {
|
||||
beforeUnmount() {
|
||||
if (this.unObserveItems) {
|
||||
this.unObserveItems();
|
||||
}
|
||||
this.unwatchDisplayResize();
|
||||
this.openmct.selection.off('change', this.setSelection);
|
||||
this.composition.off('add', this.addChild);
|
||||
this.composition.off('remove', this.removeChild);
|
||||
@ -252,9 +260,15 @@ export default {
|
||||
this.$el.click();
|
||||
},
|
||||
watchDisplayResize() {
|
||||
const resizeObserver = new ResizeObserver(() => this.updateGrid());
|
||||
this.unwatchDisplayResize();
|
||||
this.resizeObserver = new ResizeObserver(this.updateGrid);
|
||||
|
||||
resizeObserver.observe(this.$el);
|
||||
this.resizeObserver.observe(this.$el);
|
||||
},
|
||||
unwatchDisplayResize() {
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect();
|
||||
}
|
||||
},
|
||||
addElement(itemType, element) {
|
||||
this.addItem(itemType + '-view', element);
|
||||
@ -624,7 +638,7 @@ export default {
|
||||
return this.openmct.objects.makeKeyString(item.identifier) !== keyString;
|
||||
}
|
||||
});
|
||||
this.layoutItems = layoutItems;
|
||||
this.layoutItems = [...layoutItems];
|
||||
this.mutate('configuration.items', layoutItems);
|
||||
this.clearSelection();
|
||||
},
|
||||
|
@ -25,14 +25,16 @@
|
||||
:item="item"
|
||||
:grid-size="gridSize"
|
||||
:is-editing="isEditing"
|
||||
@move="(gridDelta) => $emit('move', gridDelta)"
|
||||
@endMove="() => $emit('endMove')"
|
||||
@move="move"
|
||||
@endMove="endMove"
|
||||
>
|
||||
<div
|
||||
class="c-ellipse-view u-style-receiver js-style-receiver"
|
||||
:class="[styleClass]"
|
||||
:style="style"
|
||||
></div>
|
||||
<template #content>
|
||||
<div
|
||||
class="c-ellipse-view u-style-receiver js-style-receiver"
|
||||
:class="[styleClass]"
|
||||
:style="style"
|
||||
></div>
|
||||
</template>
|
||||
</layout-frame>
|
||||
</template>
|
||||
|
||||
@ -115,10 +117,18 @@ export default {
|
||||
this.initSelect
|
||||
);
|
||||
},
|
||||
unmounted() {
|
||||
beforeUnmount() {
|
||||
if (this.removeSelectable) {
|
||||
this.removeSelectable();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
move(gridDelta) {
|
||||
this.$emit('move', gridDelta);
|
||||
},
|
||||
endMove() {
|
||||
this.$emit('endMove');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -25,10 +25,12 @@
|
||||
:item="item"
|
||||
:grid-size="gridSize"
|
||||
:is-editing="isEditing"
|
||||
@move="(gridDelta) => $emit('move', gridDelta)"
|
||||
@endMove="() => $emit('endMove')"
|
||||
@move="move"
|
||||
@endMove="endMove"
|
||||
>
|
||||
<div class="c-image-view" :class="[styleClass]" :style="style"></div>
|
||||
<template #content>
|
||||
<div class="c-image-view" :style="style"></div>
|
||||
</template>
|
||||
</layout-frame>
|
||||
</template>
|
||||
|
||||
@ -118,10 +120,18 @@ export default {
|
||||
this.initSelect
|
||||
);
|
||||
},
|
||||
unmounted() {
|
||||
beforeUnmount() {
|
||||
if (this.removeSelectable) {
|
||||
this.removeSelectable();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
move(gridDelta) {
|
||||
this.$emit('move', gridDelta);
|
||||
},
|
||||
endMove() {
|
||||
this.$emit('endMove');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -29,7 +29,7 @@
|
||||
}"
|
||||
:style="style"
|
||||
>
|
||||
<slot></slot>
|
||||
<slot name="content"></slot>
|
||||
<div class="c-frame__move-bar" @mousedown.left="startMove($event)"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -20,24 +20,26 @@
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<template>
|
||||
<LayoutFrame
|
||||
<layout-frame
|
||||
:item="item"
|
||||
:grid-size="gridSize"
|
||||
:is-editing="isEditing"
|
||||
@move="(gridDelta) => $emit('move', gridDelta)"
|
||||
@endMove="() => $emit('endMove')"
|
||||
@move="move"
|
||||
@endMove="endMove"
|
||||
>
|
||||
<ObjectFrame
|
||||
v-if="domainObject"
|
||||
ref="objectFrame"
|
||||
:domain-object="domainObject"
|
||||
:object-path="currentObjectPath"
|
||||
:has-frame="item.hasFrame"
|
||||
:show-edit-view="false"
|
||||
:layout-font-size="item.fontSize"
|
||||
:layout-font="item.font"
|
||||
/>
|
||||
</LayoutFrame>
|
||||
<template #content>
|
||||
<ObjectFrame
|
||||
v-if="domainObject"
|
||||
ref="objectFrame"
|
||||
:domain-object="domainObject"
|
||||
:object-path="currentObjectPath"
|
||||
:has-frame="item.hasFrame"
|
||||
:show-edit-view="false"
|
||||
:layout-font-size="item.fontSize"
|
||||
:layout-font="item.font"
|
||||
/>
|
||||
</template>
|
||||
</layout-frame>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -104,8 +106,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
domainObject: undefined,
|
||||
currentObjectPath: [],
|
||||
mutablePromise: undefined
|
||||
currentObjectPath: []
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@ -168,6 +169,12 @@ export default {
|
||||
delete this.immediatelySelect;
|
||||
}
|
||||
});
|
||||
},
|
||||
move(gridDelta) {
|
||||
this.$emit('move', gridDelta);
|
||||
},
|
||||
endMove() {
|
||||
this.$emit('endMove');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -25,42 +25,44 @@
|
||||
:item="item"
|
||||
:grid-size="gridSize"
|
||||
:is-editing="isEditing"
|
||||
@move="(gridDelta) => $emit('move', gridDelta)"
|
||||
@endMove="() => $emit('endMove')"
|
||||
@move="move"
|
||||
@endMove="endMove"
|
||||
>
|
||||
<div
|
||||
v-if="domainObject"
|
||||
ref="telemetryViewWrapper"
|
||||
class="c-telemetry-view u-style-receiver"
|
||||
:class="[itemClasses]"
|
||||
:style="styleObject"
|
||||
:data-font-size="item.fontSize"
|
||||
:data-font="item.font"
|
||||
@contextmenu.prevent="showContextMenu"
|
||||
@mouseover.ctrl="showToolTip"
|
||||
@mouseleave="hideToolTip"
|
||||
>
|
||||
<div class="is-status__indicator" :title="`This item is ${status}`"></div>
|
||||
<div v-if="showLabel" class="c-telemetry-view__label">
|
||||
<div class="c-telemetry-view__label-text">
|
||||
{{ domainObject.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #content>
|
||||
<div
|
||||
v-if="showValue"
|
||||
:title="fieldName"
|
||||
class="c-telemetry-view__value"
|
||||
:class="[telemetryClass]"
|
||||
v-if="domainObject"
|
||||
ref="telemetryViewWrapper"
|
||||
class="c-telemetry-view u-style-receiver"
|
||||
:class="[itemClasses]"
|
||||
:style="styleObject"
|
||||
:data-font-size="item.fontSize"
|
||||
:data-font="item.font"
|
||||
@contextmenu.prevent="showContextMenu"
|
||||
@mouseover.ctrl="showToolTip"
|
||||
@mouseleave="hideToolTip"
|
||||
>
|
||||
<div class="c-telemetry-view__value-text">
|
||||
{{ telemetryValue }}
|
||||
<span v-if="unit && item.showUnits" class="c-telemetry-view__value-text__unit">
|
||||
{{ unit }}
|
||||
</span>
|
||||
<div class="is-status__indicator" :title="`This item is ${status}`"></div>
|
||||
<div v-if="showLabel" class="c-telemetry-view__label">
|
||||
<div class="c-telemetry-view__label-text">
|
||||
{{ domainObject.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="showValue"
|
||||
:title="fieldName"
|
||||
class="c-telemetry-view__value"
|
||||
:class="[telemetryClass]"
|
||||
>
|
||||
<div class="c-telemetry-view__value-text">
|
||||
{{ telemetryValue }}
|
||||
<span v-if="unit && item.showUnits" class="c-telemetry-view__value-text__unit">
|
||||
{{ unit }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</layout-frame>
|
||||
</template>
|
||||
|
||||
@ -388,6 +390,12 @@ export default {
|
||||
async showToolTip() {
|
||||
const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;
|
||||
this.buildToolTip(await this.getObjectPath(), BELOW, 'telemetryViewWrapper');
|
||||
},
|
||||
move(gridDelta) {
|
||||
this.$emit('move', gridDelta);
|
||||
},
|
||||
endMove() {
|
||||
this.$emit('endMove');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -25,18 +25,20 @@
|
||||
:item="item"
|
||||
:grid-size="gridSize"
|
||||
:is-editing="isEditing"
|
||||
@move="(gridDelta) => $emit('move', gridDelta)"
|
||||
@endMove="() => $emit('endMove')"
|
||||
@move="move"
|
||||
@endMove="endMove"
|
||||
>
|
||||
<div
|
||||
class="c-text-view u-style-receiver js-style-receiver"
|
||||
:data-font-size="item.fontSize"
|
||||
:data-font="item.font"
|
||||
:class="[styleClass]"
|
||||
:style="style"
|
||||
>
|
||||
<div class="c-text-view__text">{{ item.text }}</div>
|
||||
</div>
|
||||
<template #content>
|
||||
<div
|
||||
class="c-text-view u-style-receiver js-style-receiver"
|
||||
:data-font-size="item.fontSize"
|
||||
:data-font="item.font"
|
||||
:class="[styleClass]"
|
||||
:style="style"
|
||||
>
|
||||
<div class="c-text-view__text">{{ item.text }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</layout-frame>
|
||||
</template>
|
||||
|
||||
@ -127,10 +129,18 @@ export default {
|
||||
this.initSelect
|
||||
);
|
||||
},
|
||||
unmounted() {
|
||||
beforeUnmount() {
|
||||
if (this.removeSelectable) {
|
||||
this.removeSelectable();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
move(gridDelta) {
|
||||
this.$emit('move', gridDelta);
|
||||
},
|
||||
endMove() {
|
||||
this.$emit('endMove');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -20,7 +20,6 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import objectUtils from 'objectUtils';
|
||||
import mount from 'utils/mount';
|
||||
|
||||
import CopyToClipboardAction from './actions/CopyToClipboardAction';
|
||||
@ -38,7 +37,6 @@ class DisplayLayoutView {
|
||||
this.options = options;
|
||||
|
||||
this.component = null;
|
||||
this.app = null;
|
||||
}
|
||||
|
||||
show(container, isEditing) {
|
||||
@ -52,7 +50,6 @@ class DisplayLayoutView {
|
||||
openmct: this.openmct,
|
||||
objectPath: this.objectPath,
|
||||
options: this.options,
|
||||
objectUtils,
|
||||
currentView: this
|
||||
},
|
||||
data: () => {
|
||||
@ -84,20 +81,17 @@ class DisplayLayoutView {
|
||||
getSelectionContext() {
|
||||
return {
|
||||
item: this.domainObject,
|
||||
supportsMultiSelect: true,
|
||||
addElement: this.component && this.component.$refs.displayLayout.addElement,
|
||||
removeItem: this.component && this.component.$refs.displayLayout.removeItem,
|
||||
orderItem: this.component && this.component.$refs.displayLayout.orderItem,
|
||||
duplicateItem: this.component && this.component.$refs.displayLayout.duplicateItem,
|
||||
switchViewType: this.component && this.component.$refs.displayLayout.switchViewType,
|
||||
mergeMultipleTelemetryViews:
|
||||
this.component && this.component.$refs.displayLayout.mergeMultipleTelemetryViews,
|
||||
mergeMultipleOverlayPlots:
|
||||
this.component && this.component.$refs.displayLayout.mergeMultipleOverlayPlots,
|
||||
toggleGrid: this.component && this.component.$refs.displayLayout.toggleGrid
|
||||
supportsMultiSelect: true
|
||||
};
|
||||
}
|
||||
|
||||
contextAction() {
|
||||
const action = arguments[0];
|
||||
if (this.component && this.component.$refs.displayLayout[action]) {
|
||||
this.component.$refs.displayLayout[action](...Array.from(arguments).splice(1));
|
||||
}
|
||||
}
|
||||
|
||||
onEditModeChange(isEditing) {
|
||||
this.component.isEditing = isEditing;
|
||||
}
|
||||
@ -105,6 +99,7 @@ class DisplayLayoutView {
|
||||
destroy() {
|
||||
if (this._destroy) {
|
||||
this._destroy();
|
||||
this.component = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -122,7 +122,6 @@ export default {
|
||||
mounted() {
|
||||
let context = {
|
||||
item: this.$parent.domainObject,
|
||||
addContainer: this.addContainer,
|
||||
type: 'container',
|
||||
containerId: this.container.id
|
||||
};
|
||||
|
@ -164,16 +164,32 @@ export default {
|
||||
this.composition.on('remove', this.removeChildObject);
|
||||
this.composition.on('add', this.addFrame);
|
||||
this.composition.load();
|
||||
this.openmct.objects.observe(this.domainObject, 'configuration.containers', (containers) => {
|
||||
this.containers = containers;
|
||||
});
|
||||
this.openmct.objects.observe(this.domainObject, 'configuration.rowsLayout', (rowsLayout) => {
|
||||
this.rowsLayout = rowsLayout;
|
||||
});
|
||||
this.unObserveContainers = this.openmct.objects.observe(
|
||||
this.domainObject,
|
||||
'configuration.containers',
|
||||
(containers) => {
|
||||
this.containers = containers;
|
||||
}
|
||||
);
|
||||
this.unObserveRowsLayout = this.openmct.objects.observe(
|
||||
this.domainObject,
|
||||
'configuration.rowsLayout',
|
||||
(rowsLayout) => {
|
||||
this.rowsLayout = rowsLayout;
|
||||
}
|
||||
);
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.composition.off('remove', this.removeChildObject);
|
||||
this.composition.off('add', this.addFrame);
|
||||
|
||||
if (this.unObserveContainers) {
|
||||
this.unObserveContainers();
|
||||
}
|
||||
|
||||
if (this.unObserveRowsLayout) {
|
||||
this.unObserveRowsLayout();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
containsObject(identifier) {
|
||||
|
@ -142,6 +142,9 @@ export default {
|
||||
childContext.item = this.domainObject;
|
||||
childContext.type = 'frame';
|
||||
childContext.frameId = this.frame.id;
|
||||
if (this.unsubscribeSelection) {
|
||||
this.unsubscribeSelection();
|
||||
}
|
||||
this.unsubscribeSelection = this.openmct.selection.selectable(
|
||||
this.$refs.frame,
|
||||
childContext,
|
||||
|
@ -56,7 +56,7 @@ export default {
|
||||
document.addEventListener('dragend', this.unsetDragging);
|
||||
document.addEventListener('drop', this.unsetDragging);
|
||||
},
|
||||
unmounted() {
|
||||
beforeUnmount() {
|
||||
document.removeEventListener('dragstart', this.setDragging);
|
||||
document.removeEventListener('dragend', this.unsetDragging);
|
||||
document.removeEventListener('drop', this.unsetDragging);
|
||||
|
@ -78,12 +78,15 @@ export default class FlexibleLayoutViewProvider {
|
||||
getSelectionContext() {
|
||||
return {
|
||||
item: domainObject,
|
||||
addContainer: component.$refs.flexibleLayout.addContainer,
|
||||
deleteContainer: component.$refs.flexibleLayout.deleteContainer,
|
||||
deleteFrame: component.$refs.flexibleLayout.deleteFrame,
|
||||
type: 'flexible-layout'
|
||||
};
|
||||
},
|
||||
contextAction() {
|
||||
const action = arguments[0];
|
||||
if (component && component.$refs.flexibleLayout[action]) {
|
||||
component.$refs.flexibleLayout[action](...Array.from(arguments).splice(1));
|
||||
}
|
||||
},
|
||||
onEditModeChange(isEditing) {
|
||||
component.isEditing = isEditing;
|
||||
},
|
||||
|
@ -89,8 +89,6 @@ function ToolbarProvider(openmct) {
|
||||
control: 'button',
|
||||
domainObject: primary.context.item,
|
||||
method: function () {
|
||||
let deleteFrameAction = tertiary.context.deleteFrame;
|
||||
|
||||
let prompt = openmct.overlays.dialog({
|
||||
iconClass: 'alert',
|
||||
message: `This action will remove this frame from this Flexible Layout. Do you want to continue?`,
|
||||
@ -99,7 +97,11 @@ function ToolbarProvider(openmct) {
|
||||
label: 'OK',
|
||||
emphasis: 'true',
|
||||
callback: function () {
|
||||
deleteFrameAction(primary.context.frameId);
|
||||
openmct.objectViews.emit(
|
||||
'contextAction',
|
||||
'deleteFrame',
|
||||
primary.context.frameId
|
||||
);
|
||||
prompt.dismiss();
|
||||
}
|
||||
},
|
||||
@ -136,7 +138,9 @@ function ToolbarProvider(openmct) {
|
||||
addContainer = {
|
||||
control: 'button',
|
||||
domainObject: tertiary.context.item,
|
||||
method: tertiary.context.addContainer,
|
||||
method: function () {
|
||||
openmct.objectViews.emit('contextAction', 'addContainer', ...arguments);
|
||||
},
|
||||
key: 'add',
|
||||
icon: 'icon-plus-in-rect',
|
||||
title: 'Add Container'
|
||||
@ -152,7 +156,6 @@ function ToolbarProvider(openmct) {
|
||||
control: 'button',
|
||||
domainObject: primary.context.item,
|
||||
method: function () {
|
||||
let removeContainer = secondary.context.deleteContainer;
|
||||
let containerId = primary.context.containerId;
|
||||
|
||||
let prompt = openmct.overlays.dialog({
|
||||
@ -164,7 +167,7 @@ function ToolbarProvider(openmct) {
|
||||
label: 'OK',
|
||||
emphasis: 'true',
|
||||
callback: function () {
|
||||
removeContainer(containerId);
|
||||
openmct.objectViews.emit('contextAction', 'deleteContainer', containerId);
|
||||
prompt.dismiss();
|
||||
}
|
||||
},
|
||||
@ -185,7 +188,9 @@ function ToolbarProvider(openmct) {
|
||||
addContainer = {
|
||||
control: 'button',
|
||||
domainObject: secondary.context.item,
|
||||
method: secondary.context.addContainer,
|
||||
method: function () {
|
||||
openmct.objectViews.emit('contextAction', 'addContainer', ...arguments);
|
||||
},
|
||||
key: 'add',
|
||||
icon: 'icon-plus-in-rect',
|
||||
title: 'Add Container'
|
||||
@ -198,7 +203,9 @@ function ToolbarProvider(openmct) {
|
||||
addContainer = {
|
||||
control: 'button',
|
||||
domainObject: primary.context.item,
|
||||
method: primary.context.addContainer,
|
||||
method: function () {
|
||||
openmct.objectViews.emit('contextAction', 'addContainer', ...arguments);
|
||||
},
|
||||
key: 'add',
|
||||
icon: 'icon-plus-in-rect',
|
||||
title: 'Add Container'
|
||||
|
@ -727,7 +727,8 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
this.stopListening(this.focusedImageWrapper, 'wheel', this.wheelZoom, this);
|
||||
// remove all eventListeners
|
||||
this.stopListening();
|
||||
|
||||
Object.keys(this.imageryAnnotations).forEach((time) => {
|
||||
const imageAnnotationsForTime = this.imageryAnnotations[time];
|
||||
@ -1276,6 +1277,9 @@ export default {
|
||||
this.scrollHandler();
|
||||
},
|
||||
setSizedImageDimensions() {
|
||||
if (!this.$refs.focusedImage) {
|
||||
return;
|
||||
}
|
||||
this.focusedImageNaturalAspectRatio =
|
||||
this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight;
|
||||
if (
|
||||
|
@ -27,9 +27,7 @@
|
||||
:class="iconClass"
|
||||
:title="title"
|
||||
@click="toggleMenu"
|
||||
>
|
||||
<span class="c-button__label"></span>
|
||||
</button>
|
||||
/>
|
||||
<div v-show="showMenu" class="c-switcher-menu__content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
@ -44,7 +44,7 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<select class="c-inspector__data-pivot-range-selector" v-model="selectedDataRangeIndex">
|
||||
<select v-model="selectedDataRangeIndex" class="c-inspector__data-pivot-range-selector">
|
||||
<option
|
||||
v-for="(dataRange, index) in descendingDataRanges"
|
||||
:key="index"
|
||||
@ -78,8 +78,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import NumericData from './NumericData.vue';
|
||||
import Imagery from './Imagery.vue';
|
||||
import NumericData from './NumericData.vue';
|
||||
|
||||
const TIMESTAMP_VIEW_BUFFER = 30 * 1000;
|
||||
const timestampBufferText = `${TIMESTAMP_VIEW_BUFFER / 1000} seconds`;
|
||||
|
@ -34,8 +34,9 @@
|
||||
</template>
|
||||
<script>
|
||||
import mount from 'utils/mount';
|
||||
import TelemetryFrame from './TelemetryFrame.vue';
|
||||
|
||||
import Plot from '../plot/Plot.vue';
|
||||
import TelemetryFrame from './TelemetryFrame.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject', 'timeFormatter'],
|
||||
|
@ -43,6 +43,10 @@ export default {
|
||||
},
|
||||
unmounted() {
|
||||
this.openmct.selection.off('change', this.updateSelection);
|
||||
if (this.destroy) {
|
||||
this.destroy();
|
||||
this.$el.innerHTML = '';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateSelection(selection) {
|
||||
|
@ -56,7 +56,7 @@ export default {
|
||||
mounted() {
|
||||
this.openmct.editor.on('isEditing', this.setEditMode);
|
||||
},
|
||||
beforeUnmounted() {
|
||||
beforeUnmount() {
|
||||
this.openmct.editor.off('isEditing', this.setEditMode);
|
||||
},
|
||||
methods: {
|
||||
|
@ -292,7 +292,7 @@ export default {
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.embedsWrapperResizeObserver) {
|
||||
this.embedsWrapperResizeObserver.unobserve(this.$refs.embedsWrapper);
|
||||
this.embedsWrapperResizeObserver.disconnect();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -394,11 +394,7 @@ export default {
|
||||
);
|
||||
|
||||
this.openmct.objectViews.on('clearData', this.clearData);
|
||||
this.$on('loadingComplete', () => {
|
||||
if (this.annotationViewingAndEditingAllowed) {
|
||||
this.loadAnnotations();
|
||||
}
|
||||
});
|
||||
this.$on('loadingComplete', this.loadAnnotationsIfAllowed);
|
||||
this.openmct.selection.on('change', this.updateSelection);
|
||||
this.yAxisListWithRange = [this.config.yAxis, ...this.config.additionalYAxes];
|
||||
|
||||
@ -413,6 +409,7 @@ export default {
|
||||
document.removeEventListener('keydown', this.handleKeyDown);
|
||||
document.removeEventListener('keyup', this.handleKeyUp);
|
||||
document.body.removeEventListener('click', this.cancelSelection);
|
||||
this.$off('loadingComplete', this.loadAnnotationsIfAllowed);
|
||||
this.destroy();
|
||||
},
|
||||
methods: {
|
||||
@ -467,9 +464,7 @@ export default {
|
||||
const currentXaxis = this.config.xAxis.get('displayRange');
|
||||
const currentYaxis = this.config.yAxis.get('displayRange');
|
||||
if (!currentXaxis || !currentYaxis) {
|
||||
this.$once('loadingComplete', () => {
|
||||
resolve();
|
||||
});
|
||||
this.$once('loadingComplete', resolve);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
@ -565,31 +560,10 @@ export default {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
this.updateAxisUsageCount(yAxisId, 1);
|
||||
this.seriesModels[index] = series;
|
||||
this.listenTo(
|
||||
series,
|
||||
'change:xKey',
|
||||
(xKey) => {
|
||||
this.setDisplayRange(series, xKey);
|
||||
},
|
||||
this
|
||||
);
|
||||
this.listenTo(
|
||||
series,
|
||||
'change:yKey',
|
||||
() => {
|
||||
this.loadSeriesData(series);
|
||||
},
|
||||
this
|
||||
);
|
||||
this.listenTo(series, 'change:xKey', this.setDisplayRange.bind(this, series), this);
|
||||
this.listenTo(series, 'change:yKey', this.loadSeriesData.bind(this, series), this);
|
||||
|
||||
this.listenTo(
|
||||
series,
|
||||
'change:interpolate',
|
||||
() => {
|
||||
this.loadSeriesData(series);
|
||||
},
|
||||
this
|
||||
);
|
||||
this.listenTo(series, 'change:interpolate', this.loadSeriesData.bind(this, series), this);
|
||||
this.listenTo(series, 'change:yAxisId', this.updateTicksAndSeriesForYAxis, this);
|
||||
|
||||
this.loadSeriesData(series);
|
||||
@ -621,6 +595,11 @@ export default {
|
||||
foundYAxis.seriesCount = foundYAxis.seriesCount + updateCountBy;
|
||||
}
|
||||
},
|
||||
loadAnnotationsIfAllowed() {
|
||||
if (this.annotationViewingAndEditingAllowed) {
|
||||
this.loadAnnotations();
|
||||
}
|
||||
},
|
||||
async loadAnnotations() {
|
||||
if (!this.openmct.annotation.getAvailableTags().length) {
|
||||
// don't bother loading annotations if there are no tags
|
||||
@ -992,9 +971,6 @@ export default {
|
||||
|
||||
this.config.yAxisLabel = this.config.yAxis.get('label');
|
||||
|
||||
this.cursorGuideVertical = this.$refs.cursorGuideVertical;
|
||||
this.cursorGuideHorizontal = this.$refs.cursorGuideHorizontal;
|
||||
|
||||
this.listenTo(this.config.xAxis, 'change:displayRange', this.onXAxisChange, this);
|
||||
this.yAxisListWithRange.forEach((yAxis) => {
|
||||
this.listenTo(yAxis, 'change:displayRange', this.onYAxisChange.bind(this, yAxis.id), this);
|
||||
@ -1122,8 +1098,8 @@ export default {
|
||||
},
|
||||
|
||||
updateCrosshairs(event) {
|
||||
this.cursorGuideVertical.style.left = event.clientX - this.chartElementBounds.x + 'px';
|
||||
this.cursorGuideHorizontal.style.top = event.clientY - this.chartElementBounds.y + 'px';
|
||||
this.$refs.cursorGuideVertical.style.left = event.clientX - this.chartElementBounds.x + 'px';
|
||||
this.$refs.cursorGuideHorizontal.style.top = event.clientY - this.chartElementBounds.y + 'px';
|
||||
},
|
||||
|
||||
trackChartElementBounds(event) {
|
||||
@ -1904,6 +1880,10 @@ export default {
|
||||
configStore.deleteStore(this.config.id);
|
||||
}
|
||||
|
||||
this.config = {};
|
||||
this.canvas = undefined;
|
||||
this.abortController = undefined;
|
||||
|
||||
this.stopListening();
|
||||
|
||||
if (this.checkForSize) {
|
||||
|
@ -179,7 +179,7 @@ export default {
|
||||
this.stalenessSubscription = {};
|
||||
this.loadComposition();
|
||||
},
|
||||
beforeUnmount() {
|
||||
unmounted() {
|
||||
this.destroy();
|
||||
},
|
||||
methods: {
|
||||
@ -259,6 +259,7 @@ export default {
|
||||
this.compositionCollection.off('remove', this.removeItem);
|
||||
}
|
||||
|
||||
this.imageExporter = null;
|
||||
this.stopListening();
|
||||
},
|
||||
exportJPG() {
|
||||
|
@ -26,7 +26,20 @@
|
||||
<div class="gl-plot-chart-area">
|
||||
<span v-html="canvasTemplate"></span>
|
||||
<span v-html="canvasTemplate"></span>
|
||||
<div ref="limitArea" class="js-limit-area"></div>
|
||||
<div ref="limitArea" class="js-limit-area">
|
||||
<limit-label
|
||||
v-for="(limitLabel, index) in visibleLimitLabels"
|
||||
:key="index"
|
||||
:point="limitLabel.point"
|
||||
:limit="limitLabel.limit"
|
||||
></limit-label>
|
||||
<limit-line
|
||||
v-for="(limitLine, index) in visibleLimitLines"
|
||||
:key="index"
|
||||
:point="limitLine.point"
|
||||
:limit="limitLine.limit"
|
||||
></limit-line>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -87,6 +100,7 @@ const HANDLED_ATTRIBUTES = {
|
||||
};
|
||||
|
||||
export default {
|
||||
components: { LimitLine, LimitLabel },
|
||||
inject: ['openmct', 'domainObject', 'path'],
|
||||
props: {
|
||||
rectangles: {
|
||||
@ -133,7 +147,9 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
canvasTemplate:
|
||||
'<canvas style="position: absolute; background: none; width: 100%; height: 100%;"></canvas>'
|
||||
'<canvas style="position: absolute; background: none; width: 100%; height: 100%;"></canvas>',
|
||||
visibleLimitLabels: [],
|
||||
visibleLimitLines: []
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@ -398,6 +414,8 @@ export default {
|
||||
this.stopListening();
|
||||
this.lines.forEach((line) => line.destroy());
|
||||
this.limitLines.forEach((line) => line.destroy());
|
||||
this.pointSets.forEach((pointSet) => pointSet.destroy());
|
||||
this.alarmSets.forEach((alarmSet) => alarmSet.destroy());
|
||||
DrawLoader.releaseDrawAPI(this.drawAPI);
|
||||
},
|
||||
resetYOffsetAndSeriesDataForYAxis(yAxisId) {
|
||||
@ -706,7 +724,6 @@ export default {
|
||||
//console.timeEnd('📈 drawSeries');
|
||||
},
|
||||
updateLimitLines() {
|
||||
Array.from(this.$refs.limitArea.children).forEach((el) => el.remove());
|
||||
this.config.series.models.forEach((series) => {
|
||||
const yAxisId = series.get('yAxisId');
|
||||
|
||||
@ -727,8 +744,11 @@ export default {
|
||||
}
|
||||
|
||||
let limitPointOverlap = [];
|
||||
//reset
|
||||
this.visibleLimitLabels = [];
|
||||
this.visibleLimitLines = [];
|
||||
|
||||
this.limitLines.forEach((limitLine) => {
|
||||
let limitContainerEl = this.$refs.limitArea;
|
||||
limitLine.limits.forEach((limit) => {
|
||||
if (series.keyString !== limit.seriesKey) {
|
||||
return;
|
||||
@ -738,31 +758,43 @@ export default {
|
||||
if (showLabels) {
|
||||
const overlap = this.getLimitOverlap(limit, limitPointOverlap);
|
||||
limitPointOverlap.push(overlap);
|
||||
let limitLabelEl = this.getLimitLabel(limit, overlap);
|
||||
limitContainerEl.appendChild(limitLabelEl);
|
||||
this.visibleLimitLabels.push(this.getLimitProps(limit, overlap));
|
||||
}
|
||||
|
||||
let limitEl = this.getLimitElement(limit);
|
||||
limitContainerEl.appendChild(limitEl);
|
||||
this.visibleLimitLines.push(this.getLimitElementProps(limit));
|
||||
}, this);
|
||||
});
|
||||
},
|
||||
showLabels(seriesKey) {
|
||||
return this.showLimitLineLabels?.seriesKey === seriesKey;
|
||||
},
|
||||
getLimitElementProps(limit) {
|
||||
let point = {
|
||||
left: 0,
|
||||
top: this.drawAPI.y(limit.point.y)
|
||||
};
|
||||
|
||||
return {
|
||||
point,
|
||||
limit
|
||||
};
|
||||
},
|
||||
getLimitElement(limit) {
|
||||
let point = {
|
||||
left: 0,
|
||||
top: this.drawAPI.y(limit.point.y)
|
||||
};
|
||||
const { vNode } = mount(LimitLine, {
|
||||
const { vNode, destroy } = mount(LimitLine, {
|
||||
props: {
|
||||
point,
|
||||
limit
|
||||
}
|
||||
});
|
||||
|
||||
return vNode.el;
|
||||
return {
|
||||
el: vNode.el,
|
||||
destroy
|
||||
};
|
||||
},
|
||||
getLimitOverlap(limit, overlapMap) {
|
||||
//calculate if limit lines are too close to each other
|
||||
@ -793,19 +825,32 @@ export default {
|
||||
overlapTop: limitTop
|
||||
};
|
||||
},
|
||||
getLimitProps(limit, overlap) {
|
||||
let point = {
|
||||
left: 0,
|
||||
top: this.drawAPI.y(limit.point.y)
|
||||
};
|
||||
return {
|
||||
limit: Object.assign({}, overlap, limit),
|
||||
point
|
||||
};
|
||||
},
|
||||
getLimitLabel(limit, overlap) {
|
||||
let point = {
|
||||
left: 0,
|
||||
top: this.drawAPI.y(limit.point.y)
|
||||
};
|
||||
const { vNode } = mount(LimitLabel, {
|
||||
const { vNode, destroy } = mount(LimitLabel, {
|
||||
props: {
|
||||
limit: Object.assign({}, overlap, limit),
|
||||
point
|
||||
}
|
||||
});
|
||||
|
||||
return vNode.el;
|
||||
return {
|
||||
el: vNode.el,
|
||||
destroy
|
||||
};
|
||||
},
|
||||
drawAlarmPoints(alarmSet) {
|
||||
this.drawAPI.drawLimitPoints(
|
||||
|
@ -59,4 +59,8 @@ export default class LegendModel extends Model {
|
||||
showLegendsForChildren: true
|
||||
};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.stopListening();
|
||||
}
|
||||
}
|
||||
|
@ -168,8 +168,10 @@ export default class PlotConfigurationModel extends Model {
|
||||
onDestroy() {
|
||||
this.xAxis.destroy();
|
||||
this.yAxis.destroy();
|
||||
this.additionalYAxes.forEach((additionalYAxis) => additionalYAxis.destroy());
|
||||
this.series.destroy();
|
||||
this.legend.destroy();
|
||||
this.stopListening();
|
||||
if (this.removeMutationListener) {
|
||||
this.removeMutationListener();
|
||||
}
|
||||
|
@ -135,7 +135,9 @@ export default class PlotSeries extends Model {
|
||||
* @override
|
||||
*/
|
||||
destroy() {
|
||||
//this triggers Model.destroy which in turn triggers destroy methods for other classes.
|
||||
super.destroy();
|
||||
this.stopListening();
|
||||
this.openmct.time.off('bounds', this.updateLimits);
|
||||
|
||||
if (this.unsubscribe) {
|
||||
@ -149,6 +151,8 @@ export default class PlotSeries extends Model {
|
||||
if (this.removeMutationListener) {
|
||||
this.removeMutationListener();
|
||||
}
|
||||
|
||||
configStore.deleteStore(this.dataStoreId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -187,6 +187,12 @@ export default class SeriesCollection extends Collection {
|
||||
);
|
||||
})[0];
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
this.plot = undefined;
|
||||
this.stopListening();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -111,6 +111,11 @@ export default class XAxisModel extends Model {
|
||||
|
||||
return defaultModel;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.plot = undefined;
|
||||
this.stopListening();
|
||||
}
|
||||
}
|
||||
|
||||
/** @typedef {any} TODO */
|
||||
|
@ -384,6 +384,11 @@ export default class YAxisModel extends Model {
|
||||
range: options.model?.range
|
||||
};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.plot = undefined;
|
||||
this.stopListening();
|
||||
}
|
||||
}
|
||||
|
||||
/** @typedef {any} TODO */
|
||||
|
@ -152,6 +152,8 @@ DrawWebGL.prototype.initContext = function () {
|
||||
};
|
||||
|
||||
DrawWebGL.prototype.destroy = function () {
|
||||
this.canvas = undefined;
|
||||
this.overlay = undefined;
|
||||
this.stopListening();
|
||||
};
|
||||
|
||||
|
@ -212,6 +212,8 @@ export default {
|
||||
this.composition.off('reorder', this.compositionReorder);
|
||||
|
||||
this.stopListening();
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
configStore.deleteStore(configId);
|
||||
},
|
||||
|
||||
addChild(child) {
|
||||
|
@ -135,6 +135,9 @@ export default {
|
||||
this.removeSelectable();
|
||||
}
|
||||
|
||||
const configId = this.openmct.objects.makeKeyString(this.childObject.identifier);
|
||||
configStore.deleteStore(configId);
|
||||
|
||||
if (this._destroy) {
|
||||
this._destroy();
|
||||
}
|
||||
|
@ -1,3 +1,24 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
define([
|
||||
'../res/conditionTemplate.html',
|
||||
'./input/ObjectSelect',
|
||||
@ -42,9 +63,6 @@ define([
|
||||
this.selects = {};
|
||||
this.valueInputs = [];
|
||||
|
||||
this.remove = this.remove.bind(this);
|
||||
this.duplicate = this.duplicate.bind(this);
|
||||
|
||||
const self = this;
|
||||
|
||||
/**
|
||||
@ -65,6 +83,9 @@ define([
|
||||
});
|
||||
}
|
||||
|
||||
this.handleObjectChange = (value) => onSelectChange(value, 'object');
|
||||
this.handleKeyChange = (value) => onSelectChange(value, 'key');
|
||||
|
||||
/**
|
||||
* Event handler for this conditions value inputs
|
||||
* @param {Event} event The oninput event that triggered this callback
|
||||
@ -99,12 +120,8 @@ define([
|
||||
}
|
||||
);
|
||||
|
||||
this.selects.object.on('change', function (value) {
|
||||
onSelectChange(value, 'object');
|
||||
});
|
||||
this.selects.key.on('change', function (value) {
|
||||
onSelectChange(value, 'key');
|
||||
});
|
||||
this.selects.object.on('change', this.handleObjectChange);
|
||||
this.selects.key.on('change', this.handleKeyChange);
|
||||
|
||||
Object.values(this.selects).forEach(function (select) {
|
||||
self.domElement.querySelector('.t-configuration').append(select.getDOM());
|
||||
@ -143,6 +160,8 @@ define([
|
||||
* remove callbacks
|
||||
*/
|
||||
Condition.prototype.remove = function () {
|
||||
this.selects.object.off('change', this.handleObjectChange);
|
||||
this.selects.key.off('change', this.handleKeyChange);
|
||||
this.eventEmitter.emit('remove', this.index);
|
||||
this.destroy();
|
||||
};
|
||||
|
@ -61,6 +61,20 @@ define([
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Unregister a callback from this select.
|
||||
* @param {string} event The key for the event to stop listening to
|
||||
* @param {function} callback The function to unregister
|
||||
* @param {Object} context A reference to a scope to use as the context for the callback function
|
||||
*/
|
||||
Select.prototype.off = function (event, callback, context) {
|
||||
if (this.supportedCallbacks.includes(event)) {
|
||||
this.eventEmitter.off(event, callback, context || this);
|
||||
} else {
|
||||
throw new Error('Unsupported event type: ' + event);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the select element in the view from the current state of the data
|
||||
* model
|
||||
|
@ -161,7 +161,8 @@ export default {
|
||||
this.updateInternalDomainObject
|
||||
);
|
||||
|
||||
this.openmct.router.on('change:params', this.updateCurrentTab.bind(this));
|
||||
this.updateCurrentTab = this.updateCurrentTab.bind(this);
|
||||
this.openmct.router.on('change:params', this.updateCurrentTab);
|
||||
|
||||
this.RemoveAction = new RemoveAction(this.openmct);
|
||||
document.addEventListener('dragstart', this.dragstart);
|
||||
@ -184,7 +185,7 @@ export default {
|
||||
this.unsubscribe();
|
||||
this.clearCurrentTabIndexFromURL();
|
||||
|
||||
this.openmct.router.off('change:params', this.updateCurrentTab.bind(this));
|
||||
this.openmct.router.off('change:params', this.updateCurrentTab);
|
||||
|
||||
document.removeEventListener('dragstart', this.dragstart);
|
||||
document.removeEventListener('dragend', this.dragend);
|
||||
|
@ -90,6 +90,10 @@ export default {
|
||||
this.removeSelectable();
|
||||
}
|
||||
|
||||
if (this.removeStatusListener) {
|
||||
this.removeStatusListener();
|
||||
}
|
||||
|
||||
if (this.mutablePromise) {
|
||||
this.mutablePromise.then(() => {
|
||||
this.openmct.objects.destroyMutable(this.domainObject);
|
||||
|
@ -209,13 +209,13 @@ export default {
|
||||
|
||||
this.actionCollection = actionCollection;
|
||||
this.actionCollection.on('update', this.updateActionItems);
|
||||
this.updateActionItems(this.actionCollection.applicableActions);
|
||||
this.updateActionItems();
|
||||
},
|
||||
unlistenToActionCollection() {
|
||||
this.actionCollection.off('update', this.updateActionItems);
|
||||
delete this.actionCollection;
|
||||
},
|
||||
updateActionItems(actionItems) {
|
||||
updateActionItems() {
|
||||
const statusBarItems = this.actionCollection.getStatusBarActions();
|
||||
this.statusBarItems = this.openmct.menus.actionsToMenuItems(
|
||||
statusBarItems,
|
||||
|
@ -91,7 +91,7 @@ export default {
|
||||
return classes;
|
||||
}
|
||||
},
|
||||
unmounted() {
|
||||
beforeUnmount() {
|
||||
this.clear();
|
||||
if (this.releaseEditModeHandler) {
|
||||
this.releaseEditModeHandler();
|
||||
@ -114,6 +114,13 @@ export default {
|
||||
this.actionCollection.destroy();
|
||||
delete this.actionCollection;
|
||||
}
|
||||
this.$refs.objectViewWrapper.removeEventListener('dragover', this.onDragOver, {
|
||||
capture: true
|
||||
});
|
||||
this.$refs.objectViewWrapper.removeEventListener('drop', this.editIfEditable, {
|
||||
capture: true
|
||||
});
|
||||
this.$refs.objectViewWrapper.removeEventListener('drop', this.addObjectToParent);
|
||||
},
|
||||
created() {
|
||||
this.debounceUpdateView = _.debounce(this.updateView, 10);
|
||||
@ -137,6 +144,7 @@ export default {
|
||||
clear() {
|
||||
if (this.currentView) {
|
||||
this.currentView.destroy();
|
||||
|
||||
if (this.$refs.objectViewWrapper) {
|
||||
this.$refs.objectViewWrapper.innerHTML = '';
|
||||
}
|
||||
@ -168,6 +176,7 @@ export default {
|
||||
this.triggerUnsubscribeFromStaleness();
|
||||
|
||||
this.openmct.objectViews.off('clearData', this.clearData);
|
||||
this.openmct.objectViews.off('contextAction', this.performContextAction);
|
||||
},
|
||||
getStyleReceiver() {
|
||||
let styleReceiver;
|
||||
@ -288,6 +297,7 @@ export default {
|
||||
}
|
||||
|
||||
this.openmct.objectViews.on('clearData', this.clearData);
|
||||
this.openmct.objectViews.on('contextAction', this.performContextAction);
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.updateStyle(this.styleRuleManager?.currentStyle);
|
||||
@ -310,10 +320,6 @@ export default {
|
||||
show(object, viewKey, immediatelySelect, currentObjectPath) {
|
||||
this.updateStyle();
|
||||
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
|
||||
if (this.removeSelectable) {
|
||||
this.removeSelectable();
|
||||
delete this.removeSelectable;
|
||||
@ -462,6 +468,11 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
performContextAction() {
|
||||
if (this.currentView.contextAction) {
|
||||
this.currentView.contextAction(...arguments);
|
||||
}
|
||||
},
|
||||
isEditingAllowed() {
|
||||
let browseObject = this.openmct.layout.$refs.browseObject.domainObject;
|
||||
let objectPath = this.currentObjectPath || this.objectPath;
|
||||
|
@ -114,8 +114,9 @@ export default {
|
||||
this.styleProp = this.type === 'horizontal' ? 'width' : 'height';
|
||||
},
|
||||
created() {
|
||||
this.handleHideUrl = this.handleHideUrl.bind(this);
|
||||
// Hide tree and/or inspector pane if specified in URL
|
||||
this.openmct.router.on('change:params', this.handleHideUrl.bind(this));
|
||||
this.openmct.router.on('change:params', this.handleHideUrl);
|
||||
},
|
||||
async mounted() {
|
||||
if (this.persistPosition) {
|
||||
@ -130,6 +131,9 @@ export default {
|
||||
this.handleHideUrl();
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.openmct.router.off('change:params', this.handleHideUrl);
|
||||
},
|
||||
methods: {
|
||||
addHideParam(target) {
|
||||
this.openmct.router.setSearchParam(target, 'true');
|
||||
|
@ -16,6 +16,7 @@ export default {
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.unobserveObjects = {};
|
||||
//TODO: touch support
|
||||
this.$nextTick(() => {
|
||||
this.$refs.root.addEventListener('contextmenu', this.showContextMenu);
|
||||
@ -29,19 +30,24 @@ export default {
|
||||
|
||||
this.objectPath.forEach((object) => {
|
||||
if (object) {
|
||||
const unobserve = this.openmct.objects.observe(
|
||||
const key = this.openmct.objects.makeKeyString(object.identifier);
|
||||
this.unobserveObjects[key] = this.openmct.objects.observe(
|
||||
object,
|
||||
'*',
|
||||
updateObject.bind(this, object)
|
||||
);
|
||||
this.$once('hook:unmounted', unobserve);
|
||||
}
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.removeListeners();
|
||||
this.$refs.root.removeEventListener('contextMenu', this.showContextMenu);
|
||||
},
|
||||
methods: {
|
||||
removeListeners() {
|
||||
Object.values(this.unobserveObjects).forEach((unobserve) => unobserve());
|
||||
this.unobserveObjects = {};
|
||||
},
|
||||
showContextMenu(event) {
|
||||
if (this.readOnly) {
|
||||
return;
|
||||
|
@ -89,7 +89,7 @@ export default {
|
||||
// for edit mode changes and update toolbars if necessary.
|
||||
this.openmct.editor.on('isEditing', this.handleEditing);
|
||||
},
|
||||
unmounted() {
|
||||
beforeUnmount() {
|
||||
this.openmct.selection.off('change', this.handleSelection);
|
||||
this.openmct.editor.off('isEditing', this.handleEditing);
|
||||
this.removeListeners();
|
||||
|
@ -1,26 +1,25 @@
|
||||
import { h, render } from 'vue';
|
||||
import { createApp, defineComponent } from 'vue';
|
||||
|
||||
export default function mount(component, { props, children, element, app } = {}) {
|
||||
export default function mount(component, { props, children, element } = {}) {
|
||||
let el = element;
|
||||
|
||||
let vNode = h(component, props, children);
|
||||
if (app && app._context) {
|
||||
vNode.appContext = app._context;
|
||||
}
|
||||
if (el) {
|
||||
render(vNode, el);
|
||||
} else if (typeof document !== 'undefined') {
|
||||
render(vNode, (el = document.createElement('div')));
|
||||
if (!el) {
|
||||
el = document.createElement('div');
|
||||
}
|
||||
let vueComponent = defineComponent(component);
|
||||
let app = createApp(vueComponent);
|
||||
let mountedComponentInstance = app.mount(el);
|
||||
|
||||
// eslint-disable-next-line func-style
|
||||
const destroy = () => {
|
||||
if (el) {
|
||||
render(null, el);
|
||||
}
|
||||
el = null;
|
||||
vNode = null;
|
||||
app.unmount();
|
||||
};
|
||||
|
||||
return { vNode, destroy, el };
|
||||
return {
|
||||
vNode: {
|
||||
componentInstance: mountedComponentInstance,
|
||||
el: mountedComponentInstance.$el
|
||||
},
|
||||
destroy,
|
||||
el
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user