Compare commits
34 Commits
v3.0.2
...
dave/inspe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80ee7d3cf6 | ||
|
|
2c7294bb3c | ||
|
|
42b13c4dfb | ||
|
|
351800b32a | ||
|
|
6db390a71a | ||
|
|
9ece4e55dc | ||
|
|
0a497483f2 | ||
|
|
a52577e729 | ||
|
|
a495e86231 | ||
|
|
bada228b8f | ||
|
|
3f80b53ea6 | ||
|
|
99a3e3fc32 | ||
|
|
2d92223e16 | ||
|
|
f21685e216 | ||
|
|
6c92e31036 | ||
|
|
82b1760b0e | ||
|
|
87feb0db34 | ||
|
|
c53073b339 | ||
|
|
57743e5918 | ||
|
|
f3b819a786 | ||
|
|
50694f600c | ||
|
|
10f3e13e4d | ||
|
|
9be9c5e28e | ||
|
|
58aeac94ac | ||
|
|
1e3097f54b | ||
|
|
6a9ff91d93 | ||
|
|
accfbc96ab | ||
|
|
9942bbbc0f | ||
|
|
4287cd5413 | ||
|
|
ee6ca11558 | ||
|
|
676bb81eab | ||
|
|
c6305697c0 | ||
|
|
0421936874 | ||
|
|
95e686038d |
@@ -94,6 +94,7 @@ jobs:
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- run: npm run lint
|
||||
- run: npm run lint:spelling
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
unit-test:
|
||||
parameters:
|
||||
@@ -197,9 +198,7 @@ jobs:
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- run: npm run test:perf:memory
|
||||
- run: npm run test:perf:localhost
|
||||
- run: npm run test:perf:contract
|
||||
- run: npm run test:perf
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
|
||||
@@ -479,11 +479,7 @@
|
||||
"mediump",
|
||||
"sinonjs",
|
||||
"generatedata",
|
||||
"grandsearch",
|
||||
"websockets",
|
||||
"swgs",
|
||||
"memlab",
|
||||
"devmode"
|
||||
"grandsearch"
|
||||
],
|
||||
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US"],
|
||||
"ignorePaths": [
|
||||
|
||||
23
.github/release.yml
vendored
23
.github/release.yml
vendored
@@ -1,23 +0,0 @@
|
||||
changelog:
|
||||
categories:
|
||||
- title: 🏕 Features
|
||||
labels:
|
||||
- type:feature
|
||||
- title: 🎉 Enhancements
|
||||
labels:
|
||||
- type:enhancement
|
||||
exclude:
|
||||
labels:
|
||||
- type:feature
|
||||
- title: 🔧 Maintenance
|
||||
labels:
|
||||
- type:maintenance
|
||||
- title: ⚡ Performance
|
||||
labels:
|
||||
- performance
|
||||
- title: 👒 Dependencies
|
||||
labels:
|
||||
- dependencies
|
||||
- title: 🐛 Bug Fixes
|
||||
labels:
|
||||
- '*'
|
||||
3
.github/workflows/e2e-couchdb.yml
vendored
3
.github/workflows/e2e-couchdb.yml
vendored
@@ -31,7 +31,8 @@ jobs:
|
||||
- run: npm install --cache ~/.npm --no-audit --progress=false
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v2
|
||||
continue-on-error: true
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
5
.npmrc
5
.npmrc
@@ -1,4 +1,7 @@
|
||||
loglevel=warn
|
||||
|
||||
#Prevent folks from ignoring an important error when building from source
|
||||
engine-strict=true
|
||||
engine-strict=true
|
||||
|
||||
# Dont include lockfile
|
||||
package-lock=false
|
||||
@@ -33,16 +33,6 @@ 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',
|
||||
@@ -110,12 +100,6 @@ const config = {
|
||||
new MiniCssExtractPlugin({
|
||||
filename: '[name].css',
|
||||
chunkFilename: '[name].css'
|
||||
}),
|
||||
// Add a UTF-8 BOM to CSS output to avoid random mojibake
|
||||
new webpack.BannerPlugin({
|
||||
test: /.*Theme\.css$/,
|
||||
raw: true,
|
||||
banner: '@charset "UTF-8";',
|
||||
})
|
||||
],
|
||||
module: {
|
||||
@@ -141,7 +125,6 @@ const config = {
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
compilerOptions: {
|
||||
hoistStatic: false,
|
||||
whitespace: 'preserve',
|
||||
compatConfig: {
|
||||
MODE: 2
|
||||
|
||||
@@ -45,6 +45,14 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
11
README.md
11
README.md
@@ -18,11 +18,12 @@ Building and running Open MCT in your local dev environment is very easy. Be sur
|
||||
|
||||
`git clone https://github.com/nasa/openmct.git`
|
||||
|
||||
2. Install development dependencies. Note: Check the package.json engine for our tested and supported node versions.
|
||||
2. (Optionally) Install the correct node version using [nvm](https://github.com/nvm-sh/nvm) (`nvm install`)
|
||||
3. Install development dependencies. Note: Check the package.json engine for our tested and supported node versions.
|
||||
|
||||
`npm install`
|
||||
|
||||
3. Run a local development server
|
||||
4. Run a local development server
|
||||
|
||||
`npm start`
|
||||
|
||||
@@ -51,6 +52,8 @@ For more on developing with Open MCT, see our documentation for a guide on [Deve
|
||||
|
||||
This is a fast moving project and we do our best to test and support the widest possible range of browsers, operating systems, and nodejs APIs. We have a published list of support available in our package.json's `browserslist` key.
|
||||
|
||||
The project uses `nvm` to ensure the node and npm version used, is coherent in all projects. Install nvm (non-windows), [here](https://github.com/nvm-sh/nvm) or the windows equivalent [here](https://github.com/coreybutler/nvm-windows)
|
||||
|
||||
If you encounter an issue with a particular browser, OS, or nodejs API, please file a [GitHub issue](https://github.com/nasa/openmct/issues/new/choose)
|
||||
|
||||
## Plugins
|
||||
@@ -95,10 +98,10 @@ To run the performance tests:
|
||||
|
||||
`npm run test:perf`
|
||||
|
||||
The test suite is configured to all tests localed in `e2e/tests/` ending in `*.e2e.spec.js`. For more about the e2e test suite, please see the [README](./e2e/README.md)
|
||||
The test suite is configured to all tests located in `e2e/tests/` ending in `*.e2e.spec.js`. For more about the e2e test suite, please see the [README](./e2e/README.md)
|
||||
|
||||
### Security Tests
|
||||
Each commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/). The list of CWE coverage items is avaiable in the [CodeQL docs](https://codeql.github.com/codeql-query-help/javascript-cwe/). The CodeQL workflow is specified in the [CodeQL analysis file](./.github/workflows/codeql-analysis.yml) and the custom [CodeQL config](./.github/codeql/codeql-config.yml).
|
||||
Each commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/). The list of CWE coverage items is available in the [CodeQL docs](https://codeql.github.com/codeql-query-help/javascript-cwe/). The CodeQL workflow is specified in the [CodeQL analysis file](./.github/workflows/codeql-analysis.yml) and the custom [CodeQL config](./.github/codeql/codeql-config.yml).
|
||||
|
||||
### Test Reporting and Code Coverage
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ requirements.
|
||||
|
||||
Additionally, the following project-specific standards will be used:
|
||||
|
||||
* During development, a "-SNAPSHOT" suffix shall be appended to the
|
||||
* During development, a "-next" suffix shall be appended to the
|
||||
version number. The version number before the suffix shall reflect
|
||||
the next expected version number for release.
|
||||
* Prior to a 1.0.0 release, the _minor_ version will be incremented
|
||||
@@ -93,7 +93,7 @@ numbers by the following process:
|
||||
|
||||
1. Update version number in `package.json`
|
||||
1. Checkout branch created for the last sprint that has been successfully tested.
|
||||
2. Remove a `-SNAPSHOT` suffix from the version in `package.json`.
|
||||
2. Remove a `-next` suffix from the version in `package.json`.
|
||||
3. Verify that resulting version number meets semantic versioning
|
||||
requirements relative to previous stable version. Increment the
|
||||
version number if necessary.
|
||||
@@ -138,7 +138,7 @@ numbers by the following process:
|
||||
1. Create a new branch off the `master` branch.
|
||||
2. Remove any suffix from the version number,
|
||||
or increment the _patch_ version if there is no suffix.
|
||||
3. Append a `-SNAPSHOT` suffix.
|
||||
3. Append a `-next` suffix.
|
||||
4. Commit changes to `package.json` on the `master` branch.
|
||||
The commit message should reference the sprint being opened,
|
||||
preferably by a URL reference to the associated Milestone in
|
||||
@@ -150,6 +150,6 @@ numbers by the following process:
|
||||
Projects dependent on Open MCT being co-developed by the Open MCT
|
||||
team should follow a similar process, except that they should
|
||||
additionally update their dependency on Open MCT to point to the
|
||||
latest archive when removing their `-SNAPSHOT` status, and
|
||||
latest archive when removing their `-next` status, and
|
||||
that they should be pointed back to the `master` branch after
|
||||
this has completed.
|
||||
|
||||
126
e2e/README.md
126
e2e/README.md
@@ -82,20 +82,13 @@ To make this possible, we're leveraging a 3rd party service, [Percy](https://per
|
||||
|
||||
At present, we are using percy with two configuration files: `./e2e/.percy.nightly.yml` and `./e2e/.percy.ci.yml`. This is mainly to reduce the number of snapshots.
|
||||
|
||||
### Advanced: Snapshot Testing (Not Recommended)
|
||||
### (Advanced) Snapshot Testing
|
||||
|
||||
While snapshot testing offers a precise way to detect changes in your application without relying on third-party services like Percy.io, we've found that it doesn't offer any advantages over visual testing in our use-cases. Therefore, snapshot testing is **not recommended** for further implementation.
|
||||
Snapshot testing is very similar to visual testing but allows us to be more precise in detecting change without relying on a 3rd party service. Unfortuantely, this precision requires advanced test setup and teardown and so we're using this pattern as a last resort.
|
||||
|
||||
#### CI vs Manual Checks
|
||||
Snapshot tests can be reliably executed in Continuous Integration (CI) environments but lack the manual oversight provided by visual testing platforms like Percy.io. This means they may miss issues that a human reviewer could catch during manual checks.
|
||||
|
||||
#### Example
|
||||
A single visual test assertion in Percy.io can be executed across 10 different browser and resolution combinations without additional setup, providing comprehensive testing with minimal configuration. In contrast, a snapshot test is restricted to a single OS and browser resolution, requiring more effort to achieve the same level of coverage.
|
||||
|
||||
|
||||
#### Further Reading
|
||||
For those interested in the mechanics of snapshot testing with Playwright, you can refer to the [Playwright Snapshots Documentation](https://playwright.dev/docs/test-snapshots). However, keep in mind that we do not recommend using this approach.
|
||||
To give an example, if a _single_ visual test assertion for an Overlay plot is run through multiple DOM rendering engines at various viewports to see how the Plot looks. If that same test were run as a snapshot test, it could only be executed against a single browser, on a single platform (ubuntu docker container).
|
||||
|
||||
Read more about [Playwright Snapshots](https://playwright.dev/docs/test-snapshots)
|
||||
|
||||
#### Open MCT's implementation
|
||||
|
||||
@@ -134,11 +127,11 @@ npm run test:e2e:updatesnapshots
|
||||
|
||||
## Performance Testing
|
||||
|
||||
The open source performance tests function in three ways which match their naming and folder structure:
|
||||
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.
|
||||
|
||||
`./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.
|
||||
They're found under `./e2e/tests/performance` and are to be executed with the following npm script:
|
||||
|
||||
`npm run test:perf`
|
||||
|
||||
These tests are expected to become blocking and gating with assertions as we extend the capabilities of Playwright.
|
||||
|
||||
@@ -158,11 +151,8 @@ 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 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/performance/` | Performance tests.|
|
||||
|`./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|
|
||||
|
||||
@@ -179,7 +169,6 @@ 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
|
||||
@@ -319,27 +308,14 @@ Skipping based on browser version (Rarely used): <https://github.com/microsoft/p
|
||||
|
||||
## Test Design, Best Practices, and Tips & Tricks
|
||||
|
||||
### Test Design
|
||||
### Test Design (TODO)
|
||||
|
||||
#### Test as the User
|
||||
- How to make tests robust to function in other contexts (VISTA, VIPER, etc.)
|
||||
- Leverage the use of `appActions.js` methods such as `createDomainObjectWithDefaults()`
|
||||
- How to make tests faster and more resilient
|
||||
- When possible, navigate directly by URL:
|
||||
|
||||
In general, strive to test only through the UI as a user would. As stated in the [Playwright Best Practices](https://playwright.dev/docs/best-practices#test-user-visible-behavior):
|
||||
|
||||
> "Automated tests should verify that the application code works for the end users, and avoid relying on implementation details such as things which users will not typically use, see, or even know about such as the name of a function, whether something is an array, or the CSS class of some element. The end user will see or interact with what is rendered on the page, so your test should typically only see/interact with the same rendered output."
|
||||
|
||||
By adhering to this principle, we can create tests that are both robust and reflective of actual user experiences.
|
||||
|
||||
#### How to make tests robust to function in other contexts (VISTA, COUCHDB, YAMCS, VIPER, etc.)
|
||||
1. Leverage the use of `appActions.js` methods such as `createDomainObjectWithDefaults()`. This ensures that your tests will create unique instances of objects for your test to interact with.
|
||||
1. Do not assert on the order or structure of objects available unless you created them yourself. These tests may be used against a persistent datastore like couchdb with many objects in the tree.
|
||||
1. Do not search for your created objects. Open MCT does not performance uniqueness checks so it's possible that your tests will break when run twice.
|
||||
1. Avoid creating locator aliases. This likely means that you're compensating for a bad locator. Improve the application instead.
|
||||
1. Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });` instead of `{ waitUntil: 'networkidle' }`. Tests run against deployments with websockets often have issues with the networkidle detection.
|
||||
|
||||
#### How to make tests faster and more resilient
|
||||
1. Avoid app interaction when possible. The best way of doing this is to navigate directly by URL:
|
||||
|
||||
```js
|
||||
```javascript
|
||||
// You can capture the CreatedObjectInfo returned from this appAction:
|
||||
const clock = await createDomainObjectWithDefaults(page, { type: 'Clock' });
|
||||
|
||||
@@ -347,14 +323,12 @@ By adhering to this principle, we can create tests that are both robust and refl
|
||||
await page.goto(clock.url);
|
||||
```
|
||||
|
||||
1. Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });`
|
||||
- Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });`
|
||||
- Initial navigation should _almost_ always use the `{ waitUntil: 'domcontentloaded' }` option.
|
||||
1. Avoid repeated setup to test a single assertion. Write longer tests with multiple soft assertions.
|
||||
This ensures that your changes will be picked up with large refactors.
|
||||
- Avoid repeated setup to test to test a single assertion. Write longer tests with multiple soft assertions.
|
||||
|
||||
### How to write a great test
|
||||
|
||||
- Avoid using css locators to find elements to the page. Use modern web accessible locators like `getByRole`
|
||||
- Use our [App Actions](./appActions.js) for performing common actions whenever applicable.
|
||||
- Use `waitForPlotsToRender()` before asserting against anything that is dependent upon plot series data being loaded and drawn.
|
||||
- If you create an object outside of using the `createDomainObjectWithDefaults` App Action, make sure to fill in the 'Notes' section of your object with `page.testNotes`:
|
||||
@@ -367,29 +341,26 @@ By adhering to this principle, we can create tests that are both robust and refl
|
||||
await notesInput.fill(testNotes);
|
||||
```
|
||||
|
||||
#### How to Write a Great Visual Test
|
||||
#### How to write a great visual test
|
||||
|
||||
1. **Look for the Unknown Unknowns**: Avoid asserting on specific differences in the visual diff. Visual tests are most effective for identifying unknown unknowns.
|
||||
|
||||
2. **Get the App into Interesting States**: Prioritize getting Open MCT into unusual layouts or behaviors before capturing a visual snapshot. For instance, you could open a dropdown menu.
|
||||
|
||||
3. **Expect the Unexpected**: Use functional expect statements only to verify assumptions about the state between steps. A great visual test doesn't fail during the test itself, but rather when changes are reviewed in Percy.io.
|
||||
|
||||
4. **Control Variability**: Account for variations inherent in working with time-based telemetry and clocks.
|
||||
- Utilize `percyCSS` to ignore time-based elements. For more details, consult our [percyCSS file](./.percy.ci.yml).
|
||||
- Use Open MCT's fixed-time mode.
|
||||
- Employ the `createExampleTelemetryObject` appAction to source telemetry and specify a `name` to avoid autogenerated names.
|
||||
|
||||
5. **Hide the Tree and Inspector**: Generally, your test will not require comparisons involving the tree and inspector. These aspects are covered in component-specific tests (explained below). To exclude them from the comparison by default, navigate to the root of the main view with the tree and inspector hidden:
|
||||
- `await page.goto('./#/browse/mine?hideTree=true&hideInspector=true')`
|
||||
|
||||
6. **Component-Specific Tests**: If you wish to focus on a particular component, use the `/visual/component/` folder and limit the scope of the comparison to that component. For instance:
|
||||
- Generally speaking, you should avoid being "specific" in what you hope to find in the diff. Visual tests are best suited for finding unknown unknowns.
|
||||
- These should only use functional expect statements to verify assumptions about the state
|
||||
in a test and not for functional verification of correctness. Visual tests are not supposed
|
||||
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
|
||||
- A great visual test controls for the variation inherent to working with time-based telemetry and clocks. We do our best to remove this variation by using `percyCSS` to ignore all possible time-based components. For more, please see our [percyCSS file](./.percy.ci.yml).
|
||||
- Additionally, you should try the following:
|
||||
- Use fixed-time mode of Open MCT
|
||||
- Use the `createExampleTelemetryObject` appAction to source telemetry
|
||||
- When using the `createDomainObjectWithDefaults` appAction, make sure to specify a `name` which is explicit to avoid the autogenerated name
|
||||
- Very likely, your test will not need to compare changes on the tree. Keep it out of the comparison with the following
|
||||
- `await page.goto('./#/browse/mine')` will go to the root of the main view with the tree collapsed.
|
||||
- If you only want to compare changes on a specific component, use the /visual/component/ folder and limit the scope of the comparison to the object like so:
|
||||
- ```
|
||||
await percySnapshot(page, `Tree Pane w/ single level expanded (theme: ${theme})`, {
|
||||
scope: treePane
|
||||
});
|
||||
```js
|
||||
await percySnapshot(page, `Tree Pane w/ single level expanded (theme: ${theme})`, {
|
||||
scope: treePane
|
||||
});
|
||||
```
|
||||
- Note: The `scope` variable can be any valid CSS selector.
|
||||
- The `scope` variable can be any valid css selector
|
||||
|
||||
#### How to write a great network test
|
||||
|
||||
@@ -406,35 +377,12 @@ For now, our best practices exist as self-tested, living documentation in our [e
|
||||
|
||||
For best practices with regards to mocking network responses, see our [couchdb.e2e.spec.js](./tests/functional/couchdb.e2e.spec.js) file.
|
||||
|
||||
### Tips & Tricks
|
||||
### Tips & Tricks (TODO)
|
||||
|
||||
The following contains a list of tips and tricks which don't exactly fit into a FAQ or Best Practices doc.
|
||||
|
||||
- (Advanced) Overriding the Browser's Clock
|
||||
It is possible to override the browser's clock in order to control time-based elements. Since this can cause unwanted behavior (i.e. Tree not rendering), only use this sparingly. To do this, use the `overrideClock` fixture as such:
|
||||
|
||||
```js
|
||||
const { test, expect } = require('../../pluginFixtures.js');
|
||||
|
||||
test.describe('foo test suite', () => {
|
||||
|
||||
// All subsequent tests in this suite will override the clock
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: 1732413600000, // A timestamp given as milliseconds since the epoch
|
||||
shouldAdvanceTime: true // Should the clock tick?
|
||||
}
|
||||
});
|
||||
|
||||
test('bar test', async ({ page }) => {
|
||||
// ...
|
||||
});
|
||||
});
|
||||
```
|
||||
More info and options for `overrideClock` can be found in [baseFixtures.js](baseFixtures.js)
|
||||
|
||||
- Working with multiple pages
|
||||
There are instances where multiple browser pages will needed to verify multi-page or multi-tab application behavior. Make sure to use the `@2p` annotation as well as name each page appropriately: i.e. `page1` and `page2` or `tab1` and `tab2` depending on the intended use case. Generally pages should be used unless testing `sharedWorker` code, specifically.
|
||||
There are instances where multiple browser pages will need to be opened to verify multi-page or multi-tab application behavior.
|
||||
|
||||
### Reporting
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ async function createDomainObjectWithDefaults(
|
||||
|
||||
// Navigate to the parent object. This is necessary to create the object
|
||||
// in the correct location, such as a folder, layout, or plot.
|
||||
await page.goto(`${parentUrl}`);
|
||||
await page.goto(`${parentUrl}?hideTree=true`);
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
@@ -179,7 +179,7 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
|
||||
|
||||
// Navigate to the parent object. This is necessary to create the object
|
||||
// in the correct location, such as a folder, layout, or plot.
|
||||
await page.goto(`${parentUrl}`);
|
||||
await page.goto(`${parentUrl}?hideTree=true`);
|
||||
|
||||
// Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
@@ -232,7 +232,7 @@ async function createExampleTelemetryObject(page, parent = 'mine') {
|
||||
const name = 'VIPER Rover Heading';
|
||||
const nameInputLocator = page.getByRole('dialog').locator('input[type="text"]');
|
||||
|
||||
await page.goto(`${parentUrl}`);
|
||||
await page.goto(`${parentUrl}?hideTree=true`);
|
||||
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
/* 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;
|
||||
@@ -2,24 +2,25 @@
|
||||
// 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', //need development mode for performance.marks and others
|
||||
command: 'npm run start', //coverage not generated
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: false
|
||||
reuseExistingServer: !CI
|
||||
},
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: true,
|
||||
ignoreHTTPSErrors: false, //HTTP performance varies!
|
||||
headless: CI, //Only if running locally
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'off',
|
||||
trace: 'on-first-retry',
|
||||
video: 'off'
|
||||
@@ -27,7 +28,6 @@ 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'
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -80,7 +80,7 @@ test.describe('CouchDB Status Indicator with mocked responses @couchdb', () => {
|
||||
test.describe('CouchDB initialization with mocked responses @couchdb', () => {
|
||||
test.use({ failOnConsoleError: false });
|
||||
test("'My Items' folder is created if it doesn't exist", async ({ page }) => {
|
||||
const mockedMissingObjectResponsefromCouchDB = {
|
||||
const mockedMissingObjectResponseFromCouchDB = {
|
||||
status: 404,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({})
|
||||
@@ -92,7 +92,7 @@ test.describe('CouchDB initialization with mocked responses @couchdb', () => {
|
||||
await page.route(
|
||||
'**/mine',
|
||||
(route) => {
|
||||
route.fulfill(mockedMissingObjectResponsefromCouchDB);
|
||||
route.fulfill(mockedMissingObjectResponseFromCouchDB);
|
||||
},
|
||||
{ times: 1 }
|
||||
);
|
||||
|
||||
@@ -46,7 +46,7 @@ test.describe('Example Event Generator CRUD Operations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Example Event Generator Telemetry Event Verficiation', () => {
|
||||
test.describe('Example Event Generator Telemetry Event Verification', () => {
|
||||
test.fixme('telemetry is coming in for test event', async ({ page }) => {
|
||||
// Go to object created in step one
|
||||
// Verify the telemetry table is filled with > 1 row
|
||||
|
||||
@@ -292,7 +292,7 @@ test.describe('Move & link item tests', () => {
|
||||
});
|
||||
|
||||
test.fixme(
|
||||
'Cannot move a previously created domain object to non-peristable object in Move Modal',
|
||||
'Cannot move a previously created domain object to non-persistable object in Move Modal',
|
||||
async ({ page }) => {
|
||||
//Create a domain object
|
||||
//Save Domain object
|
||||
|
||||
@@ -73,7 +73,7 @@ const testPlan = {
|
||||
};
|
||||
|
||||
test.describe('Time Strip', () => {
|
||||
test('Create two Time Strips, add a single Plan to both, and verify they can have separate Indepdenent Time Contexts @unstable', async ({
|
||||
test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts @unstable', async ({
|
||||
page
|
||||
}) => {
|
||||
test.info().annotations.push({
|
||||
|
||||
@@ -22,12 +22,15 @@
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. Note: this
|
||||
suite is sharing state between tests which is considered an anti-pattern. Implimenting in this way to
|
||||
suite is sharing state between tests which is considered an anti-pattern. Implementing in this way to
|
||||
demonstrate some playwright for test developers. This pattern should not be re-used in other CRUD suites.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures.js');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject
|
||||
} = require('../../../../appActions');
|
||||
|
||||
let conditionSetUrl;
|
||||
let getConditionSetIdentifierFromUrl;
|
||||
@@ -127,7 +130,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||
// Verify Inspector Details has updated Name property
|
||||
expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
|
||||
|
||||
// Verify Tree reflects updated Name proprety
|
||||
// Verify Tree reflects updated Name property
|
||||
// Expand Tree
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span >> nth=3`).click();
|
||||
// Verify Condition Set Object is renamed in Tree
|
||||
@@ -150,7 +153,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||
// Verify Inspector Details has updated Name property
|
||||
expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
|
||||
|
||||
// Verify Tree reflects updated Name proprety
|
||||
// Verify Tree reflects updated Name property
|
||||
// Expand Tree
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span >> nth=3`).click();
|
||||
// Verify Condition Set Object is renamed in Tree
|
||||
@@ -205,23 +208,31 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||
});
|
||||
|
||||
test.describe('Basic Condition Set Use', () => {
|
||||
let conditionSet;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Open a browser, navigate to the main page, and wait until all network events to resolve
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
test('Can add a condition', async ({ page }) => {
|
||||
// Create a new condition set
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
conditionSet = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Condition Set',
|
||||
name: 'Test Condition Set'
|
||||
});
|
||||
});
|
||||
test('Creating a condition defaults the condition name to "Unnamed Condition"', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto(conditionSet.url);
|
||||
|
||||
// Change the object to edit mode
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
// Click Add Condition button
|
||||
await page.locator('#addCondition').click();
|
||||
// Check that the new Unnamed Condition section appears
|
||||
const numOfUnnamedConditions = await page.locator('text=Unnamed Condition').count();
|
||||
const numOfUnnamedConditions = await page
|
||||
.locator('.c-condition__name', { hasText: 'Unnamed Condition' })
|
||||
.count();
|
||||
expect(numOfUnnamedConditions).toEqual(1);
|
||||
});
|
||||
test('ConditionSet should display appropriate view options', async ({ page }) => {
|
||||
@@ -238,16 +249,13 @@ test.describe('Basic Condition Set Use', () => {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Beta Sine Wave Generator'
|
||||
});
|
||||
const conditionSet1 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Condition Set',
|
||||
name: 'Test Condition Set'
|
||||
});
|
||||
|
||||
await page.goto(conditionSet.url);
|
||||
|
||||
// Change the object to edit mode
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.goto(conditionSet1.url);
|
||||
page.click('button[title="Show selected item in tree"]');
|
||||
// Add the Alpha & Beta Sine Wave Generator to the Condition Set and save changes
|
||||
const treePane = page.getByRole('tree', {
|
||||
@@ -264,9 +272,9 @@ test.describe('Basic Condition Set Use', () => {
|
||||
await alphaGeneratorTreeItem.dragTo(conditionCollection);
|
||||
await betaGeneratorTreeItem.dragTo(conditionCollection);
|
||||
|
||||
const saveButtonLocator = page.locator('button[title="Save"]');
|
||||
await saveButtonLocator.click();
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
await page.click('button[title="Change the current view"]');
|
||||
|
||||
await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden();
|
||||
@@ -274,95 +282,89 @@ test.describe('Basic Condition Set Use', () => {
|
||||
await expect(page.getByRole('menuitem', { name: /Plot/ })).toBeVisible();
|
||||
await expect(page.getByRole('menuitem', { name: /Telemetry Table/ })).toBeVisible();
|
||||
});
|
||||
test('ConditionSet should output blank instead of the default value', async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
test('ConditionSet has correct outputs when telemetry is and is not available', async ({
|
||||
page
|
||||
}) => {
|
||||
const exampleTelemetry = await createExampleTelemetryObject(page);
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click the object specified by 'type'
|
||||
await page.click(`li[role='menuitem']:text("Sine Wave Generator")`);
|
||||
await page.getByRole('spinbutton', { name: 'Loading Delay (ms)' }).fill('8000');
|
||||
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
|
||||
await nameInput.fill('Delayed Sine Wave Generator');
|
||||
|
||||
// Click OK button and wait for Navigate event
|
||||
await Promise.all([
|
||||
page.waitForLoadState(),
|
||||
page.click('[aria-label="Save"]'),
|
||||
// Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// Create a new condition set
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Condition Set',
|
||||
name: 'Test Blank Output of Condition Set'
|
||||
});
|
||||
await page.getByTitle('Show selected item in tree').click();
|
||||
await page.goto(conditionSet.url);
|
||||
// Change the object to edit mode
|
||||
await page.locator('[title="Edit"]').click();
|
||||
|
||||
// Click Add Condition button twice
|
||||
// Create two conditions
|
||||
await page.locator('#addCondition').click();
|
||||
await page.locator('#addCondition').click();
|
||||
await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');
|
||||
await page.locator('#conditionCollection').getByRole('textbox').nth(1).fill('Second Condition');
|
||||
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
|
||||
// Add the Sine Wave Generator to the Condition Set and save changes
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: 'Delayed Sine Wave Generator'
|
||||
});
|
||||
const conditionCollection = await page.locator('#conditionCollection');
|
||||
|
||||
// Add Telemetry to ConditionSet
|
||||
const sineWaveGeneratorTreeItem = page
|
||||
.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
})
|
||||
.getByRole('treeitem', {
|
||||
name: exampleTelemetry.name
|
||||
});
|
||||
const conditionCollection = page.locator('#conditionCollection');
|
||||
await sineWaveGeneratorTreeItem.dragTo(conditionCollection);
|
||||
|
||||
const firstCriterionTelemetry = await page.locator(
|
||||
// Modify First Criterion
|
||||
const firstCriterionTelemetry = page.locator(
|
||||
'[aria-label="Criterion Telemetry Selection"] >> nth=0'
|
||||
);
|
||||
firstCriterionTelemetry.selectOption({ label: 'Delayed Sine Wave Generator' });
|
||||
|
||||
const secondCriterionTelemetry = await page.locator(
|
||||
'[aria-label="Criterion Telemetry Selection"] >> nth=1'
|
||||
);
|
||||
secondCriterionTelemetry.selectOption({ label: 'Delayed Sine Wave Generator' });
|
||||
|
||||
const firstCriterionMetadata = await page.locator(
|
||||
firstCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
|
||||
const firstCriterionMetadata = page.locator(
|
||||
'[aria-label="Criterion Metadata Selection"] >> nth=0'
|
||||
);
|
||||
firstCriterionMetadata.selectOption({ label: 'Sine' });
|
||||
const firstCriterionComparison = page.locator(
|
||||
'[aria-label="Criterion Comparison Selection"] >> nth=0'
|
||||
);
|
||||
firstCriterionComparison.selectOption({ label: 'is greater than or equal to' });
|
||||
const firstCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=0');
|
||||
await firstCriterionInput.fill('0');
|
||||
|
||||
const secondCriterionMetadata = await page.locator(
|
||||
// Modify First Criterion
|
||||
const secondCriterionTelemetry = page.locator(
|
||||
'[aria-label="Criterion Telemetry Selection"] >> nth=1'
|
||||
);
|
||||
secondCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
|
||||
|
||||
const secondCriterionMetadata = page.locator(
|
||||
'[aria-label="Criterion Metadata Selection"] >> nth=1'
|
||||
);
|
||||
secondCriterionMetadata.selectOption({ label: 'Sine' });
|
||||
|
||||
const firstCriterionComparison = await page.locator(
|
||||
'[aria-label="Criterion Comparison Selection"] >> nth=0'
|
||||
);
|
||||
firstCriterionComparison.selectOption({ label: 'is greater than or equal to' });
|
||||
|
||||
const secondCriterionComparison = await page.locator(
|
||||
const secondCriterionComparison = page.locator(
|
||||
'[aria-label="Criterion Comparison Selection"] >> nth=1'
|
||||
);
|
||||
secondCriterionComparison.selectOption({ label: 'is less than' });
|
||||
|
||||
const firstCriterionInput = await page.locator('[aria-label="Criterion Input"] >> nth=0');
|
||||
await firstCriterionInput.fill('0');
|
||||
|
||||
const secondCriterionInput = await page.locator('[aria-label="Criterion Input"] >> nth=1');
|
||||
const secondCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=1');
|
||||
await secondCriterionInput.fill('0');
|
||||
|
||||
const saveButtonLocator = page.locator('button[title="Save"]');
|
||||
await saveButtonLocator.click();
|
||||
// Save ConditionSet
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
const outputValue = await page.locator('[aria-label="Current Output Value"]');
|
||||
// Validate that the condition set is evaluating and outputting
|
||||
// the correct value when the underlying telemetry subscription is active.
|
||||
let outputValue = page.locator('[aria-label="Current Output Value"]');
|
||||
await expect(outputValue).toHaveText('false');
|
||||
|
||||
await page.goto(exampleTelemetry.url);
|
||||
|
||||
// Edit SWG to add 8 second loading delay to simulate the case
|
||||
// where telemetry is not available.
|
||||
await page.getByTitle('More options').click();
|
||||
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
|
||||
await page.getByRole('spinbutton', { name: 'Loading Delay (ms)' }).fill('8000');
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
// Expect that the output value is blank or '---' if the
|
||||
// underlying telemetry subscription is not active.
|
||||
await page.goto(conditionSet.url);
|
||||
await expect(outputValue).toHaveText('---');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -214,7 +214,7 @@ test.describe('Example Imagery Object', () => {
|
||||
await page.mouse.up();
|
||||
await Promise.all(tagHotkey.map((x) => page.keyboard.up(x)));
|
||||
|
||||
//Wait for canvas to stablize.
|
||||
//Wait for canvas to stabilize.
|
||||
await canvas.hover({ trial: true });
|
||||
|
||||
// add some tags
|
||||
@@ -394,7 +394,7 @@ test.describe('Example Imagery in Display Layout', () => {
|
||||
/**
|
||||
* Toggle layer visibility checkbox by clicking on checkbox label
|
||||
* - should toggle checkbox and layer visibility for that image view
|
||||
* - should NOT toggle checkbox and layer visibity for the first image view in display
|
||||
* - should NOT toggle checkbox and layer visibility for the first image view in display
|
||||
*/
|
||||
test('Toggle layer visibility by clicking on label', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
|
||||
@@ -48,12 +48,12 @@ test.describe('ExportAsJSON', () => {
|
||||
test.fixme('Verify that a nested Object can be exported as JSON', async ({ page }) => {
|
||||
// Create 2 objects with hierarchy
|
||||
// Export as JSON
|
||||
// Verify Hiearchy
|
||||
// Verify Hierarchy
|
||||
});
|
||||
test.fixme(
|
||||
'Verify that the ExportAsJSON dropdown does not appear for the item X',
|
||||
async ({ page }) => {
|
||||
// Other than non-persistible objects
|
||||
// Other than non-persistable objects
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@ test.describe('ExportAsJSON', () => {
|
||||
test.fixme(
|
||||
'Verify that the ImportAsJSON dropdown does not appear for the item X',
|
||||
async ({ page }) => {
|
||||
// Other than non-persistible objects
|
||||
// Other than non-persistable objects
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ test.describe('Notebook CRUD Operations', () => {
|
||||
test.fixme('Can update a Notebook Object', async ({ page }) => {});
|
||||
test.fixme('Can view a perviously created Notebook Object', async ({ page }) => {});
|
||||
test.fixme('Can Delete a Notebook Object', async ({ page }) => {
|
||||
// Other than non-persistible objects
|
||||
// Other than non-persistable objects
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ test.describe('Snapshot Menu tests', () => {
|
||||
// There should be no default notebook
|
||||
// Clear default notebook if exists using `localStorage.setItem('notebook-storage', null);`
|
||||
// refresh page
|
||||
// Click on 'Notebook Snaphot Menu'
|
||||
// Click on 'Notebook Snapshot Menu'
|
||||
// 'save to Notebook Snapshots' should be only option there
|
||||
}
|
||||
);
|
||||
@@ -62,7 +62,7 @@ test.describe('Snapshot Menu tests', () => {
|
||||
});
|
||||
test.fixme('Snapshots adjust time conductor', async ({ page }) => {
|
||||
// Create Telemetry object
|
||||
// Set Telemetry object's timeconductor to Fixed time with Start and Endtimes are recorded
|
||||
// Set Telemetry object's timeconductor to Fixed time with Start and End times are recorded
|
||||
// Embed Telemetry object into notebook
|
||||
// Set Time Conductor to Local clock
|
||||
// Click into embedded telemetry object and verify object appears with same fixed time from record
|
||||
|
||||
@@ -157,6 +157,6 @@ test.describe('Operator Status', () => {
|
||||
});
|
||||
|
||||
test.fixme('iterate through all possible response values', async ({ page }) => {
|
||||
// test all possible respone values for the poll
|
||||
// test all possible response values for the poll
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
Testsuite for plot autoscale.
|
||||
*/
|
||||
|
||||
const { selectInspectorTab, setTimeConductorBounds } = require('../../../../appActions');
|
||||
const { selectInspectorTab, createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
test.use({
|
||||
viewport: {
|
||||
@@ -34,17 +34,26 @@ test.use({
|
||||
});
|
||||
|
||||
test.describe('Autoscale', () => {
|
||||
test('User can set autoscale with a valid range @snapshot', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
test('User can set autoscale with a valid range @snapshot', async ({ page }) => {
|
||||
//This is necessary due to the size of the test suite.
|
||||
test.slow();
|
||||
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await setTimeRange(page);
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
name: 'Test Overlay Plot',
|
||||
type: 'Overlay Plot'
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
name: 'Test Sine Wave Generator',
|
||||
type: 'Sine Wave Generator',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
await createSinewaveOverlayPlot(page, myItemsFolderName);
|
||||
// Switch to fixed time, start: 2022-03-28 22:00:00.000 UTC, end: 2022-03-28 22:00:30.000 UTC
|
||||
await page.goto(
|
||||
`${overlayPlot.url}?tc.mode=fixed&tc.startBound=1648591200000&tc.endBound=1648591230000&tc.timeSystem=utc&view=plot-overlay`
|
||||
);
|
||||
|
||||
await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']);
|
||||
|
||||
@@ -67,7 +76,7 @@ test.describe('Autoscale', () => {
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
|
||||
|
||||
// Make sure that after turning off autoscale, the user entered range values are reflexted in the ticks.
|
||||
// Make sure that after turning off autoscale, the user entered range values are reflected in the ticks.
|
||||
await testYTicks(page, [
|
||||
'-2.00',
|
||||
'-1.50',
|
||||
@@ -109,7 +118,7 @@ test.describe('Autoscale', () => {
|
||||
// Ensure the drag worked.
|
||||
await testYTicks(page, ['-0.50', '0.00', '0.50', '1.00', '1.50', '2.00', '2.50', '3.00']);
|
||||
|
||||
//Wait for canvas to stablize.
|
||||
//Wait for canvas to stabilize.
|
||||
await canvas.hover({ trial: true });
|
||||
|
||||
expect
|
||||
@@ -118,72 +127,6 @@ test.describe('Autoscale', () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} start
|
||||
* @param {string} end
|
||||
*/
|
||||
async function setTimeRange(
|
||||
page,
|
||||
start = '2022-03-29 22:00:00.000Z',
|
||||
end = '2022-03-29 22:00:30.000Z'
|
||||
) {
|
||||
// Set a specific time range for consistency, otherwise it will change
|
||||
// on every test to a range based on the current time.
|
||||
|
||||
await setTimeConductorBounds(page, start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} myItemsFolderName
|
||||
*/
|
||||
async function createSinewaveOverlayPlot(page, myItemsFolderName) {
|
||||
// click create button
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add overlay plot with defaults
|
||||
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('button:has-text("OK")').click(),
|
||||
//Wait for Save Banner to appear1
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
|
||||
|
||||
// save (exit edit mode)
|
||||
await page
|
||||
.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button')
|
||||
.nth(1)
|
||||
.click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// click create button
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add sine wave generator with defaults
|
||||
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('button:has-text("OK")').click(),
|
||||
//Wait for Save Banner to appear1
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
|
||||
|
||||
// focus the overlay plot
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
|
||||
@@ -42,7 +42,7 @@ test.describe('Plot Tagging', () => {
|
||||
* @param {Number} yEnd a telemetry item with a plot
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {
|
||||
async function createTags({ page, canvas, xEnd = 700, yEnd = 480 }) {
|
||||
await canvas.hover({ trial: true });
|
||||
|
||||
//Alt+Shift Drag Start to select some points to tag
|
||||
@@ -64,7 +64,7 @@ test.describe('Plot Tagging', () => {
|
||||
await page.keyboard.up('Alt');
|
||||
await page.keyboard.up('Shift');
|
||||
|
||||
//Wait for canvas to stablize.
|
||||
//Wait for canvas to stabilize.
|
||||
await canvas.hover({ trial: true });
|
||||
|
||||
// add some tags
|
||||
@@ -173,6 +173,7 @@ test.describe('Plot Tagging', () => {
|
||||
});
|
||||
|
||||
test('Tags work with Overlay Plots', async ({ page }) => {
|
||||
test.slow();
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/6822'
|
||||
@@ -284,7 +285,7 @@ test.describe('Plot Tagging', () => {
|
||||
page,
|
||||
canvas,
|
||||
xEnd: 700,
|
||||
yEnd: 240
|
||||
yEnd: 215
|
||||
});
|
||||
await basicTagsTests(page);
|
||||
await testTelemetryItem(page, alphaSineWave);
|
||||
|
||||
@@ -25,15 +25,12 @@ const {
|
||||
openObjectTreeContextMenu,
|
||||
createDomainObjectWithDefaults
|
||||
} = require('../../../../appActions');
|
||||
import { MISSION_TIME } from '../../../../constants';
|
||||
|
||||
test.describe('Timer', () => {
|
||||
let timer;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
|
||||
await assertTimerElements(page, timer);
|
||||
});
|
||||
|
||||
test('Can perform actions on the Timer', async ({ page }) => {
|
||||
@@ -66,70 +63,6 @@ test.describe('Timer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Timer with target date', () => {
|
||||
let timer;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
|
||||
await assertTimerElements(page, timer);
|
||||
});
|
||||
|
||||
// Override clock
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: MISSION_TIME,
|
||||
shouldAdvanceTime: true
|
||||
}
|
||||
});
|
||||
|
||||
test('Can count down to a target date', async ({ page }) => {
|
||||
// Set the target date to 2024-11-24 03:30:00
|
||||
await page.getByTitle('More options').click();
|
||||
await page.getByRole('menuitem', { name: /Edit Properties.../ }).click();
|
||||
await page.getByPlaceholder('YYYY-MM-DD').fill('2024-11-24');
|
||||
await page.locator('input[name="hour"]').fill('3');
|
||||
await page.locator('input[name="min"]').fill('30');
|
||||
await page.locator('input[name="sec"]').fill('00');
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
// Get the current timer seconds value
|
||||
const timerSecValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);
|
||||
await expect(page.locator('.c-timer__direction')).toHaveClass(/icon-minus/);
|
||||
|
||||
// Wait for the timer to count down and assert
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const newTimerValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);
|
||||
return Number(newTimerValue);
|
||||
})
|
||||
.toBeLessThan(Number(timerSecValue));
|
||||
});
|
||||
|
||||
test('Can count up from a target date', async ({ page }) => {
|
||||
// Set the target date to 2020-11-23 03:30:00
|
||||
await page.getByTitle('More options').click();
|
||||
await page.getByRole('menuitem', { name: /Edit Properties.../ }).click();
|
||||
await page.getByPlaceholder('YYYY-MM-DD').fill('2020-11-23');
|
||||
await page.locator('input[name="hour"]').fill('3');
|
||||
await page.locator('input[name="min"]').fill('30');
|
||||
await page.locator('input[name="sec"]').fill('00');
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
// Get the current timer seconds value
|
||||
const timerSecValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);
|
||||
await expect(page.locator('.c-timer__direction')).toHaveClass(/icon-plus/);
|
||||
|
||||
// Wait for the timer to count up and assert
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const newTimerValue = (await page.locator('.c-timer__value').innerText()).split(':').at(-1);
|
||||
return Number(newTimerValue);
|
||||
})
|
||||
.toBeGreaterThan(Number(timerSecValue));
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Actions that can be performed on a timer from context menus.
|
||||
* @typedef {'Start' | 'Stop' | 'Pause' | 'Restart at 0'} TimerAction
|
||||
@@ -208,17 +141,14 @@ function buttonTitleFromAction(action) {
|
||||
* @param {TimerAction} action
|
||||
*/
|
||||
async function assertTimerStateAfterAction(page, action) {
|
||||
const timerValue = page.locator('.c-timer__value');
|
||||
let timerStateClass;
|
||||
switch (action) {
|
||||
case 'Start':
|
||||
case 'Restart at 0':
|
||||
timerStateClass = 'is-started';
|
||||
expect(await timerValue.innerText()).toBe('0D 00:00:00');
|
||||
break;
|
||||
case 'Stop':
|
||||
timerStateClass = 'is-stopped';
|
||||
expect(await timerValue.innerText()).toBe('--:--:--');
|
||||
break;
|
||||
case 'Pause':
|
||||
timerStateClass = 'is-paused';
|
||||
@@ -227,25 +157,3 @@ async function assertTimerStateAfterAction(page, action) {
|
||||
|
||||
await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that all the major components of a timer are present in the DOM.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {import('../../../../appActions').CreatedObjectInfo} timer
|
||||
*/
|
||||
async function assertTimerElements(page, timer) {
|
||||
const timerElement = page.locator('.c-timer');
|
||||
const resetButton = page.getByRole('button', { name: 'Reset' });
|
||||
const pausePlayButton = page
|
||||
.getByRole('button', { name: 'Pause' })
|
||||
.or(page.getByRole('button', { name: 'Start' }));
|
||||
const timerDirectionIcon = page.locator('.c-timer__direction');
|
||||
const timerValue = page.locator('.c-timer__value');
|
||||
|
||||
expect(await page.locator('.l-browse-bar__object-name').innerText()).toBe(timer.name);
|
||||
expect(timerElement).toBeAttached();
|
||||
expect(resetButton).toBeAttached();
|
||||
expect(pausePlayButton).toBeAttached();
|
||||
expect(timerDirectionIcon).toBeAttached();
|
||||
expect(timerValue).toBeAttached();
|
||||
}
|
||||
|
||||
121
e2e/tests/performance/memleak-imagery.perf.spec.js
Normal file
121
e2e/tests/performance/memleak-imagery.perf.spec.js
Normal file
@@ -0,0 +1,121 @@
|
||||
/*****************************************************************************
|
||||
* 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');
|
||||
});
|
||||
});
|
||||
@@ -1,299 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { 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;
|
||||
}
|
||||
});
|
||||
@@ -41,7 +41,7 @@ test.describe.fixme('Plot Tagging Performance', () => {
|
||||
* @param {Number} yEnd a telemetry item with a plot
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {
|
||||
async function createTags({ page, canvas, xEnd = 700, yEnd = 480 }) {
|
||||
await canvas.hover({ trial: true });
|
||||
|
||||
//Alt+Shift Drag Start to select some points to tag
|
||||
@@ -63,7 +63,7 @@ test.describe.fixme('Plot Tagging Performance', () => {
|
||||
await page.keyboard.up('Alt');
|
||||
await page.keyboard.up('Shift');
|
||||
|
||||
//Wait for canvas to stablize.
|
||||
//Wait for canvas to stabilize.
|
||||
await canvas.hover({ trial: true });
|
||||
|
||||
// add some tags
|
||||
@@ -91,7 +91,7 @@ test.describe.fixme('Plot Tagging Performance', () => {
|
||||
|
||||
const canvas = page.locator('canvas').nth(1);
|
||||
|
||||
//Wait for canvas to stablize.
|
||||
//Wait for canvas to stabilize.
|
||||
await canvas.hover({ trial: true });
|
||||
|
||||
// click on the tagged plot point
|
||||
@@ -265,7 +265,7 @@ test.describe.fixme('Plot Tagging Performance', () => {
|
||||
page,
|
||||
canvas,
|
||||
xEnd: 700,
|
||||
yEnd: 240
|
||||
yEnd: 215
|
||||
});
|
||||
await basicTagsTests(page);
|
||||
await testTelemetryItem(page, alphaSineWave);
|
||||
|
||||
@@ -46,7 +46,7 @@ test.describe('Fault Management Visual Tests', () => {
|
||||
|
||||
await percySnapshot(
|
||||
page,
|
||||
`Acknowledged faults, have a checkmark on the fault icon and appear in the acknowldeged view (theme: '${theme}')`
|
||||
`Acknowledged faults, have a checkmark on the fault icon and appear in the acknowledged view (theme: '${theme}')`
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import EventTelmetryProvider from './EventTelemetryProvider';
|
||||
import EventTelemetryProvider from './EventTelemetryProvider';
|
||||
import EventMetadataProvider from './EventMetadataProvider';
|
||||
|
||||
export default function EventGeneratorPlugin(options) {
|
||||
@@ -36,7 +36,7 @@ export default function EventGeneratorPlugin(options) {
|
||||
};
|
||||
}
|
||||
});
|
||||
openmct.telemetry.addProvider(new EventTelmetryProvider());
|
||||
openmct.telemetry.addProvider(new EventTelemetryProvider());
|
||||
openmct.telemetry.addProvider(new EventMetadataProvider());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -88,10 +88,10 @@ define(['uuid'], function ({ v4: uuid }) {
|
||||
};
|
||||
|
||||
WorkerInterface.prototype.subscribe = function (request, cb) {
|
||||
const id = request.id;
|
||||
const { id, loadDelay } = request;
|
||||
const messageId = this.dispatch('subscribe', request, (message) => {
|
||||
if (!this.staleTelemetryIds[id]) {
|
||||
cb(message.data);
|
||||
setTimeout(() => cb(message.data), Math.max(loadDelay, 0));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -40,8 +40,8 @@ const DEFAULT_IMAGE_SAMPLES = [
|
||||
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg',
|
||||
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg'
|
||||
];
|
||||
const DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS = 20000;
|
||||
const MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS = 5000;
|
||||
const DEFAULT_IMAGE_LOAD_DELAY_IN_MILLISECONDS = 20000;
|
||||
const MIN_IMAGE_LOAD_DELAY_IN_MILLISECONDS = 5000;
|
||||
|
||||
let openmctInstance;
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function () {
|
||||
initialize: (object) => {
|
||||
object.configuration = {
|
||||
imageLocation: '',
|
||||
imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS,
|
||||
imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILLISECONDS,
|
||||
imageSamples: [],
|
||||
layers: []
|
||||
};
|
||||
@@ -188,20 +188,20 @@ function getImageLoadDelay(domainObject) {
|
||||
openmctInstance.objects.mutate(
|
||||
domainObject,
|
||||
'configuration.imageLoadDelayInMilliSeconds',
|
||||
DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS
|
||||
DEFAULT_IMAGE_LOAD_DELAY_IN_MILLISECONDS
|
||||
);
|
||||
|
||||
return DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS;
|
||||
return DEFAULT_IMAGE_LOAD_DELAY_IN_MILLISECONDS;
|
||||
}
|
||||
|
||||
if (imageLoadDelay < MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS) {
|
||||
if (imageLoadDelay < MIN_IMAGE_LOAD_DELAY_IN_MILLISECONDS) {
|
||||
openmctInstance.objects.mutate(
|
||||
domainObject,
|
||||
'configuration.imageLoadDelayInMilliSeconds',
|
||||
MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS
|
||||
MIN_IMAGE_LOAD_DELAY_IN_MILLISECONDS
|
||||
);
|
||||
|
||||
return MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS;
|
||||
return MIN_IMAGE_LOAD_DELAY_IN_MILLISECONDS;
|
||||
}
|
||||
|
||||
return imageLoadDelay;
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, shrink-to-fit=no"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<title></title>
|
||||
<title>Open MCT</title>
|
||||
<script src="dist/openmct.js"></script>
|
||||
<link
|
||||
rel="icon"
|
||||
@@ -51,7 +51,7 @@
|
||||
sizes="16x16"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
<style type="text/css">
|
||||
<style>
|
||||
@keyframes splash-spinner {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) rotate(0deg);
|
||||
|
||||
@@ -45,7 +45,7 @@ module.exports = (config) => {
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', 'webpack'],
|
||||
files: [
|
||||
'indexTest.js',
|
||||
'index-test.js',
|
||||
// included means: should the files be included in the browser using <script> tag?
|
||||
// We don't want them as a <script> because the shared worker source
|
||||
// needs loaded remotely by the shared worker process.
|
||||
@@ -102,7 +102,7 @@ module.exports = (config) => {
|
||||
failFast: false
|
||||
},
|
||||
preprocessors: {
|
||||
'indexTest.js': ['webpack', 'sourcemap']
|
||||
'index-test.js': ['webpack', 'sourcemap']
|
||||
},
|
||||
webpack: webpackConfig,
|
||||
webpackMiddleware: {
|
||||
|
||||
21
package.json
21
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "3.0.2",
|
||||
"version": "3.1.0-next",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.22.5",
|
||||
@@ -19,6 +19,7 @@
|
||||
"codecov": "3.8.3",
|
||||
"comma-separated-values": "3.6.4",
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
"cspell": "6.31.2",
|
||||
"css-loader": "6.8.1",
|
||||
"d3-axis": "3.0.0",
|
||||
"d3-scale": "3.3.0",
|
||||
@@ -77,9 +78,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf ./dist ./node_modules ./package-lock.json ./coverage ./html-test-results ./test-results ./.nyc_output ",
|
||||
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
|
||||
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint; npm run lint:spelling",
|
||||
"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": "eslint example src e2e --ext .js openmct.js --max-warnings=0 && eslint example src --ext .vue",
|
||||
"lint:spelling": "cspell \"**/*.{js,md,vue}\" --show-context --gitignore",
|
||||
@@ -102,9 +102,7 @@
|
||||
"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: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",
|
||||
"test:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
|
||||
"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",
|
||||
@@ -113,6 +111,7 @@
|
||||
"cov:unit:publish": "codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit",
|
||||
"prepare": "npm run build:prod && npx tsc"
|
||||
},
|
||||
"homepage": "https://nasa.github.io/openmct",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nasa/openmct.git"
|
||||
@@ -128,6 +127,12 @@
|
||||
"ios_saf >= 16",
|
||||
"Safari >= 16"
|
||||
],
|
||||
"author": "",
|
||||
"license": "Apache-2.0"
|
||||
"author": {
|
||||
"name": "National Aeronautics and Space Administration",
|
||||
"url": "https://www.nasa.gov"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"keywords": [
|
||||
"nasa"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ define([
|
||||
'./plugins/duplicate/plugin',
|
||||
'./plugins/importFromJSONAction/plugin',
|
||||
'./plugins/exportAsJSONAction/plugin',
|
||||
'./ui/components/components',
|
||||
'vue'
|
||||
], function (
|
||||
EventEmitter,
|
||||
@@ -67,6 +68,7 @@ define([
|
||||
DuplicateActionPlugin,
|
||||
ImportFromJSONAction,
|
||||
ExportAsJSONAction,
|
||||
components,
|
||||
Vue
|
||||
) {
|
||||
/**
|
||||
@@ -428,6 +430,7 @@ define([
|
||||
};
|
||||
|
||||
MCT.prototype.plugins = plugins;
|
||||
MCT.prototype.components = components.default;
|
||||
|
||||
return MCT;
|
||||
});
|
||||
|
||||
@@ -92,7 +92,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
creatable: false,
|
||||
cssClass: 'icon-notebook',
|
||||
initialize: function (domainObject) {
|
||||
domainObject.targets = domainObject.targets || [];
|
||||
domainObject.targets = domainObject.targets || {};
|
||||
domainObject._deleted = domainObject._deleted || false;
|
||||
domainObject.originalContextPath = domainObject.originalContextPath || '';
|
||||
domainObject.tags = domainObject.tags || [];
|
||||
@@ -117,10 +117,10 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
* @property {ANNOTATION_TYPES} annotationType the type of annotation to create (e.g., PLOT_SPATIAL)
|
||||
* @property {Tag[]} tags tags to add to the annotation, e.g., SCIENCE for science related annotations
|
||||
* @property {String} contentText Some text to add to the annotation, e.g. ("This annotation is about science")
|
||||
* @property {Array<Object>} targets The targets ID keystrings and their specific properties.
|
||||
* For plots, this will be a bounding box, e.g.: {keyString: "d8385009-789d-457b-acc7-d50ba2fd55ea", maxY: 100, minY: 0, maxX: 100, minX: 0}
|
||||
* @property {Object<string, Object>} targets The targets ID keystrings and their specific properties.
|
||||
* For plots, this will be a bounding box, e.g.: {maxY: 100, minY: 0, maxX: 100, minX: 0}
|
||||
* For notebooks, this will be an entry ID, e.g.: {entryId: "entry-ecb158f5-d23c-45e1-a704-649b382622ba"}
|
||||
* @property {DomainObject>[]} targetDomainObjects the domain objects this annotation points to (e.g., telemetry objects for a plot)
|
||||
* @property {DomainObject>} targetDomainObjects the targets ID keystrings and the domain objects this annotation points to (e.g., telemetry objects for a plot)
|
||||
*/
|
||||
/**
|
||||
* @method create
|
||||
@@ -141,15 +141,11 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
throw new Error(`Unknown annotation type: ${annotationType}`);
|
||||
}
|
||||
|
||||
if (!targets.length) {
|
||||
if (!Object.keys(targets).length) {
|
||||
throw new Error(`At least one target is required to create an annotation`);
|
||||
}
|
||||
|
||||
if (targets.some((target) => !target.keyString)) {
|
||||
throw new Error(`All targets require a keyString to create an annotation`);
|
||||
}
|
||||
|
||||
if (!targetDomainObjects.length) {
|
||||
if (!Object.keys(targetDomainObjects).length) {
|
||||
throw new Error(`At least one targetDomainObject is required to create an annotation`);
|
||||
}
|
||||
|
||||
@@ -185,7 +181,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
const success = await this.openmct.objects.save(createdObject);
|
||||
if (success) {
|
||||
this.emit('annotationCreated', createdObject);
|
||||
targetDomainObjects.forEach((targetDomainObject) => {
|
||||
Object.values(targetDomainObjects).forEach((targetDomainObject) => {
|
||||
this.#updateAnnotationModified(targetDomainObject);
|
||||
});
|
||||
|
||||
@@ -325,10 +321,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
}
|
||||
|
||||
#addTagMetaInformationToTags(tags) {
|
||||
// Convert to Set and back to Array to remove duplicates
|
||||
const uniqueTags = [...new Set(tags)];
|
||||
|
||||
return uniqueTags.map((tagKey) => {
|
||||
return tags.map((tagKey) => {
|
||||
const tagModel = this.availableTags[tagKey];
|
||||
tagModel.tagID = tagKey;
|
||||
|
||||
@@ -370,8 +363,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
const modelAddedToResults = await Promise.all(
|
||||
results.map(async (result) => {
|
||||
const targetModels = await Promise.all(
|
||||
result.targets.map(async (target) => {
|
||||
const targetID = target.keyString;
|
||||
Object.keys(result.targets).map(async (targetID) => {
|
||||
const targetModel = await this.openmct.objects.get(targetID);
|
||||
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
|
||||
const originalPathObjects = await this.openmct.objects.getOriginalPath(targetKeyString);
|
||||
@@ -418,12 +410,13 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
#breakApartSeparateTargets(results) {
|
||||
const separateResults = [];
|
||||
results.forEach((result) => {
|
||||
result.targets.forEach((target) => {
|
||||
const targetID = target.keyString;
|
||||
Object.keys(result.targets).forEach((targetID) => {
|
||||
const separatedResult = {
|
||||
...result
|
||||
};
|
||||
separatedResult.targets = [target];
|
||||
separatedResult.targets = {
|
||||
[targetID]: result.targets[targetID]
|
||||
};
|
||||
separatedResult.targetModels = result.targetModels.filter((targetModel) => {
|
||||
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
|
||||
|
||||
|
||||
@@ -62,12 +62,11 @@ describe('The Annotation API', () => {
|
||||
key: 'anAnnotationKey',
|
||||
namespace: 'fooNameSpace'
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
keyString: 'fooNameSpace:some-object',
|
||||
targets: {
|
||||
'fooNameSpace:some-object': {
|
||||
entryId: 'fooBarEntry'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
mockObjectProvider = jasmine.createSpyObj('mock provider', ['create', 'update', 'get']);
|
||||
@@ -122,7 +121,7 @@ describe('The Annotation API', () => {
|
||||
tags: ['sometag'],
|
||||
contentText: 'fooContext',
|
||||
targetDomainObjects: [mockDomainObject],
|
||||
targets: [{ keyString: 'fooTarget' }]
|
||||
targets: { fooTarget: {} }
|
||||
};
|
||||
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
|
||||
expect(annotationObject).toBeDefined();
|
||||
@@ -137,7 +136,7 @@ describe('The Annotation API', () => {
|
||||
tags: ['sometag'],
|
||||
contentText: 'fooContext',
|
||||
targetDomainObjects: [mockDomainObject],
|
||||
targets: [{ keyString: 'fooTarget' }]
|
||||
targets: { fooTarget: {} }
|
||||
};
|
||||
openmct.annotation.setNamespaceToSaveAnnotations('fooNameSpace');
|
||||
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
|
||||
@@ -167,9 +166,9 @@ describe('The Annotation API', () => {
|
||||
tags: ['sometag'],
|
||||
contentText: 'fooContext',
|
||||
targetDomainObjects: [mockDomainObject],
|
||||
targets: [{ keyString: 'fooTarget' }]
|
||||
targets: { fooTarget: {} }
|
||||
};
|
||||
openmct.annotation.setNamespaceToSaveAnnotations('nameespaceThatDoesNotExist');
|
||||
openmct.annotation.setNamespaceToSaveAnnotations('namespaceThatDoesNotExist');
|
||||
await openmct.annotation.create(annotationCreationArguments);
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined();
|
||||
@@ -184,7 +183,7 @@ describe('The Annotation API', () => {
|
||||
tags: ['sometag'],
|
||||
contentText: 'fooContext',
|
||||
targetDomainObjects: [mockDomainObject],
|
||||
targets: [{ keyString: 'fooTarget' }]
|
||||
targets: { fooTarget: {} }
|
||||
};
|
||||
openmct.annotation.setNamespaceToSaveAnnotations('immutableProvider');
|
||||
await openmct.annotation.create(annotationCreationArguments);
|
||||
@@ -203,7 +202,7 @@ describe('The Annotation API', () => {
|
||||
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
|
||||
tags: ['aWonderfulTag'],
|
||||
contentText: 'fooContext',
|
||||
targets: [{ keyString: 'fooNameSpace:some-object', entryId: 'fooBarEntry' }],
|
||||
targets: { 'fooNameSpace:some-object': { entryId: 'fooBarEntry' } },
|
||||
targetDomainObjects: [mockDomainObject]
|
||||
};
|
||||
});
|
||||
@@ -273,19 +272,17 @@ describe('The Annotation API', () => {
|
||||
let comparator;
|
||||
|
||||
beforeEach(() => {
|
||||
targets = [
|
||||
{
|
||||
keyString: 'fooTarget',
|
||||
targets = {
|
||||
fooTarget: {
|
||||
foo: 42
|
||||
}
|
||||
];
|
||||
otherTargets = [
|
||||
{
|
||||
keyString: 'fooTarget',
|
||||
};
|
||||
otherTargets = {
|
||||
fooTarget: {
|
||||
bar: 42
|
||||
}
|
||||
];
|
||||
comparator = (t1, t2) => t1[0].foo === t2[0].bar;
|
||||
};
|
||||
comparator = (t1, t2) => t1.fooTarget.foo === t2.fooTarget.bar;
|
||||
});
|
||||
|
||||
it('can add a comparator function', () => {
|
||||
|
||||
@@ -185,10 +185,9 @@ export default class CompositionProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
const onMutation = this.#onMutation.bind(this);
|
||||
this.#publicAPI.objects.eventEmitter.on('mutation', onMutation);
|
||||
this.#publicAPI.objects.eventEmitter.on('mutation', this.#onMutation.bind(this));
|
||||
this.topicListener = () => {
|
||||
this.#publicAPI.objects.eventEmitter.off('mutation', onMutation);
|
||||
this.#publicAPI.objects.eventEmitter.off('mutation', this.#onMutation.bind(this));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export default class FormsAPI {
|
||||
* This function accepts element, model and onChange function
|
||||
* element - html element (place holder) to render a row view
|
||||
* model - row data for rendering name, value etc for given row type
|
||||
* onChange - an onChange event callback funtion to keep track of any change in value
|
||||
* onChange - an onChange event callback function to keep track of any change in value
|
||||
* @property {function} destroy a callback function when a vue component gets destroyed
|
||||
*/
|
||||
|
||||
|
||||
@@ -123,6 +123,7 @@ export default {
|
||||
formatDatetime(timestamp = this.model.value) {
|
||||
if (!timestamp) {
|
||||
this.resetValues();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -136,7 +137,7 @@ export default {
|
||||
|
||||
const data = {
|
||||
model,
|
||||
value: new Date(timestamp).toISOString()
|
||||
value: timestamp
|
||||
};
|
||||
|
||||
this.$emit('onChange', data);
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
import NotificationAPI from './NotificationAPI';
|
||||
|
||||
describe('The Notifiation API', () => {
|
||||
describe('The Notification API', () => {
|
||||
let notificationAPIInstance;
|
||||
let defaultTimeout = 4000;
|
||||
|
||||
@@ -108,7 +108,7 @@ describe('The Notifiation API', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('the error method notificiation', () => {
|
||||
describe('the error method notification', () => {
|
||||
let message = 'Minimized error message';
|
||||
|
||||
afterAll(() => {
|
||||
|
||||
@@ -435,8 +435,7 @@ class InMemorySearchProvider {
|
||||
}
|
||||
|
||||
localIndexAnnotation(objectToIndex, model) {
|
||||
model.targets.forEach((target) => {
|
||||
const targetID = target.keyString;
|
||||
Object.keys(model.targets).forEach((targetID) => {
|
||||
if (!this.localIndexedAnnotationsByDomainObject[targetID]) {
|
||||
this.localIndexedAnnotationsByDomainObject[targetID] = [];
|
||||
}
|
||||
|
||||
@@ -57,8 +57,7 @@
|
||||
};
|
||||
|
||||
function indexAnnotation(objectToIndex, model) {
|
||||
model.targets.forEach((target) => {
|
||||
const targetID = target.keyString;
|
||||
Object.keys(model.targets).forEach((targetID) => {
|
||||
if (!indexedAnnotationsByDomainObject[targetID]) {
|
||||
indexedAnnotationsByDomainObject[targetID] = [];
|
||||
}
|
||||
|
||||
@@ -103,6 +103,10 @@ export default class ObjectAPI {
|
||||
this.errors = {
|
||||
Conflict: ConflictError
|
||||
};
|
||||
this.calls = {
|
||||
get: {},
|
||||
makeKeyString: {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -177,6 +181,31 @@ export default class ObjectAPI {
|
||||
* has been updated, or be rejected if it cannot be saved
|
||||
*/
|
||||
|
||||
addCall(type, identifier) {
|
||||
let keyString;
|
||||
|
||||
if (!identifier) {
|
||||
throw new Error('Cannot make key string from null identifier logging');
|
||||
}
|
||||
|
||||
if (typeof identifier === 'string') {
|
||||
keyString = identifier;
|
||||
} else if (!identifier.namespace) {
|
||||
keyString = identifier.key;
|
||||
} else {
|
||||
keyString = [identifier.namespace.replace(/:/g, '\\:'), identifier.key].join(':');
|
||||
}
|
||||
|
||||
|
||||
if (this.calls[type][keyString]) {
|
||||
this.calls[type][keyString]++;
|
||||
} else {
|
||||
this.calls[type][keyString] = 1;
|
||||
}
|
||||
|
||||
Object.entries(this.calls.get).forEach(([key, value]) => console.log(`get: ${key}: ${value}`));
|
||||
Object.entries(this.calls.makeKeyString).forEach(([key, value]) => console.log(`makeKeyString: ${key}: ${value}`));
|
||||
}
|
||||
/**
|
||||
* Delete this domain object.
|
||||
*
|
||||
@@ -199,6 +228,7 @@ export default class ObjectAPI {
|
||||
* has been saved, or be rejected if it cannot be saved
|
||||
*/
|
||||
get(identifier, abortSignal, forceRemote = false) {
|
||||
this.addCall('get', identifier);
|
||||
let keystring = this.makeKeyString(identifier);
|
||||
|
||||
if (!forceRemote) {
|
||||
@@ -520,7 +550,7 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* Inovke interceptors if applicable for a given domain object.
|
||||
* Invoke interceptors if applicable for a given domain object.
|
||||
* @private
|
||||
*/
|
||||
applyGetInterceptors(identifier, domainObject) {
|
||||
@@ -729,6 +759,7 @@ export default class ObjectAPI {
|
||||
* @returns {string} A string representation of the given identifier, including namespace and key
|
||||
*/
|
||||
makeKeyString(identifier) {
|
||||
this.addCall('makeKeyString', identifier);
|
||||
return utils.makeKeyString(identifier);
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ describe('The Object API', () => {
|
||||
await objectAPI.save(mockDomainObject);
|
||||
expect(mockDomainObject.createdBy).toBe(USERNAME);
|
||||
});
|
||||
it("Sets the current user for 'modifedBy' on existing objects", async () => {
|
||||
it("Sets the current user for 'modifiedBy' on existing objects", async () => {
|
||||
mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;
|
||||
mockDomainObject.modified = Date.now();
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ export default class TelemetryAPI {
|
||||
|
||||
/**
|
||||
* Register a request interceptor that transforms a request via module:openmct.TelemetryAPI.request
|
||||
* The request will be modifyed when it is received and will be returned in it's modified state
|
||||
* The request will be modified when it is received and will be returned in it's modified state
|
||||
* The request will be transformed only if the interceptor is applicable to that domain object as defined by the RequestInterceptorDef
|
||||
*
|
||||
* @param {module:openmct.RequestInterceptorDef} requestInterceptorDef the request interceptor definition to add
|
||||
@@ -269,7 +269,7 @@ export default class TelemetryAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set greedy LAD. For stategy "latest" telemetry in
|
||||
* Get or set greedy LAD. For strategy "latest" telemetry in
|
||||
* realtime mode the start bound will be ignored if true and
|
||||
* there is no new data to replace the existing data.
|
||||
* defaults to true
|
||||
|
||||
@@ -92,7 +92,7 @@ define(['lodash'], function (_) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an array of valueMetadatas that posess all hints requested.
|
||||
* Get an array of valueMetadatas that possess all hints requested.
|
||||
* Array is sorted based on hint priority.
|
||||
*
|
||||
*/
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
* A Type describes a kind of domain object that may appear or be
|
||||
* created within Open MCT.
|
||||
*
|
||||
* @param {module:opemct.TypeRegistry~TypeDefinition} definition
|
||||
* @param {module:openmct.TypeRegistry~TypeDefinition} definition
|
||||
* @class Type
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
|
||||
@@ -43,7 +43,7 @@ class UserAPI extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Set the user provider for the user API. This allows you
|
||||
* to specifiy ONE user provider to be used with Open MCT.
|
||||
* to specify ONE user provider to be used with Open MCT.
|
||||
* @method setProvider
|
||||
* @memberof module:openmct.UserAPI#
|
||||
* @param {module:openmct.UserAPI~UserProvider} provider the new
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('The User API', () => {
|
||||
provider.autoLogin(USERNAME);
|
||||
});
|
||||
|
||||
it('to check if a user (not specific) is loged in', (done) => {
|
||||
it('to check if a user (not specific) is logged in', (done) => {
|
||||
expect(openmct.user.isLoggedIn()).toBeFalse();
|
||||
|
||||
openmct.user.on('providerAdded', () => {
|
||||
|
||||
@@ -35,8 +35,8 @@ describe('The Image Exporter', () => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
describe('basic instatation', () => {
|
||||
it('can be instatiated', () => {
|
||||
describe('basic instantiation', () => {
|
||||
it('can be instantiated', () => {
|
||||
imageExporter = new ImageExporter(openmct);
|
||||
|
||||
expect(imageExporter).not.toEqual(null);
|
||||
|
||||
@@ -65,7 +65,7 @@ export default {
|
||||
},
|
||||
hasUnits: {
|
||||
type: Boolean,
|
||||
requred: true
|
||||
required: true
|
||||
},
|
||||
isStale: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -127,7 +127,7 @@ describe('The LAD Table', () => {
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should reject non-telemtry producing objects', () => {
|
||||
it('should reject non-telemetry producing objects', () => {
|
||||
expect(() => {
|
||||
ladTableCompositionCollection.add(mockObj.ladTable);
|
||||
}).toThrow();
|
||||
@@ -254,10 +254,10 @@ describe('The LAD Table', () => {
|
||||
|
||||
it('should show aggregate telemetry type with blank data', async () => {
|
||||
await Vue.nextTick();
|
||||
const lastestData = parent
|
||||
const latestData = parent
|
||||
.querySelectorAll(TABLE_BODY_ROWS)[1]
|
||||
.querySelectorAll('td')[2].innerText;
|
||||
expect(lastestData).toBe('---');
|
||||
expect(latestData).toBe('---');
|
||||
const dataType = parent
|
||||
.querySelectorAll(TABLE_BODY_ROWS)[1]
|
||||
.querySelector('.js-type-data').innerText;
|
||||
|
||||
@@ -86,19 +86,16 @@ export default {
|
||||
handler: 'updateData'
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.registerListeners();
|
||||
},
|
||||
mounted() {
|
||||
this.plotResizeObserver.observe(this.$refs.plotWrapper);
|
||||
Plotly.newPlot(this.$refs.plot, Array.from(this.data), this.getLayout(), {
|
||||
responsive: true,
|
||||
displayModeBar: false
|
||||
});
|
||||
this.registerListeners();
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.plotResizeObserver) {
|
||||
this.plotResizeObserver.disconnect();
|
||||
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
|
||||
clearTimeout(this.resizeTimer);
|
||||
}
|
||||
|
||||
@@ -229,6 +226,7 @@ export default {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}, 250);
|
||||
});
|
||||
this.plotResizeObserver.observe(this.$refs.plotWrapper);
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
|
||||
@@ -158,7 +158,7 @@ export default {
|
||||
this.composition.remove(telemetryObject);
|
||||
},
|
||||
addTelemetryObject(telemetryObject) {
|
||||
// grab information we need from the added telmetry object
|
||||
// grab information we need from the added telemetry object
|
||||
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
this.telemetryObjects[key] = telemetryObject;
|
||||
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||
@@ -188,7 +188,7 @@ export default {
|
||||
);
|
||||
}
|
||||
|
||||
// ask for the current telemetry data, then subcribe for changes
|
||||
// ask for the current telemetry data, then subscribe for changes
|
||||
this.requestDataFor(telemetryObject);
|
||||
this.subscribeToObject(telemetryObject);
|
||||
},
|
||||
|
||||
@@ -43,16 +43,17 @@ export default function BarGraphViewProvider(openmct) {
|
||||
return domainObject && domainObject.type === BAR_GRAPH_KEY;
|
||||
},
|
||||
|
||||
view(domainObject, objectPath) {
|
||||
view: function (domainObject, objectPath) {
|
||||
let _destroy = null;
|
||||
let component = null;
|
||||
|
||||
return {
|
||||
show(element) {
|
||||
show: function (element) {
|
||||
let isCompact = isCompactView(objectPath);
|
||||
|
||||
const { vNode, destroy } = mount(
|
||||
{
|
||||
el: element,
|
||||
components: {
|
||||
BarGraphView
|
||||
},
|
||||
@@ -78,7 +79,7 @@ export default function BarGraphViewProvider(openmct) {
|
||||
_destroy = destroy;
|
||||
component = vNode.componentInstance;
|
||||
},
|
||||
destroy() {
|
||||
destroy: function () {
|
||||
_destroy();
|
||||
},
|
||||
onClearData() {
|
||||
|
||||
@@ -220,7 +220,7 @@ describe('the plugin', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('The spectral plot view for telemetry objects with array values', () => {
|
||||
xdescribe('The spectral plot view for telemetry objects with array values', () => {
|
||||
let barGraphObject;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let mockComposition;
|
||||
@@ -256,7 +256,7 @@ describe('the plugin', function () {
|
||||
await Vue.nextTick();
|
||||
});
|
||||
|
||||
it('Renders spectral plots', async () => {
|
||||
it('Renders spectral plots', () => {
|
||||
const dotFullTelemetryObject = {
|
||||
identifier: {
|
||||
namespace: 'someNamespace',
|
||||
@@ -304,12 +304,11 @@ describe('the plugin', function () {
|
||||
barGraphView.show(child, true);
|
||||
mockComposition.emit('add', dotFullTelemetryObject);
|
||||
|
||||
await Vue.nextTick();
|
||||
await Vue.nextTick();
|
||||
|
||||
const plotElement = element.querySelector('.cartesianlayer .scatterlayer .trace .lines');
|
||||
expect(plotElement).not.toBeNull();
|
||||
barGraphView.destroy();
|
||||
return Vue.nextTick().then(() => {
|
||||
const plotElement = element.querySelector('.cartesianlayer .scatterlayer .trace .lines');
|
||||
expect(plotElement).not.toBeNull();
|
||||
barGraphView.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ export default {
|
||||
this.composition.remove(telemetryObject);
|
||||
},
|
||||
addTelemetryObject(telemetryObject) {
|
||||
// grab information we need from the added telmetry object
|
||||
// grab information we need from the added telemetry object
|
||||
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
this.telemetryObjects[key] = telemetryObject;
|
||||
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||
|
||||
@@ -115,7 +115,7 @@ export default {
|
||||
}
|
||||
|
||||
if (this.plotResizeObserver) {
|
||||
this.plotResizeObserver.disconnect();
|
||||
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
|
||||
clearTimeout(this.resizeTimer);
|
||||
}
|
||||
|
||||
|
||||
@@ -65,9 +65,6 @@ 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) {
|
||||
@@ -198,15 +195,15 @@ export default class Condition extends EventEmitter {
|
||||
if (found) {
|
||||
const newCriterionConfiguration = this.generateCriterion(criterionConfiguration);
|
||||
let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct);
|
||||
newCriterion.on('criterionUpdated', this.handleCriterionUpdated);
|
||||
newCriterion.on('telemetryIsOld', this.handleOldTelemetryCriterion);
|
||||
newCriterion.on('telemetryStaleness', this.handleTelemetryStaleness);
|
||||
newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
newCriterion.on('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
|
||||
newCriterion.on('telemetryStaleness', () => this.handleTelemetryStaleness());
|
||||
|
||||
let criterion = found.item;
|
||||
criterion.unsubscribe();
|
||||
criterion.off('criterionUpdated', this.handleCriterionUpdated);
|
||||
criterion.off('telemetryIsOld', this.handleOldTelemetryCriterion);
|
||||
newCriterion.off('telemetryStaleness', this.handleTelemetryStaleness);
|
||||
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
criterion.off('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
|
||||
newCriterion.off('telemetryStaleness', () => this.handleTelemetryStaleness());
|
||||
this.criteria.splice(found.index, 1, newCriterion);
|
||||
}
|
||||
}
|
||||
@@ -215,9 +212,9 @@ export default class Condition extends EventEmitter {
|
||||
let found = this.findCriterion(id);
|
||||
if (found) {
|
||||
let criterion = found.item;
|
||||
criterion.off('criterionUpdated', this.handleCriterionUpdated);
|
||||
criterion.off('telemetryIsOld', this.handleOldTelemetryCriterion);
|
||||
criterion.off('telemetryStaleness', this.handleTelemetryStaleness);
|
||||
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
criterion.off('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
|
||||
criterion.off('telemetryStaleness', () => this.handleTelemetryStaleness());
|
||||
criterion.destroy();
|
||||
this.criteria.splice(found.index, 1);
|
||||
|
||||
|
||||
@@ -399,7 +399,7 @@ export default class ConditionManager extends EventEmitter {
|
||||
}
|
||||
|
||||
shouldEvaluateNewTelemetry(currentTimestamp) {
|
||||
return this.openmct.time.bounds().end >= currentTimestamp;
|
||||
return this.openmct.time.getBounds().end >= currentTimestamp;
|
||||
}
|
||||
|
||||
telemetryReceived(endpoint, datum) {
|
||||
|
||||
@@ -127,7 +127,6 @@ 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();
|
||||
}
|
||||
},
|
||||
unmounted() {
|
||||
beforeUnmount() {
|
||||
this.stopListeningToConditionSetChanges();
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -170,7 +170,7 @@ define(['lodash'], function (_) {
|
||||
if (form) {
|
||||
showForm(form, name, selectionPath);
|
||||
} else {
|
||||
openmct.objectViews.emit('contextAction', 'addElement', name);
|
||||
selectionPath[0].context.addElement(name);
|
||||
}
|
||||
},
|
||||
key: 'add',
|
||||
@@ -236,6 +236,7 @@ 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?`,
|
||||
@@ -244,11 +245,7 @@ define(['lodash'], function (_) {
|
||||
label: 'OK',
|
||||
emphasis: 'true',
|
||||
callback: function () {
|
||||
openmct.objectViews.emit(
|
||||
'contextAction',
|
||||
'removeItem',
|
||||
getAllTypes(selection)
|
||||
);
|
||||
removeItem(getAllTypes(selection));
|
||||
prompt.dismiss();
|
||||
}
|
||||
},
|
||||
@@ -293,12 +290,7 @@ define(['lodash'], function (_) {
|
||||
}
|
||||
],
|
||||
method: function (option) {
|
||||
openmct.objectViews.emit(
|
||||
'contextAction',
|
||||
'orderItem',
|
||||
option.value,
|
||||
getAllTypes(selectedObjects)
|
||||
);
|
||||
selectionPath[1].context.orderItem(option.value, getAllTypes(selectedObjects));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -482,7 +474,9 @@ define(['lodash'], function (_) {
|
||||
icon: 'icon-duplicate',
|
||||
title: 'Duplicate the selected object',
|
||||
method: function () {
|
||||
openmct.objectViews.emit('contextAction', 'duplicateItem', selection);
|
||||
let duplicateItem = selectionPath[1].context.duplicateItem;
|
||||
|
||||
duplicateItem(selection);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -561,7 +555,6 @@ 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;
|
||||
@@ -581,18 +574,14 @@ define(['lodash'], function (_) {
|
||||
label: 'View type',
|
||||
options: viewOptions,
|
||||
method: function (option) {
|
||||
openmct.objectViews.emit(
|
||||
'contextAction',
|
||||
'switchViewType',
|
||||
selectedItemContext,
|
||||
option.value,
|
||||
selection
|
||||
);
|
||||
displayLayoutContext.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,
|
||||
@@ -601,15 +590,12 @@ define(['lodash'], function (_) {
|
||||
label: 'View type',
|
||||
options: APPLICABLE_VIEWS['telemetry-view-multi'],
|
||||
method: function (option) {
|
||||
openmct.objectViews.emit(
|
||||
'contextAction',
|
||||
'mergeMultipleTelemetryViews',
|
||||
selection,
|
||||
option.value
|
||||
);
|
||||
displayLayoutContext.mergeMultipleTelemetryViews(selection, option.value);
|
||||
}
|
||||
};
|
||||
} else if (areAllViews('telemetry.plot.overlay', 'item.type', selection)) {
|
||||
let displayLayoutContext = selectionPath[1].context;
|
||||
|
||||
return {
|
||||
control: 'menu',
|
||||
domainObject: selectedParent,
|
||||
@@ -617,12 +603,7 @@ define(['lodash'], function (_) {
|
||||
title: 'Merge into a stacked plot',
|
||||
options: APPLICABLE_VIEWS['telemetry.plot.overlay-multi'],
|
||||
method: function (option) {
|
||||
openmct.objectViews.emit(
|
||||
'contextAction',
|
||||
'mergeMultipleOverlayPlots',
|
||||
selection,
|
||||
option.value
|
||||
);
|
||||
displayLayoutContext.mergeMultipleOverlayPlots(selection, option.value);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -646,7 +627,7 @@ define(['lodash'], function (_) {
|
||||
domainObject: displayLayoutContext.item,
|
||||
icon: ICON_GRID_SHOW,
|
||||
method: function () {
|
||||
openmct.objectViews.emit('contextAction', 'toggleGrid');
|
||||
displayLayoutContext.toggleGrid();
|
||||
|
||||
this.icon = this.icon === ICON_GRID_SHOW ? ICON_GRID_HIDE : ICON_GRID_SHOW;
|
||||
},
|
||||
@@ -672,7 +653,7 @@ define(['lodash'], function (_) {
|
||||
|
||||
function showForm(formStructure, name, selectionPath) {
|
||||
openmct.forms.showForm(formStructure).then((changes) => {
|
||||
openmct.objectViews.emit('contextAction', 'addElement', name, changes);
|
||||
selectionPath[0].context.addElement(name, changes);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -25,16 +25,14 @@
|
||||
:item="item"
|
||||
:grid-size="gridSize"
|
||||
:is-editing="isEditing"
|
||||
@move="move"
|
||||
@endMove="endMove"
|
||||
@move="(gridDelta) => $emit('move', gridDelta)"
|
||||
@endMove="() => $emit('endMove')"
|
||||
>
|
||||
<template #content>
|
||||
<div
|
||||
class="c-box-view u-style-receiver js-style-receiver"
|
||||
:class="[styleClass]"
|
||||
:style="style"
|
||||
></div>
|
||||
</template>
|
||||
<div
|
||||
class="c-box-view u-style-receiver js-style-receiver"
|
||||
:class="[styleClass]"
|
||||
:style="style"
|
||||
></div>
|
||||
</layout-frame>
|
||||
</template>
|
||||
|
||||
@@ -117,18 +115,10 @@ export default {
|
||||
this.initSelect
|
||||
);
|
||||
},
|
||||
beforeUnmount() {
|
||||
unmounted() {
|
||||
if (this.removeSelectable) {
|
||||
this.removeSelectable();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
move(gridDelta) {
|
||||
this.$emit('move', gridDelta);
|
||||
},
|
||||
endMove() {
|
||||
this.$emit('endMove');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -144,7 +144,7 @@ function getItemDefinition(itemType, ...options) {
|
||||
|
||||
export default {
|
||||
components: components,
|
||||
inject: ['openmct', 'objectPath', 'options', 'currentView'],
|
||||
inject: ['openmct', 'objectPath', 'options', 'objectUtils', 'currentView'],
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
@@ -221,21 +221,13 @@ export default {
|
||||
this.composition.load();
|
||||
this.gridDimensions = [this.$el.offsetWidth, this.$el.scrollHeight];
|
||||
|
||||
this.unObserveItems = this.openmct.objects.observe(
|
||||
this.domainObject,
|
||||
'configuration.items',
|
||||
(items) => {
|
||||
this.layoutItems = [...items];
|
||||
}
|
||||
);
|
||||
this.openmct.objects.observe(this.domainObject, 'configuration.items', (items) => {
|
||||
this.layoutItems = items;
|
||||
});
|
||||
|
||||
this.watchDisplayResize();
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.unObserveItems) {
|
||||
this.unObserveItems();
|
||||
}
|
||||
this.unwatchDisplayResize();
|
||||
unmounted() {
|
||||
this.openmct.selection.off('change', this.setSelection);
|
||||
this.composition.off('add', this.addChild);
|
||||
this.composition.off('remove', this.removeChild);
|
||||
@@ -259,15 +251,9 @@ export default {
|
||||
this.$el.click();
|
||||
},
|
||||
watchDisplayResize() {
|
||||
this.unwatchDisplayResize();
|
||||
this.resizeObserver = new ResizeObserver(this.updateGrid);
|
||||
const resizeObserver = new ResizeObserver(() => this.updateGrid());
|
||||
|
||||
this.resizeObserver.observe(this.$el);
|
||||
},
|
||||
unwatchDisplayResize() {
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect();
|
||||
}
|
||||
resizeObserver.observe(this.$el);
|
||||
},
|
||||
addElement(itemType, element) {
|
||||
this.addItem(itemType + '-view', element);
|
||||
@@ -637,7 +623,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,16 +25,14 @@
|
||||
:item="item"
|
||||
:grid-size="gridSize"
|
||||
:is-editing="isEditing"
|
||||
@move="move"
|
||||
@endMove="endMove"
|
||||
@move="(gridDelta) => $emit('move', gridDelta)"
|
||||
@endMove="() => $emit('endMove')"
|
||||
>
|
||||
<template #content>
|
||||
<div
|
||||
class="c-ellipse-view u-style-receiver js-style-receiver"
|
||||
:class="[styleClass]"
|
||||
:style="style"
|
||||
></div>
|
||||
</template>
|
||||
<div
|
||||
class="c-ellipse-view u-style-receiver js-style-receiver"
|
||||
:class="[styleClass]"
|
||||
:style="style"
|
||||
></div>
|
||||
</layout-frame>
|
||||
</template>
|
||||
|
||||
@@ -117,18 +115,10 @@ export default {
|
||||
this.initSelect
|
||||
);
|
||||
},
|
||||
beforeUnmount() {
|
||||
unmounted() {
|
||||
if (this.removeSelectable) {
|
||||
this.removeSelectable();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
move(gridDelta) {
|
||||
this.$emit('move', gridDelta);
|
||||
},
|
||||
endMove() {
|
||||
this.$emit('endMove');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -25,12 +25,10 @@
|
||||
:item="item"
|
||||
:grid-size="gridSize"
|
||||
:is-editing="isEditing"
|
||||
@move="move"
|
||||
@endMove="endMove"
|
||||
@move="(gridDelta) => $emit('move', gridDelta)"
|
||||
@endMove="() => $emit('endMove')"
|
||||
>
|
||||
<template #content>
|
||||
<div class="c-image-view" :style="style"></div>
|
||||
</template>
|
||||
<div class="c-image-view" :class="[styleClass]" :style="style"></div>
|
||||
</layout-frame>
|
||||
</template>
|
||||
|
||||
@@ -120,18 +118,10 @@ export default {
|
||||
this.initSelect
|
||||
);
|
||||
},
|
||||
beforeUnmount() {
|
||||
unmounted() {
|
||||
if (this.removeSelectable) {
|
||||
this.removeSelectable();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
move(gridDelta) {
|
||||
this.$emit('move', gridDelta);
|
||||
},
|
||||
endMove() {
|
||||
this.$emit('endMove');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
}"
|
||||
:style="style"
|
||||
>
|
||||
<slot name="content"></slot>
|
||||
<slot></slot>
|
||||
<div class="c-frame__move-bar" @mousedown.left="startMove($event)"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -20,26 +20,24 @@
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<template>
|
||||
<layout-frame
|
||||
<LayoutFrame
|
||||
:item="item"
|
||||
:grid-size="gridSize"
|
||||
:is-editing="isEditing"
|
||||
@move="move"
|
||||
@endMove="endMove"
|
||||
@move="(gridDelta) => $emit('move', gridDelta)"
|
||||
@endMove="() => $emit('endMove')"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<script>
|
||||
@@ -106,7 +104,8 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
domainObject: undefined,
|
||||
currentObjectPath: []
|
||||
currentObjectPath: [],
|
||||
mutablePromise: undefined
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@@ -169,12 +168,6 @@ export default {
|
||||
delete this.immediatelySelect;
|
||||
}
|
||||
});
|
||||
},
|
||||
move(gridDelta) {
|
||||
this.$emit('move', gridDelta);
|
||||
},
|
||||
endMove() {
|
||||
this.$emit('endMove');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,44 +25,42 @@
|
||||
:item="item"
|
||||
:grid-size="gridSize"
|
||||
:is-editing="isEditing"
|
||||
@move="move"
|
||||
@endMove="endMove"
|
||||
@move="(gridDelta) => $emit('move', gridDelta)"
|
||||
@endMove="() => $emit('endMove')"
|
||||
>
|
||||
<template #content>
|
||||
<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>
|
||||
|
||||
<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
|
||||
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>
|
||||
|
||||
<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>
|
||||
</layout-frame>
|
||||
</template>
|
||||
|
||||
@@ -389,12 +387,6 @@ 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,20 +25,18 @@
|
||||
:item="item"
|
||||
:grid-size="gridSize"
|
||||
:is-editing="isEditing"
|
||||
@move="move"
|
||||
@endMove="endMove"
|
||||
@move="(gridDelta) => $emit('move', gridDelta)"
|
||||
@endMove="() => $emit('endMove')"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
</layout-frame>
|
||||
</template>
|
||||
|
||||
@@ -129,18 +127,10 @@ export default {
|
||||
this.initSelect
|
||||
);
|
||||
},
|
||||
beforeUnmount() {
|
||||
unmounted() {
|
||||
if (this.removeSelectable) {
|
||||
this.removeSelectable();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
move(gridDelta) {
|
||||
this.$emit('move', gridDelta);
|
||||
},
|
||||
endMove() {
|
||||
this.$emit('endMove');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -20,14 +20,14 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import mount from 'utils/mount';
|
||||
|
||||
import CopyToClipboardAction from './actions/CopyToClipboardAction';
|
||||
import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js';
|
||||
import CopyToClipboardAction from './actions/CopyToClipboardAction';
|
||||
import DisplayLayout from './components/DisplayLayout.vue';
|
||||
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
|
||||
import DisplayLayoutType from './DisplayLayoutType.js';
|
||||
import DisplayLayoutDrawingObjectTypes from './DrawingObjectTypes.js';
|
||||
import objectUtils from 'objectUtils';
|
||||
import mount from 'utils/mount';
|
||||
|
||||
class DisplayLayoutView {
|
||||
constructor(openmct, domainObject, objectPath, options) {
|
||||
@@ -37,6 +37,7 @@ class DisplayLayoutView {
|
||||
this.options = options;
|
||||
|
||||
this.component = null;
|
||||
this.app = null;
|
||||
}
|
||||
|
||||
show(container, isEditing) {
|
||||
@@ -50,6 +51,7 @@ class DisplayLayoutView {
|
||||
openmct: this.openmct,
|
||||
objectPath: this.objectPath,
|
||||
options: this.options,
|
||||
objectUtils,
|
||||
currentView: this
|
||||
},
|
||||
data: () => {
|
||||
@@ -81,17 +83,20 @@ class DisplayLayoutView {
|
||||
getSelectionContext() {
|
||||
return {
|
||||
item: this.domainObject,
|
||||
supportsMultiSelect: true
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -99,7 +104,6 @@ class DisplayLayoutView {
|
||||
destroy() {
|
||||
if (this._destroy) {
|
||||
this._destroy();
|
||||
this.component = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ describe('the plugin', function () {
|
||||
};
|
||||
displayLayoutItem = {
|
||||
composition: [
|
||||
// no item in compostion, but item in configuration items
|
||||
// no item in composition, but item in configuration items
|
||||
],
|
||||
configuration: {
|
||||
items: [item],
|
||||
@@ -165,7 +165,7 @@ describe('the plugin', function () {
|
||||
Vue.nextTick(done);
|
||||
});
|
||||
|
||||
it('will sync compostion and layout items', () => {
|
||||
it('will sync composition and layout items', () => {
|
||||
expect(displayLayoutItem.configuration.items.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('Export as JSON plugin', () => {
|
||||
expect(exportAsJSONAction.appliesTo([domainObject])).toEqual(true);
|
||||
});
|
||||
|
||||
it('ExportAsJSONAction does not applie to non-persistable objects', () => {
|
||||
it('ExportAsJSONAction does not apply to non-persistable objects', () => {
|
||||
domainObject = {
|
||||
identifier: {
|
||||
key: 'export-testing',
|
||||
@@ -212,7 +212,7 @@ describe('Export as JSON plugin', () => {
|
||||
const parent = {
|
||||
composition: [
|
||||
{
|
||||
key: 'infinteChild',
|
||||
key: 'infiniteChild',
|
||||
namespace: ''
|
||||
}
|
||||
],
|
||||
@@ -235,7 +235,7 @@ describe('Export as JSON plugin', () => {
|
||||
}
|
||||
],
|
||||
identifier: {
|
||||
key: 'infinteChild',
|
||||
key: 'infiniteChild',
|
||||
namespace: ''
|
||||
},
|
||||
name: 'child',
|
||||
@@ -265,7 +265,7 @@ describe('Export as JSON plugin', () => {
|
||||
Object.prototype.hasOwnProperty.call(completedTree.openmct, 'infiniteParent')
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
Object.prototype.hasOwnProperty.call(completedTree.openmct, 'infinteChild')
|
||||
Object.prototype.hasOwnProperty.call(completedTree.openmct, 'infiniteChild')
|
||||
).toBeTruthy();
|
||||
|
||||
done();
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<div class="c-inspect-properties__header">Fault Details</div>
|
||||
<ul class="c-inspect-properties__section">
|
||||
<DetailText :detail="sourceDetails" />
|
||||
<DetailText :detail="occuredDetails" />
|
||||
<DetailText :detail="occurredDetails" />
|
||||
<DetailText :detail="criticalityDetails" />
|
||||
<DetailText :detail="descriptionDetails" />
|
||||
</ul>
|
||||
@@ -72,9 +72,9 @@ export default {
|
||||
value: this.selectedFault?.shortDescription
|
||||
};
|
||||
},
|
||||
occuredDetails() {
|
||||
occurredDetails() {
|
||||
return {
|
||||
name: 'Occured',
|
||||
name: 'Occurred',
|
||||
value: this.selectedFault?.triggerTime
|
||||
};
|
||||
},
|
||||
|
||||
@@ -122,6 +122,7 @@ export default {
|
||||
mounted() {
|
||||
let context = {
|
||||
item: this.$parent.domainObject,
|
||||
addContainer: this.addContainer,
|
||||
type: 'container',
|
||||
containerId: this.container.id
|
||||
};
|
||||
|
||||
@@ -164,32 +164,16 @@ export default {
|
||||
this.composition.on('remove', this.removeChildObject);
|
||||
this.composition.on('add', this.addFrame);
|
||||
this.composition.load();
|
||||
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;
|
||||
}
|
||||
);
|
||||
this.openmct.objects.observe(this.domainObject, 'configuration.containers', (containers) => {
|
||||
this.containers = containers;
|
||||
});
|
||||
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,9 +142,6 @@ 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);
|
||||
},
|
||||
beforeUnmount() {
|
||||
unmounted() {
|
||||
document.removeEventListener('dragstart', this.setDragging);
|
||||
document.removeEventListener('dragend', this.unsetDragging);
|
||||
document.removeEventListener('drop', this.unsetDragging);
|
||||
|
||||
@@ -77,15 +77,12 @@ 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,6 +89,8 @@ 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?`,
|
||||
@@ -97,11 +99,7 @@ function ToolbarProvider(openmct) {
|
||||
label: 'OK',
|
||||
emphasis: 'true',
|
||||
callback: function () {
|
||||
openmct.objectViews.emit(
|
||||
'contextAction',
|
||||
'deleteFrame',
|
||||
primary.context.frameId
|
||||
);
|
||||
deleteFrameAction(primary.context.frameId);
|
||||
prompt.dismiss();
|
||||
}
|
||||
},
|
||||
@@ -138,9 +136,7 @@ function ToolbarProvider(openmct) {
|
||||
addContainer = {
|
||||
control: 'button',
|
||||
domainObject: tertiary.context.item,
|
||||
method: function () {
|
||||
openmct.objectViews.emit('contextAction', 'addContainer', ...arguments);
|
||||
},
|
||||
method: tertiary.context.addContainer,
|
||||
key: 'add',
|
||||
icon: 'icon-plus-in-rect',
|
||||
title: 'Add Container'
|
||||
@@ -156,6 +152,7 @@ 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({
|
||||
@@ -167,7 +164,7 @@ function ToolbarProvider(openmct) {
|
||||
label: 'OK',
|
||||
emphasis: 'true',
|
||||
callback: function () {
|
||||
openmct.objectViews.emit('contextAction', 'deleteContainer', containerId);
|
||||
removeContainer(containerId);
|
||||
prompt.dismiss();
|
||||
}
|
||||
},
|
||||
@@ -188,9 +185,7 @@ function ToolbarProvider(openmct) {
|
||||
addContainer = {
|
||||
control: 'button',
|
||||
domainObject: secondary.context.item,
|
||||
method: function () {
|
||||
openmct.objectViews.emit('contextAction', 'addContainer', ...arguments);
|
||||
},
|
||||
method: secondary.context.addContainer,
|
||||
key: 'add',
|
||||
icon: 'icon-plus-in-rect',
|
||||
title: 'Add Container'
|
||||
@@ -203,9 +198,7 @@ function ToolbarProvider(openmct) {
|
||||
addContainer = {
|
||||
control: 'button',
|
||||
domainObject: primary.context.item,
|
||||
method: function () {
|
||||
openmct.objectViews.emit('contextAction', 'addContainer', ...arguments);
|
||||
},
|
||||
method: primary.context.addContainer,
|
||||
key: 'add',
|
||||
icon: 'icon-plus-in-rect',
|
||||
title: 'Add Container'
|
||||
|
||||
@@ -33,8 +33,6 @@
|
||||
|
||||
<script>
|
||||
import Flatbush from 'flatbush';
|
||||
import { toRaw } from 'vue';
|
||||
|
||||
const EXISTING_ANNOTATION_STROKE_STYLE = '#D79078';
|
||||
const EXISTING_ANNOTATION_FILL_STYLE = 'rgba(202, 202, 142, 0.2)';
|
||||
const SELECTED_ANNOTATION_STROKE_COLOR = '#BD8ECC';
|
||||
@@ -72,9 +70,7 @@ export default {
|
||||
// create a flatbush index for the annotations
|
||||
const builtAnnotationsIndex = new Flatbush(this.imageryAnnotations.length);
|
||||
this.imageryAnnotations.forEach((annotation) => {
|
||||
const annotationRectangle = annotation.targets.find(
|
||||
(target) => target.keyString === this.keyString
|
||||
)?.rectangle;
|
||||
const annotationRectangle = annotation.targets[this.keyString].rectangle;
|
||||
const annotationRectangleForPixelDepth =
|
||||
this.transformRectangleToPixelDense(annotationRectangle);
|
||||
const indexNumber = builtAnnotationsIndex.add(
|
||||
@@ -145,17 +141,20 @@ export default {
|
||||
this.prepareExistingAnnotationSelection(incomingSelectedAnnotations);
|
||||
},
|
||||
prepareExistingAnnotationSelection(annotations) {
|
||||
const targetDetails = [];
|
||||
const targetDomainObjects = {};
|
||||
targetDomainObjects[this.keyString] = this.domainObject;
|
||||
|
||||
const targetDetails = {};
|
||||
annotations.forEach((annotation) => {
|
||||
annotation.targets.forEach((target) => {
|
||||
targetDetails.push(toRaw(target));
|
||||
Object.entries(annotation.targets).forEach(([key, value]) => {
|
||||
targetDetails[key] = value;
|
||||
});
|
||||
});
|
||||
this.selectedAnnotations = annotations;
|
||||
this.drawAnnotations();
|
||||
|
||||
return {
|
||||
targetDomainObjects: [this.domainObject],
|
||||
targetDomainObjects,
|
||||
targetDetails
|
||||
};
|
||||
},
|
||||
@@ -293,6 +292,9 @@ export default {
|
||||
this.dragging = false;
|
||||
this.selectedAnnotations = [];
|
||||
|
||||
const targetDomainObjects = {};
|
||||
targetDomainObjects[this.keyString] = this.domainObject;
|
||||
const targetDetails = {};
|
||||
const rectangleFromCanvas = {
|
||||
x: this.newAnnotationRectangle.x,
|
||||
y: this.newAnnotationRectangle.y,
|
||||
@@ -300,16 +302,13 @@ export default {
|
||||
height: this.newAnnotationRectangle.height
|
||||
};
|
||||
const rectangleWithoutPixelScale = this.transformRectangleFromPixelDense(rectangleFromCanvas);
|
||||
const targetDetails = [
|
||||
{
|
||||
rectangle: rectangleWithoutPixelScale,
|
||||
time: this.image.time,
|
||||
keyString: this.keyString
|
||||
}
|
||||
];
|
||||
targetDetails[this.keyString] = {
|
||||
rectangle: rectangleWithoutPixelScale,
|
||||
time: this.image.time
|
||||
};
|
||||
this.selectImageAnnotations({
|
||||
targetDetails,
|
||||
targetDomainObjects: [this.domainObject],
|
||||
targetDomainObjects,
|
||||
annotations: []
|
||||
});
|
||||
},
|
||||
@@ -404,10 +403,9 @@ export default {
|
||||
if (annotation._deleted) {
|
||||
return;
|
||||
}
|
||||
const annotationRectangle = annotation.targets.find(
|
||||
(target) => target.keyString === this.keyString
|
||||
)?.rectangle;
|
||||
const rectangleForPixelDensity = this.transformRectangleToPixelDense(annotationRectangle);
|
||||
const rectangleForPixelDensity = this.transformRectangleToPixelDense(
|
||||
annotation.targets[this.keyString].rectangle
|
||||
);
|
||||
if (this.isSelectedAnnotation(annotation)) {
|
||||
this.drawRectInCanvas(
|
||||
rectangleForPixelDensity,
|
||||
|
||||
@@ -727,8 +727,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// remove all eventListeners
|
||||
this.stopListening();
|
||||
this.stopListening(this.focusedImageWrapper, 'wheel', this.wheelZoom, this);
|
||||
|
||||
Object.keys(this.imageryAnnotations).forEach((time) => {
|
||||
const imageAnnotationsForTime = this.imageryAnnotations[time];
|
||||
@@ -790,7 +789,7 @@ export default {
|
||||
}
|
||||
},
|
||||
expand() {
|
||||
// check for modifier keys so it doesnt interfere with the layout
|
||||
// check for modifier keys so it doesn't interfere with the layout
|
||||
if (this.cursorStates.modifierKeyPressed) {
|
||||
return;
|
||||
}
|
||||
@@ -865,7 +864,6 @@ export default {
|
||||
if (this.domainObject.configuration) {
|
||||
const persistedLayers = this.domainObject.configuration.layers;
|
||||
if (!persistedLayers) {
|
||||
this.layers.forEach((layer) => (layer.visible = false));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1277,9 +1275,6 @@ export default {
|
||||
this.scrollHandler();
|
||||
},
|
||||
setSizedImageDimensions() {
|
||||
if (!this.$refs.focusedImage) {
|
||||
return;
|
||||
}
|
||||
this.focusedImageNaturalAspectRatio =
|
||||
this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight;
|
||||
if (
|
||||
|
||||
@@ -27,7 +27,9 @@
|
||||
: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>
|
||||
|
||||
@@ -81,7 +81,7 @@ export default class RelatedTelemetry {
|
||||
this[key].realtime.telemetryObjectId &&
|
||||
this[key].realtime.telemetryObjectId !== ''
|
||||
) {
|
||||
await this._intializeRealtime(key);
|
||||
await this._initializeRealtime(key);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -131,7 +131,7 @@ export default class RelatedTelemetry {
|
||||
}
|
||||
}
|
||||
|
||||
async _intializeRealtime(key) {
|
||||
async _initializeRealtime(key) {
|
||||
this[key].realtimeDomainObject = await this._openmct.objects.get(
|
||||
this[key].realtime.telemetryObjectId
|
||||
);
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="c-data-visualization-inspect-properties c-inspector__data-pivot c-data-visualization-inspector__flex-column"
|
||||
>
|
||||
<div class="c-inspect-properties">
|
||||
<div class="c-inspect-properties__header">Data Visualization</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoading" class="c-inspector__data-pivot-placeholder">Loading...</div>
|
||||
|
||||
<div v-else-if="hasDataRanges">
|
||||
<div
|
||||
v-if="selectedDataRange !== undefined && hasDescription"
|
||||
class="c-inspector__data-pivot-coordinates-wrapper"
|
||||
>
|
||||
<span
|
||||
class="c-tree__item__type-icon c-object-label__type-icon"
|
||||
:class="description.icon"
|
||||
></span>
|
||||
<span class="c-inspector__data-pivot-coordinates">
|
||||
{{ description.text }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<select v-model="selectedDataRangeIndex" class="c-inspector__data-pivot-range-selector">
|
||||
<option
|
||||
v-for="(dataRange, index) in descendingDataRanges"
|
||||
:key="index"
|
||||
:value="index"
|
||||
:selected="selectedDataRangeIndex === index"
|
||||
>
|
||||
{{ displayDataRange(dataRange) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="dataRanges && dataRanges.length === 0"
|
||||
class="c-inspector__data-pivot-placeholder"
|
||||
>
|
||||
No data for the current {{ description.name }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="hasPlaceholderText" class="c-inspector__data-pivot-placeholder">
|
||||
{{ placeholderText }}
|
||||
</div>
|
||||
|
||||
<template v-if="selectedBounds !== undefined">
|
||||
<NumericData
|
||||
:bounds="selectedBounds"
|
||||
:telemetry-keys="plotTelemetryKeys"
|
||||
:no-numeric-data-text="noNumericDataText"
|
||||
/>
|
||||
<Imagery v-if="hasImagery" :bounds="selectedBounds" :telemetry-keys="imageryTelemetryKeys" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Imagery from './Imagery.vue';
|
||||
import NumericData from './NumericData.vue';
|
||||
|
||||
const TIMESTAMP_VIEW_BUFFER = 30 * 1000;
|
||||
const timestampBufferText = `${TIMESTAMP_VIEW_BUFFER / 1000} seconds`;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NumericData,
|
||||
Imagery
|
||||
},
|
||||
inject: ['timeFormatter', 'placeholderText', 'plotOptions', 'imageryOptions'],
|
||||
props: {
|
||||
description: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
dataRanges: {
|
||||
type: Array,
|
||||
default: () => undefined
|
||||
},
|
||||
plotTelemetryKeys: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedDataRangeIndex: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasPlaceholderText() {
|
||||
return this.placeholderText.length > 0;
|
||||
},
|
||||
descendingDataRanges() {
|
||||
return this.dataRanges?.slice().reverse();
|
||||
},
|
||||
hasDescription() {
|
||||
return this.description?.text?.length > 0;
|
||||
},
|
||||
hasDataRanges() {
|
||||
return this.dataRanges?.length > 0;
|
||||
},
|
||||
selectedDataRange() {
|
||||
if (!this.hasDataRanges || this.selectedDataRangeIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.descendingDataRanges[this.selectedDataRangeIndex];
|
||||
},
|
||||
selectedBounds() {
|
||||
if (this.selectedDataRange === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { start, end } = this.selectedDataRange.bounds;
|
||||
|
||||
if (start === end) {
|
||||
return {
|
||||
start: start - TIMESTAMP_VIEW_BUFFER,
|
||||
end: end + TIMESTAMP_VIEW_BUFFER
|
||||
};
|
||||
}
|
||||
|
||||
return this.selectedDataRange.bounds;
|
||||
},
|
||||
imageryTelemetryKeys() {
|
||||
return this.imageryOptions?.telemetryKeys;
|
||||
},
|
||||
hasImagery() {
|
||||
return this.imageryTelemetryKeys?.length;
|
||||
},
|
||||
noNumericDataText() {
|
||||
return this.plotOptions?.noNumericDataText;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
shortDate(date) {
|
||||
return date.slice(0, date.indexOf('.')).replace('T', ' ');
|
||||
},
|
||||
displayDataRange(dataRange) {
|
||||
const startTime = dataRange.bounds.start;
|
||||
const endTime = dataRange.bounds.end;
|
||||
if (startTime === endTime) {
|
||||
return `${this.shortDate(this.timeFormatter.format(startTime))} +/- ${timestampBufferText}`;
|
||||
}
|
||||
return `${this.shortDate(this.timeFormatter.format(startTime))} - ${this.shortDate(
|
||||
this.timeFormatter.format(endTime)
|
||||
)}`;
|
||||
},
|
||||
isSelectedDataRange(dataRange, index) {
|
||||
const selectedDataRange = this.descendingDataRanges[index];
|
||||
|
||||
return (
|
||||
dataRange.bounds.start === selectedDataRange.bounds.start &&
|
||||
dataRange.bounds.end === selectedDataRange.bounds.end
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,127 +0,0 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="camerasWithImagesInBounds.length > 0"
|
||||
class="c-inspect-properties c-inspector__imagery-view"
|
||||
>
|
||||
<div class="c-inspect-properties__header">Imagery View</div>
|
||||
<div
|
||||
v-for="(camera, index) in camerasWithImagesInBounds"
|
||||
:key="index"
|
||||
class="c-imagery-view__camera-image-set"
|
||||
>
|
||||
<TelemetryFrame :bounds="bounds" :telemetry-object="camera">
|
||||
<div class="c-imagery-view__camera-image-list">
|
||||
<span
|
||||
v-for="(cameraImage, imageIndex) in camera.imagesInBounds"
|
||||
:key="imageIndex"
|
||||
class="c-imagery-view__camera-image"
|
||||
>
|
||||
<img :src="cameraImage.value" />
|
||||
<span class="c-imagery-view__camera-image-timestamp">
|
||||
{{ cameraImage.timestamp }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</TelemetryFrame>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TelemetryFrame from './TelemetryFrame.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TelemetryFrame
|
||||
},
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
bounds: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
telemetryKeys: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
camerasWithImagesInBounds: []
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
bounds() {
|
||||
this.getCameraImagesInBounds();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getCameraImagesInBounds();
|
||||
},
|
||||
methods: {
|
||||
async getCameraImagesInBounds() {
|
||||
this.camerasWithImagesInBounds = [];
|
||||
this.cameraImagesList = [];
|
||||
const { start, end } = this.bounds;
|
||||
const cameraObjectPromises = [];
|
||||
this.telemetryKeys.forEach((telemetryKey) => {
|
||||
const cameraPromise = this.openmct.objects.get(telemetryKey);
|
||||
cameraObjectPromises.push(cameraPromise);
|
||||
});
|
||||
const cameraObjects = await Promise.all(cameraObjectPromises);
|
||||
|
||||
const cameraTelemetryPromises = [];
|
||||
cameraObjects.forEach((cameraObject) => {
|
||||
const cameraTelemetryPromise = this.openmct.telemetry.request(cameraObject, {
|
||||
start,
|
||||
end
|
||||
});
|
||||
cameraTelemetryPromises.push(cameraTelemetryPromise);
|
||||
});
|
||||
const cameraImages = await Promise.all(cameraTelemetryPromises);
|
||||
|
||||
cameraObjects.forEach((cameraObject, index) => {
|
||||
cameraObject.images = cameraImages[index];
|
||||
});
|
||||
|
||||
cameraObjects.forEach((cameraObject) => {
|
||||
if (cameraObject.images.length > 0) {
|
||||
const imagesInBounds = cameraObject.images.filter((imageDetails) => {
|
||||
if (!imageDetails.timestamp) {
|
||||
return false;
|
||||
}
|
||||
const timestamp = Date.parse(imageDetails.timestamp);
|
||||
return timestamp >= start && timestamp <= end;
|
||||
});
|
||||
if (imagesInBounds.length > 0) {
|
||||
cameraObject.imagesInBounds = imagesInBounds;
|
||||
this.camerasWithImagesInBounds.push(cameraObject);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,65 +0,0 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="c-inspector__properties c-data-visualization-inspector__properties c-data-visualization-inspector__flex-column"
|
||||
>
|
||||
<DataVisualization
|
||||
:data-ranges="dataRanges"
|
||||
:plot-telemetry-keys="plotTelemetryKeys"
|
||||
:description="description"
|
||||
:is-loading="isLoading"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DataVisualization from './DataVisualization.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DataVisualization
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
context: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dataRanges() {
|
||||
return this.context.dataRanges;
|
||||
},
|
||||
plotTelemetryKeys() {
|
||||
return this.context.telemetryKeys;
|
||||
},
|
||||
description() {
|
||||
return this.context.description;
|
||||
},
|
||||
isLoading() {
|
||||
return Boolean(this.context.loading);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,166 +0,0 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="c-inspector__numeric-data">
|
||||
<div class="c-inspect-properties">
|
||||
<div class="c-inspect-properties__header">Numeric Data</div>
|
||||
</div>
|
||||
<div ref="numericDataView"></div>
|
||||
|
||||
<div v-if="!hasNumericData">
|
||||
{{ noNumericDataText }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import mount from 'utils/mount';
|
||||
|
||||
import Plot from '../plot/Plot.vue';
|
||||
import TelemetryFrame from './TelemetryFrame.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject', 'timeFormatter'],
|
||||
props: {
|
||||
bounds: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
telemetryKeys: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
noNumericDataText: {
|
||||
type: String,
|
||||
default: 'No Numeric Data to display.'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
plotObjects: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasNumericData() {
|
||||
return this.plotObjects.length > 0;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
telemetryKeys: {
|
||||
handler() {
|
||||
this.renderNumericData();
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
bounds: {
|
||||
handler() {
|
||||
this.renderNumericData();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.renderNumericData();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.clearPlots();
|
||||
},
|
||||
methods: {
|
||||
renderNumericData() {
|
||||
this.clearPlots();
|
||||
|
||||
this.unregisterTimeContextList = [];
|
||||
this.elementsList = [];
|
||||
this.componentsList = [];
|
||||
|
||||
this.telemetryKeys.forEach(async (telemetryKey) => {
|
||||
const plotObject = await this.openmct.objects.get(telemetryKey);
|
||||
|
||||
this.plotObjects.push(plotObject);
|
||||
this.unregisterTimeContextList.push(this.setIndependentTimeContextForComponent(plotObject));
|
||||
this.renderPlot(plotObject);
|
||||
});
|
||||
},
|
||||
setIndependentTimeContextForComponent(plotObject) {
|
||||
const keyString = this.openmct.objects.makeKeyString(plotObject.identifier);
|
||||
|
||||
// get an independent time context for object
|
||||
this.openmct.time.getContextForView([plotObject]);
|
||||
// set the time context of the object to the selected time range
|
||||
return this.openmct.time.addIndependentContext(keyString, this.bounds);
|
||||
},
|
||||
renderPlot(plotObject) {
|
||||
const { vNode, destroy } = mount(
|
||||
{
|
||||
components: {
|
||||
TelemetryFrame,
|
||||
Plot
|
||||
},
|
||||
provide: {
|
||||
openmct: this.openmct,
|
||||
path: [plotObject]
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
plotObject,
|
||||
bounds: this.bounds
|
||||
};
|
||||
},
|
||||
template: `<TelemetryFrame
|
||||
:bounds="bounds"
|
||||
:telemetry-object="plotObject"
|
||||
>
|
||||
<Plot />
|
||||
</TelemetryFrame>`
|
||||
},
|
||||
{
|
||||
app: this.openmct.app
|
||||
}
|
||||
);
|
||||
|
||||
this.componentsList.push(destroy);
|
||||
this.elementsList.push(vNode.el);
|
||||
this.$refs.numericDataView.append(vNode.el);
|
||||
},
|
||||
clearPlots() {
|
||||
if (this.componentsList?.length) {
|
||||
this.componentsList.forEach((destroy) => destroy());
|
||||
delete this.componentsList;
|
||||
}
|
||||
|
||||
if (this.elementsList?.length) {
|
||||
this.elementsList.forEach((element) => element.remove());
|
||||
delete this.elementsList;
|
||||
}
|
||||
|
||||
if (this.plotObjects?.length) {
|
||||
this.plotObjects = [];
|
||||
}
|
||||
|
||||
if (this.unregisterTimeContextList?.length) {
|
||||
this.unregisterTimeContextList.forEach((unregisterTimeContext) => unregisterTimeContext());
|
||||
delete this.unregisterTimeContextList;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,124 +0,0 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="c-telemetry-frame">
|
||||
<div class="c-telemetry-frame__title-bar">
|
||||
<span class="c-telemetry-frame__title">
|
||||
<span class="c-telemetry-frame__title-icon icon-telemetry"></span>
|
||||
<span class="title-text">{{ telemetryObject.name }}</span>
|
||||
</span>
|
||||
<button
|
||||
ref="menu-button"
|
||||
title="More options"
|
||||
class="l-browse-bar__actions c-icon-button icon-3-dots"
|
||||
@click="toggleMenu"
|
||||
></button>
|
||||
</div>
|
||||
<div
|
||||
v-if="showMenu"
|
||||
class="c-menu c-menu__inspector-telemetry-options"
|
||||
aria-label="Telemetry Options"
|
||||
@blur="showMenu = false"
|
||||
>
|
||||
<ul>
|
||||
<li
|
||||
v-if="telemetryObject.type === 'yamcs.telemetry'"
|
||||
role="menuitem"
|
||||
title="View Full Screen"
|
||||
class="icon-eye-open"
|
||||
@click="previewTelemetry"
|
||||
>
|
||||
View Full Screen
|
||||
</li>
|
||||
<li
|
||||
role="menuitem"
|
||||
title="Open in a new browser tab"
|
||||
class="icon-new-window"
|
||||
@click="openInNewTab"
|
||||
>
|
||||
Open In New Tab
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
provide() {
|
||||
return {
|
||||
domainObject: this.telemetryObject
|
||||
};
|
||||
},
|
||||
props: {
|
||||
bounds: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
telemetryObject: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showMenu: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleMenu() {
|
||||
this.showMenu = !this.showMenu;
|
||||
},
|
||||
async getTelemetryPath() {
|
||||
let sourceTelem;
|
||||
if (this.telemetryObject.type === 'yamcs.telemetry') {
|
||||
sourceTelem = this.openmct.objects.makeKeyString(this.telemetryObject.identifier);
|
||||
} else if (this.telemetryObject.type === 'yamcs.image') {
|
||||
sourceTelem = this.openmct.objects.makeKeyString(this.telemetryObject.identifier);
|
||||
}
|
||||
const telemetryPath = await this.openmct.objects.getOriginalPath(sourceTelem);
|
||||
return telemetryPath;
|
||||
},
|
||||
async openInNewTab() {
|
||||
const telemetryPath = await this.getTelemetryPath();
|
||||
const sourceTelemObject = telemetryPath[0];
|
||||
const timeBounds = this.bounds;
|
||||
const urlParams = {
|
||||
'tc.startBound': timeBounds?.start,
|
||||
'tc.endBound': timeBounds?.end,
|
||||
'tc.mode': 'fixed'
|
||||
};
|
||||
const newTabAction = this.openmct.actions.getAction('newTab');
|
||||
newTabAction.invoke([sourceTelemObject], urlParams);
|
||||
this.showMenu = false;
|
||||
},
|
||||
previewTelemetry() {
|
||||
const previewAction = this.openmct.actions.getAction('preview');
|
||||
previewAction.invoke([this.telemetryObject]);
|
||||
this.showMenu = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user