Compare commits

...

39 Commits

Author SHA1 Message Date
John Hill
c1b36b6bbd Attempt to run testing-library/vue to layoutspec.js 2023-08-09 13:55:44 -07:00
Scott Bell
9be9c5e28e Check for null in DuplicateAction (#6904)
check for null before checking before hasOwnProperty
2023-08-09 11:38:17 -07:00
Even Stensberg
58aeac94ac Feat(tooling): add cspell (#6892)
* feat(tooling): add cspell

* fix: pin dep

* ci(linting): add spelling
2023-08-09 08:34:45 -07:00
Jesse Mazzella
1e3097f54b chore: bump Playwright to 1.36.2 (#6901)
* chore: bump Playwright to `1.36.2`

* chore: remove `playwright/core` dependency

* chore: temporarily disable cacheing step

* chore: temp disable cacheing for e2e-couchdb run

* chore: restore cacheing step

* chore: remove `--prefer-offline` option
2023-08-08 10:44:16 -07:00
Shefali Joshi
6a9ff91d93 Dismiss the independent time conductor popup on unmount (#6859)
* Don't set conductor popup to null unless the view is being destroyed

* Replace beforeDestroy with beforeUnmount

* Propagate plot tick widths to timeline view

* Check if conductor popup exists before trying to remove it from the dom

* Fix imagery e2e test

* Revert accidental commit

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-08-07 23:19:13 +00:00
Charles Hacskaylo
accfbc96ab Fix Plan View duplicate scrollbars (#6865)
* Closes #6864
- CSS fixes to remove problematic duplicate overflow handling.

* fix(e2e): stabilize autoscale test

* fix(e2e): mark overlay plot tagging test as slow

* fix(e2e): stabilize ITC e2e test

* fix(e2e): don't use hard wait

* fix: remove .only 😳

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-08-07 22:48:29 +00:00
Even Stensberg
9942bbbc0f chore: move indexTest to kebab-case (#6860) 2023-08-06 12:54:36 -07:00
John Hill
4287cd5413 [CI] Update docker login step to work across forks and non-nasa-users (#6891)
Continue on error
2023-08-05 15:05:40 -07:00
Scott Bell
ee6ca11558 Only load annotations in fixed time mode or frozen (#6866)
* fix annotations load to not happen on start

* remove range checking per annotated point

* back to local variable

* reduce tagging size to improve reliability of test

* remove .only

* remove .only

* reduce hz rate for functional test and keep high frequency test in performance

* remove console.debugs

* this test runs pretty fast now

* fix network request tests to match new behavior
2023-08-03 09:40:52 -07:00
Andrew Henry
676bb81eab Synchronize timers between multiple users (#6885)
* created a throttle util and using it in timer plugin to throttle refreshing the timer domain object

* Simplify timer logic

* Clarify code a little

* refactor: lint:fix

* Fix linting issue

---------

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-08-02 16:30:51 -07:00
Shefali Joshi
c6305697c0 Set the raw series limits so that we can get the raw series limits (#6877)
* Set the raw series limits so that we can get the raw series limits

* fix: `toRaw()` the other gets/sets/deletes

---------

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2023-08-02 09:50:03 -07:00
Jesse Mazzella
0421936874 fix: suppress deprecation warnings to once per unique args (#6875) 2023-08-02 09:11:41 -07:00
Jesse Mazzella
95e686038d fix: toggling markers, alarm markers, marker style + update Vue.extend() usage to Vue 3 (#6868)
* fix: update to `defineComponent` from `Vue.extend()`
* fix: unwrap Proxy arg before WeakMap.get()
* refactor: `defineComponent` not needed here
2023-08-01 14:07:59 -07:00
Shefali Joshi
f705bf9a61 Wait for bounds change to reset telemetry collection data (#6857)
* Reset and re-request telemetry only after receiving bounds following a mode change

* Don't check for tick - just in case the mode is set without bounds

* Use the imagery view timeContext to get related telemetry.

---------

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
2023-07-31 18:15:08 +00:00
Shefali Joshi
50559ac502 Don't allow editing line more when not editing display layout (#6858) 2023-07-31 10:16:52 -07:00
Jesse Mazzella
f0ef93dd3f fix: remove tree-item-destroyed event (#6856)
* fix: remove `tree-item-destroyed` event

- Composition listeners should only be removed if the item has been deleted or its parent has been collapsed. Both are handled by `mct-tree` already, so doing this additionally when tree-items unmount is redundant.

- In addition to that, any time the `visibleTreeItems` array changes, all tree-items will unmount, so this just doesn't work as intended-- it will unregister all composition listeners whenever the tree changes!

* test: stabilize imagery test

- Use keyboard gestures to navigate

* fix: lint:fix
2023-07-31 11:57:11 -05:00
Jesse Mazzella
3ae14cf786 Revert "[CI] Temporarily disable some tests" (#6853)
* Revert "[CI] Temporarily disable some tests (#6806)"

This reverts commit 85974fc5f1.

* fix(e2e): fix visual tests

* refactor: lint:fix

* fix: revert localStorage data changes

---------

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2023-07-28 17:20:06 -07:00
Jesse Mazzella
194eb43607 fix(#6854): [LADTableSet] prevent compositions from becoming reactive (#6855)
* fix: prevent compositions from becoming reactive
2023-07-28 12:35:11 -07:00
Shefali Joshi
3c2b032526 Plan rendering inside a timestrip (#6852)
* Use the width and height of the container of the plan to set the activity widths and now markers

* Use the right parent to determine height and width

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-07-28 10:24:48 -07:00
Shefali Joshi
d4e51cbaf1 Use the current timestamp from the global clock (#6851)
* Use the current timestamp from the global clock. Use mode changes to set if the view is fixed time or real time

* Reload the page after adding a plan and then change the url params.
2023-07-28 09:37:11 -07:00
Khalid Adil
7c58b19c3e Switch staleness provider for SWG to use modeChanged instead of clock (#6845)
* Switch staleness provider for SWG to use modeChanged instead of clock
2023-07-28 05:04:42 +00:00
Jesse Mazzella
16e1ac2529 Fixes for e2e tests following the Vue 3 compat upgrade (#6837)
* clock, timeConductor and appActions fixes

* Ensure realtime uses upstream context when available
Eliminate ambiguity when looking for time conductor locator

* Fix log plot e2e tests

* Fix displayLayout e2e tests

* Specify global time conductor to fix issues with duplicate selectors with independent time contexts

* a11y: ARIA for conductor and independent time conductor

* a11y: fix label collisions, specify 'Menu' in label

* Add watch mode

* fix(e2e): update appActions and tests to use a11y locators for ITC

* Don't remove the itc popup from the DOM. Just show/hide it once it's added the first time.

* test(e2e): disable one imagery test due to known bug

* Add fixme to tagging tests, issue described in 6822

* Fix locator for time conductor popups

* Improve how time bounds are set in independent time conductor.
Fix tests for flexible layout and timestrip

* Fix some tests for itc for display layouts

* Fix Inspector tabs remounting on change

* fix autoscale test and snapshot

* Fix telemetry table test

* Fix timestrip test

* e2e: move test info annotations to within test

* 6826: Fixes padStart error due to using it on a number rather than a string

* fix(e2e): update snapshots

* fix(e2e): fix restricted notebook locator

* fix(restrictedNotebook): fix issue causing sections not to update on lock

* fix(restrictedNotebook): fix issue causing snapshots to not be able to be deleted from a locked page

- Using `this.$delete(arr, index)` does not update the `length` property on the underlying target object, so it can lead to bizarre issues where your array is of length 4 but it has 3 objects in it.

* fix: replace all instances of `$delete` with `Array.splice()` or `delete`

* fix(e2e): fix grand search test

* fix(#3117): can remove item from displayLayout via tree context menu while viewing another item

* fix: remove typo 

* Wait for background image to load

* fix(#6832): timelist events can tick down

* fix: ensure that menuitems have the raw objects so emits work

* fix: assign new arrays instead of editing state in-place

* refactor(timelist): use `getClock()` instead of `clock()`

* Revert "refactor(timelist): use `getClock()` instead of `clock()`"

This reverts commit d888553112.

* refactor(timelist): use new timeAPI

* Stop ticking when the independent time context is disabled (#6833)

* Turn off the clock ticket for independent time conductor when it is disabled

* Fix linting issues

---------

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>

* test: update couchdb notebook test

* fix: codeQL warnings

* fix(tree-item): infinite spinner issue

- Using `indexOf()` with an object was failing due to some items in the tree being Proxy-wrapped and others not. So instead, use `findIndex()` with a predicate that compares the navigationPaths of both objects

* [Timer] Remove "refresh" call, it is not needed (#6841)

* removing an unneccessary refresh that waas causing many get requests
* lets just pretend this never happened

* fix(mct-tree): maintain reactivity of all tree items

* Hide change role button in the indicator in cases where there is only… (#6840)

Hide change role button in the indicator in cases where there is only a single role available for the current user

---------

Co-authored-by: Shefali <simplyrender@gmail.com>
Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: David Tsay <david.e.tsay@nasa.gov>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2023-07-28 02:06:41 +00:00
Jesse Mazzella
4885c816dc Migrate to Vue 3 Migration Build (#6767)
* Replacing all instances of the new Vue() component creation pattern
* In Vue 3, components cannot be created on the fly and mounted off-DOM. The suggested fix from Vue is to use createApp, but in the context of Open MCT this means dozens of Vue apps being created and destroyed at any given moment. Instead, we have used a community hack for creating individual components.
* beforeDestroy() -> beforeUnmount()
* destroyed() -> unmounted()
* The addition of deep: true option on Array listeners is now required to detect Array changes
* Open MCT is now mounted on a child div instead of directly on document.body


---------

Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-07-19 11:22:23 -07:00
Jamie V
42b545917c [Time] Conductors and API Enhancements (#6768)
* Fixed #4975 - Compact Time Conductor styling
* Fixed #5773 - Ubiquitous global clock
* Mode functionality added to TimeAPI
* TimeAPI modified to always have a ticking clock
* Mode dropdown added to independent and regular time conductors
* Overall conductor appearance modifications and enhancements
* TimeAPI methods deprecated with warnings
* Significant updates to markup, styling and behavior of main Time Conductor and independent version.


---------

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Shefali <simplyrender@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Scott Bell <scott@traclabs.com>
2023-07-18 17:32:05 -07:00
John Hill
85974fc5f1 [CI] Temporarily disable some tests (#6806)
Temporarily disable some tests
2023-07-17 14:03:47 -07:00
Jesse Mazzella
761d4ce7e4 chore: bump version to 3.0.0-SNAPSHOT (#6800) 2023-07-15 08:26:56 -07:00
Shefali Joshi
5b1298f221 Adds limits subscription to the Telemetry API (#6735)
* Add subscription for limits for domain objects
---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2023-07-14 17:09:05 -07:00
Andrew Henry
662d14354c Suppress role selection if no roles available (#6802) 2023-07-14 16:22:25 -07:00
Shefali Joshi
e386036dbf Enhance telemetry tables to allow in place updates for data (#6694)
* cherry-pick(#6602) : [ExportAsJson] Multiple Aliases in Export and Co… (#6658)

cherry-pick(#6602) : [ExportAsJson] Multiple Aliases in Export and Conditional Styles Fixes (#6602)

Fixes issues that prevent import and export from being completed successfully. Specifically:

* if multiple aliases are detected, the first is created as a new object and and added to it's parent's composition, any subsequent aliases of the same object will not be recreated, but the originally created one will be added to the current parent's composition, creating an alias.

* Also, there are cases were conditionSetIdentifiers are stored in an object keyed by an item id in the configuration.objectstyles object, this fix will handle these as well.

* Replaces an errant `return` statement with a `continue` statement to prevent early exit from a recursive function.

---------

Co-authored-by: Andrew Henry <akhenry@gmail.com>

* chore: bump version to `2.2.3` (#6685)

* Add configuration detection to update table rows in place

* Fix typo for datum access

* First add new rows to the table and then update rows in place

* Each row much be checked for in place updates and inserted as needed

* Fix typo. Remove unused code.

* Update datum only. And don't allow undefined values for columns

* Fix typo

* Rename function for clarity

* Use telemetry metadata to indicate datum property to use for in place updates

* Fix typo for method call

* Fix typo for return value

* fullDatum is the datum BEFORE normalizing.

---------

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2023-07-14 14:51:23 -07:00
Michael Rogers
6e79e5e2b0 [Timelist] Fixed Time use Now as start time - 5772 (#6497)
* Selectively filter activities only for realtime

* Remove unnecessary logic

* Adjust hideAll and showAll flags for non-realtime mode

* Filter out past events for fixed time

* Set the timestamp on bounds change

* Cleanup

* Removed duplicated listing since handled by different method

* Inverted variable

* removed setting showAll flag

* Remove unusued showAll value

* Removed noCurrent state and isCurrent logic check based on noCurrent

* Set formatted start / end to utc mode to synchronize with current time counductor value

* Add missed file

* Lint fixes

* Formatter improvements to use the Time API and lint fix

* Updated test to use Time API formatter instead of moment directly

* Linting fix to pluginSpec

* Prettier one line

---------

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2023-07-14 15:19:33 -05:00
Michael Rogers
32529ff6b2 Role selection for operator status roles (#6706)
* Add additional test roles to example user

* Add session storage and role to user indicator

* Update example user provider

* Added selection dialog to overlays and implemented for operator status

* Display role in user indicator

* Updates to broadcast channel lifecycle

* Update comment

* Comment width

* UserAPI role updates and UserIndicator improvement

* Moved prompt to UserIndicator

* Reconnect channel on error and UserIndicator updates

* Updates to status api canPRovideStatusForRole

* Cleanup

* Store status roles in an array instead of a singular value

* Added success notification and cleanup

* Lint

* Removed unused role param from status api call

* Remove default status role from example user plugin

* Removed status.getStatusRoleForCurrentUser

* Cleanup

* Cleanup

* Moved roleChannel to private field

* Separated input value from active role value

* More flight like status role names and parameter names

* Update statusRole parameter name

* Update default selection for roles if input is not chosen

* Update OperatorStatusIndicator install to hide if an observer

* console.log

* Return null instead of undefined

* Remove unneccesary filter on allRoles

* refactor: format with prettier

* Undid merge error

* Merge conflict extra line

* Copyright statement

* RoleChannelProvider to RoleChannel

* Throw error on no provider

* Change RoleChannel to ActiveRoleSynchronizer and update method calls to match

* iconClass to alert

* Add role selection step to beforeEach

* example-role to flight

* Dismiss overlay from exampleUser plugin which affected menu api positioning

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
2023-07-14 19:10:58 +00:00
Jamie V
92329b3d8e Tree item abort (#6757)
* adding abortSignal back to composition load
* suppress AbortError console.errors from couch, delay requests for test to trigger abort
---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2023-07-14 17:49:10 +00:00
Khalid Adil
cde8fbbb0d [Tooltips] Add tooltips on hover (#6756)
* Add tooltip api, extend object api to add telemetry composition lookups, and add tooltips to gauges/notebook embeds/plot legends/object frames/object names/time strips/recent objects/search results/object tree

* Add tooltips to telemetry/lad tables

* Styling normalization, sanding and polishing.

* Add tooltips for Conditional widgets and Tab Views

* Add tests

* Switch to using enum-ish consts for tooltip locations

* Trim LAD table row name to account for spacing required by linting rules

---------

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
2023-07-13 21:37:59 -07:00
Shefali Joshi
795d7a7ec7 Fix couchdbsearchfolder and allow clocky reports (#6770)
* Fix CouchDBSearchFolder plugin to have unique identifiers.
Allow ttt-reports to be viewed as web pages

* Remove ttt-report type from WebPage view provider. This is being moved to the viper-openmct repo instead

* Adds check for classList

* Add WebPage to the components list

* Remove uuid and use the folder name as the identifier instead

* Remove focused test

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2023-07-13 19:50:52 +00:00
Michael Rogers
5031010a00 Add role attribution to notebook entries and export (#6793)
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2023-07-13 19:09:00 +00:00
Andrew Henry
ac22bebe76 Batch Couch DB create calls (#6779)
* Implement persistence batching for Couch DB

* Add tests for persistence batching

---------

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2023-07-12 04:36:00 +00:00
Shefali Joshi
d08ea62932 Toggle between showing aggregate stacked plot legend or per-plot legend (#6758)
* New option to show/hide stacked plot aggregate legend - defaulted to not show.
Use the Plot component in the StackedPlotItem component for simplicity and show/hide sub-legends as needed.

* Fix position and expanded classes when children are showing their legends

* Fix broken tests and ensure gridlines and cursorguides work.

* Adds e2e test for new legend configuration for stacked plot

* Address review comments - Remove commented out code, optimize property lookup, fix bug with staleness

* Remove the isStale icon in the legend when a plot is inside a stacked plot.

---------

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2023-07-11 23:16:46 +00:00
John Hill
293f25df19 [CI] Update Github Actions to combine deploysentinel PR reports and driveby (#6784)
* include git hash

* skip a test
2023-07-11 14:31:23 -07:00
John Hill
9c22bcfb3e [CI] Fix couchdb e2e trigger and run nightly, part 3 (#6782)
* Run nightly, fix triggers

* contains

* driveby: remove github reporter

* update tests to match

* redo opened logic

* don't run pr:e2e and pr:platform
2023-07-08 13:03:14 -07:00
414 changed files with 10543 additions and 5106 deletions

View File

@@ -2,7 +2,7 @@ version: 2.1
executors:
pw-focal-development:
docker:
- image: mcr.microsoft.com/playwright:v1.32.3-focal
- image: mcr.microsoft.com/playwright:v1.36.2-focal
environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
@@ -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:
@@ -162,7 +163,7 @@ jobs:
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- run: npx playwright@1.32.3 install #Necessary for bare ubuntu machine
- run: npx playwright@1.36.2 install #Necessary for bare ubuntu machine
- run: |
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach

569
.cspell.json Normal file
View File

@@ -0,0 +1,569 @@
{
"version": "0.2",
"language": "en,en-us",
"words": [
"gress",
"doctoc",
"minmax",
"openmct",
"datasources",
"recieved",
"evalute",
"Sinewave",
"deregistration",
"unregisters",
"configutation",
"configuation",
"codecov",
"carryforward",
"Chacon",
"Straub",
"OWASP",
"Testathon",
"exploratorily",
"Testathons",
"testathon",
"npmjs",
"publishj",
"treeitem",
"timespan",
"Timespan",
"spinbutton",
"popout",
"textbox",
"tablist",
"Telem",
"codecoverage",
"browserless",
"networkidle",
"nums",
"mgmt",
"faultname",
"gantt",
"sharded",
"perfromance",
"MMOC",
"deploysentinel",
"codegen",
"Unfortuantely",
"viewports",
"updatesnapshots",
"excercised",
"Circel",
"browsercontexts",
"miminum",
"testcase",
"testsuite",
"domcontentloaded",
"Tracefile",
"lcov",
"linecov",
"Browserless",
"webserver",
"yamcs",
"quickstart",
"subobject",
"autosize",
"Horz",
"vehicula",
"Praesent",
"pharetra",
"Duis",
"eget",
"arcu",
"elementum",
"mauris",
"Donec",
"nunc",
"quis",
"Proin",
"elit",
"Nunc",
"Aenean",
"mollis",
"hendrerit",
"Vestibulum",
"placerat",
"velit",
"augue",
"Quisque",
"mattis",
"lectus",
"rutrum",
"Fusce",
"tincidunt",
"nibh",
"blandit",
"urna",
"Nullam",
"congue",
"enim",
"Morbi",
"bibendum",
"Vivamus",
"imperdiet",
"Pellentesque",
"cursus",
"Aliquam",
"orci",
"Suspendisse",
"amet",
"justo",
"Etiam",
"vestibulum",
"ullamcorper",
"Cras",
"aliquet",
"Mauris",
"Nulla",
"scelerisque",
"viverra",
"metus",
"condimentum",
"varius",
"nulla",
"sapien",
"Curabitur",
"tristique",
"Nonsectetur",
"convallis",
"accumsan",
"lacus",
"posuere",
"turpis",
"egestas",
"feugiat",
"tortor",
"faucibus",
"euismod",
"pratices",
"pathing",
"pases",
"testcases",
"Responsefrom",
"Verficiation",
"persistable",
"Noneditable",
"Persistability",
"persistability",
"peristable",
"listitem",
"Gantt",
"timelist",
"Indepdenent",
"timestrip",
"Implimenting",
"proprety",
"networkevents",
"stablize",
"fetchpriority",
"visibity",
"Hiearchy",
"persistible",
"testdata",
"Testdata",
"metdata",
"Snaphot",
"timeconductor",
"Endtimes",
"contenteditable",
"respone",
"Testsuite",
"autoscale",
"Autoscale",
"reflexted",
"prepan",
"sinewave",
"cyanish",
"driv",
"searchbox",
"datetime",
"timeframe",
"recents",
"recentobjects",
"gsearch",
"Disp",
"Cloc",
"noselect",
"requestfailed",
"viewlarge",
"Imageurl",
"thumbstrip",
"checkmark",
"acknowldeged",
"Unshelve",
"autosized",
"chacskaylo",
"Telmetry",
"numberfield",
"OPENMCT",
"MILISECONDS",
"Autoflow",
"Timelist",
"faultmanagement",
"localed",
"avaiable",
"GEOSPATIAL",
"geospatial",
"plotspatial",
"annnotation",
"keystrings",
"undelete",
"sometag",
"nameespace",
"containee",
"composability",
"mutables",
"Mutables",
"composee",
"handleoutsideclick",
"Datetime",
"funtion",
"Perc",
"autodismiss",
"Notifiation",
"notificiation",
"filetree",
"deeptailor",
"keystring",
"Persistable",
"Inovke",
"reindex",
"modifed",
"unlisten",
"symbolsfont",
"ellipsize",
"dismissable",
"TIMESYSTEM",
"Metadatas",
"modifyed",
"stategy",
"stalenes",
"receieves",
"unsub",
"callbacktwo",
"unsubscribetwo",
"telem",
"Telemetery",
"unemitted",
"granually",
"timesystem",
"metadatas",
"posess",
"iteratees",
"metadatum",
"printj",
"sprintf",
"unlisteners",
"amts",
"reregistered",
"opemct",
"specifiy",
"loged",
"hudsonfoo",
"onclone",
"instatation",
"instatiated",
"autoflow",
"xdescribe",
"mockmct",
"Autoflowed",
"plotly",
"relayout",
"Plotly",
"Yaxis",
"showlegend",
"textposition",
"xaxis",
"automargin",
"fixedrange",
"yaxis",
"Axistype",
"showline",
"bglayer",
"autorange",
"telmetry",
"subcribe",
"hoverinfo",
"dotful",
"Dotful",
"cartesianlayer",
"scatterlayer",
"textfont",
"ampm",
"cdef",
"horz",
"STYLEABLE",
"styleable",
"afff",
"shdw",
"braintree",
"vals",
"Subobject",
"Shdw",
"Movebar",
"inspectable",
"Stringformatter",
"sclk",
"compostion",
"Objectpath",
"Keystring",
"duplicatable",
"composees",
"Composees",
"Composee",
"callthrough",
"objectpath",
"createable",
"applie",
"noneditable",
"infinte",
"occured",
"Occured",
"Classname",
"classname",
"selectedfaults",
"accum",
"newpersisted",
"Metadatum",
"MCWS",
"YAMCS",
"frameid",
"containerid",
"mmgis",
"PERC",
"curval",
"viewbox",
"mutablegauge",
"Flatbush",
"flatbush",
"Indicies",
"Marqueed",
"NSEW",
"nsew",
"vrover",
"gimbled",
"Pannable",
"unsynced",
"Unsynced",
"pannable",
"autoscroll",
"TIMESTRIP",
"TWENTYFOUR",
"FULLSIZE",
"doesnt",
"intialize",
"Timestrip",
"spyon",
"Unlistener",
"multipane",
"DATESTRING",
"requred",
"telemtry",
"lastest",
"akhenry",
"Niklas",
"Hertzen",
"Kash",
"Nouroozi",
"Bostock",
"BOSTOCK",
"Arnout",
"Kazemier",
"Karolis",
"Narkevicius",
"Ashkenas",
"Madhavan",
"Iskren",
"Ivov",
"Chernev",
"Borshchov",
"painterro",
"sheetjs",
"Yuxi",
"ACITON",
"localstorage",
"Compostion",
"Linkto",
"Painterro",
"Editability",
"filteredsnapshots",
"Fromimage",
"muliple",
"notebookstorage",
"Andpage",
"pixelize",
"incremement",
"Couche",
"namepaces",
"Quickstart",
"indexhtml",
"youradminpassword",
"chttpd",
"sourcefiles",
"USERPASS",
"XPUT",
"referer",
"adipiscing",
"eiusmod",
"tempor",
"incididunt",
"labore",
"dolore",
"aliqua",
"perspiciatis",
"iteree",
"submodels",
"symlog",
"Plottable",
"dont",
"sinwave",
"necesarry",
"antisymlog",
"docstrings",
"unavailab",
"eelement",
"mediump",
"webglcontextlost",
"gridlines",
"Xaxis",
"Crosshairs",
"telemetrylimit",
"createble",
"xscale",
"yscale",
"temparature",
"configurration",
"envoke",
"untracks",
"Recieve",
"swatched",
"NULLVALUE",
"intial",
"Retreives",
"assoicated",
"cotains",
"thet",
"Initialze",
"unobserver",
"unsubscriber",
"generateor",
"Intantiate",
"drap",
"historial",
"evalutes",
"initialzes",
"Averager",
"averager",
"movecolumnfromindex",
"callout",
"Konqueror",
"unmark",
"hitarea",
"Hitarea",
"Unmark",
"controlbar",
"examplar",
"reactified",
"valuelue",
"perc",
"DHMS",
"timespans",
"timeframes",
"Timesystems",
"Syste",
"Hilite",
"screan",
"datetimes",
"momentified",
"ucontents",
"TIMELIST",
"Timeframe",
"Guirk",
"resizeable",
"iframing",
"Btns",
"Ctrls",
"Chakra",
"Petch",
"propor",
"phoneandtablet",
"desktopandtablet",
"Imgs",
"UNICODES",
"datatable",
"csvg",
"cpath",
"cellipse",
"xlink",
"cstyle",
"bfill",
"ctitle",
"eicon",
"ccircle",
"interactability",
"AFFORDANCES",
"affordance",
"scrollcontainer",
"Icomoon",
"icomoon",
"configurability",
"btns",
"AUTOFLOW",
"DATETIME",
"infobubble",
"thumbsbubble",
"codehilite",
"vscroll",
"bgsize",
"togglebutton",
"Hacskaylo",
"noie",
"fullscreen",
"horiz",
"menubutton",
"SNAPSHOTTING",
"snapshotting",
"PAINTERRO",
"ptro",
"PLOTLY",
"gridlayer",
"xtick",
"ytick",
"notabook",
"subobjects",
"Ucontents",
"Userand",
"Userbefore",
"brdr",
"pushs",
"ALPH",
"Recents",
"Qbert",
"Infobubble",
"haslink",
"VPID",
"vpid",
"paramater",
"paramaters",
"updatedtest",
"broser",
"KHTML",
"Chromezilla",
"Safarifox",
"deregistering",
"hundredtized",
"dhms",
"unthrottled",
"Codecov"
],
"dictionaries": ["npm", "software-terms"],
"ignorePaths": [
"package.json",
"dist/**",
"package-lock.json",
"node_modules",
"coverage",
"*.log"
]
}

View File

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

View File

@@ -1,13 +1,17 @@
name: 'e2e-couchdb'
on:
push:
branches: master
workflow_dispatch:
pull_request:
types:
- labeled
- opened
schedule:
- cron: '0 0 * * *'
jobs:
e2e-couchdb:
if: github.event.label.name == 'pr:e2e:couchdb' || github.event.action == 'opened' && github.actor == 'dependabot[bot]'
if: contains(github.event.pull_request.labels.*.name, 'pr:e2e:couchdb') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.action == 'opened'
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
@@ -24,15 +28,16 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- run: npm install --cache ~/.npm --prefer-offline --no-audit --progress=false
- 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 }}
- run: npx playwright@1.32.3 install
- run: npx playwright@1.36.2 install
- name: Start CouchDB Docker Container and Init with Setup Scripts
run: |
@@ -45,6 +50,7 @@ jobs:
- name: Run CouchDB Tests and publish to deploysentinel
env:
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha }}
run: npm run test:e2e:couchdb
- name: Publish Results to Codecov.io

View File

@@ -1,13 +1,17 @@
name: 'e2e-pr'
on:
push:
branches: master
workflow_dispatch:
pull_request:
types:
- labeled
- opened
schedule:
- cron: '0 0 * * *'
jobs:
e2e-full:
if: github.event.label.name == 'pr:e2e' || github.event.action == 'opened' && github.actor == 'dependabot[bot]'
if: contains(github.event.pull_request.labels.*.name, 'pr:e2e') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
@@ -29,9 +33,9 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- run: npx playwright@1.32.3 install
- run: npx playwright@1.36.2 install
- run: npx playwright install chrome-beta
- run: npm install --cache ~/.npm --prefer-offline --no-audit --progress=false
- run: npm install --cache ~/.npm --no-audit --progress=false
- run: npm run test:e2e:full -- --max-failures=40
- run: npm run cov:e2e:report || true
- shell: bash

View File

@@ -1,14 +1,17 @@
name: 'pr-platform'
on:
push:
branches: master
workflow_dispatch:
pull_request:
types:
- labeled
- opened
schedule:
- cron: '0 0 * * *'
jobs:
pr-platform:
if: github.event.label.name == 'pr:platform' || github.event.action == 'opened' && github.actor == 'dependabot[bot]'
if: contains(github.event.pull_request.labels.*.name, 'pr:platform') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
@@ -42,7 +45,7 @@ jobs:
restore-keys: |
${{ runner.os }}-${{ matrix.node_version }}-
- run: npm install --cache ~/.npm --prefer-offline --no-audit --progress=false
- run: npm install --cache ~/.npm --no-audit --progress=false
- run: npm test

View File

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

View File

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

View File

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

6
API.md
View File

@@ -2,7 +2,7 @@
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**
- [Building Applications With Open MCT](#developing-applications-with-open-mct)
- [Developing Applications With Open MCT](#developing-applications-with-open-mct)
- [Scope and purpose of this document](#scope-and-purpose-of-this-document)
- [Building From Source](#building-from-source)
- [Starting an Open MCT application](#starting-an-open-mct-application)
@@ -26,7 +26,7 @@
- [Value Hints](#value-hints)
- [The Time Conductor and Telemetry](#the-time-conductor-and-telemetry)
- [Telemetry Providers](#telemetry-providers)
- [Telemetry Requests and Responses.](#telemetry-requests-and-responses)
- [Telemetry Requests and Responses](#telemetry-requests-and-responses)
- [Request Strategies **draft**](#request-strategies-draft)
- [`latest` request strategy](#latest-request-strategy)
- [`minmax` request strategy](#minmax-request-strategy)
@@ -873,6 +873,8 @@ function without any arguments.
#### Stopping an active clock
_As of July 2023, this method will be deprecated. Open MCT will always have a ticking clock._
The `stopClock` method can be used to stop an active clock, and to clear it. It
will stop the clock from ticking, and set the active clock to `undefined`.

View File

@@ -35,6 +35,7 @@
* @property {string} type the type of domain object to create (e.g.: "Sine Wave Generator").
* @property {string} [name] the desired name of the created domain object.
* @property {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the Identifier or uuid of the parent object.
* @property {Object<string, string>} [customParameters] any additional parameters to be passed to the domain object's form. E.g. '[aria-label="Data Rate (hz)"]': {'0.1'}
*/
/**
@@ -65,7 +66,10 @@ const { expect } = require('@playwright/test');
* @param {CreateObjectOptions} options
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
*/
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
async function createDomainObjectWithDefaults(
page,
{ type, name, parent = 'mine', customParameters = {} }
) {
if (!name) {
name = `${type}:${genUuid()}`;
}
@@ -94,6 +98,13 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
await notesInput.fill(page.testNotes);
}
// If there are any further parameters, fill them in
for (const [key, value] of Object.entries(customParameters)) {
const input = page.locator(`form[name="mctForm"] ${key}`);
await input.fill('');
await input.fill(value);
}
// Click OK button and wait for Navigate event
await Promise.all([
page.waitForLoadState(),
@@ -314,13 +325,13 @@ async function _isInEditMode(page, identifier) {
*/
async function setTimeConductorMode(page, isFixedTimespan = true) {
// Click 'mode' button
await page.locator('.c-mode-button').click();
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
await page.getByRole('button', { name: 'Time Conductor Mode Menu' }).click();
// Switch time conductor mode
if (isFixedTimespan) {
await page.locator('data-testid=conductor-modeOption-fixed').click();
await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click();
} else {
await page.locator('data-testid=conductor-modeOption-realtime').click();
await page.getByRole('menuitem', { name: /Real-Time/ }).click();
}
}
@@ -342,9 +353,12 @@ async function setRealTimeMode(page) {
/**
* @typedef {Object} OffsetValues
* @property {string | undefined} hours
* @property {string | undefined} mins
* @property {string | undefined} secs
* @property {string | undefined} startHours
* @property {string | undefined} startMins
* @property {string | undefined} startSecs
* @property {string | undefined} endHours
* @property {string | undefined} endMins
* @property {string | undefined} endSecs
*/
/**
@@ -353,23 +367,36 @@ async function setRealTimeMode(page) {
* @param {OffsetValues} offset
* @param {import('@playwright/test').Locator} offsetButton
*/
async function setTimeConductorOffset(page, { hours, mins, secs }, offsetButton) {
await offsetButton.click();
if (hours) {
await page.fill('.pr-time-controls__hrs', hours);
async function setTimeConductorOffset(
page,
{ startHours, startMins, startSecs, endHours, endMins, endSecs }
) {
if (startHours) {
await page.getByRole('spinbutton', { name: 'Start offset hours' }).fill(startHours);
}
if (mins) {
await page.fill('.pr-time-controls__mins', mins);
if (startMins) {
await page.getByRole('spinbutton', { name: 'Start offset minutes' }).fill(startMins);
}
if (secs) {
await page.fill('.pr-time-controls__secs', secs);
if (startSecs) {
await page.getByRole('spinbutton', { name: 'Start offset seconds' }).fill(startSecs);
}
if (endHours) {
await page.getByRole('spinbutton', { name: 'End offset hours' }).fill(endHours);
}
if (endMins) {
await page.getByRole('spinbutton', { name: 'End offset minutes' }).fill(endMins);
}
if (endSecs) {
await page.getByRole('spinbutton', { name: 'End offset seconds' }).fill(endSecs);
}
// Click the check button
await page.locator('.pr-time__buttons .icon-check').click();
await page.locator('.pr-time-input--buttons .icon-check').click();
}
/**
@@ -378,8 +405,9 @@ async function setTimeConductorOffset(page, { hours, mins, secs }, offsetButton)
* @param {OffsetValues} offset
*/
async function setStartOffset(page, offset) {
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
await setTimeConductorOffset(page, offset, startOffsetButton);
// Click 'mode' button
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
await setTimeConductorOffset(page, offset);
}
/**
@@ -388,8 +416,52 @@ async function setStartOffset(page, offset) {
* @param {OffsetValues} offset
*/
async function setEndOffset(page, offset) {
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
await setTimeConductorOffset(page, offset, endOffsetButton);
// Click 'mode' button
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
await setTimeConductorOffset(page, offset);
}
async function setTimeConductorBounds(page, startDate, endDate) {
// Bring up the time conductor popup
await page.click('.l-shell__time-conductor.c-compact-tc');
await setTimeBounds(page, startDate, endDate);
await page.keyboard.press('Enter');
}
async function setIndependentTimeConductorBounds(page, startDate, endDate) {
// Activate Independent Time Conductor in Fixed Time Mode
await page.getByRole('switch').click();
// Bring up the time conductor popup
await page.click('.c-conductor-holder--compact .c-compact-tc');
await expect(page.locator('.itc-popout')).toBeVisible();
await setTimeBounds(page, startDate, endDate);
await page.keyboard.press('Enter');
}
async function setTimeBounds(page, startDate, endDate) {
if (startDate) {
// Fill start time
await page
.getByRole('textbox', { name: 'Start date' })
.fill(startDate.toString().substring(0, 10));
await page
.getByRole('textbox', { name: 'Start time' })
.fill(startDate.toString().substring(11, 19));
}
if (endDate) {
// Fill end time
await page.getByRole('textbox', { name: 'End date' }).fill(endDate.toString().substring(0, 10));
await page
.getByRole('textbox', { name: 'End time' })
.fill(endDate.toString().substring(11, 19));
}
}
/**
@@ -401,14 +473,7 @@ async function setEndOffset(page, offset) {
async function selectInspectorTab(page, name) {
const inspectorTabs = page.getByRole('tablist');
const inspectorTab = inspectorTabs.getByTitle(name);
const inspectorTabClass = await inspectorTab.getAttribute('class');
const isSelectedInspectorTab = inspectorTabClass.includes('is-current');
// do not click a tab that is already selected or it will timeout your test
// do to a { pointer-events: none; } on selected tabs
if (!isSelectedInspectorTab) {
await inspectorTab.click();
}
await inspectorTab.click();
}
/**
@@ -510,6 +575,8 @@ module.exports = {
setRealTimeMode,
setStartOffset,
setEndOffset,
setTimeConductorBounds,
setIndependentTimeConductorBounds,
selectInspectorTab,
waitForPlotsToRender
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,7 +29,8 @@ relates to how we've extended it (i.e. ./e2e/baseFixtures.js) and assumptions ma
const { test } = require('../../baseFixtures.js');
test.describe('baseFixtures tests', () => {
test('Verify that tests fail if console.error is thrown', async ({ page }) => {
//Skip this test for now https://github.com/nasa/openmct/issues/6785
test.fixme('Verify that tests fail if console.error is thrown', async ({ page }) => {
test.fail();
//Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });

View File

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

View File

@@ -28,10 +28,10 @@ const { createDomainObjectWithDefaults, createNotification } = require('../../ap
const { test, expect } = require('../../pluginFixtures');
test.describe('Notifications List', () => {
test('Notifications can be dismissed individually', async ({ page }) => {
test.fixme('Notifications can be dismissed individually', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6122'
description: 'https://github.com/nasa/openmct/issues/6820'
});
// Go to baseURL

View File

@@ -98,6 +98,8 @@ test.describe('Time List', () => {
const startBound = testPlan.TEST_GROUP[0].start;
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
await page.goto(timelist.url);
// Switch to fixed time mode with all plan events within the bounds
await page.goto(
`${timelist.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=timelist.view`
@@ -110,7 +112,7 @@ test.describe('Time List', () => {
await test.step('Does not show milliseconds in times', async () => {
// Get the first activity
const row = await page.locator('.js-list-item').first();
const row = page.locator('.js-list-item').first();
// Verify that none fo the times have milliseconds displayed.
// Example: 2024-11-17T16:00:00Z is correct and 2024-11-17T16:00:00.000Z is wrong

View File

@@ -21,7 +21,11 @@
*****************************************************************************/
const { test, expect } = require('../../../pluginFixtures');
const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../../appActions');
const {
createDomainObjectWithDefaults,
createPlanFromJSON,
setIndependentTimeConductorBounds
} = require('../../../appActions');
const testPlan = {
TEST_GROUP: [
@@ -78,9 +82,6 @@ test.describe('Time Strip', () => {
});
// Constant locators
const independentTimeConductorInputs = page.locator(
'.l-shell__main-independent-time-conductor .c-input--datetime'
);
const activityBounds = page.locator('.activity-bounds');
// Goto baseURL
@@ -122,9 +123,7 @@ test.describe('Time Strip', () => {
});
await test.step('TimeStrip can use the Independent Time Conductor', async () => {
// Activate Independent Time Conductor in Fixed Time Mode
await page.click('.c-toggle-switch__slider');
expect(await activityBounds.count()).toEqual(0);
expect(await activityBounds.count()).toEqual(5);
// Set the independent time bounds so that only one event is shown
const startBound = testPlan.TEST_GROUP[0].start;
@@ -132,12 +131,7 @@ test.describe('Time Strip', () => {
const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
await independentTimeConductorInputs.nth(0).fill('');
await independentTimeConductorInputs.nth(0).fill(startBoundString);
await page.keyboard.press('Enter');
await independentTimeConductorInputs.nth(1).fill('');
await independentTimeConductorInputs.nth(1).fill(endBoundString);
await page.keyboard.press('Enter');
await setIndependentTimeConductorBounds(page, startBoundString, endBoundString);
expect(await activityBounds.count()).toEqual(1);
});
@@ -156,9 +150,6 @@ test.describe('Time Strip', () => {
await page.click("button[title='Save']");
await page.click("li[title='Save and Finish Editing']");
// Activate Independent Time Conductor in Fixed Time Mode
await page.click('.c-toggle-switch__slider');
// All events should be displayed at this point because the
// initial independent context bounds will match the global bounds
expect(await activityBounds.count()).toEqual(5);
@@ -169,12 +160,7 @@ test.describe('Time Strip', () => {
const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
await independentTimeConductorInputs.nth(0).fill('');
await independentTimeConductorInputs.nth(0).fill(startBoundString);
await page.keyboard.press('Enter');
await independentTimeConductorInputs.nth(1).fill('');
await independentTimeConductorInputs.nth(1).fill(endBoundString);
await page.keyboard.press('Enter');
await setIndependentTimeConductorBounds(page, startBoundString, endBoundString);
// Verify that two events are displayed
expect(await activityBounds.count()).toEqual(2);

View File

@@ -41,7 +41,7 @@ test.describe('Clock Generator CRUD Operations', () => {
await page.click('button:has-text("Create")');
// Click Clock
await page.click('text=Clock');
await page.getByRole('menuitem').first().click();
// Click .icon-arrow-down
await page.locator('.icon-arrow-down').click();

View File

@@ -25,7 +25,8 @@ const {
createDomainObjectWithDefaults,
setStartOffset,
setFixedTimeMode,
setRealTimeMode
setRealTimeMode,
setIndependentTimeConductorBounds
} = require('../../../../appActions');
test.describe('Display Layout', () => {
@@ -206,9 +207,60 @@ test.describe('Display Layout', () => {
expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
});
test('independent time works with display layouts and its children', async ({ page }) => {
await setFixedTimeMode(page);
// Create Example Imagery
const exampleImageryObject = await createDomainObjectWithDefaults(page, {
type: 'Example Imagery'
});
// Create a Display Layout
await createDomainObjectWithDefaults(page, {
type: 'Display Layout'
});
// Edit Display Layout
await page.locator('[title="Edit"]').click();
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the Display Layout and save changes
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const exampleImageryTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(exampleImageryObject.name)
});
let layoutGridHolder = page.locator('.l-layout__grid-holder');
await exampleImageryTreeItem.dragTo(layoutGridHolder);
//adjust so that we can see the independent time conductor toggle
// Adjust object height
await page.locator('div[title="Resize object height"] > input').click();
await page.locator('div[title="Resize object height"] > input').fill('70');
// Adjust object width
await page.locator('div[title="Resize object width"] > input').click();
await page.locator('div[title="Resize object width"] > input').fill('70');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
const startDate = '2021-12-30 01:01:00.000Z';
const endDate = '2021-12-30 01:11:00.000Z';
await setIndependentTimeConductorBounds(page, startDate, endDate);
// check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
// flip it off
await page.getByRole('switch').click();
// timestamp shouldn't be in the past anymore
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
});
test('When multiple plots are contained in a layout, we only ask for annotations once @couchdb', async ({
page
}) => {
await setFixedTimeMode(page);
// Create another Sine Wave Generator
const anotherSineWaveObject = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator'
@@ -265,10 +317,20 @@ test.describe('Display Layout', () => {
// wait for annotations requests to be batched and requested
await page.waitForLoadState('networkidle');
// Network requests for the composite telemetry with multiple items should be:
// 1. a single batched request for annotations
expect(networkRequests.length).toBe(1);
await setRealTimeMode(page);
networkRequests = [];
await page.reload();
// wait for annotations to not load (if we have any, we've got a problem)
await page.waitForLoadState('networkidle');
// In real time mode, we don't fetch annotations at all
expect(networkRequests.length).toBe(0);
});
});

View File

@@ -21,7 +21,10 @@
*****************************************************************************/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const {
createDomainObjectWithDefaults,
setIndependentTimeConductorBounds
} = require('../../../../appActions');
test.describe('Flexible Layout', () => {
let sineWaveObject;
@@ -158,4 +161,47 @@ test.describe('Flexible Layout', () => {
// Verify that the item has been removed from the layout
expect(await page.locator('.c-fl-container__frame').count()).toEqual(0);
});
test('independent time works with flexible layouts and its children', async ({ page }) => {
// Create Example Imagery
const exampleImageryObject = await createDomainObjectWithDefaults(page, {
type: 'Example Imagery'
});
// Create a Flexible Layout
await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout'
});
// Edit Display Layout
await page.locator('[title="Edit"]').click();
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the Flexible Layout and save changes
const treePane = page.getByRole('tree', {
name: 'Main Tree'
});
const exampleImageryTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(exampleImageryObject.name)
});
// Add the Sine Wave Generator to the Flexible Layout and save changes
await exampleImageryTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
// flip on independent time conductor
await setIndependentTimeConductorBounds(
page,
'2021-12-30 01:01:00.000Z',
'2021-12-30 01:11:00.000Z'
);
// check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
// flip it off
await page.getByRole('switch').click();
// timestamp shouldn't be in the past anymore
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
});
});

View File

@@ -27,7 +27,7 @@ but only assume that example imagery is present.
/* globals process */
const { waitForAnimations } = require('../../../../baseFixtures');
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const { createDomainObjectWithDefaults, setRealTimeMode } = require('../../../../appActions');
const backgroundImageSelector = '.c-imagery__main-image__background-image';
const panHotkey = process.platform === 'linux' ? ['Shift', 'Alt'] : ['Alt'];
const tagHotkey = ['Shift', 'Alt'];
@@ -46,6 +46,7 @@ test.describe('Example Imagery Object', () => {
// Verify that the created object is focused
await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);
await page.locator('.c-imagery__main-image__bg').hover({ trial: true });
await page.locator(backgroundImageSelector).waitFor();
});
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
@@ -70,6 +71,71 @@ test.describe('Example Imagery Object', () => {
await dragContrastSliderAndAssertFilterValues(page);
});
test('Can use independent time conductor to change time', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6821'
});
// Test independent fixed time with global fixed time
// flip on independent time conductor
await page.getByRole('switch', { name: 'Enable Independent Time Conductor' }).click();
// Adding in delay to address flakiness of ITC test-- button event handlers not registering in time
await expect(page.locator('#independentTCToggle')).toBeChecked();
await expect(page.locator('.c-compact-tc').first()).toBeVisible();
await page.getByRole('button', { name: 'Independent Time Conductor Settings' }).click();
await page.getByRole('textbox', { name: 'Start date' }).fill('2021-12-30');
await page.keyboard.press('Tab');
await page.getByRole('textbox', { name: 'Start time' }).fill('01:01:00');
await page.keyboard.press('Tab');
await page.getByRole('textbox', { name: 'End date' }).fill('2021-12-30');
await page.keyboard.press('Tab');
await page.getByRole('textbox', { name: 'End time' }).fill('01:11:00');
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
// check image date
await expect(page.getByText('2021-12-30 01:01:00.000Z').first()).toBeVisible();
// flip it off
await page.getByRole('switch', { name: 'Disable Independent Time Conductor' }).click();
// timestamp shouldn't be in the past anymore
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
// Test independent fixed time with global realtime
await setRealTimeMode(page);
await expect(
page.getByRole('switch', { name: 'Enable Independent Time Conductor' })
).toBeEnabled();
await page.getByRole('switch', { name: 'Enable Independent Time Conductor' }).click();
// check image date to be in the past
await expect(page.getByText('2021-12-30 01:01:00.000Z').first()).toBeVisible();
// flip it off
await page.getByRole('switch', { name: 'Disable Independent Time Conductor' }).click();
// timestamp shouldn't be in the past anymore
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
// Test independent realtime with global realtime
await page.getByRole('switch', { name: 'Enable Independent Time Conductor' }).click();
// check image date
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
// change independent time to realtime
await page.getByRole('button', { name: 'Independent Time Conductor Settings' }).click();
await page.getByRole('button', { name: 'Independent Time Conductor Mode Menu' }).click();
await page.getByRole('menuitem', { name: /Real-Time/ }).click();
// timestamp shouldn't be in the past anymore
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
// back to the past
await page.getByRole('button', { name: 'Independent Time Conductor Mode Menu' }).click();
await page.getByRole('menuitem', { name: /Real-Time/ }).click();
await page.getByRole('button', { name: 'Independent Time Conductor Mode Menu' }).click();
await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click();
// check image date to be in the past
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
});
test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
const deltaYStep = 100; //equivalent to 1x zoom
@@ -189,11 +255,9 @@ test.describe('Example Imagery Object', () => {
test('Using the zoom features does not pause telemetry', async ({ page }) => {
const pausePlayButton = page.locator('.c-button.pause-play');
// open the time conductor drop down
await page.locator('.c-mode-button').click();
// switch to realtime
await setRealTimeMode(page);
// Click local clock
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
// Zoom in via button
@@ -203,7 +267,7 @@ test.describe('Example Imagery Object', () => {
test('Uses low fetch priority', async ({ page }) => {
const priority = await page.locator('.js-imageryView-image').getAttribute('fetchpriority');
await expect(priority).toBe('low');
expect(priority).toBe('low');
});
});
@@ -233,14 +297,11 @@ test.describe('Example Imagery in Display Layout', () => {
description: 'https://github.com/nasa/openmct/issues/3647'
});
// Click time conductor mode button
await page.locator('.c-mode-button').click();
// set realtime mode
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
await setRealTimeMode(page);
// pause/play button
const pausePlayButton = await page.locator('.c-button.pause-play');
const pausePlayButton = page.locator('.c-button.pause-play');
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
@@ -259,14 +320,11 @@ test.describe('Example Imagery in Display Layout', () => {
description: 'https://github.com/nasa/openmct/issues/3647'
});
// Click time conductor mode button
await page.locator('.c-mode-button').click();
// set realtime mode
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
await setRealTimeMode(page);
// pause/play button
const pausePlayButton = await page.locator('.c-button.pause-play');
const pausePlayButton = page.locator('.c-button.pause-play');
await pausePlayButton.click();
await expect.soft(pausePlayButton).toHaveClass(/is-paused/);
@@ -544,11 +602,8 @@ async function performImageryViewOperationsAndAssert(page) {
const nextImageButton = page.locator('.c-nav--next');
await nextImageButton.click();
// Click time conductor mode button
await page.locator('.c-mode-button').click();
// Select local clock mode
await page.locator('[data-testid=conductor-modeOption-realtime]').click();
// set realtime mode
await setRealTimeMode(page);
// Zoom in on next image
await mouseZoomOnImageAndAssert(page, 2);

View File

@@ -51,10 +51,9 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
page.on('request', (request) => notebookElementsRequests.push(request));
//Clicking Add Page generates
let [notebookUrlRequest, allDocsRequest] = await Promise.all([
let [notebookUrlRequest] = await Promise.all([
// Waits for the next request with the specified url
page.waitForRequest(`**/openmct/${testNotebook.uuid}`),
page.waitForRequest('**/openmct/_all_docs?include_docs=true'),
// Triggers the request
page.click('[aria-label="Add Page"]')
]);
@@ -64,15 +63,13 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
// Assert that only two requests are made
// Network Requests are:
// 1) The actual POST to create the page
// 2) The shared worker event from 👆 request
expect(notebookElementsRequests.length).toBe(2);
expect(notebookElementsRequests.length).toBe(1);
// Assert on request object
expect(notebookUrlRequest.postDataJSON().metadata.name).toBe(testNotebook.name);
expect(notebookUrlRequest.postDataJSON().model.persisted).toBeGreaterThanOrEqual(
notebookUrlRequest.postDataJSON().model.modified
);
expect(allDocsRequest.postDataJSON().keys).toContain(testNotebook.uuid);
// Add an entry
// Network Requests are:

View File

@@ -134,7 +134,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
// Click the context menu button for the new page
await page.getByTitle('Open context menu').click();
// Delete the page
await page.getByRole('listitem', { name: 'Delete Page' }).click();
await page.getByRole('menuitem', { name: 'Delete Page' }).click();
// Click OK button
await page.getByRole('button', { name: 'Ok' }).click();

View File

@@ -47,6 +47,11 @@ test.describe('Operator Status', () => {
path: path.join(__dirname, '../../../../helper/', 'addInitOperatorStatus.js')
});
await page.goto('./', { waitUntil: 'domcontentloaded' });
await expect(page.getByText('Select Role')).toBeVisible();
// set role
await page.getByRole('button', { name: 'Select' }).click();
// dismiss role confirmation popup
await page.getByRole('button', { name: 'Dismiss' }).click();
});
// verify that operator status is visible

View File

@@ -24,7 +24,7 @@
Testsuite for plot autoscale.
*/
const { selectInspectorTab } = 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']);
@@ -107,7 +116,7 @@ test.describe('Autoscale', () => {
await page.keyboard.up('Alt');
// Ensure the drag worked.
await testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00', '2.50', '3.00', '3.50']);
await testYTicks(page, ['-0.50', '0.00', '0.50', '1.00', '1.50', '2.00', '2.50', '3.00']);
//Wait for canvas to stablize.
await canvas.hover({ trial: true });
@@ -118,77 +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.
const timeInputs = page.locator('input.c-input--datetime');
await timeInputs.first().click();
await timeInputs.first().fill(start);
await timeInputs.nth(1).click();
await timeInputs.nth(1).fill(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
*/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -26,7 +26,7 @@ necessarily be used for reference when writing new tests in this area.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { selectInspectorTab } = require('../../../../appActions');
const { selectInspectorTab, setTimeConductorBounds } = require('../../../../appActions');
test.describe('Log plot tests', () => {
test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({
@@ -87,12 +87,10 @@ async function makeOverlayPlot(page, myItemsFolderName) {
// Set a specific time range for consistency, otherwise it will change
// on every test to a range based on the current time.
const timeInputs = page.locator('input.c-input--datetime');
await timeInputs.first().click();
await timeInputs.first().fill('2022-03-29 22:00:00.000Z');
const start = '2022-03-29 22:00:00.000Z';
const end = '2022-03-29 22:00:30.000Z';
await timeInputs.nth(1).click();
await timeInputs.nth(1).fill('2022-03-29 22:00:30.000Z');
await setTimeConductorBounds(page, start, end);
// create overlay plot

View File

@@ -26,7 +26,11 @@ necessarily be used for reference when writing new tests in this area.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions');
const {
createDomainObjectWithDefaults,
selectInspectorTab,
waitForPlotsToRender
} = require('../../../../appActions');
test.describe('Stacked Plot', () => {
let stackedPlot;
@@ -227,4 +231,45 @@ test.describe('Stacked Plot', () => {
page.locator('[aria-label="Plot Series Properties"] .c-object-label')
).toContainText(swgC.name);
});
test('the legend toggles between aggregate and per child', async ({ page }) => {
await page.goto(stackedPlot.url);
// Go into edit mode
await page.click('button[title="Edit"]');
await selectInspectorTab(page, 'Config');
let legendProperties = await page.locator('[aria-label="Legend Properties"]');
await legendProperties.locator('[title="Display legends per sub plot."]~div input').uncheck();
await assertAggregateLegendIsVisible(page);
// Save (exit edit mode)
await page.locator('button[title="Save"]').click();
await page.locator('li[title="Save and Finish Editing"]').click();
await assertAggregateLegendIsVisible(page);
await page.reload();
await assertAggregateLegendIsVisible(page);
});
});
/**
* Asserts that aggregate stacked plot legend is visible
* @param {import('@playwright/test').Page} page
*/
async function assertAggregateLegendIsVisible(page) {
// Wait for plot series data to load
await waitForPlotsToRender(page);
// Wait for plot legend to be shown
await page.waitForSelector('.js-stacked-plot-legend', { state: 'attached' });
// There should be 3 legend items
expect(
await page
.locator('.js-stacked-plot-legend .c-plot-legend__wrapper div.plot-legend-item')
.count()
).toBe(3);
}

View File

@@ -41,7 +41,7 @@ test.describe('Plot Tagging', () => {
* @param {Number} yEnd a telemetry item with a plot
* @returns {Promise}
*/
async function createTags({ page, canvas, xEnd, yEnd }) {
async function createTags({ page, canvas, xEnd = 700, yEnd = 480 }) {
await canvas.hover({ trial: true });
//Alt+Shift Drag Start to select some points to tag
@@ -97,8 +97,8 @@ test.describe('Plot Tagging', () => {
// click on the tagged plot point
await canvas.click({
position: {
x: 325,
y: 377
x: 100,
y: 100
}
});
@@ -167,8 +167,11 @@ test.describe('Plot Tagging', () => {
});
test('Tags work with Overlay Plots', async ({ page }) => {
//Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
test.slow();
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6822'
});
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
@@ -177,13 +180,19 @@ test.describe('Plot Tagging', () => {
const alphaSineWave = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Alpha Sine Wave',
parent: overlayPlot.uuid
parent: overlayPlot.uuid,
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.01'
}
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Beta Sine Wave',
parent: overlayPlot.uuid
parent: overlayPlot.uuid,
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.02'
}
});
await page.goto(overlayPlot.url);
@@ -196,9 +205,7 @@ test.describe('Plot Tagging', () => {
await createTags({
page,
canvas,
xEnd: 700,
yEnd: 480
canvas
});
await setFixedTimeMode(page);
@@ -228,15 +235,15 @@ test.describe('Plot Tagging', () => {
test('Tags work with Plot View of telemetry items', async ({ page }) => {
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator'
type: 'Sine Wave Generator',
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.01'
}
});
const canvas = page.locator('canvas').nth(1);
await createTags({
page,
canvas,
xEnd: 700,
yEnd: 480
canvas
});
await basicTagsTests(page);
});
@@ -249,13 +256,19 @@ test.describe('Plot Tagging', () => {
const alphaSineWave = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Alpha Sine Wave',
parent: stackedPlot.uuid
parent: stackedPlot.uuid,
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.01'
}
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Beta Sine Wave',
parent: stackedPlot.uuid
parent: stackedPlot.uuid,
customParameters: {
'[aria-label="Data Rate (hz)"]': '0.02'
}
});
await page.goto(stackedPlot.url);

View File

@@ -20,7 +20,10 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const {
createDomainObjectWithDefaults,
setTimeConductorBounds
} = require('../../../../appActions');
const { test, expect } = require('../../../../pluginFixtures');
test.describe('Telemetry Table', () => {
@@ -51,18 +54,14 @@ test.describe('Telemetry Table', () => {
await expect(tableWrapper).toHaveClass(/is-paused/);
// Subtract 5 minutes from the current end bound datetime and set it
const endTimeInput = page.locator('input[type="text"].c-input--datetime').nth(1);
await endTimeInput.click();
let endDate = await endTimeInput.inputValue();
// Bring up the time conductor popup
let endDate = await page.locator('[aria-label="End bounds"]').textContent();
endDate = new Date(endDate);
endDate.setUTCMinutes(endDate.getUTCMinutes() - 5);
endDate = endDate.toISOString().replace(/T/, ' ');
await endTimeInput.fill('');
await endTimeInput.fill(endDate);
await page.keyboard.press('Enter');
await setTimeConductorBounds(page, undefined, endDate);
await expect(tableWrapper).not.toHaveClass(/is-paused/);

View File

@@ -25,7 +25,8 @@ const {
setFixedTimeMode,
setRealTimeMode,
setStartOffset,
setEndOffset
setEndOffset,
setTimeConductorBounds
} = require('../../../../appActions');
test.describe('Time conductor operations', () => {
@@ -40,38 +41,36 @@ test.describe('Time conductor operations', () => {
let endDate = 'xxxx-01-01 02:00:00.000Z';
endDate = year + endDate.substring(4);
const startTimeLocator = page.locator('input[type="text"]').first();
const endTimeLocator = page.locator('input[type="text"]').nth(1);
// Click start time
await startTimeLocator.click();
// Click end time
await endTimeLocator.click();
await endTimeLocator.fill(endDate.toString());
await startTimeLocator.fill(startDate.toString());
await setTimeConductorBounds(page, startDate, endDate);
// invalid start date
startDate = year + 1 + startDate.substring(4);
await startTimeLocator.fill(startDate.toString());
await endTimeLocator.click();
await setTimeConductorBounds(page, startDate);
const startDateValidityStatus = await startTimeLocator.evaluate((element) =>
// Bring up the time conductor popup
const timeConductorMode = await page.locator('.c-compact-tc');
await timeConductorMode.click();
const startDateLocator = page.locator('input[type="text"]').first();
const endDateLocator = page.locator('input[type="text"]').nth(2);
await endDateLocator.click();
const startDateValidityStatus = await startDateLocator.evaluate((element) =>
element.checkValidity()
);
expect(startDateValidityStatus).not.toBeTruthy();
// fix to valid start date
startDate = year - 1 + startDate.substring(4);
await startTimeLocator.fill(startDate.toString());
await setTimeConductorBounds(page, startDate);
// invalid end date
endDate = year - 2 + endDate.substring(4);
await endTimeLocator.fill(endDate.toString());
await startTimeLocator.click();
await setTimeConductorBounds(page, undefined, endDate);
const endDateValidityStatus = await endTimeLocator.evaluate((element) =>
await startDateLocator.click();
const endDateValidityStatus = await endDateLocator.evaluate((element) =>
element.checkValidity()
);
expect(endDateValidityStatus).not.toBeTruthy();
@@ -83,11 +82,11 @@ test.describe('Time conductor operations', () => {
test.describe('Time conductor input fields real-time mode', () => {
test('validate input fields in real-time mode', async ({ page }) => {
const startOffset = {
secs: '23'
startSecs: '23'
};
const endOffset = {
secs: '31'
endSecs: '31'
};
// Go to baseURL
@@ -100,15 +99,13 @@ test.describe('Time conductor input fields real-time mode', () => {
await setStartOffset(page, startOffset);
// Verify time was updated on time offset button
await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText(
'00:30:23'
);
await expect(page.locator('.c-compact-tc__setting-value.icon-minus')).toContainText('00:30:23');
// Set end time offset
await setEndOffset(page, endOffset);
// Verify time was updated on preceding time offset button
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:31');
await expect(page.locator('.c-compact-tc__setting-value.icon-plus')).toContainText('00:00:31');
});
/**
@@ -119,12 +116,12 @@ test.describe('Time conductor input fields real-time mode', () => {
page
}) => {
const startOffset = {
mins: '30',
secs: '23'
startMins: '30',
startSecs: '23'
};
const endOffset = {
secs: '01'
endSecs: '01'
};
// Convert offsets to milliseconds
@@ -150,12 +147,10 @@ test.describe('Time conductor input fields real-time mode', () => {
await setRealTimeMode(page);
// Verify updated start time offset persists after mode switch
await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText(
'00:30:23'
);
await expect(page.locator('.c-compact-tc__setting-value.icon-minus')).toContainText('00:30:23');
// Verify updated end time offset persists after mode switch
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01');
await expect(page.locator('.c-compact-tc__setting-value.icon-plus')).toContainText('00:00:01');
// Verify url parameters persist after mode switch
await page.waitForNavigation({ waitUntil: 'networkidle' });
@@ -203,11 +198,11 @@ test.describe('Time Conductor History', () => {
// with startBound at 2022-01-01 00:00:00.000Z
// and endBound at 2022-01-01 00:00:00.200Z
await page.goto(
'./#/browse/mine?view=grid&tc.mode=fixed&tc.startBound=1640995200000&tc.endBound=1640995200200&tc.timeSystem=utc&hideInspector=true',
{ waitUntil: 'networkidle' }
'./#/browse/mine?view=grid&tc.mode=fixed&tc.startBound=1640995200000&tc.endBound=1640995200200&tc.timeSystem=utc&hideInspector=true'
);
await page.locator("[aria-label='Time Conductor History']").hover({ trial: true });
await page.locator("[aria-label='Time Conductor History']").click();
await page.getByRole('button', { name: 'Time Conductor Settings' }).click();
await page.getByRole('button', { name: 'Time Conductor History' }).hover({ trial: true });
await page.getByRole('button', { name: 'Time Conductor History' }).click();
// Validate history item format
const historyItem = page.locator('text="2022-01-01 00:00:00 + 200ms"');

View File

@@ -59,53 +59,60 @@ test.describe('Recent Objects', () => {
await page.mouse.move(0, 100);
await page.mouse.up();
});
test('Navigated objects show up in recents, object renames and deletions are reflected', async ({
page
}) => {
// Verify that both created objects appear in the list and are in the correct order
await assertInitialRecentObjectsListState();
// Navigate to the folder by clicking on the main object name in the recent objects list item
await page.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click();
await page.waitForURL(`**/${folderA.uuid}?*`);
expect(recentObjectsList.getByRole('listitem').nth(0).getByText(folderA.name)).toBeTruthy();
// Rename
folderA.name = `${folderA.name}-NEW!`;
await page.locator('.l-browse-bar__object-name').fill('');
await page.locator('.l-browse-bar__object-name').fill(folderA.name);
await page.keyboard.press('Enter');
// Verify rename has been applied in recent objects list item and objects paths
expect(
await page
.getByRole('navigation', {
name: clock.name
})
.locator('a')
.filter({
hasText: folderA.name
})
.count()
).toBeGreaterThan(0);
expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy();
// Delete
await page.click('button[title="Show selected item in tree"]');
// Delete the folder via the left tree pane treeitem context menu
await page
.getByRole('treeitem', { name: new RegExp(folderA.name) })
.locator('a')
.click({
button: 'right'
test.fixme(
'Navigated objects show up in recents, object renames and deletions are reflected',
async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6818'
});
await page.getByRole('menuitem', { name: /Remove/ }).click();
await page.getByRole('button', { name: 'OK' }).click();
// Verify that the folder and clock are no longer in the recent objects list
await expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeHidden();
await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeHidden();
});
// Verify that both created objects appear in the list and are in the correct order
await assertInitialRecentObjectsListState();
// Navigate to the folder by clicking on the main object name in the recent objects list item
await page.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click();
await page.waitForURL(`**/${folderA.uuid}?*`);
expect(recentObjectsList.getByRole('listitem').nth(0).getByText(folderA.name)).toBeTruthy();
// Rename
folderA.name = `${folderA.name}-NEW!`;
await page.locator('.l-browse-bar__object-name').fill('');
await page.locator('.l-browse-bar__object-name').fill(folderA.name);
await page.keyboard.press('Enter');
// Verify rename has been applied in recent objects list item and objects paths
expect(
await page
.getByRole('navigation', {
name: clock.name
})
.locator('a')
.filter({
hasText: folderA.name
})
.count()
).toBeGreaterThan(0);
expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy();
// Delete
await page.click('button[title="Show selected item in tree"]');
// Delete the folder via the left tree pane treeitem context menu
await page
.getByRole('treeitem', { name: new RegExp(folderA.name) })
.locator('a')
.click({
button: 'right'
});
await page.getByRole('menuitem', { name: /Remove/ }).click();
await page.getByRole('button', { name: 'OK' }).click();
// Verify that the folder and clock are no longer in the recent objects list
await expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeHidden();
await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeHidden();
}
);
test('Clicking on an object in the path of a recent object navigates to the object', async ({
page,
openmctConfig

View File

@@ -77,11 +77,11 @@ test.describe('Grand Search', () => {
// Click [aria-label="OpenMCT Search"] a >> nth=0
await page.locator('[aria-label="Search Result"] >> nth=0').click();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeInViewport();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo');
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toBeInViewport();
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
await page

View File

@@ -0,0 +1,398 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which can quickly verify that any openmct installation is
operable and that any type of testing can proceed.
Ideally, smoke tests should make zero assumptions about how and where they are run. This makes them
more resilient to change and therefor a better indicator of failure. Smoke tests will also run quickly
as they cover a very "thin surface" of functionality.
When deciding between authoring new smoke tests or functional tests, ask yourself "would I feel
comfortable running this test during a live mission?" Avoid creating or deleting Domain Objects.
Make no assumptions about the order that elements appear in the DOM.
*/
const { test, expect } = require('../../pluginFixtures');
const { createDomainObjectWithDefaults, expandEntireTree } = require('../../appActions');
test.describe('Verify tooltips', () => {
let folder1;
let folder2;
let folder3;
let sineWaveObject1;
let sineWaveObject2;
let sineWaveObject3;
const swg1Path = 'My Items / Folder Foo / SWG 1';
const swg2Path = 'My Items / Folder Foo / Folder Bar / SWG 2';
const swg3Path = 'My Items / Folder Foo / Folder Bar / Folder Baz / SWG 3';
test.beforeEach(async ({ page, openmctConfig }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
folder1 = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Folder Foo'
});
folder2 = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Folder Bar',
parent: folder1.uuid
});
folder3 = await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Folder Baz',
parent: folder2.uuid
});
// Create Sine Wave Generator
sineWaveObject1 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'SWG 1',
parent: folder1.uuid
});
sineWaveObject1.path = swg1Path;
sineWaveObject2 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'SWG 2',
parent: folder2.uuid
});
sineWaveObject2.path = swg2Path;
sineWaveObject3 = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'SWG 3',
parent: folder3.uuid
});
sineWaveObject3.path = swg3Path;
// Expand all folders
await expandEntireTree(page);
});
// LAD Tables - DONE
// Expanded collapsed plot legend - DONE
// Object Labels - DONE
// Display Layout headers - DONE
// Flexible Layout headers - DONE
// Tab View layout headers - DONE
// Search - DONE
// Gauge -
// Notebook Embed - DONE
// Telemetry Table -
// Timeline Objects
// Tree - DONE
// Recent Objects
test('display correct paths for LAD tables', async ({ page, openmctConfig }) => {
// Create LAD table
await createDomainObjectWithDefaults(page, {
type: 'LAD Table',
name: 'Test LAD Table'
});
// Edit LAD table
await page.locator('[title="Edit"]').click();
// Add the Sine Wave Generator to the LAD table and save changes
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-lad-table-wrapper');
await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.c-lad-table-wrapper');
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-lad-table-wrapper');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.keyboard.down('Control');
async function getToolTip(object) {
await page.locator('.c-create-button').hover();
await page.getByRole('cell', { name: object.name }).hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
return tooltipText.replace('\n', '').trim();
}
expect(await getToolTip(sineWaveObject1)).toBe(sineWaveObject1.path);
expect(await getToolTip(sineWaveObject2)).toBe(sineWaveObject2.path);
expect(await getToolTip(sineWaveObject3)).toBe(sineWaveObject3.path);
});
test('display correct paths for expanded and collapsed plot legend items', async ({ page }) => {
// Create Overlay Plot
await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: 'Test Overlay Plots'
});
// Edit Overlay Plot
await page.locator('[title="Edit"]').click();
// Add the Sine Wave Generator to the LAD table and save changes
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.gl-plot');
await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.gl-plot');
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.gl-plot');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.keyboard.down('Control');
async function getCollapsedLegendToolTip(object) {
await page.locator('.c-create-button').hover();
await page
.locator('.plot-series-name', { has: page.locator(`text="${object.name} Hz"`) })
.hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
return tooltipText.replace('\n', '').trim();
}
async function getExpandedLegendToolTip(object) {
await page.locator('.c-create-button').hover();
await page
.locator('.plot-series-name', { has: page.locator(`text="${object.name}"`) })
.hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
return tooltipText.replace('\n', '').trim();
}
expect(await getCollapsedLegendToolTip(sineWaveObject1)).toBe(sineWaveObject1.path);
expect(await getCollapsedLegendToolTip(sineWaveObject2)).toBe(sineWaveObject2.path);
expect(await getCollapsedLegendToolTip(sineWaveObject3)).toBe(sineWaveObject3.path);
await page.keyboard.up('Control');
await page.locator('.gl-plot-legend__view-control.c-disclosure-triangle').click();
await page.keyboard.down('Control');
expect(await getExpandedLegendToolTip(sineWaveObject1)).toBe(sineWaveObject1.path);
expect(await getExpandedLegendToolTip(sineWaveObject2)).toBe(sineWaveObject2.path);
expect(await getExpandedLegendToolTip(sineWaveObject3)).toBe(sineWaveObject3.path);
});
test('display correct paths when hovering over object labels', async ({ page }) => {
async function getObjectLabelTooltip(object) {
await page
.locator('.c-tree__item__name.c-object-label__name', {
has: page.locator(`text="${object.name}"`)
})
.click();
await page.keyboard.down('Control');
await page
.locator('.l-browse-bar__object-name.c-object-label__name', {
has: page.locator(`text="${object.name}"`)
})
.hover();
const tooltipText = await page.locator('.c-tooltip').textContent();
await page.keyboard.up('Control');
return tooltipText.replace('\n', '').trim();
}
expect(await getObjectLabelTooltip(sineWaveObject1)).toBe(sineWaveObject1.path);
expect(await getObjectLabelTooltip(sineWaveObject3)).toBe(sineWaveObject3.path);
});
test('display correct paths when hovering over display layout pane headers', async ({ page }) => {
// Create Overlay Plot
await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: 'Test Overlay Plot'
});
// Edit Overlay Plot
await page.locator('[title="Edit"]').click();
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.gl-plot');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
// Create Stacked Plot
await createDomainObjectWithDefaults(page, {
type: 'Stacked Plot',
name: 'Test Stacked Plot'
});
// Edit Stacked Plot
await page.locator('[title="Edit"]').click();
await page.dragAndDrop(`text=${sineWaveObject2.name}`, '.c-plot--stacked.holder');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
// Create Display Layout
await createDomainObjectWithDefaults(page, {
type: 'Display Layout',
name: 'Test Display Layout'
});
// Edit Display Layout
await page.locator('[title="Edit"]').click();
await page.dragAndDrop("text='Test Overlay Plot'", '.l-layout__grid-holder', {
targetPosition: { x: 0, y: 0 }
});
await page.dragAndDrop("text='Test Stacked Plot'", '.l-layout__grid-holder', {
targetPosition: { x: 0, y: 250 }
});
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.l-layout__grid-holder', {
targetPosition: { x: 500, y: 200 }
});
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.keyboard.down('Control');
await page.getByText('Test Overlay Plot').nth(2).hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe('My Items / Test Overlay Plot');
// await page.keyboard.up('Control');
// await page.locator('.c-plot-legend__view-control >> nth=0').click();
// await page.keyboard.down('Control');
// await page.locator('.plot-wrapper-expanded-legend .plot-series-name').first().hover();
// tooltipText = await page.locator('.c-tooltip').textContent();
// tooltipText = tooltipText.replace('\n', '').trim();
// expect(tooltipText).toBe(sineWaveObject1.path);
await page.getByText('Test Stacked Plot').nth(2).hover();
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe('My Items / Test Stacked Plot');
await page.getByText('SWG 3').nth(2).hover();
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(sineWaveObject3.path).toBe(tooltipText);
});
test('display correct paths when hovering over flexible object labels', async ({ page }) => {
await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout',
name: 'Test Flexible Layout'
});
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-fl__container >> nth=0');
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-fl__container >> nth=1');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.keyboard.down('Control');
await page.getByText('SWG 1').nth(2).hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject1.path);
await page.getByText('SWG 3').nth(2).hover();
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
});
test('display correct paths when hovering over tab view labels', async ({ page }) => {
await createDomainObjectWithDefaults(page, {
type: 'Tabs View',
name: 'Test Tabs View'
});
await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-tabs-view__tabs-holder');
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-tabs-view__tabs-holder');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
await page.keyboard.down('Control');
await page.getByText('SWG 1').nth(2).hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject1.path);
await page.getByText('SWG 3').nth(2).hover();
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
});
test('display correct paths when hovering tree items', async ({ page }) => {
await page.keyboard.down('Control');
await page.getByText('SWG 1').nth(0).hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject1.path);
await page.getByText('SWG 3').nth(0).hover();
tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
});
test('display correct paths when hovering search items', async ({ page }) => {
await page.getByRole('searchbox', { name: 'Search Input' }).click();
await page.fill('.c-search__input', 'SWG 3');
await page.keyboard.down('Control');
await page.locator('.c-gsearch-result__title').hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
});
test('display path for source telemetry when hovering over gauge', ({ page }) => {
expect(true).toBe(true);
// await createDomainObjectWithDefaults(page, {
// type: 'Gauge',
// name: 'Test Gauge'
// });
// await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-gauge__wrapper');
// await page.keyboard.down('Control');
// await page.locator('.c-gauge__current-value-text-wrapper').hover();
// let tooltipText = await page.locator('.c-tooltip').textContent();
// tooltipText = tooltipText.replace('\n', '').trim();
// expect(tooltipText).toBe(sineWaveObject3.path);
});
test('display tooltip path for notebook embeds', async ({ page }) => {
await createDomainObjectWithDefaults(page, {
type: 'Notebook',
name: 'Test Notebook'
});
await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-notebook__drag-area');
await page.keyboard.down('Control');
await page.locator('.c-ne__embed').hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
tooltipText = tooltipText.replace('\n', '').trim();
expect(tooltipText).toBe(sineWaveObject3.path);
});
// test('display tooltip path for telemetry table names', async ({ page }) => {
// await setEndOffset(page, { secs: '10' });
// await createDomainObjectWithDefaults(page, {
// type: 'Telemetry Table',
// name: 'Test Telemetry Table'
// });
// await page.dragAndDrop(`text=${sineWaveObject1.name}`, '.c-telemetry-table');
// await page.dragAndDrop(`text=${sineWaveObject3.name}`, '.c-telemetry-table');
// await page.locator('button[title="Save"]').click();
// await page.locator('text=Save and Finish Editing').click();
// // .c-telemetry-table__body
// await page.keyboard.down('Control');
// await page.locator('.noselect > [title="SWG 3"]').first().hover();
// let tooltipText = await page.locator('.c-tooltip').textContent();
// tooltipText = tooltipText.replace('\n', '').trim();
// expect(tooltipText).toBe(sineWaveObject3.path);
// });
});

View File

@@ -174,6 +174,42 @@ test.describe('Main Tree', () => {
]);
});
});
test('Opening and closing an item before the request has been fulfilled will abort the request @couchdb', async ({
page,
openmctConfig
}) => {
const { myItemsFolderName } = openmctConfig;
let requestWasAborted = false;
page.on('requestfailed', (request) => {
// check if the request was aborted
if (request.failure().errorText === 'net::ERR_ABORTED') {
requestWasAborted = true;
}
});
await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Foo'
});
// Intercept and delay request
const delayInMs = 500;
await page.route('**', async (route, request) => {
await new Promise((resolve) => setTimeout(resolve, delayInMs));
route.continue();
});
// Quickly Expand/close the root folder
await page
.getByRole('button', {
name: `Expand ${myItemsFolderName} folder`
})
.dblclick({ delay: 400 });
expect(requestWasAborted).toBe(true);
});
});
/**

View File

@@ -0,0 +1,273 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
Tests to verify plot tagging performance.
*/
const { test, expect } = require('../../pluginFixtures');
const {
createDomainObjectWithDefaults,
setRealTimeMode,
setFixedTimeMode,
waitForPlotsToRender
} = require('../../appActions');
test.describe.fixme('Plot Tagging Performance', () => {
/**
* Given a canvas and a set of points, tags the points on the canvas.
* @param {import('@playwright/test').Page} page
* @param {HTMLCanvasElement} canvas a telemetry item with a plot
* @param {Number} xEnd a telemetry item with a plot
* @param {Number} yEnd a telemetry item with a plot
* @returns {Promise}
*/
async function createTags({ page, canvas, xEnd = 700, yEnd = 480 }) {
await canvas.hover({ trial: true });
//Alt+Shift Drag Start to select some points to tag
await page.keyboard.down('Alt');
await page.keyboard.down('Shift');
await canvas.dragTo(canvas, {
sourcePosition: {
x: 1,
y: 1
},
targetPosition: {
x: xEnd,
y: yEnd
}
});
//Alt Drag End
await page.keyboard.up('Alt');
await page.keyboard.up('Shift');
//Wait for canvas to stablize.
await canvas.hover({ trial: true });
// add some tags
await page.getByText('Annotations').click();
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Driving').click();
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Science').click();
}
/**
* Given a telemetry item (e.g., a Sine Wave Generator) with a plot, tests that the plot can be tagged.
* @param {import('@playwright/test').Page} page
* @param {import('../../../../appActions').CreatedObjectInfo} telemetryItem a telemetry item with a plot
* @returns {Promise}
*/
async function testTelemetryItem(page, telemetryItem) {
// Check that telemetry item also received the tag
await page.goto(telemetryItem.url);
await expect(page.getByText('No tags to display for this item')).toBeVisible();
const canvas = page.locator('canvas').nth(1);
//Wait for canvas to stablize.
await canvas.hover({ trial: true });
// click on the tagged plot point
await canvas.click({
position: {
x: 100,
y: 100
}
});
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeHidden();
}
/**
* Given a page, tests that tags are searchable, deletable, and persist across reloads.
* @param {import('@playwright/test').Page} page
* @returns {Promise}
*/
async function basicTagsTests(page) {
// Search for Driving
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Clicking elsewhere should cause annotation selection to be cleared
await expect(page.getByText('No tags to display for this item')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv');
// click on the search result
await page
.getByRole('searchbox', { name: 'OpenMCT Search' })
.getByText(/Sine Wave/)
.first()
.click();
// Delete Driving
await page.hover('[aria-label="Tag"]:has-text("Driving")');
await page.locator('[aria-label="Remove tag Driving"]').click();
// Search for Science
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
await expect(page.locator('[aria-label="Search Result"]').nth(0)).toContainText('Science');
await expect(page.locator('[aria-label="Search Result"]').nth(0)).not.toContainText('Drilling');
// Search for Driving
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv');
await expect(page.getByText('No results found')).toBeVisible();
//Reload Page
await page.reload({ waitUntil: 'domcontentloaded' });
// wait for plots to load
await waitForPlotsToRender(page);
await page.getByText('Annotations').click();
await expect(page.getByText('No tags to display for this item')).toBeVisible();
const canvas = page.locator('canvas').nth(1);
// click on the tagged plot point
await canvas.click({
position: {
x: 100,
y: 100
}
});
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeHidden();
}
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
});
test('Tags work with Overlay Plots', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6822'
});
//Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
test.slow();
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
const alphaSineWave = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Alpha Sine Wave',
parent: overlayPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Beta Sine Wave',
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
let canvas = page.locator('canvas').nth(1);
// Switch to real-time mode
// Adding tags should pause the plot
await setRealTimeMode(page);
await createTags({
page,
canvas
});
await setFixedTimeMode(page);
await basicTagsTests(page);
await testTelemetryItem(page, alphaSineWave);
// set to real time mode
await setRealTimeMode(page);
// Search for Science
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
// click on the search result
await page
.getByRole('searchbox', { name: 'OpenMCT Search' })
.getByText('Alpha Sine Wave')
.first()
.click();
// wait for plots to load
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
// expect plot to be paused
await expect(page.locator('[title="Resume displaying real-time data"]')).toBeVisible();
await setFixedTimeMode(page);
});
test('Tags work with Plot View of telemetry items', async ({ page }) => {
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator'
});
const canvas = page.locator('canvas').nth(1);
await createTags({
page,
canvas
});
await basicTagsTests(page);
});
test('Tags work with Stacked Plots', async ({ page }) => {
const stackedPlot = await createDomainObjectWithDefaults(page, {
type: 'Stacked Plot'
});
const alphaSineWave = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Alpha Sine Wave',
parent: stackedPlot.uuid
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'Beta Sine Wave',
parent: stackedPlot.uuid
});
await page.goto(stackedPlot.url);
const canvas = page.locator('canvas').nth(1);
await createTags({
page,
canvas,
xEnd: 700,
yEnd: 215
});
await basicTagsTests(page);
await testTelemetryItem(page, alphaSineWave);
});
});

View File

@@ -52,7 +52,8 @@ test.describe('Visual - addInit', () => {
path: path.join(__dirname, '../../helper', './addInitRestrictedNotebook.js')
});
//Go to baseURL
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
await page.getByTitle('Collapse Browse Pane').click();
await createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });

View File

@@ -41,7 +41,8 @@ test.describe('Visual - Controlled Clock @localStorage', () => {
test('Overlay Plot Loading Indicator @localStorage', async ({ page, theme }) => {
// Go to baseURL
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
await page.getByTitle('Collapse Browse Pane').click();
await page.locator('a:has-text("Unnamed Overlay Plot Overlay Plot")').click();
//Ensure that we're on the Unnamed Overlay Plot object

View File

@@ -39,7 +39,8 @@ const { createDomainObjectWithDefaults } = require('../../appActions');
test.describe('Visual - Default', () => {
test.beforeEach(async ({ page }) => {
//Go to baseURL and Hide Tree
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
await page.getByTitle('Collapse Browse Pane').click();
});
test.use({
clockOptions: {
@@ -99,6 +100,8 @@ test.describe('Visual - Default', () => {
let endDate = 'xxxx-01-01 02:00:00.000Z';
endDate = year + endDate.substring(4);
await page.getByRole('button', { name: 'Time Conductor Settings' }).click();
await page.locator('input[type="text"]').nth(1).fill(endDate.toString());
await page.locator('input[type="text"]').first().fill(startDate.toString());

View File

@@ -32,7 +32,8 @@ const percySnapshot = require('@percy/playwright');
test.describe('Grand Search', () => {
test.beforeEach(async ({ page, theme }) => {
//Go to baseURL and Hide Tree
await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
await page.getByTitle('Collapse Browse Pane').click();
});
test.use({
clockOptions: {

View File

@@ -63,16 +63,24 @@ const STATUSES = [
* @implements {StatusUserProvider}
*/
export default class ExampleUserProvider extends EventEmitter {
constructor(openmct, { defaultStatusRole } = { defaultStatusRole: undefined }) {
constructor(
openmct,
{ statusRoles } = {
statusRoles: []
}
) {
super();
this.openmct = openmct;
this.user = undefined;
this.loggedIn = false;
this.autoLoginUser = undefined;
this.status = STATUSES[0];
this.statusRoleValues = statusRoles.map((role) => ({
role: role,
status: STATUSES[0]
}));
this.pollQuestion = undefined;
this.defaultStatusRole = defaultStatusRole;
this.statusRoles = statusRoles;
this.ExampleUser = createExampleUser(this.openmct.user.User);
this.loginPromise = undefined;
@@ -94,14 +102,13 @@ export default class ExampleUserProvider extends EventEmitter {
return this.loginPromise;
}
canProvideStatusForRole() {
return Promise.resolve(true);
canProvideStatusForRole(role) {
return this.statusRoles.includes(role);
}
canSetPollQuestion() {
return Promise.resolve(true);
}
hasRole(roleId) {
if (!this.loggedIn) {
Promise.resolve(undefined);
@@ -110,16 +117,18 @@ export default class ExampleUserProvider extends EventEmitter {
return Promise.resolve(this.user.getRoles().includes(roleId));
}
getStatusRoleForCurrentUser() {
return Promise.resolve(this.defaultStatusRole);
getPossibleRoles() {
return this.user.getRoles();
}
getAllStatusRoles() {
return Promise.resolve([this.defaultStatusRole]);
return Promise.resolve(this.statusRoles);
}
getStatusForRole(role) {
return Promise.resolve(this.status);
const statusForRole = this.statusRoleValues.find((statusRole) => statusRole.role === role);
return Promise.resolve(statusForRole?.status);
}
async getDefaultStatusForRole(role) {
@@ -130,7 +139,8 @@ export default class ExampleUserProvider extends EventEmitter {
setStatusForRole(role, status) {
status.timestamp = Date.now();
this.status = status;
const matchingIndex = this.statusRoleValues.findIndex((statusRole) => statusRole.role === role);
this.statusRoleValues[matchingIndex].status = status;
this.emit('statusChange', {
role,
status
@@ -175,7 +185,7 @@ export default class ExampleUserProvider extends EventEmitter {
// for testing purposes, this will skip the form, this wouldn't be used in
// a normal authentication process
if (this.autoLoginUser) {
this.user = new this.ExampleUser(id, this.autoLoginUser, ['example-role']);
this.user = new this.ExampleUser(id, this.autoLoginUser, ['flight', 'driver', 'observer']);
this.loggedIn = true;
return Promise.resolve();

View File

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

View File

@@ -62,7 +62,7 @@ export default class SinewaveLimitProvider extends EventEmitter {
const id = this.#getObjectKeyString(domainObject);
if (this.#isRealTime === undefined) {
this.#updateRealTime(this.#openmct.time.clock());
this.#updateRealTime(this.#openmct.time.getMode());
}
this.#handleClockUpdate();
@@ -92,15 +92,15 @@ export default class SinewaveLimitProvider extends EventEmitter {
if (observers && !this.#watchingTheClock) {
this.#watchingTheClock = true;
this.#openmct.time.on('clock', this.#updateRealTime, this);
this.#openmct.time.on('modeChanged', this.#updateRealTime, this);
} else if (!observers && this.#watchingTheClock) {
this.#watchingTheClock = false;
this.#openmct.time.off('clock', this.#updateRealTime, this);
this.#openmct.time.off('modeChanged', this.#updateRealTime, this);
}
}
#updateRealTime(clock) {
this.#isRealTime = clock !== undefined;
#updateRealTime(mode) {
this.#isRealTime = mode !== 'fixed';
if (!this.#isRealTime) {
Object.keys(this.#observingStaleness).forEach((id) => {

View File

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

View File

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

View File

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

View File

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

View File

@@ -56,6 +56,7 @@ if (document.currentScript) {
* @property {import('./src/api/notifications/NotificationAPI').default} notifications
* @property {import('./src/api/Editor').default} editor
* @property {import('./src/api/overlays/OverlayAPI')} overlays
* @property {import('./src/api/tooltips/ToolTipAPI')} tooltips
* @property {import('./src/api/menu/MenuAPI').default} menus
* @property {import('./src/api/actions/ActionsAPI').default} actions
* @property {import('./src/api/status/StatusAPI').default} status

View File

@@ -1,6 +1,6 @@
{
"name": "openmct",
"version": "2.2.6-SNAPSHOT",
"version": "3.0.0-SNAPSHOT",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.22.5",
@@ -8,22 +8,27 @@
"@deploysentinel/playwright": "0.3.4",
"@percy/cli": "1.26.0",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.32.3",
"@playwright/test": "1.36.2",
"@testing-library/jasmine-dom": "^1.3.3",
"@testing-library/vue": "^7.0.0",
"@types/eventemitter3": "1.2.0",
"@types/jasmine": "4.3.4",
"@types/lodash": "4.14.192",
"@vue/compat": "^3.1.0",
"@vue/compiler-sfc": "^3.1.0",
"babel-loader": "9.1.0",
"babel-plugin-istanbul": "6.1.1",
"codecov": "3.8.3",
"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",
"d3-selection": "3.0.0",
"eslint": "8.43.0",
"eslint-plugin-compat": "4.1.4",
"eslint-config-prettier": "8.8.0",
"eslint-plugin-compat": "4.1.4",
"eslint-plugin-playwright": "0.12.0",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-vue": "9.15.0",
@@ -53,7 +58,6 @@
"moment-timezone": "0.5.41",
"nyc": "15.1.0",
"painterro": "1.2.78",
"playwright-core": "1.32.3",
"plotly.js-basic-dist": "2.20.0",
"plotly.js-gl2d-dist": "2.20.0",
"prettier": "2.8.7",
@@ -66,10 +70,9 @@
"style-loader": "3.3.3",
"typescript": "5.1.3",
"uuid": "9.0.0",
"vue": "2.6.14",
"vue": "^3.1.0",
"vue-eslint-parser": "9.3.1",
"vue-loader": "15.9.8",
"vue-template-compiler": "2.6.14",
"vue-loader": "^16.0.0",
"webpack": "5.88.0",
"webpack-cli": "5.1.1",
"webpack-dev-server": "4.15.1",
@@ -77,10 +80,11 @@
},
"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:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.js",
"lint": "eslint example src e2e --ext .js,.vue openmct.js --max-warnings=0",
"lint:spelling": "cspell \"**/*.*\"",
"lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix",
"build:prod": "webpack --config ./.webpack/webpack.prod.js",
"build:dev": "webpack --config ./.webpack/webpack.dev.js",
@@ -97,6 +101,7 @@
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots",
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js --grep-invert @unstable",
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js --grep-invert @couchdb",
"test:e2e:watch": "npx playwright test --ui --config=e2e/playwright-ci.config.js",
"test:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
"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'",

View File

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

View File

@@ -21,6 +21,7 @@
*****************************************************************************/
import objectUtils from '../objects/object-utils';
import CompositionProvider from './CompositionProvider';
import { toRaw } from 'vue';
/**
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
@@ -167,7 +168,7 @@ export default class DefaultCompositionProvider extends CompositionProvider {
*/
add(parent, childId) {
if (!this.includes(parent, childId)) {
const composition = structuredClone(parent.composition);
const composition = structuredClone(toRaw(parent.composition));
composition.push(childId);
this.publicAPI.objects.mutate(parent, 'composition', composition);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,6 +23,7 @@
import MenuAPI from './MenuAPI';
import Menu from './menu';
import { createOpenMct, createMouseEvent, resetApplicationState } from '../../utils/testing';
import Vue from 'vue';
describe('The Menu API', () => {
let openmct;
@@ -137,14 +138,13 @@ describe('The Menu API', () => {
it('invokes the destroy method when menu is dismissed', (done) => {
menuOptions.onDestroy = done;
menuAPI.showMenu(x, y, actionsArray, menuOptions);
spyOn(menuAPI, '_clearMenuComponent').and.callThrough();
const vueComponent = menuAPI.menuComponent.component;
spyOn(vueComponent, '$destroy');
menuAPI.showMenu(x, y, actionsArray, menuOptions);
document.body.click();
expect(vueComponent.$destroy).toHaveBeenCalled();
expect(menuAPI._clearMenuComponent).toHaveBeenCalled();
});
it('invokes the onDestroy callback if passed in', (done) => {
@@ -185,7 +185,7 @@ describe('The Menu API', () => {
superMenuItem.dispatchEvent(mouseOverEvent);
const itemDescription = document.querySelector('.l-item-description__description');
menuAPI.menuComponent.component.$nextTick(() => {
Vue.nextTick(() => {
expect(menuElement).not.toBeNull();
expect(itemDescription.innerText).toEqual(actionsArray[0].description);

View File

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

View File

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

View File

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

View File

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

View File

@@ -159,6 +159,9 @@ class InMemorySearchProvider {
return pendingQuery.promise;
}
/**
* @private
*/
#localQueryFallBack({ queryId, searchType, query, maxResults }) {
if (searchType === this.searchTypes.OBJECTS) {
return this.localSearchForObjects(queryId, query, maxResults);
@@ -371,7 +374,7 @@ class InMemorySearchProvider {
delete provider.pendingIndex[keyString];
try {
if (domainObject) {
if (domainObject && domainObject.identifier) {
await provider.index(domainObject);
}
} catch (error) {

View File

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

View File

@@ -242,11 +242,16 @@ export default class ObjectAPI {
return domainObject;
})
.catch((error) => {
console.warn(`Failed to retrieve ${keystring}:`, error);
delete this.cache[keystring];
const result = this.applyGetInterceptors(identifier);
return result;
// suppress abort errors
if (error.name === 'AbortError') {
return;
}
console.warn(`Failed to retrieve ${keystring}:`, error);
return this.applyGetInterceptors(identifier);
});
this.cache[keystring] = objectPromise;
@@ -540,6 +545,40 @@ export default class ObjectAPI {
.join('/');
}
/**
* Return path of telemetry objects in the object composition
* @param {object} identifier the identifier for the domain object to query for
* @param {object} [telemetryIdentifier] the specific identifier for the telemetry
* to look for in the composition, uses first object in composition otherwise
* @returns {Array} path of telemetry object in object composition
*/
async getTelemetryPath(identifier, telemetryIdentifier) {
const objectDetails = await this.get(identifier);
const telemetryPath = [];
if (objectDetails.composition && !['folder'].includes(objectDetails.type)) {
let sourceTelemetry = objectDetails.composition[0];
if (telemetryIdentifier) {
sourceTelemetry = objectDetails.composition.find(
(telemetrySource) =>
this.makeKeyString(telemetrySource) === this.makeKeyString(telemetryIdentifier)
);
}
const compositionElement = await this.get(sourceTelemetry);
if (!['yamcs.telemetry', 'generator'].includes(compositionElement.type)) {
return telemetryPath;
}
const telemetryKey = compositionElement.identifier.key;
const telemetryPathObjects = await this.getOriginalPath(telemetryKey);
telemetryPathObjects.forEach((pathObject) => {
if (pathObject.type === 'root') {
return;
}
telemetryPath.unshift(pathObject.name);
});
}
return telemetryPath;
}
/**
* Modify a domain object. Internal to ObjectAPI, won't call save after.
* @private

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,29 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import Overlay from './Overlay';
import Dialog from './Dialog';
import ProgressDialog from './ProgressDialog';
import Selection from './Selection';
/**
* The OverlayAPI is responsible for pre-pending templates to
@@ -130,6 +153,13 @@ class OverlayAPI {
return progressDialog;
}
selection(options) {
let selection = new Selection(options);
this.showOverlay(selection);
return selection;
}
}
export default OverlayAPI;

View File

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

View File

@@ -0,0 +1,69 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import SelectionComponent from './components/SelectionComponent.vue';
import Overlay from './Overlay';
import mount from 'utils/mount';
class Selection extends Overlay {
constructor({
iconClass,
title,
message,
selectionOptions,
onChange,
currentSelection,
...options
}) {
const { vNode, destroy } = mount({
components: {
SelectionComponent: SelectionComponent
},
provide: {
iconClass,
title,
message,
selectionOptions,
onChange,
currentSelection
},
template: '<selection-component></selection-component>'
});
const component = vNode.componentInstance;
super({
element: component.$el,
size: 'fit',
dismissable: false,
onChange,
currentSelection,
...options
});
this.once('destroy', () => {
destroy();
});
}
}
export default Selection;

View File

@@ -27,7 +27,7 @@
v-if="dismissable"
aria-label="Close"
class="c-click-icon c-overlay__close-button icon-x"
@click="destroy"
@click.stop="destroy"
></button>
<div
ref="element"
@@ -71,16 +71,16 @@ export default {
});
},
methods: {
destroy: function () {
destroy() {
if (this.dismissable) {
this.dismiss();
}
},
buttonClickHandler: function (method) {
buttonClickHandler(method) {
method();
this.$emit('destroy');
},
getElementForFocus: function () {
getElementForFocus() {
const defaultElement = this.$refs.element;
if (!this.$refs.buttons) {
return defaultElement;

View File

@@ -0,0 +1,34 @@
<template>
<div class="c-message">
<!--Uses flex-row -->
<div class="c-message__icon" :class="['u-icon-bg-color-' + iconClass]"></div>
<div class="c-message__text">
<!-- Uses flex-column -->
<div v-if="title" class="c-message__title">
{{ title }}
</div>
<div v-if="message" class="c-message__action-text">
{{ message }}
</div>
<select @change="onChange">
<option
v-for="option in selectionOptions"
:key="option.key"
:value="option.key"
:selected="option.key === currentSelection"
>
{{ option.name }}
</option>
</select>
<slot></slot>
</div>
</div>
</template>
<script>
export default {
inject: ['iconClass', 'title', 'message', 'selectionOptions', 'currentSelection', 'onChange']
};
</script>

View File

@@ -204,27 +204,25 @@ export default class TelemetryAPI {
*/
standardizeRequestOptions(options = {}) {
if (!Object.hasOwn(options, 'start')) {
if (options.timeContext?.bounds()) {
options.start = options.timeContext.bounds().start;
const bounds = options.timeContext?.getBounds();
if (bounds?.start) {
options.start = options.timeContext.getBounds().start;
} else {
options.start = this.openmct.time.bounds().start;
options.start = this.openmct.time.getBounds().start;
}
}
if (!Object.hasOwn(options, 'end')) {
if (options.timeContext?.bounds()) {
options.end = options.timeContext.bounds().end;
const bounds = options.timeContext?.getBounds();
if (bounds?.end) {
options.end = options.timeContext.getBounds().end;
} else {
options.end = this.openmct.time.bounds().end;
options.end = this.openmct.time.getBounds().end;
}
}
if (!Object.hasOwn(options, 'domain')) {
options.domain = this.openmct.time.timeSystem().key;
}
if (!Object.hasOwn(options, 'timeContext')) {
options.timeContext = this.openmct.time;
options.domain = this.openmct.time.getTimeSystem().key;
}
return options;
@@ -489,6 +487,62 @@ export default class TelemetryAPI {
}.bind(this);
}
/**
* Subscribe to run-time changes in configured telemetry limits for a specific domain object.
* The callback will be called whenever data is received from a
* limit provider.
*
* @method subscribeToLimits
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
* @param {module:openmct.DomainObject} domainObject the object
* which has associated limits
* @param {Function} callback the callback to invoke with new data, as
* it becomes available
* @returns {Function} a function which may be called to terminate
* the subscription
*/
subscribeToLimits(domainObject, callback) {
if (domainObject.type === 'unknown') {
return () => {};
}
const provider = this.#findLimitEvaluator(domainObject);
if (!this.limitsSubscribeCache) {
this.limitsSubscribeCache = {};
}
const keyString = objectUtils.makeKeyString(domainObject.identifier);
let subscriber = this.limitsSubscribeCache[keyString];
if (!subscriber) {
subscriber = this.limitsSubscribeCache[keyString] = {
callbacks: [callback]
};
if (provider && provider.subscribeToLimits) {
subscriber.unsubscribe = provider.subscribeToLimits(domainObject, function (value) {
subscriber.callbacks.forEach(function (cb) {
cb(value);
});
});
} else {
subscriber.unsubscribe = function () {};
}
} else {
subscriber.callbacks.push(callback);
}
return function unsubscribe() {
subscriber.callbacks = subscriber.callbacks.filter(function (cb) {
return cb !== callback;
});
if (subscriber.callbacks.length === 0) {
subscriber.unsubscribe();
delete this.limitsSubscribeCache[keyString];
}
}.bind(this);
}
/**
* Request telemetry staleness for a domain object.
*
@@ -676,7 +730,7 @@ export default class TelemetryAPI {
*
* @param {module:openmct.DomainObject} domainObject the domain
* object for which to get limits
* @returns {module:openmct.TelemetryAPI~LimitEvaluator}
* @returns {LimitsResponseObject}
* @method limits
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/
@@ -723,18 +777,8 @@ export default class TelemetryAPI {
*
* @param {module:openmct.DomainObject} domainObject the domain
* object for which to display limits
* @returns {module:openmct.TelemetryAPI~LimitEvaluator}
* @method limits returns a limits object of
* type {
* level1: {
* low: { key1: value1, key2: value2, color: <supportedColor> },
* high: { key1: value1, key2: value2, color: <supportedColor> }
* },
* level2: {
* low: { key1: value1, key2: value2 },
* high: { key1: value1, key2: value2 }
* }
* }
* @returns {LimitsResponseObject}
* @method limits returns a limits object of type {LimitsResponseObject}
* supported colors are purple, red, orange, yellow and cyan
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/
@@ -766,7 +810,7 @@ export default class TelemetryAPI {
* @param {*} datum the telemetry datum to evaluate
* @param {TelemetryProperty} the property to check for limit violations
* @memberof module:openmct.TelemetryAPI~LimitEvaluator
* @returns {module:openmct.TelemetryAPI~LimitViolation} metadata about
* @returns {LimitViolation} metadata about
* the limit violation, or undefined if a value is within limits
*/
@@ -777,6 +821,42 @@ export default class TelemetryAPI {
* @property {string} cssClass the class (or space-separated classes) to
* apply to display elements for values which violate this limit
* @property {string} name the human-readable name for the limit violation
* @property {number} low a lower limit for violation
* @property {number} high a higher limit violation
*/
/**
* @typedef {object} LimitsResponseObject
* @memberof {module:openmct.TelemetryAPI~}
* @property {LimitDefinition} limitLevel the level name and it's limit definition
* @example {
* [limitLevel]: {
* low: {
* color: lowColor,
* value: lowValue
* },
* high: {
* color: highColor,
* value: highValue
* }
* }
* }
*/
/**
* Limit defined for a telemetry property.
* @typedef LimitDefinition
* @memberof {module:openmct.TelemetryAPI~}
* @property {LimitDefinitionValue} low a lower limit
* @property {LimitDefinitionValue} high a higher limit
*/
/**
* Limit definition for a Limit of a telemetry property.
* @typedef LimitDefinitionValue
* @memberof {module:openmct.TelemetryAPI~}
* @property {string} color color to represent this limit
* @property {Number} value the limit value
*/
/**

View File

@@ -29,15 +29,20 @@ describe('Telemetry API', () => {
beforeEach(() => {
openmct = {
time: jasmine.createSpyObj('timeAPI', ['timeSystem', 'bounds']),
time: jasmine.createSpyObj('timeAPI', ['timeSystem', 'getTimeSystem', 'bounds', 'getBounds']),
types: jasmine.createSpyObj('typeRegistry', ['get'])
};
openmct.time.timeSystem.and.returnValue({ key: 'system' });
openmct.time.getTimeSystem.and.returnValue({ key: 'system' });
openmct.time.bounds.and.returnValue({
start: 0,
end: 1
});
openmct.time.getBounds.and.returnValue({
start: 0,
end: 1
});
telemetryAPI = new TelemetryAPI(openmct);
});
@@ -261,16 +266,14 @@ describe('Telemetry API', () => {
signal,
start: 0,
end: 1,
domain: 'system',
timeContext: jasmine.any(Object)
domain: 'system'
});
expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), {
signal,
start: 0,
end: 1,
domain: 'system',
timeContext: jasmine.any(Object)
domain: 'system'
});
telemetryProvider.supportsRequest.calls.reset();
@@ -281,16 +284,14 @@ describe('Telemetry API', () => {
signal,
start: 0,
end: 1,
domain: 'system',
timeContext: jasmine.any(Object)
domain: 'system'
});
expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), {
signal,
start: 0,
end: 1,
domain: 'system',
timeContext: jasmine.any(Object)
domain: 'system'
});
});
@@ -309,16 +310,14 @@ describe('Telemetry API', () => {
start: 20,
end: 30,
domain: 'someDomain',
signal,
timeContext: jasmine.any(Object)
signal
});
expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), {
start: 20,
end: 30,
domain: 'someDomain',
signal,
timeContext: jasmine.any(Object)
signal
});
});
});

View File

@@ -23,6 +23,7 @@
import _ from 'lodash';
import EventEmitter from 'EventEmitter';
import { LOADED_ERROR, TIMESYSTEM_KEY_NOTIFICATION, TIMESYSTEM_KEY_WARNING } from './constants';
import { TIME_CONTEXT_EVENTS } from '../time/constants';
/**
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
@@ -60,13 +61,17 @@ export default class TelemetryCollection extends EventEmitter {
this.futureBuffer = [];
this.parseTime = undefined;
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
if (!Object.hasOwn(options, 'timeContext')) {
options.timeContext = this.openmct.time;
}
this.options = options;
this.unsubscribe = undefined;
this.options = this.openmct.telemetry.standardizeRequestOptions(options);
this.pageState = undefined;
this.lastBounds = undefined;
this.requestAbort = undefined;
this.isStrategyLatest = this.options.strategy === 'latest';
this.dataOutsideTimeBounds = false;
this.modeChanged = false;
}
/**
@@ -78,11 +83,11 @@ export default class TelemetryCollection extends EventEmitter {
this._error(LOADED_ERROR);
}
this._setTimeSystem(this.options.timeContext.timeSystem());
this.lastBounds = this.options.timeContext.bounds();
this._setTimeSystem(this.options.timeContext.getTimeSystem());
this.lastBounds = this.options.timeContext.getBounds();
this._watchBounds();
this._watchTimeSystem();
this._watchTimeModeChange();
this._requestHistoricalTelemetry();
this._initiateSubscriptionTelemetry();
@@ -101,6 +106,7 @@ export default class TelemetryCollection extends EventEmitter {
this._unwatchBounds();
this._unwatchTimeSystem();
this._unwatchTimeModeChange();
if (this.unsubscribe) {
this.unsubscribe();
}
@@ -121,7 +127,7 @@ export default class TelemetryCollection extends EventEmitter {
* @private
*/
async _requestHistoricalTelemetry() {
let options = { ...this.options };
let options = this.openmct.telemetry.standardizeRequestOptions({ ...this.options });
const historicalProvider = this.openmct.telemetry.findRequestProvider(
this.domainObject,
options
@@ -301,6 +307,12 @@ export default class TelemetryCollection extends EventEmitter {
* @private
*/
_bounds(bounds, isTick) {
if (this.modeChanged) {
this.modeChanged = false;
this._reset();
return;
}
let startChanged = this.lastBounds.start !== bounds.start;
let endChanged = this.lastBounds.end !== bounds.end;
@@ -433,6 +445,11 @@ export default class TelemetryCollection extends EventEmitter {
this._reset();
}
_timeModeChanged() {
//We're need this so that when the bounds change comes in after this mode change, we can reset and request historic telemetry
this.modeChanged = true;
}
/**
* Reset the telemetry data of the collection, and re-request
* historical telemetry
@@ -450,19 +467,35 @@ export default class TelemetryCollection extends EventEmitter {
}
/**
* adds the _bounds callback to the 'bounds' timeAPI listener
* adds the _bounds callback to the 'boundsChanged' timeAPI listener
* @private
*/
_watchBounds() {
this.options.timeContext.on('bounds', this._bounds, this);
this.options.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this._bounds, this);
}
/**
* removes the _bounds callback from the 'bounds' timeAPI listener
* removes the _bounds callback from the 'boundsChanged' timeAPI listener
* @private
*/
_unwatchBounds() {
this.options.timeContext.off('bounds', this._bounds, this);
this.options.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this._bounds, this);
}
/**
* adds the _timeModeChanged callback to the 'modeChanged' timeAPI listener
* @private
*/
_watchTimeModeChange() {
this.options.timeContext.on(TIME_CONTEXT_EVENTS.modeChanged, this._timeModeChanged, this);
}
/**
* removes the _timeModeChanged callback from the 'modeChanged' timeAPI listener
* @private
*/
_unwatchTimeModeChange() {
this.options.timeContext.off(TIME_CONTEXT_EVENTS.modeChanged, this._timeModeChanged, this);
}
/**
@@ -470,7 +503,11 @@ export default class TelemetryCollection extends EventEmitter {
* @private
*/
_watchTimeSystem() {
this.options.timeContext.on('timeSystem', this._setTimeSystemAndFetchData, this);
this.options.timeContext.on(
TIME_CONTEXT_EVENTS.timeSystemChanged,
this._setTimeSystemAndFetchData,
this
);
}
/**
@@ -478,7 +515,11 @@ export default class TelemetryCollection extends EventEmitter {
* @private
*/
_unwatchTimeSystem() {
this.options.timeContext.off('timeSystem', this._setTimeSystemAndFetchData, this);
this.options.timeContext.off(
TIME_CONTEXT_EVENTS.timeSystemChanged,
this._setTimeSystemAndFetchData,
this
);
}
/**

View File

@@ -134,6 +134,14 @@ define(['lodash'], function (_) {
);
};
TelemetryMetadataManager.prototype.getUseToUpdateInPlaceValue = function () {
return this.valueMetadatas.find(this.isInPlaceUpdateValue);
};
TelemetryMetadataManager.prototype.isInPlaceUpdateValue = function (metadatum) {
return metadatum.useToUpdateInPlace === true;
};
TelemetryMetadataManager.prototype.getDefaultDisplayValue = function () {
let valueMetadata = this.valuesForHints(['range'])[0];

View File

@@ -20,7 +20,8 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TimeContext, { TIME_CONTEXT_EVENTS } from './TimeContext';
import TimeContext from './TimeContext';
import { MODES, REALTIME_MODE_KEY, TIME_CONTEXT_EVENTS } from './constants';
/**
* The IndependentTimeContext handles getting and setting time of the openmct application in general.
@@ -46,7 +47,7 @@ class IndependentTimeContext extends TimeContext {
this.globalTimeContext.on('removeOwnContext', this.removeIndependentContext);
}
bounds(newBounds) {
bounds() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.bounds(...arguments);
} else {
@@ -54,7 +55,23 @@ class IndependentTimeContext extends TimeContext {
}
}
tick(timestamp) {
getBounds() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getBounds();
} else {
return super.getBounds();
}
}
setBounds() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setBounds(...arguments);
} else {
return super.setBounds(...arguments);
}
}
tick() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.tick(...arguments);
} else {
@@ -62,7 +79,7 @@ class IndependentTimeContext extends TimeContext {
}
}
clockOffsets(offsets) {
clockOffsets() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.clockOffsets(...arguments);
} else {
@@ -70,11 +87,19 @@ class IndependentTimeContext extends TimeContext {
}
}
stopClock() {
getClockOffsets() {
if (this.upstreamTimeContext) {
this.upstreamTimeContext.stopClock();
return this.upstreamTimeContext.getClockOffsets();
} else {
super.stopClock();
return super.getClockOffsets();
}
}
setClockOffsets() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setClockOffsets(...arguments);
} else {
return super.setClockOffsets(...arguments);
}
}
@@ -86,10 +111,19 @@ class IndependentTimeContext extends TimeContext {
return this.globalTimeContext.timeSystem(...arguments);
}
/**
* Get the time system of the TimeAPI.
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method getTimeSystem
*/
getTimeSystem() {
return this.globalTimeContext.getTimeSystem();
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock
* can be unset by calling {@link stopClock}.
* and ticking will begin. Offsets from 'now' must also be provided.
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
@@ -126,15 +160,19 @@ class IndependentTimeContext extends TimeContext {
this.activeClock = clock;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* The active clock has changed.
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit('clock', this.activeClock);
this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);
if (this.activeClock !== undefined) {
//set the mode here or isRealtime will be false even if we're in clock mode
this.setMode(REALTIME_MODE_KEY);
this.clockOffsets(offsets);
this.activeClock.on('tick', this.tick);
}
@@ -145,6 +183,138 @@ class IndependentTimeContext extends TimeContext {
return this.activeClock;
}
/**
* Get the active clock.
* @return {Clock} the currently active clock;
*/
getClock() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getClock();
}
return this.activeClock;
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and the currently ticking will begin.
* Offsets from 'now', if provided, will be used to set realtime mode offsets
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
setClock(keyOrClock) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setClock(...arguments);
}
let clock;
if (typeof keyOrClock === 'string') {
clock = this.globalTimeContext.clocks.get(keyOrClock);
if (clock === undefined) {
throw `Unknown clock ${keyOrClock}. Has it been registered with 'addClock'?`;
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.globalTimeContext.clocks.has(clock.key)) {
throw `Unknown clock ${keyOrClock.key}. Has it been registered with 'addClock'?`;
}
}
const previousClock = this.activeClock;
if (previousClock) {
previousClock.off('tick', this.tick);
}
this.activeClock = clock;
this.activeClock.on('tick', this.tick);
/**
* The active clock has changed.
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);
return this.activeClock;
}
/**
* Get the current mode.
* @return {Mode} the current mode;
*/
getMode() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getMode();
}
return this.mode;
}
/**
* Set the mode to either fixed or realtime.
*
* @param {Mode} mode The mode to activate
* @param {TimeBounds | ClockOffsets} offsetsOrBounds A time window of a fixed width
* @fires module:openmct.TimeAPI~clock
* @return {Mode} the currently active mode;
*/
setMode(mode, offsetsOrBounds) {
if (!mode) {
return;
}
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setMode(...arguments);
}
if (mode === MODES.realtime && this.activeClock === undefined) {
throw `Unknown clock. Has a clock been registered with 'addClock'?`;
}
if (mode !== this.mode) {
this.mode = mode;
/**
* The active mode has changed.
* @event modeChanged
* @memberof module:openmct.TimeAPI~
* @property {Mode} mode The newly activated mode
*/
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.mode));
}
//We are also going to set bounds here
if (offsetsOrBounds !== undefined) {
if (this.mode === REALTIME_MODE_KEY) {
this.setClockOffsets(offsetsOrBounds);
} else {
this.setBounds(offsetsOrBounds);
}
}
return this.mode;
}
isRealTime() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.isRealTime(...arguments);
} else {
return super.isRealTime(...arguments);
}
}
now() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.now(...arguments);
} else {
return super.now(...arguments);
}
}
/**
* Causes this time context to follow another time context (either the global context, or another upstream time context)
* This allows views to have their own time context which points to the appropriate upstream context as necessary, achieving nesting.
@@ -152,7 +322,7 @@ class IndependentTimeContext extends TimeContext {
followTimeContext() {
this.stopFollowingTimeContext();
if (this.upstreamTimeContext) {
TIME_CONTEXT_EVENTS.forEach((eventName) => {
Object.values(TIME_CONTEXT_EVENTS).forEach((eventName) => {
const thisTimeContext = this;
this.upstreamTimeContext.on(eventName, passthrough);
this.unlisteners.push(() => {
@@ -197,6 +367,7 @@ class IndependentTimeContext extends TimeContext {
// Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.bounds());
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
}
hasOwnContext() {
@@ -237,6 +408,9 @@ class IndependentTimeContext extends TimeContext {
if (viewKey && key === viewKey) {
//this is necessary as the upstream context gets reassigned after this
this.stopFollowingTimeContext();
if (this.activeClock !== undefined) {
this.activeClock.off('tick', this.tick);
}
let timeContext = this.globalTimeContext;
@@ -259,11 +433,16 @@ class IndependentTimeContext extends TimeContext {
this.followTimeContext();
// Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.bounds());
this.emit('bounds', this.getBounds());
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
// now that the view's context is set, tell others to check theirs in case they were following this view's context.
this.globalTimeContext.emit('refreshContext', viewKey);
}
}
#copy(object) {
return JSON.parse(JSON.stringify(object));
}
}
export default IndependentTimeContext;

View File

@@ -22,6 +22,7 @@
import GlobalTimeContext from './GlobalTimeContext';
import IndependentTimeContext from '@/api/time/IndependentTimeContext';
import { FIXED_MODE_KEY, REALTIME_MODE_KEY } from '@/api/time/constants';
/**
* The public API for setting and querying the temporal state of the
@@ -134,14 +135,15 @@ class TimeAPI extends GlobalTimeContext {
*/
addIndependentContext(key, value, clockKey) {
let timeContext = this.getIndependentContext(key);
//stop following upstream time context since the view has it's own
timeContext.resetContext();
if (clockKey) {
timeContext.clock(clockKey, value);
timeContext.setClock(clockKey);
timeContext.setMode(REALTIME_MODE_KEY, value);
} else {
timeContext.stopClock();
timeContext.bounds(value);
timeContext.setMode(FIXED_MODE_KEY, value);
}
// Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context
@@ -185,6 +187,7 @@ class TimeAPI extends GlobalTimeContext {
}
let viewTimeContext = this.getIndependentContext(viewKey);
if (!viewTimeContext) {
// If the context doesn't exist yet, create it.
viewTimeContext = new IndependentTimeContext(this.openmct, this, objectPath);

View File

@@ -87,7 +87,7 @@ describe('The Time API', function () {
expect(function () {
api.timeSystem(timeSystem, bounds);
}).not.toThrow();
expect(api.timeSystem()).toBe(timeSystem);
expect(api.timeSystem()).toEqual(timeSystem);
});
it('Disallows setting of time system without bounds', function () {
@@ -110,7 +110,7 @@ describe('The Time API', function () {
expect(function () {
api.timeSystem(timeSystemKey);
}).not.toThrow();
expect(api.timeSystem()).toBe(timeSystem);
expect(api.timeSystem()).toEqual(timeSystem);
});
it('Emits an event when time system changes', function () {
@@ -202,12 +202,12 @@ describe('The Time API', function () {
expect(mockTickSource.off).toHaveBeenCalledWith('tick', jasmine.any(Function));
});
it('Allows the active clock to be set and unset', function () {
xit('Allows the active clock to be set and unset', function () {
expect(api.clock()).toBeUndefined();
api.clock('mts', mockOffsets);
expect(api.clock()).toBeDefined();
api.stopClock();
expect(api.clock()).toBeUndefined();
// api.stopClock();
// expect(api.clock()).toBeUndefined();
});
it('Provides a default time context', () => {

View File

@@ -21,8 +21,7 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
export const TIME_CONTEXT_EVENTS = ['bounds', 'clock', 'timeSystem', 'clockOffsets'];
import { TIME_CONTEXT_EVENTS, MODES, REALTIME_MODE_KEY, FIXED_MODE_KEY } from './constants';
class TimeContext extends EventEmitter {
constructor() {
@@ -42,6 +41,8 @@ class TimeContext extends EventEmitter {
this.activeClock = undefined;
this.offsets = undefined;
this.mode = undefined;
this.warnCounts = {};
this.tick = this.tick.bind(this);
}
@@ -56,6 +57,8 @@ class TimeContext extends EventEmitter {
* @method timeSystem
*/
timeSystem(timeSystemOrKey, bounds) {
this.#warnMethodDeprecated('"timeSystem"', '"getTimeSystem" and "setTimeSystem"');
if (arguments.length >= 1) {
if (arguments.length === 1 && !this.activeClock) {
throw new Error('Must specify bounds when changing time system without an active clock.');
@@ -91,7 +94,7 @@ class TimeContext extends EventEmitter {
throw 'Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key';
}
this.system = timeSystem;
this.system = this.#copy(timeSystem);
/**
* The time system used by the time
@@ -102,7 +105,10 @@ class TimeContext extends EventEmitter {
* @property {TimeSystem} The value of the currently applied
* Time System
* */
this.emit('timeSystem', this.system);
const system = this.#copy(this.system);
this.emit('timeSystem', system);
this.emit(TIME_CONTEXT_EVENTS.timeSystemChanged, system);
if (bounds) {
this.bounds(bounds);
}
@@ -163,6 +169,8 @@ class TimeContext extends EventEmitter {
* @method bounds
*/
bounds(newBounds) {
this.#warnMethodDeprecated('"bounds"', '"getBounds" and "setBounds"');
if (arguments.length > 0) {
const validationResult = this.validateBounds(newBounds);
if (validationResult.valid !== true) {
@@ -170,7 +178,7 @@ class TimeContext extends EventEmitter {
}
//Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
this.boundsVal = this.#copy(newBounds);
/**
* The start time, end time, or both have been updated.
* @event bounds
@@ -180,10 +188,11 @@ class TimeContext extends EventEmitter {
* a "tick" event (ie. was an automatic update), false otherwise.
*/
this.emit('bounds', this.boundsVal, false);
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsVal, false);
}
//Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal));
return this.#copy(this.boundsVal);
}
/**
@@ -248,6 +257,8 @@ class TimeContext extends EventEmitter {
* @returns {ClockOffsets}
*/
clockOffsets(offsets) {
this.#warnMethodDeprecated('"clockOffsets"', '"getClockOffsets" and "setClockOffsets"');
if (arguments.length > 0) {
const validationResult = this.validateOffsets(offsets);
if (validationResult.valid !== true) {
@@ -278,20 +289,19 @@ class TimeContext extends EventEmitter {
}
/**
* Stop the currently active clock from ticking, and unset it. This will
* Stop following the currently active clock. This will
* revert all views to showing a static time frame defined by the current
* bounds.
*/
stopClock() {
if (this.activeClock) {
this.clock(undefined, undefined);
}
this.#warnMethodDeprecated('"stopClock"');
this.setMode(FIXED_MODE_KEY);
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock
* can be unset by calling {@link stopClock}.
* and ticking will begin. Offsets from 'now' must also be provided.
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
@@ -301,6 +311,8 @@ class TimeContext extends EventEmitter {
* @return {Clock} the currently active clock;
*/
clock(keyOrClock, offsets) {
this.#warnMethodDeprecated('"clock"', '"getClock" and "setClock"');
if (arguments.length === 2) {
let clock;
@@ -324,15 +336,19 @@ class TimeContext extends EventEmitter {
this.activeClock = clock;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* The active clock has changed.
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit('clock', this.activeClock);
this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);
if (this.activeClock !== undefined) {
//set the mode or isRealtime will be false even though we're in clock mode
this.setMode(REALTIME_MODE_KEY);
this.clockOffsets(offsets);
this.activeClock.on('tick', this.tick);
}
@@ -340,7 +356,7 @@ class TimeContext extends EventEmitter {
throw 'When setting the clock, clock offsets must also be provided';
}
return this.activeClock;
return this.isRealTime() ? this.activeClock : undefined;
}
/**
@@ -349,29 +365,315 @@ class TimeContext extends EventEmitter {
* using current offsets.
*/
tick(timestamp) {
if (!this.activeClock) {
return;
// always emit the timestamp
this.emit('tick', timestamp);
if (this.mode === REALTIME_MODE_KEY) {
const newBounds = {
start: timestamp + this.offsets.start,
end: timestamp + this.offsets.end
};
this.boundsVal = newBounds;
// "bounds" will be deprecated in a future release
this.emit('bounds', this.boundsVal, true);
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsVal, true);
}
const newBounds = {
start: timestamp + this.offsets.start,
end: timestamp + this.offsets.end
};
this.boundsVal = newBounds;
this.emit('bounds', this.boundsVal, true);
}
/**
* Checks if this time context is in real-time mode or not.
* Get the timestamp of the current clock
* @returns {number} current timestamp of current clock regardless of mode
* @memberof module:openmct.TimeAPI#
* @method now
*/
now() {
return this.activeClock.currentValue();
}
/**
* Get the time system of the TimeAPI.
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method getTimeSystem
*/
getTimeSystem() {
return this.system;
}
/**
* Set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystemOrKey
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method setTimeSystem
*/
setTimeSystem(timeSystemOrKey, bounds) {
if (timeSystemOrKey === undefined) {
throw 'Please provide a time system';
}
let timeSystem;
if (typeof timeSystemOrKey === 'string') {
timeSystem = this.timeSystems.get(timeSystemOrKey);
if (timeSystem === undefined) {
throw `Unknown time system ${timeSystemOrKey}. Has it been registered with 'addTimeSystem'?`;
}
} else if (typeof timeSystemOrKey === 'object') {
timeSystem = timeSystemOrKey;
if (!this.timeSystems.has(timeSystem.key)) {
throw `Unknown time system ${timeSystemOrKey.key}. Has it been registered with 'addTimeSystem'?`;
}
} else {
throw 'Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key';
}
this.system = this.#copy(timeSystem);
/**
* The time system used by the time
* conductor has changed. A change in Time System will always be
* followed by a bounds event specifying new query bounds.
*
* @event module:openmct.TimeAPI~timeSystem
* @property {TimeSystem} The value of the currently applied
* Time System
* */
this.emit(TIME_CONTEXT_EVENTS.timeSystemChanged, this.#copy(this.system));
this.emit('timeSystem', this.#copy(this.system));
if (bounds) {
this.setBounds(bounds);
}
}
/**
* Get the start and end time of the time conductor. Basic validation
* of bounds is performed.
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
getBounds() {
//Return a copy to prevent direct mutation of time conductor bounds.
return this.#copy(this.boundsVal);
}
/**
* Set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
setBounds(newBounds) {
const validationResult = this.validateBounds(newBounds);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
//Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = this.#copy(newBounds);
/**
* The start time, end time, or both have been updated.
* @event bounds
* @memberof module:openmct.TimeAPI~
* @property {TimeConductorBounds} bounds The newly updated bounds
* @property {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (i.e. was an automatic update), false otherwise.
*/
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsVal, false);
this.emit('bounds', this.boundsVal, false);
}
/**
* Get the active clock.
* @return {Clock} the currently active clock;
*/
getClock() {
return this.activeClock;
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and the currently ticking will begin.
* Offsets from 'now', if provided, will be used to set realtime mode offsets
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
setClock(keyOrClock) {
let clock;
if (typeof keyOrClock === 'string') {
clock = this.clocks.get(keyOrClock);
if (clock === undefined) {
throw `Unknown clock ${keyOrClock}. Has it been registered with 'addClock'?`;
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.clocks.has(clock.key)) {
throw `Unknown clock ${keyOrClock.key}. Has it been registered with 'addClock'?`;
}
}
const previousClock = this.activeClock;
if (previousClock) {
previousClock.off('tick', this.tick);
}
this.activeClock = clock;
this.activeClock.on('tick', this.tick);
/**
* The active clock has changed.
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);
this.emit('clock', this.activeClock);
}
/**
* Get the current mode.
* @return {Mode} the current mode;
*/
getMode() {
return this.mode;
}
/**
* Set the mode to either fixed or realtime.
*
* @param {Mode} mode The mode to activate
* @param {TimeBounds | ClockOffsets} offsetsOrBounds A time window of a fixed width
* @fires module:openmct.TimeAPI~clock
* @return {Mode} the currently active mode;
*/
setMode(mode, offsetsOrBounds) {
if (!mode) {
return;
}
if (mode === MODES.realtime && this.activeClock === undefined) {
throw `Unknown clock. Has a clock been registered with 'addClock'?`;
}
if (mode !== this.mode) {
this.mode = mode;
/**
* The active mode has changed.
* @event modeChanged
* @memberof module:openmct.TimeAPI~
* @property {Mode} mode The newly activated mode
*/
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.mode));
}
if (offsetsOrBounds !== undefined) {
if (this.isRealTime()) {
this.setClockOffsets(offsetsOrBounds);
} else {
this.setBounds(offsetsOrBounds);
}
}
}
/**
* Checks if this time context is in realtime mode or not.
* @returns {boolean} true if this context is in real-time mode, false if not
*/
isRealTime() {
if (this.clock()) {
return true;
return this.mode === MODES.realtime;
}
/**
* Checks if this time context is in fixed mode or not.
* @returns {boolean} true if this context is in fixed mode, false if not
*/
isFixed() {
return this.mode === MODES.fixed;
}
/**
* Get the currently applied clock offsets.
* @returns {ClockOffsets}
*/
getClockOffsets() {
return this.offsets;
}
/**
* Set the currently applied clock offsets. If no parameter is provided,
* the current value will be returned. If provided, the new value will be
* used as the new clock offsets.
* @param {ClockOffsets} offsets
* @returns {ClockOffsets}
*/
setClockOffsets(offsets) {
const validationResult = this.validateOffsets(offsets);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
return false;
this.offsets = this.#copy(offsets);
const currentValue = this.activeClock.currentValue();
const newBounds = {
start: currentValue + offsets.start,
end: currentValue + offsets.end
};
this.setBounds(newBounds);
/**
* Event that is triggered when clock offsets change.
* @event clockOffsets
* @memberof module:openmct.TimeAPI~
* @property {ClockOffsets} clockOffsets The newly activated clock
* offsets.
*/
this.emit(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.#copy(offsets));
}
#warnMethodDeprecated(method, newMethod) {
const MAX_CALLS = 1; // Only warn once per unique method and newMethod combination
const key = `${method}.${newMethod}`;
const currentWarnCount = this.warnCounts[key] || 0;
if (currentWarnCount >= MAX_CALLS) {
return; // Don't warn if already warned once
}
this.warnCounts[key] = currentWarnCount + 1;
let message = `[DEPRECATION WARNING]: The ${method} API method is deprecated and will be removed in a future version of Open MCT.`;
if (newMethod) {
message += ` Please use the ${newMethod} API method(s) instead.`;
}
// TODO: add docs and point to them in warning.
// For more information and migration instructions, visit [link to documentation or migration guide].
console.warn(message);
}
#copy(object) {
return JSON.parse(JSON.stringify(object));
}
}

22
src/api/time/constants.js Normal file
View File

@@ -0,0 +1,22 @@
export const TIME_CONTEXT_EVENTS = {
//old API events - to be deprecated
bounds: 'bounds',
clock: 'clock',
timeSystem: 'timeSystem',
clockOffsets: 'clockOffsets',
//new API events
tick: 'tick',
modeChanged: 'modeChanged',
boundsChanged: 'boundsChanged',
clockChanged: 'clockChanged',
timeSystemChanged: 'timeSystemChanged',
clockOffsetsChanged: 'clockOffsetsChanged'
};
export const REALTIME_MODE_KEY = 'realtime';
export const FIXED_MODE_KEY = 'fixed';
export const MODES = {
[FIXED_MODE_KEY]: FIXED_MODE_KEY,
[REALTIME_MODE_KEY]: REALTIME_MODE_KEY
};

View File

@@ -0,0 +1,72 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TooltipComponent from './components/TooltipComponent.vue';
import EventEmitter from 'EventEmitter';
import mount from 'utils/mount';
class Tooltip extends EventEmitter {
constructor(
{ toolTipText, toolTipLocation, parentElement } = {
tooltipText: '',
toolTipLocation: 'below',
parentElement: null
}
) {
super();
const { vNode, destroy } = mount({
components: {
TooltipComponent: TooltipComponent
},
provide: {
toolTipText,
toolTipLocation,
parentElement
},
template: '<tooltip-component toolTipText="toolTipText"></tooltip-component>'
});
this.component = vNode.componentInstance;
this._destroy = destroy;
this.isActive = null;
}
destroy() {
if (!this.isActive) {
return;
}
this._destroy();
this.isActive = false;
}
/**
* @private
**/
show() {
document.body.appendChild(this.component.$el);
this.isActive = true;
}
}
export default Tooltip;

View File

@@ -0,0 +1,90 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import Tooltip from './ToolTip';
/**
* @readonly
* @enum {String} TooltipLocation
* @property {String} ABOVE The string for locating tooltips above an element
* @property {String} BELOW The string for locating tooltips below an element
* @property {String} RIGHT The pixel-spatial annotation type
* @property {String} LEFT The temporal annotation type
* @property {String} CENTER The plot-spatial annotation type
*/
const TOOLTIP_LOCATIONS = Object.freeze({
ABOVE: 'above',
BELOW: 'below',
RIGHT: 'right',
LEFT: 'left',
CENTER: 'center'
});
/**
* The TooltipAPI is responsible for adding custom tooltips to
* the desired elements on the screen
*
* @memberof api/tooltips
* @constructor
*/
class TooltipAPI {
constructor() {
this.activeToolTips = [];
this.TOOLTIP_LOCATIONS = TOOLTIP_LOCATIONS;
}
/**
* @private for platform-internal use
*/
showTooltip(tooltip) {
for (let i = this.activeToolTips.length - 1; i > -1; i--) {
this.activeToolTips[i].destroy();
this.activeToolTips.splice(i, 1);
}
this.activeToolTips.push(tooltip);
tooltip.show();
}
/**
* A description of option properties that can be passed into the tooltip
* @typedef {Object} TooltipOptions
* @property {string} tooltipText text to show in the tooltip
* @property {TOOLTIP_LOCATIONS} tooltipLocation location to show the tooltip relative to the parentElement
* @property {HTMLElement} parentElement reference to the DOM node we're adding the tooltip to
*/
/**
* Tooltips take an options object that consists of the string, tooltipLocation, and parentElement
* @param {TooltipOptions} options
*/
tooltip(options) {
let tooltip = new Tooltip(options);
this.showTooltip(tooltip);
return tooltip;
}
}
export default TooltipAPI;

View File

@@ -0,0 +1,61 @@
<!--
Open MCT, Copyright (c) 2014-2023, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div ref="tooltip-wrapper" class="c-menu c-tooltip-wrapper" :style="toolTipLocationStyle">
<div class="c-tooltip">
{{ toolTipText }}
</div>
</div>
</template>
<script>
export default {
inject: ['toolTipText', 'toolTipLocation', 'parentElement'],
computed: {
toolTipCoordinates() {
return this.parentElement.getBoundingClientRect();
},
toolTipLocationStyle() {
const { top, left, height, width } = this.toolTipCoordinates;
let toolTipLocationStyle = {};
if (this.toolTipLocation === 'above') {
toolTipLocationStyle = { top: `${top - 5}px`, left: `${left}px` };
}
if (this.toolTipLocation === 'below') {
toolTipLocationStyle = { top: `${top + height}px`, left: `${left}px` };
}
if (this.toolTipLocation === 'right') {
toolTipLocationStyle = { top: `${top}px`, left: `${left + width}px` };
}
if (this.toolTipLocation === 'left') {
toolTipLocationStyle = { top: `${top}px`, left: `${left - width}px` };
}
if (this.toolTipLocation === 'center') {
toolTipLocationStyle = { top: `${top + height / 2}px`, left: `${left + width / 2}px` };
}
return toolTipLocationStyle;
}
}
};
</script>

View File

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

View File

@@ -0,0 +1,72 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
const tooltipHelpers = {
methods: {
async getTelemetryPathString(telemetryIdentifier) {
let telemetryPathString = '';
if (!this.domainObject?.identifier) {
return;
}
const telemetryPath = await this.openmct.objects.getTelemetryPath(
this.domainObject.identifier,
telemetryIdentifier
);
if (telemetryPath.length) {
telemetryPathString = telemetryPath.join(' / ');
}
return telemetryPathString;
},
async getObjectPath(objectIdentifier) {
if (!objectIdentifier && !this.domainObject) {
return;
}
const domainObjectIdentifier = objectIdentifier || this.domainObject.identifier;
const objectPathList = await this.openmct.objects.getOriginalPath(domainObjectIdentifier);
objectPathList.pop();
return objectPathList
.map((pathItem) => pathItem.name)
.reverse()
.join(' / ');
},
buildToolTip(tooltipText, tooltipLocation, elementRef) {
if (!tooltipText || tooltipText.length < 1) {
return;
}
let parentElement = this.$refs[elementRef];
if (Array.isArray(parentElement)) {
parentElement = parentElement[0];
}
this.tooltip = this.openmct.tooltips.tooltip({
toolTipText: tooltipText,
toolTipLocation: tooltipLocation,
parentElement: parentElement
});
},
hideToolTip() {
this.tooltip?.destroy();
this.tooltip = null;
}
}
};
export default tooltipHelpers;

View File

@@ -0,0 +1,37 @@
import { ACTIVE_ROLE_BROADCAST_CHANNEL_NAME } from './constants';
class ActiveRoleSynchronizer {
#roleChannel;
constructor(openmct) {
this.openmct = openmct;
this.#roleChannel = new BroadcastChannel(ACTIVE_ROLE_BROADCAST_CHANNEL_NAME);
this.setActiveRoleFromChannelMessage = this.setActiveRoleFromChannelMessage.bind(this);
this.subscribeToRoleChanges(this.setActiveRoleFromChannelMessage);
}
subscribeToRoleChanges(callback) {
this.#roleChannel.addEventListener('message', callback);
}
unsubscribeFromRoleChanges(callback) {
this.#roleChannel.removeEventListener('message', callback);
}
setActiveRoleFromChannelMessage(event) {
const role = event.data;
this.openmct.user.setActiveRole(role);
}
broadcastNewRole(role) {
if (!this.#roleChannel.name) {
return false;
}
this.#roleChannel.postMessage(role);
}
destroy() {
this.unsubscribeFromRoleChanges(this.setActiveRoleFromChannelMessage);
this.#roleChannel.close();
}
}
export default ActiveRoleSynchronizer;

View File

@@ -140,9 +140,9 @@ export default class StatusAPI extends EventEmitter {
const provider = this.#userAPI.getProvider();
if (provider.canProvideStatusForRole) {
return provider.canProvideStatusForRole(role);
return Promise.resolve(provider.canProvideStatusForRole(role));
} else {
return false;
return Promise.resolve(false);
}
}
@@ -151,11 +151,16 @@ export default class StatusAPI extends EventEmitter {
* @param {Status} status The status to set for the provided role
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
setStatusForRole(role, status) {
setStatusForRole(status) {
const provider = this.#userAPI.getProvider();
if (provider.setStatusForRole) {
return provider.setStatusForRole(role, status);
const activeRole = this.#userAPI.getActiveRole();
if (!provider.canProvideStatusForRole(activeRole)) {
return false;
}
return provider.setStatusForRole(activeRole, status);
} else {
this.#userAPI.error('User provider does not support setting role status');
}
@@ -216,21 +221,6 @@ export default class StatusAPI extends EventEmitter {
}
}
/**
* The status role of the current user. A user may have multiple roles, but will only have one role
* that provides status at any time.
* @returns {Promise<import("./UserAPI").Role>} the role for which the current user can provide status.
*/
getStatusRoleForCurrentUser() {
const provider = this.#userAPI.getProvider();
if (provider.getStatusRoleForCurrentUser) {
return provider.getStatusRoleForCurrentUser();
} else {
this.#userAPI.error('User provider cannot provide role status for this user');
}
}
/**
* @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the currently logged in user, false otherwise.
* @see StatusUserProvider
@@ -238,14 +228,13 @@ export default class StatusAPI extends EventEmitter {
async canProvideStatusForCurrentUser() {
const provider = this.#userAPI.getProvider();
if (provider.getStatusRoleForCurrentUser) {
const activeStatusRole = await this.#userAPI.getProvider().getStatusRoleForCurrentUser();
const canProvideStatus = await this.canProvideStatusForRole(activeStatusRole);
return canProvideStatus;
} else {
if (!provider) {
return false;
}
const activeStatusRole = await this.#userAPI.getActiveRole();
const canProvideStatus = await this.canProvideStatusForRole(activeStatusRole);
return canProvideStatus;
}
/**

View File

@@ -77,5 +77,4 @@ export default class StatusUserProvider extends UserProvider {
/**
* @returns {Promise<import("./UserAPI").Role>} the active status role for the currently logged in user
*/
async getStatusRoleForCurrentUser() {}
}

View File

@@ -0,0 +1,37 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { ACTIVE_ROLE_LOCAL_STORAGE_KEY } from './constants';
class StoragePersistance {
getActiveRole() {
return localStorage.getItem(ACTIVE_ROLE_LOCAL_STORAGE_KEY);
}
setActiveRole(role) {
return localStorage.setItem(ACTIVE_ROLE_LOCAL_STORAGE_KEY, role);
}
clearActiveRole() {
return localStorage.removeItem(ACTIVE_ROLE_LOCAL_STORAGE_KEY);
}
}
export default new StoragePersistance();

View File

@@ -24,6 +24,7 @@ import EventEmitter from 'EventEmitter';
import { MULTIPLE_PROVIDER_ERROR, NO_PROVIDER_ERROR } from './constants';
import StatusAPI from './StatusAPI';
import User from './User';
import StoragePersistance from './StoragePersistance';
class UserAPI extends EventEmitter {
/**
@@ -86,6 +87,58 @@ class UserAPI extends EventEmitter {
return this._provider.getCurrentUser();
}
}
/**
* If a user provider is set, it will return an array of possible roles
* that can be selected by the current user
* @memberof module:openmct.UserAPI#
* @returns {Array}
* @throws Will throw an error if no user provider is set
*/
getPossibleRoles() {
if (!this.hasProvider()) {
this.error(NO_PROVIDER_ERROR);
}
return this._provider.getPossibleRoles();
}
/**
* If a user provider is set, it will return the active role or null
* @memberof module:openmct.UserAPI#
* @returns {string|null}
*/
getActiveRole() {
if (!this.hasProvider()) {
return null;
}
// get from session storage
const sessionStorageValue = StoragePersistance.getActiveRole();
return sessionStorageValue;
}
/**
* Set the active role in session storage
* @memberof module:openmct.UserAPI#
* @returns {undefined}
*/
setActiveRole(role) {
StoragePersistance.setActiveRole(role);
this.emit('roleChanged', role);
}
/**
* Will return if a role can provide a operator status response
* @memberof module:openmct.UserApi#
* @returns {Boolean}
*/
canProvideStatusForRole() {
if (!this.hasProvider()) {
return null;
}
const activeRole = this.getActiveRole();
return this._provider.canProvideStatusForRole?.(activeRole);
}
/**
* If a user provider is set, it will return the user provider's

View File

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

View File

@@ -32,6 +32,7 @@ describe('The User Status API', () => {
'setPollQuestion',
'getPollQuestion',
'getCurrentUser',
'getPossibleRoles',
'getPossibleStatuses',
'getAllStatusRoles',
'canSetPollQuestion',
@@ -42,6 +43,7 @@ describe('The User Status API', () => {
mockUser = new openmct.user.User('test-user', 'A test user');
userProvider.getCurrentUser.and.returnValue(Promise.resolve(mockUser));
userProvider.getPossibleStatuses.and.returnValue(Promise.resolve([]));
userProvider.getPossibleRoles.and.returnValue(Promise.resolve([]));
userProvider.getAllStatusRoles.and.returnValue(Promise.resolve([]));
userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(false));
userProvider.isLoggedIn.and.returnValue(true);

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