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:
Shefali Joshi 2023-09-20 10:34:05 -07:00 committed by GitHub
parent 61e7050391
commit b8949db767
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 887 additions and 393 deletions

View File

@ -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:

View File

@ -480,7 +480,10 @@
"sinonjs",
"generatedata",
"grandsearch",
"websockets"
"websockets",
"swgs",
"memlab",
"devmode"
],
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US"],
"ignorePaths": [

View File

@ -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

View File

@ -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
}
}
}
});

View File

@ -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

View File

@ -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'
}

View 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;

File diff suppressed because one or more lines are too long

View File

@ -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');
});
});

View 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;
}
});

View File

@ -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",

View File

@ -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);
}
}

View File

@ -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);
};
}

View File

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

View File

@ -115,7 +115,7 @@ export default {
}
if (this.plotResizeObserver) {
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
this.plotResizeObserver.disconnect();
clearTimeout(this.resizeTimer);
}

View File

@ -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);

View File

@ -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();
}

View File

@ -81,7 +81,7 @@ export default {
this.listenToConditionSetChanges();
}
},
beforeUnmount() {
unmounted() {
this.stopListeningToConditionSetChanges();
},
methods: {

View File

@ -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);
});
}

View File

@ -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>

View File

@ -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();
},

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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');
}
}
};

View File

@ -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');
}
}
};

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -122,7 +122,6 @@ export default {
mounted() {
let context = {
item: this.$parent.domainObject,
addContainer: this.addContainer,
type: 'container',
containerId: this.container.id
};

View File

@ -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) {

View File

@ -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,

View File

@ -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);

View File

@ -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;
},

View File

@ -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'

View File

@ -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 (

View File

@ -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>

View File

@ -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`;

View File

@ -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'],

View File

@ -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) {

View File

@ -56,7 +56,7 @@ export default {
mounted() {
this.openmct.editor.on('isEditing', this.setEditMode);
},
beforeUnmounted() {
beforeUnmount() {
this.openmct.editor.off('isEditing', this.setEditMode);
},
methods: {

View File

@ -292,7 +292,7 @@ export default {
},
beforeUnmount() {
if (this.embedsWrapperResizeObserver) {
this.embedsWrapperResizeObserver.unobserve(this.$refs.embedsWrapper);
this.embedsWrapperResizeObserver.disconnect();
}
},
methods: {

View File

@ -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) {

View File

@ -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() {

View File

@ -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(

View File

@ -59,4 +59,8 @@ export default class LegendModel extends Model {
showLegendsForChildren: true
};
}
destroy() {
this.stopListening();
}
}

View File

@ -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();
}

View File

@ -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);
}
/**

View File

@ -187,6 +187,12 @@ export default class SeriesCollection extends Collection {
);
})[0];
}
destroy() {
super.destroy();
this.plot = undefined;
this.stopListening();
}
}
/**

View File

@ -111,6 +111,11 @@ export default class XAxisModel extends Model {
return defaultModel;
}
destroy() {
this.plot = undefined;
this.stopListening();
}
}
/** @typedef {any} TODO */

View File

@ -384,6 +384,11 @@ export default class YAxisModel extends Model {
range: options.model?.range
};
}
destroy() {
this.plot = undefined;
this.stopListening();
}
}
/** @typedef {any} TODO */

View File

@ -152,6 +152,8 @@ DrawWebGL.prototype.initContext = function () {
};
DrawWebGL.prototype.destroy = function () {
this.canvas = undefined;
this.overlay = undefined;
this.stopListening();
};

View File

@ -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) {

View File

@ -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();
}

View File

@ -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();
};

View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -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,

View File

@ -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;

View File

@ -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');

View File

@ -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;

View File

@ -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();

View File

@ -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
};
}