Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nasa/openmct.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJamie V <jamie.j.vigliotta@nasa.gov>2022-09-27 20:46:42 +0300
committerGitHub <noreply@github.com>2022-09-27 20:46:42 +0300
commit43857159be9825968c763a0486ba243973618e61 (patch)
treeb4775c75b1226f5912b7a05b8bee2916b271be6f
parent2951ff6972bb1101319cc6747aee35e2c319e1ce (diff)
parent6bdb8c9e1c9421c150a3db48eb52ff5d2d130b11 (diff)
Merge branch 'master' into test-heuristicstest-heuristics
-rw-r--r--.circleci/config.yml136
-rw-r--r--.eslintrc.js1
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md4
-rw-r--r--.github/PULL_REQUEST_TEMPLATE.md4
-rw-r--r--.github/dependabot.yml11
-rw-r--r--.github/workflows/e2e-couchdb.yml38
-rw-r--r--.github/workflows/e2e-pr.yml3
-rw-r--r--.github/workflows/e2e-visual.yml25
-rw-r--r--.github/workflows/prcop-config.json2
-rw-r--r--.gitignore24
-rw-r--r--API.md6
-rw-r--r--CONTRIBUTING.md4
-rw-r--r--README.md68
-rw-r--r--app.js32
-rw-r--r--babel.coverage.js9
-rw-r--r--codecov.yml17
-rw-r--r--e2e/.eslintrc.js13
-rw-r--r--e2e/.percy.yml3
-rw-r--r--e2e/README.md380
-rw-r--r--e2e/appActions.js336
-rw-r--r--e2e/baseFixtures.js174
-rw-r--r--e2e/helper/addInitExampleFaultProvider.js28
-rw-r--r--e2e/helper/addInitExampleFaultProviderStatic.js30
-rw-r--r--e2e/helper/addInitFaultManagementPlugin.js28
-rw-r--r--e2e/helper/addInitRestrictedNotebook.js30
-rw-r--r--e2e/helper/addNoneditableObject.js (renamed from e2e/tests/persistence/addNoneditableObject.js)0
-rw-r--r--e2e/helper/faultUtils.js277
-rw-r--r--e2e/helper/notebookUtils.js65
-rw-r--r--e2e/helper/useSnowTheme.js30
-rw-r--r--e2e/playwright-ci.config.js50
-rw-r--r--e2e/playwright-local.config.js65
-rw-r--r--e2e/playwright-performance.config.js43
-rw-r--r--e2e/playwright-visual.config.js44
-rw-r--r--e2e/pluginFixtures.js161
-rw-r--r--e2e/test-data/PerformanceDisplayLayout.json1
-rw-r--r--e2e/test-data/PerformanceNotebook.json1
-rw-r--r--e2e/test-data/VisualTestData_storage.json22
-rw-r--r--e2e/test-data/recycled_local_storage.json22
-rw-r--r--e2e/tests/example/generator/SinewaveLimitProvider.e2e.spec.js166
-rw-r--r--e2e/tests/framework/appActions.e2e.spec.js88
-rw-r--r--e2e/tests/framework/baseFixtures.e2e.spec.js55
-rw-r--r--e2e/tests/framework/exampleTemplate.e2e.spec.js148
-rw-r--r--e2e/tests/framework/generateVisualTestData.e2e.spec.js64
-rw-r--r--e2e/tests/framework/pluginFixtures.e2e.spec.js46
-rw-r--r--e2e/tests/framework/testData.e2e.spec.js36
-rw-r--r--e2e/tests/functional/branding.e2e.spec.js (renamed from e2e/tests/branding.e2e.spec.js)15
-rw-r--r--e2e/tests/functional/couchdb.e2e.spec.js108
-rw-r--r--e2e/tests/functional/example/eventGenerator.e2e.spec.js (renamed from e2e/tests/example/eventGenerator.e2e.spec.js)51
-rw-r--r--e2e/tests/functional/example/generator/sineWaveLimitProvider.e2e.spec.js119
-rw-r--r--e2e/tests/functional/forms.e2e.spec.js100
-rw-r--r--e2e/tests/functional/menu.e2e.spec.js (renamed from e2e/tests/persistence/persistability.e2e.spec.js)41
-rw-r--r--e2e/tests/functional/moveAndLinkObjects.e2e.spec.js212
-rw-r--r--e2e/tests/functional/planning/plan.e2e.spec.js87
-rw-r--r--e2e/tests/functional/planning/timestrip.e2e.spec.js181
-rw-r--r--e2e/tests/functional/plugins/clocks/clock.e2e.spec.js (renamed from e2e/tests/plugins/clock/Clock.e2e.spec.js)16
-rw-r--r--e2e/tests/functional/plugins/clocks/remoteClock.e2e.spec.js41
-rw-r--r--e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js (renamed from e2e/tests/plugins/condition/condition.e2e.spec.js)139
-rw-r--r--e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js186
-rw-r--r--e2e/tests/functional/plugins/faultManagement/faultManagement.e2e.spec.js237
-rw-r--r--e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js66
-rw-r--r--e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js720
-rw-r--r--e2e/tests/functional/plugins/importAndExportAsJSON/exportAsJson.e2e.spec.js (renamed from e2e/tests/plugins/ExportAsJSON/exportAsJson.e2e.spec.js)4
-rw-r--r--e2e/tests/functional/plugins/importAndExportAsJSON/importAsJson.e2e.spec.js (renamed from e2e/tests/plugins/ImportAsJSON/importAsJson.e2e.spec.js)4
-rw-r--r--e2e/tests/functional/plugins/lad/lad.e2e.spec.js120
-rw-r--r--e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js335
-rw-r--r--e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js193
-rw-r--r--e2e/tests/functional/plugins/notebook/tags.e2e.spec.js212
-rw-r--r--e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js (renamed from e2e/tests/plugins/plot/autoscale.e2e.spec.js)58
-rw-r--r--e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-darwinbin0 -> 16185 bytes
-rw-r--r--e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-linux.pngbin0 -> 15796 bytes
-rw-r--r--e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-darwinbin0 -> 18393 bytes
-rw-r--r--e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-linux.pngbin0 -> 18089 bytes
-rw-r--r--e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js (renamed from e2e/tests/plugins/plot/logPlot.e2e.spec.js)52
-rw-r--r--e2e/tests/functional/plugins/plot/missingPlotObj.e2e.spec.js157
-rw-r--r--e2e/tests/functional/plugins/plot/plotLegendSwatch.e2e.spec.js110
-rw-r--r--e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js75
-rw-r--r--e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js170
-rw-r--r--e2e/tests/functional/plugins/timer/timer.e2e.spec.js156
-rw-r--r--e2e/tests/functional/search.e2e.spec.js271
-rw-r--r--e2e/tests/functional/smoke.e2e.spec.js (renamed from e2e/tests/smoke.e2e.spec.js)18
-rw-r--r--e2e/tests/functional/tree.e2e.spec.js138
-rw-r--r--e2e/tests/moveObjects.e2e.spec.js141
-rw-r--r--e2e/tests/performance/imagery.perf.spec.js177
-rw-r--r--e2e/tests/performance/memleak-imagery.perf.spec.js119
-rw-r--r--e2e/tests/performance/notebook.perf.spec.js158
-rw-r--r--e2e/tests/plugins/imagery/exampleImagery.e2e.spec.js238
-rw-r--r--e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome.pngbin15248 -> 0 bytes
-rw-r--r--e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome.pngbin17964 -> 0 bytes
-rw-r--r--e2e/tests/plugins/timeConductor/timeConductor.e2e.spec.js112
-rw-r--r--e2e/tests/recycled_storage.json22
-rw-r--r--e2e/tests/visual/addInit.visual.spec.js62
-rw-r--r--e2e/tests/visual/components/tree.visual.spec.js101
-rw-r--r--e2e/tests/visual/controlledClock.visual.spec.js56
-rw-r--r--e2e/tests/visual/default.visual.spec.js197
-rw-r--r--e2e/tests/visual/faultManagement.visual.spec.js78
-rw-r--r--e2e/tests/visual/notebook.visual.spec.js51
-rw-r--r--e2e/tests/visual/search.visual.spec.js84
-rw-r--r--example/exampleTags/plugin.js33
-rw-r--r--example/exampleTags/tags.json19
-rw-r--r--example/exampleUser/ExampleUserProvider.js107
-rw-r--r--example/exampleUser/plugin.js15
-rw-r--r--example/exampleUser/pluginSpec.js7
-rw-r--r--example/faultManagement/exampleFaultSource.js60
-rw-r--r--example/faultManagement/pluginSpec.js47
-rw-r--r--example/faultManagement/utils.js76
-rw-r--r--example/generator/GeneratorMetadataProvider.js18
-rw-r--r--example/generator/GeneratorProvider.js6
-rw-r--r--example/generator/WorkerInterface.js2
-rw-r--r--example/generator/generatorWorker.js39
-rw-r--r--example/generator/plugin.js20
-rw-r--r--example/imagery/plugin.js28
-rw-r--r--index.html6
-rw-r--r--karma.conf.js19
-rw-r--r--package.json103
-rw-r--r--src/MCT.js311
-rw-r--r--src/api/actions/ActionCollection.js3
-rw-r--r--src/api/annotation/AnnotationAPI.js286
-rw-r--r--src/api/annotation/AnnotationAPISpec.js190
-rw-r--r--src/api/api.js14
-rw-r--r--src/api/faultmanagement/FaultManagementAPI.js106
-rw-r--r--src/api/faultmanagement/FaultManagementAPISpec.js144
-rw-r--r--src/api/forms/FormsAPI.js34
-rw-r--r--src/api/forms/FormsAPISpec.js157
-rw-r--r--src/api/forms/components/FormProperties.vue39
-rw-r--r--src/api/forms/components/FormRow.vue19
-rw-r--r--src/api/forms/components/controls/AutoCompleteField.vue101
-rw-r--r--src/api/forms/components/controls/Datetime.vue90
-rw-r--r--src/api/forms/components/controls/FileInput.vue18
-rw-r--r--src/api/forms/components/controls/NumberField.vue1
-rw-r--r--src/api/forms/components/controls/ToggleSwitchField.vue2
-rw-r--r--src/api/indicators/IndicatorAPI.js34
-rw-r--r--src/api/indicators/SimpleIndicator.js141
-rw-r--r--src/api/menu/MenuAPISpec.js178
-rw-r--r--src/api/menu/components/Menu.vue4
-rw-r--r--src/api/menu/components/SuperMenu.vue2
-rw-r--r--src/api/objects/InMemorySearchProvider.js359
-rw-r--r--src/api/objects/InMemorySearchWorker.js172
-rw-r--r--src/api/objects/ObjectAPI.js1065
-rw-r--r--src/api/objects/ObjectAPISearchSpec.js24
-rw-r--r--src/api/objects/ObjectAPISpec.js117
-rw-r--r--src/api/objects/object-utils.js4
-rw-r--r--src/api/overlays/Overlay.js3
-rw-r--r--src/api/overlays/components/OverlayComponent.vue1
-rw-r--r--src/api/overlays/components/overlay-component.scss17
-rw-r--r--src/api/telemetry/TelemetryAPI.js378
-rw-r--r--src/api/telemetry/TelemetryAPISpec.js2
-rw-r--r--src/api/telemetry/TelemetryCollection.js90
-rw-r--r--src/api/telemetry/TelemetryCollectionSpec.js101
-rw-r--r--src/api/telemetry/TelemetryMetadataManager.js14
-rw-r--r--src/api/telemetry/TelemetryRequestInterceptor.js68
-rw-r--r--src/api/telemetry/TelemetryValueFormatter.js40
-rw-r--r--src/api/telemetry/constants.js25
-rw-r--r--src/api/time/TimeAPI.js37
-rw-r--r--src/api/user/StatusAPI.js295
-rw-r--r--src/api/user/StatusUserProvider.js81
-rw-r--r--src/api/user/UserAPI.js48
-rw-r--r--src/api/user/UserAPISpec.js4
-rw-r--r--src/api/user/UserProvider.js36
-rw-r--r--src/api/user/UserStatusAPISpec.js103
-rw-r--r--src/exporters/ImageExporter.js4
-rw-r--r--src/plugins/DeviceClassifier/src/DeviceClassifier.js1
-rw-r--r--src/plugins/LADTable/components/LADRow.vue2
-rw-r--r--src/plugins/LADTable/components/LadTableSet.vue9
-rw-r--r--src/plugins/LADTable/plugin.js4
-rw-r--r--src/plugins/LADTable/pluginSpec.js4
-rw-r--r--src/plugins/URLIndicatorPlugin/URLIndicator.js23
-rw-r--r--src/plugins/URLIndicatorPlugin/URLIndicatorSpec.js41
-rw-r--r--src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js61
-rw-r--r--src/plugins/autoflow/AutoflowTabularPluginSpec.js27
-rw-r--r--src/plugins/charts/bar/BarGraphCompositionPolicy.js (renamed from src/plugins/charts/BarGraphCompositionPolicy.js)0
-rw-r--r--src/plugins/charts/bar/BarGraphConstants.js (renamed from src/plugins/charts/BarGraphConstants.js)0
-rw-r--r--src/plugins/charts/bar/BarGraphPlot.vue (renamed from src/plugins/charts/BarGraphPlot.vue)0
-rw-r--r--src/plugins/charts/bar/BarGraphView.vue (renamed from src/plugins/charts/BarGraphView.vue)172
-rw-r--r--src/plugins/charts/bar/BarGraphViewProvider.js (renamed from src/plugins/charts/BarGraphViewProvider.js)5
-rw-r--r--src/plugins/charts/bar/inspector/BarGraphInspectorViewProvider.js (renamed from src/plugins/charts/inspector/BarGraphInspectorViewProvider.js)0
-rw-r--r--src/plugins/charts/bar/inspector/BarGraphOptions.vue399
-rw-r--r--src/plugins/charts/bar/inspector/SeriesOptions.vue (renamed from src/plugins/charts/inspector/SeriesOptions.vue)25
-rw-r--r--src/plugins/charts/bar/plugin.js (renamed from src/plugins/charts/plugin.js)9
-rw-r--r--src/plugins/charts/bar/pluginSpec.js (renamed from src/plugins/charts/pluginSpec.js)206
-rw-r--r--src/plugins/charts/scatter/ScatterPlotCompositionPolicy.js57
-rw-r--r--src/plugins/charts/scatter/ScatterPlotForm.vue146
-rw-r--r--src/plugins/charts/scatter/ScatterPlotView.vue351
-rw-r--r--src/plugins/charts/scatter/ScatterPlotViewProvider.js79
-rw-r--r--src/plugins/charts/scatter/ScatterPlotWithUnderlay.vue393
-rw-r--r--src/plugins/charts/scatter/inspector/PlotOptions.vue (renamed from src/plugins/charts/inspector/BarGraphOptions.vue)34
-rw-r--r--src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue153
-rw-r--r--src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue262
-rw-r--r--src/plugins/charts/scatter/inspector/ScatterPlotInspectorViewProvider.js48
-rw-r--r--src/plugins/charts/scatter/plugin.js127
-rw-r--r--src/plugins/charts/scatter/pluginSpec.js421
-rw-r--r--src/plugins/charts/scatter/scatterPlotConstants.js4
-rw-r--r--src/plugins/clock/plugin.js3
-rw-r--r--src/plugins/condition/Condition.js2
-rw-r--r--src/plugins/condition/ConditionManager.js7
-rw-r--r--src/plugins/condition/StyleRuleManager.js6
-rw-r--r--src/plugins/condition/components/Condition.vue2
-rw-r--r--src/plugins/condition/criterion/TelemetryCriterion.js6
-rw-r--r--src/plugins/condition/plugin.js2
-rw-r--r--src/plugins/conditionWidget/components/ConditionWidget.vue4
-rw-r--r--src/plugins/displayLayout/DisplayLayoutToolbar.js42
-rw-r--r--src/plugins/displayLayout/DrawingObjectTypes.js34
-rw-r--r--src/plugins/displayLayout/components/DisplayLayout.vue16
-rw-r--r--src/plugins/displayLayout/components/LayoutFrame.vue3
-rw-r--r--src/plugins/displayLayout/components/SubobjectView.vue2
-rw-r--r--src/plugins/displayLayout/components/TelemetryView.vue16
-rw-r--r--src/plugins/displayLayout/components/layout-frame.scss11
-rw-r--r--src/plugins/displayLayout/components/telemetry-view.scss10
-rw-r--r--src/plugins/displayLayout/plugin.js6
-rw-r--r--src/plugins/displayLayout/pluginSpec.js87
-rw-r--r--src/plugins/duplicate/DuplicateTask.js2
-rw-r--r--src/plugins/exportAsJSONAction/ExportAsJSONAction.js81
-rw-r--r--src/plugins/exportAsJSONAction/ExportAsJSONActionSpec.js53
-rw-r--r--src/plugins/faultManagement/FaultManagementInspector.vue129
-rw-r--r--src/plugins/faultManagement/FaultManagementInspectorViewProvider.js71
-rw-r--r--src/plugins/faultManagement/FaultManagementListHeader.vue105
-rw-r--r--src/plugins/faultManagement/FaultManagementListItem.vue223
-rw-r--r--src/plugins/faultManagement/FaultManagementListView.vue301
-rw-r--r--src/plugins/faultManagement/FaultManagementObjectProvider.js56
-rw-r--r--src/plugins/faultManagement/FaultManagementPlugin.js42
-rw-r--r--src/plugins/faultManagement/FaultManagementSearch.vue90
-rw-r--r--src/plugins/faultManagement/FaultManagementToolbar.vue102
-rw-r--r--src/plugins/faultManagement/FaultManagementView.vue76
-rw-r--r--src/plugins/faultManagement/FaultManagementViewProvider.js69
-rw-r--r--src/plugins/faultManagement/constants.js122
-rw-r--r--src/plugins/faultManagement/fault-manager.scss268
-rw-r--r--src/plugins/faultManagement/pluginSpec.js103
-rw-r--r--src/plugins/flexibleLayout/components/flexible-layout.scss4
-rw-r--r--src/plugins/flexibleLayout/components/flexibleLayout.vue4
-rw-r--r--src/plugins/flexibleLayout/components/frame.vue20
-rw-r--r--src/plugins/flexibleLayout/pluginSpec.js13
-rw-r--r--src/plugins/flexibleLayout/toolbarProvider.js2
-rw-r--r--src/plugins/flexibleLayout/utils/container.js2
-rw-r--r--src/plugins/flexibleLayout/utils/frame.js2
-rw-r--r--src/plugins/formActions/CreateAction.js2
-rw-r--r--src/plugins/formActions/CreateActionSpec.js128
-rw-r--r--src/plugins/formActions/EditPropertiesAction.js14
-rw-r--r--src/plugins/formActions/pluginSpec.js229
-rw-r--r--src/plugins/gauge/GaugePlugin.js46
-rw-r--r--src/plugins/gauge/GaugePluginSpec.js44
-rw-r--r--src/plugins/gauge/components/Gauge.vue526
-rw-r--r--src/plugins/gauge/components/GaugeFormController.vue26
-rw-r--r--src/plugins/gauge/gauge.scss171
-rw-r--r--src/plugins/hyperlink/plugin.js2
-rw-r--r--src/plugins/imagery/components/FilterSettings.vue78
-rw-r--r--src/plugins/imagery/components/ImageControls.vue140
-rw-r--r--src/plugins/imagery/components/ImageThumbnail.vue69
-rw-r--r--src/plugins/imagery/components/ImageryView.vue200
-rw-r--r--src/plugins/imagery/components/ImageryViewMenuSwitcher.vue65
-rw-r--r--src/plugins/imagery/components/LayerSettings.vue59
-rw-r--r--src/plugins/imagery/components/ZoomSettings.vue89
-rw-r--r--src/plugins/imagery/components/imagery-view.scss253
-rw-r--r--src/plugins/imagery/layers/example-imagery-layer-16x9.pngbin0 -> 8554 bytes
-rw-r--r--src/plugins/imagery/layers/example-imagery-layer-safe.pngbin0 -> 9245 bytes
-rw-r--r--src/plugins/imagery/layers/example-imagery-layer-scale.pngbin0 -> 11616 bytes
-rw-r--r--src/plugins/imagery/mixins/imageryData.js134
-rw-r--r--src/plugins/imagery/pluginSpec.js366
-rw-r--r--src/plugins/importFromJSONAction/ImportFromJSONAction.js29
-rw-r--r--src/plugins/interceptors/missingObjectInterceptor.js5
-rw-r--r--src/plugins/licenses/third-party-licenses.json7
-rw-r--r--src/plugins/linkAction/LinkAction.js22
-rw-r--r--src/plugins/localStorage/LocalStorageObjectProvider.js4
-rw-r--r--src/plugins/move/MoveAction.js18
-rw-r--r--src/plugins/move/pluginSpec.js7
-rw-r--r--src/plugins/myItems/pluginSpec.js18
-rw-r--r--src/plugins/newFolderAction/pluginSpec.js6
-rw-r--r--src/plugins/notebook/NotebookType.js88
-rw-r--r--src/plugins/notebook/NotebookViewProvider.js72
-rw-r--r--src/plugins/notebook/components/Notebook.vue161
-rw-r--r--src/plugins/notebook/components/NotebookEmbed.vue24
-rw-r--r--src/plugins/notebook/components/NotebookEntry.vue99
-rw-r--r--src/plugins/notebook/components/PageCollection.vue2
-rw-r--r--src/plugins/notebook/components/PageComponent.vue88
-rw-r--r--src/plugins/notebook/components/PopupMenu.vue6
-rw-r--r--src/plugins/notebook/components/SearchResults.vue1
-rw-r--r--src/plugins/notebook/components/SectionCollection.vue2
-rw-r--r--src/plugins/notebook/components/SectionComponent.vue63
-rw-r--r--src/plugins/notebook/components/Sidebar.vue39
-rw-r--r--src/plugins/notebook/components/sidebar.scss46
-rw-r--r--src/plugins/notebook/monkeyPatchObjectAPIForNotebooks.js4
-rw-r--r--src/plugins/notebook/notebook-constants.js13
-rw-r--r--src/plugins/notebook/plugin.js299
-rw-r--r--src/plugins/notebook/pluginSpec.js64
-rw-r--r--src/plugins/notebook/utils/notebook-entries.js9
-rw-r--r--src/plugins/notebook/utils/notebook-image.js2
-rw-r--r--src/plugins/notebook/utils/notebook-key-code.js3
-rw-r--r--src/plugins/objectMigration/Migrations.js4
-rw-r--r--src/plugins/operatorStatus/AbstractStatusIndicator.js106
-rw-r--r--src/plugins/operatorStatus/operator-status.scss142
-rw-r--r--src/plugins/operatorStatus/operatorStatus/OperatorStatus.vue188
-rw-r--r--src/plugins/operatorStatus/operatorStatus/OperatorStatusIndicator.js63
-rw-r--r--src/plugins/operatorStatus/plugin.js50
-rw-r--r--src/plugins/operatorStatus/pollQuestion/PollQuestion.vue190
-rw-r--r--src/plugins/operatorStatus/pollQuestion/PollQuestionIndicator.js63
-rw-r--r--src/plugins/persistence/couch/.env.ci5
-rw-r--r--src/plugins/persistence/couch/CouchChangesFeed.js35
-rw-r--r--src/plugins/persistence/couch/CouchDocument.js3
-rw-r--r--src/plugins/persistence/couch/CouchObjectProvider.js261
-rw-r--r--src/plugins/persistence/couch/CouchSearchProvider.js103
-rw-r--r--src/plugins/persistence/couch/CouchStatusIndicator.js88
-rw-r--r--src/plugins/persistence/couch/README.md161
-rw-r--r--src/plugins/persistence/couch/couchdb-compose.yaml14
-rw-r--r--src/plugins/persistence/couch/plugin.js7
-rw-r--r--src/plugins/persistence/couch/pluginSpec.js172
-rw-r--r--src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh3
-rwxr-xr-xsrc/plugins/persistence/couch/setup-couchdb.sh144
-rw-r--r--src/plugins/plan/Plan.vue4
-rw-r--r--src/plugins/plan/PlanViewProvider.js2
-rw-r--r--src/plugins/plan/inspector/PlanActivitiesView.vue2
-rw-r--r--src/plugins/plan/inspector/PlanActivityView.vue2
-rw-r--r--src/plugins/plan/pluginSpec.js72
-rw-r--r--src/plugins/plan/util.js6
-rw-r--r--src/plugins/plot/MctPlot.vue91
-rw-r--r--src/plugins/plot/Plot.vue67
-rw-r--r--src/plugins/plot/PlotViewProvider.js9
-rw-r--r--src/plugins/plot/actions/ViewActions.js57
-rw-r--r--src/plugins/plot/actions/utils.js3
-rw-r--r--src/plugins/plot/axis/XAxis.vue22
-rw-r--r--src/plugins/plot/axis/YAxis.vue22
-rw-r--r--src/plugins/plot/chart/MctChart.vue1
-rw-r--r--src/plugins/plot/configuration/Model.js6
-rw-r--r--src/plugins/plot/configuration/PlotConfigurationModel.js3
-rw-r--r--src/plugins/plot/configuration/PlotSeries.js16
-rw-r--r--src/plugins/plot/configuration/SeriesCollection.js2
-rw-r--r--src/plugins/plot/configuration/YAxisModel.js2
-rw-r--r--src/plugins/plot/inspector/PlotOptionsBrowse.vue74
-rw-r--r--src/plugins/plot/inspector/PlotOptionsEdit.vue57
-rw-r--r--src/plugins/plot/inspector/PlotsInspectorViewProvider.js6
-rw-r--r--src/plugins/plot/inspector/StackedPlotsInspectorViewProvider.js59
-rw-r--r--src/plugins/plot/inspector/forms/LegendForm.vue4
-rw-r--r--src/plugins/plot/inspector/forms/SeriesForm.vue67
-rw-r--r--src/plugins/plot/inspector/forms/YAxisForm.vue21
-rw-r--r--src/plugins/plot/legend/PlotLegend.vue8
-rw-r--r--src/plugins/plot/legend/PlotLegendItemCollapsed.vue20
-rw-r--r--src/plugins/plot/legend/PlotLegendItemExpanded.vue18
-rw-r--r--src/plugins/plot/overlayPlot/OverlayPlotViewProvider.js9
-rw-r--r--src/plugins/plot/plugin.js22
-rw-r--r--src/plugins/plot/pluginSpec.js387
-rw-r--r--src/plugins/plot/stackedPlot/StackedPlot.vue169
-rw-r--r--src/plugins/plot/stackedPlot/StackedPlotItem.vue146
-rw-r--r--src/plugins/plot/stackedPlot/StackedPlotViewProvider.js9
-rw-r--r--src/plugins/plot/stackedPlot/mixins/objectStyles-mixin.js10
-rw-r--r--src/plugins/plot/stackedPlot/pluginSpec.js771
-rw-r--r--src/plugins/plot/stackedPlot/stackedPlotConfigurationInterceptor.js38
-rw-r--r--src/plugins/plugins.js31
-rw-r--r--src/plugins/remoteClock/RemoteClock.js30
-rw-r--r--src/plugins/remoteClock/RemoteClockSpec.js6
-rw-r--r--src/plugins/remoteClock/requestInterceptor.js44
-rw-r--r--src/plugins/remove/RemoveAction.js5
-rw-r--r--src/plugins/staticRootPlugin/StaticModelProvider.js4
-rw-r--r--src/plugins/staticRootPlugin/static-provider-test.json2
-rw-r--r--src/plugins/summaryWidget/src/Condition.js41
-rw-r--r--src/plugins/summaryWidget/src/ConditionManager.js12
-rw-r--r--src/plugins/summaryWidget/src/Rule.js154
-rw-r--r--src/plugins/summaryWidget/src/SummaryWidget.js71
-rw-r--r--src/plugins/summaryWidget/src/TestDataItem.js28
-rw-r--r--src/plugins/summaryWidget/src/TestDataManager.js18
-rw-r--r--src/plugins/summaryWidget/src/WidgetDnD.js27
-rw-r--r--src/plugins/summaryWidget/src/input/ColorPalette.js21
-rw-r--r--src/plugins/summaryWidget/src/input/IconPalette.js30
-rw-r--r--src/plugins/summaryWidget/src/input/Palette.js71
-rw-r--r--src/plugins/summaryWidget/src/input/Select.js48
-rw-r--r--src/plugins/summaryWidget/test/ConditionSpec.js60
-rw-r--r--src/plugins/summaryWidget/test/RuleSpec.js29
-rw-r--r--src/plugins/summaryWidget/test/SummaryWidgetSpec.js12
-rw-r--r--src/plugins/summaryWidget/test/TestDataItemSpec.js53
-rw-r--r--src/plugins/summaryWidget/test/TestDataManagerSpec.js8
-rw-r--r--src/plugins/tabs/plugin.js2
-rw-r--r--src/plugins/telemetryTable/TelemetryTableType.js4
-rw-r--r--src/plugins/telemetryTable/TelemetryTableViewProvider.js2
-rw-r--r--src/plugins/telemetryTable/collections/TableRowCollection.js2
-rw-r--r--src/plugins/telemetryTable/components/table.scss5
-rw-r--r--src/plugins/telemetryTable/components/table.vue36
-rw-r--r--src/plugins/telemetryTable/pluginSpec.js199
-rw-r--r--src/plugins/themes/maelstrom-theme.scss22
-rw-r--r--src/plugins/themes/maelstrom.js7
-rw-r--r--src/plugins/timeConductor/ConductorHistory.vue32
-rw-r--r--src/plugins/timeConductor/ConductorInputsFixed.vue8
-rw-r--r--src/plugins/timeConductor/ConductorInputsRealtime.vue10
-rw-r--r--src/plugins/timeConductor/ConductorMode.vue5
-rw-r--r--src/plugins/timeConductor/date-picker.scss3
-rw-r--r--src/plugins/timeConductor/independent/IndependentTimeConductor.vue8
-rw-r--r--src/plugins/timeConductor/pluginSpec.js8
-rw-r--r--src/plugins/timeline/TimelineCompositionPolicy.js70
-rw-r--r--src/plugins/timeline/TimelineObjectView.vue21
-rw-r--r--src/plugins/timeline/TimelineViewLayout.vue17
-rw-r--r--src/plugins/timeline/plugin.js3
-rw-r--r--src/plugins/timeline/pluginSpec.js220
-rw-r--r--src/plugins/timelist/Timelist.vue116
-rw-r--r--src/plugins/timelist/TimelistCompositionPolicy.js34
-rw-r--r--src/plugins/timelist/TimelistViewProvider.js3
-rw-r--r--src/plugins/timelist/inspector/TimelistPropertiesView.vue2
-rw-r--r--src/plugins/timelist/plugin.js20
-rw-r--r--src/plugins/timelist/pluginSpec.js220
-rw-r--r--src/plugins/timelist/timelist.scss6
-rw-r--r--src/plugins/timer/actions/PauseTimerAction.js9
-rw-r--r--src/plugins/timer/actions/RestartTimerAction.js9
-rw-r--r--src/plugins/timer/actions/StartTimerAction.js9
-rw-r--r--src/plugins/timer/actions/StopTimerAction.js9
-rw-r--r--src/plugins/timer/components/Timer.vue28
-rw-r--r--src/plugins/viewDatumAction/pluginSpec.js4
-rw-r--r--src/plugins/webPage/pluginSpec.js106
-rw-r--r--src/styles/_constants-espresso.scss12
-rw-r--r--src/styles/_constants-maelstrom.scss10
-rw-r--r--src/styles/_constants-snow.scss8
-rwxr-xr-xsrc/styles/_constants.scss32
-rw-r--r--src/styles/_controls.scss184
-rw-r--r--src/styles/_forms.scss52
-rw-r--r--src/styles/_global.scss21
-rwxr-xr-xsrc/styles/_glyphs.scss17
-rw-r--r--src/styles/_legacy-plots.scss18
-rw-r--r--src/styles/_legacy.scss11
-rw-r--r--src/styles/_mixins.scss10
-rw-r--r--src/styles/_table.scss2
-rw-r--r--src/styles/fonts/Open MCT Symbols 16px.json522
-rw-r--r--src/styles/fonts/Open-MCT-Symbols-16px.svg323
-rw-r--r--src/styles/fonts/Open-MCT-Symbols-16px.ttfbin24452 -> 26292 bytes
-rw-r--r--src/styles/fonts/Open-MCT-Symbols-16px.woffbin24528 -> 26368 bytes
-rw-r--r--src/styles/notebook.scss151
-rw-r--r--src/styles/vue-styles.scss5
-rw-r--r--src/ui/color/ColorSwatch.vue16
-rw-r--r--src/ui/components/ObjectFrame.vue26
-rw-r--r--src/ui/components/ObjectLabel.vue7
-rw-r--r--src/ui/components/ObjectPath.vue104
-rw-r--r--src/ui/components/ObjectView.vue5
-rw-r--r--src/ui/components/components.js29
-rw-r--r--src/ui/components/componentsSpec.js48
-rw-r--r--src/ui/components/object-frame.scss15
-rw-r--r--src/ui/components/object-label.scss2
-rw-r--r--src/ui/components/search.vue1
-rw-r--r--src/ui/components/tags/TagEditor.vue176
-rw-r--r--src/ui/components/tags/TagSelection.vue152
-rw-r--r--src/ui/components/tags/tags.scss67
-rw-r--r--src/ui/inspector/Location.vue5
-rw-r--r--src/ui/inspector/ObjectName.vue9
-rw-r--r--src/ui/inspector/details/Properties.vue8
-rw-r--r--src/ui/inspector/elements.scss4
-rw-r--r--src/ui/inspector/inspector.scss22
-rw-r--r--src/ui/layout/CreateButton.vue42
-rw-r--r--src/ui/layout/Layout.vue6
-rw-r--r--src/ui/layout/MCTSearch.vue13
-rw-r--r--src/ui/layout/layout.scss3
-rw-r--r--src/ui/layout/mct-search.scss10
-rw-r--r--src/ui/layout/mct-tree.scss32
-rw-r--r--src/ui/layout/mct-tree.vue158
-rw-r--r--src/ui/layout/pane.vue10
-rw-r--r--src/ui/layout/search/AnnotationSearchResult.vue148
-rw-r--r--src/ui/layout/search/GrandSearch.vue173
-rw-r--r--src/ui/layout/search/GrandSearchSpec.js285
-rw-r--r--src/ui/layout/search/ObjectSearchResult.vue136
-rw-r--r--src/ui/layout/search/SearchResultsDropDown.vue127
-rw-r--r--src/ui/layout/search/search.scss144
-rw-r--r--src/ui/layout/status-bar/Indicators.vue13
-rw-r--r--src/ui/layout/tree-item.vue4
-rw-r--r--src/ui/mixins/context-menu-gesture.js4
-rw-r--r--src/ui/preview/PreviewAction.js6
-rw-r--r--src/ui/router/ApplicationRouter.js4
-rw-r--r--src/ui/router/ApplicationRouterSpec.js3
-rw-r--r--src/ui/router/Browse.js4
-rw-r--r--src/ui/toolbar/components/toolbar-toggle-button.vue12
-rw-r--r--src/utils/agent/Agent.js16
-rw-r--r--src/utils/agent/AgentSpec.js30
-rw-r--r--src/utils/duration.js16
-rw-r--r--src/utils/raf.js14
-rw-r--r--src/utils/rafSpec.js61
-rw-r--r--src/utils/template/templateHelpers.js14
-rw-r--r--src/utils/template/templateHelpersSpec.js106
-rw-r--r--src/utils/textHighlight/TextHighlight.vue2
-rw-r--r--tsconfig.json9
-rw-r--r--webpack.common.js45
-rw-r--r--webpack.coverage.js15
-rw-r--r--webpack.dev.js11
-rw-r--r--webpack.prod.js2
472 files changed, 27562 insertions, 5789 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 5422dd9f6..7b6b4a6f0 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -2,9 +2,11 @@ version: 2.1
executors:
pw-focal-development:
docker:
- - image: mcr.microsoft.com/playwright:v1.21.1-focal
+ - image: mcr.microsoft.com/playwright:v1.25.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
+ PERCY_LOGLEVEL: 'debug' # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742)
parameters:
BUST_CACHE:
description: "Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!"
@@ -12,7 +14,7 @@ parameters:
type: boolean
commands:
build_and_install:
- description: "All steps used to build and install. Will not work on node10"
+ description: "All steps used to build and install. Will use cache if found"
parameters:
node-version:
type: string
@@ -23,7 +25,7 @@ commands:
- node/install:
install-npm: true
node-version: << parameters.node-version >>
- - run: npm install
+ - run: npm install --prefer-offline --no-audit --progress=false
restore_cache_cmd:
description: "Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache"
parameters:
@@ -31,7 +33,7 @@ commands:
type: string
steps:
- when:
- condition:
+ condition:
equal: [false, << pipeline.parameters.BUST_CACHE >> ]
steps:
- restore_cache:
@@ -41,7 +43,7 @@ commands:
parameters:
node-version:
type: string
- steps:
+ steps:
- save_cache:
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
paths:
@@ -58,10 +60,14 @@ commands:
ls -latR >> /tmp/artifacts/dir.txt
- store_artifacts:
path: /tmp/artifacts/
- upload_code_covio:
- description: "Command to upload code coverage reports to codecov.io"
- steps:
- - run: curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov
+ generate_e2e_code_cov_report:
+ description: "Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test"
+ parameters:
+ suite:
+ type: string
+ steps:
+ - run: npm run cov:e2e:report || true
+ - run: npm run cov:e2e:<<parameters.suite>>:publish
orbs:
node: circleci/node@4.9.0
browser-tools: circleci/browser-tools@1.3.0
@@ -90,106 +96,120 @@ jobs:
parameters:
node-version:
type: string
- browser:
- type: string
executor: pw-focal-development
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- - when:
- condition:
- equal: [ "FirefoxESR", <<parameters.browser>> ]
- steps:
- - browser-tools/install-firefox:
- version: "91.7.1esr" #https://archive.mozilla.org/pub/firefox/releases/
- - when:
- condition:
- equal: [ "FirefoxHeadless", <<parameters.browser>> ]
- steps:
- - browser-tools/install-firefox
- - when:
- condition:
- equal: [ "ChromeHeadless", <<parameters.browser>> ]
- steps:
- - browser-tools/install-chrome:
- replace-existing: false
- - run: npm run test -- --browsers=<<parameters.browser>>
+ - browser-tools/install-chrome:
+ replace-existing: false
+ - run: npm run test
+ - run: npm run cov:unit:publish
- save_cache_cmd:
node-version: <<parameters.node-version>>
- store_test_results:
path: dist/reports/tests/
- store_artifacts:
- path: dist/reports/
+ path: coverage
- generate_and_store_version_and_filesystem_artifacts
e2e-test:
parameters:
node-version:
type: string
- suite:
+ suite: #stable or full
type: string
executor: pw-focal-development
+ parallelism: 4
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- - run: npx playwright install
- - run: npm run test:e2e:<<parameters.suite>>
+ - when: #Only install chrome-beta when running the 'full' suite to save $$$
+ condition:
+ equal: [ "full", <<parameters.suite>> ]
+ steps:
+ - run: npx playwright install chrome-beta
+ - run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
+ - generate_e2e_code_cov_report:
+ suite: <<parameters.suite>>
- store_test_results:
path: test-results/results.xml
- store_artifacts:
path: test-results
+ - store_artifacts:
+ path: coverage
+ - store_artifacts:
+ path: html-test-results
+ - generate_and_store_version_and_filesystem_artifacts
+ perf-test:
+ parameters:
+ node-version:
+ type: string
+ executor: pw-focal-development
+ steps:
+ - build_and_install:
+ node-version: <<parameters.node-version>>
+ - run: npm run test:perf
+ - store_test_results:
+ path: test-results/results.xml
+ - store_artifacts:
+ path: test-results
+ - store_artifacts:
+ path: html-test-results
+ - generate_and_store_version_and_filesystem_artifacts
+ visual-test:
+ parameters:
+ node-version:
+ type: string
+ executor: pw-focal-development
+ steps:
+ - build_and_install:
+ node-version: <<parameters.node-version>>
+ - run: npm run test:e2e:visual
+ - store_test_results:
+ path: test-results/results.xml
+ - store_artifacts:
+ path: test-results
+ - store_artifacts:
+ path: html-test-results
- generate_and_store_version_and_filesystem_artifacts
workflows:
overall-circleci-commit-status: #These jobs run on every commit
jobs:
- lint:
- name: node16-lint
- node-version: lts/gallium
- - unit-test:
- name: node14-chrome
+ name: node14-lint
node-version: lts/fermium
- browser: ChromeHeadless
- post-steps:
- - upload_code_covio
- - unit-test:
- name: node16-chrome
- node-version: lts/gallium
- browser: ChromeHeadless
- unit-test:
name: node18-chrome
node-version: "18"
- browser: ChromeHeadless
- e2e-test:
- name: e2e-ci
+ name: e2e-stable
+ node-version: lts/gallium
+ suite: stable
+ - perf-test:
node-version: lts/gallium
- suite: ci
+ - visual-test:
+ node-version: lts/gallium
+
the-nightly: #These jobs do not run on PRs, but against master at night
jobs:
- unit-test:
- name: node16-firefoxESR-nightly
- node-version: lts/gallium
- browser: FirefoxESR
- - unit-test:
- name: node14-firefox-nightly
- node-version: lts/fermium
- browser: FirefoxHeadless
- - unit-test:
name: node14-chrome-nightly
node-version: lts/fermium
- browser: ChromeHeadless
- unit-test:
name: node16-chrome-nightly
node-version: lts/gallium
- browser: ChromeHeadless
- unit-test:
name: node18-chrome
node-version: "18"
- browser: ChromeHeadless
- npm-audit:
node-version: lts/gallium
- e2e-test:
name: e2e-full-nightly
node-version: lts/gallium
suite: full
+ - perf-test:
+ node-version: lts/gallium
+ - visual-test:
+ node-version: lts/gallium
triggers:
- schedule:
cron: "0 0 * * *"
diff --git a/.eslintrc.js b/.eslintrc.js
index 319d7cf44..26e807490 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -29,6 +29,7 @@ module.exports = {
"you-dont-need-lodash-underscore/omit": "off",
"you-dont-need-lodash-underscore/throttle": "off",
"you-dont-need-lodash-underscore/flatten": "off",
+ "you-dont-need-lodash-underscore/get": "off",
"no-bitwise": "error",
"curly": "error",
"eqeqeq": "error",
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 5fe33cf1b..2fd6b8091 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -27,7 +27,7 @@ assignees: ''
#### Environment
<!--- If encountered on local machine, execute the following:
-<!--- npx envinfo --system --browsers --npmPackages --binaries --languages --markdown -->
+<!--- npx envinfo --system --browsers --npmPackages --binaries --markdown -->
* Open MCT Version: <!--- date of build, version, or SHA -->
* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yamcs? -->
* OS:
@@ -40,6 +40,8 @@ assignees: ''
- [ ] Is there a workaround available?
- [ ] Does this impact a critical component?
- [ ] Is this just a visual bug with no functional impact?
+- [ ] Does this block the execution of e2e tests?
+- [ ] Does this have an impact on Performance?
#### Additional Information
<!--- Include any screenshots, gifs, or logs which will expedite triage -->
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 89fb12a9e..a5525f4ce 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -13,10 +13,10 @@ Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will op
### Author Checklist
* [ ] Changes address original issue?
-* [ ] Unit tests included and/or updated with changes?
+* [ ] Tests included and/or updated with changes?
* [ ] Command line build passes?
* [ ] Has this been smoke tested?
-* [ ] Testing instructions included in associated issue?
+* [ ] Testing instructions included in associated issue OR is this a dependency/testcase change?
### Reviewer Checklist
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index a5545f0cd..70d43361d 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -7,12 +7,19 @@ updates:
interval: "daily"
open-pull-requests-limit: 10
labels:
+ - "pr:e2e"
- "type:maintenance"
- "dependencies"
- - "pr:e2e"
- "pr:daveit"
- - "pr:visual"
- "pr:platform"
+ ignore:
+ #We have to source the container which is not detected by Dependabot
+ - dependency-name: "@playwright/test"
+ #Lots of noise in these type patch releases.
+ - dependency-name: "@babel/eslint-parser"
+ update-types: ["version-update:semver-patch"]
+ - dependency-name: "eslint-plugin-vue"
+ update-types: ["version-update:semver-patch"]
- package-ecosystem: "github-actions"
directory: "/"
diff --git a/.github/workflows/e2e-couchdb.yml b/.github/workflows/e2e-couchdb.yml
new file mode 100644
index 000000000..ee610a51e
--- /dev/null
+++ b/.github/workflows/e2e-couchdb.yml
@@ -0,0 +1,38 @@
+name: "e2e-couchdb"
+on:
+ workflow_dispatch:
+ pull_request:
+ types:
+ - labeled
+ - opened
+env:
+ OPENMCT_DATABASE_NAME: openmct
+ COUCH_ADMIN_USER: admin
+ COUCH_ADMIN_PASSWORD: password
+ COUCH_BASE_LOCAL: http://localhost:5984
+ COUCH_NODE_NAME: nonode@nohost
+jobs:
+ e2e-couchdb:
+ if: ${{ github.event.label.name == 'pr:e2e:couchdb' }}
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - run : docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
+ - run : sleep 3 # wait until CouchDB has started (TODO: there must be a better way)
+ - run : bash src/plugins/persistence/couch/setup-couchdb.sh
+ - uses: actions/setup-node@v3
+ with:
+ node-version: '16'
+ - run: npx playwright@1.25.2 install
+ - run: npm install
+ - run: sh src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
+ - run: npm run test:e2e:couchdb
+ - run: ls -latr
+ - name: Archive test results
+ uses: actions/upload-artifact@v3
+ with:
+ path: test-results
+ - name: Archive html test results
+ uses: actions/upload-artifact@v3
+ with:
+ path: html-test-results
diff --git a/.github/workflows/e2e-pr.yml b/.github/workflows/e2e-pr.yml
index d90f31c36..317d0ef37 100644
--- a/.github/workflows/e2e-pr.yml
+++ b/.github/workflows/e2e-pr.yml
@@ -30,7 +30,8 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: '16'
- - run: npx playwright@1.21.1 install
+ - run: npx playwright@1.25.2 install
+ - run: npx playwright install chrome-beta
- run: npm install
- run: npm run test:e2e:full
- name: Archive test results
diff --git a/.github/workflows/e2e-visual.yml b/.github/workflows/e2e-visual.yml
deleted file mode 100644
index bd0ec056f..000000000
--- a/.github/workflows/e2e-visual.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-name: "e2e-visual"
-on:
- workflow_dispatch:
- pull_request:
- types:
- - labeled
- - opened
- schedule:
- - cron: '28 21 * * 1-5'
-
-jobs:
- e2e-visual:
- if: ${{ github.event.label.name == 'pr:visual' }} || ${{ github.event.workflow_dispatch }} || ${{ github.event.schedule }}
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-node@v3
- with:
- node-version: '16'
- - run: npx playwright@1.21.1 install
- - run: npm install
- - name: Run the e2e visual tests
- run: npm run test:e2e:visual
- env:
- PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
diff --git a/.github/workflows/prcop-config.json b/.github/workflows/prcop-config.json
index 5c278b0ec..a85deb756 100644
--- a/.github/workflows/prcop-config.json
+++ b/.github/workflows/prcop-config.json
@@ -3,7 +3,7 @@
{
"name": "descriptionRegexp",
"config": {
- "regexp": "x] Testing instructions",
+ "regexp": "[x|X]] Testing instructions",
"errorMessage": ":police_officer: PR Description does not confirm that associated issue(s) contain Testing instructions"
}
},
diff --git a/.gitignore b/.gitignore
index 7c608343c..a865c26db 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,8 +15,6 @@
*.idea
*.iml
-# External dependencies
-
# Build output
target
dist
@@ -24,30 +22,28 @@ dist
# Mac OS X Finder
.DS_Store
-# Closed source libraries
-closed-lib
-
# Node, Bower dependencies
node_modules
bower_components
-# Protractor logs
-protractor/logs
-
# npm-debug log
npm-debug.log
# karma reports
report.*.json
-# Lighthouse reports
-.lighthouseci
-
# e2e test artifacts
test-results
-allure-results
+html-test-results
-package-lock.json
+# couchdb scripting artifacts
+src/plugins/persistence/couch/.env.local
+index.html.bak
-#codecov artifacts
+# codecov artifacts
+.nyc_output
+coverage
codecov
+
+# :(
+package-lock.json
diff --git a/API.md b/API.md
index a56a0de18..5c3c1afd5 100644
--- a/API.md
+++ b/API.md
@@ -390,7 +390,7 @@ A telemetry object is a domain object with a telemetry property. To take an exa
{
"key": "value",
"name": "Value",
- "units": "kilograms",
+ "unit": "kilograms",
"format": "float",
"min": 0,
"max": 100,
@@ -425,7 +425,7 @@ attribute | type | flags | notes
`name` | string | optional | a human readable label for this field. If omitted, defaults to `key`.
`source` | string | optional | identifies the property of a datum where this value is stored. If omitted, defaults to `key`.
`format` | string | optional | a specific format identifier, mapping to a formatter. If omitted, uses a default formatter. For enumerations, use `enum`. For timestamps, use `utc` if you are using utc dates, otherwise use a key mapping to your custom date format.
-`units` | string | optional | the units of this value, e.g. `km`, `seconds`, `parsecs`
+`unit` | string | optional | the unit of this value, e.g. `km`, `seconds`, `parsecs`
`min` | number | optional | the minimum possible value of this measurement. Will be used by plots, gauges, etc to automatically set a min value.
`max` | number | optional | the maximum possible value of this measurement. Will be used by plots, gauges, etc to automatically set a max value.
`enumerations` | array | optional | for objects where `format` is `"enum"`, this array tracks all possible enumerations of the value. Each entry in this array is an object, with a `value` property that is the numerical value of the enumeration, and a `string` property that is the text value of the enumeration. ex: `{"value": 0, "string": "OFF"}`. If you use an enumerations array, `min` and `max` will be set automatically for you.
@@ -1082,4 +1082,4 @@ View provider Example:
return openmct.priority.HIGH;
}
}
-``` \ No newline at end of file
+```
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 70cc2709e..8d7e0a435 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -173,7 +173,7 @@ The following guidelines are provided for anyone contributing source code to the
1. Avoid deep nesting (especially of functions), except where necessary
(e.g. due to closure scope).
1. End with a single new-line character.
-1. Always use ES6 `Class`es and inheritence rather than the pre-ES6 prototypal
+1. Always use ES6 `Class`es and inheritance rather than the pre-ES6 prototypal
pattern.
1. Within a given function's scope, do not mix declarations and imperative
code, and present these in the following order:
@@ -328,4 +328,4 @@ checklist).
Write out a small list of tests performed with just enough detail for another developer on the team
to execute.
-i.e. ```When Clicking on Add button, new `object` appears in dropdown.``` \ No newline at end of file
+i.e. ```When Clicking on Add button, new `object` appears in dropdown.```
diff --git a/README.md b/README.md
index 832167476..dc685fac1 100644
--- a/README.md
+++ b/README.md
@@ -11,22 +11,6 @@ Once you've created something amazing with Open MCT, showcase your work in our G
Try Open MCT now with our [live demo](https://openmct-demo.herokuapp.com/).
![Demo](https://nasa.github.io/openmct/static/res/images/Open-MCT.Browse.Layout.Mars-Weather-1.jpg)
-## Open MCT v2.0.0
-Support for our legacy bundle-based API, and the libraries that it was built on (like Angular 1.x), have now been removed entirely from this repository.
-
-For now if you have an Open MCT application that makes use of the legacy API, [a plugin](https://github.com/nasa/openmct-legacy-plugin) is provided that bootstraps the legacy bundling mechanism and API. This plugin will not be maintained over the long term however, and the legacy support plugin will not be tested for compatibility with future versions of Open MCT. It is provided for convenience only.
-
-### How do I know if I am using legacy API?
-You might still be using legacy API if your source code
-
-* Contains files named bundle.js, or bundle.json,
-* Makes calls to `openmct.$injector()`, or `openmct.$angular`,
-* Makes calls to `openmct.legacyRegistry`, `openmct.legacyExtension`, or `openmct.legacyBundle`.
-
-
-### What should I do if I am using legacy API?
-Please refer to [the modern Open MCT API](https://nasa.github.io/openmct/documentation/). Post any questions to the [Discussions section](https://github.com/nasa/openmct/discussions) of the Open MCT GitHub repository.
-
## Building and Running Open MCT Locally
Building and running Open MCT in your local dev environment is very easy. Be sure you have [Git](https://git-scm.com/downloads) and [Node.js](https://nodejs.org/) installed, then follow the directions below. Need additional information? Check out the [Getting Started](https://nasa.github.io/openmct/getting-started/) page on our website.
@@ -84,7 +68,10 @@ For information on writing plugins, please see [our API documentation](https://g
## Tests
-Tests are written for [Jasmine 3](https://jasmine.github.io/api/3.1/global)
+Our automated test coverage comes in the form of unit, e2e, visual, performance, and security tests.
+
+### Unit Tests
+Unit Tests are written for [Jasmine](https://jasmine.github.io/api/edge/global)
and run by [Karma](http://karma-runner.github.io). To run:
`npm test`
@@ -93,16 +80,33 @@ The test suite is configured to load any scripts ending with `Spec.js` found
in the `src` hierarchy. Full configuration details are found in
`karma.conf.js`. By convention, unit test scripts should be located
alongside the units that they test; for example, `src/foo/Bar.js` would be
-tested by `src/foo/BarSpec.js`. (For legacy reasons, some existing tests may
-be located in separate `test` folders near the units they test, but the
-naming convention is otherwise the same.)
+tested by `src/foo/BarSpec.js`.
+
+### e2e, Visual, and Performance tests
+The e2e, Visual, and Performance tests are written for playwright and run by playwright's new test runner [@playwright/test](https://playwright.dev/).
+
+To run the e2e tests which are part of every commit:
+
+`npm run test:e2e:stable`
+
+To run the visual test suite:
-### Test Reporting
+`npm run test:e2e:visual`
-When `npm test` is run, test results will be written as HTML to
-`dist/reports/tests/`. Code coverage information is written to `dist/reports/coverage`.
+To run the performance tests:
-Code Coverage Reports are available from [codecov.io](https://app.codecov.io/gh/nasa/openmct/)
+`npm run test:perf`
+
+The test suite is configured to all tests localed in `e2e/tests/` ending in `*.e2e.spec.js`. For more about the e2e test suite, please see the [README](./e2e/README.md)
+
+### Security Tests
+Each commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/) and our overall security report is available on [LGTM](https://lgtm.com/projects/g/nasa/openmct/)
+
+### Test Reporting and Code Coverage
+
+Each test suite generates a report in CircleCI. For a complete overview of testing functionality, please see our [Circle CI Test Insights Dashboard](https://app.circleci.com/insights/github/nasa/openmct/workflows/the-nightly/overview?branch=master&reporting-window=last-30-days)
+
+Our code coverage is generated during the runtime of our unit, e2e, and visual tests. The combination of those reports is published to [codecov.io](https://app.codecov.io/gh/nasa/openmct/)
# Glossary
@@ -143,3 +147,19 @@ documentation, may presume an understanding of these terms.
user makes another such choice.)
* _namespace_: A name used to identify a persistence store. A running open MCT
application could potentially use multiple persistence stores, with the
+
+## Open MCT v2.0.0
+Support for our legacy bundle-based API, and the libraries that it was built on (like Angular 1.x), have now been removed entirely from this repository.
+
+For now if you have an Open MCT application that makes use of the legacy API, [a plugin](https://github.com/nasa/openmct-legacy-plugin) is provided that bootstraps the legacy bundling mechanism and API. This plugin will not be maintained over the long term however, and the legacy support plugin will not be tested for compatibility with future versions of Open MCT. It is provided for convenience only.
+
+### How do I know if I am using legacy API?
+You might still be using legacy API if your source code
+
+* Contains files named bundle.js, or bundle.json,
+* Makes calls to `openmct.$injector()`, or `openmct.$angular`,
+* Makes calls to `openmct.legacyRegistry`, `openmct.legacyExtension`, or `openmct.legacyBundle`.
+
+
+### What should I do if I am using legacy API?
+Please refer to [the modern Open MCT API](https://nasa.github.io/openmct/documentation/). Post any questions to the [Discussions section](https://github.com/nasa/openmct/discussions) of the Open MCT GitHub repository.
diff --git a/app.js b/app.js
index b1ddaada9..c7ecd9de3 100644
--- a/app.js
+++ b/app.js
@@ -12,6 +12,7 @@ const express = require('express');
const app = express();
const fs = require('fs');
const request = require('request');
+const __DEV__ = !process.env.NODE_ENV || process.env.NODE_ENV === 'development';
// Defaults
options.port = options.port || options.p || 8080;
@@ -49,14 +50,18 @@ class WatchRunPlugin {
}
const webpack = require('webpack');
-const webpackConfig = require('./webpack.dev.js');
-webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
-webpackConfig.plugins.push(new WatchRunPlugin());
-
-webpackConfig.entry.openmct = [
- 'webpack-hot-middleware/client?reload=true',
- webpackConfig.entry.openmct
-];
+let webpackConfig;
+if (__DEV__) {
+ webpackConfig = require('./webpack.dev');
+ webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
+ webpackConfig.entry.openmct = [
+ 'webpack-hot-middleware/client?reload=true',
+ webpackConfig.entry.openmct
+ ];
+ webpackConfig.plugins.push(new WatchRunPlugin());
+} else {
+ webpackConfig = require('./webpack.coverage');
+}
const compiler = webpack(webpackConfig);
@@ -68,10 +73,12 @@ app.use(require('webpack-dev-middleware')(
}
));
-app.use(require('webpack-hot-middleware')(
- compiler,
- {}
-));
+if (__DEV__) {
+ app.use(require('webpack-hot-middleware')(
+ compiler,
+ {}
+ ));
+}
// Expose index.html for development users.
app.get('/', function (req, res) {
@@ -82,3 +89,4 @@ app.get('/', function (req, res) {
app.listen(options.port, options.host, function () {
console.log('Open MCT application running at %s:%s', options.host, options.port);
});
+
diff --git a/babel.coverage.js b/babel.coverage.js
deleted file mode 100644
index 6a752a9e5..000000000
--- a/babel.coverage.js
+++ /dev/null
@@ -1,9 +0,0 @@
-// This is a Babel config that webpack.coverage.js uses in order to instrument
-// code with coverage instrumentation.
-const babelConfig = {
- plugins: [['babel-plugin-istanbul', {
- extension: ['.js', '.vue']
- }]]
-};
-
-module.exports = babelConfig;
diff --git a/codecov.yml b/codecov.yml
index 30b299937..d69c35bbd 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -13,17 +13,16 @@ coverage:
round: down
range: "66...100"
-ignore:
-
-parsers:
- gcov:
- branch_detection:
- conditional: true
- loop: true
- method: false
- macro: false
+flags:
+ unit:
+ carryforward: true
+ e2e-ci:
+ carryforward: true
+ e2e-full:
+ carryforward: true
comment:
layout: "reach,diff,flags,files,footer"
behavior: default
require_changes: false
+ show_carryforward_flags: true \ No newline at end of file
diff --git a/e2e/.eslintrc.js b/e2e/.eslintrc.js
index 78d37c29a..5d6b77581 100644
--- a/e2e/.eslintrc.js
+++ b/e2e/.eslintrc.js
@@ -1,4 +1,15 @@
/* eslint-disable no-undef */
module.exports = {
- "extends": ["plugin:playwright/playwright-test"]
+ "extends": ["plugin:playwright/playwright-test"],
+ "rules": {
+ "playwright/max-nested-describe": ["error", { "max": 1 }]
+ },
+ "overrides": [
+ {
+ "files": ["tests/visual/*.spec.js"],
+ "rules": {
+ "playwright/no-wait-for-timeout": "off"
+ }
+ }
+ ]
};
diff --git a/e2e/.percy.yml b/e2e/.percy.yml
index fc3ff095d..11495d9d4 100644
--- a/e2e/.percy.yml
+++ b/e2e/.percy.yml
@@ -2,4 +2,5 @@ version: 2
snapshot:
widths: [1024, 2000]
min-height: 1440 # px
-
+discovery:
+ concurrency: 2 # https://github.com/percy/cli/discussions/1067
diff --git a/e2e/README.md b/e2e/README.md
new file mode 100644
index 000000000..2ce872710
--- /dev/null
+++ b/e2e/README.md
@@ -0,0 +1,380 @@
+# e2e testing
+
+This document captures information specific to the e2e testing of Open MCT. For general information about testing, please see [the Open MCT README](https://github.com/nasa/openmct/blob/master/README.md#tests).
+
+## Table of Contents
+
+This document is designed to capture on the What, Why, and How's of writing and running e2e tests in Open MCT. Please use the built-in Github Table of Contents functionality at the top left of this page or the markup.
+
+1. [Getting Started](#getting-started)
+2. [Types of Testing](#types-of-e2e-testing)
+3. [Architecture](#architecture)
+
+## Getting Started
+
+While our team does our best to lower the barrier to entry to working with our e2e framework and Open MCT, there is a bit of work required to get from 0 to 1 test contributed.
+
+### Getting started with Playwright
+
+If this is your first time ever using the Playwright framework, we recommend going through the [Getting Started Guide](https://playwright.dev/docs/next/intro) which can be completed in about 15 minutes. This will give you a concise tour of Playwright's functionality and an understanding of the official Playwright documentation which we leverage in Open MCT.
+
+### Getting started with Open MCT's implementation of Playwright
+
+Once you've got an understanding of Playwright, you'll need a baseline understanding of Open MCT:
+
+1. Follow the steps [Building and Running Open MCT Locally](../README.md#building-and-running-open-mct-locally)
+2. Once you're serving Open MCT locally, create a 'Display Layout' object. Save it.
+3. Create a 'Plot' Object (e.g.: 'Stacked Plot')
+4. Create an Example Telemetry Object (e.g.: 'Sine Wave Generator')
+5. Expand the Tree and note the hierarchy of objects which were created.
+6. Navigate to the Demo Display Layout Object to edit and modify the embedded plot.
+7. Modify the embedded plot with Telemetry Data.
+
+What you've created is a display which mimics the display that a mission control operator might use to understand and model telemetry data.
+
+Recreate the steps above with Playwright's codegen tool:
+
+1. `npm run start` in a terminal window to serve Open MCT locally
+2. `npx @playwright/test install` to install playwright and dependencies
+3. Open another terminal window and start the Playwright codegen application `npx playwright codegen`
+4. Navigate the browser to `http://localhost:8080`
+5. Click the Create button and notice how your actions in the browser are being recorded in the Playwright Inspector
+6. Continue through the steps 2-6 above
+
+What you've created is an automated test which mimics the creation of a mission control display.
+
+Next, you should walk through our implementation of Playwright in Open MCT:
+
+1. Close any terminals which are serving up a local instance of Open MCT
+2. Run our 'Getting Started' test in debug mode with `npm run test:e2e:local -- exampleTemplate --debug`
+3. Step through each test step in the Playwright Inspector to see how we leverage Playwright's capabilities to test Open MCT
+
+## Types of e2e Testing
+
+e2e testing describes the layer at which a test is performed without prescribing the assertions which are made. Generally, when writing an e2e test, we have three choices to make on an assertion strategy:
+
+1. Functional - Verifies the functional correctness of the application. Sometimes interchanged with e2e or regression testing.
+2. Visual - Verifies the "look and feel" of the application and can only detect _undesirable changes when compared to a previous baseline_.
+3. Snapshot - Similar to Visual in that it captures the "look" of the application and can only detect _undesirable changes when compared to a previous baseline_. **Generally not preferred due to advanced setup necessary.**
+
+When choosing between the different testing strategies, think only about the assertion that is made at the end of the series of test steps. "I want to verify that the Timer plugin functions correctly" vs "I want to verify that the Timer plugin does not look different than originally designed".
+
+We do not want to interleave visual and functional testing inside the same suite because visual test verification of correctness must happen with a 3rd party service. This service is not available when executing these tests in other contexts (i.e. VIPER).
+
+### Functional Testing
+
+The bulk of our e2e coverage lies in "functional" test coverage which verifies that Open MCT is functionally correct as well as defining _how we expect it to behave_. This enables us to test the application exactly as a user would, while prescribing exactly how a user can interact with the application via a web browser.
+
+### Visual Testing
+
+Visual Testing is an essential part of our e2e strategy as it ensures that the application _appears_ correctly to a user while it compliments the functional e2e suite. It would be impractical to make thousands of assertions functional assertions on the look and feel of the application. Visual testing is interested in getting the DOM into a specified state and then comparing that it has not changed against a baseline.
+
+For a better understanding of the visual issues which affect Open MCT, please see our bug tracker with the `label:visual` filter applied [here](https://github.com/nasa/openmct/issues?q=label%3Abug%3Avisual+)
+To read about how to write a good visual test, please see [How to write a great Visual Test](#how-to-write-a-great-visual-test).
+
+`npm run test:e2e:visual` will run all of the visual tests against a local instance of Open MCT. If no `PERCY_TOKEN` API key is found in the terminal or command line environment variables, no visual comparisons will be made.
+
+#### Percy.io
+
+To make this possible, we're leveraging a 3rd party service, [Percy](https://percy.io/). This service maintains a copy of all changes, users, scm-metadata, and baselines to verify that the application looks and feels the same _unless approved by a Open MCT developer_. To request a Percy API token, please reach out to the Open MCT Dev team on GitHub. For more information, please see the official [Percy documentation](https://docs.percy.io/docs/visual-testing-basics)
+
+### (Advanced) Snapshot Testing
+
+Snapshot testing is very similar to visual testing but allows us to be more precise in detecting change without relying on a 3rd party service. Unfortuantely, this precision requires advanced test setup and teardown and so we're using this pattern as a last resort.
+
+To give an example, if a _single_ visual test assertion for an Overlay plot is run through multiple DOM rendering engines at various viewports to see how the Plot looks. If that same test were run as a snapshot test, it could only be executed against a single browser, on a single platform (ubuntu docker container).
+
+Read more about [Playwright Snapshots](https://playwright.dev/docs/test-snapshots)
+
+#### Open MCT's implementation
+
+- Our Snapshot tests receive a `@snapshot` tag.
+- Snapshots need to be executed within the official Playwright container to ensure we're using the exact rendering platform in CI and locally.
+
+```sh
+docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:[GET THIS VERSION FROM OUR CIRCLECI CONFIG FILE]-focal /bin/bash
+npm install
+npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot
+```
+
+### (WIP) Updating Snapshots
+
+When the `@snapshot` tests fail, they will need to be evaluated to see if the failure is an acceptable change or
+
+## Performance Testing
+
+The open source performance tests function mostly as a contract for the locator logic, functionality, and assumptions will work in our downstream, closed source test suites.
+
+They're found under `./e2e/tests/performance` and are to be executed with the following npm script:
+
+`npm run test:perf`
+
+These tests are expected to become blocking and gating with assertions as we extend the capabilities of Playwright.
+
+## Test Architecture and CI
+
+### Architecture (TODO)
+
+### File Structure
+
+Our file structure follows the type of type of testing being excercised at the e2e layer and files containing test suites which matcher application behavior or our `src` and `example` layout. This area is not well refined as we figure out what works best for closed source and downstream projects. This may change altogether if we move `e2e` to it's own npm package.
+
+- `./helper` - contains helper functions or scripts which are leveraged directly within the testsuites. i.e. non-default plugin scripts injected into DOM
+- `./test-data` - contains test data which is leveraged or generated in the functional, performance, or visual test suites. i.e. localStorage data
+- `./tests/functional` - the bulk of the tests are contained within this folder to verify the functionality of open mct
+- `./tests/functional/example/` - tests which specifically verify the example plugins
+- `./tests/functional/plugins/` - tests which loosely test each plugin. This folder is the most likely to change. Note: some @snapshot tests are still contained within this structure
+- `./tests/framework/` - tests which verify that our testframework functionality and assumptions will continue to work based on further refactoring or playwright version changes
+- `./tests/performance/` - performance tests
+- `./tests/visual/` - Visual tests
+- `./appActions.js` - Contains common fixtures which can be leveraged by testcase authors to quickly move through the application when writing new tests.
+- `./baseFixture.js` - Contains base fixtures which only extend default `@playwright/test` functionality. The goal is to remove these fixtures as native Playwright APIs improve.
+
+Our functional tests end in `*.e2e.spec.js`, visual tests in `*.visual.spec.js` and performance tests in `*.perf.spec.js`.
+
+### Configuration
+
+Where possible, we try to run Open MCT without modification or configuration change so that the Open MCT doesn't fail exclusively in "test mode" or in "production mode".
+
+Open MCT is leveraging the [config file](https://playwright.dev/docs/test-configuration) pattern to describe the capabilities of Open MCT e2e _where_ it's run
+
+- `./playwright-ci.config.js` - Used when running in CI or to debug CI issues locally
+- `./playwright-local.config.js` - Used when running locally
+- `./playwright-performance.config.js` - Used when running performance tests in CI or locally
+- `./playwright-visual.config.js` - Used to run the visual tests in CI or locally
+
+#### Test Tags
+
+Test tags are a great way of organizing tests outside of a file structure. To learn more see the official documentation [here](https://playwright.dev/docs/test-annotations#tag-tests).
+
+Current list of test tags:
+
+- `@ipad` - Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no Create button).
+- `@gds` - Denotes a GDS Test Case used in the VIPER Mission.
+- `@addInit` - Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of app.js.
+- `@localStorage` - Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB).
+- `@snapshot` - Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container.
+- `@unstable` - A new test or test which is known to be flaky.
+- `@2p` - Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity.
+
+### Continuous Integration
+
+The cheapest time to catch a bug is pre-merge. Unfortuantely, this is the most expensive time to run all of the tests since each merge event can consist of hundreds of commits. For this reason, we're selective in _what we run_ as much as _when we run it_.
+
+We leverage CircleCI to run tests against each commit and inject the Test Reports which are generated by Playwright so that they team can keep track of flaky and [historical test trends](https://app.circleci.com/insights/github/nasa/openmct/workflows/overall-circleci-commit-status/tests?branch=master&reporting-window=last-30-days)
+
+We leverage Github Actions / Workflows to execute tests as it gives us the ability to run against multiple operating systems with greater control over git event triggers (i.e. Run on a PR Comment event).
+
+Our CI environment consists of 3 main modes of operation:
+
+#### 1. Per-Commit Testing
+
+CircleCI
+
+- Stable e2e tests against ubuntu and chrome
+- Performance tests against ubuntu and chrome
+- e2e tests are linted
+
+#### 2. Per-Merge Testing
+
+Github Actions / Workflow
+
+- Full suite against all browsers/projects. Triggered with Github Label Event 'pr:e2e'
+- Visual Tests. Triggered with Github Label Event 'pr:visual'
+
+#### 3. Scheduled / Batch Testing
+
+Nightly Testing in Circle CI
+
+- Full e2e suite against ubuntu and chrome
+- Performance tests against ubuntu and chrome
+
+Github Actions / Workflow
+
+- Visual Test baseline generation.
+
+#### Parallelism and Fast Feedback
+
+In order to provide fast feedback in the Per-Commit context, we try to keep total test feedback at 5 minutes or less. That is to say, A developer should have a pass/fail result in under 5 minutes.
+
+Playwright has native support for semi-intelligent sharding. Read about it [here](https://playwright.dev/docs/test-parallel#shard-tests-between-multiple-machines).
+
+We will be adjusting the parallelization of the Per-Commit tests to keep below the 5 minute total runtime threshold.
+
+In addition to the Parallelization of Test Runners (Sharding), we're also running two concurrent threads on every Shard. This is the functional limit of what CircelCI Agents can support from a memory and CPU resource constraint.
+
+So for every commit, Playwright is effectively running 4 x 2 concurrent browsercontexts to keep the overall runtime to a miminum.
+
+At the same time, we don't want to waste CI resources on parallel runs, so we've configured each shard to fail after 5 test failures. Test failure logs are recorded and stored to allow fast triage.
+
+#### Test Promotion
+
+In order to maintain fast and reliable feedback, tests go through a promotion process. All new test cases or test suites must be labeled with the `@unstable` annotation. The Open MCT dev team runs these unstable tests in our private repos to ensure they work downstream and are reliable.
+
+To run the stable tests, use the ```npm run test:e2e:stable``` command. To run the new and flaky tests, use the ```npm run test:e2e:unstable``` command.
+
+A testcase and testsuite are to be unmarked as @unstable when:
+
+1. They run as part of "full" run 5 times without failure.
+2. They've been by a Open MCT Developer 5 times in the closed source repo without failure.
+
+### Cross-browser and Cross-operating system
+
+#### **What's supported:**
+
+We are leveraging the `browserslist` project to declare our supported list of browsers.
+
+#### **Where it's tested:**
+
+We lint on `browserslist` to ensure that we're not implementing deprecated browser APIs and are aware of browser API improvements over time.
+
+We also have the need to execute our e2e tests across this published list of browsers. Our browsers and browser version matrix is found inside of our `./playwright-*.config.js`, but mostly follows in order of bleeding edge to stable:
+
+- `playwright-chromium channel:beta`
+ - A beta version of Chromium from official chromium channels. As close to the bleeding edge as we can get.
+- `playwright-chromium`
+ - A stable version of Chromium from the official chromium channels. This is always at least 1 version ahead of desktop chrome.
+- `playwright-chrome`
+ - The stable channel of Chrome from the official chrome channels. This is always 2 versions behind chromium.
+
+#### **Mobile**
+
+We have the Mission-need to support iPad. To run our iPad suite, please see our `playwright-*.config.js` with the 'iPad' project.
+
+#### **Skipping or executing tests based on browser, os, and/os browser version:**
+
+Conditionally skipping tests based on browser (**RECOMMENDED**):
+
+```js
+test('Can adjust image brightness/contrast by dragging the sliders', async ({ page, browserName }) => {
+ // eslint-disable-next-line playwright/no-skipped-test
+ test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox');
+
+ // ...
+```
+
+Conditionally skipping tests based on OS:
+
+```js
+test('Can adjust image brightness/contrast by dragging the sliders', async ({ page }) => {
+ // eslint-disable-next-line playwright/no-skipped-test
+ test.skip(process.platform === 'darwin', 'This test needs to be updated to work with MacOS');
+
+ // ...
+```
+
+Skipping based on browser version (Rarely used): <https://github.com/microsoft/playwright/discussions/17318>
+
+## Test Design, Best Practices, and Tips & Tricks
+
+### Test Design (TODO)
+
+- How to make tests robust to function in other contexts (VISTA, VIPER, etc.)
+ - Leverage the use of `appActions.js` methods such as `createDomainObjectWithDefaults()`
+- How to make tests faster and more resilient
+ - When possible, navigate directly by URL
+ - Leverage `await page.goto('./', { waitUntil: 'networkidle' });`
+ - Avoid repeated setup to test to test a single assertion. Write longer tests with multiple soft assertions.
+
+### How to write a great test (TODO)
+
+#### How to write a great visual test (TODO)
+
+### Best Practices
+
+For now, our best practices exist as self-tested, living documentation in our [exampleTemplate.e2e.spec.js](./tests/framework/exampleTemplate.e2e.spec.js) file.
+
+### Tips & Tricks (TODO)
+
+The following contains a list of tips and tricks which don't exactly fit into a FAQ or Best Practices doc.
+
+- Working with multiple pages
+There are instances where multiple browser pages will need to be opened to verify multi-page or multi-tab application behavior.
+
+### Reporting
+
+Test Reporting is done through official Playwright reporters and the CI Systems which execute them.
+
+We leverage the following official Playwright reporters:
+
+- HTML
+- junit
+- github annotations
+- Tracefile
+- Screenshots
+
+When running the tests locally with the `npm run test:local` command, the html report will open automatically on failure. Inside this HTML report will be a complete summary of the finished tests. If the tests failed, you'll see embedded links to screenshot failure, execution logs, and the Tracefile.
+
+When looking at the reports run in CI, you'll leverage this same HTML Report which is hosted either in CircleCI or Github Actions as a build artifact.
+
+### e2e Code Coverage
+
+Code coverage is collected during test execution using our custom [baseFixture](./baseFixtures.js). The raw coverage files are stored in a `.nyc_report` directory to be converted into a lcov file with the following [nyc](https://github.com/istanbuljs/nyc) command:
+
+```npm run cov:e2e:report```
+
+At this point, the nyc linecov report can be published to [codecov.io](https://about.codecov.io/) with the following command:
+
+```npm run cov:e2e:stable:publish``` for the stable suite running in ubuntu.
+or
+```npm run cov:e2e:full:publish``` for the full suite running against all available platforms.
+
+Codecov.io will combine each of the above commands with [Codecov.io Flags](https://docs.codecov.com/docs/flags). Effectively, this allows us to combine multiple reports which are run at various stages of our CI Pipeline or run as part of a parallel process.
+
+This e2e coverage is combined with our unit test report to give a comprehensive (if flawed) view of line coverage.
+
+## Other
+
+### About e2e testing
+
+e2e testing is an industry-standard approach to automating the testing of web-based UIs such as Open MCT. Broadly speaking, e2e tests differentiate themselves from unit tests by preferring replication of real user interactions over execution of raw JavaScript functions.
+
+Historically, the abstraction necessary to replicate real user behavior meant that:
+
+- e2e tests were "expensive" due to how much code each test executed. The closer a test replicates the user, the more code is needed run during test execution. Unit tests could run smaller units of code more efficiently.
+- e2e tests were flaky due to network conditions or the underlying protocols associated with testing a browser.
+- e2e frameworks relied on a browser communication standard which lacked the observability and controls necessary needed to reach the code paths possible with unit and integration tests.
+- e2e frameworks provided insufficient debug information on test failure
+
+However, as the web ecosystem has matured to the point where mission-critical UIs can be written for the web (Open MCT), the e2e testing tools have matured as well. There are now fewer "trade-offs" when choosing to write an e2e test over any other type of test.
+
+Modern e2e frameworks:
+
+- Bypass the surface layer of the web-application-under-test and use a raw debugging protocol to observe and control application and browser state.
+- These new browser-internal protocols enable near-instant, bi-directional communication between test code and the browser, speeding up test execution and making the tests as reliable as the application itself.
+- Provide test debug tooling which enables developers to pinpoint failure
+
+Furthermore, the abstraction necessary to run e2e tests as a user enables them to be extended to run within a variety of contexts. This matches the extensible design of Open MCT.
+
+A single e2e test in Open MCT is extended to run:
+
+- Against a matrix of browser versions.
+- Against a matrix of OS platforms.
+- Against a local development version of Open MCT.
+- A version of Open MCT loaded as a dependency (VIPER, VISTA, etc)
+- Against a variety of data sources or telemetry endpoints.
+
+### Why Playwright?
+
+[Playwright](https://playwright.dev/) was chosen as our e2e framework because it solves a few VIPER Mission needs:
+
+1. First-class support for Automated Performance Testing
+2. Official Chrome, Chrome Canary, and iPad Capabilities
+3. Support for Browserless.io to run tests in a "hermetically sealed" environment
+4. Ability to generate code coverage reports
+
+### FAQ
+
+- How does this help NASA missions?
+- When should I write an e2e test instead of a unit test?
+- When should I write a functional vs visual test?
+- How is Open MCT extending default Playwright functionality?
+- What about Component Testing?
+
+### Troubleshooting
+
+- Why is my test failing on CI and not locally?
+- How can I view the failing tests on CI?
+- Tests won't start because 'Error: <http://localhost:8080/># is already used...'
+This error will appear when running the tests locally. Sometimes, the webserver is left in an orphaned state and needs to be cleaned up. To clear up the orphaned webserver, execute the following from your Terminal:
+```lsof -n -i4TCP:8080 | awk '{print$2}' | tail -1 | xargs kill -9```
diff --git a/e2e/appActions.js b/e2e/appActions.js
new file mode 100644
index 000000000..3064ecf07
--- /dev/null
+++ b/e2e/appActions.js
@@ -0,0 +1,336 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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.
+ *****************************************************************************/
+
+/**
+ * The fixtures in this file are to be used to consolidate common actions performed by the
+ * various test suites. The goal is only to avoid duplication of code across test suites and not to abstract
+ * away the underlying functionality of the application. For more about the App Action pattern, see /e2e/README.md)
+ *
+ * For example, if two functions are nearly identical in
+ * timer.e2e.spec.js and notebook.e2e.spec.js, that function should be generalized and moved into this file.
+ */
+
+/**
+ * Defines parameters to be used in the creation of a domain object.
+ * @typedef {Object} CreateObjectOptions
+ * @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.
+ */
+
+/**
+ * Contains information about the newly created domain object.
+ * @typedef {Object} CreatedObjectInfo
+ * @property {string} name the name of the created object
+ * @property {string} uuid the uuid of the created object
+ * @property {string} url the relative url to the object (for use with `page.goto()`)
+ */
+
+const Buffer = require('buffer').Buffer;
+
+/**
+ * This common function creates a domain object with the default options. It is the preferred way of creating objects
+ * in the e2e suite when uninterested in properties of the objects themselves.
+ *
+ * @param {import('@playwright/test').Page} page
+ * @param {CreateObjectOptions} options
+ * @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
+ */
+async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
+ const parentUrl = await getHashUrlToDomainObject(page, parent);
+
+ // Navigate to the parent object. This is necessary to create the object
+ // in the correct location, such as a folder, layout, or plot.
+ await page.goto(`${parentUrl}?hideTree=true`);
+ await page.waitForLoadState('networkidle');
+
+ //Click the Create button
+ await page.click('button:has-text("Create")');
+
+ // Click the object specified by 'type'
+ await page.click(`li:text("${type}")`);
+
+ // Modify the name input field of the domain object to accept 'name'
+ if (name) {
+ const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
+ await nameInput.fill("");
+ await nameInput.fill(name);
+ }
+
+ // Click OK button and wait for Navigate event
+ await Promise.all([
+ page.waitForLoadState(),
+ page.click('[aria-label="Save"]'),
+ // Wait for Save Banner to appear
+ page.waitForSelector('.c-message-banner__message')
+ ]);
+
+ // Wait until the URL is updated
+ await page.waitForURL(`**/${parent}/*`);
+ const uuid = await getFocusedObjectUuid(page);
+ const objectUrl = await getHashUrlToDomainObject(page, uuid);
+
+ if (await _isInEditMode(page, uuid)) {
+ // Save (exit edit mode)
+ await page.locator('button[title="Save"]').click();
+ await page.locator('li[title="Save and Finish Editing"]').click();
+ }
+
+ return {
+ name: name || `Unnamed ${type}`,
+ uuid: uuid,
+ url: objectUrl
+ };
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ * @param {string} name
+ */
+async function expandTreePaneItemByName(page, name) {
+ const treePane = page.locator('#tree-pane');
+ const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
+ const expandTriangle = treeItem.locator('.c-disclosure-triangle');
+ await expandTriangle.click();
+}
+
+/**
+ * Create a Plan object from JSON with the provided options.
+ * @param {import('@playwright/test').Page} page
+ * @param {*} options
+ * @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
+ */
+async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
+ const parentUrl = await getHashUrlToDomainObject(page, parent);
+
+ // Navigate to the parent object. This is necessary to create the object
+ // in the correct location, such as a folder, layout, or plot.
+ await page.goto(`${parentUrl}?hideTree=true`);
+
+ //Click the Create button
+ await page.click('button:has-text("Create")');
+
+ // Click 'Plan' menu option
+ await page.click(`li:text("Plan")`);
+
+ // Modify the name input field of the domain object to accept 'name'
+ if (name) {
+ const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
+ await nameInput.fill("");
+ await nameInput.fill(name);
+ }
+
+ // Upload buffer from memory
+ await page.locator('input#fileElem').setInputFiles({
+ name: 'plan.txt',
+ mimeType: 'text/plain',
+ buffer: Buffer.from(JSON.stringify(json))
+ });
+
+ // Click OK button and wait for Navigate event
+ await Promise.all([
+ page.waitForLoadState(),
+ page.click('[aria-label="Save"]'),
+ // Wait for Save Banner to appear
+ page.waitForSelector('.c-message-banner__message')
+ ]);
+
+ // Wait until the URL is updated
+ await page.waitForURL(`**/mine/*`);
+ const uuid = await getFocusedObjectUuid(page);
+ const objectUrl = await getHashUrlToDomainObject(page, uuid);
+
+ return {
+ uuid,
+ name,
+ url: objectUrl
+ };
+}
+
+/**
+* Open the given `domainObject`'s context menu from the object tree.
+* Expands the path to the object and scrolls to it if necessary.
+*
+* @param {import('@playwright/test').Page} page
+* @param {string} url the url to the object
+*/
+async function openObjectTreeContextMenu(page, url) {
+ await page.goto(url);
+ await page.click('button[title="Show selected item in tree"]');
+ await page.locator('.is-navigated-object').click({
+ button: 'right'
+ });
+}
+
+/**
+ * Gets the UUID of the currently focused object by parsing the current URL
+ * and returning the last UUID in the path.
+ * @param {import('@playwright/test').Page} page
+ * @returns {Promise<string>} the uuid of the focused object
+ */
+async function getFocusedObjectUuid(page) {
+ const UUIDv4Regexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi;
+ const focusedObjectUuid = await page.evaluate((regexp) => {
+ return window.location.href.split('?')[0].match(regexp).at(-1);
+ }, UUIDv4Regexp);
+
+ return focusedObjectUuid;
+}
+
+/**
+ * Returns the hashUrl to the domainObject given its uuid.
+ * Useful for directly navigating to the given domainObject.
+ *
+ * URLs returned will be of the form `'./browse/#/mine/<uuid0>/<uuid1>/...'`
+ *
+ * @param {import('@playwright/test').Page} page
+ * @param {string} uuid the uuid of the object to get the url for
+ * @returns {Promise<string>} the url of the object
+ */
+async function getHashUrlToDomainObject(page, uuid) {
+ const hashUrl = await page.evaluate(async (objectUuid) => {
+ const path = await window.openmct.objects.getOriginalPath(objectUuid);
+ let url = './#/browse/' + [...path].reverse()
+ .map((object) => window.openmct.objects.makeKeyString(object.identifier))
+ .join('/');
+
+ // Drop the vestigial '/ROOT' if it exists
+ if (url.includes('/ROOT')) {
+ url = url.split('/ROOT').join('');
+ }
+
+ return url;
+ }, uuid);
+
+ return hashUrl;
+}
+
+/**
+ * Utilizes the OpenMCT API to detect if the given object has an active transaction (is in Edit mode).
+ * @private
+ * @param {import('@playwright/test').Page} page
+ * @param {string | import('../src/api/objects/ObjectAPI').Identifier} identifier
+ * @return {Promise<boolean>} true if the object has an active transaction, false otherwise
+ */
+async function _isInEditMode(page, identifier) {
+ // eslint-disable-next-line no-return-await
+ return await page.evaluate((objectIdentifier) => window.openmct.objects.isTransactionActive(objectIdentifier), identifier);
+}
+
+/**
+ * Set the time conductor mode to either fixed timespan or realtime mode.
+ * @param {import('@playwright/test').Page} page
+ * @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
+ */
+async function setTimeConductorMode(page, isFixedTimespan = true) {
+ // Click 'mode' button
+ await page.locator('.c-mode-button').click();
+
+ // Switch time conductor mode
+ if (isFixedTimespan) {
+ await page.locator('data-testid=conductor-modeOption-fixed').click();
+ } else {
+ await page.locator('data-testid=conductor-modeOption-realtime').click();
+ }
+}
+
+/**
+ * Set the time conductor to fixed timespan mode
+ * @param {import('@playwright/test').Page} page
+ */
+async function setFixedTimeMode(page) {
+ await setTimeConductorMode(page, true);
+}
+
+/**
+ * Set the time conductor to realtime mode
+ * @param {import('@playwright/test').Page} page
+ */
+async function setRealTimeMode(page) {
+ await setTimeConductorMode(page, false);
+}
+
+/**
+ * @typedef {Object} OffsetValues
+ * @property {string | undefined} hours
+ * @property {string | undefined} mins
+ * @property {string | undefined} secs
+ */
+
+/**
+ * Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
+ * @param {import('@playwright/test').Page} 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);
+ }
+
+ if (mins) {
+ await page.fill('.pr-time-controls__mins', mins);
+ }
+
+ if (secs) {
+ await page.fill('.pr-time-controls__secs', secs);
+ }
+
+ // Click the check button
+ await page.locator('.pr-time__buttons .icon-check').click();
+}
+
+/**
+ * Set the values (hours, mins, secs) for the start time offset when in realtime mode
+ * @param {import('@playwright/test').Page} page
+ * @param {OffsetValues} offset
+ */
+async function setStartOffset(page, offset) {
+ const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
+ await setTimeConductorOffset(page, offset, startOffsetButton);
+}
+
+/**
+ * Set the values (hours, mins, secs) for the end time offset when in realtime mode
+ * @param {import('@playwright/test').Page} page
+ * @param {OffsetValues} offset
+ */
+async function setEndOffset(page, offset) {
+ const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
+ await setTimeConductorOffset(page, offset, endOffsetButton);
+}
+
+// eslint-disable-next-line no-undef
+module.exports = {
+ createDomainObjectWithDefaults,
+ expandTreePaneItemByName,
+ createPlanFromJSON,
+ openObjectTreeContextMenu,
+ getHashUrlToDomainObject,
+ getFocusedObjectUuid,
+ setFixedTimeMode,
+ setRealTimeMode,
+ setStartOffset,
+ setEndOffset
+};
diff --git a/e2e/baseFixtures.js b/e2e/baseFixtures.js
new file mode 100644
index 000000000..298d96522
--- /dev/null
+++ b/e2e/baseFixtures.js
@@ -0,0 +1,174 @@
+/* eslint-disable no-undef */
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 file is dedicated to extending the base functionality of the `@playwright/test` framework.
+ * The functions in this file should be viewed as temporary or a shim to be removed as the RFEs in
+ * the Playwright GitHub repo are implemented. Functions which serve those RFEs are marked with corresponding
+ * GitHub issues.
+ */
+
+const base = require('@playwright/test');
+const { expect } = base;
+const fs = require('fs');
+const path = require('path');
+const { v4: uuid } = require('uuid');
+const sinon = require('sinon');
+
+/**
+ * Takes a `ConsoleMessage` and returns a formatted string. Used to enable console log error detection.
+ * @see {@link https://github.com/microsoft/playwright/discussions/11690 Github Discussion}
+ * @private
+ * @param {import('@playwright/test').ConsoleMessage} msg
+ * @returns {String} formatted string with message type, text, url, and line and column numbers
+ */
+function _consoleMessageToString(msg) {
+ const { url, lineNumber, columnNumber } = msg.location();
+
+ return `[${msg.type()}] ${msg.text()} at (${url} ${lineNumber}:${columnNumber})`;
+}
+
+/**
+ * Wait for all animations within the given element and subtrees to finish. Useful when
+ * verifying that css transitions have completed.
+ * @see {@link https://github.com/microsoft/playwright/issues/15660 Github RFE}
+ * @param {import('@playwright/test').Locator} locator
+ * @return {Promise<Animation[]>}
+ */
+function waitForAnimations(locator) {
+ return locator
+ .evaluate((element) =>
+ Promise.all(
+ element
+ .getAnimations({ subtree: true })
+ .map((animation) => animation.finished)));
+}
+
+/**
+ * This is part of our codecoverage shim.
+ * @see {@link https://github.com/mxschmitt/playwright-test-coverage Github Example Project}
+ * @constant {string}
+ */
+const istanbulCLIOutput = path.join(process.cwd(), '.nyc_output');
+
+exports.test = base.test.extend({
+ /**
+ * This allows the test to manipulate the browser clock. This is useful for Visual and Snapshot tests which need
+ * the Time Indicator Clock to be in a specific state.
+ * Usage:
+ * ```
+ * test.use({
+ * clockOptions: {
+ * now: 0,
+ * shouldAdvanceTime: true
+ * ```
+ * If clockOptions are provided, will override the default clock with fake timers provided by SinonJS.
+ *
+ * Default: `undefined`
+ *
+ * @see {@link https://github.com/microsoft/playwright/issues/6347 Github RFE}
+ * @see {@link https://github.com/sinonjs/fake-timers/#var-clock--faketimersinstallconfig SinonJS FakeTimers Config}
+ */
+ clockOptions: [undefined, { option: true }],
+ overrideClock: [async ({ context, clockOptions }, use) => {
+ if (clockOptions !== undefined) {
+ await context.addInitScript({
+ path: path.join(__dirname, '../', './node_modules/sinon/pkg/sinon.js')
+ });
+ await context.addInitScript((options) => {
+ window.__clock = sinon.useFakeTimers(options);
+ }, clockOptions);
+ }
+
+ await use(context);
+ }, {
+ auto: true,
+ scope: 'test'
+ }],
+ /**
+ * Extends the base context class to add codecoverage shim.
+ * @see {@link https://github.com/mxschmitt/playwright-test-coverage Github Project}
+ */
+ context: async ({ context }, use) => {
+ await context.addInitScript(() =>
+ window.addEventListener('beforeunload', () =>
+ (window).collectIstanbulCoverage(JSON.stringify((window).__coverage__))
+ )
+ );
+ await fs.promises.mkdir(istanbulCLIOutput, { recursive: true });
+ await context.exposeFunction('collectIstanbulCoverage', (coverageJSON) => {
+ if (coverageJSON) {
+ fs.writeFileSync(path.join(istanbulCLIOutput, `playwright_coverage_${uuid()}.json`), coverageJSON);
+ }
+ });
+
+ await use(context);
+ for (const page of context.pages()) {
+ await page.evaluate(() => (window).collectIstanbulCoverage(JSON.stringify((window).__coverage__)));
+ }
+ },
+ /**
+ * If true, will assert against any console.error calls that occur during the test. Assertions occur
+ * during test teardown (after the test has completed).
+ *
+ * Default: `true`
+ */
+ failOnConsoleError: [true, { option: true }],
+ /**
+ * Extends the base page class to enable console log error detection.
+ * @see {@link https://github.com/microsoft/playwright/discussions/11690 Github Discussion}
+ */
+ page: async ({ page, failOnConsoleError }, use) => {
+ // Capture any console errors during test execution
+ const messages = [];
+ page.on('console', (msg) => messages.push(msg));
+
+ await use(page);
+
+ // Assert against console errors during teardown
+ if (failOnConsoleError) {
+ messages.forEach(
+ msg => expect.soft(msg.type(), `Console error detected: ${_consoleMessageToString(msg)}`).not.toEqual('error')
+ );
+ }
+ },
+ /**
+ * Extends the base browser class to enable CDP connection definition in playwright.config.js. Once
+ * that RFE is implemented, this function can be removed.
+ * @see {@link https://github.com/microsoft/playwright/issues/8379 Github RFE}
+ */
+ browser: async ({ playwright, browser }, use, workerInfo) => {
+ // Use browserless if configured
+ if (workerInfo.project.name.match(/browserless/)) {
+ const vBrowser = await playwright.chromium.connectOverCDP({
+ endpointURL: 'ws://localhost:3003'
+ });
+ await use(vBrowser);
+ } else {
+ // Use Local Browser for testing.
+ await use(browser);
+ }
+ }
+});
+exports.expect = expect;
+exports.waitForAnimations = waitForAnimations;
diff --git a/e2e/helper/addInitExampleFaultProvider.js b/e2e/helper/addInitExampleFaultProvider.js
new file mode 100644
index 000000000..1bf7b0209
--- /dev/null
+++ b/e2e/helper/addInitExampleFaultProvider.js
@@ -0,0 +1,28 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 should be used to install the Example Fault Provider, this will also install the FaultManagementPlugin (neither of which are installed by default).
+
+document.addEventListener('DOMContentLoaded', () => {
+ const openmct = window.openmct;
+ openmct.install(openmct.plugins.example.ExampleFaultSource());
+});
diff --git a/e2e/helper/addInitExampleFaultProviderStatic.js b/e2e/helper/addInitExampleFaultProviderStatic.js
new file mode 100644
index 000000000..fc7ec5397
--- /dev/null
+++ b/e2e/helper/addInitExampleFaultProviderStatic.js
@@ -0,0 +1,30 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 should be used to install the Example Fault Provider, this will also install the FaultManagementPlugin (neither of which are installed by default).
+
+document.addEventListener('DOMContentLoaded', () => {
+ const openmct = window.openmct;
+ const staticFaults = true;
+
+ openmct.install(openmct.plugins.example.ExampleFaultSource(staticFaults));
+});
diff --git a/e2e/helper/addInitFaultManagementPlugin.js b/e2e/helper/addInitFaultManagementPlugin.js
new file mode 100644
index 000000000..4f1c396fa
--- /dev/null
+++ b/e2e/helper/addInitFaultManagementPlugin.js
@@ -0,0 +1,28 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 should be used to install the Example Fault Provider, this will also install the FaultManagementPlugin (neither of which are installed by default).
+
+document.addEventListener('DOMContentLoaded', () => {
+ const openmct = window.openmct;
+ openmct.install(openmct.plugins.FaultManagement());
+});
diff --git a/e2e/helper/addInitRestrictedNotebook.js b/e2e/helper/addInitRestrictedNotebook.js
new file mode 100644
index 000000000..71917895a
--- /dev/null
+++ b/e2e/helper/addInitRestrictedNotebook.js
@@ -0,0 +1,30 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 should be used to install the non-default Restricted Notebook plugin since it is not installed by default.
+// e.g.
+// await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') });
+
+document.addEventListener('DOMContentLoaded', () => {
+ const openmct = window.openmct;
+ openmct.install(openmct.plugins.RestrictedNotebook('CUSTOM_NAME'));
+});
diff --git a/e2e/tests/persistence/addNoneditableObject.js b/e2e/helper/addNoneditableObject.js
index 55da25358..55da25358 100644
--- a/e2e/tests/persistence/addNoneditableObject.js
+++ b/e2e/helper/addNoneditableObject.js
diff --git a/e2e/helper/faultUtils.js b/e2e/helper/faultUtils.js
new file mode 100644
index 000000000..819c4b42b
--- /dev/null
+++ b/e2e/helper/faultUtils.js
@@ -0,0 +1,277 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 path = require('path');
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function navigateToFaultManagementWithExample(page) {
+ // eslint-disable-next-line no-undef
+ await page.addInitScript({ path: path.join(__dirname, './', 'addInitExampleFaultProvider.js') });
+
+ await navigateToFaultItemInTree(page);
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function navigateToFaultManagementWithStaticExample(page) {
+ // eslint-disable-next-line no-undef
+ await page.addInitScript({ path: path.join(__dirname, './', 'addInitExampleFaultProviderStatic.js') });
+
+ await navigateToFaultItemInTree(page);
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function navigateToFaultManagementWithoutExample(page) {
+ // eslint-disable-next-line no-undef
+ await page.addInitScript({ path: path.join(__dirname, './', 'addInitFaultManagementPlugin.js') });
+
+ await navigateToFaultItemInTree(page);
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function navigateToFaultItemInTree(page) {
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ // Click text=Fault Management
+ await page.click('text=Fault Management'); // this verifies the plugin has been added
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function acknowledgeFault(page, rowNumber) {
+ await openFaultRowMenu(page, rowNumber);
+ await page.locator('.c-menu >> text="Acknowledge"').click();
+ // Click [aria-label="Save"]
+ await page.locator('[aria-label="Save"]').click();
+
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function shelveMultipleFaults(page, ...nums) {
+ const selectRows = nums.map((num) => {
+ return selectFaultItem(page, num);
+ });
+ await Promise.all(selectRows);
+
+ await page.locator('button:has-text("Shelve")').click();
+ await page.locator('[aria-label="Save"]').click();
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function acknowledgeMultipleFaults(page, ...nums) {
+ const selectRows = nums.map((num) => {
+ return selectFaultItem(page, num);
+ });
+ await Promise.all(selectRows);
+
+ await page.locator('button:has-text("Acknowledge")').click();
+ await page.locator('[aria-label="Save"]').click();
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function shelveFault(page, rowNumber) {
+ await openFaultRowMenu(page, rowNumber);
+ await page.locator('.c-menu >> text="Shelve"').click();
+ // Click [aria-label="Save"]
+ await page.locator('[aria-label="Save"]').click();
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function changeViewTo(page, view) {
+ await page.locator('.c-fault-mgmt__search-row select').first().selectOption(view);
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function sortFaultsBy(page, sort) {
+ await page.locator('.c-fault-mgmt__list-header-sortButton select').selectOption(sort);
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function enterSearchTerm(page, term) {
+ await page.locator('.c-fault-mgmt-search [aria-label="Search Input"]').fill(term);
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function clearSearch(page) {
+ await enterSearchTerm(page, '');
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function selectFaultItem(page, rowNumber) {
+ // eslint-disable-next-line playwright/no-force-option
+ await page.check(`.c-fault-mgmt-item > input >> nth=${rowNumber - 1}`, { force: true }); // this will not work without force true, saw this may be a pw bug
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function getHighestSeverity(page) {
+ const criticalCount = await page.locator('[title=CRITICAL]').count();
+ const warningCount = await page.locator('[title=WARNING]').count();
+
+ if (criticalCount > 0) {
+ return 'CRITICAL';
+ } else if (warningCount > 0) {
+ return 'WARNING';
+ }
+
+ return 'WATCH';
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function getLowestSeverity(page) {
+ const warningCount = await page.locator('[title=WARNING]').count();
+ const watchCount = await page.locator('[title=WATCH]').count();
+
+ if (watchCount > 0) {
+ return 'WATCH';
+ } else if (warningCount > 0) {
+ return 'WARNING';
+ }
+
+ return 'CRITICAL';
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function getFaultResultCount(page) {
+ const count = await page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').count();
+
+ return count;
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+function getFault(page, rowNumber) {
+ const fault = page.locator(`.c-faults-list-view-item-body > .c-fault-mgmt__list >> nth=${rowNumber - 1}`);
+
+ return fault;
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+function getFaultByName(page, name) {
+ const fault = page.locator(`.c-fault-mgmt__list-faultname:has-text("${name}")`);
+
+ return fault;
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function getFaultName(page, rowNumber) {
+ const faultName = await page.locator(`.c-fault-mgmt__list-faultname >> nth=${rowNumber - 1}`).textContent();
+
+ return faultName;
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function getFaultSeverity(page, rowNumber) {
+ const faultSeverity = await page.locator(`.c-faults-list-view-item-body .c-fault-mgmt__list-severity >> nth=${rowNumber - 1}`).getAttribute('title');
+
+ return faultSeverity;
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function getFaultNamespace(page, rowNumber) {
+ const faultNamespace = await page.locator(`.c-fault-mgmt__list-path >> nth=${rowNumber - 1}`).textContent();
+
+ return faultNamespace;
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function getFaultTriggerTime(page, rowNumber) {
+ const faultTriggerTime = await page.locator(`.c-fault-mgmt__list-trigTime >> nth=${rowNumber - 1} >> .c-fault-mgmt-item__value`).textContent();
+
+ return faultTriggerTime.toString().trim();
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function openFaultRowMenu(page, rowNumber) {
+ // select
+ await page.locator(`.c-fault-mgmt-item > .c-fault-mgmt__list-action-button >> nth=${rowNumber - 1}`).click();
+
+}
+
+// eslint-disable-next-line no-undef
+module.exports = {
+ navigateToFaultManagementWithExample,
+ navigateToFaultManagementWithStaticExample,
+ navigateToFaultManagementWithoutExample,
+ navigateToFaultItemInTree,
+ acknowledgeFault,
+ shelveMultipleFaults,
+ acknowledgeMultipleFaults,
+ shelveFault,
+ changeViewTo,
+ sortFaultsBy,
+ enterSearchTerm,
+ clearSearch,
+ selectFaultItem,
+ getHighestSeverity,
+ getLowestSeverity,
+ getFaultResultCount,
+ getFault,
+ getFaultByName,
+ getFaultName,
+ getFaultSeverity,
+ getFaultNamespace,
+ getFaultTriggerTime,
+ openFaultRowMenu
+};
diff --git a/e2e/helper/notebookUtils.js b/e2e/helper/notebookUtils.js
new file mode 100644
index 000000000..5fdd97363
--- /dev/null
+++ b/e2e/helper/notebookUtils.js
@@ -0,0 +1,65 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function enterTextEntry(page, text) {
+ // Click .c-notebook__drag-area
+ await page.locator(NOTEBOOK_DROP_AREA).click();
+
+ // enter text
+ await page.locator('div.c-ne__text').click();
+ await page.locator('div.c-ne__text').fill(text);
+ await page.locator('div.c-ne__text').press('Enter');
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function dragAndDropEmbed(page, myItemsFolderName) {
+ // Click button:has-text("Create")
+ await page.locator('button:has-text("Create")').click();
+ // Click li:has-text("Sine Wave Generator")
+ await page.locator('li:has-text("Sine Wave Generator")').click();
+ // Click form[name="mctForm"] >> text=My Items
+ await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
+ // Click text=OK
+ await page.locator('text=OK').click();
+ // Click text=Open MCT My Items >> span >> nth=3
+ await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
+ // Click text=Unnamed CUSTOM_NAME
+ await Promise.all([
+ page.waitForNavigation(),
+ page.locator('text=Unnamed CUSTOM_NAME').click()
+ ]);
+
+ await page.dragAndDrop('text=UNNAMED SINE WAVE GENERATOR', NOTEBOOK_DROP_AREA);
+}
+
+// eslint-disable-next-line no-undef
+module.exports = {
+ enterTextEntry,
+ dragAndDropEmbed
+};
diff --git a/e2e/helper/useSnowTheme.js b/e2e/helper/useSnowTheme.js
new file mode 100644
index 000000000..565656868
--- /dev/null
+++ b/e2e/helper/useSnowTheme.js
@@ -0,0 +1,30 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 should be used to install the Snow theme for Open MCT. Espresso is the default
+// e.g.
+// await page.addInitScript({ path: path.join(__dirname, 'useSnowTheme.js') });
+
+document.addEventListener('DOMContentLoaded', () => {
+ const openmct = window.openmct;
+ openmct.install(openmct.plugins.Snow());
+});
diff --git a/e2e/playwright-ci.config.js b/e2e/playwright-ci.config.js
index 8f241a8cd..aeff66882 100644
--- a/e2e/playwright-ci.config.js
+++ b/e2e/playwright-ci.config.js
@@ -2,37 +2,44 @@
// playwright.config.js
// @ts-check
+// eslint-disable-next-line no-unused-vars
const { devices } = require('@playwright/test');
+const MAX_FAILURES = 5;
+const NUM_WORKERS = 2;
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
- retries: 1,
+ retries: 2, //Retries 2 times for a total of 3 runs. When running sharded and with maxFailures = 5, this should ensure that flake is managed without failing the full suite
testDir: 'tests',
+ testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
timeout: 60 * 1000,
webServer: {
- command: 'npm run start',
- port: 8080,
+ command: 'cross-env NODE_ENV=test npm run start',
+ url: 'http://localhost:8080/#',
timeout: 200 * 1000,
- reuseExistingServer: !process.env.CI
+ reuseExistingServer: false
},
- workers: 2, //Limit to 2 for CircleCI Agent
+ maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste
+ workers: NUM_WORKERS, //Limit to 2 for CircleCI Agent
use: {
baseURL: 'http://localhost:8080/',
headless: true,
ignoreHTTPSErrors: true,
- screenshot: 'on',
- trace: 'on',
- video: 'on'
+ screenshot: 'only-on-failure',
+ trace: 'on-first-retry',
+ video: 'off'
},
projects: [
{
name: 'chrome',
+ testMatch: '**/*.e2e.spec.js', // only run e2e tests
use: {
browserName: 'chromium'
}
},
{
name: 'MMOC',
+ testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
@@ -41,19 +48,32 @@ const config = {
height: 1440
}
}
- }
- /*{
- name: 'ipad',
+ },
+ {
+ name: 'firefox',
+ testMatch: '**/*.e2e.spec.js', // only run e2e tests
+ grepInvert: /@snapshot/,
use: {
- browserName: 'webkit',
- ...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
+ browserName: 'firefox'
}
- }*/
+ },
+ {
+ name: 'chrome-beta', //Only Chrome Beta is available on ubuntu -- not chrome canary
+ testMatch: '**/*.e2e.spec.js', // only run e2e tests
+ grepInvert: /@snapshot/,
+ use: {
+ browserName: 'chromium',
+ channel: 'chrome-beta'
+ }
+ }
],
reporter: [
['list'],
+ ['html', {
+ open: 'never',
+ outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
+ }],
['junit', { outputFile: 'test-results/results.xml' }],
- ['allure-playwright'],
['github']
]
};
diff --git a/e2e/playwright-local.config.js b/e2e/playwright-local.config.js
index f5b2bdb5c..87365fee2 100644
--- a/e2e/playwright-local.config.js
+++ b/e2e/playwright-local.config.js
@@ -2,18 +2,23 @@
// playwright.config.js
// @ts-check
+// eslint-disable-next-line no-unused-vars
const { devices } = require('@playwright/test');
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 0,
testDir: 'tests',
+ testIgnore: '**/*.perf.spec.js',
timeout: 30 * 1000,
webServer: {
+ env: {
+ NODE_ENV: 'test'
+ },
command: 'npm run start',
- port: 8080,
+ url: 'http://localhost:8080/#',
timeout: 120 * 1000,
- reuseExistingServer: !process.env.CI
+ reuseExistingServer: true
},
workers: 1,
use: {
@@ -21,9 +26,9 @@ const config = {
baseURL: 'http://localhost:8080/',
headless: false,
ignoreHTTPSErrors: true,
- screenshot: 'on',
- trace: 'on',
- video: 'on'
+ screenshot: 'only-on-failure',
+ trace: 'retain-on-failure',
+ video: 'off'
},
projects: [
{
@@ -34,6 +39,7 @@ const config = {
},
{
name: 'MMOC',
+ testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
@@ -42,18 +48,59 @@ const config = {
height: 1440
}
}
- }
- /*{
+ },
+ {
+ name: 'safari',
+ testMatch: '**/*.e2e.spec.js', // only run e2e tests
+ grep: /@ipad/, // only run ipad tests due to this bug https://github.com/microsoft/playwright/issues/8340
+ grepInvert: /@snapshot/,
+ use: {
+ browserName: 'webkit'
+ }
+ },
+ {
+ name: 'firefox',
+ testMatch: '**/*.e2e.spec.js', // only run e2e tests
+ grepInvert: /@snapshot/,
+ use: {
+ browserName: 'firefox'
+ }
+ },
+ {
+ name: 'canary',
+ testMatch: '**/*.e2e.spec.js', // only run e2e tests
+ grepInvert: /@snapshot/,
+ use: {
+ browserName: 'chromium',
+ channel: 'chrome-canary' //Note this is not available in ubuntu/CircleCI
+ }
+ },
+ {
+ name: 'chrome-beta',
+ testMatch: '**/*.e2e.spec.js', // only run e2e tests
+ grepInvert: /@snapshot/,
+ use: {
+ browserName: 'chromium',
+ channel: 'chrome-beta'
+ }
+ },
+ {
name: 'ipad',
+ testMatch: '**/*.e2e.spec.js', // only run e2e tests
+ grep: /@ipad/,
+ grepInvert: /@snapshot/,
use: {
browserName: 'webkit',
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
- }*/
+ }
],
reporter: [
['list'],
- ['allure-playwright']
+ ['html', {
+ open: 'on-failure',
+ outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
+ }]
]
};
diff --git a/e2e/playwright-performance.config.js b/e2e/playwright-performance.config.js
new file mode 100644
index 000000000..de79304f1
--- /dev/null
+++ b/e2e/playwright-performance.config.js
@@ -0,0 +1,43 @@
+/* eslint-disable no-undef */
+// playwright.config.js
+// @ts-check
+
+const CI = process.env.CI === 'true';
+
+/** @type {import('@playwright/test').PlaywrightTestConfig} */
+const config = {
+ retries: 1, //Only for debugging purposes because trace is enabled only on first retry
+ testDir: 'tests/performance/',
+ timeout: 60 * 1000,
+ workers: 1, //Only run in serial with 1 worker
+ webServer: {
+ command: 'cross-env NODE_ENV=test npm run start',
+ url: 'http://localhost:8080/#',
+ timeout: 200 * 1000,
+ reuseExistingServer: !CI
+ },
+ use: {
+ browserName: "chromium",
+ baseURL: 'http://localhost:8080/',
+ headless: CI, //Only if running locally
+ ignoreHTTPSErrors: true,
+ screenshot: 'off',
+ trace: 'on-first-retry',
+ video: 'off'
+ },
+ projects: [
+ {
+ name: 'chrome',
+ use: {
+ browserName: 'chromium'
+ }
+ }
+ ],
+ reporter: [
+ ['list'],
+ ['junit', { outputFile: 'test-results/results.xml' }],
+ ['json', { outputFile: 'test-results/results.json' }]
+ ]
+};
+
+module.exports = config;
diff --git a/e2e/playwright-visual.config.js b/e2e/playwright-visual.config.js
index 7f6df513f..1123de808 100644
--- a/e2e/playwright-visual.config.js
+++ b/e2e/playwright-visual.config.js
@@ -2,31 +2,49 @@
// playwright.config.js
// @ts-check
-/** @type {import('@playwright/test').PlaywrightTestConfig} */
+/** @type {import('@playwright/test').PlaywrightTestConfig<{ theme: string }>} */
const config = {
- retries: 0,
- testDir: 'tests',
- timeout: 90 * 1000,
- workers: 1,
+ retries: 1, // visual tests should never retry due to snapshot comparison errors. Leaving as a shim
+ testDir: 'tests/visual',
+ testMatch: '**/*.visual.spec.js', // only run visual tests
+ timeout: 60 * 1000,
+ workers: 1, //Lower stress on Circle CI Agent for Visual tests https://github.com/percy/cli/discussions/1067
webServer: {
- command: 'npm run start',
- port: 8080,
+ command: 'cross-env NODE_ENV=test npm run start',
+ url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: !process.env.CI
},
use: {
- browserName: "chromium",
baseURL: 'http://localhost:8080/',
- headless: true,
+ headless: true, // this needs to remain headless to avoid visual changes due to GPU rendering in headed browsers
ignoreHTTPSErrors: true,
- screenshot: 'on',
- trace: 'off',
- video: 'on'
+ screenshot: 'only-on-failure',
+ trace: 'on-first-retry',
+ video: 'off'
},
+ projects: [
+ {
+ name: 'chrome',
+ use: {
+ browserName: 'chromium'
+ }
+ },
+ {
+ name: 'chrome-snow-theme',
+ use: {
+ browserName: 'chromium',
+ theme: 'snow'
+ }
+ }
+ ],
reporter: [
['list'],
['junit', { outputFile: 'test-results/results.xml' }],
- ['allure-playwright']
+ ['html', {
+ open: 'on-failure',
+ outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
+ }]
]
};
diff --git a/e2e/pluginFixtures.js b/e2e/pluginFixtures.js
new file mode 100644
index 000000000..a3250054d
--- /dev/null
+++ b/e2e/pluginFixtures.js
@@ -0,0 +1,161 @@
+/* eslint-disable no-undef */
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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.
+ *****************************************************************************/
+
+/**
+ * The file contains custom fixtures which extend the base functionality of the Playwright fixtures
+ * and appActions. These fixtures should be generalized across all plugins.
+ */
+
+const { test, expect } = require('./baseFixtures');
+// const { createDomainObjectWithDefaults } = require('./appActions');
+const path = require('path');
+
+/**
+ * @typedef {Object} ObjectCreateOptions
+ * @property {string} type
+ * @property {string} name
+ */
+
+/**
+ * **NOTE: This feature is a work-in-progress and should not currently be used.**
+ *
+ * Used to create a new domain object as a part of getOrCreateDomainObject.
+ * @type {Map<string, string>}
+ */
+// const createdObjects = new Map();
+
+/**
+ * **NOTE: This feature is a work-in-progress and should not currently be used.**
+ *
+ * This action will create a domain object for the test to reference and return the uuid. If an object
+ * of a given name already exists, it will return the uuid of that object to the test instead of creating
+ * a new file. The intent is to move object creation out of test suites which are not explicitly worried
+ * about object creation, while providing a consistent interface to retrieving objects in a persistentContext.
+ * @param {import('@playwright/test').Page} page
+ * @param {ObjectCreateOptions} options
+ * @returns {Promise<string>} uuid of the domain object
+ */
+// async function getOrCreateDomainObject(page, options) {
+// const { type, name } = options;
+// const objectName = name ? `${type}:${name}` : type;
+
+// if (createdObjects.has(objectName)) {
+// return createdObjects.get(objectName);
+// }
+
+// await createDomainObjectWithDefaults(page, type, name);
+
+// // Once object is created, get the uuid from the url
+// const uuid = await page.evaluate(() => {
+// return window.location.href.match(/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/)[0];
+// });
+
+// createdObjects.set(objectName, uuid);
+
+// return uuid;
+// }
+
+/**
+ * **NOTE: This feature is a work-in-progress and should not currently be used.**
+ *
+ * If provided, these options will be used to get or create the desired domain object before
+ * any tests or test hooks have run.
+ * The `uuid` of the `domainObject` will then be available to use within the scoped tests.
+ *
+ * ### Example:
+ * ```js
+ * test.describe("My test suite", () => {
+ * test.use({ objectCreateOptions: { type: "Telemetry Table", name: "My Telemetry Table" }});
+ * test("'My Telemetry Table' is created and provides a uuid", async ({ page, domainObject }) => {
+ * const { uuid } = domainObject;
+ * expect(uuid).toBeDefined();
+ * }))
+ * });
+ * ```
+ * @type {ObjectCreateOptions}
+ */
+// const objectCreateOptions = null;
+
+/**
+ * The default theme for VIPER and Open MCT is the 'espresso' theme. Overriding this value with 'snow' in our playwright config.js
+ * will override the default theme by injecting the 'snow' theme on launch.
+ *
+ * ### Example:
+ * ```js
+ * projects: [
+ * {
+ * name: 'chrome-snow-theme',
+ * use: {
+ * browserName: 'chromium',
+ * theme: 'snow'
+ * ```
+ * @type {'snow' | 'espresso'}
+ */
+const theme = 'espresso';
+
+/**
+ * The name of the "My Items" folder in the domain object tree.
+ *
+ * Default: `"My Items"`
+ *
+ * @type {string}
+ */
+const myItemsFolderName = "My Items";
+
+exports.test = test.extend({
+ // This should follow in the Project's configuration. Can be set to 'snow' in playwright config.js
+ theme: [theme, { option: true }],
+ // eslint-disable-next-line no-shadow
+ page: async ({ page, theme }, use) => {
+ // eslint-disable-next-line playwright/no-conditional-in-test
+ if (theme === 'snow') {
+ //inject snow theme
+ await page.addInitScript({ path: path.join(__dirname, './helper', './useSnowTheme.js') });
+ }
+
+ await use(page);
+ },
+ myItemsFolderName: [myItemsFolderName, { option: true }],
+ // eslint-disable-next-line no-shadow
+ openmctConfig: async ({ myItemsFolderName }, use) => {
+ await use({ myItemsFolderName });
+ }
+ // objectCreateOptions: [objectCreateOptions, {option: true}],
+ // eslint-disable-next-line no-shadow
+ // domainObject: [async ({ page, objectCreateOptions }, use) => {
+ // // FIXME: This is a false-positive caused by a bug in the eslint-plugin-playwright rule.
+ // // eslint-disable-next-line playwright/no-conditional-in-test
+ // if (objectCreateOptions === null) {
+ // await use(page);
+
+ // return;
+ // }
+
+ // //Go to baseURL
+ // await page.goto('./', { waitUntil: 'networkidle' });
+
+ // const uuid = await getOrCreateDomainObject(page, objectCreateOptions);
+ // await use({ uuid });
+ // }, { auto: true }]
+});
+exports.expect = expect;
diff --git a/e2e/test-data/PerformanceDisplayLayout.json b/e2e/test-data/PerformanceDisplayLayout.json
new file mode 100644
index 000000000..de81d7b4c
--- /dev/null
+++ b/e2e/test-data/PerformanceDisplayLayout.json
@@ -0,0 +1 @@
+{"openmct":{"b3cee102-86dd-4c0a-8eec-4d5d276f8691":{"identifier":{"key":"b3cee102-86dd-4c0a-8eec-4d5d276f8691","namespace":""},"name":"Performance Display Layout","type":"layout","composition":[{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""}],"configuration":{"items":[{"width":32,"height":18,"x":12,"y":9,"identifier":{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"23ca351d-a67d-46aa-a762-290eb742d2f1"}],"layoutGrid":[10,10]},"modified":1654299875432,"location":"mine","persisted":1654299878751},"9666e7b4-be0c-47a5-94b8-99accad7155e":{"identifier":{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""},"name":"Performance Example Imagery","type":"example.imagery","configuration":{"imageLocation":"","imageLoadDelayInMilliSeconds":20000,"imageSamples":[],"layers":[{"source":"dist/imagery/example-imagery-layer-16x9.png","name":"16:9","visible":false},{"source":"dist/imagery/example-imagery-layer-safe.png","name":"Safe","visible":false},{"source":"dist/imagery/example-imagery-layer-scale.png","name":"Scale","visible":false}]},"telemetry":{"values":[{"name":"Name","key":"name"},{"name":"Time","key":"utc","format":"utc","hints":{"domain":2}},{"name":"Local Time","key":"local","format":"local-format","hints":{"domain":1}},{"name":"Image","key":"url","format":"image","hints":{"image":1},"layers":[{"source":"dist/imagery/example-imagery-layer-16x9.png","name":"16:9"},{"source":"dist/imagery/example-imagery-layer-safe.png","name":"Safe"},{"source":"dist/imagery/example-imagery-layer-scale.png","name":"Scale"}]},{"name":"Image Download Name","key":"imageDownloadName","format":"imageDownloadName","hints":{"imageDownloadName":1}}]},"modified":1654299840077,"location":"b3cee102-86dd-4c0a-8eec-4d5d276f8691","persisted":1654299840078}},"rootId":"b3cee102-86dd-4c0a-8eec-4d5d276f8691"} \ No newline at end of file
diff --git a/e2e/test-data/PerformanceNotebook.json b/e2e/test-data/PerformanceNotebook.json
new file mode 100644
index 000000000..ae0843187
--- /dev/null
+++ b/e2e/test-data/PerformanceNotebook.json
@@ -0,0 +1 @@
+{"openmct":{"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d":{"identifier":{"key":"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d","namespace":""},"name":"Performance Notebook","type":"notebook","configuration":{"defaultSort":"oldest","entries":{"3e31c412-33ba-4757-8ade-e9821f6ba321":{"8c8f6035-631c-45af-8c24-786c60295335":[{"id":"entry-1652815305457","createdOn":1652815305457,"createdBy":"","text":"Existing Entry 1","embeds":[]},{"id":"entry-1652815313465","createdOn":1652815313465,"createdBy":"","text":"Existing Entry 2","embeds":[]},{"id":"entry-1652815399955","createdOn":1652815399955,"createdBy":"","text":"Existing Entry 3","embeds":[]}]}},"imageMigrationVer":"v1","pageTitle":"Page","sections":[{"id":"3e31c412-33ba-4757-8ade-e9821f6ba321","isDefault":false,"isSelected":false,"name":"Section1","pages":[{"id":"8c8f6035-631c-45af-8c24-786c60295335","isDefault":false,"isSelected":false,"name":"Page1","pageTitle":"Page"},{"id":"36555942-c9aa-439c-bbdb-0aaf50db50f5","isDefault":false,"isSelected":false,"name":"Page2","pageTitle":"Page"}],"sectionTitle":"Section"},{"id":"dab0bd1d-2c5a-405c-987f-107123d6189a","isDefault":false,"isSelected":true,"name":"Section2","pages":[{"id":"f625a86a-cb99-4898-8082-80543c8de534","isDefault":false,"isSelected":false,"name":"Page1","pageTitle":"Page"},{"id":"e77ef810-f785-42a7-942e-07e999b79c59","isDefault":false,"isSelected":true,"name":"Page2","pageTitle":"Page"}],"sectionTitle":"Section"}],"sectionTitle":"Section","type":"General","showTime":"0"},"modified":1652815915219,"location":"mine","persisted":1652815915222}},"rootId":"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d"} \ No newline at end of file
diff --git a/e2e/test-data/VisualTestData_storage.json b/e2e/test-data/VisualTestData_storage.json
new file mode 100644
index 000000000..2ee2eaf6c
--- /dev/null
+++ b/e2e/test-data/VisualTestData_storage.json
@@ -0,0 +1,22 @@
+{
+ "cookies": [],
+ "origins": [
+ {
+ "origin": "http://localhost:8080",
+ "localStorage": [
+ {
+ "name": "tcHistory",
+ "value": "{\"utc\":[{\"start\":1658617611983,\"end\":1658619411983}]}"
+ },
+ {
+ "name": "mct",
+ "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"7fa5749b-8969-494c-9d85-c272516d333c\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1658619412848,\"modified\":1658619412848},\"7fa5749b-8969-494c-9d85-c272516d333c\":{\"identifier\":{\"key\":\"7fa5749b-8969-494c-9d85-c272516d333c\",\"namespace\":\"\"},\"name\":\"Unnamed Overlay Plot\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\",\"namespace\":\"\"}}]},\"modified\":1658619413566,\"location\":\"mine\",\"persisted\":1658619413567},\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\":{\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"identifier\":{\"key\":\"67cbb9fc-af46-4148-b9e5-aea11179ae4b\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\"},\"modified\":1658619413552,\"location\":\"7fa5749b-8969-494c-9d85-c272516d333c\",\"persisted\":1658619413552}}"
+ },
+ {
+ "name": "mct-tree-expanded",
+ "value": "[\"/browse/mine\"]"
+ }
+ ]
+ }
+ ]
+} \ No newline at end of file
diff --git a/e2e/test-data/recycled_local_storage.json b/e2e/test-data/recycled_local_storage.json
new file mode 100644
index 000000000..9f816a227
--- /dev/null
+++ b/e2e/test-data/recycled_local_storage.json
@@ -0,0 +1,22 @@
+{
+ "cookies": [],
+ "origins": [
+ {
+ "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}}"
+ },
+ {
+ "name": "mct-tree-expanded",
+ "value": "[]"
+ }
+ ]
+ }
+ ]
+} \ No newline at end of file
diff --git a/e2e/tests/example/generator/SinewaveLimitProvider.e2e.spec.js b/e2e/tests/example/generator/SinewaveLimitProvider.e2e.spec.js
deleted file mode 100644
index 6bf747aac..000000000
--- a/e2e/tests/example/generator/SinewaveLimitProvider.e2e.spec.js
+++ /dev/null
@@ -1,166 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2022, 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 verify the basic operations surrounding conditionSets.
-*/
-
-const { test, expect } = require('@playwright/test');
-
-test.describe('Sine Wave Generator', () => {
- test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page }) => {
- //Go to baseURL
- await page.goto('/', { waitUntil: 'networkidle' });
-
- //Click the Create button
- await page.click('button:has-text("Create")');
-
- // Click Sine Wave Generator
- await page.click('text=Sine Wave Generator');
-
- // Verify that the each required field has required indicator
- // Title
- await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(['c-form-row__state-indicator req']);
-
- // Verify that the Notes row does not have a required indicator
- await expect(page.locator('.c-form__section div:nth-child(3) .form-row .c-form-row__state-indicator')).not.toContain('.req');
-
- // Period
- await expect(page.locator('.c-form__section div:nth-child(4) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req']);
-
- // Amplitude
- await expect(page.locator('.c-form__section div:nth-child(5) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req']);
-
- // Offset
- await expect(page.locator('.c-form__section div:nth-child(6) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req']);
-
- // Data Rate
- await expect(page.locator('.c-form__section div:nth-child(7) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req']);
-
- // Phase
- await expect(page.locator('.c-form__section div:nth-child(8) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req']);
-
- // Randomness
- await expect(page.locator('.c-form__section div:nth-child(9) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req']);
-
- // Verify that by removing value from required text field shows invalid indicator
- await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('');
- await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(['c-form-row__state-indicator req invalid']);
-
- // Verify that by adding value to empty required text field changes invalid to valid indicator
- await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('non empty');
- await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(['c-form-row__state-indicator req valid']);
-
- // Verify that by removing value from required number field shows invalid indicator
- await page.locator('.field.control.l-input-sm input').first().fill('');
- await expect(page.locator('.c-form__section div:nth-child(4) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req invalid']);
-
- // Verify that by adding value to empty required number field changes invalid to valid indicator
- await page.locator('.field.control.l-input-sm input').first().fill('3');
- await expect(page.locator('.c-form__section div:nth-child(4) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req valid']);
-
- // Verify that can change value of number field by up/down arrows keys
- // Click .field.control.l-input-sm input >> nth=0
- await page.locator('.field.control.l-input-sm input').first().click();
- // Press ArrowUp 3 times to change value from 3 to 6
- await page.locator('.field.control.l-input-sm input').first().press('ArrowUp');
- await page.locator('.field.control.l-input-sm input').first().press('ArrowUp');
- await page.locator('.field.control.l-input-sm input').first().press('ArrowUp');
-
- const value = await page.locator('.field.control.l-input-sm input').first().inputValue();
- await expect(value).toBe('6');
-
- // Click .c-form-row__state-indicator.grows
- await page.locator('.c-form-row__state-indicator.grows').click();
-
- // Click text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]
- await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').click();
-
- // Click .c-form-row__state-indicator >> nth=0
- await page.locator('.c-form-row__state-indicator').first().click();
-
- // Fill text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]
- await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('New Sine Wave Generator');
-
- // Double click div:nth-child(4) .form-row .c-form-row__controls
- await page.locator('div:nth-child(4) .form-row .c-form-row__controls').dblclick();
-
- // Click .field.control.l-input-sm input >> nth=0
- await page.locator('.field.control.l-input-sm input').first().click();
-
- // Click div:nth-child(4) .form-row .c-form-row__state-indicator
- await page.locator('div:nth-child(4) .form-row .c-form-row__state-indicator').click();
-
- // Click .field.control.l-input-sm input >> nth=0
- await page.locator('.field.control.l-input-sm input').first().click();
-
- // Click .field.control.l-input-sm input >> nth=0
- await page.locator('.field.control.l-input-sm input').first().click();
-
- // Click div:nth-child(5) .form-row .c-form-row__controls .form-control .field input
- await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click();
-
- // Click div:nth-child(5) .form-row .c-form-row__controls .form-control .field input
- await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click();
-
- // Click div:nth-child(5) .form-row .c-form-row__controls .form-control .field input
- await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click();
-
- // Click div:nth-child(6) .form-row .c-form-row__controls .form-control .field input
- await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').click();
-
- // Double click div:nth-child(7) .form-row .c-form-row__controls .form-control .field input
- await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').dblclick();
-
- // Click div:nth-child(7) .form-row .c-form-row__state-indicator
- await page.locator('div:nth-child(7) .form-row .c-form-row__state-indicator').click();
-
- // Click div:nth-child(7) .form-row .c-form-row__controls .form-control .field input
- await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').click();
-
- // Fill div:nth-child(7) .form-row .c-form-row__controls .form-control .field input
- await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').fill('3');
-
- //Click text=OK
- await Promise.all([
- page.waitForNavigation(),
- page.click('text=OK')
- ]);
-
- // Verify that the Sine Wave Generator is displayed and correct
- // Verify object properties
- await expect(page.locator('.l-browse-bar__object-name')).toContainText('New Sine Wave Generator');
-
- // Verify canvas rendered
- await page.locator('canvas').nth(1).click({
- position: {
- x: 341,
- y: 28
- }
- });
-
- // Verify that where we click on canvas shows the number we clicked on
- // Note that any number will do, we just care that a number exists
- await expect(page.locator('.value-to-display-nearestValue')).toContainText(/[+-]?([0-9]*[.])?[0-9]+/);
-
- });
-});
diff --git a/e2e/tests/framework/appActions.e2e.spec.js b/e2e/tests/framework/appActions.e2e.spec.js
new file mode 100644
index 000000000..3a95000ba
--- /dev/null
+++ b/e2e/tests/framework/appActions.e2e.spec.js
@@ -0,0 +1,88 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+const { test, expect } = require('../../baseFixtures.js');
+const { createDomainObjectWithDefaults } = require('../../appActions.js');
+
+test.describe('AppActions', () => {
+ test('createDomainObjectsWithDefaults', async ({ page }) => {
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ const e2eFolder = await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: 'e2e folder'
+ });
+
+ await test.step('Create multiple flat objects in a row', async () => {
+ const timer1 = await createDomainObjectWithDefaults(page, {
+ type: 'Timer',
+ name: 'Timer Foo',
+ parent: e2eFolder.uuid
+ });
+ const timer2 = await createDomainObjectWithDefaults(page, {
+ type: 'Timer',
+ name: 'Timer Bar',
+ parent: e2eFolder.uuid
+ });
+ const timer3 = await createDomainObjectWithDefaults(page, {
+ type: 'Timer',
+ name: 'Timer Baz',
+ parent: e2eFolder.uuid
+ });
+
+ await page.goto(timer1.url, { waitUntil: 'networkidle' });
+ await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Foo');
+ await page.goto(timer2.url, { waitUntil: 'networkidle' });
+ await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Bar');
+ await page.goto(timer3.url, { waitUntil: 'networkidle' });
+ await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Timer Baz');
+ });
+
+ await test.step('Create multiple nested objects in a row', async () => {
+ const folder1 = await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: 'Folder Foo',
+ parent: e2eFolder.uuid
+ });
+ const folder2 = await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: 'Folder Bar',
+ parent: folder1.uuid
+ });
+ const folder3 = await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: 'Folder Baz',
+ parent: folder2.uuid
+ });
+ await page.goto(folder1.url, { waitUntil: 'networkidle' });
+ await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Foo');
+ await page.goto(folder2.url, { waitUntil: 'networkidle' });
+ await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Bar');
+ await page.goto(folder3.url, { waitUntil: 'networkidle' });
+ await expect(page.locator('.l-browse-bar__object-name')).toHaveText('Folder Baz');
+
+ expect(folder1.url).toBe(`${e2eFolder.url}/${folder1.uuid}`);
+ expect(folder2.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}`);
+ expect(folder3.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}/${folder3.uuid}`);
+ });
+ });
+});
diff --git a/e2e/tests/framework/baseFixtures.e2e.spec.js b/e2e/tests/framework/baseFixtures.e2e.spec.js
new file mode 100644
index 000000000..86ae7c7d3
--- /dev/null
+++ b/e2e/tests/framework/baseFixtures.e2e.spec.js
@@ -0,0 +1,55 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 testing our use of the playwright framework as it
+relates to how we've extended it (i.e. ./e2e/baseFixtures.js) and assumptions made in our dev environment
+(app.js and ./e2e/webpack-dev-middleware.js)
+*/
+
+const { test } = require('../../baseFixtures.js');
+
+test.describe('baseFixtures tests', () => {
+ test('Verify that tests fail if console.error is thrown', async ({ page }) => {
+ test.fail();
+ //Go to baseURL
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ //Verify that ../fixtures.js detects console log errors
+ await Promise.all([
+ page.evaluate(() => console.error('This should result in a failure')),
+ page.waitForEvent('console') // always wait for the event to happen while triggering it!
+ ]);
+
+ });
+ test('Verify that tests pass if console.warn is thrown', async ({ page }) => {
+ //Go to baseURL
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ //Verify that ../fixtures.js detects console log errors
+ await Promise.all([
+ page.evaluate(() => console.warn('This should result in a pass')),
+ page.waitForEvent('console') // always wait for the event to happen while triggering it!
+ ]);
+
+ });
+});
diff --git a/e2e/tests/framework/exampleTemplate.e2e.spec.js b/e2e/tests/framework/exampleTemplate.e2e.spec.js
new file mode 100644
index 000000000..1b8ac4490
--- /dev/null
+++ b/e2e/tests/framework/exampleTemplate.e2e.spec.js
@@ -0,0 +1,148 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 template is to be used when creating new test suites. It will be kept up to date with the latest improvements
+* made by the Open MCT team. It will also follow our best pratices as those evolve. Please use this structure as a _reference_ and clear
+* or update any references when creating a new test suite!
+*
+* To illustrate current best practices, we've included a mocked up test suite for Renaming a Timer domain object.
+*
+* Demonstrated:
+* - Using appActions to leverage existing functions
+* - Structure
+* - @unstable annotation
+* - await, expect, test, describe syntax
+* - Writing a custom function for a test suite
+* - Test stub for unfinished test coverage (test.fixme)
+*
+* The structure should follow
+* 1. imports
+* 2. test.describe()
+* 3. -> test1
+* -> test2
+* -> test3(stub)
+* 4. Any custom functions
+*/
+
+// Structure: Some standard Imports. Please update the required pathing.
+const { test, expect } = require('../../baseFixtures');
+const { createDomainObjectWithDefaults } = require('../../appActions');
+
+/**
+ * Structure:
+ * Try to keep a single describe block per logical groups of tests.
+ * If your test runtime exceeds 5 minutes or 500 lines, it's likely that it will need to be split.
+ *
+ * Annotations:
+ * Please use the @unstable tag at the end of the test title so that our automation can pick it up
+ * as a part of our test promotion pipeline.
+ */
+test.describe('Renaming Timer Object', () => {
+ // Top-level declaration of the Timer object created in beforeEach().
+ // We can then use this throughout the entire test suite.
+ let timer;
+ test.beforeEach(async ({ page }) => {
+ // Open a browser, navigate to the main page, and wait until all network events to resolve
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ // We provide some helper functions in appActions like `createDomainObjectWithDefaults()`.
+ // This example will create a Timer object with default properties, under the root folder:
+ timer = await createDomainObjectWithDefaults(page, { type: 'Timer' });
+
+ // Assert the object to be created and check its name in the title
+ await expect(page.locator('.l-browse-bar__object-name')).toContainText(timer.name);
+ });
+
+ /**
+ * Make sure to use testcase names which are descriptive and easy to understand.
+ * A good testcase name concisely describes the test's goal(s) and should give
+ * some hint as to what went wrong if the test fails.
+ */
+ test('An existing Timer object can be renamed via the 3dot actions menu', async ({ page }) => {
+ const newObjectName = "Renamed Timer";
+
+ // We've created an example of a shared function which pases the page and newObjectName values
+ await renameTimerFrom3DotMenu(page, timer.url, newObjectName);
+
+ // Assert that the name has changed in the browser bar to the value we assigned above
+ await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName);
+ });
+
+ test('An existing Timer object can be renamed twice', async ({ page }) => {
+ const newObjectName = "Renamed Timer";
+ const newObjectName2 = "Re-Renamed Timer";
+
+ await renameTimerFrom3DotMenu(page, timer.url, newObjectName);
+
+ // Assert that the name has changed in the browser bar to the value we assigned above
+ await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName);
+
+ // Rename the Timer object again
+ await renameTimerFrom3DotMenu(page, timer.url, newObjectName2);
+
+ // Assert that the name has changed in the browser bar to the second value
+ await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName2);
+ });
+
+ /**
+ * If you run out of time to write new tests, please stub in the missing tests
+ * in-place with a test.fixme and BDD-style test steps.
+ * Someone will carry the baton!
+ */
+ test.fixme('Can Rename Timer Object from Tree', async ({ page }) => {
+ //Create a new object
+ //Copy this object
+ //Delete first object
+ //Expect copied object to persist
+ });
+});
+
+/**
+ * Structure:
+ * Custom functions should be declared last.
+ * We are leaning on JSDoc pretty heavily to describe functionality. It is not required, but highly recommended.
+ */
+
+/**
+ * This is an example of a function which is shared between testcases in this test suite. When refactoring, we'll be looking
+ * for common functionality which makes sense to generalize for the entire test framework.
+ * @param {import('@playwright/test').Page} page
+ * @param {string} timerUrl The URL of the timer object to be renamed
+ * @param {string} newNameForTimer New name for object
+ */
+async function renameTimerFrom3DotMenu(page, timerUrl, newNameForTimer) {
+ // Navigate to the timer object
+ await page.goto(timerUrl);
+
+ // Click on 3 Dot Menu
+ await page.locator('button[title="More options"]').click();
+
+ // Click text=Edit Properties...
+ await page.locator('text=Edit Properties...').click();
+
+ // Rename the timer object
+ await page.locator('text=Properties Title Notes >> input[type="text"]').fill(newNameForTimer);
+
+ // Click Ok button to Save
+ await page.locator('text=OK').click();
+}
diff --git a/e2e/tests/framework/generateVisualTestData.e2e.spec.js b/e2e/tests/framework/generateVisualTestData.e2e.spec.js
new file mode 100644
index 000000000..7cb37719f
--- /dev/null
+++ b/e2e/tests/framework/generateVisualTestData.e2e.spec.js
@@ -0,0 +1,64 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 generating LocalStorage via Session Storage to be used
+in some visual test suites like controlledClock.visual.spec.js. This suite should run to completion
+and generate an artifact named ./e2e/test-data/VisualTestData_storage.json . This will run
+on every Commit to ensure that this object still loads into tests correctly and will retain the
+.e2e.spec.js suffix.
+
+TODO: Provide additional validation of object properties as it grows.
+
+*/
+
+const { createDomainObjectWithDefaults } = require('../../appActions.js');
+const { test, expect } = require('../../pluginFixtures.js');
+
+test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
+ //Go to baseURL
+ await page.goto('./', { waitUntil: 'networkidle' });
+ const overlayPlot = await createDomainObjectWithDefaults(page, { type: 'Overlay Plot' });
+
+ // click create button
+ await page.locator('button:has-text("Create")').click();
+
+ // add sine wave generator with defaults
+ await page.locator('li:has-text("Sine Wave Generator")').click();
+
+ //Add a 5000 ms Delay
+ await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000');
+
+ await Promise.all([
+ page.waitForNavigation(),
+ page.locator('text=OK').click(),
+ //Wait for Save Banner to appear
+ page.waitForSelector('.c-message-banner__message')
+ ]);
+
+ // focus the overlay plot
+ await page.goto(overlayPlot.url);
+
+ await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
+ //Save localStorage for future test execution
+ await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' });
+});
diff --git a/e2e/tests/framework/pluginFixtures.e2e.spec.js b/e2e/tests/framework/pluginFixtures.e2e.spec.js
new file mode 100644
index 000000000..0f58a075e
--- /dev/null
+++ b/e2e/tests/framework/pluginFixtures.e2e.spec.js
@@ -0,0 +1,46 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 testing our use of our custom fixtures to verify
+that they are working as expected.
+*/
+
+const { test } = require('../../pluginFixtures.js');
+
+// eslint-disable-next-line playwright/no-skipped-test
+test.describe.skip('pluginFixtures tests', () => {
+ // test.use({ domainObjectName: 'Timer' });
+ // let timerUUID;
+
+ // test('Creates a timer object @framework @unstable', ({ domainObject }) => {
+ // const { uuid } = domainObject;
+ // const uuidRegexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/;
+ // expect(uuid).toMatch(uuidRegexp);
+ // timerUUID = uuid;
+ // });
+
+ // test('Provides same uuid for subsequent uses of the same object @framework', ({ domainObject }) => {
+ // const { uuid } = domainObject;
+ // expect(uuid).toEqual(timerUUID);
+ // });
+});
diff --git a/e2e/tests/framework/testData.e2e.spec.js b/e2e/tests/framework/testData.e2e.spec.js
new file mode 100644
index 000000000..37d30b204
--- /dev/null
+++ b/e2e/tests/framework/testData.e2e.spec.js
@@ -0,0 +1,36 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 template is to be used when verifying Test Data files found in /e2e/test-data/
+*/
+
+const { test } = require('../../baseFixtures');
+
+test.describe('recycled_local_storage @localStorage', () => {
+ //We may want to do some additional level of verification of this file. For now, we just verify that it exists and can be used in a test suite.
+ test.use({ storageState: './e2e/test-data/recycled_local_storage.json' });
+ test('Can use recycled_local_storage file', async ({ page }) => {
+ await page.goto('./', { waitUntil: 'networkidle' });
+ });
+});
+
diff --git a/e2e/tests/branding.e2e.spec.js b/e2e/tests/functional/branding.e2e.spec.js
index 7a8c1b63d..2f494af9d 100644
--- a/e2e/tests/branding.e2e.spec.js
+++ b/e2e/tests/functional/branding.e2e.spec.js
@@ -24,30 +24,30 @@
This test suite is dedicated to tests which verify branding related components.
*/
-const { test, expect } = require('@playwright/test');
+const { test, expect } = require('../../baseFixtures.js');
test.describe('Branding tests', () => {
test('About Modal launches with basic branding properties', async ({ page }) => {
// Go to baseURL
- await page.goto('/', { waitUntil: 'networkidle' });
+ await page.goto('./', { waitUntil: 'networkidle' });
// Click About button
await page.click('.l-shell__app-logo');
// Verify that the NASA Logo Appears
- await expect(await page.locator('.c-about__image')).toBeVisible();
+ await expect(page.locator('.c-about__image')).toBeVisible();
// Modify the Build information in 'about' Modal
- const versionInformationLocator = page.locator('ul.t-info.l-info.s-info');
+ const versionInformationLocator = page.locator('ul.t-info.l-info.s-info').first();
await expect(versionInformationLocator).toBeEnabled();
await expect.soft(versionInformationLocator).toContainText(/Version: \d/);
await expect.soft(versionInformationLocator).toContainText(/Build Date: ((?:Mon|Tue|Wed|Thu|Fri|Sat|Sun))/);
await expect.soft(versionInformationLocator).toContainText(/Revision: \b[0-9a-f]{5,40}\b/);
await expect.soft(versionInformationLocator).toContainText(/Branch: ./);
});
- test('Verify Links in About Modal', async ({ page }) => {
+ test('Verify Links in About Modal @2p', async ({ page }) => {
// Go to baseURL
- await page.goto('/', { waitUntil: 'networkidle' });
+ await page.goto('./', { waitUntil: 'networkidle' });
// Click About button
await page.click('.l-shell__app-logo');
@@ -57,6 +57,7 @@ test.describe('Branding tests', () => {
page.waitForEvent('popup'),
page.locator('text=click here for third party licensing information').click()
]);
- expect(page2.waitForURL('**\/licenses**')).toBeTruthy();
+ await page2.waitForLoadState('networkidle'); //Avoids timing issues with juggler/firefox
+ expect(page2.waitForURL('**/licenses**')).toBeTruthy();
});
});
diff --git a/e2e/tests/functional/couchdb.e2e.spec.js b/e2e/tests/functional/couchdb.e2e.spec.js
new file mode 100644
index 000000000..b3c2fd918
--- /dev/null
+++ b/e2e/tests/functional/couchdb.e2e.spec.js
@@ -0,0 +1,108 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 meant to be executed against a couchdb container. More doc to come
+*
+*/
+
+const { test, expect } = require('../../baseFixtures');
+
+test.describe("CouchDB Status Indicator @couchdb", () => {
+ test.use({ failOnConsoleError: false });
+ //TODO BeforeAll Verify CouchDB Connectivity with APIContext
+ test('Shows green if connected', async ({ page }) => {
+ await page.route('**/openmct/mine', route => {
+ route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({})
+ });
+ });
+
+ //Go to baseURL
+ await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { waitUntil: 'networkidle' });
+ await expect(page.locator('div:has-text("CouchDB is connected")').nth(3)).toBeVisible();
+ });
+ test('Shows red if not connected', async ({ page }) => {
+ await page.route('**/openmct/**', route => {
+ route.fulfill({
+ status: 503,
+ contentType: 'application/json',
+ body: JSON.stringify({})
+ });
+ });
+
+ //Go to baseURL
+ await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { waitUntil: 'networkidle' });
+ await expect(page.locator('div:has-text("CouchDB is offline")').nth(3)).toBeVisible();
+ });
+ test('Shows unknown if it receives an unexpected response code', async ({ page }) => {
+ await page.route('**/openmct/mine', route => {
+ route.fulfill({
+ status: 418,
+ contentType: 'application/json',
+ body: JSON.stringify({})
+ });
+ });
+
+ //Go to baseURL
+ await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { waitUntil: 'networkidle' });
+ await expect(page.locator('div:has-text("CouchDB connectivity unknown")').nth(3)).toBeVisible();
+ });
+});
+
+test.describe("CouchDB initialization @couchdb", () => {
+ test.use({ failOnConsoleError: false });
+ test("'My Items' folder is created if it doesn't exist", async ({ page }) => {
+ // Store any relevant PUT requests that happen on the page
+ const createMineFolderRequests = [];
+ page.on('request', req => {
+ // eslint-disable-next-line playwright/no-conditional-in-test
+ if (req.method() === 'PUT' && req.url().endsWith('openmct/mine')) {
+ createMineFolderRequests.push(req);
+ }
+ });
+
+ // Override the first request to GET openmct/mine to return a 404
+ await page.route('**/openmct/mine', route => {
+ route.fulfill({
+ status: 404,
+ contentType: 'application/json',
+ body: JSON.stringify({})
+ });
+ }, { times: 1 });
+
+ // Go to baseURL
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ // Verify that error banner is displayed
+ const bannerMessage = await page.locator('.c-message-banner__message').innerText();
+ expect(bannerMessage).toEqual('Failed to retrieve object mine');
+
+ // Verify that a PUT request to create "My Items" folder was made
+ await expect.poll(() => createMineFolderRequests.length, {
+ message: 'Verify that PUT request to create "mine" folder was made',
+ timeout: 1000
+ }).toBeGreaterThanOrEqual(1);
+ });
+});
diff --git a/e2e/tests/example/eventGenerator.e2e.spec.js b/e2e/tests/functional/example/eventGenerator.e2e.spec.js
index facd49059..0db74c480 100644
--- a/e2e/tests/example/eventGenerator.e2e.spec.js
+++ b/e2e/tests/functional/example/eventGenerator.e2e.spec.js
@@ -24,39 +24,36 @@
This test suite is dedicated to tests which verify the basic operations surrounding the example event generator.
*/
-const { test, expect } = require('@playwright/test');
+const { test, expect } = require('../../../baseFixtures');
+const { createDomainObjectWithDefaults } = require('../../../appActions');
-test.describe('Example Event Generator Operations', () => {
- test('Can create example event generator with a name', async ({ page }) => {
+test.describe('Example Event Generator CRUD Operations', () => {
+ test('Can create a Test Event Generator and it results in the table View', async ({ page }) => {
//Go to baseURL
- await page.goto('/', { waitUntil: 'networkidle' });
- // let's make an event generator
- await page.locator('button:has-text("Create")').click();
- // Click li:has-text("Event Message Generator")
- await page.locator('li:has-text("Event Message Generator")').click();
- // Click text=Properties Title Notes >> input[type="text"]
- await page.locator('text=Properties Title Notes >> input[type="text"]').click();
- // Fill text=Properties Title Notes >> input[type="text"]
- await page.locator('text=Properties Title Notes >> input[type="text"]').fill('Test Event Generator');
- // Press Enter
- await page.locator('text=Properties Title Notes >> input[type="text"]').press('Enter');
- // Click text=OK
- await Promise.all([
- page.waitForNavigation({ url: /.*&view=table/ }),
- page.locator('text=OK').click()
- ]);
-
- await expect(page.locator('.l-browse-bar__object-name')).toContainText('Test Event Generator');
- // Click button:has-text("Fixed Timespan")
- await page.locator('button:has-text("Fixed Timespan")').click();
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ //Create a name for the object
+ const newObjectName = 'Test Event Generator';
+
+ await createDomainObjectWithDefaults(page, {
+ type: 'Event Message Generator',
+ name: newObjectName
+ });
+
+ //Assertions against newly created object which define standard behavior
+ await expect(page.waitForURL(/.*&view=table/)).toBeTruthy();
+ await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName);
});
+});
+
+test.describe('Example Event Generator Telemetry Event Verficiation', () => {
test.fixme('telemetry is coming in for test event', async ({ page }) => {
- // Go to object created in step one
- // Verify the telemetry table is filled with > 1 row
+ // Go to object created in step one
+ // Verify the telemetry table is filled with > 1 row
});
test.fixme('telemetry is sorted by time ascending', async ({ page }) => {
- // Go to object created in step one
- // Verify the telemetry table has a class with "is-sorting asc"
+ // Go to object created in step one
+ // Verify the telemetry table has a class with "is-sorting asc"
});
});
diff --git a/e2e/tests/functional/example/generator/sineWaveLimitProvider.e2e.spec.js b/e2e/tests/functional/example/generator/sineWaveLimitProvider.e2e.spec.js
new file mode 100644
index 000000000..767540692
--- /dev/null
+++ b/e2e/tests/functional/example/generator/sineWaveLimitProvider.e2e.spec.js
@@ -0,0 +1,119 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 verify the basic operations surrounding conditionSets.
+*/
+
+const { test, expect } = require('../../../../baseFixtures');
+
+test.describe('Sine Wave Generator', () => {
+ test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page, browserName }) => {
+ // eslint-disable-next-line playwright/no-skipped-test
+ test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox');
+
+ //Go to baseURL
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ //Click the Create button
+ await page.click('button:has-text("Create")');
+
+ // Click Sine Wave Generator
+ await page.click('text=Sine Wave Generator');
+
+ // Verify that the each required field has required indicator
+ // Title
+ await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/req/);
+
+ // Verify that the Notes row does not have a required indicator
+ await expect(page.locator('.c-form__section div:nth-child(3) .form-row .c-form-row__state-indicator')).not.toContain('.req');
+ await page.locator('textarea[type="text"]').fill('Optional Note Text');
+
+ // Period
+ await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/req/);
+
+ // Amplitude
+ await expect(page.locator('div:nth-child(5) .c-form-row__state-indicator')).toHaveClass(/req/);
+
+ // Offset
+ await expect(page.locator('div:nth-child(6) .c-form-row__state-indicator')).toHaveClass(/req/);
+
+ // Data Rate
+ await expect(page.locator('div:nth-child(7) .c-form-row__state-indicator')).toHaveClass(/req/);
+
+ // Phase
+ await expect(page.locator('div:nth-child(8) .c-form-row__state-indicator')).toHaveClass(/req/);
+
+ // Randomness
+ await expect(page.locator('div:nth-child(9) .c-form-row__state-indicator')).toHaveClass(/req/);
+
+ // Verify that by removing value from required text field shows invalid indicator
+ await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('');
+ await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/);
+
+ // Verify that by adding value to empty required text field changes invalid to valid indicator
+ await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('New Sine Wave Generator');
+ await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/valid/);
+
+ // Verify that by removing value from required number field shows invalid indicator
+ await page.locator('.field.control.l-input-sm input').first().fill('');
+ await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/invalid/);
+
+ // Verify that by adding value to empty required number field changes invalid to valid indicator
+ await page.locator('.field.control.l-input-sm input').first().fill('3');
+ await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/valid/);
+
+ // Verify that can change value of number field by up/down arrows keys
+ // Click .field.control.l-input-sm input >> nth=0
+ await page.locator('.field.control.l-input-sm input').first().click();
+ // Press ArrowUp 3 times to change value from 3 to 6
+ await page.locator('.field.control.l-input-sm input').first().press('ArrowUp');
+ await page.locator('.field.control.l-input-sm input').first().press('ArrowUp');
+ await page.locator('.field.control.l-input-sm input').first().press('ArrowUp');
+
+ const value = await page.locator('.field.control.l-input-sm input').first().inputValue();
+ await expect(value).toBe('6');
+
+ //Click text=OK
+ await Promise.all([
+ page.waitForNavigation(),
+ page.click('text=OK')
+ ]);
+
+ // Verify that the Sine Wave Generator is displayed and correct
+ // Verify object properties
+ await expect(page.locator('.l-browse-bar__object-name')).toContainText('New Sine Wave Generator');
+
+ // Verify canvas rendered and can be interacted with
+ await page.locator('canvas').nth(1).click({
+ position: {
+ x: 341,
+ y: 28
+ }
+ });
+
+ // Verify that where we click on canvas shows the number we clicked on
+ // Note that any number will do, we just care that a number exists
+ await expect(page.locator('.value-to-display-nearestValue')).toContainText(/[+-]?([0-9]*[.])?[0-9]+/);
+
+ });
+});
diff --git a/e2e/tests/functional/forms.e2e.spec.js b/e2e/tests/functional/forms.e2e.spec.js
new file mode 100644
index 000000000..9da49274a
--- /dev/null
+++ b/e2e/tests/functional/forms.e2e.spec.js
@@ -0,0 +1,100 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 verify form functionality in isolation
+*/
+
+const { test, expect } = require('../../baseFixtures');
+const path = require('path');
+
+const TEST_FOLDER = 'test folder';
+
+test.describe('Form Validation Behavior', () => {
+ test('Required Field indicators appear if title is empty and can be corrected', async ({ page }) => {
+ //Go to baseURL
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ await page.click('button:has-text("Create")');
+ await page.click(':nth-match(:text("Folder"), 2)');
+
+ // Fill in empty string into title and trigger validation with 'Tab'
+ await page.click('text=Properties Title Notes >> input[type="text"]');
+ await page.fill('text=Properties Title Notes >> input[type="text"]', '');
+ await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
+
+ //Required Field Form Validation
+ await expect(page.locator('text=OK')).toBeDisabled();
+ await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/);
+
+ //Correct Form Validation for missing title and trigger validation with 'Tab'
+ await page.click('text=Properties Title Notes >> input[type="text"]');
+ await page.fill('text=Properties Title Notes >> input[type="text"]', TEST_FOLDER);
+ await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
+
+ //Required Field Form Validation is corrected
+ await expect(page.locator('text=OK')).toBeEnabled();
+ await expect(page.locator('.c-form-row__state-indicator').first()).not.toHaveClass(/invalid/);
+
+ //Finish Creating Domain Object
+ await Promise.all([
+ page.waitForNavigation(),
+ page.click('text=OK')
+ ]);
+
+ //Verify that the Domain Object has been created with the corrected title property
+ await expect(page.locator('.l-browse-bar__object-name')).toContainText(TEST_FOLDER);
+ });
+});
+
+test.describe('Persistence operations @addInit', () => {
+ // add non persistable root item
+ test.beforeEach(async ({ page }) => {
+ // eslint-disable-next-line no-undef
+ await page.addInitScript({ path: path.join(__dirname, '../../helper', 'addNoneditableObject.js') });
+ });
+
+ test('Persistability should be respected in the create form location field', async ({ page }) => {
+ test.info().annotations.push({
+ type: 'issue',
+ description: 'https://github.com/nasa/openmct/issues/4323'
+ });
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ await page.click('button:has-text("Create")');
+
+ await page.click('text=Condition Set');
+
+ await page.locator('form[name="mctForm"] >> text=Persistence Testing').click();
+
+ const okButton = page.locator('button:has-text("OK")');
+ await expect(okButton).toBeDisabled();
+ });
+});
+
+test.describe('Form Correctness by Object Type', () => {
+ test.fixme('Verify correct behavior of number object (SWG)', async ({page}) => {});
+ test.fixme('Verify correct behavior of number object Timer', async ({page}) => {});
+ test.fixme('Verify correct behavior of number object Plan View', async ({page}) => {});
+ test.fixme('Verify correct behavior of number object Clock', async ({page}) => {});
+ test.fixme('Verify correct behavior of number object Hyperlink', async ({page}) => {});
+});
diff --git a/e2e/tests/persistence/persistability.e2e.spec.js b/e2e/tests/functional/menu.e2e.spec.js
index 56e48e658..9a1e600dd 100644
--- a/e2e/tests/persistence/persistability.e2e.spec.js
+++ b/e2e/tests/functional/menu.e2e.spec.js
@@ -21,57 +21,30 @@
*****************************************************************************/
/*
-This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
+This test suite is dedicated to tests which verify persistability checks
*/
-const { test, expect } = require('@playwright/test');
-const path = require('path');
+const { test, expect } = require('../../baseFixtures.js');
-// https://github.com/nasa/openmct/issues/4323#issuecomment-1067282651
+const path = require('path');
-test.describe('Persistence operations', () => {
+test.describe('Persistence operations @addInit', () => {
// add non persistable root item
test.beforeEach(async ({ page }) => {
// eslint-disable-next-line no-undef
- await page.addInitScript({ path: path.join(__dirname, 'addNoneditableObject.js') });
+ await page.addInitScript({ path: path.join(__dirname, '../../helper', 'addNoneditableObject.js') });
});
- test('Persistability should be respected in the create form location field', async ({ page }) => {
- // Go to baseURL
- await page.goto('/', { waitUntil: 'networkidle' });
-
- // Click the Create button
- await page.click('button:has-text("Create")');
-
- // Click text=Condition Set
- await page.click('text=Condition Set');
-
- // Click form[name="mctForm"] >> text=Persistence Testing
- await page.locator('form[name="mctForm"] >> text=Persistence Testing').click();
-
- // Check that "OK" button is disabled
- const okButton = page.locator('button:has-text("OK")');
- await expect(okButton).toBeDisabled();
- });
test('Non-persistable objects should not show persistence related actions', async ({ page }) => {
- // Go to baseURL
- await page.goto('/', { waitUntil: 'networkidle' });
+ await page.goto('./', { waitUntil: 'networkidle' });
- // Click text=Persistence Testing >> nth=0
await page.locator('text=Persistence Testing').first().click({
button: 'right'
});
- const menuOptions = page.locator('.c-menu ul');
+ const menuOptions = page.locator('.c-menu li');
await expect.soft(menuOptions).toContainText(['Open In New Tab', 'View', 'Create Link']);
await expect(menuOptions).not.toContainText(['Move', 'Duplicate', 'Remove', 'Add New Folder', 'Edit Properties...', 'Export as JSON', 'Import from JSON']);
});
- test.fixme('Cannot move a previously created domain object to non-peristable boject in Move Modal', async ({ page }) => {
- //Create a domain object
- //Save Domain object
- //Move Object and verify that cannot select non-persistable object
- //Move Object to My Items
- //Verify successful move
- });
});
diff --git a/e2e/tests/functional/moveAndLinkObjects.e2e.spec.js b/e2e/tests/functional/moveAndLinkObjects.e2e.spec.js
new file mode 100644
index 000000000..78f20cb65
--- /dev/null
+++ b/e2e/tests/functional/moveAndLinkObjects.e2e.spec.js
@@ -0,0 +1,212 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 verify the basic operations surrounding moving & linking objects.
+*/
+
+const { test, expect } = require('../../pluginFixtures');
+const { createDomainObjectWithDefaults } = require('../../appActions');
+
+test.describe('Move & link item tests', () => {
+ test('Create a basic object and verify that it can be moved to another folder', async ({ page, openmctConfig }) => {
+ const { myItemsFolderName } = openmctConfig;
+
+ // Go to Open MCT
+ await page.goto('./');
+
+ const parentFolder = await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: 'Parent Folder'
+ });
+ const childFolder = await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: 'Child Folder',
+ parent: parentFolder.uuid
+ });
+ await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: 'Grandchild Folder',
+ parent: childFolder.uuid
+ });
+
+ // Attempt to move parent to its own grandparent
+ await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
+ await page.locator('.c-disclosure-triangle >> nth=0').click();
+
+ await page.locator(`a:has-text("Parent Folder") >> nth=0`).click({
+ button: 'right'
+ });
+
+ await page.locator('li.icon-move').click();
+ await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=0').click();
+ await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
+ await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
+ await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=1').click();
+ await page.locator('form[name="mctForm"] >> text=Child Folder').click();
+ await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
+ await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=2').click();
+ await page.locator('form[name="mctForm"] >> text=Grandchild Folder').click();
+ await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
+ await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
+ await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
+ await page.locator('[aria-label="Cancel"]').click();
+
+ // Move Child Folder from Parent Folder to My Items
+ await page.locator('.c-disclosure-triangle >> nth=0').click();
+ await page.locator('.c-disclosure-triangle >> nth=1').click();
+
+ await page.locator(`a:has-text("Child Folder") >> nth=0`).click({
+ button: 'right'
+ });
+ await page.locator('li.icon-move').click();
+ await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
+
+ await page.locator('text=OK').click();
+
+ // Expect that Child Folder is in My Items, the root folder
+ expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=Child Folder)`)).toBeTruthy();
+ });
+ test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({ page, openmctConfig }) => {
+ const { myItemsFolderName } = openmctConfig;
+
+ // Go to Open MCT
+ await page.goto('./');
+
+ // Create Telemetry Table
+ let telemetryTable = 'Test Telemetry Table';
+ await page.locator('button:has-text("Create")').click();
+ await page.locator('li:has-text("Telemetry Table")').click();
+ await page.locator('text=Properties Title Notes >> input[type="text"]').click();
+ await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable);
+
+ await page.locator('text=OK').click();
+
+ // Finish editing and save Telemetry Table
+ await page.locator('.c-button--menu.c-button--major.icon-save').click();
+ await page.locator('text=Save and Finish Editing').click();
+
+ // Create New Folder Basic Domain Object
+ let folder = 'Test Folder';
+ await page.locator('button:has-text("Create")').click();
+ await page.locator('li:has-text("Folder")').click();
+ await page.locator('text=Properties Title Notes >> input[type="text"]').click();
+ await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder);
+
+ // See if it's possible to put the folder in the Telemetry object during creation (Soft Assert)
+ await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
+ let okButton = await page.locator('button.c-button.c-button--major:has-text("OK")');
+ let okButtonStateDisabled = await okButton.isDisabled();
+ expect.soft(okButtonStateDisabled).toBeTruthy();
+
+ // Continue test regardless of assertion and create it in My Items
+ await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
+ await page.locator('text=OK').click();
+
+ // Open My Items
+ await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
+
+ // Select Folder Object and select Move from context menu
+ await Promise.all([
+ page.waitForNavigation(),
+ page.locator(`a:has-text("${folder}")`).click()
+ ]);
+ await page.locator('.c-tree__item.is-navigated-object .c-tree__item__label .c-tree__item__type-icon').click({
+ button: 'right'
+ });
+ await page.locator('li.icon-move').click();
+
+ // See if it's possible to put the folder in the Telemetry object after creation
+ await page.locator(`text=Location Open MCT ${myItemsFolderName} >> span`).nth(3).click();
+ await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
+ let okButton2 = await page.locator('button.c-button.c-button--major:has-text("OK")');
+ let okButtonStateDisabled2 = await okButton2.isDisabled();
+ expect(okButtonStateDisabled2).toBeTruthy();
+ });
+
+ test('Create a basic object and verify that it can be linked to another folder', async ({ page, openmctConfig }) => {
+ const { myItemsFolderName } = openmctConfig;
+
+ // Go to Open MCT
+ await page.goto('./');
+
+ const parentFolder = await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: 'Parent Folder'
+ });
+ const childFolder = await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: 'Child Folder',
+ parent: parentFolder.uuid
+ });
+ await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: 'Grandchild Folder',
+ parent: childFolder.uuid
+ });
+
+ // Attempt to link parent to its own grandparent
+ await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
+ await page.locator('.c-disclosure-triangle >> nth=0').click();
+
+ await page.locator(`a:has-text("Parent Folder") >> nth=0`).click({
+ button: 'right'
+ });
+
+ await page.locator('li.icon-link').click();
+ await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=0').click();
+ await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
+ await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
+ await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=1').click();
+ await page.locator('form[name="mctForm"] >> text=Child Folder').click();
+ await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
+ await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=2').click();
+ await page.locator('form[name="mctForm"] >> text=Grandchild Folder').click();
+ await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
+ await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
+ await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
+ await page.locator('[aria-label="Cancel"]').click();
+
+ // Link Child Folder from Parent Folder to My Items
+ await page.locator('.c-disclosure-triangle >> nth=0').click();
+ await page.locator('.c-disclosure-triangle >> nth=1').click();
+
+ await page.locator(`a:has-text("Child Folder") >> nth=0`).click({
+ button: 'right'
+ });
+ await page.locator('li.icon-link').click();
+ await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
+
+ await page.locator('text=OK').click();
+
+ // Expect that Child Folder is in My Items, the root folder
+ expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=Child Folder)`)).toBeTruthy();
+ });
+});
+
+test.fixme('Cannot move a previously created domain object to non-peristable object in Move Modal', async ({ page }) => {
+ //Create a domain object
+ //Save Domain object
+ //Move Object and verify that cannot select non-persistable object
+ //Move Object to My Items
+ //Verify successful move
+});
diff --git a/e2e/tests/functional/planning/plan.e2e.spec.js b/e2e/tests/functional/planning/plan.e2e.spec.js
new file mode 100644
index 000000000..c8b7ece8d
--- /dev/null
+++ b/e2e/tests/functional/planning/plan.e2e.spec.js
@@ -0,0 +1,87 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+const { test, expect } = require('../../../pluginFixtures');
+const { createPlanFromJSON } = require('../../../appActions');
+
+const testPlan = {
+ "TEST_GROUP": [
+ {
+ "name": "Past event 1",
+ "start": 1660320408000,
+ "end": 1660343797000,
+ "type": "TEST-GROUP",
+ "color": "orange",
+ "textColor": "white"
+ },
+ {
+ "name": "Past event 2",
+ "start": 1660406808000,
+ "end": 1660429160000,
+ "type": "TEST-GROUP",
+ "color": "orange",
+ "textColor": "white"
+ },
+ {
+ "name": "Past event 3",
+ "start": 1660493208000,
+ "end": 1660503981000,
+ "type": "TEST-GROUP",
+ "color": "orange",
+ "textColor": "white"
+ },
+ {
+ "name": "Past event 4",
+ "start": 1660579608000,
+ "end": 1660624108000,
+ "type": "TEST-GROUP",
+ "color": "orange",
+ "textColor": "white"
+ },
+ {
+ "name": "Past event 5",
+ "start": 1660666008000,
+ "end": 1660681529000,
+ "type": "TEST-GROUP",
+ "color": "orange",
+ "textColor": "white"
+ }
+ ]
+};
+
+test.describe("Plan", () => {
+ test("Create a Plan and display all plan events @unstable", async ({ page }) => {
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ const plan = await createPlanFromJSON(page, {
+ name: 'Test Plan',
+ json: testPlan
+ });
+ const startBound = testPlan.TEST_GROUP[0].start;
+ const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
+
+ // Switch to fixed time mode with all plan events within the bounds
+ await page.goto(`${plan.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=plan.view`);
+ const eventCount = await page.locator('.activity-bounds').count();
+ expect(eventCount).toEqual(testPlan.TEST_GROUP.length);
+ });
+});
+
diff --git a/e2e/tests/functional/planning/timestrip.e2e.spec.js b/e2e/tests/functional/planning/timestrip.e2e.spec.js
new file mode 100644
index 000000000..e1f751144
--- /dev/null
+++ b/e2e/tests/functional/planning/timestrip.e2e.spec.js
@@ -0,0 +1,181 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+const { test, expect } = require('../../../pluginFixtures');
+const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../../appActions');
+
+const testPlan = {
+ "TEST_GROUP": [
+ {
+ "name": "Past event 1",
+ "start": 1660320408000,
+ "end": 1660343797000,
+ "type": "TEST-GROUP",
+ "color": "orange",
+ "textColor": "white"
+ },
+ {
+ "name": "Past event 2",
+ "start": 1660406808000,
+ "end": 1660429160000,
+ "type": "TEST-GROUP",
+ "color": "orange",
+ "textColor": "white"
+ },
+ {
+ "name": "Past event 3",
+ "start": 1660493208000,
+ "end": 1660503981000,
+ "type": "TEST-GROUP",
+ "color": "orange",
+ "textColor": "white"
+ },
+ {
+ "name": "Past event 4",
+ "start": 1660579608000,
+ "end": 1660624108000,
+ "type": "TEST-GROUP",
+ "color": "orange",
+ "textColor": "white"
+ },
+ {
+ "name": "Past event 5",
+ "start": 1660666008000,
+ "end": 1660681529000,
+ "type": "TEST-GROUP",
+ "color": "orange",
+ "textColor": "white"
+ }
+ ]
+};
+
+test.describe("Time Strip", () => {
+ test("Create two Time Strips, add a single Plan to both, and verify they can have separate Indepdenent Time Contexts @unstable", async ({ page }) => {
+ test.info().annotations.push({
+ type: 'issue',
+ description: 'https://github.com/nasa/openmct/issues/5627'
+ });
+
+ // Constant locators
+ const independentTimeConductorInputs = page.locator('.l-shell__main-independent-time-conductor .c-input--datetime');
+ const activityBounds = page.locator('.activity-bounds');
+
+ // Goto baseURL
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ const timestrip = await test.step("Create a Time Strip", async () => {
+ const createdTimeStrip = await createDomainObjectWithDefaults(page, { type: 'Time Strip' });
+ const objectName = await page.locator('.l-browse-bar__object-name').innerText();
+ expect(objectName).toBe(createdTimeStrip.name);
+
+ return createdTimeStrip;
+ });
+
+ const plan = await test.step("Create a Plan and add it to the timestrip", async () => {
+ const createdPlan = await createPlanFromJSON(page, {
+ name: 'Test Plan',
+ json: testPlan
+ });
+
+ await page.goto(timestrip.url);
+ // Expand the tree to show the plan
+ await page.click("button[title='Show selected item in tree']");
+ await page.dragAndDrop(`role=treeitem[name=/${createdPlan.name}/]`, '.c-object-view');
+ await page.click("button[title='Save']");
+ await page.click("li[title='Save and Finish Editing']");
+ const startBound = testPlan.TEST_GROUP[0].start;
+ const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
+
+ // Switch to fixed time mode with all plan events within the bounds
+ await page.goto(`${timestrip.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=time-strip.view`);
+
+ // Verify all events are displayed
+ const eventCount = await page.locator('.activity-bounds').count();
+ expect(eventCount).toEqual(testPlan.TEST_GROUP.length);
+
+ return createdPlan;
+ });
+
+ 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);
+
+ // Set the independent time bounds so that only one event is shown
+ const startBound = testPlan.TEST_GROUP[0].start;
+ const endBound = testPlan.TEST_GROUP[0].end;
+ 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');
+ expect(await activityBounds.count()).toEqual(1);
+ });
+
+ await test.step("Can have multiple TimeStrips with the same plan linked and different Independent Time Contexts", async () => {
+ // Create another Time Strip and verify that it has been created
+ const createdTimeStrip = await createDomainObjectWithDefaults(page, {
+ type: 'Time Strip',
+ name: "Another Time Strip"
+ });
+
+ const objectName = await page.locator('.l-browse-bar__object-name').innerText();
+ expect(objectName).toBe(createdTimeStrip.name);
+
+ // Drag the existing Plan onto the newly created Time Strip, and save.
+ await page.dragAndDrop(`role=treeitem[name=/${plan.name}/]`, '.c-object-view');
+ 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);
+
+ // Set the independent time bounds so that two events are shown
+ const startBound = testPlan.TEST_GROUP[0].start;
+ const endBound = testPlan.TEST_GROUP[1].end;
+ 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');
+
+ // Verify that two events are displayed
+ expect(await activityBounds.count()).toEqual(2);
+
+ // Switch to the previous Time Strip and verify that only one event is displayed
+ await page.goto(timestrip.url);
+ expect(await activityBounds.count()).toEqual(1);
+ });
+ });
+});
diff --git a/e2e/tests/plugins/clock/Clock.e2e.spec.js b/e2e/tests/functional/plugins/clocks/clock.e2e.spec.js
index 7ff0274c8..db1a28868 100644
--- a/e2e/tests/plugins/clock/Clock.e2e.spec.js
+++ b/e2e/tests/functional/plugins/clocks/clock.e2e.spec.js
@@ -24,9 +24,9 @@
This test suite is dedicated to tests which verify the basic operations surrounding Clock.
*/
-const { test, expect } = require('@playwright/test');
+const { test, expect } = require('../../../../baseFixtures');
-test.describe('Clock Generator', () => {
+test.describe('Clock Generator CRUD Operations', () => {
test('Timezone dropdown will collapse when clicked outside or on dropdown icon again', async ({ page }) => {
test.info().annotations.push({
@@ -34,7 +34,7 @@ test.describe('Clock Generator', () => {
description: 'https://github.com/nasa/openmct/issues/4878'
});
//Go to baseURL
- await page.goto('/', { waitUntil: 'networkidle' });
+ await page.goto('./', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
@@ -45,22 +45,22 @@ test.describe('Clock Generator', () => {
// Click .icon-arrow-down
await page.locator('.icon-arrow-down').click();
//verify if the autocomplete dropdown is visible
- await expect(page.locator(".optionPreSelected")).toBeVisible();
+ await expect(page.locator(".c-input--autocomplete__options")).toBeVisible();
// Click .icon-arrow-down
await page.locator('.icon-arrow-down').click();
// Verify clicking on the autocomplete arrow collapses the dropdown
- await expect(page.locator(".optionPreSelected")).not.toBeVisible();
+ await expect(page.locator(".c-input--autocomplete__options")).toBeHidden();
// Click timezone input to open dropdown
- await page.locator('.autocompleteInput').click();
+ await page.locator('.c-input--autocomplete__input').click();
//verify if the autocomplete dropdown is visible
- await expect(page.locator(".optionPreSelected")).toBeVisible();
+ await expect(page.locator(".c-input--autocomplete__options")).toBeVisible();
// Verify clicking outside the autocomplete dropdown collapses it
await page.locator('text=Timezone').click();
// Verify clicking on the autocomplete arrow collapses the dropdown
- await expect(page.locator(".optionPreSelected")).not.toBeVisible();
+ await expect(page.locator(".c-input--autocomplete__options")).toBeHidden();
});
});
diff --git a/e2e/tests/functional/plugins/clocks/remoteClock.e2e.spec.js b/e2e/tests/functional/plugins/clocks/remoteClock.e2e.spec.js
new file mode 100644
index 000000000..648060d71
--- /dev/null
+++ b/e2e/tests/functional/plugins/clocks/remoteClock.e2e.spec.js
@@ -0,0 +1,41 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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.
+ *****************************************************************************/
+
+// FIXME: Remove this eslint exception once tests are implemented
+// eslint-disable-next-line no-unused-vars
+const { test, expect } = require('../../../../baseFixtures');
+
+test.describe('Remote Clock', () => {
+ // eslint-disable-next-line require-await
+ test.fixme('blocks historical requests until first tick is received', async ({ page }) => {
+ test.info().annotations.push({
+ type: 'issue',
+ description: 'https://github.com/nasa/openmct/issues/5221'
+ });
+ // addInitScript to with remote clock
+ // Switch time conductor mode to 'remote clock'
+ // Navigate to telemetry
+ // Verify that the plot renders historical data within the correct bounds
+ // Refresh the page
+ // Verify again that the plot renders historical data within the correct bounds
+ });
+});
diff --git a/e2e/tests/plugins/condition/condition.e2e.spec.js b/e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js
index 225752548..f3f9826ae 100644
--- a/e2e/tests/plugins/condition/condition.e2e.spec.js
+++ b/e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js
@@ -26,54 +26,52 @@ suite is sharing state between tests which is considered an anti-pattern. Implim
demonstrate some playwright for test developers. This pattern should not be re-used in other CRUD suites.
*/
-const { test, expect } = require('@playwright/test');
+const { test, expect } = require('../../../../pluginFixtures.js');
+const { createDomainObjectWithDefaults } = require('../../../../appActions');
let conditionSetUrl;
let getConditionSetIdentifierFromUrl;
-test('Create new Condition Set object and store @localStorage', async ({ page, context }) => {
- //Go to baseURL
- await page.goto('/', { waitUntil: 'networkidle' });
-
- //Click the Create button
- await page.click('button:has-text("Create")');
-
- // Click text=Condition Set
- await page.click('text=Condition Set');
+test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
+ test.beforeAll(async ({ browser}) => {
+ //TODO: This needs to be refactored
+ const context = await browser.newContext();
+ const page = await context.newPage();
+ await page.goto('./', { waitUntil: 'networkidle' });
+ await page.click('button:has-text("Create")');
- // Click text=OK
- await Promise.all([
- page.waitForNavigation(),
- page.click('text=OK')
- ]);
+ await page.locator('li:has-text("Condition Set")').click();
- await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
- //Save localStorage for future test execution
- await context.storageState({ path: './e2e/tests/recycled_storage.json' });
+ await Promise.all([
+ page.waitForNavigation(),
+ page.click('text=OK')
+ ]);
- //Set object identifier from url
- conditionSetUrl = await page.url();
- console.log('conditionSetUrl ' + conditionSetUrl);
+ //Save localStorage for future test execution
+ await context.storageState({ path: './e2e/test-data/recycled_local_storage.json' });
- getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0];
- console.log('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
+ //Set object identifier from url
+ conditionSetUrl = page.url();
+ console.log('conditionSetUrl ' + conditionSetUrl);
-});
+ getConditionSetIdentifierFromUrl = conditionSetUrl.split('/').pop().split('?')[0];
+ console.debug('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
+ await page.close();
+ });
-test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
//Load localStorage for subsequent tests
- test.use({ storageState: './e2e/tests/recycled_storage.json' });
+ test.use({ storageState: './e2e/test-data/recycled_local_storage.json' });
//Begin suite of tests again localStorage
- test('Condition set object properties persist in main view and inspector', async ({ page }) => {
+ test('Condition set object properties persist in main view and inspector @localStorage', async ({ page }) => {
//Navigate to baseURL with injected localStorage
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
- //Assertions on loaded Condition Set in main view
+ //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector
- await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy;
+ expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
//Reload Page
await Promise.all([
@@ -84,13 +82,15 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
//Re-verify after reload
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector
- await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy;
+ expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
});
- test('condition set object can be modified on @localStorage', async ({ page }) => {
+ test('condition set object can be modified on @localStorage', async ({ page, openmctConfig }) => {
+ const { myItemsFolderName } = openmctConfig;
+
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
- //Assertions on loaded Condition Set in main view
+ //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Update the Condition Set properties
@@ -110,18 +110,18 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
// Verify Inspector properties
// Verify Inspector has updated Name property
- await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
+ expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
// Verify Inspector Details has updated Name property
- await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
+ expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
// Verify Tree reflects updated Name proprety
// Expand Tree
- await page.locator('text=Open MCT My Items >> span >> nth=3').click();
+ await page.locator(`text=Open MCT ${myItemsFolderName} >> span >> nth=3`).click();
// Verify Condition Set Object is renamed in Tree
- await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
+ expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
// Verify Search Tree reflects renamed Name property
- await page.locator('input[type="search"]').fill('Renamed');
- await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed');
+ expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
//Reload Page
await Promise.all([
@@ -134,45 +134,43 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
// Verify Inspector properties
// Verify Inspector has updated Name property
- await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
+ expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
// Verify Inspector Details has updated Name property
- await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
+ expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
// Verify Tree reflects updated Name proprety
// Expand Tree
- await page.locator('text=Open MCT My Items >> span >> nth=3').click();
+ await page.locator(`text=Open MCT ${myItemsFolderName} >> span >> nth=3`).click();
// Verify Condition Set Object is renamed in Tree
- await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
+ expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
// Verify Search Tree reflects renamed Name property
- await page.locator('input[type="search"]').fill('Renamed');
- await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed');
+ expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
});
test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => {
//Navigate to baseURL
- await page.goto('/', { waitUntil: 'networkidle' });
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
+ await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set") >> nth=0')).toBeVisible();
- //Expect Unnamed Condition Set to be visible in Main View
- await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).toBeVisible();
+ const numberOfConditionSetsToStart = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count();
// Search for Unnamed Condition Set
- await page.locator('input[type="search"]').fill('Unnamed Condition Set');
- // Right Click to Open Actions Menu
- await page.locator('a:has-text("Unnamed Condition Set")').click({
- button: 'right'
- });
- // Click Remove Action
- await page.locator('text=Remove').click();
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed Condition Set');
+ // Click Search Result
+ await page.locator('[aria-label="OpenMCT Search"] >> text=Unnamed Condition Set').first().click();
+ // Click hamburger button
+ await page.locator('[title="More options"]').click();
+ // Click text=Remove
+ await page.locator('text=Remove').click();
await page.locator('text=OK').click();
//Expect Unnamed Condition Set to be removed in Main View
- await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).not.toBeVisible();
+ const numberOfConditionSetsAtEnd = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count();
- await page.locator('.c-search__clear-input').click();
- // Search for Unnamed Condition Set
- await page.locator('input[type="search"]').fill('Unnamed Condition Set');
- // Expect Unnamed Condition Set to be removed
- await expect(page.locator('a:has-text("Unnamed Condition Set")')).not.toBeVisible();
+ expect(numberOfConditionSetsAtEnd).toEqual(numberOfConditionSetsToStart - 1);
//Feature?
//Domain Object is still available by direct URL after delete
@@ -181,3 +179,24 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
});
});
+
+test.describe('Basic Condition Set Use', () => {
+ test('Can add a condition', async ({ page }) => {
+ //Navigate to baseURL
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ // Create a new condition set
+ await createDomainObjectWithDefaults(page, {
+ type: 'Condition Set',
+ name: "Test Condition Set"
+ });
+ // Change the object to edit mode
+ await page.locator('[title="Edit"]').click();
+
+ // Click Add Condition button
+ await page.locator('#addCondition').click();
+ // Check that the new Unnamed Condition section appears
+ const numOfUnnamedConditions = await page.locator('text=Unnamed Condition').count();
+ expect(numOfUnnamedConditions).toEqual(1);
+ });
+});
diff --git a/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js b/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js
new file mode 100644
index 000000000..3d6456e2e
--- /dev/null
+++ b/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js
@@ -0,0 +1,186 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+const { test, expect } = require('../../../../pluginFixtures');
+const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
+
+test.describe('Testing Display Layout @unstable', () => {
+ let sineWaveObject;
+ test.beforeEach(async ({ page }) => {
+ await page.goto('./', { waitUntil: 'networkidle' });
+ await setRealTimeMode(page);
+
+ // Create Sine Wave Generator
+ sineWaveObject = await createDomainObjectWithDefaults(page, {
+ type: 'Sine Wave Generator',
+ name: "Test Sine Wave Generator"
+ });
+ });
+ test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in real time', async ({ page }) => {
+ // Create a Display Layout
+ await createDomainObjectWithDefaults(page, {
+ type: 'Display Layout',
+ name: "Test 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
+ await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
+ await page.locator('button[title="Save"]').click();
+ await page.locator('text=Save and Finish Editing').click();
+
+ // Subscribe to the Sine Wave Generator data
+ // On getting data, check if the value found in the Display Layout is the most recent value
+ // from the Sine Wave Generator
+ const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
+ const formattedTelemetryValue = await getTelemValuePromise;
+ const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`);
+ const displayLayoutValue = await displayLayoutValuePromise.textContent();
+ const trimmedDisplayValue = displayLayoutValue.trim();
+
+ await expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
+ });
+ test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in fixed time', async ({ page }) => {
+ // Create a Display Layout
+ await createDomainObjectWithDefaults(page, {
+ type: 'Display Layout',
+ name: "Test 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
+ await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
+ await page.locator('button[title="Save"]').click();
+ await page.locator('text=Save and Finish Editing').click();
+
+ // Subscribe to the Sine Wave Generator data
+ const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
+ // Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window
+ await setStartOffset(page, { mins: '1' });
+ await setFixedTimeMode(page);
+
+ // On getting data, check if the value found in the Display Layout is the most recent value
+ // from the Sine Wave Generator
+ const formattedTelemetryValue = await getTelemValuePromise;
+ const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`);
+ const displayLayoutValue = await displayLayoutValuePromise.textContent();
+ const trimmedDisplayValue = displayLayoutValue.trim();
+
+ await expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
+ });
+ test('items in a display layout can be removed with object tree context menu when viewing the display layout', async ({ page }) => {
+ // Create a Display Layout
+ await createDomainObjectWithDefaults(page, {
+ type: 'Display Layout',
+ name: "Test 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
+ await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
+ await page.locator('button[title="Save"]').click();
+ await page.locator('text=Save and Finish Editing').click();
+
+ expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
+
+ // Expand the Display Layout so we can remove the sine wave generator
+ await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
+
+ // Bring up context menu and remove
+ await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').first().click({ button: 'right' });
+ await page.locator('text=Remove').click();
+ await page.locator('text=OK').click();
+
+ // delete
+
+ expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
+ });
+ test('items in a display layout can be removed with object tree context menu when viewing another item', async ({ page }) => {
+ // Create a Display Layout
+ await createDomainObjectWithDefaults(page, {
+ type: 'Display Layout',
+ name: "Test 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
+ await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
+ await page.locator('button[title="Save"]').click();
+ await page.locator('text=Save and Finish Editing').click();
+
+ expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
+
+ // Expand the Display Layout so we can remove the sine wave generator
+ await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
+
+ // Click the original Sine Wave Generator to navigate away from the Display Layout
+ await page.locator('.c-tree__item .c-tree__item__name:text("Test Sine Wave Generator")').click();
+
+ // Bring up context menu and remove
+ await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').click({ button: 'right' });
+ await page.locator('text=Remove').click();
+ await page.locator('text=OK').click();
+
+ // navigate back to the display layout to confirm it has been removed
+ await page.locator('.c-tree__item .c-tree__item__name:text("Test Display Layout")').click();
+
+ expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0);
+ });
+});
+
+/**
+ * Util for subscribing to a telemetry object by object identifier
+ * Limitations: Currently only works to return telemetry once to the node scope
+ * To Do: See if there's a way to await this multiple times to allow for multiple
+ * values to be returned over time
+ * @param {import('@playwright/test').Page} page
+ * @param {string} objectIdentifier identifier for object
+ * @returns {Promise<string>} the formatted sin telemetry value
+ */
+async function subscribeToTelemetry(page, objectIdentifier) {
+ const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getTelemValue', resolve));
+
+ await page.evaluate(async (telemetryIdentifier) => {
+ const telemetryObject = await window.openmct.objects.get(telemetryIdentifier);
+ const metadata = window.openmct.telemetry.getMetadata(telemetryObject);
+ const formats = await window.openmct.telemetry.getFormatMap(metadata);
+ window.openmct.telemetry.subscribe(telemetryObject, (obj) => {
+ const sinVal = obj.sin;
+ const formattedSinVal = formats.sin.format(sinVal);
+ window.getTelemValue(formattedSinVal);
+ });
+ }, objectIdentifier);
+
+ return getTelemValuePromise;
+}
diff --git a/e2e/tests/functional/plugins/faultManagement/faultManagement.e2e.spec.js b/e2e/tests/functional/plugins/faultManagement/faultManagement.e2e.spec.js
new file mode 100644
index 000000000..ff35dc79b
--- /dev/null
+++ b/e2e/tests/functional/plugins/faultManagement/faultManagement.e2e.spec.js
@@ -0,0 +1,237 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+const { test, expect } = require('../../../../pluginFixtures');
+const utils = require('../../../../helper/faultUtils');
+
+test.describe('The Fault Management Plugin using example faults', () => {
+ test.beforeEach(async ({ page }) => {
+ await utils.navigateToFaultManagementWithExample(page);
+ });
+
+ test('Shows a criticality icon for every fault @unstable', async ({ page }) => {
+ const faultCount = await page.locator('c-fault-mgmt__list').count();
+ const criticalityIconCount = await page.locator('c-fault-mgmt__list-severity').count();
+
+ expect.soft(faultCount).toEqual(criticalityIconCount);
+ });
+
+ test('When selecting a fault, it has an "is-selected" class and it\'s information shows in the inspector @unstable', async ({ page }) => {
+ await utils.selectFaultItem(page, 1);
+
+ const selectedFaultName = await page.locator('.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname').textContent();
+ const inspectorFaultNameCount = await page.locator(`.c-inspector__properties >> :text("${selectedFaultName}")`).count();
+
+ await expect.soft(page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first()).toHaveClass(/is-selected/);
+ expect.soft(inspectorFaultNameCount).toEqual(1);
+ });
+
+ test('When selecting multiple faults, no specific fault information is shown in the inspector @unstable', async ({ page }) => {
+ await utils.selectFaultItem(page, 1);
+ await utils.selectFaultItem(page, 2);
+
+ const selectedRows = page.locator('.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname');
+ expect.soft(await selectedRows.count()).toEqual(2);
+
+ const firstSelectedFaultName = await selectedRows.nth(0).textContent();
+ const secondSelectedFaultName = await selectedRows.nth(1).textContent();
+ const firstNameInInspectorCount = await page.locator(`.c-inspector__properties >> :text("${firstSelectedFaultName}")`).count();
+ const secondNameInInspectorCount = await page.locator(`.c-inspector__properties >> :text("${secondSelectedFaultName}")`).count();
+
+ expect.soft(firstNameInInspectorCount).toEqual(0);
+ expect.soft(secondNameInInspectorCount).toEqual(0);
+ });
+
+ test('Allows you to shelve a fault @unstable', async ({ page }) => {
+ const shelvedFaultName = await utils.getFaultName(page, 2);
+ const beforeShelvedFault = utils.getFaultByName(page, shelvedFaultName);
+
+ expect.soft(await beforeShelvedFault.count()).toBe(1);
+
+ await utils.shelveFault(page, 2);
+
+ // check it is removed from standard view
+ const afterShelvedFault = utils.getFaultByName(page, shelvedFaultName);
+ expect.soft(await afterShelvedFault.count()).toBe(0);
+
+ await utils.changeViewTo(page, 'shelved');
+
+ const shelvedViewFault = utils.getFaultByName(page, shelvedFaultName);
+
+ expect.soft(await shelvedViewFault.count()).toBe(1);
+ });
+
+ test('Allows you to acknowledge a fault @unstable', async ({ page }) => {
+ const acknowledgedFaultName = await utils.getFaultName(page, 3);
+
+ await utils.acknowledgeFault(page, 3);
+
+ const fault = utils.getFault(page, 3);
+ await expect.soft(fault).toHaveClass(/is-acknowledged/);
+
+ await utils.changeViewTo(page, 'acknowledged');
+
+ const acknowledgedViewFaultName = await utils.getFaultName(page, 1);
+ expect.soft(acknowledgedFaultName).toEqual(acknowledgedViewFaultName);
+ });
+
+ test('Allows you to shelve multiple faults @unstable', async ({ page }) => {
+ const shelvedFaultNameOne = await utils.getFaultName(page, 1);
+ const shelvedFaultNameFour = await utils.getFaultName(page, 4);
+
+ const beforeShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
+ const beforeShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
+
+ expect.soft(await beforeShelvedFaultOne.count()).toBe(1);
+ expect.soft(await beforeShelvedFaultFour.count()).toBe(1);
+
+ await utils.shelveMultipleFaults(page, 1, 4);
+
+ // check it is removed from standard view
+ const afterShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
+ const afterShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
+ expect.soft(await afterShelvedFaultOne.count()).toBe(0);
+ expect.soft(await afterShelvedFaultFour.count()).toBe(0);
+
+ await utils.changeViewTo(page, 'shelved');
+
+ const shelvedViewFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
+ const shelvedViewFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
+
+ expect.soft(await shelvedViewFaultOne.count()).toBe(1);
+ expect.soft(await shelvedViewFaultFour.count()).toBe(1);
+ });
+
+ test('Allows you to acknowledge multiple faults @unstable', async ({ page }) => {
+ const acknowledgedFaultNameTwo = await utils.getFaultName(page, 2);
+ const acknowledgedFaultNameFive = await utils.getFaultName(page, 5);
+
+ await utils.acknowledgeMultipleFaults(page, 2, 5);
+
+ const faultTwo = utils.getFault(page, 2);
+ const faultFive = utils.getFault(page, 5);
+
+ // check they have been acknowledged
+ await expect.soft(faultTwo).toHaveClass(/is-acknowledged/);
+ await expect.soft(faultFive).toHaveClass(/is-acknowledged/);
+
+ await utils.changeViewTo(page, 'acknowledged');
+
+ const acknowledgedViewFaultTwo = utils.getFaultByName(page, acknowledgedFaultNameTwo);
+ const acknowledgedViewFaultFive = utils.getFaultByName(page, acknowledgedFaultNameFive);
+
+ expect.soft(await acknowledgedViewFaultTwo.count()).toBe(1);
+ expect.soft(await acknowledgedViewFaultFive.count()).toBe(1);
+ });
+
+ test('Allows you to search faults @unstable', async ({ page }) => {
+ const faultThreeNamespace = await utils.getFaultNamespace(page, 3);
+ const faultTwoName = await utils.getFaultName(page, 2);
+ const faultFiveTriggerTime = await utils.getFaultTriggerTime(page, 5);
+
+ // should be all faults (5)
+ let faultResultCount = await utils.getFaultResultCount(page);
+ expect.soft(faultResultCount).toEqual(5);
+
+ // search namespace
+ await utils.enterSearchTerm(page, faultThreeNamespace);
+
+ faultResultCount = await utils.getFaultResultCount(page);
+ expect.soft(faultResultCount).toEqual(1);
+ expect.soft(await utils.getFaultNamespace(page, 1)).toEqual(faultThreeNamespace);
+
+ // all faults
+ await utils.clearSearch(page);
+ faultResultCount = await utils.getFaultResultCount(page);
+ expect.soft(faultResultCount).toEqual(5);
+
+ // search name
+ await utils.enterSearchTerm(page, faultTwoName);
+
+ faultResultCount = await utils.getFaultResultCount(page);
+ expect.soft(faultResultCount).toEqual(1);
+ expect.soft(await utils.getFaultName(page, 1)).toEqual(faultTwoName);
+
+ // all faults
+ await utils.clearSearch(page);
+ faultResultCount = await utils.getFaultResultCount(page);
+ expect.soft(faultResultCount).toEqual(5);
+
+ // search triggerTime
+ await utils.enterSearchTerm(page, faultFiveTriggerTime);
+
+ faultResultCount = await utils.getFaultResultCount(page);
+ expect.soft(faultResultCount).toEqual(1);
+ expect.soft(await utils.getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime);
+ });
+
+ test('Allows you to sort faults @unstable', async ({ page }) => {
+ const highestSeverity = await utils.getHighestSeverity(page);
+ const lowestSeverity = await utils.getLowestSeverity(page);
+ const faultOneName = 'Example Fault 1';
+ const faultFiveName = 'Example Fault 5';
+ let firstFaultName = await utils.getFaultName(page, 1);
+
+ expect.soft(firstFaultName).toEqual(faultOneName);
+
+ await utils.sortFaultsBy(page, 'oldest-first');
+
+ firstFaultName = await utils.getFaultName(page, 1);
+ expect.soft(firstFaultName).toEqual(faultFiveName);
+
+ await utils.sortFaultsBy(page, 'severity');
+
+ const sortedHighestSeverity = await utils.getFaultSeverity(page, 1);
+ const sortedLowestSeverity = await utils.getFaultSeverity(page, 5);
+ expect.soft(sortedHighestSeverity).toEqual(highestSeverity);
+ expect.soft(sortedLowestSeverity).toEqual(lowestSeverity);
+ });
+
+});
+
+test.describe('The Fault Management Plugin without using example faults', () => {
+ test.beforeEach(async ({ page }) => {
+ await utils.navigateToFaultManagementWithoutExample(page);
+ });
+
+ test('Shows no faults when no faults are provided @unstable', async ({ page }) => {
+ const faultCount = await page.locator('c-fault-mgmt__list').count();
+
+ expect.soft(faultCount).toEqual(0);
+
+ await utils.changeViewTo(page, 'acknowledged');
+ const acknowledgedCount = await page.locator('c-fault-mgmt__list').count();
+ expect.soft(acknowledgedCount).toEqual(0);
+
+ await utils.changeViewTo(page, 'shelved');
+ const shelvedCount = await page.locator('c-fault-mgmt__list').count();
+ expect.soft(shelvedCount).toEqual(0);
+ });
+
+ test('Will return no faults when searching @unstable', async ({ page }) => {
+ await utils.enterSearchTerm(page, 'fault');
+
+ const faultCount = await page.locator('c-fault-mgmt__list').count();
+
+ expect.soft(faultCount).toEqual(0);
+ });
+});
diff --git a/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js b/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js
new file mode 100644
index 000000000..484091d7c
--- /dev/null
+++ b/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js
@@ -0,0 +1,66 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+const { test, expect } = require('../../../../pluginFixtures');
+const { createDomainObjectWithDefaults } = require('../../../../appActions');
+
+test.describe('Testing Flexible Layout @unstable', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ // Create Sine Wave Generator
+ await createDomainObjectWithDefaults(page, {
+ type: 'Sine Wave Generator',
+ name: "Test Sine Wave Generator"
+ });
+
+ // Create Clock Object
+ await createDomainObjectWithDefaults(page, {
+ type: 'Clock',
+ name: "Test Clock"
+ });
+ });
+ test('panes have the appropriate draggable attribute while in Edit and Browse modes', async ({ page }) => {
+ // Create a Flexible Layout
+ await createDomainObjectWithDefaults(page, {
+ type: 'Flexible Layout',
+ name: "Test Flexible Layout"
+ });
+ // Edit Flexible 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').first().click();
+ // Add the Sine Wave Generator and Clock to the Flexible Layout
+ await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
+ await page.dragAndDrop('text=Test Clock', '.c-fl__container.is-empty');
+ // Check that panes can be dragged while Flexible Layout is in Edit mode
+ let dragWrapper = await page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
+ await expect(dragWrapper).toHaveAttribute('draggable', 'true');
+ // Save Flexible Layout
+ await page.locator('button[title="Save"]').click();
+ await page.locator('text=Save and Finish Editing').click();
+ // Check that panes are not draggable while Flexible Layout is in Browse mode
+ dragWrapper = await page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
+ await expect(dragWrapper).toHaveAttribute('draggable', 'false');
+ });
+});
diff --git a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js
new file mode 100644
index 000000000..f399daee0
--- /dev/null
+++ b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js
@@ -0,0 +1,720 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 verify the basic operations surrounding imagery,
+but only assume that example imagery is present.
+*/
+/* globals process */
+const { v4: uuid } = require('uuid');
+const { waitForAnimations } = require('../../../../baseFixtures');
+const { test, expect } = require('../../../../pluginFixtures');
+const { createDomainObjectWithDefaults } = require('../../../../appActions');
+const backgroundImageSelector = '.c-imagery__main-image__background-image';
+const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
+const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
+
+//The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects.
+test.describe('Example Imagery Object', () => {
+ test.beforeEach(async ({ page }) => {
+ //Go to baseURL
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ // Create a default 'Example Imagery' object
+ await createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
+
+ // Verify that the created object is focused
+ await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
+ await page.locator(backgroundImageSelector).hover({trial: true});
+ });
+
+ test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
+ // Zoom in x2 and assert
+ await mouseZoomOnImageAndAssert(page, 2);
+
+ // Zoom out x2 and assert
+ await mouseZoomOnImageAndAssert(page, -2);
+ });
+
+ test('Can adjust image brightness/contrast by dragging the sliders', async ({ page, browserName }) => {
+ // eslint-disable-next-line playwright/no-skipped-test
+ test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox');
+ // Open the image filter menu
+ await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click();
+
+ // Drag the brightness and contrast sliders around and assert filter values
+ await dragBrightnessSliderAndAssertFilterValues(page);
+ await dragContrastSliderAndAssertFilterValues(page);
+ });
+
+ test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
+ const deltaYStep = 100; //equivalent to 1x zoom
+
+ await page.locator(backgroundImageSelector).hover({trial: true});
+
+ // zoom in
+ await page.mouse.wheel(0, deltaYStep * 2);
+ await page.locator(backgroundImageSelector).hover({trial: true});
+ const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
+ const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
+ const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
+ // move to the right
+
+ // center the mouse pointer
+ await page.mouse.move(imageCenterX, imageCenterY);
+
+ //Get Diagnostic info about process environment
+ console.log('process.platform is ' + process.platform);
+ const getUA = await page.evaluate(() => navigator.userAgent);
+ console.log('navigator.userAgent ' + getUA);
+ // Pan Imagery Hints
+ const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
+ expect(expectedAltText).toEqual(imageryHintsText);
+
+ // pan right
+ await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
+ await page.mouse.down();
+ await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
+ await page.mouse.up();
+ await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
+ const afterRightPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
+ expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
+
+ // pan left
+ await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
+ await page.mouse.down();
+ await page.mouse.move(imageCenterX, imageCenterY, 10);
+ await page.mouse.up();
+ await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
+ const afterLeftPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
+ expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
+
+ // pan up
+ await page.mouse.move(imageCenterX, imageCenterY);
+ await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
+ await page.mouse.down();
+ await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
+ await page.mouse.up();
+ await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
+ const afterUpPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
+ expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y);
+
+ // pan down
+ await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
+ await page.mouse.down();
+ await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
+ await page.mouse.up();
+ await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
+ const afterDownPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
+ expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
+
+ });
+
+ test('Can use + - buttons to zoom on the image @unstable', async ({ page }) => {
+ await buttonZoomOnImageAndAssert(page);
+ });
+
+ test('Can use the reset button to reset the image @unstable', async ({ page }, testInfo) => {
+ test.slow(testInfo.project === 'chrome-beta', "This test is slow in chrome-beta");
+ // Get initial image dimensions
+ const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
+
+ // Zoom in twice via button
+ await zoomIntoImageryByButton(page);
+ await zoomIntoImageryByButton(page);
+
+ // Get and assert zoomed in image dimensions
+ const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
+ expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
+ expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
+
+ // Reset pan and zoom and assert against initial image dimensions
+ await resetImageryPanAndZoom(page);
+ const finalBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
+ expect(finalBoundingBox).toEqual(initialBoundingBox);
+ });
+
+ 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();
+
+ // Click local clock
+ await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
+ await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
+
+ // Zoom in via button
+ await zoomIntoImageryByButton(page);
+ await expect(pausePlayButton).not.toHaveClass(/is-paused/);
+ });
+
+ test('Uses low fetch priority', async ({ page }) => {
+ const priority = await page.locator('.js-imageryView-image').getAttribute('fetchpriority');
+ await expect(priority).toBe('low');
+ });
+});
+
+test.describe('Example Imagery in Display Layout', () => {
+ let displayLayout;
+ test.beforeEach(async ({ page }) => {
+ // Go to baseURL
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ displayLayout = await createDomainObjectWithDefaults(page, { type: 'Display Layout' });
+ await page.goto(displayLayout.url);
+
+ /* Create Sine Wave Generator with minimum Image Load Delay */
+ // Click the Create button
+ await page.click('button:has-text("Create")');
+
+ // Click text=Example Imagery
+ await page.click('text=Example Imagery');
+
+ // Clear and set Image load delay to minimum value
+ await page.locator('input[type="number"]').fill('');
+ await page.locator('input[type="number"]').fill('5000');
+
+ // Click text=OK
+ await Promise.all([
+ page.waitForNavigation({waitUntil: 'networkidle'}),
+ page.click('text=OK'),
+ //Wait for Save Banner to appear
+ page.waitForSelector('.c-message-banner__message')
+ ]);
+
+ await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
+
+ await page.goto(displayLayout.url);
+ });
+
+ test('Imagery View operations @unstable', async ({ page }) => {
+ test.info().annotations.push({
+ type: 'issue',
+ description: 'https://github.com/nasa/openmct/issues/5265'
+ });
+
+ // Edit mode
+ await page.click('button[title="Edit"]');
+
+ // Click on example imagery to expose toolbar
+ await page.locator('.c-so-view__header').click();
+
+ // Adjust object height
+ await page.locator('div[title="Resize object height"] > input').click();
+ await page.locator('div[title="Resize object height"] > input').fill('50');
+
+ // Adjust object width
+ await page.locator('div[title="Resize object width"] > input').click();
+ await page.locator('div[title="Resize object width"] > input').fill('50');
+
+ await performImageryViewOperationsAndAssert(page);
+ });
+
+ test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => {
+ const thumbsWrapperLocator = page.locator('.c-imagery__thumbs-wrapper');
+ // Edit mode
+ await page.click('button[title="Edit"]');
+
+ // Click on example imagery to expose toolbar
+ await page.locator('.c-so-view__header').click();
+
+ // expect thumbnails not be visible when first added
+ expect.soft(thumbsWrapperLocator.isHidden()).toBeTruthy();
+
+ // Resize the example imagery vertically to change the thumbnail visibility
+ /*
+ The following arbitrary values are added to observe the separate visual
+ conditions of the thumbnails (hidden, small thumbnails, regular thumbnails).
+ Specifically, height is set to 50px for small thumbs and 100px for regular
+ */
+ await page.locator('div[title="Resize object height"] > input').click();
+ await page.locator('div[title="Resize object height"] > input').fill('50');
+
+ expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
+ await expect(thumbsWrapperLocator).toHaveClass(/is-small-thumbs/);
+
+ // Resize the example imagery vertically to change the thumbnail visibility
+ await page.locator('div[title="Resize object height"] > input').click();
+ await page.locator('div[title="Resize object height"] > input').fill('100');
+
+ expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
+ await expect(thumbsWrapperLocator).not.toHaveClass(/is-small-thumbs/);
+ });
+});
+
+test.describe('Example Imagery in Flexible layout', () => {
+ let flexibleLayout;
+ test.beforeEach(async ({ page }) => {
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ flexibleLayout = await createDomainObjectWithDefaults(page, { type: 'Flexible Layout' });
+ await page.goto(flexibleLayout.url);
+
+ /* Create Sine Wave Generator with minimum Image Load Delay */
+ // Click the Create button
+ await page.click('button:has-text("Create")');
+
+ // Click text=Example Imagery
+ await page.click('text=Example Imagery');
+
+ // Clear and set Image load delay to minimum value
+ await page.locator('input[type="number"]').fill('');
+ await page.locator('input[type="number"]').fill('5000');
+
+ // Click text=OK
+ await Promise.all([
+ page.waitForNavigation({waitUntil: 'networkidle'}),
+ page.click('text=OK'),
+ //Wait for Save Banner to appear
+ page.waitForSelector('.c-message-banner__message')
+ ]);
+
+ await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
+
+ await page.goto(flexibleLayout.url);
+ });
+ test('Imagery View operations @unstable', async ({ page, browserName }) => {
+ test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
+ test.info().annotations.push({
+ type: 'issue',
+ description: 'https://github.com/nasa/openmct/issues/5326'
+ });
+
+ await performImageryViewOperationsAndAssert(page);
+ });
+});
+
+test.describe('Example Imagery in Tabs View', () => {
+ let tabsView;
+ test.beforeEach(async ({ page }) => {
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ tabsView = await createDomainObjectWithDefaults(page, { type: 'Tabs View' });
+ await page.goto(tabsView.url);
+
+ /* Create Sine Wave Generator with minimum Image Load Delay */
+ // Click the Create button
+ await page.click('button:has-text("Create")');
+
+ // Click text=Example Imagery
+ await page.click('text=Example Imagery');
+
+ // Clear and set Image load delay to minimum value
+ await page.locator('input[type="number"]').fill('');
+ await page.locator('input[type="number"]').fill('5000');
+
+ // Click text=OK
+ await Promise.all([
+ page.waitForNavigation({waitUntil: 'networkidle'}),
+ page.click('text=OK'),
+ //Wait for Save Banner to appear
+ page.waitForSelector('.c-message-banner__message')
+ ]);
+
+ await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
+
+ await page.goto(tabsView.url);
+ });
+ test('Imagery View operations @unstable', async ({ page }) => {
+ await performImageryViewOperationsAndAssert(page);
+ });
+});
+
+test.describe('Example Imagery in Time Strip', () => {
+ let timeStripObject;
+ test.beforeEach(async ({ page }) => {
+ await page.goto('./', { waitUntil: 'networkidle' });
+ timeStripObject = await createDomainObjectWithDefaults(page, {
+ type: 'Time Strip',
+ name: 'Time Strip'.concat(' ', uuid())
+ });
+
+ await createDomainObjectWithDefaults(page, {
+ type: 'Example Imagery',
+ name: 'Example Imagery'.concat(' ', uuid()),
+ parent: timeStripObject.uuid
+ });
+ // Navigate to timestrip
+ await page.goto(timeStripObject.url);
+ });
+ test('Clicking a thumbnail loads the image in large view', async ({ page, browserName }) => {
+ test.info().annotations.push({
+ type: 'issue',
+ description: 'https://github.com/nasa/openmct/issues/5632'
+ });
+ await page.locator('.c-imagery-tsv-container').hover();
+ // get url of the hovered image
+ const hoveredImg = page.locator('.c-imagery-tsv div.c-imagery-tsv__image-wrapper:hover img');
+ const hoveredImgSrc = await hoveredImg.getAttribute('src');
+ expect(hoveredImgSrc).toBeTruthy();
+ await page.locator('.c-imagery-tsv-container').click();
+ // get image of view large container
+ const viewLargeImg = page.locator('img.c-imagery__main-image__image');
+ const viewLargeImgSrc = await viewLargeImg.getAttribute('src');
+ expect(viewLargeImgSrc).toBeTruthy();
+ expect(viewLargeImgSrc).toEqual(hoveredImgSrc);
+ });
+});
+
+/**
+ * Perform the common actions and assertions for the Imagery View.
+ * This function verifies the following in order:
+ * 1. Can zoom in/out using the zoom buttons
+ * 2. Can zoom in/out using the mouse wheel
+ * 3. Can pan the image using the pan hotkey + mouse drag
+ * 4. Clicking on the left arrow button pauses imagery and moves to the previous image
+ * 5. Imagery is updated as new images stream in, regardless of pause status
+ * 6. Old images are discarded when new images stream in
+ * 7. Image brightness/contrast can be adjusted by dragging the sliders
+ * @param {import('@playwright/test').Page} page
+ */
+async function performImageryViewOperationsAndAssert(page) {
+ // Click previous image button
+ const previousImageButton = page.locator('.c-nav--prev');
+ await previousImageButton.click();
+
+ // Verify previous image
+ const selectedImage = page.locator('.selected');
+ await expect(selectedImage).toBeVisible();
+
+ // Use the zoom buttons to zoom in and out
+ await buttonZoomOnImageAndAssert(page);
+
+ // Use Mouse Wheel to zoom in to previous image
+ await mouseZoomOnImageAndAssert(page, 2);
+
+ // Use alt+drag to move around image once zoomed in
+ await panZoomAndAssertImageProperties(page);
+
+ // Use Mouse Wheel to zoom out of previous image
+ await mouseZoomOnImageAndAssert(page, -2);
+
+ // Click next image button
+ 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();
+
+ // Zoom in on next image
+ await mouseZoomOnImageAndAssert(page, 2);
+
+ // Clicking on the left arrow should pause the imagery and go to previous image
+ await previousImageButton.click();
+ await expect(page.locator('.c-button.pause-play')).toHaveClass(/is-paused/);
+ await expect(selectedImage).toBeVisible();
+
+ // The imagery view should be updated when new images come in
+ const imageCount = await page.locator('.c-imagery__thumb').count();
+ await expect.poll(async () => {
+ const newImageCount = await page.locator('.c-imagery__thumb').count();
+
+ return newImageCount;
+ }, {
+ message: "verify that old images are discarded",
+ timeout: 7 * 1000
+ }).toBe(imageCount);
+
+ // Verify selected image is still displayed
+ await expect(selectedImage).toBeVisible();
+
+ // Unpause imagery
+ await page.locator('.pause-play').click();
+
+ //Get background-image url from background-image css prop
+ await assertBackgroundImageUrlFromBackgroundCss(page);
+
+ // Open the image filter menu
+ await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click();
+
+ // Drag the brightness and contrast sliders around and assert filter values
+ await dragBrightnessSliderAndAssertFilterValues(page);
+ await dragContrastSliderAndAssertFilterValues(page);
+}
+
+/**
+ * Drag the brightness slider to max, min, and midpoint and assert the filter values
+ * @param {import('@playwright/test').Page} page
+ */
+async function dragBrightnessSliderAndAssertFilterValues(page) {
+ const brightnessSlider = 'div.c-image-controls__slider-wrapper.icon-brightness > input';
+ const brightnessBoundingBox = await page.locator(brightnessSlider).boundingBox();
+ const brightnessMidX = brightnessBoundingBox.x + brightnessBoundingBox.width / 2;
+ const brightnessMidY = brightnessBoundingBox.y + brightnessBoundingBox.height / 2;
+
+ await page.locator(brightnessSlider).hover({trial: true});
+ await page.mouse.down();
+ await page.mouse.move(brightnessBoundingBox.x + brightnessBoundingBox.width, brightnessMidY);
+ await assertBackgroundImageBrightness(page, '500');
+ await page.mouse.move(brightnessBoundingBox.x, brightnessMidY);
+ await assertBackgroundImageBrightness(page, '0');
+ await page.mouse.move(brightnessMidX, brightnessMidY);
+ await assertBackgroundImageBrightness(page, '250');
+ await page.mouse.up();
+}
+
+/**
+ * Drag the contrast slider to max, min, and midpoint and assert the filter values
+ * @param {import('@playwright/test').Page} page
+ */
+async function dragContrastSliderAndAssertFilterValues(page) {
+ const contrastSlider = 'div.c-image-controls__slider-wrapper.icon-contrast > input';
+ const contrastBoundingBox = await page.locator(contrastSlider).boundingBox();
+ const contrastMidX = contrastBoundingBox.x + contrastBoundingBox.width / 2;
+ const contrastMidY = contrastBoundingBox.y + contrastBoundingBox.height / 2;
+
+ await page.locator(contrastSlider).hover({trial: true});
+ await page.mouse.down();
+ await page.mouse.move(contrastBoundingBox.x + contrastBoundingBox.width, contrastMidY);
+ await assertBackgroundImageContrast(page, '500');
+ await page.mouse.move(contrastBoundingBox.x, contrastMidY);
+ await assertBackgroundImageContrast(page, '0');
+ await page.mouse.move(contrastMidX, contrastMidY);
+ await assertBackgroundImageContrast(page, '250');
+ await page.mouse.up();
+}
+
+/**
+ * Gets the filter:brightness value of the current background-image and
+ * asserts against an expected value
+ * @param {import('@playwright/test').Page} page
+ * @param {String} expected The expected brightness value
+ */
+async function assertBackgroundImageBrightness(page, expected) {
+ const backgroundImage = page.locator('.c-imagery__main-image__background-image');
+
+ // Get the brightness filter value (i.e: filter: brightness(500%) => "500")
+ const actual = await backgroundImage.evaluate((el) => {
+ return el.style.filter.match(/brightness\((\d{1,3})%\)/)[1];
+ });
+ expect(actual).toBe(expected);
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function assertBackgroundImageUrlFromBackgroundCss(page) {
+ const backgroundImage = page.locator('.c-imagery__main-image__background-image');
+ let backgroundImageUrl = await backgroundImage.evaluate((el) => {
+ return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
+ });
+ let backgroundImageUrl1 = backgroundImageUrl.slice(1, -1); //forgive me, padre
+ console.log('backgroundImageUrl1 ' + backgroundImageUrl1);
+
+ let backgroundImageUrl2;
+ await expect.poll(async () => {
+ // Verify next image has updated
+ let backgroundImageUrlNext = await backgroundImage.evaluate((el) => {
+ return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
+ });
+ backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre
+
+ return backgroundImageUrl2;
+ }, {
+ message: "verify next image has updated",
+ timeout: 7 * 1000
+ }).not.toBe(backgroundImageUrl1);
+ console.log('backgroundImageUrl2 ' + backgroundImageUrl2);
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function panZoomAndAssertImageProperties(page) {
+ const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
+ expect(expectedAltText).toEqual(imageryHintsText);
+ const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
+ const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
+ const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
+
+ // Pan right
+ await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
+ await page.mouse.down();
+ await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
+ await page.mouse.up();
+ await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
+ const afterRightPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
+ expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
+
+ // Pan left
+ await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
+ await page.mouse.down();
+ await page.mouse.move(imageCenterX, imageCenterY, 10);
+ await page.mouse.up();
+ await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
+ const afterLeftPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
+ expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
+
+ // Pan up
+ await page.mouse.move(imageCenterX, imageCenterY);
+ await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
+ await page.mouse.down();
+ await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
+ await page.mouse.up();
+ await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
+ const afterUpPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
+ expect(afterUpPanBoundingBox.y).toBeGreaterThanOrEqual(afterLeftPanBoundingBox.y);
+
+ // Pan down
+ await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
+ await page.mouse.down();
+ await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
+ await page.mouse.up();
+ await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
+ const afterDownPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
+ expect(afterDownPanBoundingBox.y).toBeLessThanOrEqual(afterUpPanBoundingBox.y);
+}
+
+/**
+ * Use the mouse wheel to zoom in or out of an image and assert that the image
+ * has successfully zoomed in or out.
+ * @param {import('@playwright/test').Page} page
+ * @param {number} [factor = 2] The zoom factor. Positive for zoom in, negative for zoom out.
+*/
+async function mouseZoomOnImageAndAssert(page, factor = 2) {
+ // Zoom in
+ const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
+ await page.locator(backgroundImageSelector).hover({trial: true});
+ const deltaYStep = 100; // equivalent to 1x zoom
+ await page.mouse.wheel(0, deltaYStep * factor);
+ const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
+ const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
+ const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
+
+ // center the mouse pointer
+ await page.mouse.move(imageCenterX, imageCenterY);
+
+ // Wait for zoom animation to finish
+ await page.locator(backgroundImageSelector).hover({trial: true});
+ const imageMouseZoomed = await page.locator(backgroundImageSelector).boundingBox();
+
+ if (factor > 0) {
+ expect(imageMouseZoomed.height).toBeGreaterThan(originalImageDimensions.height);
+ expect(imageMouseZoomed.width).toBeGreaterThan(originalImageDimensions.width);
+ } else {
+ expect(imageMouseZoomed.height).toBeLessThan(originalImageDimensions.height);
+ expect(imageMouseZoomed.width).toBeLessThan(originalImageDimensions.width);
+ }
+}
+
+/**
+ * Zoom in and out of the image using the buttons, and assert that the image has
+ * been successfully zoomed in or out.
+ * @param {import('@playwright/test').Page} page
+ */
+async function buttonZoomOnImageAndAssert(page) {
+ // Get initial image dimensions
+ const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
+
+ // Zoom in twice via button
+ await zoomIntoImageryByButton(page);
+ await zoomIntoImageryByButton(page);
+
+ // Get and assert zoomed in image dimensions
+ const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
+ expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
+ expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
+
+ // Zoom out once via button
+ await zoomOutOfImageryByButton(page);
+
+ // Get and assert zoomed out image dimensions
+ const zoomedOutBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
+ expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
+ expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
+
+ // Zoom out again via button, assert against the initial image dimensions
+ await zoomOutOfImageryByButton(page);
+ const finalBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
+ expect(finalBoundingBox).toEqual(initialBoundingBox);
+}
+
+/**
+ * Gets the filter:contrast value of the current background-image and
+ * asserts against an expected value
+ * @param {import('@playwright/test').Page} page
+ * @param {String} expected The expected contrast value
+ */
+async function assertBackgroundImageContrast(page, expected) {
+ const backgroundImage = page.locator('.c-imagery__main-image__background-image');
+
+ // Get the contrast filter value (i.e: filter: contrast(500%) => "500")
+ const actual = await backgroundImage.evaluate((el) => {
+ return el.style.filter.match(/contrast\((\d{1,3})%\)/)[1];
+ });
+ expect(actual).toBe(expected);
+}
+
+/**
+ * Use the '+' button to zoom in. Hovers first if the toolbar is not visible
+ * and waits for the zoom animation to finish afterwards.
+ * @param {import('@playwright/test').Page} page
+ */
+async function zoomIntoImageryByButton(page) {
+ // FIXME: There should only be one set of imagery buttons, but there are two?
+ const zoomInBtn = page.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-in").nth(0);
+ const backgroundImage = page.locator(backgroundImageSelector);
+ if (!(await zoomInBtn.isVisible())) {
+ await backgroundImage.hover({trial: true});
+ }
+
+ await zoomInBtn.click();
+ await waitForAnimations(backgroundImage);
+}
+
+/**
+ * Use the '-' button to zoom out. Hovers first if the toolbar is not visible
+ * and waits for the zoom animation to finish afterwards.
+ * @param {import('@playwright/test').Page} page
+ */
+async function zoomOutOfImageryByButton(page) {
+ // FIXME: There should only be one set of imagery buttons, but there are two?
+ const zoomOutBtn = page.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-out").nth(0);
+ const backgroundImage = page.locator(backgroundImageSelector);
+ if (!(await zoomOutBtn.isVisible())) {
+ await backgroundImage.hover({trial: true});
+ }
+
+ await zoomOutBtn.click();
+ await waitForAnimations(backgroundImage);
+}
+
+/**
+ * Use the reset button to reset image pan and zoom. Hovers first if the toolbar is not visible
+ * and waits for the zoom animation to finish afterwards.
+ * @param {import('@playwright/test').Page} page
+ */
+async function resetImageryPanAndZoom(page) {
+ // FIXME: There should only be one set of imagery buttons, but there are two?
+ const panZoomResetBtn = page.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-reset").nth(0);
+ const backgroundImage = page.locator(backgroundImageSelector);
+ if (!(await panZoomResetBtn.isVisible())) {
+ await backgroundImage.hover({trial: true});
+ }
+
+ await panZoomResetBtn.click();
+ await waitForAnimations(backgroundImage);
+}
diff --git a/e2e/tests/plugins/ExportAsJSON/exportAsJson.e2e.spec.js b/e2e/tests/functional/plugins/importAndExportAsJSON/exportAsJson.e2e.spec.js
index 96c41c463..4e36221b6 100644
--- a/e2e/tests/plugins/ExportAsJSON/exportAsJson.e2e.spec.js
+++ b/e2e/tests/functional/plugins/importAndExportAsJSON/exportAsJson.e2e.spec.js
@@ -24,7 +24,9 @@
This test suite is dedicated to tests which verify the basic operations surrounding exportAsJSON.
*/
-const { test, expect } = require('@playwright/test');
+// FIXME: Remove this eslint exception once tests are implemented
+// eslint-disable-next-line no-unused-vars
+const { test, expect } = require('../../../../baseFixtures');
test.describe('ExportAsJSON', () => {
test.fixme('Create a basic object and verify that it can be exported as JSON from Tree', async ({ page }) => {
diff --git a/e2e/tests/plugins/ImportAsJSON/importAsJson.e2e.spec.js b/e2e/tests/functional/plugins/importAndExportAsJSON/importAsJson.e2e.spec.js
index 29516c2b0..ad0ff3f6b 100644
--- a/e2e/tests/plugins/ImportAsJSON/importAsJson.e2e.spec.js
+++ b/e2e/tests/functional/plugins/importAndExportAsJSON/importAsJson.e2e.spec.js
@@ -24,7 +24,9 @@
This test suite is dedicated to tests which verify the basic operations surrounding importAsJSON.
*/
-const { test, expect } = require('@playwright/test');
+// FIXME: Remove this eslint exception once tests are implemented
+// eslint-disable-next-line no-unused-vars
+const { test, expect } = require('../../../../baseFixtures');
test.describe('ExportAsJSON', () => {
test.fixme('Verify that domain object can be importAsJSON from Tree', async ({ page }) => {
diff --git a/e2e/tests/functional/plugins/lad/lad.e2e.spec.js b/e2e/tests/functional/plugins/lad/lad.e2e.spec.js
new file mode 100644
index 000000000..4ec084c1b
--- /dev/null
+++ b/e2e/tests/functional/plugins/lad/lad.e2e.spec.js
@@ -0,0 +1,120 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+const { test, expect } = require('../../../../pluginFixtures');
+const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
+
+test.describe('Testing LAD table @unstable', () => {
+ let sineWaveObject;
+ test.beforeEach(async ({ page }) => {
+ await page.goto('./', { waitUntil: 'networkidle' });
+ await setRealTimeMode(page);
+
+ // Create Sine Wave Generator
+ sineWaveObject = await createDomainObjectWithDefaults(page, {
+ type: 'Sine Wave Generator',
+ name: "Test Sine Wave Generator"
+ });
+ });
+ test('telemetry value exactly matches latest telemetry value received in real time', async ({ page }) => {
+ // Create LAD table
+ await createDomainObjectWithDefaults(page, {
+ type: 'LAD Table',
+ name: "Test LAD Table"
+ });
+ // Edit LAD table
+ 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 LAD table and save changes
+ await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper');
+ await page.locator('button[title="Save"]').click();
+ await page.locator('text=Save and Finish Editing').click();
+
+ // Subscribe to the Sine Wave Generator data
+ // On getting data, check if the value found in the LAD table is the most recent value
+ // from the Sine Wave Generator
+ const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
+ const subscribeTelemValue = await getTelemValuePromise;
+ const ladTableValuePromise = await page.waitForSelector(`text="${subscribeTelemValue}"`);
+ const ladTableValue = await ladTableValuePromise.textContent();
+
+ expect(ladTableValue).toBe(subscribeTelemValue);
+ });
+ test('telemetry value exactly matches latest telemetry value received in fixed time', async ({ page }) => {
+ // Create LAD table
+ await createDomainObjectWithDefaults(page, {
+ type: 'LAD Table',
+ name: "Test LAD Table"
+ });
+ // Edit LAD table
+ 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 LAD table and save changes
+ await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper');
+ await page.locator('button[title="Save"]').click();
+ await page.locator('text=Save and Finish Editing').click();
+
+ // Subscribe to the Sine Wave Generator data
+ const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
+ // Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window
+ await setStartOffset(page, { mins: '1' });
+ await setFixedTimeMode(page);
+
+ // On getting data, check if the value found in the LAD table is the most recent value
+ // from the Sine Wave Generator
+ const subscribeTelemValue = await getTelemValuePromise;
+ const ladTableValuePromise = await page.waitForSelector(`text="${subscribeTelemValue}"`);
+ const ladTableValue = await ladTableValuePromise.textContent();
+
+ expect(ladTableValue).toBe(subscribeTelemValue);
+ });
+});
+
+/**
+ * Util for subscribing to a telemetry object by object identifier
+ * Limitations: Currently only works to return telemetry once to the node scope
+ * To Do: See if there's a way to await this multiple times to allow for multiple
+ * values to be returned over time
+ * @param {import('@playwright/test').Page} page
+ * @param {string} objectIdentifier identifier for object
+ * @returns {Promise<string>} the formatted sin telemetry value
+ */
+async function subscribeToTelemetry(page, objectIdentifier) {
+ const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getTelemValue', resolve));
+
+ await page.evaluate(async (telemetryIdentifier) => {
+ const telemetryObject = await window.openmct.objects.get(telemetryIdentifier);
+ const metadata = window.openmct.telemetry.getMetadata(telemetryObject);
+ const formats = await window.openmct.telemetry.getFormatMap(metadata);
+ window.openmct.telemetry.subscribe(telemetryObject, (obj) => {
+ const sinVal = obj.sin;
+ const formattedSinVal = formats.sin.format(sinVal);
+ window.getTelemValue(formattedSinVal);
+ });
+ }, objectIdentifier);
+
+ return getTelemValuePromise;
+}
diff --git a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js
new file mode 100644
index 000000000..58e3e0f38
--- /dev/null
+++ b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js
@@ -0,0 +1,335 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 verify the basic operations surrounding Notebooks.
+*/
+
+// FIXME: Remove this eslint exception once tests are implemented
+// eslint-disable-next-line no-unused-vars
+const { test, expect } = require('../../../../baseFixtures');
+const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../../../appActions');
+const nbUtils = require('../../../../helper/notebookUtils');
+
+test.describe('Notebook CRUD Operations', () => {
+ test.fixme('Can create a Notebook Object', async ({ page }) => {
+ //Create domain object
+ //Newly created notebook should have one Section and one page, 'Unnamed Section'/'Unnamed Page'
+ });
+ test.fixme('Can update a Notebook Object', async ({ page }) => {});
+ test.fixme('Can view a perviously created Notebook Object', async ({ page }) => {});
+ test.fixme('Can Delete a Notebook Object', async ({ page }) => {
+ // Other than non-persistible objects
+ });
+});
+
+test.describe('Default Notebook', () => {
+ // General Default Notebook statements
+ // ## Useful commands:
+ // 1. - To check default notebook:
+ // `JSON.parse(localStorage.getItem('notebook-storage'));`
+ // 1. - Clear default notebook:
+ // `localStorage.setItem('notebook-storage', null);`
+ test.fixme('A newly created Notebook is automatically set as the default notebook if no other notebooks exist', async ({ page }) => {
+ //Create new notebook
+ //Verify Default Notebook Characteristics
+ });
+ test.fixme('A newly created Notebook is automatically set as the default notebook if at least one other notebook exists', async ({ page }) => {
+ //Create new notebook A
+ //Create second notebook B
+ //Verify Non-Default Notebook A Characteristics
+ //Verify Default Notebook B Characteristics
+ });
+ test.fixme('If a default notebook is deleted, the second most recent notebook becomes the default', async ({ page }) => {
+ //Create new notebook A
+ //Create second notebook B
+ //Delete Notebook B
+ //Verify Default Notebook A Characteristics
+ });
+});
+
+test.describe('Notebook section tests', () => {
+ //The following test cases are associated with Notebook Sections
+ test.beforeEach(async ({ page }) => {
+ //Navigate to baseURL
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ // Create Notebook
+ await createDomainObjectWithDefaults(page, {
+ type: 'Notebook',
+ name: "Test Notebook"
+ });
+ });
+ test('Default and new sections are automatically named Unnamed Section with Unnamed Page', async ({ page }) => {
+ // Check that the default section and page are created and the name matches the defaults
+ const defaultSectionName = await page.locator('.c-notebook__sections .c-list__item__name').textContent();
+ expect(defaultSectionName).toBe('Unnamed Section');
+ const defaultPageName = await page.locator('.c-notebook__pages .c-list__item__name').textContent();
+ expect(defaultPageName).toBe('Unnamed Page');
+
+ // Expand sidebar and add a section
+ await page.locator('.c-notebook__toggle-nav-button').click();
+ await page.locator('.js-sidebar-sections .c-icon-button.icon-plus').click();
+
+ // Check that new section and page within the new section match the defaults
+ const newSectionName = await page.locator('.c-notebook__sections .c-list__item__name').nth(1).textContent();
+ expect(newSectionName).toBe('Unnamed Section');
+ const newPageName = await page.locator('.c-notebook__pages .c-list__item__name').textContent();
+ expect(newPageName).toBe('Unnamed Page');
+ });
+ test.fixme('Section selection operations and associated behavior', async ({ page }) => {
+ //Create new notebook A
+ //Add Sections until 6 total with no default section/page
+ //Select 3rd section
+ //Delete 4th section
+ //3rd section is still selected
+ //Delete 3rd section
+ //1st section is selected
+ //Set 3rd section as default
+ //Delete 2nd section
+ //3rd section is still default
+ //Delete 3rd section
+ //1st is selected and there is no default notebook
+ });
+ test.fixme('Section rename operations', async ({ page }) => {
+ // Create a new notebook
+ // Add a section
+ // Rename the section but do not confirm
+ // Keyboard press 'Escape'
+ // Verify that the section name reverts to the default name
+ // Rename the section but do not confirm
+ // Keyboard press 'Enter'
+ // Verify that the section name is updated
+ // Rename the section to "" (empty string)
+ // Keyboard press 'Enter' to confirm
+ // Verify that the section name reverts to the default name
+ // Rename the section to something long that overflows the text box
+ // Verify that the section name is not truncated while input is active
+ // Confirm the section name edit
+ // Verify that the section name is truncated now that input is not active
+ });
+});
+
+test.describe('Notebook page tests', () => {
+ //The following test cases are associated with Notebook Pages
+ test.beforeEach(async ({ page }) => {
+ //Navigate to baseURL
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ // Create Notebook
+ await createDomainObjectWithDefaults(page, {
+ type: 'Notebook',
+ name: "Test Notebook"
+ });
+ });
+ //Test will need to be implemented after a refactor in #5713
+ // eslint-disable-next-line playwright/no-skipped-test
+ test.skip('Delete page popup is removed properly on clicking dropdown again', async ({ page }) => {
+ test.info().annotations.push({
+ type: 'issue',
+ description: 'https://github.com/nasa/openmct/issues/5713'
+ });
+ // Expand sidebar and add a second page
+ await page.locator('.c-notebook__toggle-nav-button').click();
+ await page.locator('text=Page Add >> button').click();
+
+ // Click on the 2nd page dropdown button and expect the Delete Page option to appear
+ await page.locator('button[title="Open context menu"]').nth(2).click();
+ await expect(page.locator('text=Delete Page')).toBeEnabled();
+ // Clicking on the same page a second time causes the same Delete Page option to recreate
+ await page.locator('button[title="Open context menu"]').nth(2).click();
+ await expect(page.locator('text=Delete Page')).toBeEnabled();
+ // Clicking on the first page causes the first delete button to detach and recreate on the first page
+ await page.locator('button[title="Open context menu"]').nth(1).click();
+ const numOfDeletePagePopups = await page.locator('li[title="Delete Page"]').count();
+ expect(numOfDeletePagePopups).toBe(1);
+ });
+ test.fixme('Page selection operations and associated behavior', async ({ page }) => {
+ //Create new notebook A
+ //Delete existing Page
+ //New 'Unnamed Page' automatically created
+ //Create 6 total Pages without a default page
+ //Select 3rd
+ //Delete 3rd
+ //First is now selected
+ //Set 3rd as default
+ //Select 2nd page
+ //Delete 2nd page
+ //3rd (default) is now selected
+ //Set 3rd as default page
+ //Select 3rd (default) page
+ //Delete 3rd page
+ //First is now selected and there is no default notebook
+ });
+ test.fixme('Page rename operations', async ({ page }) => {
+ // Create a new notebook
+ // Add a page
+ // Rename the page but do not confirm
+ // Keyboard press 'Escape'
+ // Verify that the page name reverts to the default name
+ // Rename the page but do not confirm
+ // Keyboard press 'Enter'
+ // Verify that the page name is updated
+ // Rename the page to "" (empty string)
+ // Keyboard press 'Enter' to confirm
+ // Verify that the page name reverts to the default name
+ // Rename the page to something long that overflows the text box
+ // Verify that the page name is not truncated while input is active
+ // Confirm the page name edit
+ // Verify that the page name is truncated now that input is not active
+ });
+});
+
+test.describe('Notebook search tests', () => {
+ test.fixme('Can search for a single result', async ({ page }) => {});
+ test.fixme('Can search for many results', async ({ page }) => {});
+ test.fixme('Can search for new and recently modified entries', async ({ page }) => {});
+ test.fixme('Can search for section text', async ({ page }) => {});
+ test.fixme('Can search for page text', async ({ page }) => {});
+ test.fixme('Can search for entry text', async ({ page }) => {});
+});
+
+test.describe('Notebook entry tests', () => {
+ test.fixme('When a new entry is created, it should be focused', async ({ page }) => {});
+ test('When an object is dropped into a notebook, a new entry is created and it should be focused @unstable', async ({ page }) => {
+ await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
+
+ // Create Notebook
+ const notebook = await createDomainObjectWithDefaults(page, {
+ type: 'Notebook',
+ name: "Embed Test Notebook"
+ });
+ // Create Overlay Plot
+ await createDomainObjectWithDefaults(page, {
+ type: 'Overlay Plot',
+ name: "Dropped Overlay Plot"
+ });
+
+ await expandTreePaneItemByName(page, 'My Items');
+
+ await page.goto(notebook.url);
+ await page.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', '.c-notebook__drag-area');
+
+ const embed = page.locator('.c-ne__embed__link');
+ const embedName = await embed.textContent();
+
+ await expect(embed).toHaveClass(/icon-plot-overlay/);
+ expect(embedName).toBe('Dropped Overlay Plot');
+ });
+ test('When an object is dropped into a notebooks existing entry, it should be focused @unstable', async ({ page }) => {
+ await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
+
+ // Create Notebook
+ const notebook = await createDomainObjectWithDefaults(page, {
+ type: 'Notebook',
+ name: "Embed Test Notebook"
+ });
+ // Create Overlay Plot
+ await createDomainObjectWithDefaults(page, {
+ type: 'Overlay Plot',
+ name: "Dropped Overlay Plot"
+ });
+
+ await expandTreePaneItemByName(page, 'My Items');
+
+ await page.goto(notebook.url);
+
+ await nbUtils.enterTextEntry(page, 'Entry to drop into');
+ await page.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', 'text=Entry to drop into');
+
+ const existingEntry = page.locator('.c-ne__content', { has: page.locator('text="Entry to drop into"') });
+ const embed = existingEntry.locator('.c-ne__embed__link');
+ const embedName = await embed.textContent();
+
+ await expect(embed).toHaveClass(/icon-plot-overlay/);
+ expect(embedName).toBe('Dropped Overlay Plot');
+ });
+ test.fixme('new entries persist through navigation events without save', async ({ page }) => {});
+ test.fixme('previous and new entries can be deleted', async ({ page }) => {});
+});
+
+test.describe('Snapshot Menu tests', () => {
+ test.fixme('When no default notebook is selected, Snapshot Menu dropdown should only have a single option', async ({ page }) => {
+ // There should be no default notebook
+ // Clear default notebook if exists using `localStorage.setItem('notebook-storage', null);`
+ // refresh page
+ // Click on 'Notebook Snaphot Menu'
+ // 'save to Notebook Snapshots' should be only option there
+ });
+ test.fixme('When default notebook is updated selected, Snapshot Menu dropdown should list it as the newest option', async ({ page }) => {
+ // Create 2a notebooks
+ // Set Notebook A as Default
+ // Open Snapshot Menu and note that Notebook A is listed
+ // Close Snapshot Menu
+ // Set Default Notebook to Notebook B
+ // Open Snapshot Notebook and note that Notebook B is listed
+ // Select Default Notebook Option and verify that Snapshot is added to Notebook B
+ });
+ test.fixme('Can add Snapshots via Snapshot Menu and details are correct', async ({ page }) => {
+ //Note this should be a visual test, too
+ // Create Telemetry object
+ // Create A notebook with many pages and sections.
+ // Set page and section defaults to be between first and last of many. i.e. 3 of 5
+ // Navigate to Telemetry object
+ // Select Default Notebook Option and verify that Snapshot is added to Notebook A
+ // Verify Snapshot Details appear correctly
+ });
+ test.fixme('Snapshots adjust time conductor', async ({ page }) => {
+ // Create Telemetry object
+ // Set Telemetry object's timeconductor to Fixed time with Start and Endtimes are recorded
+ // Embed Telemetry object into notebook
+ // Set Time Conductor to Local clock
+ // Click into embedded telemetry object and verify object appears with same fixed time from record
+ });
+});
+
+test.describe('Snapshot Container tests', () => {
+ test.fixme('5 Snapshots can be added to a container', async ({ page }) => {});
+ test.fixme('5 Snapshots can be added to a container and Deleted with Delete All action', async ({ page }) => {});
+ test.fixme('A snapshot can be Deleted from Container', async ({ page }) => {});
+ test.fixme('A snapshot can be Previewed from Container', async ({ page }) => {});
+ test.fixme('A snapshot Container can be open and closed', async ({ page }) => {});
+ test.fixme('Can add object to Snapshot container and pull into notebook and create a new entry', async ({ page }) => {
+ //Create Notebook
+ //Create Telemetry Object
+ //From Telemetry Object, use 'save to Notebook Snapshots'
+ //Snapshots indicator should blink, click on it to view snapshots
+ //Navigate to Notebook
+ //Drag and Drop onto droppable area for new entry
+ //New Entry created with given snapshot added
+ //Snapshot removed from container?
+ });
+ test.fixme('Can add object to Snapshot container and pull into notebook and existing entry', async ({ page }) => {
+ //Create Notebook
+ //Create Telemetry Object
+ //From Telemetry Object, use 'save to Notebook Snapshots'
+ //Snapshots indicator should blink, click on it to view snapshots
+ //Navigate to Notebook
+ //Drag and Drop into exiting entry
+ //Existing Entry updated with given snapshot
+ //Snapshot removed from container?
+ });
+ test.fixme('Verify Embedded options for PNG, JPG, and Annotate work correctly', async ({ page }) => {
+ //Add snapshot to container
+ //Verify PNG, JPG, and Annotate buttons work correctly
+ });
+});
diff --git a/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js b/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js
new file mode 100644
index 000000000..8594f5866
--- /dev/null
+++ b/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js
@@ -0,0 +1,193 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+const { test, expect } = require('../../../../pluginFixtures');
+const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
+const path = require('path');
+const nbUtils = require('../../../../helper/notebookUtils');
+
+const TEST_TEXT = 'Testing text for entries.';
+const TEST_TEXT_NAME = 'Test Page';
+const CUSTOM_NAME = 'CUSTOM_NAME';
+
+test.describe('Restricted Notebook', () => {
+ let notebook;
+ test.beforeEach(async ({ page }) => {
+ notebook = await startAndAddRestrictedNotebookObject(page);
+ });
+
+ test('Can be renamed @addInit', async ({ page }) => {
+ await expect(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`);
+ });
+
+ test('Can be deleted if there are no locked pages @addInit', async ({ page, openmctConfig }) => {
+ await openObjectTreeContextMenu(page, notebook.url);
+
+ const menuOptions = page.locator('.c-menu ul');
+ await expect.soft(menuOptions).toContainText('Remove');
+
+ const restrictedNotebookTreeObject = page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`);
+
+ // notebook tree object exists
+ expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1);
+
+ // Click Remove Text
+ await page.locator('text=Remove').click();
+
+ // Click 'OK' on confirmation window and wait for save banner to appear
+ await Promise.all([
+ page.waitForNavigation(),
+ page.locator('text=OK').click(),
+ page.waitForSelector('.c-message-banner__message')
+ ]);
+
+ // has been deleted
+ expect(await restrictedNotebookTreeObject.count()).toEqual(0);
+ });
+
+ test('Can be locked if at least one page has one entry @addInit', async ({ page }) => {
+
+ await nbUtils.enterTextEntry(page, TEST_TEXT);
+
+ const commitButton = page.locator('button:has-text("Commit Entries")');
+ expect(await commitButton.count()).toEqual(1);
+ });
+
+});
+
+test.describe('Restricted Notebook with at least one entry and with the page locked @addInit', () => {
+ let notebook;
+ test.beforeEach(async ({ page }) => {
+ notebook = await startAndAddRestrictedNotebookObject(page);
+ await nbUtils.enterTextEntry(page, TEST_TEXT);
+ await lockPage(page);
+
+ // open sidebar
+ await page.locator('button.c-notebook__toggle-nav-button').click();
+ });
+
+ test('Locked page should now be in a locked state @addInit @unstable', async ({ page }, testInfo) => {
+ // eslint-disable-next-line playwright/no-skipped-test
+ test.skip(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta");
+ // main lock message on page
+ const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed');
+ expect.soft(await lockMessage.count()).toEqual(1);
+
+ // lock icon on page in sidebar
+ const pageLockIcon = page.locator('ul.c-notebook__pages li div.icon-lock');
+ expect.soft(await pageLockIcon.count()).toEqual(1);
+
+ // no way to remove a restricted notebook with a locked page
+ await openObjectTreeContextMenu(page, notebook.url);
+ const menuOptions = page.locator('.c-menu ul');
+
+ await expect(menuOptions).not.toContainText('Remove');
+ });
+
+ test('Can still: add page, rename, add entry, delete unlocked pages @addInit', async ({ page }) => {
+ // Click text=Page Add >> button
+ await Promise.all([
+ page.waitForNavigation(),
+ page.locator('text=Page Add >> button').click()
+ ]);
+ // Click text=Unnamed Page >> nth=1
+ await page.locator('text=Unnamed Page').nth(1).click();
+ // Press a with modifiers
+ await page.locator('text=Unnamed Page').nth(1).fill(TEST_TEXT_NAME);
+
+ // expect to be able to rename unlocked pages
+ const newPageElement = page.locator(`text=${TEST_TEXT_NAME}`);
+ const newPageCount = await newPageElement.count();
+ await newPageElement.press('Enter'); // exit contenteditable state
+ expect.soft(newPageCount).toEqual(1);
+
+ // enter test text
+ await nbUtils.enterTextEntry(page, TEST_TEXT);
+
+ // expect new page to be lockable
+ const commitButton = page.locator('BUTTON:HAS-TEXT("COMMIT ENTRIES")');
+ expect.soft(await commitButton.count()).toEqual(1);
+
+ // Click text=Unnamed PageTest Page >> button
+ await page.locator('text=Unnamed PageTest Page >> button').click();
+ // Click text=Delete Page
+ await page.locator('text=Delete Page').click();
+ // Click text=Ok
+ await Promise.all([
+ page.waitForNavigation(),
+ page.locator('text=Ok').click()
+ ]);
+
+ // deleted page, should no longer exist
+ const deletedPageElement = page.locator(`text=${TEST_TEXT_NAME}`);
+ expect(await deletedPageElement.count()).toEqual(0);
+ });
+});
+
+test.describe('Restricted Notebook with a page locked and with an embed @addInit', () => {
+
+ test.beforeEach(async ({ page, openmctConfig }) => {
+ const { myItemsFolderName } = openmctConfig;
+ await startAndAddRestrictedNotebookObject(page);
+ await nbUtils.dragAndDropEmbed(page, myItemsFolderName);
+ });
+
+ test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => {
+ // Click .c-ne__embed__name .c-popup-menu-button
+ await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
+
+ const embedMenu = page.locator('body >> .c-menu');
+ await expect(embedMenu).toContainText('Remove This Embed');
+ });
+
+ test('Disallows embeds to be deleted if page locked @addInit', async ({ page }) => {
+ await lockPage(page);
+ // Click .c-ne__embed__name .c-popup-menu-button
+ await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
+
+ const embedMenu = page.locator('body >> .c-menu');
+ await expect(embedMenu).not.toContainText('Remove This Embed');
+ });
+
+});
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function startAndAddRestrictedNotebookObject(page) {
+ // eslint-disable-next-line no-undef
+ await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitRestrictedNotebook.js') });
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ return createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ */
+async function lockPage(page) {
+ const commitButton = page.locator('button:has-text("Commit Entries")');
+ await commitButton.click();
+
+ //Wait until Lock Banner is visible
+ await page.locator('text=Lock Page').click();
+}
diff --git a/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js b/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js
new file mode 100644
index 000000000..11533197c
--- /dev/null
+++ b/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js
@@ -0,0 +1,212 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 verify form functionality.
+*/
+
+const { test, expect } = require('../../../../pluginFixtures');
+const { createDomainObjectWithDefaults } = require('../../../../appActions');
+
+/**
+ * Creates a notebook object and adds an entry.
+ * @param {import('@playwright/test').Page} - page to load
+ * @param {number} [iterations = 1] - the number of entries to create
+ */
+async function createNotebookAndEntry(page, iterations = 1) {
+ //Go to baseURL
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ createDomainObjectWithDefaults(page, { type: 'Notebook' });
+
+ for (let iteration = 0; iteration < iterations; iteration++) {
+ // Click text=To start a new entry, click here or drag and drop any object
+ await page.locator('text=To start a new entry, click here or drag and drop any object').click();
+ const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = ${iteration}`;
+ await page.locator(entryLocator).click();
+ await page.locator(entryLocator).fill(`Entry ${iteration}`);
+ }
+}
+
+/**
+ * Creates a notebook object, adds an entry, and adds a tag.
+ * @param {import('@playwright/test').Page} page
+ * @param {number} [iterations = 1] - the number of entries (and tags) to create
+ */
+async function createNotebookEntryAndTags(page, iterations = 1) {
+ await createNotebookAndEntry(page, iterations);
+
+ for (let iteration = 0; iteration < iterations; iteration++) {
+ // Hover and click "Add Tag" button
+ // Hover is needed here to "slow down" the actions while running in headless mode
+ await page.hover(`button:has-text("Add Tag") >> nth = ${iteration}`);
+ await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
+
+ // Click inside the tag search input
+ await page.locator('[placeholder="Type to select tag"]').click();
+ // Select the "Driving" tag
+ await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
+
+ // Hover and click "Add Tag" button
+ // Hover is needed here to "slow down" the actions while running in headless mode
+ await page.hover(`button:has-text("Add Tag") >> nth = ${iteration}`);
+ await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
+ // Click inside the tag search input
+ await page.locator('[placeholder="Type to select tag"]').click();
+ // Select the "Science" tag
+ await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
+ }
+}
+
+test.describe('Tagging in Notebooks @addInit', () => {
+ test('Can load tags', async ({ page }) => {
+
+ await createNotebookAndEntry(page);
+ // Click text=To start a new entry, click here or drag and drop any object
+ await page.locator('button:has-text("Add Tag")').click();
+
+ // Click [placeholder="Type to select tag"]
+ await page.locator('[placeholder="Type to select tag"]').click();
+
+ await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Science");
+ await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
+ await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Driving");
+ });
+ test('Can add tags', async ({ page }) => {
+ await createNotebookEntryAndTags(page);
+
+ await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
+ await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving");
+
+ // Click button:has-text("Add Tag")
+ await page.locator('button:has-text("Add Tag")').click();
+ // Click [placeholder="Type to select tag"]
+ await page.locator('[placeholder="Type to select tag"]').click();
+
+ await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Science");
+ await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Driving");
+ await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
+ });
+ test('Can search for tags', async ({ page }) => {
+ await createNotebookEntryAndTags(page);
+ // Click [aria-label="OpenMCT Search"] input[type="search"]
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
+ // Fill [aria-label="OpenMCT Search"] input[type="search"]
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
+ await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science");
+ await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving");
+
+ // Click [aria-label="OpenMCT Search"] input[type="search"]
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
+ // Fill [aria-label="OpenMCT Search"] input[type="search"]
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc');
+ await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science");
+ await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving");
+
+ // Click [aria-label="OpenMCT Search"] input[type="search"]
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
+ // Fill [aria-label="OpenMCT Search"] input[type="search"]
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq');
+ await expect(page.locator('[aria-label="Search Result"]')).toBeHidden();
+ await expect(page.locator('[aria-label="Search Result"]')).toBeHidden();
+ });
+
+ test('Can delete tags', async ({ page }) => {
+ await createNotebookEntryAndTags(page);
+ await page.locator('[aria-label="Notebook Entries"]').click();
+ // Delete Driving
+ await page.hover('.c-tag__label:has-text("Driving")');
+ await page.locator('.c-tag__label:has-text("Driving") ~ .c-completed-tag-deletion').click();
+
+ await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
+ await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving");
+
+ // Fill [aria-label="OpenMCT Search"] input[type="search"]
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
+ await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
+ });
+
+ test('Can delete objects with tags and neither return in search', async ({ page }) => {
+ await createNotebookEntryAndTags(page);
+ // Delete Notebook
+ await page.locator('button[title="More options"]').click();
+ await page.locator('li[title="Remove this object from its containing object."]').click();
+ await page.locator('button:has-text("OK")').click();
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ // Fill [aria-label="OpenMCT Search"] input[type="search"]
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed');
+ await expect(page.locator('text=No results found')).toBeVisible();
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sci');
+ await expect(page.locator('text=No results found')).toBeVisible();
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('dri');
+ await expect(page.locator('text=No results found')).toBeVisible();
+ });
+ test('Tags persist across reload', async ({ page }) => {
+ //Go to baseURL
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ await createDomainObjectWithDefaults(page, { type: 'Clock' });
+
+ const ITERATIONS = 4;
+ await createNotebookEntryAndTags(page, ITERATIONS);
+
+ for (let iteration = 0; iteration < ITERATIONS; iteration++) {
+ const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
+ await expect(page.locator(entryLocator)).toContainText("Science");
+ await expect(page.locator(entryLocator)).toContainText("Driving");
+ }
+
+ await Promise.all([
+ page.waitForNavigation(),
+ page.goto('./#/browse/mine?hideTree=false'),
+ page.click('.c-disclosure-triangle')
+ ]);
+ // Click Unnamed Clock
+ await page.click('text="Unnamed Clock"');
+
+ // Click Unnamed Notebook
+ await page.click('text="Unnamed Notebook"');
+
+ for (let iteration = 0; iteration < ITERATIONS; iteration++) {
+ const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
+ await expect(page.locator(entryLocator)).toContainText("Science");
+ await expect(page.locator(entryLocator)).toContainText("Driving");
+ }
+
+ //Reload Page
+ await Promise.all([
+ page.reload(),
+ page.waitForLoadState('networkidle')
+ ]);
+
+ // Click Unnamed Notebook
+ await page.click('text="Unnamed Notebook"');
+
+ for (let iteration = 0; iteration < ITERATIONS; iteration++) {
+ const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
+ await expect(page.locator(entryLocator)).toContainText("Science");
+ await expect(page.locator(entryLocator)).toContainText("Driving");
+ }
+
+ });
+});
diff --git a/e2e/tests/plugins/plot/autoscale.e2e.spec.js b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js
index ecbaee404..81640b95c 100644
--- a/e2e/tests/plugins/plot/autoscale.e2e.spec.js
+++ b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js
@@ -24,21 +24,7 @@
Testsuite for plot autoscale.
*/
-const { test: _test, expect } = require('@playwright/test');
-
-// create a new `test` API that will not append platform details to snapshot
-// file names, only for the tests in this file, so that the same snapshots will
-// be used for all platforms.
-const test = _test.extend({
- _autoSnapshotSuffix: [
- async ({}, use, testInfo) => {
- testInfo.snapshotSuffix = '';
- await use();
- },
- { auto: true }
- ]
-});
-
+const { test, expect } = require('../../../../pluginFixtures');
test.use({
viewport: {
width: 1280,
@@ -47,27 +33,32 @@ test.use({
});
test.describe('ExportAsJSON', () => {
- test.slow('User can set autoscale with a valid range @snapshot', async ({ page }) => {
- await page.goto('/', { waitUntil: 'networkidle' });
+ test('User can set autoscale with a valid range @snapshot', async ({ page, openmctConfig }) => {
+ const { myItemsFolderName } = openmctConfig;
+
+ //This is necessary due to the size of the test suite.
+ test.slow();
+
+ await page.goto('./', { waitUntil: 'networkidle' });
await setTimeRange(page);
- await createSinewaveOverlayPlot(page);
+ await createSinewaveOverlayPlot(page, myItemsFolderName);
await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']);
await turnOffAutoscale(page);
+ // Make sure that after turning off autoscale, the user selected range values start at the same values the plot had prior.
+ await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']);
+
const canvas = page.locator('canvas').nth(1);
- // Make sure that after turning off autoscale, the user selected range values start at the same values the plot had prior.
- await Promise.all([
- testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']),
- new Promise(r => setTimeout(r, 100))
- .then(() => canvas.screenshot())
- .then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-prepan.png', { maxDiffPixels: 40 }))
- ]);
+ await canvas.hover({trial: true});
+
+ expect(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-prepan.png', { animations: 'disabled' });
+ //Alt Drag Start
await page.keyboard.down('Alt');
await canvas.dragTo(canvas, {
@@ -81,15 +72,15 @@ test.describe('ExportAsJSON', () => {
}
});
+ //Alt Drag End
await page.keyboard.up('Alt');
// Ensure the drag worked.
- await Promise.all([
- testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']),
- new Promise(r => setTimeout(r, 100))
- .then(() => canvas.screenshot())
- .then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-panned.png', { maxDiffPixels: 40 }))
- ]);
+ await testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']);
+
+ await canvas.hover({trial: true});
+
+ expect(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-panned.png', { animations: 'disabled' });
});
});
@@ -112,8 +103,9 @@ async function setTimeRange(page, start = '2022-03-29 22:00:00.000Z', end = '202
/**
* @param {import('@playwright/test').Page} page
+ * @param {string} myItemsFolderName
*/
-async function createSinewaveOverlayPlot(page) {
+async function createSinewaveOverlayPlot(page, myItemsFolderName) {
// click create button
await page.locator('button:has-text("Create")').click();
@@ -149,7 +141,7 @@ async function createSinewaveOverlayPlot(page) {
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
// focus the overlay plot
- await page.locator('text=Open MCT My Items >> span').nth(3).click();
+ await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Overlay Plot').first().click()
diff --git a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-darwin b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-darwin
new file mode 100644
index 000000000..d6d4dd21e
--- /dev/null
+++ b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-darwin
Binary files differ
diff --git a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-linux.png b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-linux.png
new file mode 100644
index 000000000..4a882660e
--- /dev/null
+++ b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome-linux.png
Binary files differ
diff --git a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-darwin b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-darwin
new file mode 100644
index 000000000..ef5455e5c
--- /dev/null
+++ b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-darwin
Binary files differ
diff --git a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-linux.png b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-linux.png
new file mode 100644
index 000000000..ffdedffd2
--- /dev/null
+++ b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome-linux.png
Binary files differ
diff --git a/e2e/tests/plugins/plot/logPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js
index 5341139b3..c76bf82cc 100644
--- a/e2e/tests/plugins/plot/logPlot.e2e.spec.js
+++ b/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js
@@ -25,11 +25,15 @@ Tests to verify log plot functionality. Note this test suite if very much under
necessarily be used for reference when writing new tests in this area.
*/
-const { test, expect } = require('@playwright/test');
-
+const { test, expect } = require('../../../../pluginFixtures');
test.describe('Log plot tests', () => {
- test.slow('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ page }) => {
- await makeOverlayPlot(page);
+ test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ page, openmctConfig }) => {
+ const { myItemsFolderName } = openmctConfig;
+
+ //Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
+ test.slow();
+
+ await makeOverlayPlot(page, myItemsFolderName);
await testRegularTicks(page);
await enableEditMode(page);
await enableLogMode(page);
@@ -40,21 +44,14 @@ test.describe('Log plot tests', () => {
await testLogTicks(page);
await saveOverlayPlot(page);
await testLogTicks(page);
- //await testLogPlotPixels(page);
-
- // refresh page and wait for charts and ticks to load
- await page.waitForTimeout(1 * 1000);
- await page.reload({ waitUntil: 'networkidle'});
- await page.waitForSelector('.gl-plot-chart-area');
- await page.waitForSelector('.gl-plot-y-tick-label');
-
- // test log ticks hold up after refresh
- await testLogTicks(page);
- //await testLogPlotPixels(page);
});
- test.skip('Verify that log mode option is reflected in import/export JSON', async ({ page }) => {
- await makeOverlayPlot(page);
+ // Leaving test as 'TODO' for now.
+ // NOTE: Not eligible for community contributions.
+ test.fixme('Verify that log mode option is reflected in import/export JSON', async ({ page, openmctConfig }) => {
+ const { myItemsFolderName } = openmctConfig;
+
+ await makeOverlayPlot(page, myItemsFolderName);
await enableEditMode(page);
await enableLogMode(page);
await saveOverlayPlot(page);
@@ -72,10 +69,11 @@ test.describe('Log plot tests', () => {
/**
* Makes an overlay plot with a sine wave generator and clicks on the overlay plot in the sidebar so it is the active thing displayed.
* @param {import('@playwright/test').Page} page
+ * @param {string} myItemsFolderName
*/
-async function makeOverlayPlot(page) {
+async function makeOverlayPlot(page, myItemsFolderName) {
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
- await page.goto('/', { waitUntil: 'networkidle' });
+ await page.goto('./', { waitUntil: 'networkidle' });
// Set a specific time range for consistency, otherwise it will change
// on every test to a range based on the current time.
@@ -112,14 +110,14 @@ async function makeOverlayPlot(page) {
// set amplitude to 6, offset 4, period 2
- await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click();
- await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').fill('6');
+ await page.locator('div:nth-child(5) .c-form-row__controls .form-control .field input').click();
+ await page.locator('div:nth-child(5) .c-form-row__controls .form-control .field input').fill('6');
- await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').click();
- await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').fill('4');
+ await page.locator('div:nth-child(6) .c-form-row__controls .form-control .field input').click();
+ await page.locator('div:nth-child(6) .c-form-row__controls .form-control .field input').fill('4');
- await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').click();
- await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').fill('2');
+ await page.locator('div:nth-child(7) .c-form-row__controls .form-control .field input').click();
+ await page.locator('div:nth-child(7) .c-form-row__controls .form-control .field input').fill('2');
// Click OK to make generator
@@ -135,7 +133,7 @@ async function makeOverlayPlot(page) {
// click on overlay plot
- await page.locator('text=Open MCT My Items >> span').nth(3).click();
+ await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Overlay Plot').first().click()
@@ -238,6 +236,8 @@ async function saveOverlayPlot(page) {
/**
* @param {import('@playwright/test').Page} page
*/
+// FIXME: Remove this eslint exception once implemented
+// eslint-disable-next-line no-unused-vars
async function testLogPlotPixels(page) {
const pixelsMatch = await page.evaluate(async () => {
// TODO get canvas pixels at a few locations to make sure they're the correct color, to test that the plot comes out as expected.
diff --git a/e2e/tests/functional/plugins/plot/missingPlotObj.e2e.spec.js b/e2e/tests/functional/plugins/plot/missingPlotObj.e2e.spec.js
new file mode 100644
index 000000000..0e1eec5c7
--- /dev/null
+++ b/e2e/tests/functional/plugins/plot/missingPlotObj.e2e.spec.js
@@ -0,0 +1,157 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 log plot functionality when objects are missing
+*/
+
+const { test, expect } = require('../../../../pluginFixtures');
+
+test.describe('Handle missing object for plots', () => {
+ test('Displays empty div for missing stacked plot item @unstable', async ({ page, browserName, openmctConfig }) => {
+ // eslint-disable-next-line playwright/no-skipped-test
+ test.skip(browserName === 'firefox', 'Firefox failing due to console events being missed');
+
+ const { myItemsFolderName } = openmctConfig;
+ const errorLogs = [];
+
+ page.on("console", (message) => {
+ if (message.type() === 'warning' && message.text().includes('Missing domain object')) {
+ errorLogs.push(message.text());
+ }
+ });
+
+ //Make stacked plot
+ await makeStackedPlot(page, myItemsFolderName);
+
+ //Gets local storage and deletes the last sine wave generator in the stacked plot
+ const localStorage = await page.evaluate(() => window.localStorage);
+ const parsedData = JSON.parse(localStorage.mct);
+ const keys = Object.keys(parsedData);
+ const lastKey = keys[keys.length - 1];
+
+ delete parsedData[lastKey];
+
+ //Sets local storage with missing object
+ await page.evaluate(
+ `window.localStorage.setItem('mct', '${JSON.stringify(parsedData)}')`
+ );
+
+ //Reloads page and clicks on stacked plot
+ await Promise.all([
+ page.reload(),
+ page.waitForLoadState('networkidle')
+ ]);
+
+ //Verify Main section is there on load
+ await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Stacked Plot');
+
+ await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
+ await Promise.all([
+ page.waitForNavigation(),
+ page.locator('text=Unnamed Stacked Plot').first().click()
+ ]);
+
+ //Check that there is only one stacked item plot with a plot, the missing one will be empty
+ await expect(page.locator(".c-plot--stacked-container:has(.gl-plot)")).toHaveCount(1);
+ //Verify that console.warn is thrown
+ expect(errorLogs).toHaveLength(1);
+ });
+});
+
+/**
+ * This is used the create a stacked plot object
+ * @private
+ */
+async function makeStackedPlot(page, myItemsFolderName) {
+ // fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ // create stacked plot
+ await page.locator('button.c-create-button').click();
+ await page.locator('li:has-text("Stacked Plot")').click();
+
+ await Promise.all([
+ page.waitForNavigation({ waitUntil: 'networkidle'}),
+ page.locator('text=OK').click(),
+ //Wait for Save Banner to appear
+ page.waitForSelector('.c-message-banner__message')
+ ]);
+
+ // save the stacked plot
+ await saveStackedPlot(page);
+
+ // create a sinewave generator
+ await createSineWaveGenerator(page);
+
+ // click on stacked plot
+ await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
+ await Promise.all([
+ page.waitForNavigation(),
+ page.locator('text=Unnamed Stacked Plot').first().click()
+ ]);
+
+ // create a second sinewave generator
+ await createSineWaveGenerator(page);
+
+ // click on stacked plot
+ await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
+ await Promise.all([
+ page.waitForNavigation(),
+ page.locator('text=Unnamed Stacked Plot').first().click()
+ ]);
+}
+
+/**
+ * This is used to save a stacked plot object
+ * @private
+ */
+async function saveStackedPlot(page) {
+ // save stacked plot
+ await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
+
+ await Promise.all([
+ page.locator('text=Save and Finish Editing').click(),
+ //Wait for Save Banner to appear
+ 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' });
+}
+
+/**
+ * This is used to create a sine wave generator object
+ * @private
+ */
+async function createSineWaveGenerator(page) {
+ //Create sine wave generator
+ await page.locator('button.c-create-button').click();
+ await page.locator('li:has-text("Sine Wave Generator")').click();
+
+ await Promise.all([
+ page.waitForNavigation({ waitUntil: 'networkidle'}),
+ page.locator('text=OK').click(),
+ //Wait for Save Banner to appear
+ page.waitForSelector('.c-message-banner__message')
+ ]);
+}
diff --git a/e2e/tests/functional/plugins/plot/plotLegendSwatch.e2e.spec.js b/e2e/tests/functional/plugins/plot/plotLegendSwatch.e2e.spec.js
new file mode 100644
index 000000000..25a5e348d
--- /dev/null
+++ b/e2e/tests/functional/plugins/plot/plotLegendSwatch.e2e.spec.js
@@ -0,0 +1,110 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 log plot functionality. Note this test suite if very much under active development and should not
+necessarily be used for reference when writing new tests in this area.
+*/
+
+const { test, expect } = require('../../../../pluginFixtures');
+
+test.describe('Legend color in sync with plot color', () => {
+ test('Testing', async ({ page }) => {
+ await makeOverlayPlot(page);
+
+ // navigate to plot series color palette
+ await page.click('.l-browse-bar__actions__edit');
+ await page.locator('li.c-tree__item.menus-to-left .c-disclosure-triangle').click();
+ await page.locator('.c-click-swatch--menu').click();
+ await page.locator('.c-palette__item[style="background: rgb(255, 166, 61);"]').click();
+
+ // gets color for swatch located in legend
+ const element = await page.waitForSelector('.plot-series-color-swatch');
+ const color = await element.evaluate((el) => {
+ return window.getComputedStyle(el).getPropertyValue('background-color');
+ });
+
+ expect(color).toBe('rgb(255, 166, 61)');
+ });
+});
+
+async function saveOverlayPlot(page) {
+ // save overlay plot
+ await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
+
+ await Promise.all([
+ page.locator('text=Save and Finish Editing').click(),
+ //Wait for Save Banner to appear
+ 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' });
+}
+
+async function makeOverlayPlot(page) {
+ // fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
+ await page.goto('/', { waitUntil: 'networkidle' });
+
+ // create overlay plot
+
+ await page.locator('button.c-create-button').click();
+ await page.locator('li:has-text("Overlay Plot")').click();
+ await Promise.all([
+ page.waitForNavigation({ waitUntil: 'networkidle'}),
+ page.locator('text=OK').click(),
+ //Wait for Save Banner to appear
+ 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 the overlay plot
+
+ await saveOverlayPlot(page);
+
+ // create a sinewave generator
+
+ await page.locator('button.c-create-button').click();
+ await page.locator('li:has-text("Sine Wave Generator")').click();
+
+ // Click OK to make generator
+
+ await Promise.all([
+ page.waitForNavigation({ waitUntil: 'networkidle'}),
+ page.locator('text=OK').click(),
+ //Wait for Save Banner to appear
+ 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'});
+
+ // click on overlay plot
+
+ await page.locator('text=Open MCT My Items >> span').nth(3).click();
+ await Promise.all([
+ page.waitForNavigation(),
+ page.locator('text=Unnamed Overlay Plot').first().click()
+ ]);
+}
diff --git a/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js b/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js
new file mode 100644
index 000000000..15656603e
--- /dev/null
+++ b/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js
@@ -0,0 +1,75 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 { createDomainObjectWithDefaults } = require('../../../../appActions');
+const { test, expect } = require('../../../../pluginFixtures');
+
+test.describe('Telemetry Table', () => {
+ test('unpauses and filters data when paused by button and user changes bounds', async ({ page }) => {
+ test.info().annotations.push({
+ type: 'issue',
+ description: 'https://github.com/nasa/openmct/issues/5113'
+ });
+
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
+ await createDomainObjectWithDefaults(page, {
+ type: 'Sine Wave Generator',
+ parent: table.uuid
+ });
+
+ // focus the Telemetry Table
+ page.goto(table.url);
+
+ // Click pause button
+ const pauseButton = page.locator('button.c-button.icon-pause');
+ await pauseButton.click();
+
+ const tableWrapper = page.locator('div.c-table-wrapper');
+ 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();
+ 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 expect(tableWrapper).not.toHaveClass(/is-paused/);
+
+ // Get the most recent telemetry date
+ const latestTelemetryDate = await page.locator('table.c-telemetry-table__body > tbody > tr').last().locator('td').nth(1).getAttribute('title');
+
+ // Verify that it is <= our new end bound
+ const latestMilliseconds = Date.parse(latestTelemetryDate);
+ const endBoundMilliseconds = Date.parse(endDate);
+ expect(latestMilliseconds).toBeLessThanOrEqual(endBoundMilliseconds);
+ });
+});
diff --git a/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js b/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js
new file mode 100644
index 000000000..ea50e8530
--- /dev/null
+++ b/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js
@@ -0,0 +1,170 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+const { test, expect } = require('../../../../baseFixtures');
+const { setFixedTimeMode, setRealTimeMode, setStartOffset, setEndOffset } = require('../../../../appActions');
+
+test.describe('Time conductor operations', () => {
+ test('validate start time does not exceeds end time', async ({ page }) => {
+ // Go to baseURL
+ await page.goto('./', { waitUntil: 'networkidle' });
+ const year = new Date().getFullYear();
+
+ let startDate = 'xxxx-01-01 01:00:00.000Z';
+ startDate = year + startDate.substring(4);
+
+ 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());
+
+ // invalid start date
+ startDate = (year + 1) + startDate.substring(4);
+ await startTimeLocator.fill(startDate.toString());
+ await endTimeLocator.click();
+
+ const startDateValidityStatus = await startTimeLocator.evaluate((element) => element.checkValidity());
+ expect(startDateValidityStatus).not.toBeTruthy();
+
+ // fix to valid start date
+ startDate = (year - 1) + startDate.substring(4);
+ await startTimeLocator.fill(startDate.toString());
+
+ // invalid end date
+ endDate = (year - 2) + endDate.substring(4);
+ await endTimeLocator.fill(endDate.toString());
+ await startTimeLocator.click();
+
+ const endDateValidityStatus = await endTimeLocator.evaluate((element) => element.checkValidity());
+ expect(endDateValidityStatus).not.toBeTruthy();
+ });
+});
+
+// Testing instructions:
+// Try to change the realtime offsets when in realtime (local clock) mode.
+test.describe('Time conductor input fields real-time mode', () => {
+ test('validate input fields in real-time mode', async ({ page }) => {
+ const startOffset = {
+ secs: '23'
+ };
+
+ const endOffset = {
+ secs: '31'
+ };
+
+ // Go to baseURL
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ // Switch to real-time mode
+ await setRealTimeMode(page);
+
+ // Set start time offset
+ 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');
+
+ // 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');
+ });
+
+ /**
+ * Verify that offsets and url params are preserved when switching
+ * between fixed timespan and real-time mode.
+ */
+ test('preserve offsets and url params when switching between fixed and real-time mode', async ({ page }) => {
+ const startOffset = {
+ mins: '30',
+ secs: '23'
+ };
+
+ const endOffset = {
+ secs: '01'
+ };
+
+ // Convert offsets to milliseconds
+ const startDelta = (30 * 60 * 1000) + (23 * 1000);
+ const endDelta = (1 * 1000);
+
+ // Go to baseURL
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ // Switch to real-time mode
+ await setRealTimeMode(page);
+
+ // Set start time offset
+ await setStartOffset(page, startOffset);
+
+ // Set end time offset
+ await setEndOffset(page, endOffset);
+
+ // Switch to fixed timespan mode
+ await setFixedTimeMode(page);
+
+ // Switch back to 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');
+
+ // Verify updated end time offset persists after mode switch
+ await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01');
+
+ // Verify url parameters persist after mode switch
+ await page.waitForNavigation({ waitUntil: 'networkidle' });
+ expect(page.url()).toContain(`startDelta=${startDelta}`);
+ expect(page.url()).toContain(`endDelta=${endDelta}`);
+ });
+
+ test.fixme('time conductor history in fixed time mode will track changing start and end times', async ({ page }) => {
+ // change start time, verify it's tracked in history
+ // change end time, verify it's tracked in history
+ });
+
+ test.fixme('time conductor history in realtime mode will track changing start and end times', async ({ page }) => {
+ // change start offset, verify it's tracked in history
+ // change end offset, verify it's tracked in history
+ });
+
+ test.fixme('time conductor history allows you to set a historical timeframe', async ({ page }) => {
+ // make sure there are historical history options
+ // select an option and make sure the time conductor start and end bounds are updated correctly
+ });
+
+ test.fixme('time conductor history allows you to set a realtime offsets', async ({ page }) => {
+ // make sure there are realtime history options
+ // select an option and verify the offsets are updated correctly
+ });
+});
diff --git a/e2e/tests/functional/plugins/timer/timer.e2e.spec.js b/e2e/tests/functional/plugins/timer/timer.e2e.spec.js
new file mode 100644
index 000000000..16c6b5def
--- /dev/null
+++ b/e2e/tests/functional/plugins/timer/timer.e2e.spec.js
@@ -0,0 +1,156 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+const { test, expect } = require('../../../../pluginFixtures');
+const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
+
+test.describe('Timer', () => {
+ let timer;
+ test.beforeEach(async ({ page }) => {
+ await page.goto('./', { waitUntil: 'networkidle' });
+ timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
+ });
+
+ test('Can perform actions on the Timer', async ({ page, openmctConfig }) => {
+ test.info().annotations.push({
+ type: 'issue',
+ description: 'https://github.com/nasa/openmct/issues/4313'
+ });
+
+ const timerUrl = timer.url;
+
+ await test.step("From the tree context menu", async () => {
+ await triggerTimerContextMenuAction(page, timerUrl, 'Start');
+ await triggerTimerContextMenuAction(page, timerUrl, 'Pause');
+ await triggerTimerContextMenuAction(page, timerUrl, 'Restart at 0');
+ await triggerTimerContextMenuAction(page, timerUrl, 'Stop');
+ });
+
+ await test.step("From the 3dot menu", async () => {
+ await triggerTimer3dotMenuAction(page, 'Start');
+ await triggerTimer3dotMenuAction(page, 'Pause');
+ await triggerTimer3dotMenuAction(page, 'Restart at 0');
+ await triggerTimer3dotMenuAction(page, 'Stop');
+ });
+
+ await test.step("From the object view", async () => {
+ await triggerTimerViewAction(page, 'Start');
+ await triggerTimerViewAction(page, 'Pause');
+ await triggerTimerViewAction(page, 'Restart at 0');
+ });
+ });
+});
+
+/**
+ * Actions that can be performed on a timer from context menus.
+ * @typedef {'Start' | 'Stop' | 'Pause' | 'Restart at 0'} TimerAction
+ */
+
+/**
+ * Actions that can be performed on a timer from the object view.
+ * @typedef {'Start' | 'Pause' | 'Restart at 0'} TimerViewAction
+ */
+
+/**
+ * Trigger a timer action from the tree context menu
+ * @param {import('@playwright/test').Page} page
+ * @param {TimerAction} action
+ */
+async function triggerTimerContextMenuAction(page, timerUrl, action) {
+ const menuAction = `.c-menu ul li >> text="${action}"`;
+ await openObjectTreeContextMenu(page, timerUrl);
+ await page.locator(menuAction).click();
+ assertTimerStateAfterAction(page, action);
+}
+
+/**
+ * Trigger a timer action from the 3dot menu
+ * @param {import('@playwright/test').Page} page
+ * @param {TimerAction} action
+ */
+async function triggerTimer3dotMenuAction(page, action) {
+ const menuAction = `.c-menu ul li >> text="${action}"`;
+ const threeDotMenuButton = 'button[title="More options"]';
+ let isActionAvailable = false;
+ let iterations = 0;
+ // Dismiss/open the 3dot menu until the action is available
+ // or a maximum number of iterations is reached
+ while (!isActionAvailable && iterations <= 20) {
+ await page.click('.c-object-view');
+ await page.click(threeDotMenuButton);
+ isActionAvailable = await page.locator(menuAction).isVisible();
+ iterations++;
+ }
+
+ await page.locator(menuAction).click();
+ assertTimerStateAfterAction(page, action);
+}
+
+/**
+ * Trigger a timer action from the object view
+ * @param {import('@playwright/test').Page} page
+ * @param {TimerViewAction} action
+ */
+async function triggerTimerViewAction(page, action) {
+ await page.locator('.c-timer').hover({trial: true});
+ const buttonTitle = buttonTitleFromAction(action);
+ await page.click(`button[title="${buttonTitle}"]`);
+ assertTimerStateAfterAction(page, action);
+}
+
+/**
+ * Takes in a TimerViewAction and returns the button title
+ * @param {TimerViewAction} action
+ */
+function buttonTitleFromAction(action) {
+ switch (action) {
+ case 'Start':
+ return 'Start';
+ case 'Pause':
+ return 'Pause';
+ case 'Restart at 0':
+ return 'Reset';
+ }
+}
+
+/**
+ * Verify the timer state after a timer action has been performed.
+ * @param {import('@playwright/test').Page} page
+ * @param {TimerAction} action
+ */
+async function assertTimerStateAfterAction(page, action) {
+ let timerStateClass;
+ switch (action) {
+ case 'Start':
+ case 'Restart at 0':
+ timerStateClass = "is-started";
+ break;
+ case 'Stop':
+ timerStateClass = 'is-stopped';
+ break;
+ case 'Pause':
+ timerStateClass = 'is-paused';
+ break;
+ }
+
+ await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass));
+}
diff --git a/e2e/tests/functional/search.e2e.spec.js b/e2e/tests/functional/search.e2e.spec.js
new file mode 100644
index 000000000..a94459969
--- /dev/null
+++ b/e2e/tests/functional/search.e2e.spec.js
@@ -0,0 +1,271 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 verify search functionalities.
+ */
+
+const { test, expect } = require('../../pluginFixtures');
+const { createDomainObjectWithDefaults } = require('../../appActions');
+const { v4: uuid } = require('uuid');
+
+test.describe('Grand Search', () => {
+ test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page, openmctConfig }) => {
+ const { myItemsFolderName } = openmctConfig;
+
+ await createObjectsForSearch(page, myItemsFolderName);
+
+ // Click [aria-label="OpenMCT Search"] input[type="search"]
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
+ // Fill [aria-label="OpenMCT Search"] input[type="search"]
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl');
+ await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(`Clock A ${myItemsFolderName} Red Folder Blue Folder`);
+ await expect(page.locator('[aria-label="Search Result"] >> nth=1')).toContainText(`Clock B ${myItemsFolderName} Red Folder Blue Folder`);
+ await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText(`Clock C ${myItemsFolderName} Red Folder Blue Folder`);
+ await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText(`Clock D ${myItemsFolderName} Red Folder Blue Folder`);
+ // Click text=Elements >> nth=0
+ await page.locator('text=Elements').first().click();
+ await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
+
+ await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
+ await page.locator('[aria-label="Clock A clock result"] >> text=Clock A').click();
+ await expect(page.locator('.js-preview-window')).toBeVisible();
+
+ // Click [aria-label="Close"]
+ await page.locator('[aria-label="Close"]').click();
+ await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeVisible();
+ await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(`Clock A ${myItemsFolderName} Red Folder Blue Folder`);
+
+ // Click [aria-label="OpenMCT Search"] a >> nth=0
+ await page.locator('[aria-label="OpenMCT Search"] a').first().click();
+ await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden();
+
+ // 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();
+
+ // Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
+ await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
+ // Click text=Save and Finish Editing
+ await page.locator('text=Save and Finish Editing').click();
+ // Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
+ await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
+ // Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"]
+ await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
+ await Promise.all([
+ page.waitForNavigation(),
+ page.locator('text=Clock A').click()
+ ]);
+ await expect(page.locator('.is-object-type-clock')).toBeVisible();
+
+ await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Disp');
+ await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText('Unnamed Display Layout');
+ await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toContainText('Folder');
+
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Clock C');
+ await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(`Clock C ${myItemsFolderName} Red Folder Blue Folder`);
+
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cloc');
+ await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(`Clock A ${myItemsFolderName} Red Folder Blue Folder`);
+ await expect(page.locator('[aria-label="Search Result"] >> nth=1')).toContainText(`Clock B ${myItemsFolderName} Red Folder Blue Folder`);
+ await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText(`Clock C ${myItemsFolderName} Red Folder Blue Folder`);
+ await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText(`Clock D ${myItemsFolderName} Red Folder Blue Folder`);
+ });
+});
+
+test.describe("Search Tests @unstable", () => {
+ const searchResultSelector = '.c-gsearch-result__title';
+
+ test('Validate empty search result', async ({ page }) => {
+ // Go to baseURL
+ await page.goto("./", { waitUntil: "networkidle" });
+
+ // Invalid search for objects
+ await page.type("input[type=search]", 'not found');
+
+ // Wait for search to complete
+ await waitForSearchCompletion(page);
+
+ // Get the search results
+ const searchResults = await page.locator(searchResultSelector);
+
+ // Verify that no results are found
+ expect(await searchResults.count()).toBe(0);
+
+ // Verify proper message appears
+ await expect(page.locator('text=No results found')).toBeVisible();
+ });
+
+ test('Validate single object in search result @couchdb', async ({ page }) => {
+ //Go to baseURL
+ await page.goto("./", { waitUntil: "networkidle" });
+
+ // Create a folder object
+ const folderName = uuid();
+ await createDomainObjectWithDefaults(page, {
+ type: 'folder',
+ name: folderName
+ });
+
+ // Full search for object
+ await page.type("input[type=search]", folderName);
+
+ // Wait for search to complete
+ await waitForSearchCompletion(page);
+
+ // Get the search results
+ const searchResults = page.locator(searchResultSelector);
+
+ // Verify that one result is found
+ expect(await searchResults.count()).toBe(1);
+ await expect(searchResults).toHaveText(folderName);
+ });
+
+ test("Validate multiple objects in search results return partial matches", async ({ page }) => {
+ test.info().annotations.push({
+ type: 'issue',
+ description: 'https://github.com/nasa/openmct/issues/4667'
+ });
+
+ // Go to baseURL
+ await page.goto("/", { waitUntil: "networkidle" });
+
+ // Create folder objects
+ const folderName = "e928a26e-e924-4ea0";
+ const folderName2 = "e928a26e-e924-4001";
+
+ await createFolderObject(page, folderName);
+ await createFolderObject(page, folderName2);
+
+ // Partial search for objects
+ await page.type("input[type=search]", 'e928a26e');
+
+ // Wait for search to finish
+ await waitForSearchCompletion(page);
+
+ // Get the search results
+ const searchResults = await page.locator(searchResultSelector);
+
+ // Verify that the search result/s correctly match the search query
+ expect(await searchResults.count()).toBe(2);
+ await expect(await searchResults.first()).toHaveText(folderName);
+ await expect(await searchResults.last()).toHaveText(folderName2);
+ });
+});
+
+async function createFolderObject(page, folderName) {
+ // Open Create menu
+ await page.locator('button:has-text("Create")').click();
+
+ // Select Folder object
+ await page.locator('text=Folder').nth(1).click();
+
+ // Click folder title to enter edit mode
+ await page.locator('text=Properties Title Notes >> input[type="text"]').click();
+
+ // Enter folder name
+ await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folderName);
+
+ // Create folder object
+ await page.locator('text=OK').click();
+}
+
+async function waitForSearchCompletion(page) {
+ // Wait loading spinner to disappear
+ await page.waitForSelector('.c-tree-and-search__loading', { state: 'detached' });
+}
+
+/**
+ * Creates some domain objects for searching
+ * @param {import('@playwright/test').Page} page
+ */
+async function createObjectsForSearch(page, myItemsFolderName) {
+ //Go to baseURL
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ await page.locator('button:has-text("Create")').click();
+ await page.locator('li:has-text("Folder") >> nth=1').click();
+ await Promise.all([
+ page.waitForNavigation(),
+ await page.locator('text=Properties Title Notes >> input[type="text"]').fill('Red Folder'),
+ await page.locator(`text=Save In Open MCT ${myItemsFolderName} >> span`).nth(3).click(),
+ page.locator('button:has-text("OK")').click()
+ ]);
+
+ await page.locator('button:has-text("Create")').click();
+ await page.locator('li:has-text("Folder") >> nth=2').click();
+ await Promise.all([
+ page.waitForNavigation(),
+ await page.locator('text=Properties Title Notes >> input[type="text"]').fill('Blue Folder'),
+ await page.locator('form[name="mctForm"] >> text=Red Folder').click(),
+ page.locator('button:has-text("OK")').click()
+ ]);
+
+ await page.locator('button:has-text("Create")').click();
+ await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
+ await Promise.all([
+ page.waitForNavigation(),
+ await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock A'),
+ await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
+ page.locator('button:has-text("OK")').click()
+ ]);
+
+ await page.locator('button:has-text("Create")').click();
+ await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
+ await Promise.all([
+ page.waitForNavigation(),
+ await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock B'),
+ await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
+ page.locator('button:has-text("OK")').click()
+ ]);
+
+ await page.locator('button:has-text("Create")').click();
+ await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
+ await Promise.all([
+ page.waitForNavigation(),
+ await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock C'),
+ await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
+ page.locator('button:has-text("OK")').click()
+ ]);
+
+ await page.locator('button:has-text("Create")').click();
+ await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
+ await Promise.all([
+ page.waitForNavigation(),
+ await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock D'),
+ await page.locator('form[name="mctForm"] >> text=Blue Folder').click(),
+ page.locator('button:has-text("OK")').click()
+ ]);
+
+ await Promise.all([
+ page.waitForNavigation(),
+ page.locator(`a:has-text("${myItemsFolderName}") >> nth=0`).click()
+ ]);
+ // Click button:has-text("Create")
+ await page.locator('button:has-text("Create")').click();
+ // Click li:has-text("Notebook")
+ await page.locator('li:has-text("Display Layout")').click();
+ // Click button:has-text("OK")
+ await Promise.all([
+ page.waitForNavigation(),
+ page.locator('button:has-text("OK")').click()
+ ]);
+}
diff --git a/e2e/tests/smoke.e2e.spec.js b/e2e/tests/functional/smoke.e2e.spec.js
index 0d8591b63..12cb25d12 100644
--- a/e2e/tests/smoke.e2e.spec.js
+++ b/e2e/tests/functional/smoke.e2e.spec.js
@@ -33,17 +33,27 @@ comfortable running this test during a live mission?" Avoid creating or deleting
Make no assumptions about the order that elements appear in the DOM.
*/
-const { test, expect } = require('@playwright/test');
+const { test, expect } = require('../../pluginFixtures');
test('Verify that the create button appears and that the Folder Domain Object is available for selection', async ({ page }) => {
//Go to baseURL
- await page.goto('/', { waitUntil: 'networkidle' });
+ await page.goto('./', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
// Verify that Create Folder appears in the dropdown
- const locator = page.locator(':nth-match(:text("Folder"), 2)');
- await expect(locator).toBeEnabled();
+ await expect(page.locator(':nth-match(:text("Folder"), 2)')).toBeEnabled();
+});
+
+test('Verify that My Items Tree appears @ipad', async ({ page, openmctConfig }) => {
+ const { myItemsFolderName } = openmctConfig;
+ //Test.slow annotation is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
+ test.slow();
+ //Go to baseURL
+ await page.goto('./');
+
+ //My Items to be visible
+ await expect(page.locator(`a:has-text("${myItemsFolderName}")`)).toBeEnabled();
});
diff --git a/e2e/tests/functional/tree.e2e.spec.js b/e2e/tests/functional/tree.e2e.spec.js
new file mode 100644
index 000000000..691f7f127
--- /dev/null
+++ b/e2e/tests/functional/tree.e2e.spec.js
@@ -0,0 +1,138 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+const { test, expect } = require('../../pluginFixtures.js');
+const {
+ createDomainObjectWithDefaults,
+ openObjectTreeContextMenu
+} = require('../../appActions.js');
+
+test.describe('Tree operations', () => {
+ test('Renaming an object reorders the tree @unstable', async ({ page, openmctConfig }) => {
+ const { myItemsFolderName } = openmctConfig;
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: 'Foo'
+ });
+
+ await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: 'Bar'
+ });
+
+ await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: 'Baz'
+ });
+
+ const clock1 = await createDomainObjectWithDefaults(page, {
+ type: 'Clock',
+ name: 'aaa'
+ });
+
+ await createDomainObjectWithDefaults(page, {
+ type: 'Clock',
+ name: 'www'
+ });
+
+ // Expand the root folder
+ await expandTreePaneItemByName(page, myItemsFolderName);
+
+ await test.step("Reorders objects with the same tree depth", async () => {
+ await getAndAssertTreeItems(page, ['aaa', 'Bar', 'Baz', 'Foo', 'www']);
+ await renameObjectFromContextMenu(page, clock1.url, 'zzz');
+ await getAndAssertTreeItems(page, ['Bar', 'Baz', 'Foo', 'www', 'zzz']);
+ });
+
+ await test.step("Reorders links to objects as well as original objects", async () => {
+ await page.click('role=treeitem[name=/Bar/]');
+ await page.dragAndDrop('role=treeitem[name=/www/]', '.c-object-view');
+ await page.dragAndDrop('role=treeitem[name=/zzz/]', '.c-object-view');
+ await page.click('role=treeitem[name=/Baz/]');
+ await page.dragAndDrop('role=treeitem[name=/www/]', '.c-object-view');
+ await page.dragAndDrop('role=treeitem[name=/zzz/]', '.c-object-view');
+ await page.click('role=treeitem[name=/Foo/]');
+ await page.dragAndDrop('role=treeitem[name=/www/]', '.c-object-view');
+ await page.dragAndDrop('role=treeitem[name=/zzz/]', '.c-object-view');
+ // Expand the unopened folders
+ await expandTreePaneItemByName(page, 'Bar');
+ await expandTreePaneItemByName(page, 'Baz');
+ await expandTreePaneItemByName(page, 'Foo');
+
+ await renameObjectFromContextMenu(page, clock1.url, '___');
+ await getAndAssertTreeItems(page,
+ [
+ "___",
+ "Bar",
+ "___",
+ "www",
+ "Baz",
+ "___",
+ "www",
+ "Foo",
+ "___",
+ "www",
+ "www"
+ ]);
+ });
+ });
+});
+
+/**
+ * @param {import('@playwright/test').Page} page
+ * @param {Array<string>} expected
+ */
+async function getAndAssertTreeItems(page, expected) {
+ const treeItems = page.locator('[role="treeitem"]');
+ const allTexts = await treeItems.allInnerTexts();
+ // Get rid of root folder ('My Items') as its position will not change
+ allTexts.shift();
+ expect(allTexts).toEqual(expected);
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ * @param {string} name
+ */
+async function expandTreePaneItemByName(page, name) {
+ const treePane = page.locator('#tree-pane');
+ const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
+ const expandTriangle = treeItem.locator('.c-disclosure-triangle');
+ await expandTriangle.click();
+}
+
+/**
+ * @param {import('@playwright/test').Page} page
+ * @param {string} myItemsFolderName
+ * @param {string} url
+ * @param {string} newName
+ */
+async function renameObjectFromContextMenu(page, url, newName) {
+ await openObjectTreeContextMenu(page, url);
+ await page.click('li:text("Edit Properties")');
+ const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
+ await nameInput.fill("");
+ await nameInput.fill(newName);
+ await page.click('[aria-label="Save"]');
+}
diff --git a/e2e/tests/moveObjects.e2e.spec.js b/e2e/tests/moveObjects.e2e.spec.js
deleted file mode 100644
index d6e28ee38..000000000
--- a/e2e/tests/moveObjects.e2e.spec.js
+++ /dev/null
@@ -1,141 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2022, 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 verify the basic operations surrounding moving objects.
-*/
-
-const { test, expect } = require('@playwright/test');
-
-test.describe('Move item tests', () => {
- test('Create a basic object and verify that it can be moved to another folder', async ({ page }) => {
- // Go to Open MCT
- await page.goto('/');
-
- // Create a new folder in the root my items folder
- let folder1 = "Folder1";
- await page.locator('button:has-text("Create")').click();
- await page.locator('li.icon-folder').click();
-
- await page.locator('text=Properties Title Notes >> input[type="text"]').click();
- await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder1);
- await Promise.all([
- page.waitForNavigation(),
- page.locator('text=OK').click(),
- 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'});
-
- // Create another folder with a new name at default location, which is currently inside Folder 1
- let folder2 = "Folder2";
- await page.locator('button:has-text("Create")').click();
- await page.locator('li.icon-folder').click();
- await page.locator('text=Properties Title Notes >> input[type="text"]').click();
- await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder2);
- await Promise.all([
- page.waitForNavigation(),
- page.locator('text=OK').click(),
- 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'});
-
- // Move Folder 2 from Folder 1 to My Items
- await page.locator('text=Open MCT My Items >> span').nth(3).click();
- await page.locator('.c-tree__scrollable div div:nth-child(2) .c-tree__item .c-tree__item__view-control').click();
-
- await page.locator(`a:has-text("${folder2}")`).click({
- button: 'right'
- });
- await page.locator('li.icon-move').click();
- await page.locator('form[name="mctForm"] >> text=My Items').click();
- await Promise.all([
- page.waitForNavigation(),
- page.locator('text=OK').click()
- ]);
-
- // Expect that Folder 2 is in My Items, the root folder
- expect(page.locator(`text=My Items >> nth=0:has(text=${folder2})`)).toBeTruthy();
- });
- test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({ page }) => {
- // Go to Open MCT
- await page.goto('/');
-
- // Create Telemetry Table
- let telemetryTable = 'Test Telemetry Table';
- await page.locator('button:has-text("Create")').click();
- await page.locator('li:has-text("Telemetry Table")').click();
- await page.locator('text=Properties Title Notes >> input[type="text"]').click();
- await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable);
- await Promise.all([
- page.waitForNavigation(),
- page.locator('text=OK').click()
- ]);
-
- // Finish editing and save Telemetry Table
- await page.locator('.c-button--menu.c-button--major.icon-save').click();
- await page.locator('text=Save and Finish Editing').click();
-
- // Create New Folder Basic Domain Object
- let folder = 'Test Folder';
- await page.locator('button:has-text("Create")').click();
- await page.locator('li:has-text("Folder")').click();
- await page.locator('text=Properties Title Notes >> input[type="text"]').click();
- await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder);
-
- // See if it's possible to put the folder in the Telemetry object during creation (Soft Assert)
- await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
- let okButton = await page.locator('button.c-button.c-button--major:has-text("OK")');
- let okButtonStateDisabled = await okButton.isDisabled();
- expect.soft(okButtonStateDisabled).toBeTruthy();
-
- // Continue test regardless of assertion and create it in My Items
- await page.locator('form[name="mctForm"] >> text=My Items').click();
- await Promise.all([
- page.waitForNavigation(),
- page.locator('text=OK').click()
- ]);
-
- // Open My Items
- await page.locator('text=Open MCT My Items >> span').nth(3).click();
-
- // Select Folder Object and select Move from context menu
- await Promise.all([
- page.waitForNavigation(),
- page.locator(`a:has-text("${folder}")`).click()
- ]);
- await page.locator('.c-tree__item.is-navigated-object .c-tree__item__label .c-tree__item__type-icon').click({
- button: 'right'
- });
- await page.locator('li.icon-move').click();
-
- // See if it's possible to put the folder in the Telemetry object after creation
- await page.locator('text=Location Open MCT My Items >> span').nth(3).click();
- await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
- let okButton2 = await page.locator('button.c-button.c-button--major:has-text("OK")');
- let okButtonStateDisabled2 = await okButton2.isDisabled();
- expect(okButtonStateDisabled2).toBeTruthy();
- });
-});
diff --git a/e2e/tests/performance/imagery.perf.spec.js b/e2e/tests/performance/imagery.perf.spec.js
new file mode 100644
index 000000000..396f249af
--- /dev/null
+++ b/e2e/tests/performance/imagery.perf.spec.js
@@ -0,0 +1,177 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 performance tests to ensure that testability of performance
+is not broken upstream on Open MCT. Any assumptions made downstream will be tested here
+
+TODO:
+ - Update resolution of performance config
+ - Add Performance Observer on init to push all performance marks
+ - Move client CDP connection to before or to a fixture
+ -
+
+*/
+
+const { test, expect } = require('@playwright/test');
+
+const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
+
+test.describe('Performance tests', () => {
+ test.beforeEach(async ({ page, browser }, testInfo) => {
+ // Go to baseURL
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ // Click a:has-text("My Items")
+ await page.locator('a:has-text("My Items")').click({
+ button: 'right'
+ });
+
+ // Click text=Import from JSON
+ await page.locator('text=Import from JSON').click();
+
+ // Upload Performance Display Layout.json
+ await page.setInputFiles('#fileElem', filePath);
+
+ // Click text=OK
+ await page.locator('text=OK').click();
+
+ await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();
+
+ //Create a Chrome Performance Timeline trace to store as a test artifact
+ console.log("\n==== Devtools: startTracing ====\n");
+ await browser.startTracing(page, {
+ path: `${testInfo.outputPath()}-trace.json`,
+ screenshots: true
+ });
+ });
+ test.afterEach(async ({ page, browser}) => {
+ console.log("\n==== Devtools: stopTracing ====\n");
+ await browser.stopTracing();
+
+ /* Measurement Section
+ / The following section includes a block of performance measurements.
+ */
+ //Get time difference between viewlarge actionability and evaluate time
+ await page.evaluate(() => (window.performance.measure("machine-time-difference", "viewlarge.start", "viewLarge.start.test")));
+
+ //Get StartTime
+ const startTime = await page.evaluate(() => window.performance.timing.navigationStart);
+ console.log('window.performance.timing.navigationStart', startTime);
+
+ //Get All Performance Marks
+ const getAllMarksJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("mark")));
+ const getAllMarks = JSON.parse(getAllMarksJson);
+ console.log('window.performance.getEntriesByType("mark")', getAllMarks);
+
+ //Get All Performance Measures
+ const getAllMeasuresJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("measure")));
+ const getAllMeasures = JSON.parse(getAllMeasuresJson);
+ console.log('window.performance.getEntriesByType("measure")', getAllMeasures);
+
+ });
+ /* The following test will navigate to a previously created Performance Display Layout and measure the
+ / following metrics:
+ / - ElementResourceTiming
+ / - Interaction Timing
+ */
+ test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {
+ const client = await page.context().newCDPSession(page);
+ // Tell the DevTools session to record performance metrics
+ // https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics
+ await client.send('Performance.enable');
+ // Go to baseURL
+ await page.goto('./');
+
+ // Search Available after Launch
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
+ await page.evaluate(() => window.performance.mark("search-available"));
+ // Fill Search input
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Display Layout');
+ await page.evaluate(() => window.performance.mark("search-entered"));
+ //Search Result Appears and is clicked
+ await Promise.all([
+ page.waitForNavigation(),
+ page.locator('a:has-text("Performance Display Layout")').first().click(),
+ page.evaluate(() => window.performance.mark("click-search-result"))
+ ]);
+
+ //Time to Example Imagery Frame loads within Display Layout
+ await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
+ //Time to Example Imagery object loads
+ await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
+
+ //Get background-image url from background-image css prop
+ const backgroundImage = await page.locator('.c-imagery__main-image__background-image');
+ let backgroundImageUrl = await backgroundImage.evaluate((el) => {
+ return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
+ });
+ backgroundImageUrl = backgroundImageUrl.slice(1, -1); //forgive me, padre
+ console.log('backgroundImageurl ' + backgroundImageUrl);
+
+ //Get ResourceTiming of background-image jpg
+ const resourceTimingJson = await page.evaluate((bgImageUrl) =>
+ JSON.stringify(window.performance.getEntriesByName(bgImageUrl).pop()),
+ backgroundImageUrl
+ );
+ console.log('resourceTimingJson ' + resourceTimingJson);
+
+ //Open Large view
+ await page.locator('button:has-text("Large View")').click(); //This action includes the performance.mark named 'viewLarge.start'
+ await page.evaluate(() => window.performance.mark("viewLarge.start.test")); //This is a mark only to compare evaluate timing
+
+ //Time to Imagery Rendered in Large Frame
+ await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
+ await page.evaluate(() => window.performance.mark("background-image-frame"));
+
+ //Time to Example Imagery object loads
+ await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
+ await page.evaluate(() => window.performance.mark("background-image-visible"));
+
+ // Get Current number of images in thumbstrip
+ await page.waitForSelector('.c-imagery__thumb');
+ const thumbCount = await page.locator('.c-imagery__thumb').count();
+ console.log('number of thumbs rendered ' + thumbCount);
+ await page.locator('.c-imagery__thumb').last().click();
+
+ //Get ResourceTiming of all jpg resources
+ const resourceTimingJson2 = await page.evaluate(() =>
+ JSON.stringify(window.performance.getEntriesByType('resource'))
+ );
+ const resourceTiming = JSON.parse(resourceTimingJson2);
+ const jpgResourceTiming = resourceTiming.find((element) =>
+ element.name.includes('.jpg')
+ );
+ console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming));
+
+ // Click Close Icon
+ await page.locator('[aria-label="Close"]').click();
+ await page.evaluate(() => window.performance.mark("view-large-close-button"));
+
+ //await client.send('HeapProfiler.enable');
+ await client.send('HeapProfiler.collectGarbage');
+
+ let performanceMetrics = await client.send('Performance.getMetrics');
+ console.log(performanceMetrics.metrics);
+
+ });
+});
diff --git a/e2e/tests/performance/memleak-imagery.perf.spec.js b/e2e/tests/performance/memleak-imagery.perf.spec.js
new file mode 100644
index 000000000..066c38591
--- /dev/null
+++ b/e2e/tests/performance/memleak-imagery.perf.spec.js
@@ -0,0 +1,119 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+/*
+This test suite is an initial example for memory leak testing using performance. This configuration and execution must
+be kept separate from the traditional performance measurements to avoid any "observer" effects associated with tracing
+or profiling playwright and/or the browser.
+
+Based on a pattern identified in https://github.com/trentmwillis/devtools-protocol-demos/blob/master/testing-demos/memory-leak-by-heap.js
+and https://github.com/paulirish/automated-chrome-profiling/issues/3
+
+Best path forward: https://github.com/cowchimp/headless-devtools/blob/master/src/Memory/example.js
+
+*/
+
+const { test, expect } = require('@playwright/test');
+
+const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
+
+// eslint-disable-next-line playwright/no-skipped-test
+test.describe.skip('Memory Performance tests', () => {
+ test.beforeEach(async ({ page, browser }, testInfo) => {
+ // Go to baseURL
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ // Click a:has-text("My Items")
+ await page.locator('a:has-text("My Items")').click({
+ button: 'right'
+ });
+
+ // Click text=Import from JSON
+ await page.locator('text=Import from JSON').click();
+
+ // Upload Performance Display Layout.json
+ await page.setInputFiles('#fileElem', filePath);
+
+ // Click text=OK
+ await page.locator('text=OK').click();
+
+ await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();
+ });
+
+ test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {
+
+ await page.goto('./', {waitUntil: 'networkidle'});
+
+ // To to Search Available after Launch
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
+ // Fill Search input
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Display Layout');
+ //Search Result Appears and is clicked
+ await Promise.all([
+ page.waitForNavigation(),
+ page.locator('a:has-text("Performance Display Layout")').first().click()
+ ]);
+
+ //Time to Example Imagery Frame loads within Display Layout
+ await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
+ //Time to Example Imagery object loads
+ await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
+
+ const client = await page.context().newCDPSession(page);
+ await client.send('HeapProfiler.enable');
+ await client.send('HeapProfiler.startSampling');
+ // await client.send('HeapProfiler.collectGarbage');
+ await client.send('Performance.enable');
+
+ let performanceMetricsBefore = await client.send('Performance.getMetrics');
+ console.log(performanceMetricsBefore.metrics);
+
+ //await client.send('Performance.disable');
+
+ //Open Large view
+ await page.locator('button:has-text("Large View")').click();
+ await client.send('HeapProfiler.takeHeapSnapshot');
+
+ //Time to Imagery Rendered in Large Frame
+ await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
+
+ //Time to Example Imagery object loads
+ await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
+
+ // Click Close Icon
+ await page.locator('.c-click-icon').click();
+
+ //Time to Example Imagery Frame loads within Display Layout
+ await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
+ //Time to Example Imagery object loads
+ await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
+
+ await client.send('HeapProfiler.collectGarbage');
+ //await client.send('Performance.enable');
+
+ let performanceMetricsAfter = await client.send('Performance.getMetrics');
+ console.log(performanceMetricsAfter.metrics);
+
+ //await client.send('Performance.disable');
+
+ });
+});
diff --git a/e2e/tests/performance/notebook.perf.spec.js b/e2e/tests/performance/notebook.perf.spec.js
new file mode 100644
index 000000000..d25a7d0f4
--- /dev/null
+++ b/e2e/tests/performance/notebook.perf.spec.js
@@ -0,0 +1,158 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 performance tests to ensure that testability of performance
+is not broken upstream on Open MCT. Any assumptions made downstream will be tested here.
+
+TODO:
+ - Update resolution of performance config
+ - Add Performance Observer on init to push all performance marks
+ - Move client CDP connection to before or to a fixture
+
+*/
+
+const { test, expect } = require('@playwright/test');
+
+const notebookFilePath = 'e2e/test-data/PerformanceNotebook.json';
+
+test.describe('Performance tests', () => {
+ test.beforeEach(async ({ page, browser }, testInfo) => {
+ // Go to baseURL
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ // Click a:has-text("My Items")
+ await page.locator('a:has-text("My Items")').click({
+ button: 'right'
+ });
+
+ // Click text=Import from JSON
+ await page.locator('text=Import from JSON').click();
+
+ // Upload Performance Display Layout.json
+ await page.setInputFiles('#fileElem', notebookFilePath);
+
+ // TODO Fix this
+ await page.locator('text=OK >> nth=1').click();
+
+ await expect(page.locator('a:has-text("Performance Notebook")')).toBeVisible();
+
+ //Create a Chrome Performance Timeline trace to store as a test artifact
+ console.log("\n==== Devtools: startTracing ====\n");
+ await browser.startTracing(page, {
+ path: `${testInfo.outputPath()}-trace.json`,
+ screenshots: true
+ });
+ });
+ test.afterEach(async ({ page, browser}) => {
+ console.log("\n==== Devtools: stopTracing ====\n");
+ await browser.stopTracing();
+
+ /* Measurement Section
+ / The following section includes a block of performance measurements.
+ */
+ const startTime = await page.evaluate(() => window.performance.timing.navigationStart);
+ console.log('window.performance.timing.navigationStart', startTime);
+
+ //Get All Performance Marks
+ const getAllMarksJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("mark")));
+ const getAllMarks = JSON.parse(getAllMarksJson);
+ console.log('window.performance.getEntriesByType("mark")', getAllMarks);
+
+ //Get All Performance Measures
+ const getAllMeasuresJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("measure")));
+ const getAllMeasures = JSON.parse(getAllMeasuresJson);
+ console.log('window.performance.getEntriesByType("measure")', getAllMeasures);
+
+ });
+ /* The following test will navigate to a previously created Performance Display Layout and measure the
+ / following metrics:
+ / - ElementResourceTiming
+ / - Interaction Timing
+ */
+ test('Notebook Search, Add Entry, Update Entry are performant', async ({ page, browser }) => {
+ const client = await page.context().newCDPSession(page);
+ // Tell the DevTools session to record performance metrics
+ // https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics
+ await client.send('Performance.enable');
+ // Go to baseURL
+ await page.goto('./');
+
+ // To to Search Available after Launch
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
+ await page.evaluate(() => window.performance.mark("search-available"));
+ // Fill Search input
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Notebook');
+ await page.evaluate(() => window.performance.mark("search-entered"));
+ //Search Result Appears and is clicked
+ await Promise.all([
+ page.waitForNavigation(),
+ page.locator('a:has-text("Performance Notebook")').first().click(),
+ page.evaluate(() => window.performance.mark("click-search-result"))
+ ]);
+
+ await page.waitForSelector('.c-tree__item c-tree-and-search__loading loading', {state: 'hidden'});
+ await page.evaluate(() => window.performance.mark("search-spinner-gone"));
+
+ await page.waitForSelector('.l-browse-bar__object-name', { state: 'visible'});
+ await page.evaluate(() => window.performance.mark("object-title-appears"));
+
+ await page.waitForSelector('.c-notebook__entry >> nth=0', { state: 'visible'});
+ await page.evaluate(() => window.performance.mark("notebook-entry-appears"));
+
+ // Click Add new Notebook Entry
+ await page.locator('.c-notebook__drag-area').click();
+ await page.evaluate(() => window.performance.mark("new-notebook-entry-created"));
+
+ // Enter Notebook Entry text
+ await page.locator('div.c-ne__text').last().fill('New Entry');
+ await page.keyboard.press('Enter');
+ await page.evaluate(() => window.performance.mark("new-notebook-entry-filled"));
+
+ //Individual Notebook Entry Search
+ await page.evaluate(() => window.performance.mark("notebook-search-start"));
+ await page.locator('.c-notebook__search >> input').fill('Existing Entry');
+ await page.evaluate(() => window.performance.mark("notebook-search-filled"));
+ await page.waitForSelector('text=Search Results (3)', { state: 'visible'});
+ await page.evaluate(() => window.performance.mark("notebook-search-processed"));
+ await page.waitForSelector('.c-notebook__entry >> nth=2', { state: 'visible'});
+ await page.evaluate(() => window.performance.mark("notebook-search-processed"));
+
+ //Clear Search
+ await page.locator('.c-search.c-notebook__search .c-search__clear-input').click();
+ await page.evaluate(() => window.performance.mark("notebook-search-processed"));
+
+ // Hover on Last
+ await page.evaluate(() => window.performance.mark("new-notebook-entry-delete"));
+ await page.locator('div.c-ne__time-and-content').last().hover();
+ await page.locator('button[title="Delete this entry"]').last().click();
+ await page.locator('button:has-text("Ok")').click();
+ await page.waitForSelector('.c-notebook__entry >> nth=3', { state: 'detached'});
+ await page.evaluate(() => window.performance.mark("new-notebook-entry-deleted"));
+
+ //await client.send('HeapProfiler.enable');
+ await client.send('HeapProfiler.collectGarbage');
+
+ let performanceMetrics = await client.send('Performance.getMetrics');
+ console.log(performanceMetrics.metrics);
+ });
+});
diff --git a/e2e/tests/plugins/imagery/exampleImagery.e2e.spec.js b/e2e/tests/plugins/imagery/exampleImagery.e2e.spec.js
deleted file mode 100644
index 960f16ebe..000000000
--- a/e2e/tests/plugins/imagery/exampleImagery.e2e.spec.js
+++ /dev/null
@@ -1,238 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2022, 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 verify the basic operations surrounding imagery,
-but only assume that example imagery is present.
-*/
-/* globals process */
-
-const { test, expect } = require('@playwright/test');
-
-test.describe('Example Imagery', () => {
-
- test.beforeEach(async ({ page }) => {
- page.on('console', msg => console.log(msg.text()));
- //Go to baseURL
- await page.goto('/', { waitUntil: 'networkidle' });
-
- //Click the Create button
- await page.click('button:has-text("Create")');
-
- // Click text=Example Imagery
- await page.click('text=Example Imagery');
-
- // Click text=OK
- await Promise.all([
- page.waitForNavigation({waitUntil: 'networkidle'}),
- page.click('text=OK'),
- //Wait for Save Banner to appear
- page.waitForSelector('.c-message-banner__message')
- ]);
- //Wait until Save Banner is gone
- await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
- await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
- });
-
- const backgroundImageSelector = '.c-imagery__main-image__background-image';
- test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
- const bgImageLocator = await page.locator(backgroundImageSelector);
- const deltaYStep = 100; //equivalent to 1x zoom
- await bgImageLocator.hover();
- const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
- // zoom in
- await bgImageLocator.hover();
- await page.mouse.wheel(0, deltaYStep * 2);
- // wait for zoom animation to finish
- await bgImageLocator.hover();
- const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
- // zoom out
- await bgImageLocator.hover();
- await page.mouse.wheel(0, -deltaYStep);
- // wait for zoom animation to finish
- await bgImageLocator.hover();
- const imageMouseZoomedOut = await page.locator(backgroundImageSelector).boundingBox();
-
- expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
- expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
- expect(imageMouseZoomedOut.height).toBeLessThan(imageMouseZoomedIn.height);
- expect(imageMouseZoomedOut.width).toBeLessThan(imageMouseZoomedIn.width);
-
- });
-
- test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
- const deltaYStep = 100; //equivalent to 1x zoom
- const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
-
- const bgImageLocator = await page.locator(backgroundImageSelector);
- await bgImageLocator.hover();
-
- // zoom in
- await page.mouse.wheel(0, deltaYStep * 2);
- await bgImageLocator.hover();
- const zoomedBoundingBox = await bgImageLocator.boundingBox();
- const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
- const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
- // move to the right
-
- // center the mouse pointer
- await page.mouse.move(imageCenterX, imageCenterY);
-
- //Get Diagnostic info about process environment
- console.log('process.platform is ' + process.platform);
- const getUA = await page.evaluate(() => navigator.userAgent);
- console.log('navigator.userAgent ' + getUA);
- // Pan Imagery Hints
- const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
- const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
- expect(expectedAltText).toEqual(imageryHintsText);
-
- // pan right
- await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
- await page.mouse.down();
- await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
- await page.mouse.up();
- await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
- const afterRightPanBoundingBox = await bgImageLocator.boundingBox();
- expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
-
- // pan left
- await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
- await page.mouse.down();
- await page.mouse.move(imageCenterX, imageCenterY, 10);
- await page.mouse.up();
- await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
- const afterLeftPanBoundingBox = await bgImageLocator.boundingBox();
- expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
-
- // pan up
- await page.mouse.move(imageCenterX, imageCenterY);
- await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
- await page.mouse.down();
- await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
- await page.mouse.up();
- await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
- const afterUpPanBoundingBox = await bgImageLocator.boundingBox();
- expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y);
-
- // pan down
- await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
- await page.mouse.down();
- await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
- await page.mouse.up();
- await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
- const afterDownPanBoundingBox = await bgImageLocator.boundingBox();
- expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
-
- });
-
- test('Can use + - buttons to zoom on the image', async ({ page }) => {
- const bgImageLocator = await page.locator(backgroundImageSelector);
- await bgImageLocator.hover();
- const zoomInBtn = await page.locator('.t-btn-zoom-in');
- const zoomOutBtn = await page.locator('.t-btn-zoom-out');
- const initialBoundingBox = await bgImageLocator.boundingBox();
-
- await zoomInBtn.click();
- await zoomInBtn.click();
- // wait for zoom animation to finish
- await bgImageLocator.hover();
- const zoomedInBoundingBox = await bgImageLocator.boundingBox();
- expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
- expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
-
- await zoomOutBtn.click();
- // wait for zoom animation to finish
- await bgImageLocator.hover();
- const zoomedOutBoundingBox = await bgImageLocator.boundingBox();
- expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
- expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
-
- });
-
- test('Can use the reset button to reset the image', async ({ page }) => {
- const bgImageLocator = await page.locator(backgroundImageSelector);
- // wait for zoom animation to finish
- await bgImageLocator.hover();
-
- const zoomInBtn = await page.locator('.t-btn-zoom-in');
- const zoomResetBtn = await page.locator('.t-btn-zoom-reset');
- const initialBoundingBox = await bgImageLocator.boundingBox();
-
- await zoomInBtn.click();
- // wait for zoom animation to finish
- await bgImageLocator.hover();
- await zoomInBtn.click();
- // wait for zoom animation to finish
- await bgImageLocator.hover();
-
- const zoomedInBoundingBox = await bgImageLocator.boundingBox();
- expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
- expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
-
- await zoomResetBtn.click();
- // wait for zoom animation to finish
- await bgImageLocator.hover();
-
- const resetBoundingBox = await bgImageLocator.boundingBox();
- expect.soft(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
- expect.soft(resetBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
-
- expect.soft(resetBoundingBox.height).toEqual(initialBoundingBox.height);
- expect(resetBoundingBox.width).toEqual(initialBoundingBox.width);
- });
-
- //test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
- //test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
- //test.fixme('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
- //test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
- //test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
- //test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
-});
-
-test.describe('Example Imagery in Display layout', () => {
- test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
- test.fixme('Can use alt+drag to move around image once zoomed in');
- test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
- test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
- test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
- test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
-});
-
-test.describe('Example Imagery in Flexible layout', () => {
- test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
- test.fixme('Can use alt+drag to move around image once zoomed in');
- test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
- test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
- test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
- test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
-});
-
-test.describe('Example Imagery in Tabs view', () => {
- test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
- test.fixme('Can use alt+drag to move around image once zoomed in');
- test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
- test.fixme('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
- test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
- test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
- test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
-});
diff --git a/e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome.png b/e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome.png
deleted file mode 100644
index 3170e0851..000000000
--- a/e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-panned-chrome.png
+++ /dev/null
Binary files differ
diff --git a/e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome.png b/e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome.png
deleted file mode 100644
index f9b3595d0..000000000
--- a/e2e/tests/plugins/plot/autoscale.e2e.spec.js-snapshots/autoscale-canvas-prepan-chrome.png
+++ /dev/null
Binary files differ
diff --git a/e2e/tests/plugins/timeConductor/timeConductor.e2e.spec.js b/e2e/tests/plugins/timeConductor/timeConductor.e2e.spec.js
deleted file mode 100644
index 4ca27a474..000000000
--- a/e2e/tests/plugins/timeConductor/timeConductor.e2e.spec.js
+++ /dev/null
@@ -1,112 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2022, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-
-const { test, expect } = require('@playwright/test');
-
-test.describe('Time counductor operations', () => {
- test('validate start time does not exceeds end time', async ({ page }) => {
- //Go to baseURL
- await page.goto('/', { waitUntil: 'networkidle' });
- const year = new Date().getFullYear();
-
- let startDate = 'xxxx-01-01 01:00:00.000Z';
- startDate = year + startDate.substring(4);
-
- 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());
-
- // invalid start date
- startDate = (year + 1) + startDate.substring(4);
- await startTimeLocator.fill(startDate.toString());
- await endTimeLocator.click();
-
- const startDateValidityStatus = await startTimeLocator.evaluate((element) => element.checkValidity());
- expect(startDateValidityStatus).not.toBeTruthy();
-
- // fix to valid start date
- startDate = (year - 1) + startDate.substring(4);
- await startTimeLocator.fill(startDate.toString());
-
- // invalid end date
- endDate = (year - 2) + endDate.substring(4);
- await endTimeLocator.fill(endDate.toString());
- await startTimeLocator.click();
-
- const endDateValidityStatus = await endTimeLocator.evaluate((element) => element.checkValidity());
- expect(endDateValidityStatus).not.toBeTruthy();
- });
-});
-
-
-// Testing instructions:
-// Try to change the realtime offsets when in realtime (local clock) mode.
-test.describe('Time conductor input fields real-time mode', () => {
- test('validate input fields in real-time mode', async ({ page }) => {
- //Go to baseURL
- await page.goto('/', { waitUntil: 'networkidle' });
-
- // Set realtime "local clock" mode offsets
- const timeInputs = page.locator('input.c-input--datetime');
-
- // Click fixed timespan button
- await page.locator('.c-button__label >> text=Fixed Timespan').click();
-
- // Click local clock
- await page.locator('.icon-clock >> text=Local Clock').click();
-
- // Click time offset button
- await page.locator('.c-conductor__delta-button >> text=00:30:00').click();
-
- // Input start time offset
- await page.fill('.pr-time-controls__secs', '23');
-
- // Click the check button
- await page.locator('.icon-check').click();
-
- // Verify time was updated on time offset button
- await expect(page.locator('.c-conductor__delta-button').first()).toContainText('00:30:23');
-
- // Click time offset set preceding now button
- await page.locator('.c-conductor__delta-button >> text=00:00:30').click();
-
- // Input preceding time offset
- await page.fill('.pr-time-controls__secs', '31')
-
- // Click the check buttons
- await page.locator('.icon-check').click();
-
- // Verify time was updated on preceding time offset button
- await expect(page.locator('.c-conductor__delta-button').nth(1)).toContainText('00:00:31');
- });
-});
diff --git a/e2e/tests/recycled_storage.json b/e2e/tests/recycled_storage.json
deleted file mode 100644
index 86c3f906b..000000000
--- a/e2e/tests/recycled_storage.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "cookies": [],
- "origins": [
- {
- "origin": "http://localhost:8080",
- "localStorage": [
- {
- "name": "tcHistory",
- "value": "{\"utc\":[{\"start\":1651513945533,\"end\":1651515745533}]}"
- },
- {
- "name": "mct",
- "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1651515746374,\"modified\":1651515746374},\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"e35a066b-eb0e-4b05-a4c9-cc31dc202572\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1651515746373,\"location\":\"mine\",\"persisted\":1651515746373}}"
- },
- {
- "name": "mct-tree-expanded",
- "value": "[]"
- }
- ]
- }
- ]
-} \ No newline at end of file
diff --git a/e2e/tests/visual/addInit.visual.spec.js b/e2e/tests/visual/addInit.visual.spec.js
new file mode 100644
index 000000000..5c73a82ab
--- /dev/null
+++ b/e2e/tests/visual/addInit.visual.spec.js
@@ -0,0 +1,62 @@
+/* eslint-disable no-undef */
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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.
+ *****************************************************************************/
+
+/*
+Collection of Visual Tests set to run with modified init scripts to inject plugins not otherwise available in the default contexts.
+
+These should only use functional expect statements to verify assumptions about the state
+in a test and not for functional verification of correctness. Visual tests are not supposed
+to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
+
+Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
+*/
+
+// eslint-disable-next-line no-unused-vars
+const { test, expect } = require('../../pluginFixtures');
+const { createDomainObjectWithDefaults } = require('../../appActions');
+const percySnapshot = require('@percy/playwright');
+const path = require('path');
+
+const CUSTOM_NAME = 'CUSTOM_NAME';
+
+test.describe('Visual - addInit', () => {
+ test.use({
+ clockOptions: {
+ now: 0, //Set browser clock to UNIX Epoch
+ shouldAdvanceTime: false //Don't advance the clock
+ }
+ });
+
+ test('Restricted Notebook is visually correct @addInit @unstable', async ({ page, theme }) => {
+ // eslint-disable-next-line no-undef
+ await page.addInitScript({ path: path.join(__dirname, '../../helper', './addInitRestrictedNotebook.js') });
+ //Go to baseURL
+ await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
+
+ await createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });
+
+ // Take a snapshot of the newly created CUSTOM_NAME notebook
+ await percySnapshot(page, `Restricted Notebook with CUSTOM_NAME (theme: '${theme}')`);
+
+ });
+});
diff --git a/e2e/tests/visual/components/tree.visual.spec.js b/e2e/tests/visual/components/tree.visual.spec.js
new file mode 100644
index 000000000..0ad2aca75
--- /dev/null
+++ b/e2e/tests/visual/components/tree.visual.spec.js
@@ -0,0 +1,101 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+const { test } = require('../../../pluginFixtures.js');
+const { createDomainObjectWithDefaults } = require('../../../appActions.js');
+
+const percySnapshot = require('@percy/playwright');
+
+test.describe('Visual - Tree Pane', () => {
+ test('Tree pane in various states @unstable', async ({ page, theme, openmctConfig }) => {
+ const { myItemsFolderName } = openmctConfig;
+ await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
+
+ const foo = await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: "Foo Folder"
+ });
+
+ const bar = await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: "Bar Folder",
+ parent: foo.uuid
+ });
+
+ const baz = await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: "Baz Folder",
+ parent: bar.uuid
+ });
+
+ await createDomainObjectWithDefaults(page, {
+ type: 'Clock',
+ name: 'A Clock'
+ });
+
+ await createDomainObjectWithDefaults(page, {
+ type: 'Clock',
+ name: 'Z Clock'
+ });
+
+ const treePane = "#tree-pane";
+
+ await percySnapshot(page, `Tree Pane w/ collapsed tree (theme: ${theme})`, {
+ scope: treePane
+ });
+
+ await expandTreePaneItemByName(page, myItemsFolderName);
+
+ await page.goto(foo.url);
+ await page.dragAndDrop('role=treeitem[name=/A Clock/]', '.c-object-view');
+ await page.dragAndDrop('role=treeitem[name=/Z Clock/]', '.c-object-view');
+ await page.goto(bar.url);
+ await page.dragAndDrop('role=treeitem[name=/A Clock/]', '.c-object-view');
+ await page.dragAndDrop('role=treeitem[name=/Z Clock/]', '.c-object-view');
+ await page.goto(baz.url);
+ await page.dragAndDrop('role=treeitem[name=/A Clock/]', '.c-object-view');
+ await page.dragAndDrop('role=treeitem[name=/Z Clock/]', '.c-object-view');
+
+ await percySnapshot(page, `Tree Pane w/ single level expanded (theme: ${theme})`, {
+ scope: treePane
+ });
+
+ await expandTreePaneItemByName(page, foo.name);
+ await expandTreePaneItemByName(page, bar.name);
+ await expandTreePaneItemByName(page, baz.name);
+
+ await percySnapshot(page, `Tree Pane w/ multiple levels expanded (theme: ${theme})`, {
+ scope: treePane
+ });
+ });
+});
+
+/**
+ * @param {import('@playwright/test').Page} page
+ * @param {string} name
+ */
+async function expandTreePaneItemByName(page, name) {
+ const treePane = page.locator('#tree-pane');
+ const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
+ const expandTriangle = treeItem.locator('.c-disclosure-triangle');
+ await expandTriangle.click();
+}
diff --git a/e2e/tests/visual/controlledClock.visual.spec.js b/e2e/tests/visual/controlledClock.visual.spec.js
new file mode 100644
index 000000000..c4d6b5767
--- /dev/null
+++ b/e2e/tests/visual/controlledClock.visual.spec.js
@@ -0,0 +1,56 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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.
+ *****************************************************************************/
+
+/*
+Collection of Visual Tests set to run in a default context. The tests within this suite
+are only meant to run against openmct's app.js started by `npm run start` within the
+`./e2e/playwright-visual.config.js` file.
+
+*/
+
+const { test, expect } = require('../../pluginFixtures');
+const percySnapshot = require('@percy/playwright');
+
+test.describe('Visual - Controlled Clock @localStorage', () => {
+ test.use({
+ storageState: './e2e/test-data/VisualTestData_storage.json',
+ clockOptions: {
+ now: 0, //Set browser clock to UNIX Epoch
+ shouldAdvanceTime: false //Don't advance the clock
+ }
+ });
+
+ test('Overlay Plot Loading Indicator @localStorage', async ({ page, theme }) => {
+ // Go to baseURL
+ await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
+
+ await page.locator('a:has-text("Unnamed Overlay Plot Overlay Plot")').click();
+ //Ensure that we're on the Unnamed Overlay Plot object
+ await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
+
+ //Wait for canvas to be rendered and stop animating
+ await page.locator('canvas >> nth=1').hover({trial: true});
+
+ //Take snapshot of Sine Wave Generator within Overlay Plot
+ await percySnapshot(page, `SineWaveInOverlayPlot (theme: '${theme}')`);
+ });
+});
diff --git a/e2e/tests/visual/default.visual.spec.js b/e2e/tests/visual/default.visual.spec.js
index 7a870c412..b8c10b35f 100644
--- a/e2e/tests/visual/default.visual.spec.js
+++ b/e2e/tests/visual/default.visual.spec.js
@@ -32,142 +32,137 @@ to "fail" on assertions. Instead, they should be used to detect changes between
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
*/
-const { test, expect } = require('@playwright/test');
+const { test, expect } = require('../../pluginFixtures');
const percySnapshot = require('@percy/playwright');
-const path = require('path');
-const sinon = require('sinon');
+const { createDomainObjectWithDefaults } = require('../../appActions');
-const VISUAL_GRACE_PERIOD = 5 * 1000; //Lets the application "simmer" before the snapshot is taken
-
-// Snippet from https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758
-// Will replace with cy.clock() equivalent
-test.beforeEach(async ({ context }) => {
- await context.addInitScript({
- // eslint-disable-next-line no-undef
- path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js')
+test.describe('Visual - Default', () => {
+ test.beforeEach(async ({ page }) => {
+ //Go to baseURL and Hide Tree
+ await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' });
});
- await context.addInitScript(() => {
- window.__clock = sinon.useFakeTimers(); //Set browser clock to UNIX Epoch
+ test.use({
+ clockOptions: {
+ now: 0, //Set browser clock to UNIX Epoch
+ shouldAdvanceTime: false //Don't advance the clock
+ }
});
-});
-test('Visual - Root and About', async ({ page }) => {
- // Go to baseURL
- await page.goto('/', { waitUntil: 'networkidle' });
+ test('Visual - Root and About', async ({ page, theme }) => {
+ // Verify that Create button is actionable
+ await expect(page.locator('button:has-text("Create")')).toBeEnabled();
- // Verify that Create button is actionable
- const createButtonLocator = page.locator('button:has-text("Create")');
- await expect(createButtonLocator).toBeEnabled();
+ // Take a snapshot of the Dashboard
+ await percySnapshot(page, `Root (theme: '${theme}')`);
- // Take a snapshot of the Dashboard
- await page.waitForTimeout(VISUAL_GRACE_PERIOD);
- await percySnapshot(page, 'Root');
+ // Click About button
+ await page.click('.l-shell__app-logo');
- // Click About button
- await page.click('.l-shell__app-logo');
+ // Modify the Build information in 'about' to be consistent run-over-run
+ const versionInformationLocator = page.locator('ul.t-info.l-info.s-info').first();
+ await expect(versionInformationLocator).toBeEnabled();
+ await versionInformationLocator.evaluate(node => node.innerHTML = '<li>Version: visual-snapshot</li> <li>Build Date: Mon Nov 15 2021 08:07:51 GMT-0800 (Pacific Standard Time)</li> <li>Revision: 93049cdbc6c047697ca204893db9603b864b8c9f</li> <li>Branch: master</li>');
- // Modify the Build information in 'about' to be consistent run-over-run
- const versionInformationLocator = page.locator('ul.t-info.l-info.s-info');
- await expect(versionInformationLocator).toBeEnabled();
- await versionInformationLocator.evaluate(node => node.innerHTML = '<li>Version: visual-snapshot</li> <li>Build Date: Mon Nov 15 2021 08:07:51 GMT-0800 (Pacific Standard Time)</li> <li>Revision: 93049cdbc6c047697ca204893db9603b864b8c9f</li> <li>Branch: master</li>');
+ // Take a snapshot of the About modal
+ await percySnapshot(page, `About (theme: '${theme}')`);
+ });
- // Take a snapshot of the About modal
- await page.waitForTimeout(VISUAL_GRACE_PERIOD);
- await percySnapshot(page, 'About');
-});
+ test('Visual - Default Condition Set @unstable', async ({ page, theme }) => {
-test('Visual - Default Condition Set', async ({ page }) => {
- //Go to baseURL
- await page.goto('/', { waitUntil: 'networkidle' });
+ await createDomainObjectWithDefaults(page, { type: 'Condition Set' });
- //Click the Create button
- await page.click('button:has-text("Create")');
+ // Take a snapshot of the newly created Condition Set object
+ await percySnapshot(page, `Default Condition Set (theme: '${theme}')`);
+ });
- // Click text=Condition Set
- await page.click('text=Condition Set');
+ test('Visual - Default Condition Widget @unstable', async ({ page, theme }) => {
+ test.info().annotations.push({
+ type: 'issue',
+ description: 'https://github.com/nasa/openmct/issues/5349'
+ });
- // Click text=OK
- await page.click('text=OK');
+ await createDomainObjectWithDefaults(page, { type: 'Condition Widget' });
- // Take a snapshot of the newly created Condition Set object
- await page.waitForTimeout(VISUAL_GRACE_PERIOD);
- await percySnapshot(page, 'Default Condition Set');
-});
+ // Take a snapshot of the newly created Condition Widget object
+ await percySnapshot(page, `Default Condition Widget (theme: '${theme}')`);
+ });
-test('Visual - Default Condition Widget', async ({ page }) => {
- //Go to baseURL
- await page.goto('/', { waitUntil: 'networkidle' });
+ test('Visual - Time Conductor start time is less than end time', async ({ page, theme }) => {
+ const year = new Date().getFullYear();
- //Click the Create button
- await page.click('button:has-text("Create")');
+ let startDate = 'xxxx-01-01 01:00:00.000Z';
+ startDate = year + startDate.substring(4);
- // Click text=Condition Widget
- await page.click('text=Condition Widget');
+ let endDate = 'xxxx-01-01 02:00:00.000Z';
+ endDate = year + endDate.substring(4);
- // Click text=OK
- await page.click('text=OK');
+ await page.locator('input[type="text"]').nth(1).fill(endDate.toString());
+ await page.locator('input[type="text"]').first().fill(startDate.toString());
- // Take a snapshot of the newly created Condition Widget object
- await page.waitForTimeout(VISUAL_GRACE_PERIOD);
- await percySnapshot(page, 'Default Condition Widget');
-});
+ // verify no error msg
+ await percySnapshot(page, `Default Time conductor (theme: '${theme}')`);
-test('Visual - Time Conductor start time is less than end time', async ({ page }) => {
- //Go to baseURL
- await page.goto('/', { waitUntil: 'networkidle' });
- const year = new Date().getFullYear();
+ startDate = (year + 1) + startDate.substring(4);
+ await page.locator('input[type="text"]').first().fill(startDate.toString());
+ await page.locator('input[type="text"]').nth(1).click();
- let startDate = 'xxxx-01-01 01:00:00.000Z';
- startDate = year + startDate.substring(4);
+ // verify error msg for start time (unable to capture snapshot of popup)
+ await percySnapshot(page, `Start time error (theme: '${theme}')`);
- let endDate = 'xxxx-01-01 02:00:00.000Z';
- endDate = year + endDate.substring(4);
+ startDate = (year - 1) + startDate.substring(4);
+ await page.locator('input[type="text"]').first().fill(startDate.toString());
- await page.locator('input[type="text"]').nth(1).fill(endDate.toString());
- await page.locator('input[type="text"]').first().fill(startDate.toString());
+ endDate = (year - 2) + endDate.substring(4);
+ await page.locator('input[type="text"]').nth(1).fill(endDate.toString());
- // verify no error msg
- await page.waitForTimeout(VISUAL_GRACE_PERIOD);
- await percySnapshot(page, 'Default Time conductor');
+ await page.locator('input[type="text"]').first().click();
- startDate = (year + 1) + startDate.substring(4);
- await page.locator('input[type="text"]').first().fill(startDate.toString());
- await page.locator('input[type="text"]').nth(1).click();
+ // verify error msg for end time (unable to capture snapshot of popup)
+ await percySnapshot(page, `End time error (theme: '${theme}')`);
+ });
- // verify error msg for start time (unable to capture snapshot of popup)
- await page.waitForTimeout(VISUAL_GRACE_PERIOD);
- await percySnapshot(page, 'Start time error');
+ test('Visual - Sine Wave Generator Form', async ({ page, theme }) => {
+ //Click the Create button
+ await page.click('button:has-text("Create")');
- startDate = (year - 1) + startDate.substring(4);
- await page.locator('input[type="text"]').first().fill(startDate.toString());
+ // Click text=Sine Wave Generator
+ await page.click('text=Sine Wave Generator');
- endDate = (year - 2) + endDate.substring(4);
- await page.locator('input[type="text"]').nth(1).fill(endDate.toString());
+ await percySnapshot(page, `Default Sine Wave Generator Form (theme: '${theme}')`);
- await page.locator('input[type="text"]').first().click();
+ await page.locator('.field.control.l-input-sm input').first().click();
+ await page.locator('.field.control.l-input-sm input').first().fill('');
- // verify error msg for end time (unable to capture snapshot of popup)
- await page.waitForTimeout(VISUAL_GRACE_PERIOD);
- await percySnapshot(page, 'End time error');
-});
+ // Validate red x mark
+ await percySnapshot(page, `removed amplitude property value (theme: '${theme}')`);
+ });
+
+ test('Visual - Save Successful Banner @unstable', async ({ page, theme }) => {
+ await createDomainObjectWithDefaults(page, { type: 'Timer' });
-test('Visual - Sine Wave Generator Form', async ({ page }) => {
- //Go to baseURL
- await page.goto('/', { waitUntil: 'networkidle' });
+ await page.locator('.c-message-banner__message').hover({ trial: true });
+ await percySnapshot(page, `Banner message shown (theme: '${theme}')`);
- //Click the Create button
- await page.click('button:has-text("Create")');
+ //Wait until Save Banner is gone
+ await page.locator('.c-message-banner__close-button').click();
+ await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
+ await percySnapshot(page, `Banner message gone (theme: '${theme}')`);
+ });
+
+ test('Visual - Display Layout Icon is correct', async ({ page, theme }) => {
+ //Click the Create button
+ await page.click('button:has-text("Create")');
- // Click text=Sine Wave Generator
- await page.click('text=Sine Wave Generator');
+ //Hover on Display Layout option.
+ await page.locator('text=Display Layout').hover();
+ await percySnapshot(page, `Display Layout Create Menu (theme: '${theme}')`);
- await page.waitForTimeout(VISUAL_GRACE_PERIOD);
- await percySnapshot(page, 'Default Sine Wave Generator Form');
+ });
- await page.locator('.field.control.l-input-sm input').first().click();
- await page.locator('.field.control.l-input-sm input').first().fill('');
+ test('Visual - Default Gauge is correct @unstable', async ({ page, theme }) => {
+ await createDomainObjectWithDefaults(page, { type: 'Gauge' });
- // Validate red x mark
- await page.waitForTimeout(VISUAL_GRACE_PERIOD);
- await percySnapshot(page, 'removed amplitude property value');
+ // Take a snapshot of the newly created Gauge object
+ await percySnapshot(page, `Default Gauge (theme: '${theme}')`);
+ });
});
diff --git a/e2e/tests/visual/faultManagement.visual.spec.js b/e2e/tests/visual/faultManagement.visual.spec.js
new file mode 100644
index 000000000..ab6b34e34
--- /dev/null
+++ b/e2e/tests/visual/faultManagement.visual.spec.js
@@ -0,0 +1,78 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 path = require('path');
+const { test } = require('../../pluginFixtures');
+const percySnapshot = require('@percy/playwright');
+
+const utils = require('../../helper/faultUtils');
+
+test.describe('The Fault Management Plugin Visual Test', () => {
+
+ test('icon test', async ({ page, theme }) => {
+ // eslint-disable-next-line no-undef
+ await page.addInitScript({ path: path.join(__dirname, '../../helper/', 'addInitFaultManagementPlugin.js') });
+ await page.goto('./', { waitUntil: 'networkidle' });
+
+ await percySnapshot(page, `Fault Management icon appears in tree (theme: '${theme}')`);
+ });
+
+ test('fault list and acknowledged faults', async ({ page, theme }) => {
+ await utils.navigateToFaultManagementWithStaticExample(page);
+
+ await percySnapshot(page, `Shows a list of faults in the standard view (theme: '${theme}')`);
+
+ await utils.acknowledgeFault(page, 1);
+ await utils.changeViewTo(page, 'acknowledged');
+
+ await percySnapshot(page, `Acknowledged faults, have a checkmark on the fault icon and appear in the acknowldeged view (theme: '${theme}')`);
+ });
+
+ test('shelved faults', async ({ page, theme }) => {
+ await utils.navigateToFaultManagementWithStaticExample(page);
+
+ await utils.shelveFault(page, 1);
+ await utils.changeViewTo(page, 'shelved');
+
+ await percySnapshot(page, `Shelved faults appear in the shelved view (theme: '${theme}')`);
+
+ await utils.openFaultRowMenu(page, 1);
+
+ await percySnapshot(page, `Shelved faults have a 3-dot menu with Unshelve option enabled (theme: '${theme}')`);
+ });
+
+ test('3-dot menu for fault', async ({ page, theme }) => {
+ await utils.navigateToFaultManagementWithStaticExample(page);
+
+ await utils.openFaultRowMenu(page, 1);
+
+ await percySnapshot(page, `Faults have a 3-dot menu with Acknowledge, Shelve and Unshelve (Unshelve is disabled) options (theme: '${theme}')`);
+ });
+
+ test('ability to acknowledge or shelve', async ({ page, theme }) => {
+ await utils.navigateToFaultManagementWithStaticExample(page);
+
+ await utils.selectFaultItem(page, 1);
+
+ await percySnapshot(page, `Selected faults highlight the ability to Acknowledge or Shelve above the fault list (theme: '${theme}')`);
+ });
+});
diff --git a/e2e/tests/visual/notebook.visual.spec.js b/e2e/tests/visual/notebook.visual.spec.js
new file mode 100644
index 000000000..5062f57a0
--- /dev/null
+++ b/e2e/tests/visual/notebook.visual.spec.js
@@ -0,0 +1,51 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+const { test } = require('../../pluginFixtures');
+const percySnapshot = require('@percy/playwright');
+const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../appActions');
+
+test.describe('Visual - Notebook', () => {
+ test('Accepts dropped objects as embeds @unstable', async ({ page, theme, openmctConfig }) => {
+ const { myItemsFolderName } = openmctConfig;
+ await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
+
+ // Create Notebook
+ const notebook = await createDomainObjectWithDefaults(page, {
+ type: 'Notebook',
+ name: "Embed Test Notebook"
+ });
+ // Create Overlay Plot
+ await createDomainObjectWithDefaults(page, {
+ type: 'Overlay Plot',
+ name: "Dropped Overlay Plot"
+ });
+
+ await expandTreePaneItemByName(page, myItemsFolderName);
+
+ await page.goto(notebook.url);
+ await page.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', '.c-notebook__drag-area');
+
+ await percySnapshot(page, `Notebook w/ dropped embed (theme: ${theme})`);
+
+ });
+});
diff --git a/e2e/tests/visual/search.visual.spec.js b/e2e/tests/visual/search.visual.spec.js
new file mode 100644
index 000000000..1cbf29250
--- /dev/null
+++ b/e2e/tests/visual/search.visual.spec.js
@@ -0,0 +1,84 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 verify search functionality.
+*/
+
+const { test, expect } = require('../../pluginFixtures');
+const { createDomainObjectWithDefaults } = require('../../appActions');
+
+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' });
+ });
+ test.use({
+ clockOptions: {
+ now: 0, //Set browser clock to UNIX Epoch
+ shouldAdvanceTime: false //Don't advance the clock
+ }
+ });
+ //This needs to be rewritten to use a non clock or non display layout object
+ test('Can search for objects, and subsequent search dropdown behaves properly @unstable', async ({ page, theme }) => {
+ // await createDomainObjectWithDefaults(page, 'Display Layout');
+ // 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();
+ const folder1 = 'Folder1';
+ await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: folder1
+ });
+
+ // Click [aria-label="OpenMCT Search"] input[type="search"]
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
+ // Fill [aria-label="OpenMCT Search"] input[type="search"]
+ await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill(folder1);
+ await expect(page.locator('[aria-label="Search Result"]')).toContainText(folder1);
+ await percySnapshot(page, 'Searching for Folder Object');
+
+ await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
+ await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click();
+ await percySnapshot(page, 'Preview for clock should display when editing enabled and search item clicked');
+
+ await page.locator('[aria-label="Close"]').click();
+ await percySnapshot(page, 'Search should still be showing after preview closed');
+
+ 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();
+
+ await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
+
+ await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
+
+ await Promise.all([
+ page.waitForNavigation(),
+ page.locator('text=Unnamed Clock').click()
+ ]);
+ await percySnapshot(page, `Clicking on search results should navigate to them if not editing (theme: '${theme}')`);
+
+ });
+});
+
diff --git a/example/exampleTags/plugin.js b/example/exampleTags/plugin.js
new file mode 100644
index 000000000..b78ad89eb
--- /dev/null
+++ b/example/exampleTags/plugin.js
@@ -0,0 +1,33 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 availableTags from './tags.json';
+/**
+ * @returns {function} The plugin install function
+ */
+export default function exampleTagsPlugin() {
+ return function install(openmct) {
+ Object.keys(availableTags.tags).forEach(tagKey => {
+ const tagDefinition = availableTags.tags[tagKey];
+ openmct.annotation.defineTag(tagKey, tagDefinition);
+ });
+ };
+}
diff --git a/example/exampleTags/tags.json b/example/exampleTags/tags.json
new file mode 100644
index 000000000..31a1b823a
--- /dev/null
+++ b/example/exampleTags/tags.json
@@ -0,0 +1,19 @@
+{
+ "tags": {
+ "46a62ad1-bb86-4f88-9a17-2a029e12669d": {
+ "label": "Science",
+ "backgroundColor": "#cc0000",
+ "foregroundColor": "#ffffff"
+ },
+ "65f150ef-73b7-409a-b2e8-258cbd8b7323": {
+ "label": "Driving",
+ "backgroundColor": "#ffad32",
+ "foregroundColor": "#333333"
+ },
+ "f156b038-c605-46db-88a6-67cf2489a371": {
+ "label": "Drilling",
+ "backgroundColor": "#b0ac4e",
+ "foregroundColor": "#FFFFFF"
+ }
+ }
+}
diff --git a/example/exampleUser/ExampleUserProvider.js b/example/exampleUser/ExampleUserProvider.js
index bf25d7aae..8fdd02923 100644
--- a/example/exampleUser/ExampleUserProvider.js
+++ b/example/exampleUser/ExampleUserProvider.js
@@ -21,19 +21,56 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
-import uuid from 'uuid';
+import { v4 as uuid } from 'uuid';
import createExampleUser from './exampleUserCreator';
+const STATUSES = [{
+ key: "NO_STATUS",
+ label: "Not set",
+ iconClass: "icon-question-mark",
+ iconClassPoll: "icon-status-poll-question-mark"
+}, {
+ key: "GO",
+ label: "Go",
+ iconClass: "icon-check",
+ iconClassPoll: "icon-status-poll-question-mark",
+ statusClass: "s-status-ok",
+ statusBgColor: "#33cc33",
+ statusFgColor: "#000"
+}, {
+ key: "MAYBE",
+ label: "Maybe",
+ iconClass: "icon-alert-triangle",
+ iconClassPoll: "icon-status-poll-question-mark",
+ statusClass: "s-status-warning",
+ statusBgColor: "#ffb66c",
+ statusFgColor: "#000"
+}, {
+ key: "NO_GO",
+ label: "No go",
+ iconClass: "icon-circle-slash",
+ iconClassPoll: "icon-status-poll-question-mark",
+ statusClass: "s-status-error",
+ statusBgColor: "#9900cc",
+ statusFgColor: "#fff"
+}];
+/**
+ * @implements {StatusUserProvider}
+ */
export default class ExampleUserProvider extends EventEmitter {
- constructor(openmct) {
+ constructor(openmct, {defaultStatusRole} = {defaultStatusRole: undefined}) {
super();
this.openmct = openmct;
this.user = undefined;
this.loggedIn = false;
this.autoLoginUser = undefined;
+ this.status = STATUSES[1];
+ this.pollQuestion = undefined;
+ this.defaultStatusRole = defaultStatusRole;
this.ExampleUser = createExampleUser(this.openmct.user.User);
+ this.loginPromise = undefined;
}
isLoggedIn() {
@@ -45,11 +82,19 @@ export default class ExampleUserProvider extends EventEmitter {
}
getCurrentUser() {
- if (this.loggedIn) {
- return Promise.resolve(this.user);
+ if (!this.loginPromise) {
+ this.loginPromise = this._login().then(() => this.user);
}
- return this._login().then(() => this.user);
+ return this.loginPromise;
+ }
+
+ canProvideStatusForRole() {
+ return Promise.resolve(true);
+ }
+
+ canSetPollQuestion() {
+ return Promise.resolve(true);
}
hasRole(roleId) {
@@ -60,6 +105,55 @@ export default class ExampleUserProvider extends EventEmitter {
return Promise.resolve(this.user.getRoles().includes(roleId));
}
+ getStatusRoleForCurrentUser() {
+ return Promise.resolve(this.defaultStatusRole);
+ }
+
+ getAllStatusRoles() {
+ return Promise.resolve([this.defaultStatusRole]);
+ }
+
+ getStatusForRole(role) {
+ return Promise.resolve(this.status);
+ }
+
+ async getDefaultStatusForRole(role) {
+ const allRoles = await this.getPossibleStatuses();
+
+ return allRoles?.[0];
+ }
+
+ setStatusForRole(role, status) {
+ this.status = status;
+ this.emit('statusChange', {
+ role,
+ status
+ });
+
+ return true;
+ }
+
+ getPollQuestion() {
+ return Promise.resolve({
+ question: 'Set "GO" if your position is ready for a boarding action on the Klingon cruiser',
+ timestamp: Date.now()
+ });
+ }
+
+ setPollQuestion(pollQuestion) {
+ this.pollQuestion = {
+ question: pollQuestion,
+ timestamp: Date.now()
+ };
+ this.emit("pollQuestionChange", this.pollQuestion);
+
+ return true;
+ }
+
+ getPossibleStatuses() {
+ return Promise.resolve(STATUSES);
+ }
+
_login() {
const id = uuid();
@@ -108,3 +202,6 @@ export default class ExampleUserProvider extends EventEmitter {
);
}
}
+/**
+ * @typedef {import('@/api/user/StatusUserProvider').default} StatusUserProvider
+ */
diff --git a/example/exampleUser/plugin.js b/example/exampleUser/plugin.js
index f7094131e..af533f098 100644
--- a/example/exampleUser/plugin.js
+++ b/example/exampleUser/plugin.js
@@ -22,8 +22,19 @@
import ExampleUserProvider from './ExampleUserProvider';
-export default function ExampleUserPlugin() {
+export default function ExampleUserPlugin({autoLoginUser, defaultStatusRole} = {
+ autoLoginUser: 'guest',
+ defaultStatusRole: 'test-role'
+}) {
return function install(openmct) {
- openmct.user.setProvider(new ExampleUserProvider(openmct));
+ const userProvider = new ExampleUserProvider(openmct, {
+ defaultStatusRole
+ });
+
+ if (autoLoginUser !== undefined) {
+ userProvider.autoLogin(autoLoginUser);
+ }
+
+ openmct.user.setProvider(userProvider);
};
}
diff --git a/example/exampleUser/pluginSpec.js b/example/exampleUser/pluginSpec.js
index dd8ea6bba..02719d99d 100644
--- a/example/exampleUser/pluginSpec.js
+++ b/example/exampleUser/pluginSpec.js
@@ -26,7 +26,7 @@ import {
} from '../../src/utils/testing';
import ExampleUserProvider from './ExampleUserProvider';
-xdescribe("The Example User Plugin", () => {
+describe("The Example User Plugin", () => {
let openmct;
beforeEach(() => {
@@ -47,9 +47,4 @@ xdescribe("The Example User Plugin", () => {
});
openmct.install(openmct.plugins.example.ExampleUser());
});
-
- // The rest of the functionality of the ExampleUser Plugin is
- // tested in both the UserAPISpec.js and in the UserIndicatorPlugin spec.
- // If that changes, those tests can be moved here.
-
});
diff --git a/example/faultManagement/exampleFaultSource.js b/example/faultManagement/exampleFaultSource.js
new file mode 100644
index 000000000..9e296ad7f
--- /dev/null
+++ b/example/faultManagement/exampleFaultSource.js
@@ -0,0 +1,60 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 utils from './utils';
+
+export default function (staticFaults = false) {
+ return function install(openmct) {
+ openmct.install(openmct.plugins.FaultManagement());
+
+ const faultsData = utils.randomFaults(staticFaults);
+
+ openmct.faults.addProvider({
+ request(domainObject, options) {
+ return Promise.resolve(faultsData);
+ },
+ subscribe(domainObject, callback) {
+ return () => {};
+ },
+ supportsRequest(domainObject) {
+ return domainObject.type === 'faultManagement';
+ },
+ supportsSubscribe(domainObject) {
+ return domainObject.type === 'faultManagement';
+ },
+ acknowledgeFault(fault, { comment = '' }) {
+ utils.acknowledgeFault(fault);
+
+ return Promise.resolve({
+ success: true
+ });
+ },
+ shelveFault(fault, duration) {
+ utils.shelveFault(fault, duration);
+
+ return Promise.resolve({
+ success: true
+ });
+ }
+ });
+ };
+}
diff --git a/example/faultManagement/pluginSpec.js b/example/faultManagement/pluginSpec.js
new file mode 100644
index 000000000..b7a0fa680
--- /dev/null
+++ b/example/faultManagement/pluginSpec.js
@@ -0,0 +1,47 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 {
+ createOpenMct,
+ resetApplicationState
+} from '../../src/utils/testing';
+
+describe("The Example Fault Source Plugin", () => {
+ let openmct;
+
+ beforeEach(() => {
+ openmct = createOpenMct();
+ });
+
+ afterEach(() => {
+ return resetApplicationState(openmct);
+ });
+
+ it('is not installed by default', () => {
+ expect(openmct.faults.provider).toBeUndefined();
+ });
+
+ it('can be installed', () => {
+ openmct.install(openmct.plugins.example.ExampleFaultSource());
+ expect(openmct.faults.provider).not.toBeUndefined();
+ });
+});
diff --git a/example/faultManagement/utils.js b/example/faultManagement/utils.js
new file mode 100644
index 000000000..1287d570b
--- /dev/null
+++ b/example/faultManagement/utils.js
@@ -0,0 +1,76 @@
+const SEVERITIES = ['WATCH', 'WARNING', 'CRITICAL'];
+const NAMESPACE = '/Example/fault-';
+const getRandom = {
+ severity: () => SEVERITIES[Math.floor(Math.random() * 3)],
+ value: () => Math.random() + Math.floor(Math.random() * 21) - 10,
+ fault: (num, staticFaults) => {
+ let val = getRandom.value();
+ let severity = getRandom.severity();
+ let time = Date.now() - num;
+
+ if (staticFaults) {
+ let severityIndex = num > 3 ? num % 3 : num;
+
+ val = num;
+ severity = SEVERITIES[severityIndex - 1];
+ time = num;
+ }
+
+ return {
+ type: num,
+ fault: {
+ acknowledged: false,
+ currentValueInfo: {
+ value: val,
+ rangeCondition: severity,
+ monitoringResult: severity
+ },
+ id: `id-${num}`,
+ name: `Example Fault ${num}`,
+ namespace: NAMESPACE + num,
+ seqNum: 0,
+ severity: severity,
+ shelved: false,
+ shortDescription: '',
+ triggerTime: time,
+ triggerValueInfo: {
+ value: val,
+ rangeCondition: severity,
+ monitoringResult: severity
+ }
+ }
+ };
+ }
+};
+
+function shelveFault(fault, opts = {
+ shelved: true,
+ comment: '',
+ shelveDuration: 90000
+}) {
+ fault.shelved = true;
+
+ setTimeout(() => {
+ fault.shelved = false;
+ }, opts.shelveDuration);
+}
+
+function acknowledgeFault(fault) {
+ fault.acknowledged = true;
+}
+
+function randomFaults(staticFaults, count = 5) {
+ let faults = [];
+
+ for (let x = 1, y = count + 1; x < y; x++) {
+ faults.push(getRandom.fault(x, staticFaults));
+ }
+
+ return faults;
+}
+
+export default {
+ randomFaults,
+ shelveFault,
+ acknowledgeFault
+};
diff --git a/example/generator/GeneratorMetadataProvider.js b/example/generator/GeneratorMetadataProvider.js
index 7a8cd9832..f274d2d53 100644
--- a/example/generator/GeneratorMetadataProvider.js
+++ b/example/generator/GeneratorMetadataProvider.js
@@ -29,12 +29,12 @@ define([
}
},
{
- key: "cos",
- name: "Cosine",
- unit: "deg",
- formatString: '%0.2f',
+ key: "wavelengths",
+ name: "Wavelength",
+ unit: "nm",
+ format: 'string[]',
hints: {
- domain: 3
+ range: 4
}
},
// Need to enable "LocalTimeSystem" plugin to make use of this
@@ -64,6 +64,14 @@ define([
hints: {
range: 2
}
+ },
+ {
+ key: "intensities",
+ name: "Intensities",
+ format: 'number[]',
+ hints: {
+ range: 3
+ }
}
]
},
diff --git a/example/generator/GeneratorProvider.js b/example/generator/GeneratorProvider.js
index 2b845a1aa..ee0bf98f9 100644
--- a/example/generator/GeneratorProvider.js
+++ b/example/generator/GeneratorProvider.js
@@ -32,7 +32,8 @@ define([
offset: 0,
dataRateInHz: 1,
randomness: 0,
- phase: 0
+ phase: 0,
+ loadDelay: 0
};
function GeneratorProvider(openmct) {
@@ -53,8 +54,9 @@ define([
'period',
'offset',
'dataRateInHz',
+ 'randomness',
'phase',
- 'randomness'
+ 'loadDelay'
];
request = request || {};
diff --git a/example/generator/WorkerInterface.js b/example/generator/WorkerInterface.js
index 2ddb3ee18..1573800ff 100644
--- a/example/generator/WorkerInterface.js
+++ b/example/generator/WorkerInterface.js
@@ -23,7 +23,7 @@
define([
'uuid'
], function (
- uuid
+ { v4: uuid }
) {
function WorkerInterface(openmct) {
// eslint-disable-next-line no-undef
diff --git a/example/generator/generatorWorker.js b/example/generator/generatorWorker.js
index 6cf873063..bc9083da3 100644
--- a/example/generator/generatorWorker.js
+++ b/example/generator/generatorWorker.js
@@ -77,7 +77,8 @@
utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness),
- wavelength: wavelength(start, nextStep),
+ wavelengths: wavelengths(),
+ intensities: intensities(),
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
}
});
@@ -115,6 +116,7 @@
var dataRateInHz = request.dataRateInHz;
var phase = request.phase;
var randomness = request.randomness;
+ var loadDelay = Math.max(request.loadDelay, 0);
var step = 1000 / dataRateInHz;
var nextStep = start - (start % step) + step;
@@ -126,11 +128,20 @@
utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, period, amplitude, offset, phase, randomness),
- wavelength: wavelength(start, nextStep),
+ wavelengths: wavelengths(),
+ intensities: intensities(),
cos: cos(nextStep, period, amplitude, offset, phase, randomness)
});
}
+ if (loadDelay === 0) {
+ postOnRequest(message, request, data);
+ } else {
+ setTimeout(() => postOnRequest(message, request, data), loadDelay);
+ }
+ }
+
+ function postOnRequest(message, request, data) {
self.postMessage({
id: message.id,
data: request.spectra ? {
@@ -154,8 +165,28 @@
* Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
}
- function wavelength(start, nextStep) {
- return (nextStep - start) / 10;
+ function wavelengths() {
+ let values = [];
+ while (values.length < 5) {
+ const randomValue = Math.random() * 100;
+ if (!values.includes(randomValue)) {
+ values.push(String(randomValue));
+ }
+ }
+
+ return values;
+ }
+
+ function intensities() {
+ let values = [];
+ while (values.length < 5) {
+ const randomValue = Math.random() * 10;
+ if (!values.includes(randomValue)) {
+ values.push(String(randomValue));
+ }
+ }
+
+ return values;
}
function sendError(error, message) {
diff --git a/example/generator/plugin.js b/example/generator/plugin.js
index 953383aad..7444e1b9c 100644
--- a/example/generator/plugin.js
+++ b/example/generator/plugin.js
@@ -36,7 +36,7 @@ define([
openmct.types.addType("example.state-generator", {
name: "State Generator",
- description: "For development use. Generates test enumerated telemetry by cycling through a given set of states",
+ description: "For development use. Generates example enumerated telemetry by cycling through a given set of states.",
cssClass: "icon-generator-telemetry",
creatable: true,
form: [
@@ -81,7 +81,7 @@ define([
{
name: "Amplitude",
control: "numberfield",
- cssClass: "l-input-sm l-numeric",
+ cssClass: "l-numeric",
key: "amplitude",
required: true,
property: [
@@ -92,7 +92,7 @@ define([
{
name: "Offset",
control: "numberfield",
- cssClass: "l-input-sm l-numeric",
+ cssClass: "l-numeric",
key: "offset",
required: true,
property: [
@@ -132,6 +132,17 @@ define([
"telemetry",
"randomness"
]
+ },
+ {
+ name: "Loading Delay (ms)",
+ control: "numberfield",
+ cssClass: "l-input-sm l-numeric",
+ key: "loadDelay",
+ required: true,
+ property: [
+ "telemetry",
+ "loadDelay"
+ ]
}
],
initialize: function (object) {
@@ -141,7 +152,8 @@ define([
offset: 0,
dataRateInHz: 1,
phase: 0,
- randomness: 0
+ randomness: 0,
+ loadDelay: 0
};
}
});
diff --git a/example/imagery/plugin.js b/example/imagery/plugin.js
index 6823ede50..2f323356d 100644
--- a/example/imagery/plugin.js
+++ b/example/imagery/plugin.js
@@ -59,7 +59,8 @@ export default function () {
object.configuration = {
imageLocation: '',
imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS,
- imageSamples: []
+ imageSamples: [],
+ layers: []
};
object.telemetry = {
@@ -90,7 +91,21 @@ export default function () {
format: 'image',
hints: {
image: 1
- }
+ },
+ layers: [
+ {
+ source: 'dist/imagery/example-imagery-layer-16x9.png',
+ name: '16:9'
+ },
+ {
+ source: 'dist/imagery/example-imagery-layer-safe.png',
+ name: 'Safe'
+ },
+ {
+ source: 'dist/imagery/example-imagery-layer-scale.png',
+ name: 'Scale'
+ }
+ ]
},
{
name: 'Image Download Name',
@@ -153,7 +168,7 @@ function getImageUrlListFromConfig(configuration) {
}
function getImageLoadDelay(domainObject) {
- const imageLoadDelay = domainObject.configuration.imageLoadDelayInMilliSeconds;
+ const imageLoadDelay = Math.trunc(Number(domainObject.configuration.imageLoadDelayInMilliSeconds));
if (!imageLoadDelay) {
openmctInstance.objects.mutate(domainObject, 'configuration.imageLoadDelayInMilliSeconds', DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS);
@@ -175,7 +190,9 @@ function getRealtimeProvider() {
subscribe: (domainObject, callback) => {
const delay = getImageLoadDelay(domainObject);
const interval = setInterval(() => {
- callback(pointForTimestamp(Date.now(), domainObject.name, getImageSamples(domainObject.configuration), delay));
+ const imageSamples = getImageSamples(domainObject.configuration);
+ const datum = pointForTimestamp(Date.now(), domainObject.name, imageSamples, delay);
+ callback(datum);
}, delay);
return () => {
@@ -214,8 +231,9 @@ function getLadProvider() {
},
request: (domainObject, options) => {
const delay = getImageLoadDelay(domainObject);
+ const datum = pointForTimestamp(Date.now(), domainObject.name, getImageSamples(domainObject.configuration), delay);
- return Promise.resolve([pointForTimestamp(Date.now(), domainObject.name, delay)]);
+ return Promise.resolve([datum]);
}
};
}
diff --git a/index.html b/index.html
index 5aae47c16..d8ec226c4 100644
--- a/index.html
+++ b/index.html
@@ -75,12 +75,12 @@
const TWO_HOURS = ONE_HOUR * 2;
const ONE_DAY = ONE_HOUR * 24;
-
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.example.Generator());
openmct.install(openmct.plugins.example.EventGeneratorPlugin());
openmct.install(openmct.plugins.example.ExampleImagery());
+ openmct.install(openmct.plugins.example.ExampleTags());
openmct.install(openmct.plugins.Espresso());
openmct.install(openmct.plugins.MyItems());
@@ -191,11 +191,13 @@
openmct.install(openmct.plugins.ObjectMigration());
openmct.install(openmct.plugins.ClearData(
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked', 'example.imagery'],
- {indicator: true}
+ { indicator: true }
));
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
openmct.install(openmct.plugins.Timer());
openmct.install(openmct.plugins.Timelist());
+ openmct.install(openmct.plugins.BarChart());
+ openmct.install(openmct.plugins.ScatterPlot());
openmct.start();
</script>
</html>
diff --git a/karma.conf.js b/karma.conf.js
index e5fbe40ab..a24f6d0f3 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -58,29 +58,20 @@ module.exports = (config) => {
base: 'Chrome',
flags: ['--remote-debugging-port=9222'],
debug: true
- },
- FirefoxESR: {
- base: 'FirefoxHeadless',
- name: 'FirefoxESR'
}
},
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
junitReporter: {
- outputDir: "dist/reports/tests",
- outputFile: "test-results.xml",
- useBrowserName: false
+ outputDir: "dist/reports/tests", //Useful for CircleCI
+ outputFile: "test-results.xml", //Useful for CircleCI
+ useBrowserName: false //Disable since we only want chrome
},
coverageIstanbulReporter: {
fixWebpackSourcePaths: true,
- dir: "dist/reports/coverage",
- reports: ['lcovonly', 'text-summary'],
- thresholds: {
- global: {
- lines: 52
- }
- }
+ dir: "coverage/unit", //Sets coverage file to be consumed by codecov.io
+ reports: ['lcovonly']
},
specReporter: {
maxLogLines: 5,
diff --git a/package.json b/package.json
index 9f6c33a30..d6255ac1c 100644
--- a/package.json
+++ b/package.json
@@ -1,108 +1,107 @@
{
"name": "openmct",
- "version": "2.0.3",
+ "version": "2.1.1-SNAPSHOT",
"description": "The Open MCT core platform",
"devDependencies": {
- "@babel/eslint-parser": "7.16.3",
+ "@babel/eslint-parser": "7.18.9",
"@braintree/sanitize-url": "6.0.0",
- "@percy/cli": "1.0.4",
- "@percy/playwright": "1.0.2",
- "@playwright/test": "1.21.1",
+ "@percy/cli": "1.10.3",
+ "@percy/playwright": "1.0.4",
+ "@playwright/test": "1.25.2",
"@types/eventemitter3": "^1.0.0",
"@types/jasmine": "^4.0.1",
"@types/karma": "^6.3.2",
"@types/lodash": "^4.14.178",
"@types/mocha": "^9.1.0",
- "allure-playwright": "2.0.0-beta.15",
- "babel-loader": "8.2.3",
+ "babel-loader": "8.2.5",
"babel-plugin-istanbul": "6.1.1",
"comma-separated-values": "3.6.4",
- "copy-webpack-plugin": "10.2.0",
+ "codecov":"3.8.3",
+ "copy-webpack-plugin": "11.0.0",
"cross-env": "7.0.3",
- "css-loader": "4.0.0",
+ "css-loader": "6.7.1",
"d3-axis": "3.0.0",
"d3-scale": "3.3.0",
"d3-selection": "3.0.0",
- "eslint": "8.13.0",
+ "eslint": "8.23.1",
"eslint-plugin-compat": "4.0.2",
- "eslint-plugin-playwright": "0.9.0",
- "eslint-plugin-vue": "8.5.0",
+ "eslint-plugin-playwright": "0.11.2",
+ "eslint-plugin-vue": "9.3.0",
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
"eventemitter3": "1.2.0",
- "exports-loader": "0.7.0",
"express": "4.13.1",
"file-saver": "2.0.5",
"git-rev-sync": "3.0.2",
"html2canvas": "1.4.1",
- "imports-loader": "0.8.0",
- "jasmine-core": "4.0.1",
- "jsdoc": "3.5.5",
- "karma": "6.3.18",
+ "imports-loader": "4.0.1",
+ "jasmine-core": "4.4.0",
+ "karma": "6.3.20",
"karma-chrome-launcher": "3.1.1",
"karma-cli": "2.0.0",
- "karma-coverage": "2.1.1",
+ "karma-coverage": "2.2.0",
"karma-coverage-istanbul-reporter": "3.0.3",
- "karma-firefox-launcher": "2.1.2",
- "karma-jasmine": "4.0.1",
+ "karma-jasmine": "5.1.0",
"karma-junit-reporter": "2.0.1",
"karma-sourcemap-loader": "0.3.8",
"karma-spec-reporter": "0.0.34",
"karma-webpack": "5.0.0",
- "lighthouse": "9.5.0",
"location-bar": "3.0.1",
"lodash": "4.17.21",
- "mini-css-extract-plugin": "2.6.0",
- "moment": "2.29.3",
+ "mini-css-extract-plugin": "2.6.1",
+ "moment": "2.29.4",
"moment-duration-format": "2.3.2",
- "moment-timezone": "0.5.34",
- "node-bourbon": "4.2.3",
- "painterro": "1.2.56",
- "plotly.js-basic-dist": "2.5.0",
- "plotly.js-gl2d-dist": "2.5.0",
+ "moment-timezone": "0.5.37",
+ "nyc":"15.1.0",
+ "painterro": "1.2.78",
+ "plotly.js-basic-dist": "2.14.0",
+ "plotly.js-gl2d-dist": "2.14.0",
"printj": "1.3.1",
"request": "2.88.2",
"resolve-url-loader": "5.0.0",
- "sass": "1.49.9",
- "sass-loader": "12.6.0",
- "sinon": "13.0.1",
- "style-loader": "^1.0.1",
- "uuid": "3.3.3",
+ "sass": "1.55.0",
+ "sass-loader": "13.0.2",
+ "sinon": "14.0.0",
+ "style-loader": "^3.3.1",
+ "uuid": "9.0.0",
"vue": "2.6.14",
- "vue-eslint-parser": "8.3.0",
+ "vue-eslint-parser": "9.1.0",
"vue-loader": "15.9.8",
"vue-template-compiler": "2.6.14",
- "webpack": "5.68.0",
- "webpack-cli": "4.9.2",
- "webpack-dev-middleware": "5.3.1",
- "webpack-hot-middleware": "2.25.1",
- "webpack-merge": "5.8.0",
- "zepto": "1.2.0"
+ "webpack": "5.74.0",
+ "webpack-cli": "4.10.0",
+ "webpack-dev-middleware": "5.3.3",
+ "webpack-hot-middleware": "2.25.2",
+ "webpack-merge": "5.8.0"
},
"scripts": {
"clean": "rm -rf ./dist ./node_modules ./package-lock.json",
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
"start": "node app.js",
- "lint": "eslint example src --ext .js,.vue openmct.js",
- "lint:fix": "eslint example src --ext .js,.vue openmct.js --fix",
+ "lint": "eslint example src e2e --ext .js,.vue openmct.js --max-warnings=0",
+ "lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix",
"build:prod": "cross-env webpack --config webpack.prod.js",
"build:dev": "webpack --config webpack.dev.js",
"build:coverage": "webpack --config webpack.coverage.js",
"build:watch": "webpack --config webpack.dev.js --watch",
"info": "npx envinfo --system --browsers --npmPackages --binaries --languages --markdown",
- "test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
- "test:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
+ "test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
- "test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke default condition timeConductor branding clock",
+ "test:e2e": "npx playwright test",
+ "test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb",
+ "test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb\"",
+ "test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
- "test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-local.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 default",
- "test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js",
- "test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
- "jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
+ "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:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
+ "test:watch": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
"update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2022/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\\-2022/gm'",
- "otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'",
- "docs": "npm run jsdoc ; npm run otherdoc",
+ "cov:e2e:report":"nyc report --reporter=lcovonly --report-dir=./coverage/e2e",
+ "cov:e2e:full:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full",
+ "cov:e2e:stable:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-stable",
+ "cov:unit:publish":"codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit",
"prepare": "npm run build:prod"
},
"repository": {
diff --git a/src/MCT.js b/src/MCT.js
index 00624460d..595afa7b0 100644
--- a/src/MCT.js
+++ b/src/MCT.js
@@ -42,6 +42,7 @@ define([
'./plugins/duplicate/plugin',
'./plugins/importFromJSONAction/plugin',
'./plugins/exportAsJSONAction/plugin',
+ './ui/components/components',
'vue'
], function (
EventEmitter,
@@ -65,6 +66,7 @@ define([
DuplicateActionPlugin,
ImportFromJSONAction,
ExportAsJSONAction,
+ components,
Vue
) {
/**
@@ -94,155 +96,170 @@ define([
};
this.destroy = this.destroy.bind(this);
- /**
- * Tracks current selection state of the application.
- * @private
- */
- this.selection = new Selection(this);
-
- /**
- * MCT's time conductor, which may be used to synchronize view contents
- * for telemetry- or time-based views.
- * @type {module:openmct.TimeConductor}
- * @memberof module:openmct.MCT#
- * @name conductor
- */
- this.time = new api.TimeAPI(this);
-
- /**
- * An interface for interacting with the composition of domain objects.
- * The composition of a domain object is the list of other domain
- * objects it "contains" (for instance, that should be displayed
- * beneath it in the tree.)
- *
- * `composition` may be called as a function, in which case it acts
- * as [`composition.get`]{@link module:openmct.CompositionAPI#get}.
- *
- * @type {module:openmct.CompositionAPI}
- * @memberof module:openmct.MCT#
- * @name composition
- */
- this.composition = new api.CompositionAPI(this);
-
- /**
- * Registry for views of domain objects which should appear in the
- * main viewing area.
- *
- * @type {module:openmct.ViewRegistry}
- * @memberof module:openmct.MCT#
- * @name objectViews
- */
- this.objectViews = new ViewRegistry();
-
- /**
- * Registry for views which should appear in the Inspector area.
- * These views will be chosen based on the selection state.
- *
- * @type {module:openmct.InspectorViewRegistry}
- * @memberof module:openmct.MCT#
- * @name inspectorViews
- */
- this.inspectorViews = new InspectorViewRegistry();
-
- /**
- * Registry for views which should appear in Edit Properties
- * dialogs, and similar user interface elements used for
- * modifying domain objects external to its regular views.
- *
- * @type {module:openmct.ViewRegistry}
- * @memberof module:openmct.MCT#
- * @name propertyEditors
- */
- this.propertyEditors = new ViewRegistry();
-
- /**
- * Registry for views which should appear in the status indicator area.
- * @type {module:openmct.ViewRegistry}
- * @memberof module:openmct.MCT#
- * @name indicators
- */
- this.indicators = new ViewRegistry();
-
- /**
- * Registry for views which should appear in the toolbar area while
- * editing. These views will be chosen based on the selection state.
- *
- * @type {module:openmct.ToolbarRegistry}
- * @memberof module:openmct.MCT#
- * @name toolbars
- */
- this.toolbars = new ToolbarRegistry();
-
- /**
- * Registry for domain object types which may exist within this
- * instance of Open MCT.
- *
- * @type {module:openmct.TypeRegistry}
- * @memberof module:openmct.MCT#
- * @name types
- */
- this.types = new api.TypeRegistry();
-
- /**
- * An interface for interacting with domain objects and the domain
- * object hierarchy.
- *
- * @type {module:openmct.ObjectAPI}
- * @memberof module:openmct.MCT#
- * @name objects
- */
- this.objects = new api.ObjectAPI.default(this.types, this);
-
- /**
- * An interface for retrieving and interpreting telemetry data associated
- * with a domain object.
- *
- * @type {module:openmct.TelemetryAPI}
- * @memberof module:openmct.MCT#
- * @name telemetry
- */
- this.telemetry = new api.TelemetryAPI(this);
-
- /**
- * An interface for creating new indicators and changing them dynamically.
- *
- * @type {module:openmct.IndicatorAPI}
- * @memberof module:openmct.MCT#
- * @name indicators
- */
- this.indicators = new api.IndicatorAPI(this);
-
- /**
- * MCT's user awareness management, to enable user and
- * role specific functionality.
- * @type {module:openmct.UserAPI}
- * @memberof module:openmct.MCT#
- * @name user
- */
- this.user = new api.UserAPI(this);
-
- this.notifications = new api.NotificationAPI();
-
- this.editor = new api.EditorAPI.default(this);
-
- this.overlays = new OverlayAPI.default();
-
- this.menus = new api.MenuAPI(this);
-
- this.actions = new api.ActionsAPI(this);
-
- this.status = new api.StatusAPI(this);
-
- this.priority = api.PriorityAPI;
-
- this.router = new ApplicationRouter(this);
- this.forms = new api.FormsAPI.default(this);
-
- this.branding = BrandingAPI.default;
+ [
+ /**
+ * Tracks current selection state of the application.
+ * @private
+ */
+ ['selection', () => new Selection(this)],
+
+ /**
+ * MCT's time conductor, which may be used to synchronize view contents
+ * for telemetry- or time-based views.
+ * @type {module:openmct.TimeConductor}
+ * @memberof module:openmct.MCT#
+ * @name conductor
+ */
+ ['time', () => new api.TimeAPI(this)],
+
+ /**
+ * An interface for interacting with the composition of domain objects.
+ * The composition of a domain object is the list of other domain
+ * objects it "contains" (for instance, that should be displayed
+ * beneath it in the tree.)
+ *
+ * `composition` may be called as a function, in which case it acts
+ * as [`composition.get`]{@link module:openmct.CompositionAPI#get}.
+ *
+ * @type {module:openmct.CompositionAPI}
+ * @memberof module:openmct.MCT#
+ * @name composition
+ */
+ ['composition', () => new api.CompositionAPI(this)],
+
+ /**
+ * Registry for views of domain objects which should appear in the
+ * main viewing area.
+ *
+ * @type {module:openmct.ViewRegistry}
+ * @memberof module:openmct.MCT#
+ * @name objectViews
+ */
+ ['objectViews', () => new ViewRegistry()],
+
+ /**
+ * Registry for views which should appear in the Inspector area.
+ * These views will be chosen based on the selection state.
+ *
+ * @type {module:openmct.InspectorViewRegistry}
+ * @memberof module:openmct.MCT#
+ * @name inspectorViews
+ */
+ ['inspectorViews', () => new InspectorViewRegistry()],
+
+ /**
+ * Registry for views which should appear in Edit Properties
+ * dialogs, and similar user interface elements used for
+ * modifying domain objects external to its regular views.
+ *
+ * @type {module:openmct.ViewRegistry}
+ * @memberof module:openmct.MCT#
+ * @name propertyEditors
+ */
+ ['propertyEditors', () => new ViewRegistry()],
+
+ /**
+ * Registry for views which should appear in the toolbar area while
+ * editing. These views will be chosen based on the selection state.
+ *
+ * @type {module:openmct.ToolbarRegistry}
+ * @memberof module:openmct.MCT#
+ * @name toolbars
+ */
+ ['toolbars', () => new ToolbarRegistry()],
+
+ /**
+ * Registry for domain object types which may exist within this
+ * instance of Open MCT.
+ *
+ * @type {module:openmct.TypeRegistry}
+ * @memberof module:openmct.MCT#
+ * @name types
+ */
+ ['types', () => new api.TypeRegistry()],
+
+ /**
+ * An interface for interacting with domain objects and the domain
+ * object hierarchy.
+ *
+ * @type {module:openmct.ObjectAPI}
+ * @memberof module:openmct.MCT#
+ * @name objects
+ */
+ ['objects', () => new api.ObjectAPI.default(this.types, this)],
+
+ /**
+ * An interface for retrieving and interpreting telemetry data associated
+ * with a domain object.
+ *
+ * @type {module:openmct.TelemetryAPI}
+ * @memberof module:openmct.MCT#
+ * @name telemetry
+ */
+ ['telemetry', () => new api.TelemetryAPI.default(this)],
+
+ /**
+ * An interface for creating new indicators and changing them dynamically.
+ *
+ * @type {module:openmct.IndicatorAPI}
+ * @memberof module:openmct.MCT#
+ * @name indicators
+ */
+ ['indicators', () => new api.IndicatorAPI(this)],
+
+ /**
+ * MCT's user awareness management, to enable user and
+ * role specific functionality.
+ * @type {module:openmct.UserAPI}
+ * @memberof module:openmct.MCT#
+ * @name user
+ */
+ ['user', () => new api.UserAPI(this)],
+
+ ['notifications', () => new api.NotificationAPI()],
+
+ ['editor', () => new api.EditorAPI.default(this)],
+
+ ['overlays', () => new OverlayAPI.default()],
+
+ ['menus', () => new api.MenuAPI(this)],
+
+ ['actions', () => new api.ActionsAPI(this)],
+
+ ['status', () => new api.StatusAPI(this)],
+
+ ['priority', () => api.PriorityAPI],
+
+ ['router', () => new ApplicationRouter(this)],
+
+ ['faults', () => new api.FaultManagementAPI.default(this)],
+
+ ['forms', () => new api.FormsAPI.default(this)],
+
+ ['branding', () => BrandingAPI.default],
+
+ /**
+ * MCT's annotation API that enables
+ * human-created comments and categorization linked to data products
+ * @type {module:openmct.AnnotationAPI}
+ * @memberof module:openmct.MCT#
+ * @name annotation
+ */
+ ['annotation', () => new api.AnnotationAPI(this)]
+ ].forEach(apiEntry => {
+ const apiName = apiEntry[0];
+ const apiObject = apiEntry[1]();
+
+ Object.defineProperty(this, apiName, {
+ value: apiObject,
+ enumerable: false,
+ configurable: false,
+ writable: true
+ });
+ });
// Plugins that are installed by default
this.install(this.plugins.Plot());
- this.install(this.plugins.Chart());
this.install(this.plugins.TelemetryTable.default());
this.install(PreviewPlugin.default());
this.install(LicensesPlugin.default());
@@ -270,6 +287,7 @@ define([
this.install(this.plugins.ObjectInterceptors());
this.install(this.plugins.DeviceClassifier());
this.install(this.plugins.UserIndicator());
+ this.install(this.plugins.Gauge());
}
MCT.prototype = Object.create(EventEmitter.prototype);
@@ -378,6 +396,7 @@ define([
};
MCT.prototype.plugins = plugins;
+ MCT.prototype.components = components.default;
return MCT;
});
diff --git a/src/api/actions/ActionCollection.js b/src/api/actions/ActionCollection.js
index eed8be625..6606616b0 100644
--- a/src/api/actions/ActionCollection.js
+++ b/src/api/actions/ActionCollection.js
@@ -85,8 +85,6 @@ class ActionCollection extends EventEmitter {
}
destroy() {
- super.removeAllListeners();
-
if (!this.skipEnvironmentObservers) {
this.objectUnsubscribes.forEach(unsubscribe => {
unsubscribe();
@@ -96,6 +94,7 @@ class ActionCollection extends EventEmitter {
}
this.emit('destroy', this.view);
+ this.removeAllListeners();
}
getVisibleActions() {
diff --git a/src/api/annotation/AnnotationAPI.js b/src/api/annotation/AnnotationAPI.js
new file mode 100644
index 000000000..6b9e910be
--- /dev/null
+++ b/src/api/annotation/AnnotationAPI.js
@@ -0,0 +1,286 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 { v4 as uuid } from 'uuid';
+import EventEmitter from 'EventEmitter';
+
+/**
+ * @readonly
+ * @enum {String} AnnotationType
+ * @property {String} NOTEBOOK The notebook annotation type
+ * @property {String} GEOSPATIAL The geospatial annotation type
+ * @property {String} PIXEL_SPATIAL The pixel-spatial annotation type
+ * @property {String} TEMPORAL The temporal annotation type
+ * @property {String} PLOT_SPATIAL The plot-spatial annotation type
+ */
+const ANNOTATION_TYPES = Object.freeze({
+ NOTEBOOK: 'NOTEBOOK',
+ GEOSPATIAL: 'GEOSPATIAL',
+ PIXEL_SPATIAL: 'PIXEL_SPATIAL',
+ TEMPORAL: 'TEMPORAL',
+ PLOT_SPATIAL: 'PLOT_SPATIAL'
+});
+
+const ANNOTATION_TYPE = 'annotation';
+
+/**
+ * @typedef {Object} Tag
+ * @property {String} key a unique identifier for the tag
+ * @property {String} backgroundColor eg. "#cc0000"
+ * @property {String} foregroundColor eg. "#ffffff"
+ */
+export default class AnnotationAPI extends EventEmitter {
+ constructor(openmct) {
+ super();
+ this.openmct = openmct;
+ this.availableTags = {};
+
+ this.ANNOTATION_TYPES = ANNOTATION_TYPES;
+
+ this.openmct.types.addType(ANNOTATION_TYPE, {
+ name: 'Annotation',
+ description: 'A user created note or comment about time ranges, pixel space, and geospatial features.',
+ creatable: false,
+ cssClass: 'icon-notebook',
+ initialize: function (domainObject) {
+ domainObject.targets = domainObject.targets || {};
+ domainObject.originalContextPath = domainObject.originalContextPath || '';
+ domainObject.tags = domainObject.tags || [];
+ domainObject.contentText = domainObject.contentText || '';
+ domainObject.annotationType = domainObject.annotationType || 'plotspatial';
+ }
+ });
+ }
+
+ /**
+ * Create the a generic annotation
+ * @typedef {Object} CreateAnnotationOptions
+ * @property {String} name a name for the new parameter
+ * @property {import('../objects/ObjectAPI').DomainObject} domainObject the domain object to create
+ * @property {ANNOTATION_TYPES} annotationType the type of annotation to create
+ * @property {Tag[]} tags
+ * @property {String} contentText
+ * @property {import('../objects/ObjectAPI').Identifier[]} targets
+ */
+ /**
+ * @method create
+ * @param {CreateAnnotationOptions} options
+ * @returns {Promise<import('../objects/ObjectAPI').DomainObject>} a promise which will resolve when the domain object
+ * has been created, or be rejected if it cannot be saved
+ */
+ async create({name, domainObject, annotationType, tags, contentText, targets}) {
+ if (!Object.keys(this.ANNOTATION_TYPES).includes(annotationType)) {
+ throw new Error(`Unknown annotation type: ${annotationType}`);
+ }
+
+ if (!Object.keys(targets).length) {
+ throw new Error(`At least one target is required to create an annotation`);
+ }
+
+ const domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);
+ const originalPathObjects = await this.openmct.objects.getOriginalPath(domainObjectKeyString);
+ const originalContextPath = this.openmct.objects.getRelativePath(originalPathObjects);
+ const namespace = domainObject.identifier.namespace;
+ const type = 'annotation';
+ const typeDefinition = this.openmct.types.get(type);
+ const definition = typeDefinition.definition;
+
+ const createdObject = {
+ name,
+ type,
+ identifier: {
+ key: uuid(),
+ namespace
+ },
+ tags,
+ annotationType,
+ contentText,
+ originalContextPath
+ };
+
+ if (definition.initialize) {
+ definition.initialize(createdObject);
+ }
+
+ createdObject.targets = targets;
+ createdObject.originalContextPath = originalContextPath;
+
+ const success = await this.openmct.objects.save(createdObject);
+ if (success) {
+ this.emit('annotationCreated', createdObject);
+
+ return createdObject;
+ } else {
+ throw new Error('Failed to create object');
+ }
+ }
+
+ defineTag(tagKey, tagsDefinition) {
+ this.availableTags[tagKey] = tagsDefinition;
+ }
+
+ isAnnotation(domainObject) {
+ return domainObject && (domainObject.type === ANNOTATION_TYPE);
+ }
+
+ getAvailableTags() {
+ if (this.availableTags) {
+ const rearrangedToArray = Object.keys(this.availableTags).map(tagKey => {
+ return {
+ id: tagKey,
+ ...this.availableTags[tagKey]
+ };
+ });
+
+ return rearrangedToArray;
+ } else {
+ return [];
+ }
+ }
+
+ async getAnnotation(query, searchType) {
+ let foundAnnotation = null;
+
+ const searchResults = (await Promise.all(this.openmct.objects.search(query, null, searchType))).flat();
+ if (searchResults) {
+ foundAnnotation = searchResults[0];
+ }
+
+ return foundAnnotation;
+ }
+
+ async addAnnotationTag(existingAnnotation, targetDomainObject, targetSpecificDetails, annotationType, tag) {
+ if (!existingAnnotation) {
+ const targets = {};
+ const targetKeyString = this.openmct.objects.makeKeyString(targetDomainObject.identifier);
+ targets[targetKeyString] = targetSpecificDetails;
+ const contentText = `${annotationType} tag`;
+ const annotationCreationArguments = {
+ name: contentText,
+ domainObject: targetDomainObject,
+ annotationType,
+ tags: [tag],
+ contentText,
+ targets
+ };
+ const newAnnotation = await this.create(annotationCreationArguments);
+
+ return newAnnotation;
+ } else {
+ const tagArray = [tag, ...existingAnnotation.tags];
+ this.openmct.objects.mutate(existingAnnotation, 'tags', tagArray);
+
+ return existingAnnotation;
+ }
+ }
+
+ removeAnnotationTag(existingAnnotation, tagToRemove) {
+ if (existingAnnotation && existingAnnotation.tags.includes(tagToRemove)) {
+ const cleanedArray = existingAnnotation.tags.filter(extantTag => extantTag !== tagToRemove);
+ this.openmct.objects.mutate(existingAnnotation, 'tags', cleanedArray);
+ } else {
+ throw new Error(`Asked to remove tag (${tagToRemove}) that doesn't exist`, existingAnnotation);
+ }
+ }
+
+ removeAnnotationTags(existingAnnotation) {
+ // just removes tags on the annotation as we can't really delete objects
+ if (existingAnnotation && existingAnnotation.tags) {
+ this.openmct.objects.mutate(existingAnnotation, 'tags', []);
+ }
+ }
+
+ #getMatchingTags(query) {
+ if (!query) {
+ return [];
+ }
+
+ const matchingTags = Object.keys(this.availableTags).filter(tagKey => {
+ if (this.availableTags[tagKey] && this.availableTags[tagKey].label) {
+ return this.availableTags[tagKey].label.toLowerCase().includes(query.toLowerCase());
+ }
+
+ return false;
+ });
+
+ return matchingTags;
+ }
+
+ #addTagMetaInformationToResults(results, matchingTagKeys) {
+ const tagsAddedToResults = results.map(result => {
+ const fullTagModels = result.tags.map(tagKey => {
+ const tagModel = this.availableTags[tagKey];
+ tagModel.tagID = tagKey;
+
+ return tagModel;
+ });
+
+ return {
+ fullTagModels,
+ matchingTagKeys,
+ ...result
+ };
+ });
+
+ return tagsAddedToResults;
+ }
+
+ async #addTargetModelsToResults(results) {
+ const modelAddedToResults = await Promise.all(results.map(async result => {
+ const targetModels = await Promise.all(Object.keys(result.targets).map(async (targetID) => {
+ const targetModel = await this.openmct.objects.get(targetID);
+ const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
+ const originalPathObjects = await this.openmct.objects.getOriginalPath(targetKeyString);
+
+ return {
+ originalPath: originalPathObjects,
+ ...targetModel
+ };
+ }));
+
+ return {
+ targetModels,
+ ...result
+ };
+ }));
+
+ return modelAddedToResults;
+ }
+
+ /**
+ * @method searchForTags
+ * @param {String} query A query to match against tags. E.g., "dr" will match the tags "drilling" and "driving"
+ * @param {Object} abortController An optional abort method to stop the query
+ * @returns {Promise} returns a model of matching tags with their target domain objects attached
+ */
+ async searchForTags(query, abortController) {
+ const matchingTagKeys = this.#getMatchingTags(query);
+ const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat();
+ const appliedTagSearchResults = this.#addTagMetaInformationToResults(searchResults, matchingTagKeys);
+ const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults);
+ const resultsWithValidPath = appliedTargetsModels.filter(result => {
+ return this.openmct.objects.isReachable(result.targetModels?.[0]?.originalPath);
+ });
+
+ return resultsWithValidPath;
+ }
+}
diff --git a/src/api/annotation/AnnotationAPISpec.js b/src/api/annotation/AnnotationAPISpec.js
new file mode 100644
index 000000000..8aa45864d
--- /dev/null
+++ b/src/api/annotation/AnnotationAPISpec.js
@@ -0,0 +1,190 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 { createOpenMct, resetApplicationState } from '../../utils/testing';
+import ExampleTagsPlugin from "../../../example/exampleTags/plugin";
+
+describe("The Annotation API", () => {
+ let openmct;
+ let mockObjectProvider;
+ let mockDomainObject;
+ let mockFolderObject;
+ let mockAnnotationObject;
+
+ beforeEach((done) => {
+ openmct = createOpenMct();
+ openmct.install(new ExampleTagsPlugin());
+ const availableTags = openmct.annotation.getAvailableTags();
+ mockFolderObject = {
+ type: 'root',
+ name: 'folderFoo',
+ location: '',
+ identifier: {
+ key: 'someParent',
+ namespace: 'fooNameSpace'
+ }
+ };
+ mockDomainObject = {
+ type: 'notebook',
+ name: 'fooRabbitNotebook',
+ location: 'fooNameSpace:someParent',
+ identifier: {
+ key: 'some-object',
+ namespace: 'fooNameSpace'
+ }
+ };
+ mockAnnotationObject = {
+ type: 'annotation',
+ name: 'Some Notebook Annotation',
+ annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
+ tags: [availableTags[0].id, availableTags[1].id],
+ identifier: {
+ key: 'anAnnotationKey',
+ namespace: 'fooNameSpace'
+ },
+ targets: {
+ 'fooNameSpace:some-object': {
+ entryId: 'fooBarEntry'
+ }
+ }
+ };
+
+ mockObjectProvider = jasmine.createSpyObj("mock provider", [
+ "create",
+ "update",
+ "get"
+ ]);
+ // eslint-disable-next-line require-await
+ mockObjectProvider.get = async (identifier) => {
+ if (identifier.key === mockDomainObject.identifier.key) {
+ return mockDomainObject;
+ } else if (identifier.key === mockAnnotationObject.identifier.key) {
+ return mockAnnotationObject;
+ } else if (identifier.key === mockFolderObject.identifier.key) {
+ return mockFolderObject;
+ } else {
+ return null;
+ }
+ };
+
+ mockObjectProvider.create.and.returnValue(Promise.resolve(true));
+ mockObjectProvider.update.and.returnValue(Promise.resolve(true));
+
+ openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
+ openmct.on('start', done);
+ openmct.startHeadless();
+ });
+ afterEach(async () => {
+ openmct.objects.providers = {};
+ await resetApplicationState(openmct);
+ });
+ it("is defined", () => {
+ expect(openmct.annotation).toBeDefined();
+ });
+
+ describe("Creation", () => {
+ it("can create annotations", async () => {
+ const annotationCreationArguments = {
+ name: 'Test Annotation',
+ domainObject: mockDomainObject,
+ annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
+ tags: ['sometag'],
+ contentText: "fooContext",
+ targets: {'fooTarget': {}}
+ };
+ const annotationObject = await openmct.annotation.create(annotationCreationArguments);
+ expect(annotationObject).toBeDefined();
+ expect(annotationObject.type).toEqual('annotation');
+ });
+ it("fails if annotation is an unknown type", async () => {
+ try {
+ await openmct.annotation.create('Garbage Annotation', mockDomainObject, 'garbageAnnotation', ['sometag'], "fooContext", {'fooTarget': {}});
+ } catch (error) {
+ expect(error).toBeDefined();
+ }
+ });
+ });
+
+ describe("Tagging", () => {
+ it("can create a tag", async () => {
+ const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
+ expect(annotationObject).toBeDefined();
+ expect(annotationObject.type).toEqual('annotation');
+ expect(annotationObject.tags).toContain('aWonderfulTag');
+ });
+ it("can delete a tag", async () => {
+ const originalAnnotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
+ const annotationObject = await openmct.annotation.addAnnotationTag(originalAnnotationObject, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'anotherTagToRemove');
+ expect(annotationObject).toBeDefined();
+ openmct.annotation.removeAnnotationTag(annotationObject, 'anotherTagToRemove');
+ expect(annotationObject.tags).toEqual(['aWonderfulTag']);
+ openmct.annotation.removeAnnotationTag(annotationObject, 'aWonderfulTag');
+ expect(annotationObject.tags).toEqual([]);
+ });
+ it("throws an error if deleting non-existent tag", async () => {
+ const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
+ expect(annotationObject).toBeDefined();
+ expect(() => {
+ openmct.annotation.removeAnnotationTag(annotationObject, 'ThisTagShouldNotExist');
+ }).toThrow();
+ });
+ it("can remove all tags", async () => {
+ const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
+ expect(annotationObject).toBeDefined();
+ expect(() => {
+ openmct.annotation.removeAnnotationTags(annotationObject);
+ }).not.toThrow();
+ expect(annotationObject.tags).toEqual([]);
+ });
+ });
+
+ describe("Search", () => {
+ let sharedWorkerToRestore;
+ beforeEach(async () => {
+ // use local worker
+ sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
+ openmct.objects.inMemorySearchProvider.worker = null;
+ await openmct.objects.inMemorySearchProvider.index(mockFolderObject);
+ await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
+ await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject);
+ });
+ afterEach(() => {
+ openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
+ });
+ it("can search for tags", async () => {
+ const results = await openmct.annotation.searchForTags('S');
+ expect(results).toBeDefined();
+ expect(results.length).toEqual(1);
+ });
+ it("can get notebook annotations", async () => {
+ const targetKeyString = openmct.objects.makeKeyString(mockDomainObject.identifier);
+ const query = {
+ targetKeyString,
+ entryId: 'fooBarEntry'
+ };
+
+ const results = await openmct.annotation.getAnnotation(query, openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS);
+ expect(results).toBeDefined();
+ expect(results.tags.length).toEqual(2);
+ });
+ });
+});
diff --git a/src/api/api.js b/src/api/api.js
index 1a0174d57..505213476 100644
--- a/src/api/api.js
+++ b/src/api/api.js
@@ -24,6 +24,7 @@ define([
'./actions/ActionsAPI',
'./composition/CompositionAPI',
'./Editor',
+ './faultmanagement/FaultManagementAPI',
'./forms/FormsAPI',
'./indicators/IndicatorAPI',
'./menu/MenuAPI',
@@ -34,11 +35,13 @@ define([
'./telemetry/TelemetryAPI',
'./time/TimeAPI',
'./types/TypeRegistry',
- './user/UserAPI'
+ './user/UserAPI',
+ './annotation/AnnotationAPI'
], function (
ActionsAPI,
CompositionAPI,
EditorAPI,
+ FaultManagementAPI,
FormsAPI,
IndicatorAPI,
MenuAPI,
@@ -49,14 +52,16 @@ define([
TelemetryAPI,
TimeAPI,
TypeRegistry,
- UserAPI
+ UserAPI,
+ AnnotationAPI
) {
return {
ActionsAPI: ActionsAPI.default,
CompositionAPI: CompositionAPI,
EditorAPI: EditorAPI,
+ FaultManagementAPI: FaultManagementAPI,
FormsAPI: FormsAPI,
- IndicatorAPI: IndicatorAPI,
+ IndicatorAPI: IndicatorAPI.default,
MenuAPI: MenuAPI.default,
NotificationAPI: NotificationAPI.default,
ObjectAPI: ObjectAPI,
@@ -65,6 +70,7 @@ define([
TelemetryAPI: TelemetryAPI,
TimeAPI: TimeAPI.default,
TypeRegistry: TypeRegistry,
- UserAPI: UserAPI.default
+ UserAPI: UserAPI.default,
+ AnnotationAPI: AnnotationAPI.default
};
});
diff --git a/src/api/faultmanagement/FaultManagementAPI.js b/src/api/faultmanagement/FaultManagementAPI.js
new file mode 100644
index 000000000..b22a85c09
--- /dev/null
+++ b/src/api/faultmanagement/FaultManagementAPI.js
@@ -0,0 +1,106 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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.
+ *****************************************************************************/
+
+export default class FaultManagementAPI {
+ constructor(openmct) {
+ this.openmct = openmct;
+ }
+
+ addProvider(provider) {
+ this.provider = provider;
+ }
+
+ supportsActions() {
+ return this.provider?.acknowledgeFault !== undefined && this.provider?.shelveFault !== undefined;
+ }
+
+ request(domainObject) {
+ if (!this.provider?.supportsRequest(domainObject)) {
+ return Promise.reject();
+ }
+
+ return this.provider.request(domainObject);
+ }
+
+ subscribe(domainObject, callback) {
+ if (!this.provider?.supportsSubscribe(domainObject)) {
+ return Promise.reject();
+ }
+
+ return this.provider.subscribe(domainObject, callback);
+ }
+
+ acknowledgeFault(fault, ackData) {
+ return this.provider.acknowledgeFault(fault, ackData);
+ }
+
+ shelveFault(fault, shelveData) {
+ return this.provider.shelveFault(fault, shelveData);
+ }
+}
+
+/** @typedef {object} Fault
+ * @property {string} type
+ * @property {object} fault
+ * @property {boolean} fault.acknowledged
+ * @property {object} fault.currentValueInfo
+ * @property {number} fault.currentValueInfo.value
+ * @property {string} fault.currentValueInfo.rangeCondition
+ * @property {string} fault.currentValueInfo.monitoringResult
+ * @property {string} fault.id
+ * @property {string} fault.name
+ * @property {string} fault.namespace
+ * @property {number} fault.seqNum
+ * @property {string} fault.severity
+ * @property {boolean} fault.shelved
+ * @property {string} fault.shortDescription
+ * @property {string} fault.triggerTime
+ * @property {object} fault.triggerValueInfo
+ * @property {number} fault.triggerValueInfo.value
+ * @property {string} fault.triggerValueInfo.rangeCondition
+ * @property {string} fault.triggerValueInfo.monitoringResult
+ * @example
+ * {
+ * "type": "",
+ * "fault": {
+ * "acknowledged": true,
+ * "currentValueInfo": {
+ * "value": 0,
+ * "rangeCondition": "",
+ * "monitoringResult": ""
+ * },
+ * "id": "",
+ * "name": "",
+ * "namespace": "",
+ * "seqNum": 0,
+ * "severity": "",
+ * "shelved": true,
+ * "shortDescription": "",
+ * "triggerTime": "",
+ * "triggerValueInfo": {
+ * "value": 0,
+ * "rangeCondition": "",
+ * "monitoringResult": ""
+ * }
+ * }
+ * }
+ */
diff --git a/src/api/faultmanagement/FaultManagementAPISpec.js b/src/api/faultmanagement/FaultManagementAPISpec.js
new file mode 100644
index 000000000..61c87402e
--- /dev/null
+++ b/src/api/faultmanagement/FaultManagementAPISpec.js
@@ -0,0 +1,144 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 {
+ createOpenMct,
+ resetApplicationState
+} from '../../utils/testing';
+
+const faultName = 'super duper fault';
+const aFault = {
+ type: '',
+ fault: {
+ acknowledged: true,
+ currentValueInfo: {
+ value: 0,
+ rangeCondition: '',
+ monitoringResult: ''
+ },
+ id: '',
+ name: faultName,
+ namespace: '',
+ seqNum: 0,
+ severity: '',
+ shelved: true,
+ shortDescription: '',
+ triggerTime: '',
+ triggerValueInfo: {
+ value: 0,
+ rangeCondition: '',
+ monitoringResult: ''
+ }
+ }
+};
+const faultDomainObject = {
+ name: 'it is not your fault',
+ type: 'faultManagement',
+ identifier: {
+ key: 'nobodies',
+ namespace: 'fault'
+ }
+};
+const aComment = 'THIS is my fault.';
+const faultManagementProvider = {
+ request() {
+ return Promise.resolve([aFault]);
+ },
+ subscribe(domainObject, callback) {
+ return () => {};
+ },
+ supportsRequest(domainObject) {
+ return domainObject.type === 'faultManagement';
+ },
+ supportsSubscribe(domainObject) {
+ return domainObject.type === 'faultManagement';
+ },
+ acknowledgeFault(fault, { comment = '' }) {
+ return Promise.resolve({
+ success: true
+ });
+ },
+ shelveFault(fault, shelveData) {
+ return Promise.resolve({
+ success: true
+ });
+ }
+};
+
+describe('The Fault Management API', () => {
+ let openmct;
+
+ beforeEach(() => {
+ openmct = createOpenMct();
+ openmct.install(openmct.plugins.FaultManagement());
+ // openmct.install(openmct.plugins.example.ExampleFaultSource());
+ openmct.faults.addProvider(faultManagementProvider);
+ });
+
+ afterEach(() => {
+ return resetApplicationState(openmct);
+ });
+
+ it('allows you to request a fault', async () => {
+ spyOn(faultManagementProvider, 'supportsRequest').and.callThrough();
+
+ let faultResponse = await openmct.faults.request(faultDomainObject);
+
+ expect(faultManagementProvider.supportsRequest).toHaveBeenCalledWith(faultDomainObject);
+ expect(faultResponse[0].fault.name).toEqual(faultName);
+ });
+
+ it('allows you to subscribe to a fault', () => {
+ spyOn(faultManagementProvider, 'subscribe').and.callThrough();
+ spyOn(faultManagementProvider, 'supportsSubscribe').and.callThrough();
+
+ let unsubscribe = openmct.faults.subscribe(faultDomainObject, () => {});
+
+ expect(unsubscribe).toEqual(jasmine.any(Function));
+ expect(faultManagementProvider.supportsSubscribe).toHaveBeenCalledWith(faultDomainObject);
+ expect(faultManagementProvider.subscribe).toHaveBeenCalledOnceWith(faultDomainObject, jasmine.any(Function));
+
+ });
+
+ it('will tell you if the fault management provider supports actions', () => {
+ expect(openmct.faults.supportsActions()).toBeTrue();
+ });
+
+ it('will allow you to acknowledge a fault', async () => {
+ spyOn(faultManagementProvider, 'acknowledgeFault').and.callThrough();
+
+ let ackResponse = await openmct.faults.acknowledgeFault(aFault, aComment);
+
+ expect(faultManagementProvider.acknowledgeFault).toHaveBeenCalledWith(aFault, aComment);
+ expect(ackResponse.success).toBeTrue();
+ });
+
+ it('will allow you to shelve a fault', async () => {
+ spyOn(faultManagementProvider, 'shelveFault').and.callThrough();
+
+ let shelveResponse = await openmct.faults.shelveFault(aFault, aComment);
+
+ expect(faultManagementProvider.shelveFault).toHaveBeenCalledWith(aFault, aComment);
+ expect(shelveResponse.success).toBeTrue();
+ });
+
+});
diff --git a/src/api/forms/FormsAPI.js b/src/api/forms/FormsAPI.js
index e2bce0304..c0bdc78c6 100644
--- a/src/api/forms/FormsAPI.js
+++ b/src/api/forms/FormsAPI.js
@@ -23,10 +23,13 @@
import FormController from './FormController';
import FormProperties from './components/FormProperties.vue';
+import EventEmitter from 'EventEmitter';
import Vue from 'vue';
-export default class FormsAPI {
+export default class FormsAPI extends EventEmitter {
constructor(openmct) {
+ super();
+
this.openmct = openmct;
this.formController = new FormController(openmct);
}
@@ -107,15 +110,17 @@ export default class FormsAPI {
let onDismiss;
let onSave;
+ const self = this;
+
const promise = new Promise((resolve, reject) => {
- onSave = onFormSave(resolve);
- onDismiss = onFormDismiss(reject);
+ onSave = onFormAction(resolve);
+ onDismiss = onFormAction(reject);
});
const vm = new Vue({
components: { FormProperties },
provide: {
- openmct: this.openmct
+ openmct: self.openmct
},
data() {
return {
@@ -132,14 +137,15 @@ export default class FormsAPI {
if (element) {
element.append(formElement);
} else {
- overlay = this.openmct.overlays.overlay({
+ overlay = self.openmct.overlays.overlay({
element: vm.$el,
- size: 'small',
+ size: 'dialog',
onDestroy: () => vm.$destroy()
});
}
function onFormPropertyChange(data) {
+ self.emit('onFormPropertyChange', data);
if (onChange) {
onChange(data);
}
@@ -156,7 +162,7 @@ export default class FormsAPI {
}
}
- function onFormDismiss(dismiss) {
+ function onFormAction(callback) {
return () => {
if (element) {
formElement.remove();
@@ -164,18 +170,8 @@ export default class FormsAPI {
overlay.dismiss();
}
- if (dismiss) {
- dismiss();
- }
- };
- }
-
- function onFormSave(save) {
- return () => {
- overlay.dismiss();
-
- if (save) {
- save(changes);
+ if (callback) {
+ callback(changes);
}
};
}
diff --git a/src/api/forms/FormsAPISpec.js b/src/api/forms/FormsAPISpec.js
new file mode 100644
index 000000000..ac7f0fc9f
--- /dev/null
+++ b/src/api/forms/FormsAPISpec.js
@@ -0,0 +1,157 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 { createOpenMct, resetApplicationState } from '../../utils/testing';
+
+describe('The Forms API', () => {
+ let openmct;
+ let element;
+
+ beforeEach((done) => {
+ element = document.createElement('div');
+ element.style.display = 'block';
+ element.style.width = '1920px';
+ element.style.height = '1080px';
+
+ openmct = createOpenMct();
+ openmct.on('start', done);
+
+ openmct.startHeadless(element);
+ });
+
+ afterEach(() => {
+ return resetApplicationState(openmct);
+ });
+
+ it('openmct supports form API', () => {
+ expect(openmct.forms).not.toBe(null);
+ });
+
+ describe('check default form controls exists', () => {
+ it('autocomplete', () => {
+ const control = openmct.forms.getFormControl('autocomplete');
+ expect(control).not.toBe(null);
+ });
+
+ it('clock', () => {
+ const control = openmct.forms.getFormControl('composite');
+ expect(control).not.toBe(null);
+ });
+
+ it('datetime', () => {
+ const control = openmct.forms.getFormControl('datetime');
+ expect(control).not.toBe(null);
+ });
+
+ it('file-input', () => {
+ const control = openmct.forms.getFormControl('file-input');
+ expect(control).not.toBe(null);
+ });
+
+ it('locator', () => {
+ const control = openmct.forms.getFormControl('locator');
+ expect(control).not.toBe(null);
+ });
+
+ it('numberfield', () => {
+ const control = openmct.forms.getFormControl('numberfield');
+ expect(control).not.toBe(null);
+ });
+
+ it('select', () => {
+ const control = openmct.forms.getFormControl('select');
+ expect(control).not.toBe(null);
+ });
+
+ it('textarea', () => {
+ const control = openmct.forms.getFormControl('textarea');
+ expect(control).not.toBe(null);
+ });
+
+ it('textfield', () => {
+ const control = openmct.forms.getFormControl('textfield');
+ expect(control).not.toBe(null);
+ });
+ });
+
+ it('supports user defined form controls', () => {
+ const newFormControl = {
+ show: () => {
+ console.log('show new control');
+ },
+ destroy: () => {
+ console.log('destroy');
+ }
+ };
+ openmct.forms.addNewFormControl('newFormControl', newFormControl);
+ const control = openmct.forms.getFormControl('newFormControl');
+ expect(control).not.toBe(null);
+ expect(control.show).not.toBe(null);
+ expect(control.destroy).not.toBe(null);
+ });
+
+ describe('show form on UI', () => {
+ let formStructure;
+
+ beforeEach(() => {
+ formStructure = {
+ title: 'Test Show Form',
+ sections: [
+ {
+ rows: [
+ {
+ key: 'name',
+ control: 'textfield',
+ name: 'Title',
+ pattern: '\\S+',
+ required: false,
+ cssClass: 'l-input-lg',
+ value: 'Test Name'
+ }
+ ]
+ }
+ ]
+ };
+ });
+
+ it('when container element is provided', (done) => {
+ openmct.forms.showForm(formStructure, { element }).catch(() => {
+ done();
+ });
+ const titleElement = element.querySelector('.c-overlay__dialog-title');
+ expect(titleElement.textContent).toBe(formStructure.title);
+
+ element.querySelector('.js-cancel-button').click();
+ });
+
+ it('when container element is not provided', (done) => {
+ openmct.forms.showForm(formStructure).catch(() => {
+ done();
+ });
+
+ const titleElement = document.querySelector('.c-overlay__dialog-title');
+ const title = titleElement.textContent;
+
+ expect(title).toBe(formStructure.title);
+ document.querySelector('.js-cancel-button').click();
+ });
+ });
+});
diff --git a/src/api/forms/components/FormProperties.vue b/src/api/forms/components/FormProperties.vue
index 830fd0753..2ca84d0a7 100644
--- a/src/api/forms/components/FormProperties.vue
+++ b/src/api/forms/components/FormProperties.vue
@@ -21,10 +21,13 @@
*****************************************************************************/
<template>
-<div class="c-form">
+<div class="c-form js-form">
<div class="c-overlay__top-bar c-form__top-bar">
- <div class="c-overlay__dialog-title">{{ model.title }}</div>
- <div class="c-overlay__dialog-hint hint">All fields marked <span class="req icon-asterisk"></span> are required.</div>
+ <div class="c-overlay__dialog-title js-form-title">{{ model.title }}</div>
+ <div
+ v-if="hasRequiredFields"
+ class="c-overlay__dialog-hint hint"
+ >All fields marked <span class="req icon-asterisk"></span> are required.</div>
</div>
<form
name="mctForm"
@@ -44,18 +47,14 @@
>
{{ section.name }}
</h2>
- <div
+ <FormRow
v-for="(row, index) in section.rows"
:key="row.id"
- class="u-contents"
- >
- <FormRow
- :css-class="section.cssClass"
- :first="index < 1"
- :row="row"
- @onChange="onChange"
- />
- </div>
+ :css-class="row.cssClass"
+ :first="index < 1"
+ :row="row"
+ @onChange="onChange"
+ />
</div>
</form>
@@ -64,13 +63,16 @@
tabindex="0"
:disabled="isInvalid"
class="c-button c-button--major"
+ aria-label="Save"
@click="onSave"
>
{{ submitLabel }}
</button>
<button
+ v-if="!shouldHideCancelButton"
tabindex="0"
- class="c-button"
+ class="c-button js-cancel-button"
+ aria-label="Cancel"
@click="onDismiss"
>
{{ cancelLabel }}
@@ -81,7 +83,7 @@
<script>
import FormRow from "@/api/forms/components/FormRow.vue";
-import uuid from 'uuid';
+import { v4 as uuid } from 'uuid';
export default {
components: {
@@ -107,6 +109,10 @@ export default {
};
},
computed: {
+ hasRequiredFields() {
+ return this.model.sections.some(section =>
+ section.rows.some(row => row.required));
+ },
isInvalid() {
return Object.entries(this.invalidProperties)
.some(([key, value]) => {
@@ -134,6 +140,9 @@ export default {
}
return 'Cancel';
+ },
+ shouldHideCancelButton() {
+ return this.model.buttons?.cancel?.hide === true;
}
},
mounted() {
diff --git a/src/api/forms/components/FormRow.vue b/src/api/forms/components/FormRow.vue
index c6e3aba45..a7e9ac5b4 100644
--- a/src/api/forms/components/FormRow.vue
+++ b/src/api/forms/components/FormRow.vue
@@ -23,7 +23,10 @@
<template>
<div
class="form-row c-form__row"
- :class="[{ 'first': first }]"
+ :class="[
+ { 'first': first },
+ cssClass
+ ]"
@onChange="onChange"
>
<div
@@ -34,7 +37,7 @@
</div>
<div
class="c-form-row__state-indicator"
- :class="rowClass"
+ :class="reqClass"
>
</div>
<div
@@ -76,24 +79,22 @@ export default {
};
},
computed: {
- rowClass() {
- let cssClass = this.cssClass;
+ reqClass() {
+ let reqClass = 'req';
if (!this.row.required) {
return;
}
- cssClass = `${cssClass} req`;
-
if (this.visited && this.valid !== undefined) {
if (this.valid === true) {
- cssClass = `${cssClass} valid`;
+ reqClass = 'valid';
} else {
- cssClass = `${cssClass} invalid`;
+ reqClass = 'invalid';
}
}
- return cssClass;
+ return reqClass;
}
},
mounted() {
diff --git a/src/api/forms/components/controls/AutoCompleteField.vue b/src/api/forms/components/controls/AutoCompleteField.vue
index 1c6e2aad8..bc3910526 100644
--- a/src/api/forms/components/controls/AutoCompleteField.vue
+++ b/src/api/forms/components/controls/AutoCompleteField.vue
@@ -19,35 +19,47 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
-
<template>
-<div class="form-control autocomplete">
- <span class="autocompleteInputAndArrow">
+<div
+ ref="autoCompleteForm"
+ class="form-control c-input--autocomplete js-autocomplete"
+>
+ <div
+ class="c-input--autocomplete__wrapper"
+ >
<input
+ ref="autoCompleteInput"
v-model="field"
- class="autocompleteInput"
+ class="c-input--autocomplete__input js-autocomplete__input"
type="text"
+ :placeholder="placeHolderText"
@click="inputClicked()"
@keydown="keyDown($event)"
>
- <span
- class="icon-arrow-down"
+ <div
+ class="icon-arrow-down c-icon-button c-input--autocomplete__afford-arrow js-autocomplete__afford-arrow"
@click="arrowClicked()"
- ></span>
- </span>
+ ></div>
+ </div>
<div
- class="autocompleteOptions"
+ v-if="!hideOptions"
+ class="c-menu c-input--autocomplete__options"
+ aria-label="Autocomplete Options"
@blur="hideOptions = true"
>
- <ul v-if="!hideOptions">
+ <ul>
<li
v-for="opt in filteredOptions"
:key="opt.optionId"
- :class="{'optionPreSelected': optionIndex === opt.optionId}"
+ :class="[
+ {'optionPreSelected': optionIndex === opt.optionId},
+ itemCssClass
+ ]"
+ :style="itemStyle(opt)"
@click="fillInputWithString(opt.name)"
@mouseover="optionMouseover(opt.optionId)"
>
- <span class="optionText">{{ opt.name }}</span>
+ {{ opt.name }}
</li>
</ul>
</div>
@@ -65,7 +77,23 @@ export default {
props: {
model: {
type: Object,
- required: true
+ required: true,
+ default() {
+ return {};
+ }
+ },
+ placeHolderText: {
+ type: String,
+ default() {
+ return "";
+ }
+ },
+ itemCssClass: {
+ type: String,
+ required: false,
+ default() {
+ return "";
+ }
}
},
data() {
@@ -78,31 +106,40 @@ export default {
},
computed: {
filteredOptions() {
- const options = this.optionNames || [];
+ const fullOptions = this.options || [];
if (this.showFilteredOptions) {
- return options
+ const optionsFiltered = fullOptions
.filter(option => {
- return option.toLowerCase().indexOf(this.field.toLowerCase()) >= 0;
+ if (option.name && this.field) {
+ return option.name.toLowerCase().indexOf(this.field.toLowerCase()) >= 0;
+ }
+
+ return false;
}).map((option, index) => {
return {
optionId: index,
- name: option
+ name: option.name,
+ color: option.color
};
});
+
+ return optionsFiltered;
}
- return options.map((option, index) => {
+ const optionsFiltered = fullOptions.map((option, index) => {
return {
optionId: index,
- name: option
+ name: option.name,
+ color: option.color
};
});
+
+ return optionsFiltered;
}
},
watch: {
field(newValue, oldValue) {
if (newValue !== oldValue) {
-
const data = {
model: this.model,
value: newValue
@@ -123,17 +160,17 @@ export default {
}
},
mounted() {
- this.options = this.model.options;
- this.autocompleteInputAndArrow = this.$el.getElementsByClassName('autocompleteInputAndArrow')[0];
- this.autocompleteInputElement = this.$el.getElementsByClassName('autocompleteInput')[0];
- if (this.options[0].name) {
- // If "options" include name, value pair
- this.optionNames = this.options.map((opt) => {
- return opt.name;
+ this.autocompleteInputAndArrow = this.$refs.autoCompleteForm;
+ this.autocompleteInputElement = this.$refs.autoCompleteInput;
+ if (this.model.options && this.model.options.length && !this.model.options[0].name) {
+ // If options is only an array of string.
+ this.options = this.model.options.map((option) => {
+ return {
+ name: option
+ };
});
} else {
- // If options is only an array of string.
- this.optionNames = this.options;
+ this.options = this.model.options;
}
},
destroyed() {
@@ -222,6 +259,12 @@ export default {
});
}
});
+ },
+ itemStyle(option) {
+ if (option.color) {
+
+ return { '--optionIconColor': option.color };
+ }
}
}
};
diff --git a/src/api/forms/components/controls/Datetime.vue b/src/api/forms/components/controls/Datetime.vue
index 5c6bee75d..fa82a3415 100644
--- a/src/api/forms/components/controls/Datetime.vue
+++ b/src/api/forms/components/controls/Datetime.vue
@@ -32,53 +32,49 @@
prevent
class="u-contents"
>
- <div class="field control date">
- <input
- v-model="date"
- :pattern="/\d{4}-\d{2}-\d{2}/"
- :placeholder="format"
- type="date"
- name="date"
- @change="onChange"
- >
- </div>
- <div class="field control hour sm">
- <input
- v-model="hour"
- :pattern="/\d+/"
- type="number"
- name="hour"
- maxlength="10"
- min="0"
- max="23"
- @change="onChange"
- >
- </div>
- <div class="field control min sm">
- <input
- v-model="min"
- :pattern="/\d+/"
- type="number"
- name="min"
- maxlength="2"
- min="0"
- max="59"
- @change="onChange"
- >
- </div>
- <div class="field control sec sm">
- <input
- v-model="sec"
- :pattern="/\d+/"
- type="number"
- name="sec"
- maxlength="2"
- min="0"
- max="59"
- @change="onChange"
- >
- </div>
- <div class="field control timezone">
+ <input
+ v-model="date"
+ class="field control date"
+ :pattern="/\d{4}-\d{2}-\d{2}/"
+ :placeholder="format"
+ type="date"
+ name="date"
+ @change="onChange"
+ >
+ <input
+ v-model="hour"
+ class="field control hour c-input--sm"
+ :pattern="/\d+/"
+ type="number"
+ name="hour"
+ maxlength="10"
+ min="0"
+ max="23"
+ @change="onChange"
+ >
+ <input
+ v-model="min"
+ class="field control min c-input--sm"
+ :pattern="/\d+/"
+ type="number"
+ name="min"
+ maxlength="2"
+ min="0"
+ max="59"
+ @change="onChange"
+ >
+ <input
+ v-model="sec"
+ class="field control sec c-input--sm"
+ :pattern="/\d+/"
+ type="number"
+ name="sec"
+ maxlength="2"
+ min="0"
+ max="59"
+ @change="onChange"
+ >
+ <div class="field control hint timezone">
UTC
</div>
</form>
diff --git a/src/api/forms/components/controls/FileInput.vue b/src/api/forms/components/controls/FileInput.vue
index f1b10f8f3..ef9a6a3b8 100644
--- a/src/api/forms/components/controls/FileInput.vue
+++ b/src/api/forms/components/controls/FileInput.vue
@@ -40,6 +40,12 @@
>
{{ name }}
</button>
+ <button
+ v-if="removable"
+ class="c-button icon-trash"
+ title="Remove file"
+ @click="removeFile"
+ ></button>
</span>
</span>
</template>
@@ -63,6 +69,9 @@ export default {
const fileInfo = this.fileInfo || this.model.value;
return fileInfo && fileInfo.name || this.model.text;
+ },
+ removable() {
+ return (this.fileInfo || this.model.value) && this.model.removable;
}
},
mounted() {
@@ -97,6 +106,15 @@ export default {
},
selectFile() {
this.$refs.fileInput.click();
+ },
+ removeFile() {
+ this.model.value = undefined;
+ this.fileInfo = undefined;
+ const data = {
+ model: this.model,
+ value: undefined
+ };
+ this.$emit('onChange', data);
}
}
};
diff --git a/src/api/forms/components/controls/NumberField.vue b/src/api/forms/components/controls/NumberField.vue
index d4edd2b1b..ef85cc81a 100644
--- a/src/api/forms/components/controls/NumberField.vue
+++ b/src/api/forms/components/controls/NumberField.vue
@@ -28,6 +28,7 @@
>
<input
v-model="field"
+ :aria-label="model.name"
type="number"
:min="model.min"
:max="model.max"
diff --git a/src/api/forms/components/controls/ToggleSwitchField.vue b/src/api/forms/components/controls/ToggleSwitchField.vue
index 0c3a3e118..bea9224e3 100644
--- a/src/api/forms/components/controls/ToggleSwitchField.vue
+++ b/src/api/forms/components/controls/ToggleSwitchField.vue
@@ -39,7 +39,7 @@
import toggleMixin from '../../toggle-check-box-mixin';
import ToggleSwitch from '@/ui/components/ToggleSwitch.vue';
-import uuid from 'uuid';
+import { v4 as uuid } from 'uuid';
export default {
components: {
diff --git a/src/api/indicators/IndicatorAPI.js b/src/api/indicators/IndicatorAPI.js
index ef81f6788..98d78112c 100644
--- a/src/api/indicators/IndicatorAPI.js
+++ b/src/api/indicators/IndicatorAPI.js
@@ -19,27 +19,27 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
-define([
- './SimpleIndicator',
- 'lodash'
-], function (
- SimpleIndicator,
- _
-) {
- function IndicatorAPI(openmct) {
+
+import EventEmitter from "EventEmitter";
+import SimpleIndicator from "./SimpleIndicator";
+
+class IndicatorAPI extends EventEmitter {
+ constructor(openmct) {
+ super();
+
this.openmct = openmct;
this.indicatorObjects = [];
}
- IndicatorAPI.prototype.getIndicatorObjectsByPriority = function () {
+ getIndicatorObjectsByPriority() {
const sortedIndicators = this.indicatorObjects.sort((a, b) => b.priority - a.priority);
return sortedIndicators;
- };
+ }
- IndicatorAPI.prototype.simpleIndicator = function () {
+ simpleIndicator() {
return new SimpleIndicator(this.openmct);
- };
+ }
/**
* Accepts an indicator object, which is a simple object
@@ -62,14 +62,16 @@ define([
* myIndicator.iconClass("icon-info");
*
*/
- IndicatorAPI.prototype.add = function (indicator) {
+ add(indicator) {
if (!indicator.priority) {
indicator.priority = this.openmct.priority.DEFAULT;
}
this.indicatorObjects.push(indicator);
- };
- return IndicatorAPI;
+ this.emit('addIndicator', indicator);
+ }
+
+}
-});
+export default IndicatorAPI;
diff --git a/src/api/indicators/SimpleIndicator.js b/src/api/indicators/SimpleIndicator.js
index 7556dd512..31ce745a5 100644
--- a/src/api/indicators/SimpleIndicator.js
+++ b/src/api/indicators/SimpleIndicator.js
@@ -20,82 +20,101 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
-define(['zepto', './res/indicator-template.html'],
- function ($, indicatorTemplate) {
- const DEFAULT_ICON_CLASS = 'icon-info';
-
- function SimpleIndicator(openmct) {
- this.openmct = openmct;
- this.element = $(indicatorTemplate)[0];
- this.priority = openmct.priority.DEFAULT;
-
- this.textElement = this.element.querySelector('.js-indicator-text');
-
- //Set defaults
- this.text('New Indicator');
- this.description('');
- this.iconClass(DEFAULT_ICON_CLASS);
- this.statusClass('');
- }
+import EventEmitter from 'EventEmitter';
+import indicatorTemplate from './res/indicator-template.html';
+import { convertTemplateToHTML } from '@/utils/template/templateHelpers';
- SimpleIndicator.prototype.text = function (text) {
- if (text !== undefined && text !== this.textValue) {
- this.textValue = text;
- this.textElement.innerText = text;
+const DEFAULT_ICON_CLASS = 'icon-info';
- if (!text) {
- this.element.classList.add('hidden');
- } else {
- this.element.classList.remove('hidden');
- }
- }
+class SimpleIndicator extends EventEmitter {
+ constructor(openmct) {
+ super();
+
+ this.openmct = openmct;
+ this.element = convertTemplateToHTML(indicatorTemplate)[0];
+ this.priority = openmct.priority.DEFAULT;
+
+ this.textElement = this.element.querySelector('.js-indicator-text');
+
+ //Set defaults
+ this.text('New Indicator');
+ this.description('');
+ this.iconClass(DEFAULT_ICON_CLASS);
+
+ this.click = this.click.bind(this);
- return this.textValue;
- };
+ this.element.addEventListener('click', this.click);
+ openmct.once('destroy', () => {
+ this.removeAllListeners();
+ this.element.removeEventListener('click', this.click);
+ });
+ }
+
+ text(text) {
+ if (text !== undefined && text !== this.textValue) {
+ this.textValue = text;
+ this.textElement.innerText = text;
- SimpleIndicator.prototype.description = function (description) {
- if (description !== undefined && description !== this.descriptionValue) {
- this.descriptionValue = description;
- this.element.title = description;
+ if (!text) {
+ this.element.classList.add('hidden');
+ } else {
+ this.element.classList.remove('hidden');
}
+ }
- return this.descriptionValue;
- };
+ return this.textValue;
+ }
- SimpleIndicator.prototype.iconClass = function (iconClass) {
- if (iconClass !== undefined && iconClass !== this.iconClassValue) {
- // element.classList is precious and throws errors if you try and add
- // or remove empty strings
- if (this.iconClassValue) {
- this.element.classList.remove(this.iconClassValue);
- }
+ description(description) {
+ if (description !== undefined && description !== this.descriptionValue) {
+ this.descriptionValue = description;
+ this.element.title = description;
+ }
- if (iconClass) {
- this.element.classList.add(iconClass);
- }
+ return this.descriptionValue;
+ }
- this.iconClassValue = iconClass;
+ iconClass(iconClass) {
+ if (iconClass !== undefined && iconClass !== this.iconClassValue) {
+ // element.classList is precious and throws errors if you try and add
+ // or remove empty strings
+ if (this.iconClassValue) {
+ this.element.classList.remove(this.iconClassValue);
}
- return this.iconClassValue;
- };
+ if (iconClass) {
+ this.element.classList.add(iconClass);
+ }
+
+ this.iconClassValue = iconClass;
+ }
- SimpleIndicator.prototype.statusClass = function (statusClass) {
- if (statusClass !== undefined && statusClass !== this.statusClassValue) {
- if (this.statusClassValue) {
- this.element.classList.remove(this.statusClassValue);
- }
+ return this.iconClassValue;
+ }
- if (statusClass) {
- this.element.classList.add(statusClass);
- }
+ statusClass(statusClass) {
+ if (arguments.length === 1 && statusClass !== this.statusClassValue) {
+ if (this.statusClassValue) {
+ this.element.classList.remove(this.statusClassValue);
+ }
- this.statusClassValue = statusClass;
+ if (statusClass !== undefined) {
+ this.element.classList.add(statusClass);
}
- return this.statusClassValue;
- };
+ this.statusClassValue = statusClass;
+ }
- return SimpleIndicator;
+ return this.statusClassValue;
}
-);
+
+ click(event) {
+ this.emit('click', event);
+ }
+
+ getElement() {
+ return this.element;
+ }
+}
+
+export default SimpleIndicator;
diff --git a/src/api/menu/MenuAPISpec.js b/src/api/menu/MenuAPISpec.js
index c747ab646..00a55f873 100644
--- a/src/api/menu/MenuAPISpec.js
+++ b/src/api/menu/MenuAPISpec.js
@@ -26,29 +26,31 @@ import { createOpenMct, createMouseEvent, resetApplicationState } from '../../ut
describe ('The Menu API', () => {
let openmct;
- let element;
+ let appHolder;
let menuAPI;
let actionsArray;
- let x;
- let y;
let result;
- let onDestroy;
+ let menuElement;
+
+ const x = 8;
+ const y = 16;
+
+ const menuOptions = {
+ onDestroy: () => {
+ console.log('default onDestroy');
+ }
+ };
beforeEach((done) => {
- const appHolder = document.createElement('div');
+ appHolder = document.createElement('div');
appHolder.style.display = 'block';
appHolder.style.width = '1920px';
appHolder.style.height = '1080px';
openmct = createOpenMct();
- element = document.createElement('div');
- element.style.display = 'block';
- element.style.width = '1920px';
- element.style.height = '1080px';
-
openmct.on('start', done);
- openmct.startHeadless(appHolder);
+ openmct.startHeadless();
menuAPI = new MenuAPI(openmct);
actionsArray = [
@@ -56,7 +58,7 @@ describe ('The Menu API', () => {
key: 'test-css-class-1',
name: 'Test Action 1',
cssClass: 'icon-clock',
- description: 'This is a test action',
+ description: 'This is a test action 1',
onItemClicked: () => {
result = 'Test Action 1 Invoked';
}
@@ -65,149 +67,165 @@ describe ('The Menu API', () => {
key: 'test-css-class-2',
name: 'Test Action 2',
cssClass: 'icon-clock',
- description: 'This is a test action',
+ description: 'This is a test action 2',
onItemClicked: () => {
result = 'Test Action 2 Invoked';
}
}
];
- x = 8;
- y = 16;
});
afterEach(() => {
return resetApplicationState(openmct);
});
- describe("showMenu method", () => {
- it("creates an instance of Menu when invoked", () => {
- menuAPI.showMenu(x, y, actionsArray);
-
- expect(menuAPI.menuComponent).toBeInstanceOf(Menu);
+ describe('showMenu method', () => {
+ beforeAll(() => {
+ spyOn(menuOptions, 'onDestroy').and.callThrough();
});
- describe("creates a menu component", () => {
- let menuComponent;
- let vueComponent;
+ it('creates an instance of Menu when invoked', (done) => {
+ menuOptions.onDestroy = done;
- beforeEach(() => {
- onDestroy = jasmine.createSpy('onDestroy');
+ menuAPI.showMenu(x, y, actionsArray, menuOptions);
- const menuOptions = {
- onDestroy
- };
+ expect(menuAPI.menuComponent).toBeInstanceOf(Menu);
+ document.body.click();
+ });
- menuAPI.showMenu(x, y, actionsArray, menuOptions);
- vueComponent = menuAPI.menuComponent.component;
- menuComponent = document.querySelector(".c-menu");
+ describe('creates a menu component', () => {
+ it('with all the actions passed in', (done) => {
+ menuOptions.onDestroy = done;
- spyOn(vueComponent, '$destroy');
- });
+ menuAPI.showMenu(x, y, actionsArray, menuOptions);
+ menuElement = document.querySelector('.c-menu');
+ expect(menuElement).toBeDefined();
- it("renders a menu component in the expected x and y coordinates", () => {
- let boundingClientRect = menuComponent.getBoundingClientRect();
- let left = boundingClientRect.left;
- let top = boundingClientRect.top;
+ const listItems = menuElement.children[0].children;
- expect(left).toEqual(x);
- expect(top).toEqual(y);
+ expect(listItems.length).toEqual(actionsArray.length);
+ document.body.click();
});
- it("with all the actions passed in", () => {
- expect(menuComponent).toBeDefined();
+ it('with click-able menu items, that will invoke the correct callBack', (done) => {
+ menuOptions.onDestroy = done;
- let listItems = menuComponent.children[0].children;
-
- expect(listItems.length).toEqual(actionsArray.length);
- });
+ menuAPI.showMenu(x, y, actionsArray, menuOptions);
- it("with click-able menu items, that will invoke the correct callBacks", () => {
- let listItem1 = menuComponent.children[0].children[0];
+ menuElement = document.querySelector('.c-menu');
+ const listItem1 = menuElement.children[0].children[0];
listItem1.click();
- expect(result).toEqual("Test Action 1 Invoked");
+ expect(result).toEqual('Test Action 1 Invoked');
});
- it("dismisses the menu when action is clicked on", () => {
- let listItem1 = menuComponent.children[0].children[0];
+ it('dismisses the menu when action is clicked on', (done) => {
+ menuOptions.onDestroy = done;
+ menuAPI.showMenu(x, y, actionsArray, menuOptions);
+
+ menuElement = document.querySelector('.c-menu');
+ const listItem1 = menuElement.children[0].children[0];
listItem1.click();
- let menu = document.querySelector('.c-menu');
+ menuElement = document.querySelector('.c-menu');
- expect(menu).toBeNull();
+ expect(menuElement).toBeNull();
});
- it("invokes the destroy method when menu is dismissed", () => {
+ it('invokes the destroy method when menu is dismissed', (done) => {
+ menuOptions.onDestroy = done;
+
+ menuAPI.showMenu(x, y, actionsArray, menuOptions);
+
+ const vueComponent = menuAPI.menuComponent.component;
+ spyOn(vueComponent, '$destroy');
+
document.body.click();
expect(vueComponent.$destroy).toHaveBeenCalled();
});
- it("invokes the onDestroy callback if passed in", () => {
- document.body.click();
+ it('invokes the onDestroy callback if passed in', (done) => {
+ let count = 0;
+ menuOptions.onDestroy = () => {
+ count++;
+ expect(count).toEqual(1);
+ done();
+ };
- expect(onDestroy).toHaveBeenCalled();
+ menuAPI.showMenu(x, y, actionsArray, menuOptions);
+
+ document.body.click();
});
});
});
- describe("superMenu method", () => {
- it("creates a superMenu", () => {
- menuAPI.showSuperMenu(x, y, actionsArray);
+ describe('superMenu method', () => {
+ it('creates a superMenu', (done) => {
+ menuOptions.onDestroy = done;
- const superMenu = document.querySelector('.c-super-menu__menu');
+ menuAPI.showSuperMenu(x, y, actionsArray, menuOptions);
+ menuElement = document.querySelector('.c-super-menu__menu');
- expect(superMenu).not.toBeNull();
+ expect(menuElement).not.toBeNull();
+ document.body.click();
});
- it("Mouse over a superMenu shows correct description", (done) => {
- menuAPI.showSuperMenu(x, y, actionsArray);
+ it('Mouse over a superMenu shows correct description', (done) => {
+ menuOptions.onDestroy = done;
+
+ menuAPI.showSuperMenu(x, y, actionsArray, menuOptions);
+ menuElement = document.querySelector('.c-super-menu__menu');
- const superMenu = document.querySelector('.c-super-menu__menu');
- const superMenuItem = superMenu.querySelector('li');
+ const superMenuItem = menuElement.querySelector('li');
const mouseOverEvent = createMouseEvent('mouseover');
superMenuItem.dispatchEvent(mouseOverEvent);
const itemDescription = document.querySelector('.l-item-description__description');
- setTimeout(() => {
+ menuAPI.menuComponent.component.$nextTick(() => {
+ expect(menuElement).not.toBeNull();
expect(itemDescription.innerText).toEqual(actionsArray[0].description);
- expect(superMenu).not.toBeNull();
- done();
- }, 300);
+
+ document.body.click();
+ });
});
});
- describe("Menu Placements", () => {
- it("default menu position BOTTOM_RIGHT", () => {
- menuAPI.showMenu(x, y, actionsArray);
+ describe('Menu Placements', () => {
+ it('default menu position BOTTOM_RIGHT', (done) => {
+ menuOptions.onDestroy = done;
- const menu = document.querySelector('.c-menu');
+ menuAPI.showMenu(x, y, actionsArray, menuOptions);
+ menuElement = document.querySelector('.c-menu');
- const boundingClientRect = menu.getBoundingClientRect();
+ const boundingClientRect = menuElement.getBoundingClientRect();
const left = boundingClientRect.left;
const top = boundingClientRect.top;
expect(left).toEqual(x);
expect(top).toEqual(y);
+
+ document.body.click();
});
- it("menu position BOTTOM_RIGHT", () => {
- const menuOptions = {
- placement: openmct.menus.menuPlacement.BOTTOM_RIGHT
- };
+ it('menu position BOTTOM_RIGHT', (done) => {
+ menuOptions.onDestroy = done;
+ menuOptions.placement = openmct.menus.menuPlacement.BOTTOM_RIGHT;
menuAPI.showMenu(x, y, actionsArray, menuOptions);
+ menuElement = document.querySelector('.c-menu');
- const menu = document.querySelector('.c-menu');
- const boundingClientRect = menu.getBoundingClientRect();
+ const boundingClientRect = menuElement.getBoundingClientRect();
const left = boundingClientRect.left;
const top = boundingClientRect.top;
expect(left).toEqual(x);
expect(top).toEqual(y);
+
+ document.body.click();
});
});
});
diff --git a/src/api/menu/components/Menu.vue b/src/api/menu/components/Menu.vue
index 0073062e0..23a11f19b 100644
--- a/src/api/menu/components/Menu.vue
+++ b/src/api/menu/components/Menu.vue
@@ -12,6 +12,7 @@
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
+ :data-testid="action.testId || false"
@click="action.onItemClicked"
>
{{ action.name }}
@@ -35,8 +36,9 @@
<li
v-for="action in options.actions"
:key="action.name"
- :class="action.cssClass"
+ :class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
+ :data-testid="action.testId || false"
@click="action.onItemClicked"
>
{{ action.name }}
diff --git a/src/api/menu/components/SuperMenu.vue b/src/api/menu/components/SuperMenu.vue
index d7fe6a7a6..7b66c68b6 100644
--- a/src/api/menu/components/SuperMenu.vue
+++ b/src/api/menu/components/SuperMenu.vue
@@ -15,6 +15,7 @@
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
+ :data-testid="action.testId || false"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
@@ -45,6 +46,7 @@
:key="action.name"
:class="action.cssClass"
:title="action.description"
+ :data-testid="action.testId || false"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
diff --git a/src/api/objects/InMemorySearchProvider.js b/src/api/objects/InMemorySearchProvider.js
index df67c03ae..6feadaf44 100644
--- a/src/api/objects/InMemorySearchProvider.js
+++ b/src/api/objects/InMemorySearchProvider.js
@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
-import uuid from 'uuid';
+import { v4 as uuid } from 'uuid';
class InMemorySearchProvider {
/**
@@ -39,11 +39,10 @@ class InMemorySearchProvider {
* If max results is not specified in query, use this as default.
*/
this.DEFAULT_MAX_RESULTS = 100;
-
this.openmct = openmct;
-
this.indexedIds = {};
this.indexedCompositions = {};
+ this.indexedTags = {};
this.idsToIndex = [];
this.pendingIndex = {};
this.pendingRequests = 0;
@@ -52,11 +51,20 @@ class InMemorySearchProvider {
/**
* If we don't have SharedWorkers available (e.g., iOS)
*/
- this.localIndexedItems = {};
+ this.localIndexedDomainObjects = {};
+ this.localIndexedAnnotationsByDomainObject = {};
+ this.localIndexedAnnotationsByTag = {};
this.pendingQueries = {};
this.onWorkerMessage = this.onWorkerMessage.bind(this);
this.onWorkerMessageError = this.onWorkerMessageError.bind(this);
+ this.localSearchForObjects = this.localSearchForObjects.bind(this);
+ this.localSearchForAnnotations = this.localSearchForAnnotations.bind(this);
+ this.localSearchForTags = this.localSearchForTags.bind(this);
+ this.localSearchForNotebookAnnotations = this.localSearchForNotebookAnnotations.bind(this);
+ this.onAnnotationCreation = this.onAnnotationCreation.bind(this);
+ this.onCompositionAdded = this.onCompositionAdded.bind(this);
+ this.onCompositionRemoved = this.onCompositionRemoved.bind(this);
this.onerror = this.onWorkerError.bind(this);
this.startIndexing = this.startIndexing.bind(this);
@@ -69,6 +77,12 @@ class InMemorySearchProvider {
this.worker.port.close();
}
+ Object.keys(this.indexedCompositions).forEach(keyString => {
+ const composition = this.indexedCompositions[keyString];
+ composition.off('add', this.onCompositionAdded);
+ composition.off('remove', this.onCompositionRemoved);
+ });
+
this.destroyObservers(this.indexedIds);
this.destroyObservers(this.indexedCompositions);
});
@@ -76,13 +90,39 @@ class InMemorySearchProvider {
startIndexing() {
const rootObject = this.openmct.objects.rootProvider.rootObject;
+
+ this.searchTypes = this.openmct.objects.SEARCH_TYPES;
+
+ this.supportedSearchTypes = [this.searchTypes.OBJECTS, this.searchTypes.ANNOTATIONS, this.searchTypes.NOTEBOOK_ANNOTATIONS, this.searchTypes.TAGS];
+
this.scheduleForIndexing(rootObject.identifier);
+ this.indexAnnotations();
+
if (typeof SharedWorker !== 'undefined') {
this.worker = this.startSharedWorker();
} else {
// we must be on iOS
}
+
+ this.openmct.annotation.on('annotationCreated', this.onAnnotationCreation);
+
+ }
+
+ indexAnnotations() {
+ const theInMemorySearchProvider = this;
+ Object.values(this.openmct.objects.providers).forEach(objectProvider => {
+ if (objectProvider.getAllObjects) {
+ const allObjects = objectProvider.getAllObjects();
+ if (allObjects) {
+ Object.values(allObjects).forEach(domainObject => {
+ if (domainObject.type === 'annotation') {
+ theInMemorySearchProvider.scheduleForIndexing(domainObject.identifier);
+ }
+ });
+ }
+ }
+ });
}
/**
@@ -98,51 +138,60 @@ class InMemorySearchProvider {
return intermediateResponse;
}
- /**
- * Query the search provider for results.
- *
- * @param {String} input the string to search by.
- * @param {Number} maxResults max number of results to return.
- * @returns {Promise} a promise for a modelResults object.
- */
- query(input, maxResults) {
- if (!maxResults) {
- maxResults = this.DEFAULT_MAX_RESULTS;
- }
-
+ search(query, searchType) {
const queryId = uuid();
const pendingQuery = this.getIntermediateResponse();
this.pendingQueries[queryId] = pendingQuery;
+ const searchOptions = {
+ queryId,
+ searchType,
+ query,
+ maxResults: this.DEFAULT_MAX_RESULTS
+ };
if (this.worker) {
- this.dispatchSearch(queryId, input, maxResults);
+ this.#dispatchSearchToWorker(searchOptions);
} else {
- this.localSearch(queryId, input, maxResults);
+ this.#localQueryFallBack(searchOptions);
}
return pendingQuery.promise;
}
+ #localQueryFallBack({queryId, searchType, query, maxResults}) {
+ if (searchType === this.searchTypes.OBJECTS) {
+ return this.localSearchForObjects(queryId, query, maxResults);
+ } else if (searchType === this.searchTypes.ANNOTATIONS) {
+ return this.localSearchForAnnotations(queryId, query, maxResults);
+ } else if (searchType === this.searchTypes.NOTEBOOK_ANNOTATIONS) {
+ return this.localSearchForNotebookAnnotations(queryId, query, maxResults);
+ } else if (searchType === this.searchTypes.TAGS) {
+ return this.localSearchForTags(queryId, query, maxResults);
+ } else {
+ throw new Error(`🤷‍♂️ Unknown search type passed: ${searchType}`);
+ }
+ }
+
+ supportsSearchType(searchType) {
+ return this.supportedSearchTypes.includes(searchType);
+ }
+
/**
- * Handle messages from the worker. Only really knows how to handle search
- * results, which are parsed, transformed into a modelResult object, which
- * is used to resolve the corresponding promise.
+ * Handle messages from the worker.
* @private
*/
async onWorkerMessage(event) {
- if (event.data.request !== 'search') {
- return;
- }
-
const pendingQuery = this.pendingQueries[event.data.queryId];
const modelResults = {
total: event.data.total
};
modelResults.hits = await Promise.all(event.data.results.map(async (hit) => {
- const identifier = this.openmct.objects.parseKeyString(hit.keyString);
- const domainObject = await this.openmct.objects.get(identifier);
+ if (hit && hit.keyString) {
+ const identifier = this.openmct.objects.parseKeyString(hit.keyString);
+ const domainObject = await this.openmct.objects.get(identifier);
- return domainObject;
+ return domainObject;
+ }
}));
pendingQuery.resolve(modelResults);
@@ -183,7 +232,8 @@ class InMemorySearchProvider {
/**
* Schedule an id to be indexed at a later date. If there are less
- * pending requests then allowed, will kick off an indexing request.
+ * pending requests than the maximum allowed, this will kick off an indexing request.
+ * This is done only when indexing first begins and we need to index a lot of objects.
*
* @private
* @param {identifier} id to be indexed.
@@ -216,6 +266,14 @@ class InMemorySearchProvider {
}
}
+ onAnnotationCreation(annotationObject) {
+ const objectProvider = this.openmct.objects.getProvider(annotationObject.identifier);
+ if (objectProvider === undefined || objectProvider.search === undefined) {
+ const provider = this;
+ provider.index(annotationObject);
+ }
+ }
+
onNameMutation(domainObject, name) {
const provider = this;
@@ -223,17 +281,41 @@ class InMemorySearchProvider {
provider.index(domainObject);
}
- onCompositionMutation(domainObject, composition) {
+ onTagMutation(domainObject, newTags) {
+ domainObject.tags = newTags;
const provider = this;
- const indexedComposition = domainObject.composition;
- const identifiersToIndex = composition
- .filter(identifier => !indexedComposition
- .some(indexedIdentifier => this.openmct.objects
- .areIdsEqual([identifier, indexedIdentifier])));
-
- identifiersToIndex.forEach(identifier => {
- this.openmct.objects.get(identifier).then(objectToIndex => provider.index(objectToIndex));
- });
+
+ provider.index(domainObject);
+ }
+
+ onCompositionAdded(newDomainObjectToIndex) {
+ const provider = this;
+ // The object comes in as a mutable domain object, which has functions,
+ // which the index function cannot handle as it will eventually be serialized
+ // using structuredClone. Thus we're using JSON.parse/JSON.stringify to discard
+ // those functions.
+ const nonMutableDomainObject = JSON.parse(JSON.stringify(newDomainObjectToIndex));
+
+ const objectProvider = this.openmct.objects.getProvider(nonMutableDomainObject.identifier);
+ if (objectProvider === undefined || objectProvider.search === undefined) {
+ provider.index(nonMutableDomainObject);
+ }
+ }
+
+ onCompositionRemoved(domainObjectToRemoveIdentifier) {
+ const keyString = this.openmct.objects.makeKeyString(domainObjectToRemoveIdentifier);
+ if (this.indexedIds[keyString]) {
+ // we store the unobserve function in the indexedId map
+ this.indexedIds[keyString]();
+ delete this.indexedIds[keyString];
+ }
+
+ const composition = this.indexedCompositions[keyString];
+ if (composition) {
+ composition.off('add', this.onCompositionAdded);
+ composition.off('remove', this.onCompositionRemoved);
+ delete this.indexedCompositions[keyString];
+ }
}
/**
@@ -247,6 +329,7 @@ class InMemorySearchProvider {
async index(domainObject) {
const provider = this;
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
+ const composition = this.openmct.composition.get(domainObject);
if (!this.indexedIds[keyString]) {
this.indexedIds[keyString] = this.openmct.objects.observe(
@@ -254,11 +337,19 @@ class InMemorySearchProvider {
'name',
this.onNameMutation.bind(this, domainObject)
);
- this.indexedCompositions[keyString] = this.openmct.objects.observe(
- domainObject,
- 'composition',
- this.onCompositionMutation.bind(this, domainObject)
- );
+ if (composition) {
+ composition.on('add', this.onCompositionAdded);
+ composition.on('remove', this.onCompositionRemoved);
+ this.indexedCompositions[keyString] = composition;
+ }
+
+ if (domainObject.type === 'annotation') {
+ this.indexedTags[keyString] = this.openmct.objects.observe(
+ domainObject,
+ 'tags',
+ this.onTagMutation.bind(this, domainObject)
+ );
+ }
}
if ((keyString !== 'ROOT')) {
@@ -273,8 +364,6 @@ class InMemorySearchProvider {
}
}
- const composition = this.openmct.composition.get(domainObject);
-
if (composition !== undefined) {
const children = await composition.load();
@@ -317,26 +406,83 @@ class InMemorySearchProvider {
* @private
* @returns {String} a unique query Id for the query.
*/
- dispatchSearch(queryId, searchInput, maxResults) {
+ #dispatchSearchToWorker({queryId, searchType, query, maxResults}) {
const message = {
- request: 'search',
- input: searchInput,
+ request: searchType.toString(),
+ input: query,
maxResults,
queryId
};
this.worker.port.postMessage(message);
}
+ localIndexTags(keyString, objectToIndex, model) {
+ // add new tags
+ model.tags.forEach(tagID => {
+ if (!this.localIndexedAnnotationsByTag[tagID]) {
+ this.localIndexedAnnotationsByTag[tagID] = [];
+ }
+
+ const existsInIndex = this.localIndexedAnnotationsByTag[tagID].some(indexedObject => {
+ return indexedObject.keyString === objectToIndex.keyString;
+ });
+
+ if (!existsInIndex) {
+ this.localIndexedAnnotationsByTag[tagID].push(objectToIndex);
+ }
+
+ });
+ const tagsToRemoveFromIndex = Object.keys(this.localIndexedAnnotationsByTag).filter(indexedTag => {
+ return !(model.tags.includes(indexedTag));
+ });
+ tagsToRemoveFromIndex.forEach(tagToRemoveFromIndex => {
+ this.localIndexedAnnotationsByTag[tagToRemoveFromIndex] = this.localIndexedAnnotationsByTag[tagToRemoveFromIndex].filter(indexedAnnotation => {
+ const shouldKeep = indexedAnnotation.keyString !== keyString;
+
+ return shouldKeep;
+ });
+ });
+ }
+
+ localIndexAnnotation(objectToIndex, model) {
+ Object.keys(model.targets).forEach(targetID => {
+ if (!this.localIndexedAnnotationsByDomainObject[targetID]) {
+ this.localIndexedAnnotationsByDomainObject[targetID] = [];
+ }
+
+ objectToIndex.targets = model.targets;
+ objectToIndex.tags = model.tags;
+ const existsInIndex = this.localIndexedAnnotationsByDomainObject[targetID].some(indexedObject => {
+ return indexedObject.keyString === objectToIndex.keyString;
+ });
+
+ if (!existsInIndex) {
+ this.localIndexedAnnotationsByDomainObject[targetID].push(objectToIndex);
+ }
+ });
+ }
+
/**
* A local version of the same SharedWorker function
* if we don't have SharedWorkers available (e.g., iOS)
*/
localIndexItem(keyString, model) {
- this.localIndexedItems[keyString] = {
+ const objectToIndex = {
type: model.type,
name: model.name,
keyString
};
+ if (model && (model.type === 'annotation')) {
+ if (model.targets) {
+ this.localIndexAnnotation(objectToIndex, model);
+ }
+
+ if (model.tags) {
+ this.localIndexTags(keyString, objectToIndex, model);
+ }
+ } else {
+ this.localIndexedDomainObjects[keyString] = objectToIndex;
+ }
}
/**
@@ -346,21 +492,122 @@ class InMemorySearchProvider {
* Gets search results from the indexedItems based on provided search
* input. Returns matching results from indexedItems
*/
- localSearch(queryId, searchInput, maxResults) {
+ localSearchForObjects(queryId, searchInput, maxResults) {
// This results dictionary will have domain object ID keys which
// point to the value the domain object's score.
- let results;
+ let results = [];
const input = searchInput.trim().toLowerCase();
const message = {
- request: 'search',
- results: {},
+ request: 'searchForObjects',
+ results: [],
total: 0,
queryId
};
- results = Object.values(this.localIndexedItems).filter((indexedItem) => {
+ results = Object.values(this.localIndexedDomainObjects).filter((indexedItem) => {
return indexedItem.name.toLowerCase().includes(input);
- });
+ }) || [];
+
+ message.total = results.length;
+ message.results = results
+ .slice(0, maxResults);
+ const eventToReturn = {
+ data: message
+ };
+ this.onWorkerMessage(eventToReturn);
+ }
+
+ /**
+ * A local version of the same SharedWorker function
+ * if we don't have SharedWorkers available (e.g., iOS)
+ */
+ localSearchForAnnotations(queryId, searchInput, maxResults) {
+ // This results dictionary will have domain object ID keys which
+ // point to the value the domain object's score.
+ let results = [];
+ const message = {
+ request: 'searchForAnnotations',
+ results: [],
+ total: 0,
+ queryId
+ };
+
+ results = this.localIndexedAnnotationsByDomainObject[searchInput] || [];
+
+ message.total = results.length;
+ message.results = results
+ .slice(0, maxResults);
+ const eventToReturn = {
+ data: message
+ };
+ this.onWorkerMessage(eventToReturn);
+ }
+
+ /**
+ * A local version of the same SharedWorker function
+ * if we don't have SharedWorkers available (e.g., iOS)
+ */
+ localSearchForTags(queryId, matchingTagKeys, maxResults) {
+ let results = [];
+ const message = {
+ request: 'searchForTags',
+ results: [],
+ total: 0,
+ queryId
+ };
+
+ if (matchingTagKeys) {
+ matchingTagKeys.forEach(matchingTag => {
+ const matchingAnnotations = this.localIndexedAnnotationsByTag[matchingTag];
+ if (matchingAnnotations) {
+ matchingAnnotations.forEach(matchingAnnotation => {
+ const existsInResults = results.some(indexedObject => {
+ return matchingAnnotation.keyString === indexedObject.keyString;
+ });
+ if (!existsInResults) {
+ results.push(matchingAnnotation);
+ }
+ });
+ }
+ });
+ }
+
+ message.total = results.length;
+ message.results = results
+ .slice(0, maxResults);
+ const eventToReturn = {
+ data: message
+ };
+ this.onWorkerMessage(eventToReturn);
+ }
+
+ /**
+ * A local version of the same SharedWorker function
+ * if we don't have SharedWorkers available (e.g., iOS)
+ */
+ localSearchForNotebookAnnotations(queryId, {entryId, targetKeyString}, maxResults) {
+ // This results dictionary will have domain object ID keys which
+ // point to the value the domain object's score.
+ let results = [];
+ const message = {
+ request: 'searchForNotebookAnnotations',
+ results: [],
+ total: 0,
+ queryId
+ };
+
+ const matchingAnnotations = this.localIndexedAnnotationsByDomainObject[targetKeyString];
+ if (matchingAnnotations) {
+ results = matchingAnnotations.filter(matchingAnnotation => {
+ if (!matchingAnnotation.targets) {
+ return false;
+ }
+
+ const target = matchingAnnotation.targets[targetKeyString];
+
+ return (target && target.entryId && (target.entryId === entryId));
+ });
+ }
message.total = results.length;
message.results = results
diff --git a/src/api/objects/InMemorySearchWorker.js b/src/api/objects/InMemorySearchWorker.js
index b7e7ca8be..a2bb53a02 100644
--- a/src/api/objects/InMemorySearchWorker.js
+++ b/src/api/objects/InMemorySearchWorker.js
@@ -26,16 +26,27 @@
(function () {
// An object composed of domain object IDs and models
// {id: domainObject's ID, name: domainObject's name}
- const indexedItems = {};
+ const indexedDomainObjects = {};
+ const indexedAnnotationsByDomainObject = {};
+ const indexedAnnotationsByTag = {};
self.onconnect = function (e) {
const port = e.ports[0];
port.onmessage = function (event) {
- if (event.data.request === 'index') {
+ const requestType = event.data.request;
+ if (requestType === 'index') {
indexItem(event.data.keyString, event.data.model);
- } else if (event.data.request === 'search') {
- port.postMessage(search(event.data));
+ } else if (requestType === 'OBJECTS') {
+ port.postMessage(searchForObjects(event.data));
+ } else if (requestType === 'ANNOTATIONS') {
+ port.postMessage(searchForAnnotations(event.data));
+ } else if (requestType === 'TAGS') {
+ port.postMessage(searchForTags(event.data));
+ } else if (requestType === 'NOTEBOOK_ANNOTATIONS') {
+ port.postMessage(searchForNotebookAnnotations(event.data));
+ } else {
+ throw new Error(`Unknown request ${event.data.request}`);
}
};
@@ -48,12 +59,70 @@
console.error('Error on feed', error);
};
+ function indexAnnotation(objectToIndex, model) {
+ Object.keys(model.targets).forEach(targetID => {
+ if (!indexedAnnotationsByDomainObject[targetID]) {
+ indexedAnnotationsByDomainObject[targetID] = [];
+ }
+
+ objectToIndex.targets = model.targets;
+ objectToIndex.tags = model.tags;
+ const existsInIndex = indexedAnnotationsByDomainObject[targetID].some(indexedObject => {
+ return indexedObject.keyString === objectToIndex.keyString;
+ });
+
+ if (!existsInIndex) {
+ indexedAnnotationsByDomainObject[targetID].push(objectToIndex);
+ }
+ });
+ }
+
+ function indexTags(keyString, objectToIndex, model) {
+ // add new tags
+ model.tags.forEach(tagID => {
+ if (!indexedAnnotationsByTag[tagID]) {
+ indexedAnnotationsByTag[tagID] = [];
+ }
+
+ const existsInIndex = indexedAnnotationsByTag[tagID].some(indexedObject => {
+ return indexedObject.keyString === objectToIndex.keyString;
+ });
+
+ if (!existsInIndex) {
+ indexedAnnotationsByTag[tagID].push(objectToIndex);
+ }
+
+ });
+ // remove old tags
+ const tagsToRemoveFromIndex = Object.keys(indexedAnnotationsByTag).filter(indexedTag => {
+ return !(model.tags.includes(indexedTag));
+ });
+ tagsToRemoveFromIndex.forEach(tagToRemoveFromIndex => {
+ indexedAnnotationsByTag[tagToRemoveFromIndex] = indexedAnnotationsByTag[tagToRemoveFromIndex].filter(indexedAnnotation => {
+ const shouldKeep = indexedAnnotation.keyString !== keyString;
+
+ return shouldKeep;
+ });
+ });
+ }
+
function indexItem(keyString, model) {
- indexedItems[keyString] = {
+ const objectToIndex = {
type: model.type,
name: model.name,
keyString
};
+ if (model && (model.type === 'annotation')) {
+ if (model.targets) {
+ indexAnnotation(objectToIndex, model);
+ }
+
+ if (model.tags) {
+ indexTags(keyString, objectToIndex, model);
+ }
+ } else {
+ indexedDomainObjects[keyString] = objectToIndex;
+ }
}
/**
@@ -65,21 +134,98 @@
* * maxResults: The maximum number of search results desired
* * queryId: an id identifying this query, will be returned.
*/
- function search(data) {
- // This results dictionary will have domain object ID keys which
- // point to the value the domain object's score.
- let results;
+ function searchForObjects(data) {
+ let results = [];
const input = data.input.trim().toLowerCase();
const message = {
- request: 'search',
- results: {},
+ request: 'searchForObjects',
+ results: [],
total: 0,
queryId: data.queryId
};
- results = Object.values(indexedItems).filter((indexedItem) => {
+ results = Object.values(indexedDomainObjects).filter((indexedItem) => {
return indexedItem.name.toLowerCase().includes(input);
- });
+ }) || [];
+
+ message.total = results.length;
+ message.results = results
+ .slice(0, data.maxResults);
+
+ return message;
+ }
+
+ function searchForAnnotations(data) {
+ let results = [];
+ const message = {
+ request: 'searchForAnnotations',
+ results: [],
+ total: 0,
+ queryId: data.queryId
+ };
+
+ results = indexedAnnotationsByDomainObject[data.input] || [];
+
+ message.total = results.length;
+ message.results = results
+ .slice(0, data.maxResults);
+
+ return message;
+ }
+
+ function searchForTags(data) {
+ let results = [];
+ const message = {
+ request: 'searchForTags',
+ results: [],
+ total: 0,
+ queryId: data.queryId
+ };
+
+ if (data.input) {
+ data.input.forEach(matchingTag => {
+ const matchingAnnotations = indexedAnnotationsByTag[matchingTag];
+ if (matchingAnnotations) {
+ matchingAnnotations.forEach(matchingAnnotation => {
+ const existsInResults = results.some(indexedObject => {
+ return matchingAnnotation.keyString === indexedObject.keyString;
+ });
+ if (!existsInResults) {
+ results.push(matchingAnnotation);
+ }
+ });
+ }
+ });
+ }
+
+ message.total = results.length;
+ message.results = results
+ .slice(0, data.maxResults);
+
+ return message;
+ }
+
+ function searchForNotebookAnnotations(data) {
+ let results = [];
+ const message = {
+ request: 'searchForNotebookAnnotations',
+ results: {},
+ total: 0,
+ queryId: data.queryId
+ };
+
+ const matchingAnnotations = indexedAnnotationsByDomainObject[data.input.targetKeyString];
+ if (matchingAnnotations) {
+ results = matchingAnnotations.filter(matchingAnnotation => {
+ if (!matchingAnnotation.targets) {
+ return false;
+ }
+
+ const target = matchingAnnotation.targets[data.input.targetKeyString];
+
+ return (target && target.entryId && (target.entryId === data.input.entryId));
+ });
+ }
message.total = results.length;
message.results = results
diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js
index 23b27ca40..64167f3c7 100644
--- a/src/api/objects/ObjectAPI.js
+++ b/src/api/objects/ObjectAPI.js
@@ -31,612 +31,653 @@ import ConflictError from './ConflictError';
import InMemorySearchProvider from './InMemorySearchProvider';
/**
- * Utilities for loading, saving, and manipulating domain objects.
- * @interface ObjectAPI
- * @memberof module:openmct
- */
-
-function ObjectAPI(typeRegistry, openmct) {
- this.openmct = openmct;
- this.typeRegistry = typeRegistry;
- this.eventEmitter = new EventEmitter();
- this.providers = {};
- this.rootRegistry = new RootRegistry(openmct);
- this.inMemorySearchProvider = new InMemorySearchProvider(openmct);
-
- this.rootProvider = new RootObjectProvider(this.rootRegistry);
- this.cache = {};
- this.interceptorRegistry = new InterceptorRegistry();
-
- this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan'];
-
- this.errors = {
- Conflict: ConflictError
- };
-}
-
-/**
- * Set fallback provider, this is an internal API for legacy reasons.
- * @private
- */
-ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) {
- this.fallbackProvider = p;
-};
-
-/**
- * Retrieve the provider for a given identifier.
- * @private
- */
-ObjectAPI.prototype.getProvider = function (identifier) {
-
- if (identifier.key === 'ROOT') {
- return this.rootProvider;
- }
-
- return this.providers[identifier.namespace] || this.fallbackProvider;
-};
-
-/**
- * Get an active transaction instance
- * @returns {Transaction} a transaction object
- */
-ObjectAPI.prototype.getActiveTransaction = function () {
- return this.transaction;
-};
-
-/**
- * Get the root-level object.
- * @returns {Promise.<DomainObject>} a promise for the root object
- */
-ObjectAPI.prototype.getRoot = function () {
- return this.rootProvider.get();
-};
-
-/**
- * Register a new object provider for a particular namespace.
+ * Uniquely identifies a domain object.
*
- * @param {string} namespace the namespace for which to provide objects
- * @param {module:openmct.ObjectProvider} provider the provider which
- * will handle loading domain objects from this namespace
- * @memberof {module:openmct.ObjectAPI#}
- * @name addProvider
+ * @typedef Identifier
+ * @property {string} namespace the namespace to/from which this domain
+ * object should be loaded/stored.
+ * @property {string} key a unique identifier for the domain object
+ * within that namespace
+ * @memberof module:openmct.ObjectAPI~
*/
-ObjectAPI.prototype.addProvider = function (namespace, provider) {
- this.providers[namespace] = provider;
-};
/**
- * Provides the ability to read, write, and delete domain objects.
+ * A domain object is an entity of relevance to a user's workflow, that
+ * should appear as a distinct and meaningful object within the user
+ * interface. Examples of domain objects are folders, telemetry sensors,
+ * and so forth.
*
- * When registering a new object provider, all methods on this interface
- * are optional.
+ * A few common properties are defined for domain objects. Beyond these,
+ * individual types of domain objects may add more as they see fit.
*
- * @interface ObjectProvider
+ * @typedef DomainObject
+ * @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which
+ * uniquely identifies this domain object
+ * @property {string} type the type of domain object
+ * @property {string} name the human-readable name for this domain object
+ * @property {string} [creator] the user name of the creator of this domain
+ * object
+ * @property {number} [modified] the time, in milliseconds since the UNIX
+ * epoch, at which this domain object was last modified
+ * @property {module:openmct.ObjectAPI~Identifier[]} [composition] if
+ * present, this will be used by the default composition provider
+ * to load domain objects
* @memberof module:openmct
*/
-
/**
- * Create the given domain object in the corresponding persistence store
- *
- * @method create
- * @memberof module:openmct.ObjectProvider#
- * @param {module:openmct.DomainObject} domainObject the domain object to
- * create
- * @returns {Promise} a promise which will resolve when the domain object
- * has been created, or be rejected if it cannot be saved
+ * Utilities for loading, saving, and manipulating domain objects.
+ * @interface ObjectAPI
+ * @memberof module:openmct
*/
+export default class ObjectAPI {
+ constructor(typeRegistry, openmct) {
+ this.openmct = openmct;
+ this.typeRegistry = typeRegistry;
+ this.SEARCH_TYPES = Object.freeze({
+ OBJECTS: 'OBJECTS',
+ ANNOTATIONS: 'ANNOTATIONS',
+ NOTEBOOK_ANNOTATIONS: 'NOTEBOOK_ANNOTATIONS',
+ TAGS: 'TAGS'
+ });
+ this.eventEmitter = new EventEmitter();
+ this.providers = {};
+ this.rootRegistry = new RootRegistry(openmct);
+ this.inMemorySearchProvider = new InMemorySearchProvider(openmct);
-/**
- * Update this domain object in its persistence store
- *
- * @method update
- * @memberof module:openmct.ObjectProvider#
- * @param {module:openmct.DomainObject} domainObject the domain object to
- * update
- * @returns {Promise} a promise which will resolve when the domain object
- * has been updated, or be rejected if it cannot be saved
- */
+ this.rootProvider = new RootObjectProvider(this.rootRegistry);
+ this.cache = {};
+ this.interceptorRegistry = new InterceptorRegistry();
-/**
- * Delete this domain object.
- *
- * @method delete
- * @memberof module:openmct.ObjectProvider#
- * @param {module:openmct.DomainObject} domainObject the domain object to
- * delete
- * @returns {Promise} a promise which will resolve when the domain object
- * has been deleted, or be rejected if it cannot be deleted
- */
+ this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan', 'annotation'];
-/**
- * Get a domain object.
- *
- * @method get
- * @memberof module:openmct.ObjectProvider#
- * @param {string} key the key for the domain object to load
- * @param {AbortController.signal} abortSignal (optional) signal to abort fetch requests
- * @returns {Promise} a promise which will resolve when the domain object
- * has been saved, or be rejected if it cannot be saved
- */
+ this.errors = {
+ Conflict: ConflictError
+ };
+ }
-ObjectAPI.prototype.get = function (identifier, abortSignal) {
- let keystring = this.makeKeyString(identifier);
+ /**
+ * Retrieve the provider for a given identifier.
+ */
+ getProvider(identifier) {
+ if (identifier.key === 'ROOT') {
+ return this.rootProvider;
+ }
- if (this.cache[keystring] !== undefined) {
- return this.cache[keystring];
+ return this.providers[identifier.namespace] || this.fallbackProvider;
}
- identifier = utils.parseKeyString(identifier);
- let dirtyObject;
- if (this.isTransactionActive()) {
- dirtyObject = this.transaction.getDirtyObject(identifier);
+ /**
+ * Get an active transaction instance
+ * @returns {Transaction} a transaction object
+ */
+ getActiveTransaction() {
+ return this.transaction;
}
- if (dirtyObject) {
- return Promise.resolve(dirtyObject);
+ /**
+ * Get the root-level object.
+ * @returns {Promise.<DomainObject>} a promise for the root object
+ */
+ getRoot() {
+ return this.rootProvider.get();
}
- const provider = this.getProvider(identifier);
-
- if (!provider) {
- throw new Error('No Provider Matched');
+ /**
+ * Register a new object provider for a particular namespace.
+ *
+ * @param {string} namespace the namespace for which to provide objects
+ * @param {module:openmct.ObjectProvider} provider the provider which
+ * will handle loading domain objects from this namespace
+ * @memberof {module:openmct.ObjectAPI#}
+ * @name addProvider
+ */
+ addProvider(namespace, provider) {
+ this.providers[namespace] = provider;
}
- if (!provider.get) {
- throw new Error('Provider does not support get!');
- }
+ /**
+ * Provides the ability to read, write, and delete domain objects.
+ *
+ * When registering a new object provider, all methods on this interface
+ * are optional.
+ *
+ * @interface ObjectProvider
+ * @memberof module:openmct
+ */
+
+ /**
+ * Create the given domain object in the corresponding persistence store
+ *
+ * @method create
+ * @memberof module:openmct.ObjectProvider#
+ * @param {module:openmct.DomainObject} domainObject the domain object to
+ * create
+ * @returns {Promise} a promise which will resolve when the domain object
+ * has been created, or be rejected if it cannot be saved
+ */
+
+ /**
+ * Update this domain object in its persistence store
+ *
+ * @method update
+ * @memberof module:openmct.ObjectProvider#
+ * @param {module:openmct.DomainObject} domainObject the domain object to
+ * update
+ * @returns {Promise} a promise which will resolve when the domain object
+ * has been updated, or be rejected if it cannot be saved
+ */
+
+ /**
+ * Delete this domain object.
+ *
+ * @method delete
+ * @memberof module:openmct.ObjectProvider#
+ * @param {module:openmct.DomainObject} domainObject the domain object to
+ * delete
+ * @returns {Promise} a promise which will resolve when the domain object
+ * has been deleted, or be rejected if it cannot be deleted
+ */
+
+ /**
+ * Get a domain object.
+ *
+ * @method get
+ * @memberof module:openmct.ObjectProvider#
+ * @param {string} key the key for the domain object to load
+ * @param {AbortController.signal} abortSignal (optional) signal to abort fetch requests
+ * @returns {Promise} a promise which will resolve when the domain object
+ * has been saved, or be rejected if it cannot be saved
+ */
+
+ get(identifier, abortSignal) {
+ let keystring = this.makeKeyString(identifier);
+
+ if (this.cache[keystring] !== undefined) {
+ return this.cache[keystring];
+ }
- let objectPromise = provider.get(identifier, abortSignal).then(result => {
- delete this.cache[keystring];
+ identifier = utils.parseKeyString(identifier);
+ let dirtyObject;
+ if (this.isTransactionActive()) {
+ dirtyObject = this.transaction.getDirtyObject(identifier);
+ }
- result = this.applyGetInterceptors(identifier, result);
- if (result.isMutable) {
- result.$refresh(result);
- } else {
- let mutableDomainObject = this._toMutable(result);
- mutableDomainObject.$refresh(result);
+ if (dirtyObject) {
+ return Promise.resolve(dirtyObject);
}
- return result;
- }).catch((result) => {
- console.warn(`Failed to retrieve ${keystring}:`, result);
+ const provider = this.getProvider(identifier);
- delete this.cache[keystring];
+ if (!provider) {
+ throw new Error('No Provider Matched');
+ }
- result = this.applyGetInterceptors(identifier);
+ if (!provider.get) {
+ throw new Error('Provider does not support get!');
+ }
- return result;
- });
+ let objectPromise = provider.get(identifier, abortSignal).then(result => {
+ delete this.cache[keystring];
- this.cache[keystring] = objectPromise;
+ result = this.applyGetInterceptors(identifier, result);
+ if (result.isMutable) {
+ result.$refresh(result);
+ } else {
+ let mutableDomainObject = this._toMutable(result);
+ mutableDomainObject.$refresh(result);
+ }
- return objectPromise;
-};
+ return result;
+ }).catch((result) => {
+ console.warn(`Failed to retrieve ${keystring}:`, result);
-/**
- * Search for domain objects.
- *
- * Object providersSearches and combines results of each object provider search.
- * Objects without search provided will have been indexed
- * and will be searched using the fallback in-memory search.
- * Search results are asynchronous and resolve in parallel.
- *
- * @method search
- * @memberof module:openmct.ObjectAPI#
- * @param {string} query the term to search for
- * @param {AbortController.signal} abortSignal (optional) signal to cancel downstream fetch requests
- * @returns {Array.<Promise.<module:openmct.DomainObject>>}
- * an array of promises returned from each object provider's search function
- * each resolving to domain objects matching provided search query and options.
- */
-ObjectAPI.prototype.search = function (query, abortSignal) {
- const searchPromises = Object.values(this.providers)
- .filter(provider => provider.search !== undefined)
- .map(provider => provider.search(query, abortSignal));
- // abortSignal doesn't seem to be used in generic search?
- searchPromises.push(this.inMemorySearchProvider.query(query, null)
- .then(results => results.hits
- .map(hit => {
- return hit;
- })));
-
- return searchPromises;
-};
+ delete this.cache[keystring];
-/**
- * Will fetch object for the given identifier, returning a version of the object that will automatically keep
- * itself updated as it is mutated. Before using this function, you should ask yourself whether you really need it.
- * The platform will provide mutable objects to views automatically if the underlying object can be mutated. The
- * platform will manage the lifecycle of any mutable objects that it provides. If you use `getMutable` you are
- * committing to managing that lifecycle yourself. `.destroy` should be called when the object is no longer needed.
- *
- * @memberof {module:openmct.ObjectAPI#}
- * @returns {Promise.<MutableDomainObject>} a promise that will resolve with a MutableDomainObject if
- * the object can be mutated.
- */
-ObjectAPI.prototype.getMutable = function (identifier) {
- if (!this.supportsMutation(identifier)) {
- throw new Error(`Object "${this.makeKeyString(identifier)}" does not support mutation.`);
+ result = this.applyGetInterceptors(identifier);
+
+ return result;
+ });
+
+ this.cache[keystring] = objectPromise;
+
+ return objectPromise;
}
- return this.get(identifier).then((object) => {
- return this._toMutable(object);
- });
-};
+ /**
+ * Search for domain objects.
+ *
+ * Object providersSearches and combines results of each object provider search.
+ * Objects without search provided will have been indexed
+ * and will be searched using the fallback in-memory search.
+ * Search results are asynchronous and resolve in parallel.
+ *
+ * @method search
+ * @memberof module:openmct.ObjectAPI#
+ * @param {string} query the term to search for
+ * @param {AbortController.signal} abortSignal (optional) signal to cancel downstream fetch requests
+ * @param {string} searchType the type of search as defined by SEARCH_TYPES
+ * @returns {Array.<Promise.<module:openmct.DomainObject>>}
+ * an array of promises returned from each object provider's search function
+ * each resolving to domain objects matching provided search query and options.
+ */
+ search(query, abortSignal, searchType = this.SEARCH_TYPES.OBJECTS) {
+ if (!Object.keys(this.SEARCH_TYPES).includes(searchType.toUpperCase())) {
+ throw new Error(`Unknown search type: ${searchType}`);
+ }
+
+ const searchPromises = Object.values(this.providers)
+ .filter(provider => {
+ return ((provider.supportsSearchType !== undefined) && provider.supportsSearchType(searchType));
+ })
+ .map(provider => provider.search(query, abortSignal, searchType));
+ if (!this.inMemorySearchProvider.supportsSearchType(searchType)) {
+ throw new Error(`${searchType} not implemented in inMemorySearchProvider`);
+ }
-/**
- * This function is for cleaning up a mutable domain object when you're done with it.
- * You only need to use this if you retrieved the object using `getMutable()`. If the object was provided by the
- * platform (eg. passed into a `view()` function) then the platform is responsible for its lifecycle.
- * @param {MutableDomainObject} domainObject
- */
-ObjectAPI.prototype.destroyMutable = function (domainObject) {
- if (domainObject.isMutable) {
- return domainObject.$destroy();
- } else {
- throw new Error("Attempted to destroy non-mutable domain object");
+ searchPromises.push(this.inMemorySearchProvider.search(query, searchType)
+ .then(results => results.hits
+ .map(hit => {
+ return hit;
+ })));
+
+ return searchPromises;
}
-};
-ObjectAPI.prototype.delete = function () {
- throw new Error('Delete not implemented');
-};
+ /**
+ * Will fetch object for the given identifier, returning a version of the object that will automatically keep
+ * itself updated as it is mutated. Before using this function, you should ask yourself whether you really need it.
+ * The platform will provide mutable objects to views automatically if the underlying object can be mutated. The
+ * platform will manage the lifecycle of any mutable objects that it provides. If you use `getMutable` you are
+ * committing to managing that lifecycle yourself. `.destroy` should be called when the object is no longer needed.
+ *
+ * @memberof {module:openmct.ObjectAPI#}
+ * @returns {Promise.<MutableDomainObject>} a promise that will resolve with a MutableDomainObject if
+ * the object can be mutated.
+ */
+ getMutable(identifier) {
+ if (!this.supportsMutation(identifier)) {
+ throw new Error(`Object "${this.makeKeyString(identifier)}" does not support mutation.`);
+ }
-ObjectAPI.prototype.isPersistable = function (idOrKeyString) {
- let identifier = utils.parseKeyString(idOrKeyString);
- let provider = this.getProvider(identifier);
+ return this.get(identifier).then((object) => {
+ return this._toMutable(object);
+ });
+ }
- return provider !== undefined
- && provider.create !== undefined
- && provider.update !== undefined;
-};
+ /**
+ * This function is for cleaning up a mutable domain object when you're done with it.
+ * You only need to use this if you retrieved the object using `getMutable()`. If the object was provided by the
+ * platform (eg. passed into a `view()` function) then the platform is responsible for its lifecycle.
+ * @param {MutableDomainObject} domainObject
+ */
+ destroyMutable(domainObject) {
+ if (domainObject.isMutable) {
+ return domainObject.$destroy();
+ } else {
+ throw new Error("Attempted to destroy non-mutable domain object");
+ }
+ }
-ObjectAPI.prototype.isMissing = function (domainObject) {
- let identifier = utils.makeKeyString(domainObject.identifier);
- let missingName = 'Missing: ' + identifier;
+ delete() {
+ throw new Error('Delete not implemented');
+ }
- return domainObject.name === missingName;
-};
+ isPersistable(idOrKeyString) {
+ let identifier = utils.parseKeyString(idOrKeyString);
+ let provider = this.getProvider(identifier);
-/**
- * Save this domain object in its current state. EXPERIMENTAL
- *
- * @private
- * @memberof module:openmct.ObjectAPI#
- * @param {module:openmct.DomainObject} domainObject the domain object to
- * save
- * @returns {Promise} a promise which will resolve when the domain object
- * has been saved, or be rejected if it cannot be saved
- */
-ObjectAPI.prototype.save = function (domainObject) {
- let provider = this.getProvider(domainObject.identifier);
- let savedResolve;
- let savedReject;
- let result;
-
- if (!this.isPersistable(domainObject.identifier)) {
- result = Promise.reject('Object provider does not support saving');
- } else if (hasAlreadyBeenPersisted(domainObject)) {
- result = Promise.resolve(true);
- } else {
- const persistedTime = Date.now();
- if (domainObject.persisted === undefined) {
- result = new Promise((resolve, reject) => {
- savedResolve = resolve;
- savedReject = reject;
- });
- domainObject.persisted = persistedTime;
- const newObjectPromise = provider.create(domainObject);
- if (newObjectPromise) {
- newObjectPromise.then(response => {
- this.mutate(domainObject, 'persisted', persistedTime);
- savedResolve(response);
- }).catch((error) => {
- savedReject(error);
+ return provider !== undefined
+ && provider.create !== undefined
+ && provider.update !== undefined;
+ }
+
+ isMissing(domainObject) {
+ let identifier = utils.makeKeyString(domainObject.identifier);
+ let missingName = 'Missing: ' + identifier;
+
+ return domainObject.name === missingName;
+ }
+
+ /**
+ * Save this domain object in its current state.
+ *
+ * @memberof module:openmct.ObjectAPI#
+ * @param {module:openmct.DomainObject} domainObject the domain object to
+ * save
+ * @returns {Promise} a promise which will resolve when the domain object
+ * has been saved, or be rejected if it cannot be saved
+ */
+ save(domainObject) {
+ let provider = this.getProvider(domainObject.identifier);
+ let savedResolve;
+ let savedReject;
+ let result;
+
+ if (!this.isPersistable(domainObject.identifier)) {
+ result = Promise.reject('Object provider does not support saving');
+ } else if (this.#hasAlreadyBeenPersisted(domainObject)) {
+ result = Promise.resolve(true);
+ } else {
+ const persistedTime = Date.now();
+ if (domainObject.persisted === undefined) {
+ result = new Promise((resolve, reject) => {
+ savedResolve = resolve;
+ savedReject = reject;
});
+ domainObject.persisted = persistedTime;
+ const newObjectPromise = provider.create(domainObject);
+ if (newObjectPromise) {
+ newObjectPromise.then(response => {
+ this.mutate(domainObject, 'persisted', persistedTime);
+ savedResolve(response);
+ }).catch((error) => {
+ savedReject(error);
+ });
+ } else {
+ result = Promise.reject(`[ObjectAPI][save] Object provider returned ${newObjectPromise} when creating new object.`);
+ }
} else {
- result = Promise.reject(`[ObjectAPI][save] Object provider returned ${newObjectPromise} when creating new object.`);
+ domainObject.persisted = persistedTime;
+ this.mutate(domainObject, 'persisted', persistedTime);
+ result = provider.update(domainObject);
}
- } else {
- domainObject.persisted = persistedTime;
- this.mutate(domainObject, 'persisted', persistedTime);
- result = provider.update(domainObject);
}
- }
- return result;
-};
+ return result.catch((error) => {
+ if (error instanceof this.errors.Conflict) {
+ this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`);
+ }
-/**
- * After entering into edit mode, creates a new instance of Transaction to keep track of changes in Objects
- */
-ObjectAPI.prototype.startTransaction = function () {
- if (this.isTransactionActive()) {
- throw new Error("Unable to start new Transaction: Previous Transaction is active");
+ throw error;
+ });
}
- this.transaction = new Transaction(this);
-};
+ /**
+ * After entering into edit mode, creates a new instance of Transaction to keep track of changes in Objects
+ */
+ startTransaction() {
+ if (this.isTransactionActive()) {
+ throw new Error("Unable to start new Transaction: Previous Transaction is active");
+ }
-/**
- * Clear instance of Transaction
- */
-ObjectAPI.prototype.endTransaction = function () {
- this.transaction = null;
-};
+ this.transaction = new Transaction(this);
+ }
-/**
- * Add a root-level object.
- * @param {module:openmct.ObjectAPI~Identifier|array|function} identifier an identifier or
- * an array of identifiers for root level objects, or a function that returns a
- * promise for an identifier or an array of root level objects.
- * @param {module:openmct.PriorityAPI~priority|Number} priority a number representing
- * this item(s) position in the root object's composition (example: order in object tree).
- * For arrays, they are treated as blocks.
- * @method addRoot
- * @memberof module:openmct.ObjectAPI#
- */
-ObjectAPI.prototype.addRoot = function (identifier, priority) {
- this.rootRegistry.addRoot(identifier, priority);
-};
+ /**
+ * Clear instance of Transaction
+ */
+ endTransaction() {
+ this.transaction = null;
+ }
-/**
- * Register an object interceptor that transforms a domain object requested via module:openmct.ObjectAPI.get
- * The domain object will be transformed after it is retrieved from the persistence store
- * The domain object will be transformed only if the interceptor is applicable to that domain object as defined by the InterceptorDef
- *
- * @param {module:openmct.InterceptorDef} interceptorDef the interceptor definition to add
- * @method addGetInterceptor
- * @memberof module:openmct.InterceptorRegistry#
- */
-ObjectAPI.prototype.addGetInterceptor = function (interceptorDef) {
- this.interceptorRegistry.addInterceptor(interceptorDef);
-};
+ /**
+ * Add a root-level object.
+ * @param {module:openmct.ObjectAPI~Identifier|array|function} identifier an identifier or
+ * an array of identifiers for root level objects, or a function that returns a
+ * promise for an identifier or an array of root level objects.
+ * @param {module:openmct.PriorityAPI~priority|Number} priority a number representing
+ * this item(s) position in the root object's composition (example: order in object tree).
+ * For arrays, they are treated as blocks.
+ * @method addRoot
+ * @memberof module:openmct.ObjectAPI#
+ */
+ addRoot(identifier, priority) {
+ this.rootRegistry.addRoot(identifier, priority);
+ }
-/**
- * Retrieve the interceptors for a given domain object.
- * @private
- */
-ObjectAPI.prototype.listGetInterceptors = function (identifier, object) {
- return this.interceptorRegistry.getInterceptors(identifier, object);
-};
+ /**
+ * Register an object interceptor that transforms a domain object requested via module:openmct.ObjectAPI.get
+ * The domain object will be transformed after it is retrieved from the persistence store
+ * The domain object will be transformed only if the interceptor is applicable to that domain object as defined by the InterceptorDef
+ *
+ * @param {module:openmct.InterceptorDef} interceptorDef the interceptor definition to add
+ * @method addGetInterceptor
+ * @memberof module:openmct.InterceptorRegistry#
+ */
+ addGetInterceptor(interceptorDef) {
+ this.interceptorRegistry.addInterceptor(interceptorDef);
+ }
-/**
- * Inovke interceptors if applicable for a given domain object.
- * @private
- */
-ObjectAPI.prototype.applyGetInterceptors = function (identifier, domainObject) {
- const interceptors = this.listGetInterceptors(identifier, domainObject);
- interceptors.forEach(interceptor => {
- domainObject = interceptor.invoke(identifier, domainObject);
- });
+ /**
+ * Retrieve the interceptors for a given domain object.
+ * @private
+ */
+ #listGetInterceptors(identifier, object) {
+ return this.interceptorRegistry.getInterceptors(identifier, object);
+ }
- return domainObject;
-};
+ /**
+ * Inovke interceptors if applicable for a given domain object.
+ * @private
+ */
+ applyGetInterceptors(identifier, domainObject) {
+ const interceptors = this.#listGetInterceptors(identifier, domainObject);
+ interceptors.forEach(interceptor => {
+ domainObject = interceptor.invoke(identifier, domainObject);
+ });
-/**
- * Return relative url path from a given object path
- * eg: #/browse/mine/cb56f6bf-c900-43b7-b923-2e3b64b412db/6e89e858-77ce-46e4-a1ad-749240286497/....
- * @param {Array} objectPath
- * @returns {string} relative url for object
- */
-ObjectAPI.prototype.getRelativePath = function (objectPath) {
- return objectPath
- .map(p => this.makeKeyString(p.identifier))
- .reverse()
- .join('/')
- ;
-};
+ return domainObject;
+ }
-/**
- * Modify a domain object.
- * @param {module:openmct.DomainObject} object the object to mutate
- * @param {string} path the property to modify
- * @param {*} value the new value for this property
- * @method mutate
- * @memberof module:openmct.ObjectAPI#
- */
-ObjectAPI.prototype.mutate = function (domainObject, path, value) {
- if (!this.supportsMutation(domainObject.identifier)) {
- throw `Error: Attempted to mutate immutable object ${domainObject.name}`;
+ /**
+ * Return relative url path from a given object path
+ * eg: #/browse/mine/cb56f6bf-c900-43b7-b923-2e3b64b412db/6e89e858-77ce-46e4-a1ad-749240286497/....
+ * @param {Array} objectPath
+ * @returns {string} relative url for object
+ */
+ getRelativePath(objectPath) {
+ return objectPath
+ .map(p => this.makeKeyString(p.identifier))
+ .reverse()
+ .join('/');
}
- if (domainObject.isMutable) {
- domainObject.$set(path, value);
- } else {
- //Creating a temporary mutable domain object allows other mutable instances of the
- //object to be kept in sync.
- let mutableDomainObject = this._toMutable(domainObject);
+ /**
+ * Modify a domain object.
+ * @param {module:openmct.DomainObject} object the object to mutate
+ * @param {string} path the property to modify
+ * @param {*} value the new value for this property
+ * @method mutate
+ * @memberof module:openmct.ObjectAPI#
+ */
+ mutate(domainObject, path, value) {
+ if (!this.supportsMutation(domainObject.identifier)) {
+ throw `Error: Attempted to mutate immutable object ${domainObject.name}`;
+ }
+
+ if (domainObject.isMutable) {
+ domainObject.$set(path, value);
+ } else {
+ //Creating a temporary mutable domain object allows other mutable instances of the
+ //object to be kept in sync.
+ let mutableDomainObject = this._toMutable(domainObject);
- //Mutate original object
- MutableDomainObject.mutateObject(domainObject, path, value);
+ //Mutate original object
+ MutableDomainObject.mutateObject(domainObject, path, value);
- //Mutate temporary mutable object, in the process informing any other mutable instances
- mutableDomainObject.$set(path, value);
+ //Mutate temporary mutable object, in the process informing any other mutable instances
+ mutableDomainObject.$set(path, value);
- //Destroy temporary mutable object
- this.destroyMutable(mutableDomainObject);
- }
+ //Destroy temporary mutable object
+ this.destroyMutable(mutableDomainObject);
+ }
- if (this.isTransactionActive()) {
- this.transaction.add(domainObject);
- } else {
- this.save(domainObject);
+ if (this.isTransactionActive()) {
+ this.transaction.add(domainObject);
+ } else {
+ this.save(domainObject);
+ }
}
-};
-/**
- * @private
- */
-ObjectAPI.prototype._toMutable = function (object) {
- let mutableObject;
+ /**
+ * @private
+ */
+ _toMutable(object) {
+ let mutableObject;
- if (object.isMutable) {
- mutableObject = object;
- } else {
- mutableObject = MutableDomainObject.createMutable(object, this.eventEmitter);
+ if (object.isMutable) {
+ mutableObject = object;
+ } else {
+ mutableObject = MutableDomainObject.createMutable(object, this.eventEmitter);
+
+ // Check if provider supports realtime updates
+ let identifier = utils.parseKeyString(mutableObject.identifier);
+ let provider = this.getProvider(identifier);
+
+ if (provider !== undefined
+ && provider.observe !== undefined
+ && this.SYNCHRONIZED_OBJECT_TYPES.includes(object.type)) {
+ let unobserve = provider.observe(identifier, (updatedModel) => {
+ if (updatedModel.persisted > mutableObject.modified) {
+ //Don't replace with a stale model. This can happen on slow connections when multiple mutations happen
+ //in rapid succession and intermediate persistence states are returned by the observe function.
+ updatedModel = this.applyGetInterceptors(identifier, updatedModel);
+ mutableObject.$refresh(updatedModel);
+ }
+ });
+ mutableObject.$on('$_destroy', () => {
+ unobserve();
+ });
+ }
+ }
- // Check if provider supports realtime updates
- let identifier = utils.parseKeyString(mutableObject.identifier);
- let provider = this.getProvider(identifier);
+ return mutableObject;
+ }
- if (provider !== undefined
- && provider.observe !== undefined
- && this.SYNCHRONIZED_OBJECT_TYPES.includes(object.type)) {
- let unobserve = provider.observe(identifier, (updatedModel) => {
- if (updatedModel.persisted > mutableObject.modified) {
- //Don't replace with a stale model. This can happen on slow connections when multiple mutations happen
- //in rapid succession and intermediate persistence states are returned by the observe function.
- updatedModel = this.applyGetInterceptors(identifier, updatedModel);
- mutableObject.$refresh(updatedModel);
- }
- });
- mutableObject.$on('$_destroy', () => {
- unobserve();
- });
+ /**
+ * Updates a domain object based on its latest persisted state. Note that this will mutate the provided object.
+ * @param {module:openmct.DomainObject} domainObject an object to refresh from its persistence store
+ * @returns {Promise} the provided object, updated to reflect the latest persisted state of the object.
+ */
+ async refresh(domainObject) {
+ const refreshedObject = await this.get(domainObject.identifier);
+
+ if (domainObject.isMutable) {
+ domainObject.$refresh(refreshedObject);
+ } else {
+ utils.refresh(domainObject, refreshedObject);
}
+
+ return domainObject;
}
- return mutableObject;
-};
+ /**
+ * @param module:openmct.ObjectAPI~Identifier identifier An object identifier
+ * @returns {boolean} true if the object can be mutated, otherwise returns false
+ */
+ supportsMutation(identifier) {
+ return this.isPersistable(identifier);
+ }
-/**
- * Updates a domain object based on its latest persisted state. Note that this will mutate the provided object.
- * @param {module:openmct.DomainObject} domainObject an object to refresh from its persistence store
- * @returns {Promise} the provided object, updated to reflect the latest persisted state of the object.
- */
-ObjectAPI.prototype.refresh = async function (domainObject) {
- const refreshedObject = await this.get(domainObject.identifier);
+ /**
+ * Observe changes to a domain object.
+ * @param {module:openmct.DomainObject} object the object to observe
+ * @param {string} path the property to observe
+ * @param {Function} callback a callback to invoke when new values for
+ * this property are observed
+ * @method observe
+ * @memberof module:openmct.ObjectAPI#
+ */
+ observe(domainObject, path, callback) {
+ if (domainObject.isMutable) {
+ return domainObject.$observe(path, callback);
+ } else {
+ let mutable = this._toMutable(domainObject);
+ mutable.$observe(path, callback);
- if (domainObject.isMutable) {
- domainObject.$refresh(refreshedObject);
- } else {
- utils.refresh(domainObject, refreshedObject);
+ return () => mutable.$destroy();
+ }
}
- return domainObject;
-};
+ /**
+ * @param {module:openmct.ObjectAPI~Identifier} identifier
+ * @returns {string} A string representation of the given identifier, including namespace and key
+ */
+ makeKeyString(identifier) {
+ return utils.makeKeyString(identifier);
+ }
-/**
- * @param module:openmct.ObjectAPI~Identifier identifier An object identifier
- * @returns {boolean} true if the object can be mutated, otherwise returns false
- */
-ObjectAPI.prototype.supportsMutation = function (identifier) {
- return this.isPersistable(identifier);
-};
+ /**
+ * @param {string} keyString A string representation of the given identifier, that is, a namespace and key separated by a colon.
+ * @returns {module:openmct.ObjectAPI~Identifier} An identifier object
+ */
+ parseKeyString(keyString) {
+ return utils.parseKeyString(keyString);
+ }
-/**
- * Observe changes to a domain object.
- * @param {module:openmct.DomainObject} object the object to observe
- * @param {string} path the property to observe
- * @param {Function} callback a callback to invoke when new values for
- * this property are observed
- * @method observe
- * @memberof module:openmct.ObjectAPI#
- */
-ObjectAPI.prototype.observe = function (domainObject, path, callback) {
- if (domainObject.isMutable) {
- return domainObject.$observe(path, callback);
- } else {
- let mutable = this._toMutable(domainObject);
- mutable.$observe(path, callback);
+ /**
+ * Given any number of identifiers, will return true if they are all equal, otherwise false.
+ * @param {module:openmct.ObjectAPI~Identifier[]} identifiers
+ */
+ areIdsEqual(...identifiers) {
+ const firstIdentifier = utils.parseKeyString(identifiers[0]);
+
+ return identifiers.map(utils.parseKeyString)
+ .every(identifier => {
+ return identifier === firstIdentifier
+ || (identifier.namespace === firstIdentifier.namespace
+ && identifier.key === firstIdentifier.key);
+ });
+ }
+
+ /**
+ * Given an original path check if the path is reachable via root
+ * @param {Array<Object>} originalPath an array of path objects to check
+ * @returns {boolean} whether the domain object is reachable
+ */
+ isReachable(originalPath) {
+ if (originalPath && originalPath.length) {
+ return (originalPath[originalPath.length - 1].type === 'root');
+ }
- return () => mutable.$destroy();
+ return false;
}
-};
-/**
- * @param {module:openmct.ObjectAPI~Identifier} identifier
- * @returns {string} A string representation of the given identifier, including namespace and key
- */
-ObjectAPI.prototype.makeKeyString = function (identifier) {
- return utils.makeKeyString(identifier);
-};
+ #pathContainsDomainObject(keyStringToCheck, path) {
+ if (!keyStringToCheck) {
+ return false;
+ }
-/**
- * @param {string} keyString A string representation of the given identifier, that is, a namespace and key separated by a colon.
- * @returns {module:openmct.ObjectAPI~Identifier} An identifier object
- */
-ObjectAPI.prototype.parseKeyString = function (keyString) {
- return utils.parseKeyString(keyString);
-};
+ return path.some(pathElement => {
+ const identifierToCheck = utils.parseKeyString(keyStringToCheck);
-/**
- * Given any number of identifiers, will return true if they are all equal, otherwise false.
- * @param {module:openmct.ObjectAPI~Identifier[]} identifiers
- */
-ObjectAPI.prototype.areIdsEqual = function (...identifiers) {
- return identifiers.map(utils.parseKeyString)
- .every(identifier => {
- return identifier === identifiers[0]
- || (identifier.namespace === identifiers[0].namespace
- && identifier.key === identifiers[0].key);
+ return this.areIdsEqual(identifierToCheck, pathElement.identifier);
});
-};
+ }
-ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) {
- return this.get(identifier).then((domainObject) => {
+ /**
+ * Given an identifier, constructs the original path by walking up its parents
+ * @param {module:openmct.ObjectAPI~Identifier} identifier
+ * @param {Array<module:openmct.DomainObject>} path an array of path objects
+ * @returns {Promise<Array<module:openmct.DomainObject>>} a promise containing an array of domain objects
+ */
+ async getOriginalPath(identifier, path = []) {
+ const domainObject = await this.get(identifier);
path.push(domainObject);
- let location = domainObject.location;
-
- if (location) {
+ const { location } = domainObject;
+ if (location && (!this.#pathContainsDomainObject(location, path))) {
+ // if we have a location, and we don't already have this in our constructed path,
+ // then keep walking up the path
return this.getOriginalPath(utils.parseKeyString(location), path);
} else {
return path;
}
- });
-};
-
-ObjectAPI.prototype.isObjectPathToALink = function (domainObject, objectPath) {
- return objectPath !== undefined
- && objectPath.length > 1
- && domainObject.location !== this.makeKeyString(objectPath[1].identifier);
-};
-
-ObjectAPI.prototype.isTransactionActive = function () {
- return Boolean(this.transaction && this.openmct.editor.isEditing());
-};
+ }
-/**
- * Uniquely identifies a domain object.
- *
- * @typedef Identifier
- * @memberof module:openmct.ObjectAPI~
- * @property {string} namespace the namespace to/from which this domain
- * object should be loaded/stored.
- * @property {string} key a unique identifier for the domain object
- * within that namespace
- */
+ isObjectPathToALink(domainObject, objectPath) {
+ return objectPath !== undefined
+ && objectPath.length > 1
+ && domainObject.location !== this.makeKeyString(objectPath[1].identifier);
+ }
-/**
- * A domain object is an entity of relevance to a user's workflow, that
- * should appear as a distinct and meaningful object within the user
- * interface. Examples of domain objects are folders, telemetry sensors,
- * and so forth.
- *
- * A few common properties are defined for domain objects. Beyond these,
- * individual types of domain objects may add more as they see fit.
- *
- * @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which
- * uniquely identifies this domain object
- * @property {string} type the type of domain object
- * @property {string} name the human-readable name for this domain object
- * @property {string} [creator] the user name of the creator of this domain
- * object
- * @property {number} [modified] the time, in milliseconds since the UNIX
- * epoch, at which this domain object was last modified
- * @property {module:openmct.ObjectAPI~Identifier[]} [composition] if
- * present, this will be used by the default composition provider
- * to load domain objects
- * @typedef DomainObject
- * @memberof module:openmct
- */
+ isTransactionActive() {
+ return Boolean(this.transaction && this.openmct.editor.isEditing());
+ }
-function hasAlreadyBeenPersisted(domainObject) {
- const result = domainObject.persisted !== undefined
- && domainObject.persisted >= domainObject.modified;
+ #hasAlreadyBeenPersisted(domainObject) {
+ const result = domainObject.persisted !== undefined
+ && domainObject.persisted >= domainObject.modified;
- return result;
+ return result;
+ }
}
-
-export default ObjectAPI;
diff --git a/src/api/objects/ObjectAPISearchSpec.js b/src/api/objects/ObjectAPISearchSpec.js
index 73dfd84da..1b25c63ae 100644
--- a/src/api/objects/ObjectAPISearchSpec.js
+++ b/src/api/objects/ObjectAPISearchSpec.js
@@ -17,13 +17,16 @@ describe("The Object API Search Function", () => {
openmct = createOpenMct();
mockObjectProvider = jasmine.createSpyObj("mock object provider", [
- "search"
+ "search", "supportsSearchType"
]);
anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [
- "search"
+ "search", "supportsSearchType"
]);
openmct.objects.addProvider('objects', mockObjectProvider);
openmct.objects.addProvider('other-objects', anotherMockObjectProvider);
+ mockObjectProvider.supportsSearchType.and.callFake(() => {
+ return true;
+ });
mockObjectProvider.search.and.callFake(() => {
return new Promise(resolve => {
const mockProviderSearch = {
@@ -38,6 +41,9 @@ describe("The Object API Search Function", () => {
}, MOCK_PROVIDER_SEARCH_DELAY);
});
});
+ anotherMockObjectProvider.supportsSearchType.and.callFake(() => {
+ return true;
+ });
anotherMockObjectProvider.search.and.callFake(() => {
return new Promise(resolve => {
const anotherMockProviderSearch = {
@@ -110,8 +116,8 @@ describe("The Object API Search Function", () => {
namespace: ''
});
openmct.objects.addProvider('foo', defaultObjectProvider);
- spyOn(openmct.objects.inMemorySearchProvider, "query").and.callThrough();
- spyOn(openmct.objects.inMemorySearchProvider, "localSearch").and.callThrough();
+ spyOn(openmct.objects.inMemorySearchProvider, "search").and.callThrough();
+ spyOn(openmct.objects.inMemorySearchProvider, "localSearchForObjects").and.callThrough();
openmct.on('start', async () => {
mockIdentifier1 = {
@@ -155,7 +161,7 @@ describe("The Object API Search Function", () => {
it("can provide indexing without a provider", () => {
openmct.objects.search('foo');
- expect(openmct.objects.inMemorySearchProvider.query).toHaveBeenCalled();
+ expect(openmct.objects.inMemorySearchProvider.search).toHaveBeenCalled();
});
it("can do partial search", async () => {
@@ -177,16 +183,22 @@ describe("The Object API Search Function", () => {
});
describe("Without Shared Workers", () => {
+ let sharedWorkerToRestore;
beforeEach(async () => {
+ // use local worker
+ sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
openmct.objects.inMemorySearchProvider.worker = null;
// reindex locally
await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
});
+ afterEach(() => {
+ openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
+ });
it("calls local search", () => {
openmct.objects.search('foo');
- expect(openmct.objects.inMemorySearchProvider.localSearch).toHaveBeenCalled();
+ expect(openmct.objects.inMemorySearchProvider.localSearchForObjects).toHaveBeenCalled();
});
it("can do partial search", async () => {
diff --git a/src/api/objects/ObjectAPISpec.js b/src/api/objects/ObjectAPISpec.js
index 8887a04c7..e473fc572 100644
--- a/src/api/objects/ObjectAPISpec.js
+++ b/src/api/objects/ObjectAPISpec.js
@@ -7,6 +7,7 @@ describe("The Object API", () => {
let openmct = {};
let mockDomainObject;
const TEST_NAMESPACE = "test-namespace";
+ const TEST_KEY = "test-key";
const FIFTEEN_MINUTES = 15 * 60 * 1000;
beforeEach((done) => {
@@ -22,7 +23,7 @@ describe("The Object API", () => {
mockDomainObject = {
identifier: {
namespace: TEST_NAMESPACE,
- key: "test-key"
+ key: TEST_KEY
},
name: "test object",
type: "test-type"
@@ -84,6 +85,31 @@ describe("The Object API", () => {
expect(mockProvider.create).not.toHaveBeenCalled();
expect(mockProvider.update).not.toHaveBeenCalled();
});
+
+ describe("Shows a notification on persistence conflict", () => {
+ beforeEach(() => {
+ openmct.notifications.error = jasmine.createSpy('error');
+ });
+
+ it("on create", () => {
+ mockProvider.create.and.returnValue(Promise.reject(new openmct.objects.errors.Conflict("Test Conflict error")));
+
+ return objectAPI.save(mockDomainObject).catch(() => {
+ expect(openmct.notifications.error).toHaveBeenCalledWith(`Conflict detected while saving ${TEST_NAMESPACE}:${TEST_KEY}`);
+ });
+
+ });
+
+ it("on update", () => {
+ mockProvider.update.and.returnValue(Promise.reject(new openmct.objects.errors.Conflict("Test Conflict error")));
+ mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;
+ mockDomainObject.modified = Date.now();
+
+ return objectAPI.save(mockDomainObject).catch(() => {
+ expect(openmct.notifications.error).toHaveBeenCalledWith(`Conflict detected while saving ${TEST_NAMESPACE}:${TEST_KEY}`);
+ });
+ });
+ });
});
});
@@ -138,21 +164,33 @@ describe("The Object API", () => {
});
it("Caches multiple requests for the same object", () => {
+ const promises = [];
expect(mockProvider.get.calls.count()).toBe(0);
- objectAPI.get(mockDomainObject.identifier);
+ promises.push(objectAPI.get(mockDomainObject.identifier));
expect(mockProvider.get.calls.count()).toBe(1);
- objectAPI.get(mockDomainObject.identifier);
+ promises.push(objectAPI.get(mockDomainObject.identifier));
expect(mockProvider.get.calls.count()).toBe(1);
+
+ return Promise.all(promises);
});
it("applies any applicable interceptors", () => {
expect(mockDomainObject.changed).toBeUndefined();
- objectAPI.get(mockDomainObject.identifier).then((object) => {
+
+ return objectAPI.get(mockDomainObject.identifier).then((object) => {
expect(object.changed).toBeTrue();
expect(object.alsoChanged).toBeTrue();
expect(object.shouldNotBeChanged).toBeUndefined();
});
});
+
+ it("displays a notification in the event of an error", () => {
+ mockProvider.get.and.returnValue(Promise.reject());
+
+ return objectAPI.get(mockDomainObject.identifier).catch(() => {
+ expect(openmct.notifications.error).toHaveBeenCalledWith(`Failed to retrieve object ${TEST_NAMESPACE}:${TEST_KEY}`);
+ });
+ });
});
});
@@ -168,7 +206,7 @@ describe("The Object API", () => {
testObject = {
identifier: {
namespace: TEST_NAMESPACE,
- key: 'test-key'
+ key: TEST_KEY
},
name: 'test object',
type: 'notebook',
@@ -195,6 +233,8 @@ describe("The Object API", () => {
"observeObjectChanges"
]);
mockProvider.get.and.returnValue(Promise.resolve(testObject));
+ mockProvider.create.and.returnValue(Promise.resolve(true));
+ mockProvider.update.and.returnValue(Promise.resolve(true));
mockProvider.observeObjectChanges.and.callFake(() => {
callbacks[0](updatedTestObject);
callbacks.splice(0, 1);
@@ -337,6 +377,73 @@ describe("The Object API", () => {
});
});
+ describe("getOriginalPath", () => {
+ let mockGrandParentObject;
+ let mockParentObject;
+ let mockChildObject;
+
+ beforeEach(() => {
+ const mockObjectProvider = jasmine.createSpyObj("mock object provider", [
+ "create",
+ "update",
+ "get"
+ ]);
+
+ mockGrandParentObject = {
+ type: 'folder',
+ name: 'Grand Parent Folder',
+ location: 'fooNameSpace:child',
+ identifier: {
+ key: 'grandParent',
+ namespace: 'fooNameSpace'
+ }
+ };
+ mockParentObject = {
+ type: 'folder',
+ name: 'Parent Folder',
+ location: 'fooNameSpace:grandParent',
+ identifier: {
+ key: 'parent',
+ namespace: 'fooNameSpace'
+ }
+ };
+ mockChildObject = {
+ type: 'folder',
+ name: 'Child Folder',
+ location: 'fooNameSpace:parent',
+ identifier: {
+ key: 'child',
+ namespace: 'fooNameSpace'
+ }
+ };
+
+ // eslint-disable-next-line require-await
+ mockObjectProvider.get = async (identifier) => {
+ if (identifier.key === mockGrandParentObject.identifier.key) {
+ return mockGrandParentObject;
+ } else if (identifier.key === mockParentObject.identifier.key) {
+ return mockParentObject;
+ } else if (identifier.key === mockChildObject.identifier.key) {
+ return mockChildObject;
+ } else {
+ return null;
+ }
+ };
+
+ openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
+
+ mockObjectProvider.create.and.returnValue(Promise.resolve(true));
+ mockObjectProvider.update.and.returnValue(Promise.resolve(true));
+
+ openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
+ });
+
+ it('can construct paths even with cycles', async () => {
+ const objectPath = await objectAPI.getOriginalPath(mockChildObject.identifier);
+ expect(objectPath.length).toEqual(3);
+ });
+ });
+
describe("transactions", () => {
beforeEach(() => {
spyOn(openmct.editor, 'isEditing').and.returnValue(true);
diff --git a/src/api/objects/object-utils.js b/src/api/objects/object-utils.js
index 6b4a78a77..abcb59c41 100644
--- a/src/api/objects/object-utils.js
+++ b/src/api/objects/object-utils.js
@@ -91,6 +91,10 @@ define([
* @returns keyString
*/
function makeKeyString(identifier) {
+ if (!identifier) {
+ throw new Error("Cannot make key string from null identifier");
+ }
+
if (isKeyString(identifier)) {
return identifier;
}
diff --git a/src/api/overlays/Overlay.js b/src/api/overlays/Overlay.js
index efc4ae22e..c63285a2e 100644
--- a/src/api/overlays/Overlay.js
+++ b/src/api/overlays/Overlay.js
@@ -6,7 +6,8 @@ const cssClasses = {
large: 'l-overlay-large',
small: 'l-overlay-small',
fit: 'l-overlay-fit',
- fullscreen: 'l-overlay-fullscreen'
+ fullscreen: 'l-overlay-fullscreen',
+ dialog: 'l-overlay-dialog'
};
class Overlay extends EventEmitter {
diff --git a/src/api/overlays/components/OverlayComponent.vue b/src/api/overlays/components/OverlayComponent.vue
index f8a66f759..9742fd736 100644
--- a/src/api/overlays/components/OverlayComponent.vue
+++ b/src/api/overlays/components/OverlayComponent.vue
@@ -7,6 +7,7 @@
<div class="c-overlay__outer">
<button
v-if="dismissable"
+ aria-label="Close"
class="c-click-icon c-overlay__close-button icon-x"
@click="destroy"
></button>
diff --git a/src/api/overlays/components/overlay-component.scss b/src/api/overlays/components/overlay-component.scss
index 419d22048..67838d97d 100644
--- a/src/api/overlays/components/overlay-component.scss
+++ b/src/api/overlays/components/overlay-component.scss
@@ -1,4 +1,4 @@
-@mixin overlaySizing($marginTB: 5%, $marginLR: $marginTB, $width: auto, $height: auto) {
+@mixin overlaySizing($marginTB: auto, $marginLR: auto, $width: auto, $height: auto) {
position: absolute;
top: $marginTB; right: $marginLR; bottom: $marginTB; left: $marginLR;
width: $width;
@@ -98,6 +98,7 @@ body.desktop {
// Overlay types, styling for desktop. Appended to .l-overlay-wrapper element.
.l-overlay-large,
.l-overlay-small,
+ .l-overlay-dialog,
.l-overlay-fit {
.c-overlay__outer {
border-radius: $overlayCr;
@@ -108,7 +109,7 @@ body.desktop {
.l-overlay-fullscreen {
// Used by About > Licenses display
.c-overlay__outer {
- @include overlaySizing($overlayOuterMarginFullscreen);
+ @include overlaySizing(nth($overlayOuterMarginFullscreen, 1), nth($overlayOuterMarginFullscreen, 2));
}
}
@@ -119,7 +120,7 @@ body.desktop {
$lrPad: $pad;
.c-overlay {
&__outer {
- @include overlaySizing($overlayOuterMarginLarge);
+ @include overlaySizing(nth($overlayOuterMarginLarge, 1), nth($overlayOuterMarginLarge, 2));
padding: $tbPad $lrPad;
}
@@ -137,14 +138,20 @@ body.desktop {
.l-overlay-small {
.c-overlay__outer {
- @include overlaySizing($overlayOuterMarginDialog);
+ @include overlaySizing(nth($overlayOuterMarginSmall, 1), nth($overlayOuterMarginSmall, 2));
+ }
+ }
+
+ .l-overlay-dialog {
+ .c-overlay__outer {
+ @include overlaySizing(nth($overlayOuterMarginDialog, 1), nth($overlayOuterMarginDialog, 2));
}
}
.t-dialog-sm .l-overlay-small, // Legacy dialog support
.l-overlay-fit {
.c-overlay__outer {
- @include overlaySizing(auto);
+ @include overlaySizing(auto, auto);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
diff --git a/src/api/telemetry/TelemetryAPI.js b/src/api/telemetry/TelemetryAPI.js
index 93a53a528..15cef5b19 100644
--- a/src/api/telemetry/TelemetryAPI.js
+++ b/src/api/telemetry/TelemetryAPI.js
@@ -20,122 +20,18 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
-const { TelemetryCollection } = require("./TelemetryCollection");
-
-define([
- '../../plugins/displayLayout/CustomStringFormatter',
- './TelemetryMetadataManager',
- './TelemetryValueFormatter',
- './DefaultMetadataProvider',
- 'objectUtils',
- 'lodash'
-], function (
- CustomStringFormatter,
- TelemetryMetadataManager,
- TelemetryValueFormatter,
- DefaultMetadataProvider,
- objectUtils,
- _
-) {
- /**
- * A LimitEvaluator may be used to detect when telemetry values
- * have exceeded nominal conditions.
- *
- * @interface LimitEvaluator
- * @memberof module:openmct.TelemetryAPI~
- */
-
- /**
- * Check for any limit violations associated with a telemetry datum.
- * @method evaluate
- * @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
- * the limit violation, or undefined if a value is within limits
- */
-
- /**
- * A violation of limits defined for a telemetry property.
- * @typedef LimitViolation
- * @memberof {module:openmct.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
- */
-
- /**
- * A TelemetryFormatter converts telemetry values for purposes of
- * display as text.
- *
- * @interface TelemetryFormatter
- * @memberof module:openmct.TelemetryAPI~
- */
-
- /**
- * Retrieve the 'key' from the datum and format it accordingly to
- * telemetry metadata in domain object.
- *
- * @method format
- * @memberof module:openmct.TelemetryAPI~TelemetryFormatter#
- */
-
- /**
- * Describes a property which would be found in a datum of telemetry
- * associated with a particular domain object.
- *
- * @typedef TelemetryProperty
- * @memberof module:openmct.TelemetryAPI~
- * @property {string} key the name of the property in the datum which
- * contains this telemetry value
- * @property {string} name the human-readable name for this property
- * @property {string} [units] the units associated with this property
- * @property {boolean} [temporal] true if this property is a timestamp, or
- * may be otherwise used to order telemetry in a time-like
- * fashion; default is false
- * @property {boolean} [numeric] true if the values for this property
- * can be interpreted plainly as numbers; default is true
- * @property {boolean} [enumerated] true if this property may have only
- * certain specific values; default is false
- * @property {string} [values] for enumerated states, an ordered list
- * of possible values
- */
-
- /**
- * Describes and bounds requests for telemetry data.
- *
- * @typedef TelemetryRequest
- * @memberof module:openmct.TelemetryAPI~
- * @property {string} sort the key of the property to sort by. This may
- * be prefixed with a "+" or a "-" sign to sort in ascending
- * or descending order respectively. If no prefix is present,
- * ascending order will be used.
- * @property {*} start the lower bound for values of the sorting property
- * @property {*} end the upper bound for values of the sorting property
- * @property {string[]} strategies symbolic identifiers for strategies
- * (such as `minmax`) which may be recognized by providers;
- * these will be tried in order until an appropriate provider
- * is found
- */
-
- /**
- * Provides telemetry data. To connect to new data sources, new
- * TelemetryProvider implementations should be
- * [registered]{@link module:openmct.TelemetryAPI#addProvider}.
- *
- * @interface TelemetryProvider
- * @memberof module:openmct.TelemetryAPI~
- */
-
- /**
- * An interface for retrieving telemetry data associated with a domain
- * object.
- *
- * @interface TelemetryAPI
- * @augments module:openmct.TelemetryAPI~TelemetryProvider
- * @memberof module:openmct
- */
- function TelemetryAPI(openmct) {
+import TelemetryCollection from './TelemetryCollection';
+import TelemetryRequestInterceptorRegistry from './TelemetryRequestInterceptor';
+import CustomStringFormatter from '../../plugins/displayLayout/CustomStringFormatter';
+import TelemetryMetadataManager from './TelemetryMetadataManager';
+import TelemetryValueFormatter from './TelemetryValueFormatter';
+import DefaultMetadataProvider from './DefaultMetadataProvider';
+import objectUtils from 'objectUtils';
+import _ from 'lodash';
+
+export default class TelemetryAPI {
+
+ constructor(openmct) {
this.openmct = openmct;
this.formatMapCache = new WeakMap();
@@ -148,12 +44,14 @@ define([
this.requestProviders = [];
this.subscriptionProviders = [];
this.valueFormatterCache = new WeakMap();
+
+ this.requestInterceptorRegistry = new TelemetryRequestInterceptorRegistry();
}
- TelemetryAPI.prototype.abortAllRequests = function () {
+ abortAllRequests() {
this.requestAbortControllers.forEach((controller) => controller.abort());
this.requestAbortControllers.clear();
- };
+ }
/**
* Return Custom String Formatter
@@ -162,9 +60,9 @@ define([
* @param {string} format custom formatter string (eg: %.4f, &lts etc.)
* @returns {CustomStringFormatter}
*/
- TelemetryAPI.prototype.customStringFormatter = function (valueMetadata, format) {
- return new CustomStringFormatter.default(this.openmct, valueMetadata, format);
- };
+ customStringFormatter(valueMetadata, format) {
+ return new CustomStringFormatter(this.openmct, valueMetadata, format);
+ }
/**
* Return true if the given domainObject is a telemetry object. A telemetry
@@ -174,9 +72,9 @@ define([
* @param {module:openmct.DomainObject} domainObject
* @returns {boolean} true if the object is a telemetry object.
*/
- TelemetryAPI.prototype.isTelemetryObject = function (domainObject) {
+ isTelemetryObject(domainObject) {
return Boolean(this.findMetadataProvider(domainObject));
- };
+ }
/**
* Check if this provider can supply telemetry data associated with
@@ -188,10 +86,10 @@ define([
* @returns {boolean} true if telemetry can be provided
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/
- TelemetryAPI.prototype.canProvideTelemetry = function (domainObject) {
+ canProvideTelemetry(domainObject) {
return Boolean(this.findSubscriptionProvider(domainObject))
- || Boolean(this.findRequestProvider(domainObject));
- };
+ || Boolean(this.findRequestProvider(domainObject));
+ }
/**
* Register a telemetry provider with the telemetry service. This
@@ -201,7 +99,7 @@ define([
* @param {module:openmct.TelemetryAPI~TelemetryProvider} provider the new
* telemetry provider
*/
- TelemetryAPI.prototype.addProvider = function (provider) {
+ addProvider(provider) {
if (provider.supportsRequest) {
this.requestProviders.unshift(provider);
}
@@ -217,54 +115,54 @@ define([
if (provider.supportsLimits) {
this.limitProviders.unshift(provider);
}
- };
+ }
/**
* @private
*/
- TelemetryAPI.prototype.findSubscriptionProvider = function () {
+ findSubscriptionProvider() {
const args = Array.prototype.slice.apply(arguments);
function supportsDomainObject(provider) {
return provider.supportsSubscribe.apply(provider, args);
}
return this.subscriptionProviders.filter(supportsDomainObject)[0];
- };
+ }
/**
* @private
*/
- TelemetryAPI.prototype.findRequestProvider = function (domainObject) {
+ findRequestProvider(domainObject) {
const args = Array.prototype.slice.apply(arguments);
function supportsDomainObject(provider) {
return provider.supportsRequest.apply(provider, args);
}
return this.requestProviders.filter(supportsDomainObject)[0];
- };
+ }
/**
* @private
*/
- TelemetryAPI.prototype.findMetadataProvider = function (domainObject) {
+ findMetadataProvider(domainObject) {
return this.metadataProviders.filter(function (p) {
return p.supportsMetadata(domainObject);
})[0];
- };
+ }
/**
* @private
*/
- TelemetryAPI.prototype.findLimitEvaluator = function (domainObject) {
+ findLimitEvaluator(domainObject) {
return this.limitProviders.filter(function (p) {
return p.supportsLimits(domainObject);
})[0];
- };
+ }
/**
* @private
*/
- TelemetryAPI.prototype.standardizeRequestOptions = function (options) {
+ standardizeRequestOptions(options) {
if (!Object.prototype.hasOwnProperty.call(options, 'start')) {
options.start = this.openmct.time.bounds().start;
}
@@ -276,7 +174,47 @@ define([
if (!Object.prototype.hasOwnProperty.call(options, 'domain')) {
options.domain = this.openmct.time.timeSystem().key;
}
- };
+ }
+
+ /**
+ * Register a request interceptor that transforms a request via module:openmct.TelemetryAPI.request
+ * The request will be modifyed when it is received and will be returned in it's modified state
+ * The request will be transformed only if the interceptor is applicable to that domain object as defined by the RequestInterceptorDef
+ *
+ * @param {module:openmct.RequestInterceptorDef} requestInterceptorDef the request interceptor definition to add
+ * @method addRequestInterceptor
+ * @memberof module:openmct.TelemetryRequestInterceptorRegistry#
+ */
+ addRequestInterceptor(requestInterceptorDef) {
+ this.requestInterceptorRegistry.addInterceptor(requestInterceptorDef);
+ }
+
+ /**
+ * Retrieve the request interceptors for a given domain object.
+ * @private
+ */
+ #getInterceptorsForRequest(identifier, request) {
+ return this.requestInterceptorRegistry.getInterceptors(identifier, request);
+ }
+
+ /**
+ * Invoke interceptors if applicable for a given domain object.
+ */
+ async applyRequestInterceptors(domainObject, request) {
+ const interceptors = this.#getInterceptorsForRequest(domainObject.identifier, request);
+
+ if (interceptors.length === 0) {
+ return request;
+ }
+
+ let modifiedRequest = { ...request };
+
+ for (let interceptor of interceptors) {
+ modifiedRequest = await interceptor.invoke(modifiedRequest);
+ }
+
+ return modifiedRequest;
+ }
/**
* Request telemetry collection for a domain object.
@@ -292,13 +230,13 @@ define([
* options for this telemetry collection request
* @returns {TelemetryCollection} a TelemetryCollection instance
*/
- TelemetryAPI.prototype.requestCollection = function (domainObject, options = {}) {
+ requestCollection(domainObject, options = {}) {
return new TelemetryCollection(
this.openmct,
domainObject,
options
);
- };
+ }
/**
* Request historical telemetry for a domain object.
@@ -315,7 +253,7 @@ define([
* @returns {Promise.<object[]>} a promise for an array of
* telemetry data
*/
- TelemetryAPI.prototype.request = function (domainObject) {
+ async request(domainObject) {
if (this.noRequestProviderForAllObjects) {
return Promise.resolve([]);
}
@@ -330,6 +268,7 @@ define([
this.requestAbortControllers.add(abortController);
this.standardizeRequestOptions(arguments[1]);
+
const provider = this.findRequestProvider.apply(this, arguments);
if (!provider) {
this.requestAbortControllers.delete(abortController);
@@ -337,6 +276,8 @@ define([
return this.handleMissingRequestProvider(domainObject);
}
+ arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]);
+
return provider.request.apply(provider, arguments)
.catch((rejected) => {
if (rejected.name !== 'AbortError') {
@@ -348,7 +289,7 @@ define([
}).finally(() => {
this.requestAbortControllers.delete(abortController);
});
- };
+ }
/**
* Subscribe to realtime telemetry for a specific domain object.
@@ -364,7 +305,7 @@ define([
* @returns {Function} a function which may be called to terminate
* the subscription
*/
- TelemetryAPI.prototype.subscribe = function (domainObject, callback, options) {
+ subscribe(domainObject, callback, options) {
const provider = this.findSubscriptionProvider(domainObject);
if (!this.subscribeCache) {
@@ -401,7 +342,7 @@ define([
delete this.subscribeCache[keyString];
}
}.bind(this);
- };
+ }
/**
* Get telemetry metadata for a given domain object. Returns a telemetry
@@ -410,7 +351,7 @@ define([
*
* @returns {TelemetryMetadataManager}
*/
- TelemetryAPI.prototype.getMetadata = function (domainObject) {
+ getMetadata(domainObject) {
if (!this.metadataCache.has(domainObject)) {
const metadataProvider = this.findMetadataProvider(domainObject);
if (!metadataProvider) {
@@ -426,14 +367,14 @@ define([
}
return this.metadataCache.get(domainObject);
- };
+ }
/**
* Return an array of valueMetadatas that are common to all supplied
* telemetry objects and match the requested hints.
*
*/
- TelemetryAPI.prototype.commonValuesForHints = function (metadatas, hints) {
+ commonValuesForHints(metadatas, hints) {
const options = metadatas.map(function (metadata) {
const values = metadata.valuesForHints(hints);
@@ -453,14 +394,14 @@ define([
});
return _.sortBy(options, sortKeys);
- };
+ }
/**
* Get a value formatter for a given valueMetadata.
*
* @returns {TelemetryValueFormatter}
*/
- TelemetryAPI.prototype.getValueFormatter = function (valueMetadata) {
+ getValueFormatter(valueMetadata) {
if (!this.valueFormatterCache.has(valueMetadata)) {
this.valueFormatterCache.set(
valueMetadata,
@@ -469,7 +410,7 @@ define([
}
return this.valueFormatterCache.get(valueMetadata);
- };
+ }
/**
* Get a value formatter for a given key.
@@ -477,9 +418,9 @@ define([
*
* @returns {Format}
*/
- TelemetryAPI.prototype.getFormatter = function (key) {
+ getFormatter(key) {
return this.formatters.get(key);
- };
+ }
/**
* Get a format map of all value formatters for a given piece of telemetry
@@ -487,7 +428,7 @@ define([
*
* @returns {Object<String, {TelemetryValueFormatter}>}
*/
- TelemetryAPI.prototype.getFormatMap = function (metadata) {
+ getFormatMap(metadata) {
if (!metadata) {
return {};
}
@@ -502,17 +443,17 @@ define([
}
return this.formatMapCache.get(metadata);
- };
+ }
/**
* Error Handling: Missing Request provider
*
* @returns Promise
*/
- TelemetryAPI.prototype.handleMissingRequestProvider = function (domainObject) {
+ handleMissingRequestProvider(domainObject) {
this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => {
const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments);
- const hasRequestProvider = Object.hasOwn(requestProvider, 'request');
+ const hasRequestProvider = Object.prototype.hasOwnProperty.call(requestProvider, 'request') && typeof requestProvider.request === 'function';
return supportsRequest && hasRequestProvider;
});
@@ -529,18 +470,18 @@ define([
}
this.openmct.notifications.error(message);
- console.error(detailMessage);
+ console.warn(detailMessage);
return Promise.resolve([]);
- };
+ }
/**
* Register a new telemetry data formatter.
* @param {Format} format the
*/
- TelemetryAPI.prototype.addFormat = function (format) {
+ addFormat(format) {
this.formatters.set(format.key, format);
- };
+ }
/**
* Get a limit evaluator for this domain object.
@@ -558,9 +499,9 @@ define([
* @method limitEvaluator
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/
- TelemetryAPI.prototype.limitEvaluator = function (domainObject) {
+ limitEvaluator(domainObject) {
return this.getLimitEvaluator(domainObject);
- };
+ }
/**
* Get a limits for this domain object.
@@ -578,9 +519,9 @@ define([
* @method limits
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/
- TelemetryAPI.prototype.limitDefinition = function (domainObject) {
+ limitDefinition(domainObject) {
return this.getLimits(domainObject);
- };
+ }
/**
* Get a limit evaluator for this domain object.
@@ -598,7 +539,7 @@ define([
* @method limitEvaluator
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/
- TelemetryAPI.prototype.getLimitEvaluator = function (domainObject) {
+ getLimitEvaluator(domainObject) {
const provider = this.findLimitEvaluator(domainObject);
if (!provider) {
return {
@@ -607,7 +548,7 @@ define([
}
return provider.getLimitEvaluator(domainObject);
- };
+ }
/**
* Get a limit definitions for this domain object.
@@ -636,7 +577,7 @@ define([
* supported colors are purple, red, orange, yellow and cyan
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/
- TelemetryAPI.prototype.getLimits = function (domainObject) {
+ getLimits(domainObject) {
const provider = this.findLimitEvaluator(domainObject);
if (!provider || !provider.getLimits) {
return {
@@ -647,7 +588,104 @@ define([
}
return provider.getLimits(domainObject);
- };
+ }
+}
+
+/**
+ * A LimitEvaluator may be used to detect when telemetry values
+ * have exceeded nominal conditions.
+ *
+ * @interface LimitEvaluator
+ * @memberof module:openmct.TelemetryAPI~
+ */
+
+/**
+ * Check for any limit violations associated with a telemetry datum.
+ * @method evaluate
+ * @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
+ * the limit violation, or undefined if a value is within limits
+ */
+
+/**
+ * A violation of limits defined for a telemetry property.
+ * @typedef LimitViolation
+ * @memberof {module:openmct.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
+ */
+
+/**
+ * A TelemetryFormatter converts telemetry values for purposes of
+ * display as text.
+ *
+ * @interface TelemetryFormatter
+ * @memberof module:openmct.TelemetryAPI~
+ */
+
+/**
+ * Retrieve the 'key' from the datum and format it accordingly to
+ * telemetry metadata in domain object.
+ *
+ * @method format
+ * @memberof module:openmct.TelemetryAPI~TelemetryFormatter#
+ */
- return TelemetryAPI;
-});
+/**
+ * Describes a property which would be found in a datum of telemetry
+ * associated with a particular domain object.
+ *
+ * @typedef TelemetryProperty
+ * @memberof module:openmct.TelemetryAPI~
+ * @property {string} key the name of the property in the datum which
+ * contains this telemetry value
+ * @property {string} name the human-readable name for this property
+ * @property {string} [units] the units associated with this property
+ * @property {boolean} [temporal] true if this property is a timestamp, or
+ * may be otherwise used to order telemetry in a time-like
+ * fashion; default is false
+ * @property {boolean} [numeric] true if the values for this property
+ * can be interpreted plainly as numbers; default is true
+ * @property {boolean} [enumerated] true if this property may have only
+ * certain specific values; default is false
+ * @property {string} [values] for enumerated states, an ordered list
+ * of possible values
+ */
+
+/**
+ * Describes and bounds requests for telemetry data.
+ *
+ * @typedef TelemetryRequest
+ * @memberof module:openmct.TelemetryAPI~
+ * @property {string} sort the key of the property to sort by. This may
+ * be prefixed with a "+" or a "-" sign to sort in ascending
+ * or descending order respectively. If no prefix is present,
+ * ascending order will be used.
+ * @property {*} start the lower bound for values of the sorting property
+ * @property {*} end the upper bound for values of the sorting property
+ * @property {string[]} strategies symbolic identifiers for strategies
+ * (such as `minmax`) which may be recognized by providers;
+ * these will be tried in order until an appropriate provider
+ * is found
+ */
+
+/**
+ * Provides telemetry data. To connect to new data sources, new
+ * TelemetryProvider implementations should be
+ * [registered]{@link module:openmct.TelemetryAPI#addProvider}.
+ *
+ * @interface TelemetryProvider
+ * @memberof module:openmct.TelemetryAPI~
+ */
+
+/**
+ * An interface for retrieving telemetry data associated with a domain
+ * object.
+ *
+ * @interface TelemetryAPI
+ * @augments module:openmct.TelemetryAPI~TelemetryProvider
+ * @memberof module:openmct
+ */
diff --git a/src/api/telemetry/TelemetryAPISpec.js b/src/api/telemetry/TelemetryAPISpec.js
index 0b3c91533..5af944009 100644
--- a/src/api/telemetry/TelemetryAPISpec.js
+++ b/src/api/telemetry/TelemetryAPISpec.js
@@ -21,7 +21,7 @@
*****************************************************************************/
import { createOpenMct, resetApplicationState } from 'utils/testing';
import TelemetryAPI from './TelemetryAPI';
-const { TelemetryCollection } = require("./TelemetryCollection");
+import TelemetryCollection from './TelemetryCollection';
describe('Telemetry API', function () {
let openmct;
diff --git a/src/api/telemetry/TelemetryCollection.js b/src/api/telemetry/TelemetryCollection.js
index 4386a4729..b15ed8331 100644
--- a/src/api/telemetry/TelemetryCollection.js
+++ b/src/api/telemetry/TelemetryCollection.js
@@ -22,15 +22,11 @@
import _ from 'lodash';
import EventEmitter from 'EventEmitter';
-
-const ERRORS = {
- TIMESYSTEM_KEY: 'All telemetry metadata must have a telemetry value with a key that matches the key of the active time system.',
- LOADED: 'Telemetry Collection has already been loaded.'
-};
+import { LOADED_ERROR, TIMESYSTEM_KEY_NOTIFICATION, TIMESYSTEM_KEY_WARNING } from './constants';
/** Class representing a Telemetry Collection. */
-export class TelemetryCollection extends EventEmitter {
+export default class TelemetryCollection extends EventEmitter {
/**
* Creates a Telemetry Collection
*
@@ -53,6 +49,7 @@ export class TelemetryCollection extends EventEmitter {
this.pageState = undefined;
this.lastBounds = undefined;
this.requestAbort = undefined;
+ this.isStrategyLatest = this.options.strategy === 'latest';
}
/**
@@ -61,7 +58,7 @@ export class TelemetryCollection extends EventEmitter {
*/
load() {
if (this.loaded) {
- this._error(ERRORS.LOADED);
+ this._error(LOADED_ERROR);
}
this._setTimeSystem(this.openmct.time.timeSystem());
@@ -130,7 +127,8 @@ export class TelemetryCollection extends EventEmitter {
this.requestAbort = new AbortController();
options.signal = this.requestAbort.signal;
this.emit('requestStarted');
- historicalData = await historicalProvider.request(this.domainObject, options);
+ const modifiedOptions = await this.openmct.telemetry.applyRequestInterceptors(this.domainObject, options);
+ historicalData = await historicalProvider.request(this.domainObject, modifiedOptions);
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Error requesting telemetry data...');
@@ -172,17 +170,18 @@ export class TelemetryCollection extends EventEmitter {
* @private
*/
_processNewTelemetry(telemetryData) {
- performance.mark('tlm:process:start');
if (telemetryData === undefined) {
return;
}
+ let latestBoundedDatum = this.boundedTelemetry[this.boundedTelemetry.length - 1];
let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData];
let parsedValue;
let beforeStartOfBounds;
let afterEndOfBounds;
let added = [];
+ // loop through, sort and dedupe
for (let datum of data) {
parsedValue = this.parseTime(datum);
beforeStartOfBounds = parsedValue < this.lastBounds.start;
@@ -222,7 +221,17 @@ export class TelemetryCollection extends EventEmitter {
}
if (added.length) {
- this.emit('add', added);
+ // if latest strategy is requested, we need to check if the value is the latest unmitted value
+ if (this.isStrategyLatest) {
+ this.boundedTelemetry = [this.boundedTelemetry[this.boundedTelemetry.length - 1]];
+
+ // if true, then this value has yet to be emitted
+ if (this.boundedTelemetry[0] !== latestBoundedDatum) {
+ this.emit('add', this.boundedTelemetry);
+ }
+ } else {
+ this.emit('add', added);
+ }
}
}
@@ -267,6 +276,10 @@ export class TelemetryCollection extends EventEmitter {
this.lastBounds = bounds;
if (isTick) {
+ if (this.timeKey === undefined) {
+ return;
+ }
+
// need to check futureBuffer and need to check
// if anything has fallen out of bounds
let startIndex = 0;
@@ -278,13 +291,20 @@ export class TelemetryCollection extends EventEmitter {
if (startChanged) {
testDatum[this.timeKey] = bounds.start;
- // Calculate the new index of the first item within the bounds
- startIndex = _.sortedIndexBy(
- this.boundedTelemetry,
- testDatum,
- datum => this.parseTime(datum)
- );
- discarded = this.boundedTelemetry.splice(0, startIndex);
+
+ // a little more complicated if not latest strategy
+ if (!this.isStrategyLatest) {
+ // Calculate the new index of the first item within the bounds
+ startIndex = _.sortedIndexBy(
+ this.boundedTelemetry,
+ testDatum,
+ datum => this.parseTime(datum)
+ );
+ discarded = this.boundedTelemetry.splice(0, startIndex);
+ } else if (this.parseTime(testDatum) > this.parseTime(this.boundedTelemetry[0])) {
+ discarded = this.boundedTelemetry;
+ this.boundedTelemetry = [];
+ }
}
if (endChanged) {
@@ -296,7 +316,6 @@ export class TelemetryCollection extends EventEmitter {
datum => this.parseTime(datum)
);
added = this.futureBuffer.splice(0, endIndex);
- this.boundedTelemetry = [...this.boundedTelemetry, ...added];
}
if (discarded.length > 0) {
@@ -304,9 +323,15 @@ export class TelemetryCollection extends EventEmitter {
}
if (added.length > 0) {
+ if (!this.isStrategyLatest) {
+ this.boundedTelemetry = [...this.boundedTelemetry, ...added];
+ } else {
+ added = [added[added.length - 1]];
+ this.boundedTelemetry = added;
+ }
+
this.emit('add', added);
}
-
} else {
// user bounds change, reset
this._reset();
@@ -323,16 +348,26 @@ export class TelemetryCollection extends EventEmitter {
* @private
*/
_setTimeSystem(timeSystem) {
- let domains = this.metadata.valuesForHints(['domain']);
+ let domains = [];
+ let metadataValue = { format: timeSystem.key };
+
+ if (this.metadata) {
+ domains = this.metadata.valuesForHints(['domain']);
+ metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key };
+ }
+
let domain = domains.find((d) => d.key === timeSystem.key);
- if (domain === undefined) {
- this._error(ERRORS.TIMESYSTEM_KEY);
+ if (domain !== undefined) {
+ // timeKey is used to create a dummy datum used for sorting
+ this.timeKey = domain.source;
+ } else {
+ this.timeKey = undefined;
+
+ this._warn(TIMESYSTEM_KEY_WARNING);
+ this.openmct.notifications.alert(TIMESYSTEM_KEY_NOTIFICATION);
}
- // timeKey is used to create a dummy datum used for sorting
- this.timeKey = domain.source; // this defaults to key if no source is set
- let metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key };
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
this.parseTime = (datum) => {
@@ -353,7 +388,6 @@ export class TelemetryCollection extends EventEmitter {
* @todo handle subscriptions more granually
*/
_reset() {
- performance.mark('tlm:reset');
this.boundedTelemetry = [];
this.futureBuffer = [];
@@ -402,4 +436,8 @@ export class TelemetryCollection extends EventEmitter {
_error(message) {
throw new Error(message);
}
+
+ _warn(message) {
+ console.warn(message);
+ }
}
diff --git a/src/api/telemetry/TelemetryCollectionSpec.js b/src/api/telemetry/TelemetryCollectionSpec.js
new file mode 100644
index 000000000..49907c3f5
--- /dev/null
+++ b/src/api/telemetry/TelemetryCollectionSpec.js
@@ -0,0 +1,101 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 {
+ createOpenMct,
+ resetApplicationState
+} from 'utils/testing';
+import { TIMESYSTEM_KEY_WARNING } from './constants';
+
+describe('Telemetry Collection', () => {
+ let openmct;
+ let mockMetadataProvider;
+ let mockMetadata = {};
+ let domainObject;
+
+ beforeEach(done => {
+ openmct = createOpenMct();
+ openmct.on('start', done);
+
+ domainObject = {
+ identifier: {
+ key: 'a',
+ namespace: 'b'
+ },
+ type: 'sample-type'
+ };
+
+ mockMetadataProvider = {
+ key: 'mockMetadataProvider',
+ supportsMetadata() {
+ return true;
+ },
+ getMetadata() {
+ return mockMetadata;
+ }
+ };
+
+ openmct.telemetry.addProvider(mockMetadataProvider);
+ openmct.startHeadless();
+ });
+
+ afterEach(() => {
+ return resetApplicationState();
+ });
+
+ it('Warns if telemetry metadata does not match the active timesystem', () => {
+ mockMetadata.values = [
+ {
+ key: 'foo',
+ name: 'Bar',
+ hints: {
+ domain: 1
+ }
+ }
+ ];
+
+ const telemetryCollection = openmct.telemetry.requestCollection(domainObject);
+ spyOn(telemetryCollection, '_warn');
+ telemetryCollection.load();
+
+ expect(telemetryCollection._warn).toHaveBeenCalledOnceWith(TIMESYSTEM_KEY_WARNING);
+ });
+
+ it('Does not warn if telemetry metadata matches the active timesystem', () => {
+ mockMetadata.values = [
+ {
+ key: 'utc',
+ name: 'Timestamp',
+ format: 'utc',
+ hints: {
+ domain: 1
+ }
+ }
+ ];
+
+ const telemetryCollection = openmct.telemetry.requestCollection(domainObject);
+ spyOn(telemetryCollection, '_warn');
+ telemetryCollection.load();
+
+ expect(telemetryCollection._warn).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/api/telemetry/TelemetryMetadataManager.js b/src/api/telemetry/TelemetryMetadataManager.js
index 1f55f5829..4d9761e3f 100644
--- a/src/api/telemetry/TelemetryMetadataManager.js
+++ b/src/api/telemetry/TelemetryMetadataManager.js
@@ -121,6 +121,18 @@ define([
return _.sortBy(matchingMetadata, ...iteratees);
};
+ /**
+ * check out of a given metadata has array values
+ */
+ TelemetryMetadataManager.prototype.isArrayValue = function (metadata) {
+ const regex = /\[\]$/g;
+ if (!metadata.format && !metadata.formatString) {
+ return false;
+ }
+
+ return (metadata.format || metadata.formatString).match(regex) !== null;
+ };
+
TelemetryMetadataManager.prototype.getFilterableValues = function () {
return this.valueMetadatas.filter(metadatum => metadatum.filters && metadatum.filters.length > 0);
};
@@ -138,7 +150,7 @@ define([
valueMetadata = this.values()[0];
}
- return valueMetadata.key;
+ return valueMetadata;
};
return TelemetryMetadataManager;
diff --git a/src/api/telemetry/TelemetryRequestInterceptor.js b/src/api/telemetry/TelemetryRequestInterceptor.js
new file mode 100644
index 000000000..7204ee332
--- /dev/null
+++ b/src/api/telemetry/TelemetryRequestInterceptor.js
@@ -0,0 +1,68 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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.
+ *****************************************************************************/
+
+export default class TelemetryRequestInterceptorRegistry {
+ /**
+ * A TelemetryRequestInterceptorRegistry maintains the definitions for different interceptors that may be invoked on telemetry
+ * requests.
+ * @interface TelemetryRequestInterceptorRegistry
+ * @memberof module:openmct
+ */
+ constructor() {
+ this.interceptors = [];
+ }
+
+ /**
+ * @interface TelemetryRequestInterceptorDef
+ * @property {function} appliesTo function that determines if this interceptor should be called for the given identifier/request
+ * @property {function} invoke function that transforms the provided request and returns the transformed request
+ * @property {function} priority the priority for this interceptor. A higher number returned has more weight than a lower number
+ * @memberof module:openmct TelemetryRequestInterceptorRegistry#
+ */
+
+ /**
+ * Register a new telemetry request interceptor.
+ *
+ * @param {module:openmct.RequestInterceptorDef} requestInterceptorDef the interceptor to add
+ * @method addInterceptor
+ * @memberof module:openmct.TelemetryRequestInterceptorRegistry#
+ */
+ addInterceptor(interceptorDef) {
+ //TODO: sort by priority
+ this.interceptors.push(interceptorDef);
+ }
+
+ /**
+ * Retrieve all interceptors applicable to a domain object/request.
+ * @method getInterceptors
+ * @returns [module:openmct.RequestInterceptorDef] the registered interceptors for this identifier/request
+ * @memberof module:openmct.TelemetryRequestInterceptorRegistry#
+ */
+ getInterceptors(identifier, request) {
+ return this.interceptors.filter(interceptor => {
+ return typeof interceptor.appliesTo === 'function'
+ && interceptor.appliesTo(identifier, request);
+ });
+ }
+
+}
+
diff --git a/src/api/telemetry/TelemetryValueFormatter.js b/src/api/telemetry/TelemetryValueFormatter.js
index eb97fd062..3e8c0b62c 100644
--- a/src/api/telemetry/TelemetryValueFormatter.js
+++ b/src/api/telemetry/TelemetryValueFormatter.js
@@ -43,9 +43,23 @@ define([
};
this.valueMetadata = valueMetadata;
- this.formatter = formatMap.get(valueMetadata.format) || numberFormatter;
- if (valueMetadata.format === 'enum') {
+ function getNonArrayValue(value) {
+ //metadata format could have array formats ex. string[]/number[]
+ const arrayRegex = /\[\]$/g;
+ if (value && value.match(arrayRegex)) {
+ return value.replace(arrayRegex, '');
+ }
+
+ return value;
+ }
+
+ let valueMetadataFormat = getNonArrayValue(valueMetadata.format);
+
+ //Is there an existing formatter for the format specified? If not, default to number format
+ this.formatter = formatMap.get(valueMetadataFormat) || numberFormatter;
+
+ if (valueMetadataFormat === 'enum') {
this.formatter = {};
this.enumerations = valueMetadata.enumerations.reduce(function (vm, e) {
vm.byValue[e.value] = e.string;
@@ -77,13 +91,13 @@ define([
// Check for formatString support once instead of per format call.
if (valueMetadata.formatString) {
const baseFormat = this.formatter.format;
- const formatString = valueMetadata.formatString;
+ const formatString = getNonArrayValue(valueMetadata.formatString);
this.formatter.format = function (value) {
return printj.sprintf(formatString, baseFormat.call(this, value));
};
}
- if (valueMetadata.format === 'string') {
+ if (valueMetadataFormat === 'string') {
this.formatter.parse = function (value) {
if (value === undefined) {
return '';
@@ -108,7 +122,14 @@ define([
TelemetryValueFormatter.prototype.parse = function (datum) {
if (_.isObject(datum)) {
- return this.formatter.parse(datum[this.valueMetadata.source]);
+ const objectDatum = datum[this.valueMetadata.source];
+ if (Array.isArray(objectDatum)) {
+ return objectDatum.map((item) => {
+ return this.formatter.parse(item);
+ });
+ } else {
+ return this.formatter.parse(objectDatum);
+ }
}
return this.formatter.parse(datum);
@@ -116,7 +137,14 @@ define([
TelemetryValueFormatter.prototype.format = function (datum) {
if (_.isObject(datum)) {
- return this.formatter.format(datum[this.valueMetadata.source]);
+ const objectDatum = datum[this.valueMetadata.source];
+ if (Array.isArray(objectDatum)) {
+ return objectDatum.map((item) => {
+ return this.formatter.format(item);
+ });
+ } else {
+ return this.formatter.format(objectDatum);
+ }
}
return this.formatter.format(datum);
diff --git a/src/api/telemetry/constants.js b/src/api/telemetry/constants.js
new file mode 100644
index 000000000..66b5c50bb
--- /dev/null
+++ b/src/api/telemetry/constants.js
@@ -0,0 +1,25 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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.
+ *****************************************************************************/
+
+export const TIMESYSTEM_KEY_WARNING = 'All telemetry metadata must have a telemetry value with a key that matches the key of the active time system.';
+export const TIMESYSTEM_KEY_NOTIFICATION = 'Telemetry metadata does not match the active time system.';
+export const LOADED_ERROR = 'Telemetry Collection has already been loaded.';
diff --git a/src/api/time/TimeAPI.js b/src/api/time/TimeAPI.js
index c7e3ddeff..d22361d8d 100644
--- a/src/api/time/TimeAPI.js
+++ b/src/api/time/TimeAPI.js
@@ -171,27 +171,38 @@ class TimeAPI extends GlobalTimeContext {
* @memberof module:openmct.TimeAPI#
* @method getContextForView
*/
- getContextForView(objectPath = []) {
+ getContextForView(objectPath) {
+ if (!objectPath || !Array.isArray(objectPath)) {
+ throw new Error('No objectPath provided');
+ }
+
const viewKey = objectPath.length && this.openmct.objects.makeKeyString(objectPath[0].identifier);
- if (viewKey) {
- let viewTimeContext = this.getIndependentContext(viewKey);
- if (viewTimeContext) {
- this.independentContexts.delete(viewKey);
- } else {
- viewTimeContext = new IndependentTimeContext(this.openmct, this, objectPath);
- }
+ if (!viewKey) {
+ // Return the global time context
+ return this;
+ }
- // return a new IndependentContext in case the objectPath is different
+ let viewTimeContext = this.getIndependentContext(viewKey);
+ if (!viewTimeContext) {
+ // If the context doesn't exist yet, create it.
+ viewTimeContext = new IndependentTimeContext(this.openmct, this, objectPath);
this.independentContexts.set(viewKey, viewTimeContext);
+ } else {
+ // If it already exists, compare the objectPath to see if it needs to be updated.
+ const currentPath = this.openmct.objects.getRelativePath(viewTimeContext.objectPath);
+ const newPath = this.openmct.objects.getRelativePath(objectPath);
- return viewTimeContext;
+ if (currentPath !== newPath) {
+ // If the path has changed, update the context.
+ this.independentContexts.delete(viewKey);
+ viewTimeContext = new IndependentTimeContext(this.openmct, this, objectPath);
+ this.independentContexts.set(viewKey, viewTimeContext);
+ }
}
- // always follow the global time context
- return this;
+ return viewTimeContext;
}
-
}
export default TimeAPI;
diff --git a/src/api/user/StatusAPI.js b/src/api/user/StatusAPI.js
new file mode 100644
index 000000000..9c1318c38
--- /dev/null
+++ b/src/api/user/StatusAPI.js
@@ -0,0 +1,295 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 EventEmitter from "EventEmitter";
+
+export default class StatusAPI extends EventEmitter {
+ #userAPI;
+ #openmct;
+
+ constructor(userAPI, openmct) {
+ super();
+ this.#userAPI = userAPI;
+ this.#openmct = openmct;
+
+ this.onProviderStatusChange = this.onProviderStatusChange.bind(this);
+ this.onProviderPollQuestionChange = this.onProviderPollQuestionChange.bind(this);
+ this.listenToStatusEvents = this.listenToStatusEvents.bind(this);
+
+ this.#openmct.once('destroy', () => {
+ const provider = this.#userAPI.getProvider();
+
+ if (typeof provider?.off === 'function') {
+ provider.off('statusChange', this.onProviderStatusChange);
+ provider.off('pollQuestionChange', this.onProviderPollQuestionChange);
+ }
+ });
+
+ this.#userAPI.on('providerAdded', this.listenToStatusEvents);
+ }
+
+ /**
+ * Fetch the currently defined operator status poll question. When presented with a status poll question, all operators will reply with their current status.
+ * @returns {Promise<PollQuestion>}
+ */
+ getPollQuestion() {
+ const provider = this.#userAPI.getProvider();
+
+ if (provider.getPollQuestion) {
+ return provider.getPollQuestion();
+ } else {
+ this.#userAPI.error("User provider does not support polling questions");
+ }
+ }
+
+ /**
+ * Set a poll question for operators to respond to. When presented with a status poll question, all operators will reply with their current status.
+ * @param {String} questionText - The text of the question
+ * @returns {Promise<Boolean>} true if operation was successful, otherwise false.
+ */
+ async setPollQuestion(questionText) {
+ const canSetPollQuestion = await this.canSetPollQuestion();
+
+ if (canSetPollQuestion) {
+ const provider = this.#userAPI.getProvider();
+
+ const result = await provider.setPollQuestion(questionText);
+
+ try {
+ await this.resetAllStatuses();
+ } catch (error) {
+ console.warn("Poll question set but unable to clear operator statuses.");
+ console.error(error);
+ }
+
+ return result;
+ } else {
+ this.#userAPI.error("User provider does not support setting polling question");
+ }
+ }
+
+ /**
+ * Can the currently logged in user set the operator status poll question.
+ * @returns {Promise<Boolean>}
+ */
+ canSetPollQuestion() {
+ const provider = this.#userAPI.getProvider();
+
+ if (provider.canSetPollQuestion) {
+ return provider.canSetPollQuestion();
+ } else {
+ return Promise.resolve(false);
+ }
+ }
+
+ /**
+ * @returns {Promise<Array<Status>>} the complete list of possible states that an operator can reply to a poll question with.
+ */
+ async getPossibleStatuses() {
+ const provider = this.#userAPI.getProvider();
+
+ if (provider.getPossibleStatuses) {
+ const possibleStatuses = await provider.getPossibleStatuses() || [];
+
+ return possibleStatuses.map(status => status);
+ } else {
+ this.#userAPI.error("User provider cannot provide statuses");
+ }
+ }
+
+ /**
+ * @param {import("./UserAPI").Role} role The role to fetch the current status for.
+ * @returns {Promise<Status>} the current status of the provided role
+ */
+ async getStatusForRole(role) {
+ const provider = this.#userAPI.getProvider();
+
+ if (provider.getStatusForRole) {
+ const status = await provider.getStatusForRole(role);
+
+ return status;
+ } else {
+ this.#userAPI.error("User provider does not support role status");
+ }
+ }
+
+ /**
+ * @param {import("./UserAPI").Role} role
+ * @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the given role
+ * @see StatusUserProvider
+ */
+ canProvideStatusForRole(role) {
+ const provider = this.#userAPI.getProvider();
+
+ if (provider.canProvideStatusForRole) {
+ return provider.canProvideStatusForRole(role);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @param {import("./UserAPI").Role} role The role to set the status for.
+ * @param {Status} status The status to set for the provided role
+ * @returns {Promise<Boolean>} true if operation was successful, otherwise false.
+ */
+ setStatusForRole(role, status) {
+ const provider = this.#userAPI.getProvider();
+
+ if (provider.setStatusForRole) {
+ return provider.setStatusForRole(role, status);
+ } else {
+ this.#userAPI.error("User provider does not support setting role status");
+ }
+ }
+
+ /**
+ * Resets the status of the provided role back to its default status.
+ * @param {import("./UserAPI").Role} role The role to set the status for.
+ * @returns {Promise<Boolean>} true if operation was successful, otherwise false.
+ */
+ async resetStatusForRole(role) {
+ const provider = this.#userAPI.getProvider();
+ const defaultStatus = await this.getDefaultStatusForRole(role);
+
+ if (provider.setStatusForRole) {
+ return provider.setStatusForRole(role, defaultStatus);
+ } else {
+ this.#userAPI.error("User provider does not support resetting role status");
+ }
+ }
+
+ /**
+ * Resets the status of all operators to their default status
+ * @returns {Promise<Boolean>} true if operation was successful, otherwise false.
+ */
+ async resetAllStatuses() {
+ const allStatusRoles = await this.getAllStatusRoles();
+
+ return Promise.all(allStatusRoles.map(role => this.resetStatusForRole(role)));
+ }
+
+ /**
+ * The default status. This is the status that will be used before the user has selected any status.
+ * @param {import("./UserAPI").Role} role
+ * @returns {Promise<Status>} the default operator status if no other has been set.
+ */
+ async getDefaultStatusForRole(role) {
+ const provider = this.#userAPI.getProvider();
+ const defaultStatus = await provider.getDefaultStatusForRole(role);
+
+ return defaultStatus;
+ }
+
+ /**
+ * All possible status roles. A status role is a user role that can provide status. In some systems
+ * this may be all user roles, but there may be cases where some users are not are not polled
+ * for status if they do not have a real-time operational role.
+ *
+ * @returns {Promise<Array<import("./UserAPI").Role>>} the default operator status if no other has been set.
+ */
+ getAllStatusRoles() {
+ const provider = this.#userAPI.getProvider();
+
+ if (provider.getAllStatusRoles) {
+ return provider.getAllStatusRoles();
+ } else {
+ this.#userAPI.error("User provider cannot provide all status roles");
+ }
+ }
+
+ /**
+ * 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
+ */
+ 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 {
+ return false;
+ }
+ }
+
+ /**
+ * Private internal function that cannot be made #private because it needs to be registered as a callback to the user provider
+ * @private
+ */
+ listenToStatusEvents(provider) {
+ if (typeof provider.on === 'function') {
+ provider.on('statusChange', this.onProviderStatusChange);
+ provider.on('pollQuestionChange', this.onProviderPollQuestionChange);
+ }
+ }
+
+ /**
+ * @private
+ */
+ onProviderStatusChange(newStatus) {
+ this.emit('statusChange', newStatus);
+ }
+
+ /**
+ * @private
+ */
+ onProviderPollQuestionChange(pollQuestion) {
+ this.emit('pollQuestionChange', pollQuestion);
+ }
+}
+
+/**
+ * @typedef {import('./UserProvider')} UserProvider
+ */
+/**
+ * @typedef {import('./StatusUserProvider')} StatusUserProvider
+ */
+/**
+ * The PollQuestion type
+ * @typedef {Object} PollQuestion
+ * @property {String} question - The question to be presented to users
+ * @property {Number} timestamp - The time that the poll question was set.
+ */
+
+/**
+ * The Status type
+ * @typedef {Object} Status
+ * @property {String} key - A unique identifier for this status
+ * @property {Number} label - A human readable label for this status
+ */
diff --git a/src/api/user/StatusUserProvider.js b/src/api/user/StatusUserProvider.js
new file mode 100644
index 000000000..b474fdbed
--- /dev/null
+++ b/src/api/user/StatusUserProvider.js
@@ -0,0 +1,81 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 UserProvider from "./UserProvider";
+
+export default class StatusUserProvider extends UserProvider {
+ /**
+ * @param {('statusChange'|'pollQuestionChange')} event the name of the event to listen to
+ * @param {Function} callback a function to invoke when this event occurs
+ */
+ on(event, callback) {}
+ /**
+ * @param {('statusChange'|'pollQuestionChange')} event the name of the event to stop listen to
+ * @param {Function} callback the callback function used to register the listener
+ */
+ off(event, callback) {}
+ /**
+ * @returns {import("./StatusAPI").PollQuestion} the current status poll question
+ */
+ async getPollQuestion() {}
+ /**
+ * @param {import("./StatusAPI").PollQuestion} pollQuestion a new poll question to set
+ * @returns {Promise<Boolean>} true if operation was successful, otherwise false
+ */
+ async setPollQuestion(pollQuestion) {}
+ /**
+ * @returns {Promise<Boolean>} true if the current user can set the poll question, otherwise false
+ */
+ async canSetPollQuestion() {}
+ /**
+ * @returns {Promise<Array<import("./StatusAPI").Status>>} a list of the possible statuses that an operator can be in
+ */
+ async getPossibleStatuses() {}
+ /**
+ * @param {import("./UserAPI").Role} role
+ * @returns {Promise<import("./StatusAPI").Status}
+ */
+ async getStatusForRole(role) {}
+ /**
+ * @param {import("./UserAPI").Role} role
+ * @returns {Promise<import("./StatusAPI").Status}
+ */
+ async getDefaultStatusForRole(role) {}
+ /**
+ * @param {import("./UserAPI").Role} role
+ * @param {*} status
+ * @returns {Promise<Boolean>} true if operation was successful, otherwise false.
+ */
+ async setStatusForRole(role, status) {}
+ /**
+ * @param {import("./UserAPI").Role} role
+ * @returns {Promise<Boolean} true if the user provider can provide status for the given role
+ */
+ async canProvideStatusForRole(role) {}
+ /**
+ * @returns {Promise<Array<import("./UserAPI").Role>>} a list of all available status roles, if user permissions allow it.
+ */
+ async getAllStatusRoles() {}
+ /**
+ * @returns {Promise<import("./UserAPI").Role>} the active status role for the currently logged in user
+ */
+ async getStatusRoleForCurrentUser() {}
+}
diff --git a/src/api/user/UserAPI.js b/src/api/user/UserAPI.js
index 194802173..5d5e45476 100644
--- a/src/api/user/UserAPI.js
+++ b/src/api/user/UserAPI.js
@@ -25,16 +25,22 @@ import {
MULTIPLE_PROVIDER_ERROR,
NO_PROVIDER_ERROR
} from './constants';
+import StatusAPI from './StatusAPI';
import User from './User';
class UserAPI extends EventEmitter {
- constructor(openmct) {
+ /**
+ * @param {OpenMCT} openmct
+ * @param {UserAPIConfiguration} config
+ */
+ constructor(openmct, config) {
super();
this._openmct = openmct;
this._provider = undefined;
this.User = User;
+ this.status = new StatusAPI(this, openmct, config);
}
/**
@@ -47,14 +53,17 @@ class UserAPI extends EventEmitter {
*/
setProvider(provider) {
if (this.hasProvider()) {
- this._error(MULTIPLE_PROVIDER_ERROR);
+ this.error(MULTIPLE_PROVIDER_ERROR);
}
this._provider = provider;
-
this.emit('providerAdded', this._provider);
}
+ getProvider() {
+ return this._provider;
+ }
+
/**
* Return true if the user provider has been set.
*
@@ -74,9 +83,11 @@ class UserAPI extends EventEmitter {
* @throws Will throw an error if no user provider is set
*/
getCurrentUser() {
- this._noProviderCheck();
-
- return this._provider.getCurrentUser();
+ if (!this.hasProvider()) {
+ return Promise.resolve(undefined);
+ } else {
+ return this._provider.getCurrentUser();
+ }
}
/**
@@ -105,7 +116,7 @@ class UserAPI extends EventEmitter {
* @throws Will throw an error if no user provider is set
*/
hasRole(roleId) {
- this._noProviderCheck();
+ this.noProviderCheck();
return this._provider.hasRole(roleId);
}
@@ -116,9 +127,9 @@ class UserAPI extends EventEmitter {
* @private
* @throws Will throw an error if no user provider is set
*/
- _noProviderCheck() {
+ noProviderCheck() {
if (!this.hasProvider()) {
- this._error(NO_PROVIDER_ERROR);
+ this.error(NO_PROVIDER_ERROR);
}
}
@@ -129,9 +140,26 @@ class UserAPI extends EventEmitter {
* @param {string} error description of error
* @throws Will throw error passed in
*/
- _error(error) {
+ error(error) {
throw new Error(error);
}
}
export default UserAPI;
+/**
+ * @typedef {String} Role
+ */
+/**
+ * @typedef {Object} OpenMCT
+ */
+/**
+ * @typedef {{statusStyles: Object.<string, StatusStyleDefinition>}} UserAPIConfiguration
+ */
+/**
+ * @typedef {Object} StatusStyleDefinition
+ * @property {String} iconClass The icon class to apply to the status indicator when this status is active "icon-circle-slash",
+ * @property {String} iconClassPoll The icon class to apply to the poll question indicator when this style is active eg. "icon-status-poll-question-mark"
+ * @property {String} statusClass The class to apply to the indicator when this status is active eg. "s-status-error"
+ * @property {String} statusBgColor The background color to apply in the status summary section of the poll question popup for this status eg."#9900cc"
+ * @property {String} statusFgColor The foreground color to apply in the status summary section of the poll question popup for this status eg. "#fff"
+ */
diff --git a/src/api/user/UserAPISpec.js b/src/api/user/UserAPISpec.js
index ebc06d8b8..d85bd8c04 100644
--- a/src/api/user/UserAPISpec.js
+++ b/src/api/user/UserAPISpec.js
@@ -40,11 +40,13 @@ describe("The User API", () => {
});
afterEach(() => {
+ const activeOverlays = openmct.overlays.activeOverlays;
+ activeOverlays.forEach(overlay => overlay.dismiss());
+
return resetApplicationState(openmct);
});
describe('with regard to user providers', () => {
-
it('allows you to specify a user provider', () => {
openmct.user.on('providerAdded', (provider) => {
expect(provider).toBeInstanceOf(ExampleUserProvider);
diff --git a/src/api/user/UserProvider.js b/src/api/user/UserProvider.js
new file mode 100644
index 000000000..8502dd54e
--- /dev/null
+++ b/src/api/user/UserProvider.js
@@ -0,0 +1,36 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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.
+ *****************************************************************************/
+export default class UserProvider {
+ /**
+ * @returns {Promise<User>} A promise that resolves with the currently logged in user
+ */
+ getCurrentUser() {}
+ /**
+ * @returns {Boolean} true if a user is currently logged in, otherwise false
+ */
+ isLoggedIn() {}
+ /**
+ * @param {String} role
+ * @returns {Promise<Boolean>} true if the current user has the given role
+ */
+ hasRole(role) {}
+}
diff --git a/src/api/user/UserStatusAPISpec.js b/src/api/user/UserStatusAPISpec.js
new file mode 100644
index 000000000..30df2820c
--- /dev/null
+++ b/src/api/user/UserStatusAPISpec.js
@@ -0,0 +1,103 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 {
+ createOpenMct,
+ resetApplicationState
+} from '../../utils/testing';
+
+describe("The User Status API", () => {
+ let openmct;
+ let userProvider;
+ let mockUser;
+
+ beforeEach(() => {
+ userProvider = jasmine.createSpyObj("userProvider", [
+ "setPollQuestion",
+ "getPollQuestion",
+ "getCurrentUser",
+ "getPossibleStatuses",
+ "getAllStatusRoles",
+ "canSetPollQuestion",
+ "isLoggedIn",
+ "on"
+ ]);
+ openmct = createOpenMct();
+ mockUser = new openmct.user.User("test-user", "A test user");
+ userProvider.getCurrentUser.and.returnValue(Promise.resolve(mockUser));
+ userProvider.getPossibleStatuses.and.returnValue(Promise.resolve([]));
+ userProvider.getAllStatusRoles.and.returnValue(Promise.resolve([]));
+ userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(false));
+ userProvider.isLoggedIn.and.returnValue(true);
+ });
+
+ afterEach(() => {
+ return resetApplicationState(openmct);
+ });
+
+ describe("the poll question", () => {
+ it('can be set via a user status provider if supported', () => {
+ openmct.user.setProvider(userProvider);
+ userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(true));
+
+ return openmct.user.status.setPollQuestion('This is a poll question').then(() => {
+ expect(userProvider.setPollQuestion).toHaveBeenCalledWith('This is a poll question');
+ });
+ });
+ // fit('emits an event when the poll question changes', () => {
+ // const pollQuestionChangeCallback = jasmine.createSpy('pollQuestionChangeCallback');
+ // let pollQuestionListener;
+
+ // userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(true));
+ // userProvider.on.and.callFake((eventName, listener) => {
+ // if (eventName === 'pollQuestionChange') {
+ // pollQuestionListener = listener;
+ // }
+ // });
+
+ // openmct.user.on('pollQuestionChange', pollQuestionChangeCallback);
+
+ // openmct.user.setProvider(userProvider);
+
+ // return openmct.user.status.setPollQuestion('This is a poll question').then(() => {
+ // expect(pollQuestionListener).toBeDefined();
+ // pollQuestionListener();
+ // expect(pollQuestionChangeCallback).toHaveBeenCalled();
+
+ // const pollQuestion = pollQuestionChangeCallback.calls.mostRecent().args[0];
+ // expect(pollQuestion.question).toBe('This is a poll question');
+
+ // openmct.user.off('pollQuestionChange', pollQuestionChangeCallback);
+ // });
+ // });
+ it('cannot be set if the user is not permitted', () => {
+ openmct.user.setProvider(userProvider);
+ userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(false));
+
+ return openmct.user.status.setPollQuestion('This is a poll question').catch((error) => {
+ expect(error).toBeInstanceOf(Error);
+ }).finally(() => {
+ expect(userProvider.setPollQuestion).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/src/exporters/ImageExporter.js b/src/exporters/ImageExporter.js
index 5ff877cc6..d0459451d 100644
--- a/src/exporters/ImageExporter.js
+++ b/src/exporters/ImageExporter.js
@@ -33,7 +33,7 @@ function replaceDotsWithUnderscores(filename) {
import {saveAs} from 'saveAs';
import html2canvas from 'html2canvas';
-import uuid from 'uuid';
+import { v4 as uuid } from 'uuid';
class ImageExporter {
constructor(openmct) {
@@ -51,7 +51,7 @@ class ImageExporter {
const overlays = this.openmct.overlays;
const dialog = overlays.dialog({
iconClass: 'info',
- message: 'Caputuring an image',
+ message: 'Capturing image, please wait...',
buttons: [
{
label: 'Cancel',
diff --git a/src/plugins/DeviceClassifier/src/DeviceClassifier.js b/src/plugins/DeviceClassifier/src/DeviceClassifier.js
index d305112d7..7bba65d96 100644
--- a/src/plugins/DeviceClassifier/src/DeviceClassifier.js
+++ b/src/plugins/DeviceClassifier/src/DeviceClassifier.js
@@ -52,7 +52,6 @@ export default (agent, document) => {
if (agent.isMobile()) {
const mediaQuery = window.matchMedia("(orientation: landscape)");
function eventHandler(event) {
- console.log("changed");
if (event.matches) {
body.classList.remove("portrait");
body.classList.add("landscape");
diff --git a/src/plugins/LADTable/components/LADRow.vue b/src/plugins/LADTable/components/LADRow.vue
index e18f1f753..929a4e201 100644
--- a/src/plugins/LADTable/components/LADRow.vue
+++ b/src/plugins/LADTable/components/LADRow.vue
@@ -197,7 +197,7 @@ export default {
}
},
setUnit() {
- this.unit = this.valueMetadata.unit || '';
+ this.unit = this.valueMetadata ? this.valueMetadata.unit : '';
},
firstNonDomainAttribute(metadata) {
return metadata
diff --git a/src/plugins/LADTable/components/LadTableSet.vue b/src/plugins/LADTable/components/LadTableSet.vue
index c17d30cd6..d03fb8318 100644
--- a/src/plugins/LADTable/components/LadTableSet.vue
+++ b/src/plugins/LADTable/components/LadTableSet.vue
@@ -83,9 +83,12 @@ export default {
for (let ladTable of ladTables) {
for (let telemetryObject of ladTable) {
let metadata = this.openmct.telemetry.getMetadata(telemetryObject.domainObject);
- for (let metadatum of metadata.valueMetadatas) {
- if (metadatum.unit) {
- return true;
+
+ if (metadata) {
+ for (let metadatum of metadata.valueMetadatas) {
+ if (metadatum.unit) {
+ return true;
+ }
}
}
}
diff --git a/src/plugins/LADTable/plugin.js b/src/plugins/LADTable/plugin.js
index e686ce9a8..5be33faa5 100644
--- a/src/plugins/LADTable/plugin.js
+++ b/src/plugins/LADTable/plugin.js
@@ -32,7 +32,7 @@ export default function plugin() {
openmct.types.addType('LadTable', {
name: "LAD Table",
creatable: true,
- description: "A Latest Available Data tabular view in which each row displays the values for one or more contained telemetry objects.",
+ description: "Display the current value for one or more telemetry end points in a fixed table. Each row is a telemetry end point.",
cssClass: 'icon-tabular-lad',
initialize(domainObject) {
domainObject.composition = [];
@@ -42,7 +42,7 @@ export default function plugin() {
openmct.types.addType('LadTableSet', {
name: "LAD Table Set",
creatable: true,
- description: "A Latest Available Data tabular view in which each row displays the values for one or more contained telemetry objects.",
+ description: "Group LAD Tables together into a single view with sub-headers.",
cssClass: 'icon-tabular-lad-set',
initialize(domainObject) {
domainObject.composition = [];
diff --git a/src/plugins/LADTable/pluginSpec.js b/src/plugins/LADTable/pluginSpec.js
index 33985983e..b03c99afc 100644
--- a/src/plugins/LADTable/pluginSpec.js
+++ b/src/plugins/LADTable/pluginSpec.js
@@ -155,7 +155,7 @@ describe("The LAD Table", () => {
// add another telemetry object as composition in lad table to test multi rows
mockObj.ladTable.composition.push(anotherTelemetryObj.identifier);
- beforeEach(async (done) => {
+ beforeEach(async () => {
let telemetryRequestResolve;
let telemetryObjectResolve;
let anotherTelemetryObjectResolve;
@@ -204,8 +204,6 @@ describe("The LAD Table", () => {
await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]);
await Vue.nextTick();
-
- done();
});
it("should show one row per object in the composition", () => {
diff --git a/src/plugins/URLIndicatorPlugin/URLIndicator.js b/src/plugins/URLIndicatorPlugin/URLIndicator.js
index 1bc83450e..5a6785e54 100644
--- a/src/plugins/URLIndicatorPlugin/URLIndicator.js
+++ b/src/plugins/URLIndicatorPlugin/URLIndicator.js
@@ -20,10 +20,8 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
-define(
- ['zepto'],
- function ($) {
-
+define([],
+ function () {
// Set of connection states; changing among these states will be
// reflected in the indicator's appearance.
// CONNECTED: Everything nominal, expect to be able to read/write.
@@ -75,12 +73,17 @@ define(
};
URLIndicator.prototype.fetchUrl = function () {
- $.ajax({
- type: 'GET',
- url: this.URLpath,
- success: this.handleSuccess,
- error: this.handleError
- });
+ fetch(this.URLpath)
+ .then(response => {
+ if (response.ok) {
+ this.handleSuccess();
+ } else {
+ this.handleError();
+ }
+ })
+ .catch(error => {
+ this.handleError();
+ });
};
URLIndicator.prototype.handleError = function (e) {
diff --git a/src/plugins/URLIndicatorPlugin/URLIndicatorSpec.js b/src/plugins/URLIndicatorPlugin/URLIndicatorSpec.js
index 408a98cd0..cf6cd39ff 100644
--- a/src/plugins/URLIndicatorPlugin/URLIndicatorSpec.js
+++ b/src/plugins/URLIndicatorPlugin/URLIndicatorSpec.js
@@ -25,37 +25,35 @@ define(
"utils/testing",
"./URLIndicator",
"./URLIndicatorPlugin",
- "../../MCT",
- "zepto"
+ "../../MCT"
],
function (
testingUtils,
URLIndicator,
URLIndicatorPlugin,
- MCT,
- $
+ MCT
) {
- const defaultAjaxFunction = $.ajax;
-
describe("The URLIndicator", function () {
let openmct;
let indicatorElement;
let pluginOptions;
- let ajaxOptions;
let urlIndicator; // eslint-disable-line
+ let fetchSpy;
beforeEach(function () {
jasmine.clock().install();
openmct = new testingUtils.createOpenMct();
spyOn(openmct.indicators, 'add');
- spyOn($, 'ajax');
- $.ajax.and.callFake(function (options) {
- ajaxOptions = options;
- });
+ fetchSpy = spyOn(window, 'fetch').and.callFake(() => Promise.resolve({
+ ok: true
+ }));
});
afterEach(function () {
- $.ajax = defaultAjaxFunction;
+ if (window.fetch.restore) {
+ window.fetch.restore();
+ }
+
jasmine.clock().uninstall();
return testingUtils.resetApplicationState(openmct);
@@ -96,11 +94,11 @@ define(
expect(indicatorElement.classList.contains('iconClass-checked')).toBe(true);
});
it("uses custom interval", function () {
- expect($.ajax.calls.count()).toEqual(1);
+ expect(window.fetch).toHaveBeenCalledTimes(1);
jasmine.clock().tick(1);
- expect($.ajax.calls.count()).toEqual(1);
+ expect(window.fetch).toHaveBeenCalledTimes(1);
jasmine.clock().tick(pluginOptions.interval + 1);
- expect($.ajax.calls.count()).toEqual(2);
+ expect(window.fetch).toHaveBeenCalledTimes(2);
});
it("uses custom label if supplied in initialization", function () {
expect(indicatorElement.textContent.indexOf(pluginOptions.label) >= 0).toBe(true);
@@ -120,18 +118,21 @@ define(
it("requests the provided URL", function () {
jasmine.clock().tick(pluginOptions.interval + 1);
- expect(ajaxOptions.url).toEqual(pluginOptions.url);
+ expect(window.fetch).toHaveBeenCalledWith(pluginOptions.url);
});
- it("indicates success if connection is nominal", function () {
+ it("indicates success if connection is nominal", async function () {
jasmine.clock().tick(pluginOptions.interval + 1);
- ajaxOptions.success();
+ await urlIndicator.fetchUrl();
expect(indicatorElement.classList.contains('s-status-on')).toBe(true);
});
- it("indicates an error when the server cannot be reached", function () {
+ it("indicates an error when the server cannot be reached", async function () {
+ fetchSpy.and.callFake(() => Promise.resolve({
+ ok: false
+ }));
jasmine.clock().tick(pluginOptions.interval + 1);
- ajaxOptions.error();
+ await urlIndicator.fetchUrl();
expect(indicatorElement.classList.contains('s-status-warning-hi')).toBe(true);
});
});
diff --git a/src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js b/src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js
index ad09b3b30..196c6732c 100644
--- a/src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js
+++ b/src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js
@@ -56,85 +56,76 @@ describe("The URLTimeSettingsSynchronizer", () => {
it("initial clock is set to fixed is reflected in URL", (done) => {
resolveFunction = () => {
oldHash = window.location.hash;
- expect(window.location.hash.includes('tc.mode=fixed')).toBe(true);
+ expect(window.location.hash).toContain('tc.mode=fixed');
openmct.router.removeListener('change:hash', resolveFunction);
done();
};
+ // We have a debounce set to 300ms on setHash, so if we don't flush,
+ // the above resolve function sometimes doesn't fire due to a race condition.
+ openmct.router.setHash.flush();
openmct.router.on('change:hash', resolveFunction);
});
it("when the clock is set via the time API, it is reflected in the URL", (done) => {
- let success;
-
resolveFunction = () => {
openmct.time.clock('local', {
start: -2000,
end: 200
});
-
- const hasStartDelta = window.location.hash.includes('tc.startDelta=2000');
- const hasEndDelta = window.location.hash.includes('tc.endDelta=200');
- const hasLocalClock = window.location.hash.includes('tc.mode=local');
- success = hasStartDelta && hasEndDelta && hasLocalClock;
- if (success) {
- expect(success).toBe(true);
-
- openmct.router.removeListener('change:hash', resolveFunction);
- done();
- }
+ openmct.router.setHash.flush();
+ const urlHash = window.location.hash;
+ expect(urlHash).toContain('tc.startDelta=2000');
+ expect(urlHash).toContain('tc.endDelta=200');
+ expect(urlHash).toContain('tc.mode=local');
+ openmct.router.removeListener('change:hash', resolveFunction);
+ done();
};
+ // We have a debounce set to 300ms on setHash, so if we don't flush,
+ // the above resolve function sometimes doesn't fire due to a race condition.
+ openmct.router.setHash.flush();
openmct.router.on('change:hash', resolveFunction);
});
it("when the clock mode is set to local, it is reflected in the URL", (done) => {
- let success;
-
resolveFunction = () => {
let hash = window.location.hash;
hash = hash.replace('tc.mode=fixed', 'tc.mode=local');
window.location.hash = hash;
- success = window.location.hash.includes('tc.mode=local');
- if (success) {
- expect(success).toBe(true);
- done();
- }
+ expect(window.location.hash).toContain('tc.mode=local');
+ done();
};
+ // We have a debounce set to 300ms on setHash, so if we don't flush,
+ // the above resolve function sometimes doesn't fire due to a race condition.
+ openmct.router.setHash.flush();
openmct.router.on('change:hash', resolveFunction);
});
it("when the clock mode is set to local, it is reflected in the URL", (done) => {
- let success;
-
resolveFunction = () => {
let hash = window.location.hash;
hash = hash.replace('tc.mode=fixed', 'tc.mode=local');
window.location.hash = hash;
- success = window.location.hash.includes('tc.mode=local');
- if (success) {
- expect(success).toBe(true);
- done();
- }
+ expect(window.location.hash).toContain('tc.mode=local');
+ done();
};
+ // We have a debounce set to 300ms on setHash, so if we don't flush,
+ // the above resolve function sometimes doesn't fire due to a race condition.
+ openmct.router.setHash.flush();
openmct.router.on('change:hash', resolveFunction);
});
it("reset hash", (done) => {
- let success;
-
window.location.hash = oldHash;
resolveFunction = () => {
- success = window.location.hash === oldHash;
- if (success) {
- expect(success).toBe(true);
- done();
- }
+ expect(window.location.hash).toBe(oldHash);
+ done();
};
openmct.router.on('change:hash', resolveFunction);
diff --git a/src/plugins/autoflow/AutoflowTabularPluginSpec.js b/src/plugins/autoflow/AutoflowTabularPluginSpec.js
index ce20cab1d..5e5d49489 100644
--- a/src/plugins/autoflow/AutoflowTabularPluginSpec.js
+++ b/src/plugins/autoflow/AutoflowTabularPluginSpec.js
@@ -21,7 +21,6 @@
*****************************************************************************/
import AutoflowTabularPlugin from './AutoflowTabularPlugin';
import AutoflowTabularConstants from './AutoflowTabularConstants';
-import $ from 'zepto';
import DOMObserver from './dom-observer';
import {
createOpenMct,
@@ -122,7 +121,7 @@ xdescribe("AutoflowTabularPlugin", () => {
name: "Object " + key
};
});
- testContainer = $('<div>')[0];
+ testContainer = document.createElement('div');
domObserver = new DOMObserver(testContainer);
testHistories = testKeys.reduce((histories, key, index) => {
@@ -195,7 +194,7 @@ xdescribe("AutoflowTabularPlugin", () => {
describe("when rows have been populated", () => {
function rowsMatch() {
- const rows = $(testContainer).find(".l-autoflow-row").length;
+ const rows = testContainer.querySelectorAll(".l-autoflow-row").length;
return rows === testChildren.length;
}
@@ -241,20 +240,20 @@ xdescribe("AutoflowTabularPlugin", () => {
const nextWidth =
initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP;
- expect($(testContainer).find('.l-autoflow-col').css('width'))
+ expect(testContainer.querySelector('.l-autoflow-col').css('width'))
.toEqual(initialWidth + 'px');
- $(testContainer).find('.change-column-width').click();
+ testContainer.querySelector('.change-column-width').click();
function widthHasChanged() {
- const width = $(testContainer).find('.l-autoflow-col').css('width');
+ const width = testContainer.querySelector('.l-autoflow-col').css('width');
return width !== initialWidth + 'px';
}
return domObserver.when(widthHasChanged)
.then(() => {
- expect($(testContainer).find('.l-autoflow-col').css('width'))
+ expect(testContainer.querySelector('.l-autoflow-col').css('width'))
.toEqual(nextWidth + 'px');
});
});
@@ -267,13 +266,13 @@ xdescribe("AutoflowTabularPlugin", () => {
it("displays historical telemetry", () => {
function rowTextDefined() {
- return $(testContainer).find(".l-autoflow-item").filter(".r").text() !== "";
+ return testContainer.querySelector(".l-autoflow-item").filter(".r").text() !== "";
}
return domObserver.when(rowTextDefined).then(() => {
testKeys.forEach((key, index) => {
const datum = testHistories[key];
- const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
+ const $cell = testContainer.querySelector(".l-autoflow-row").eq(index).find(".r");
expect($cell.text()).toEqual(String(datum.range));
});
});
@@ -294,7 +293,7 @@ xdescribe("AutoflowTabularPlugin", () => {
return waitsForChange().then(() => {
testData.forEach((datum, index) => {
- const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
+ const $cell = testContainer.querySelector(".l-autoflow-row").eq(index).find(".r");
expect($cell.text()).toEqual(String(datum.range));
});
});
@@ -312,7 +311,7 @@ xdescribe("AutoflowTabularPlugin", () => {
return waitsForChange().then(() => {
testKeys.forEach((datum, index) => {
- const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
+ const $cell = testContainer.querySelector(".l-autoflow-row").eq(index).find(".r");
expect($cell.hasClass(testClass)).toBe(true);
});
});
@@ -322,16 +321,16 @@ xdescribe("AutoflowTabularPlugin", () => {
const rowHeight = AutoflowTabularConstants.ROW_HEIGHT;
const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT;
const count = testKeys.length;
- const $container = $(testContainer);
+ const $container = testContainer;
let promiseChain = Promise.resolve();
function columnsHaveAutoflowed() {
- const itemsHeight = $container.find('.l-autoflow-items').height();
+ const itemsHeight = $container.querySelector('.l-autoflow-items').height();
const availableHeight = itemsHeight - sliderHeight;
const availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1);
const columns = Math.ceil(count / availableRows);
- return $container.find('.l-autoflow-col').length === columns;
+ return $container.querySelector('.l-autoflow-col').length === columns;
}
$container.find('.abs').css({
diff --git a/src/plugins/charts/BarGraphCompositionPolicy.js b/src/plugins/charts/bar/BarGraphCompositionPolicy.js
index 7da0a7fc7..7da0a7fc7 100644
--- a/src/plugins/charts/BarGraphCompositionPolicy.js
+++ b/src/plugins/charts/bar/BarGraphCompositionPolicy.js
diff --git a/src/plugins/charts/BarGraphConstants.js b/src/plugins/charts/bar/BarGraphConstants.js
index bdd88a7ce..bdd88a7ce 100644
--- a/src/plugins/charts/BarGraphConstants.js
+++ b/src/plugins/charts/bar/BarGraphConstants.js
diff --git a/src/plugins/charts/BarGraphPlot.vue b/src/plugins/charts/bar/BarGraphPlot.vue
index ad5894afc..ad5894afc 100644
--- a/src/plugins/charts/BarGraphPlot.vue
+++ b/src/plugins/charts/bar/BarGraphPlot.vue
diff --git a/src/plugins/charts/BarGraphView.vue b/src/plugins/charts/bar/BarGraphView.vue
index dbb07b4a7..11b1a7a07 100644
--- a/src/plugins/charts/BarGraphView.vue
+++ b/src/plugins/charts/bar/BarGraphView.vue
@@ -67,7 +67,9 @@ export default {
this.setTimeContext();
this.loadComposition();
-
+ this.unobserveAxes = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.refreshData);
+ this.unobserveInterpolation = this.openmct.objects.observe(this.domainObject, 'configuration.useInterpolation', this.refreshData);
+ this.unobserveBar = this.openmct.objects.observe(this.domainObject, 'configuration.useBar', this.refreshData);
},
beforeDestroy() {
this.stopFollowingTimeContext();
@@ -78,8 +80,19 @@ export default {
return;
}
- this.composition.off('add', this.addTelemetryObject);
+ this.composition.off('add', this.addToComposition);
this.composition.off('remove', this.removeTelemetryObject);
+ if (this.unobserveAxes) {
+ this.unobserveAxes();
+ }
+
+ if (this.unobserveInterpolation) {
+ this.unobserveInterpolation();
+ }
+
+ if (this.unobserveBar) {
+ this.unobserveBar();
+ }
},
methods: {
setTimeContext() {
@@ -97,6 +110,42 @@ export default {
this.timeContext.off('bounds', this.refreshData);
}
},
+ addToComposition(telemetryObject) {
+ if (Object.values(this.telemetryObjects).length > 0) {
+ this.confirmRemoval(telemetryObject);
+ } else {
+ this.addTelemetryObject(telemetryObject);
+ }
+ },
+ confirmRemoval(telemetryObject) {
+ const dialog = this.openmct.overlays.dialog({
+ iconClass: 'alert',
+ message: 'This action will replace the current telemetry source. Do you want to continue?',
+ buttons: [
+ {
+ label: 'Ok',
+ emphasis: true,
+ callback: () => {
+ const oldTelemetryObject = Object.values(this.telemetryObjects)[0];
+ this.removeFromComposition(oldTelemetryObject);
+ this.removeTelemetryObject(oldTelemetryObject.identifier);
+ this.addTelemetryObject(telemetryObject);
+ dialog.dismiss();
+ }
+ },
+ {
+ label: 'Cancel',
+ callback: () => {
+ this.removeFromComposition(telemetryObject);
+ dialog.dismiss();
+ }
+ }
+ ]
+ });
+ },
+ removeFromComposition(telemetryObject) {
+ this.composition.remove(telemetryObject);
+ },
addTelemetryObject(telemetryObject) {
// grab information we need from the added telmetry object
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
@@ -129,6 +178,26 @@ export default {
this.requestDataFor(telemetryObject);
this.subscribeToObject(telemetryObject);
},
+ setTrace(key, name, axisMetadata, xValues, yValues) {
+ let trace = {
+ key,
+ name: name,
+ x: xValues,
+ y: yValues,
+ xAxisMetadata: {},
+ yAxisMetadata: axisMetadata.yAxisMetadata,
+ type: this.domainObject.configuration.useBar ? 'bar' : 'scatter',
+ mode: 'lines',
+ line: {
+ shape: this.domainObject.configuration.useInterpolation
+ },
+ marker: {
+ color: this.domainObject.configuration.barStyles.series[key].color
+ },
+ hoverinfo: this.domainObject.configuration.useBar ? 'skip' : 'x+y'
+ };
+ this.addTrace(trace, key);
+ },
addTrace(trace, key) {
if (!this.trace.length) {
this.trace = this.trace.concat([trace]);
@@ -157,7 +226,12 @@ export default {
const yAxisMetadata = metadata.valuesForHints(['range'])[0];
//Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only
- const xAxisMetadata = metadata.valuesForHints(['range']);
+ const xAxisMetadata = metadata.valuesForHints(['range'])
+ .map((metaDatum) => {
+ metaDatum.isArrayValue = metadata.isArrayValue(metaDatum);
+
+ return metaDatum;
+ });
return {
xAxisMetadata,
@@ -175,20 +249,22 @@ export default {
loadComposition() {
this.composition = this.openmct.composition.get(this.domainObject);
- if (!this.composition) {
- this.addTelemetryObject(this.domainObject);
-
- return;
- }
-
- this.composition.on('add', this.addTelemetryObject);
+ this.composition.on('add', this.addToComposition);
this.composition.on('remove', this.removeTelemetryObject);
this.composition.load();
},
refreshData(bounds, isTick) {
if (!isTick) {
const telemetryObjects = Object.values(this.telemetryObjects);
- telemetryObjects.forEach(this.requestDataFor);
+ telemetryObjects.forEach((telemetryObject) => {
+ //clear existing data
+ const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
+ const axisMetadata = this.getAxisMetadata(telemetryObject);
+ this.setTrace(key, telemetryObject.name, axisMetadata, [], []);
+ //request new data
+ this.requestDataFor(telemetryObject);
+ this.subscribeToObject(telemetryObject);
+ });
}
},
removeAllSubscriptions() {
@@ -204,7 +280,10 @@ export default {
},
removeTelemetryObject(identifier) {
const key = this.openmct.objects.makeKeyString(identifier);
- delete this.telemetryObjects[key];
+ if (this.telemetryObjects[key]) {
+ delete this.telemetryObjects[key];
+ }
+
if (this.telemetryObjectFormats && this.telemetryObjectFormats[key]) {
delete this.telemetryObjectFormats[key];
}
@@ -229,44 +308,54 @@ export default {
this.openmct.notifications.alert(data.message);
}
- if (!this.isDataInTimeRange(data, key)) {
+ if (!this.isDataInTimeRange(data, key, telemetryObject)) {
+ return;
+ }
+
+ if (this.domainObject.configuration.axes.xKey === undefined || this.domainObject.configuration.axes.yKey === undefined) {
return;
}
let xValues = [];
let yValues = [];
-
- //populate X and Y values for plotly
- axisMetadata.xAxisMetadata.forEach((metadata) => {
- xValues.push(metadata.name);
- if (data[metadata.key]) {
- const formattedValue = this.format(key, metadata.key, data);
- yValues.push(formattedValue);
- } else {
- yValues.push(null);
+ let xAxisMetadata = axisMetadata.xAxisMetadata.find(metadata => metadata.key === this.domainObject.configuration.axes.xKey);
+ if (xAxisMetadata && xAxisMetadata.isArrayValue) {
+ //populate x and y values
+ let metadataKey = this.domainObject.configuration.axes.xKey;
+ if (data[metadataKey] !== undefined) {
+ xValues = this.parse(key, metadataKey, data);
}
- });
- const trace = {
- key,
- name: telemetryObject.name,
- x: xValues,
- y: yValues,
- text: yValues.map(String),
- xAxisMetadata: axisMetadata.xAxisMetadata,
- yAxisMetadata: axisMetadata.yAxisMetadata,
- type: 'bar',
- marker: {
- color: this.domainObject.configuration.barStyles.series[key].color
- },
- hoverinfo: 'skip'
- };
+ metadataKey = this.domainObject.configuration.axes.yKey;
+ if (data[metadataKey] !== undefined) {
+ yValues = this.parse(key, metadataKey, data);
+ }
+ } else {
+ //populate X and Y values for plotly
+ axisMetadata.xAxisMetadata.filter(metadataObj => !metadataObj.isArrayValue).forEach((metadata) => {
+ if (!xAxisMetadata) {
+ //Assign the first metadata to use for any formatting
+ xAxisMetadata = metadata;
+ }
+
+ xValues.push(metadata.name);
+ if (data[metadata.key]) {
+ const parsedValue = this.parse(key, metadata.key, data);
+ yValues.push(parsedValue);
+ } else {
+ yValues.push(null);
+ }
+ });
+ }
- this.addTrace(trace, key);
+ this.setTrace(key, telemetryObject.name, axisMetadata, xValues, yValues);
},
- isDataInTimeRange(datum, key) {
+ isDataInTimeRange(datum, key, telemetryObject) {
const timeSystemKey = this.timeContext.timeSystem().key;
- let currentTimestamp = this.parse(key, timeSystemKey, datum);
+ const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
+ let metadataValue = metadata.value(timeSystemKey) || { key: timeSystemKey };
+
+ let currentTimestamp = this.parse(key, metadataValue.key, datum);
return currentTimestamp && this.timeContext.bounds().end >= currentTimestamp;
},
@@ -286,7 +375,8 @@ export default {
},
requestDataFor(telemetryObject) {
const axisMetadata = this.getAxisMetadata(telemetryObject);
- this.openmct.telemetry.request(telemetryObject)
+ const options = this.getOptions();
+ this.openmct.telemetry.request(telemetryObject, options)
.then(data => {
data.forEach((datum) => {
this.addDataToGraph(telemetryObject, datum, axisMetadata);
diff --git a/src/plugins/charts/BarGraphViewProvider.js b/src/plugins/charts/bar/BarGraphViewProvider.js
index 87ccf8e6e..a5cbbaca4 100644
--- a/src/plugins/charts/BarGraphViewProvider.js
+++ b/src/plugins/charts/bar/BarGraphViewProvider.js
@@ -66,12 +66,15 @@ export default function BarGraphViewProvider(openmct) {
}
};
},
- template: '<bar-graph-view :options="options"></bar-graph-view>'
+ template: '<bar-graph-view ref="graphComponent" :options="options"></bar-graph-view>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
+ },
+ onClearData() {
+ component.$refs.graphComponent.refreshData();
}
};
}
diff --git a/src/plugins/charts/inspector/BarGraphInspectorViewProvider.js b/src/plugins/charts/bar/inspector/BarGraphInspectorViewProvider.js
index 0028ea40d..0028ea40d 100644
--- a/src/plugins/charts/inspector/BarGraphInspectorViewProvider.js
+++ b/src/plugins/charts/bar/inspector/BarGraphInspectorViewProvider.js
diff --git a/src/plugins/charts/bar/inspector/BarGraphOptions.vue b/src/plugins/charts/bar/inspector/BarGraphOptions.vue
new file mode 100644
index 000000000..24bb0b617
--- /dev/null
+++ b/src/plugins/charts/bar/inspector/BarGraphOptions.vue
@@ -0,0 +1,399 @@
+<!--
+ Open MCT, Copyright (c) 2014-2022, United States Government
+ as represented by the Administrator of the National Aeronautics and Space
+ Administration. All rights reserved.
+
+ Open MCT is licensed under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0.
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ License for the specific language governing permissions and limitations
+ under the License.
+
+ Open MCT includes source code licensed under additional open source
+ licenses. See the Open Source Licenses file (LICENSES.md) included with
+ this source code distribution or the Licensing information page available
+ at runtime from the About dialog for additional information.
+-->
+<template>
+<div class="c-bar-graph-options js-bar-plot-option">
+ <ul class="c-tree">
+ <h2 title="Display properties for this object">Bar Graph Series</h2>
+ <li>
+ <series-options
+ v-for="series in plotSeries"
+ :key="series.key"
+ :item="series"
+ :color-palette="colorPalette"
+ />
+ </li>
+ </ul>
+ <div class="grid-properties">
+ <ul class="l-inspector-part">
+ <h2 title="Y axis settings for this object">Axes</h2>
+ <li class="grid-row">
+ <div
+ class="grid-cell label"
+ title="X axis selection."
+ >X Axis</div>
+ <div
+ v-if="isEditing"
+ class="grid-cell value"
+ >
+ <select
+ v-model="xKey"
+ @change="updateForm('xKey')"
+ >
+ <option
+ v-for="option in xKeyOptions"
+ :key="`xKey-${option.value}`"
+ :value="option.value"
+ :selected="option.value === xKey"
+ >
+ {{ option.name }}
+ </option>
+ </select>
+ </div>
+ <div
+ v-else
+ class="grid-cell value"
+ >{{ xKeyLabel }}</div>
+ </li>
+ <li
+ v-if="yKey !== ''"
+ class="grid-row"
+ >
+ <div
+ class="grid-cell label"
+ title="Y axis selection."
+ >Y Axis</div>
+ <div
+ v-if="isEditing"
+ class="grid-cell value"
+ >
+ <select
+ v-model="yKey"
+ @change="updateForm('yKey')"
+ >
+ <option
+ v-for="option in yKeyOptions"
+ :key="`yKey-${option.value}`"
+ :value="option.value"
+ :selected="option.value === yKey"
+ >
+ {{ option.name }}
+ </option>
+ </select>
+ </div>
+ <div
+ v-else
+ class="grid-cell value"
+ >{{ yKeyLabel }}</div>
+ </li>
+ </ul>
+ </div>
+ <div class="grid-properties">
+ <ul class="l-inspector-part">
+ <h2 title="Settings for plot">Settings</h2>
+ <li class="grid-row">
+ <div
+ v-if="isEditing"
+ class="grid-cell label"
+ title="Display style for the plot"
+ >Display Style</div>
+ <div
+ v-if="isEditing"
+ class="grid-cell value"
+ >
+ <select
+ v-model="useBar"
+ @change="updateBar"
+ >
+ <option :value="true">Bar</option>
+ <option :value="false">Line</option>
+ </select>
+ </div>
+ <div
+ v-if="!isEditing"
+ class="grid-cell label"
+ title="Display style for plot"
+ >Display Style</div>
+ <div
+ v-if="!isEditing"
+ class="grid-cell value"
+ >{{ {
+ 'true': 'Bar',
+ 'false': 'Line'
+ }[useBar] }}
+ </div>
+ </li>
+ <li
+ v-if="!useBar"
+ class="grid-row"
+ >
+ <div
+ v-if="isEditing"
+ class="grid-cell label"
+ title="The rendering method to join lines for this series."
+ >Line Method</div>
+ <div
+ v-if="isEditing"
+ class="grid-cell value"
+ >
+ <select
+ v-model="useInterpolation"
+ @change="updateInterpolation"
+ >
+ <option value="linear">Linear interpolate</option>
+ <option value="hv">Step after</option>
+ </select>
+ </div>
+ <div
+ v-if="!isEditing"
+ class="grid-cell label"
+ title="The rendering method to join lines for this series."
+ >Line Method</div>
+ <div
+ v-if="!isEditing"
+ class="grid-cell value"
+ >{{ {
+ 'linear': 'Linear interpolation',
+ 'hv': 'Step After'
+ }[useInterpolation] }}
+ </div>
+ </li>
+ </ul>
+ </div>
+</div>
+</template>
+
+<script>
+import SeriesOptions from "./SeriesOptions.vue";
+import ColorPalette from '@/ui/color/ColorPalette';
+
+export default {
+ components: {
+ SeriesOptions
+ },
+ inject: ['openmct', 'domainObject'],
+ data() {
+ return {
+ xKey: this.domainObject.configuration.axes.xKey,
+ yKey: this.domainObject.configuration.axes.yKey,
+ xKeyLabel: '',
+ yKeyLabel: '',
+ plotSeries: [],
+ yKeyOptions: [],
+ xKeyOptions: [],
+ isEditing: this.openmct.editor.isEditing(),
+ colorPalette: this.colorPalette,
+ useInterpolation: this.domainObject.configuration.useInterpolation,
+ useBar: this.domainObject.configuration.useBar
+ };
+ },
+ computed: {
+ canEdit() {
+ return this.isEditing && !this.domainObject.locked;
+ }
+ },
+ beforeMount() {
+ this.colorPalette = new ColorPalette();
+ },
+ mounted() {
+ this.openmct.editor.on('isEditing', this.setEditState);
+ this.composition = this.openmct.composition.get(this.domainObject);
+ this.registerListeners();
+ this.composition.load();
+ },
+ beforeDestroy() {
+ this.openmct.editor.off('isEditing', this.setEditState);
+ this.stopListening();
+ },
+ methods: {
+ setEditState(isEditing) {
+ this.isEditing = isEditing;
+ },
+ registerListeners() {
+ this.composition.on('add', this.addSeries);
+ this.composition.on('remove', this.removeSeries);
+ this.unobserve = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.setKeysAndSetupOptions);
+ },
+ stopListening() {
+ this.composition.off('add', this.addSeries);
+ this.composition.off('remove', this.removeSeries);
+ if (this.unobserve) {
+ this.unobserve();
+ }
+ },
+ addSeries(series, index) {
+ this.$set(this.plotSeries, this.plotSeries.length, series);
+ this.setupOptions();
+ },
+ removeSeries(seriesIdentifier) {
+ const index = this.plotSeries.findIndex(plotSeries => this.openmct.objects.areIdsEqual(seriesIdentifier, plotSeries.identifier));
+ if (index >= 0) {
+ this.$delete(this.plotSeries, index);
+ this.setupOptions();
+ }
+ },
+ setKeysAndSetupOptions() {
+ this.xKey = this.domainObject.configuration.axes.xKey;
+ this.yKey = this.domainObject.configuration.axes.yKey;
+ this.setupOptions();
+ },
+ setupOptions() {
+ this.xKeyOptions = [];
+ this.yKeyOptions = [];
+ if (this.plotSeries.length <= 0) {
+ return;
+ }
+
+ let update = false;
+ const series = this.plotSeries[0];
+ const metadata = this.openmct.telemetry.getMetadata(series);
+ const metadataRangeValues = metadata.valuesForHints(['range']).map((metaDatum) => {
+ metaDatum.isArrayValue = metadata.isArrayValue(metaDatum);
+
+ return metaDatum;
+ });
+ const metadataArrayValues = metadataRangeValues.filter(metadataObj => metadataObj.isArrayValue);
+ const metadataValues = metadataRangeValues.filter(metadataObj => !metadataObj.isArrayValue);
+ metadataArrayValues.forEach((metadataValue) => {
+ this.xKeyOptions.push({
+ name: metadataValue.name || metadataValue.key,
+ value: metadataValue.key,
+ isArrayValue: metadataValue.isArrayValue
+ });
+ this.yKeyOptions.push({
+ name: metadataValue.name || metadataValue.key,
+ value: metadataValue.key,
+ isArrayValue: metadataValue.isArrayValue
+ });
+ });
+
+ //Metadata values that are not array values will be grouped together as x-axis only option.
+ // Here, the y-axis is not relevant.
+ if (metadataValues.length) {
+ this.xKeyOptions.push(
+ metadataValues.reduce((previousValue, currentValue) => {
+ return {
+ name: previousValue?.name ? `${previousValue.name}, ${currentValue.name}` : `${currentValue.name}`,
+ value: currentValue.key,
+ isArrayValue: currentValue.isArrayValue
+ };
+ }, {name: ''})
+ );
+ }
+
+ let xKeyOptionIndex;
+ let yKeyOptionIndex;
+
+ if (this.domainObject.configuration.axes.xKey) {
+ xKeyOptionIndex = this.xKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.xKey);
+ if (xKeyOptionIndex > -1) {
+ this.xKey = this.xKeyOptions[xKeyOptionIndex].value;
+ this.xKeyLabel = this.xKeyOptions[xKeyOptionIndex].name;
+ }
+ } else {
+ if (this.xKey === undefined) {
+ update = true;
+ xKeyOptionIndex = 0;
+ this.xKey = this.xKeyOptions[xKeyOptionIndex].value;
+ this.xKeyLabel = this.xKeyOptions[xKeyOptionIndex].name;
+ }
+ }
+
+ if (metadataRangeValues.length > 1) {
+ if (this.domainObject.configuration.axes.yKey && this.domainObject.configuration.axes.yKey !== 'none') {
+ yKeyOptionIndex = this.yKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.yKey);
+ if (yKeyOptionIndex > -1 && yKeyOptionIndex !== xKeyOptionIndex) {
+ this.yKey = this.yKeyOptions[yKeyOptionIndex].value;
+ this.yKeyLabel = this.yKeyOptions[yKeyOptionIndex].name;
+ }
+ } else {
+ if (this.yKey === undefined) {
+ if (metadataValues.length && metadataArrayValues.length === 0) {
+ update = true;
+ this.yKey = 'none';
+ } else {
+ yKeyOptionIndex = this.yKeyOptions.findIndex((option, index) => index !== xKeyOptionIndex);
+ if (yKeyOptionIndex > -1) {
+ update = true;
+ this.yKey = this.yKeyOptions[yKeyOptionIndex].value;
+ this.yKeyLabel = this.yKeyOptions[yKeyOptionIndex].name;
+ }
+ }
+ }
+ }
+
+ this.yKeyOptions = this.yKeyOptions.map((option, index) => {
+ if (index === xKeyOptionIndex) {
+ option.name = `${option.name} (swap)`;
+ option.swap = yKeyOptionIndex;
+ } else {
+ option.name = option.name.replace(' (swap)', '');
+ option.swap = undefined;
+ }
+
+ return option;
+ });
+ } else if (this.xKey !== undefined && this.domainObject.configuration.axes.yKey === undefined) {
+ this.domainObject.configuration.axes.yKey = 'none';
+ }
+
+ this.xKeyOptions = this.xKeyOptions.map((option, index) => {
+ if (index === yKeyOptionIndex) {
+ option.name = `${option.name} (swap)`;
+ option.swap = xKeyOptionIndex;
+ } else {
+ option.name = option.name.replace(' (swap)', '');
+ option.swap = undefined;
+ }
+
+ return option;
+ });
+
+ if (update === true) {
+ this.saveConfiguration();
+ }
+ },
+ updateForm(property) {
+ if (property === 'xKey') {
+ const xKeyOption = this.xKeyOptions.find(option => option.value === this.xKey);
+ if (xKeyOption.swap !== undefined) {
+ //swap
+ this.yKey = this.xKeyOptions[xKeyOption.swap].value;
+ } else if (!xKeyOption.isArrayValue) {
+ this.yKey = 'none';
+ } else {
+ this.yKey = undefined;
+ }
+ } else if (property === 'yKey') {
+ const yKeyOption = this.yKeyOptions.find(option => option.value === this.yKey);
+ if (yKeyOption.swap !== undefined) {
+ //swap
+ this.xKey = this.yKeyOptions[yKeyOption.swap].value;
+ }
+ }
+
+ this.saveConfiguration();
+ },
+ saveConfiguration() {
+ this.openmct.objects.mutate(this.domainObject, `configuration.axes`, {
+ xKey: this.xKey,
+ yKey: this.yKey
+ });
+ },
+ updateInterpolation(event) {
+ this.openmct.objects.mutate(this.domainObject, `configuration.useInterpolation`, this.useInterpolation);
+ },
+ updateBar(event) {
+ this.openmct.objects.mutate(this.domainObject, `configuration.useBar`, this.useBar);
+ }
+ }
+};
+</script>
diff --git a/src/plugins/charts/inspector/SeriesOptions.vue b/src/plugins/charts/bar/inspector/SeriesOptions.vue
index 29c1f9cc3..8bb892446 100644
--- a/src/plugins/charts/inspector/SeriesOptions.vue
+++ b/src/plugins/charts/bar/inspector/SeriesOptions.vue
@@ -38,16 +38,19 @@
<div class="c-object-label__name">{{ name }}</div>
</div>
</li>
- <ColorSwatch
- v-if="expanded"
- :current-color="currentColor"
- title="Manually set the color for this bar graph series."
- edit-title="Manually set the color for this bar graph series"
- view-title="The color for this bar graph series."
- short-label="Color"
- class="grid-properties"
- @colorSet="setColor"
- />
+ <ul class="grid-properties">
+ <li class="grid-row">
+ <ColorSwatch
+ v-if="expanded"
+ :current-color="currentColor"
+ title="Manually set the color for this bar graph series."
+ edit-title="Manually set the color for this bar graph series."
+ view-title="The color for this bar graph series."
+ short-label="Color"
+ @colorSet="setColor"
+ />
+ </li>
+ </ul>
</ul>
</template>
@@ -109,7 +112,6 @@ export default {
}
},
mounted() {
- this.key = this.openmct.objects.makeKeyString(this.item);
this.initColorAndName();
this.removeBarStylesListener = this.openmct.objects.observe(this.domainObject, `configuration.barStyles.series["${this.key}"]`, this.initColorAndName);
},
@@ -120,6 +122,7 @@ export default {
},
methods: {
initColorAndName() {
+ this.key = this.openmct.objects.makeKeyString(this.item.identifier);
// this is called before the plot is initialized
if (!this.domainObject.configuration.barStyles.series[this.key]) {
const color = this.colorPalette.getNextColor().asHexString();
diff --git a/src/plugins/charts/plugin.js b/src/plugins/charts/bar/plugin.js
index 7a15d1cb6..c0117b07c 100644
--- a/src/plugins/charts/plugin.js
+++ b/src/plugins/charts/bar/plugin.js
@@ -28,14 +28,17 @@ export default function () {
return function install(openmct) {
openmct.types.addType(BAR_GRAPH_KEY, {
key: BAR_GRAPH_KEY,
- name: "Bar Graph",
+ name: "Graph",
cssClass: "icon-bar-chart",
- description: "View data as a bar graph. Can be added to Display Layouts.",
+ description: "Visualize data as a bar or line graph.",
creatable: true,
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {
- barStyles: { series: {} }
+ barStyles: { series: {} },
+ axes: {},
+ useInterpolation: 'linear',
+ useBar: true
};
},
priority: 891
diff --git a/src/plugins/charts/pluginSpec.js b/src/plugins/charts/bar/pluginSpec.js
index 56e3577e2..b63906bd5 100644
--- a/src/plugins/charts/pluginSpec.js
+++ b/src/plugins/charts/bar/pluginSpec.js
@@ -57,18 +57,18 @@ describe("the plugin", function () {
const testTelemetry = [
{
'utc': 1,
- 'some-key': 'some-value 1',
- 'some-other-key': 'some-other-value 1'
+ 'some-key': ['1.3222'],
+ 'some-other-key': [1]
},
{
'utc': 2,
- 'some-key': 'some-value 2',
- 'some-other-key': 'some-other-value 2'
+ 'some-key': ['2.555'],
+ 'some-other-key': [2]
},
{
'utc': 3,
- 'some-key': 'some-value 3',
- 'some-other-key': 'some-other-value 3'
+ 'some-key': ['3.888'],
+ 'some-other-key': [3]
}
];
@@ -123,7 +123,6 @@ describe("the plugin", function () {
});
describe("The bar graph view", () => {
- let testDomainObject;
let barGraphObject;
// eslint-disable-next-line no-unused-vars
let component;
@@ -135,22 +134,62 @@ describe("the plugin", function () {
namespace: "",
key: "test-plot"
},
+ configuration: {
+ barStyles: {
+ series: {}
+ },
+ axes: {},
+ useInterpolation: 'linear',
+ useBar: true
+ },
type: "telemetry.plot.bar-graph",
name: "Test Bar Graph"
};
- testDomainObject = {
- identifier: {
- namespace: "",
- key: "test-object"
+ mockComposition = new EventEmitter();
+ mockComposition.load = () => {
+ return [];
+ };
+
+ spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
+
+ let viewContainer = document.createElement("div");
+ child.append(viewContainer);
+ component = new Vue({
+ el: viewContainer,
+ components: {
+ BarGraph
},
- configuration: {
- barStyles: {
- series: {}
- }
+ provide: {
+ openmct: openmct,
+ domainObject: barGraphObject,
+ composition: openmct.composition.get(barGraphObject)
},
- type: "test-object",
- name: "Test Object",
+ template: "<BarGraph></BarGraph>"
+ });
+
+ await Vue.nextTick();
+ });
+
+ it("provides a bar graph view", () => {
+ const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);
+ const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW);
+ expect(plotViewProvider).toBeDefined();
+ });
+
+ it("Renders plotly bar graph", () => {
+ let barChartElement = element.querySelectorAll(".plotly");
+ expect(barChartElement.length).toBe(1);
+ });
+
+ it("Handles dots in telemetry id", () => {
+ const dotFullTelemetryObject = {
+ identifier: {
+ namespace: "someNamespace",
+ key: "~OpenMCT~outer.test-object.foo.bar"
+ },
+ type: "test-dotful-object",
+ name: "A Dotful Object",
telemetry: {
values: [{
key: "utc",
@@ -160,14 +199,14 @@ describe("the plugin", function () {
domain: 1
}
}, {
- key: "some-key",
- name: "Some attribute",
+ key: "some-key.foo.name.45",
+ name: "Some dotful attribute",
hints: {
range: 1
}
}, {
- key: "some-other-key",
- name: "Another attribute",
+ key: "some-other-key.bar.344.rad",
+ name: "Another dotful attribute",
hints: {
range: 2
}
@@ -175,11 +214,46 @@ describe("the plugin", function () {
}
};
+ const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);
+ const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW);
+ const barGraphView = plotViewProvider.view(barGraphObject, [barGraphObject]);
+ barGraphView.show(child, true);
+ mockComposition.emit('add', dotFullTelemetryObject);
+ expect(barGraphObject.configuration.barStyles.series["someNamespace:~OpenMCT~outer.test-object.foo.bar"].name).toEqual("A Dotful Object");
+ barGraphView.destroy();
+ });
+ });
+
+ describe("The spectral plot view for telemetry objects with array values", () => {
+ let barGraphObject;
+ // eslint-disable-next-line no-unused-vars
+ let component;
+ let mockComposition;
+
+ beforeEach(async () => {
+ barGraphObject = {
+ identifier: {
+ namespace: "",
+ key: "test-plot"
+ },
+ configuration: {
+ barStyles: {
+ series: {}
+ },
+ axes: {
+ xKey: 'some-key',
+ yKey: 'some-other-key'
+ },
+ useInterpolation: 'linear',
+ useBar: false
+ },
+ type: "telemetry.plot.bar-graph",
+ name: "Test Bar Graph"
+ };
+
mockComposition = new EventEmitter();
mockComposition.load = () => {
- mockComposition.emit('add', testDomainObject);
-
- return [testDomainObject];
+ return [];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
@@ -202,18 +276,7 @@ describe("the plugin", function () {
await Vue.nextTick();
});
- it("provides a bar graph view", () => {
- const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);
- const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW);
- expect(plotViewProvider).toBeDefined();
- });
-
- it("Renders plotly bar graph", () => {
- let barChartElement = element.querySelectorAll(".plotly");
- expect(barChartElement.length).toBe(1);
- });
-
- it("Handles dots in telemetry id", () => {
+ it("Renders spectral plots", () => {
const dotFullTelemetryObject = {
identifier: {
namespace: "someNamespace",
@@ -230,29 +293,36 @@ describe("the plugin", function () {
domain: 1
}
}, {
- key: "some-key.foo.name.45",
- name: "Some dotful attribute",
+ key: "some-key",
+ name: "Some attribute",
+ formatString: '%0.2f[]',
hints: {
range: 1
- }
+ },
+ source: 'some-key'
}, {
- key: "some-other-key.bar.344.rad",
- name: "Another dotful attribute",
+ key: "some-other-key",
+ name: "Another attribute",
+ format: "number[]",
hints: {
range: 2
- }
+ },
+ source: 'some-other-key'
}]
}
};
const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);
const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW);
- const barGraphView = plotViewProvider.view(testDomainObject, [testDomainObject]);
+ const barGraphView = plotViewProvider.view(barGraphObject, [barGraphObject]);
barGraphView.show(child, true);
- expect(testDomainObject.configuration.barStyles.series["test-object"].name).toEqual("Test Object");
mockComposition.emit('add', dotFullTelemetryObject);
- expect(testDomainObject.configuration.barStyles.series["someNamespace:~OpenMCT~outer.test-object.foo.bar"].name).toEqual("A Dotful Object");
- barGraphView.destroy();
+
+ return Vue.nextTick().then(() => {
+ const plotElement = element.querySelector('.cartesianlayer .scatterlayer .trace .lines');
+ expect(plotElement).not.toBeNull();
+ barGraphView.destroy();
+ });
});
});
@@ -297,19 +367,26 @@ describe("the plugin", function () {
type: "test-object",
name: "Test Object",
telemetry: {
- values: [{
- key: "some-key",
- name: "Some attribute",
- hints: {
- domain: 1
- }
- }, {
- key: "some-other-key",
- name: "Another attribute",
- hints: {
- range: 1
- }
- }]
+ values: [
+ {
+ key: "some-key",
+ source: "some-key",
+ name: "Some attribute",
+ format: "enum",
+ enumerations: [
+ {
+ value: 0,
+ string: "OFF"
+ },
+ {
+ value: 1,
+ string: "ON"
+ }
+ ],
+ hints: {
+ range: 1
+ }
+ }]
}
};
const composition = openmct.composition.get(parent);
@@ -412,7 +489,7 @@ describe("the plugin", function () {
testDomainObject = {
identifier: {
namespace: "",
- key: "test-object"
+ key: "~Some~foo.bar"
},
type: "test-object",
name: "Test Object",
@@ -460,11 +537,16 @@ describe("the plugin", function () {
isAlias: true
}
}
- }
+ },
+ axes: {},
+ useInterpolation: 'linear',
+ useBar: true
},
composition: [
{
- key: '~Some~foo.bar'
+ identifier: {
+ key: '~Some~foo.bar'
+ }
}
]
}
diff --git a/src/plugins/charts/scatter/ScatterPlotCompositionPolicy.js b/src/plugins/charts/scatter/ScatterPlotCompositionPolicy.js
new file mode 100644
index 000000000..710fb3402
--- /dev/null
+++ b/src/plugins/charts/scatter/ScatterPlotCompositionPolicy.js
@@ -0,0 +1,57 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2021, 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 { SCATTER_PLOT_KEY } from './scatterPlotConstants';
+
+export default function ScatterPlotCompositionPolicy(openmct) {
+ function hasRange(metadata) {
+ const rangeValues = metadata.valuesForHints(['range']).map((value) => {
+ return value.source;
+ });
+
+ const uniqueRangeValues = new Set(rangeValues);
+
+ return uniqueRangeValues && uniqueRangeValues.size > 1;
+ }
+
+ function hasScatterPlotTelemetry(domainObject) {
+ if (!openmct.telemetry.isTelemetryObject(domainObject)) {
+ return false;
+ }
+
+ let metadata = openmct.telemetry.getMetadata(domainObject);
+
+ return metadata.values().length > 0 && hasRange(metadata);
+ }
+
+ return {
+ allow: function (parent, child) {
+ if (parent.type === SCATTER_PLOT_KEY) {
+ if ((child.type === 'conditionSet') || (!hasScatterPlotTelemetry(child))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ };
+}
diff --git a/src/plugins/charts/scatter/ScatterPlotForm.vue b/src/plugins/charts/scatter/ScatterPlotForm.vue
new file mode 100644
index 000000000..adef666dc
--- /dev/null
+++ b/src/plugins/charts/scatter/ScatterPlotForm.vue
@@ -0,0 +1,146 @@
+/*****************************************************************************
+* Open MCT, Copyright (c) 2014-2022, 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>
+<span class="form-control">
+ <span
+ class="field control"
+ :class="model.cssClass"
+ >
+ <div
+ class="c-form--sub-grid"
+ >
+ <div class="c-form__row">
+ <span
+ class="req-indicator"
+ :class="{'req': isRequired}"
+ >
+ </span>
+ <label>Minimum X axis value</label>
+ <input
+ ref="domainMin"
+ v-model.number="domainMin"
+ data-field-name="domainMin"
+ type="number"
+ @input="onChange('domainMin')"
+ >
+ </div>
+
+ <div class="c-form__row">
+ <span
+ class="req-indicator"
+ :class="{'req': isRequired}"
+ >
+ </span>
+ <label>Maximum X axis value</label>
+ <input
+ ref="domainMax"
+ v-model.number="domainMax"
+ data-field-name="domainMax"
+ type="number"
+ @input="onChange('domainMax')"
+ >
+ </div>
+
+ <div class="c-form__row">
+ <span
+ class="req-indicator"
+ :class="{'req': isRequired}"
+ >
+ </span>
+ <label>Minimum Y axis value</label>
+ <input
+ ref="rangeMin"
+ v-model.number="rangeMin"
+ data-field-name="rangeMin"
+ type="number"
+ @input="onChange('rangeMin')"
+ >
+ </div>
+
+ <div class="c-form__row">
+ <span
+ class="req-indicator"
+ :class="{'req': isRequired}"
+ >
+ </span>
+ <label>Maximum Y axis value</label>
+ <input
+ ref="rangeMax"
+ v-model.number="rangeMax"
+ data-field-name="rangeMax"
+ type="number"
+ @input="onChange('rangeMax')"
+ >
+ </div>
+ </div>
+ </span>
+</span>
+</template>
+
+<script>
+
+export default {
+ props: {
+ model: {
+ type: Object,
+ required: true
+ }
+ },
+ data() {
+ return {
+ rangeMax: this.model.value.rangeMax,
+ rangeMin: this.model.value.rangeMin,
+ domainMax: this.model.value.domainMax,
+ domainMin: this.model.value.domainMin
+ };
+ },
+ computed: {
+ isRequired() {
+ return [this.rangeMax, this.rangeMin, this.domainMin, this.domainMax].some(value => value !== undefined && value !== '');
+ }
+ },
+ methods: {
+ onChange(property) {
+ if (this[property] === '') {
+ this[property] = undefined;
+ }
+
+ const data = {
+ model: this.model,
+ value: {
+ rangeMax: this.rangeMax,
+ rangeMin: this.rangeMin,
+ domainMax: this.domainMax,
+ domainMin: this.domainMin
+ }
+ };
+
+ if (property) {
+ this.model.validate(data);
+ }
+
+ this.$emit('onChange', data);
+ }
+ }
+};
+</script>
diff --git a/src/plugins/charts/scatter/ScatterPlotView.vue b/src/plugins/charts/scatter/ScatterPlotView.vue
new file mode 100644
index 000000000..129a3bca9
--- /dev/null
+++ b/src/plugins/charts/scatter/ScatterPlotView.vue
@@ -0,0 +1,351 @@
+<!--
+ Open MCT, Copyright (c) 2014-2021, 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>
+<ScatterPlotWithUnderlay
+ class="c-plot c-scatter-chart-view"
+ :data="trace"
+ :plot-axis-title="plotAxisTitle"
+ @subscribe="subscribeToAll"
+ @unsubscribe="removeAllSubscriptions"
+/>
+</template>
+
+<script>
+import ScatterPlotWithUnderlay from './ScatterPlotWithUnderlay.vue';
+import _ from 'lodash';
+
+export default {
+ components: {
+ ScatterPlotWithUnderlay
+ },
+ inject: ['openmct', 'domainObject', 'path'],
+ data() {
+ this.telemetryObjects = {};
+ this.telemetryObjectFormats = {};
+ this.valuesByTimestamp = {};
+ this.subscriptions = [];
+
+ return {
+ trace: []
+ };
+ },
+ computed: {
+ plotAxisTitle() {
+ const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {};
+ const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : '';
+ const yAxisUnit = yAxisMetadata.units ? `(${yAxisMetadata.units})` : '';
+
+ return {
+ xAxisTitle: `${xAxisMetadata.name || ''} ${xAxisUnit}`,
+ yAxisTitle: `${yAxisMetadata.name || ''} ${yAxisUnit}`
+ };
+ }
+ },
+ mounted() {
+ this.setTimeContext();
+ this.loadComposition();
+ this.reloadTelemetry = this.reloadTelemetry.bind(this);
+ this.reloadTelemetry = _.debounce(this.reloadTelemetry, 500);
+ this.unobserve = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.reloadTelemetry);
+ this.unobserveUnderlayRanges = this.openmct.objects.observe(this.domainObject, 'configuration.ranges', this.reloadTelemetry);
+ },
+ beforeDestroy() {
+ this.stopFollowingTimeContext();
+
+ if (!this.composition) {
+ return;
+ }
+
+ this.removeAllSubscriptions();
+
+ this.composition.off('add', this.addToComposition);
+ this.composition.off('remove', this.removeTelemetryObject);
+ if (this.unobserve) {
+ this.unobserve();
+ }
+
+ if (this.unobserveUnderlayRanges) {
+ this.unobserveUnderlayRanges();
+ }
+ },
+ methods: {
+ setTimeContext() {
+ this.stopFollowingTimeContext();
+
+ this.timeContext = this.openmct.time.getContextForView(this.path);
+ this.followTimeContext();
+
+ },
+ followTimeContext() {
+ this.timeContext.on('bounds', this.reloadTelemetryOnBoundsChange);
+ },
+ stopFollowingTimeContext() {
+ if (this.timeContext) {
+ this.timeContext.off('bounds', this.reloadTelemetryOnBoundsChange);
+ }
+ },
+ addToComposition(telemetryObject) {
+ if (Object.values(this.telemetryObjects).length > 0) {
+ this.confirmRemoval(telemetryObject);
+ } else {
+ this.addTelemetryObject(telemetryObject);
+ }
+ },
+ removeFromComposition(telemetryObject) {
+ let composition = this.domainObject.composition.filter(id =>
+ !this.openmct.objects.areIdsEqual(id, telemetryObject.identifier)
+ );
+
+ this.openmct.objects.mutate(this.domainObject, 'composition', composition);
+ },
+ addTelemetryObject(telemetryObject) {
+ // grab information we need from the added telmetry object
+ const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
+ this.telemetryObjects[key] = telemetryObject;
+ const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
+ this.telemetryObjectFormats[key] = this.openmct.telemetry.getFormatMap(metadata);
+ this.getDataForTelemetry(key);
+ },
+ confirmRemoval(telemetryObject) {
+ const dialog = this.openmct.overlays.dialog({
+ iconClass: 'alert',
+ message: 'This action will replace the current telemetry source. Do you want to continue?',
+ buttons: [
+ {
+ label: 'Ok',
+ emphasis: true,
+ callback: () => {
+ const oldTelemetryObject = Object.values(this.telemetryObjects)[0];
+ this.removeFromComposition(oldTelemetryObject);
+ this.removeTelemetryObject(oldTelemetryObject.identifier);
+ this.valuesByTimestamp = {};
+ this.addTelemetryObject(telemetryObject);
+ dialog.dismiss();
+ }
+ },
+ {
+ label: 'Cancel',
+ callback: () => {
+ this.removeFromComposition(telemetryObject);
+ dialog.dismiss();
+ }
+ }
+ ]
+ });
+ },
+ getTelemetryProcessor(keyString) {
+ return (telemetry) => {
+ //Check that telemetry object has not been removed since telemetry was requested.
+ const telemetryObject = this.telemetryObjects[keyString];
+ if (!telemetryObject) {
+ return;
+ }
+
+ telemetry.forEach(datum => {
+ this.addDataToGraph(telemetryObject, datum);
+ });
+ this.updateTrace(telemetryObject);
+ };
+ },
+ getAxisMetadata(telemetryObject) {
+ const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
+ if (!metadata) {
+ return {};
+ }
+
+ return metadata.valuesForHints(['range']);
+ },
+ loadComposition() {
+ this.composition = this.openmct.composition.get(this.domainObject);
+ this.composition.on('add', this.addToComposition);
+ this.composition.on('remove', this.removeTelemetryObject);
+ this.composition.load();
+ },
+ reloadTelemetryOnBoundsChange(bounds, isTick) {
+ if (!isTick) {
+ this.reloadTelemetry();
+ }
+ },
+ reloadTelemetry() {
+ this.valuesByTimestamp = {};
+
+ Object.keys(this.telemetryObjects).forEach(key => {
+ this.getDataForTelemetry(key);
+ });
+ },
+ getDataForTelemetry(key) {
+ const telemetryObject = this.telemetryObjects[key];
+ if (!telemetryObject) {
+ return;
+ }
+
+ const telemetryProcessor = this.getTelemetryProcessor(key);
+ const options = this.getOptions();
+ this.openmct.telemetry.request(telemetryObject, options).then(telemetryProcessor);
+ this.subscribeToObject(telemetryObject);
+ },
+ removeTelemetryObject(identifier) {
+ const key = this.openmct.objects.makeKeyString(identifier);
+ if (this.telemetryObjects[key]) {
+ delete this.telemetryObjects[key];
+ }
+
+ if (this.telemetryObjectFormats && this.telemetryObjectFormats[key]) {
+ delete this.telemetryObjectFormats[key];
+ }
+
+ this.removeSubscription(key);
+ },
+ addDataToGraph(telemetryObject, data) {
+ const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
+
+ if (data.message) {
+ this.openmct.notifications.alert(data.message);
+ }
+
+ if (!this.domainObject.configuration.axes.xKey || !this.domainObject.configuration.axes.yKey) {
+ return;
+ }
+
+ const timestamp = this.getTimestampForDatum(data, key, telemetryObject);
+ let valueForTimestamp = this.valuesByTimestamp[timestamp] || {};
+
+ //populate x values
+ let metadataKey = this.domainObject.configuration.axes.xKey;
+ if (data[metadataKey] !== undefined) {
+ valueForTimestamp.x = this.format(key, metadataKey, data);
+ }
+
+ metadataKey = this.domainObject.configuration.axes.yKey;
+ if (data[metadataKey] !== undefined) {
+ valueForTimestamp.y = this.format(key, metadataKey, data);
+ }
+
+ this.valuesByTimestamp[timestamp] = valueForTimestamp;
+ },
+ updateTrace(telemetryObject) {
+ const xAndyValues = Object.values(this.valuesByTimestamp);
+ const xValues = xAndyValues.map(value => value.x);
+ const yValues = xAndyValues.map(value => value.y);
+ const axisMetadata = this.getAxisMetadata(telemetryObject);
+ const xAxisMetadata = axisMetadata.find(metadata => metadata.source === this.domainObject.configuration.axes.xKey);
+ let yAxisMetadata = {};
+ if (this.domainObject.configuration.axes.yKey) {
+ yAxisMetadata = axisMetadata.find(metadata => metadata.source === this.domainObject.configuration.axes.yKey);
+ }
+
+ let trace = {
+ key: this.openmct.objects.makeKeyString(this.domainObject.identifier),
+ name: this.domainObject.name,
+ x: xValues,
+ y: yValues,
+ text: yValues.map(String),
+ xAxisMetadata: xAxisMetadata,
+ yAxisMetadata: yAxisMetadata,
+ type: 'scatter',
+ mode: 'markers',
+ marker: {
+ color: this.domainObject.configuration.styles.color
+ },
+ hoverinfo: 'x+y'
+ };
+
+ if (this.domainObject.configuration.ranges !== undefined && this.domainObject.configuration.ranges.domainMin !== undefined && this.domainObject.configuration.ranges.domainMax !== undefined) {
+ trace.xaxis = {
+ min: this.domainObject.configuration.ranges.domainMin,
+ max: this.domainObject.configuration.ranges.domainMax
+ };
+ }
+
+ if (this.domainObject.configuration.ranges !== undefined && this.domainObject.configuration.ranges.rangeMin !== undefined && this.domainObject.configuration.ranges.rangeMax !== undefined) {
+ trace.yaxis = {
+ min: this.domainObject.configuration.ranges.rangeMin,
+ max: this.domainObject.configuration.ranges.rangeMax
+ };
+ }
+
+ this.trace = [trace];
+ },
+ getTimestampForDatum(datum, key, telemetryObject) {
+ const timeSystemKey = this.timeContext.timeSystem().key;
+ const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
+ let metadataValue = metadata.value(timeSystemKey) || { format: timeSystemKey };
+
+ return this.parse(key, metadataValue.source, datum);
+ },
+ format(telemetryObjectKey, metadataKey, data) {
+ const formats = this.telemetryObjectFormats[telemetryObjectKey];
+
+ return formats[metadataKey].format(data);
+ },
+ parse(telemetryObjectKey, metadataKey, datum) {
+ if (!datum) {
+ return;
+ }
+
+ const formats = this.telemetryObjectFormats[telemetryObjectKey];
+
+ return formats[metadataKey].parse(datum);
+ },
+ getOptions() {
+ const { start, end } = this.timeContext.bounds();
+
+ return {
+ end,
+ start
+ };
+ },
+ subscribeToObject(telemetryObject) {
+ const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
+
+ this.removeSubscription(key);
+
+ const options = this.getOptions();
+ const unsubscribe = this.openmct.telemetry.subscribe(telemetryObject,
+ data => this.addDataToGraph(telemetryObject, data)
+ , options);
+
+ this.subscriptions.push({
+ key,
+ unsubscribe
+ });
+ },
+ subscribeToAll() {
+ const telemetryObjects = Object.values(this.telemetryObjects);
+ telemetryObjects.forEach(this.subscribeToObject);
+ },
+ removeAllSubscriptions() {
+ this.subscriptions.forEach(subscription => subscription.unsubscribe());
+ this.subscriptions = [];
+ },
+ removeSubscription(key) {
+ const found = this.subscriptions.findIndex(subscription => subscription.key === key);
+ if (found > -1) {
+ this.subscriptions[found].unsubscribe();
+ this.subscriptions.splice(found, 1);
+ }
+ }
+ }
+};
+
+</script>
diff --git a/src/plugins/charts/scatter/ScatterPlotViewProvider.js b/src/plugins/charts/scatter/ScatterPlotViewProvider.js
new file mode 100644
index 000000000..338d2eb3e
--- /dev/null
+++ b/src/plugins/charts/scatter/ScatterPlotViewProvider.js
@@ -0,0 +1,79 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2021, 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 ScatterPlotView from './ScatterPlotView.vue';
+import { SCATTER_PLOT_KEY, SCATTER_PLOT_VIEW, TIME_STRIP_KEY } from './scatterPlotConstants.js';
+import Vue from 'vue';
+
+export default function ScatterPlotViewProvider(openmct) {
+ function isCompactView(objectPath) {
+ let isChildOfTimeStrip = objectPath.find(object => object.type === TIME_STRIP_KEY);
+
+ return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
+ }
+
+ return {
+ key: SCATTER_PLOT_VIEW,
+ name: 'Scatter Plot',
+ cssClass: 'icon-telemetry',
+ canView(domainObject, objectPath) {
+ return domainObject && domainObject.type === SCATTER_PLOT_KEY;
+ },
+
+ canEdit(domainObject, objectPath) {
+ return domainObject && domainObject.type === SCATTER_PLOT_KEY;
+ },
+
+ view: function (domainObject, objectPath) {
+ let component;
+
+ return {
+ show: function (element) {
+ let isCompact = isCompactView(objectPath);
+ component = new Vue({
+ el: element,
+ components: {
+ ScatterPlotView
+ },
+ provide: {
+ openmct,
+ domainObject,
+ path: objectPath
+ },
+ data() {
+ return {
+ options: {
+ compact: isCompact
+ }
+ };
+ },
+ template: '<scatter-plot-view :options="options"></scatter-plot-view>'
+ });
+ },
+ destroy: function () {
+ component.$destroy();
+ component = undefined;
+ }
+ };
+ }
+ };
+}
diff --git a/src/plugins/charts/scatter/ScatterPlotWithUnderlay.vue b/src/plugins/charts/scatter/ScatterPlotWithUnderlay.vue
new file mode 100644
index 000000000..796a252ac
--- /dev/null
+++ b/src/plugins/charts/scatter/ScatterPlotWithUnderlay.vue
@@ -0,0 +1,393 @@
+<template>
+<div
+ ref="plotWrapper"
+ class="has-local-controls"
+ :class="{ 's-unsynced' : isZoomed }"
+>
+ <div
+ v-if="isZoomed"
+ class="l-state-indicators"
+ >
+ <span
+ class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle"
+ title="This plot is not currently displaying the latest data. Reset pan/zoom to view latest data."
+ ></span>
+ </div>
+ <div
+ ref="plot"
+ class="c-scatter-chart"
+ ></div>
+ <div
+ ref="localControl"
+ class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover"
+ >
+ <button
+ v-if="data.length"
+ class="c-button icon-reset"
+ :disabled="!isZoomed"
+ title="Reset pan/zoom"
+ @click="reset()"
+ >
+ </button>
+ </div>
+</div>
+</template>
+<script>
+import Plotly from 'plotly-basic';
+
+const MULTI_AXES_X_PADDING_PERCENT = {
+ LEFT: 8,
+ RIGHT: 94
+};
+
+import { getValidatedData } from "@/plugins/plan/util";
+
+const PATH_COLORS = ['blue', 'red', 'green'];
+const MARKER_COLOR = 'white';
+
+export default {
+ inject: ['openmct', 'domainObject'],
+ props: {
+ data: {
+ type: Array,
+ default() {
+ return [];
+ }
+ },
+ plotAxisTitle: {
+ type: Object,
+ default() {
+ return {};
+ }
+ }
+ },
+ data() {
+ return {
+ isZoomed: false,
+ yAxisRange: {
+ min: '',
+ max: ''
+ },
+ xAxisRange: {
+ min: '',
+ max: ''
+ }
+ };
+ },
+ watch: {
+ data: {
+ immediate: false,
+ handler: 'updateData'
+ }
+ },
+ mounted() {
+ this.getUnderlayPlotData();
+
+ Plotly.newPlot(this.$refs.plot, Array.from(this.data.concat(this.getShapes(this.shapesData))), this.getLayout(), {
+ responsive: true,
+ displayModeBar: false
+ });
+ this.registerListeners();
+
+ this.$refs.plot.on('plotly_relayout', this.zoom);
+ },
+ beforeDestroy() {
+ if (this.$refs.plot && this.$refs.plot.off) {
+ this.$refs.plot.off('plotly_relayout', this.zoom);
+ }
+
+ if (this.plotResizeObserver) {
+ this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
+ clearTimeout(this.resizeTimer);
+ }
+
+ if (this.unlistenUnderlay) {
+ this.unlistenUnderlay();
+ }
+
+ if (this.unlistenUnderlayRanges) {
+ this.unlistenUnderlayRanges();
+ }
+
+ if (this.unobserveColorChanges) {
+ this.unobserveColorChanges();
+ }
+ },
+ methods: {
+ getUnderlayPlotData() {
+ if (this.domainObject.selectFile) {
+ this.shapesData = getValidatedData(this.domainObject);
+ } else {
+ this.shapesData = [];
+ }
+ },
+ observeForUnderlayPlotChanges() {
+ this.getUnderlayPlotData();
+ this.updateData();
+ },
+ getAxisMinMax() {
+ if (!this.data.length) {
+ return;
+ }
+
+ // For now, use x and y axes min, max values only if an underlay is available
+ if (this.shapesData.length && this.data[0].xaxis) {
+ this.xAxisRange = this.data[0].xaxis;
+ }
+
+ if (this.shapesData.length && this.data[0].yaxis) {
+ this.yAxisRange = this.data[0].yaxis;
+ }
+ },
+ getLayout() {
+ this.getAxisMinMax();
+
+ const yAxesMeta = this.getYAxisMeta();
+ const primaryYaxis = this.getYaxisLayout(yAxesMeta['1']);
+ const xAxisDomain = this.getXAxisDomain(yAxesMeta);
+
+ const shapes = this.shapesData.map((shapeData, index) => {
+ if (!shapeData.x || !shapeData.y
+ || !shapeData.x.length || !shapeData.y.length
+ || shapeData.x.length !== shapeData.y.length) {
+ return "";
+ }
+
+ let path = `M ${shapeData.x[0]},${shapeData.y[0]}`;
+ shapeData.x.forEach((point, shapeIndex) => {
+ if (shapeIndex > 0) {
+ path = `${path} L${point},${shapeData.y[shapeIndex]}`;
+ }
+ });
+
+ return {
+ path,
+ type: 'path',
+ line: {
+ color: PATH_COLORS[index]
+ },
+ opacity: 0.5
+ };
+ });
+
+ return {
+ autosize: true,
+ showlegend: false,
+ textposition: 'auto',
+ font: {
+ family: 'Helvetica Neue, Helvetica, Arial, sans-serif',
+ size: '12px',
+ color: '#666'
+ },
+ xaxis: {
+ domain: xAxisDomain,
+ range: [this.xAxisRange.min, this.xAxisRange.max],
+ title: this.plotAxisTitle.xAxisTitle,
+ automargin: true
+ },
+ yaxis: primaryYaxis,
+ margin: {
+ l: 5,
+ r: 5,
+ t: 5,
+ b: 0
+ },
+ paper_bgcolor: 'transparent',
+ plot_bgcolor: 'transparent',
+ shapes,
+ layer: 'below'
+ };
+ },
+ getYAxisMeta() {
+ const yAxisMeta = {};
+
+ this.data.forEach(datum => {
+ const yAxisMetadata = datum.yAxisMetadata;
+ const range = '1';
+ const side = 'left';
+ const name = yAxisMetadata.name;
+ const unit = yAxisMetadata.units;
+
+ yAxisMeta[range] = {
+ range,
+ side,
+ name,
+ unit
+ };
+ });
+
+ return yAxisMeta;
+ },
+ getXAxisDomain(yAxisMeta) {
+ let leftPaddingPerc = 0;
+ let rightPaddingPerc = 100;
+ let rightSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'right'));
+ let leftSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'left'));
+ if (yAxisMeta && rightSide.length > 1) {
+ rightPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.RIGHT;
+ }
+
+ if (yAxisMeta && leftSide.length > 1) {
+ leftPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.LEFT;
+ }
+
+ return [leftPaddingPerc / 100, rightPaddingPerc / 100];
+ },
+ getYaxisLayout(yAxisMeta) {
+ if (!yAxisMeta) {
+ return {};
+ }
+
+ const { name, range, side = 'left', unit } = yAxisMeta;
+ const title = `${name} ${unit ? '(' + unit + ')' : ''}`;
+ const yaxis = {
+ automargin: true,
+ title
+ };
+
+ let yRange = this.yAxisRange;
+ if (range === '1') {
+ yaxis.range = [yRange.min, yRange.max];
+
+ return yaxis;
+ }
+
+ yaxis.range = [yRange.min, yRange.max];
+ yaxis.anchor = side.toLowerCase() === 'left'
+ ? 'free'
+ : 'x';
+ yaxis.showline = side.toLowerCase() === 'left';
+ yaxis.side = side.toLowerCase();
+ yaxis.overlaying = 'y';
+ yaxis.position = 0.01;
+
+ return yaxis;
+ },
+ registerListeners() {
+ this.unobserveColorChanges = this.openmct.objects.observe(this.domainObject, 'configuration.styles.color', this.updateColors);
+ this.unlistenUnderlay = this.openmct.objects.observe(this.domainObject, 'selectFile', this.observeForUnderlayPlotChanges);
+ this.unlistenUnderlayRanges = this.openmct.objects.observe(this.domainObject, 'configuration.ranges', this.updateData);
+ this.resizeTimer = false;
+ if (window.ResizeObserver) {
+ this.plotResizeObserver = new ResizeObserver(() => {
+ // debounce and trigger window resize so that plotly can resize the plot
+ clearTimeout(this.resizeTimer);
+ this.resizeTimer = setTimeout(() => {
+ window.dispatchEvent(new Event('resize'));
+ }, 250);
+ });
+ this.plotResizeObserver.observe(this.$refs.plotWrapper);
+ }
+ },
+ updateColors() {
+ const colors = [];
+ const indices = [];
+ this.data.forEach((item, index) => {
+ const colorExists = this.domainObject.configuration.styles.color;
+ indices.push(index);
+ if (colorExists) {
+ colors.push(this.domainObject.configuration.styles.color);
+ } else {
+ colors.push(item.marker.color);
+ }
+ });
+ const plotUpdate = {
+ 'marker.color': colors
+ };
+
+ Plotly.restyle(this.$refs.plot, plotUpdate, indices);
+ },
+ reset() {
+ this.isZoomed = false;
+
+ this.updatePlot();
+ this.$emit('subscribe');
+ },
+ updateData() {
+ this.updatePlot();
+ },
+ updateLocalControlPosition() {
+ const localControl = this.$refs.localControl;
+ localControl.style.display = 'none';
+
+ const plot = this.$refs.plot;
+ const bgLayer = this.$el.querySelector('.bglayer');
+
+ const plotBoundingRect = plot.getBoundingClientRect();
+ const bgLayerBoundingRect = bgLayer.getBoundingClientRect();
+
+ const top = bgLayerBoundingRect.top - plotBoundingRect.top + 5;
+ const left = bgLayerBoundingRect.left - plotBoundingRect.left + 5;
+
+ localControl.style.top = `${top}px`;
+ localControl.style.left = `${left}px`;
+ localControl.style.display = 'block';
+ },
+ updatePlot() {
+ if (!this.$refs || !this.$refs.plot || this.isZoomed) {
+ return;
+ }
+
+ Plotly.react(this.$refs.plot, Array.from(this.data.concat(this.getShapes(this.shapesData))), this.getLayout());
+ },
+ zoom(eventData) {
+ const autorange = eventData['xaxis.autorange'];
+ const { autosize } = eventData;
+
+ if (autosize || autorange) {
+ return;
+ }
+
+ this.isZoomed = true;
+ this.$emit('unsubscribe');
+ },
+ getShapes() {
+ let markerData = {
+ x: [],
+ y: []
+ };
+ const shapes = this.shapesData.map((shapeData, index) => {
+ if (!shapeData.x || !shapeData.y
+ || !shapeData.x.length || !shapeData.y.length
+ || shapeData.x.length !== shapeData.y.length) {
+ return "";
+ }
+
+ let text = [];
+ shapeData.x.forEach((point) => {
+ text.push(`${parseFloat(point).toPrecision(2)}`);
+ });
+
+ markerData.x = markerData.x.concat(shapeData.x);
+ markerData.y = markerData.y.concat(shapeData.y);
+
+ return {
+ x: shapeData.x,
+ y: shapeData.y,
+ mode: 'text',
+ text,
+ textfont: {
+ family: 'Helvetica Neue, Helvetica, Arial, sans-serif',
+ size: '12px',
+ color: PATH_COLORS[index]
+ },
+ opacity: 0.5
+ };
+ });
+
+ shapes.push({
+ x: markerData.x,
+ y: markerData.y,
+ mode: "markers",
+ marker: {
+ size: 6,
+ color: MARKER_COLOR
+ }
+ });
+
+ return shapes;
+ }
+ }
+};
+</script>
+
diff --git a/src/plugins/charts/inspector/BarGraphOptions.vue b/src/plugins/charts/scatter/inspector/PlotOptions.vue
index a17fbc28b..a72fcb8c9 100644
--- a/src/plugins/charts/inspector/BarGraphOptions.vue
+++ b/src/plugins/charts/scatter/inspector/PlotOptions.vue
@@ -20,33 +20,28 @@
at runtime from the About dialog for additional information.
-->
<template>
-<ul class="c-tree c-bar-graph-options">
- <h2 title="Display properties for this object">Bar Graph Series</h2>
- <li
- v-for="series in domainObject.composition"
- :key="series.key"
- >
- <series-options
- :item="series"
- :color-palette="colorPalette"
- />
- </li>
-</ul>
+<div>
+ <div v-if="canEdit">
+ <plot-options-edit />
+ </div>
+ <div v-else>
+ <plot-options-browse />
+ </div>
+</div>
</template>
<script>
-import SeriesOptions from "./SeriesOptions.vue";
-import ColorPalette from '@/ui/color/ColorPalette';
-
+import PlotOptionsBrowse from "./PlotOptionsBrowse.vue";
+import PlotOptionsEdit from "./PlotOptionsEdit.vue";
export default {
components: {
- SeriesOptions
+ PlotOptionsBrowse,
+ PlotOptionsEdit
},
inject: ['openmct', 'domainObject'],
data() {
return {
- isEditing: this.openmct.editor.isEditing(),
- colorPalette: this.colorPalette
+ isEditing: this.openmct.editor.isEditing()
};
},
computed: {
@@ -54,9 +49,6 @@ export default {
return this.isEditing && !this.domainObject.locked;
}
},
- beforeMount() {
- this.colorPalette = new ColorPalette();
- },
mounted() {
this.openmct.editor.on('isEditing', this.setEditState);
},
diff --git a/src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue b/src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue
new file mode 100644
index 000000000..c7af21973
--- /dev/null
+++ b/src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue
@@ -0,0 +1,153 @@
+<!--
+ Open MCT, Copyright (c) 2014-2022, United States Government
+ as represented by the Administrator of the National Aeronautics and Space
+ Administration. All rights reserved.
+
+ Open MCT is licensed under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0.
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ License for the specific language governing permissions and limitations
+ under the License.
+
+ Open MCT includes source code licensed under additional open source
+ licenses. See the Open Source Licenses file (LICENSES.md) included with
+ this source code distribution or the Licensing information page available
+ at runtime from the About dialog for additional information.
+-->
+<template>
+<div class="js-plot-options-browse grid-properties">
+ <ul class="l-inspector-part">
+ <h2 title="Object view settings">Settings</h2>
+ <li class="grid-row">
+ <div
+ class="grid-cell label"
+ title="X axis selection"
+ >X Axis</div>
+ <div class="grid-cell value">{{ xKeyLabel }}</div>
+ </li>
+ <li class="grid-row">
+ <div
+ class="grid-cell label"
+ title="Y axis selection"
+ >Y Axis</div>
+ <div class="grid-cell value">{{ yKeyLabel }}</div>
+ </li>
+ <ColorSwatch
+ :current-color="currentColor"
+ edit-title="Manually set the color for this plot"
+ view-title="The marker color for this plot"
+ short-label="Color"
+ />
+ </ul>
+</div>
+</template>
+
+<script>
+import ColorSwatch from "../../../../ui/color/ColorSwatch.vue";
+import Color from "../../../../ui/color/Color";
+import ColorPalette from "../../../../ui/color/ColorPalette";
+
+export default {
+ components: { ColorSwatch },
+ inject: ['openmct', 'domainObject'],
+ data() {
+ return {
+ xKeyLabel: '',
+ yKeyLabel: '',
+ currentColor: undefined
+ };
+ },
+ mounted() {
+ this.plotSeries = [];
+ this.colorPalette = new ColorPalette();
+ this.initColor();
+ this.composition = this.openmct.composition.get(this.domainObject);
+ this.registerListeners();
+ this.composition.load();
+ },
+ beforeDestroy() {
+ this.stopListening();
+ },
+ methods: {
+ initColor() {
+ // this is called before the plot is initialized
+ if (!this.domainObject.configuration.styles || !this.domainObject.configuration.styles.color) {
+ const color = this.colorPalette.getNextColor().asHexString();
+ this.domainObject.configuration.styles = {
+ color
+ };
+ }
+
+ this.currentColor = this.domainObject.configuration.styles.color;
+ const colorObject = Color.fromHexString(this.currentColor);
+
+ this.colorPalette.remove(colorObject);
+ },
+ registerListeners() {
+ this.composition.on('add', this.addSeries);
+ this.composition.on('remove', this.removeSeries);
+ this.unobserve = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.setAxesLabels);
+ },
+ stopListening() {
+ this.composition.off('add', this.addSeries);
+ this.composition.off('remove', this.removeSeries);
+ if (this.unobserve) {
+ this.unobserve();
+ }
+ },
+ addSeries(series, index) {
+ this.$set(this.plotSeries, this.plotSeries.length, series);
+ this.setAxesLabels();
+ },
+ removeSeries(series) {
+ const index = this.plotSeries.findIndex(plotSeries => this.openmct.objects.areIdsEqual(series.identifier, plotSeries.identifier));
+ if (index !== undefined) {
+ this.$delete(this.plotSeries, index);
+ this.setAxesLabels();
+ }
+ },
+ setAxesLabels() {
+ let xKeyOptions = [];
+ let yKeyOptions = [];
+ if (this.plotSeries.length <= 0) {
+ return;
+ }
+
+ const series = this.plotSeries[0];
+ const metadataValues = this.openmct.telemetry.getMetadata(series).valuesForHints(['range']);
+
+ metadataValues.forEach((metadataValue) => {
+ xKeyOptions.push({
+ name: metadataValue.name || metadataValue.key,
+ value: metadataValue.source || metadataValue.key
+ });
+ yKeyOptions.push({
+ name: metadataValue.name || metadataValue.key,
+ value: metadataValue.source || metadataValue.key
+ });
+ });
+ let xKeyOptionIndex;
+ let yKeyOptionIndex;
+
+ if (this.domainObject.configuration.axes.xKey) {
+ xKeyOptionIndex = xKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.xKey);
+ if (xKeyOptionIndex > -1) {
+ this.xKeyLabel = xKeyOptions[xKeyOptionIndex].name;
+ }
+ }
+
+ if (metadataValues.length > 1 && this.domainObject.configuration.axes.yKey) {
+ yKeyOptionIndex = yKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.yKey);
+ if (yKeyOptionIndex > -1) {
+ this.yKeyLabel = yKeyOptions[yKeyOptionIndex].name;
+ }
+ }
+ }
+ }
+};
+</script>
diff --git a/src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue b/src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue
new file mode 100644
index 000000000..6781a2777
--- /dev/null
+++ b/src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue
@@ -0,0 +1,262 @@
+<!--
+ Open MCT, Copyright (c) 2014-2022, United States Government
+ as represented by the Administrator of the National Aeronautics and Space
+ Administration. All rights reserved.
+
+ Open MCT is licensed under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0.
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ License for the specific language governing permissions and limitations
+ under the License.
+
+ Open MCT includes source code licensed under additional open source
+ licenses. See the Open Source Licenses file (LICENSES.md) included with
+ this source code distribution or the Licensing information page available
+ at runtime from the About dialog for additional information.
+-->
+<template>
+<div class="js-plot-options-edit grid-properties">
+ <ul class="l-inspector-part">
+ <h2 title="Object view settings">Settings</h2>
+ <li class="grid-row">
+ <div
+ class="grid-cell label"
+ title="X axis selection."
+ >X Axis</div>
+ <div class="grid-cell value">
+ <select
+ v-model="xKey"
+ @change="updateForm('xKey')"
+ >
+ <option
+ v-for="option in xKeyOptions"
+ :key="`xKey-${option.value}`"
+ :value="option.value"
+ :selected="option.value == xKey"
+ >
+ {{ option.name }}
+ </option>
+ </select>
+ </div>
+ </li>
+ <li class="grid-row">
+ <div
+ class="grid-cell label"
+ title="Y axis selection."
+ >Y Axis</div>
+ <div class="grid-cell value">
+ <select
+ v-model="yKey"
+ @change="updateForm('yKey')"
+ >
+ <option
+ v-for="option in yKeyOptions"
+ :key="`yKey-${option.value}`"
+ :value="option.value"
+ :selected="option.value == yKey"
+ >
+ {{ option.name }}
+ </option>
+ </select>
+ </div>
+ </li>
+ <ColorSwatch
+ :current-color="currentColor"
+ title="Manually set the line and marker color for this plot."
+ edit-title="Manually set the line and marker color for this plot."
+ view-title="The line and marker color for this plot."
+ short-label="Color"
+ @colorSet="setColor"
+ />
+ </ul>
+</div>
+</template>
+<script>
+import Color from "../../../../ui/color/Color";
+import ColorPalette from "../../../../ui/color/ColorPalette";
+import ColorSwatch from "../../../../ui/color/ColorSwatch.vue";
+
+export default {
+ components: { ColorSwatch },
+ inject: ['openmct', 'domainObject'],
+ data() {
+ return {
+ xKey: undefined,
+ yKey: undefined,
+ xKeyOptions: [],
+ yKeyOptions: [],
+ currentColor: undefined
+ };
+ },
+ mounted() {
+ this.plotSeries = [];
+ this.colorPalette = new ColorPalette();
+ this.initColor();
+ this.composition = this.openmct.composition.get(this.domainObject);
+ this.registerListeners();
+ this.composition.load();
+ },
+ beforeDestroy() {
+ this.stopListening();
+ },
+ methods: {
+ initColor() {
+ // this is called before the plot is initialized
+ if (!this.domainObject.configuration.styles || !this.domainObject.configuration.styles.color) {
+ const color = this.colorPalette.getNextColor().asHexString();
+ this.domainObject.configuration.styles = {
+ color
+ };
+ }
+
+ this.currentColor = this.domainObject.configuration.styles.color;
+ const colorObject = Color.fromHexString(this.currentColor);
+
+ this.colorPalette.remove(colorObject);
+ },
+ setColor(chosenColor) {
+ this.currentColor = chosenColor.asHexString();
+ this.openmct.objects.mutate(
+ this.domainObject,
+ `configuration.styles.color`,
+ this.currentColor
+ );
+ },
+ registerListeners() {
+ this.composition.on('add', this.addSeries);
+ this.composition.on('remove', this.removeSeries);
+ this.unobserve = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.setupOptions);
+ },
+ stopListening() {
+ this.composition.off('add', this.addSeries);
+ this.composition.off('remove', this.removeSeries);
+ if (this.unobserve) {
+ this.unobserve();
+ }
+ },
+ addSeries(series, index) {
+ this.$set(this.plotSeries, this.plotSeries.length, series);
+ this.setupOptions();
+ },
+ removeSeries(seriesIdentifier) {
+ const index = this.plotSeries.findIndex(plotSeries => this.openmct.objects.areIdsEqual(seriesIdentifier, plotSeries.identifier));
+ if (index >= 0) {
+ this.$delete(this.plotSeries, index);
+ this.setupOptions();
+ }
+ },
+ setupOptions() {
+ this.xKeyOptions = [];
+ this.yKeyOptions = [];
+ if (this.plotSeries.length <= 0) {
+ return;
+ }
+
+ let update = false;
+ const series = this.plotSeries[0];
+ const metadataValues = this.openmct.telemetry.getMetadata(series).valuesForHints(['range']);
+ metadataValues.forEach((metadataValue) => {
+ this.xKeyOptions.push({
+ name: metadataValue.name || metadataValue.key,
+ value: metadataValue.source || metadataValue.key
+ });
+ this.yKeyOptions.push({
+ name: metadataValue.name || metadataValue.key,
+ value: metadataValue.source || metadataValue.key
+ });
+ });
+
+ let xKeyOptionIndex;
+ let yKeyOptionIndex;
+
+ if (this.domainObject.configuration.axes.xKey) {
+ xKeyOptionIndex = this.xKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.xKey);
+ if (xKeyOptionIndex > -1) {
+ this.xKey = this.xKeyOptions[xKeyOptionIndex].value;
+ } else {
+ this.xKey = undefined;
+ }
+ }
+
+ if (this.xKey === undefined) {
+ update = true;
+ xKeyOptionIndex = 0;
+ this.xKey = this.xKeyOptions[xKeyOptionIndex].value;
+ }
+
+ if (metadataValues.length > 1) {
+ if (this.domainObject.configuration.axes.yKey) {
+ yKeyOptionIndex = this.yKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.yKey);
+ if (yKeyOptionIndex > -1 && yKeyOptionIndex !== xKeyOptionIndex) {
+ this.yKey = this.yKeyOptions[yKeyOptionIndex].value;
+ } else {
+ this.yKey = undefined;
+ }
+ }
+
+ if (this.yKey === undefined) {
+ update = true;
+ yKeyOptionIndex = this.yKeyOptions.findIndex((option, index) => index !== xKeyOptionIndex);
+ this.yKey = this.yKeyOptions[yKeyOptionIndex].value;
+ }
+
+ this.yKeyOptions = this.yKeyOptions.map((option, index) => {
+ if (index === xKeyOptionIndex) {
+ option.name = `${option.name} (swap)`;
+ option.swap = yKeyOptionIndex;
+ } else {
+ option.name = option.name.replace(' (swap)', '');
+ option.swap = undefined;
+ }
+
+ return option;
+ });
+ }
+
+ this.xKeyOptions = this.xKeyOptions.map((option, index) => {
+ if (index === yKeyOptionIndex) {
+ option.name = `${option.name} (swap)`;
+ option.swap = xKeyOptionIndex;
+ } else {
+ option.name = option.name.replace(' (swap)', '');
+ option.swap = undefined;
+ }
+
+ return option;
+ });
+
+ if (update === true) {
+ this.saveConfiguration();
+ }
+ },
+ updateForm(property) {
+ if (property === 'xKey') {
+ const xKeyOption = this.xKeyOptions.find(option => option.value === this.xKey);
+ if (xKeyOption.swap !== undefined) {
+ //swap
+ this.yKey = this.xKeyOptions[xKeyOption.swap].value;
+ }
+ } else if (property === 'yKey') {
+ const yKeyOption = this.yKeyOptions.find(option => option.value === this.yKey);
+ if (yKeyOption.swap !== undefined) {
+ //swap
+ this.xKey = this.yKeyOptions[yKeyOption.swap].value;
+ }
+ }
+
+ this.saveConfiguration();
+ },
+ saveConfiguration() {
+ this.openmct.objects.mutate(this.domainObject, `configuration.axes`, {
+ xKey: this.xKey,
+ yKey: this.yKey
+ });
+ }
+ }
+};
+</script>
diff --git a/src/plugins/charts/scatter/inspector/ScatterPlotInspectorViewProvider.js b/src/plugins/charts/scatter/inspector/ScatterPlotInspectorViewProvider.js
new file mode 100644
index 000000000..54487dfe3
--- /dev/null
+++ b/src/plugins/charts/scatter/inspector/ScatterPlotInspectorViewProvider.js
@@ -0,0 +1,48 @@
+import { SCATTER_PLOT_INSPECTOR_KEY, SCATTER_PLOT_KEY } from '../scatterPlotConstants';
+import Vue from 'vue';
+import PlotOptions from "./PlotOptions.vue";
+
+export default function ScatterPlotInspectorViewProvider(openmct) {
+ return {
+ key: SCATTER_PLOT_INSPECTOR_KEY,
+ name: 'Bar Graph Inspector View',
+ canView: function (selection) {
+ if (selection.length === 0 || selection[0].length === 0) {
+ return false;
+ }
+
+ let object = selection[0][0].context.item;
+
+ return object
+ && object.type === SCATTER_PLOT_KEY;
+ },
+ view: function (selection) {
+ let component;
+
+ return {
+ show: function (element) {
+ component = new Vue({
+ el: element,
+ components: {
+ PlotOptions
+ },
+ provide: {
+ openmct,
+ domainObject: selection[0][0].context.item
+ },
+ template: '<plot-options></plot-options>'
+ });
+ },
+ destroy: function () {
+ if (component) {
+ component.$destroy();
+ component = undefined;
+ }
+ }
+ };
+ },
+ priority: function () {
+ return 1;
+ }
+ };
+}
diff --git a/src/plugins/charts/scatter/plugin.js b/src/plugins/charts/scatter/plugin.js
new file mode 100644
index 000000000..600c2970f
--- /dev/null
+++ b/src/plugins/charts/scatter/plugin.js
@@ -0,0 +1,127 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 { SCATTER_PLOT_KEY } from './scatterPlotConstants.js';
+import ScatterPlotViewProvider from './ScatterPlotViewProvider';
+import ScatterPlotInspectorViewProvider from './inspector/ScatterPlotInspectorViewProvider';
+import ScatterPlotCompositionPolicy from './ScatterPlotCompositionPolicy';
+import Vue from "vue";
+import ScatterPlotForm from "./ScatterPlotForm.vue";
+
+export default function () {
+ return function install(openmct) {
+ openmct.forms.addNewFormControl('scatter-plot-form-control', getScatterPlotFormControl(openmct));
+
+ openmct.types.addType(SCATTER_PLOT_KEY, {
+ key: SCATTER_PLOT_KEY,
+ name: "Scatter Plot",
+ cssClass: "icon-plot-scatter",
+ description: "View data as a scatter plot.",
+ creatable: true,
+ initialize: function (domainObject) {
+ domainObject.composition = [];
+ domainObject.configuration = {
+ styles: {},
+ axes: {},
+ ranges: {}
+ };
+ },
+ form: [
+ {
+ name: 'Underlay data (JSON file)',
+ key: 'selectFile',
+ control: 'file-input',
+ text: 'Select File...',
+ type: 'application/json',
+ removable: true,
+ hideFromInspector: true,
+ property: [
+ "selectFile"
+ ]
+ },
+ {
+ name: "Underlay ranges",
+ control: "scatter-plot-form-control",
+ cssClass: "l-input",
+ key: "scatterPlotForm",
+ required: false,
+ hideFromInspector: false,
+ property: [
+ "configuration",
+ "ranges"
+ ],
+ validate: ({ value }, callback) => {
+ const { rangeMin, rangeMax, domainMin, domainMax } = value;
+ const valid = {
+ rangeMin,
+ rangeMax,
+ domainMin,
+ domainMax
+ };
+
+ if (callback) {
+ callback(valid);
+ }
+
+ const values = Object.values(valid);
+ const hasAllValues = values.every(rangeValue => rangeValue !== undefined);
+ const hasNoValues = values.every(rangeValue => rangeValue === undefined);
+
+ return hasAllValues || hasNoValues;
+ }
+ }
+ ],
+ priority: 891
+ });
+
+ openmct.objectViews.addProvider(new ScatterPlotViewProvider(openmct));
+
+ openmct.inspectorViews.addProvider(new ScatterPlotInspectorViewProvider(openmct));
+
+ openmct.composition.addPolicy(new ScatterPlotCompositionPolicy(openmct).allow);
+ };
+
+ function getScatterPlotFormControl(openmct) {
+ return {
+ show(element, model, onChange) {
+ const rowComponent = new Vue({
+ el: element,
+ components: {
+ ScatterPlotForm
+ },
+ provide: {
+ openmct
+ },
+ data() {
+ return {
+ model,
+ onChange
+ };
+ },
+ template: `<scatter-plot-form :model="model" @onChange="onChange"></scatter-plot-form>`
+ });
+
+ return rowComponent;
+ }
+ };
+ }
+}
+
diff --git a/src/plugins/charts/scatter/pluginSpec.js b/src/plugins/charts/scatter/pluginSpec.js
new file mode 100644
index 000000000..2eb17c7a4
--- /dev/null
+++ b/src/plugins/charts/scatter/pluginSpec.js
@@ -0,0 +1,421 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 {createOpenMct, resetApplicationState} from "utils/testing";
+import Vue from "vue";
+import ScatterPlotPlugin from "./plugin";
+import ScatterPlot from './ScatterPlotView.vue';
+import EventEmitter from "EventEmitter";
+import { SCATTER_PLOT_VIEW, SCATTER_PLOT_KEY } from './scatterPlotConstants';
+
+describe("the plugin", function () {
+ let element;
+ let child;
+ let openmct;
+ let telemetryPromise;
+ let telemetryPromiseResolve;
+ let mockObjectPath;
+
+ beforeEach((done) => {
+ mockObjectPath = [
+ {
+ name: 'mock folder',
+ type: 'fake-folder',
+ identifier: {
+ key: 'mock-folder',
+ namespace: ''
+ }
+ }
+ ];
+ const testTelemetry = [
+ {
+ 'utc': 1,
+ 'some-key': 'some-value 1',
+ 'some-other-key': 'some-other-value 1'
+ },
+ {
+ 'utc': 2,
+ 'some-key': 'some-value 2',
+ 'some-other-key': 'some-other-value 2'
+ },
+ {
+ 'utc': 3,
+ 'some-key': 'some-value 3',
+ 'some-other-key': 'some-other-value 3'
+ }
+ ];
+
+ openmct = createOpenMct();
+
+ telemetryPromise = new Promise((resolve) => {
+ telemetryPromiseResolve = resolve;
+ });
+
+ spyOn(openmct.telemetry, 'request').and.callFake(() => {
+ telemetryPromiseResolve(testTelemetry);
+
+ return telemetryPromise;
+ });
+
+ openmct.install(new ScatterPlotPlugin());
+
+ element = document.createElement("div");
+ element.style.width = "640px";
+ element.style.height = "480px";
+ child = document.createElement("div");
+ child.style.width = "640px";
+ child.style.height = "480px";
+ element.appendChild(child);
+ document.body.appendChild(element);
+
+ spyOn(window, 'ResizeObserver').and.returnValue({
+ observe() {},
+ unobserve() {},
+ disconnect() {}
+ });
+
+ openmct.time.timeSystem("utc", {
+ start: 0,
+ end: 4
+ });
+
+ openmct.types.addType("test-object", {
+ creatable: true
+ });
+
+ openmct.on("start", done);
+ openmct.startHeadless();
+ });
+
+ afterEach((done) => {
+ openmct.time.timeSystem('utc', {
+ start: 0,
+ end: 1
+ });
+ resetApplicationState(openmct).then(done).catch(done);
+ });
+
+ describe("The scatter plot view", () => {
+ let testDomainObject;
+ let scatterPlotObject;
+ // eslint-disable-next-line no-unused-vars
+ let component;
+ let mockComposition;
+
+ beforeEach(async () => {
+ scatterPlotObject = {
+ identifier: {
+ namespace: "",
+ key: "test-plot"
+ },
+ type: "telemetry.plot.scatter-plot",
+ name: "Test Scatter Plot",
+ configuration: {
+ axes: {},
+ styles: {}
+ }
+ };
+
+ testDomainObject = {
+ identifier: {
+ namespace: "",
+ key: "test-object"
+ },
+ type: "test-object",
+ name: "Test Object",
+ telemetry: {
+ values: [{
+ key: "utc",
+ format: "utc",
+ name: "Time",
+ hints: {
+ domain: 1
+ }
+ }, {
+ key: "some-key",
+ name: "Some attribute",
+ hints: {
+ range: 1
+ }
+ }, {
+ key: "some-other-key",
+ name: "Another attribute",
+ hints: {
+ range: 2
+ }
+ }]
+ }
+ };
+
+ mockComposition = new EventEmitter();
+ mockComposition.load = () => {
+ mockComposition.emit('add', testDomainObject);
+
+ return [testDomainObject];
+ };
+
+ spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
+
+ let viewContainer = document.createElement("div");
+ child.append(viewContainer);
+ component = new Vue({
+ el: viewContainer,
+ components: {
+ ScatterPlot
+ },
+ provide: {
+ openmct: openmct,
+ domainObject: scatterPlotObject,
+ composition: openmct.composition.get(scatterPlotObject)
+ },
+ template: "<ScatterPlot></ScatterPlot>"
+ });
+
+ await Vue.nextTick();
+ });
+
+ it("provides a scatter plot view", () => {
+ const applicableViews = openmct.objectViews.get(scatterPlotObject, mockObjectPath);
+ const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === SCATTER_PLOT_VIEW);
+ expect(plotViewProvider).toBeDefined();
+ });
+
+ it("Renders plotly scatter plot", () => {
+ let scatterPlotElement = element.querySelectorAll(".plotly");
+ expect(scatterPlotElement.length).toBe(1);
+ });
+ });
+
+ describe("the scatter plot objects", () => {
+ const mockObject = {
+ name: 'A very nice scatter plot',
+ key: SCATTER_PLOT_KEY,
+ creatable: true
+ };
+
+ it('defines a scatter plot object type with the correct key', () => {
+ const objectDef = openmct.types.get(SCATTER_PLOT_KEY).definition;
+ expect(objectDef.key).toEqual(mockObject.key);
+ });
+
+ it('is creatable', () => {
+ const objectDef = openmct.types.get(SCATTER_PLOT_KEY).definition;
+ expect(objectDef.creatable).toEqual(mockObject.creatable);
+ });
+ });
+
+ describe("The scatter plot composition policy", () => {
+ it("allows composition for telemetry that contain at least 2 ranges", () => {
+ const parent = {
+ "composition": [],
+ "configuration": {
+ axes: {},
+ styles: {}
+ },
+ "name": "Some Scatter Plot",
+ "type": "telemetry.plot.scatter-plot",
+ "location": "mine",
+ "modified": 1631005183584,
+ "persisted": 1631005183502,
+ "identifier": {
+ "namespace": "",
+ "key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
+ }
+ };
+ const testTelemetryObject = {
+ identifier: {
+ namespace: "",
+ key: "test-object"
+ },
+ type: "test-object",
+ name: "Test Object",
+ telemetry: {
+ values: [{
+ key: "some-key",
+ name: "Some attribute",
+ hints: {
+ domain: 1
+ }
+ }, {
+ key: "some-other-key",
+ name: "Another attribute",
+ hints: {
+ range: 1
+ }
+ }, {
+ key: "some-other-key2",
+ name: "Another attribute2",
+ hints: {
+ range: 2
+ }
+ }]
+ }
+ };
+ const composition = openmct.composition.get(parent);
+ expect(() => {
+ composition.add(testTelemetryObject);
+ }).not.toThrow();
+ expect(parent.composition.length).toBe(1);
+ });
+
+ it("disallows composition for telemetry that don't contain at least 2 range hints", () => {
+ const parent = {
+ "composition": [],
+ "configuration": {
+ axes: {},
+ styles: {}
+ },
+ "name": "Some Scatter Plot",
+ "type": "telemetry.plot.scatter-plot",
+ "location": "mine",
+ "modified": 1631005183584,
+ "persisted": 1631005183502,
+ "identifier": {
+ "namespace": "",
+ "key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
+ }
+ };
+ const testTelemetryObject = {
+ identifier: {
+ namespace: "",
+ key: "test-object"
+ },
+ type: "test-object",
+ name: "Test Object",
+ telemetry: {
+ values: [{
+ key: "some-key",
+ name: "Some attribute",
+ hints: {
+ domain: 1
+ }
+ }, {
+ key: "some-other-key",
+ name: "Another attribute",
+ hints: {
+ range: 1
+ }
+ }]
+ }
+ };
+ const composition = openmct.composition.get(parent);
+ expect(() => {
+ composition.add(testTelemetryObject);
+ }).toThrow();
+ expect(parent.composition.length).toBe(0);
+ });
+ });
+ describe('the inspector view', () => {
+ let mockComposition;
+ let testDomainObject;
+ let selection;
+ let plotInspectorView;
+ let viewContainer;
+ let optionsElement;
+ beforeEach(async () => {
+ testDomainObject = {
+ identifier: {
+ namespace: "",
+ key: "test-object"
+ },
+ type: "test-object",
+ name: "Test Object",
+ telemetry: {
+ values: [{
+ key: "utc",
+ format: "utc",
+ name: "Time",
+ hints: {
+ domain: 1
+ }
+ }, {
+ key: "some-key",
+ name: "Some attribute",
+ hints: {
+ range: 1
+ }
+ }, {
+ key: "some-other-key",
+ name: "Another attribute",
+ hints: {
+ range: 2
+ }
+ }]
+ }
+ };
+
+ selection = [
+ [
+ {
+ context: {
+ item: {
+ id: "test-object",
+ identifier: {
+ key: "test-object",
+ namespace: ''
+ },
+ type: "telemetry.plot.scatter-plot",
+ configuration: {
+ axes: {},
+ styles: {
+ }
+ },
+ composition: [
+ {
+ key: '~Some~foo.scatter'
+ }
+ ]
+ }
+ }
+ }
+ ]
+ ];
+
+ mockComposition = new EventEmitter();
+ mockComposition.load = () => {
+ mockComposition.emit('add', testDomainObject);
+
+ return [testDomainObject];
+ };
+
+ spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
+
+ viewContainer = document.createElement('div');
+ child.append(viewContainer);
+
+ const applicableViews = openmct.inspectorViews.get(selection);
+ plotInspectorView = applicableViews[0];
+ plotInspectorView.show(viewContainer);
+
+ await Vue.nextTick();
+ optionsElement = element.querySelector('.c-scatter-plot-options');
+ });
+
+ afterEach(() => {
+ plotInspectorView.destroy();
+ });
+
+ it('it renders the options', () => {
+ expect(optionsElement).toBeDefined();
+ });
+ });
+});
diff --git a/src/plugins/charts/scatter/scatterPlotConstants.js b/src/plugins/charts/scatter/scatterPlotConstants.js
new file mode 100644
index 000000000..e458be37c
--- /dev/null
+++ b/src/plugins/charts/scatter/scatterPlotConstants.js
@@ -0,0 +1,4 @@
+export const SCATTER_PLOT_VIEW = 'scatter-plot.view';
+export const SCATTER_PLOT_KEY = 'telemetry.plot.scatter-plot';
+export const SCATTER_PLOT_INSPECTOR_KEY = 'telemetry.plot.scatter-plot.inspector';
+export const TIME_STRIP_KEY = 'time-strip';
diff --git a/src/plugins/clock/plugin.js b/src/plugins/clock/plugin.js
index 72b8dadde..0965985a7 100644
--- a/src/plugins/clock/plugin.js
+++ b/src/plugins/clock/plugin.js
@@ -32,7 +32,7 @@ export default function ClockPlugin(options) {
const CLOCK_INDICATOR_FORMAT = 'YYYY/MM/DD HH:mm:ss';
openmct.types.addType('clock', {
name: 'Clock',
- description: 'A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts.',
+ description: 'A digital clock that uses system time and supports a variety of display formats and timezones.',
creatable: true,
cssClass: 'icon-clock',
initialize: function (domainObject) {
@@ -89,6 +89,7 @@ export default function ClockPlugin(options) {
"key": "timezone",
"name": "Timezone",
"control": "autocomplete",
+ "cssClass": "c-clock__timezone-selection c-menu--no-icon",
"options": momentTimezone.tz.names(),
property: [
'configuration',
diff --git a/src/plugins/condition/Condition.js b/src/plugins/condition/Condition.js
index d3709d967..9896e9746 100644
--- a/src/plugins/condition/Condition.js
+++ b/src/plugins/condition/Condition.js
@@ -21,7 +21,7 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
-import uuid from 'uuid';
+import { v4 as uuid } from 'uuid';
import TelemetryCriterion from "./criterion/TelemetryCriterion";
import { evaluateResults } from './utils/evaluator';
import { getLatestTimestamp } from './utils/time';
diff --git a/src/plugins/condition/ConditionManager.js b/src/plugins/condition/ConditionManager.js
index c0393351a..d04ef8d3c 100644
--- a/src/plugins/condition/ConditionManager.js
+++ b/src/plugins/condition/ConditionManager.js
@@ -22,7 +22,7 @@
import Condition from "./Condition";
import { getLatestTimestamp } from './utils/time';
-import uuid from "uuid";
+import { v4 as uuid } from 'uuid';
import EventEmitter from 'EventEmitter';
export default class ConditionManager extends EventEmitter {
@@ -300,8 +300,11 @@ export default class ConditionManager extends EventEmitter {
return this.compositionLoad.then(() => {
let latestTimestamp;
let conditionResults = {};
+ let nextLegOptions = {...options};
+ delete nextLegOptions.onPartialResponse;
+
const conditionRequests = this.conditions
- .map(condition => condition.requestLADConditionResult(options));
+ .map(condition => condition.requestLADConditionResult(nextLegOptions));
return Promise.all(conditionRequests)
.then((results) => {
diff --git a/src/plugins/condition/StyleRuleManager.js b/src/plugins/condition/StyleRuleManager.js
index e7f201ca7..18063b337 100644
--- a/src/plugins/condition/StyleRuleManager.js
+++ b/src/plugins/condition/StyleRuleManager.js
@@ -78,11 +78,13 @@ export default class StyleRuleManager extends EventEmitter {
this.openmct.objects.get(this.conditionSetIdentifier).then((conditionSetDomainObject) => {
this.openmct.telemetry.request(conditionSetDomainObject)
.then(output => {
- if (output && output.length) {
+ if (output && output.length && (this.conditionSetIdentifier && this.openmct.objects.areIdsEqual(conditionSetDomainObject.identifier, this.conditionSetIdentifier))) {
this.handleConditionSetResultUpdated(output[0]);
}
});
- this.stopProvidingTelemetry = this.openmct.telemetry.subscribe(conditionSetDomainObject, this.handleConditionSetResultUpdated.bind(this));
+ if (this.conditionSetIdentifier && this.openmct.objects.areIdsEqual(conditionSetDomainObject.identifier, this.conditionSetIdentifier)) {
+ this.stopProvidingTelemetry = this.openmct.telemetry.subscribe(conditionSetDomainObject, this.handleConditionSetResultUpdated.bind(this));
+ }
});
}
diff --git a/src/plugins/condition/components/Condition.vue b/src/plugins/condition/components/Condition.vue
index 791b37117..7201b6b31 100644
--- a/src/plugins/condition/components/Condition.vue
+++ b/src/plugins/condition/components/Condition.vue
@@ -214,7 +214,7 @@
import Criterion from './Criterion.vue';
import ConditionDescription from "./ConditionDescription.vue";
import { TRIGGER, TRIGGER_LABEL } from "@/plugins/condition/utils/constants";
-import uuid from 'uuid';
+import { v4 as uuid } from 'uuid';
export default {
components: {
diff --git a/src/plugins/condition/criterion/TelemetryCriterion.js b/src/plugins/condition/criterion/TelemetryCriterion.js
index e343b9d59..75e91f7dd 100644
--- a/src/plugins/condition/criterion/TelemetryCriterion.js
+++ b/src/plugins/condition/criterion/TelemetryCriterion.js
@@ -51,7 +51,11 @@ export default class TelemetryCriterion extends EventEmitter {
}
initialize() {
- this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
+ this.telemetryObjectIdAsString = "";
+ if (![undefined, null, ""].includes(this.telemetryDomainObjectDefinition?.telemetry)) {
+ this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
+ }
+
this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects);
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData();
diff --git a/src/plugins/condition/plugin.js b/src/plugins/condition/plugin.js
index 318cc4c5a..df56d35df 100644
--- a/src/plugins/condition/plugin.js
+++ b/src/plugins/condition/plugin.js
@@ -23,7 +23,7 @@ import ConditionSetViewProvider from './ConditionSetViewProvider.js';
import ConditionSetCompositionPolicy from "./ConditionSetCompositionPolicy";
import ConditionSetMetadataProvider from './ConditionSetMetadataProvider';
import ConditionSetTelemetryProvider from './ConditionSetTelemetryProvider';
-import uuid from "uuid";
+import { v4 as uuid } from 'uuid';
export default function ConditionPlugin() {
diff --git a/src/plugins/conditionWidget/components/ConditionWidget.vue b/src/plugins/conditionWidget/components/ConditionWidget.vue
index 80220dd75..ef865b1c6 100644
--- a/src/plugins/conditionWidget/components/ConditionWidget.vue
+++ b/src/plugins/conditionWidget/components/ConditionWidget.vue
@@ -136,8 +136,8 @@ export default {
this.url = url;
}
- const conditionSetIdentifier = domainObject.configuration.objectStyles.conditionSetIdentifier;
- if (this.conditionSetIdentifier !== conditionSetIdentifier) {
+ const conditionSetIdentifier = domainObject.configuration?.objectStyles?.conditionSetIdentifier;
+ if (conditionSetIdentifier && this.conditionSetIdentifier !== conditionSetIdentifier) {
this.conditionSetIdentifier = conditionSetIdentifier;
}
diff --git a/src/plugins/displayLayout/DisplayLayoutToolbar.js b/src/plugins/displayLayout/DisplayLayoutToolbar.js
index 24038b855..9bcd7facf 100644
--- a/src/plugins/displayLayout/DisplayLayoutToolbar.js
+++ b/src/plugins/displayLayout/DisplayLayoutToolbar.js
@@ -93,7 +93,7 @@ define(['lodash'], function (_) {
'table': {
value: 'table',
name: 'Table',
- class: 'icon-tabular-realtime'
+ class: 'icon-tabular-scrolling'
}
};
const APPLICABLE_VIEWS = {
@@ -211,13 +211,15 @@ define(['lodash'], function (_) {
options: [
{
value: false,
- icon: 'icon-frame-show',
- title: "Frame visible"
+ icon: 'icon-frame-hide',
+ title: "Frame visible",
+ label: 'Hide frame'
},
{
value: true,
- icon: 'icon-frame-hide',
- title: "Frame hidden"
+ icon: 'icon-frame-show',
+ title: "Frame hidden",
+ label: 'Show frame'
}
]
};
@@ -401,6 +403,7 @@ define(['lodash'], function (_) {
},
icon: "icon-pencil",
title: "Edit text properties",
+ label: "Edit text",
dialog: DIALOG_FORM.text
};
}
@@ -514,12 +517,14 @@ define(['lodash'], function (_) {
{
value: true,
icon: 'icon-eye-open',
- title: "Show units"
+ title: "Show units",
+ label: "Show units"
},
{
value: false,
icon: 'icon-eye-disabled',
- title: "Hide units"
+ title: "Hide units",
+ label: "Hide units"
}
]
};
@@ -562,6 +567,7 @@ define(['lodash'], function (_) {
domainObject: selectedParent,
icon: "icon-object",
title: "Switch the way this telemetry is displayed",
+ label: "View type",
options: viewOptions,
method: function (option) {
displayLayoutContext.switchViewType(selectedItemContext, option.value, selection);
@@ -662,9 +668,9 @@ define(['lodash'], function (_) {
'display-mode': [],
'telemetry-value': [],
'style': [],
+ 'unit-toggle': [],
'position': [],
'duplicate': [],
- 'unit-toggle': [],
'remove': [],
'toggle-grid': []
};
@@ -689,6 +695,7 @@ define(['lodash'], function (_) {
if (toolbar.position.length === 0) {
toolbar.position = [
getStackOrder(selectedParent, selectionPath),
+ getSeparator(),
getXInput(selectedParent, selectedObjects),
getYInput(selectedParent, selectedObjects),
getHeightInput(selectedParent, selectedObjects),
@@ -712,9 +719,17 @@ define(['lodash'], function (_) {
toolbar['telemetry-value'] = [getTelemetryValueMenu(selectionPath, selectedObjects)];
}
+ if (toolbar['unit-toggle'].length === 0) {
+ let toggleUnitsButton = getToggleUnitsButton(selectedParent, selectedObjects);
+ if (toggleUnitsButton) {
+ toolbar['unit-toggle'] = [toggleUnitsButton];
+ }
+ }
+
if (toolbar.position.length === 0) {
toolbar.position = [
getStackOrder(selectedParent, selectionPath),
+ getSeparator(),
getXInput(selectedParent, selectedObjects),
getYInput(selectedParent, selectedObjects),
getHeightInput(selectedParent, selectedObjects),
@@ -729,17 +744,11 @@ define(['lodash'], function (_) {
if (toolbar.viewSwitcher.length === 0) {
toolbar.viewSwitcher = [getViewSwitcherMenu(selectedParent, selectionPath, selectedObjects)];
}
-
- if (toolbar['unit-toggle'].length === 0) {
- let toggleUnitsButton = getToggleUnitsButton(selectedParent, selectedObjects);
- if (toggleUnitsButton) {
- toolbar['unit-toggle'] = [toggleUnitsButton];
- }
- }
} else if (layoutItem.type === 'text-view') {
if (toolbar.position.length === 0) {
toolbar.position = [
getStackOrder(selectedParent, selectionPath),
+ getSeparator(),
getXInput(selectedParent, selectedObjects),
getYInput(selectedParent, selectedObjects),
getHeightInput(selectedParent, selectedObjects),
@@ -758,6 +767,7 @@ define(['lodash'], function (_) {
if (toolbar.position.length === 0) {
toolbar.position = [
getStackOrder(selectedParent, selectionPath),
+ getSeparator(),
getXInput(selectedParent, selectedObjects),
getYInput(selectedParent, selectedObjects),
getHeightInput(selectedParent, selectedObjects),
@@ -772,6 +782,7 @@ define(['lodash'], function (_) {
if (toolbar.position.length === 0) {
toolbar.position = [
getStackOrder(selectedParent, selectionPath),
+ getSeparator(),
getXInput(selectedParent, selectedObjects),
getYInput(selectedParent, selectedObjects),
getHeightInput(selectedParent, selectedObjects),
@@ -786,6 +797,7 @@ define(['lodash'], function (_) {
if (toolbar.position.length === 0) {
toolbar.position = [
getStackOrder(selectedParent, selectionPath),
+ getSeparator(),
getXInput(selectedParent, selectedObjects),
getYInput(selectedParent, selectedObjects),
getX2Input(selectedParent, selectedObjects),
diff --git a/src/plugins/displayLayout/DrawingObjectTypes.js b/src/plugins/displayLayout/DrawingObjectTypes.js
new file mode 100644
index 000000000..9b34e808c
--- /dev/null
+++ b/src/plugins/displayLayout/DrawingObjectTypes.js
@@ -0,0 +1,34 @@
+const displayLayoutDrawingObjectTypes = {
+ 'box-view': {
+ name: "Box",
+ creatable: false,
+ description: 'A rectangle shape.',
+ cssClass: 'icon-box-round-corners'
+ },
+ 'ellipse-view': {
+ name: "Ellipse",
+ creatable: false,
+ description: 'A ellipse shape.',
+ cssClass: 'icon-circle'
+ },
+ 'line-view': {
+ name: "Line",
+ creatable: false,
+ description: 'A line.',
+ cssClass: 'icon-line-horz'
+ },
+ 'text-view': {
+ name: "Text",
+ creatable: false,
+ description: 'An editable text box.',
+ cssClass: 'icon-font'
+ },
+ 'image-view': {
+ name: "Image",
+ creatable: false,
+ description: 'An image.',
+ cssClass: 'icon-image'
+ }
+};
+
+export default displayLayoutDrawingObjectTypes;
diff --git a/src/plugins/displayLayout/components/DisplayLayout.vue b/src/plugins/displayLayout/components/DisplayLayout.vue
index d260af3dc..98afcba65 100644
--- a/src/plugins/displayLayout/components/DisplayLayout.vue
+++ b/src/plugins/displayLayout/components/DisplayLayout.vue
@@ -73,7 +73,7 @@
</template>
<script>
-import uuid from 'uuid';
+import { v4 as uuid } from 'uuid';
import SubobjectView from './SubobjectView.vue';
import TelemetryView from './TelemetryView.vue';
import BoxView from './BoxView.vue';
@@ -517,7 +517,19 @@ export default {
initializeItems() {
this.telemetryViewMap = {};
this.objectViewMap = {};
- this.layoutItems.forEach(this.trackItem);
+
+ let removedItems = [];
+ this.layoutItems.forEach((item) => {
+ if (item.identifier) {
+ if (this.containsObject(item.identifier)) {
+ this.trackItem(item);
+ } else {
+ removedItems.push(this.openmct.objects.makeKeyString(item.identifier));
+ }
+ }
+ });
+
+ removedItems.forEach(this.removeFromConfiguration);
},
isItemAlreadyTracked(child) {
let found = false;
diff --git a/src/plugins/displayLayout/components/LayoutFrame.vue b/src/plugins/displayLayout/components/LayoutFrame.vue
index 052fa63a3..c81fb80f7 100644
--- a/src/plugins/displayLayout/components/LayoutFrame.vue
+++ b/src/plugins/displayLayout/components/LayoutFrame.vue
@@ -25,8 +25,7 @@
class="l-layout__frame c-frame"
:class="{
'no-frame': !item.hasFrame,
- 'u-inspectable': inspectable,
- 'is-in-small-container': size.width < 600 || size.height < 600
+ 'u-inspectable': inspectable
}"
:style="style"
>
diff --git a/src/plugins/displayLayout/components/SubobjectView.vue b/src/plugins/displayLayout/components/SubobjectView.vue
index 8e58f89dd..0e0a0ea65 100644
--- a/src/plugins/displayLayout/components/SubobjectView.vue
+++ b/src/plugins/displayLayout/components/SubobjectView.vue
@@ -147,7 +147,7 @@ export default {
this.mutablePromise.then(() => {
this.openmct.objects.destroyMutable(this.domainObject);
});
- } else if (this.domainObject.isMutable) {
+ } else if (this?.domainObject?.isMutable) {
this.openmct.objects.destroyMutable(this.domainObject);
}
},
diff --git a/src/plugins/displayLayout/components/TelemetryView.vue b/src/plugins/displayLayout/components/TelemetryView.vue
index 3a759db9d..8ae30d913 100644
--- a/src/plugins/displayLayout/components/TelemetryView.vue
+++ b/src/plugins/displayLayout/components/TelemetryView.vue
@@ -91,7 +91,7 @@ export default {
width: DEFAULT_TELEMETRY_DIMENSIONS[0],
height: DEFAULT_TELEMETRY_DIMENSIONS[1],
displayMode: 'all',
- value: metadata.getDefaultDisplayValue(),
+ value: metadata.getDefaultDisplayValue()?.key,
stroke: "",
fill: "",
color: "",
@@ -152,7 +152,7 @@ export default {
},
unit() {
let value = this.item.value;
- let unit = this.metadata.value(value).unit;
+ let unit = this.metadata ? this.metadata.value(value).unit : '';
return unit;
},
@@ -232,16 +232,18 @@ export default {
this.removeSelectable();
}
- this.telemetryCollection.off('add', this.setLatestValues);
- this.telemetryCollection.off('clear', this.refreshData);
+ if (this.telemetryCollection) {
+ this.telemetryCollection.off('add', this.setLatestValues);
+ this.telemetryCollection.off('clear', this.refreshData);
- this.telemetryCollection.destroy();
+ this.telemetryCollection.destroy();
+ }
if (this.mutablePromise) {
this.mutablePromise.then(() => {
this.openmct.objects.destroyMutable(this.domainObject);
});
- } else if (this.domainObject.isMutable) {
+ } else if (this?.domainObject?.isMutable) {
this.openmct.objects.destroyMutable(this.domainObject);
}
},
@@ -280,7 +282,7 @@ export default {
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
- const valueMetadata = this.metadata.value(this.item.value);
+ const valueMetadata = this.metadata ? this.metadata.value(this.item.value) : {};
this.customStringformatter = this.openmct.telemetry.customStringFormatter(valueMetadata, this.item.format);
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
diff --git a/src/plugins/displayLayout/components/layout-frame.scss b/src/plugins/displayLayout/components/layout-frame.scss
index 63f2299ec..a80f587fc 100644
--- a/src/plugins/displayLayout/components/layout-frame.scss
+++ b/src/plugins/displayLayout/components/layout-frame.scss
@@ -9,10 +9,6 @@
> *:first-child {
flex: 1 1 auto;
}
-
- &.is-in-small-container {
- //background: rgba(blue, 0.1);
- }
}
.c-frame__move-bar {
@@ -32,7 +28,6 @@
&[s-selected] {
// All frames selected while editing
- border: $editFrameSelectedBorder;
box-shadow: $editFrameSelectedShdw;
.c-frame__move-bar {
@@ -79,14 +74,15 @@
transition-delay: $moveBarOutDelay;
@include userSelectNone();
background: $editFrameMovebarColorBg;
- box-shadow: rgba(black, 0.2) 0 1px;
+ box-shadow: rgba(black, 0.3) 0 2px;
bottom: auto;
display: block;
height: 0; // Height is set on hover below
- opacity: 0.8;
+ opacity: 0.9;
max-height: 100%;
overflow: hidden;
text-align: center;
+ z-index: 10;
&:before {
// Grippy
@@ -109,7 +105,6 @@
> .c-so-view.has-complex-content {
transition: $transIn;
transition-delay: 0s;
- padding-top: $editFrameMovebarH + $interiorMarginSm;
> .c-so-view__local-controls {
transform: translateY($editFrameMovebarH);
diff --git a/src/plugins/displayLayout/components/telemetry-view.scss b/src/plugins/displayLayout/components/telemetry-view.scss
index 0dbfc75ac..b1dc92c1e 100644
--- a/src/plugins/displayLayout/components/telemetry-view.scss
+++ b/src/plugins/displayLayout/components/telemetry-view.scss
@@ -4,7 +4,7 @@
> * {
// Label and value holders
- flex: 1 1 auto;
+ flex: 1 1 50%;
display: flex;
flex-direction: row;
align-items: center;
@@ -17,14 +17,14 @@
}
}
- > * + * {
- margin-left: $interiorMargin;
- }
-
&__value {
@include isLimit();
}
+ &__label {
+ margin-right: $interiorMargin;
+ }
+
.c-frame & {
@include abs();
border: 1px solid transparent;
diff --git a/src/plugins/displayLayout/plugin.js b/src/plugins/displayLayout/plugin.js
index 1d949e32c..ad99fc47b 100644
--- a/src/plugins/displayLayout/plugin.js
+++ b/src/plugins/displayLayout/plugin.js
@@ -25,6 +25,7 @@ import CopyToClipboardAction from './actions/CopyToClipboardAction';
import DisplayLayout from './components/DisplayLayout.vue';
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
import DisplayLayoutType from './DisplayLayoutType.js';
+import DisplayLayoutDrawingObjectTypes from './DrawingObjectTypes.js';
import objectUtils from 'objectUtils';
@@ -125,6 +126,11 @@ export default function DisplayLayoutPlugin(options) {
return true;
}
});
+
+ for (const [type, definition] of Object.entries(DisplayLayoutDrawingObjectTypes)) {
+ openmct.types.addType(type, definition);
+ }
+
DisplayLayoutPlugin._installed = true;
};
}
diff --git a/src/plugins/displayLayout/pluginSpec.js b/src/plugins/displayLayout/pluginSpec.js
index 643f13be6..e70e754b6 100644
--- a/src/plugins/displayLayout/pluginSpec.js
+++ b/src/plugins/displayLayout/pluginSpec.js
@@ -21,6 +21,7 @@
*****************************************************************************/
import { createOpenMct, resetApplicationState } from 'utils/testing';
+import Vue from 'vue';
import DisplayLayoutPlugin from './plugin';
describe('the plugin', function () {
@@ -41,7 +42,7 @@ describe('the plugin', function () {
element.appendChild(child);
openmct.on('start', done);
- openmct.startHeadless();
+ openmct.start(child);
});
afterEach(() => {
@@ -88,6 +89,88 @@ describe('the plugin', function () {
expect(displayLayoutViewProvider).toBeDefined();
});
+ it('renders a display layout view without errors', () => {
+ const testViewObject = {
+ identifier: {
+ namespace: 'test-namespace',
+ key: 'test-key'
+ },
+ type: 'layout',
+ configuration: {
+ items: [],
+ layoutGrid: [10, 10]
+ },
+ composition: []
+ };
+
+ const applicableViews = openmct.objectViews.get(testViewObject, []);
+ let displayLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'layout.view');
+ let view = displayLayoutViewProvider.view(testViewObject);
+ let error;
+
+ try {
+ view.show(child, false);
+ } catch (e) {
+ error = e;
+ }
+
+ expect(error).toBeUndefined();
+
+ });
+
+ describe('on load', () => {
+ let displayLayoutItem;
+ let item;
+
+ beforeEach((done) => {
+ item = {
+ 'width': 32,
+ 'height': 18,
+ 'x': 78,
+ 'y': 8,
+ 'identifier': {
+ 'namespace': '',
+ 'key': 'bdeb91ab-3a7e-4a71-9dd2-39d73644e136'
+ },
+ 'hasFrame': true,
+ 'type': 'line-view', // so no telemetry functionality is triggered, just want to test the sync
+ 'id': 'c0ff485a-344c-4e70-8d83-a9d9998a69fc'
+
+ };
+ displayLayoutItem = {
+ 'composition': [
+ // no item in compostion, but item in configuration items
+ ],
+ 'configuration': {
+ 'items': [
+ item
+ ],
+ 'layoutGrid': [
+ 10,
+ 10
+ ]
+ },
+ 'name': 'Display Layout',
+ 'type': 'layout',
+ 'identifier': {
+ 'namespace': '',
+ 'key': 'c5e636c1-6771-4c9c-b933-8665cab189b3'
+ }
+ };
+
+ const applicableViews = openmct.objectViews.get(displayLayoutItem, []);
+ const displayLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'layout.view');
+ const view = displayLayoutViewProvider.view(displayLayoutItem);
+ view.show(child, false);
+
+ Vue.nextTick(done);
+ });
+
+ it('will sync compostion and layout items', () => {
+ expect(displayLayoutItem.configuration.items.length).toBe(0);
+ });
+ });
+
describe('the alpha numeric format view', () => {
let displayLayoutItem;
let telemetryItem;
@@ -351,7 +434,7 @@ describe('the plugin', function () {
it('provides controls including separators', () => {
const displayLayoutToolbar = openmct.toolbars.get(selection);
- expect(displayLayoutToolbar.length).toBe(7);
+ expect(displayLayoutToolbar.length).toBe(8);
});
});
});
diff --git a/src/plugins/duplicate/DuplicateTask.js b/src/plugins/duplicate/DuplicateTask.js
index f06f26dc1..4c9ca0a10 100644
--- a/src/plugins/duplicate/DuplicateTask.js
+++ b/src/plugins/duplicate/DuplicateTask.js
@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
-import uuid from 'uuid';
+import { v4 as uuid } from 'uuid';
/**
* This class encapsulates the process of duplicating/copying a domain object
diff --git a/src/plugins/exportAsJSONAction/ExportAsJSONAction.js b/src/plugins/exportAsJSONAction/ExportAsJSONAction.js
index ba2f665da..506c34745 100644
--- a/src/plugins/exportAsJSONAction/ExportAsJSONAction.js
+++ b/src/plugins/exportAsJSONAction/ExportAsJSONAction.js
@@ -22,7 +22,7 @@
import JSONExporter from '/src/exporters/JSONExporter.js';
import _ from 'lodash';
-import uuid from "uuid";
+import { v4 as uuid } from 'uuid';
export default class ExportAsJSONAction {
constructor(openmct) {
@@ -128,6 +128,30 @@ export default class ExportAsJSONAction {
return copyOfChild;
}
+
+ /**
+ * @private
+ * @param {object} child
+ * @param {object} parent
+ * @returns {object}
+ */
+ _rewriteLinkForReference(child, parent) {
+ const childId = this._getId(child);
+ this.externalIdentifiers.push(childId);
+ const copyOfChild = JSON.parse(JSON.stringify(child));
+
+ copyOfChild.identifier.key = uuid();
+ const newIdString = this._getId(copyOfChild);
+ const parentId = this._getId(parent);
+
+ this.idMap[childId] = newIdString;
+ copyOfChild.location = null;
+ parent.configuration.objectStyles.conditionSetIdentifier = copyOfChild.identifier;
+ this.tree[newIdString] = copyOfChild;
+ this.tree[parentId].configuration.objectStyles.conditionSetIdentifier = copyOfChild.identifier;
+
+ return copyOfChild;
+ }
/**
* @private
*/
@@ -159,23 +183,27 @@ export default class ExportAsJSONAction {
"rootId": this._getId(this.root)
};
}
+
/**
* @private
* @param {object} parent
*/
_write(parent) {
this.calls++;
+ //conditional object styles are not saved on the composition, so we need to check for them
+ let childObjectReferenceId = parent.configuration?.objectStyles?.conditionSetIdentifier;
+
const composition = this.openmct.composition.get(parent);
if (composition !== undefined) {
composition.load()
.then((children) => {
children.forEach((child, index) => {
- // Only export if object is creatable
+ // Only export if object is creatable
if (this._isCreatableAndPersistable(child)) {
- // Prevents infinite export of self-contained objs
+ // Prevents infinite export of self-contained objs
if (!Object.prototype.hasOwnProperty.call(this.tree, this._getId(child))) {
- // If object is a link to something absent from
- // tree, generate new id and treat as new object
+ // If object is a link to something absent from
+ // tree, generate new id and treat as new object
if (this._isLinkedObject(child, parent)) {
child = this._rewriteLink(child, parent);
} else {
@@ -186,18 +214,41 @@ export default class ExportAsJSONAction {
}
}
});
- this.calls--;
- if (this.calls === 0) {
- this._rewriteReferences();
- this._saveAs(this._wrapTree());
+ this._decrementCallsAndSave();
+ });
+ } else if (!childObjectReferenceId) {
+ this._decrementCallsAndSave();
+ }
+
+ if (childObjectReferenceId) {
+ this.openmct.objects.get(childObjectReferenceId)
+ .then((child) => {
+ // Only export if object is creatable
+ if (this._isCreatableAndPersistable(child)) {
+ // Prevents infinite export of self-contained objs
+ if (!Object.prototype.hasOwnProperty.call(this.tree, this._getId(child))) {
+ // If object is a link to something absent from
+ // tree, generate new id and treat as new object
+ if (this._isLinkedObject(child, parent)) {
+ child = this._rewriteLinkForReference(child, parent);
+ } else {
+ this.tree[this._getId(child)] = child;
+ }
+
+ this._write(child);
+ }
}
+
+ this._decrementCallsAndSave();
});
- } else {
- this.calls--;
- if (this.calls === 0) {
- this._rewriteReferences();
- this._saveAs(this._wrapTree());
- }
+ }
+ }
+
+ _decrementCallsAndSave() {
+ this.calls--;
+ if (this.calls === 0) {
+ this._rewriteReferences();
+ this._saveAs(this._wrapTree());
}
}
}
diff --git a/src/plugins/exportAsJSONAction/ExportAsJSONActionSpec.js b/src/plugins/exportAsJSONAction/ExportAsJSONActionSpec.js
index b55feb25a..7ecb3b963 100644
--- a/src/plugins/exportAsJSONAction/ExportAsJSONActionSpec.js
+++ b/src/plugins/exportAsJSONAction/ExportAsJSONActionSpec.js
@@ -322,4 +322,57 @@ describe('Export as JSON plugin', () => {
exportAsJSONAction.invoke([parent]);
});
+
+ it('ExportAsJSONAction exports object references from tree', (done) => {
+ const parent = {
+ composition: [],
+ configuration: {
+ objectStyles: {
+ conditionSetIdentifier: {
+ key: 'child',
+ namespace: ''
+ }
+ }
+ },
+ identifier: {
+ key: 'parent',
+ namespace: ''
+ },
+ name: 'Parent',
+ type: 'folder',
+ modified: 1503598129176,
+ location: 'mine',
+ persisted: 1503598129176
+ };
+
+ const child = {
+ composition: [],
+ identifier: {
+ key: 'child',
+ namespace: ''
+ },
+ name: 'Child',
+ type: 'folder',
+ modified: 1503598132428,
+ location: null,
+ persisted: 1503598132428
+ };
+
+ spyOn(openmct.objects, 'get').and.callFake(object => {
+ return Promise.resolve(child);
+ });
+
+ spyOn(exportAsJSONAction, '_saveAs').and.callFake(completedTree => {
+ expect(Object.keys(completedTree).length).toBe(2);
+ const conditionSetId = Object.keys(completedTree.openmct)[1];
+ expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy();
+ expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy();
+ expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'parent')).toBeTruthy();
+ expect(completedTree.openmct[conditionSetId].name).toBe('Child');
+
+ done();
+ });
+
+ exportAsJSONAction.invoke([parent]);
+ });
});
diff --git a/src/plugins/faultManagement/FaultManagementInspector.vue b/src/plugins/faultManagement/FaultManagementInspector.vue
new file mode 100644
index 000000000..7059a4ff7
--- /dev/null
+++ b/src/plugins/faultManagement/FaultManagementInspector.vue
@@ -0,0 +1,129 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+<template>
+<div
+ v-if="isShowDetails"
+ class="c-inspector__properties c-inspect-properties"
+>
+ <div class="c-inspect-properties__header">Fault Details</div>
+ <ul
+ class="c-inspect-properties__section"
+ >
+ <DetailText :detail="sourceDetails" />
+ <DetailText :detail="occuredDetails" />
+ <DetailText :detail="criticalityDetails" />
+ <DetailText :detail="descriptionDetails" />
+ </ul>
+
+ <div class="c-inspect-properties__header">Telemetry</div>
+ <ul
+ class="c-inspect-properties__section"
+ >
+ <DetailText :detail="systemDetails" />
+ <DetailText :detail="tripValueDetails" />
+ <DetailText :detail="currentValueDetails" />
+ </ul>
+</div>
+</template>
+
+<script>
+import DetailText from '@/ui/inspector/details/DetailText.vue';
+
+export default {
+ name: 'FaultManagementInspector',
+ components: {
+ DetailText
+ },
+ inject: ['openmct'],
+ data() {
+ return {
+ isShowDetails: false
+ };
+ },
+ computed: {
+ criticalityDetails() {
+ return {
+ name: 'Criticality',
+ value: this.selectedFault?.severity
+ };
+ },
+ currentValueDetails() {
+ return {
+ name: 'Live value',
+ value: this.selectedFault?.currentValueInfo?.value
+ };
+ },
+ descriptionDetails() {
+ return {
+ name: 'Description',
+ value: this.selectedFault?.shortDescription
+ };
+ },
+ occuredDetails() {
+ return {
+ name: 'Occured',
+ value: this.selectedFault?.triggerTime
+ };
+ },
+ sourceDetails() {
+ return {
+ name: 'Source',
+ value: this.selectedFault?.name
+ };
+ },
+ systemDetails() {
+ return {
+ name: 'System',
+ value: this.selectedFault?.namespace
+ };
+ },
+ tripValueDetails() {
+ return {
+ name: 'Trip Value',
+ value: this.selectedFault?.triggerValueInfo?.value
+ };
+ }
+ },
+ mounted() {
+ this.updateSelectedFaults();
+ },
+ methods: {
+ updateSelectedFaults() {
+ const selection = this.openmct.selection.get();
+ this.isShowDetails = false;
+
+ if (selection.length === 0 || selection[0].length < 2) {
+ return;
+ }
+
+ const selectedFaults = selection[0][1].context.selectedFaults;
+ if (selectedFaults.length !== 1) {
+ return;
+ }
+
+ this.isShowDetails = true;
+ this.selectedFault = selectedFaults[0];
+ }
+ }
+};
+</script>
diff --git a/src/plugins/faultManagement/FaultManagementInspectorViewProvider.js b/src/plugins/faultManagement/FaultManagementInspectorViewProvider.js
new file mode 100644
index 000000000..b4500496f
--- /dev/null
+++ b/src/plugins/faultManagement/FaultManagementInspectorViewProvider.js
@@ -0,0 +1,71 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 FaultManagementInspector from './FaultManagementInspector.vue';
+
+import Vue from 'vue';
+
+import { FAULT_MANAGEMENT_INSPECTOR, FAULT_MANAGEMENT_TYPE } from './constants';
+
+export default function FaultManagementInspectorViewProvider(openmct) {
+ return {
+ openmct: openmct,
+ key: FAULT_MANAGEMENT_INSPECTOR,
+ name: 'FAULT_MANAGEMENT_TYPE',
+ canView: (selection) => {
+ if (selection.length !== 1 || selection[0].length === 0) {
+ return false;
+ }
+
+ let object = selection[0][0].context.item;
+
+ return object && object.type === FAULT_MANAGEMENT_TYPE;
+ },
+ view: (selection) => {
+ let component;
+
+ return {
+ show: function (element) {
+ component = new Vue({
+ el: element,
+ components: {
+ FaultManagementInspector
+ },
+ provide: {
+ openmct
+ },
+ template: '<FaultManagementInspector></FaultManagementInspector>'
+ });
+ },
+ destroy: function () {
+ if (component) {
+ component.$destroy();
+ component = undefined;
+ }
+ }
+ };
+ },
+ priority: () => {
+ return 1;
+ }
+ };
+}
diff --git a/src/plugins/faultManagement/FaultManagementListHeader.vue b/src/plugins/faultManagement/FaultManagementListHeader.vue
new file mode 100644
index 000000000..11c3bd628
--- /dev/null
+++ b/src/plugins/faultManagement/FaultManagementListHeader.vue
@@ -0,0 +1,105 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+<template>
+<div class="c-fault-mgmt-item-header c-fault-mgmt__list-header c-fault-mgmt__list">
+ <div class="c-fault-mgmt-item-header c-fault-mgmt__checkbox">
+ <input
+ type="checkbox"
+ :checked="isSelectAll"
+ @input="selectAll"
+ >
+ </div>
+ <div class="c-fault-mgmt-item-header c-fault-mgmt__list-header-results c-fault-mgmt__list-severity">
+ {{ totalFaultsCount }} Results
+ </div>
+ <div class="c-fault-mgmt__list-header-content">
+ <div class="c-fault-mgmt__list-content-right">
+ <div class="c-fault-mgmt-item-header c-fault-mgmt__list-header-tripVal">Trip Value</div>
+ <div class="c-fault-mgmt-item-header c-fault-mgmt__list-header-liveVal">Live Value</div>
+ <div class="c-fault-mgmt-item-header c-fault-mgmt__list-header-trigTime">Trigger Time</div>
+ </div>
+ </div>
+ <div class=" c-fault-mgmt-item-header c-fault-mgmt__list-header-action-wrapper">
+ <div class="c-fault-mgmt__list-header-sortButton c-fault-mgmt__list-action-button">
+ <SelectField
+ class="c-fault-mgmt-viewButton"
+ title="Sort By"
+ :model="model"
+ @onChange="onChange"
+ />
+ </div>
+ </div>
+</div>
+</template>
+
+<script>
+import SelectField from '@/api/forms/components/controls/SelectField.vue';
+
+import { SORT_ITEMS } from './constants';
+
+export default {
+ components: {
+ SelectField
+ },
+ inject: ['openmct', 'domainObject'],
+ props: {
+ selectedFaults: {
+ type: Array,
+ default() {
+ return [];
+ }
+ },
+ totalFaultsCount: {
+ type: Number,
+ default() {
+ return 0;
+ }
+ }
+ },
+ data() {
+ return {
+ model: {}
+ };
+ },
+ computed: {
+ isSelectAll() {
+ return this.totalFaultsCount > 0 && this.selectedFaults.length === this.totalFaultsCount;
+ }
+ },
+ beforeMount() {
+ const options = Object.values(SORT_ITEMS);
+ this.model = {
+ options,
+ value: options[0].value
+ };
+ },
+ methods: {
+ onChange(data) {
+ this.$emit('sortChanged', data);
+ },
+ selectAll(e) {
+ this.$emit('selectAll', e.target.checked);
+ }
+ }
+};
+</script>
diff --git a/src/plugins/faultManagement/FaultManagementListItem.vue b/src/plugins/faultManagement/FaultManagementListItem.vue
new file mode 100644
index 000000000..2a2f6bf85
--- /dev/null
+++ b/src/plugins/faultManagement/FaultManagementListItem.vue
@@ -0,0 +1,223 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+<template>
+<div
+ class="c-fault-mgmt__list data-selectable"
+ :class="classesFromState"
+>
+ <div class="c-fault-mgmt-item c-fault-mgmt__list-checkbox">
+ <input
+ type="checkbox"
+ :checked="isSelected"
+ @input="toggleSelected"
+ >
+ </div>
+ <div class="c-fault-mgmt-item">
+ <div
+ class="c-fault-mgmt__list-severity"
+ :title="fault.severity"
+ :class="[
+ 'is-severity-' + severity
+ ]"
+ >
+ </div>
+ </div>
+ <div class="c-fault-mgmt-item c-fault-mgmt__list-content">
+ <div class="c-fault-mgmt-item c-fault-mgmt__list-pathname">
+ <div class="c-fault-mgmt__list-path">{{ fault.namespace }}</div>
+ <div class="c-fault-mgmt__list-faultname">{{ fault.name }}</div>
+ </div>
+ <div class="c-fault-mgmt__list-content-right">
+ <div class="c-fault-mgmt-item c-fault-mgmt__list-trigVal">
+ <div
+ class="c-fault-mgmt-item__value"
+ :class="tripValueClassname"
+ title="Trip Value"
+ >{{ fault.triggerValueInfo.value }}</div>
+ </div>
+ <div class="c-fault-mgmt-item c-fault-mgmt__list-curVal">
+ <div
+ class="c-fault-mgmt-item__value"
+ :class="liveValueClassname"
+ title="Live Value"
+ >{{ fault.currentValueInfo.value }}</div>
+ </div>
+ <div class="c-fault-mgmt-item c-fault-mgmt__list-trigTime">
+ <div
+ class="c-fault-mgmt-item__value"
+ title="Last Trigger Time"
+ >{{ fault.triggerTime }}
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="c-fault-mgmt-item c-fault-mgmt__list-action-wrapper">
+ <button
+ class="c-fault-mgmt__list-action-button l-browse-bar__actions c-icon-button icon-3-dots"
+ title="Disposition Actions"
+ @click="showActionMenu"
+ ></button>
+ </div>
+</div>
+</template>
+<script>
+
+const RANGE_CONDITION_CLASS = {
+ 'LOW': 'is-limit--lwr',
+ 'HIGH': 'is-limit--upr'
+};
+
+const SEVERITY_CLASS = {
+ 'CRITICAL': 'is-limit--red',
+ 'WARNING': 'is-limit--yellow',
+ 'WATCH': 'is-limit--cyan'
+};
+
+export default {
+ inject: ['openmct', 'domainObject'],
+ props: {
+ fault: {
+ type: Object,
+ required: true
+ },
+ isSelected: {
+ type: Boolean,
+ default: () => {
+ return false;
+ }
+ }
+ },
+ computed: {
+ classesFromState() {
+ const exclusiveStates = [
+ {
+ className: 'is-shelved',
+ test: () => this.fault.shelved
+ },
+ {
+ className: 'is-unacknowledged',
+ test: () => !this.fault.acknowledged && !this.fault.shelved
+ },
+ {
+ className: 'is-acknowledged',
+ test: () => this.fault.acknowledged && !this.fault.shelved
+ }
+ ];
+
+ const classes = [];
+
+ if (this.isSelected) {
+ classes.push('is-selected');
+ }
+
+ const matchingState = exclusiveStates.find(stateDefinition => stateDefinition.test());
+
+ if (matchingState !== undefined) {
+ classes.push(matchingState.className);
+ }
+
+ return classes;
+ },
+ liveValueClassname() {
+ const currentValueInfo = this.fault?.currentValueInfo;
+ if (!currentValueInfo || currentValueInfo.monitoringResult === 'IN_LIMITS') {
+ return '';
+ }
+
+ let classname = RANGE_CONDITION_CLASS[currentValueInfo.rangeCondition] || '';
+ classname += ' ';
+ classname += SEVERITY_CLASS[currentValueInfo.monitoringResult] || '';
+
+ return classname.trim();
+ },
+ name() {
+ return `${this.fault?.name}/${this.fault?.namespace}`;
+ },
+ severity() {
+ return this.fault?.severity?.toLowerCase();
+ },
+ triggerTime() {
+ return this.fault?.triggerTime;
+ },
+ triggerValue() {
+ return this.fault?.triggerValueInfo?.value;
+ },
+ tripValueClassname() {
+ const triggerValueInfo = this.fault?.triggerValueInfo;
+ if (!triggerValueInfo || triggerValueInfo.monitoringResult === 'IN_LIMITS') {
+ return '';
+ }
+
+ let classname = RANGE_CONDITION_CLASS[triggerValueInfo.rangeCondition] || '';
+ classname += ' ';
+ classname += SEVERITY_CLASS[triggerValueInfo.monitoringResult] || '';
+
+ return classname.trim();
+ }
+ },
+ methods: {
+ showActionMenu(event) {
+ event.stopPropagation();
+
+ const menuItems = [
+ {
+ cssClass: 'icon-check',
+ isDisabled: this.fault.acknowledged,
+ name: 'Acknowledge',
+ description: '',
+ onItemClicked: (e) => {
+ this.$emit('acknowledgeSelected', [this.fault]);
+ }
+ },
+ {
+ cssClass: 'icon-timer',
+ name: 'Shelve',
+ description: '',
+ onItemClicked: () => {
+ this.$emit('shelveSelected', [this.fault], { shelved: true });
+ }
+ },
+ {
+ cssClass: 'icon-timer',
+ isDisabled: Boolean(!this.fault.shelved),
+ name: 'Unshelve',
+ description: '',
+ onItemClicked: () => {
+ this.$emit('shelveSelected', [this.fault], { shelved: false });
+ }
+ }
+ ];
+
+ this.openmct.menus.showMenu(event.x, event.y, menuItems);
+ },
+ toggleSelected(event) {
+ const faultData = {
+ fault: this.fault,
+ selected: event.target.checked
+ };
+
+ this.$emit('toggleSelected', faultData);
+ }
+ }
+};
+</script>
diff --git a/src/plugins/faultManagement/FaultManagementListView.vue b/src/plugins/faultManagement/FaultManagementListView.vue
new file mode 100644
index 000000000..be19cbfe5
--- /dev/null
+++ b/src/plugins/faultManagement/FaultManagementListView.vue
@@ -0,0 +1,301 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+<template>
+<div class="c-faults-list-view">
+ <FaultManagementSearch
+ :search-term="searchTerm"
+ @filterChanged="updateFilter"
+ @updateSearchTerm="updateSearchTerm"
+ />
+
+ <FaultManagementToolbar
+ v-if="showToolbar"
+ :selected-faults="selectedFaults"
+ @acknowledgeSelected="toggleAcknowledgeSelected"
+ @shelveSelected="toggleShelveSelected"
+ />
+
+ <div class="c-faults-list-view-header-item-container-wrapper">
+ <div class="c-faults-list-view-header-item-container">
+ <FaultManagementListHeader
+ class="header"
+ :selected-faults="Object.values(selectedFaults)"
+ :total-faults-count="filteredFaultsList.length"
+ @selectAll="selectAll"
+ @sortChanged="sortChanged"
+ />
+
+ <div class="c-faults-list-view-item-body">
+ <template v-if="filteredFaultsList.length > 0">
+ <FaultManagementListItem
+ v-for="fault of filteredFaultsList"
+ :key="fault.id"
+ :fault="fault"
+ :is-selected="isSelected(fault)"
+ @toggleSelected="toggleSelected"
+ @acknowledgeSelected="toggleAcknowledgeSelected"
+ @shelveSelected="toggleShelveSelected"
+ />
+ </template>
+ </div>
+ </div>
+ </div>
+</div>
+</template>
+
+<script>
+import FaultManagementListHeader from './FaultManagementListHeader.vue';
+import FaultManagementListItem from './FaultManagementListItem.vue';
+import FaultManagementSearch from './FaultManagementSearch.vue';
+import FaultManagementToolbar from './FaultManagementToolbar.vue';
+
+import { FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS, FILTER_ITEMS, SORT_ITEMS } from './constants';
+
+const SEARCH_KEYS = ['id', 'triggerValueInfo', 'currentValueInfo', 'triggerTime', 'severity', 'name', 'shortDescription', 'namespace'];
+
+export default {
+ components: {
+ FaultManagementListHeader,
+ FaultManagementListItem,
+ FaultManagementSearch,
+ FaultManagementToolbar
+ },
+ inject: ['openmct', 'domainObject'],
+ props: {
+ faultsList: {
+ type: Array,
+ default: () => []
+ }
+ },
+ data() {
+ return {
+ filterIndex: 0,
+ searchTerm: '',
+ selectedFaults: {},
+ sortBy: Object.values(SORT_ITEMS)[0].value
+ };
+ },
+ computed: {
+ filteredFaultsList() {
+ const filterName = FILTER_ITEMS[this.filterIndex];
+ let list = this.faultsList;
+
+ // Exclude shelved alarms from all views except the Shelved view
+ if (filterName !== 'Shelved') {
+ list = list.filter(fault => fault.shelved !== true);
+ }
+
+ if (filterName === 'Acknowledged') {
+ list = list.filter(fault => fault.acknowledged);
+ } else if (filterName === 'Unacknowledged') {
+ list = list.filter(fault => !fault.acknowledged);
+ } else if (filterName === 'Shelved') {
+ list = list.filter(fault => fault.shelved);
+ }
+
+ if (this.searchTerm.length > 0) {
+ list = list.filter(this.filterUsingSearchTerm);
+ }
+
+ list.sort(SORT_ITEMS[this.sortBy].sortFunction);
+
+ return list;
+ },
+ showToolbar() {
+ return this.openmct.faults.supportsActions();
+ }
+ },
+ methods: {
+ filterUsingSearchTerm(fault) {
+ if (!fault) {
+ return false;
+ }
+
+ let match = false;
+
+ SEARCH_KEYS.forEach((key) => {
+ if (fault[key]?.toString().toLowerCase().includes(this.searchTerm)) {
+ match = true;
+ }
+ });
+
+ return match;
+ },
+ isSelected(fault) {
+ return Boolean(this.selectedFaults[fault.id]);
+ },
+ selectAll(toggle = false) {
+ this.faultsList.forEach(fault => {
+ const faultData = {
+ fault,
+ selected: toggle
+ };
+ this.toggleSelected(faultData);
+ });
+ },
+ sortChanged(sort) {
+ this.sortBy = sort.value;
+ },
+ toggleSelected({ fault, selected = false}) {
+ if (selected) {
+ this.$set(this.selectedFaults, fault.id, fault);
+ } else {
+ this.$delete(this.selectedFaults, fault.id);
+ }
+
+ const selectedFaults = Object.values(this.selectedFaults);
+ this.openmct.selection.select(
+ [
+ {
+ element: this.$el,
+ context: {
+ item: this.openmct.router.path[0]
+ }
+ },
+ {
+ element: this.$el,
+ context: {
+ selectedFaults
+ }
+ }
+ ],
+ false);
+ },
+ toggleAcknowledgeSelected(faults = Object.values(this.selectedFaults)) {
+ let title = '';
+ if (faults.length > 1) {
+ title = `Acknowledge ${faults.length} selected faults`;
+ } else {
+ title = `Acknowledge fault: ${faults[0].name}`;
+ }
+
+ const formStructure = {
+ title,
+ sections: [
+ {
+ rows: [
+ {
+ key: 'comment',
+ control: 'textarea',
+ name: 'Optional comment',
+ pattern: '\\S+',
+ required: false,
+ cssClass: 'l-input-lg',
+ value: ''
+ }
+ ]
+ }
+ ],
+ buttons: {
+ submit: {
+ label: 'Acknowledge'
+ }
+ }
+ };
+
+ this.openmct.forms.showForm(formStructure)
+ .then(data => {
+ Object.values(faults)
+ .forEach(selectedFault => {
+ this.openmct.faults.acknowledgeFault(selectedFault, data);
+ });
+ });
+
+ this.selectedFaults = {};
+ },
+ async toggleShelveSelected(faults = Object.values(this.selectedFaults), shelveData = {}) {
+ const { shelved = true } = shelveData;
+ if (shelved) {
+ let title = faults.length > 1
+ ? `Shelve ${faults.length} selected faults`
+ : `Shelve fault: ${faults[0].name}`
+ ;
+
+ const formStructure = {
+ title,
+ sections: [
+ {
+ rows: [
+ {
+ key: 'comment',
+ control: 'textarea',
+ name: 'Optional comment',
+ pattern: '\\S+',
+ required: false,
+ cssClass: 'l-input-lg',
+ value: ''
+ },
+ {
+ key: 'shelveDuration',
+ control: 'select',
+ name: 'Shelve duration',
+ options: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS,
+ required: false,
+ cssClass: 'l-input-lg',
+ value: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value
+ }
+ ]
+ }
+ ],
+ buttons: {
+ submit: {
+ label: 'Shelve'
+ }
+ }
+ };
+
+ let data;
+ try {
+ data = await this.openmct.forms.showForm(formStructure);
+ } catch (e) {
+ return;
+ }
+
+ shelveData.comment = data.comment || '';
+ shelveData.shelveDuration = data.shelveDuration !== undefined
+ ? data.shelveDuration
+ : FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value;
+ } else {
+ shelveData = {
+ shelved: false
+ };
+ }
+
+ Object.values(faults)
+ .forEach(selectedFault => {
+ this.openmct.faults.shelveFault(selectedFault, shelveData);
+ });
+
+ this.selectedFaults = {};
+ },
+ updateFilter(filter) {
+ this.selectAll();
+
+ this.filterIndex = filter.model.options.findIndex(option => option.value === filter.value);
+ },
+ updateSearchTerm(term = '') {
+ this.searchTerm = term.toLowerCase();
+ }
+ }
+};
+</script>
diff --git a/src/plugins/faultManagement/FaultManagementObjectProvider.js b/src/plugins/faultManagement/FaultManagementObjectProvider.js
new file mode 100644
index 000000000..9565c27c1
--- /dev/null
+++ b/src/plugins/faultManagement/FaultManagementObjectProvider.js
@@ -0,0 +1,56 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 { FAULT_MANAGEMENT_TYPE, FAULT_MANAGEMENT_VIEW, FAULT_MANAGEMENT_NAMESPACE } from './constants';
+
+export default class FaultManagementObjectProvider {
+ constructor(openmct) {
+ this.openmct = openmct;
+ this.namespace = FAULT_MANAGEMENT_NAMESPACE;
+ this.key = FAULT_MANAGEMENT_VIEW;
+ this.objects = {};
+
+ this.createFaultManagementRootObject();
+ }
+
+ createFaultManagementRootObject() {
+ this.rootObject = {
+ identifier: {
+ key: this.key,
+ namespace: this.namespace
+ },
+ name: 'Fault Management',
+ type: FAULT_MANAGEMENT_TYPE,
+ location: 'ROOT'
+ };
+
+ this.openmct.objects.addRoot(this.rootObject.identifier);
+ }
+
+ get(identifier) {
+ if (identifier.key === FAULT_MANAGEMENT_VIEW) {
+ return Promise.resolve(this.rootObject);
+ }
+
+ return Promise.reject();
+ }
+}
diff --git a/src/plugins/faultManagement/FaultManagementPlugin.js b/src/plugins/faultManagement/FaultManagementPlugin.js
new file mode 100644
index 000000000..93dda8f5b
--- /dev/null
+++ b/src/plugins/faultManagement/FaultManagementPlugin.js
@@ -0,0 +1,42 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 FaultManagementViewProvider from './FaultManagementViewProvider';
+import FaultManagementObjectProvider from './FaultManagementObjectProvider';
+import FaultManagementInspectorViewProvider from './FaultManagementInspectorViewProvider';
+
+import { FAULT_MANAGEMENT_TYPE, FAULT_MANAGEMENT_NAMESPACE } from './constants';
+
+export default function FaultManagementPlugin() {
+ return function (openmct) {
+ openmct.types.addType(FAULT_MANAGEMENT_TYPE, {
+ name: 'Fault Management',
+ creatable: false,
+ description: 'Fault Management View',
+ cssClass: 'icon-bell'
+ });
+
+ openmct.objectViews.addProvider(new FaultManagementViewProvider(openmct));
+ openmct.inspectorViews.addProvider(new FaultManagementInspectorViewProvider(openmct));
+ openmct.objects.addProvider(FAULT_MANAGEMENT_NAMESPACE, new FaultManagementObjectProvider(openmct));
+ };
+}
diff --git a/src/plugins/faultManagement/FaultManagementSearch.vue b/src/plugins/faultManagement/FaultManagementSearch.vue
new file mode 100644
index 000000000..bfd2060ff
--- /dev/null
+++ b/src/plugins/faultManagement/FaultManagementSearch.vue
@@ -0,0 +1,90 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+<template>
+<div class="c-fault-mgmt__search-row">
+ <Search
+ class="c-fault-mgmt-search"
+ :value="searchTerm"
+ @input="updateSearchTerm"
+ @clear="updateSearchTerm"
+ />
+
+ <SelectField
+ class="c-fault-mgmt-viewButton"
+ title="View Filter"
+ :model="model"
+ @onChange="onChange"
+ />
+</div>
+</template>
+
+<script>
+import SelectField from '@/api/forms/components/controls/SelectField.vue';
+import Search from '@/ui/components/search.vue';
+
+import { FILTER_ITEMS } from './constants';
+
+export default {
+ components: {
+ SelectField,
+ Search
+ },
+ inject: ['openmct', 'domainObject'],
+ props: {
+ searchTerm: {
+ type: String,
+ default: ''
+ }
+ },
+ data() {
+ return {
+ items: []
+ };
+ },
+ computed: {
+ model() {
+ return {
+ options: this.items,
+ value: this.items[0] ? this.items[0].value : FILTER_ITEMS[0].toLowerCase()
+ };
+ }
+ },
+ mounted() {
+ this.items = FILTER_ITEMS
+ .map(item => {
+ return {
+ name: item,
+ value: item.toLowerCase()
+ };
+ });
+ },
+ methods: {
+ onChange(data) {
+ this.$emit('filterChanged', data);
+ },
+ updateSearchTerm(searchTerm) {
+ this.$emit('updateSearchTerm', searchTerm);
+ }
+ }
+};
+</script>
diff --git a/src/plugins/faultManagement/FaultManagementToolbar.vue b/src/plugins/faultManagement/FaultManagementToolbar.vue
new file mode 100644
index 000000000..6134a449b
--- /dev/null
+++ b/src/plugins/faultManagement/FaultManagementToolbar.vue
@@ -0,0 +1,102 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+<template>
+<div class="c-fault-mgmt__toolbar">
+ <button
+ class="c-icon-button icon-check"
+ title="Acknowledge selected faults"
+ :disabled="disableAcknowledge"
+ @click="acknowledgeSelected"
+ >
+ <div
+ title="Acknowledge selected faults"
+ class="c-icon-button__label"
+ >
+ Acknowledge
+ </div>
+ </button>
+
+ <button
+ class="c-icon-button icon-timer"
+ title="Shelve selected faults"
+ :disabled="disableShelve"
+ @click="shelveSelected"
+ >
+ <div
+ title="Shelve selected items"
+ class="c-icon-button__label"
+ >
+ Shelve
+ </div>
+ </button>
+</div>
+</template>
+
+<script>
+export default {
+ inject: ['openmct', 'domainObject'],
+ props: {
+ selectedFaults: {
+ type: Object,
+ default() {
+ return {};
+ }
+ }
+ },
+ data() {
+ return {
+ disableAcknowledge: true,
+ disableShelve: true
+ };
+ },
+ watch: {
+ selectedFaults(newSelectedFaults) {
+ const selectedfaults = Object.values(newSelectedFaults);
+
+ let disableAcknowledge = true;
+ let disableShelve = true;
+
+ selectedfaults.forEach(fault => {
+ if (!fault.shelved) {
+ disableShelve = false;
+ }
+
+ if (!fault.acknowledged) {
+ disableAcknowledge = false;
+ }
+ });
+
+ this.disableAcknowledge = disableAcknowledge;
+ this.disableShelve = disableShelve;
+ }
+ },
+ methods: {
+ acknowledgeSelected() {
+ this.$emit('acknowledgeSelected');
+ },
+ shelveSelected() {
+ this.$emit('shelveSelected');
+ }
+ }
+};
+</script>
diff --git a/src/plugins/faultManagement/FaultManagementView.vue b/src/plugins/faultManagement/FaultManagementView.vue
new file mode 100644
index 000000000..71ba7cfe7
--- /dev/null
+++ b/src/plugins/faultManagement/FaultManagementView.vue
@@ -0,0 +1,76 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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>
+<FaultManagementListView
+ :faults-list="faultsList"
+/>
+</template>
+
+<script>
+
+import FaultManagementListView from './FaultManagementListView.vue';
+import { FAULT_MANAGEMENT_ALARMS, FAULT_MANAGEMENT_GLOBAL_ALARMS } from './constants';
+
+export default {
+ components: {
+ FaultManagementListView
+ },
+ inject: ['openmct', 'domainObject'],
+ data() {
+ return {
+ faultsList: []
+ };
+ },
+ mounted() {
+ this.updateFaultList();
+
+ this.unsubscribe = this.openmct.faults
+ .subscribe(this.domainObject, this.updateFault);
+ },
+ beforeDestroy() {
+ if (this.unsubscribe) {
+ this.unsubscribe();
+ }
+ },
+ methods: {
+ updateFault({ fault, type }) {
+ if (type === FAULT_MANAGEMENT_GLOBAL_ALARMS) {
+ this.updateFaultList();
+ } else if (type === FAULT_MANAGEMENT_ALARMS) {
+ this.faultsList.forEach((faultValue, i) => {
+ if (fault.id === faultValue.id) {
+ this.$set(this.faultsList, i, fault);
+ }
+ });
+ }
+ },
+ updateFaultList() {
+ this.openmct.faults
+ .request(this.domainObject)
+ .then(faultsData => {
+ this.faultsList = faultsData.map(fd => fd.fault);
+ });
+ }
+ }
+};
+</script>
diff --git a/src/plugins/faultManagement/FaultManagementViewProvider.js b/src/plugins/faultManagement/FaultManagementViewProvider.js
new file mode 100644
index 000000000..9576cfd97
--- /dev/null
+++ b/src/plugins/faultManagement/FaultManagementViewProvider.js
@@ -0,0 +1,69 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 FaultManagementView from './FaultManagementView.vue';
+import { FAULT_MANAGEMENT_TYPE, FAULT_MANAGEMENT_VIEW } from './constants';
+import Vue from 'vue';
+
+export default class FaultManagementViewProvider {
+ constructor(openmct) {
+ this.openmct = openmct;
+ this.key = FAULT_MANAGEMENT_VIEW;
+ }
+
+ canView(domainObject) {
+ return domainObject.type === FAULT_MANAGEMENT_TYPE;
+ }
+
+ canEdit(domainObject) {
+ return false;
+ }
+
+ view(domainObject) {
+ let component;
+ const openmct = this.openmct;
+
+ return {
+ show: (element) => {
+ component = new Vue({
+ el: element,
+ components: {
+ FaultManagementView
+ },
+ provide: {
+ openmct,
+ domainObject
+ },
+ template: '<FaultManagementView></FaultManagementView>'
+ });
+ },
+ destroy: () => {
+ if (!component) {
+ return;
+ }
+
+ component.$destroy();
+ component = undefined;
+ }
+ };
+ }
+}
diff --git a/src/plugins/faultManagement/constants.js b/src/plugins/faultManagement/constants.js
new file mode 100644
index 000000000..9f0be44a5
--- /dev/null
+++ b/src/plugins/faultManagement/constants.js
@@ -0,0 +1,122 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 FAULT_SEVERITY = {
+ 'CRITICAL': {
+ name: 'CRITICAL',
+ value: 'critical',
+ priority: 0
+ },
+ 'WARNING': {
+ name: 'WARNING',
+ value: 'warning',
+ priority: 1
+ },
+ 'WATCH': {
+ name: 'WATCH',
+ value: 'watch',
+ priority: 2
+ }
+};
+
+export const FAULT_MANAGEMENT_TYPE = 'faultManagement';
+export const FAULT_MANAGEMENT_INSPECTOR = 'faultManagementInspector';
+export const FAULT_MANAGEMENT_ALARMS = 'alarms';
+export const FAULT_MANAGEMENT_GLOBAL_ALARMS = 'global-alarm-status';
+export const FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS = [
+ {
+ name: '5 Minutes',
+ value: 300000
+ },
+ {
+ name: '10 Minutes',
+ value: 600000
+ },
+ {
+ name: '15 Minutes',
+ value: 900000
+ },
+ {
+ name: 'Indefinite',
+ value: 0
+ }
+];
+export const FAULT_MANAGEMENT_VIEW = 'faultManagement.view';
+export const FAULT_MANAGEMENT_NAMESPACE = 'faults.taxonomy';
+export const FILTER_ITEMS = [
+ 'Standard View',
+ 'Acknowledged',
+ 'Unacknowledged',
+ 'Shelved'
+];
+export const SORT_ITEMS = {
+ 'newest-first': {
+ name: 'Newest First',
+ value: 'newest-first',
+ sortFunction: (a, b) => {
+ if (b.triggerTime > a.triggerTime) {
+ return 1;
+ }
+
+ if (a.triggerTime > b.triggerTime) {
+ return -1;
+ }
+
+ return 0;
+ }
+ },
+ 'oldest-first': {
+ name: 'Oldest First',
+ value: 'oldest-first',
+ sortFunction: (a, b) => {
+ if (a.triggerTime > b.triggerTime) {
+ return 1;
+ }
+
+ if (a.triggerTime < b.triggerTime) {
+ return -1;
+ }
+
+ return 0;
+ }
+ },
+ 'severity': {
+ name: 'Severity',
+ value: 'severity',
+ sortFunction: (a, b) => {
+ const diff = FAULT_SEVERITY[a.severity].priority - FAULT_SEVERITY[b.severity].priority;
+ if (diff !== 0) {
+ return diff;
+ }
+
+ if (b.triggerTime > a.triggerTime) {
+ return 1;
+ }
+
+ if (a.triggerTime > b.triggerTime) {
+ return -1;
+ }
+
+ return 0;
+ }
+ }
+};
diff --git a/src/plugins/faultManagement/fault-manager.scss b/src/plugins/faultManagement/fault-manager.scss
new file mode 100644
index 000000000..e1c97443d
--- /dev/null
+++ b/src/plugins/faultManagement/fault-manager.scss
@@ -0,0 +1,268 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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.
+ *****************************************************************************/
+$colorFaultItemFg: $colorBodyFg;
+$colorFaultItemFgEmphasis: $colorBodyFgEm;
+$colorFaultItemBg: pullForward($colorBodyBg, 5%);
+
+/*********************************************** SEARCH */
+.c-fault-mgmt__search-row {
+ display: flex;
+ align-items: center;
+ flex: 0 0 auto;
+ > * + * {
+ margin-left: 10px;
+ float: right;
+ }
+}
+
+.c-fault-mgmt-search {
+ width: 95%;
+}
+
+/*********************************************** TOOLBAR */
+.c-fault-mgmt__toolbar {
+ display: flex;
+ justify-content: center;
+ flex: 0 0 auto;
+ > * + * {
+ margin-left: $interiorMargin;
+ }
+}
+
+/*********************************************** LIST VIEW */
+.c-faults-list-view {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+
+ > * + * {
+ margin-top: $interiorMargin;
+ }
+}
+
+.c-faults-list-view-header-item-container {
+ display: grid;
+ width: 100%;
+ grid-template-columns: max-content max-content repeat(5,minmax(max-content, 20%)) max-content;
+ grid-row-gap: $interiorMargin;
+
+ &-wrapper {
+ flex: 1 1 auto;
+ padding-right: $interiorMargin; // Fend of from scrollbar
+ overflow-y: auto;
+ }
+
+ .--width-less-than-600 & {
+ grid-template-columns: max-content max-content 1fr 1fr max-content;
+ }
+}
+
+.c-faults-list-view-item-body {
+ display: contents;
+}
+
+/*********************************************** LIST */
+.c-fault-mgmt__list {
+ display: contents;
+ color: $colorFaultItemFg;
+
+ &-checkbox{
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+ }
+
+ &-severity {
+ font-size: 2em;
+
+ &.is-severity-critical {
+ @include glyphBefore($glyph-icon-alert-triangle);
+ color: $colorStatusError;
+ }
+
+ &.is-severity-warning {
+ @include glyphBefore($glyph-icon-alert-rect);
+ color: $colorStatusAlert;
+ }
+
+ &.is-severity-watch {
+ @include glyphBefore($glyph-icon-info);
+ color: $colorCommand;
+ }
+ }
+
+ &-content {
+ display: contents;
+
+ .--width-less-than-600 & {
+ display: flex;
+ flex-wrap: wrap;
+ grid-column: span 2;
+ }
+ }
+
+ &-pathname {
+ padding-right: $interiorMarginLg;
+ overflow-wrap: anywhere;
+ min-width: 100px;
+
+ }
+ &-path {
+ font-size: .85em;
+ margin-left: $interiorMargin;
+ }
+
+ &-faultname{
+ font-size: 1.3em;
+ margin-left: $interiorMargin;
+ }
+
+ &-content-right {
+ display: contents;
+ }
+
+ &-trigTime {
+ grid-column: 6 / span 2;
+ }
+
+ &-action-wrapper {
+ text-align: right;
+ flex: 0 0 auto;
+ align-items: stretch;
+ }
+
+ &-action-button {
+ flex: 0 0 auto;
+ margin-left: auto;
+ text-align: right;
+ }
+
+ // STATES
+ &.is-unacknowledged {
+ color: $colorFaultItemFgEmphasis;
+ .c-fault-mgmt__list-severity {
+ @include pulse($animName: severityAnim, $dur: 200ms);
+ }
+ }
+
+ &.is-acknowledged,
+ &.is-shelved {
+ .c-fault-mgmt__list-severity {
+ &:before {
+ opacity: 60%;
+ //font-size: 1.5em;
+ }
+
+ &:after {
+ color: $colorFaultItemFgEmphasis;
+ display: block;
+ font-family: symbolsfont;
+ position: absolute;
+ //text-shadow: black 0 0 2px;
+ right: -3px;
+ bottom: -3px;
+ transform-origin: right bottom;
+ transform: scale(0.6);
+ }
+ }
+ }
+
+ &.is-shelved {
+ .c-fault-mgmt__list-pathname {
+ font-style: italic;
+ }
+ }
+
+ &.is-acknowledged .c-fault-mgmt__list-severity:after {
+ content: $glyph-icon-check;
+ }
+
+ &.is-shelved .c-fault-mgmt__list-severity:after {
+ content: $glyph-icon-timer;
+ }
+}
+
+/*********************************************** LIST HEADER */
+.c-fault-mgmt__list-header {
+ display: contents;
+ border-radius: $controlCr;
+ align-items: center;
+
+ * {
+ margin: 0px;
+ border-radius: 0px;
+ }
+
+ .--width-less-than-600 & {
+ .c-fault-mgmt__list-content-right {
+ display:none;
+ }
+ }
+
+ &-content {
+ display: contents;
+ }
+
+ &-results {
+ grid-column: 2 / span 2;
+ font-size: 1em;
+ height: auto;
+ }
+
+ &-action-wrapper {
+ grid-column: 7 / span 2;
+
+ .--width-less-than-600 & {
+ grid-column: 4 / span 2;
+ }
+ }
+}
+
+/*********************************************** GRID ITEM */
+.c-fault-mgmt-item {
+ $p: $interiorMargin;
+ padding: $p;
+ background: $colorFaultItemBg;
+ white-space: nowrap;
+
+ &-header {
+ $c: $colorBodyBg;
+ background: $c;
+ border-bottom: 5px solid $c; // Creates illusion of "space" beneath header
+ min-height: 30px; // Needed to align cells
+ padding: $p;
+ position: sticky;
+ top: 0;
+ z-index: 2;
+ }
+
+ &__value {
+ @include isLimit();
+ background: rgba($colorBodyFg, 0.1);
+ padding: $p;
+ border-radius: $controlCr;
+ display: inline-flex;
+ }
+
+ .is-selected & {
+ background: $colorSelectedBg;
+ }
+}
diff --git a/src/plugins/faultManagement/pluginSpec.js b/src/plugins/faultManagement/pluginSpec.js
new file mode 100644
index 000000000..29169c05c
--- /dev/null
+++ b/src/plugins/faultManagement/pluginSpec.js
@@ -0,0 +1,103 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 {
+ createOpenMct,
+ resetApplicationState
+} from '../../utils/testing';
+import {
+ FAULT_MANAGEMENT_TYPE,
+ FAULT_MANAGEMENT_VIEW,
+ FAULT_MANAGEMENT_NAMESPACE
+} from './constants';
+
+describe("The Fault Management Plugin", () => {
+ let openmct;
+ const faultDomainObject = {
+ name: 'it is not your fault',
+ type: FAULT_MANAGEMENT_TYPE,
+ identifier: {
+ key: 'nobodies',
+ namespace: 'fault'
+ }
+ };
+
+ beforeEach(() => {
+ openmct = createOpenMct();
+ });
+
+ afterEach(() => {
+ return resetApplicationState(openmct);
+ });
+
+ it('is not installed by default', () => {
+ const typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
+
+ expect(typeDef.name).toBe('Unknown Type');
+ });
+
+ it('can be installed', () => {
+ openmct.install(openmct.plugins.FaultManagement());
+ const typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
+
+ expect(typeDef.name).toBe('Fault Management');
+ });
+
+ describe('once it is installed', () => {
+ beforeEach(() => {
+ openmct.install(openmct.plugins.FaultManagement());
+ });
+
+ it('provides a view for fault management types', () => {
+ const applicableViews = openmct.objectViews.get(faultDomainObject, []);
+ const faultManagementView = applicableViews.find(
+ (viewProvider) => viewProvider.key === FAULT_MANAGEMENT_VIEW
+ );
+
+ expect(applicableViews.length).toEqual(1);
+ expect(faultManagementView).toBeDefined();
+ });
+
+ it('provides an inspector view for fault management types', () => {
+ const faultDomainObjectSelection = [[
+ {
+ context: {
+ item: faultDomainObject
+ }
+ }
+ ]];
+ const applicableInspectorViews = openmct.inspectorViews.get(faultDomainObjectSelection);
+
+ expect(applicableInspectorViews.length).toEqual(1);
+ });
+
+ it('creates a root object for fault management', async () => {
+ const root = await openmct.objects.getRoot();
+ const rootCompositionCollection = openmct.composition.get(root);
+ const rootComposition = await rootCompositionCollection.load();
+ const faultObject = rootComposition.find(obj => obj.identifier.namespace === FAULT_MANAGEMENT_NAMESPACE);
+
+ expect(faultObject).toBeDefined();
+ });
+
+ });
+});
diff --git a/src/plugins/flexibleLayout/components/flexible-layout.scss b/src/plugins/flexibleLayout/components/flexible-layout.scss
index ac44eb3d3..6fe96a446 100644
--- a/src/plugins/flexibleLayout/components/flexible-layout.scss
+++ b/src/plugins/flexibleLayout/components/flexible-layout.scss
@@ -141,6 +141,10 @@
}
}
}
+
+ [s-selected].c-fl-frame__drag-wrapper {
+ border: $editFrameSelectedBorder;
+ }
}
/****** THEIR FRAMES */
diff --git a/src/plugins/flexibleLayout/components/flexibleLayout.vue b/src/plugins/flexibleLayout/components/flexibleLayout.vue
index 503db9a1f..c2f8958de 100644
--- a/src/plugins/flexibleLayout/components/flexibleLayout.vue
+++ b/src/plugins/flexibleLayout/components/flexibleLayout.vue
@@ -281,6 +281,10 @@ export default {
return false;
}
+ if (!this.isEditing) {
+ return false;
+ }
+
let containerId = event.dataTransfer.getData('containerid');
let container = this.containers.filter((c) => c.id === containerId)[0];
let containerPos = this.containers.indexOf(container);
diff --git a/src/plugins/flexibleLayout/components/frame.vue b/src/plugins/flexibleLayout/components/frame.vue
index 70e6802a6..8515e718b 100644
--- a/src/plugins/flexibleLayout/components/frame.vue
+++ b/src/plugins/flexibleLayout/components/frame.vue
@@ -31,7 +31,7 @@
<div
ref="frame"
class="c-frame c-fl-frame__drag-wrapper is-selectable u-inspectable is-moveable"
- draggable="true"
+ :draggable="draggable"
@dragstart="initDrag"
>
<object-frame
@@ -93,18 +93,20 @@ export default {
computed: {
hasFrame() {
return !this.frame.noFrame;
+ },
+ draggable() {
+ return this.isEditing;
}
},
mounted() {
if (this.frame.domainObjectIdentifier) {
- let domainObjectPromise;
if (this.openmct.objects.supportsMutation(this.frame.domainObjectIdentifier)) {
- domainObjectPromise = this.openmct.objects.getMutable(this.frame.domainObjectIdentifier);
+ this.domainObjectPromise = this.openmct.objects.getMutable(this.frame.domainObjectIdentifier);
} else {
- domainObjectPromise = this.openmct.objects.get(this.frame.domainObjectIdentifier);
+ this.domainObjectPromise = this.openmct.objects.get(this.frame.domainObjectIdentifier);
}
- domainObjectPromise.then((object) => {
+ this.domainObjectPromise.then((object) => {
this.setDomainObject(object);
});
}
@@ -112,7 +114,13 @@ export default {
this.dragGhost = document.getElementById('js-fl-drag-ghost');
},
beforeDestroy() {
- if (this.domainObject.isMutable) {
+ if (this.domainObjectPromise) {
+ this.domainObjectPromise.then(() => {
+ if (this?.domainObject?.isMutable) {
+ this.openmct.objects.destroyMutable(this.domainObject);
+ }
+ });
+ } else if (this?.domainObject?.isMutable) {
this.openmct.objects.destroyMutable(this.domainObject);
}
diff --git a/src/plugins/flexibleLayout/pluginSpec.js b/src/plugins/flexibleLayout/pluginSpec.js
index c88c7ffea..470fb6e8b 100644
--- a/src/plugins/flexibleLayout/pluginSpec.js
+++ b/src/plugins/flexibleLayout/pluginSpec.js
@@ -22,6 +22,7 @@
import { createOpenMct, resetApplicationState } from 'utils/testing';
import FlexibleLayout from './plugin';
+import Vue from 'vue';
describe('the plugin', function () {
let element;
@@ -61,7 +62,7 @@ describe('the plugin', function () {
element.appendChild(child);
openmct.on('start', done);
- openmct.startHeadless();
+ openmct.start(child);
});
afterEach(() => {
@@ -83,6 +84,16 @@ describe('the plugin', function () {
it('provides a view', () => {
expect(flexibleLayoutViewProvider).toBeDefined();
});
+
+ it('renders a view', async () => {
+ const flexibleView = flexibleLayoutViewProvider.view(testViewObject, []);
+ flexibleView.show(child, false);
+
+ await Vue.nextTick();
+ const flexTitle = child.querySelector('.l-browse-bar .c-object-label__name');
+
+ expect(flexTitle).not.toBeNull();
+ });
});
describe('the toolbar', () => {
diff --git a/src/plugins/flexibleLayout/toolbarProvider.js b/src/plugins/flexibleLayout/toolbarProvider.js
index 49fdff416..5d6663f66 100644
--- a/src/plugins/flexibleLayout/toolbarProvider.js
+++ b/src/plugins/flexibleLayout/toolbarProvider.js
@@ -159,7 +159,7 @@ function ToolbarProvider(openmct) {
let prompt = openmct.overlays.dialog({
iconClass: 'alert',
- message: 'This action will permanently delete this container from this Flexible Layout',
+ message: 'This action will permanently delete this container from this Flexible Layout. Do you want to continue?',
buttons: [
{
label: 'OK',
diff --git a/src/plugins/flexibleLayout/utils/container.js b/src/plugins/flexibleLayout/utils/container.js
index 9ee51b225..a26bf08ad 100644
--- a/src/plugins/flexibleLayout/utils/container.js
+++ b/src/plugins/flexibleLayout/utils/container.js
@@ -1,4 +1,4 @@
-import uuid from 'uuid';
+import { v4 as uuid } from 'uuid';
class Container {
constructor(size) {
diff --git a/src/plugins/flexibleLayout/utils/frame.js b/src/plugins/flexibleLayout/utils/frame.js
index a444a20ea..767464419 100644
--- a/src/plugins/flexibleLayout/utils/frame.js
+++ b/src/plugins/flexibleLayout/utils/frame.js
@@ -1,4 +1,4 @@
-import uuid from 'uuid';
+import { v4 as uuid } from 'uuid';
class Frame {
constructor(domainObjectIdentifier, size) {
diff --git a/src/plugins/formActions/CreateAction.js b/src/plugins/formActions/CreateAction.js
index 2b780466e..796b3557d 100644
--- a/src/plugins/formActions/CreateAction.js
+++ b/src/plugins/formActions/CreateAction.js
@@ -23,7 +23,7 @@
import PropertiesAction from './PropertiesAction';
import CreateWizard from './CreateWizard';
-import uuid from 'uuid';
+import { v4 as uuid } from 'uuid';
export default class CreateAction extends PropertiesAction {
constructor(openmct, type, parentDomainObject) {
diff --git a/src/plugins/formActions/CreateActionSpec.js b/src/plugins/formActions/CreateActionSpec.js
new file mode 100644
index 000000000..2071da471
--- /dev/null
+++ b/src/plugins/formActions/CreateActionSpec.js
@@ -0,0 +1,128 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 CreateAction from './CreateAction';
+
+import {
+ createOpenMct,
+ resetApplicationState
+} from 'utils/testing';
+
+import { debounce } from 'lodash';
+
+let parentObject;
+let parentObjectPath;
+let unObserve;
+
+describe("The create action plugin", () => {
+ let openmct;
+
+ const TYPES = [
+ 'clock',
+ 'conditionWidget',
+ 'conditionWidget',
+ 'example.imagery',
+ 'example.state-generator',
+ 'flexible-layout',
+ 'folder',
+ 'generator',
+ 'hyperlink',
+ 'LadTable',
+ 'LadTableSet',
+ 'layout',
+ 'mmgis',
+ 'notebook',
+ 'plan',
+ 'table',
+ 'tabs',
+ 'telemetry-mean',
+ 'telemetry.plot.bar-graph',
+ 'telemetry.plot.overlay',
+ 'telemetry.plot.stacked',
+ 'time-strip',
+ 'timer',
+ 'webpage'
+ ];
+
+ beforeEach((done) => {
+ openmct = createOpenMct();
+
+ openmct.on('start', done);
+ openmct.startHeadless();
+ });
+
+ afterEach(() => {
+ return resetApplicationState(openmct);
+ });
+
+ describe('creates new objects for a', () => {
+ beforeEach(() => {
+ parentObject = {
+ name: 'mock folder',
+ type: 'folder',
+ identifier: {
+ key: 'mock-folder',
+ namespace: ''
+ },
+ composition: []
+ };
+ parentObjectPath = [parentObject];
+
+ spyOn(openmct.objects, 'save');
+ openmct.objects.save.and.callThrough();
+ spyOn(openmct.forms, 'showForm');
+ openmct.forms.showForm.and.callFake(formStructure => {
+ return Promise.resolve({
+ name: 'test',
+ notes: 'test notes',
+ location: parentObjectPath
+ });
+ });
+ });
+
+ afterEach(() => {
+ parentObject = null;
+ unObserve();
+ });
+
+ TYPES.forEach(type => {
+ it(`type ${type}`, (done) => {
+ function callback(newObject) {
+ const composition = newObject.composition;
+
+ openmct.objects.get(composition[0])
+ .then(object => {
+ expect(object.type).toEqual(type);
+ expect(object.location).toEqual(openmct.objects.makeKeyString(parentObject.identifier));
+
+ done();
+ });
+ }
+
+ const deBouncedCallback = debounce(callback, 300);
+ unObserve = openmct.objects.observe(parentObject, '*', deBouncedCallback);
+
+ const createAction = new CreateAction(openmct, type, parentObject);
+ createAction.invoke();
+ });
+ });
+ });
+});
diff --git a/src/plugins/formActions/EditPropertiesAction.js b/src/plugins/formActions/EditPropertiesAction.js
index 4937b2ab3..65ceaaadd 100644
--- a/src/plugins/formActions/EditPropertiesAction.js
+++ b/src/plugins/formActions/EditPropertiesAction.js
@@ -45,7 +45,7 @@ export default class EditPropertiesAction extends PropertiesAction {
}
invoke(objectPath) {
- this._showEditForm(objectPath);
+ return this._showEditForm(objectPath);
}
/**
@@ -79,6 +79,13 @@ export default class EditPropertiesAction extends PropertiesAction {
/**
* @private
*/
+ _onCancel() {
+ //noop
+ }
+
+ /**
+ * @private
+ */
_showEditForm(objectPath) {
this.domainObject = objectPath[0];
@@ -86,7 +93,8 @@ export default class EditPropertiesAction extends PropertiesAction {
const formStructure = createWizard.getFormStructure(false);
formStructure.title = 'Edit ' + this.domainObject.name;
- this.openmct.forms.showForm(formStructure)
- .then(this._onSave.bind(this));
+ return this.openmct.forms.showForm(formStructure)
+ .then(this._onSave.bind(this))
+ .catch(this._onCancel.bind(this));
}
}
diff --git a/src/plugins/formActions/pluginSpec.js b/src/plugins/formActions/pluginSpec.js
new file mode 100644
index 000000000..232ff0d30
--- /dev/null
+++ b/src/plugins/formActions/pluginSpec.js
@@ -0,0 +1,229 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 {
+ createMouseEvent,
+ createOpenMct,
+ resetApplicationState
+} from 'utils/testing';
+
+import { debounce } from 'lodash';
+
+describe('EditPropertiesAction plugin', () => {
+ let editPropertiesAction;
+ let openmct;
+ let element;
+
+ beforeEach((done) => {
+ element = document.createElement('div');
+ element.style.display = 'block';
+ element.style.width = '1920px';
+ element.style.height = '1080px';
+
+ openmct = createOpenMct();
+ openmct.on('start', done);
+ openmct.startHeadless(element);
+
+ editPropertiesAction = openmct.actions.getAction('properties');
+ });
+
+ afterEach(() => {
+ editPropertiesAction = null;
+
+ return resetApplicationState(openmct);
+ });
+
+ it('editPropertiesAction exists', () => {
+ expect(editPropertiesAction.key).toEqual('properties');
+ });
+
+ it('edit properties action applies to only persistable objects', () => {
+ spyOn(openmct.objects, 'isPersistable').and.returnValue(true);
+
+ const domainObject = {
+ name: 'mock folder',
+ type: 'folder',
+ identifier: {
+ key: 'mock-folder',
+ namespace: ''
+ },
+ composition: []
+ };
+ const isApplicableTo = editPropertiesAction.appliesTo([domainObject]);
+ expect(isApplicableTo).toBe(true);
+ });
+
+ it('edit properties action does not apply to non persistable objects', () => {
+ spyOn(openmct.objects, 'isPersistable').and.returnValue(false);
+
+ const domainObject = {
+ name: 'mock folder',
+ type: 'folder',
+ identifier: {
+ key: 'mock-folder',
+ namespace: ''
+ },
+ composition: []
+ };
+ const isApplicableTo = editPropertiesAction.appliesTo([domainObject]);
+ expect(isApplicableTo).toBe(false);
+ });
+
+ it('edit properties action when invoked shows form', (done) => {
+ const domainObject = {
+ name: 'mock folder',
+ notes: 'mock notes',
+ type: 'folder',
+ identifier: {
+ key: 'mock-folder',
+ namespace: ''
+ },
+ modified: 1643065068597,
+ persisted: 1643065068600,
+ composition: []
+ };
+
+ const deBouncedFormChange = debounce(handleFormPropertyChange, 500);
+ openmct.forms.on('onFormPropertyChange', deBouncedFormChange);
+
+ function handleFormPropertyChange(data) {
+ const form = document.querySelector('.js-form');
+ const title = form.querySelector('input');
+ expect(title.value).toEqual(domainObject.name);
+
+ const notes = form.querySelector('textArea');
+ expect(notes.value).toEqual(domainObject.notes);
+
+ const buttons = form.querySelectorAll('button');
+ expect(buttons[0].textContent.trim()).toEqual('OK');
+ expect(buttons[1].textContent.trim()).toEqual('Cancel');
+
+ const clickEvent = createMouseEvent('click');
+ buttons[1].dispatchEvent(clickEvent);
+
+ openmct.forms.off('onFormPropertyChange', deBouncedFormChange);
+ }
+
+ editPropertiesAction.invoke([domainObject])
+ .then(() => {
+ done();
+ })
+ .catch(() => {
+ done();
+ });
+ });
+
+ it('edit properties action saves changes', (done) => {
+ const oldName = 'mock folder';
+ const newName = 'renamed mock folder';
+ const domainObject = {
+ name: oldName,
+ notes: 'mock notes',
+ type: 'folder',
+ identifier: {
+ key: 'mock-folder',
+ namespace: ''
+ },
+ modified: 1643065068597,
+ persisted: 1643065068600,
+ composition: []
+ };
+ let unObserve;
+
+ function callback(newObject) {
+ expect(newObject.name).not.toEqual(oldName);
+ expect(newObject.name).toEqual(newName);
+
+ unObserve();
+ done();
+ }
+
+ const deBouncedCallback = debounce(callback, 300);
+ unObserve = openmct.objects.observe(domainObject, '*', deBouncedCallback);
+
+ let changed = false;
+ const deBouncedFormChange = debounce(handleFormPropertyChange, 500);
+ openmct.forms.on('onFormPropertyChange', deBouncedFormChange);
+
+ function handleFormPropertyChange(data) {
+ const form = document.querySelector('.js-form');
+ const title = form.querySelector('input');
+ const notes = form.querySelector('textArea');
+
+ const buttons = form.querySelectorAll('button');
+ expect(buttons[0].textContent.trim()).toEqual('OK');
+ expect(buttons[1].textContent.trim()).toEqual('Cancel');
+
+ if (!changed) {
+ expect(title.value).toEqual(domainObject.name);
+ expect(notes.value).toEqual(domainObject.notes);
+
+ // change input field value and dispatch event for it
+ title.focus();
+ title.value = newName;
+ title.dispatchEvent(new Event('input'));
+ title.blur();
+
+ changed = true;
+ } else {
+ // click ok to save form changes
+ const clickEvent = createMouseEvent('click');
+ buttons[0].dispatchEvent(clickEvent);
+
+ openmct.forms.off('onFormPropertyChange', deBouncedFormChange);
+ }
+ }
+
+ editPropertiesAction.invoke([domainObject]);
+ });
+
+ it('edit properties action discards changes', (done) => {
+ const name = 'mock folder';
+ const domainObject = {
+ name,
+ notes: 'mock notes',
+ type: 'folder',
+ identifier: {
+ key: 'mock-folder',
+ namespace: ''
+ },
+ modified: 1643065068597,
+ persisted: 1643065068600,
+ composition: []
+ };
+
+ editPropertiesAction.invoke([domainObject])
+ .then(() => {
+ expect(domainObject.name).toEqual(name);
+ done();
+ })
+ .catch(() => {
+ expect(domainObject.name).toEqual(name);
+
+ done();
+ });
+
+ const form = document.querySelector('.js-form');
+ const buttons = form.querySelectorAll('button');
+ const clickEvent = createMouseEvent('click');
+ buttons[1].dispatchEvent(clickEvent);
+ });
+});
diff --git a/src/plugins/gauge/GaugePlugin.js b/src/plugins/gauge/GaugePlugin.js
index c9db912df..441e53cd5 100644
--- a/src/plugins/gauge/GaugePlugin.js
+++ b/src/plugins/gauge/GaugePlugin.js
@@ -49,6 +49,7 @@ export default function () {
gaugeType: GAUGE_TYPES[0][1],
isDisplayMinMax: true,
isDisplayCurVal: true,
+ isDisplayUnits: true,
isUseTelemetryLimits: true,
limitLow: 10,
limitHigh: 90,
@@ -60,6 +61,23 @@ export default function () {
},
form: [
{
+ name: "Gauge type",
+ options: GAUGE_TYPES.map(type => {
+ return {
+ name: type[0],
+ value: type[1]
+ };
+ }),
+ control: "select",
+ cssClass: "l-input-sm",
+ key: "gaugeController",
+ property: [
+ "configuration",
+ "gaugeController",
+ "gaugeType"
+ ]
+ },
+ {
name: "Display current value",
control: "toggleSwitch",
cssClass: "l-input",
@@ -71,6 +89,17 @@ export default function () {
]
},
{
+ name: "Display units",
+ control: "toggleSwitch",
+ cssClass: "l-input",
+ key: "isDisplayUnits",
+ property: [
+ "configuration",
+ "gaugeController",
+ "isDisplayUnits"
+ ]
+ },
+ {
name: "Display range values",
control: "toggleSwitch",
cssClass: "l-input",
@@ -93,23 +122,6 @@ export default function () {
]
},
{
- name: "Gauge type",
- options: GAUGE_TYPES.map(type => {
- return {
- name: type[0],
- value: type[1]
- };
- }),
- control: "select",
- cssClass: "l-input-sm",
- key: "gaugeController",
- property: [
- "configuration",
- "gaugeController",
- "gaugeType"
- ]
- },
- {
name: "Value ranges and limits",
control: "gauge-controller",
cssClass: "l-input",
diff --git a/src/plugins/gauge/GaugePluginSpec.js b/src/plugins/gauge/GaugePluginSpec.js
index 589449806..601c2bc7f 100644
--- a/src/plugins/gauge/GaugePluginSpec.js
+++ b/src/plugins/gauge/GaugePluginSpec.js
@@ -63,30 +63,30 @@ describe('Gauge plugin', () => {
});
it('Plugin installed by default', () => {
- const gaugueType = openmct.types.get('gauge');
+ const GaugeType = openmct.types.get('gauge');
- expect(gaugueType).not.toBeNull();
- expect(gaugueType.definition.name).toEqual('Gauge');
+ expect(GaugeType).not.toBeNull();
+ expect(GaugeType.definition.name).toEqual('Gauge');
});
- it('Gaugue plugin is creatable', () => {
- const gaugueType = openmct.types.get('gauge');
+ it('Gauge plugin is creatable', () => {
+ const GaugeType = openmct.types.get('gauge');
- expect(gaugueType.definition.creatable).toBeTrue();
+ expect(GaugeType.definition.creatable).toBeTrue();
});
- it('Gaugue plugin is creatable', () => {
- const gaugueType = openmct.types.get('gauge');
+ it('Gauge plugin is creatable', () => {
+ const GaugeType = openmct.types.get('gauge');
- expect(gaugueType.definition.creatable).toBeTrue();
+ expect(GaugeType.definition.creatable).toBeTrue();
});
- it('Gaugue form controller', () => {
+ it('Gauge form controller', () => {
const gaugeController = openmct.forms.getFormControl('gauge-controller');
expect(gaugeController).toBeDefined();
});
- describe('Gaugue with Filled Dial', () => {
+ describe('Gauge with Filled Dial', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
@@ -105,6 +105,7 @@ describe('Gauge plugin', () => {
gaugeType: 'dial-filled',
isDisplayMinMax: true,
isDisplayCurVal: true,
+ isDisplayUnits: true,
isUseTelemetryLimits: false,
limitLow: -0.9,
limitHigh: 0.9,
@@ -222,7 +223,7 @@ describe('Gauge plugin', () => {
});
});
- describe('Gaugue with Needle Dial', () => {
+ describe('Gauge with Needle Dial', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
@@ -240,6 +241,7 @@ describe('Gauge plugin', () => {
gaugeType: 'dial-needle',
isDisplayMinMax: true,
isDisplayCurVal: true,
+ isDisplayUnits: true,
isUseTelemetryLimits: false,
limitLow: -0.9,
limitHigh: 0.9,
@@ -357,7 +359,7 @@ describe('Gauge plugin', () => {
});
});
- describe('Gaugue with Vertical Meter', () => {
+ describe('Gauge with Vertical Meter', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
@@ -375,6 +377,7 @@ describe('Gauge plugin', () => {
gaugeType: 'meter-vertical',
isDisplayMinMax: true,
isDisplayCurVal: true,
+ isDisplayUnits: true,
isUseTelemetryLimits: false,
limitLow: -0.9,
limitHigh: 0.9,
@@ -469,7 +472,7 @@ describe('Gauge plugin', () => {
it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range');
- const valueElement = gaugeHolder.querySelector('.js-meter-current-value');
+ const valueElement = gaugeHolder.querySelector('.js-gauge-current-value');
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
@@ -482,7 +485,7 @@ describe('Gauge plugin', () => {
it('renders correct current value', (done) => {
function WatchUpdateValue() {
- const textElement = gaugeHolder.querySelector('.js-meter-current-value');
+ const textElement = gaugeHolder.querySelector('.js-gauge-current-value');
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
done();
}
@@ -492,7 +495,7 @@ describe('Gauge plugin', () => {
});
});
- describe('Gaugue with Vertical Meter Inverted', () => {
+ describe('Gauge with Vertical Meter Inverted', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
@@ -506,6 +509,7 @@ describe('Gauge plugin', () => {
gaugeType: 'meter-vertical',
isDisplayMinMax: true,
isDisplayCurVal: true,
+ isDisplayUnits: true,
isUseTelemetryLimits: false,
limitLow: -0.9,
limitHigh: 0.9,
@@ -566,7 +570,7 @@ describe('Gauge plugin', () => {
it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range');
- const valueElement = gaugeHolder.querySelector('.js-meter-current-value');
+ const valueElement = gaugeHolder.querySelector('.js-gauge-current-value');
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
@@ -574,7 +578,7 @@ describe('Gauge plugin', () => {
});
});
- describe('Gaugue with Horizontal Meter', () => {
+ describe('Gauge with Horizontal Meter', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
@@ -588,6 +592,7 @@ describe('Gauge plugin', () => {
gaugeType: 'meter-vertical',
isDisplayMinMax: true,
isDisplayCurVal: true,
+ isDisplayUnits: true,
isUseTelemetryLimits: false,
limitLow: -0.9,
limitHigh: 0.9,
@@ -656,7 +661,7 @@ describe('Gauge plugin', () => {
});
});
- describe('Gaugue with Filled Dial with Use Telemetry Limits', () => {
+ describe('Gauge with Filled Dial with Use Telemetry Limits', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
@@ -673,6 +678,7 @@ describe('Gauge plugin', () => {
gaugeType: 'dial-filled',
isDisplayMinMax: true,
isDisplayCurVal: true,
+ isDisplayUnits: true,
isUseTelemetryLimits: true,
limitLow: 10,
limitHigh: 90,
diff --git a/src/plugins/gauge/components/Gauge.vue b/src/plugins/gauge/components/Gauge.vue
index 91036ce96..2d250158e 100644
--- a/src/plugins/gauge/components/Gauge.vue
+++ b/src/plugins/gauge/components/Gauge.vue
@@ -23,179 +23,218 @@
<div
class="c-gauge__wrapper js-gauge-wrapper"
:class="`c-gauge--${gaugeType}`"
+ :title="gaugeTitle"
>
<template v-if="typeDial">
<svg
- width="0"
- height="0"
- class="c-dial__clip-paths"
- >
- <defs>
- <clipPath
- id="gaugeBgMask"
- clipPathUnits="objectBoundingBox"
- >
- <path d="M0.853553 0.853553C0.944036 0.763071 1 0.638071 1 0.5C1 0.223858 0.776142 0 0.5 0C0.223858 0 0 0.223858 0 0.5C0 0.638071 0.0559644 0.763071 0.146447 0.853553L0.285934 0.714066C0.23115 0.659281 0.197266 0.583598 0.197266 0.5C0.197266 0.332804 0.332804 0.197266 0.5 0.197266C0.667196 0.197266 0.802734 0.332804 0.802734 0.5C0.802734 0.583598 0.76885 0.659281 0.714066 0.714066L0.853553 0.853553Z" />
- </clipPath>
- <clipPath
- id="gaugeValueMask"
- clipPathUnits="objectBoundingBox"
- >
- <path d="M0.18926 0.81074C0.109735 0.731215 0.0605469 0.621351 0.0605469 0.5C0.0605469 0.257298 0.257298 0.0605469 0.5 0.0605469C0.742702 0.0605469 0.939453 0.257298 0.939453 0.5C0.939453 0.621351 0.890265 0.731215 0.81074 0.81074L0.714066 0.714066C0.76885 0.659281 0.802734 0.583599 0.802734 0.5C0.802734 0.332804 0.667196 0.197266 0.5 0.197266C0.332804 0.197266 0.197266 0.332804 0.197266 0.5C0.197266 0.583599 0.23115 0.659281 0.285934 0.714066L0.18926 0.81074Z" />
- </clipPath>
- </defs>
- </svg>
-
- <svg
- class="c-dial__range c-gauge__range js-gauge-dial-range"
- viewBox="0 0 512 512"
- >
- <text
- v-if="displayMinMax"
- font-size="35"
- transform="translate(105 455) rotate(-45)"
- >{{ rangeLow }}</text>
- <text
- v-if="displayMinMax"
- font-size="35"
- transform="translate(407 455) rotate(45)"
- text-anchor="end"
- >{{ rangeHigh }}</text>
- </svg>
-
- <svg
- class="c-dial__current-value-text-wrapper"
- viewBox="0 0 512 512"
- >
- <svg
- v-if="displayCurVal"
- class="c-dial__current-value-text-sizer"
- :viewBox="curValViewBox"
- >
- <text
- class="c-dial__current-value-text js-dial-current-value"
- lengthAdjust="spacing"
- text-anchor="middle"
- style="transform: translate(50%, 70%)"
- >{{ curVal }}</text>
- </svg>
- </svg>
-
- <svg
- class="c-dial__bg"
+ class="c-gauge c-dial"
viewBox="0 0 10 10"
>
+ <g class="c-dial__masks">
+ <mask id="gaugeValueMask">
+ <path
+ d="M1.8926 8.1074C1.09734 7.31215 0.605469 6.21352 0.605469 5C0.605469 2.57297 2.57297 0.605469 5 0.605469C7.42703 0.605469 9.39453 2.57297 9.39453 5C9.39453 6.21352 8.90266 7.31215 8.1074 8.1074L7.14066 7.14066C7.6885 6.59281 8.02734 5.83598 8.02734 5C8.02734 3.32804 6.67196 1.97266 5 1.97266C3.32804 1.97266 1.97266 3.32804 1.97266 5C1.97266 5.83598 2.3115 6.59281 2.85934 7.14066L1.8926 8.1074Z"
+ fill="white"
+ />
+ </mask>
+ <mask id="gaugeBgMask">
+ <path
+ d="M8.53553 8.53553C9.44036 7.63071 10 6.38071 10 5C10 2.23858 7.76142 0 5 0C2.23858 0 0 2.23858 0 5C0 6.38071 0.559644 7.63071 1.46447 8.53553L2.85934 7.14066C2.3115 6.59281 1.97266 5.83598 1.97266 5C1.97266 3.32804 3.32804 1.97266 5 1.97266C6.67196 1.97266 8.02734 3.32804 8.02734 5C8.02734 5.83598 7.6885 6.59281 7.14066 7.14066L8.53553 8.53553Z"
+ fill="white"
+ />
+ </mask>
+ </g>
<g
- v-if="limitLow !== null && dialLowLimitDeg < getLimitDegree('low', 'max')"
- class="c-dial__limit-low"
- :style="`transform: rotate(${dialLowLimitDeg}deg)`"
+ class="c-dial__graphics"
+ mask="url(#gaugeBgMask)"
>
<rect
- v-if="dialLowLimitDeg >= getLimitDegree('low', 'q1')"
- class="c-dial__low-limit__low"
- x="5"
- y="5"
- width="5"
- height="5"
- />
- <rect
- v-if="dialLowLimitDeg >= getLimitDegree('low', 'q2')"
- class="c-dial__low-limit__mid"
- x="5"
- y="0"
- width="5"
- height="5"
- />
- <rect
- v-if="dialLowLimitDeg >= getLimitDegree('low', 'q3')"
- class="c-dial__low-limit__high"
+ class="c-dial__bg"
x="0"
y="0"
- width="5"
- height="5"
+ width="10"
+ height="10"
/>
+ <g
+ v-if="isDialLowLimit"
+ class="c-dial__limit-low"
+ :style="`transform: rotate(${dialLowLimitDeg}deg)`"
+ >
+ <rect
+ v-if="isDialLowLimitLow"
+ class="c-dial__low-limit__low"
+ x="5"
+ y="5"
+ width="5"
+ height="5"
+ />
+ <rect
+ v-if="isDialLowLimitMid"
+ class="c-dial__low-limit__mid"
+ x="5"
+ y="0"
+ width="5"
+ height="5"
+ />
+ <rect
+ v-if="isDialLowLimitHigh"
+ class="c-dial__low-limit__high"
+ x="0"
+ y="0"
+ width="5"
+ height="5"
+ />
+ </g>
+ <g
+ v-if="isDialHighLimit"
+ class="c-dial__limit-high"
+ :style="`transform: rotate(${dialHighLimitDeg}deg)`"
+ >
+ <rect
+ v-if="isDialHighLimitLow"
+ class="c-dial__high-limit__low"
+ x="0"
+ y="5"
+ width="5"
+ height="5"
+ />
+ <rect
+ v-if="isDialHighLimitMid"
+ class="c-dial__high-limit__mid"
+ x="0"
+ y="0"
+ width="5"
+ height="5"
+ />
+ <rect
+ v-if="isDialHighLimitHigh"
+ class="c-dial__high-limit__high"
+ x="5"
+ y="0"
+ width="5"
+ height="5"
+ />
+ </g>
</g>
<g
- v-if="limitHigh !== null && dialHighLimitDeg < getLimitDegree('high', 'max')"
- class="c-dial__limit-high"
- :style="`transform: rotate(${dialHighLimitDeg}deg)`"
+ class="c-dial__graphics"
+ mask="url(#gaugeValueMask)"
>
- <rect
- v-if="dialHighLimitDeg <= getLimitDegree('high', 'max')"
- class="c-dial__high-limit__low"
- x="0"
- y="5"
- width="5"
- height="5"
- />
- <rect
- v-if="dialHighLimitDeg <= getLimitDegree('high', 'q2')"
- class="c-dial__high-limit__mid"
- x="0"
- y="0"
- width="5"
- height="5"
- />
- <rect
- v-if="dialHighLimitDeg <= getLimitDegree('high', 'q3')"
- class="c-dial__high-limit__high"
- x="5"
- y="0"
- width="5"
- height="5"
+ <g
+ v-if="typeFilledDial"
+ class="c-dial__filled-value"
+ :style="`transform: rotate(${degValueFilledDial}deg)`"
+ >
+ <rect
+ v-if="isDialFilledValueLow"
+ class="c-dial__filled-value__low"
+ x="5"
+ y="5"
+ width="5"
+ height="5"
+ />
+ <rect
+ v-if="isDialFilledValueMid"
+ class="c-dial__filled-value__mid"
+ x="5"
+ y="0"
+ width="5"
+ height="5"
+ />
+ <rect
+ v-if="isDialFilledValueHigh"
+ class="c-dial__filled-value__high"
+ x="0"
+ y="0"
+ width="5"
+ height="5"
+ />
+ </g>
+ <g
+ v-if="valueInBounds && typeNeedleDial"
+ class="c-dial__needle-value"
+ :style="`transform: rotate(${degValue}deg)`"
+ >
+ <path d="M4.90234 9.39453L5.09766 9.39453L5.30146 8.20874C6.93993 8.05674 8.22265 6.67817 8.22266 5C8.22266 3.22018 6.77982 1.77734 5 1.77734C3.22018 1.77734 1.77734 3.22018 1.77734 5C1.77734 6.67817 3.06007 8.05674 4.69854 8.20874L4.90234 9.39453Z" />
+ </g>
+ <path
+ id="dialTextPath"
+ class="c-dial__range-msg-path"
+ d="M8.3501 5.0001C8.3501 6.85025 6.85025 8.3501 5.0001 8.3501C3.14994 8.3501 1.6501 6.85025 1.6501 5.0001C1.6501 3.14994 3.14994 1.6501 5.0001 1.6501C6.85025 1.6501 8.3501 3.14994 8.3501 5.0001Z"
+ fill="none"
+ style="transform-origin: center; transform: rotate(182deg)"
/>
</g>
- </svg>
+ <g class="c-dial__text">
+ <text
+ v-if="displayUnits"
+ x="50%"
+ y="70%"
+ text-anchor="middle"
+ class="c-gauge__units"
+ font-size="8%"
+ >{{ units }}</text>
+
+ <g
+ v-if="displayMinMax"
+ class="c-dial__range-text js-gauge-dial-range"
+ :font-size="rangeFontSize"
+ >
+ <text
+ transform="translate(1.5 8.7) rotate(-45)"
+ dominant-baseline="hanging"
+ >{{ rangeLow }}</text>
+ <text
+ transform="translate(8.4 8.7) rotate(45)"
+ dominant-baseline="hanging"
+ text-anchor="end"
+ >{{ rangeHigh }}</text>
+ </g>
+ </g>
- <svg
- v-if="typeFilledDial"
- class="c-dial__filled-value-wrapper"
- viewBox="0 0 10 10"
- >
- <g
- class="c-dial__filled-value"
- :style="`transform: rotate(${degValueFilledDial}deg)`"
+ <svg
+ v-if="!valueInBounds && valueExpected"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 512 512"
+ xml:space="preserve"
+ class="c-dial__value-oor-indicator"
+ x="45%"
+ y="80%"
+ width="1"
+ height="1"
+ ><path
+ d="M448 0H64C28.7.1.1 28.7 0 64v384c.1 35.3 28.7 63.9 64 64h384c35.3-.1 63.9-28.7 64-64V64c-.1-35.3-28.7-63.9-64-64zM288 448h-64v-64h64v64zm10.9-192L280 352h-48l-18.9-96V64H299v192h-.1z"
+ /></svg>
+
+ <svg
+ class="c-gauge__current-value-text-wrapper"
+ :viewBox="curValViewBox"
+ preserveAspectRatio="xMidYMid meet"
>
<rect
- v-if="degValue >= getLimitDegree('low', 'q1')"
- class="c-dial__filled-value__low"
- x="5"
- y="5"
- width="5"
- height="5"
- />
- <rect
- v-if="degValue >= getLimitDegree('low', 'q2')"
- class="c-dial__filled-value__mid"
- x="5"
- y="0"
- width="5"
- height="5"
- />
- <rect
- v-if="degValue >= getLimitDegree('low', 'q3')"
- class="c-dial__filled-value__high"
+ class="svg-viewbox-debug"
x="0"
y="0"
- width="5"
- height="5"
+ width="100%"
+ height="100%"
/>
- </g>
- </svg>
+ <text
+ class="c-dial__current-value-text js-dial-current-value"
+ font-size="3.5"
+ lengthAdjust="spacing"
+ text-anchor="middle"
+ dominant-baseline="middle"
+ x="50%"
+ y="50%"
+ >
+ <template v-if="displayCurVal">
+ <tspan>{{ curVal }}</tspan>
+ </template>
+ </text>
+ </svg>
- <svg
- v-if="valueInBounds && typeNeedleDial"
- class="c-dial__needle-value-wrapper"
- viewBox="0 0 10 10"
- >
- <g
- class="c-dial__needle-value"
- :style="`transform: rotate(${degValue}deg)`"
- >
- <path d="M4.90234 9.39453L5.09766 9.39453L5.30146 8.20874C6.93993 8.05674 8.22265 6.67817 8.22266 5C8.22266 3.22018 6.77982 1.77734 5 1.77734C3.22018 1.77734 1.77734 3.22018 1.77734 5C1.77734 6.67817 3.06007 8.05674 4.69854 8.20874L4.90234 9.39453Z" />
- </g>
</svg>
</template>
@@ -209,20 +248,33 @@
<div class="c-meter__range__low">{{ rangeLow }}</div>
</div>
<div class="c-meter__bg">
+ <div
+ v-if="!valueInBounds && valueExpected"
+ class="c-meter__value-oor-indicator"
+ ><svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 512 512"
+ :preserveAspectRatio="meterOutOfRangeIndicatorAspectRatio"
+ ><path
+ d="M448 0H64C28.7.1.1 28.7 0 64v384c.1 35.3 28.7 63.9 64 64h384c35.3-.1 63.9-28.7 64-64V64c-.1-35.3-28.7-63.9-64-64zM288 448h-64v-64h64v64zm10.9-192L280 352h-48l-18.9-96V64H299v192h-.1z"
+ /></svg></div>
+
<template v-if="typeMeterVertical">
<div
+ v-if="valueExpected"
class="c-meter__value"
+ :class="{'c-meter__value-needle' : typeNeedleMeter }"
:style="`transform: translateY(${meterValueToPerc}%)`"
></div>
<div
- v-if="limitHigh !== null && meterHighLimitPerc > 0"
+ v-if="isMeterLimitHigh"
class="c-meter__limit-high"
:style="`height: ${meterHighLimitPerc}%`"
></div>
<div
- v-if="limitLow !== null && meterLowLimitPerc > 0"
+ v-if="isMeterLimitLow"
class="c-meter__limit-low"
:style="`height: ${meterLowLimitPerc}%`"
></div>
@@ -230,40 +282,62 @@
<template v-if="typeMeterHorizontal">
<div
+ v-if="valueExpected"
class="c-meter__value"
+ :class="{'c-meter__value-needle' : typeNeedleMeter }"
:style="`transform: translateX(${meterValueToPerc * -1}%)`"
></div>
<div
- v-if="limitHigh !== null && meterHighLimitPerc > 0"
+ v-if="isMeterLimitHigh"
class="c-meter__limit-high"
:style="`width: ${meterHighLimitPerc}%`"
></div>
<div
- v-if="limitLow !== null && meterLowLimitPerc > 0"
+ v-if="isMeterLimitLow"
class="c-meter__limit-low"
:style="`width: ${meterLowLimitPerc}%`"
></div>
</template>
<svg
- class="c-meter__current-value-text-wrapper"
- viewBox="0 0 512 512"
+ class="c-gauge__current-value-text-wrapper"
+ :viewBox="curValViewBox"
+ preserveAspectRatio="xMidYMid meet"
>
- <svg
- v-if="displayCurVal"
- class="c-meter__current-value-text-sizer"
- :viewBox="curValViewBox"
- preserveAspectRatio="xMidYMid meet"
+ <rect
+ class="svg-viewbox-debug"
+ x="0"
+ y="0"
+ width="100%"
+ height="100%"
+ />
+ <text
+ class="c-meter__current-value-text js-gauge-current-value"
+ font-size="4"
+ lengthAdjust="spacing"
+ text-anchor="middle"
+ :dominant-baseline="meterTextBaseline"
+ x="50%"
+ y="50%"
>
- <text
- class="c-dial__current-value-text js-meter-current-value"
- lengthAdjust="spacing"
- text-anchor="middle"
- style="transform: translate(50%, 70%)"
- >{{ curVal }}</text>
- </svg>
+ <template v-if="displayCurVal">
+ <tspan>{{ curVal }}</tspan>
+ <tspan
+ v-if="typeMeterHorizontal && displayUnits"
+ class="c-gauge__units"
+ font-size="80%"
+ >{{ units }}</tspan>
+ <tspan
+ v-if="typeMeterVertical && displayUnits"
+ x="50%"
+ dy="3.5"
+ class="c-gauge__units"
+ font-size="80%"
+ >{{ units }}</tspan>
+ </template>
+ </text>
</svg>
</div>
</div>
@@ -275,6 +349,7 @@
import { DIAL_VALUE_DEG_OFFSET, getLimitDegree } from '../gauge-limit-util';
const LIMIT_PADDING_IN_PERCENT = 10;
+const DEFAULT_CURRENT_VALUE = '--';
export default {
name: 'Gauge',
@@ -283,17 +358,20 @@ export default {
let gaugeController = this.domainObject.configuration.gaugeController;
return {
- curVal: 0,
+ curVal: DEFAULT_CURRENT_VALUE,
digits: 3,
precision: gaugeController.precision,
displayMinMax: gaugeController.isDisplayMinMax,
displayCurVal: gaugeController.isDisplayCurVal,
+ displayUnits: gaugeController.isDisplayUnits,
limitHigh: gaugeController.limitHigh,
limitLow: gaugeController.limitLow,
rangeHigh: gaugeController.max,
rangeLow: gaugeController.min,
gaugeType: gaugeController.gaugeType,
- activeTimeSystem: this.openmct.time.timeSystem()
+ showUnits: gaugeController.showUnits,
+ activeTimeSystem: this.openmct.time.timeSystem(),
+ units: ''
};
},
computed: {
@@ -313,12 +391,68 @@ export default {
dialLowLimitDeg() {
return this.percentToDegrees(this.valToPercent(this.limitLow));
},
+ meterOutOfRangeIndicatorAspectRatio() {
+ return this.typeMeterVertical ? 'xMidYMax meet' : 'xMinYMid meet';
+ },
+ meterTextBaseline() {
+ return this.typeMeterVertical ? 'auto' : 'middle';
+ },
curValViewBox() {
- const DIGITS_RATIO = 10;
- const VIEWBOX_STR = '0 0 X 15';
+ const DIGITS_RATIO = 3;
+ const VIEWBOX_STR = '0 0 X 10';
return VIEWBOX_STR.replace('X', this.digits * DIGITS_RATIO);
},
+ rangeFontSize() {
+ const CHAR_THRESHOLD = 3;
+ const START_PERC = 8.5;
+ const REDUCE_PERC = 0.8;
+ const RANGE_CHARS_MAX = Math.max(this.rangeLow.toString().length, this.rangeHigh.toString().length);
+
+ return this.fontSizeFromChars(RANGE_CHARS_MAX, CHAR_THRESHOLD, START_PERC, REDUCE_PERC);
+ },
+ isDialLowLimit() {
+ return this.limitLow.toString().length > 0 && this.dialLowLimitDeg < getLimitDegree('low', 'max');
+ },
+ isDialLowLimitLow() {
+ return this.dialLowLimitDeg >= getLimitDegree('low', 'q1');
+ },
+ isDialLowLimitMid() {
+ return this.dialLowLimitDeg >= getLimitDegree('low', 'q2');
+ },
+ isDialLowLimitHigh() {
+ return this.dialLowLimitDeg >= getLimitDegree('low', 'q3');
+ },
+ isDialHighLimit() {
+ return this.limitHigh.toString().length > 0 && this.dialHighLimitDeg < getLimitDegree('high', 'max');
+ },
+ isDialHighLimitLow() {
+ return this.dialHighLimitDeg <= getLimitDegree('high', 'max');
+ },
+ isDialHighLimitMid() {
+ return this.dialHighLimitDeg <= getLimitDegree('high', 'q2');
+ },
+ isDialHighLimitHigh() {
+ return this.dialHighLimitDeg <= getLimitDegree('high', 'q3');
+ },
+ isDialFilledValueLow() {
+ return this.degValue >= getLimitDegree('low', 'q1');
+ },
+ isDialFilledValueMid() {
+ return this.degValue >= getLimitDegree('low', 'q2');
+ },
+ isDialFilledValueHigh() {
+ return this.degValue >= getLimitDegree('low', 'q3');
+ },
+ isMeterLimitHigh() {
+ return this.limitHigh.toString().length > 0 && this.meterHighLimitPerc > 0;
+ },
+ isMeterLimitLow() {
+ return this.limitLow.toString().length > 0 && this.meterLowLimitPerc > 0;
+ },
+ gaugeTitle() {
+ return this.valueInBounds ? 'Gauge' : 'Value is currently out of range and cannot be graphically displayed';
+ },
typeDial() {
return this.matchGaugeType('dial');
},
@@ -340,15 +474,25 @@ export default {
typeMeterInverted() {
return this.matchGaugeType('inverted');
},
+ typeFilledMeter() {
+ return true; // Stubbing in for future capability
+ },
+ typeNeedleMeter() {
+ return false; // Stubbing in for future capability
+ },
meterValueToPerc() {
const meterDirection = (this.typeMeterInverted) ? -1 : 1;
- if (this.curVal <= this.rangeLow) {
- return meterDirection * 100;
- }
+ if (this.typeFilledMeter) {
+ // Filled meter is a filled rectangle that is transformed along a vertical or horizontal axis
+ // So never move it below the low range more than 100%, or above the high range more than 0%
+ if (this.curVal <= this.rangeLow) {
+ return meterDirection * 100;
+ }
- if (this.curVal >= this.rangeHigh) {
- return 0;
+ if (this.curVal >= this.rangeHigh) {
+ return 0;
+ }
}
return this.valToPercentMeter(this.curVal) * meterDirection;
@@ -359,6 +503,13 @@ export default {
meterLowLimitPerc() {
return 100 - this.valToPercentMeter(this.limitLow);
},
+ valueExpected() {
+ if (this.curVal === undefined || Object.is(this.curVal, 'null')) {
+ return false;
+ }
+
+ return this.curVal.toString().indexOf(DEFAULT_CURRENT_VALUE) === -1;
+ },
valueInBounds() {
return (this.curVal >= this.rangeLow && this.curVal <= this.rangeHigh);
},
@@ -435,6 +586,11 @@ export default {
]
});
},
+ fontSizeFromChars(charNum, charThreshold, startPerc, reducePerc) {
+ const fs = (charNum <= charThreshold) ? startPerc : (startPerc - ((charNum - charThreshold) * reducePerc));
+
+ return fs.toString() + "%";
+ },
matchGaugeType(str) {
return this.gaugeType.indexOf(str) !== -1;
},
@@ -459,13 +615,14 @@ export default {
this.unsubscribe = null;
}
- this.metadata = null;
+ this.curVal = DEFAULT_CURRENT_VALUE;
this.formats = null;
- this.valueKey = null;
- this.limitHigh = null;
- this.limitLow = null;
+ this.limitHigh = '';
+ this.limitLow = '';
+ this.metadata = null;
this.rangeHigh = null;
this.rangeLow = null;
+ this.valueKey = null;
},
request(domainObject = this.telemetryObject) {
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
@@ -484,6 +641,8 @@ export default {
const length = values.length;
this.updateValue(values[length - 1]);
});
+
+ this.units = this.metadata.value(this.valueKey).unit || '';
},
round(val, decimals = this.precision) {
let precision = Math.pow(10, decimals);
@@ -518,13 +677,20 @@ export default {
} else if (telemetryLimit.WATCH) {
limits = telemetryLimit.WATCH;
} else {
- this.openmct.notifications.error('No limits definition for given telemetry');
+ this.openmct.notifications.error('No limits definition for given telemetry, hiding low and high limits');
+ this.displayMinMax = false;
+ this.limitHigh = '';
+ this.limitLow = '';
+
+ return;
}
this.limitHigh = this.round(limits.high[this.valueKey]);
this.limitLow = this.round(limits.low[this.valueKey]);
this.rangeHigh = this.round(this.limitHigh + this.limitHigh * LIMIT_PADDING_IN_PERCENT / 100);
this.rangeLow = this.round(this.limitLow - Math.abs(this.limitLow * LIMIT_PADDING_IN_PERCENT / 100));
+
+ this.displayMinMax = this.domainObject.configuration.gaugeController.isDisplayMinMax;
},
updateValue(datum) {
this.datum = datum;
diff --git a/src/plugins/gauge/components/GaugeFormController.vue b/src/plugins/gauge/components/GaugeFormController.vue
index ea8045788..b9556e79b 100644
--- a/src/plugins/gauge/components/GaugeFormController.vue
+++ b/src/plugins/gauge/components/GaugeFormController.vue
@@ -40,7 +40,7 @@
<div class="c-form__row">
<span class="req-indicator req">
</span>
- <label>Range minimum value</label>
+ <label>Minimum value</label>
<input
ref="min"
v-model.number="min"
@@ -53,7 +53,7 @@
<div class="c-form__row">
<span class="req-indicator">
</span>
- <label>Range low limit</label>
+ <label>Low limit</label>
<input
ref="limitLow"
v-model.number="limitLow"
@@ -64,26 +64,26 @@
</div>
<div class="c-form__row">
- <span class="req-indicator req">
+ <span class="req-indicator">
</span>
- <label>Range maximum value</label>
+ <label>High limit</label>
<input
- ref="max"
- v-model.number="max"
- data-field-name="max"
+ ref="limitHigh"
+ v-model.number="limitHigh"
+ data-field-name="limitHigh"
type="number"
@input="onChange"
>
</div>
<div class="c-form__row">
- <span class="req-indicator">
+ <span class="req-indicator req">
</span>
- <label>Range high limit</label>
+ <label>Maximum value</label>
<input
- ref="limitHigh"
- v-model.number="limitHigh"
- data-field-name="limitHigh"
+ ref="max"
+ v-model.number="max"
+ data-field-name="max"
type="number"
@input="onChange"
>
@@ -111,6 +111,7 @@ export default {
isUseTelemetryLimits: this.model.value.isUseTelemetryLimits,
isDisplayMinMax: this.model.value.isDisplayMinMax,
isDisplayCurVal: this.model.value.isDisplayCurVal,
+ isDisplayUnits: this.model.value.isDisplayUnits,
limitHigh: this.model.value.limitHigh,
limitLow: this.model.value.limitLow,
max: this.model.value.max,
@@ -125,6 +126,7 @@ export default {
gaugeType: this.model.value.gaugeType,
isDisplayMinMax: this.isDisplayMinMax,
isDisplayCurVal: this.isDisplayCurVal,
+ isDisplayUnits: this.isDisplayUnits,
isUseTelemetryLimits: this.isUseTelemetryLimits,
limitLow: this.limitLow,
limitHigh: this.limitHigh,
diff --git a/src/plugins/gauge/gauge.scss b/src/plugins/gauge/gauge.scss
index a56f566a6..252216d3d 100644
--- a/src/plugins/gauge/gauge.scss
+++ b/src/plugins/gauge/gauge.scss
@@ -1,3 +1,8 @@
+$meterNeedlePerc: 1%;
+$meterNeedleMinPx: 4px;
+$meterNeedleMaxPx: 20px;
+$meterNeedleBorderRadius: 5px;
+
.is-object-type-gauge {
overflow: hidden;
}
@@ -16,75 +21,78 @@
// Both dial and meter types
overflow: hidden;
- &__range {
+ &__range,
+ &__units,
+ &__units text {
$c: $colorGaugeRange;
color: $c;
-
- text {
- fill: $c;
- }
+ fill: $c;
}
&__wrapper {
@include abs();
overflow: hidden;
}
+
+ &__current-value-text-wrapper {
+ // SVG
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ }
+}
+
+.c-dial__value-oor-indicator,
+.c-meter__value-oor-indicator {
+ fill: $colorGaugeRange;
+ opacity: 0.5;
}
/********************************************** DIAL GAUGE */
-svg[class*='c-dial'] {
+.c-dial {
max-height: 100%;
max-width: 100%;
- position: absolute;
+ display: block;
+ margin: auto; // Centers SVG in container while allowing scaling
- g {
- transform-origin: center;
- }
-}
-
-.c-dial {
&__bg {
- background: $colorGaugeBg;
- clip-path: url(#gaugeBgMask);
+ fill: $colorGaugeBg;
}
-
- &__limit-high rect { fill: $colorGaugeLimitHigh; }
- &__limit-low rect { fill: $colorGaugeLimitLow; }
-
- &__filled-value-wrapper {
- clip-path: url(#gaugeValueMask);
+ &__limit-high rect {
+ fill: $colorGaugeLimitHigh;
}
-
- &__needle-value-wrapper {
- clip-path: url(#gaugeValueMask);
+ &__limit-low rect {
+ fill: $colorGaugeLimitLow;
+ }
+ &__filled-value,
+ &__range-msg-text {
+ fill: $colorGaugeValue;
}
-
- &__filled-value { fill: $colorGaugeValue; }
-
&__needle-value {
fill: $colorGaugeValue;
- transition: transform $transitionTimeGauge;
}
-
&__current-value-text {
fill: $colorGaugeTextValue;
font-family: $heroFont;
}
+
+ &__units-text,
+ &__range-text {
+ fill: $colorGaugeRange;
+ }
+
+ &__graphics g {
+ transform-origin: center;
+ }
}
/********************************************** METER GAUGE */
.c-meter {
// Common styles for c-meter
+ $meterOutOfRangeIndicatorMaxSize: 50%;
@include abs();
display: flex;
- svg {
- // current-value-text
- position: absolute;
- height: 100%;
- width: 100%;
- }
-
&__range {
display: flex;
flex: 0 0 auto;
@@ -102,10 +110,42 @@ svg[class*='c-dial'] {
// Filled area
position: absolute;
background: $colorGaugeValue;
- transition: transform $transitionTimeGauge;
z-index: 1;
}
+ &__value-needle {
+ background: none !important;
+ &:before {
+ @include abs();
+ content: '';
+ display: block;
+ background: $colorGaugeValue;
+ }
+ }
+
+ &__value-oor-indicator {
+ $mxPx: 50px;
+ $wh: 50%;
+ position: absolute;
+ height: $wh;
+ width: $wh;
+ max-height: $mxPx;
+ max-width: $mxPx;
+
+ svg {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ max-height: 100%;
+ max-width: 100%;
+ }
+ }
+
+ &__current-value-text {
+ fill: $colorGaugeTextValue;
+ font-family: $heroFont;
+ }
+
.c-gauge__curval {
fill: $colorGaugeMeterTextValue !important;
}
@@ -142,10 +182,28 @@ svg[class*='c-dial'] {
bottom: 0;
}
+ &__value-needle {
+ right: 0;
+
+ &:before {
+ border-bottom-left-radius: $meterNeedleBorderRadius;
+ border-top-left-radius: $meterNeedleBorderRadius;
+ height: $meterNeedlePerc;
+ min-height: $meterNeedleMinPx;
+ max-height: $meterNeedleMaxPx;
+ }
+ }
+
[class*='limit'] {
left: 0;
right: 0;
}
+
+ .c-meter__value-oor-indicator {
+ bottom: 10%;
+ left: 50%;
+ transform: translateX(-50%);
+ }
}
.c-gauge--meter-vertical & {
@@ -156,6 +214,13 @@ svg[class*='c-dial'] {
&__limit-high {
top: 0;
}
+
+ &__value-needle {
+ &:before {
+ bottom: auto;
+ transform: translateY(-50%);
+ }
+ }
}
.c-gauge--meter-vertical-inverted & {
@@ -174,6 +239,13 @@ svg[class*='c-dial'] {
&__range__high {
order: 2;
}
+
+ &__value-needle {
+ &:before {
+ top: auto;
+ transform: translateY(50%);
+ }
+ }
}
.c-gauge--meter-horizontal & {
@@ -207,6 +279,20 @@ svg[class*='c-dial'] {
right: 0;
}
+ &__value-needle {
+ top: 0;
+
+ &:before {
+ border-bottom-left-radius: $meterNeedleBorderRadius;
+ border-bottom-right-radius: $meterNeedleBorderRadius;
+ left: auto;
+ width: $meterNeedlePerc;
+ min-width: $meterNeedleMinPx;
+ max-width: $meterNeedleMaxPx;
+ transform: translateX(50%);
+ }
+ }
+
[class*='limit'] {
top: 0;
bottom: 0;
@@ -219,5 +305,16 @@ svg[class*='c-dial'] {
&__limit-high {
right: 0;
}
+
+ .c-meter__value-oor-indicator {
+ // Horizontal meter
+ left: 2%;
+ top: 50%;
+ transform: translateY(-50%);
+ }
}
}
+.svg-viewbox-debug {
+ fill: rgba(deeppink, 0.5);
+ display: none;
+}
diff --git a/src/plugins/hyperlink/plugin.js b/src/plugins/hyperlink/plugin.js
index 4452a5ad4..3d6245170 100644
--- a/src/plugins/hyperlink/plugin.js
+++ b/src/plugins/hyperlink/plugin.js
@@ -27,7 +27,7 @@ export default function () {
openmct.types.addType('hyperlink', {
name: 'Hyperlink',
key: 'hyperlink',
- description: 'A hyperlink to redirect to a different link',
+ description: 'A text element or button that links to any URL including Open MCT views.',
creatable: true,
cssClass: 'icon-chain-links',
initialize: function (domainObject) {
diff --git a/src/plugins/imagery/components/FilterSettings.vue b/src/plugins/imagery/components/FilterSettings.vue
new file mode 100644
index 000000000..16aaadbb5
--- /dev/null
+++ b/src/plugins/imagery/components/FilterSettings.vue
@@ -0,0 +1,78 @@
+<template>
+<div
+ class="c-control-menu c-menu--to-left c-menu--has-close-btn c-image-controls c-image-controls--filters"
+ @click="handleClose"
+>
+ <div
+ class="c-image-controls__controls"
+ @click="$event.stopPropagation()"
+ >
+ <span class="c-image-controls__sliders">
+ <div class="c-image-controls__slider-wrapper icon-brightness">
+ <input
+ v-model="filters.brightness"
+ type="range"
+ min="0"
+ max="500"
+ draggable="true"
+ @dragstart.stop.prevent
+ @change="notifyFiltersChanged"
+ @input="notifyFiltersChanged"
+ >
+ </div>
+ <div class="c-image-controls__slider-wrapper icon-contrast">
+ <input
+ v-model="filters.contrast"
+ type="range"
+ min="0"
+ max="500"
+ draggable="true"
+ @dragstart.stop.prevent
+ @change="notifyFiltersChanged"
+ @input="notifyFiltersChanged"
+ >
+ </div>
+ </span>
+ <span class="c-image-controls__reset-btn">
+ <a
+ class="s-icon-button icon-reset t-btn-reset"
+ @click="resetFilters"
+ ></a>
+ </span>
+ </div>
+
+ <button class="c-click-icon icon-x t-btn-close c-switcher-menu__close-button"></button>
+</div>
+</template>
+
+<script>
+export default {
+ inject: ['openmct'],
+ data() {
+ return {
+ filters: {
+ brightness: 100,
+ contrast: 100
+ }
+ };
+ },
+ methods: {
+ handleClose(e) {
+ const closeButton = e.target.classList.contains('c-switcher-menu__close-button');
+ if (!closeButton) {
+ e.stopPropagation();
+ }
+ },
+ notifyFiltersChanged() {
+ this.$emit('filterChanged', this.filters);
+ },
+ resetFilters() {
+ this.filters = {
+ brightness: 100,
+ contrast: 100
+ };
+ this.notifyFiltersChanged();
+ }
+ }
+};
+</script>
diff --git a/src/plugins/imagery/components/ImageControls.vue b/src/plugins/imagery/components/ImageControls.vue
index 0f555ca55..96cf702bc 100644
--- a/src/plugins/imagery/components/ImageControls.vue
+++ b/src/plugins/imagery/components/ImageControls.vue
@@ -21,75 +21,66 @@
*****************************************************************************/
<template>
-<div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover c-image-controls__controls">
- <div class="c-image-controls__control c-image-controls__zoom icon-magnify">
- <div class="c-button-set c-button-set--strip-h">
- <button
- class="c-button t-btn-zoom-out icon-minus"
- title="Zoom out"
- @click="zoomOut"
- ></button>
+<div
+ class="h-local-controls h-local-controls--overlay-content h-local-controls--menus-aligned c-local-controls--show-on-hover"
+ role="toolbar"
+ aria-label="Image controls"
+>
+ <imagery-view-menu-switcher
+ :icon-class="'icon-brightness'"
+ :title="'Brightness and contrast'"
+ >
+ <filter-settings @filterChanged="updateFilterValues" />
+ </imagery-view-menu-switcher>
- <button
- class="c-button t-btn-zoom-in icon-plus"
- title="Zoom in"
- @click="zoomIn"
- ></button>
- </div>
+ <imagery-view-menu-switcher
+ v-if="layers.length"
+ :icon-class="'icon-layers'"
+ :title="'Layers'"
+ >
+ <layer-settings
+ :layers="layers"
+ @toggleLayerVisibility="toggleLayerVisibility"
+ />
+ </imagery-view-menu-switcher>
- <button
- class="c-button t-btn-zoom-lock"
- title="Lock current zoom and pan across all images"
- :class="{'icon-unlocked': !panZoomLocked, 'icon-lock': panZoomLocked}"
- @click="toggleZoomLock"
- ></button>
+ <zoom-settings
+ class="--hide-if-less-than-220"
+ :pan-zoom-locked="panZoomLocked"
+ :zoom-factor="zoomFactor"
+ @zoomOut="zoomOut"
+ @zoomIn="zoomIn"
+ @toggleZoomLock="toggleZoomLock"
+ @handleResetImage="handleResetImage"
+ />
- <button
- class="c-button icon-reset t-btn-zoom-reset"
- title="Remove zoom and pan"
- @click="handleResetImage"
- ></button>
-
- <span class="c-image-controls__zoom-factor">x{{ formattedZoomFactor }}</span>
- </div>
- <div class="c-image-controls__control c-image-controls__brightness-contrast">
- <span
- class="c-image-controls__sliders"
- draggable="true"
- @dragstart.stop.prevent
- >
- <div class="c-image-controls__input icon-brightness">
- <input
- v-model="filters.contrast"
- type="range"
- min="0"
- max="500"
- @change="notifyFiltersChanged"
- >
- </div>
- <div class="c-image-controls__input icon-contrast">
- <input
- v-model="filters.brightness"
- type="range"
- min="0"
- max="500"
- @change="notifyFiltersChanged"
- >
- </div>
- </span>
- <span class="t-reset-btn-holder c-imagery__lc__reset-btn c-image-controls__btn-reset">
- <button
- class="c-icon-link icon-reset t-btn-reset"
- @click="handleResetFilters"
- ></button>
- </span>
- </div>
+ <imagery-view-menu-switcher
+ class="--show-if-less-than-220"
+ :icon-class="'icon-magnify'"
+ :title="'Zoom settings'"
+ >
+ <zoom-settings
+ :pan-zoom-locked="panZoomLocked"
+ :class="'c-control-menu c-menu--has-close-btn'"
+ :zoom-factor="zoomFactor"
+ :is-menu="true"
+ @zoomOut="zoomOut"
+ @zoomIn="zoomIn"
+ @toggleZoomLock="toggleZoomLock"
+ @handleResetImage="handleResetImage"
+ />
+ </imagery-view-menu-switcher>
</div>
</template>
<script>
import _ from 'lodash';
+import FilterSettings from "./FilterSettings.vue";
+import LayerSettings from "./LayerSettings.vue";
+import ZoomSettings from "./ZoomSettings.vue";
+import ImageryViewMenuSwitcher from "./ImageryViewMenuSwitcher.vue";
+
const DEFAULT_FILTER_VALUES = {
brightness: '100',
contrast: '100'
@@ -101,13 +92,28 @@ const ZOOM_STEP = 1;
const ZOOM_WHEEL_SENSITIVITY_REDUCTION = 0.01;
export default {
+ components: {
+ FilterSettings,
+ LayerSettings,
+ ImageryViewMenuSwitcher,
+ ZoomSettings
+ },
inject: ['openmct', 'domainObject'],
props: {
+ layers: {
+ type: Array,
+ required: true
+ },
zoomFactor: {
type: Number,
required: true
},
- imageUrl: String
+ imageUrl: {
+ type: String,
+ default: () => {
+ return '';
+ }
+ }
},
data() {
return {
@@ -123,9 +129,6 @@ export default {
};
},
computed: {
- formattedZoomFactor() {
- return Number.parseFloat(this.zoomFactor).toPrecision(2);
- },
cursorStates() {
const isPannable = this.altPressed && this.zoomFactor > 1;
const showCursorZoomIn = this.metaPressed && !this.shiftPressed;
@@ -174,7 +177,7 @@ export default {
this.$emit('filtersUpdated', this.filters);
},
handleResetFilters() {
- this.filters = DEFAULT_FILTER_VALUES;
+ this.filters = {...DEFAULT_FILTER_VALUES};
this.notifyFiltersChanged();
},
limitZoomRange(factor) {
@@ -267,6 +270,13 @@ export default {
const newScaleFactor = this.zoomFactor + (this.shiftPressed ? -ZOOM_STEP : ZOOM_STEP);
this.zoomImage(newScaleFactor, e.clientX, e.clientY);
+ },
+ toggleLayerVisibility(index) {
+ this.$emit('toggleLayerVisibility', index);
+ },
+ updateFilterValues(filters) {
+ this.filters = filters;
+ this.notifyFiltersChanged();
}
}
};
diff --git a/src/plugins/imagery/components/ImageThumbnail.vue b/src/plugins/imagery/components/ImageThumbnail.vue
new file mode 100644
index 000000000..87eda1fc1
--- /dev/null
+++ b/src/plugins/imagery/components/ImageThumbnail.vue
@@ -0,0 +1,69 @@
+<!--
+ Open MCT, Copyright (c) 2014-2022, United States Government
+ as represented by the Administrator of the National Aeronautics and Space
+ Administration. All rights reserved.
+
+ Open MCT is licensed under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0.
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ License for the specific language governing permissions and limitations
+ under the License.
+
+ Open MCT includes source code licensed under additional open source
+ licenses. See the Open Source Licenses file (LICENSES.md) included with
+ this source code distribution or the Licensing information page available
+ at runtime from the About dialog for additional information.
+-->
+
+<template>
+<div
+ class="c-imagery__thumb c-thumb"
+ :class="{
+ 'active': active,
+ 'selected': selected,
+ 'real-time': realTime
+ }"
+ :title="image.formattedTime"
+>
+ <a
+ href=""
+ :download="image.imageDownloadName"
+ @click.prevent
+ >
+ <img
+ class="c-thumb__image"
+ :src="image.url"
+ fetchpriority="low"
+ >
+ </a>
+ <div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
+</div>
+</template>
+
+<script>
+export default {
+ props: {
+ image: {
+ type: Object,
+ required: true
+ },
+ active: {
+ type: Boolean,
+ required: true
+ },
+ selected: {
+ type: Boolean,
+ required: true
+ },
+ realTime: {
+ type: Boolean,
+ required: true
+ }
+ }
+};
+</script>
diff --git a/src/plugins/imagery/components/ImageryView.vue b/src/plugins/imagery/components/ImageryView.vue
index a0ef2b8e4..f9a5dfb4c 100644
--- a/src/plugins/imagery/components/ImageryView.vue
+++ b/src/plugins/imagery/components/ImageryView.vue
@@ -28,34 +28,34 @@
@keydown="arrowDownHandler"
@mouseover="focusElement"
>
- <div class="c-imagery__main-image-wrapper has-local-controls">
+ <div
+ class="c-imagery__main-image-wrapper has-local-controls"
+ :class="imageWrapperStyle"
+ @mousedown="handlePanZoomClick"
+ >
<ImageControls
ref="imageControls"
:zoom-factor="zoomFactor"
:image-url="imageUrl"
+ :layers="layers"
@resetImage="resetImage"
@panZoomUpdated="handlePanZoomUpdate"
@filtersUpdated="setFilters"
@cursorsUpdated="setCursorStates"
@startPan="startPan"
+ @toggleLayerVisibility="toggleLayerVisibility"
/>
-
<div
ref="imageBG"
class="c-imagery__main-image__bg"
- :class="{
- 'paused unnsynced': isPaused && !isFixed,
- 'stale': false,
- 'pannable': cursorStates.isPannable,
- 'cursor-zoom-in': cursorStates.showCursorZoomIn,
- 'cursor-zoom-out': cursorStates.showCursorZoomOut
- }"
@click="expand"
>
<div
v-if="zoomFactor > 1"
class="c-imagery__hints"
- >{{formatImageAltText}}</div>
+ >
+ {{ formatImageAltText }}
+ </div>
<div
ref="focusedImageWrapper"
class="image-wrapper"
@@ -65,6 +65,13 @@
}"
@mousedown="handlePanZoomClick"
>
+ <div
+ v-for="(layer, index) in visibleLayers"
+ :key="index"
+ class="layer-image s-image-layer c-imagery__layer-image js-layer-image"
+ :style="getVisibleLayerStyles(layer)"
+ >
+ </div>
<img
ref="focusedImage"
class="c-imagery__main-image__image js-imageryView-image "
@@ -75,31 +82,14 @@
}"
:data-openmct-image-timestamp="time"
:data-openmct-object-keystring="keyString"
+ fetchpriority="low"
>
<div
v-if="imageUrl"
ref="focusedImageElement"
class="c-imagery__main-image__background-image"
:draggable="!isSelectable"
- :style="{
- 'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`,
- 'background-image':
- `${imageUrl ? (
- `url(${imageUrl}),
- repeating-linear-gradient(
- 45deg,
- transparent,
- transparent 4px,
- rgba(125,125,125,.2) 4px,
- rgba(125,125,125,.2) 8px
- )`
- ) : ''}`,
- 'transform': `scale(${zoomFactor}) translate(${imageTranslateX}px, ${imageTranslateY}px)`,
- 'transition': `${!pan && animateZoom ? 'transform 250ms ease-in' : 'initial'}`,
- 'width': `${sizedImageWidth}px`,
- 'height': `${sizedImageHeight}px`,
-
- }"
+ :style="focusImageStyles"
></div>
<Compass
v-if="shouldDisplayCompass"
@@ -177,26 +167,15 @@
class="c-imagery__thumbs-scroll-area"
@scroll="handleScroll"
>
- <div
+ <ImageThumbnail
v-for="(image, index) in imageHistory"
:key="image.url + image.time"
- class="c-imagery__thumb c-thumb"
- :class="{ selected: focusedImageIndex === index && isPaused }"
- :title="image.formattedTime"
- @click="thumbnailClicked(index)"
- >
- <a
- href=""
- :download="image.imageDownloadName"
- @click.prevent
- >
- <img
- class="c-thumb__image"
- :src="image.url"
- >
- </a>
- <div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
- </div>
+ :image="image"
+ :active="focusedImageIndex === index"
+ :selected="focusedImageIndex === index && isPaused"
+ :real-time="!isFixed"
+ @click.native="thumbnailClicked(index)"
+ />
</div>
<button
@@ -216,6 +195,7 @@ import moment from 'moment';
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
import Compass from './Compass/Compass.vue';
import ImageControls from './ImageControls.vue';
+import ImageThumbnail from './ImageThumbnail.vue';
import imageryData from "../../imagery/mixins/imageryData";
const REFRESH_CSS_MS = 500;
@@ -240,9 +220,11 @@ const SHOW_THUMBS_THRESHOLD_HEIGHT = 200;
const SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600;
export default {
+ name: 'ImageryView',
components: {
Compass,
- ImageControls
+ ImageControls,
+ ImageThumbnail
},
mixins: [imageryData],
inject: ['openmct', 'domainObject', 'objectPath', 'currentView', 'imageFreshnessOptions'],
@@ -260,8 +242,12 @@ export default {
this.requestCount = 0;
return {
+ timeFormat: '',
+ layers: [],
+ visibleLayers: [],
durationFormatter: undefined,
imageHistory: [],
+ bounds: {},
timeSystem: timeSystem,
keyString: undefined,
autoScroll: true,
@@ -323,12 +309,41 @@ export default {
displayThumbnailsSmall() {
return this.viewHeight > SHOW_THUMBS_THRESHOLD_HEIGHT && this.viewHeight <= SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT;
},
+ focusImageStyles() {
+ return {
+ 'filter': `brightness(${this.filters.brightness}%) contrast(${this.filters.contrast}%)`,
+ 'background-image':
+ `${this.imageUrl ? (
+ `url(${this.imageUrl}),
+ repeating-linear-gradient(
+ 45deg,
+ transparent,
+ transparent 4px,
+ rgba(125,125,125,.2) 4px,
+ rgba(125,125,125,.2) 8px
+ )`
+ ) : ''}`,
+ 'transform': `scale(${this.zoomFactor}) translate(${this.imageTranslateX}px, ${this.imageTranslateY}px)`,
+ 'transition': `${!this.pan && this.animateZoom ? 'transform 250ms ease-in' : 'initial'}`,
+ 'width': `${this.sizedImageWidth}px`,
+ 'height': `${this.sizedImageHeight}px`
+ };
+ },
time() {
return this.formatTime(this.focusedImage);
},
imageUrl() {
return this.formatImageUrl(this.focusedImage);
},
+ imageWrapperStyle() {
+ return {
+ 'cursor-zoom-in': this.cursorStates.showCursorZoomIn,
+ 'cursor-zoom-out': this.cursorStates.showCursorZoomOut,
+ 'pannable': this.cursorStates.isPannable,
+ 'paused unnsynced': this.isPaused && !this.isFixed,
+ 'stale': false
+ };
+ },
isImageNew() {
let cutoff = FIVE_MINUTES;
if (this.imageFreshnessOptions) {
@@ -382,6 +397,9 @@ export default {
formattedDuration() {
let result = 'N/A';
let negativeAge = -1;
+ if (!Number.isInteger(this.numericDuration)) {
+ return result;
+ }
if (this.numericDuration > TWENTYFOUR_HOURS) {
negativeAge *= (this.numericDuration / TWENTYFOUR_HOURS);
@@ -502,20 +520,17 @@ export default {
},
watch: {
imageHistory: {
- handler(newHistory, oldHistory) {
+ handler(newHistory, _oldHistory) {
const newSize = newHistory.length;
- let imageIndex;
+ let imageIndex = newSize > 0 ? newSize - 1 : undefined;
if (this.focusedImageTimestamp !== undefined) {
const foundImageIndex = newHistory.findIndex(img => img.time === this.focusedImageTimestamp);
- imageIndex = foundImageIndex > -1
- ? foundImageIndex
- : newSize - 1;
- } else {
- imageIndex = newSize > 0
- ? newSize - 1
- : undefined;
+ if (foundImageIndex > -1) {
+ imageIndex = foundImageIndex;
+ }
}
+ this.setFocusedImage(imageIndex);
this.nextImageIndex = imageIndex;
if (this.previousFocusedImage && newHistory.length) {
@@ -545,6 +560,16 @@ export default {
this.resetAgeCSS();
this.updateRelatedTelemetryForFocusedImage();
this.getImageNaturalDimensions();
+ },
+ bounds() {
+ this.scrollToFocused();
+ },
+ isFixed(newValue) {
+ const isRealTime = !newValue;
+ // if realtime unpause which will focus on latest image
+ if (isRealTime) {
+ this.paused(false);
+ }
}
},
async mounted() {
@@ -586,6 +611,7 @@ export default {
this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY);
this.handleThumbWindowResizeEnded = _.debounce(this.handleThumbWindowResizeEnded, SCROLL_LATENCY);
this.handleThumbWindowResizeStart = _.debounce(this.handleThumbWindowResizeStart, SCROLL_LATENCY);
+ this.scrollToFocused = _.debounce(this.scrollToFocused, 400);
if (this.$refs.thumbsWrapper) {
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
@@ -593,8 +619,10 @@ export default {
}
this.listenTo(this.focusedImageWrapper, 'wheel', this.wheelZoom, this);
+ this.loadVisibleLayers();
},
beforeDestroy() {
+ this.persistVisibleLayers();
this.stopFollowingTimeContext();
if (this.thumbWrapperResizeObserver) {
@@ -625,6 +653,13 @@ export default {
calculateViewHeight() {
this.viewHeight = this.$el.clientHeight;
},
+ getVisibleLayerStyles(layer) {
+ return {
+ 'background-image': `url(${layer.source})`,
+ 'transform': `scale(${this.zoomFactor}) translate(${this.imageTranslateX}px, ${this.imageTranslateY}px)`,
+ 'transition': `${!this.pan && this.animateZoom ? 'transform 250ms ease-in' : 'initial'}`
+ };
+ },
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
@@ -693,6 +728,37 @@ export default {
return mostRecent[valueKey];
},
+ loadVisibleLayers() {
+ const metaDataValues = this.metadata.valuesForHints(['image'])[0];
+ this.imageFormat = this.openmct.telemetry.getValueFormatter(metaDataValues);
+ let layersMetadata = metaDataValues.layers;
+ if (layersMetadata) {
+ this.layers = layersMetadata;
+ if (this.domainObject.configuration) {
+ let persistedLayers = this.domainObject.configuration.layers;
+ layersMetadata.forEach((layer) => {
+ const persistedLayer = persistedLayers.find(object => object.name === layer.name);
+ if (persistedLayer) {
+ layer.visible = persistedLayer.visible === true;
+ }
+ });
+ this.visibleLayers = this.layers.filter(layer => layer.visible);
+ } else {
+ this.visibleLayers = [];
+ this.layers.forEach((layer) => {
+ layer.visible = false;
+ });
+ }
+ }
+ },
+ persistVisibleLayers() {
+ if (this.domainObject.configuration) {
+ this.openmct.objects.mutate(this.domainObject, 'configuration.layers', this.layers);
+ }
+
+ this.visibleLayers = [];
+ this.layers = [];
+ },
// will subscribe to data for this key if not already done
subscribeToDataForKey(key) {
if (this.relatedTelemetry[key].isSubscribed) {
@@ -781,7 +847,8 @@ export default {
if (domThumb) {
domThumb.scrollIntoView({
behavior: 'smooth',
- block: 'center'
+ block: 'center',
+ inline: 'center'
});
}
},
@@ -844,8 +911,10 @@ export default {
let currentTime = this.timeContext.clock() && this.timeContext.clock().currentValue();
if (currentTime === undefined) {
this.numericDuration = currentTime;
- } else {
+ } else if (Number.isInteger(this.parsedSelectedTime)) {
this.numericDuration = currentTime - this.parsedSelectedTime;
+ } else {
+ this.numericDuration = undefined;
}
},
resetAgeCSS() {
@@ -887,10 +956,6 @@ export default {
this.imageTranslateY = 0;
},
handlePanZoomUpdate({ newScaleFactor, screenClientX, screenClientY }) {
- if (!this.isPaused) {
- this.paused(true);
- }
-
if (!(screenClientX || screenClientY)) {
return this.updatePanZoom(newScaleFactor, 0, 0);
}
@@ -1034,15 +1099,11 @@ export default {
this.resizingWindow = false;
});
},
- // debounced method
clearWheelZoom() {
this.$refs.imageControls.clearWheelZoom();
},
wheelZoom(e) {
e.preventDefault();
- if (!this.isPaused) {
- this.paused(true);
- }
this.$refs.imageControls.wheelZoom(e);
},
@@ -1100,6 +1161,11 @@ export default {
},
setCursorStates(states) {
this.cursorStates = states;
+ },
+ toggleLayerVisibility(index) {
+ let isVisible = this.layers[index].visible === true;
+ this.layers[index].visible = !isVisible;
+ this.visibleLayers = this.layers.filter(layer => layer.visible);
}
}
};
diff --git a/src/plugins/imagery/components/ImageryViewMenuSwitcher.vue b/src/plugins/imagery/components/ImageryViewMenuSwitcher.vue
new file mode 100644
index 000000000..784cdff46
--- /dev/null
+++ b/src/plugins/imagery/components/ImageryViewMenuSwitcher.vue
@@ -0,0 +1,65 @@
+<template>
+<div class="c-switcher-menu">
+ <button
+ :id="id"
+ class="c-button c-button--menu c-switcher-menu__button"
+ :class="iconClass"
+ :title="title"
+ @click="toggleMenu"
+ >
+ <span class="c-button__label"></span>
+ </button>
+ <div
+ v-show="showMenu"
+ class="c-switcher-menu__content"
+ >
+ <slot></slot>
+ </div>
+</div>
+</template>
+
+<script>
+import {v4 as uuid} from 'uuid';
+
+export default {
+ inject: ['openmct'],
+ props: {
+ iconClass: {
+ type: String,
+ default() {
+ return '';
+ }
+ },
+ title: {
+ type: String,
+ default() {
+ return '';
+ }
+ }
+ },
+ data() {
+ return {
+ id: uuid(),
+ showMenu: false
+ };
+ },
+ mounted() {
+ document.addEventListener('click', this.hideMenu);
+ },
+ destroyed() {
+ document.removeEventListener('click', this.hideMenu);
+ },
+ methods: {
+ toggleMenu() {
+ this.showMenu = !this.showMenu;
+ },
+ hideMenu(e) {
+ if (this.id === e.target.id) {
+ return;
+ }
+
+ this.showMenu = false;
+ }
+ }
+};
+</script>
diff --git a/src/plugins/imagery/components/LayerSettings.vue b/src/plugins/imagery/components/LayerSettings.vue
new file mode 100644
index 000000000..1e99a0aee
--- /dev/null
+++ b/src/plugins/imagery/components/LayerSettings.vue
@@ -0,0 +1,59 @@
+<template>
+<div
+ class="c-control-menu c-menu--to-left c-menu--has-close-btn c-image-controls"
+ @click="handleClose"
+>
+ <div class="c-checkbox-list js-checkbox-menu c-menu--to-left c-menu--has-close-btn">
+ <ul
+ @click="$event.stopPropagation()"
+ >
+ <li
+ v-for="(layer, index) in layers"
+ :key="index"
+ >
+ <input
+ v-if="layer.visible"
+ :id="index + 'LayerControl'"
+ checked
+ type="checkbox"
+ @change="toggleLayerVisibility(index)"
+ >
+ <input
+ v-else
+ :id="index + 'LayerControl'"
+ type="checkbox"
+ @change="toggleLayerVisibility(index)"
+ >
+ <label :for="index + 'LayerControl'">{{ layer.name }}</label>
+ </li>
+ </ul>
+ </div>
+
+ <button class="c-click-icon icon-x t-btn-close c-switcher-menu__close-button"></button>
+</div>
+</template>
+
+<script>
+export default {
+ inject: ['openmct'],
+ props: {
+ layers: {
+ type: Array,
+ default() {
+ return [];
+ }
+ }
+ },
+ methods: {
+ handleClose(e) {
+ const closeButton = e.target.classList.contains('c-switcher-menu__close-button');
+ if (!closeButton) {
+ e.stopPropagation();
+ }
+ },
+ toggleLayerVisibility(index) {
+ this.$emit('toggleLayerVisibility', index);
+ }
+ }
+};
+</script>
diff --git a/src/plugins/imagery/components/ZoomSettings.vue b/src/plugins/imagery/components/ZoomSettings.vue
new file mode 100644
index 000000000..e53d6289e
--- /dev/null
+++ b/src/plugins/imagery/components/ZoomSettings.vue
@@ -0,0 +1,89 @@
+<template>
+<div
+ class="c-image-controls__controls-wrapper"
+ @click="handleClose"
+>
+ <div class="c-image-controls__control c-image-controls__zoom">
+ <div class="c-button-set c-button-set--strip-h">
+ <button
+ class="c-button t-btn-zoom-out icon-minus"
+ title="Zoom out"
+ @click="zoomOut"
+ ></button>
+
+ <button
+ class="c-button t-btn-zoom-in icon-plus"
+ title="Zoom in"
+ @click="zoomIn"
+ ></button>
+
+ <button
+ class="c-button t-btn-zoom-lock"
+ title="Lock current zoom and pan across all images"
+ :class="{'icon-unlocked': !panZoomLocked, 'icon-lock': panZoomLocked}"
+ @click="toggleZoomLock"
+ ></button>
+
+ <button
+ class="c-button icon-reset t-btn-zoom-reset"
+ title="Remove zoom and pan"
+ @click="handleResetImage"
+ ></button>
+ </div>
+ <div class="c-image-controls__zoom-factor">x{{ formattedZoomFactor }}</div>
+ </div>
+ <button
+ v-if="isMenu"
+ class="c-click-icon icon-x t-btn-close c-switcher-menu__close-button"
+ ></button>
+</div>
+</template>
+
+<script>
+export default {
+ inject: ['openmct'],
+ props: {
+ zoomFactor: {
+ type: Number,
+ required: true
+ },
+ panZoomLocked: {
+ type: Boolean,
+ required: true
+ },
+ isMenu: {
+ type: Boolean,
+ required: false
+ }
+ },
+ data() {
+ return {
+ };
+ },
+ computed: {
+ formattedZoomFactor() {
+ return Number.parseFloat(this.zoomFactor).toPrecision(2);
+ }
+ },
+ methods: {
+ handleClose(e) {
+ const closeButton = e.target.classList.contains('c-switcher-menu__close-button');
+ if (!closeButton) {
+ e.stopPropagation();
+ }
+ },
+ handleResetImage() {
+ this.$emit('handleResetImage');
+ },
+ toggleZoomLock() {
+ this.$emit('toggleZoomLock');
+ },
+ zoomIn() {
+ this.$emit('zoomIn');
+ },
+ zoomOut() {
+ this.$emit('zoomOut');
+ }
+ }
+};
+</script>
diff --git a/src/plugins/imagery/components/imagery-view.scss b/src/plugins/imagery/components/imagery-view.scss
index 8cdae861d..ee1305225 100644
--- a/src/plugins/imagery/components/imagery-view.scss
+++ b/src/plugins/imagery/components/imagery-view.scss
@@ -28,6 +28,27 @@
display: flex;
flex-direction: column;
flex: 1 1 auto;
+
+ &.unnsynced{
+ @include sUnsynced();
+ }
+
+ &.cursor-zoom-in {
+ cursor: zoom-in;
+ }
+
+ &.cursor-zoom-out {
+ cursor: zoom-out;
+ }
+
+ &.pannable {
+ @include cursorGrab();
+ }
+ }
+
+ .image-wrapper {
+ overflow: visible clip;
+ background-image: repeating-linear-gradient(45deg, transparent, transparent 4px, rgba(125, 125, 125, 0.2) 4px, rgba(125, 125, 125, 0.2) 8px);
}
.image-wrapper {
@@ -45,30 +66,26 @@
flex: 1 1 auto;
height: 0;
overflow: hidden;
- &.unnsynced{
- @include sUnsynced();
- }
- &.cursor-zoom-in {
- cursor: zoom-in;
- }
- &.cursor-zoom-out {
- cursor: zoom-out;
- }
- &.pannable {
- @include cursorGrab();
-
- }
}
&__background-image {
+ // Actually does the image display
background-position: center;
background-repeat: no-repeat;
background-size: contain;
+ height: 100%; //fallback value
}
&__image {
+ // Present to allow Save As... image
+ position: absolute;
height: 100%;
width: 100%;
- visibility: hidden;
- display: contents;
+ opacity: 0;
+ }
+
+ &__image-save-proxy {
+ height: 100%;
+ width: 100%;
+ z-index: 10;
}
}
@@ -77,6 +94,7 @@
background: rgba(black, 0.2);
border-radius: $smallCr;
padding: 2px $interiorMargin;
+ pointer-events: none;
position: absolute;
right: $m;
top: $m;
@@ -146,6 +164,11 @@
}
+ &__layer-image {
+ pointer-events: none;
+ z-index: 1;
+ }
+
&__thumbs-wrapper {
display: flex; // Uses row layout
justify-content: flex-end;
@@ -179,6 +202,51 @@
font-size: 0.8em;
margin: $interiorMarginSm;
}
+
+ .c-control-menu {
+ // Controls on left of flex column layout, close btn on right
+ @include menuOuter();
+
+ border-radius: $controlCr;
+ display: flex;
+ align-items: flex-start;
+ flex-direction: row;
+ justify-content: space-between;
+ padding: $interiorMargin;
+ width: max-content;
+
+ > * + * {
+ margin-left: $interiorMargin;
+ }
+ }
+
+ .c-switcher-menu {
+ display: contents;
+
+ &__content {
+ // Menu panel
+ top: 28px;
+ position: absolute;
+
+ .c-so-view & {
+ top: 25px;
+ }
+ }
+ }
+}
+
+.--width-less-than-220 .--show-if-less-than-220.c-switcher-menu {
+ display: contents !important;
+}
+
+.s-image-layer {
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ opacity: 0.5;
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center;
}
/*************************************** THUMBS */
@@ -190,13 +258,22 @@
min-width: $w;
width: $w;
+ &.active {
+ background: $colorSelectedBg;
+ color: $colorSelectedFg;
+ }
&:hover {
background: $colorThumbHoverBg;
}
-
&.selected {
- background: $colorPausedBg !important;
- color: $colorPausedFg !important;
+ // fixed time - selected bg will match active bg color
+ background: $colorSelectedBg;
+ color: $colorSelectedFg;
+ &.real-time {
+ // real time - bg orange when selected
+ background: $colorPausedBg !important;
+ color: $colorPausedFg !important;
+ }
}
&__image {
@@ -229,70 +306,36 @@
/*************************************** IMAGERY LOCAL CONTROLS*/
.c-imagery {
.h-local-controls--overlay-content {
+ display: flex;
+ flex-direction: row;
position: absolute;
left: $interiorMargin; top: $interiorMargin;
z-index: 70;
background: $colorLocalControlOvrBg;
border-radius: $basicCr;
- max-width: 250px;
- min-width: 170px;
- width: 35%;
align-items: center;
- padding: $interiorMargin $interiorMarginLg;
-
- input[type="range"] {
- display: block;
- width: 100%;
- &:not(:first-child) {
- margin-top: $interiorMarginLg;
- }
-
- &:before {
- margin-right: $interiorMarginSm;
- }
- }
+ padding: $interiorMargin $interiorMargin;
.s-status-taking-snapshot & {
display: none;
}
}
-
- &__lc {
- &__reset-btn {
- // Span that holds bracket graphics and button
- $bc: $scrollbarTrackColorBg;
-
- &:before,
- &:after {
- border-right: 1px solid $bc;
- content:'';
- display: block;
- width: 5px;
- height: 4px;
- }
-
- &:before {
- border-top: 1px solid $bc;
- margin-bottom: 2px;
- }
-
- &:after {
- border-bottom: 1px solid $bc;
- margin-top: 2px;
- }
-
- .c-icon-link {
- color: $colorBtnFg;
- }
+ [class*='--menus-aligned'] {
+ > * + * {
+ button { margin-left: $interiorMarginSm; }
}
}
}
.c-image-controls {
+ &__controls-wrapper {
+ // Wraps __controls and __close-btn
+ display: flex;
+ }
+
&__controls {
display: flex;
align-items: stretch;
- flex-direction: column;
> * + * {
margin-top: $interiorMargin;
@@ -305,7 +348,6 @@
&__input {
display: flex;
align-items: center;
- width: 100%;
&:before {
color: rgba($colorMenuFg, 0.5);
@@ -314,30 +356,69 @@
}
- &__input {
- // A wrapper is needed to add the type icon to left of each control
-
- input[type='range'] {
- //width: 100%; // Do we need this?
- }
- }
-
&__zoom {
- > * + * { margin-left: $interiorMargin; }
+ > * + * { margin-left: $interiorMargin; } // Is this used?
}
- &__sliders {
- display: flex;
- flex: 1 1 auto;
- flex-direction: column;
+ &--filters {
+ // Styles specific to the brightness and contrast controls
+ .c-image-controls {
+ &__controls {
+ width: 80px; // About the minimum this element can be; cannot size based on % due to markup structure
+ }
- > * + * {
- margin-top: 11px;
- }
- }
+ &__sliders {
+ display: flex;
+ flex: 1 1 auto;
+ flex-direction: column;
+ width: 100%;
- &__btn-reset {
- flex: 0 0 auto;
+ > * + * {
+ margin-top: 11px;
+ }
+
+ input[type="range"] {
+ display: block;
+ width: 100%;
+ }
+ }
+
+ &__slider-wrapper {
+ display: flex;
+ align-items: center;
+
+ &:before { margin-right: $interiorMargin; }
+ }
+
+ &__reset-btn {
+ // Span that holds bracket graphics and button
+ $bc: $scrollbarTrackColorBg;
+ flex: 0 0 auto;
+
+ &:before,
+ &:after {
+ border-right: 1px solid $bc;
+ content:'';
+ display: block;
+ width: 5px;
+ height: 4px;
+ }
+
+ &:before {
+ border-top: 1px solid $bc;
+ margin-bottom: 2px;
+ }
+
+ &:after {
+ border-bottom: 1px solid $bc;
+ margin-top: 2px;
+ }
+
+ .c-icon-link {
+ color: $colorBtnFg;
+ }
+ }
+ }
}
}
@@ -383,7 +464,7 @@
@include cArrowButtonSizing($dimOuter: 48px);
border-radius: $controlCr;
- .is-in-small-container & {
+ .--width-less-than-600 & {
@include cArrowButtonSizing($dimOuter: 32px);
}
}
@@ -409,10 +490,6 @@
background-color: $colorBodyFg;
}
- //[class*='__image-placeholder'] {
- // display: none;
- //}
-
img {
display: block !important;
}
diff --git a/src/plugins/imagery/layers/example-imagery-layer-16x9.png b/src/plugins/imagery/layers/example-imagery-layer-16x9.png
new file mode 100644
index 000000000..877a0e603
--- /dev/null
+++ b/src/plugins/imagery/layers/example-imagery-layer-16x9.png
Binary files differ
diff --git a/src/plugins/imagery/layers/example-imagery-layer-safe.png b/src/plugins/imagery/layers/example-imagery-layer-safe.png
new file mode 100644
index 000000000..f99907f29
--- /dev/null
+++ b/src/plugins/imagery/layers/example-imagery-layer-safe.png
Binary files differ
diff --git a/src/plugins/imagery/layers/example-imagery-layer-scale.png b/src/plugins/imagery/layers/example-imagery-layer-scale.png
new file mode 100644
index 000000000..7f5f41211
--- /dev/null
+++ b/src/plugins/imagery/layers/example-imagery-layer-scale.png
Binary files differ
diff --git a/src/plugins/imagery/mixins/imageryData.js b/src/plugins/imagery/mixins/imageryData.js
index 933df01c1..94118e681 100644
--- a/src/plugins/imagery/mixins/imageryData.js
+++ b/src/plugins/imagery/mixins/imageryData.js
@@ -30,7 +30,7 @@ export default {
this.timeSystemChange = this.timeSystemChange.bind(this);
this.setDataTimeContext = this.setDataTimeContext.bind(this);
this.setDataTimeContext();
- this.openmct.objectViews.on('clearData', this.clearData);
+ this.openmct.objectViews.on('clearData', this.dataCleared);
// set
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
@@ -44,9 +44,11 @@ export default {
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
- // kickoff
- this.subscribe();
- this.requestHistory();
+ this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {});
+ this.telemetryCollection.on('add', this.dataAdded);
+ this.telemetryCollection.on('remove', this.dataRemoved);
+ this.telemetryCollection.on('clear', this.dataCleared);
+ this.telemetryCollection.load();
},
beforeDestroy() {
if (this.unsubscribe) {
@@ -55,9 +57,34 @@ export default {
}
this.stopFollowingDataTimeContext();
- this.openmct.objectViews.off('clearData', this.clearData);
+ this.openmct.objectViews.off('clearData', this.dataCleared);
+
+ this.telemetryCollection.off('add', this.dataAdded);
+ this.telemetryCollection.off('remove', this.dataRemoved);
+ this.telemetryCollection.off('clear', this.dataCleared);
+
+ this.telemetryCollection.destroy();
},
methods: {
+ dataAdded(dataToAdd) {
+ const normalizedDataToAdd = dataToAdd.map(datum => this.normalizeDatum(datum));
+ this.imageHistory = this.imageHistory.concat(normalizedDataToAdd);
+ },
+ dataCleared() {
+ this.imageHistory = [];
+ },
+ dataRemoved(dataToRemove) {
+ this.imageHistory = this.imageHistory.filter(existingDatum => {
+ const shouldKeep = dataToRemove.some(datumToRemove => {
+ const existingDatumTimestamp = this.parseTime(existingDatum);
+ const datumToRemoveTimestamp = this.parseTime(datumToRemove);
+
+ return (existingDatumTimestamp !== datumToRemoveTimestamp);
+ });
+
+ return shouldKeep;
+ });
+ },
setDataTimeContext() {
this.stopFollowingDataTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
@@ -71,23 +98,6 @@ export default {
this.timeContext.off('timeSystem', this.timeSystemChange);
}
},
- datumIsNotValid(datum) {
- if (this.imageHistory.length === 0) {
- return false;
- }
-
- const datumURL = this.formatImageUrl(datum);
- const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]);
-
- // datum is not valid if it matches the last datum in history,
- // or it is before the last datum in the history
- const datumTimeCheck = this.parseTime(datum);
- const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]);
- const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL);
- const isStale = datumTimeCheck < historyTimeCheck;
-
- return matchesLast || isStale;
- },
formatImageUrl(datum) {
if (!datum) {
return;
@@ -129,48 +139,7 @@ export default {
// forcibly reset the imageContainer size to prevent an aspect ratio distortion
delete this.imageContainerWidth;
delete this.imageContainerHeight;
-
- return this.requestHistory();
- },
- async requestHistory() {
- let bounds = this.timeContext.bounds();
- this.requestCount++;
- const requestId = this.requestCount;
- this.imageHistory = [];
-
- let data = await this.openmct.telemetry
- .request(this.domainObject, bounds) || [];
-
- if (this.requestCount === requestId) {
- let imagery = [];
- data.forEach((datum) => {
- let image = this.normalizeDatum(datum);
- if (image) {
- imagery.push(image);
- }
- });
- //this is to optimize anything that reacts to imageHistory length
- this.imageHistory = imagery;
- }
- },
- clearData(domainObjectToClear) {
- // global clearData button is accepted therefore no truthy check on inputted param
- const clearDataForObjectSelected = Boolean(domainObjectToClear);
- if (clearDataForObjectSelected) {
- const idsEqual = this.openmct.objects.areIdsEqual(
- domainObjectToClear.identifier,
- this.domainObject.identifier
- );
- if (!idsEqual) {
- return;
- }
- }
-
- // splice array to encourage garbage collection
- this.imageHistory.splice(0, this.imageHistory.length);
-
- // requesting history effectively clears imageHistory array
- return this.requestHistory();
+ this.bounds = bounds; // setting bounds for ImageryView watcher
},
timeSystemChange() {
this.timeSystem = this.timeContext.timeSystem();
@@ -178,32 +147,19 @@ export default {
this.timeFormatter = this.getFormatter(this.timeKey);
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
},
- subscribe() {
- this.unsubscribe = this.openmct.telemetry
- .subscribe(this.domainObject, (datum) => {
- let parsedTimestamp = this.parseTime(datum);
- let bounds = this.timeContext.bounds();
-
- if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
- let image = this.normalizeDatum(datum);
- if (image) {
- this.imageHistory.push(image);
- }
- }
- });
- },
normalizeDatum(datum) {
- if (this.datumIsNotValid(datum)) {
- return;
- }
-
- let image = { ...datum };
- image.formattedTime = this.formatTime(datum);
- image.url = this.formatImageUrl(datum);
- image.time = this.parseTime(image.formattedTime);
- image.imageDownloadName = this.getImageDownloadName(datum);
-
- return image;
+ const formattedTime = this.formatTime(datum);
+ const url = this.formatImageUrl(datum);
+ const time = this.parseTime(formattedTime);
+ const imageDownloadName = this.getImageDownloadName(datum);
+
+ return {
+ ...datum,
+ formattedTime,
+ url,
+ time,
+ imageDownloadName
+ };
},
getFormatter(key) {
let metadataValue = this.metadata.value(key) || { format: key };
diff --git a/src/plugins/imagery/pluginSpec.js b/src/plugins/imagery/pluginSpec.js
index 8ba66a24c..e595117a8 100644
--- a/src/plugins/imagery/pluginSpec.js
+++ b/src/plugins/imagery/pluginSpec.js
@@ -84,11 +84,11 @@ describe("The Imagery View Layouts", () => {
let telemetryPromise;
let telemetryPromiseResolve;
let cleanupFirst;
- let isClearDataTriggered;
let openmct;
let parent;
let child;
+ let historicalProvider;
let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT);
let imageryObject = {
identifier: {
@@ -100,61 +100,29 @@ describe("The Imagery View Layouts", () => {
location: "parentId",
modified: 0,
persisted: 0,
+ configuration: {
+ layers: [{
+ name: '16:9',
+ visible: true
+ }]
+ },
telemetry: {
values: [
{
"name": "Image",
"key": "url",
"format": "image",
+ "layers": [
+ {
+ source: location.host + '/images/bg-splash.jpg',
+ name: '16:9'
+ }
+ ],
"hints": {
"image": 1,
"priority": 3
},
"source": "url"
- // "relatedTelemetry": {
- // "heading": {
- // "comparisonFunction": comparisonFunction,
- // "historical": {
- // "telemetryObjectId": "heading",
- // "valueKey": "value"
- // }
- // },
- // "roll": {
- // "comparisonFunction": comparisonFunction,
- // "historical": {
- // "telemetryObjectId": "roll",
- // "valueKey": "value"
- // }
- // },
- // "pitch": {
- // "comparisonFunction": comparisonFunction,
- // "historical": {
- // "telemetryObjectId": "pitch",
- // "valueKey": "value"
- // }
- // },
- // "cameraPan": {
- // "comparisonFunction": comparisonFunction,
- // "historical": {
- // "telemetryObjectId": "cameraPan",
- // "valueKey": "value"
- // }
- // },
- // "cameraTilt": {
- // "comparisonFunction": comparisonFunction,
- // "historical": {
- // "telemetryObjectId": "cameraTilt",
- // "valueKey": "value"
- // }
- // },
- // "sunOrientation": {
- // "comparisonFunction": comparisonFunction,
- // "historical": {
- // "telemetryObjectId": "sunOrientation",
- // "valueKey": "value"
- // }
- // }
- // }
},
{
"name": "Name",
@@ -193,20 +161,19 @@ describe("The Imagery View Layouts", () => {
cleanupFirst = [];
openmct = createOpenMct();
- openmct.time.timeSystem('utc', {
- start: START - (5 * ONE_MINUTE),
- end: START + (5 * ONE_MINUTE)
- });
telemetryPromise = new Promise((resolve) => {
telemetryPromiseResolve = resolve;
});
- spyOn(openmct.telemetry, 'request').and.callFake(() => {
- if (isClearDataTriggered) {
- return [];
+ historicalProvider = {
+ request: () => {
+ return Promise.resolve(imageTelemetry);
}
+ };
+ spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider);
+ spyOn(openmct.telemetry, 'request').and.callFake(() => {
telemetryPromiseResolve(imageTelemetry);
return telemetryPromise;
@@ -325,60 +292,112 @@ describe("The Imagery View Layouts", () => {
expect(imageryView).toBeDefined();
});
- describe("imagery view", () => {
+ describe("Clear data action for imagery", () => {
let applicableViews;
let imageryViewProvider;
let imageryView;
+ let componentView;
let clearDataPlugin;
let clearDataAction;
beforeEach(() => {
+ openmct.time.timeSystem('utc', {
+ start: START - (5 * ONE_MINUTE),
+ end: START + (5 * ONE_MINUTE)
+ });
applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]);
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey);
imageryView = imageryViewProvider.view(imageryObject, [imageryObject]);
imageryView.show(child);
+ componentView = imageryView._getInstance().$children[0];
+
clearDataPlugin = new ClearDataPlugin(
['example.imagery'],
{indicator: true}
);
openmct.install(clearDataPlugin);
clearDataAction = openmct.actions.getAction('clear-data-action');
- // force show the thumbnails
- imageryView._getInstance().$children[0].forceShowThumbnails = true;
return Vue.nextTick();
});
- afterEach(() => {
- isClearDataTriggered = false;
- // openmct.time.stopClock();
- // openmct.router.removeListener('change:hash', resolveFunction);
- // imageryView.destroy();
+
+ it('clear data action is installed', () => {
+ expect(clearDataAction).toBeDefined();
});
- it("on mount should show the the most recent image", (done) => {
- //Looks like we need Vue.nextTick here so that computed properties settle down
+ it('on clearData action should clear data for object is selected', (done) => {
+ // force show the thumbnails
+ componentView.forceShowThumbnails = true;
Vue.nextTick(() => {
- const imageInfo = getImageInfo(parent);
+ let clearDataResolve;
+ let telemetryRequestPromise = new Promise((resolve) => {
+ clearDataResolve = resolve;
+ });
+ expect(parent.querySelectorAll('.c-imagery__thumb').length).not.toBe(0);
- expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
- done();
- });
- });
+ openmct.objectViews.on('clearData', (_domainObject) => {
+ return Vue.nextTick(() => {
+ expect(parent.querySelectorAll('.c-imagery__thumb').length).toBe(0);
- it("should show the clicked thumbnail as the main image", (done) => {
- //Looks like we need Vue.nextTick here so that computed properties settle down
- Vue.nextTick(() => {
- const target = imageTelemetry[5].url;
- parent.querySelectorAll(`img[src='${target}']`)[0].click();
- Vue.nextTick(() => {
- const imageInfo = getImageInfo(parent);
+ clearDataResolve();
+ });
+ });
+ clearDataAction.invoke(imageryObject);
- expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);
+ telemetryRequestPromise.then(() => {
done();
});
});
});
+ });
+
+ describe("imagery view", () => {
+ let applicableViews;
+ let imageryViewProvider;
+ let imageryView;
+
+ beforeEach(() => {
+ openmct.time.timeSystem('utc', {
+ start: START - (5 * ONE_MINUTE),
+ end: START + (5 * ONE_MINUTE)
+ });
+
+ applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]);
+ imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey);
+ imageryView = imageryViewProvider.view(imageryObject, [imageryObject]);
+ imageryView.show(child);
+
+ imageryView._getInstance().$children[0].forceShowThumbnails = true;
+
+ return Vue.nextTick();
+ });
+
+ it("on mount should show the the most recent image", async () => {
+ //Looks like we need Vue.nextTick here so that computed properties settle down
+ await Vue.nextTick();
+ const imageInfo = getImageInfo(parent);
+ expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
+ });
+
+ it("on mount should show the any image layers", async () => {
+ //Looks like we need Vue.nextTick here so that computed properties settle down
+ await Vue.nextTick();
+ const layerEls = parent.querySelectorAll('.js-layer-image');
+ console.log(layerEls);
+ expect(layerEls.length).toEqual(1);
+ });
+
+ it("should show the clicked thumbnail as the main image", async () => {
+ //Looks like we need Vue.nextTick here so that computed properties settle down
+ await Vue.nextTick();
+ const target = imageTelemetry[5].url;
+ parent.querySelectorAll(`img[src='${target}']`)[0].click();
+ await Vue.nextTick();
+ const imageInfo = getImageInfo(parent);
+
+ expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);
+ });
xit("should show that an image is new", (done) => {
openmct.time.clock('local', {
@@ -396,71 +415,60 @@ describe("The Imagery View Layouts", () => {
});
});
- it("should show that an image is not new", (done) => {
- Vue.nextTick(() => {
- const target = imageTelemetry[2].url;
- parent.querySelectorAll(`img[src='${target}']`)[0].click();
+ it("should show that an image is not new", async () => {
+ await Vue.nextTick();
+ const target = imageTelemetry[4].url;
+ parent.querySelectorAll(`img[src='${target}']`)[0].click();
- Vue.nextTick(() => {
- const imageIsNew = isNew(parent);
+ await Vue.nextTick();
+ const imageIsNew = isNew(parent);
- expect(imageIsNew).toBeFalse();
- done();
- });
- });
+ expect(imageIsNew).toBeFalse();
});
- it("should navigate via arrow keys", (done) => {
- Vue.nextTick(() => {
- let keyOpts = {
- element: parent.querySelector('.c-imagery'),
- key: 'ArrowLeft',
- keyCode: 37,
- type: 'keyup'
- };
+ it("should navigate via arrow keys", async () => {
+ await Vue.nextTick();
+ const keyOpts = {
+ element: parent.querySelector('.c-imagery'),
+ key: 'ArrowLeft',
+ keyCode: 37,
+ type: 'keyup'
+ };
- simulateKeyEvent(keyOpts);
+ simulateKeyEvent(keyOpts);
- Vue.nextTick(() => {
- const imageInfo = getImageInfo(parent);
-
- expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);
- done();
- });
- });
+ await Vue.nextTick();
+ const imageInfo = getImageInfo(parent);
+ expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);
});
- it("should navigate via numerous arrow keys", (done) => {
- Vue.nextTick(() => {
- let element = parent.querySelector('.c-imagery');
- let type = 'keyup';
- let leftKeyOpts = {
- element,
- type,
- key: 'ArrowLeft',
- keyCode: 37
- };
- let rightKeyOpts = {
- element,
- type,
- key: 'ArrowRight',
- keyCode: 39
- };
-
- // left thrice
- simulateKeyEvent(leftKeyOpts);
- simulateKeyEvent(leftKeyOpts);
- simulateKeyEvent(leftKeyOpts);
- // right once
- simulateKeyEvent(rightKeyOpts);
-
- Vue.nextTick(() => {
- const imageInfo = getImageInfo(parent);
+ it("should navigate via numerous arrow keys", async () => {
+ await Vue.nextTick();
+ const element = parent.querySelector('.c-imagery');
+ const type = 'keyup';
+ const leftKeyOpts = {
+ element,
+ type,
+ key: 'ArrowLeft',
+ keyCode: 37
+ };
+ const rightKeyOpts = {
+ element,
+ type,
+ key: 'ArrowRight',
+ keyCode: 39
+ };
+
+ // left thrice
+ simulateKeyEvent(leftKeyOpts);
+ simulateKeyEvent(leftKeyOpts);
+ simulateKeyEvent(leftKeyOpts);
+ // right once
+ simulateKeyEvent(rightKeyOpts);
- expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);
- done();
- });
- });
+ await Vue.nextTick();
+ const imageInfo = getImageInfo(parent);
+ expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);
});
it ('shows an auto scroll button when scroll to left', (done) => {
Vue.nextTick(() => {
@@ -521,23 +529,32 @@ describe("The Imagery View Layouts", () => {
done();
});
- it('clear data action is installed', () => {
- expect(clearDataAction).toBeDefined();
- });
-
- it('on clearData action should clear data for object is selected', async (done) => {
- // force show the thumbnails
- imageryView._getInstance().$children[0].forceShowThumbnails = true;
+ it('should reset the brightness and contrast when clicking the reset button', async () => {
+ const viewInstance = imageryView._getInstance();
await Vue.nextTick();
- expect(parent.querySelectorAll('.c-imagery__thumb').length).not.toBe(0);
- openmct.objectViews.on('clearData', async (_domainObject) => {
- await Vue.nextTick();
- expect(parent.querySelectorAll('.c-imagery__thumb').length).toBe(0);
- done();
+
+ // Save the original brightness and contrast values
+ const origBrightness = viewInstance.$refs.ImageryContainer.filters.brightness;
+ const origContrast = viewInstance.$refs.ImageryContainer.filters.contrast;
+
+ // Change them to something else (default: 100)
+ viewInstance.$refs.ImageryContainer.setFilters({
+ brightness: 200,
+ contrast: 200
});
- // stubbed telemetry data will return empty array when true
- isClearDataTriggered = true;
- clearDataAction.invoke(imageryObject);
+ await Vue.nextTick();
+
+ // Verify that the values actually changed
+ expect(viewInstance.$refs.ImageryContainer.filters.brightness).toBe(200);
+ expect(viewInstance.$refs.ImageryContainer.filters.contrast).toBe(200);
+
+ // Click the reset button
+ parent.querySelector('.t-btn-reset').click();
+ await Vue.nextTick();
+
+ // Verify that the values were reset
+ expect(viewInstance.$refs.ImageryContainer.filters.brightness).toBe(origBrightness);
+ expect(viewInstance.$refs.ImageryContainer.filters.contrast).toBe(origContrast);
});
});
@@ -553,6 +570,20 @@ describe("The Imagery View Layouts", () => {
end: START + (5 * ONE_MINUTE)
});
+ const mockClock = jasmine.createSpyObj("clock", [
+ "on",
+ "off",
+ "currentValue"
+ ]);
+ mockClock.key = 'mockClock';
+ mockClock.currentValue.and.returnValue(1);
+
+ openmct.time.addClock(mockClock);
+ openmct.time.clock('mockClock', {
+ start: START - (5 * ONE_MINUTE),
+ end: START + (5 * ONE_MINUTE)
+ });
+
openmct.router.path = [{
identifier: {
key: 'test-timestrip',
@@ -587,7 +618,7 @@ describe("The Imagery View Layouts", () => {
it("on mount should show imagery within the given bounds", (done) => {
Vue.nextTick(() => {
const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper');
- expect(imageElements.length).toEqual(6);
+ expect(imageElements.length).toEqual(5);
done();
});
});
@@ -607,5 +638,46 @@ describe("The Imagery View Layouts", () => {
});
});
});
+
+ it("should remove images when clock advances", async () => {
+ openmct.time.tick(ONE_MINUTE * 2);
+ await Vue.nextTick();
+ await Vue.nextTick();
+ const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper');
+ expect(imageElements.length).toEqual(4);
+ });
+
+ it("should remove images when start bounds shorten", async () => {
+ openmct.time.timeSystem('utc', {
+ start: START,
+ end: START + (5 * ONE_MINUTE)
+ });
+ await Vue.nextTick();
+ await Vue.nextTick();
+ const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper');
+ expect(imageElements.length).toEqual(1);
+ });
+
+ it("should remove images when end bounds shorten", async () => {
+ openmct.time.timeSystem('utc', {
+ start: START - (5 * ONE_MINUTE),
+ end: START - (2 * ONE_MINUTE)
+ });
+ await Vue.nextTick();
+ await Vue.nextTick();
+ const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper');
+ expect(imageElements.length).toEqual(4);
+ });
+
+ it("should remove images when both bounds shorten", async () => {
+ openmct.time.timeSystem('utc', {
+ start: START - (2 * ONE_MINUTE),
+ end: START + (2 * ONE_MINUTE)
+ });
+ await Vue.nextTick();
+ await Vue.nextTick();
+ const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper');
+ expect(imageElements.length).toEqual(3);
+ });
});
});
diff --git a/src/plugins/importFromJSONAction/ImportFromJSONAction.js b/src/plugins/importFromJSONAction/ImportFromJSONAction.js
index a040f5bb7..5b28c0f60 100644
--- a/src/plugins/importFromJSONAction/ImportFromJSONAction.js
+++ b/src/plugins/importFromJSONAction/ImportFromJSONAction.js
@@ -21,7 +21,7 @@
*****************************************************************************/
import objectUtils from 'objectUtils';
-import uuid from "uuid";
+import { v4 as uuid } from 'uuid';
export default class ImportAsJSONAction {
constructor(openmct) {
@@ -82,12 +82,14 @@ export default class ImportAsJSONAction {
* @param {object} seen
*/
_deepInstantiate(parent, tree, seen) {
- if (this.openmct.composition.get(parent)) {
+ let objectIdentifiers = this._getObjectReferenceIds(parent);
+
+ if (objectIdentifiers.length) {
let newObj;
seen.push(parent.id);
- parent.composition.forEach(async (childId) => {
+ objectIdentifiers.forEach(async (childId) => {
const keystring = this.openmct.objects.makeKeyString(childId);
if (!tree[keystring] || seen.includes(keystring)) {
return;
@@ -103,6 +105,27 @@ export default class ImportAsJSONAction {
}
/**
* @private
+ * @param {object} parent
+ * @returns [identifiers]
+ */
+ _getObjectReferenceIds(parent) {
+ let objectIdentifiers = [];
+
+ let parentComposition = this.openmct.composition.get(parent);
+ if (parentComposition) {
+ objectIdentifiers = Array.from(parentComposition.domainObject.composition);
+ }
+
+ //conditional object styles are not saved on the composition, so we need to check for them
+ let parentObjectReference = parent.configuration?.objectStyles?.conditionSetIdentifier;
+ if (parentObjectReference) {
+ objectIdentifiers.push(parentObjectReference);
+ }
+
+ return objectIdentifiers;
+ }
+ /**
+ * @private
* @param {object} tree
* @param {string} namespace
* @returns {object}
diff --git a/src/plugins/interceptors/missingObjectInterceptor.js b/src/plugins/interceptors/missingObjectInterceptor.js
index 4a2167080..9eb2134d1 100644
--- a/src/plugins/interceptors/missingObjectInterceptor.js
+++ b/src/plugins/interceptors/missingObjectInterceptor.js
@@ -27,10 +27,13 @@ export default function MissingObjectInterceptor(openmct) {
},
invoke: (identifier, object) => {
if (object === undefined) {
+ const keyString = openmct.objects.makeKeyString(identifier);
+ openmct.notifications.error(`Failed to retrieve object ${keyString}`);
+
return {
identifier,
type: 'unknown',
- name: 'Missing: ' + openmct.objects.makeKeyString(identifier)
+ name: 'Missing: ' + keyString
};
}
diff --git a/src/plugins/licenses/third-party-licenses.json b/src/plugins/licenses/third-party-licenses.json
index 184024eb4..c139298ce 100644
--- a/src/plugins/licenses/third-party-licenses.json
+++ b/src/plugins/licenses/third-party-licenses.json
@@ -256,13 +256,6 @@
"licenseFile": "/Users/akhenry/Code/licenses/node_modules/vue/LICENSE",
"licenseText": "The MIT License (MIT)\n\nCopyright (c) 2013-present, Yuxi (Evan) You\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.",
"copyright": "Copyright (c) 2013-present, Yuxi (Evan) You"
- },
- "zepto@1.2.0": {
- "licenses": "MIT",
- "repository": "https://github.com/madrobby/zepto",
- "path": "/Users/akhenry/Code/licenses/node_modules/zepto",
- "licenseFile": "/Users/akhenry/Code/licenses/node_modules/zepto/README.md",
- "licenseText": "Copyright (c) 2010-2018 Thomas Fuchs\nhttp://zeptojs.com/\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
}
}
diff --git a/src/plugins/linkAction/LinkAction.js b/src/plugins/linkAction/LinkAction.js
index 9390b3e5a..576e53d35 100644
--- a/src/plugins/linkAction/LinkAction.js
+++ b/src/plugins/linkAction/LinkAction.js
@@ -83,7 +83,6 @@ export default class LinkAction {
}
]
};
-
this.openmct.forms.showForm(formStructure)
.then(this.onSave.bind(this));
}
@@ -91,8 +90,8 @@ export default class LinkAction {
validate(currentParent) {
return (data) => {
- // default current parent to ROOT, if it's undefined, then it's a root level item
- if (currentParent === undefined) {
+ // default current parent to ROOT, if it's null, then it's a root level item
+ if (!currentParent) {
currentParent = {
identifier: {
key: 'ROOT',
@@ -101,24 +100,23 @@ export default class LinkAction {
};
}
- const parentCandidate = data.value[0];
- const currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
- const parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
+ const parentCandidatePath = data.value;
+ const parentCandidate = parentCandidatePath[0];
const objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {
return false;
}
- if (!parentCandidateKeystring || !currentParentKeystring) {
- return false;
- }
-
- if (parentCandidateKeystring === currentParentKeystring) {
+ // check if moving to same place
+ if (this.openmct.objects.areIdsEqual(parentCandidate.identifier, currentParent.identifier)) {
return false;
}
- if (parentCandidateKeystring === objectKeystring) {
+ // check if moving to a child
+ if (parentCandidatePath.some(candidatePath => {
+ return this.openmct.objects.areIdsEqual(candidatePath.identifier, this.object.identifier);
+ })) {
return false;
}
diff --git a/src/plugins/localStorage/LocalStorageObjectProvider.js b/src/plugins/localStorage/LocalStorageObjectProvider.js
index 679435b5f..293fb1c60 100644
--- a/src/plugins/localStorage/LocalStorageObjectProvider.js
+++ b/src/plugins/localStorage/LocalStorageObjectProvider.js
@@ -41,6 +41,10 @@ export default class LocalStorageObjectProvider {
}
}
+ getAllObjects() {
+ return this.getSpaceAsObject();
+ }
+
create(object) {
return this.persistObject(object);
}
diff --git a/src/plugins/move/MoveAction.js b/src/plugins/move/MoveAction.js
index d9a4d144e..594317c3b 100644
--- a/src/plugins/move/MoveAction.js
+++ b/src/plugins/move/MoveAction.js
@@ -145,25 +145,23 @@ export default class MoveAction {
const parentCandidatePath = data.value;
const parentCandidate = parentCandidatePath[0];
- if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {
+ // check if moving to same place
+ if (this.openmct.objects.areIdsEqual(parentCandidate.identifier, currentParent.identifier)) {
return false;
}
- let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
- let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
- let objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
-
- if (!parentCandidateKeystring || !currentParentKeystring) {
+ // check if moving to a child
+ if (parentCandidatePath.some(candidatePath => {
+ return this.openmct.objects.areIdsEqual(candidatePath.identifier, this.object.identifier);
+ })) {
return false;
}
- if (parentCandidateKeystring === currentParentKeystring) {
+ if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {
return false;
}
- if (parentCandidateKeystring === objectKeystring) {
- return false;
- }
+ let objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
const parentCandidateComposition = parentCandidate.composition;
if (parentCandidateComposition && parentCandidateComposition.indexOf(objectKeystring) !== -1) {
diff --git a/src/plugins/move/pluginSpec.js b/src/plugins/move/pluginSpec.js
index bf96dab0d..3e33b05f9 100644
--- a/src/plugins/move/pluginSpec.js
+++ b/src/plugins/move/pluginSpec.js
@@ -91,6 +91,7 @@ describe("The Move Action plugin", () => {
});
describe("when moving an object to a new parent and removing from the old parent", () => {
+ let unObserve;
beforeEach((done) => {
openmct.router.path = [];
@@ -104,7 +105,7 @@ describe("The Move Action plugin", () => {
});
});
- openmct.objects.observe(parentObject, '*', (newObject) => {
+ unObserve = openmct.objects.observe(parentObject, '*', (newObject) => {
done();
});
@@ -113,6 +114,10 @@ describe("The Move Action plugin", () => {
moveAction.invoke([childObject, parentObject]);
});
+ afterEach(() => {
+ unObserve();
+ });
+
it("the child object's identifier should be in the new parent's composition", () => {
let newParentChild = anotherParentObject.composition[0];
expect(newParentChild).toEqual(childObject.identifier);
diff --git a/src/plugins/myItems/pluginSpec.js b/src/plugins/myItems/pluginSpec.js
index b463b0a5e..867fbf243 100644
--- a/src/plugins/myItems/pluginSpec.js
+++ b/src/plugins/myItems/pluginSpec.js
@@ -69,27 +69,27 @@ describe("the plugin", () => {
});
describe('adds an interceptor that returns a "My Items" model for', () => {
- let myItemsMissing;
- let mockMissingProvider;
+ let myItemsObject;
+ let mockNotFoundProvider;
let activeProvider;
beforeEach(async () => {
- mockMissingProvider = {
- get: () => Promise.resolve(missingObj),
+ mockNotFoundProvider = {
+ get: () => Promise.reject(new Error('Not found')),
create: () => Promise.resolve(missingObj),
update: () => Promise.resolve(missingObj)
};
- activeProvider = mockMissingProvider;
+ activeProvider = mockNotFoundProvider;
spyOn(openmct.objects, 'getProvider').and.returnValue(activeProvider);
- myItemsMissing = await openmct.objects.get(myItemsIdentifier);
+ myItemsObject = await openmct.objects.get(myItemsIdentifier);
});
it('missing objects', () => {
- let idsMatchMissing = openmct.objects.areIdsEqual(myItemsMissing.identifier, myItemsIdentifier);
+ let idsMatch = openmct.objects.areIdsEqual(myItemsObject.identifier, myItemsIdentifier);
- expect(myItemsMissing).toBeDefined();
- expect(idsMatchMissing).toBeTrue();
+ expect(myItemsObject).toBeDefined();
+ expect(idsMatch).toBeTrue();
});
});
diff --git a/src/plugins/newFolderAction/pluginSpec.js b/src/plugins/newFolderAction/pluginSpec.js
index 1880b11d5..084df1ed9 100644
--- a/src/plugins/newFolderAction/pluginSpec.js
+++ b/src/plugins/newFolderAction/pluginSpec.js
@@ -49,6 +49,7 @@ describe("the plugin", () => {
let parentObject;
let parentObjectPath;
let changedParentObject;
+ let unobserve;
beforeEach((done) => {
parentObject = {
name: 'mock folder',
@@ -73,7 +74,7 @@ describe("the plugin", () => {
});
});
- openmct.objects.observe(parentObject, '*', (newObject) => {
+ unobserve = openmct.objects.observe(parentObject, '*', (newObject) => {
changedParentObject = newObject;
done();
@@ -81,6 +82,9 @@ describe("the plugin", () => {
newFolderAction.invoke(parentObjectPath);
});
+ afterEach(() => {
+ unobserve();
+ });
it('creates a new folder object', () => {
expect(openmct.objects.save).toHaveBeenCalled();
diff --git a/src/plugins/notebook/NotebookType.js b/src/plugins/notebook/NotebookType.js
new file mode 100644
index 000000000..d112502b7
--- /dev/null
+++ b/src/plugins/notebook/NotebookType.js
@@ -0,0 +1,88 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 { IMAGE_MIGRATION_VER } from '../notebook/utils/notebook-migration';
+
+export default class NotebookType {
+ constructor(name, description, icon) {
+ this.name = name;
+ this.description = description;
+ this.cssClass = icon;
+ this.creatable = true;
+ this.form = [
+ {
+ key: 'defaultSort',
+ name: 'Entry Sorting',
+ control: 'select',
+ options: [
+ {
+ name: 'Newest First',
+ value: "newest"
+ },
+ {
+ name: 'Oldest First',
+ value: "oldest"
+ }
+ ],
+ cssClass: 'l-inline',
+ property: [
+ "configuration",
+ "defaultSort"
+ ]
+ },
+ {
+ key: 'sectionTitle',
+ name: 'Section Title',
+ control: 'textfield',
+ cssClass: 'l-inline',
+ required: true,
+ property: [
+ "configuration",
+ "sectionTitle"
+ ]
+ },
+ {
+ key: 'pageTitle',
+ name: 'Page Title',
+ control: 'textfield',
+ cssClass: 'l-inline',
+ required: true,
+ property: [
+ "configuration",
+ "pageTitle"
+ ]
+ }
+ ];
+ }
+
+ initialize(domainObject) {
+ domainObject.configuration = {
+ defaultSort: 'oldest',
+ entries: {},
+ imageMigrationVer: IMAGE_MIGRATION_VER,
+ pageTitle: 'Page',
+ sections: [],
+ sectionTitle: 'Section',
+ type: 'General'
+ };
+ }
+}
diff --git a/src/plugins/notebook/NotebookViewProvider.js b/src/plugins/notebook/NotebookViewProvider.js
new file mode 100644
index 000000000..66617789c
--- /dev/null
+++ b/src/plugins/notebook/NotebookViewProvider.js
@@ -0,0 +1,72 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 Vue from 'vue';
+import Notebook from './components/Notebook.vue';
+import Agent from '@/utils/agent/Agent';
+
+export default class NotebookViewProvider {
+ constructor(openmct, name, key, type, cssClass, snapshotContainer) {
+ this.openmct = openmct;
+ this.key = key;
+ this.name = `${name} View`;
+ this.type = type;
+ this.cssClass = cssClass;
+ this.snapshotContainer = snapshotContainer;
+ }
+
+ canView(domainObject) {
+ return domainObject.type === this.type;
+ }
+
+ view(domainObject) {
+ let component;
+ let openmct = this.openmct;
+ let snapshotContainer = this.snapshotContainer;
+ let agent = new Agent(window);
+
+ return {
+ show(container) {
+ component = new Vue({
+ el: container,
+ components: {
+ Notebook
+ },
+ provide: {
+ openmct,
+ snapshotContainer,
+ agent
+ },
+ data() {
+ return {
+ domainObject
+ };
+ },
+ template: '<Notebook :domain-object="domainObject"></Notebook>'
+ });
+ },
+ destroy() {
+ component.$destroy();
+ }
+ };
+ }
+}
diff --git a/src/plugins/notebook/components/Notebook.vue b/src/plugins/notebook/components/Notebook.vue
index f045832a9..9719de33e 100644
--- a/src/plugins/notebook/components/Notebook.vue
+++ b/src/plugins/notebook/components/Notebook.vue
@@ -21,7 +21,10 @@
*****************************************************************************/
<template>
-<div class="c-notebook">
+<div
+ class="c-notebook"
+ :class="[{'c-notebook--restricted' : isRestricted }]"
+>
<div class="c-notebook__head">
<Search
class="c-notebook__search"
@@ -119,6 +122,7 @@
</div>
</div>
<div
+ v-if="selectedPage && !selectedPage.isLocked"
class="c-notebook__drag-area icon-plus"
@click="newEntry()"
@dragover="dragOver"
@@ -130,9 +134,17 @@
</span>
</div>
<div
+ v-if="selectedPage && selectedPage.isLocked"
+ class="c-notebook__page-locked"
+ >
+ <div class="icon-lock"></div>
+ <div class="c-notebook__page-locked__message">This page has been committed and cannot be modified or removed</div>
+ </div>
+ <div
v-if="selectedSection && selectedPage"
ref="notebookEntries"
class="c-notebook__entries"
+ aria-label="Notebook Entries"
>
<NotebookEntry
v-for="entry in filteredAndSortedEntries"
@@ -142,12 +154,24 @@
:selected-page="selectedPage"
:selected-section="selectedSection"
:read-only="false"
+ :is-locked="selectedPage.isLocked"
@cancelEdit="cancelTransaction"
@editingEntry="startTransaction"
@deleteEntry="deleteEntry"
@updateEntry="updateEntry"
/>
</div>
+ <div
+ v-if="showLockButton"
+ class="c-notebook__commit-entries-control"
+ >
+ <button
+ class="c-button c-button--major commit-button icon-lock"
+ title="Commit entries and lock this page from further changes"
+ @click="lockPage()"
+ >
+ <span class="c-button__label">Commit Entries</span>
+ </button></div>
</div>
</div>
</div>
@@ -161,7 +185,7 @@ import Sidebar from './Sidebar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSectionId, setDefaultNotebookPageId } from '../utils/notebook-storage';
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
-import { NOTEBOOK_VIEW_TYPE } from '../notebook-constants';
+import { isNotebookViewType, RESTRICTED_NOTEBOOK_TYPE } from '../notebook-constants';
import { debounce } from 'lodash';
import objectLink from '../../../ui/mixins/object-link';
@@ -177,7 +201,7 @@ export default {
SearchResults,
Sidebar
},
- inject: ['openmct', 'snapshotContainer'],
+ inject: ['agent', 'openmct', 'snapshotContainer'],
props: {
domainObject: {
type: Object,
@@ -192,27 +216,16 @@ export default {
selectedPageId: this.getSelectedPageId(),
defaultSort: this.domainObject.configuration.defaultSort,
focusEntryId: null,
+ isRestricted: this.domainObject.type === RESTRICTED_NOTEBOOK_TYPE,
search: '',
searchResults: [],
showTime: this.domainObject.configuration.showTime || 0,
showNav: false,
- sidebarCoversEntries: false
+ sidebarCoversEntries: false,
+ filteredAndSortedEntries: []
};
},
computed: {
- filteredAndSortedEntries() {
- const filterTime = Date.now();
- const pageEntries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage) || [];
-
- const hours = parseInt(this.showTime, 10);
- const filteredPageEntriesByTime = hours
- ? pageEntries.filter(entry => (filterTime - entry.createdOn) <= hours * 60 * 60 * 1000)
- : pageEntries;
-
- return this.defaultSort === 'oldest'
- ? filteredPageEntriesByTime
- : [...filteredPageEntriesByTime].reverse();
- },
pages() {
return this.getPages() || [];
},
@@ -253,6 +266,11 @@ export default {
}
return this.sections[0];
+ },
+ showLockButton() {
+ const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
+
+ return entries && entries.length > 0 && this.isRestricted && !this.selectedPage.isLocked;
}
},
watch: {
@@ -261,6 +279,7 @@ export default {
},
defaultSort() {
mutateObject(this.openmct, this.domainObject, 'configuration.defaultSort', this.defaultSort);
+ this.filterAndSortEntries();
},
showTime() {
mutateObject(this.openmct, this.domainObject, 'configuration.showTime', this.showTime);
@@ -276,12 +295,18 @@ export default {
window.addEventListener('orientationchange', this.formatSidebar);
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
+ this.filterAndSortEntries();
+ this.unobserveEntries = this.openmct.objects.observe(this.domainObject, '*', this.filterAndSortEntries);
},
beforeDestroy() {
if (this.unlisten) {
this.unlisten();
}
+ if (this.unobserveEntries) {
+ this.unobserveEntries();
+ }
+
window.removeEventListener('orientationchange', this.formatSidebar);
window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
},
@@ -292,7 +317,7 @@ export default {
},
methods: {
changeSectionPage(newParams, oldParams, changedParams) {
- if (newParams.view !== NOTEBOOK_VIEW_TYPE) {
+ if (isNotebookViewType(newParams.view)) {
return;
}
@@ -313,6 +338,19 @@ export default {
}
});
},
+ filterAndSortEntries() {
+ const filterTime = Date.now();
+ const pageEntries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage) || [];
+
+ const hours = parseInt(this.showTime, 10);
+ const filteredPageEntriesByTime = hours
+ ? pageEntries.filter(entry => (filterTime - entry.createdOn) <= hours * 60 * 60 * 1000)
+ : pageEntries;
+
+ this.filteredAndSortedEntries = this.defaultSort === 'oldest'
+ ? filteredPageEntriesByTime
+ : [...filteredPageEntriesByTime].reverse();
+ },
changeSelectedSection({ sectionId, pageId }) {
const sections = this.sections.map(s => {
s.isSelected = false;
@@ -345,6 +383,43 @@ export default {
this.removeDefaultClass(this.domainObject.identifier);
clearDefaultNotebook();
},
+ lockPage() {
+ let prompt = this.openmct.overlays.dialog({
+ iconClass: 'alert',
+ message: "This action will lock this page and disallow any new entries, or editing of existing entries. Do you want to continue?",
+ buttons: [
+ {
+ label: 'Lock Page',
+ callback: () => {
+ let sections = this.getSections();
+ this.selectedPage.isLocked = true;
+
+ // cant be default if it's locked
+ if (this.selectedPage.id === this.defaultPageId) {
+ this.cleanupDefaultNotebook();
+ }
+
+ if (!this.selectedSection.isLocked) {
+ this.selectedSection.isLocked = true;
+ }
+
+ mutateObject(this.openmct, this.domainObject, 'configuration.sections', sections);
+
+ if (!this.domainObject.locked) {
+ mutateObject(this.openmct, this.domainObject, 'locked', true);
+ }
+
+ prompt.dismiss();
+ }
+ }, {
+ label: 'Cancel',
+ callback: () => {
+ prompt.dismiss();
+ }
+ }
+ ]
+ });
+ },
setSectionAndPageFromUrl() {
let sectionId = this.getSectionIdFromUrl() || this.getDefaultSectionId() || this.getSelectedSectionId();
let pageId = this.getPageIdFromUrl() || this.getDefaultPageId() || this.getSelectedPageId();
@@ -384,16 +459,40 @@ export default {
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
entries.splice(entryPos, 1);
this.updateEntries(entries);
+ this.filterAndSortEntries();
+ this.removeAnnotations(entryId);
dialog.dismiss();
}
},
{
label: "Cancel",
- callback: () => dialog.dismiss()
+ callback: () => {
+ dialog.dismiss();
+ }
}
]
});
},
+ async removeAnnotations(entryId) {
+ const targetKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
+ const query = {
+ targetKeyString,
+ entryId
+ };
+ const existingAnnotation = await this.openmct.annotation.getAnnotation(query, this.openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS);
+ this.openmct.annotation.removeAnnotationTags(existingAnnotation);
+ },
+ checkEntryPos(entry) {
+ const entryPos = getEntryPosById(entry.id, this.domainObject, this.selectedSection, this.selectedPage);
+ if (entryPos === -1) {
+ this.openmct.notifications.alert('Warning: unable to tag entry');
+ console.error(`unable to tag entry ${entry} from section ${this.selectedSection}, page ${this.selectedPage}`);
+
+ return false;
+ }
+
+ return true;
+ },
dragOver(event) {
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
@@ -404,7 +503,7 @@ export default {
this.openmct.editor.cancel();
}
},
- dropOnEntry(event) {
+ async dropOnEntry(event) {
event.preventDefault();
event.stopImmediatePropagation();
@@ -430,7 +529,8 @@ export default {
objectPath,
openmct: this.openmct
};
- const embed = createNewEmbed(snapshotMeta);
+ const embed = await createNewEmbed(snapshotMeta);
+
this.newEntry(embed);
},
focusOnEntryId() {
@@ -455,12 +555,9 @@ export default {
- tablet portrait
- in a layout frame (within .c-so-view)
*/
- const classList = document.querySelector('body').classList;
- const isPhone = Array.from(classList).includes('phone');
- const isTablet = Array.from(classList).includes('tablet');
- // address in https://github.com/nasa/openmct/issues/4875
- // eslint-disable-next-line compat/compat
- const isPortrait = window.screen.orientation.type.includes('portrait');
+ const isPhone = this.agent.isPhone();
+ const isTablet = this.agent.isTablet();
+ const isPortrait = this.agent.isPortrait();
const isInLayout = Boolean(this.$el.closest('.c-so-view'));
const sidebarCoversEntries = (isPhone || (isTablet && isPortrait) || isInLayout);
this.sidebarCoversEntries = sidebarCoversEntries;
@@ -614,13 +711,13 @@ export default {
return section.id;
},
- newEntry(embed = null) {
+ async newEntry(embed = null) {
this.resetSearch();
const notebookStorage = this.createNotebookStorageObject();
this.updateDefaultNotebook(notebookStorage);
- addNotebookEntry(this.openmct, this.domainObject, notebookStorage, embed).then(id => {
- this.focusEntryId = id;
- });
+ const id = await addNotebookEntry(this.openmct, this.domainObject, notebookStorage, embed);
+ this.focusEntryId = id;
+ this.filterAndSortEntries();
},
orientationChange() {
this.formatSidebar();
@@ -740,6 +837,7 @@ export default {
this.selectedPageId = pageId;
this.syncUrlWithPageAndSection();
+ this.filterAndSortEntries();
},
selectSection(sectionId) {
if (!sectionId) {
@@ -752,6 +850,7 @@ export default {
this.selectPage(pageId);
this.syncUrlWithPageAndSection();
+ this.filterAndSortEntries();
},
activeTransaction() {
return this.openmct.objects.getActiveTransaction();
diff --git a/src/plugins/notebook/components/NotebookEmbed.vue b/src/plugins/notebook/components/NotebookEmbed.vue
index 2fdd4d7de..49cace8f2 100644
--- a/src/plugins/notebook/components/NotebookEmbed.vue
+++ b/src/plugins/notebook/components/NotebookEmbed.vue
@@ -51,6 +51,12 @@ export default {
return {};
}
},
+ isLocked: {
+ type: Boolean,
+ default() {
+ return false;
+ }
+ },
isSnapshotContainer: {
type: Boolean,
default() {
@@ -79,6 +85,15 @@ export default {
: this.embed.snapshot.src;
}
},
+ watch: {
+ isLocked(value) {
+ if (value === true) {
+ let index = this.popupMenuItems.findIndex((item) => item.id === 'removeEmbed');
+
+ this.$delete(this.popupMenuItems, index);
+ }
+ }
+ },
mounted() {
this.addPopupMenuItems();
this.imageExporter = new ImageExporter(this.openmct);
@@ -86,17 +101,24 @@ export default {
methods: {
addPopupMenuItems() {
const removeEmbed = {
+ id: 'removeEmbed',
cssClass: 'icon-trash',
name: this.removeActionString,
callback: this.getRemoveDialog.bind(this)
};
const preview = {
+ id: 'preview',
cssClass: 'icon-eye-open',
name: 'Preview',
callback: this.previewEmbed.bind(this)
};
- this.popupMenuItems = [removeEmbed, preview];
+ this.popupMenuItems = [preview];
+
+ if (!this.isLocked) {
+ this.popupMenuItems.unshift(removeEmbed);
+ }
+
},
annotateSnapshot() {
const annotateVue = new Vue({
diff --git a/src/plugins/notebook/components/NotebookEntry.vue b/src/plugins/notebook/components/NotebookEntry.vue
index 9261063a0..947d1b4ff 100644
--- a/src/plugins/notebook/components/NotebookEntry.vue
+++ b/src/plugins/notebook/components/NotebookEntry.vue
@@ -22,18 +22,24 @@
<template>
<div
- class="c-notebook__entry c-ne has-local-controls"
+ class="c-notebook__entry c-ne has-local-controls has-tag-applier"
+ aria-label="Notebook Entry"
+ :class="{ 'locked': isLocked }"
@dragover="changeCursor"
@drop.capture="cancelEditMode"
@drop.prevent="dropOnEntry"
>
<div class="c-ne__time-and-content">
- <div class="c-ne__time">
- <template v-if="entry.createdBy">
- <span class="c-icon icon-person">{{ entry.createdBy }}</span>
- </template>
- <span>{{ createdOnDate }}</span>
- <span>{{ createdOnTime }}</span>
+ <div class="c-ne__time-and-creator">
+ <span class="c-ne__created-date">{{ createdOnDate }}</span>
+ <span class="c-ne__created-time">{{ createdOnTime }}</span>
+
+ <span
+ v-if="entry.createdBy"
+ class="c-ne__creator"
+ >
+ <span class="icon-person"></span> {{ entry.createdBy }}
+ </span>
</div>
<div class="c-ne__content">
<template v-if="readOnly && result">
@@ -49,12 +55,13 @@
/>
</div>
</template>
- <template v-else>
+ <template v-else-if="!isLocked">
<div
:id="entry.id"
class="c-ne__text c-ne__input"
+ aria-label="Notebook Entry Input"
tabindex="0"
- contenteditable
+ contenteditable="true"
@focus="editingEntry()"
@blur="updateEntryValue($event)"
@keydown.enter.exact.prevent
@@ -63,11 +70,33 @@
>
</div>
</template>
+
+ <template v-else>
+ <div
+ :id="entry.id"
+ class="c-ne__text"
+ contenteditable="false"
+ tabindex="0"
+ v-text="entry.text"
+ >
+ </div>
+ </template>
+
+ <TagEditor
+ :domain-object="domainObject"
+ :annotation-query="annotationQuery"
+ :annotation-type="openmct.annotation.ANNOTATION_TYPES.NOTEBOOK"
+ :annotation-search-type="openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS"
+ :target-specific-details="{entryId: entry.id}"
+ @tags-updated="timestampAndUpdate"
+ />
+
<div class="c-snapshots c-ne__embeds">
<NotebookEmbed
v-for="embed in entry.embeds"
:key="embed.id"
:embed="embed"
+ :is-locked="isLocked"
@removeEmbed="removeEmbed"
@updateEmbed="updateEmbed"
/>
@@ -75,7 +104,7 @@
</div>
</div>
<div
- v-if="!readOnly"
+ v-if="!readOnly && !isLocked"
class="c-ne__local-controls--hidden"
>
<button
@@ -111,16 +140,20 @@
<script>
import NotebookEmbed from './NotebookEmbed.vue';
+import TagEditor from '../../../ui/components/tags/TagEditor.vue';
import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue';
import { createNewEmbed } from '../utils/notebook-entries';
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
import Moment from 'moment';
+const UNKNOWN_USER = 'Unknown';
+
export default {
components: {
NotebookEmbed,
- TextHighlight
+ TextHighlight,
+ TagEditor
},
inject: ['openmct', 'snapshotContainer'],
props: {
@@ -159,12 +192,27 @@ export default {
default() {
return true;
}
+ },
+ isLocked: {
+ type: Boolean,
+ default() {
+ return false;
+ }
}
},
computed: {
createdOnDate() {
return this.formatTime(this.entry.createdOn, 'YYYY-MM-DD');
},
+ annotationQuery() {
+ const targetKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
+
+ return {
+ targetKeyString,
+ entryId: this.entry.id,
+ modified: this.entry.modified
+ };
+ },
createdOnTime() {
return this.formatTime(this.entry.createdOn, 'HH:mm:ss');
},
@@ -208,15 +256,21 @@ export default {
this.openmct.editor.cancel();
}
},
- changeCursor() {
+ changeCursor(event) {
event.preventDefault();
- event.dataTransfer.dropEffect = "copy";
+
+ if (!this.isLocked) {
+ event.dataTransfer.dropEffect = 'copy';
+ } else {
+ event.dataTransfer.dropEffect = 'none';
+ event.dataTransfer.effectAllowed = 'none';
+ }
},
deleteEntry() {
this.$emit('deleteEntry', this.entry.id);
},
async dropOnEntry($event) {
- event.stopImmediatePropagation();
+ $event.stopImmediatePropagation();
const snapshotId = $event.dataTransfer.getData('openmct/snapshot/id');
if (snapshotId.length) {
@@ -233,7 +287,7 @@ export default {
await this.addNewEmbed(objectPath);
}
- this.$emit('updateEntry', this.entry);
+ this.timestampAndUpdate();
},
findPositionInArray(array, id) {
let position = -1;
@@ -271,7 +325,7 @@ export default {
// TODO: remove notebook snapshot object using object remove API
this.entry.embeds.splice(embedPosition, 1);
- this.$emit('updateEntry', this.entry);
+ this.timestampAndUpdate();
},
updateEmbed(newEmbed) {
this.entry.embeds.some(e => {
@@ -283,6 +337,17 @@ export default {
return found;
});
+ this.timestampAndUpdate();
+ },
+ async timestampAndUpdate() {
+ const user = await this.openmct.user.getCurrentUser();
+
+ if (user === undefined) {
+ this.entry.modifiedBy = UNKNOWN_USER;
+ }
+
+ this.entry.modified = Date.now();
+
this.$emit('updateEntry', this.entry);
},
editingEntry() {
@@ -292,7 +357,7 @@ export default {
const value = $event.target.innerText;
if (value !== this.entry.text && value.match(/\S/)) {
this.entry.text = value;
- this.$emit('updateEntry', this.entry);
+ this.timestampAndUpdate();
} else {
this.$emit('cancelEdit');
}
diff --git a/src/plugins/notebook/components/PageCollection.vue b/src/plugins/notebook/components/PageCollection.vue
index 15de8ffb8..3d88b51f4 100644
--- a/src/plugins/notebook/components/PageCollection.vue
+++ b/src/plugins/notebook/components/PageCollection.vue
@@ -1,5 +1,5 @@
<template>
-<ul class="c-list">
+<ul class="c-list c-notebook__pages">
<li
v-for="page in pages"
:key="page.id"
diff --git a/src/plugins/notebook/components/PageComponent.vue b/src/plugins/notebook/components/PageComponent.vue
index 3f59fd454..cf623112a 100644
--- a/src/plugins/notebook/components/PageComponent.vue
+++ b/src/plugins/notebook/components/PageComponent.vue
@@ -1,23 +1,42 @@
<template>
<div
class="c-list__item js-list__item"
- :class="[{ 'is-selected': isSelected, 'is-notebook-default' : (defaultPageId === page.id) }]"
+ :class="[{
+ 'is-selected': isSelected,
+ 'is-notebook-default' : (defaultPageId === page.id),
+ 'icon-lock' : page.isLocked
+ }]"
:data-id="page.id"
@click="selectPage"
>
- <span
- class="c-list__item__name js-list__item__name"
- :data-id="page.id"
- @keydown.enter="updateName"
- @blur="updateName"
- >{{ page.name.length ? page.name : `Unnamed ${pageTitle}` }}</span>
- <PopupMenu :popup-menu-items="popupMenuItems" />
+ <template v-if="!page.isLocked">
+ <div
+ class="c-list__item__name js-list__item__name"
+ :class="[{ 'c-input-inline': isSelected }]"
+ :data-id="page.id"
+ :contenteditable="isSelected"
+ @keydown.escape="updateName"
+ @keydown.enter="updateName"
+ @blur="updateName"
+ >{{ pageName }}</div>
+ <PopupMenu
+ :popup-menu-items="popupMenuItems"
+ />
+ </template>
+ <template v-else>
+ <div
+ class="c-list__item__name js-list__item__name"
+ :data-id="page.id"
+ :contenteditable="false"
+ >{{ pageName }}</div>
+ </template>
</div>
</template>
<script>
-import PopupMenu from './PopupMenu.vue';
+import { KEY_ENTER, KEY_ESCAPE } from '../utils/notebook-key-code';
import RemoveDialog from '../utils/removeDialog';
+import PopupMenu from './PopupMenu.vue';
export default {
components: {
@@ -55,16 +74,13 @@ export default {
computed: {
isSelected() {
return this.selectedPageId === this.page.id;
- }
- },
- watch: {
- page(newPage) {
- this.toggleContentEditable(newPage);
+ },
+ pageName() {
+ return this.page.name.length ? this.page.name : `Unnamed ${this.pageTitle}`;
}
},
mounted() {
this.addPopupMenuItems();
- this.toggleContentEditable();
},
methods: {
addPopupMenuItems() {
@@ -94,43 +110,39 @@ export default {
removeDialog.show();
},
selectPage(event) {
- const target = event.target;
- const page = target.closest('.js-list__item');
- const input = page.querySelector('.js-list__item__name');
+ const { target: { dataset: { id } } } = event;
- if (page.className.indexOf('is-selected') > -1) {
- input.contentEditable = true;
- input.classList.add('c-input-inline');
+ if (this.isSelected || !id) {
+ return;
+ }
+ this.$emit('selectPage', id);
+ },
+ renamePage(target) {
+ if (!target) {
return;
}
- const id = target.dataset.id;
- if (!id) {
+ target.textContent = target.textContent ? target.textContent.trim() : `Unnamed ${this.pageTitle}`;
+
+ if (this.page.name === target.textContent) {
return;
}
- this.$emit('selectPage', id);
- },
- toggleContentEditable(page = this.page) {
- const pageTitle = this.$el.querySelector('span');
- pageTitle.contentEditable = page.isSelected;
+ this.$emit('renamePage', Object.assign(this.page, { name: target.textContent }));
},
updateName(event) {
- const target = event.target;
- const name = target.textContent.toString();
- target.contentEditable = false;
- target.classList.remove('c-input-inline');
+ const { target, keyCode, type } = event;
- if (this.page.name === name) {
- return;
+ if (keyCode === KEY_ESCAPE) {
+ target.textContent = this.page.name;
+ } else if (keyCode === KEY_ENTER || type === 'blur') {
+ this.renamePage(target);
}
- if (name === '') {
- return;
- }
+ target.scrollLeft = '0';
- this.$emit('renamePage', Object.assign(this.page, { name }));
+ target.blur();
}
}
};
diff --git a/src/plugins/notebook/components/PopupMenu.vue b/src/plugins/notebook/components/PopupMenu.vue
index 3fb392432..36fde2786 100644
--- a/src/plugins/notebook/components/PopupMenu.vue
+++ b/src/plugins/notebook/components/PopupMenu.vue
@@ -1,7 +1,7 @@
<template>
<button
class="c-popup-menu-button c-disclosure-button"
- title="popup menu"
+ title="Open context menu"
@click="showMenuItems"
>
</button>
@@ -65,6 +65,10 @@ export default {
return;
},
showMenuItems($event) {
+ if (this.menuItems) {
+ this.hideMenuItems();
+ }
+
const menuItems = new Vue({
components: {
MenuItems
diff --git a/src/plugins/notebook/components/SearchResults.vue b/src/plugins/notebook/components/SearchResults.vue
index e44b8ce91..91582138b 100644
--- a/src/plugins/notebook/components/SearchResults.vue
+++ b/src/plugins/notebook/components/SearchResults.vue
@@ -33,6 +33,7 @@
:read-only="true"
:selected-page="result.page"
:selected-section="result.section"
+ :is-locked="result.page.isLocked"
@editingEntry="editingEntry"
@cancelEdit="cancelEdit"
@changeSectionPage="changeSectionPage"
diff --git a/src/plugins/notebook/components/SectionCollection.vue b/src/plugins/notebook/components/SectionCollection.vue
index c5adf2082..fcc279908 100644
--- a/src/plugins/notebook/components/SectionCollection.vue
+++ b/src/plugins/notebook/components/SectionCollection.vue
@@ -1,5 +1,5 @@
<template>
-<ul class="c-list">
+<ul class="c-list c-notebook__sections">
<li
v-for="section in sections"
:key="section.id"
diff --git a/src/plugins/notebook/components/SectionComponent.vue b/src/plugins/notebook/components/SectionComponent.vue
index a5392eeb9..069735424 100644
--- a/src/plugins/notebook/components/SectionComponent.vue
+++ b/src/plugins/notebook/components/SectionComponent.vue
@@ -7,17 +7,24 @@
>
<span
class="c-list__item__name js-list__item__name"
+ :class="[{ 'c-input-inline': isSelected && !section.isLocked }]"
:data-id="section.id"
+ :contenteditable="isSelected && !section.isLocked"
+ @keydown.escape="updateName"
@keydown.enter="updateName"
@blur="updateName"
- >{{ section.name.length ? section.name : `Unnamed ${sectionTitle}` }}</span>
- <PopupMenu :popup-menu-items="popupMenuItems" />
+ >{{ sectionName }}</span>
+ <PopupMenu
+ v-if="!section.isLocked"
+ :popup-menu-items="popupMenuItems"
+ />
</div>
</template>
<script>
-import PopupMenu from './PopupMenu.vue';
+import { KEY_ENTER, KEY_ESCAPE } from '../utils/notebook-key-code';
import RemoveDialog from '../utils/removeDialog';
+import PopupMenu from './PopupMenu.vue';
export default {
components: {
@@ -55,16 +62,13 @@ export default {
computed: {
isSelected() {
return this.selectedSectionId === this.section.id;
- }
- },
- watch: {
- section(newSection) {
- this.toggleContentEditable(newSection);
+ },
+ sectionName() {
+ return this.section.name.length ? this.section.name : `Unnamed ${this.sectionTitle}`;
}
},
mounted() {
this.addPopupMenuItems();
- this.toggleContentEditable();
},
methods: {
addPopupMenuItems() {
@@ -95,44 +99,39 @@ export default {
removeDialog.show();
},
selectSection(event) {
- const target = event.target;
- const section = target.closest('.js-list__item');
- const input = section.querySelector('.js-list__item__name');
+ const { target: { dataset: { id } } } = event;
- if (section.className.indexOf('is-selected') > -1) {
- input.contentEditable = true;
- input.classList.add('c-input-inline');
+ if (this.isSelected || !id) {
+ return;
+ }
+ this.$emit('selectSection', id);
+ },
+ renameSection(target) {
+ if (!target) {
return;
}
- const id = target.dataset.id;
+ target.textContent = target.textContent ? target.textContent.trim() : `Unnamed ${this.sectionTitle}`;
- if (!id) {
+ if (this.section.name === target.textContent) {
return;
}
- this.$emit('selectSection', id);
- },
- toggleContentEditable(section = this.section) {
- const sectionTitle = this.$el.querySelector('span');
- sectionTitle.contentEditable = section.isSelected;
+ this.$emit('renameSection', Object.assign(this.section, { name: target.textContent }));
},
updateName(event) {
- const target = event.target;
- target.contentEditable = false;
- target.classList.remove('c-input-inline');
- const name = target.textContent.trim();
+ const { target, keyCode, type } = event;
- if (this.section.name === name) {
- return;
+ if (keyCode === KEY_ESCAPE) {
+ target.textContent = this.section.name;
+ } else if (keyCode === KEY_ENTER || type === 'blur') {
+ this.renameSection(target);
}
- if (name === '') {
- return;
- }
+ target.scrollLeft = '0';
- this.$emit('renameSection', Object.assign(this.section, { name }));
+ target.blur();
}
}
};
diff --git a/src/plugins/notebook/components/Sidebar.vue b/src/plugins/notebook/components/Sidebar.vue
index f34c47979..c7476d035 100644
--- a/src/plugins/notebook/components/Sidebar.vue
+++ b/src/plugins/notebook/components/Sidebar.vue
@@ -4,16 +4,15 @@
<div class="c-sidebar__header-w">
<div class="c-sidebar__header">
<span class="c-sidebar__header-label">{{ sectionTitle }}</span>
+ <button
+ class="c-icon-button c-icon-button--major icon-plus"
+ @click="addSection"
+ >
+ <span class="c-list-button__label">Add</span>
+ </button>
</div>
</div>
<div class="c-sidebar__contents-and-controls">
- <button
- class="c-list-button"
- @click="addSection"
- >
- <span class="c-button c-list-button__button icon-plus"></span>
- <span class="c-list-button__label">Add {{ sectionTitle }}</span>
- </button>
<SectionCollection
class="c-sidebar__contents"
:default-section-id="defaultSectionId"
@@ -31,21 +30,17 @@
<div class="c-sidebar__header-w">
<div class="c-sidebar__header">
<span class="c-sidebar__header-label">{{ pageTitle }}</span>
+
+ <button
+ class="c-icon-button c-icon-button--major icon-plus"
+ @click="addPage"
+ >
+ <span class="c-icon-button__label">Add</span>
+ </button>
</div>
- <button
- class="c-click-icon c-click-icon--major icon-x-in-circle"
- @click="toggleNav"
- ></button>
</div>
<div class="c-sidebar__contents-and-controls">
- <button
- class="c-list-button"
- @click="addPage"
- >
- <span class="c-button c-list-button__button icon-plus"></span>
- <span class="c-list-button__label">Add {{ pageTitle }}</span>
- </button>
<PageCollection
ref="pageCollection"
class="c-sidebar__contents"
@@ -63,13 +58,19 @@
/>
</div>
</div>
+ <div class="c-sidebar__right-edge">
+ <button
+ class="c-icon-button c-icon-button--major icon-line-horz"
+ @click="toggleNav"
+ ></button>
+ </div>
</div>
</template>
<script>
import SectionCollection from './SectionCollection.vue';
import PageCollection from './PageCollection.vue';
-import uuid from 'uuid';
+import { v4 as uuid } from 'uuid';
export default {
components: {
diff --git a/src/plugins/notebook/components/sidebar.scss b/src/plugins/notebook/components/sidebar.scss
index e5c8a8cd0..63c1b2ab3 100644
--- a/src/plugins/notebook/components/sidebar.scss
+++ b/src/plugins/notebook/components/sidebar.scss
@@ -3,19 +3,18 @@
background: $sideBarBg;
display: flex;
justify-content: stretch;
- max-width: 300px;
+ max-width: 600px;
&.c-drawer--push.is-expanded {
margin-right: $interiorMargin;
- width: 50%;
+ width: 30%;
}
&.c-drawer--overlays.is-expanded {
width: 95%;
}
- > * {
- // Hardcoded for two columns
+ &__pane {
background: $sideBarBg;
display: flex;
flex: 1 1 50%;
@@ -31,32 +30,30 @@
}
}
- &__pane {
- > * + * { margin-top: $interiorMargin; }
+ &__right-edge {
+ flex: 0 0 auto;
+ padding: $interiorMarginSm;
}
&__header-w {
- // Wraps header, used for page pane with collapse button
+ // Wraps header, used for page pane with collapse buttons
display: flex;
flex: 0 0 auto;
background: $sideBarHeaderBg;
align-items: center;
-
- .c-icon-button {
- font-size: 0.8em;
- color: $colorBodyFg;
- }
}
&__header {
color: $sideBarHeaderFg;
display: flex;
+ align-items: center;
flex: 1 1 auto;
- padding: $interiorMargin;
+ padding: $interiorMarginSm $interiorMargin;
text-transform: uppercase;
- > * {
+ &-label {
@include ellipsize();
+ flex: 1 1 auto;
}
}
@@ -66,17 +63,8 @@
flex-direction: column;
flex: 1 1 auto;
- > * {
- margin: auto $interiorMargin $interiorMargin $interiorMargin;
-
- &:first-child {
- border-bottom: 1px solid $colorInteriorBorder;
- flex: 0 0 auto;
- }
-
- + * {
- margin-top: $interiorMargin;
- }
+ > * + * {
+ margin-top: $interiorMargin;
}
}
@@ -87,12 +75,6 @@
padding: auto $interiorMargin;
}
- .c-list-button {
- .c-button {
- font-size: 0.8em;
- }
- }
-
.c-list__item {
@include hover() {
[class*="__menu-indicator"] {
@@ -106,7 +88,7 @@
}
&__name {
- flex: 0 1 auto;
+ flex: 1 1 auto;
}
&__menu-indicator {
diff --git a/src/plugins/notebook/monkeyPatchObjectAPIForNotebooks.js b/src/plugins/notebook/monkeyPatchObjectAPIForNotebooks.js
index e4c22db37..8222b5eff 100644
--- a/src/plugins/notebook/monkeyPatchObjectAPIForNotebooks.js
+++ b/src/plugins/notebook/monkeyPatchObjectAPIForNotebooks.js
@@ -1,10 +1,10 @@
-import {NOTEBOOK_TYPE} from './notebook-constants';
+import { isNotebookType } from './notebook-constants';
export default function (openmct) {
const apiSave = openmct.objects.save.bind(openmct.objects);
openmct.objects.save = async (domainObject) => {
- if (domainObject.type !== NOTEBOOK_TYPE) {
+ if (!isNotebookType(domainObject)) {
return apiSave(domainObject);
}
diff --git a/src/plugins/notebook/notebook-constants.js b/src/plugins/notebook/notebook-constants.js
index d5163f797..6f2e5af3e 100644
--- a/src/plugins/notebook/notebook-constants.js
+++ b/src/plugins/notebook/notebook-constants.js
@@ -1,5 +1,18 @@
export const NOTEBOOK_TYPE = 'notebook';
+export const RESTRICTED_NOTEBOOK_TYPE = 'restricted-notebook';
export const EVENT_SNAPSHOTS_UPDATED = 'SNAPSHOTS_UPDATED';
export const NOTEBOOK_DEFAULT = 'DEFAULT';
export const NOTEBOOK_SNAPSHOT = 'SNAPSHOT';
export const NOTEBOOK_VIEW_TYPE = 'notebook-vue';
+export const RESTRICTED_NOTEBOOK_VIEW_TYPE = 'restricted-notebook-vue';
+export const NOTEBOOK_INSTALLED_KEY = '_NOTEBOOK_PLUGIN_INSTALLED';
+export const RESTRICTED_NOTEBOOK_INSTALLED_KEY = '_RESTRICTED_NOTEBOOK_PLUGIN_INSTALLED';
+
+// these only deals with constants, figured this could skip going into a utils file
+export function isNotebookType(domainObject) {
+ return [NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE].includes(domainObject.type);
+}
+
+export function isNotebookViewType(view) {
+ return [NOTEBOOK_VIEW_TYPE, RESTRICTED_NOTEBOOK_VIEW_TYPE].includes(view);
+}
diff --git a/src/plugins/notebook/plugin.js b/src/plugins/notebook/plugin.js
index 033b04f8f..7fcabbe74 100644
--- a/src/plugins/notebook/plugin.js
+++ b/src/plugins/notebook/plugin.js
@@ -1,176 +1,155 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 CopyToNotebookAction from './actions/CopyToNotebookAction';
-import Notebook from './components/Notebook.vue';
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
+import NotebookViewProvider from './NotebookViewProvider';
+import NotebookType from './NotebookType';
import SnapshotContainer from './snapshot-container';
import monkeyPatchObjectAPIForNotebooks from './monkeyPatchObjectAPIForNotebooks.js';
-import { notebookImageMigration, IMAGE_MIGRATION_VER } from '../notebook/utils/notebook-migration';
-import { NOTEBOOK_TYPE } from './notebook-constants';
+import { notebookImageMigration } from '../notebook/utils/notebook-migration';
+import {
+ NOTEBOOK_TYPE,
+ RESTRICTED_NOTEBOOK_TYPE,
+ NOTEBOOK_VIEW_TYPE,
+ RESTRICTED_NOTEBOOK_VIEW_TYPE,
+ NOTEBOOK_INSTALLED_KEY,
+ RESTRICTED_NOTEBOOK_INSTALLED_KEY
+} from './notebook-constants';
import Vue from 'vue';
-export default function NotebookPlugin() {
+let notebookSnapshotContainer;
+function getSnapshotContainer(openmct) {
+ if (!notebookSnapshotContainer) {
+ notebookSnapshotContainer = new SnapshotContainer(openmct);
+ }
+
+ return notebookSnapshotContainer;
+}
+
+function addLegacyNotebookGetInterceptor(openmct) {
+ openmct.objects.addGetInterceptor({
+ appliesTo: (identifier, domainObject) => {
+ return domainObject && domainObject.type === NOTEBOOK_TYPE;
+ },
+ invoke: (identifier, domainObject) => {
+ notebookImageMigration(openmct, domainObject);
+
+ return domainObject;
+ }
+ });
+}
+
+function installBaseNotebookFunctionality(openmct) {
+ // only need to do this once
+ if (openmct[NOTEBOOK_INSTALLED_KEY] || openmct[RESTRICTED_NOTEBOOK_INSTALLED_KEY]) {
+ return;
+ }
+
+ const snapshotContainer = getSnapshotContainer(openmct);
+ const notebookSnapshotImageType = {
+ name: 'Notebook Snapshot Image Storage',
+ description: 'Notebook Snapshot Image Storage object',
+ creatable: false,
+ initialize: domainObject => {
+ domainObject.configuration = {
+ fullSizeImageURL: undefined,
+ thumbnailImageURL: undefined
+ };
+ }
+ };
+ openmct.types.addType('notebookSnapshotImage', notebookSnapshotImageType);
+ openmct.actions.register(new CopyToNotebookAction(openmct));
+
+ const notebookSnapshotIndicator = new Vue ({
+ components: {
+ NotebookSnapshotIndicator
+ },
+ provide: {
+ openmct,
+ snapshotContainer
+ },
+ template: '<NotebookSnapshotIndicator></NotebookSnapshotIndicator>'
+ });
+ const indicator = {
+ element: notebookSnapshotIndicator.$mount().$el,
+ key: 'notebook-snapshot-indicator',
+ priority: openmct.priority.DEFAULT
+ };
+
+ openmct.indicators.add(indicator);
+
+ monkeyPatchObjectAPIForNotebooks(openmct);
+}
+
+function NotebookPlugin(name = 'Notebook') {
return function install(openmct) {
- if (openmct._NOTEBOOK_PLUGIN_INSTALLED) {
+ if (openmct[NOTEBOOK_INSTALLED_KEY]) {
return;
- } else {
- openmct._NOTEBOOK_PLUGIN_INSTALLED = true;
}
- openmct.actions.register(new CopyToNotebookAction(openmct));
-
- const notebookType = {
- name: 'Notebook',
- description: 'Create and save timestamped notes with embedded object snapshots.',
- creatable: true,
- cssClass: 'icon-notebook',
- initialize: domainObject => {
- domainObject.configuration = {
- defaultSort: 'oldest',
- entries: {},
- imageMigrationVer: IMAGE_MIGRATION_VER,
- pageTitle: 'Page',
- sections: [],
- sectionTitle: 'Section',
- type: 'General'
- };
- },
- form: [
- {
- key: 'defaultSort',
- name: 'Entry Sorting',
- control: 'select',
- options: [
- {
- name: 'Newest First',
- value: "newest"
- },
- {
- name: 'Oldest First',
- value: "oldest"
- }
- ],
- cssClass: 'l-inline',
- property: [
- "configuration",
- "defaultSort"
- ]
- },
- {
- key: 'type',
- name: 'Note book Type',
- control: 'textfield',
- cssClass: 'l-inline',
- property: [
- "configuration",
- "type"
- ]
- },
- {
- key: 'sectionTitle',
- name: 'Section Title',
- control: 'textfield',
- cssClass: 'l-inline',
- required: true,
- property: [
- "configuration",
- "sectionTitle"
- ]
- },
- {
- key: 'pageTitle',
- name: 'Page Title',
- control: 'textfield',
- cssClass: 'l-inline',
- required: true,
- property: [
- "configuration",
- "pageTitle"
- ]
- }
- ]
- };
+ const icon = 'icon-notebook';
+ const description = 'Create and save timestamped notes with embedded object snapshots.';
+ const snapshotContainer = getSnapshotContainer(openmct);
+
+ addLegacyNotebookGetInterceptor(openmct);
+
+ const notebookType = new NotebookType(name, description, icon);
openmct.types.addType(NOTEBOOK_TYPE, notebookType);
- const notebookSnapshotImageType = {
- name: 'Notebook Snapshot Image Storage',
- description: 'Notebook Snapshot Image Storage object',
- creatable: false,
- initialize: domainObject => {
- domainObject.configuration = {
- fullSizeImageURL: undefined,
- thumbnailImageURL: undefined
- };
- }
- };
- openmct.types.addType('notebookSnapshotImage', notebookSnapshotImageType);
-
- const snapshotContainer = new SnapshotContainer(openmct);
- const notebookSnapshotIndicator = new Vue ({
- components: {
- NotebookSnapshotIndicator
- },
- provide: {
- openmct,
- snapshotContainer
- },
- template: '<NotebookSnapshotIndicator></NotebookSnapshotIndicator>'
- });
- const indicator = {
- element: notebookSnapshotIndicator.$mount().$el,
- key: 'notebook-snapshot-indicator',
- priority: openmct.priority.DEFAULT
- };
-
- openmct.indicators.add(indicator);
-
- openmct.objectViews.addProvider({
- key: 'notebook-vue',
- name: 'Notebook View',
- cssClass: 'icon-notebook',
- canView: function (domainObject) {
- return domainObject.type === 'notebook';
- },
- view: function (domainObject) {
- let component;
-
- return {
- show(container) {
- component = new Vue({
- el: container,
- components: {
- Notebook
- },
- provide: {
- openmct,
- snapshotContainer
- },
- data() {
- return {
- domainObject
- };
- },
- template: '<Notebook :domain-object="domainObject"></Notebook>'
- });
- },
- destroy() {
- component.$destroy();
- }
- };
- }
- });
-
- openmct.objects.addGetInterceptor({
- appliesTo: (identifier, domainObject) => {
- return domainObject && domainObject.type === 'notebook';
- },
- invoke: (identifier, domainObject) => {
- notebookImageMigration(openmct, domainObject);
-
- return domainObject;
- }
- });
-
- monkeyPatchObjectAPIForNotebooks(openmct);
+ const notebookView = new NotebookViewProvider(openmct, name, NOTEBOOK_VIEW_TYPE, NOTEBOOK_TYPE, icon, snapshotContainer);
+ openmct.objectViews.addProvider(notebookView);
+
+ installBaseNotebookFunctionality(openmct);
+
+ openmct[NOTEBOOK_INSTALLED_KEY] = true;
+ };
+}
+
+function RestrictedNotebookPlugin(name = 'Notebook Shift Log') {
+ return function install(openmct) {
+ if (openmct[RESTRICTED_NOTEBOOK_INSTALLED_KEY]) {
+ return;
+ }
+
+ const icon = 'icon-notebook-shift-log';
+ const description = 'Create and save timestamped notes with embedded object snapshots with the ability to commit and lock pages.';
+ const snapshotContainer = getSnapshotContainer(openmct);
+
+ const notebookType = new NotebookType(name, description, icon);
+ openmct.types.addType(RESTRICTED_NOTEBOOK_TYPE, notebookType);
+
+ const notebookView = new NotebookViewProvider(openmct, name, RESTRICTED_NOTEBOOK_VIEW_TYPE, RESTRICTED_NOTEBOOK_TYPE, icon, snapshotContainer);
+ openmct.objectViews.addProvider(notebookView);
+
+ installBaseNotebookFunctionality(openmct);
+
+ openmct[RESTRICTED_NOTEBOOK_INSTALLED_KEY] = true;
};
}
+
+export {
+ NotebookPlugin,
+ RestrictedNotebookPlugin
+};
diff --git a/src/plugins/notebook/pluginSpec.js b/src/plugins/notebook/pluginSpec.js
index a9af172dc..4fa3e8770 100644
--- a/src/plugins/notebook/pluginSpec.js
+++ b/src/plugins/notebook/pluginSpec.js
@@ -21,7 +21,7 @@
*****************************************************************************/
import { createOpenMct, createMouseEvent, resetApplicationState } from 'utils/testing';
-import notebookPlugin from './plugin';
+import { NotebookPlugin } from './plugin';
import Vue from 'vue';
describe("Notebook plugin:", () => {
@@ -33,6 +33,7 @@ describe("Notebook plugin:", () => {
let objectProviderObserver;
let notebookDomainObject;
+ let originalAnnotations;
beforeEach((done) => {
notebookDomainObject = {
@@ -54,7 +55,12 @@ describe("Notebook plugin:", () => {
child = document.createElement('div');
element.appendChild(child);
- openmct.install(notebookPlugin());
+ openmct.install(NotebookPlugin());
+ originalAnnotations = openmct.annotation.getNotebookAnnotation;
+ // eslint-disable-next-line require-await
+ openmct.annotation.getNotebookAnnotation = async function () {
+ return null;
+ };
notebookDefinition = openmct.types.get('notebook').definition;
notebookDefinition.initialize(notebookDomainObject);
@@ -65,6 +71,7 @@ describe("Notebook plugin:", () => {
afterEach(() => {
appHolder.remove();
+ openmct.annotation.getNotebookAnnotation = originalAnnotations;
return resetApplicationState(openmct);
});
@@ -83,7 +90,7 @@ describe("Notebook plugin:", () => {
let notebookViewObject;
let mutableNotebookObject;
- beforeEach(() => {
+ beforeEach(async () => {
notebookViewObject = {
...notebookDomainObject,
id: "test-object",
@@ -161,16 +168,14 @@ describe("Notebook plugin:", () => {
testObjectProvider.create.and.returnValue(Promise.resolve(true));
testObjectProvider.update.and.returnValue(Promise.resolve(true));
- return openmct.objects.getMutable(notebookViewObject.identifier).then((mutableObject) => {
- mutableNotebookObject = mutableObject;
- objectProviderObserver = testObjectProvider.observe.calls.mostRecent().args[1];
+ const mutableObject = await openmct.objects.getMutable(notebookViewObject.identifier);
+ mutableNotebookObject = mutableObject;
+ objectProviderObserver = testObjectProvider.observe.calls.mostRecent().args[1];
- notebookView = notebookViewProvider.view(mutableNotebookObject);
- notebookView.show(child);
-
- return Vue.nextTick();
- });
+ notebookView = notebookViewProvider.view(mutableNotebookObject);
+ notebookView.show(child);
+ await Vue.nextTick();
});
afterEach(() => {
@@ -206,10 +211,17 @@ describe("Notebook plugin:", () => {
describe("synchronization", () => {
+ let objectCloneToSyncFrom;
+
+ beforeEach(() => {
+ objectCloneToSyncFrom = structuredClone(notebookViewObject);
+ objectCloneToSyncFrom.persisted = notebookViewObject.modified + 1;
+ });
+
it("updates an entry when another user modifies it", () => {
expect(getEntryText(0).innerText).toBe("First Test Entry");
- notebookViewObject.configuration.entries["test-section-1"]["test-page-1"][0].text = "Modified entry text";
- objectProviderObserver(notebookViewObject);
+ objectCloneToSyncFrom.configuration.entries["test-section-1"]["test-page-1"][0].text = "Modified entry text";
+ objectProviderObserver(objectCloneToSyncFrom);
return Vue.nextTick().then(() => {
expect(getEntryText(0).innerText).toBe("Modified entry text");
@@ -218,13 +230,13 @@ describe("Notebook plugin:", () => {
it("shows new entry when another user adds one", () => {
expect(allNotebookEntryElements().length).toBe(2);
- notebookViewObject.configuration.entries["test-section-1"]["test-page-1"].push({
+ objectCloneToSyncFrom.configuration.entries["test-section-1"]["test-page-1"].push({
"id": "entry-3",
"createdOn": 0,
"text": "Third Test Entry",
"embeds": []
});
- objectProviderObserver(notebookViewObject);
+ objectProviderObserver(objectCloneToSyncFrom);
return Vue.nextTick().then(() => {
expect(allNotebookEntryElements().length).toBe(3);
@@ -232,9 +244,9 @@ describe("Notebook plugin:", () => {
});
it("removes an entry when another user removes one", () => {
expect(allNotebookEntryElements().length).toBe(2);
- let entries = notebookViewObject.configuration.entries["test-section-1"]["test-page-1"];
- notebookViewObject.configuration.entries["test-section-1"]["test-page-1"] = entries.splice(0, 1);
- objectProviderObserver(notebookViewObject);
+ let entries = objectCloneToSyncFrom.configuration.entries["test-section-1"]["test-page-1"];
+ objectCloneToSyncFrom.configuration.entries["test-section-1"]["test-page-1"] = entries.splice(0, 1);
+ objectProviderObserver(objectCloneToSyncFrom);
return Vue.nextTick().then(() => {
expect(allNotebookEntryElements().length).toBe(1);
@@ -251,8 +263,8 @@ describe("Notebook plugin:", () => {
};
expect(allNotebookPageElements().length).toBe(2);
- notebookViewObject.configuration.sections[0].pages.push(newPage);
- objectProviderObserver(notebookViewObject);
+ objectCloneToSyncFrom.configuration.sections[0].pages.push(newPage);
+ objectProviderObserver(objectCloneToSyncFrom);
return Vue.nextTick().then(() => {
expect(allNotebookPageElements().length).toBe(3);
@@ -262,8 +274,8 @@ describe("Notebook plugin:", () => {
it("updates the notebook when a user removes a page", () => {
expect(allNotebookPageElements().length).toBe(2);
- notebookViewObject.configuration.sections[0].pages.splice(0, 1);
- objectProviderObserver(notebookViewObject);
+ objectCloneToSyncFrom.configuration.sections[0].pages.splice(0, 1);
+ objectProviderObserver(objectCloneToSyncFrom);
return Vue.nextTick().then(() => {
expect(allNotebookPageElements().length).toBe(1);
@@ -286,8 +298,8 @@ describe("Notebook plugin:", () => {
};
expect(allNotebookSectionElements().length).toBe(2);
- notebookViewObject.configuration.sections.push(newSection);
- objectProviderObserver(notebookViewObject);
+ objectCloneToSyncFrom.configuration.sections.push(newSection);
+ objectProviderObserver(objectCloneToSyncFrom);
return Vue.nextTick().then(() => {
expect(allNotebookSectionElements().length).toBe(3);
@@ -296,8 +308,8 @@ describe("Notebook plugin:", () => {
it("updates the notebook when a user removes a section", () => {
expect(allNotebookSectionElements().length).toBe(2);
- notebookViewObject.configuration.sections.splice(0, 1);
- objectProviderObserver(notebookViewObject);
+ objectCloneToSyncFrom.configuration.sections.splice(0, 1);
+ objectProviderObserver(objectCloneToSyncFrom);
return Vue.nextTick().then(() => {
expect(allNotebookSectionElements().length).toBe(1);
diff --git a/src/plugins/notebook/utils/notebook-entries.js b/src/plugins/notebook/utils/notebook-entries.js
index 56c23999f..42995c013 100644
--- a/src/plugins/notebook/utils/notebook-entries.js
+++ b/src/plugins/notebook/utils/notebook-entries.js
@@ -1,4 +1,5 @@
import objectLink from '../../../ui/mixins/object-link';
+import { v4 as uuid } from 'uuid';
async function getUsername(openmct) {
let username = '';
@@ -123,8 +124,8 @@ export async function addNotebookEntry(openmct, domainObject, notebookStorage, e
? [embed]
: [];
+ const id = `entry-${uuid()}`;
const createdBy = await getUsername(openmct);
- const id = `entry-${date}`;
const entry = {
id,
createdOn: date,
@@ -142,7 +143,7 @@ export async function addNotebookEntry(openmct, domainObject, notebookStorage, e
}
export function getNotebookEntries(domainObject, selectedSection, selectedPage) {
- if (!domainObject || !selectedSection || !selectedPage) {
+ if (!domainObject || !selectedSection || !selectedPage || !domainObject.configuration) {
return;
}
@@ -159,7 +160,9 @@ export function getNotebookEntries(domainObject, selectedSection, selectedPage)
return;
}
- return entries[selectedSection.id][selectedPage.id];
+ const specificEntries = entries[selectedSection.id][selectedPage.id];
+
+ return specificEntries;
}
export function getEntryPosById(entryId, domainObject, selectedSection, selectedPage) {
diff --git a/src/plugins/notebook/utils/notebook-image.js b/src/plugins/notebook/utils/notebook-image.js
index cde3ae6d7..0f7e4ab30 100644
--- a/src/plugins/notebook/utils/notebook-image.js
+++ b/src/plugins/notebook/utils/notebook-image.js
@@ -1,4 +1,4 @@
-import uuid from 'uuid';
+import { v4 as uuid } from 'uuid';
export const DEFAULT_SIZE = {
width: 30,
diff --git a/src/plugins/notebook/utils/notebook-key-code.js b/src/plugins/notebook/utils/notebook-key-code.js
new file mode 100644
index 000000000..64515e935
--- /dev/null
+++ b/src/plugins/notebook/utils/notebook-key-code.js
@@ -0,0 +1,3 @@
+// Key codes for `KeyboardEvent.keyCode`.
+export const KEY_ENTER = 13;
+export const KEY_ESCAPE = 27;
diff --git a/src/plugins/objectMigration/Migrations.js b/src/plugins/objectMigration/Migrations.js
index 8bf5d445a..493845e1f 100644
--- a/src/plugins/objectMigration/Migrations.js
+++ b/src/plugins/objectMigration/Migrations.js
@@ -23,7 +23,7 @@
define([
'uuid'
], function (
- uuid
+ { v4: uuid }
) {
return function Migrations(openmct) {
function getColumnNameKeyMap(domainObject) {
@@ -145,7 +145,7 @@ define([
item.size = element.size || DEFAULT_SIZE;
item.identifier = telemetryObjects[element.id].identifier;
item.displayMode = element.titled ? 'all' : 'value';
- item.value = openmct.telemetry.getMetadata(telemetryObjects[element.id]).getDefaultDisplayValue();
+ item.value = openmct.telemetry.getMetadata(telemetryObjects[element.id]).getDefaultDisplayValue()?.key;
} else if (element.type === 'fixed.box') {
item.type = "box-view";
item.stroke = element.stroke || DEFAULT_STROKE;
diff --git a/src/plugins/operatorStatus/AbstractStatusIndicator.js b/src/plugins/operatorStatus/AbstractStatusIndicator.js
new file mode 100644
index 000000000..7d2a01293
--- /dev/null
+++ b/src/plugins/operatorStatus/AbstractStatusIndicator.js
@@ -0,0 +1,106 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 raf from '@/utils/raf';
+
+export default class AbstractStatusIndicator {
+ #popupComponent;
+ #indicator;
+ #configuration;
+
+ /**
+ * @param {*} openmct the Open MCT API (proper jsdoc to come)
+ * @param {import('@/api/user/UserAPI').UserAPIConfiguration} configuration Per-deployment status styling. See the type definition in UserAPI
+ */
+ constructor(openmct, configuration) {
+ this.openmct = openmct;
+ this.#configuration = configuration;
+
+ this.showPopup = this.showPopup.bind(this);
+ this.clearPopup = this.clearPopup.bind(this);
+ this.positionBox = this.positionBox.bind(this);
+ this.positionBox = raf(this.positionBox);
+
+ this.#indicator = this.createIndicator();
+ this.#popupComponent = this.createPopupComponent();
+ }
+
+ install() {
+ this.openmct.indicators.add(this.#indicator);
+ }
+
+ showPopup() {
+ const popupElement = this.getPopupElement();
+
+ document.body.appendChild(popupElement.$el);
+ //Use capture so we don't trigger immediately on the same iteration of the event loop
+ document.addEventListener('click', this.clearPopup, {
+ capture: true
+ });
+
+ this.positionBox();
+
+ window.addEventListener('resize', this.positionBox);
+ }
+
+ positionBox() {
+ const popupElement = this.getPopupElement();
+ const indicator = this.getIndicator();
+
+ let indicatorBox = indicator.element.getBoundingClientRect();
+ popupElement.positionX = indicatorBox.left;
+ popupElement.positionY = indicatorBox.bottom;
+
+ const popupRight = popupElement.positionX + popupElement.$el.clientWidth;
+ const offsetLeft = Math.min(window.innerWidth - popupRight, 0);
+ popupElement.positionX = popupElement.positionX + offsetLeft;
+ }
+
+ clearPopup(clickAwayEvent) {
+ const popupElement = this.getPopupElement();
+
+ if (!popupElement.$el.contains(clickAwayEvent.target)) {
+ popupElement.$el.remove();
+ document.removeEventListener('click', this.clearPopup);
+ window.removeEventListener('resize', this.positionBox);
+ }
+ }
+
+ createPopupComponent() {
+ throw new Error('Must override createPopupElement method');
+ }
+
+ getPopupElement() {
+ return this.#popupComponent;
+ }
+
+ createIndicator() {
+ throw new Error('Must override createIndicator method');
+ }
+
+ getIndicator() {
+ return this.#indicator;
+ }
+
+ getConfiguration() {
+ return this.#configuration;
+ }
+}
diff --git a/src/plugins/operatorStatus/operator-status.scss b/src/plugins/operatorStatus/operator-status.scss
new file mode 100644
index 000000000..9482c650f
--- /dev/null
+++ b/src/plugins/operatorStatus/operator-status.scss
@@ -0,0 +1,142 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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.
+ *****************************************************************************/
+
+ $statusCountWidth: 30px;
+
+.c-status-poll-panel {
+ @include menuOuter();
+ display: flex;
+ flex-direction: column;
+ padding: $interiorMarginLg;
+ min-width: 350px;
+ max-width: 35%;
+
+ > * + * {
+ margin-top: $interiorMarginLg;
+ }
+
+ *:before {
+ font-size: 0.8em;
+ margin-right: $interiorMarginSm;
+ }
+
+ &__section {
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+
+ > * + * {
+ margin-left: $interiorMarginLg;
+ }
+ }
+
+ &__top {
+ text-transform: uppercase;
+ }
+
+ &__user-role,
+ &__updated {
+ opacity: 50%;
+ }
+
+ &__updated {
+ flex: 1 1 auto;
+ text-align: right;
+ }
+
+ &__poll-question {
+ background: $colorBodyFg;
+ color: $colorBodyBg;
+ border-radius: $controlCr;
+ font-weight: bold;
+ padding: $interiorMarginSm $interiorMargin;
+
+ .c-status-poll-panel--admin & {
+ background: rgba($colorBodyFg, 0.1);
+ color: $colorBodyFg;
+ }
+ }
+
+ /****** Admin interface */
+ &__content {
+ $m: $interiorMargin;
+ display: grid;
+ grid-template-columns: max-content 1fr;
+ grid-column-gap: $m;
+ grid-row-gap: $m;
+
+ [class*='__label'] {
+ padding: 3px 0;
+ }
+
+ [class*='new-question'] {
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ > * + * { margin-left: $interiorMargin; }
+
+ input {
+ flex: 1 1 auto;
+ height: $btnStdH;
+ }
+
+ button { flex: 0 0 auto; }
+ }
+ }
+}
+
+.c-status-poll-report {
+ display: flex;
+ flex-direction: row;
+ > * + * { margin-left: $interiorMargin; }
+
+ &__count {
+ background: rgba($colorBodyFg, 0.2);
+ border-radius: $controlCr;
+ display: flex;
+ flex-direction: row;
+ font-size: 1.25em;
+ align-items: center;
+ padding: $interiorMarginSm $interiorMarginLg;
+
+ &-type {
+ line-height: 1em;
+ opacity: 0.6;
+ }
+ }
+}
+
+.c-indicator {
+ &:before {
+ // Indicator icon
+ color: $colorKey;
+ }
+
+ &--operator-status {
+ cursor: pointer;
+ max-width: 150px;
+
+ @include hover() {
+ background: $colorIndicatorBgHov;
+ }
+ }
+}
diff --git a/src/plugins/operatorStatus/operatorStatus/OperatorStatus.vue b/src/plugins/operatorStatus/operatorStatus/OperatorStatus.vue
new file mode 100644
index 000000000..9b7d411a1
--- /dev/null
+++ b/src/plugins/operatorStatus/operatorStatus/OperatorStatus.vue
@@ -0,0 +1,188 @@
+<!--
+ Open MCT, Copyright (c) 2014-2022, 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
+ :style="position"
+ class="c-status-poll-panel c-status-poll-panel--operator"
+ @click.stop="noop"
+>
+ <div class="c-status-poll-panel__section c-status-poll-panel__top">
+ <div
+ class="c-status-poll-panel__title"
+ >Status Poll</div>
+ <div class="c-status-poll-panel__user-role icon-person">{{ role }}</div>
+ <div class="c-status-poll-panel__updated">{{ pollQuestionUpdated }}</div>
+ </div>
+
+ <div class="c-status-poll-panel__section c-status-poll-panel__poll-question">
+ {{ currentPollQuestion }}
+ </div>
+
+ <div class="c-status-poll-panel__section c-status-poll-panel__bottom">
+ <div class="c-status-poll-panel__set-status-label">My status:</div>
+ <select
+ v-model="selectedStatus"
+ name="setStatus"
+ @change="changeStatus"
+ >
+ <option
+ v-for="status in allStatuses"
+ :key="status.key"
+ :value="status.key"
+ >
+ {{ status.label }}
+ </option>
+ </select>
+ </div>
+</div>
+</template>
+
+<script>
+const DEFAULT_POLL_QUESTION = 'NO POLL QUESTION';
+
+export default {
+ inject: ['openmct', 'indicator', 'configuration'],
+ props: {
+ positionX: {
+ type: Number,
+ required: true
+ },
+ positionY: {
+ type: Number,
+ required: true
+ }
+ },
+ data() {
+ return {
+ allRoles: [],
+ role: '--',
+ pollQuestionUpdated: '--',
+ currentPollQuestion: DEFAULT_POLL_QUESTION,
+ selectedStatus: undefined,
+ allStatuses: []
+ };
+ },
+ computed: {
+ position() {
+ return {
+ position: 'absolute',
+ left: `${this.positionX}px`,
+ top: `${this.positionY}px`
+ };
+ }
+ },
+ beforeDestroy() {
+ this.openmct.user.status.off('statusChange', this.setStatus);
+ this.openmct.user.status.off('pollQuestionChange', this.setPollQuestion);
+ },
+ async mounted() {
+ this.unsubscribe = [];
+ await this.fetchUser();
+ await this.findFirstApplicableRole();
+ this.fetchPossibleStatusesForUser();
+ this.fetchCurrentPoll();
+ this.fetchMyStatus();
+ this.subscribeToMyStatus();
+ this.subscribeToPollQuestion();
+ },
+ methods: {
+ async findFirstApplicableRole() {
+ this.role = await this.openmct.user.status.getStatusRoleForCurrentUser();
+ },
+ async fetchUser() {
+ this.user = await this.openmct.user.getCurrentUser();
+ },
+ async fetchCurrentPoll() {
+ const pollQuestion = await this.openmct.user.status.getPollQuestion();
+ if (pollQuestion !== undefined) {
+ this.setPollQuestion(pollQuestion);
+ }
+ },
+ async fetchPossibleStatusesForUser() {
+ this.allStatuses = await this.openmct.user.status.getPossibleStatuses();
+ },
+ setPollQuestion(pollQuestion) {
+ this.currentPollQuestion = pollQuestion.question;
+ this.pollQuestionUpdated = new Date(pollQuestion.timestamp).toISOString();
+
+ this.indicator.text(pollQuestion?.question || '');
+ },
+ async fetchMyStatus() {
+ const activeStatusRole = await this.openmct.user.status.getStatusRoleForCurrentUser();
+ const status = await this.openmct.user.status.getStatusForRole(activeStatusRole);
+
+ if (status !== undefined) {
+ this.setStatus({status});
+ }
+ },
+ subscribeToMyStatus() {
+ this.openmct.user.status.on('statusChange', this.setStatus);
+ },
+ subscribeToPollQuestion() {
+ this.openmct.user.status.on('pollQuestionChange', this.setPollQuestion);
+ },
+ setStatus({role, status}) {
+ status = this.applyStyling(status);
+ this.selectedStatus = status.key;
+ this.indicator.iconClass(status.iconClassPoll);
+ this.indicator.statusClass(status.statusClass);
+ if (this.isDefaultStatus(status)) {
+ this.indicator.text(this.currentPollQuestion);
+ } else {
+ this.indicator.text(status.label);
+ }
+ },
+ isDefaultStatus(status) {
+ return status.key === this.allStatuses[0].key;
+ },
+ findStatusByKey(statusKey) {
+ return this.allStatuses.find(possibleMatch => possibleMatch.key === statusKey);
+ },
+ async changeStatus() {
+ if (this.selectedStatus !== undefined) {
+ const statusObject = this.findStatusByKey(this.selectedStatus);
+
+ const result = await this.openmct.user.status.setStatusForRole(this.role, statusObject);
+
+ if (result === true) {
+ this.openmct.notifications.info("Successfully set operator status");
+ } else {
+ this.openmct.notifications.error("Unable to set operator status");
+ }
+ }
+ },
+ applyStyling(status) {
+ const stylesForStatus = this.configuration?.statusStyles?.[status.label];
+
+ if (stylesForStatus !== undefined) {
+ return {
+ ...status,
+ ...stylesForStatus
+ };
+ } else {
+ return status;
+ }
+ },
+ noop() {}
+ }
+};
+</script>
diff --git a/src/plugins/operatorStatus/operatorStatus/OperatorStatusIndicator.js b/src/plugins/operatorStatus/operatorStatus/OperatorStatusIndicator.js
new file mode 100644
index 000000000..9eb96e938
--- /dev/null
+++ b/src/plugins/operatorStatus/operatorStatus/OperatorStatusIndicator.js
@@ -0,0 +1,63 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 Vue from 'vue';
+
+import AbstractStatusIndicator from '../AbstractStatusIndicator';
+import OperatorStatusComponent from './OperatorStatus.vue';
+
+export default class OperatorStatusIndicator extends AbstractStatusIndicator {
+ createPopupComponent() {
+ const indicator = this.getIndicator();
+ const popupElement = new Vue({
+ components: {
+ OperatorStatus: OperatorStatusComponent
+ },
+ provide: {
+ openmct: this.openmct,
+ indicator: indicator,
+ configuration: this.getConfiguration()
+ },
+ data() {
+ return {
+ positionX: 0,
+ positionY: 0
+ };
+ },
+ template: '<operator-status :positionX="positionX" :positionY="positionY" />'
+ }).$mount();
+
+ return popupElement;
+ }
+
+ createIndicator() {
+ const operatorIndicator = this.openmct.indicators.simpleIndicator();
+
+ operatorIndicator.text("My Operator Status");
+ operatorIndicator.description("Set my operator status");
+ operatorIndicator.iconClass('icon-status-poll-question-mark');
+ operatorIndicator.element.classList.add("c-indicator--operator-status");
+ operatorIndicator.element.classList.add("no-minify");
+ operatorIndicator.on('click', this.showPopup);
+
+ return operatorIndicator;
+ }
+}
diff --git a/src/plugins/operatorStatus/plugin.js b/src/plugins/operatorStatus/plugin.js
new file mode 100644
index 000000000..3d449d1eb
--- /dev/null
+++ b/src/plugins/operatorStatus/plugin.js
@@ -0,0 +1,50 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 OperatorStatusIndicator from './operatorStatus/OperatorStatusIndicator';
+import PollQuestionIndicator from './pollQuestion/PollQuestionIndicator';
+
+/**
+ * @param {import('@/api/user/UserAPI').UserAPIConfiguration} configuration
+ * @returns {function} The plugin install function
+ */
+export default function operatorStatusPlugin(configuration) {
+ return function install(openmct) {
+
+ if (openmct.user.hasProvider()) {
+ openmct.user.status.canProvideStatusForCurrentUser().then(canProvideStatus => {
+ if (canProvideStatus) {
+ const operatorStatusIndicator = new OperatorStatusIndicator(openmct, configuration);
+
+ operatorStatusIndicator.install();
+ }
+ });
+
+ openmct.user.status.canSetPollQuestion().then(canSetPollQuestion => {
+ if (canSetPollQuestion) {
+ const pollQuestionIndicator = new PollQuestionIndicator(openmct, configuration);
+
+ pollQuestionIndicator.install();
+ }
+ });
+ }
+ };
+}
diff --git a/src/plugins/operatorStatus/pollQuestion/PollQuestion.vue b/src/plugins/operatorStatus/pollQuestion/PollQuestion.vue
new file mode 100644
index 000000000..ff8443cf9
--- /dev/null
+++ b/src/plugins/operatorStatus/pollQuestion/PollQuestion.vue
@@ -0,0 +1,190 @@
+<!--
+ Open MCT, Copyright (c) 2014-2022, 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
+ :style="position"
+ class="c-status-poll-panel c-status-poll-panel--admin"
+ @click.stop="noop"
+>
+ <div class="c-status-poll-panel__section c-status-poll-panel__top">
+ <div
+ class="c-status-poll-panel__title"
+ >Manage Status Poll</div>
+ <div class="c-status-poll-panel__updated">Last updated: {{ pollQuestionUpdated }}</div>
+ </div>
+
+ <div class="c-status-poll__section c-status-poll-panel__content c-spq">
+ <!-- Grid layout -->
+ <div class="c-spq__label">Current poll:</div>
+ <div class="c-spq__value c-status-poll-panel__poll-question">{{ currentPollQuestion }}</div>
+
+ <template v-if="statusCountViewModel.length > 0">
+ <div class="c-spq__label">Reporting:</div>
+ <div class="c-spq__value c-status-poll-panel__poll-reporting c-status-poll-report">
+ <div
+ v-for="entry in statusCountViewModel"
+ :key="entry.status.key"
+ :title="entry.status.label"
+ class="c-status-poll-report__count"
+ :style="[{
+ background: entry.status.statusBgColor,
+ color: entry.status.statusFgColor
+ }]"
+ >
+ <div
+ class="c-status-poll-report__count-type"
+ :class="entry.status.iconClass"
+ ></div>
+ <div class="c-status-poll-report__count-value">
+ {{ entry.roleCount }}
+ </div>
+ </div>
+ </div>
+ </template>
+
+ <div class="c-spq__label">New poll:</div>
+ <div class="c-spq__value c-status-poll-panel__poll-new-question">
+ <input
+ v-model="newPollQuestion"
+ type="text"
+ name="newPollQuestion"
+ >
+ <button
+ class="c-button"
+ title="Publish a new poll question and reset previous responses"
+ @click="updatePollQuestion"
+ >Update</button>
+ </div>
+ </div>
+
+</div>
+</template>
+
+<script>
+import _ from 'lodash';
+
+export default {
+ inject: ['openmct', 'indicator', 'configuration'],
+ props: {
+ positionX: {
+ type: Number,
+ required: true
+ },
+ positionY: {
+ type: Number,
+ required: true
+ }
+ },
+ data() {
+ return {
+ pollQuestionUpdated: '--',
+ currentPollQuestion: '--',
+ newPollQuestion: undefined,
+ statusCountViewModel: []
+ };
+ },
+ computed: {
+ position() {
+ return {
+ position: 'absolute',
+ left: `${this.positionX}px`,
+ top: `${this.positionY}px`
+ };
+ }
+ },
+ mounted() {
+ this.fetchCurrentPoll();
+ this.subscribeToPollQuestion();
+ this.fetchStatusSummary();
+ this.openmct.user.status.on('statusChange', this.fetchStatusSummary);
+ },
+ beforeDestroy() {
+ this.openmct.user.status.off('statusChange', this.fetchStatusSummary);
+ this.openmct.user.status.off('pollQuestionChange', this.setPollQuestion);
+ },
+ created() {
+ this.fetchStatusSummary = _.debounce(this.fetchStatusSummary);
+ },
+ methods: {
+ async fetchCurrentPoll() {
+ const pollQuestion = await this.openmct.user.status.getPollQuestion();
+ if (pollQuestion !== undefined) {
+ this.setPollQuestion(pollQuestion);
+ }
+ },
+ subscribeToPollQuestion() {
+ this.openmct.user.status.on('pollQuestionChange', this.setPollQuestion);
+ },
+ setPollQuestion(pollQuestion) {
+ this.currentPollQuestion = pollQuestion.question;
+ this.pollQuestionUpdated = new Date(pollQuestion.timestamp).toISOString();
+ this.indicator.text(pollQuestion.question);
+ },
+ async updatePollQuestion() {
+ const result = await this.openmct.user.status.setPollQuestion(this.newPollQuestion);
+ if (result === true) {
+ this.openmct.notifications.info("Successfully set new poll question");
+ } else {
+ this.openmct.notifications.error("Unable to set new poll question.");
+ }
+
+ this.newPollQuestion = undefined;
+ },
+ async fetchStatusSummary() {
+ const allStatuses = await this.openmct.user.status.getPossibleStatuses();
+ const statusCountMap = allStatuses.reduce((statusToCountMap, status) => {
+ statusToCountMap[status.key] = 0;
+
+ return statusToCountMap;
+ }, {});
+ const allStatusRoles = await this.openmct.user.status.getAllStatusRoles();
+ const statusesForRoles = await Promise.all(allStatusRoles.map(role => this.openmct.user.status.getStatusForRole(role)));
+
+ statusesForRoles.forEach((status, i) => {
+ const currentCount = statusCountMap[status.key];
+ statusCountMap[status.key] = currentCount + 1;
+ });
+
+ this.statusCountViewModel = allStatuses.map((status) => {
+ return {
+ status: this.applyStyling(status),
+ roleCount: statusCountMap[status.key]
+ };
+ });
+ },
+ applyStyling(status) {
+ const stylesForStatus = this.configuration?.statusStyles?.[status.label];
+
+ if (stylesForStatus !== undefined) {
+ return {
+ ...status,
+ ...stylesForStatus
+ };
+ } else {
+ return status;
+ }
+ },
+ noop() {}
+ }
+
+};
+</script>
diff --git a/src/plugins/operatorStatus/pollQuestion/PollQuestionIndicator.js b/src/plugins/operatorStatus/pollQuestion/PollQuestionIndicator.js
new file mode 100644
index 000000000..ea85d5905
--- /dev/null
+++ b/src/plugins/operatorStatus/pollQuestion/PollQuestionIndicator.js
@@ -0,0 +1,63 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 Vue from 'vue';
+
+import AbstractStatusIndicator from '../AbstractStatusIndicator';
+import PollQuestionComponent from './PollQuestion.vue';
+
+export default class PollQuestionIndicator extends AbstractStatusIndicator {
+ createPopupComponent() {
+ const indicator = this.getIndicator();
+ const pollQuestionElement = new Vue({
+ components: {
+ PollQuestion: PollQuestionComponent
+ },
+ provide: {
+ openmct: this.openmct,
+ indicator: indicator,
+ configuration: this.getConfiguration()
+ },
+ data() {
+ return {
+ positionX: 0,
+ positionY: 0
+ };
+ },
+ template: '<poll-question :positionX="positionX" :positionY="positionY" />'
+ }).$mount();
+
+ return pollQuestionElement;
+ }
+
+ createIndicator() {
+ const pollQuestionIndicator = this.openmct.indicators.simpleIndicator();
+
+ pollQuestionIndicator.text("Poll Question");
+ pollQuestionIndicator.description("Set the current poll question");
+ pollQuestionIndicator.iconClass('icon-status-poll-edit');
+ pollQuestionIndicator.element.classList.add("c-indicator--operator-status");
+ pollQuestionIndicator.element.classList.add("no-minify");
+ pollQuestionIndicator.on('click', this.showPopup);
+
+ return pollQuestionIndicator;
+ }
+}
diff --git a/src/plugins/persistence/couch/.env.ci b/src/plugins/persistence/couch/.env.ci
new file mode 100644
index 000000000..104d70d6e
--- /dev/null
+++ b/src/plugins/persistence/couch/.env.ci
@@ -0,0 +1,5 @@
+OPENMCT_DATABASE_NAME=openmct
+COUCH_ADMIN_USER=admin
+COUCH_ADMIN_PASSWORD=password
+COUCH_BASE_LOCAL=http://localhost:5984
+COUCH_NODE_NAME=nonode@nohost \ No newline at end of file
diff --git a/src/plugins/persistence/couch/CouchChangesFeed.js b/src/plugins/persistence/couch/CouchChangesFeed.js
index 508ada46b..3c1445cec 100644
--- a/src/plugins/persistence/couch/CouchChangesFeed.js
+++ b/src/plugins/persistence/couch/CouchChangesFeed.js
@@ -43,11 +43,18 @@
};
self.onerror = function (error) {
+ self.updateCouchStateIndicator();
console.error('🚨 Error on CouchDB feed 🚨', error);
};
+ self.onopen = function () {
+ self.updateCouchStateIndicator();
+ };
+
self.onCouchMessage = function (event) {
+ self.updateCouchStateIndicator();
console.debug('📩 Received message from CouchDB 📩');
+
const objectChanges = JSON.parse(event.data);
connections.forEach(function (connection) {
connection.postMessage({
@@ -61,10 +68,38 @@
couchEventSource = new EventSource(url);
couchEventSource.onerror = self.onerror;
+ couchEventSource.onopen = self.onopen;
// start listening for events
couchEventSource.addEventListener('message', self.onCouchMessage);
connected = true;
console.debug('⇿ Opened connection ⇿');
};
+
+ self.updateCouchStateIndicator = function () {
+ const { readyState } = couchEventSource;
+ let message = {
+ type: 'state',
+ state: 'pending'
+ };
+ switch (readyState) {
+ case EventSource.CONNECTING:
+ message.state = 'pending';
+ break;
+ case EventSource.OPEN:
+ message.state = 'open';
+ break;
+ case EventSource.CLOSED:
+ message.state = 'close';
+ break;
+ default:
+ message.state = 'unknown';
+ console.error('🚨 Received unexpected readyState value from CouchDB EventSource feed: 🚨', readyState);
+ break;
+ }
+
+ connections.forEach(function (connection) {
+ connection.postMessage(message);
+ });
+ };
}());
diff --git a/src/plugins/persistence/couch/CouchDocument.js b/src/plugins/persistence/couch/CouchDocument.js
index e08269852..920793c6d 100644
--- a/src/plugins/persistence/couch/CouchDocument.js
+++ b/src/plugins/persistence/couch/CouchDocument.js
@@ -45,8 +45,7 @@ export default function CouchDocument(id, model, rev, markDeleted) {
"category": "domain object",
"type": model.type,
"owner": "admin",
- "name": model.name,
- "created": Date.now()
+ "name": model.name
},
"model": model
};
diff --git a/src/plugins/persistence/couch/CouchObjectProvider.js b/src/plugins/persistence/couch/CouchObjectProvider.js
index e7ab40025..8c3fa5fa1 100644
--- a/src/plugins/persistence/couch/CouchObjectProvider.js
+++ b/src/plugins/persistence/couch/CouchObjectProvider.js
@@ -22,7 +22,8 @@
import CouchDocument from "./CouchDocument";
import CouchObjectQueue from "./CouchObjectQueue";
-import { NOTEBOOK_TYPE } from '../../notebook/notebook-constants.js';
+import { PENDING, CONNECTED, DISCONNECTED, UNKNOWN } from "./CouchStatusIndicator";
+import { isNotebookType } from '../../notebook/notebook-constants.js';
const REV = "_rev";
const ID = "_id";
@@ -30,9 +31,10 @@ const HEARTBEAT = 50000;
const ALL_DOCS = "_all_docs?include_docs=true";
class CouchObjectProvider {
- constructor(openmct, options, namespace) {
- options = this._normalize(options);
+ constructor(openmct, options, namespace, indicator) {
+ options = this.#normalize(options);
this.openmct = openmct;
+ this.indicator = indicator;
this.url = options.url;
this.namespace = namespace;
this.objectQueue = {};
@@ -45,7 +47,7 @@ class CouchObjectProvider {
/**
* @private
*/
- startSharedWorker() {
+ #startSharedWorker() {
let provider = this;
let sharedWorker;
@@ -82,6 +84,9 @@ class CouchObjectProvider {
onSharedWorkerMessage(event) {
if (event.data.type === 'connection') {
this.changesFeedSharedWorkerConnectionId = event.data.connectionId;
+ } else if (event.data.type === 'state') {
+ const state = this.#messageToIndicatorState(event.data.state);
+ this.indicator.setIndicatorToState(state);
} else {
let objectChanges = event.data.objectChanges;
const objectIdentifier = {
@@ -103,8 +108,72 @@ class CouchObjectProvider {
}
}
+ /**
+ * Takes in a state message from the CouchDB SharedWorker and returns an IndicatorState.
+ * @private
+ * @param {'open'|'close'|'pending'} message
+ * @returns {import('./CouchStatusIndicator').IndicatorState}
+ */
+ #messageToIndicatorState(message) {
+ let state;
+ switch (message) {
+ case 'open':
+ state = CONNECTED;
+ break;
+ case 'close':
+ state = DISCONNECTED;
+ break;
+ case 'pending':
+ state = PENDING;
+ break;
+ case 'unknown':
+ state = UNKNOWN;
+ break;
+ }
+
+ return state;
+ }
+
+ /**
+ * Takes an HTTP status code and returns an IndicatorState
+ * @private
+ * @param {number} statusCode
+ * @returns {import("./CouchStatusIndicator").IndicatorState}
+ */
+ #statusCodeToIndicatorState(statusCode) {
+ let state;
+ switch (statusCode) {
+ case CouchObjectProvider.HTTP_OK:
+ case CouchObjectProvider.HTTP_CREATED:
+ case CouchObjectProvider.HTTP_ACCEPTED:
+ case CouchObjectProvider.HTTP_NOT_MODIFIED:
+ case CouchObjectProvider.HTTP_BAD_REQUEST:
+ case CouchObjectProvider.HTTP_UNAUTHORIZED:
+ case CouchObjectProvider.HTTP_FORBIDDEN:
+ case CouchObjectProvider.HTTP_NOT_FOUND:
+ case CouchObjectProvider.HTTP_METHOD_NOT_ALLOWED:
+ case CouchObjectProvider.HTTP_NOT_ACCEPTABLE:
+ case CouchObjectProvider.HTTP_CONFLICT:
+ case CouchObjectProvider.HTTP_PRECONDITION_FAILED:
+ case CouchObjectProvider.HTTP_REQUEST_ENTITY_TOO_LARGE:
+ case CouchObjectProvider.HTTP_UNSUPPORTED_MEDIA_TYPE:
+ case CouchObjectProvider.HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
+ case CouchObjectProvider.HTTP_EXPECTATION_FAILED:
+ case CouchObjectProvider.HTTP_SERVER_ERROR:
+ state = CONNECTED;
+ break;
+ case CouchObjectProvider.HTTP_SERVICE_UNAVAILABLE:
+ state = DISCONNECTED;
+ break;
+ default:
+ state = UNKNOWN;
+ }
+
+ return state;
+ }
+
//backwards compatibility, options used to be a url. Now it's an object
- _normalize(options) {
+ #normalize(options) {
if (typeof options === 'string') {
return {
url: options
@@ -114,10 +183,11 @@ class CouchObjectProvider {
return options;
}
- request(subPath, method, body, signal) {
+ async request(subPath, method, body, signal) {
let fetchOptions = {
method,
body,
+ priority: 'high',
signal
};
@@ -129,14 +199,49 @@ class CouchObjectProvider {
};
}
- return fetch(this.url + '/' + subPath, fetchOptions)
- .then((response) => {
- if (response.status === CouchObjectProvider.HTTP_CONFLICT) {
- throw new this.openmct.objects.errors.Conflict(`Conflict persisting ${fetchOptions.body.name}`);
- }
+ let response = null;
- return response.json();
- });
+ if (!this.isObservingObjectChanges()) {
+ this.#observeObjectChanges();
+ }
+
+ try {
+ response = await fetch(this.url + '/' + subPath, fetchOptions);
+ const { status } = response;
+ const json = await response.json();
+ this.#handleResponseCode(status, json, fetchOptions);
+
+ return json;
+ } catch (error) {
+ // Network error, CouchDB unreachable.
+ if (response === null) {
+ this.indicator.setIndicatorToState(DISCONNECTED);
+ console.error(error.message);
+ throw new Error(`CouchDB Error - No response"`);
+ } else {
+ console.error(error.message);
+
+ throw error;
+ }
+ }
+ }
+
+ /**
+ * Handle the response code from a CouchDB request.
+ * Sets the CouchDB indicator status and throws an error if needed.
+ * @private
+ */
+ #handleResponseCode(status, json, fetchOptions) {
+ this.indicator.setIndicatorToState(this.#statusCodeToIndicatorState(status));
+ if (status === CouchObjectProvider.HTTP_CONFLICT) {
+ throw new this.openmct.objects.errors.Conflict(`Conflict persisting ${fetchOptions.body.name}`);
+ } else if (status >= CouchObjectProvider.HTTP_BAD_REQUEST) {
+ if (!json.error || !json.reason) {
+ throw new Error(`CouchDB Error ${status}`);
+ }
+
+ throw new Error(`CouchDB Error ${status}: "${json.error} - ${json.reason}"`);
+ }
}
/**
@@ -146,7 +251,7 @@ class CouchObjectProvider {
* persist any queued objects
* @private
*/
- checkResponse(response, intermediateResponse, key) {
+ #checkResponse(response, intermediateResponse, key) {
let requestSuccess = false;
const id = response ? response.id : undefined;
let rev;
@@ -166,7 +271,7 @@ class CouchObjectProvider {
this.objectQueue[id].updateRevision(rev);
this.objectQueue[id].pending = false;
if (this.objectQueue[id].hasNext()) {
- this.updateQueued(id);
+ this.#updateQueued(id);
}
} else {
this.objectQueue[key].pending = false;
@@ -176,7 +281,7 @@ class CouchObjectProvider {
/**
* @private
*/
- getModel(response) {
+ #getModel(response) {
if (response && response.model) {
let key = response[ID];
let object = this.fromPersistedModel(response.model, key);
@@ -185,7 +290,7 @@ class CouchObjectProvider {
this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]);
}
- if (object.type === NOTEBOOK_TYPE) {
+ if (isNotebookType(object) || object.type === 'annotation') {
//Temporary measure until object sync is supported for all object types
//Always update notebook revision number because we have realtime sync, so always assume it's the latest.
this.objectQueue[key].updateRevision(response[REV]);
@@ -204,7 +309,7 @@ class CouchObjectProvider {
this.batchIds.push(identifier.key);
if (this.bulkPromise === undefined) {
- this.bulkPromise = this.deferBatchedGet(abortSignal);
+ this.bulkPromise = this.#deferBatchedGet(abortSignal);
}
return this.bulkPromise
@@ -216,23 +321,23 @@ class CouchObjectProvider {
/**
* @private
*/
- deferBatchedGet(abortSignal) {
+ #deferBatchedGet(abortSignal) {
// We until the next event loop cycle to "collect" all of the get
// requests triggered in this iteration of the event loop
- return this.waitOneEventCycle().then(() => {
+ return this.#waitOneEventCycle().then(() => {
let batchIds = this.batchIds;
- this.clearBatch();
+ this.#clearBatch();
if (batchIds.length === 1) {
let objectKey = batchIds[0];
//If there's only one request, just do a regular get
return this.request(objectKey, "GET", undefined, abortSignal)
- .then(this.returnAsMap(objectKey));
+ .then(this.#returnAsMap(objectKey));
} else {
- return this.bulkGet(batchIds, abortSignal);
+ return this.#bulkGet(batchIds, abortSignal);
}
});
}
@@ -240,10 +345,10 @@ class CouchObjectProvider {
/**
* @private
*/
- returnAsMap(objectKey) {
+ #returnAsMap(objectKey) {
return (result) => {
let objectMap = {};
- objectMap[objectKey] = this.getModel(result);
+ objectMap[objectKey] = this.#getModel(result);
return objectMap;
};
@@ -252,7 +357,7 @@ class CouchObjectProvider {
/**
* @private
*/
- clearBatch() {
+ #clearBatch() {
this.batchIds = [];
delete this.bulkPromise;
}
@@ -260,7 +365,7 @@ class CouchObjectProvider {
/**
* @private
*/
- waitOneEventCycle() {
+ #waitOneEventCycle() {
return new Promise((resolve) => {
setTimeout(resolve);
});
@@ -269,7 +374,7 @@ class CouchObjectProvider {
/**
* @private
*/
- bulkGet(ids, signal) {
+ #bulkGet(ids, signal) {
ids = this.removeDuplicates(ids);
const query = {
@@ -279,8 +384,10 @@ class CouchObjectProvider {
return this.request(ALL_DOCS, 'POST', query, signal).then((response) => {
if (response && response.rows !== undefined) {
return response.rows.reduce((map, row) => {
+ //row.doc === null if the document does not exist.
+ //row.doc === undefined if the document is not found.
if (row.doc !== undefined) {
- map[row.key] = this.getModel(row.doc);
+ map[row.key] = this.#getModel(row.doc);
}
return map;
@@ -349,7 +456,7 @@ class CouchObjectProvider {
if (json) {
let docs = json.docs;
docs.forEach(doc => {
- let object = this.getModel(doc);
+ let object = this.#getModel(doc);
if (object) {
objects.push(object);
}
@@ -368,7 +475,7 @@ class CouchObjectProvider {
this.observers[keyString].push(callback);
if (!this.isObservingObjectChanges()) {
- this.observeObjectChanges();
+ this.#observeObjectChanges();
}
return () => {
@@ -376,9 +483,6 @@ class CouchObjectProvider {
this.observers[keyString] = this.observers[keyString].filter(observer => observer !== callback);
if (this.observers[keyString].length === 0) {
delete this.observers[keyString];
- if (Object.keys(this.observers).length === 0 && this.isObservingObjectChanges()) {
- this.stopObservingObjectChanges();
- }
}
}
};
@@ -391,7 +495,7 @@ class CouchObjectProvider {
/**
* @private
*/
- observeObjectChanges() {
+ #observeObjectChanges() {
const sseChangesPath = `${this.url}/_changes`;
const sseURL = new URL(sseChangesPath);
sseURL.searchParams.append('feed', 'eventsource');
@@ -401,17 +505,16 @@ class CouchObjectProvider {
if (typeof SharedWorker === 'undefined') {
this.fetchChanges(sseURL.toString());
} else {
- this.initiateSharedWorkerFetchChanges(sseURL.toString());
+ this.#initiateSharedWorkerFetchChanges(sseURL.toString());
}
-
}
/**
* @private
*/
- initiateSharedWorkerFetchChanges(url) {
+ #initiateSharedWorkerFetchChanges(url) {
if (!this.changesFeedSharedWorker) {
- this.changesFeedSharedWorker = this.startSharedWorker();
+ this.changesFeedSharedWorker = this.#startSharedWorker();
if (this.isObservingObjectChanges()) {
this.stopObservingObjectChanges();
@@ -430,18 +533,24 @@ class CouchObjectProvider {
onEventError(error) {
console.error('Error on feed', error);
- if (Object.keys(this.observers).length > 0) {
- this.observeObjectChanges();
- }
+ const { readyState } = error.target;
+ this.#updateIndicatorStatus(readyState);
+ }
+
+ onEventOpen(event) {
+ const { readyState } = event.target;
+ this.#updateIndicatorStatus(readyState);
}
onEventMessage(event) {
+ const { readyState } = event.target;
const eventData = JSON.parse(event.data);
const identifier = {
namespace: this.namespace,
key: eventData.id
};
const keyString = this.openmct.objects.makeKeyString(identifier);
+ this.#updateIndicatorStatus(readyState);
let observersForObject = this.observers[keyString];
if (observersForObject) {
@@ -464,24 +573,26 @@ class CouchObjectProvider {
this.stopObservingObjectChanges = () => {
controller.abort();
- couchEventSource.removeEventListener('message', this.onEventMessage);
+ couchEventSource.removeEventListener('message', this.onEventMessage.bind(this));
delete this.stopObservingObjectChanges;
};
console.debug('⇿ Opening CouchDB change feed connection ⇿');
couchEventSource = new EventSource(url);
- couchEventSource.onerror = this.onEventError;
+ couchEventSource.onerror = this.onEventError.bind(this);
+ couchEventSource.onopen = this.onEventOpen.bind(this);
// start listening for events
- couchEventSource.addEventListener('message', this.onEventMessage);
+ couchEventSource.addEventListener('message', this.onEventMessage.bind(this));
+
console.debug('⇿ Opened connection ⇿');
}
/**
* @private
*/
- getIntermediateResponse() {
+ #getIntermediateResponse() {
let intermediateResponse = {};
intermediateResponse.promise = new Promise(function (resolve, reject) {
intermediateResponse.resolve = resolve;
@@ -492,6 +603,31 @@ class CouchObjectProvider {
}
/**
+ * Update the indicator status based on the readyState of the EventSource
+ * @private
+ */
+ #updateIndicatorStatus(readyState) {
+ let message;
+ switch (readyState) {
+ case EventSource.CONNECTING:
+ message = 'pending';
+ break;
+ case EventSource.OPEN:
+ message = 'open';
+ break;
+ case EventSource.CLOSED:
+ message = 'close';
+ break;
+ default:
+ message = 'unknown';
+ break;
+ }
+
+ const indicatorState = this.#messageToIndicatorState(message);
+ this.indicator.setIndicatorToState(indicatorState);
+ }
+
+ /**
* @private
*/
enqueueObject(key, model, intermediateResponse) {
@@ -509,7 +645,7 @@ class CouchObjectProvider {
}
create(model) {
- let intermediateResponse = this.getIntermediateResponse();
+ let intermediateResponse = this.#getIntermediateResponse();
const key = model.identifier.key;
model = this.toPersistableModel(model);
this.enqueueObject(key, model, intermediateResponse);
@@ -518,9 +654,9 @@ class CouchObjectProvider {
this.objectQueue[key].pending = true;
const queued = this.objectQueue[key].dequeue();
let document = new CouchDocument(key, queued.model);
+ document.metadata.created = Date.now();
this.request(key, "PUT", document).then((response) => {
- console.log('create check response', key);
- this.checkResponse(response, queued.intermediateResponse, key);
+ this.#checkResponse(response, queued.intermediateResponse, key);
}).catch(error => {
queued.intermediateResponse.reject(error);
this.objectQueue[key].pending = false;
@@ -533,13 +669,13 @@ class CouchObjectProvider {
/**
* @private
*/
- updateQueued(key) {
+ #updateQueued(key) {
if (!this.objectQueue[key].pending) {
this.objectQueue[key].pending = true;
const queued = this.objectQueue[key].dequeue();
let document = new CouchDocument(key, queued.model, this.objectQueue[key].rev);
this.request(key, "PUT", document).then((response) => {
- this.checkResponse(response, queued.intermediateResponse, key);
+ this.#checkResponse(response, queued.intermediateResponse, key);
}).catch((error) => {
queued.intermediateResponse.reject(error);
this.objectQueue[key].pending = false;
@@ -548,12 +684,12 @@ class CouchObjectProvider {
}
update(model) {
- let intermediateResponse = this.getIntermediateResponse();
+ let intermediateResponse = this.#getIntermediateResponse();
const key = model.identifier.key;
model = this.toPersistableModel(model);
this.enqueueObject(key, model, intermediateResponse);
- this.updateQueued(key);
+ this.#updateQueued(key);
return intermediateResponse.promise;
}
@@ -577,6 +713,25 @@ class CouchObjectProvider {
}
}
+// https://docs.couchdb.org/en/3.2.0/api/basics.html
+CouchObjectProvider.HTTP_OK = 200;
+CouchObjectProvider.HTTP_CREATED = 201;
+CouchObjectProvider.HTTP_ACCEPTED = 202;
+CouchObjectProvider.HTTP_NOT_MODIFIED = 304;
+CouchObjectProvider.HTTP_BAD_REQUEST = 400;
+CouchObjectProvider.HTTP_UNAUTHORIZED = 401;
+CouchObjectProvider.HTTP_FORBIDDEN = 403;
+CouchObjectProvider.HTTP_NOT_FOUND = 404;
+CouchObjectProvider.HTTP_METHOD_NOT_ALLOWED = 404;
+CouchObjectProvider.HTTP_NOT_ACCEPTABLE = 406;
CouchObjectProvider.HTTP_CONFLICT = 409;
+CouchObjectProvider.HTTP_PRECONDITION_FAILED = 412;
+CouchObjectProvider.HTTP_REQUEST_ENTITY_TOO_LARGE = 413;
+CouchObjectProvider.HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
+CouchObjectProvider.HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
+CouchObjectProvider.HTTP_EXPECTATION_FAILED = 417;
+CouchObjectProvider.HTTP_SERVER_ERROR = 500;
+// If CouchDB is containerized via Docker it will return 503 if service is unavailable.
+CouchObjectProvider.HTTP_SERVICE_UNAVAILABLE = 503;
export default CouchObjectProvider;
diff --git a/src/plugins/persistence/couch/CouchSearchProvider.js b/src/plugins/persistence/couch/CouchSearchProvider.js
index 3ab268d54..3b2295e99 100644
--- a/src/plugins/persistence/couch/CouchSearchProvider.js
+++ b/src/plugins/persistence/couch/CouchSearchProvider.js
@@ -30,9 +30,29 @@
class CouchSearchProvider {
constructor(couchObjectProvider) {
this.couchObjectProvider = couchObjectProvider;
+ this.searchTypes = couchObjectProvider.openmct.objects.SEARCH_TYPES;
+ this.supportedSearchTypes = [this.searchTypes.OBJECTS, this.searchTypes.ANNOTATIONS, this.searchTypes.NOTEBOOK_ANNOTATIONS, this.searchTypes.TAGS];
}
- search(query, abortSignal) {
+ supportsSearchType(searchType) {
+ return this.supportedSearchTypes.includes(searchType);
+ }
+
+ search(query, abortSignal, searchType) {
+ if (searchType === this.searchTypes.OBJECTS) {
+ return this.searchForObjects(query, abortSignal);
+ } else if (searchType === this.searchTypes.ANNOTATIONS) {
+ return this.searchForAnnotations(query, abortSignal);
+ } else if (searchType === this.searchTypes.NOTEBOOK_ANNOTATIONS) {
+ return this.searchForNotebookAnnotations(query, abortSignal);
+ } else if (searchType === this.searchTypes.TAGS) {
+ return this.searchForTags(query, abortSignal);
+ } else {
+ throw new Error(`🤷‍♂️ Unknown search type passed: ${searchType}`);
+ }
+ }
+
+ searchForObjects(query, abortSignal) {
const filter = {
"selector": {
"model": {
@@ -45,5 +65,86 @@ class CouchSearchProvider {
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
}
+
+ searchForAnnotations(keyString, abortSignal) {
+ const filter = {
+ "selector": {
+ "$and": [
+ {
+ "model": {
+ "targets": {
+ }
+ }
+ },
+ {
+ "model.type": {
+ "$eq": "annotation"
+ }
+ }
+ ]
+ }
+ };
+ filter.selector.$and[0].model.targets[keyString] = {
+ "$exists": true
+ };
+
+ return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
+ }
+
+ searchForNotebookAnnotations({targetKeyString, entryId}, abortSignal) {
+ const filter = {
+ "selector": {
+ "$and": [
+ {
+ "model.type": {
+ "$eq": "annotation"
+ }
+ },
+ {
+ "model.annotationType": {
+ "$eq": "NOTEBOOK"
+ }
+ },
+ {
+ "model": {
+ "targets": {
+ }
+ }
+ }
+ ]
+ }
+ };
+ filter.selector.$and[2].model.targets[targetKeyString] = {
+ "entryId": {
+ "$eq": entryId
+ }
+ };
+
+ return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
+ }
+
+ searchForTags(tagsArray, abortSignal) {
+ const filter = {
+ "selector": {
+ "$and": [
+ {
+ "model.tags": {
+ "$elemMatch": {
+ "$eq": `${tagsArray[0]}`
+ }
+ }
+ },
+ {
+ "model.type": {
+ "$eq": "annotation"
+ }
+ }
+ ]
+ }
+ };
+
+ return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
+ }
+
}
export default CouchSearchProvider;
diff --git a/src/plugins/persistence/couch/CouchStatusIndicator.js b/src/plugins/persistence/couch/CouchStatusIndicator.js
new file mode 100644
index 000000000..069a59e22
--- /dev/null
+++ b/src/plugins/persistence/couch/CouchStatusIndicator.js
@@ -0,0 +1,88 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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.
+ *****************************************************************************/
+
+/**
+ * @typedef {Object} IndicatorState
+ * An object defining the visible state of the indicator.
+ * @property {string} statusClass - The class to apply to the indicator.
+ * @property {string} text - The text to display in the indicator.
+ * @property {string} description - The description to display in the indicator.
+ */
+
+/**
+ * Set of CouchDB connection states; changes among these states will be
+ * reflected in the indicator's appearance.
+ * CONNECTED: Everything nominal, expect to be able to read/write.
+ * DISCONNECTED: HTTP request failed (network error). Unable to reach server at all.
+ * PENDING: Still trying to connect, and haven't failed yet.
+ * MAINTENANCE: CouchDB is connected but not accepting requests.
+ */
+
+/** @type {IndicatorState} */
+export const CONNECTED = {
+ statusClass: "s-status-on",
+ text: "CouchDB is connected",
+ description: "CouchDB is online and accepting requests."
+};
+/** @type {IndicatorState} */
+export const PENDING = {
+ statusClass: "s-status-warning-lo",
+ text: "Attempting to connect to CouchDB...",
+ description: "Checking status of CouchDB, please stand by..."
+};
+/** @type {IndicatorState} */
+export const DISCONNECTED = {
+ statusClass: "s-status-warning-hi",
+ text: "CouchDB is offline",
+ description: "CouchDB is offline and unavailable for requests."
+};
+/** @type {IndicatorState} */
+export const UNKNOWN = {
+ statusClass: "s-status-info",
+ text: "CouchDB connectivity unknown",
+ description: "CouchDB is in an unknown state of connectivity."
+};
+
+export default class CouchStatusIndicator {
+ constructor(simpleIndicator) {
+ this.indicator = simpleIndicator;
+ this.#setDefaults();
+ }
+
+ /**
+ * Set the default values for the indicator.
+ * @private
+ */
+ #setDefaults() {
+ this.setIndicatorToState(PENDING);
+ }
+
+ /**
+ * Set the indicator to the given state.
+ * @param {IndicatorState} state
+ */
+ setIndicatorToState(state) {
+ this.indicator.text(state.text);
+ this.indicator.description(state.description);
+ this.indicator.statusClass(state.statusClass);
+ }
+}
diff --git a/src/plugins/persistence/couch/README.md b/src/plugins/persistence/couch/README.md
index eaa6f99d3..8b17e1478 100644
--- a/src/plugins/persistence/couch/README.md
+++ b/src/plugins/persistence/couch/README.md
@@ -1,48 +1,145 @@
-# Introduction
-These instructions are for setting up CouchDB for a **development** environment. For a production environment, we recommend running OpenMCT behind a proxy server (e.g., Nginx or Apache), and securing the CouchDB server properly:
-https://docs.couchdb.org/en/main/intro/security.html
# Installing CouchDB
-## OSX
-1. Install CouchDB using: `brew install couchdb`
-2. Edit `/usr/local/etc/local.ini` and add and admin password:
- ```
+
+## Introduction
+
+These instructions are for setting up CouchDB for a **development** environment. For a production environment, we recommend running Open MCT behind a proxy server (e.g., Nginx or Apache), and securing the CouchDB server properly:
+<https://docs.couchdb.org/en/main/intro/security.html>
+
+## Docker Quickstart
+
+The following process is the preferred way of using CouchDB as it is automatic and closely resembles a production environment.
+
+Requirement:
+Get docker compose (or recent version of docker) installed on your machine. We recommend [Docker Desktop](https://www.docker.com/products/docker-desktop/)
+
+1. Open a terminal to this current working directory (`cd openmct/src/plugins/persistence/couch`)
+2. Create and start the `couchdb` container:
+
+```sh
+docker compose -f ./couchdb-compose.yaml up --detach
+```
+3. Copy `.env.ci` file to file named `.env.local`
+4. (Optional) Change the values of `.env.local` if desired
+5. Set the environment variables in bash by sourcing the env file
+
+```sh
+export $(cat .env.local | xargs)
+```
+
+6. Execute the configuration script:
+
+```sh
+sh ./setup-couchdb.sh
+```
+
+7. `cd` to the workspace root directory (the same directory as `index.html`)
+8. Update `index.html` to use the CouchDB plugin as persistence store:
+
+```sh
+sh ./src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
+```
+9. ✅ Done!
+
+Open MCT will now use your local CouchDB container as its persistence store. Access the CouchDB instance manager by visiting <http://localhost:5984/_utils>.
+
+## macOS
+
+While we highly recommend using the CouchDB docker-compose installation, it is still possible to install CouchDB through other means.
+
+### Installing CouchDB
+
+1. Install CouchDB using: `brew install couchdb`.
+2. Edit `/usr/local/etc/local.ini` and add the following settings:
+
+ ```txt
[admins]
admin = youradminpassword
```
+
And set the server up for single node:
- ```
+
+ ```txt
[couchdb]
single_node=true
```
-3. Start CouchDB by running: `couchdb`
-4. Add the `_global_changes` database using `curl` (note the `youradminpassword` should be changed to what you set above 👆): `curl -X PUT http://admin:youradminpassword@127.0.0.1:5984/_global_changes`
+ Enable CORS
+
+ ```txt
+ [chttpd]
+ enable_cors = true
+ [cors]
+ origins = http://localhost:8080
+ ```
+
+
+### Installing CouchDB without admin privileges to your computer
+
+If `brew` is not available on your mac machine, you'll need to get the CouchDB installed using the official sourcefiles.
+1. Install CouchDB following these instructions: <https://docs.brew.sh/Installation#untar-anywhere>.
+1. Edit `local.ini` in Homebrew's `/etc/` directory as directed above in the 'Installing with admin privileges to your computer' section.
+
## Other Operating Systems
-Follow the installation instructions from the CouchDB installation guide: https://docs.couchdb.org/en/stable/install/index.html
-# Configuring OpenMCT
-1. Navigate to http://localhost:5984/_utils
-2. Create a database called `openmct`
-3. In your OpenMCT directory, edit `openmct/index.html`, and comment out:
-```
-openmct.install(openmct.plugins.LocalStorage());
-```
-Add a line to install the CouchDB plugin for OpenMCT:
-```
-openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct"));
+Follow the installation instructions from the CouchDB installation guide: <https://docs.couchdb.org/en/stable/install/index.html>
+
+# Configuring CouchDB
+
+## Configuration script
+
+The simplest way to config a CouchDB instance is to use our provided tooling:
+1. Copy `.env.ci` file to file named `.env.local`
+2. Set the environment variables in bash by sourcing the env file
+
+```sh
+export $(cat .env.local | xargs)
```
-6. Enable cors in CouchDB by editing `~/homebrew/etc/local.ini` and add: `
+
+3. Execute the configuration script:
+
+```sh
+sh ./setup-couchdb.sh
```
-[chttpd]
-enable_cors = true
-[cors]
-origins = http://localhost:8080
+## Manual Configuration
+
+1. Start CouchDB by running: `couchdb`.
+2. Add the `_global_changes` database using `curl` (note the `youradminpassword` should be changed to what you set above 👆): `curl -X PUT http://admin:youradminpassword@127.0.0.1:5984/_global_changes`
+3. Navigate to <http://localhost:5984/_utils>
+4. Create a database called `openmct`
+5. Navigate to <http://127.0.0.1:5984/_utils/#/database/openmct/permissions>
+6. Remove permission restrictions in CouchDB from Open MCT by deleting `_admin` roles for both `Admin` and `Member`.
+
+# Configuring Open MCT to use CouchDB
+
+## Configuration script
+The simplest way to config a CouchDB instance is to use our provided tooling:
+1. `cd` to the workspace root directory (the same directory as `index.html`)
+2. Update `index.html` to use the CouchDB plugin as persistence store:
+
+```sh
+sh ./src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
```
-7. Remove permission restrictions in CouchDB from OpenMCT by navigating to http://127.0.0.1:5984/_utils/#/database/openmct/permissions and deleting `_admin` roles for both `Admin` and `Member`.
-8. Start openmct by running `npm start` in the OpenMCT directory.
-9. Navigate to http://localhost:8080/ and create a random object in OpenMCT (e.g., a `Clock`) and save. You may get an error saying that the objects failed to persist. This is a known error that you can ignore, and will only happen the first time you save.
-10. Navigate to: http://127.0.0.1:5984/_utils/#database/openmct/_all_docs
-11. Look at the `JSON` tab and ensure you can see the `Clock` object you created above.
-12. All done! 🏆
+
+## Manual Configuration
+
+1. Edit `openmct/index.html` comment out the following line:
+
+ ```js
+ openmct.install(openmct.plugins.LocalStorage());
+ ```
+
+ Add a line to install the CouchDB plugin for Open MCT:
+
+ ```js
+ openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct"));
+ ```
+
+# Validating a successful Installation
+
+1. Start Open MCT by running `npm start` in the `openmct` path.
+2. Navigate to <http://localhost:8080/> and create a random object in Open MCT (e.g., a 'Clock') and save. You may get an error saying that the object failed to persist - this is a known error that you can ignore, and will only happen the first time you save - just try again.
+3. Navigate to: <http://127.0.0.1:5984/_utils/#database/openmct/_all_docs>
+4. Look at the 'JSON' tab and ensure you can see the specific object you created above.
+5. All done! 🏆
diff --git a/src/plugins/persistence/couch/couchdb-compose.yaml b/src/plugins/persistence/couch/couchdb-compose.yaml
new file mode 100644
index 000000000..40e58ff0a
--- /dev/null
+++ b/src/plugins/persistence/couch/couchdb-compose.yaml
@@ -0,0 +1,14 @@
+version: "3"
+services:
+ couchdb:
+ image: couchdb:${COUCHDB_IMAGE_TAG:-3.2.1}
+ ports:
+ - "5984:5984"
+ - "5986:5986"
+ volumes:
+ - couchdb:/opt/couchdb/data
+ environment:
+ COUCHDB_USER: admin
+ COUCHDB_PASSWORD: password
+volumes:
+ couchdb:
diff --git a/src/plugins/persistence/couch/plugin.js b/src/plugins/persistence/couch/plugin.js
index 5796b9695..eaaed0444 100644
--- a/src/plugins/persistence/couch/plugin.js
+++ b/src/plugins/persistence/couch/plugin.js
@@ -22,13 +22,18 @@
import CouchObjectProvider from './CouchObjectProvider';
import CouchSearchProvider from './CouchSearchProvider';
+import CouchStatusIndicator from './CouchStatusIndicator';
+
const NAMESPACE = '';
const LEGACY_SPACE = 'mct';
const COUCH_SEARCH_ONLY_NAMESPACE = `COUCH_SEARCH_${Date.now()}`;
export default function CouchPlugin(options) {
return function install(openmct) {
- install.couchProvider = new CouchObjectProvider(openmct, options, NAMESPACE);
+ const simpleIndicator = openmct.indicators.simpleIndicator();
+ openmct.indicators.add(simpleIndicator);
+ const couchStatusIndicator = new CouchStatusIndicator(simpleIndicator);
+ install.couchProvider = new CouchObjectProvider(openmct, options, NAMESPACE, couchStatusIndicator);
// Unfortunately, for historical reasons, Couch DB produces objects with a mix of namepaces (alternately "mct", and "")
// Installing the same provider under both namespaces means that it can respond to object gets for both namespaces.
diff --git a/src/plugins/persistence/couch/pluginSpec.js b/src/plugins/persistence/couch/pluginSpec.js
index 3db7d7d13..eb041e542 100644
--- a/src/plugins/persistence/couch/pluginSpec.js
+++ b/src/plugins/persistence/couch/pluginSpec.js
@@ -19,11 +19,13 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
+import Vue from 'vue';
import CouchPlugin from './plugin.js';
import {
createOpenMct,
resetApplicationState, spyOnBuiltins
} from 'utils/testing';
+import { CONNECTED, DISCONNECTED, PENDING, UNKNOWN } from './CouchStatusIndicator';
describe('the plugin', () => {
let openmct;
@@ -74,10 +76,11 @@ describe('the plugin', () => {
spyOn(provider, 'get').and.callThrough();
spyOn(provider, 'create').and.callThrough();
spyOn(provider, 'update').and.callThrough();
- spyOn(provider, 'startSharedWorker').and.callThrough();
+ spyOn(provider, 'observe').and.callThrough();
spyOn(provider, 'fetchChanges').and.callThrough();
spyOn(provider, 'onSharedWorkerMessage').and.callThrough();
spyOn(provider, 'onEventMessage').and.callThrough();
+ spyOn(provider, 'isObservingObjectChanges').and.callThrough();
});
afterEach(() => {
@@ -106,10 +109,16 @@ describe('the plugin', () => {
expect(result.identifier.key).toEqual(mockDomainObject.identifier.key);
});
+ it('prioritizes couch requests above other requests', async () => {
+ await openmct.objects.get(mockDomainObject.identifier);
+ const fetchOptions = fetch.calls.mostRecent().args[1];
+ expect(fetchOptions.priority).toEqual('high');
+ });
+
it('creates an object and starts shared worker', async () => {
const result = await openmct.objects.save(mockDomainObject);
expect(provider.create).toHaveBeenCalled();
- expect(provider.startSharedWorker).toHaveBeenCalled();
+ expect(provider.observe).toHaveBeenCalled();
expect(result).toBeTrue();
});
@@ -149,7 +158,10 @@ describe('the plugin', () => {
mockDomainObject.id = mockDomainObject.identifier.key;
const fakeUpdateEvent = {
- data: JSON.stringify(mockDomainObject)
+ data: JSON.stringify(mockDomainObject),
+ target: {
+ readyState: EventSource.CONNECTED
+ }
};
// eslint-disable-next-line require-await
@@ -165,7 +177,8 @@ describe('the plugin', () => {
const result = await openmct.objects.save(mockDomainObject);
expect(result).toBeTrue();
expect(provider.create).toHaveBeenCalled();
- expect(provider.startSharedWorker).not.toHaveBeenCalled();
+ expect(provider.observe).toHaveBeenCalled();
+ expect(provider.isObservingObjectChanges).toHaveBeenCalled();
//Set modified timestamp it detects a change and persists the updated model.
mockDomainObject.modified = mockDomainObject.persisted + 1;
@@ -176,6 +189,7 @@ describe('the plugin', () => {
expect(updatedResult).toBeTrue();
expect(provider.update).toHaveBeenCalled();
expect(provider.fetchChanges).toHaveBeenCalled();
+ expect(provider.isObservingObjectChanges.calls.mostRecent().returnValue).toBe(true);
sharedWorkerCallback(fakeUpdateEvent);
expect(provider.onEventMessage).toHaveBeenCalled();
@@ -277,3 +291,153 @@ describe('the plugin', () => {
});
});
});
+
+describe('the view', () => {
+ let openmct;
+ let options;
+ let appHolder;
+ let testPath = 'http://localhost:9990/openmct';
+ let provider;
+ let mockDomainObject;
+ beforeEach((done) => {
+ openmct = createOpenMct();
+ spyOnBuiltins(['fetch'], window);
+ options = {
+ url: testPath,
+ filter: {}
+ };
+ mockDomainObject = {
+ identifier: {
+ namespace: '',
+ key: 'some-value'
+ },
+ type: 'notebook',
+ modified: 0
+ };
+ openmct.install(new CouchPlugin(options));
+ appHolder = document.createElement('div');
+ document.body.appendChild(appHolder);
+ openmct.on('start', done);
+ openmct.start(appHolder);
+ provider = openmct.objects.getProvider(mockDomainObject.identifier);
+ spyOn(provider, 'onSharedWorkerMessage').and.callThrough();
+ });
+
+ afterEach(() => {
+ return resetApplicationState(openmct);
+ });
+
+ describe('updates CouchDB status indicator', () => {
+ let mockPromise;
+
+ function assertCouchIndicatorStatus(status) {
+ const indicator = appHolder.querySelector('.c-indicator--simple');
+ expect(indicator).not.toBeNull();
+ expect(indicator).toHaveClass(status.statusClass);
+ expect(indicator.textContent).toMatch(new RegExp(status.text, 'i'));
+ expect(indicator.title).toMatch(new RegExp(status.title, 'i'));
+ }
+
+ it("to 'connected' on successful request", async () => {
+ mockPromise = Promise.resolve({
+ status: 200,
+ json: () => {
+ return {
+ ok: true,
+ _id: 'some-value',
+ id: 'some-value',
+ _rev: 1,
+ model: {}
+ };
+ }
+ });
+ fetch.and.returnValue(mockPromise);
+
+ await openmct.objects.get({
+ namespace: '',
+ key: 'object-1'
+ });
+ await Vue.nextTick();
+
+ assertCouchIndicatorStatus(CONNECTED);
+ });
+
+ it("to 'disconnected' on failed request", async () => {
+ fetch.and.throwError(new TypeError('ERR_CONNECTION_REFUSED'));
+
+ await openmct.objects.get({
+ namespace: '',
+ key: 'object-1'
+ });
+ await Vue.nextTick();
+
+ assertCouchIndicatorStatus(DISCONNECTED);
+ });
+
+ it("to 'pending'", async () => {
+ const workerMessage = {
+ data: {
+ type: 'state',
+ state: 'pending'
+ }
+ };
+ mockPromise = Promise.resolve({
+ status: 200,
+ json: () => {
+ return {
+ ok: true,
+ _id: 'some-value',
+ id: 'some-value',
+ _rev: 1,
+ model: {}
+ };
+ }
+ });
+ fetch.and.returnValue(mockPromise);
+
+ await openmct.objects.get({
+ namespace: '',
+ key: 'object-1'
+ });
+
+ // Simulate 'pending' state from worker message
+ provider.onSharedWorkerMessage(workerMessage);
+ await Vue.nextTick();
+
+ assertCouchIndicatorStatus(PENDING);
+ });
+
+ it("to 'unknown'", async () => {
+ const workerMessage = {
+ data: {
+ type: 'state',
+ state: 'unknown'
+ }
+ };
+ mockPromise = Promise.resolve({
+ status: 200,
+ json: () => {
+ return {
+ ok: true,
+ _id: 'some-value',
+ id: 'some-value',
+ _rev: 1,
+ model: {}
+ };
+ }
+ });
+ fetch.and.returnValue(mockPromise);
+
+ await openmct.objects.get({
+ namespace: '',
+ key: 'object-1'
+ });
+
+ // Simulate 'pending' state from worker message
+ provider.onSharedWorkerMessage(workerMessage);
+ await Vue.nextTick();
+
+ assertCouchIndicatorStatus(UNKNOWN);
+ });
+ });
+});
diff --git a/src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh b/src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
new file mode 100644
index 000000000..4fbc50d4e
--- /dev/null
+++ b/src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
@@ -0,0 +1,3 @@
+#!/bin/bash -e
+
+sed -i'.bak' -e 's/LocalStorage()/CouchDB("http:\/\/localhost:5984\/openmct")/g' index.html
diff --git a/src/plugins/persistence/couch/setup-couchdb.sh b/src/plugins/persistence/couch/setup-couchdb.sh
new file mode 100755
index 000000000..482f370f9
--- /dev/null
+++ b/src/plugins/persistence/couch/setup-couchdb.sh
@@ -0,0 +1,144 @@
+#!/bin/bash -e
+
+# Do a couple checks for environment variables we expect to have a value.
+
+if [ -z "${OPENMCT_DATABASE_NAME}" ] ; then
+ echo "OPENMCT_DATABASE_NAME has no value" 1>&2
+ exit 1
+fi
+
+if [ -z "${COUCH_ADMIN_USER}" ] ; then
+ echo "COUCH_ADMIN_USER has no value" 1>&2
+ exit 1
+fi
+
+if [ -z "${COUCH_BASE_LOCAL}" ] ; then
+ echo "COUCH_BASE_LOCAL has no value" 1>&2
+ exit 1
+fi
+
+# Come up with what we'll be providing to curl's -u option. Always supply the username from the environment,
+# and optionally supply the password from the environment, if it has a value.
+CURL_USERPASS_ARG="${COUCH_ADMIN_USER}"
+if [ "${COUCH_ADMIN_PASSWORD}" ] ; then
+ CURL_USERPASS_ARG+=":${COUCH_ADMIN_PASSWORD}"
+fi
+
+system_tables_exist () {
+ resource_exists $COUCH_BASE_LOCAL/_users
+}
+
+create_users_db () {
+ curl -su "${CURL_USERPASS_ARG}" -X PUT $COUCH_BASE_LOCAL/_users
+}
+
+create_replicator_db () {
+ curl -su "${CURL_USERPASS_ARG}" -X PUT $COUCH_BASE_LOCAL/_replicator
+}
+
+setup_system_tables () {
+ users_db_response=$(create_users_db)
+ if [ "{\"ok\":true}" == "${users_db_response}" ]; then
+ echo Successfully created users db
+ replicator_db_response=$(create_replicator_db)
+ if [ "{\"ok\":true}" == "${replicator_db_response}" ]; then
+ echo Successfully created replicator DB
+ else
+ echo Unable to create replicator DB
+ fi
+ else
+ echo Unable to create users db
+ fi
+}
+
+resource_exists () {
+ response=$(curl -u "${CURL_USERPASS_ARG}" -s -o /dev/null -I -w "%{http_code}" $1);
+ if [ "200" == "${response}" ]; then
+ echo "TRUE"
+ else
+ echo "FALSE";
+ fi
+}
+
+db_exists () {
+ resource_exists $COUCH_BASE_LOCAL/$OPENMCT_DATABASE_NAME
+}
+
+create_db () {
+ response=$(curl -su "${CURL_USERPASS_ARG}" -XPUT $COUCH_BASE_LOCAL/$OPENMCT_DATABASE_NAME);
+ echo $response
+}
+
+admin_user_exists () {
+ response=$(curl -su "${CURL_USERPASS_ARG}" -o /dev/null -I -w "%{http_code}" $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/admins/$COUCH_ADMIN_USER);
+ if [ "200" == "${response}" ]; then
+ echo "TRUE"
+ else
+ echo "FALSE";
+ fi
+}
+
+create_admin_user () {
+ echo Creating admin user
+ curl -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/admins/$COUCH_ADMIN_USER -d \'"$COUCH_ADMIN_PASSWORD"\'
+}
+
+is_cors_enabled() {
+ resource_exists $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/httpd/enable_cors
+}
+
+enable_cors () {
+ curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/httpd/enable_cors -d '"true"'
+ curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/cors/origins -d '"*"'
+ curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/cors/credentials -d '"true"'
+ curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/cors/methods -d '"GET, PUT, POST, HEAD, DELETE"'
+ curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/cors/headers -d '"accept, authorization, content-type, origin, referer, x-csrf-token"'
+}
+
+if [ "$(admin_user_exists)" == "FALSE" ]; then
+ echo "Admin user does not exist, creating..."
+ create_admin_user
+else
+ echo "Admin user exists"
+fi
+
+if [ "TRUE" == $(system_tables_exist) ]; then
+ echo System tables exist, skipping creation
+else
+ echo Is fresh install, creating system tables
+ setup_system_tables
+fi
+
+if [ "FALSE" == $(db_exists) ]; then
+ response=$(create_db)
+ if [ "{\"ok\":true}" == "${response}" ]; then
+ echo Database successfully created
+ else
+ echo Database creation failed
+ fi
+else
+ echo Database already exists, nothing to do
+fi
+
+echo "Updating _replicator database permissions"
+response=$(curl -su "${CURL_USERPASS_ARG}" --location --request PUT $COUCH_BASE_LOCAL/_replicator/_security --header 'Content-Type: application/json' --data-raw '{ "admins": {"roles": []},"members": {"roles": []}}');
+if [ "{\"ok\":true}" == "${response}" ]; then
+ echo "Database permissions successfully updated"
+else
+ echo "Database permissions not updated"
+fi
+
+echo "Updating ${OPENMCT_DATABASE_NAME} database permissions"
+response=$(curl -su "${CURL_USERPASS_ARG}" --location --request PUT $COUCH_BASE_LOCAL/$OPENMCT_DATABASE_NAME/_security --header 'Content-Type: application/json' --data-raw '{ "admins": {"roles": []},"members": {"roles": []}}');
+if [ "{\"ok\":true}" == "${response}" ]; then
+ echo "Database permissions successfully updated"
+else
+ echo "Database permissions not updated"
+fi
+
+if [ "FALSE" == $(is_cors_enabled) ]; then
+ echo "Enabling CORS"
+ enable_cors
+else
+ echo "CORS enabled, nothing to do"
+fi
diff --git a/src/plugins/plan/Plan.vue b/src/plugins/plan/Plan.vue
index 1f34e97a6..89ad45bdd 100644
--- a/src/plugins/plan/Plan.vue
+++ b/src/plugins/plan/Plan.vue
@@ -49,7 +49,7 @@
import * as d3Scale from 'd3-scale';
import TimelineAxis from "../../ui/components/TimeSystemAxis.vue";
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
-import { getValidatedPlan } from "./util";
+import { getValidatedData } from "./util";
import Vue from "vue";
const PADDING = 1;
@@ -161,7 +161,7 @@ export default {
return clientWidth - 200;
},
getPlanData(domainObject) {
- this.planData = getValidatedPlan(domainObject);
+ this.planData = getValidatedData(domainObject);
},
updateViewBounds(bounds) {
if (bounds) {
diff --git a/src/plugins/plan/PlanViewProvider.js b/src/plugins/plan/PlanViewProvider.js
index 95ed57a9c..fbfd6a538 100644
--- a/src/plugins/plan/PlanViewProvider.js
+++ b/src/plugins/plan/PlanViewProvider.js
@@ -39,7 +39,7 @@ export default function PlanViewProvider(openmct) {
},
canEdit(domainObject) {
- return domainObject.type === 'plan';
+ return false;
},
view: function (domainObject, objectPath) {
diff --git a/src/plugins/plan/inspector/PlanActivitiesView.vue b/src/plugins/plan/inspector/PlanActivitiesView.vue
index be737379a..394e29b94 100644
--- a/src/plugins/plan/inspector/PlanActivitiesView.vue
+++ b/src/plugins/plan/inspector/PlanActivitiesView.vue
@@ -33,7 +33,7 @@
<script>
import PlanActivityView from "./PlanActivityView.vue";
import { getPreciseDuration } from "utils/duration";
-import uuid from 'uuid';
+import { v4 as uuid } from 'uuid';
const propertyLabels = {
'start': 'Start DateTime',
diff --git a/src/plugins/plan/inspector/PlanActivityView.vue b/src/plugins/plan/inspector/PlanActivityView.vue
index ef8792d65..4ae4537ef 100644
--- a/src/plugins/plan/inspector/PlanActivityView.vue
+++ b/src/plugins/plan/inspector/PlanActivityView.vue
@@ -43,7 +43,7 @@
<script>
import ActivityProperty from './ActivityProperty.vue';
-import uuid from 'uuid';
+import { v4 as uuid } from 'uuid';
export default {
components: {
diff --git a/src/plugins/plan/pluginSpec.js b/src/plugins/plan/pluginSpec.js
index 1cad81f5c..d981ac265 100644
--- a/src/plugins/plan/pluginSpec.js
+++ b/src/plugins/plan/pluginSpec.js
@@ -23,6 +23,7 @@
import {createOpenMct, resetApplicationState} from "utils/testing";
import PlanPlugin from "../plan/plugin";
import Vue from 'vue';
+import Properties from "@/ui/inspector/details/Properties.vue";
describe('the plugin', function () {
let planDefinition;
@@ -96,6 +97,18 @@ describe('the plugin', function () {
let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');
expect(planView).toBeDefined();
});
+
+ it('is not an editable view', () => {
+ const testViewObject = {
+ id: "test-object",
+ type: "plan"
+ };
+ openmct.router.path = [testViewObject];
+
+ const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]);
+ let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');
+ expect(planView.canEdit()).toBeFalse();
+ });
});
describe('the plan view displays activities', () => {
@@ -200,4 +213,63 @@ describe('the plugin', function () {
});
});
});
+
+ describe('the plan version', () => {
+ let component;
+ let componentObject;
+ let testPlanObject = {
+ name: 'Plan',
+ type: 'plan',
+ identifier: {
+ key: 'test-plan',
+ namespace: ''
+ },
+ version: 'v1'
+ };
+
+ beforeEach(() => {
+ openmct.selection.select([{
+ element: element,
+ context: {
+ item: testPlanObject
+ }
+ }, {
+ element: openmct.layout.$refs.browseObject.$el,
+ context: {
+ item: testPlanObject,
+ supportsMultiSelect: false
+ }
+ }], false);
+
+ return Vue.nextTick().then(() => {
+ let viewContainer = document.createElement('div');
+ child.append(viewContainer);
+ component = new Vue({
+ el: viewContainer,
+ components: {
+ Properties
+ },
+ provide: {
+ openmct: openmct
+ },
+ template: '<properties/>'
+ });
+ });
+ });
+
+ afterEach(() => {
+ component.$destroy();
+ });
+
+ it('provides an inspector view with the version information if available', () => {
+ componentObject = component.$root.$children[0];
+ const propertiesEls = componentObject.$el.querySelectorAll('.c-inspect-properties__row');
+ expect(propertiesEls.length).toEqual(4);
+ const found = Array.from(propertiesEls).some((propertyEl) => {
+ return (propertyEl.children[0].innerHTML.trim() === 'Version'
+ && propertyEl.children[1].innerHTML.trim() === 'v1');
+ });
+ expect(found).toBeTrue();
+ });
+ });
});
diff --git a/src/plugins/plan/util.js b/src/plugins/plan/util.js
index cf119412f..4854f11b4 100644
--- a/src/plugins/plan/util.js
+++ b/src/plugins/plan/util.js
@@ -20,9 +20,9 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
-export function getValidatedPlan(domainObject) {
+export function getValidatedData(domainObject) {
let sourceMap = domainObject.sourceMap;
- let body = domainObject.selectFile.body;
+ let body = domainObject.selectFile?.body;
let json = {};
if (typeof body === 'string') {
try {
@@ -30,7 +30,7 @@ export function getValidatedPlan(domainObject) {
} catch (e) {
return json;
}
- } else {
+ } else if (body !== undefined) {
json = body;
}
diff --git a/src/plugins/plot/MctPlot.vue b/src/plugins/plot/MctPlot.vue
index ea5d8dd39..cf2e6dfb2 100644
--- a/src/plugins/plot/MctPlot.vue
+++ b/src/plugins/plot/MctPlot.vue
@@ -26,6 +26,7 @@
:class="[plotLegendExpandedStateClass, plotLegendPositionClass]"
>
<plot-legend
+ v-if="!isNestedWithinAStackedPlot"
:cursor-locked="!!lockHighlightPoint"
:series="seriesModels"
:highlights="highlights"
@@ -86,6 +87,7 @@
:highlights="highlights"
:show-limit-line-labels="showLimitLineLabels"
@plotReinitializeCanvas="initCanvas"
+ @chartLoaded="initialize"
/>
</div>
@@ -154,6 +156,22 @@
>
</button>
</div>
+ <div class="c-button-set c-button-set--strip-h">
+ <button
+ class="c-button icon-crosshair"
+ :class="{ 'is-active': cursorGuide }"
+ title="Toggle cursor guides"
+ @click="toggleCursorGuide"
+ >
+ </button>
+ <button
+ class="c-button"
+ :class="{ 'icon-grid-on': gridLines, 'icon-grid-off': !gridLines }"
+ title="Toggle grid lines"
+ @click="toggleGridLines"
+ >
+ </button>
+ </div>
</div>
<!--Cursor guides-->
@@ -213,16 +231,16 @@ export default {
};
}
},
- gridLines: {
+ initGridLines: {
type: Boolean,
default() {
return true;
}
},
- cursorGuide: {
+ initCursorGuide: {
type: Boolean,
default() {
- return true;
+ return false;
}
},
plotTickWidth: {
@@ -230,6 +248,18 @@ export default {
default() {
return 0;
}
+ },
+ limitLineLabels: {
+ type: Object,
+ default() {
+ return {};
+ }
+ },
+ colorPalette: {
+ type: Object,
+ default() {
+ return undefined;
+ }
}
},
data() {
@@ -250,19 +280,30 @@ export default {
isRealTime: this.openmct.time.clock() !== undefined,
loaded: false,
isTimeOutOfSync: false,
- showLimitLineLabels: undefined,
+ showLimitLineLabels: this.limitLineLabels,
isFrozenOnMouseDown: false,
- hasSameRangeValue: true
+ hasSameRangeValue: true,
+ cursorGuide: this.initCursorGuide,
+ gridLines: this.initGridLines
};
},
computed: {
+ isNestedWithinAStackedPlot() {
+ const isNavigatedObject = this.openmct.router.isNavigatedObject([this.domainObject].concat(this.path));
+
+ return !isNavigatedObject && this.path.find((pathObject, pathObjIndex) => pathObject.type === 'telemetry.plot.stacked');
+ },
isFrozen() {
return this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
},
plotLegendPositionClass() {
- return `plot-legend-${this.config.legend.get('position')}`;
+ return !this.isNestedWithinAStackedPlot ? `plot-legend-${this.config.legend.get('position')}` : '';
},
plotLegendExpandedStateClass() {
+ if (this.isNestedWithinAStackedPlot) {
+ return '';
+ }
+
if (this.config.legend.get('expanded')) {
return 'plot-legend-expanded';
} else {
@@ -273,6 +314,20 @@ export default {
return this.plotTickWidth || this.tickWidth;
}
},
+ watch: {
+ limitLineLabels: {
+ handler(limitLineLabels) {
+ this.legendHoverChanged(limitLineLabels);
+ },
+ deep: true
+ },
+ initGridLines(newGridLines) {
+ this.gridLines = newGridLines;
+ },
+ initCursorGuide(newCursorGuide) {
+ this.cursorGuide = newCursorGuide;
+ }
+ },
mounted() {
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp);
@@ -284,6 +339,11 @@ export default {
this.config = this.getConfig();
this.legend = this.config.legend;
+ if (this.isNestedWithinAStackedPlot) {
+ const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
+ this.$emit('configLoaded', configId);
+ }
+
this.listenTo(this.config.series, 'add', this.addSeries, this);
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
@@ -300,11 +360,6 @@ export default {
this.setTimeContext();
this.loaded = true;
-
- //We're referencing the canvas elements from the mct-chart in the initialize method.
- // So we need $nextTick to ensure the component is fully mounted before we can initialize stuff.
- this.$nextTick(this.initialize);
-
},
beforeDestroy() {
document.removeEventListener('keydown', this.handleKeyDown);
@@ -349,6 +404,7 @@ export default {
id: configId,
domainObject: this.domainObject,
openmct: this.openmct,
+ palette: this.colorPalette,
callback: (data) => {
this.data = data;
}
@@ -424,7 +480,7 @@ export default {
end: range.max,
domain: this.config.xAxis.get('key')
})
- .then(this.stopLoading());
+ .then(this.stopLoading.bind(this));
if (purge) {
plotSeries.purgeRecordsOutsideRange(range);
}
@@ -732,6 +788,8 @@ export default {
};
});
}
+
+ this.$emit('highlights', this.highlights);
},
untrackMousePosition() {
@@ -766,6 +824,7 @@ export default {
if (this.isMouseClick()) {
this.lockHighlightPoint = !this.lockHighlightPoint;
+ this.$emit('lockHighlightPoint', this.lockHighlightPoint);
}
if (this.pan) {
@@ -1130,6 +1189,14 @@ export default {
},
legendHoverChanged(data) {
this.showLimitLineLabels = data;
+ },
+ toggleCursorGuide() {
+ this.cursorGuide = !this.cursorGuide;
+ this.$emit('cursorGuide', this.cursorGuide);
+ },
+ toggleGridLines() {
+ this.gridLines = !this.gridLines;
+ this.$emit('gridLines', this.gridLines);
}
}
};
diff --git a/src/plugins/plot/Plot.vue b/src/plugins/plot/Plot.vue
index beb3b9245..06fb9da3d 100644
--- a/src/plugins/plot/Plot.vue
+++ b/src/plugins/plot/Plot.vue
@@ -25,53 +25,18 @@
class="c-plot holder holder-plot has-control-bar"
>
<div
- v-if="!options.compact"
- class="c-control-bar"
- >
- <span class="c-button-set c-button-set--strip-h">
- <button
- class="c-button icon-download"
- title="Export This View's Data as PNG"
- @click="exportPNG()"
- >
- <span class="c-button__label">PNG</span>
- </button>
- <button
- class="c-button"
- title="Export This View's Data as JPG"
- @click="exportJPG()"
- >
- <span class="c-button__label">JPG</span>
- </button>
- </span>
- <button
- class="c-button icon-crosshair"
- :class="{ 'is-active': cursorGuide }"
- title="Toggle cursor guides"
- @click="toggleCursorGuide"
- >
- </button>
- <button
- class="c-button"
- :class="{ 'icon-grid-on': gridLines, 'icon-grid-off': !gridLines }"
- title="Toggle grid lines"
- @click="toggleGridLines"
- >
- </button>
- </div>
-
- <div
ref="plotContainer"
class="l-view-section u-style-receiver js-style-receiver"
:class="{'s-status-timeconductor-unsynced': status && status === 'timeconductor-unsynced'}"
>
- <div
+ <progress-bar
v-show="!!loading"
- class="c-loading--overlay loading"
- ></div>
+ class="c-telemetry-table__progress-bar"
+ :model="{progressPerc: undefined}"
+ />
<mct-plot
- :grid-lines="gridLines"
- :cursor-guide="cursorGuide"
+ :init-grid-lines="gridLines"
+ :init-cursor-guide="cursorGuide"
:options="options"
@loadingUpdated="loadingUpdated"
@statusUpdated="setStatus"
@@ -84,10 +49,12 @@
import eventHelpers from './lib/eventHelpers';
import ImageExporter from '../../exporters/ImageExporter';
import MctPlot from './MctPlot.vue';
+import ProgressBar from "../../ui/components/ProgressBar.vue";
export default {
components: {
- MctPlot
+ MctPlot,
+ ProgressBar
},
inject: ['openmct', 'domainObject', 'path'],
props: {
@@ -124,26 +91,22 @@ export default {
destroy() {
this.stopListening();
},
-
exportJPG() {
const plotElement = this.$refs.plotContainer;
this.imageExporter.exportJPG(plotElement, 'plot.jpg', 'export-plot');
},
-
exportPNG() {
const plotElement = this.$refs.plotContainer;
this.imageExporter.exportPNG(plotElement, 'plot.png', 'export-plot');
},
-
- toggleCursorGuide() {
- this.cursorGuide = !this.cursorGuide;
- },
-
- toggleGridLines() {
- this.gridLines = !this.gridLines;
- },
setStatus(status) {
this.status = status;
+ },
+ getViewContext() {
+ return {
+ exportPNG: this.exportPNG,
+ exportJPG: this.exportJPG
+ };
}
}
};
diff --git a/src/plugins/plot/PlotViewProvider.js b/src/plugins/plot/PlotViewProvider.js
index c71c0a961..8d0d862a2 100644
--- a/src/plugins/plot/PlotViewProvider.js
+++ b/src/plugins/plot/PlotViewProvider.js
@@ -80,9 +80,16 @@ export default function PlotViewProvider(openmct) {
}
};
},
- template: '<plot :options="options"></plot>'
+ template: '<plot ref="plotComponent" :options="options"></plot>'
});
},
+ getViewContext() {
+ if (!component) {
+ return {};
+ }
+
+ return component.$refs.plotComponent.getViewContext();
+ },
destroy: function () {
component.$destroy();
component = undefined;
diff --git a/src/plugins/plot/actions/ViewActions.js b/src/plugins/plot/actions/ViewActions.js
new file mode 100644
index 000000000..d8dbd4b0a
--- /dev/null
+++ b/src/plugins/plot/actions/ViewActions.js
@@ -0,0 +1,57 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 {isPlotView} from "@/plugins/plot/actions/utils";
+
+const exportPNG = {
+ name: 'Export as PNG',
+ key: 'export-as-png',
+ description: 'Export This View\'s Data as PNG',
+ cssClass: 'icon-download',
+ group: 'view',
+ invoke(objectPath, view) {
+ view.getViewContext().exportPNG();
+ }
+};
+
+const exportJPG = {
+ name: 'Export as JPG',
+ key: 'export-as-jpg',
+ description: 'Export This View\'s Data as JPG',
+ cssClass: 'icon-download',
+ group: 'view',
+ invoke(objectPath, view) {
+ view.getViewContext().exportJPG();
+ }
+};
+
+const viewActions = [
+ exportPNG,
+ exportJPG
+];
+
+viewActions.forEach(action => {
+ action.appliesTo = (objectPath, view = {}) => {
+ return isPlotView(view);
+ };
+});
+
+export default viewActions;
diff --git a/src/plugins/plot/actions/utils.js b/src/plugins/plot/actions/utils.js
new file mode 100644
index 000000000..2bebbecf4
--- /dev/null
+++ b/src/plugins/plot/actions/utils.js
@@ -0,0 +1,3 @@
+export function isPlotView(view) {
+ return view.key === 'plot-single' || view.key === 'plot-overlay' || view.key === 'plot-stacked';
+}
diff --git a/src/plugins/plot/axis/XAxis.vue b/src/plugins/plot/axis/XAxis.vue
index 7d2bf2545..a6bc176aa 100644
--- a/src/plugins/plot/axis/XAxis.vue
+++ b/src/plugins/plot/axis/XAxis.vue
@@ -135,17 +135,21 @@ export default {
},
setUpXAxisOptions() {
const xAxisKey = this.xAxis.get('key');
+ this.xKeyOptions = [];
+
+ if (this.seriesModel.metadata) {
+ this.xKeyOptions = this.seriesModel.metadata
+ .valuesForHints(['domain'])
+ .map(function (o) {
+ return {
+ name: o.name,
+ key: o.key
+ };
+ });
+ }
- this.xKeyOptions = this.seriesModel.metadata
- .valuesForHints(['domain'])
- .map(function (o) {
- return {
- name: o.name,
- key: o.key
- };
- });
this.xAxisLabel = this.xAxis.get('label');
- this.selectedXKeyOptionKey = this.getXKeyOption(xAxisKey).key;
+ this.selectedXKeyOptionKey = this.xKeyOptions.length > 0 ? this.getXKeyOption(xAxisKey).key : xAxisKey;
},
onTickWidthChange(width) {
this.$emit('tickWidthChanged', width);
diff --git a/src/plugins/plot/axis/YAxis.vue b/src/plugins/plot/axis/YAxis.vue
index 9c18394a1..6e170fbd6 100644
--- a/src/plugins/plot/axis/YAxis.vue
+++ b/src/plugins/plot/axis/YAxis.vue
@@ -120,21 +120,25 @@ export default {
}
},
setUpYAxisOptions() {
- this.yKeyOptions = this.seriesModel.metadata
- .valuesForHints(['range'])
- .map(function (o) {
- return {
- name: o.name,
- key: o.key
- };
- });
+ this.yKeyOptions = [];
+
+ if (this.seriesModel.metadata) {
+ this.yKeyOptions = this.seriesModel.metadata
+ .valuesForHints(['range'])
+ .map(function (o) {
+ return {
+ name: o.name,
+ key: o.key
+ };
+ });
+ }
// set yAxisLabel if none is set yet
if (this.yAxisLabel === 'none') {
let yKey = this.seriesModel.model.yKey;
let yKeyModel = this.yKeyOptions.filter(o => o.key === yKey)[0];
- this.yAxisLabel = yKeyModel.name;
+ this.yAxisLabel = yKeyModel ? yKeyModel.name : '';
}
},
toggleYAxisLabel() {
diff --git a/src/plugins/plot/chart/MctChart.vue b/src/plugins/plot/chart/MctChart.vue
index 2ec6d74f5..2f53255a1 100644
--- a/src/plugins/plot/chart/MctChart.vue
+++ b/src/plugins/plot/chart/MctChart.vue
@@ -115,6 +115,7 @@ export default {
this.listenTo(this.config.yAxis, 'change', this.updateLimitsAndDraw);
this.listenTo(this.config.xAxis, 'change', this.updateLimitsAndDraw);
this.config.series.forEach(this.onSeriesAdd, this);
+ this.$emit('chartLoaded');
},
beforeDestroy() {
this.destroy();
diff --git a/src/plugins/plot/configuration/Model.js b/src/plugins/plot/configuration/Model.js
index b47a4872e..6465fa271 100644
--- a/src/plugins/plot/configuration/Model.js
+++ b/src/plugins/plot/configuration/Model.js
@@ -34,6 +34,12 @@ export default class Model extends EventEmitter {
*/
constructor(options) {
super();
+ Object.defineProperty(this, '_events', {
+ value: this._events,
+ enumerable: false,
+ configurable: false,
+ writable: true
+ });
//need to do this as we're already extending EventEmitter
eventHelpers.extend(this);
diff --git a/src/plugins/plot/configuration/PlotConfigurationModel.js b/src/plugins/plot/configuration/PlotConfigurationModel.js
index 0f6a56cd6..ce0c6e532 100644
--- a/src/plugins/plot/configuration/PlotConfigurationModel.js
+++ b/src/plugins/plot/configuration/PlotConfigurationModel.js
@@ -68,7 +68,8 @@ export default class PlotConfigurationModel extends Model {
this.series = new SeriesCollection({
models: options.model.series,
plot: this,
- openmct: options.openmct
+ openmct: options.openmct,
+ palette: options.palette
});
if (this.get('domainObject').type === 'telemetry.plot.overlay') {
diff --git a/src/plugins/plot/configuration/PlotSeries.js b/src/plugins/plot/configuration/PlotSeries.js
index 1c537186e..5e3c8cc43 100644
--- a/src/plugins/plot/configuration/PlotSeries.js
+++ b/src/plugins/plot/configuration/PlotSeries.js
@@ -126,10 +126,15 @@ export default class PlotSeries extends Model {
*/
destroy() {
super.destroy();
+ this.openmct.time.off('bounds', this.updateLimits);
if (this.unsubscribe) {
this.unsubscribe();
}
+
+ if (this.removeMutationListener) {
+ this.removeMutationListener();
+ }
}
/**
@@ -157,6 +162,11 @@ export default class PlotSeries extends Model {
});
this.openmct.time.on('bounds', this.updateLimits);
+ this.removeMutationListener = this.openmct.objects.observe(
+ this.domainObject,
+ 'name',
+ this.updateName.bind(this)
+ );
}
/**
@@ -225,6 +235,12 @@ export default class PlotSeries extends Model {
});
/* eslint-enable you-dont-need-lodash-underscore/concat */
}
+
+ updateName(name) {
+ if (name !== this.get('name')) {
+ this.set('name', name);
+ }
+ }
/**
* Update x formatter on x change.
*/
diff --git a/src/plugins/plot/configuration/SeriesCollection.js b/src/plugins/plot/configuration/SeriesCollection.js
index 33160a8fa..b5bb81dbb 100644
--- a/src/plugins/plot/configuration/SeriesCollection.js
+++ b/src/plugins/plot/configuration/SeriesCollection.js
@@ -39,7 +39,7 @@ export default class SeriesCollection extends Collection {
this.modelClass = PlotSeries;
this.plot = options.plot;
this.openmct = options.openmct;
- this.palette = new ColorPalette();
+ this.palette = options.palette || new ColorPalette();
this.listenTo(this, 'add', this.onSeriesAdd, this);
this.listenTo(this, 'remove', this.onSeriesRemove, this);
this.listenTo(this.plot, 'change:domainObject', this.trackPersistedConfig, this);
diff --git a/src/plugins/plot/configuration/YAxisModel.js b/src/plugins/plot/configuration/YAxisModel.js
index aaedcaae3..8c03631d6 100644
--- a/src/plugins/plot/configuration/YAxisModel.js
+++ b/src/plugins/plot/configuration/YAxisModel.js
@@ -260,7 +260,7 @@ export default class YAxisModel extends Model {
const plotModel = this.plot.get('domainObject');
const label = plotModel.configuration?.yAxis?.label;
const sampleSeries = seriesCollection.first();
- if (!sampleSeries) {
+ if (!sampleSeries || !sampleSeries.metadata) {
if (!label) {
this.unset('label');
}
diff --git a/src/plugins/plot/inspector/PlotOptionsBrowse.vue b/src/plugins/plot/inspector/PlotOptionsBrowse.vue
index b85a2683f..99c9260f3 100644
--- a/src/plugins/plot/inspector/PlotOptionsBrowse.vue
+++ b/src/plugins/plot/inspector/PlotOptionsBrowse.vue
@@ -24,7 +24,10 @@
v-if="loaded"
class="js-plot-options-browse"
>
- <ul class="c-tree">
+ <ul
+ v-if="!isStackedPlotObject"
+ class="c-tree"
+ >
<h2 title="Plot series display properties in this object">Plot Series</h2>
<plot-options-item
v-for="series in plotSeries"
@@ -36,7 +39,10 @@
v-if="plotSeries.length"
class="grid-properties"
>
- <ul class="l-inspector-part">
+ <ul
+ v-if="!isStackedPlotObject"
+ class="l-inspector-part js-yaxis-properties"
+ >
<h2 title="Y axis settings for this object">Y Axis</h2>
<li class="grid-row">
<div
@@ -84,7 +90,10 @@
<div class="grid-cell value">{{ rangeMax }}</div>
</li>
</ul>
- <ul class="l-inspector-part">
+ <ul
+ v-if="isStackedPlotObject || !isNestedWithinAStackedPlot"
+ class="l-inspector-part js-legend-properties"
+ >
<h2 title="Legend settings for this object">Legend</h2>
<li class="grid-row">
<div
@@ -127,7 +136,7 @@
<span v-if="showValueWhenExpanded">Value</span>
<span v-if="showMinimumWhenExpanded">Min</span>
<span v-if="showMaximumWhenExpanded">Max</span>
- <span v-if="showUnitsWhenExpanded">Units</span>
+ <span v-if="showUnitsWhenExpanded">Unit</span>
</div>
</li>
</ul>
@@ -144,7 +153,7 @@ export default {
components: {
PlotOptionsItem
},
- inject: ['openmct', 'domainObject'],
+ inject: ['openmct', 'domainObject', 'path'],
data() {
return {
config: {},
@@ -167,37 +176,48 @@ export default {
plotSeries: []
};
},
+ computed: {
+ isNestedWithinAStackedPlot() {
+ return this.path.find((pathObject, pathObjIndex) => pathObjIndex > 0 && pathObject.type === 'telemetry.plot.stacked');
+ },
+ isStackedPlotObject() {
+ return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject.type === 'telemetry.plot.stacked');
+ }
+ },
mounted() {
eventHelpers.extend(this);
this.config = this.getConfig();
this.registerListeners();
this.initConfiguration();
this.loaded = true;
+
},
beforeDestroy() {
this.stopListening();
},
methods: {
initConfiguration() {
- this.label = this.config.yAxis.get('label');
- this.autoscale = this.config.yAxis.get('autoscale');
- this.logMode = this.config.yAxis.get('logMode');
- this.autoscalePadding = this.config.yAxis.get('autoscalePadding');
- const range = this.config.yAxis.get('range');
- if (range) {
- this.rangeMin = range.min;
- this.rangeMax = range.max;
- }
+ if (this.config) {
+ this.label = this.config.yAxis.get('label');
+ this.autoscale = this.config.yAxis.get('autoscale');
+ this.logMode = this.config.yAxis.get('logMode');
+ this.autoscalePadding = this.config.yAxis.get('autoscalePadding');
+ const range = this.config.yAxis.get('range');
+ if (range) {
+ this.rangeMin = range.min;
+ this.rangeMax = range.max;
+ }
- this.position = this.config.legend.get('position');
- this.hideLegendWhenSmall = this.config.legend.get('hideLegendWhenSmall');
- this.expandByDefault = this.config.legend.get('expandByDefault');
- this.valueToShowWhenCollapsed = this.config.legend.get('valueToShowWhenCollapsed');
- this.showTimestampWhenExpanded = this.config.legend.get('showTimestampWhenExpanded');
- this.showValueWhenExpanded = this.config.legend.get('showValueWhenExpanded');
- this.showMinimumWhenExpanded = this.config.legend.get('showMinimumWhenExpanded');
- this.showMaximumWhenExpanded = this.config.legend.get('showMaximumWhenExpanded');
- this.showUnitsWhenExpanded = this.config.legend.get('showUnitsWhenExpanded');
+ this.position = this.config.legend.get('position');
+ this.hideLegendWhenSmall = this.config.legend.get('hideLegendWhenSmall');
+ this.expandByDefault = this.config.legend.get('expandByDefault');
+ this.valueToShowWhenCollapsed = this.config.legend.get('valueToShowWhenCollapsed');
+ this.showTimestampWhenExpanded = this.config.legend.get('showTimestampWhenExpanded');
+ this.showValueWhenExpanded = this.config.legend.get('showValueWhenExpanded');
+ this.showMinimumWhenExpanded = this.config.legend.get('showMinimumWhenExpanded');
+ this.showMaximumWhenExpanded = this.config.legend.get('showMaximumWhenExpanded');
+ this.showUnitsWhenExpanded = this.config.legend.get('showUnitsWhenExpanded');
+ }
},
getConfig() {
this.configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
@@ -205,10 +225,12 @@ export default {
return configStore.get(this.configId);
},
registerListeners() {
- this.config.series.forEach(this.addSeries, this);
+ if (this.config) {
+ this.config.series.forEach(this.addSeries, this);
- this.listenTo(this.config.series, 'add', this.addSeries, this);
- this.listenTo(this.config.series, 'remove', this.resetAllSeries, this);
+ this.listenTo(this.config.series, 'add', this.addSeries, this);
+ this.listenTo(this.config.series, 'remove', this.resetAllSeries, this);
+ }
},
addSeries(series, index) {
diff --git a/src/plugins/plot/inspector/PlotOptionsEdit.vue b/src/plugins/plot/inspector/PlotOptionsEdit.vue
index 77dc89198..9151784d5 100644
--- a/src/plugins/plot/inspector/PlotOptionsEdit.vue
+++ b/src/plugins/plot/inspector/PlotOptionsEdit.vue
@@ -24,21 +24,31 @@
v-if="loaded"
class="js-plot-options-edit"
>
- <ul class="c-tree">
+ <ul
+ v-if="!isStackedPlotObject"
+ class="c-tree"
+ >
<h2 title="Display properties for this object">Plot Series</h2>
<li
v-for="series in plotSeries"
:key="series.key"
>
- <series-form :series="series" />
+ <series-form
+ :series="series"
+ @seriesUpdated="updateSeriesConfigForObject"
+ />
</li>
</ul>
<y-axis-form
- v-if="plotSeries.length"
+ v-if="plotSeries.length && !isStackedPlotObject"
class="grid-properties"
:y-axis="config.yAxis"
+ @seriesUpdated="updateSeriesConfigForObject"
/>
- <ul class="l-inspector-part">
+ <ul
+ v-if="isStackedPlotObject || !isStackedPlotNestedObject"
+ class="l-inspector-part"
+ >
<h2 title="Legend options">Legend</h2>
<legend-form
v-if="plotSeries.length"
@@ -54,6 +64,7 @@ import YAxisForm from "./forms/YAxisForm.vue";
import LegendForm from "./forms/LegendForm.vue";
import eventHelpers from "../lib/eventHelpers";
import configStore from "../configuration/ConfigStore";
+import _ from "lodash";
export default {
components: {
@@ -61,7 +72,7 @@ export default {
SeriesForm,
YAxisForm
},
- inject: ['openmct', 'domainObject'],
+ inject: ['openmct', 'domainObject', 'path'],
data() {
return {
config: {},
@@ -69,6 +80,14 @@ export default {
loaded: false
};
},
+ computed: {
+ isStackedPlotNestedObject() {
+ return this.path.find((pathObject, pathObjIndex) => pathObjIndex > 0 && pathObject.type === 'telemetry.plot.stacked');
+ },
+ isStackedPlotObject() {
+ return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject.type === 'telemetry.plot.stacked');
+ }
+ },
mounted() {
eventHelpers.extend(this);
this.config = this.getConfig();
@@ -98,6 +117,34 @@ export default {
resetAllSeries() {
this.plotSeries = [];
this.config.series.forEach(this.addSeries, this);
+ },
+
+ updateSeriesConfigForObject(config) {
+ const stackedPlotObject = this.path.find((pathObject) => pathObject.type === 'telemetry.plot.stacked');
+ let index = stackedPlotObject.configuration.series.findIndex((seriesConfig) => {
+ return this.openmct.objects.areIdsEqual(seriesConfig.identifier, config.identifier);
+ });
+ if (index < 0) {
+ index = stackedPlotObject.configuration.series.length;
+ const configPath = `configuration.series[${index}]`;
+ let newConfig = {
+ identifier: config.identifier
+ };
+ _.set(newConfig, `${config.path}`, config.value);
+ this.openmct.objects.mutate(
+ stackedPlotObject,
+ configPath,
+ newConfig
+ );
+ } else {
+ const configPath = `configuration.series[${index}].${config.path}`;
+ this.openmct.objects.mutate(
+ stackedPlotObject,
+ configPath,
+ config.value
+ );
+ }
+
}
}
};
diff --git a/src/plugins/plot/inspector/PlotsInspectorViewProvider.js b/src/plugins/plot/inspector/PlotsInspectorViewProvider.js
index 16b88412c..aebacfa7e 100644
--- a/src/plugins/plot/inspector/PlotsInspectorViewProvider.js
+++ b/src/plugins/plot/inspector/PlotsInspectorViewProvider.js
@@ -13,8 +13,10 @@ export default function PlotsInspectorViewProvider(openmct) {
let object = selection[0][0].context.item;
- return object
- && object.type === 'telemetry.plot.overlay';
+ const isOverlayPlotObject = object && object.type === 'telemetry.plot.overlay';
+ const isStackedPlotObject = object && object.type === 'telemetry.plot.stacked';
+
+ return isStackedPlotObject || isOverlayPlotObject;
},
view: function (selection) {
let component;
diff --git a/src/plugins/plot/inspector/StackedPlotsInspectorViewProvider.js b/src/plugins/plot/inspector/StackedPlotsInspectorViewProvider.js
new file mode 100644
index 000000000..8cd6bc78d
--- /dev/null
+++ b/src/plugins/plot/inspector/StackedPlotsInspectorViewProvider.js
@@ -0,0 +1,59 @@
+
+import PlotOptions from "./PlotOptions.vue";
+import Vue from 'vue';
+
+export default function StackedPlotsInspectorViewProvider(openmct) {
+ return {
+ key: 'stacked-plots-inspector',
+ name: 'Stacked Plots Inspector View',
+ canView: function (selection) {
+ if (selection.length === 0 || selection[0].length === 0) {
+ return false;
+ }
+
+ const object = selection[0][0].context.item;
+ const parent = selection[0].length > 1 && selection[0][1].context.item;
+
+ const isOverlayPlotObject = object && object.type === 'telemetry.plot.overlay';
+ const isParentStackedPlotObject = parent && parent.type === 'telemetry.plot.stacked';
+
+ return !isOverlayPlotObject && isParentStackedPlotObject;
+ },
+ view: function (selection) {
+ let component;
+ let objectPath;
+
+ if (selection.length) {
+ objectPath = selection[0].map((selectionItem) => {
+ return selectionItem.context.item;
+ });
+ }
+
+ return {
+ show: function (element) {
+ component = new Vue({
+ el: element,
+ components: {
+ PlotOptions: PlotOptions
+ },
+ provide: {
+ openmct,
+ domainObject: selection[0][0].context.item,
+ path: objectPath
+ },
+ template: '<plot-options></plot-options>'
+ });
+ },
+ destroy: function () {
+ if (component) {
+ component.$destroy();
+ component = undefined;
+ }
+ }
+ };
+ },
+ priority: function () {
+ return 1;
+ }
+ };
+}
diff --git a/src/plugins/plot/inspector/forms/LegendForm.vue b/src/plugins/plot/inspector/forms/LegendForm.vue
index 370c1d52e..6061996b3 100644
--- a/src/plugins/plot/inspector/forms/LegendForm.vue
+++ b/src/plugins/plot/inspector/forms/LegendForm.vue
@@ -54,7 +54,7 @@
<option value="nearestValue">Nearest value</option>
<option value="min">Minimum value</option>
<option value="max">Maximum value</option>
- <option value="units">Units</option>
+ <option value="unit">Unit</option>
</select>
</div>
</li>
@@ -89,7 +89,7 @@
v-model="showUnitsWhenExpanded"
type="checkbox"
@change="updateForm('showUnitsWhenExpanded')"
- > Units</li>
+ > Unit</li>
</ul>
</div>
diff --git a/src/plugins/plot/inspector/forms/SeriesForm.vue b/src/plugins/plot/inspector/forms/SeriesForm.vue
index 8434bd349..2a7ff4d2b 100644
--- a/src/plugins/plot/inspector/forms/SeriesForm.vue
+++ b/src/plugins/plot/inspector/forms/SeriesForm.vue
@@ -298,28 +298,45 @@ export default {
this.series.set('color', color);
- const getPath = this.dynamicPathForKey('color');
- const seriesColorPath = getPath(this.domainObject, this.series);
+ if (!this.domainObject.configuration || !this.domainObject.configuration.series) {
+ this.$emit('seriesUpdated', {
+ identifier: this.domainObject.identifier,
+ path: `series.color`,
+ value: color.asHexString()
+ });
+ } else {
+ const getPath = this.dynamicPathForKey('color');
+ const seriesColorPath = getPath(this.domainObject, this.series);
- this.openmct.objects.mutate(
- this.domainObject,
- seriesColorPath,
- color.asHexString()
- );
+ this.openmct.objects.mutate(
+ this.domainObject,
+ seriesColorPath,
+ color.asHexString()
+ );
+ }
if (otherSeriesWithColor) {
otherSeriesWithColor.set('color', oldColor);
- const otherSeriesColorPath = getPath(
- this.domainObject,
- otherSeriesWithColor
- );
+ if (!this.domainObject.configuration || !this.domainObject.configuration.series) {
+ this.$emit('seriesUpdated', {
+ identifier: this.domainObject.identifier,
+ path: `series.color`,
+ value: oldColor.asHexString()
+ });
+ } else {
+ const getPath = this.dynamicPathForKey('color');
+ const otherSeriesColorPath = getPath(
+ this.domainObject,
+ otherSeriesWithColor
+ );
- this.openmct.objects.mutate(
- this.domainObject,
- otherSeriesColorPath,
- oldColor.asHexString()
- );
+ this.openmct.objects.mutate(
+ this.domainObject,
+ otherSeriesColorPath,
+ oldColor.asHexString()
+ );
+ }
}
},
toggleExpanded() {
@@ -343,11 +360,19 @@ export default {
if (!_.isEqual(coerce(newVal, formField.coerce), coerce(oldVal, formField.coerce))) {
this.series.set(formKey, coerce(newVal, formField.coerce));
if (path) {
- this.openmct.objects.mutate(
- this.domainObject,
- path(this.domainObject, this.series),
- coerce(newVal, formField.coerce)
- );
+ if (!this.domainObject.configuration || !this.domainObject.configuration.series) {
+ this.$emit('seriesUpdated', {
+ identifier: this.domainObject.identifier,
+ path: `series.${formKey}`,
+ value: coerce(newVal, formField.coerce)
+ });
+ } else {
+ this.openmct.objects.mutate(
+ this.domainObject,
+ path(this.domainObject, this.series),
+ coerce(newVal, formField.coerce)
+ );
+ }
}
}
},
diff --git a/src/plugins/plot/inspector/forms/YAxisForm.vue b/src/plugins/plot/inspector/forms/YAxisForm.vue
index bf8531230..d852e5b8b 100644
--- a/src/plugins/plot/inspector/forms/YAxisForm.vue
+++ b/src/plugins/plot/inspector/forms/YAxisForm.vue
@@ -227,14 +227,23 @@ export default {
const path = objectPath(formField.objectPath);
if (!_.isEqual(newVal, oldVal)) {
- // TODO: Why do we mutate yAxis twice, once directly, once via objects.mutate? Or are they different objects?
+ // We mutate the model for the plots first PlotConfigurationModel - this triggers changes that affects the plot behavior
this.yAxis.set(formKey, newVal);
+ // Then we mutate the domain object configuration to persist the settings
if (path) {
- this.openmct.objects.mutate(
- this.domainObject,
- path(this.domainObject, this.yAxis),
- newVal
- );
+ if (!this.domainObject.configuration || !this.domainObject.configuration.series) {
+ this.$emit('seriesUpdated', {
+ identifier: this.domainObject.identifier,
+ path: `yAxis.${formKey}`,
+ value: newVal
+ });
+ } else {
+ this.openmct.objects.mutate(
+ this.domainObject,
+ path(this.domainObject, this.yAxis),
+ newVal
+ );
+ }
}
}
}
diff --git a/src/plugins/plot/legend/PlotLegend.vue b/src/plugins/plot/legend/PlotLegend.vue
index e62fc5d99..1bdf4b3bb 100644
--- a/src/plugins/plot/legend/PlotLegend.vue
+++ b/src/plugins/plot/legend/PlotLegend.vue
@@ -49,8 +49,8 @@
title="Cursor is point locked. Click anywhere in the plot to unlock."
></div>
<plot-legend-item-collapsed
- v-for="seriesObject in series"
- :key="seriesObject.keyString"
+ v-for="(seriesObject, seriesIndex) in series"
+ :key="`seriesObject.keyString-${seriesIndex}`"
:highlights="highlights"
:value-to-show-when-collapsed="legend.get('valueToShowWhenCollapsed')"
:series-object="seriesObject"
@@ -95,8 +95,8 @@
</thead>
<tbody>
<plot-legend-item-expanded
- v-for="seriesObject in series"
- :key="seriesObject.keyString"
+ v-for="(seriesObject, seriesIndex) in series"
+ :key="`seriesObject.keyString-${seriesIndex}`"
:series-object="seriesObject"
:highlights="highlights"
:legend="legend"
diff --git a/src/plugins/plot/legend/PlotLegendItemCollapsed.vue b/src/plugins/plot/legend/PlotLegendItemCollapsed.vue
index 690afd926..20cd87e0a 100644
--- a/src/plugins/plot/legend/PlotLegendItemCollapsed.vue
+++ b/src/plugins/plot/legend/PlotLegendItemCollapsed.vue
@@ -41,7 +41,7 @@
<span class="plot-series-name">{{ nameWithUnit }}</span>
</div>
<div
- v-show="!!highlights.length && (valueToShowWhenCollapsed !== 'none')"
+ v-show="!!highlights.length && (valueToShowWhenCollapsed !== 'none' && valueToShowWhenCollapsed !== 'unit')"
class="plot-series-value hover-value-enabled"
:class="[{ 'cursor-hover': notNearest }, valueToDisplayWhenCollapsedClass, mctLimitStateClass]"
>
@@ -54,8 +54,10 @@
<script>
import {getLimitClass} from "@/plugins/plot/chart/limitUtil";
+import eventHelpers from "../lib/eventHelpers";
export default {
+ inject: ['openmct', 'domainObject'],
props: {
valueToShowWhenCollapsed: {
type: String,
@@ -103,8 +105,18 @@ export default {
}
},
mounted() {
+ eventHelpers.extend(this);
+ this.listenTo(this.seriesObject, 'change:color', (newColor) => {
+ this.updateColor(newColor);
+ }, this);
+ this.listenTo(this.seriesObject, 'change:name', () => {
+ this.updateName();
+ }, this);
this.initialize();
},
+ beforeDestroy() {
+ this.stopListening();
+ },
methods: {
initialize(highlightedObject) {
const seriesObject = highlightedObject ? highlightedObject.series : this.seriesObject;
@@ -130,6 +142,12 @@ export default {
this.formattedYValueFromStats = '';
}
},
+ updateColor(newColor) {
+ this.colorAsHexString = newColor.asHexString();
+ },
+ updateName() {
+ this.nameWithUnit = this.seriesObject.nameWithUnit();
+ },
toggleHover(hover) {
this.hover = hover;
this.$emit('legendHoverChanged', {
diff --git a/src/plugins/plot/legend/PlotLegendItemExpanded.vue b/src/plugins/plot/legend/PlotLegendItemExpanded.vue
index 1b3157172..35d1fc893 100644
--- a/src/plugins/plot/legend/PlotLegendItemExpanded.vue
+++ b/src/plugins/plot/legend/PlotLegendItemExpanded.vue
@@ -80,8 +80,10 @@
<script>
import {getLimitClass} from "@/plugins/plot/chart/limitUtil";
+import eventHelpers from "@/plugins/plot/lib/eventHelpers";
export default {
+ inject: ['openmct', 'domainObject'],
props: {
seriesObject: {
type: Object,
@@ -140,8 +142,18 @@ export default {
}
},
mounted() {
+ eventHelpers.extend(this);
+ this.listenTo(this.seriesObject, 'change:color', (newColor) => {
+ this.updateColor(newColor);
+ }, this);
+ this.listenTo(this.seriesObject, 'change:name', () => {
+ this.updateName();
+ }, this);
this.initialize();
},
+ beforeDestroy() {
+ this.stopListening();
+ },
methods: {
initialize(highlightedObject) {
const seriesObject = highlightedObject ? highlightedObject.series : this.seriesObject;
@@ -170,6 +182,12 @@ export default {
this.formattedMaxY = '';
}
},
+ updateColor(newColor) {
+ this.colorAsHexString = newColor.asHexString();
+ },
+ updateName(newName) {
+ this.nameWithUnit = this.seriesObject.nameWithUnit();
+ },
toggleHover(hover) {
this.hover = hover;
this.$emit('legendHoverChanged', {
diff --git a/src/plugins/plot/overlayPlot/OverlayPlotViewProvider.js b/src/plugins/plot/overlayPlot/OverlayPlotViewProvider.js
index 3a70da284..e34bb59d2 100644
--- a/src/plugins/plot/overlayPlot/OverlayPlotViewProvider.js
+++ b/src/plugins/plot/overlayPlot/OverlayPlotViewProvider.js
@@ -65,9 +65,16 @@ export default function OverlayPlotViewProvider(openmct) {
}
};
},
- template: '<plot :options="options"></plot>'
+ template: '<plot ref="plotComponent" :options="options"></plot>'
});
},
+ getViewContext() {
+ if (!component) {
+ return {};
+ }
+
+ return component.$refs.plotComponent.getViewContext();
+ },
destroy: function () {
component.$destroy();
component = undefined;
diff --git a/src/plugins/plot/plugin.js b/src/plugins/plot/plugin.js
index 93c3122ca..5eacd34fd 100644
--- a/src/plugins/plot/plugin.js
+++ b/src/plugins/plot/plugin.js
@@ -25,6 +25,9 @@ import StackedPlotViewProvider from './stackedPlot/StackedPlotViewProvider';
import PlotsInspectorViewProvider from './inspector/PlotsInspectorViewProvider';
import OverlayPlotCompositionPolicy from './overlayPlot/OverlayPlotCompositionPolicy';
import StackedPlotCompositionPolicy from './stackedPlot/StackedPlotCompositionPolicy';
+import PlotViewActions from "./actions/ViewActions";
+import StackedPlotsInspectorViewProvider from "./inspector/StackedPlotsInspectorViewProvider";
+import stackedPlotConfigurationInterceptor from "./stackedPlot/stackedPlotConfigurationInterceptor";
export default function () {
return function install(openmct) {
@@ -38,9 +41,8 @@ export default function () {
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {
- series: [],
- yAxis: {},
- xAxis: {}
+ //series is an array of objects of type: {identifier, series: {color...}, yAxis:{}}
+ series: []
};
},
priority: 891
@@ -54,19 +56,29 @@ export default function () {
creatable: true,
initialize: function (domainObject) {
domainObject.composition = [];
- domainObject.configuration = {};
+ domainObject.configuration = {
+ series: [],
+ yAxis: {},
+ xAxis: {}
+ };
},
priority: 890
});
+ stackedPlotConfigurationInterceptor(openmct);
+
openmct.objectViews.addProvider(new StackedPlotViewProvider(openmct));
openmct.objectViews.addProvider(new OverlayPlotViewProvider(openmct));
openmct.objectViews.addProvider(new PlotViewProvider(openmct));
openmct.inspectorViews.addProvider(new PlotsInspectorViewProvider(openmct));
+ openmct.inspectorViews.addProvider(new StackedPlotsInspectorViewProvider(openmct));
openmct.composition.addPolicy(new OverlayPlotCompositionPolicy(openmct).allow);
openmct.composition.addPolicy(new StackedPlotCompositionPolicy(openmct).allow);
+
+ PlotViewActions.forEach(action => {
+ openmct.actions.register(action);
+ });
};
}
-
diff --git a/src/plugins/plot/pluginSpec.js b/src/plugins/plot/pluginSpec.js
index f1a0e475b..36859a50d 100644
--- a/src/plugins/plot/pluginSpec.js
+++ b/src/plugins/plot/pluginSpec.js
@@ -23,7 +23,6 @@
import {createMouseEvent, createOpenMct, resetApplicationState, spyOnBuiltins} from "utils/testing";
import PlotVuePlugin from "./plugin";
import Vue from "vue";
-import StackedPlot from "./stackedPlot/StackedPlot.vue";
import configStore from "./configuration/ConfigStore";
import EventEmitter from "EventEmitter";
import PlotOptions from "./inspector/PlotOptions.vue";
@@ -348,14 +347,20 @@ describe("the plugin", function () {
}
};
+ openmct.router.path = [testTelemetryObject];
+
applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-single");
- plotView = plotViewProvider.view(testTelemetryObject, [testTelemetryObject]);
+ plotView = plotViewProvider.view(testTelemetryObject, []);
plotView.show(child, true);
return Vue.nextTick();
});
+ afterEach(() => {
+ openmct.router.path = null;
+ });
+
it("Makes only one request for telemetry on load", () => {
expect(openmct.telemetry.request).toHaveBeenCalledTimes(1);
});
@@ -523,360 +528,6 @@ describe("the plugin", function () {
});
});
- describe("The stacked plot view", () => {
- let testTelemetryObject;
- let testTelemetryObject2;
- let config;
- let stackedPlotObject;
- let component;
- let mockComposition;
- let plotViewComponentObject;
-
- beforeEach(() => {
-
- stackedPlotObject = {
- identifier: {
- namespace: "",
- key: "test-plot"
- },
- type: "telemetry.plot.stacked",
- name: "Test Stacked Plot"
- };
-
- testTelemetryObject = {
- identifier: {
- namespace: "",
- key: "test-object"
- },
- type: "test-object",
- name: "Test Object",
- telemetry: {
- values: [{
- key: "utc",
- format: "utc",
- name: "Time",
- hints: {
- domain: 1
- }
- }, {
- key: "some-key",
- name: "Some attribute",
- hints: {
- range: 1
- }
- }, {
- key: "some-other-key",
- name: "Another attribute",
- hints: {
- range: 2
- }
- }]
- },
- configuration: {
- objectStyles: {
- staticStyle: {
- style: {
- backgroundColor: 'rgb(0, 200, 0)',
- color: '',
- border: ''
- }
- },
- conditionSetIdentifier: {
- namespace: '',
- key: 'testConditionSetId'
- },
- selectedConditionId: 'conditionId1',
- defaultConditionId: 'conditionId1',
- styles: [
- {
- conditionId: 'conditionId1',
- style: {
- backgroundColor: 'rgb(0, 155, 0)',
- color: '',
- output: '',
- border: ''
- }
- }
- ]
- }
- }
- };
-
- testTelemetryObject2 = {
- identifier: {
- namespace: "",
- key: "test-object2"
- },
- type: "test-object",
- name: "Test Object2",
- telemetry: {
- values: [{
- key: "utc",
- format: "utc",
- name: "Time",
- hints: {
- domain: 1
- }
- }, {
- key: "some-key2",
- name: "Some attribute2",
- hints: {
- range: 1
- }
- }, {
- key: "some-other-key2",
- name: "Another attribute2",
- hints: {
- range: 2
- }
- }]
- }
- };
-
- mockComposition = new EventEmitter();
- mockComposition.load = () => {
- mockComposition.emit('add', testTelemetryObject);
-
- return [testTelemetryObject];
- };
-
- spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
-
- let viewContainer = document.createElement("div");
- child.append(viewContainer);
- component = new Vue({
- el: viewContainer,
- components: {
- StackedPlot
- },
- provide: {
- openmct: openmct,
- domainObject: stackedPlotObject,
- composition: openmct.composition.get(stackedPlotObject),
- path: [stackedPlotObject]
- },
- template: "<stacked-plot></stacked-plot>"
- });
-
- return telemetryPromise
- .then(Vue.nextTick())
- .then(() => {
- plotViewComponentObject = component.$root.$children[0];
- const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier);
- config = configStore.get(configId);
- });
- });
-
- it("Renders a collapsed legend for every telemetry", () => {
- let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
- expect(legend.length).toBe(1);
- expect(legend[0].innerHTML).toEqual("Test Object");
- });
-
- it("Renders an expanded legend for every telemetry", () => {
- let legendControl = element.querySelector(".c-plot-legend__view-control.gl-plot-legend__view-control.c-disclosure-triangle");
- const clickEvent = createMouseEvent("click");
-
- legendControl.dispatchEvent(clickEvent);
-
- let legend = element.querySelectorAll(".plot-wrapper-expanded-legend .plot-legend-item td");
- expect(legend.length).toBe(6);
- });
-
- it("Renders X-axis ticks for the telemetry object", (done) => {
- let xAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-x .gl-plot-tick-wrapper");
- expect(xAxisElement.length).toBe(1);
-
- config.xAxis.set('displayRange', {
- min: 0,
- max: 4
- });
-
- Vue.nextTick(() => {
- let ticks = xAxisElement[0].querySelectorAll(".gl-plot-tick");
- expect(ticks.length).toBe(9);
-
- done();
- });
- });
-
- it("Renders Y-axis ticks for the telemetry object", (done) => {
- config.yAxis.set('displayRange', {
- min: 10,
- max: 20
- });
- Vue.nextTick(() => {
- let yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper");
- expect(yAxisElement.length).toBe(1);
- let ticks = yAxisElement[0].querySelectorAll(".gl-plot-tick");
- expect(ticks.length).toBe(6);
- done();
- });
- });
-
- it("Renders Y-axis options for the telemetry object", () => {
- let yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y .gl-plot-y-label__select");
- expect(yAxisElement.length).toBe(1);
- let options = yAxisElement[0].querySelectorAll("option");
- expect(options.length).toBe(2);
- expect(options[0].value).toBe("Some attribute");
- expect(options[1].value).toBe("Another attribute");
- });
-
- it("turns on cursor Guides all telemetry objects", (done) => {
- expect(plotViewComponentObject.cursorGuide).toBeFalse();
- plotViewComponentObject.toggleCursorGuide();
- Vue.nextTick(() => {
- expect(plotViewComponentObject.$children[0].component.$children[0].cursorGuide).toBeTrue();
- done();
- });
- });
-
- it("shows grid lines for all telemetry objects", () => {
- expect(plotViewComponentObject.gridLines).toBeTrue();
- let gridLinesContainer = element.querySelectorAll(".gl-plot-display-area .js-ticks");
- let visible = 0;
- gridLinesContainer.forEach(el => {
- if (el.style.display !== "none") {
- visible++;
- }
- });
- expect(visible).toBe(2);
- });
-
- it("hides grid lines for all telemetry objects", (done) => {
- expect(plotViewComponentObject.gridLines).toBeTrue();
- plotViewComponentObject.toggleGridLines();
- Vue.nextTick(() => {
- expect(plotViewComponentObject.gridLines).toBeFalse();
- let gridLinesContainer = element.querySelectorAll(".gl-plot-display-area .js-ticks");
- let visible = 0;
- gridLinesContainer.forEach(el => {
- if (el.style.display !== "none") {
- visible++;
- }
- });
- expect(visible).toBe(0);
- done();
- });
- });
-
- it('plots a new series when a new telemetry object is added', (done) => {
- mockComposition.emit('add', testTelemetryObject2);
- Vue.nextTick(() => {
- let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
- expect(legend.length).toBe(2);
- expect(legend[1].innerHTML).toEqual("Test Object2");
- done();
- });
- });
-
- it('removes plots from series when a telemetry object is removed', (done) => {
- mockComposition.emit('remove', testTelemetryObject.identifier);
- Vue.nextTick(() => {
- let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
- expect(legend.length).toBe(0);
- done();
- });
- });
-
- it("Changes the label of the y axis when the option changes", (done) => {
- let selectEl = element.querySelector('.gl-plot-y-label__select');
- selectEl.value = 'Another attribute';
- selectEl.dispatchEvent(new Event("change"));
-
- Vue.nextTick(() => {
- expect(config.yAxis.get('label')).toEqual('Another attribute');
- done();
- });
- });
-
- it("Renders a new series when added to one of the plots", (done) => {
- mockComposition.emit('add', testTelemetryObject2);
- Vue.nextTick(() => {
- let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
- expect(legend.length).toBe(2);
- expect(legend[1].innerHTML).toEqual("Test Object2");
- done();
- });
- });
-
- it("Adds a new point to the plot", (done) => {
- let originalLength = config.series.models[0].getSeriesData().length;
- config.series.models[0].add({
- utc: 2,
- 'some-key': 1,
- 'some-other-key': 2
- });
- Vue.nextTick(() => {
- const seriesData = config.series.models[0].getSeriesData();
- expect(seriesData.length).toEqual(originalLength + 1);
- done();
- });
- });
-
- it("updates the xscale", (done) => {
- config.xAxis.set('displayRange', {
- min: 0,
- max: 10
- });
- Vue.nextTick(() => {
- expect(plotViewComponentObject.$children[0].component.$children[0].xScale.domain()).toEqual({
- min: 0,
- max: 10
- });
- done();
- });
- });
-
- it("updates the yscale", (done) => {
- config.yAxis.set('displayRange', {
- min: 10,
- max: 20
- });
- Vue.nextTick(() => {
- expect(plotViewComponentObject.$children[0].component.$children[0].yScale.domain()).toEqual({
- min: 10,
- max: 20
- });
- done();
- });
- });
-
- it("shows styles for telemetry objects if available", (done) => {
- Vue.nextTick(() => {
- let conditionalStylesContainer = element.querySelectorAll(".c-plot--stacked-container .js-style-receiver");
- let hasStyles = 0;
- conditionalStylesContainer.forEach(el => {
- if (el.style.backgroundColor !== '') {
- hasStyles++;
- }
- });
- expect(hasStyles).toBe(1);
- done();
- });
- });
-
- describe('limits', () => {
-
- it('lines are not displayed by default', () => {
- let limitEl = element.querySelectorAll(".js-limit-area hr");
- expect(limitEl.length).toBe(0);
- });
-
- it('lines are displayed when configuration is set to true', (done) => {
- config.series.models[0].set('limitLines', true);
-
- Vue.nextTick(() => {
- let limitEl = element.querySelectorAll(".js-limit-area .js-limit-line");
- expect(limitEl.length).toBe(4);
- done();
- });
-
- });
- });
- });
-
describe('the inspector view', () => {
let component;
let viewComponentObject;
@@ -955,6 +606,7 @@ describe("the plugin", function () {
]
];
+ openmct.router.path = [testTelemetryObject];
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testTelemetryObject);
@@ -993,6 +645,10 @@ describe("the plugin", function () {
});
});
+ afterEach(() => {
+ openmct.router.path = null;
+ });
+
describe('in view only mode', () => {
let browseOptionsEl;
let editOptionsEl;
@@ -1096,5 +752,24 @@ describe("the plugin", function () {
expect(colorSwatch).toBeDefined();
});
});
+
+ describe('limits', () => {
+
+ it('lines are not displayed by default', () => {
+ let limitEl = element.querySelectorAll(".js-limit-area .js-limit-line");
+ expect(limitEl.length).toBe(0);
+ });
+
+ xit('lines are displayed when configuration is set to true', (done) => {
+ config.series.models[0].set('limitLines', true);
+
+ Vue.nextTick(() => {
+ let limitEl = element.querySelectorAll(".js-limit-area .js-limit-line");
+ expect(limitEl.length).toBe(4);
+ done();
+ });
+
+ });
+ });
});
});
diff --git a/src/plugins/plot/stackedPlot/StackedPlot.vue b/src/plugins/plot/stackedPlot/StackedPlot.vue
index 1c63a0413..482744920 100644
--- a/src/plugins/plot/stackedPlot/StackedPlot.vue
+++ b/src/plugins/plot/stackedPlot/StackedPlot.vue
@@ -21,54 +21,37 @@
-->
<template>
-<div class="c-plot c-plot--stacked holder holder-plot has-control-bar">
- <div
- v-show="!hideExportButtons && !options.compact"
- class="c-control-bar"
- >
- <span class="c-button-set c-button-set--strip-h">
- <button
- class="c-button icon-download"
- title="Export This View's Data as PNG"
- @click="exportPNG()"
- >
- <span class="c-button__label">PNG</span>
- </button>
- <button
- class="c-button"
- title="Export This View's Data as JPG"
- @click="exportJPG()"
- >
- <span class="c-button__label">JPG</span>
- </button>
- </span>
- <button
- class="c-button icon-crosshair"
- :class="{ 'is-active': cursorGuide }"
- title="Toggle cursor guides"
- @click="toggleCursorGuide"
- >
- </button>
- <button
- class="c-button"
- :class="{ 'icon-grid-on': gridLines, 'icon-grid-off': !gridLines }"
- title="Toggle grid lines"
- @click="toggleGridLines"
- >
- </button>
- </div>
+<div
+ v-if="loaded"
+ class="c-plot c-plot--stacked holder holder-plot has-control-bar"
+ :class="[plotLegendExpandedStateClass, plotLegendPositionClass]"
+>
+ <plot-legend
+ :cursor-locked="!!lockHighlightPoint"
+ :series="seriesModels"
+ :highlights="highlights"
+ :legend="legend"
+ @legendHoverChanged="legendHoverChanged"
+ />
<div class="l-view-section">
<stacked-plot-item
v-for="object in compositionObjects"
:key="object.id"
class="c-plot--stacked-container"
- :object="object"
+ :child-object="object"
:options="options"
:grid-lines="gridLines"
+ :color-palette="colorPalette"
:cursor-guide="cursorGuide"
+ :show-limit-line-labels="showLimitLineLabels"
:plot-tick-width="maxTickWidth"
@plotTickWidth="onTickWidthChange"
@loadingUpdated="loadingUpdated"
+ @cursorGuide="onCursorGuideChange"
+ @gridLines="onGridLinesChange"
+ @lockHighlightPoint="lockHighlightPointUpdated"
+ @highlights="highlightsUpdated"
+ @configLoaded="registerSeriesListeners"
/>
</div>
</div>
@@ -76,12 +59,19 @@
<script>
+import PlotConfigurationModel from '../configuration/PlotConfigurationModel';
+import configStore from '../configuration/ConfigStore';
+import ColorPalette from "@/ui/color/ColorPalette";
+
+import PlotLegend from "../legend/PlotLegend.vue";
import StackedPlotItem from './StackedPlotItem.vue';
import ImageExporter from '../../../exporters/ImageExporter';
+import eventHelpers from "@/plugins/plot/lib/eventHelpers";
export default {
components: {
- StackedPlotItem
+ StackedPlotItem,
+ PlotLegend
},
inject: ['openmct', 'domainObject', 'composition', 'path'],
props: {
@@ -93,16 +83,35 @@ export default {
}
},
data() {
+ this.seriesConfig = {};
+
return {
hideExportButtons: false,
cursorGuide: false,
gridLines: true,
loading: false,
compositionObjects: [],
- tickWidthMap: {}
+ tickWidthMap: {},
+ legend: {},
+ loaded: false,
+ lockHighlightPoint: false,
+ highlights: [],
+ seriesModels: [],
+ showLimitLineLabels: undefined,
+ colorPalette: new ColorPalette()
};
},
computed: {
+ plotLegendPositionClass() {
+ return `plot-legend-${this.config.legend.get('position')}`;
+ },
+ plotLegendExpandedStateClass() {
+ if (this.config.legend.get('expanded')) {
+ return 'plot-legend-expanded';
+ } else {
+ return 'plot-legend-collapsed';
+ }
+ },
maxTickWidth() {
return Math.max(...Object.values(this.tickWidthMap));
}
@@ -111,6 +120,13 @@ export default {
this.destroy();
},
mounted() {
+ eventHelpers.extend(this);
+
+ const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
+ this.config = this.getConfig(configId);
+ this.legend = this.config.legend;
+
+ this.loaded = true;
this.imageExporter = new ImageExporter(this.openmct);
this.composition.on('add', this.addChild);
@@ -119,10 +135,29 @@ export default {
this.composition.load();
},
methods: {
+ getConfig(configId) {
+ let config = configStore.get(configId);
+ if (!config) {
+ config = new PlotConfigurationModel({
+ id: configId,
+ domainObject: this.domainObject,
+ openmct: this.openmct,
+ callback: (data) => {
+ this.data = data;
+ }
+ });
+ configStore.add(configId, config);
+ }
+
+ return config;
+ },
loadingUpdated(loaded) {
this.loading = loaded;
},
destroy() {
+ this.stopListening();
+ configStore.deleteStore(this.config.id);
+
this.composition.off('add', this.addChild);
this.composition.off('remove', this.removeChild);
this.composition.off('reorder', this.compositionReorder);
@@ -132,6 +167,7 @@ export default {
const id = this.openmct.objects.makeKeyString(child.identifier);
this.$set(this.tickWidthMap, id, 0);
+
this.compositionObjects.push(child);
},
@@ -140,6 +176,13 @@ export default {
this.$delete(this.tickWidthMap, id);
+ const configIndex = this.domainObject.configuration.series.findIndex((seriesConfig) => {
+ return this.openmct.objects.areIdsEqual(seriesConfig.identifier, childIdentifier);
+ });
+ if (configIndex > -1) {
+ this.domainObject.configuration.series.splice(configIndex, 1);
+ }
+
const childObj = this.compositionObjects.filter((c) => {
const identifier = this.openmct.objects.makeKeyString(c.identifier);
@@ -184,20 +227,52 @@ export default {
this.hideExportButtons = false;
}.bind(this));
},
-
- toggleCursorGuide() {
- this.cursorGuide = !this.cursorGuide;
- },
-
- toggleGridLines() {
- this.gridLines = !this.gridLines;
- },
onTickWidthChange(width, plotId) {
if (!Object.prototype.hasOwnProperty.call(this.tickWidthMap, plotId)) {
return;
}
this.$set(this.tickWidthMap, plotId, width);
+ },
+ legendHoverChanged(data) {
+ this.showLimitLineLabels = data;
+ },
+ lockHighlightPointUpdated(data) {
+ this.lockHighlightPoint = data;
+ },
+ highlightsUpdated(data) {
+ this.highlights = data;
+ },
+ registerSeriesListeners(configId) {
+ this.seriesConfig[configId] = this.getConfig(configId);
+ this.listenTo(this.seriesConfig[configId].series, 'add', this.addSeries, this);
+ this.listenTo(this.seriesConfig[configId].series, 'remove', this.removeSeries, this);
+
+ this.seriesConfig[configId].series.models.forEach(this.addSeries, this);
+ },
+ addSeries(series) {
+ const index = this.seriesModels.length;
+ this.$set(this.seriesModels, index, series);
+ },
+ removeSeries(plotSeries) {
+ const index = this.seriesModels.findIndex(seriesModel => this.openmct.objects.areIdsEqual(seriesModel.identifier, plotSeries.identifier));
+ if (index > -1) {
+ this.$delete(this.seriesModels, index);
+ }
+
+ this.stopListening(plotSeries);
+ },
+ onCursorGuideChange(cursorGuide) {
+ this.cursorGuide = cursorGuide === true;
+ },
+ onGridLinesChange(gridLines) {
+ this.gridLines = gridLines === true;
+ },
+ getViewContext() {
+ return {
+ exportPNG: this.exportPNG,
+ exportJPG: this.exportJPG
+ };
}
}
};
diff --git a/src/plugins/plot/stackedPlot/StackedPlotItem.vue b/src/plugins/plot/stackedPlot/StackedPlotItem.vue
index c47dacba4..b0049a361 100644
--- a/src/plugins/plot/stackedPlot/StackedPlotItem.vue
+++ b/src/plugins/plot/stackedPlot/StackedPlotItem.vue
@@ -27,12 +27,15 @@
import MctPlot from '../MctPlot.vue';
import Vue from "vue";
import conditionalStylesMixin from "./mixins/objectStyles-mixin";
+import configStore from "@/plugins/plot/configuration/ConfigStore";
+import PlotConfigurationModel from "@/plugins/plot/configuration/PlotConfigurationModel";
+import ProgressBar from "../../../ui/components/ProgressBar.vue";
export default {
mixins: [conditionalStylesMixin],
inject: ['openmct', 'domainObject', 'path'],
props: {
- object: {
+ childObject: {
type: Object,
default() {
return {};
@@ -56,6 +59,18 @@ export default {
return true;
}
},
+ showLimitLineLabels: {
+ type: Object,
+ default() {
+ return {};
+ }
+ },
+ colorPalette: {
+ type: Object,
+ default() {
+ return undefined;
+ }
+ },
plotTickWidth: {
type: Number,
default() {
@@ -72,12 +87,22 @@ export default {
},
plotTickWidth(width) {
this.updateComponentProp('plotTickWidth', width);
+ },
+ showLimitLineLabels: {
+ handler(data) {
+ this.updateComponentProp('limitLineLabels', data);
+ },
+ deep: true
}
},
mounted() {
this.updateView();
},
beforeDestroy() {
+ if (this.removeSelectable) {
+ this.removeSelectable();
+ }
+
if (this.component) {
this.component.$destroy();
}
@@ -96,21 +121,28 @@ export default {
}
const onTickWidthChange = this.onTickWidthChange;
- const loadingUpdated = this.loadingUpdated;
+ const onLockHighlightPointUpdated = this.onLockHighlightPointUpdated;
+ const onHighlightsUpdated = this.onHighlightsUpdated;
+ const onConfigLoaded = this.onConfigLoaded;
+ const onCursorGuideChange = this.onCursorGuideChange;
+ const onGridLinesChange = this.onGridLinesChange;
const setStatus = this.setStatus;
const openmct = this.openmct;
- const object = this.object;
const path = this.path;
+ //If this object is not persistable, then package it with it's parent
+ const object = this.getPlotObject();
const getProps = this.getProps;
+ const isMissing = openmct.objects.isMissing(object);
let viewContainer = document.createElement('div');
this.$el.append(viewContainer);
this.component = new Vue({
el: viewContainer,
components: {
- MctPlot
+ MctPlot,
+ ProgressBar
},
provide: {
openmct,
@@ -121,33 +153,123 @@ export default {
return {
...getProps(),
onTickWidthChange,
- loadingUpdated,
- setStatus
+ onLockHighlightPointUpdated,
+ onHighlightsUpdated,
+ onConfigLoaded,
+ onCursorGuideChange,
+ onGridLinesChange,
+ setStatus,
+ isMissing,
+ loading: true
};
},
- template: '<div ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver" :class="{\'s-status-timeconductor-unsynced\': status && status === \'timeconductor-unsynced\'}"><div v-show="!!loading" class="c-loading--overlay loading"></div><mct-plot :grid-lines="gridLines" :cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" :options="options" @plotTickWidth="onTickWidthChange" @statusUpdated="setStatus" @loadingUpdated="loadingUpdated"/></div>'
+ methods: {
+ loadingUpdated(loaded) {
+ this.loading = loaded;
+ }
+ },
+ template: '<div v-if="!isMissing" ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver" :class="{\'s-status-timeconductor-unsynced\': status && status === \'timeconductor-unsynced\'}"><progress-bar v-show="loading !== false" class="c-telemetry-table__progress-bar" :model="{progressPerc: undefined}" /><mct-plot :init-grid-lines="gridLines" :init-cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" :limit-line-labels="limitLineLabels" :color-palette="colorPalette" :options="options" @plotTickWidth="onTickWidthChange" @lockHighlightPoint="onLockHighlightPointUpdated" @highlights="onHighlightsUpdated" @configLoaded="onConfigLoaded" @cursorGuide="onCursorGuideChange" @gridLines="onGridLinesChange" @statusUpdated="setStatus" @loadingUpdated="loadingUpdated"/></div>'
});
+
+ this.setSelection();
+ },
+ onLockHighlightPointUpdated() {
+ this.$emit('lockHighlightPoint', ...arguments);
+ },
+ onHighlightsUpdated() {
+ this.$emit('highlights', ...arguments);
+ },
+ onConfigLoaded() {
+ this.$emit('configLoaded', ...arguments);
},
onTickWidthChange() {
this.$emit('plotTickWidth', ...arguments);
},
+ onCursorGuideChange() {
+ this.$emit('cursorGuide', ...arguments);
+ },
+ onGridLinesChange() {
+ this.$emit('gridLines', ...arguments);
+ },
setStatus(status) {
this.status = status;
this.updateComponentProp('status', status);
},
- loadingUpdated(loaded) {
- this.loading = loaded;
- this.updateComponentProp('loading', loaded);
+ setSelection() {
+ let childContext = {};
+ childContext.item = this.childObject;
+ this.context = childContext;
+ if (this.removeSelectable) {
+ this.removeSelectable();
+ }
+
+ this.removeSelectable = this.openmct.selection.selectable(
+ this.$el, this.context);
},
getProps() {
return {
+ limitLineLabels: this.showLimitLineLabels,
gridLines: this.gridLines,
cursorGuide: this.cursorGuide,
plotTickWidth: this.plotTickWidth,
- loading: this.loading,
options: this.options,
- status: this.status
+ status: this.status,
+ colorPalette: this.colorPalette
};
+ },
+ getPlotObject() {
+ if (this.childObject.configuration && this.childObject.configuration.series) {
+ //If the object has a configuration, allow initialization of the config from it's persisted config
+ return this.childObject;
+ } else {
+ //If object is missing, warn and return object
+ if (this.openmct.objects.isMissing(this.childObject)) {
+ console.warn('Missing domain object');
+
+ return this.childObject;
+ }
+
+ // If the object does not have configuration, initialize the series config with the persisted config from the stacked plot
+ const configId = this.openmct.objects.makeKeyString(this.childObject.identifier);
+ let config = configStore.get(configId);
+ if (!config) {
+ let persistedSeriesConfig = this.domainObject.configuration.series.find((seriesConfig) => {
+ return this.openmct.objects.areIdsEqual(seriesConfig.identifier, this.childObject.identifier);
+ });
+
+ if (!persistedSeriesConfig) {
+ persistedSeriesConfig = {
+ series: {},
+ yAxis: {}
+ };
+ }
+
+ config = new PlotConfigurationModel({
+ id: configId,
+ domainObject: {
+ ...this.childObject,
+ configuration: {
+ series: [
+ {
+ identifier: this.childObject.identifier,
+ ...persistedSeriesConfig.series
+ }
+ ],
+ yAxis: persistedSeriesConfig.yAxis
+
+ }
+ },
+ openmct: this.openmct,
+ palette: this.colorPalette,
+ callback: (data) => {
+ this.data = data;
+ }
+ });
+ configStore.add(configId, config);
+ }
+
+ return this.childObject;
+ }
}
}
};
diff --git a/src/plugins/plot/stackedPlot/StackedPlotViewProvider.js b/src/plugins/plot/stackedPlot/StackedPlotViewProvider.js
index 41008dc9f..f97ac6e8e 100644
--- a/src/plugins/plot/stackedPlot/StackedPlotViewProvider.js
+++ b/src/plugins/plot/stackedPlot/StackedPlotViewProvider.js
@@ -67,9 +67,16 @@ export default function StackedPlotViewProvider(openmct) {
}
};
},
- template: '<stacked-plot :options="options"></stacked-plot>'
+ template: '<stacked-plot ref="plotComponent" :options="options"></stacked-plot>'
});
},
+ getViewContext() {
+ if (!component) {
+ return {};
+ }
+
+ return component.$refs.plotComponent.getViewContext();
+ },
destroy: function () {
component.$destroy();
component = undefined;
diff --git a/src/plugins/plot/stackedPlot/mixins/objectStyles-mixin.js b/src/plugins/plot/stackedPlot/mixins/objectStyles-mixin.js
index 1c2ee7b53..37398c43c 100644
--- a/src/plugins/plot/stackedPlot/mixins/objectStyles-mixin.js
+++ b/src/plugins/plot/stackedPlot/mixins/objectStyles-mixin.js
@@ -31,7 +31,7 @@ export default {
};
},
mounted() {
- this.objectStyles = this.getObjectStyleForItem(this.object.configuration);
+ this.objectStyles = this.getObjectStyleForItem(this.childObject.configuration);
this.initObjectStyles();
},
beforeDestroy() {
@@ -62,18 +62,18 @@ export default {
this.stopListeningStyles();
}
- this.stopListeningStyles = this.openmct.objects.observe(this.object, 'configuration.objectStyles', (newObjectStyle) => {
+ this.stopListeningStyles = this.openmct.objects.observe(this.childObject, 'configuration.objectStyles', (newObjectStyle) => {
//Updating styles in the inspector view will trigger this so that the changes are reflected immediately
this.styleRuleManager.updateObjectStyleConfig(newObjectStyle);
});
- if (this.object && this.object.configuration && this.object.configuration.fontStyle) {
- const { fontSize, font } = this.object.configuration.fontStyle;
+ if (this.childObject && this.childObject.configuration && this.childObject.configuration.fontStyle) {
+ const { fontSize, font } = this.childObject.configuration.fontStyle;
this.setFontSize(fontSize);
this.setFont(font);
}
- this.stopListeningFontStyles = this.openmct.objects.observe(this.object, 'configuration.fontStyle', (newFontStyle) => {
+ this.stopListeningFontStyles = this.openmct.objects.observe(this.childObject, 'configuration.fontStyle', (newFontStyle) => {
this.setFontSize(newFontStyle.fontSize);
this.setFont(newFontStyle.font);
});
diff --git a/src/plugins/plot/stackedPlot/pluginSpec.js b/src/plugins/plot/stackedPlot/pluginSpec.js
new file mode 100644
index 000000000..18d4a3034
--- /dev/null
+++ b/src/plugins/plot/stackedPlot/pluginSpec.js
@@ -0,0 +1,771 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 {createMouseEvent, createOpenMct, resetApplicationState, spyOnBuiltins} from "utils/testing";
+import PlotVuePlugin from "../plugin";
+import Vue from "vue";
+import StackedPlot from "./StackedPlot.vue";
+import configStore from "../configuration/ConfigStore";
+import EventEmitter from "EventEmitter";
+import PlotConfigurationModel from "../configuration/PlotConfigurationModel";
+import PlotOptions from "../inspector/PlotOptions.vue";
+
+describe("the plugin", function () {
+ let element;
+ let child;
+ let openmct;
+ let telemetryPromise;
+ let telemetryPromiseResolve;
+ let mockObjectPath;
+ let stackedPlotObject = {
+ identifier: {
+ namespace: "",
+ key: "test-plot"
+ },
+ type: "telemetry.plot.stacked",
+ name: "Test Stacked Plot",
+ configuration: {
+ series: []
+ }
+ };
+
+ beforeEach((done) => {
+ mockObjectPath = [
+ {
+ name: 'mock folder',
+ type: 'fake-folder',
+ identifier: {
+ key: 'mock-folder',
+ namespace: ''
+ }
+ },
+ {
+ name: 'mock parent folder',
+ type: 'time-strip',
+ identifier: {
+ key: 'mock-parent-folder',
+ namespace: ''
+ }
+ }
+ ];
+ const testTelemetry = [
+ {
+ 'utc': 1,
+ 'some-key': 'some-value 1',
+ 'some-other-key': 'some-other-value 1'
+ },
+ {
+ 'utc': 2,
+ 'some-key': 'some-value 2',
+ 'some-other-key': 'some-other-value 2'
+ },
+ {
+ 'utc': 3,
+ 'some-key': 'some-value 3',
+ 'some-other-key': 'some-other-value 3'
+ }
+ ];
+
+ const timeSystem = {
+ timeSystemKey: 'utc',
+ bounds: {
+ start: 0,
+ end: 4
+ }
+ };
+
+ openmct = createOpenMct(timeSystem);
+
+ telemetryPromise = new Promise((resolve) => {
+ telemetryPromiseResolve = resolve;
+ });
+
+ spyOn(openmct.telemetry, 'request').and.callFake(() => {
+ telemetryPromiseResolve(testTelemetry);
+
+ return telemetryPromise;
+ });
+
+ openmct.install(new PlotVuePlugin());
+
+ element = document.createElement("div");
+ element.style.width = "640px";
+ element.style.height = "480px";
+ child = document.createElement("div");
+ child.style.width = "640px";
+ child.style.height = "480px";
+ element.appendChild(child);
+ document.body.appendChild(element);
+
+ spyOn(window, 'ResizeObserver').and.returnValue({
+ observe() {},
+ unobserve() {},
+ disconnect() {}
+ });
+
+ openmct.types.addType("test-object", {
+ creatable: true
+ });
+
+ spyOnBuiltins(["requestAnimationFrame"]);
+ window.requestAnimationFrame.and.callFake((callBack) => {
+ callBack();
+ });
+
+ openmct.router.path = [stackedPlotObject];
+ openmct.on("start", done);
+ openmct.startHeadless();
+ });
+
+ afterEach((done) => {
+ openmct.time.timeSystem('utc', {
+ start: 0,
+ end: 1
+ });
+ configStore.deleteAll();
+ resetApplicationState(openmct).then(done).catch(done);
+ });
+
+ afterAll(() => {
+ openmct.router.path = null;
+ });
+
+ describe("the plot views", () => {
+ it("provides a stacked plot view for objects with telemetry", () => {
+ const testTelemetryObject = {
+ id: "test-object",
+ type: "telemetry.plot.stacked",
+ telemetry: {
+ values: [{
+ key: "some-key"
+ }]
+ }
+ };
+
+ const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
+ let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-stacked");
+ expect(plotView).toBeDefined();
+ });
+
+ });
+
+ describe("The stacked plot view", () => {
+ let testTelemetryObject;
+ let testTelemetryObject2;
+ let config;
+ let component;
+ let mockComposition;
+ let plotViewComponentObject;
+
+ afterAll(() => {
+ openmct.router.path = null;
+ });
+
+ beforeEach(() => {
+ testTelemetryObject = {
+ identifier: {
+ namespace: "",
+ key: "test-object"
+ },
+ type: "test-object",
+ name: "Test Object",
+ telemetry: {
+ values: [{
+ key: "utc",
+ format: "utc",
+ name: "Time",
+ hints: {
+ domain: 1
+ }
+ }, {
+ key: "some-key",
+ name: "Some attribute",
+ hints: {
+ range: 1
+ }
+ }, {
+ key: "some-other-key",
+ name: "Another attribute",
+ hints: {
+ range: 2
+ }
+ }]
+ },
+ configuration: {
+ objectStyles: {
+ staticStyle: {
+ style: {
+ backgroundColor: 'rgb(0, 200, 0)',
+ color: '',
+ border: ''
+ }
+ },
+ conditionSetIdentifier: {
+ namespace: '',
+ key: 'testConditionSetId'
+ },
+ selectedConditionId: 'conditionId1',
+ defaultConditionId: 'conditionId1',
+ styles: [
+ {
+ conditionId: 'conditionId1',
+ style: {
+ backgroundColor: 'rgb(0, 155, 0)',
+ color: '',
+ output: '',
+ border: ''
+ }
+ }
+ ]
+ }
+ }
+ };
+
+ testTelemetryObject2 = {
+ identifier: {
+ namespace: "",
+ key: "test-object2"
+ },
+ type: "test-object",
+ name: "Test Object2",
+ telemetry: {
+ values: [{
+ key: "utc",
+ format: "utc",
+ name: "Time",
+ hints: {
+ domain: 1
+ }
+ }, {
+ key: "some-key2",
+ name: "Some attribute2",
+ hints: {
+ range: 1
+ }
+ }, {
+ key: "some-other-key2",
+ name: "Another attribute2",
+ hints: {
+ range: 2
+ }
+ }]
+ }
+ };
+
+ mockComposition = new EventEmitter();
+ mockComposition.load = () => {
+ mockComposition.emit('add', testTelemetryObject);
+
+ return [testTelemetryObject];
+ };
+
+ spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
+
+ let viewContainer = document.createElement("div");
+ child.append(viewContainer);
+ component = new Vue({
+ el: viewContainer,
+ components: {
+ StackedPlot
+ },
+ provide: {
+ openmct: openmct,
+ domainObject: stackedPlotObject,
+ composition: openmct.composition.get(stackedPlotObject),
+ path: [stackedPlotObject]
+ },
+ template: "<stacked-plot></stacked-plot>"
+ });
+
+ return telemetryPromise
+ .then(Vue.nextTick())
+ .then(() => {
+ plotViewComponentObject = component.$root.$children[0];
+ const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier);
+ config = configStore.get(configId);
+ });
+ });
+
+ it("Renders a collapsed legend for every telemetry", () => {
+ let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
+ expect(legend.length).toBe(1);
+ expect(legend[0].innerHTML).toEqual("Test Object");
+ });
+
+ it("Renders an expanded legend for every telemetry", () => {
+ let legendControl = element.querySelector(".c-plot-legend__view-control.gl-plot-legend__view-control.c-disclosure-triangle");
+ const clickEvent = createMouseEvent("click");
+
+ legendControl.dispatchEvent(clickEvent);
+
+ let legend = element.querySelectorAll(".plot-wrapper-expanded-legend .plot-legend-item td");
+ expect(legend.length).toBe(6);
+ });
+
+ it("Renders X-axis ticks for the telemetry object", (done) => {
+ let xAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-x .gl-plot-tick-wrapper");
+ expect(xAxisElement.length).toBe(1);
+
+ config.xAxis.set('displayRange', {
+ min: 0,
+ max: 4
+ });
+
+ Vue.nextTick(() => {
+ let ticks = xAxisElement[0].querySelectorAll(".gl-plot-tick");
+ expect(ticks.length).toBe(9);
+
+ done();
+ });
+ });
+
+ it("Renders Y-axis ticks for the telemetry object", (done) => {
+ config.yAxis.set('displayRange', {
+ min: 10,
+ max: 20
+ });
+ Vue.nextTick(() => {
+ let yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper");
+ expect(yAxisElement.length).toBe(1);
+ let ticks = yAxisElement[0].querySelectorAll(".gl-plot-tick");
+ expect(ticks.length).toBe(6);
+ done();
+ });
+ });
+
+ it("Renders Y-axis options for the telemetry object", () => {
+ let yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y .gl-plot-y-label__select");
+ expect(yAxisElement.length).toBe(1);
+ let options = yAxisElement[0].querySelectorAll("option");
+ expect(options.length).toBe(2);
+ expect(options[0].value).toBe("Some attribute");
+ expect(options[1].value).toBe("Another attribute");
+ });
+
+ it("turns on cursor Guides all telemetry objects", (done) => {
+ expect(plotViewComponentObject.cursorGuide).toBeFalse();
+ plotViewComponentObject.cursorGuide = true;
+ Vue.nextTick(() => {
+ let childCursorGuides = element.querySelectorAll(".c-cursor-guide--v");
+ expect(childCursorGuides.length).toBe(1);
+ done();
+ });
+ });
+
+ it("shows grid lines for all telemetry objects", () => {
+ expect(plotViewComponentObject.gridLines).toBeTrue();
+ let gridLinesContainer = element.querySelectorAll(".gl-plot-display-area .js-ticks");
+ let visible = 0;
+ gridLinesContainer.forEach(el => {
+ if (el.style.display !== "none") {
+ visible++;
+ }
+ });
+ expect(visible).toBe(2);
+ });
+
+ it("hides grid lines for all telemetry objects", (done) => {
+ expect(plotViewComponentObject.gridLines).toBeTrue();
+ plotViewComponentObject.gridLines = false;
+ Vue.nextTick(() => {
+ expect(plotViewComponentObject.gridLines).toBeFalse();
+ let gridLinesContainer = element.querySelectorAll(".gl-plot-display-area .js-ticks");
+ let visible = 0;
+ gridLinesContainer.forEach(el => {
+ if (el.style.display !== "none") {
+ visible++;
+ }
+ });
+ expect(visible).toBe(0);
+ done();
+ });
+ });
+
+ it('plots a new series when a new telemetry object is added', (done) => {
+ mockComposition.emit('add', testTelemetryObject2);
+ Vue.nextTick(() => {
+ let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
+ expect(legend.length).toBe(2);
+ expect(legend[1].innerHTML).toEqual("Test Object2");
+ done();
+ });
+ });
+
+ it('removes plots from series when a telemetry object is removed', (done) => {
+ mockComposition.emit('remove', testTelemetryObject.identifier);
+ Vue.nextTick(() => {
+ expect(plotViewComponentObject.compositionObjects.length).toBe(0);
+ done();
+ });
+ });
+
+ it("Changes the label of the y axis when the option changes", (done) => {
+ let selectEl = element.querySelector('.gl-plot-y-label__select');
+ selectEl.value = 'Another attribute';
+ selectEl.dispatchEvent(new Event("change"));
+
+ Vue.nextTick(() => {
+ expect(config.yAxis.get('label')).toEqual('Another attribute');
+ done();
+ });
+ });
+
+ it("Renders a new series when added to one of the plots", (done) => {
+ mockComposition.emit('add', testTelemetryObject2);
+ Vue.nextTick(() => {
+ let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
+ expect(legend.length).toBe(2);
+ expect(legend[1].innerHTML).toEqual("Test Object2");
+ done();
+ });
+ });
+
+ it("Adds a new point to the plot", (done) => {
+ let originalLength = config.series.models[0].getSeriesData().length;
+ config.series.models[0].add({
+ utc: 2,
+ 'some-key': 1,
+ 'some-other-key': 2
+ });
+ Vue.nextTick(() => {
+ const seriesData = config.series.models[0].getSeriesData();
+ expect(seriesData.length).toEqual(originalLength + 1);
+ done();
+ });
+ });
+
+ it("updates the xscale", (done) => {
+ config.xAxis.set('displayRange', {
+ min: 0,
+ max: 10
+ });
+ Vue.nextTick(() => {
+ expect(plotViewComponentObject.$children[1].component.$children[1].xScale.domain()).toEqual({
+ min: 0,
+ max: 10
+ });
+ done();
+ });
+ });
+
+ it("updates the yscale", (done) => {
+ config.yAxis.set('displayRange', {
+ min: 10,
+ max: 20
+ });
+ Vue.nextTick(() => {
+ expect(plotViewComponentObject.$children[1].component.$children[1].yScale.domain()).toEqual({
+ min: 10,
+ max: 20
+ });
+ done();
+ });
+ });
+
+ it("shows styles for telemetry objects if available", (done) => {
+ Vue.nextTick(() => {
+ let conditionalStylesContainer = element.querySelectorAll(".c-plot--stacked-container .js-style-receiver");
+ let hasStyles = 0;
+ conditionalStylesContainer.forEach(el => {
+ if (el.style.backgroundColor !== '') {
+ hasStyles++;
+ }
+ });
+ expect(hasStyles).toBe(1);
+ done();
+ });
+ });
+ });
+
+ describe('the stacked plot inspector view', () => {
+ let component;
+ let viewComponentObject;
+ let mockComposition;
+ let testTelemetryObject;
+ let selection;
+ let config;
+ beforeEach((done) => {
+ testTelemetryObject = {
+ identifier: {
+ namespace: "",
+ key: "test-object"
+ },
+ type: "test-object",
+ name: "Test Object",
+ telemetry: {
+ values: [{
+ key: "utc",
+ format: "utc",
+ name: "Time",
+ hints: {
+ domain: 1
+ }
+ }, {
+ key: "some-key",
+ name: "Some attribute",
+ hints: {
+ range: 1
+ }
+ }, {
+ key: "some-other-key",
+ name: "Another attribute",
+ hints: {
+ range: 2
+ }
+ }]
+ }
+ };
+
+ selection = [
+ [
+ {
+ context: {
+ item: {
+ type: 'telemetry.plot.stacked',
+ identifier: {
+ key: 'some-stacked-plot',
+ namespace: ''
+ },
+ configuration: {
+ series: []
+ }
+ }
+ }
+ }
+ ]
+ ];
+
+ openmct.router.path = [testTelemetryObject];
+ mockComposition = new EventEmitter();
+ mockComposition.load = () => {
+ mockComposition.emit('add', testTelemetryObject);
+
+ return [testTelemetryObject];
+ };
+
+ spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
+
+ const configId = openmct.objects.makeKeyString(selection[0][0].context.item.identifier);
+ config = new PlotConfigurationModel({
+ id: configId,
+ domainObject: selection[0][0].context.item,
+ openmct: openmct
+ });
+ configStore.add(configId, config);
+
+ let viewContainer = document.createElement('div');
+ child.append(viewContainer);
+ component = new Vue({
+ el: viewContainer,
+ components: {
+ PlotOptions
+ },
+ provide: {
+ openmct: openmct,
+ domainObject: selection[0][0].context.item,
+ path: [selection[0][0].context.item]
+ },
+ template: '<plot-options/>'
+ });
+
+ Vue.nextTick(() => {
+ viewComponentObject = component.$root.$children[0];
+ done();
+ });
+ });
+
+ afterEach(() => {
+ openmct.router.path = null;
+ });
+
+ describe('in view only mode', () => {
+ let browseOptionsEl;
+ beforeEach(() => {
+ browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse');
+ });
+
+ it('shows legend properties', () => {
+ const legendPropertiesEl = browseOptionsEl.querySelector('.js-legend-properties');
+ expect(legendPropertiesEl).not.toBeNull();
+ });
+
+ it('does not show series properties', () => {
+ const seriesPropertiesEl = browseOptionsEl.querySelector('.c-tree');
+ expect(seriesPropertiesEl).toBeNull();
+ });
+
+ it('does not show yaxis properties', () => {
+ const yAxisPropertiesEl = browseOptionsEl.querySelector('.js-yaxis-properties');
+ expect(yAxisPropertiesEl).toBeNull();
+ });
+ });
+
+ });
+
+ describe('inspector view of stacked plot child', () => {
+ let component;
+ let viewComponentObject;
+ let mockComposition;
+ let testTelemetryObject;
+ let selection;
+ let config;
+ beforeEach((done) => {
+ testTelemetryObject = {
+ identifier: {
+ namespace: "",
+ key: "test-object"
+ },
+ type: "test-object",
+ name: "Test Object",
+ telemetry: {
+ values: [{
+ key: "utc",
+ format: "utc",
+ name: "Time",
+ hints: {
+ domain: 1
+ }
+ }, {
+ key: "some-key",
+ name: "Some attribute",
+ hints: {
+ range: 1
+ }
+ }, {
+ key: "some-other-key",
+ name: "Another attribute",
+ hints: {
+ range: 2
+ }
+ }]
+ }
+ };
+
+ selection = [
+ [
+ {
+ context: {
+ item: {
+ id: "test-object",
+ identifier: {
+ key: "test-object",
+ namespace: ''
+ },
+ type: "telemetry.plot.overlay",
+ configuration: {
+ series: [
+ {
+ identifier: {
+ key: "test-object",
+ namespace: ''
+ }
+ }
+ ]
+ },
+ composition: []
+ }
+ }
+ },
+ {
+ context: {
+ item: {
+ type: 'telemetry.plot.stacked',
+ identifier: {
+ key: 'some-stacked-plot',
+ namespace: ''
+ },
+ configuration: {
+ series: []
+ }
+ }
+ }
+ }
+ ]
+ ];
+
+ openmct.router.path = [testTelemetryObject];
+ mockComposition = new EventEmitter();
+ mockComposition.load = () => {
+ mockComposition.emit('add', testTelemetryObject);
+
+ return [testTelemetryObject];
+ };
+
+ spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
+
+ const configId = openmct.objects.makeKeyString(selection[0][0].context.item.identifier);
+ config = new PlotConfigurationModel({
+ id: configId,
+ domainObject: selection[0][0].context.item,
+ openmct: openmct
+ });
+ configStore.add(configId, config);
+
+ let viewContainer = document.createElement('div');
+ child.append(viewContainer);
+ component = new Vue({
+ el: viewContainer,
+ components: {
+ PlotOptions
+ },
+ provide: {
+ openmct: openmct,
+ domainObject: selection[0][0].context.item,
+ path: [selection[0][0].context.item, selection[0][1].context.item]
+ },
+ template: '<plot-options/>'
+ });
+
+ Vue.nextTick(() => {
+ viewComponentObject = component.$root.$children[0];
+ done();
+ });
+ });
+
+ afterEach(() => {
+ openmct.router.path = null;
+ });
+
+ describe('in view only mode', () => {
+ let browseOptionsEl;
+ beforeEach(() => {
+ browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse');
+ });
+
+ it('hides legend properties', () => {
+ const legendPropertiesEl = browseOptionsEl.querySelector('.js-legend-properties');
+ expect(legendPropertiesEl).toBeNull();
+ });
+
+ it('shows series properties', () => {
+ const seriesPropertiesEl = browseOptionsEl.querySelector('.c-tree');
+ expect(seriesPropertiesEl).not.toBeNull();
+ });
+
+ it('shows yaxis properties', () => {
+ const yAxisPropertiesEl = browseOptionsEl.querySelector('.js-yaxis-properties');
+ expect(yAxisPropertiesEl).not.toBeNull();
+ });
+ });
+
+ });
+});
diff --git a/src/plugins/plot/stackedPlot/stackedPlotConfigurationInterceptor.js b/src/plugins/plot/stackedPlot/stackedPlotConfigurationInterceptor.js
new file mode 100644
index 000000000..d16ca1b46
--- /dev/null
+++ b/src/plugins/plot/stackedPlot/stackedPlotConfigurationInterceptor.js
@@ -0,0 +1,38 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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.
+ *****************************************************************************/
+
+export default function stackedPlotConfigurationInterceptor(openmct) {
+
+ openmct.objects.addGetInterceptor({
+ appliesTo: (identifier, domainObject) => {
+ return domainObject && domainObject.type === 'telemetry.plot.stacked';
+ },
+ invoke: (identifier, object) => {
+
+ if (object && object.configuration && object.configuration.series === undefined) {
+ object.configuration.series = [];
+ }
+
+ return object;
+ }
+ });
+}
diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js
index 6b20e3c31..c39e6c322 100644
--- a/src/plugins/plugins.js
+++ b/src/plugins/plugins.js
@@ -32,12 +32,14 @@ define([
'./autoflow/AutoflowTabularPlugin',
'./timeConductor/plugin',
'../../example/imagery/plugin',
+ '../../example/faultManagement/exampleFaultSource',
'./imagery/plugin',
'./summaryWidget/plugin',
'./URLIndicatorPlugin/URLIndicatorPlugin',
'./telemetryMean/plugin',
'./plot/plugin',
- './charts/plugin',
+ './charts/bar/plugin',
+ './charts/scatter/plugin',
'./telemetryTable/plugin',
'./staticRootPlugin/plugin',
'./notebook/plugin',
@@ -56,7 +58,6 @@ define([
'./condition/plugin',
'./conditionWidget/plugin',
'./themes/espresso',
- './themes/maelstrom',
'./themes/snow',
'./URLTimeSettingsSynchronizer/plugin',
'./notificationIndicator/plugin',
@@ -77,8 +78,11 @@ define([
'./userIndicator/plugin',
'../../example/exampleUser/plugin',
'./localStorage/plugin',
+ './operatorStatus/plugin',
'./gauge/GaugePlugin',
- './timelist/plugin'
+ './timelist/plugin',
+ './faultManagement/FaultManagementPlugin',
+ '../../example/exampleTags/plugin'
], function (
_,
UTCTimeSystem,
@@ -91,12 +95,14 @@ define([
AutoflowPlugin,
TimeConductorPlugin,
ExampleImagery,
+ ExampleFaultSource,
ImageryPlugin,
SummaryWidget,
URLIndicatorPlugin,
TelemetryMean,
PlotPlugin,
- ChartPlugin,
+ BarChartPlugin,
+ ScatterPlotPlugin,
TelemetryTablePlugin,
StaticRootPlugin,
Notebook,
@@ -115,7 +121,6 @@ define([
ConditionPlugin,
ConditionWidgetPlugin,
Espresso,
- Maelstrom,
Snow,
URLTimeSettingsSynchronizer,
NotificationIndicator,
@@ -136,15 +141,20 @@ define([
UserIndicator,
ExampleUser,
LocalStorage,
+ OperatorStatus,
GaugePlugin,
- TimeList
+ TimeList,
+ FaultManagementPlugin,
+ ExampleTags
) {
const plugins = {};
plugins.example = {};
plugins.example.ExampleUser = ExampleUser.default;
plugins.example.ExampleImagery = ExampleImagery.default;
+ plugins.example.ExampleFaultSource = ExampleFaultSource.default;
plugins.example.EventGeneratorPlugin = EventGeneratorPlugin.default;
+ plugins.example.ExampleTags = ExampleTags.default;
plugins.example.Generator = () => GeneratorPlugin;
plugins.UTCTimeSystem = UTCTimeSystem.default;
@@ -172,14 +182,17 @@ define([
plugins.ImageryPlugin = ImageryPlugin;
plugins.Plot = PlotPlugin.default;
- plugins.Chart = ChartPlugin.default;
+ plugins.BarChart = BarChartPlugin.default;
+ plugins.ScatterPlot = ScatterPlotPlugin.default;
plugins.TelemetryTable = TelemetryTablePlugin;
plugins.SummaryWidget = SummaryWidget;
plugins.TelemetryMean = TelemetryMean;
plugins.URLIndicator = URLIndicatorPlugin;
- plugins.Notebook = Notebook.default;
+ plugins.Notebook = Notebook.NotebookPlugin;
+ plugins.RestrictedNotebook = Notebook.RestrictedNotebookPlugin;
plugins.DisplayLayout = DisplayLayoutPlugin.default;
+ plugins.FaultManagement = FaultManagementPlugin.default;
plugins.FormActions = FormActions;
plugins.FolderView = FolderView;
plugins.Tabs = Tabs;
@@ -192,7 +205,6 @@ define([
plugins.ClearData = ClearData;
plugins.WebPage = WebPagePlugin.default;
plugins.Espresso = Espresso.default;
- plugins.Maelstrom = Maelstrom.default;
plugins.Snow = Snow.default;
plugins.Condition = ConditionPlugin.default;
plugins.ConditionWidget = ConditionWidgetPlugin.default;
@@ -214,6 +226,7 @@ define([
plugins.DeviceClassifier = DeviceClassifier.default;
plugins.UserIndicator = UserIndicator.default;
plugins.LocalStorage = LocalStorage.default;
+ plugins.OperatorStatus = OperatorStatus.default;
plugins.Gauge = GaugePlugin.default;
plugins.Timelist = TimeList.default;
diff --git a/src/plugins/remoteClock/RemoteClock.js b/src/plugins/remoteClock/RemoteClock.js
index 1502d9a9d..3d6e6fcf4 100644
--- a/src/plugins/remoteClock/RemoteClock.js
+++ b/src/plugins/remoteClock/RemoteClock.js
@@ -20,6 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import DefaultClock from '../../utils/clock/DefaultClock';
+import remoteClockRequestInterceptor from './requestInterceptor';
/**
* A {@link openmct.TimeAPI.Clock} that updates the temporal bounds of the
@@ -49,6 +50,14 @@ export default class RemoteClock extends DefaultClock {
this.lastTick = 0;
+ this.openmct.telemetry.addRequestInterceptor(
+ remoteClockRequestInterceptor(
+ this.openmct,
+ this.identifier,
+ this.#waitForReady.bind(this)
+ )
+ );
+
this._processDatum = this._processDatum.bind(this);
}
@@ -129,4 +138,25 @@ export default class RemoteClock extends DefaultClock {
return timeFormatter.parse(datum);
};
}
+
+ /**
+ * Waits for the clock to have a non-default tick value.
+ *
+ * @private
+ */
+ #waitForReady() {
+ const waitForInitialTick = (resolve) => {
+ if (this.lastTick > 0) {
+ const offsets = this.openmct.time.clockOffsets();
+ resolve({
+ start: this.lastTick + offsets.start,
+ end: this.lastTick + offsets.end
+ });
+ } else {
+ setTimeout(() => waitForInitialTick(resolve), 100);
+ }
+ };
+
+ return new Promise(waitForInitialTick);
+ }
}
diff --git a/src/plugins/remoteClock/RemoteClockSpec.js b/src/plugins/remoteClock/RemoteClockSpec.js
index 63aa03791..0c28d4184 100644
--- a/src/plugins/remoteClock/RemoteClockSpec.js
+++ b/src/plugins/remoteClock/RemoteClockSpec.js
@@ -71,7 +71,7 @@ describe("the RemoteClock plugin", () => {
parse: (datum) => datum.key
};
- beforeEach((done) => {
+ beforeEach(async () => {
openmct.install(openmct.plugins.RemoteClock(TIME_TELEMETRY_ID));
let clocks = openmct.time.getAllClocks();
@@ -113,9 +113,7 @@ describe("the RemoteClock plugin", () => {
end: OFFSET_END
});
- Promise.all([objectPromiseResolve, requestPromise])
- .then(done)
- .catch(done);
+ await Promise.all([objectPromiseResolve, requestPromise]);
});
it('is available and sets up initial values and listeners', () => {
diff --git a/src/plugins/remoteClock/requestInterceptor.js b/src/plugins/remoteClock/requestInterceptor.js
new file mode 100644
index 000000000..a04712910
--- /dev/null
+++ b/src/plugins/remoteClock/requestInterceptor.js
@@ -0,0 +1,44 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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.
+ *****************************************************************************/
+
+function remoteClockRequestInterceptor(openmct, _remoteClockIdentifier, waitForBounds) {
+ let remoteClockLoaded = false;
+
+ return {
+ appliesTo: () => {
+ // Get the activeClock from the Global Time Context
+ const { activeClock } = openmct.time;
+
+ return activeClock?.key === 'remote-clock' && !remoteClockLoaded;
+ },
+ invoke: async (request) => {
+ const { start, end } = await waitForBounds();
+ remoteClockLoaded = true;
+ request.start = start;
+ request.end = end;
+
+ return request;
+ }
+ };
+}
+
+export default remoteClockRequestInterceptor;
diff --git a/src/plugins/remove/RemoveAction.js b/src/plugins/remove/RemoveAction.js
index 188f4cc78..8a0b9ec4c 100644
--- a/src/plugins/remove/RemoveAction.js
+++ b/src/plugins/remove/RemoveAction.js
@@ -123,10 +123,7 @@ export default class RemoveAction {
}
if (isEditing) {
- let currentItemInView = this.openmct.router.path[0];
- let domainObject = objectPath[0];
-
- if (this.openmct.objects.areIdsEqual(currentItemInView.identifier, domainObject.identifier)) {
+ if (this.openmct.router.isNavigatedObject(objectPath)) {
return false;
}
}
diff --git a/src/plugins/staticRootPlugin/StaticModelProvider.js b/src/plugins/staticRootPlugin/StaticModelProvider.js
index 4d411dd63..f05991446 100644
--- a/src/plugins/staticRootPlugin/StaticModelProvider.js
+++ b/src/plugins/staticRootPlugin/StaticModelProvider.js
@@ -78,6 +78,10 @@ class StaticModelProvider {
}
parseTreeLeaf(leafKey, leafValue, idMap, namespace) {
+ if (leafValue === null || leafValue === undefined) {
+ return leafValue;
+ }
+
const hasChild = typeof leafValue === 'object';
if (hasChild) {
return this.parseBranchedLeaf(leafValue, idMap, namespace);
diff --git a/src/plugins/staticRootPlugin/static-provider-test.json b/src/plugins/staticRootPlugin/static-provider-test.json
index 541f8efbc..8c523de4a 100644
--- a/src/plugins/staticRootPlugin/static-provider-test.json
+++ b/src/plugins/staticRootPlugin/static-provider-test.json
@@ -1 +1 @@
-{"openmct":{"a9122832-4b6e-43ea-8219-5359c14c5de8":{"composition":["483c00d4-bb1d-4b42-b29a-c58e06b322a0","d2ac3ae4-0af2-49fe-81af-adac09936215"],"name":"import-provider-test","type":"folder","notes":"test data for import provider.","modified":1508522673278,"location":"mine","persisted":1508522673278},"483c00d4-bb1d-4b42-b29a-c58e06b322a0":{"telemetry":{"period":10,"amplitude":1,"offset":0,"dataRateInHz":1,"values":[{"key":"utc","name":"Time","format":"utc","hints":{"domain":1,"priority":0},"source":"utc"},{"key":"yesterday","name":"Yesterday","format":"utc","hints":{"domain":2,"priority":1},"source":"yesterday"},{"key":"sin","name":"Sine","hints":{"range":1,"priority":2},"source":"sin"},{"key":"cos","name":"Cosine","hints":{"range":2,"priority":3},"source":"cos"}]},"name":"SWG-10","type":"generator","modified":1508522652874,"location":"a9122832-4b6e-43ea-8219-5359c14c5de8","persisted":1508522652874},"d2ac3ae4-0af2-49fe-81af-adac09936215":{"composition":["483c00d4-bb1d-4b42-b29a-c58e06b322a0","20273193-f069-49e9-b4f7-b97a87ed755d"],"name":"Layout","type":"layout","configuration":{"layout":{"panels":{"483c00d4-bb1d-4b42-b29a-c58e06b322a0":{"position":[0,0],"dimensions":[17,8]},"20273193-f069-49e9-b4f7-b97a87ed755d":{"position":[0,8],"dimensions":[17,1],"hasFrame":false}}}},"modified":1508522745580,"location":"a9122832-4b6e-43ea-8219-5359c14c5de8","persisted":1508522745580},"20273193-f069-49e9-b4f7-b97a87ed755d":{"layoutGrid":[64,16],"composition":["483c00d4-bb1d-4b42-b29a-c58e06b322a0"],"name":"FP Test","type":"telemetry.fixed","configuration":{"fixed-display":{"elements":[{"type":"fixed.telemetry","x":0,"y":0,"id":"483c00d4-bb1d-4b42-b29a-c58e06b322a0","stroke":"transparent","color":"","titled":true,"width":8,"height":2,"useGrid":true,"size":"24px"}]}},"modified":1508522717619,"location":"d2ac3ae4-0af2-49fe-81af-adac09936215","persisted":1508522717619}},"rootId":"a9122832-4b6e-43ea-8219-5359c14c5de8"} \ No newline at end of file
+{"openmct":{"a9122832-4b6e-43ea-8219-5359c14c5de8":{"composition":["483c00d4-bb1d-4b42-b29a-c58e06b322a0","d2ac3ae4-0af2-49fe-81af-adac09936215"],"name":"import-provider-test","type":"folder","notes":null,"modified":1508522673278,"location":"mine","persisted":1508522673278},"483c00d4-bb1d-4b42-b29a-c58e06b322a0":{"telemetry":{"period":10,"amplitude":1,"offset":0,"dataRateInHz":1,"values":[{"key":"utc","name":"Time","format":"utc","hints":{"domain":1,"priority":0},"source":"utc"},{"key":"yesterday","name":"Yesterday","format":"utc","hints":{"domain":2,"priority":1},"source":"yesterday"},{"key":"sin","name":"Sine","hints":{"range":1,"priority":2},"source":"sin"},{"key":"cos","name":"Cosine","hints":{"range":2,"priority":3},"source":"cos"}]},"name":"SWG-10","type":"generator","modified":1508522652874,"location":"a9122832-4b6e-43ea-8219-5359c14c5de8","persisted":1508522652874},"d2ac3ae4-0af2-49fe-81af-adac09936215":{"composition":["483c00d4-bb1d-4b42-b29a-c58e06b322a0","20273193-f069-49e9-b4f7-b97a87ed755d"],"name":"Layout","type":"layout","configuration":{"layout":{"panels":{"483c00d4-bb1d-4b42-b29a-c58e06b322a0":{"position":[0,0],"dimensions":[17,8]},"20273193-f069-49e9-b4f7-b97a87ed755d":{"position":[0,8],"dimensions":[17,1],"hasFrame":false}}}},"modified":1508522745580,"location":"a9122832-4b6e-43ea-8219-5359c14c5de8","persisted":1508522745580},"20273193-f069-49e9-b4f7-b97a87ed755d":{"layoutGrid":[64,16],"composition":["483c00d4-bb1d-4b42-b29a-c58e06b322a0"],"name":"FP Test","type":"telemetry.fixed","configuration":{"fixed-display":{"elements":[{"type":"fixed.telemetry","x":0,"y":0,"id":"483c00d4-bb1d-4b42-b29a-c58e06b322a0","stroke":"transparent","color":"","titled":true,"width":8,"height":2,"useGrid":true,"size":"24px"}]}},"modified":1508522717619,"location":"d2ac3ae4-0af2-49fe-81af-adac09936215","persisted":1508522717619}},"rootId":"a9122832-4b6e-43ea-8219-5359c14c5de8"} \ No newline at end of file
diff --git a/src/plugins/summaryWidget/src/Condition.js b/src/plugins/summaryWidget/src/Condition.js
index 997ad67b4..66f9ecbeb 100644
--- a/src/plugins/summaryWidget/src/Condition.js
+++ b/src/plugins/summaryWidget/src/Condition.js
@@ -4,16 +4,16 @@ define([
'./input/KeySelect',
'./input/OperationSelect',
'./eventHelpers',
- 'EventEmitter',
- 'zepto'
+ '../../../utils/template/templateHelpers',
+ 'EventEmitter'
], function (
conditionTemplate,
ObjectSelect,
KeySelect,
OperationSelect,
eventHelpers,
- EventEmitter,
- $
+ templateHelpers,
+ EventEmitter
) {
/**
* Represents an individual condition for a summary widget rule. Manages the
@@ -31,12 +31,13 @@ define([
this.index = index;
this.conditionManager = conditionManager;
- this.domElement = $(conditionTemplate);
+ this.domElement = templateHelpers.convertTemplateToHTML(conditionTemplate)[0];
+
this.eventEmitter = new EventEmitter();
this.supportedCallbacks = ['remove', 'duplicate', 'change'];
- this.deleteButton = $('.t-delete', this.domElement);
- this.duplicateButton = $('.t-duplicate', this.domElement);
+ this.deleteButton = this.domElement.querySelector('.t-delete');
+ this.duplicateButton = this.domElement.querySelector('.t-duplicate');
this.selects = {};
this.valueInputs = [];
@@ -105,9 +106,10 @@ define([
});
Object.values(this.selects).forEach(function (select) {
- $('.t-configuration', self.domElement).append(select.getDOM());
+ self.domElement.querySelector('.t-configuration').append(select.getDOM());
});
- this.listenTo($('.t-value-inputs', this.domElement), 'input', onValueInput);
+
+ this.listenTo(this.domElement.querySelector('.t-value-inputs'), 'input', onValueInput);
}
Condition.prototype.getDOM = function (container) {
@@ -132,7 +134,7 @@ define([
* Hide the appropriate inputs when this is the only condition
*/
Condition.prototype.hideButtons = function () {
- this.deleteButton.hide();
+ this.deleteButton.style.display = 'none';
};
/**
@@ -172,14 +174,14 @@ define([
*/
Condition.prototype.generateValueInputs = function (operation) {
const evaluator = this.conditionManager.getEvaluator();
- const inputArea = $('.t-value-inputs', this.domElement);
+ const inputArea = this.domElement.querySelector('.t-value-inputs');
let inputCount;
let inputType;
let newInput;
let index = 0;
let emitChange = false;
- inputArea.html('');
+ inputArea.innerHTML = '';
this.valueInputs = [];
this.config.values = this.config.values || [];
@@ -189,17 +191,24 @@ define([
while (index < inputCount) {
if (inputType === 'select') {
- newInput = $('<select>' + this.generateSelectOptions() + '</select>');
+ const options = this.generateSelectOptions();
+
+ newInput = document.createElement("select");
+ newInput.innerHTML = options;
+
emitChange = true;
} else {
const defaultValue = inputType === 'number' ? 0 : '';
const value = this.config.values[index] || defaultValue;
this.config.values[index] = value;
- newInput = $('<input type = "' + inputType + '" value = "' + value + '"></input>');
+
+ newInput = document.createElement("input");
+ newInput.type = `${inputType}`;
+ newInput.value = `${value}`;
}
- this.valueInputs.push(newInput.get(0));
- inputArea.append(newInput);
+ this.valueInputs.push(newInput);
+ inputArea.appendChild(newInput);
index += 1;
}
diff --git a/src/plugins/summaryWidget/src/ConditionManager.js b/src/plugins/summaryWidget/src/ConditionManager.js
index ff90bc7bc..e50264903 100644
--- a/src/plugins/summaryWidget/src/ConditionManager.js
+++ b/src/plugins/summaryWidget/src/ConditionManager.js
@@ -2,13 +2,11 @@ define ([
'./ConditionEvaluator',
'objectUtils',
'EventEmitter',
- 'zepto',
'lodash'
], function (
ConditionEvaluator,
objectUtils,
EventEmitter,
- $,
_
) {
@@ -232,7 +230,10 @@ define ([
self.eventEmitter.emit('add', obj);
- $('.w-summary-widget').removeClass('s-status-no-data');
+ const summaryWidget = document.querySelector('.w-summary-widget');
+ if (summaryWidget) {
+ summaryWidget.classList.remove('s-status-no-data');
+ }
}
};
@@ -256,7 +257,10 @@ define ([
this.eventEmitter.emit('remove', identifier);
if (_.isEmpty(this.compositionObjs)) {
- $('.w-summary-widget').addClass('s-status-no-data');
+ const summaryWidget = document.querySelector('.w-summary-widget');
+ if (summaryWidget) {
+ summaryWidget.classList.add('s-status-no-data');
+ }
}
};
diff --git a/src/plugins/summaryWidget/src/Rule.js b/src/plugins/summaryWidget/src/Rule.js
index d9217f0e0..0b8f28804 100644
--- a/src/plugins/summaryWidget/src/Rule.js
+++ b/src/plugins/summaryWidget/src/Rule.js
@@ -4,18 +4,18 @@ define([
'./input/ColorPalette',
'./input/IconPalette',
'./eventHelpers',
+ '../../../utils/template/templateHelpers',
'EventEmitter',
- 'lodash',
- 'zepto'
+ 'lodash'
], function (
ruleTemplate,
Condition,
ColorPalette,
IconPalette,
eventHelpers,
+ templateHelpers,
EventEmitter,
- _,
- $
+ _
) {
/**
* An object representing a summary widget rule. Maintains a set of text
@@ -41,7 +41,7 @@ define([
this.widgetDnD = widgetDnD;
this.container = container;
- this.domElement = $(ruleTemplate);
+ this.domElement = templateHelpers.convertTemplateToHTML(ruleTemplate)[0];
this.eventEmitter = new EventEmitter();
this.supportedCallbacks = ['remove', 'duplicate', 'change', 'conditionChange'];
this.conditions = [];
@@ -50,31 +50,32 @@ define([
this.remove = this.remove.bind(this);
this.duplicate = this.duplicate.bind(this);
- this.thumbnail = $('.t-widget-thumb', this.domElement);
- this.thumbnailIcon = $('.js-sw__icon', this.domElement);
- this.thumbnailLabel = $('.c-sw__label', this.domElement);
- this.title = $('.rule-title', this.domElement);
- this.description = $('.rule-description', this.domElement);
- this.trigger = $('.t-trigger', this.domElement);
- this.toggleConfigButton = $('.js-disclosure', this.domElement);
- this.configArea = $('.widget-rule-content', this.domElement);
- this.grippy = $('.t-grippy', this.domElement);
- this.conditionArea = $('.t-widget-rule-config', this.domElement);
- this.jsConditionArea = $('.t-rule-js-condition-input-holder', this.domElement);
- this.deleteButton = $('.t-delete', this.domElement);
- this.duplicateButton = $('.t-duplicate', this.domElement);
- this.addConditionButton = $('.add-condition', this.domElement);
+ this.thumbnail = this.domElement.querySelector('.t-widget-thumb');
+ this.thumbnailIcon = this.domElement.querySelector('.js-sw__icon');
+ this.thumbnailLabel = this.domElement.querySelector('.c-sw__label');
+ this.title = this.domElement.querySelector('.rule-title');
+ this.description = this.domElement.querySelector('.rule-description');
+ this.trigger = this.domElement.querySelector('.t-trigger');
+ this.toggleConfigButton = this.domElement.querySelector('.js-disclosure');
+ this.configArea = this.domElement.querySelector('.widget-rule-content');
+ this.grippy = this.domElement.querySelector('.t-grippy');
+ this.conditionArea = this.domElement.querySelector('.t-widget-rule-config');
+ this.jsConditionArea = this.domElement.querySelector('.t-rule-js-condition-input-holder');
+ this.deleteButton = this.domElement.querySelector('.t-delete');
+ this.duplicateButton = this.domElement.querySelector('.t-duplicate');
+ this.addConditionButton = this.domElement.querySelector('.add-condition');
/**
* The text inputs for this rule: any input included in this object will
* have the appropriate event handlers registered to it, and it's corresponding
* field in the domain object will be updated with its value
*/
+
this.textInputs = {
- name: $('.t-rule-name-input', this.domElement),
- label: $('.t-rule-label-input', this.domElement),
- message: $('.t-rule-message-input', this.domElement),
- jsCondition: $('.t-rule-js-condition-input', this.domElement)
+ name: this.domElement.querySelector('.t-rule-name-input'),
+ label: this.domElement.querySelector('.t-rule-label-input'),
+ message: this.domElement.querySelector('.t-rule-message-input'),
+ jsCondition: this.domElement.querySelector('.t-rule-js-condition-input')
};
this.iconInput = new IconPalette('', container);
@@ -94,7 +95,7 @@ define([
function onIconInput(icon) {
self.config.icon = icon;
self.updateDomainObject('icon', icon);
- self.thumbnailIcon.removeClass().addClass(THUMB_ICON_CLASS + ' ' + icon);
+ self.thumbnailIcon.className = `${THUMB_ICON_CLASS + ' ' + icon}`;
self.eventEmitter.emit('change');
}
@@ -106,7 +107,7 @@ define([
*/
function onColorInput(color, property) {
self.config.style[property] = color;
- self.thumbnail.css(property, color);
+ self.thumbnail.style[property] = color;
self.eventEmitter.emit('change');
}
@@ -116,7 +117,10 @@ define([
* @private
*/
function encodeMsg(msg) {
- return $('<div />').text(msg).html();
+ const div = document.createElement('div');
+ div.innerText = msg;
+
+ return div.innerText;
}
/**
@@ -144,9 +148,9 @@ define([
self.config[inputKey] = text;
self.updateDomainObject();
if (inputKey === 'name') {
- self.title.html(text);
+ self.title.innerText = text;
} else if (inputKey === 'label') {
- self.thumbnailLabel.html(text);
+ self.thumbnailLabel.innerText = text;
}
self.eventEmitter.emit('change');
@@ -158,13 +162,14 @@ define([
* @private
*/
function onDragStart(event) {
- $('.t-drag-indicator').each(function () {
+ document.querySelectorAll('.t-drag-indicator').forEach(indicator => {
// eslint-disable-next-line no-invalid-this
- $(this).html($('.widget-rule-header', self.domElement).clone().get(0));
+ const ruleHeader = self.domElement.querySelectorAll('.widget-rule-header')[0].cloneNode(true);
+ indicator.innerHTML = ruleHeader;
});
- self.widgetDnD.setDragImage($('.widget-rule-header', self.domElement).clone().get(0));
+ self.widgetDnD.setDragImage(self.domElement.querySelectorAll('.widget-rule-header')[0].cloneNode(true));
self.widgetDnD.dragStart(self.config.id);
- self.domElement.hide();
+ self.domElement.style.display = 'none';
}
/**
@@ -172,20 +177,31 @@ define([
* @private
*/
function toggleConfig() {
- self.configArea.toggleClass('expanded');
- self.toggleConfigButton.toggleClass('c-disclosure-triangle--expanded');
+ if (self.configArea.classList.contains('expanded')) {
+ self.configArea.classList.remove('expanded');
+ } else {
+ self.configArea.classList.add('expanded');
+ }
+
+ if (self.toggleConfigButton.classList.contains('c-disclosure-triangle--expanded')) {
+ self.toggleConfigButton.classList.remove('c-disclosure-triangle--expanded');
+ } else {
+ self.toggleConfigButton.classList.add('c-disclosure-triangle--expanded');
+ }
+
self.config.expanded = !self.config.expanded;
}
- $('.t-rule-label-input', this.domElement).before(this.iconInput.getDOM());
+ const labelInput = this.domElement.querySelector('.t-rule-label-input');
+ labelInput.parentNode.insertBefore(this.iconInput.getDOM(), labelInput);
this.iconInput.set(self.config.icon);
this.iconInput.on('change', function (value) {
onIconInput(value);
});
// Initialize thumbs when first loading
- this.thumbnailIcon.removeClass().addClass(THUMB_ICON_CLASS + ' ' + self.config.icon);
- this.thumbnailLabel.html(self.config.label);
+ this.thumbnailIcon.className = `${THUMB_ICON_CLASS + ' ' + self.config.icon}`;
+ this.thumbnailLabel.innerText = self.config.label;
Object.keys(this.colorInputs).forEach(function (inputKey) {
const input = self.colorInputs[inputKey];
@@ -198,15 +214,17 @@ define([
self.updateDomainObject();
});
- $('.t-style-input', self.domElement).append(input.getDOM());
+ self.domElement.querySelector('.t-style-input').append(input.getDOM());
});
Object.keys(this.textInputs).forEach(function (inputKey) {
- self.textInputs[inputKey].prop('value', self.config[inputKey] || '');
- self.listenTo(self.textInputs[inputKey], 'input', function () {
- // eslint-disable-next-line no-invalid-this
- onTextInput(this, inputKey);
- });
+ if (self.textInputs[inputKey]) {
+ self.textInputs[inputKey].value = self.config[inputKey] || '';
+ self.listenTo(self.textInputs[inputKey], 'input', function () {
+ // eslint-disable-next-line no-invalid-this
+ onTextInput(this, inputKey);
+ });
+ }
});
this.listenTo(this.deleteButton, 'click', this.remove);
@@ -217,15 +235,15 @@ define([
this.listenTo(this.toggleConfigButton, 'click', toggleConfig);
this.listenTo(this.trigger, 'change', onTriggerInput);
- this.title.html(self.config.name);
- this.description.html(self.config.description);
- this.trigger.prop('value', self.config.trigger);
+ this.title.innerHTML = self.config.name;
+ this.description.innerHTML = self.config.description;
+ this.trigger.value = self.config.trigger;
this.listenTo(this.grippy, 'mousedown', onDragStart);
this.widgetDnD.on('drop', function () {
// eslint-disable-next-line no-invalid-this
this.domElement.show();
- $('.t-drag-indicator').hide();
+ document.querySelector('.t-drag-indicator').style.display = 'none';
}, this);
if (!this.conditionManager.loadCompleted()) {
@@ -233,21 +251,21 @@ define([
}
if (!this.config.expanded) {
- this.configArea.removeClass('expanded');
- this.toggleConfigButton.removeClass('c-disclosure-triangle--expanded');
+ this.configArea.classList.remove('expanded');
+ this.toggleConfigButton.classList.remove('c-disclosure-triangle--expanded');
}
if (this.domainObject.configuration.ruleOrder.length === 2) {
- $('.t-grippy', this.domElement).hide();
+ this.domElement.querySelector('.t-grippy').style.display = 'none';
}
this.refreshConditions();
//if this is the default rule, hide elements that don't apply
if (this.config.id === 'default') {
- $('.t-delete', this.domElement).hide();
- $('.t-widget-rule-config', this.domElement).hide();
- $('.t-grippy', this.domElement).hide();
+ this.domElement.querySelector('.t-delete').style.display = 'none';
+ this.domElement.querySelector('.t-widget-rule-config').style.display = 'none';
+ this.domElement.querySelector('.t-grippy').style.display = 'none';
}
}
@@ -304,8 +322,8 @@ define([
* During a rule drag event, show the placeholder element after this rule
*/
Rule.prototype.showDragIndicator = function () {
- $('.t-drag-indicator').hide();
- $('.t-drag-indicator', this.domElement).show();
+ document.querySelector('.t-drag-indicator').style.display = 'none';
+ this.domElement.querySelector('.t-drag-indicator').style.display = '';
};
/**
@@ -397,7 +415,10 @@ define([
const triggerContextStr = self.config.trigger === 'any' ? ' or ' : ' and ';
self.conditions = [];
- $('.t-condition', this.domElement).remove();
+
+ this.domElement.querySelectorAll('.t-condition').forEach(condition => {
+ condition.remove();
+ });
this.config.conditions.forEach(function (condition, index) {
const newCondition = new Condition(condition, index, self.conditionManager);
@@ -408,16 +429,23 @@ define([
});
if (this.config.trigger === 'js') {
- this.jsConditionArea.show();
- this.addConditionButton.hide();
+ if (this.jsConditionArea) {
+ this.jsConditionArea.style.display = '';
+ }
+
+ this.addConditionButton.style.display = 'none';
} else {
- this.jsConditionArea.hide();
- this.addConditionButton.show();
+ if (this.jsConditionArea) {
+ this.jsConditionArea.style.display = 'none';
+ }
+
+ this.addConditionButton.style.display = '';
self.conditions.forEach(function (condition) {
$condition = condition.getDOM();
- $('li:last-of-type', self.conditionArea).before($condition);
+ const lastOfType = self.conditionArea.querySelector('li:last-of-type');
+ lastOfType.parentNode.insertBefore($condition, lastOfType);
if (loopCnt > 0) {
- $('.t-condition-context', $condition).html(triggerContextStr + ' when');
+ $condition.querySelector('.t-condition-context').innerHTML = triggerContextStr + ' when';
}
loopCnt++;
@@ -489,7 +517,7 @@ define([
}
description = (description === '' ? this.config.description : description);
- this.description.html(description);
+ this.description.innerHTML = self.config.description;
this.config.description = description;
};
diff --git a/src/plugins/summaryWidget/src/SummaryWidget.js b/src/plugins/summaryWidget/src/SummaryWidget.js
index 1a5c1ceba..e9c1442bf 100644
--- a/src/plugins/summaryWidget/src/SummaryWidget.js
+++ b/src/plugins/summaryWidget/src/SummaryWidget.js
@@ -5,9 +5,9 @@ define([
'./TestDataManager',
'./WidgetDnD',
'./eventHelpers',
+ '../../../utils/template/templateHelpers',
'objectUtils',
'lodash',
- 'zepto',
'@braintree/sanitize-url'
], function (
widgetTemplate,
@@ -16,9 +16,9 @@ define([
TestDataManager,
WidgetDnD,
eventHelpers,
+ templateHelpers,
objectUtils,
_,
- $,
urlSanitizeLib
) {
@@ -54,20 +54,22 @@ define([
this.activeId = 'default';
this.rulesById = {};
- this.domElement = $(widgetTemplate);
- this.toggleRulesControl = $('.t-view-control-rules', this.domElement);
- this.toggleTestDataControl = $('.t-view-control-test-data', this.domElement);
- this.widgetButton = this.domElement.children('#widget');
+ this.domElement = templateHelpers.convertTemplateToHTML(widgetTemplate)[0];
+ this.toggleRulesControl = this.domElement.querySelector('.t-view-control-rules');
+ this.toggleTestDataControl = this.domElement.querySelector('.t-view-control-test-data');
+
+ this.widgetButton = this.domElement.querySelector(':scope > #widget');
+
this.editing = false;
this.container = '';
- this.editListenerUnsubscribe = $.noop;
+ this.editListenerUnsubscribe = () => {};
- this.outerWrapper = $('.widget-edit-holder', this.domElement);
- this.ruleArea = $('#ruleArea', this.domElement);
- this.configAreaRules = $('.widget-rules-wrapper', this.domElement);
+ this.outerWrapper = this.domElement.querySelector('.widget-edit-holder');
+ this.ruleArea = this.domElement.querySelector('#ruleArea');
+ this.configAreaRules = this.domElement.querySelector('.widget-rules-wrapper');
- this.testDataArea = $('.widget-test-data', this.domElement);
- this.addRuleButton = $('#addRule', this.domElement);
+ this.testDataArea = this.domElement.querySelector('.widget-test-data');
+ this.addRuleButton = this.domElement.querySelector('#addRule');
this.conditionManager = new ConditionManager(this.domainObject, this.openmct);
this.testDataManager = new TestDataManager(this.domainObject, this.conditionManager, this.openmct);
@@ -87,8 +89,17 @@ define([
* @private
*/
function toggleTestData() {
- self.outerWrapper.toggleClass('expanded-widget-test-data');
- self.toggleTestDataControl.toggleClass('c-disclosure-triangle--expanded');
+ if (self.outerWrapper.classList.contains('expanded-widget-test-data')) {
+ self.outerWrapper.classList.remove('expanded-widget-test-data');
+ } else {
+ self.outerWrapper.classList.add('expanded-widget-test-data');
+ }
+
+ if (self.toggleTestDataControl.classList.contains('c-disclosure-triangle--expanded')) {
+ self.toggleTestDataControl.classList.remove('c-disclosure-triangle--expanded');
+ } else {
+ self.toggleTestDataControl.classList.add('c-disclosure-triangle--expanded');
+ }
}
this.listenTo(this.toggleTestDataControl, 'click', toggleTestData);
@@ -98,8 +109,8 @@ define([
* @private
*/
function toggleRules() {
- self.outerWrapper.toggleClass('expanded-widget-rules');
- self.toggleRulesControl.toggleClass('c-disclosure-triangle--expanded');
+ templateHelpers.toggleClass(self.outerWrapper, 'expanded-widget-rules');
+ templateHelpers.toggleClass(self.toggleRulesControl, 'c-disclosure-triangle--expanded');
}
this.listenTo(this.toggleRulesControl, 'click', toggleRules);
@@ -113,15 +124,15 @@ define([
*/
SummaryWidget.prototype.addHyperlink = function (url, openNewTab) {
if (url) {
- this.widgetButton.attr('href', urlSanitizeLib.sanitizeUrl(url));
+ this.widgetButton.href = urlSanitizeLib.sanitizeUrl(url);
} else {
- this.widgetButton.removeAttr('href');
+ this.widgetButton.removeAttribute('href');
}
if (openNewTab === 'newTab') {
- this.widgetButton.attr('target', '_blank');
+ this.widgetButton.target = '_blank';
} else {
- this.widgetButton.removeAttr('target');
+ this.widgetButton.removeAttribute('target');
}
};
@@ -149,8 +160,8 @@ define([
SummaryWidget.prototype.show = function (container) {
const self = this;
this.container = container;
- $(container).append(this.domElement);
- $('.widget-test-data', this.domElement).append(this.testDataManager.getDOM());
+ this.container.append(this.domElement);
+ this.domElement.querySelector('.widget-test-data').append(this.testDataManager.getDOM());
this.widgetDnD = new WidgetDnD(this.domElement, this.domainObject.configuration.ruleOrder, this.rulesById);
this.initRule('default', 'Default');
this.domainObject.configuration.ruleOrder.forEach(function (ruleId) {
@@ -190,7 +201,7 @@ define([
const self = this;
const ruleOrder = self.domainObject.configuration.ruleOrder;
const rules = self.rulesById;
- self.ruleArea.html('');
+ self.ruleArea.innerHTML = '';
Object.values(ruleOrder).forEach(function (ruleId) {
self.ruleArea.append(rules[ruleId].getDOM());
});
@@ -205,9 +216,9 @@ define([
rules.forEach(function (ruleKey, index, array) {
if (array.length > 2 && index > 0) {
- $('.t-grippy', rulesById[ruleKey].domElement).show();
+ rulesById[ruleKey].domElement.querySelector('.t-grippy').style.display = '';
} else {
- $('.t-grippy', rulesById[ruleKey].domElement).hide();
+ rulesById[ruleKey].domElement.querySelector('.t-grippy').style.display = 'none';
}
});
};
@@ -218,10 +229,10 @@ define([
SummaryWidget.prototype.updateWidget = function () {
const WIDGET_ICON_CLASS = 'c-sw__icon js-sw__icon';
const activeRule = this.rulesById[this.activeId];
- this.applyStyle($('#widget', this.domElement), activeRule.getProperty('style'));
- $('#widget', this.domElement).prop('title', activeRule.getProperty('message'));
- $('#widgetLabel', this.domElement).html(activeRule.getProperty('label'));
- $('#widgetIcon', this.domElement).removeClass().addClass(WIDGET_ICON_CLASS + ' ' + activeRule.getProperty('icon'));
+ this.applyStyle(this.domElement.querySelector('#widget'), activeRule.getProperty('style'));
+ this.domElement.querySelector('#widget').title = activeRule.getProperty('message');
+ this.domElement.querySelector('#widgetLabel').innerHTML = activeRule.getProperty('label');
+ this.domElement.querySelector('#widgetIcon').classList = WIDGET_ICON_CLASS + ' ' + activeRule.getProperty('icon');
};
/**
@@ -356,7 +367,7 @@ define([
*/
SummaryWidget.prototype.applyStyle = function (elem, style) {
Object.keys(style).forEach(function (propId) {
- elem.css(propId, style[propId]);
+ elem.style[propId] = style[propId];
});
};
diff --git a/src/plugins/summaryWidget/src/TestDataItem.js b/src/plugins/summaryWidget/src/TestDataItem.js
index 32b737a90..ae005c46d 100644
--- a/src/plugins/summaryWidget/src/TestDataItem.js
+++ b/src/plugins/summaryWidget/src/TestDataItem.js
@@ -3,15 +3,15 @@ define([
'./input/ObjectSelect',
'./input/KeySelect',
'./eventHelpers',
- 'EventEmitter',
- 'zepto'
+ '../../../utils/template/templateHelpers',
+ 'EventEmitter'
], function (
itemTemplate,
ObjectSelect,
KeySelect,
eventHelpers,
- EventEmitter,
- $
+ templateHelpers,
+ EventEmitter
) {
/**
@@ -31,12 +31,12 @@ define([
this.index = index;
this.conditionManager = conditionManager;
- this.domElement = $(itemTemplate);
+ this.domElement = templateHelpers.convertTemplateToHTML(itemTemplate)[0];
this.eventEmitter = new EventEmitter();
this.supportedCallbacks = ['remove', 'duplicate', 'change'];
- this.deleteButton = $('.t-delete', this.domElement);
- this.duplicateButton = $('.t-duplicate', this.domElement);
+ this.deleteButton = this.domElement.querySelector('.t-delete');
+ this.duplicateButton = this.domElement.querySelector('.t-duplicate');
this.selects = {};
this.valueInputs = [];
@@ -101,7 +101,7 @@ define([
});
Object.values(this.selects).forEach(function (select) {
- $('.t-configuration', self.domElement).append(select.getDOM());
+ self.domElement.querySelector('.t-configuration').append(select.getDOM());
});
this.listenTo(this.domElement, 'input', onValueInput);
}
@@ -139,7 +139,7 @@ define([
* Hide the appropriate inputs when this is the only item
*/
TestDataItem.prototype.hideButtons = function () {
- this.deleteButton.hide();
+ this.deleteButton.style.display = 'none';
};
/**
@@ -177,17 +177,21 @@ define([
*/
TestDataItem.prototype.generateValueInput = function (key) {
const evaluator = this.conditionManager.getEvaluator();
- const inputArea = $('.t-value-inputs', this.domElement);
+ const inputArea = this.domElement.querySelector('.t-value-inputs');
const dataType = this.conditionManager.getTelemetryPropertyType(this.config.object, key);
const inputType = evaluator.getInputTypeById(dataType);
- inputArea.html('');
+ inputArea.innerHTML = '';
if (inputType) {
if (!this.config.value) {
this.config.value = (inputType === 'number' ? 0 : '');
}
- this.valueInput = $('<input class="sm" type = "' + inputType + '" value = "' + this.config.value + '"> </input>').get(0);
+ const newInput = document.createElement("input");
+ newInput.type = `${inputType}`;
+ newInput.value = `${this.config.value}`;
+
+ this.valueInput = newInput;
inputArea.append(this.valueInput);
}
};
diff --git a/src/plugins/summaryWidget/src/TestDataManager.js b/src/plugins/summaryWidget/src/TestDataManager.js
index 819cc5ee3..70240453d 100644
--- a/src/plugins/summaryWidget/src/TestDataManager.js
+++ b/src/plugins/summaryWidget/src/TestDataManager.js
@@ -2,13 +2,13 @@ define([
'./eventHelpers',
'../res/testDataTemplate.html',
'./TestDataItem',
- 'zepto',
+ '../../../utils/template/templateHelpers',
'lodash'
], function (
eventHelpers,
testDataTemplate,
TestDataItem,
- $,
+ templateHelpers,
_
) {
@@ -28,13 +28,13 @@ define([
this.openmct = openmct;
this.evaluator = this.manager.getEvaluator();
- this.domElement = $(testDataTemplate);
+ this.domElement = templateHelpers.convertTemplateToHTML(testDataTemplate)[0];
this.config = this.domainObject.configuration.testDataConfig;
this.testCache = {};
- this.itemArea = $('.t-test-data-config', this.domElement);
- this.addItemButton = $('.add-test-condition', this.domElement);
- this.testDataInput = $('.t-test-data-checkbox', this.domElement);
+ this.itemArea = this.domElement.querySelector('.t-test-data-config');
+ this.addItemButton = this.domElement.querySelector('.add-test-condition');
+ this.testDataInput = this.domElement.querySelector('.t-test-data-checkbox');
/**
* Toggles whether the associated {ConditionEvaluator} uses the actual
@@ -139,7 +139,10 @@ define([
}
self.items = [];
- $('.t-test-data-item', this.domElement).remove();
+
+ this.domElement.querySelectorAll('.t-test-data-item').forEach(item => {
+ item.remove();
+ });
this.config.forEach(function (item, index) {
const newItem = new TestDataItem(item, index, self.manager);
@@ -150,7 +153,6 @@ define([
});
self.items.forEach(function (item) {
- // $('li:last-of-type', self.itemArea).before(item.getDOM());
self.itemArea.prepend(item.getDOM());
});
diff --git a/src/plugins/summaryWidget/src/WidgetDnD.js b/src/plugins/summaryWidget/src/WidgetDnD.js
index e9ee2f040..90cd3b697 100644
--- a/src/plugins/summaryWidget/src/WidgetDnD.js
+++ b/src/plugins/summaryWidget/src/WidgetDnD.js
@@ -1,11 +1,11 @@
define([
'../res/ruleImageTemplate.html',
'EventEmitter',
- 'zepto'
+ '../../../utils/template/templateHelpers'
], function (
ruleImageTemplate,
EventEmitter,
- $
+ templateHelpers
) {
/**
@@ -19,8 +19,8 @@ define([
this.ruleOrder = ruleOrder;
this.rulesById = rulesById;
- this.imageContainer = $(ruleImageTemplate);
- this.image = $('.t-drag-rule-image', this.imageContainer);
+ this.imageContainer = templateHelpers.convertTemplateToHTML(ruleImageTemplate)[0];
+ this.image = this.imageContainer.querySelector('.t-drag-rule-image');
this.draggingId = '';
this.draggingRulePrevious = '';
this.eventEmitter = new EventEmitter();
@@ -29,18 +29,18 @@ define([
this.drag = this.drag.bind(this);
this.drop = this.drop.bind(this);
- $(this.container).on('mousemove', this.drag);
- $(document).on('mouseup', this.drop);
- $(this.container).before(this.imageContainer);
- $(this.imageContainer).hide();
+ this.container.addEventListener('mousemove', this.drag);
+ document.addEventListener('mouseup', this.drop);
+ this.container.parentNode.insertBefore(this.imageContainer, this.container);
+ this.imageContainer.style.display = 'none';
}
/**
* Remove event listeners registered to elements external to the widget
*/
WidgetDnD.prototype.destroy = function () {
- $(this.container).off('mousemove', this.drag);
- $(document).off('mouseup', this.drop);
+ this.container.removeEventListener('mousemove', this.drag);
+ document.removeEventListener('mouseup', this.drop);
};
/**
@@ -81,7 +81,8 @@ define([
let target = '';
ruleOrder.forEach(function (ruleId, index) {
- offset = rulesById[ruleId].getDOM().offset();
+ const ruleDOM = rulesById[ruleId].getDOM();
+ offset = window.innerWidth - (ruleDOM.offsetLeft + ruleDOM.offsetWidth);
y = offset.top;
height = offset.height;
if (index === 0) {
@@ -114,7 +115,7 @@ define([
this.imageContainer.show();
this.imageContainer.offset({
top: event.pageY - this.image.height() / 2,
- left: event.pageX - $('.t-grippy', this.image).width()
+ left: event.pageX - this.image.querySelector('.t-grippy').style.width
});
};
@@ -129,7 +130,7 @@ define([
dragTarget = this.getDropLocation(event);
this.imageContainer.offset({
top: event.pageY - this.image.height() / 2,
- left: event.pageX - $('.t-grippy', this.image).width()
+ left: event.pageX - this.image.querySelector('.t-grippy').style.width
});
if (this.rulesById[dragTarget]) {
this.rulesById[dragTarget].showDragIndicator();
diff --git a/src/plugins/summaryWidget/src/input/ColorPalette.js b/src/plugins/summaryWidget/src/input/ColorPalette.js
index 0bbe23641..2319f9830 100644
--- a/src/plugins/summaryWidget/src/input/ColorPalette.js
+++ b/src/plugins/summaryWidget/src/input/ColorPalette.js
@@ -1,10 +1,8 @@
define([
- './Palette',
- 'zepto'
+ './Palette'
],
function (
- Palette,
- $
+ Palette
) {
//The colors that will be used to instantiate this palette if none are provided
@@ -33,17 +31,16 @@ function (
this.palette.setNullOption('rgba(0,0,0,0)');
- const domElement = $(this.palette.getDOM());
+ const domElement = this.palette.getDOM();
const self = this;
- $('.c-button--menu', domElement).addClass('c-button--swatched');
- $('.t-swatch', domElement).addClass('color-swatch');
- $('.c-palette', domElement).addClass('c-palette--color');
+ domElement.querySelector('.c-button--menu').classList.add('c-button--swatched');
+ domElement.querySelector('.t-swatch').classList.add('color-swatch');
+ domElement.querySelector('.c-palette').classList.add('c-palette--color');
- $('.c-palette__item', domElement).each(function () {
+ domElement.querySelectorAll('.c-palette__item').forEach(item => {
// eslint-disable-next-line no-invalid-this
- const elem = this;
- $(elem).css('background-color', elem.dataset.item);
+ item.style.backgroundColor = item.dataset.item;
});
/**
@@ -53,7 +50,7 @@ function (
*/
function updateSwatch() {
const color = self.palette.getCurrent();
- $('.color-swatch', domElement).css('background-color', color);
+ domElement.querySelector('.color-swatch').style.backgroundColor = color;
}
this.palette.on('change', updateSwatch);
diff --git a/src/plugins/summaryWidget/src/input/IconPalette.js b/src/plugins/summaryWidget/src/input/IconPalette.js
index cdc011d5d..557cc4d95 100644
--- a/src/plugins/summaryWidget/src/input/IconPalette.js
+++ b/src/plugins/summaryWidget/src/input/IconPalette.js
@@ -1,9 +1,7 @@
define([
- './Palette',
- 'zepto'
+ './Palette'
], function (
- Palette,
- $
+ Palette
) {
//The icons that will be used to instantiate this palette if none are provided
const DEFAULT_ICONS = [
@@ -45,20 +43,19 @@ define([
this.icons = icons || DEFAULT_ICONS;
this.palette = new Palette(cssClass, container, this.icons);
- this.palette.setNullOption(' ');
- this.oldIcon = this.palette.current || ' ';
+ this.palette.setNullOption('');
+ this.oldIcon = this.palette.current || '';
- const domElement = $(this.palette.getDOM());
+ const domElement = this.palette.getDOM();
const self = this;
- $('.c-button--menu', domElement).addClass('c-button--swatched');
- $('.t-swatch', domElement).addClass('icon-swatch');
- $('.c-palette', domElement).addClass('c-palette--icon');
+ domElement.querySelector('.c-button--menu').classList.add('c-button--swatched');
+ domElement.querySelector('.t-swatch').classList.add('icon-swatch');
+ domElement.querySelector('.c-palette').classList.add('c-palette--icon');
- $('.c-palette-item', domElement).each(function () {
+ domElement.querySelectorAll('.c-palette-item').forEach(item => {
// eslint-disable-next-line no-invalid-this
- const elem = this;
- $(elem).addClass(elem.dataset.item);
+ item.classList.add(item.dataset.item);
});
/**
@@ -67,8 +64,11 @@ define([
* @private
*/
function updateSwatch() {
- $('.icon-swatch', domElement).removeClass(self.oldIcon)
- .addClass(self.palette.getCurrent());
+ if (self.oldIcon) {
+ domElement.querySelector('.icon-swatch').classList.remove(self.oldIcon);
+ }
+
+ domElement.querySelector('.icon-swatch').classList.add(self.palette.getCurrent());
self.oldIcon = self.palette.getCurrent();
}
diff --git a/src/plugins/summaryWidget/src/input/Palette.js b/src/plugins/summaryWidget/src/input/Palette.js
index ff1d3b550..96df813de 100644
--- a/src/plugins/summaryWidget/src/input/Palette.js
+++ b/src/plugins/summaryWidget/src/input/Palette.js
@@ -1,13 +1,13 @@
define([
'../eventHelpers',
'../../res/input/paletteTemplate.html',
- 'EventEmitter',
- 'zepto'
+ '../../../../utils/template/templateHelpers',
+ 'EventEmitter'
], function (
eventHelpers,
paletteTemplate,
- EventEmitter,
- $
+ templateHelpers,
+ EventEmitter
) {
/**
* Instantiates a new Open MCT Color Palette input
@@ -28,36 +28,41 @@ define([
this.items = items;
this.container = container;
- this.domElement = $(paletteTemplate);
+ this.domElement = templateHelpers.convertTemplateToHTML(paletteTemplate)[0];
+
this.itemElements = {
- nullOption: $('.c-palette__item-none .c-palette__item', this.domElement)
+ nullOption: this.domElement.querySelector('.c-palette__item-none .c-palette__item')
};
this.eventEmitter = new EventEmitter();
this.supportedCallbacks = ['change'];
this.value = this.items[0];
this.nullOption = ' ';
- this.button = $('.js-button', this.domElement);
- this.menu = $('.c-menu', this.domElement);
+ this.button = this.domElement.querySelector('.js-button');
+ this.menu = this.domElement.querySelector('.c-menu');
this.hideMenu = this.hideMenu.bind(this);
- self.button.addClass(this.cssClass);
+ if (this.cssClass) {
+ self.button.classList.add(this.cssClass);
+ }
+
self.setNullOption(this.nullOption);
self.items.forEach(function (item) {
- const itemElement = $('<div class = "c-palette__item ' + item + '"'
- + ' data-item = ' + item + '></div>');
- $('.c-palette__items', self.domElement).append(itemElement);
- self.itemElements[item] = itemElement;
+ const itemElement = `<div class = "c-palette__item ${item}" data-item = "${item}"></div>`;
+ const temp = document.createElement('div');
+ temp.innerHTML = itemElement;
+ self.itemElements[item] = temp.firstChild;
+ self.domElement.querySelector('.c-palette__items').appendChild(temp.firstChild);
});
- $('.c-menu', self.domElement).hide();
+ self.domElement.querySelector('.c-menu').style.display = 'none';
- this.listenTo($(document), 'click', this.hideMenu);
- this.listenTo($('.js-button', self.domElement), 'click', function (event) {
+ this.listenTo(window.document, 'click', this.hideMenu);
+ this.listenTo(self.domElement.querySelector('.js-button'), 'click', function (event) {
event.stopPropagation();
- $('.c-menu', self.container).hide();
- $('.c-menu', self.domElement).show();
+ self.container.querySelector('.c-menu').style.display = 'none';
+ self.domElement.querySelector('.c-menu').style.display = '';
});
/**
@@ -70,10 +75,12 @@ define([
const elem = event.currentTarget;
const item = elem.dataset.item;
self.set(item);
- $('.c-menu', self.domElement).hide();
+ self.domElement.querySelector('.c-menu').style.display = 'none';
}
- this.listenTo($('.c-palette__item', self.domElement), 'click', handleItemClick);
+ self.domElement.querySelectorAll('.c-palette__item').forEach(item => {
+ this.listenTo(item, 'click', handleItemClick);
+ });
}
/**
@@ -91,7 +98,7 @@ define([
};
Palette.prototype.hideMenu = function () {
- $('.c-menu', this.domElement).hide();
+ this.domElement.querySelector('.c-menu').style.display = 'none';
};
/**
@@ -141,12 +148,16 @@ define([
* Update the view assoicated with the currently selected item
*/
Palette.prototype.updateSelected = function (item) {
- $('.c-palette__item', this.domElement).removeClass('is-selected');
- this.itemElements[item].addClass('is-selected');
+ this.domElement.querySelectorAll('.c-palette__item').forEach(paletteItem => {
+ if (paletteItem.classList.contains('is-selected')) {
+ paletteItem.classList.remove('is-selected');
+ }
+ });
+ this.itemElements[item].classList.add('is-selected');
if (item === 'nullOption') {
- $('.t-swatch', this.domElement).addClass('no-selection');
+ this.domElement.querySelector('.t-swatch').classList.add('no-selection');
} else {
- $('.t-swatch', this.domElement).removeClass('no-selection');
+ this.domElement.querySelector('.t-swatch').classList.remove('no-selection');
}
};
@@ -157,14 +168,20 @@ define([
*/
Palette.prototype.setNullOption = function (item) {
this.nullOption = item;
- this.itemElements.nullOption.data('item', item);
+ this.itemElements.nullOption.data = { item: item };
};
/**
* Hides the 'no selection' option to be hidden in the view if it doesn't apply
*/
Palette.prototype.toggleNullOption = function () {
- $('.c-palette__item-none', this.domElement).toggle();
+ const elem = this.domElement.querySelector('.c-palette__item-none');
+
+ if (elem.style.display === 'none') {
+ this.domElement.querySelector('.c-palette__item-none').style.display = 'flex';
+ } else {
+ this.domElement.querySelector('.c-palette__item-none').style.display = 'none';
+ }
};
return Palette;
diff --git a/src/plugins/summaryWidget/src/input/Select.js b/src/plugins/summaryWidget/src/input/Select.js
index 3f89034ca..676a9791b 100644
--- a/src/plugins/summaryWidget/src/input/Select.js
+++ b/src/plugins/summaryWidget/src/input/Select.js
@@ -1,13 +1,13 @@
define([
'../eventHelpers',
'../../res/input/selectTemplate.html',
- 'EventEmitter',
- 'zepto'
+ '../../../../utils/template/templateHelpers',
+ 'EventEmitter'
], function (
eventHelpers,
selectTemplate,
- EventEmitter,
- $
+ templateHelpers,
+ EventEmitter
) {
/**
@@ -20,7 +20,8 @@ define([
const self = this;
- this.domElement = $(selectTemplate);
+ this.domElement = templateHelpers.convertTemplateToHTML(selectTemplate)[0];
+
this.options = [];
this.eventEmitter = new EventEmitter();
this.supportedCallbacks = ['change'];
@@ -35,12 +36,12 @@ define([
*/
function onChange(event) {
const elem = event.target;
- const value = self.options[$(elem).prop('selectedIndex')];
+ const value = self.options[elem.selectedIndex];
self.eventEmitter.emit('change', value[0]);
}
- this.listenTo($('select', this.domElement), 'change', onChange, this);
+ this.listenTo(this.domElement.querySelector('select'), 'change', onChange, this);
}
/**
@@ -74,16 +75,19 @@ define([
const self = this;
let selectedIndex = 0;
- selectedIndex = $('select', this.domElement).prop('selectedIndex');
- $('option', this.domElement).remove();
+ selectedIndex = this.domElement.querySelector('select').selectedIndex;
+
+ this.domElement.querySelector('select').innerHTML = '';
+
+ self.options.forEach(function (option) {
+ const optionElement = document.createElement('option');
+ optionElement.value = option[0];
+ optionElement.innerText = `+ ${option[1]}`;
- self.options.forEach(function (option, index) {
- $('select', self.domElement)
- .append('<option value = "' + option[0] + '" >'
- + option[1] + '</option>');
+ self.domElement.querySelector('select').appendChild(optionElement);
});
- $('select', this.domElement).prop('selectedIndex', selectedIndex);
+ this.domElement.querySelector('select').selectedIndex = selectedIndex;
};
/**
@@ -120,7 +124,7 @@ define([
selectedIndex = index;
}
});
- $('select', this.domElement).prop('selectedIndex', selectedIndex);
+ this.domElement.querySelector('select').selectedIndex = selectedIndex;
selectedOption = this.options[selectedIndex];
this.eventEmitter.emit('change', selectedOption[0]);
@@ -131,17 +135,21 @@ define([
* @return {string}
*/
Select.prototype.getSelected = function () {
- return $('select', this.domElement).prop('value');
+ return this.domElement.querySelector('select').value;
};
Select.prototype.hide = function () {
- $(this.domElement).addClass('hidden');
- $('.equal-to').addClass('hidden');
+ this.domElement.classList.add('hidden');
+ if (this.domElement.querySelector('.equal-to')) {
+ this.domElement.querySelector('.equal-to').classList.add('hidden');
+ }
};
Select.prototype.show = function () {
- $(this.domElement).removeClass('hidden');
- $('.equal-to').removeClass('hidden');
+ this.domElement.classList.remove('hidden');
+ if (this.domElement.querySelector('.equal-to')) {
+ this.domElement.querySelector('.equal-to').classList.remove('hidden');
+ }
};
Select.prototype.destroy = function () {
diff --git a/src/plugins/summaryWidget/test/ConditionSpec.js b/src/plugins/summaryWidget/test/ConditionSpec.js
index a69742065..8b166bf87 100644
--- a/src/plugins/summaryWidget/test/ConditionSpec.js
+++ b/src/plugins/summaryWidget/test/ConditionSpec.js
@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
-define(['../src/Condition', 'zepto'], function (Condition, $) {
+define(['../src/Condition'], function (Condition) {
xdescribe('A summary widget condition', function () {
let testCondition;
let mockConfig;
@@ -33,7 +33,7 @@ define(['../src/Condition', 'zepto'], function (Condition, $) {
let generateValuesSpy;
beforeEach(function () {
- mockContainer = $(document.createElement('div'));
+ mockContainer = document.createElement('div');
mockConfig = {
object: 'object1',
@@ -78,7 +78,7 @@ define(['../src/Condition', 'zepto'], function (Condition, $) {
it('exposes a DOM element to represent itself in the view', function () {
mockContainer.append(testCondition.getDOM());
- expect($('.t-condition', mockContainer).get().length).toEqual(1);
+ expect(mockContainer.querySelectorAll('.t-condition').length).toEqual(1);
});
it('responds to a change in its object select', function () {
@@ -111,41 +111,59 @@ define(['../src/Condition', 'zepto'], function (Condition, $) {
});
it('generates value inputs of the appropriate type and quantity', function () {
+ let inputs;
+
mockContainer.append(testCondition.getDOM());
mockEvaluator.getInputType.and.returnValue('number');
mockEvaluator.getInputCount.and.returnValue(3);
testCondition.generateValueInputs('');
- expect($('input', mockContainer).filter('[type=number]').get().length).toEqual(3);
- expect($('input', mockContainer).eq(0).prop('valueAsNumber')).toEqual(1);
- expect($('input', mockContainer).eq(1).prop('valueAsNumber')).toEqual(2);
- expect($('input', mockContainer).eq(2).prop('valueAsNumber')).toEqual(3);
+
+ inputs = mockContainer.querySelectorAll('input');
+ const numberInputs = Array.from(inputs).filter(input => input.type === 'number');
+
+ expect(numberInputs.length).toEqual(3);
+ expect(numberInputs[0].valueAsNumber).toEqual(1);
+ expect(numberInputs[1].valueAsNumber).toEqual(2);
+ expect(numberInputs[2].valueAsNumber).toEqual(3);
mockEvaluator.getInputType.and.returnValue('text');
mockEvaluator.getInputCount.and.returnValue(2);
testCondition.config.values = ['Text I Am', 'Text It Is'];
testCondition.generateValueInputs('');
- expect($('input', mockContainer).filter('[type=text]').get().length).toEqual(2);
- expect($('input', mockContainer).eq(0).prop('value')).toEqual('Text I Am');
- expect($('input', mockContainer).eq(1).prop('value')).toEqual('Text It Is');
+
+ inputs = mockContainer.querySelectorAll('input');
+ const textInputs = Array.from(inputs).filter(input => input.type === 'text');
+
+ expect(textInputs.length).toEqual(2);
+ expect(textInputs[0].value).toEqual('Text I Am');
+ expect(textInputs[1].value).toEqual('Text It Is');
});
it('ensures reasonable defaults on values if none are provided', function () {
+ let inputs;
+
mockContainer.append(testCondition.getDOM());
mockEvaluator.getInputType.and.returnValue('number');
mockEvaluator.getInputCount.and.returnValue(3);
testCondition.config.values = [];
testCondition.generateValueInputs('');
- expect($('input', mockContainer).eq(0).prop('valueAsNumber')).toEqual(0);
- expect($('input', mockContainer).eq(1).prop('valueAsNumber')).toEqual(0);
- expect($('input', mockContainer).eq(2).prop('valueAsNumber')).toEqual(0);
+
+ inputs = Array.from(mockContainer.querySelectorAll('input'));
+
+ expect(inputs[0].valueAsNumber).toEqual(0);
+ expect(inputs[1].valueAsNumber).toEqual(0);
+ expect(inputs[2].valueAsNumber).toEqual(0);
expect(testCondition.config.values).toEqual([0, 0, 0]);
mockEvaluator.getInputType.and.returnValue('text');
mockEvaluator.getInputCount.and.returnValue(2);
testCondition.config.values = [];
testCondition.generateValueInputs('');
- expect($('input', mockContainer).eq(0).prop('value')).toEqual('');
- expect($('input', mockContainer).eq(1).prop('value')).toEqual('');
+
+ inputs = Array.from(mockContainer.querySelectorAll('input'));
+
+ expect(inputs[0].value).toEqual('');
+ expect(inputs[1].value).toEqual('');
expect(testCondition.config.values).toEqual(['', '']);
});
@@ -154,8 +172,16 @@ define(['../src/Condition', 'zepto'], function (Condition, $) {
mockEvaluator.getInputType.and.returnValue('number');
mockEvaluator.getInputCount.and.returnValue(3);
testCondition.generateValueInputs('');
- $('input', mockContainer).eq(1).prop('value', 9001);
- $('input', mockContainer).eq(1).trigger('input');
+
+ const event = new Event('input', {
+ bubbles: true,
+ cancelable: true
+ });
+ const inputs = mockContainer.querySelectorAll('input');
+
+ inputs[1].value = 9001;
+ inputs[1].dispatchEvent(event);
+
expect(changeSpy).toHaveBeenCalledWith({
value: 9001,
property: 'values[1]',
diff --git a/src/plugins/summaryWidget/test/RuleSpec.js b/src/plugins/summaryWidget/test/RuleSpec.js
index 4d6c5b714..df5108f7a 100644
--- a/src/plugins/summaryWidget/test/RuleSpec.js
+++ b/src/plugins/summaryWidget/test/RuleSpec.js
@@ -1,4 +1,4 @@
-define(['../src/Rule', 'zepto'], function (Rule, $) {
+define(['../src/Rule'], function (Rule) {
describe('A Summary Widget Rule', function () {
let mockRuleConfig;
let mockDomainObject;
@@ -78,7 +78,7 @@ define(['../src/Rule', 'zepto'], function (Rule, $) {
'dragStart'
]);
- mockContainer = $(document.createElement('div'));
+ mockContainer = document.createElement('div');
removeSpy = jasmine.createSpy('removeCallback');
duplicateSpy = jasmine.createSpy('duplicateCallback');
@@ -99,7 +99,7 @@ define(['../src/Rule', 'zepto'], function (Rule, $) {
it('gets its DOM element', function () {
mockContainer.append(testRule.getDOM());
- expect($('.l-widget-rule', mockContainer).get().length).toBeGreaterThan(0);
+ expect(mockContainer.querySelectorAll('.l-widget-rule').length).toBeGreaterThan(0);
});
it('gets its configuration properties', function () {
@@ -185,7 +185,7 @@ define(['../src/Rule', 'zepto'], function (Rule, $) {
it('builds condition view from condition configuration', function () {
mockContainer.append(testRule.getDOM());
- expect($('.t-condition', mockContainer).get().length).toEqual(2);
+ expect(mockContainer.querySelectorAll('.t-condition').length).toEqual(2);
});
it('responds to input of style properties, and updates the preview', function () {
@@ -196,9 +196,9 @@ define(['../src/Rule', 'zepto'], function (Rule, $) {
testRule.colorInputs.color.set('#999999');
expect(mockRuleConfig.style.color).toEqual('#999999');
- expect(testRule.thumbnail.css('background-color')).toEqual('rgb(67, 67, 67)');
- expect(testRule.thumbnail.css('border-color')).toEqual('rgb(102, 102, 102)');
- expect(testRule.thumbnail.css('color')).toEqual('rgb(153, 153, 153)');
+ expect(testRule.thumbnail.style['background-color']).toEqual('rgb(67, 67, 67)');
+ expect(testRule.thumbnail.style['border-color']).toEqual('rgb(102, 102, 102)');
+ expect(testRule.thumbnail.style.color).toEqual('rgb(153, 153, 153)');
expect(changeSpy).toHaveBeenCalled();
});
@@ -228,8 +228,12 @@ define(['../src/Rule', 'zepto'], function (Rule, $) {
// });
it('allows input for when the rule triggers', function () {
- testRule.trigger.prop('value', 'all');
- testRule.trigger.trigger('change');
+ testRule.trigger.value = 'all';
+ const event = new Event('change', {
+ bubbles: true,
+ cancelable: true
+ });
+ testRule.trigger.dispatchEvent(event);
expect(testRule.config.trigger).toEqual('all');
expect(conditionChangeSpy).toHaveBeenCalled();
});
@@ -247,7 +251,12 @@ define(['../src/Rule', 'zepto'], function (Rule, $) {
});
it('initiates a drag event when its grippy is clicked', function () {
- testRule.grippy.trigger('mousedown');
+ const event = new Event('mousedown', {
+ bubbles: true,
+ cancelable: true
+ });
+ testRule.grippy.dispatchEvent(event);
+
expect(mockWidgetDnD.setDragImage).toHaveBeenCalled();
expect(mockWidgetDnD.dragStart).toHaveBeenCalledWith('mockRule');
});
diff --git a/src/plugins/summaryWidget/test/SummaryWidgetSpec.js b/src/plugins/summaryWidget/test/SummaryWidgetSpec.js
index 1bee993e2..877b1b366 100644
--- a/src/plugins/summaryWidget/test/SummaryWidgetSpec.js
+++ b/src/plugins/summaryWidget/test/SummaryWidgetSpec.js
@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
-define(['../src/SummaryWidget', 'zepto'], function (SummaryWidget, $) {
+define(['../src/SummaryWidget'], function (SummaryWidget) {
xdescribe('The Summary Widget', function () {
let summaryWidget;
let mockDomainObject;
@@ -111,7 +111,7 @@ define(['../src/SummaryWidget', 'zepto'], function (SummaryWidget, $) {
});
it('builds rules and rule placeholders in view from configuration', function () {
- expect($('.l-widget-rule', summaryWidget.ruleArea).get().length).toEqual(2);
+ expect(summaryWidget.ruleArea.querySelectorAll('.l-widget-rule').length).toEqual(2);
});
it('allows initializing a new rule with a particular identifier', function () {
@@ -130,7 +130,7 @@ define(['../src/SummaryWidget', 'zepto'], function (SummaryWidget, $) {
mockDomainObject.configuration.ruleOrder.forEach(function (ruleId) {
expect(mockDomainObject.configuration.ruleConfigById[ruleId]).toBeDefined();
});
- expect($('.l-widget-rule', summaryWidget.ruleArea).get().length).toEqual(6);
+ expect(summaryWidget.ruleArea.querySelectorAll('.l-widget-rule').length).toEqual(6);
});
it('allows duplicating a rule from source configuration', function () {
@@ -186,10 +186,10 @@ define(['../src/SummaryWidget', 'zepto'], function (SummaryWidget, $) {
it('adds hyperlink to the widget button and sets newTab preference', function () {
summaryWidget.addHyperlink('https://www.nasa.gov', 'newTab');
- const widgetButton = $('#widget', mockContainer);
+ const widgetButton = mockContainer.querySelector('#widget');
- expect(widgetButton.attr('href')).toEqual('https://www.nasa.gov');
- expect(widgetButton.attr('target')).toEqual('_blank');
+ expect(widgetButton.href).toEqual('https://www.nasa.gov/');
+ expect(widgetButton.target).toEqual('_blank');
});
});
});
diff --git a/src/plugins/summaryWidget/test/TestDataItemSpec.js b/src/plugins/summaryWidget/test/TestDataItemSpec.js
index dffa6c6f2..171753efe 100644
--- a/src/plugins/summaryWidget/test/TestDataItemSpec.js
+++ b/src/plugins/summaryWidget/test/TestDataItemSpec.js
@@ -1,4 +1,4 @@
-define(['../src/TestDataItem', 'zepto'], function (TestDataItem, $) {
+define(['../src/TestDataItem'], function (TestDataItem) {
describe('A summary widget test data item', function () {
let testDataItem;
let mockConfig;
@@ -11,7 +11,7 @@ define(['../src/TestDataItem', 'zepto'], function (TestDataItem, $) {
let generateValueSpy;
beforeEach(function () {
- mockContainer = $(document.createElement('div'));
+ mockContainer = document.createElement('div');
mockConfig = {
object: 'object1',
@@ -56,7 +56,7 @@ define(['../src/TestDataItem', 'zepto'], function (TestDataItem, $) {
it('exposes a DOM element to represent itself in the view', function () {
mockContainer.append(testDataItem.getDOM());
- expect($('.t-test-data-item', mockContainer).get().length).toEqual(1);
+ expect(mockContainer.querySelectorAll('.t-test-data-item').length).toEqual(1);
});
it('responds to a change in its object select', function () {
@@ -80,34 +80,54 @@ define(['../src/TestDataItem', 'zepto'], function (TestDataItem, $) {
});
it('generates a value input of the appropriate type', function () {
+ let inputs;
+
mockContainer.append(testDataItem.getDOM());
mockEvaluator.getInputTypeById.and.returnValue('number');
testDataItem.generateValueInput('');
- expect($('input', mockContainer).filter('[type=number]').get().length).toEqual(1);
- expect($('input', mockContainer).prop('valueAsNumber')).toEqual(1);
+
+ inputs = mockContainer.querySelectorAll('input');
+ const numberInputs = Array.from(inputs).filter(input => input.type === 'number');
+
+ expect(numberInputs.length).toEqual(1);
+ expect(inputs[0].valueAsNumber).toEqual(1);
mockEvaluator.getInputTypeById.and.returnValue('text');
testDataItem.config.value = 'Text I Am';
testDataItem.generateValueInput('');
- expect($('input', mockContainer).filter('[type=text]').get().length).toEqual(1);
- expect($('input', mockContainer).prop('value')).toEqual('Text I Am');
+
+ inputs = mockContainer.querySelectorAll('input');
+ const textInputs = Array.from(inputs).filter(input => input.type === 'text');
+
+ expect(textInputs.length).toEqual(1);
+ expect(inputs[0].value).toEqual('Text I Am');
});
it('ensures reasonable defaults on values if none are provided', function () {
+ let inputs;
+
mockContainer.append(testDataItem.getDOM());
mockEvaluator.getInputTypeById.and.returnValue('number');
testDataItem.config.value = undefined;
testDataItem.generateValueInput('');
- expect($('input', mockContainer).filter('[type=number]').get().length).toEqual(1);
- expect($('input', mockContainer).prop('valueAsNumber')).toEqual(0);
+
+ inputs = mockContainer.querySelectorAll('input');
+ const numberInputs = Array.from(inputs).filter(input => input.type === 'number');
+
+ expect(numberInputs.length).toEqual(1);
+ expect(inputs[0].valueAsNumber).toEqual(0);
expect(testDataItem.config.value).toEqual(0);
mockEvaluator.getInputTypeById.and.returnValue('text');
testDataItem.config.value = undefined;
testDataItem.generateValueInput('');
- expect($('input', mockContainer).filter('[type=text]').get().length).toEqual(1);
- expect($('input', mockContainer).prop('value')).toEqual('');
+
+ inputs = mockContainer.querySelectorAll('input');
+ const textInputs = Array.from(inputs).filter(input => input.type === 'text');
+
+ expect(textInputs.length).toEqual(1);
+ expect(inputs[0].value).toEqual('');
expect(testDataItem.config.value).toEqual('');
});
@@ -115,8 +135,15 @@ define(['../src/TestDataItem', 'zepto'], function (TestDataItem, $) {
mockContainer.append(testDataItem.getDOM());
mockEvaluator.getInputTypeById.and.returnValue('number');
testDataItem.generateValueInput('');
- $('input', mockContainer).prop('value', 9001);
- $('input', mockContainer).trigger('input');
+
+ const event = new Event('input', {
+ bubbles: true,
+ cancelable: true
+ });
+
+ mockContainer.querySelector('input').value = 9001;
+ mockContainer.querySelector('input').dispatchEvent(event);
+
expect(changeSpy).toHaveBeenCalledWith({
value: 9001,
property: 'value',
diff --git a/src/plugins/summaryWidget/test/TestDataManagerSpec.js b/src/plugins/summaryWidget/test/TestDataManagerSpec.js
index 70042250d..59ce37d92 100644
--- a/src/plugins/summaryWidget/test/TestDataManagerSpec.js
+++ b/src/plugins/summaryWidget/test/TestDataManagerSpec.js
@@ -1,4 +1,4 @@
-define(['../src/TestDataManager', 'zepto'], function (TestDataManager, $) {
+define(['../src/TestDataManager'], function (TestDataManager) {
describe('A Summary Widget Rule', function () {
let mockDomainObject;
let mockOpenMCT;
@@ -103,7 +103,7 @@ define(['../src/TestDataManager', 'zepto'], function (TestDataManager, $) {
mockConditionManager.getObjectName.and.returnValue('Object Name');
mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name');
- mockContainer = $(document.createElement('div'));
+ mockContainer = document.createElement('div');
testDataManager = new TestDataManager(mockDomainObject, mockConditionManager, mockOpenMCT);
});
@@ -114,7 +114,7 @@ define(['../src/TestDataManager', 'zepto'], function (TestDataManager, $) {
it('exposes a DOM element to represent itself in the view', function () {
mockContainer.append(testDataManager.getDOM());
- expect($('.t-widget-test-data-content', mockContainer).get().length).toBeGreaterThan(0);
+ expect(mockContainer.querySelectorAll('.t-widget-test-data-content').length).toBeGreaterThan(0);
});
it('generates a test cache in the format expected by a condition evaluator', function () {
@@ -207,7 +207,7 @@ define(['../src/TestDataManager', 'zepto'], function (TestDataManager, $) {
it('builds item view from item configuration', function () {
mockContainer.append(testDataManager.getDOM());
- expect($('.t-test-data-item', mockContainer).get().length).toEqual(3);
+ expect(mockContainer.querySelectorAll('.t-test-data-item').length).toEqual(3);
});
it('can remove a item from its configuration', function () {
diff --git a/src/plugins/tabs/plugin.js b/src/plugins/tabs/plugin.js
index dd9e8a4cc..5a60f0ea5 100644
--- a/src/plugins/tabs/plugin.js
+++ b/src/plugins/tabs/plugin.js
@@ -31,7 +31,7 @@ define([
openmct.types.addType('tabs', {
name: "Tabs View",
- description: 'Add multiple objects of any type to this view, and quickly navigate between them with tabs',
+ description: 'Quickly navigate between multiple objects of any type using tabs.',
creatable: true,
cssClass: 'icon-tabs-view',
initialize(domainObject) {
diff --git a/src/plugins/telemetryTable/TelemetryTableType.js b/src/plugins/telemetryTable/TelemetryTableType.js
index 3d610a9ae..af4888f5b 100644
--- a/src/plugins/telemetryTable/TelemetryTableType.js
+++ b/src/plugins/telemetryTable/TelemetryTableType.js
@@ -23,9 +23,9 @@
define(function () {
return {
name: 'Telemetry Table',
- description: 'Display telemetry values for the current time bounds in tabular form. Supports filtering and sorting.',
+ description: 'Display values for one or more telemetry end points in a scrolling table. Each row is a time-stamped value.',
creatable: true,
- cssClass: 'icon-tabular-realtime',
+ cssClass: 'icon-tabular-scrolling',
initialize(domainObject) {
domainObject.composition = [];
domainObject.configuration = {
diff --git a/src/plugins/telemetryTable/TelemetryTableViewProvider.js b/src/plugins/telemetryTable/TelemetryTableViewProvider.js
index dd4dc0ad2..ddd257851 100644
--- a/src/plugins/telemetryTable/TelemetryTableViewProvider.js
+++ b/src/plugins/telemetryTable/TelemetryTableViewProvider.js
@@ -36,7 +36,7 @@ export default function TelemetryTableViewProvider(openmct) {
return {
key: 'table',
name: 'Telemetry Table',
- cssClass: 'icon-tabular-realtime',
+ cssClass: 'icon-tabular-scrolling',
canView(domainObject) {
return domainObject.type === 'table'
|| hasTelemetry(domainObject);
diff --git a/src/plugins/telemetryTable/collections/TableRowCollection.js b/src/plugins/telemetryTable/collections/TableRowCollection.js
index a9f878040..73c28128d 100644
--- a/src/plugins/telemetryTable/collections/TableRowCollection.js
+++ b/src/plugins/telemetryTable/collections/TableRowCollection.js
@@ -225,9 +225,7 @@ define(
sortBy(sortOptions) {
if (arguments.length > 0) {
this.sortOptions = sortOptions;
- performance.mark('table:row:sort:start');
this.rows = _.orderBy(this.rows, (row) => row.getParsedValue(sortOptions.key), sortOptions.direction);
- performance.mark('table:row:sort:stop');
this.emit('sort');
}
diff --git a/src/plugins/telemetryTable/components/table.scss b/src/plugins/telemetryTable/components/table.scss
index 512af8c3a..03d54c0f7 100644
--- a/src/plugins/telemetryTable/components/table.scss
+++ b/src/plugins/telemetryTable/components/table.scss
@@ -63,8 +63,9 @@
padding-top: 0;
padding-bottom: 0;
}
- .is-in-small-container & {
- display: none;
+
+ .--width-less-than-600 & {
+ display: none !important;
}
}
}
diff --git a/src/plugins/telemetryTable/components/table.vue b/src/plugins/telemetryTable/components/table.vue
index 59172615b..e806d8c1f 100644
--- a/src/plugins/telemetryTable/components/table.vue
+++ b/src/plugins/telemetryTable/components/table.vue
@@ -144,7 +144,7 @@
<progress-bar
v-if="loading"
class="c-telemetry-table__progress-bar"
- :model="progressLoad"
+ :model="{progressPerc: undefined}"
/>
<!-- Headers table -->
@@ -385,11 +385,6 @@ export default {
};
},
computed: {
- progressLoad() {
- return {
- progressPerc: undefined
- };
- },
dropTargetStyle() {
return {
top: this.$refs.headersTable.offsetTop + 'px',
@@ -499,6 +494,8 @@ export default {
this.table.tableRows.on('sort', this.updateVisibleRows);
this.table.tableRows.on('filter', this.updateVisibleRows);
+ this.openmct.time.on('bounds', this.boundsChanged);
+
//Default sort
this.sortOptions = this.table.tableRows.sortBy();
this.scrollable = this.$el.querySelector('.js-telemetry-table__body-w');
@@ -513,7 +510,7 @@ export default {
this.table.initialize();
},
- destroyed() {
+ beforeDestroy() {
this.table.off('object-added', this.addObject);
this.table.off('object-removed', this.removeObject);
this.table.off('historical-rows-processed', this.checkForMarkedRows);
@@ -527,6 +524,8 @@ export default {
this.table.configuration.off('change', this.updateConfiguration);
+ this.openmct.time.off('bounds', this.boundsChanged);
+
clearInterval(this.resizePollHandle);
this.table.configuration.destroy();
@@ -613,7 +612,6 @@ export default {
this.calculateScrollbarWidth();
},
sortBy(columnKey) {
- performance.mark('table:sort');
// If sorting by the same column, flip the sort direction.
if (this.sortOptions.key === columnKey) {
if (this.sortOptions.direction === 'asc') {
@@ -670,7 +668,6 @@ export default {
this.setHeight();
},
rowsAdded(rows) {
- performance.mark('row:added');
this.setHeight();
let sizingRow;
@@ -692,7 +689,6 @@ export default {
this.updateVisibleRows();
},
rowsRemoved(rows) {
- performance.mark('row:removed');
this.setHeight();
this.updateVisibleRows();
},
@@ -823,16 +819,16 @@ export default {
this.visibleRows = [];
this.$nextTick().then(this.updateVisibleRows);
},
- pause(pausedByButton) {
- if (pausedByButton) {
+ pause(byButton) {
+ if (byButton) {
this.pausedByButton = true;
}
this.paused = true;
this.table.pause();
},
- unpause(unpausedByButton) {
- if (unpausedByButton) {
+ unpause(byButtonOrUserBoundsChange) {
+ if (byButtonOrUserBoundsChange) {
this.undoMarkedRows();
this.table.unpause();
this.paused = false;
@@ -847,6 +843,16 @@ export default {
this.isShowingMarkedRowsOnly = false;
},
+ boundsChanged(_bounds, isTick) {
+ if (isTick) {
+ return;
+ }
+
+ // User bounds change.
+ if (this.paused) {
+ this.unpause(true);
+ }
+ },
togglePauseByButton() {
if (this.paused) {
this.unpause(true);
@@ -854,7 +860,7 @@ export default {
this.pause(true);
}
},
- undoMarkedRows(unpause) {
+ undoMarkedRows() {
this.markedRows.forEach(r => r.marked = false);
this.markedRows = [];
},
diff --git a/src/plugins/telemetryTable/pluginSpec.js b/src/plugins/telemetryTable/pluginSpec.js
index 1d29c2e68..19cdd7e9d 100644
--- a/src/plugins/telemetryTable/pluginSpec.js
+++ b/src/plugins/telemetryTable/pluginSpec.js
@@ -133,13 +133,28 @@ describe("the plugin", () => {
let tableViewProvider;
let tableView;
let tableInstance;
+ let mockClock;
- beforeEach(() => {
+ beforeEach(async () => {
openmct.time.timeSystem('utc', {
start: 0,
end: 4
});
+ mockClock = jasmine.createSpyObj("clock", [
+ "on",
+ "off",
+ "currentValue"
+ ]);
+ mockClock.key = 'mockClock';
+ mockClock.currentValue.and.returnValue(1);
+
+ openmct.time.addClock(mockClock);
+ openmct.time.clock('mockClock', {
+ start: 0,
+ end: 4
+ });
+
testTelemetryObject = {
identifier: {
namespace: "",
@@ -195,16 +210,8 @@ describe("the plugin", () => {
'some-other-key': 'some-other-value 3'
}
];
- let telemetryPromiseResolve;
- let telemetryPromise = new Promise((resolve) => {
- telemetryPromiseResolve = resolve;
- });
-
- historicalProvider.request = () => {
- telemetryPromiseResolve(testTelemetry);
- return telemetryPromise;
- };
+ historicalProvider.request = () => Promise.resolve(testTelemetry);
openmct.router.path = [testTelemetryObject];
@@ -215,7 +222,7 @@ describe("the plugin", () => {
tableInstance = tableView.getTable();
- return telemetryPromise.then(() => Vue.nextTick());
+ await Vue.nextTick();
});
afterEach(() => {
@@ -240,13 +247,10 @@ describe("the plugin", () => {
});
- it("Renders a row for every telemetry datum returned", (done) => {
+ it("Renders a row for every telemetry datum returned", async () => {
let rows = element.querySelectorAll('table.c-telemetry-table__body tr');
- Vue.nextTick(() => {
- expect(rows.length).toBe(3);
-
- done();
- });
+ await Vue.nextTick();
+ expect(rows.length).toBe(3);
});
it("Renders a column for every item in telemetry metadata", () => {
@@ -258,7 +262,7 @@ describe("the plugin", () => {
expect(headers[3].innerText).toBe('Another attribute');
});
- it("Supports column reordering via drag and drop", () => {
+ it("Supports column reordering via drag and drop", async () => {
let columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th');
let fromColumn = columns[0];
let toColumn = columns[1];
@@ -277,54 +281,43 @@ describe("the plugin", () => {
toColumn.dispatchEvent(dragOverEvent);
toColumn.dispatchEvent(dropEvent);
- return Vue.nextTick().then(() => {
- columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th');
- let firstColumn = columns[0];
- let secondColumn = columns[1];
- let firstColumnText = firstColumn.querySelector('span.c-telemetry-table__headers__label').innerText;
- let secondColumnText = secondColumn.querySelector('span.c-telemetry-table__headers__label').innerText;
-
- expect(fromColumnText).not.toEqual(firstColumnText);
- expect(fromColumnText).toEqual(secondColumnText);
- expect(toColumnText).not.toEqual(secondColumnText);
- expect(toColumnText).toEqual(firstColumnText);
- });
+ await Vue.nextTick();
+ columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th');
+ let firstColumn = columns[0];
+ let secondColumn = columns[1];
+ let firstColumnText = firstColumn.querySelector('span.c-telemetry-table__headers__label').innerText;
+ let secondColumnText = secondColumn.querySelector('span.c-telemetry-table__headers__label').innerText;
+ expect(fromColumnText).not.toEqual(firstColumnText);
+ expect(fromColumnText).toEqual(secondColumnText);
+ expect(toColumnText).not.toEqual(secondColumnText);
+ expect(toColumnText).toEqual(firstColumnText);
});
- it("Supports filtering telemetry by regular text search", () => {
+ it("Supports filtering telemetry by regular text search", async () => {
tableInstance.tableRows.setColumnFilter("some-key", "1");
+ await Vue.nextTick();
+ let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
- return Vue.nextTick().then(() => {
- let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
-
- expect(filteredRowElements.length).toEqual(1);
-
- tableInstance.tableRows.setColumnFilter("some-key", "");
-
- return Vue.nextTick().then(() => {
- let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
+ expect(filteredRowElements.length).toEqual(1);
+ tableInstance.tableRows.setColumnFilter("some-key", "");
+ await Vue.nextTick();
- expect(allRowElements.length).toEqual(3);
- });
- });
+ let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
+ expect(allRowElements.length).toEqual(3);
});
- it("Supports filtering using Regex", () => {
+ it("Supports filtering using Regex", async () => {
tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value$");
+ await Vue.nextTick();
+ let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
- return Vue.nextTick().then(() => {
- let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
-
- expect(filteredRowElements.length).toEqual(0);
-
- tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value");
+ expect(filteredRowElements.length).toEqual(0);
- return Vue.nextTick().then(() => {
- let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
+ tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value");
+ await Vue.nextTick();
+ let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
- expect(allRowElements.length).toEqual(3);
- });
- });
+ expect(allRowElements.length).toEqual(3);
});
it("displays the correct number of column headers when the configuration is mutated", async () => {
@@ -360,5 +353,101 @@ describe("the plugin", () => {
tableRowCells = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr:first-child td');
expect(tableRowCells.length).toEqual(4);
});
+
+ it("Pauses the table when a row is marked", async () => {
+ let firstRow = element.querySelector('table.c-telemetry-table__body > tbody > tr');
+ let clickEvent = createMouseEvent('click');
+
+ // Mark a row
+ firstRow.dispatchEvent(clickEvent);
+
+ await Vue.nextTick();
+
+ // Verify table is paused
+ expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();
+ });
+
+ it("Unpauses the table on user bounds change", async () => {
+ let firstRow = element.querySelector('table.c-telemetry-table__body > tbody > tr');
+ let clickEvent = createMouseEvent('click');
+
+ // Mark a row
+ firstRow.dispatchEvent(clickEvent);
+
+ await Vue.nextTick();
+
+ // Verify table is paused
+ expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();
+
+ const currentBounds = openmct.time.bounds();
+ await Vue.nextTick();
+ const newBounds = {
+ start: currentBounds.start,
+ end: currentBounds.end - 3
+ };
+
+ // Manually change the time bounds
+ openmct.time.bounds(newBounds);
+ await Vue.nextTick();
+
+ // Verify table is no longer paused
+ expect(element.querySelector('div.c-table.is-paused')).toBeNull();
+ });
+
+ it("Unpauses the table on user bounds change if paused by button", async () => {
+ const viewContext = tableView.getViewContext();
+
+ // Pause by button
+ viewContext.togglePauseByButton();
+ await Vue.nextTick();
+
+ // Verify table is paused
+ expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();
+
+ const currentBounds = openmct.time.bounds();
+ await Vue.nextTick();
+
+ const newBounds = {
+ start: currentBounds.start,
+ end: currentBounds.end - 1
+ };
+ // Manually change the time bounds
+ openmct.time.bounds(newBounds);
+
+ await Vue.nextTick();
+
+ // Verify table is no longer paused
+ expect(element.querySelector('div.c-table.is-paused')).toBeNull();
+ });
+
+ it("Does not unpause the table on tick", async () => {
+ const viewContext = tableView.getViewContext();
+
+ // Pause by button
+ viewContext.togglePauseByButton();
+
+ await Vue.nextTick();
+
+ // Verify table displays the correct number of rows
+ let tableRows = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr');
+ expect(tableRows.length).toEqual(3);
+
+ // Verify table is paused
+ expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();
+
+ // Tick the clock
+ openmct.time.tick(1);
+
+ await Vue.nextTick();
+
+ // Verify table is still paused
+ expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();
+
+ await Vue.nextTick();
+
+ // Verify table displays the correct number of rows
+ tableRows = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr');
+ expect(tableRows.length).toEqual(3);
+ });
});
});
diff --git a/src/plugins/themes/maelstrom-theme.scss b/src/plugins/themes/maelstrom-theme.scss
deleted file mode 100644
index 69e327cf6..000000000
--- a/src/plugins/themes/maelstrom-theme.scss
+++ /dev/null
@@ -1,22 +0,0 @@
-@import "../../styles/vendor/normalize-min";
-@import "../../styles/constants";
-@import "../../styles/constants-mobile.scss";
-
-@import "../../styles/constants-maelstrom";
-
-@import "../../styles/mixins";
-@import "../../styles/animations";
-@import "../../styles/about";
-@import "../../styles/glyphs";
-@import "../../styles/global";
-@import "../../styles/status";
-@import "../../styles/limits";
-@import "../../styles/controls";
-@import "../../styles/forms";
-@import "../../styles/table";
-@import "../../styles/legacy";
-@import "../../styles/legacy-plots";
-@import "../../styles/plotly";
-@import "../../styles/legacy-messages";
-
-@import "../../styles/vue-styles.scss";
diff --git a/src/plugins/themes/maelstrom.js b/src/plugins/themes/maelstrom.js
deleted file mode 100644
index c551b1f72..000000000
--- a/src/plugins/themes/maelstrom.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { installTheme } from './installTheme';
-
-export default function plugin() {
- return function install(openmct) {
- installTheme(openmct, 'maelstrom');
- };
-}
diff --git a/src/plugins/timeConductor/ConductorHistory.vue b/src/plugins/timeConductor/ConductorHistory.vue
index a91accfa2..8df1925e8 100644
--- a/src/plugins/timeConductor/ConductorHistory.vue
+++ b/src/plugins/timeConductor/ConductorHistory.vue
@@ -39,7 +39,7 @@
const DEFAULT_DURATION_FORMATTER = 'duration';
const LOCAL_STORAGE_HISTORY_KEY_FIXED = 'tcHistory';
const LOCAL_STORAGE_HISTORY_KEY_REALTIME = 'tcHistoryRealtime';
-const DEFAULT_RECORDS = 10;
+const DEFAULT_RECORDS_LENGTH = 10;
import { millisecondsToDHMS } from "utils/duration";
import UTCTimeFormat from "../utcTimeSystem/UTCTimeFormat.js";
@@ -79,16 +79,14 @@ export default {
* @timespans {start, end} number representing timestamp
*/
fixedHistory: {},
- presets: []
+ presets: [],
+ isFixed: this.openmct.time.clock() === undefined
};
},
computed: {
currentHistory() {
return this.mode + 'History';
},
- isFixed() {
- return this.openmct.time.clock() === undefined;
- },
historyForCurrentTimeSystem() {
const history = this[this.currentHistory][this.timeSystem.key];
@@ -96,7 +94,7 @@ export default {
},
storageKey() {
let key = LOCAL_STORAGE_HISTORY_KEY_FIXED;
- if (this.mode !== 'fixed') {
+ if (!this.isFixed) {
key = LOCAL_STORAGE_HISTORY_KEY_REALTIME;
}
@@ -108,6 +106,7 @@ export default {
handler() {
// only for fixed time since we track offsets for realtime
if (this.isFixed) {
+ this.updateMode();
this.addTimespan();
}
},
@@ -115,28 +114,35 @@ export default {
},
offsets: {
handler() {
+ this.updateMode();
this.addTimespan();
},
deep: true
},
timeSystem: {
handler(ts) {
+ this.updateMode();
this.loadConfiguration();
this.addTimespan();
},
deep: true
},
mode: function () {
- this.getHistoryFromLocalStorage();
- this.initializeHistoryIfNoHistory();
+ this.updateMode();
this.loadConfiguration();
}
},
mounted() {
+ this.updateMode();
this.getHistoryFromLocalStorage();
this.initializeHistoryIfNoHistory();
},
methods: {
+ updateMode() {
+ this.isFixed = this.openmct.time.clock() === undefined;
+ this.getHistoryFromLocalStorage();
+ this.initializeHistoryIfNoHistory();
+ },
getHistoryMenuItems() {
const history = this.historyForCurrentTimeSystem.map(timespan => {
let name;
@@ -203,8 +209,8 @@ export default {
currentHistory = currentHistory.filter(ts => !(ts.start === timespan.start && ts.end === timespan.end));
currentHistory.unshift(timespan); // add to front
- if (currentHistory.length > this.records) {
- currentHistory.length = this.records;
+ if (currentHistory.length > this.MAX_RECORDS_LENGTH) {
+ currentHistory.length = this.MAX_RECORDS_LENGTH;
}
this.$set(this[this.currentHistory], key, currentHistory);
@@ -231,7 +237,7 @@ export default {
.filter(option => option.timeSystem === this.timeSystem.key);
this.presets = this.loadPresets(configurations);
- this.records = this.loadRecords(configurations);
+ this.MAX_RECORDS_LENGTH = this.loadRecords(configurations);
},
loadPresets(configurations) {
const configuration = configurations.find(option => {
@@ -243,9 +249,9 @@ export default {
},
loadRecords(configurations) {
const configuration = configurations.find(option => option.records);
- const records = configuration ? configuration.records : DEFAULT_RECORDS;
+ const maxRecordsLength = configuration ? configuration.records : DEFAULT_RECORDS_LENGTH;
- return records;
+ return maxRecordsLength;
},
formatTime(time) {
let format = this.timeSystem.timeFormat;
diff --git a/src/plugins/timeConductor/ConductorInputsFixed.vue b/src/plugins/timeConductor/ConductorInputsFixed.vue
index 106e4cee8..c3c14f72e 100644
--- a/src/plugins/timeConductor/ConductorInputsFixed.vue
+++ b/src/plugins/timeConductor/ConductorInputsFixed.vue
@@ -78,6 +78,12 @@ export default {
default() {
return undefined;
}
+ },
+ objectPath: {
+ type: Array,
+ default() {
+ return [];
+ }
}
},
data() {
@@ -127,7 +133,7 @@ export default {
methods: {
setTimeContext() {
this.stopFollowingTimeContext();
- this.timeContext = this.openmct.time.getContextForView(this.keyString ? [{identifier: this.keyString}] : []);
+ this.timeContext = this.openmct.time.getContextForView(this.keyString ? this.objectPath : []);
this.handleNewBounds(this.timeContext.bounds());
this.timeContext.on('bounds', this.handleNewBounds);
diff --git a/src/plugins/timeConductor/ConductorInputsRealtime.vue b/src/plugins/timeConductor/ConductorInputsRealtime.vue
index dc1cb77e5..4054869be 100644
--- a/src/plugins/timeConductor/ConductorInputsRealtime.vue
+++ b/src/plugins/timeConductor/ConductorInputsRealtime.vue
@@ -22,6 +22,7 @@
ref="startOffset"
class="c-button c-conductor__delta-button"
title="Set the time offset after now"
+ data-testid="conductor-start-offset-button"
@click.prevent.stop="showTimePopupStart"
>
{{ offsets.start }}
@@ -61,6 +62,7 @@
ref="endOffset"
class="c-button c-conductor__delta-button"
title="Set the time offset preceding now"
+ data-testid="conductor-end-offset-button"
@click.prevent.stop="showTimePopupEnd"
>
{{ offsets.end }}
@@ -88,6 +90,12 @@ export default {
return undefined;
}
},
+ objectPath: {
+ type: Array,
+ default() {
+ return [];
+ }
+ },
inputBounds: {
type: Object,
default() {
@@ -160,7 +168,7 @@ export default {
},
setTimeContext() {
this.stopFollowingTime();
- this.timeContext = this.openmct.time.getContextForView(this.keyString ? [{identifier: this.keyString}] : []);
+ this.timeContext = this.openmct.time.getContextForView(this.keyString ? this.objectPath : []);
this.followTime();
},
handleNewBounds(bounds) {
diff --git a/src/plugins/timeConductor/ConductorMode.vue b/src/plugins/timeConductor/ConductorMode.vue
index 869ca1b0a..fbe4fa2f7 100644
--- a/src/plugins/timeConductor/ConductorMode.vue
+++ b/src/plugins/timeConductor/ConductorMode.vue
@@ -105,6 +105,7 @@ export default {
name: 'Fixed Timespan',
description: 'Query and explore data that falls between two fixed datetimes.',
cssClass: 'icon-tabular',
+ testId: 'conductor-modeOption-fixed',
onItemClicked: () => this.setOption(key)
};
} else {
@@ -116,6 +117,7 @@ export default {
description: "Monitor streaming data in real-time. The Time "
+ "Conductor and displays will automatically advance themselves based on this clock. " + clock.description,
cssClass: clock.cssClass || 'icon-clock',
+ testId: 'conductor-modeOption-realtime',
onItemClicked: () => this.setOption(key)
};
}
@@ -148,7 +150,8 @@ export default {
if (clockKey === undefined) {
this.openmct.time.stopClock();
} else {
- this.openmct.time.clock(clockKey, configuration.clockOffsets);
+ const offsets = this.openmct.time.clockOffsets() || configuration.clockOffsets;
+ this.openmct.time.clock(clockKey, offsets);
}
},
diff --git a/src/plugins/timeConductor/date-picker.scss b/src/plugins/timeConductor/date-picker.scss
index 9ce416ca4..3c447e974 100644
--- a/src/plugins/timeConductor/date-picker.scss
+++ b/src/plugins/timeConductor/date-picker.scss
@@ -69,7 +69,8 @@
}
&.selected {
- background: #1ac6ff; // this should be a variable... CHARLESSSSSS
+ background: $colorKey;
+ color: $colorKeyFg;
}
}
diff --git a/src/plugins/timeConductor/independent/IndependentTimeConductor.vue b/src/plugins/timeConductor/independent/IndependentTimeConductor.vue
index c64b0c38d..8e40e9070 100644
--- a/src/plugins/timeConductor/independent/IndependentTimeConductor.vue
+++ b/src/plugins/timeConductor/independent/IndependentTimeConductor.vue
@@ -52,12 +52,14 @@
<conductor-inputs-fixed
v-if="isFixed"
:key-string="domainObject.identifier.key"
+ :object-path="objectPath"
@updated="saveFixedOffsets"
/>
<conductor-inputs-realtime
v-else
:key-string="domainObject.identifier.key"
+ :object-path="objectPath"
@updated="saveClockOffsets"
/>
</div>
@@ -85,6 +87,10 @@ export default {
domainObject: {
type: Object,
required: true
+ },
+ objectPath: {
+ type: Array,
+ required: true
}
},
data() {
@@ -164,7 +170,7 @@ export default {
},
setTimeContext() {
this.stopFollowingTimeContext();
- this.timeContext = this.openmct.time.getContextForView([this.domainObject]);
+ this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on('clock', this.setTimeOptions);
},
stopFollowingTimeContext() {
diff --git a/src/plugins/timeConductor/pluginSpec.js b/src/plugins/timeConductor/pluginSpec.js
index d39478413..3d12594fe 100644
--- a/src/plugins/timeConductor/pluginSpec.js
+++ b/src/plugins/timeConductor/pluginSpec.js
@@ -131,15 +131,15 @@ describe('time conductor', () => {
describe('duration functions', () => {
it('should transform milliseconds to DHMS', () => {
const functionResults = [millisecondsToDHMS(0), millisecondsToDHMS(86400000),
- millisecondsToDHMS(129600000), millisecondsToDHMS(661824000)];
- const validResults = [' ', '+ 1d', '+ 1d 12h', '+ 7d 15h 50m 24s'];
+ millisecondsToDHMS(129600000), millisecondsToDHMS(661824000), millisecondsToDHMS(213927028)];
+ const validResults = [' ', '+ 1d', '+ 1d 12h', '+ 7d 15h 50m 24s', '+ 2d 11h 25m 27s 28ms'];
expect(validResults).toEqual(functionResults);
});
it('should get precise duration', () => {
const functionResults = [getPreciseDuration(0), getPreciseDuration(643680000),
- getPreciseDuration(1605312000)];
- const validResults = ['00:00:00:00', '07:10:48:00', '18:13:55:12'];
+ getPreciseDuration(1605312000), getPreciseDuration(213927028)];
+ const validResults = ['00:00:00:00:000', '07:10:48:00:000', '18:13:55:12:000', '02:11:25:27:028'];
expect(validResults).toEqual(functionResults);
});
});
diff --git a/src/plugins/timeline/TimelineCompositionPolicy.js b/src/plugins/timeline/TimelineCompositionPolicy.js
new file mode 100644
index 000000000..b922e0499
--- /dev/null
+++ b/src/plugins/timeline/TimelineCompositionPolicy.js
@@ -0,0 +1,70 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2021, 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 ALLOWED_TYPES = [
+ 'telemetry.plot.overlay',
+ 'telemetry.plot.stacked',
+ 'plan'
+];
+const DISALLOWED_TYPES = [
+ 'telemetry.plot.bar-graph',
+ 'telemetry.plot.scatter-plot'
+];
+export default function TimelineCompositionPolicy(openmct) {
+ function hasNumericTelemetry(domainObject, metadata) {
+ const hasTelemetry = openmct.telemetry.isTelemetryObject(domainObject);
+ if (!hasTelemetry || !metadata) {
+ return false;
+ }
+
+ return metadata.values().length > 0 && hasDomainAndRange(metadata);
+ }
+
+ function hasDomainAndRange(metadata) {
+ return (metadata.valuesForHints(['range']).length > 0
+ && metadata.valuesForHints(['domain']).length > 0);
+ }
+
+ function hasImageTelemetry(domainObject, metadata) {
+ if (!metadata) {
+ return false;
+ }
+
+ return metadata.valuesForHints(['image']).length > 0;
+ }
+
+ return {
+ allow: function (parent, child) {
+ if (parent.type === 'time-strip') {
+ const metadata = openmct.telemetry.getMetadata(child);
+
+ if (!DISALLOWED_TYPES.includes(child.type)
+ && (hasNumericTelemetry(child, metadata) || hasImageTelemetry(child, metadata) || ALLOWED_TYPES.includes(child.type))) {
+ return true;
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+ };
+}
diff --git a/src/plugins/timeline/TimelineObjectView.vue b/src/plugins/timeline/TimelineObjectView.vue
index 24bd52967..2d1caf1b0 100644
--- a/src/plugins/timeline/TimelineObjectView.vue
+++ b/src/plugins/timeline/TimelineObjectView.vue
@@ -27,17 +27,18 @@
:show-ucontents="item.domainObject.type === 'plan'"
:span-rows-count="item.rowCount"
>
- <template slot="label">
+ <template #label>
{{ item.domainObject.name }}
</template>
- <object-view
- ref="objectView"
- slot="object"
- class="u-contents"
- :default-object="item.domainObject"
- :object-path="item.objectPath"
- @change-action-collection="setActionCollection"
- />
+ <template #object>
+ <object-view
+ ref="objectView"
+ class="u-contents"
+ :default-object="item.domainObject"
+ :object-path="item.objectPath"
+ @change-action-collection="setActionCollection"
+ />
+ </template>
</swim-lane>
</template>
@@ -91,7 +92,7 @@ export default {
this.mutablePromise.then(() => {
this.openmct.objects.destroyMutable(this.domainObject);
});
- } else if (this.domainObject.isMutable) {
+ } else if (this?.domainObject?.isMutable) {
this.openmct.objects.destroyMutable(this.domainObject);
}
},
diff --git a/src/plugins/timeline/TimelineViewLayout.vue b/src/plugins/timeline/TimelineViewLayout.vue
index b1764ec1a..c92e4ab73 100644
--- a/src/plugins/timeline/TimelineViewLayout.vue
+++ b/src/plugins/timeline/TimelineViewLayout.vue
@@ -29,10 +29,10 @@
v-for="timeSystemItem in timeSystems"
:key="timeSystemItem.timeSystem.key"
>
- <template slot="label">
+ <template #label>
{{ timeSystemItem.timeSystem.name }}
</template>
- <template slot="object">
+ <template #object>
<timeline-axis
:bounds="timeSystemItem.bounds"
:time-system="timeSystemItem.timeSystem"
@@ -50,7 +50,7 @@
<timeline-object-view
v-for="item in items"
:key="item.keyString"
- class="c-timeline__content"
+ class="c-timeline__content js-timeline__content"
:item="item"
/>
</div>
@@ -61,7 +61,7 @@
import TimelineObjectView from './TimelineObjectView.vue';
import TimelineAxis from '../../ui/components/TimeSystemAxis.vue';
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
-import { getValidatedPlan } from "../plan/util";
+import { getValidatedData } from "../plan/util";
const unknownObjectType = {
definition: {
@@ -93,15 +93,15 @@ export default {
this.stopFollowingTimeContext();
},
mounted() {
+ this.items = [];
+ this.setTimeContext();
+
if (this.composition) {
this.composition.on('add', this.addItem);
this.composition.on('remove', this.removeItem);
this.composition.on('reorder', this.reorder);
this.composition.load();
}
-
- this.setTimeContext();
- this.getTimeSystems();
},
methods: {
addItem(domainObject) {
@@ -110,7 +110,7 @@ export default {
let objectPath = [domainObject].concat(this.objectPath.slice());
let rowCount = 0;
if (domainObject.type === 'plan') {
- rowCount = Object.keys(getValidatedPlan(domainObject)).length;
+ rowCount = Object.keys(getValidatedData(domainObject)).length;
}
let height = domainObject.type === 'telemetry.plot.stacked' ? `${domainObject.composition.length * 100}px` : '100px';
@@ -165,6 +165,7 @@ export default {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
+ this.getTimeSystems();
this.updateViewBounds(this.timeContext.bounds());
this.timeContext.on('bounds', this.updateViewBounds);
},
diff --git a/src/plugins/timeline/plugin.js b/src/plugins/timeline/plugin.js
index 9ea701df0..8e72d0e16 100644
--- a/src/plugins/timeline/plugin.js
+++ b/src/plugins/timeline/plugin.js
@@ -22,6 +22,7 @@
import TimelineViewProvider from './TimelineViewProvider';
import timelineInterceptor from "./timelineInterceptor";
+import TimelineCompositionPolicy from "./TimelineCompositionPolicy";
export default function () {
return function install(openmct) {
@@ -39,6 +40,8 @@ export default function () {
}
});
timelineInterceptor(openmct);
+ openmct.composition.addPolicy(new TimelineCompositionPolicy(openmct).allow);
+
openmct.objectViews.addProvider(new TimelineViewProvider(openmct));
};
}
diff --git a/src/plugins/timeline/pluginSpec.js b/src/plugins/timeline/pluginSpec.js
index 5ca4130ca..0eb77e193 100644
--- a/src/plugins/timeline/pluginSpec.js
+++ b/src/plugins/timeline/pluginSpec.js
@@ -20,9 +20,10 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
-import { createOpenMct, resetApplicationState } from "utils/testing";
+import { createOpenMct, resetApplicationState } from "@/utils/testing";
import TimelinePlugin from "./plugin";
import Vue from 'vue';
+import EventEmitter from "EventEmitter";
describe('the plugin', function () {
let objectDef;
@@ -30,6 +31,65 @@ describe('the plugin', function () {
let child;
let openmct;
let mockObjectPath;
+ let mockCompositionForTimelist;
+ let planObject = {
+ identifier: {
+ key: 'test-plan-object',
+ namespace: ''
+ },
+ type: 'plan',
+ id: "test-plan-object",
+ selectFile: {
+ body: JSON.stringify({
+ "TEST-GROUP": [
+ {
+ "name": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
+ "start": 1597170002854,
+ "end": 1597171032854,
+ "type": "TEST-GROUP",
+ "color": "fuchsia",
+ "textColor": "black"
+ },
+ {
+ "name": "Sed ut perspiciatis",
+ "start": 1597171132854,
+ "end": 1597171232854,
+ "type": "TEST-GROUP",
+ "color": "fuchsia",
+ "textColor": "black"
+ }
+ ]
+ })
+ }
+ };
+ let timelineObject = {
+ "composition": [],
+ configuration: {
+ useIndependentTime: false,
+ timeOptions: {
+ mode: {
+ key: 'fixed'
+ },
+ fixedOffsets: {
+ start: 10,
+ end: 11
+ },
+ clockOffsets: {
+ start: -(30 * 60 * 1000),
+ end: (30 * 60 * 1000)
+ }
+ }
+ },
+ "name": "Some timestrip",
+ "type": "time-strip",
+ "location": "mine",
+ "modified": 1631005183584,
+ "persisted": 1631005183502,
+ "identifier": {
+ "namespace": "",
+ "key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
+ }
+ };
beforeEach((done) => {
mockObjectPath = [
@@ -102,12 +162,7 @@ describe('the plugin', function () {
beforeEach(() => {
testViewObject = {
- id: "test-object",
- identifier: {
- key: "test-object",
- namespace: ''
- },
- type: "time-strip"
+ ...timelineObject
};
const applicableViews = openmct.objectViews.get(testViewObject, mockObjectPath);
@@ -133,30 +188,57 @@ describe('the plugin', function () {
});
});
+ describe('the timeline composition', () => {
+ let timelineDomainObject;
+ let timelineView;
+
+ beforeEach(() => {
+ timelineDomainObject = {
+ ...timelineObject,
+ composition: [
+ {
+ identifier: {
+ key: 'test-plan-object',
+ namespace: ''
+ }
+ }
+ ]
+ };
+
+ mockCompositionForTimelist = new EventEmitter();
+ mockCompositionForTimelist.load = () => {
+ mockCompositionForTimelist.emit('add', planObject);
+
+ return [planObject];
+ };
+
+ spyOn(openmct.composition, 'get').withArgs(timelineDomainObject).and.returnValue(mockCompositionForTimelist);
+
+ openmct.router.path = [timelineDomainObject];
+
+ const applicableViews = openmct.objectViews.get(timelineDomainObject, [timelineDomainObject]);
+ timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view');
+ let view = timelineView.view(timelineDomainObject, [timelineDomainObject]);
+ view.show(child, true);
+
+ return Vue.nextTick();
+ });
+
+ it('loads the plan from composition', () => {
+ return Vue.nextTick(() => {
+ const items = element.querySelectorAll('.js-timeline__content');
+ expect(items.length).toEqual(1);
+ });
+ });
+ });
+
describe('the independent time conductor', () => {
let timelineView;
let testViewObject = {
- id: "test-object",
- identifier: {
- key: "test-object",
- namespace: ''
- },
- type: "time-strip",
+ ...timelineObject,
configuration: {
- useIndependentTime: true,
- timeOptions: {
- mode: {
- key: 'local'
- },
- fixedOffsets: {
- start: 10,
- end: 11
- },
- clockOffsets: {
- start: -(30 * 60 * 1000),
- end: (30 * 60 * 1000)
- }
- }
+ ...timelineObject.configuration,
+ useIndependentTime: true
}
};
@@ -181,30 +263,18 @@ describe('the plugin', function () {
});
});
- describe('the independent time conductor', () => {
+ describe('the independent time conductor - fixed', () => {
let timelineView;
let testViewObject2 = {
+ ...timelineObject,
id: "test-object2",
identifier: {
key: "test-object2",
namespace: ''
},
- type: "time-strip",
configuration: {
- useIndependentTime: true,
- timeOptions: {
- mode: {
- key: 'fixed'
- },
- fixedOffsets: {
- start: 10,
- end: 11
- },
- clockOffsets: {
- start: -(30 * 60 * 1000),
- end: (30 * 60 * 1000)
- }
- }
+ ...timelineObject.configuration,
+ useIndependentTime: true
}
};
@@ -228,4 +298,68 @@ describe('the plugin', function () {
});
});
+ describe("The timestrip composition policy", () => {
+ let testObject;
+ beforeEach(() => {
+ testObject = {
+ ...timelineObject,
+ composition: []
+ };
+ });
+
+ it("allows composition for plots", () => {
+ const testTelemetryObject = {
+ identifier: {
+ namespace: "",
+ key: "test-object"
+ },
+ type: "test-object",
+ name: "Test Object",
+ telemetry: {
+ values: [{
+ key: "some-key",
+ name: "Some attribute",
+ hints: {
+ domain: 1
+ }
+ }, {
+ key: "some-other-key",
+ name: "Another attribute",
+ hints: {
+ range: 1
+ }
+ }]
+ }
+ };
+ const composition = openmct.composition.get(testObject);
+ expect(() => {
+ composition.add(testTelemetryObject);
+ }).not.toThrow();
+ expect(testObject.composition.length).toBe(1);
+ });
+
+ it("allows composition for plans", () => {
+ const composition = openmct.composition.get(testObject);
+ expect(() => {
+ composition.add(planObject);
+ }).not.toThrow();
+ expect(testObject.composition.length).toBe(1);
+ });
+
+ it("disallows composition for non time-based plots", () => {
+ const barGraphObject = {
+ identifier: {
+ namespace: "",
+ key: "test-object"
+ },
+ type: "telemetry.plot.bar-graph",
+ name: "Test Object"
+ };
+ const composition = openmct.composition.get(testObject);
+ expect(() => {
+ composition.add(barGraphObject);
+ }).toThrow();
+ expect(testObject.composition.length).toBe(0);
+ });
+ });
});
diff --git a/src/plugins/timelist/Timelist.vue b/src/plugins/timelist/Timelist.vue
index 517822cd6..fdbf96e7a 100644
--- a/src/plugins/timelist/Timelist.vue
+++ b/src/plugins/timelist/Timelist.vue
@@ -35,14 +35,14 @@
</template>
<script>
-import {getValidatedPlan} from "../plan/util";
+import {getValidatedData} from "../plan/util";
import ListView from '../../ui/components/List/ListView.vue';
import {getPreciseDuration} from "../../utils/duration";
import ticker from 'utils/clock/Ticker';
import {SORT_ORDER_OPTIONS} from "./constants";
import moment from "moment";
-import uuid from "uuid";
+import { v4 as uuid } from 'uuid';
const SCROLL_TIMEOUT = 10000;
const ROW_HEIGHT = 30;
@@ -96,8 +96,10 @@ export default {
components: {
ListView
},
- inject: ['openmct', 'domainObject', 'path'],
+ inject: ['openmct', 'domainObject', 'path', 'composition'],
data() {
+ this.planObjects = [];
+
return {
viewBounds: undefined,
height: 0,
@@ -108,18 +110,30 @@ export default {
},
mounted() {
this.isEditing = this.openmct.editor.isEditing();
- this.timestamp = Date.now();
+ this.timestamp = this.openmct.time.clock()?.currentValue() || this.openmct.time.bounds()?.start;
+ this.openmct.time.on('clock', this.setViewFromClock);
+
this.getPlanDataAndSetConfig(this.domainObject);
- this.unlisten = this.openmct.objects.observe(this.domainObject, 'selectFile', this.getPlanDataAndSetConfig);
+ this.unlisten = this.openmct.objects.observe(this.domainObject, 'selectFile', this.planFileUpdated);
this.unlistenConfig = this.openmct.objects.observe(this.domainObject, 'configuration', this.setViewFromConfig);
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus);
this.status = this.openmct.status.get(this.domainObject.identifier);
this.unlistenTicker = ticker.listen(this.clearPreviousActivities);
+ this.openmct.time.on('bounds', this.updateTimestamp);
this.openmct.editor.on('isEditing', this.setEditState);
this.deferAutoScroll = _.debounce(this.deferAutoScroll, 500);
this.$el.parentElement.addEventListener('scroll', this.deferAutoScroll, true);
+
+ if (this.composition) {
+ this.composition.on('add', this.addToComposition);
+ this.composition.on('remove', this.removeItem);
+ this.composition.load();
+ }
+
+ this.setViewFromClock(this.openmct.time.clock());
+
},
beforeDestroy() {
if (this.unlisten) {
@@ -139,13 +153,26 @@ export default {
}
this.openmct.editor.off('isEditing', this.setEditState);
+ this.openmct.time.off('bounds', this.updateTimestamp);
+ this.openmct.time.off('clock', this.setViewFromClock);
this.$el.parentElement.removeEventListener('scroll', this.deferAutoScroll, true);
if (this.clearAutoScrollDisabledTimer) {
clearTimeout(this.clearAutoScrollDisabledTimer);
}
+
+ if (this.composition) {
+ this.composition.off('add', this.addToComposition);
+ this.composition.off('remove', this.removeItem);
+ }
},
methods: {
+ planFileUpdated(selectFile) {
+ this.getPlanData({
+ selectFile,
+ sourceMap: this.domainObject.sourceMap
+ });
+ },
getPlanDataAndSetConfig(mutatedObject) {
this.getPlanData(mutatedObject);
this.setViewFromConfig(mutatedObject.configuration);
@@ -157,14 +184,87 @@ export default {
this.showAll = true;
this.listActivities();
} else {
+
this.filterValue = configuration.filter;
this.setSort();
this.setViewBounds();
this.listActivities();
}
},
+ updateTimestamp(_bounds, isTick) {
+ if (isTick === true) {
+ this.timestamp = this.openmct.time.clock().currentValue();
+ }
+ },
+ setViewFromClock(newClock) {
+ this.filterValue = this.domainObject.configuration.filter;
+ const isFixedTime = newClock === undefined;
+ if (isFixedTime) {
+ this.hideAll = false;
+ this.showAll = true;
+ // clear invokes listActivities
+ this.clearPreviousActivities(this.openmct.time.bounds()?.start);
+ } else {
+ this.setSort();
+ this.setViewBounds();
+ this.listActivities();
+ }
+ },
+ addItem(domainObject) {
+ this.planObjects = [domainObject];
+ this.resetPlanData();
+ if (domainObject.type === 'plan') {
+ this.getPlanDataAndSetConfig({
+ ...this.domainObject,
+ selectFile: domainObject.selectFile,
+ sourceMap: domainObject.sourceMap
+ });
+ }
+ },
+ addToComposition(telemetryObject) {
+ if (this.planObjects.length > 0) {
+ this.confirmReplacePlan(telemetryObject);
+ } else {
+ this.addItem(telemetryObject);
+ }
+ },
+ confirmReplacePlan(telemetryObject) {
+ const dialog = this.openmct.overlays.dialog({
+ iconClass: 'alert',
+ message: 'This action will replace the current plan. Do you want to continue?',
+ buttons: [
+ {
+ label: 'Ok',
+ emphasis: true,
+ callback: () => {
+ const oldTelemetryObject = this.planObjects[0];
+ this.removeFromComposition(oldTelemetryObject);
+ this.addItem(telemetryObject);
+ dialog.dismiss();
+ }
+ },
+ {
+ label: 'Cancel',
+ callback: () => {
+ this.removeFromComposition(telemetryObject);
+ dialog.dismiss();
+ }
+ }
+ ]
+ });
+ },
+ removeFromComposition(telemetryObject) {
+ this.composition.remove(telemetryObject);
+ },
+ removeItem() {
+ this.planObjects = [];
+ this.resetPlanData();
+ },
+ resetPlanData() {
+ this.planData = {};
+ },
getPlanData(domainObject) {
- this.planData = getValidatedPlan(domainObject);
+ this.planData = getValidatedData(domainObject);
},
setViewBounds() {
const pastEventsIndex = this.domainObject.configuration.pastEventsIndex;
@@ -176,7 +276,7 @@ export default {
const futureEventsDurationIndex = this.domainObject.configuration.futureEventsDurationIndex;
if (pastEventsIndex === 0 && futureEventsIndex === 0 && currentEventsIndex === 0) {
- //show all events
+ //don't show all events
this.showAll = false;
this.viewBounds = undefined;
this.hideAll = true;
@@ -328,7 +428,7 @@ export default {
this.firstCurrentActivityIndex = -1;
this.currentActivitiesCount = 0;
- this.$el.parentElement.scrollTo({top: 0});
+ this.$el.parentElement?.scrollTo({top: 0});
this.autoScrolled = false;
},
setScrollTop() {
diff --git a/src/plugins/timelist/TimelistCompositionPolicy.js b/src/plugins/timelist/TimelistCompositionPolicy.js
new file mode 100644
index 000000000..75b3318a7
--- /dev/null
+++ b/src/plugins/timelist/TimelistCompositionPolicy.js
@@ -0,0 +1,34 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 {TIMELIST_TYPE} from "@/plugins/timelist/constants";
+
+export default function TimelistCompositionPolicy(openmct) {
+ return {
+ allow: function (parent, child) {
+ if (parent.type === TIMELIST_TYPE && child.type !== 'plan') {
+ return false;
+ }
+
+ return true;
+ }
+ };
+}
diff --git a/src/plugins/timelist/TimelistViewProvider.js b/src/plugins/timelist/TimelistViewProvider.js
index cbb54f61f..ed2101ccc 100644
--- a/src/plugins/timelist/TimelistViewProvider.js
+++ b/src/plugins/timelist/TimelistViewProvider.js
@@ -52,7 +52,8 @@ export default function TimelistViewProvider(openmct) {
provide: {
openmct,
domainObject,
- path: objectPath
+ path: objectPath,
+ composition: openmct.composition.get(domainObject)
},
template: '<timelist></timelist>'
});
diff --git a/src/plugins/timelist/inspector/TimelistPropertiesView.vue b/src/plugins/timelist/inspector/TimelistPropertiesView.vue
index 57a1747b7..986325f99 100644
--- a/src/plugins/timelist/inspector/TimelistPropertiesView.vue
+++ b/src/plugins/timelist/inspector/TimelistPropertiesView.vue
@@ -32,7 +32,7 @@
<div
v-if="canEdit"
class="c-inspect-properties__hint span-all"
- >These settings are not previewed and will be applied after editing is completed.</div>
+ >These settings don't affect the view while editing, but will be applied after editing is finished.</div>
<div
class="c-inspect-properties__label"
title="Sort order of the timelist."
diff --git a/src/plugins/timelist/plugin.js b/src/plugins/timelist/plugin.js
index 6aac894f8..11818a009 100644
--- a/src/plugins/timelist/plugin.js
+++ b/src/plugins/timelist/plugin.js
@@ -23,6 +23,7 @@
import TimelistViewProvider from './TimelistViewProvider';
import { TIMELIST_TYPE } from './constants';
import TimeListInspectorViewProvider from "./inspector/TimeListInspectorViewProvider";
+import TimelistCompositionPolicy from "@/plugins/timelist/TimelistCompositionPolicy";
export default function () {
return function install(openmct) {
@@ -32,37 +33,26 @@ export default function () {
description: 'A configurable, time-ordered list view of activities for a compatible mission plan file.',
creatable: true,
cssClass: 'icon-timelist',
- form: [
- {
- name: 'Upload Plan (JSON File)',
- key: 'selectFile',
- control: 'file-input',
- required: true,
- text: 'Select File...',
- type: 'application/json',
- property: [
- "selectFile"
- ]
- }
- ],
initialize: function (domainObject) {
domainObject.configuration = {
sortOrderIndex: 0,
- futureEventsIndex: 0,
+ futureEventsIndex: 1,
futureEventsDurationIndex: 0,
futureEventsDuration: 20,
currentEventsIndex: 1,
currentEventsDurationIndex: 0,
currentEventsDuration: 20,
- pastEventsIndex: 0,
+ pastEventsIndex: 1,
pastEventsDurationIndex: 0,
pastEventsDuration: 20,
filter: ''
};
+ domainObject.composition = [];
}
});
openmct.objectViews.addProvider(new TimelistViewProvider(openmct));
openmct.inspectorViews.addProvider(new TimeListInspectorViewProvider(openmct));
+ openmct.composition.addPolicy(new TimelistCompositionPolicy(openmct).allow);
};
}
diff --git a/src/plugins/timelist/pluginSpec.js b/src/plugins/timelist/pluginSpec.js
index 3615c84f4..1a19b69ee 100644
--- a/src/plugins/timelist/pluginSpec.js
+++ b/src/plugins/timelist/pluginSpec.js
@@ -25,6 +25,7 @@ import TimelistPlugin from "./plugin";
import { TIMELIST_TYPE } from "./constants";
import Vue from 'vue';
import moment from "moment";
+import EventEmitter from "EventEmitter";
const LIST_ITEM_CLASS = '.js-table__body .js-list-item';
const LIST_ITEM_VALUE_CLASS = '.js-list-item__value';
@@ -37,6 +38,41 @@ describe('the plugin', function () {
let openmct;
let appHolder;
let originalRouterPath;
+ let mockComposition;
+ let now = Date.now();
+ let twoHoursPast = now - (1000 * 60 * 60 * 2);
+ let oneHourPast = now - (1000 * 60 * 60);
+ let twoHoursFuture = now + (1000 * 60 * 60 * 2);
+ let planObject = {
+ identifier: {
+ key: 'test-plan-object',
+ namespace: ''
+ },
+ type: 'plan',
+ id: "test-plan-object",
+ selectFile: {
+ body: JSON.stringify({
+ "TEST-GROUP": [
+ {
+ "name": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
+ "start": twoHoursPast,
+ "end": oneHourPast,
+ "type": "TEST-GROUP",
+ "color": "fuchsia",
+ "textColor": "black"
+ },
+ {
+ "name": "Sed ut perspiciatis",
+ "start": now,
+ "end": twoHoursFuture,
+ "type": "TEST-GROUP",
+ "color": "fuchsia",
+ "textColor": "black"
+ }
+ ]
+ })
+ }
+ };
beforeEach((done) => {
appHolder = document.createElement('div');
@@ -58,6 +94,13 @@ describe('the plugin', function () {
originalRouterPath = openmct.router.path;
+ mockComposition = new EventEmitter();
+ // eslint-disable-next-line require-await
+ mockComposition.load = async () => {
+ return [planObject];
+ };
+
+ spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
openmct.on('start', done);
openmct.start(appHolder);
});
@@ -112,13 +155,13 @@ describe('the plugin', function () {
sortOrderIndex: 0,
futureEventsIndex: 1,
futureEventsDurationIndex: 0,
- futureEventsDuration: 20,
+ futureEventsDuration: 0,
currentEventsIndex: 1,
currentEventsDurationIndex: 0,
- currentEventsDuration: 20,
+ currentEventsDuration: 0,
pastEventsIndex: 1,
pastEventsDurationIndex: 0,
- pastEventsDuration: 20,
+ pastEventsDuration: 0,
filter: ''
},
selectFile: {
@@ -126,16 +169,16 @@ describe('the plugin', function () {
"TEST-GROUP": [
{
"name": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
- "start": 1597170002854,
- "end": 1597171032854,
+ "start": twoHoursPast,
+ "end": oneHourPast,
"type": "TEST-GROUP",
"color": "fuchsia",
"textColor": "black"
},
{
"name": "Sed ut perspiciatis",
- "start": 1597171132854,
- "end": 1597171232854,
+ "start": now,
+ "end": twoHoursFuture,
"type": "TEST-GROUP",
"color": "fuchsia",
"textColor": "black"
@@ -171,11 +214,170 @@ describe('the plugin', function () {
const itemValues = itemEls[0].querySelectorAll(LIST_ITEM_VALUE_CLASS);
expect(itemValues.length).toEqual(4);
expect(itemValues[3].innerHTML.trim()).toEqual('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua');
- expect(itemValues[0].innerHTML.trim()).toEqual(`${moment(1597170002854).format('YYYY-MM-DD HH:mm:ss:SSS')}Z`);
- expect(itemValues[1].innerHTML.trim()).toEqual(`${moment(1597171032854).format('YYYY-MM-DD HH:mm:ss:SSS')}Z`);
+ expect(itemValues[0].innerHTML.trim()).toEqual(`${moment(twoHoursPast).format('YYYY-MM-DD HH:mm:ss:SSS')}Z`);
+ expect(itemValues[1].innerHTML.trim()).toEqual(`${moment(oneHourPast).format('YYYY-MM-DD HH:mm:ss:SSS')}Z`);
done();
});
});
});
+
+ describe('the timelist composition', () => {
+ let timelistDomainObject;
+ let timelistView;
+
+ beforeEach(() => {
+ timelistDomainObject = {
+ identifier: {
+ key: 'test-object',
+ namespace: ''
+ },
+ type: TIMELIST_TYPE,
+ id: "test-object",
+ configuration: {
+ sortOrderIndex: 0,
+ futureEventsIndex: 1,
+ futureEventsDurationIndex: 0,
+ futureEventsDuration: 0,
+ currentEventsIndex: 1,
+ currentEventsDurationIndex: 0,
+ currentEventsDuration: 0,
+ pastEventsIndex: 1,
+ pastEventsDurationIndex: 0,
+ pastEventsDuration: 0,
+ filter: ''
+ },
+ composition: [{
+ identifier: {
+ key: 'test-plan-object',
+ namespace: ''
+ }
+ }]
+ };
+
+ openmct.router.path = [timelistDomainObject];
+
+ const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]);
+ timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view');
+ let view = timelistView.view(timelistDomainObject, [timelistDomainObject]);
+ view.show(child, true);
+
+ return Vue.nextTick();
+ });
+
+ it('loads the plan from composition', () => {
+ mockComposition.emit('add', planObject);
+
+ return Vue.nextTick(() => {
+ const items = element.querySelectorAll(LIST_ITEM_CLASS);
+ expect(items.length).toEqual(2);
+ });
+ });
+ });
+
+ describe('filters', () => {
+ let timelistDomainObject;
+ let timelistView;
+
+ beforeEach(() => {
+ timelistDomainObject = {
+ identifier: {
+ key: 'test-object',
+ namespace: ''
+ },
+ type: TIMELIST_TYPE,
+ id: "test-object",
+ configuration: {
+ sortOrderIndex: 0,
+ futureEventsIndex: 1,
+ futureEventsDurationIndex: 0,
+ futureEventsDuration: 0,
+ currentEventsIndex: 1,
+ currentEventsDurationIndex: 0,
+ currentEventsDuration: 0,
+ pastEventsIndex: 1,
+ pastEventsDurationIndex: 0,
+ pastEventsDuration: 0,
+ filter: 'perspiciatis'
+ },
+ composition: [{
+ identifier: {
+ key: 'test-plan-object',
+ namespace: ''
+ }
+ }]
+ };
+
+ openmct.router.path = [timelistDomainObject];
+
+ const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]);
+ timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view');
+ let view = timelistView.view(timelistDomainObject, [timelistDomainObject]);
+ view.show(child, true);
+
+ return Vue.nextTick();
+ });
+
+ it('activities', () => {
+ mockComposition.emit('add', planObject);
+
+ return Vue.nextTick(() => {
+ const items = element.querySelectorAll(LIST_ITEM_CLASS);
+ expect(items.length).toEqual(1);
+ });
+ });
+ });
+
+ describe('time filtering - past', () => {
+ let timelistDomainObject;
+ let timelistView;
+
+ beforeEach(() => {
+ timelistDomainObject = {
+ identifier: {
+ key: 'test-object',
+ namespace: ''
+ },
+ type: TIMELIST_TYPE,
+ id: "test-object",
+ configuration: {
+ sortOrderIndex: 0,
+ futureEventsIndex: 1,
+ futureEventsDurationIndex: 0,
+ futureEventsDuration: 0,
+ currentEventsIndex: 1,
+ currentEventsDurationIndex: 0,
+ currentEventsDuration: 0,
+ pastEventsIndex: 0,
+ pastEventsDurationIndex: 0,
+ pastEventsDuration: 0,
+ filter: ''
+ },
+ composition: [{
+ identifier: {
+ key: 'test-plan-object',
+ namespace: ''
+ }
+ }]
+ };
+
+ openmct.router.path = [timelistDomainObject];
+
+ const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]);
+ timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view');
+ let view = timelistView.view(timelistDomainObject, [timelistDomainObject]);
+ view.show(child, true);
+
+ return Vue.nextTick();
+ });
+
+ it('hides past events', () => {
+ mockComposition.emit('add', planObject);
+
+ return Vue.nextTick(() => {
+ const items = element.querySelectorAll(LIST_ITEM_CLASS);
+ expect(items.length).toEqual(2);
+ });
+ });
+ });
});
diff --git a/src/plugins/timelist/timelist.scss b/src/plugins/timelist/timelist.scss
index eea87d114..6ee7a50ab 100644
--- a/src/plugins/timelist/timelist.scss
+++ b/src/plugins/timelist/timelist.scss
@@ -32,6 +32,12 @@
.c-list-item {
/* Time Lists */
+ td {
+ $p: $interiorMarginSm;
+ padding-top: $p;
+ padding-bottom: $p;
+ }
+
&.--is-current {
background-color: $colorCurrentBg;
border-top: 1px solid $colorCurrentBorder !important;
diff --git a/src/plugins/timer/actions/PauseTimerAction.js b/src/plugins/timer/actions/PauseTimerAction.js
index 532e2a7cd..d729e8b21 100644
--- a/src/plugins/timer/actions/PauseTimerAction.js
+++ b/src/plugins/timer/actions/PauseTimerAction.js
@@ -43,14 +43,19 @@ export default class PauseTimerAction {
this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration);
}
- appliesTo(objectPath) {
+ appliesTo(objectPath, view = {}) {
const domainObject = objectPath[0];
if (!domainObject || !domainObject.configuration) {
return;
}
+ // Use object configuration timerState for viewless context menus,
+ // otherwise manually show/hide based on the view's timerState
+ const viewKey = view.key;
const { timerState } = domainObject.configuration;
- return domainObject.type === 'timer' && timerState === 'started';
+ return viewKey
+ ? domainObject.type === 'timer'
+ : domainObject.type === 'timer' && timerState === 'started';
}
}
diff --git a/src/plugins/timer/actions/RestartTimerAction.js b/src/plugins/timer/actions/RestartTimerAction.js
index 81411f089..23faf4db2 100644
--- a/src/plugins/timer/actions/RestartTimerAction.js
+++ b/src/plugins/timer/actions/RestartTimerAction.js
@@ -44,14 +44,19 @@ export default class RestartTimerAction {
this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration);
}
- appliesTo(objectPath) {
+ appliesTo(objectPath, view = {}) {
const domainObject = objectPath[0];
if (!domainObject || !domainObject.configuration) {
return;
}
+ // Use object configuration timerState for viewless context menus,
+ // otherwise manually show/hide based on the view's timerState
+ const viewKey = view.key;
const { timerState } = domainObject.configuration;
- return domainObject.type === 'timer' && timerState !== 'stopped';
+ return viewKey
+ ? domainObject.type === 'timer'
+ : domainObject.type === 'timer' && timerState !== 'stopped';
}
}
diff --git a/src/plugins/timer/actions/StartTimerAction.js b/src/plugins/timer/actions/StartTimerAction.js
index 8313fba4c..cdd01f3e6 100644
--- a/src/plugins/timer/actions/StartTimerAction.js
+++ b/src/plugins/timer/actions/StartTimerAction.js
@@ -63,14 +63,19 @@ export default class StartTimerAction {
newConfiguration.pausedTime = undefined;
this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration);
}
- appliesTo(objectPath) {
+ appliesTo(objectPath, view = {}) {
const domainObject = objectPath[0];
if (!domainObject || !domainObject.configuration) {
return;
}
+ // Use object configuration timerState for viewless context menus,
+ // otherwise manually show/hide based on the view's timerState
+ const viewKey = view.key;
const { timerState } = domainObject.configuration;
- return domainObject.type === 'timer' && timerState !== 'started';
+ return viewKey
+ ? domainObject.type === 'timer'
+ : domainObject.type === 'timer' && timerState !== 'started';
}
}
diff --git a/src/plugins/timer/actions/StopTimerAction.js b/src/plugins/timer/actions/StopTimerAction.js
index badf4b2bf..6ed672bb5 100644
--- a/src/plugins/timer/actions/StopTimerAction.js
+++ b/src/plugins/timer/actions/StopTimerAction.js
@@ -44,14 +44,19 @@ export default class StopTimerAction {
this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration);
}
- appliesTo(objectPath) {
+ appliesTo(objectPath, view = {}) {
const domainObject = objectPath[0];
if (!domainObject || !domainObject.configuration) {
return;
}
+ // Use object configuration timerState for viewless context menus,
+ // otherwise manually show/hide based on the view's timerState
+ const viewKey = view.key;
const { timerState } = domainObject.configuration;
- return domainObject.type === 'timer' && timerState !== 'stopped';
+ return viewKey
+ ? domainObject.type === 'timer'
+ : domainObject.type === 'timer' && timerState !== 'stopped';
}
}
diff --git a/src/plugins/timer/components/Timer.vue b/src/plugins/timer/components/Timer.vue
index 6154eac9e..b137649f4 100644
--- a/src/plugins/timer/components/Timer.vue
+++ b/src/plugins/timer/components/Timer.vue
@@ -179,6 +179,15 @@ export default {
return timerSign;
}
},
+ watch: {
+ timerState() {
+ if (!this.viewActionsCollection) {
+ return;
+ }
+
+ this.showOrHideAvailableActions();
+ }
+ },
mounted() {
this.$nextTick(() => {
if (this.configuration && this.configuration.timerState === undefined) {
@@ -190,6 +199,9 @@ export default {
this.unlisten = ticker.listen(() => {
this.openmct.objects.refresh(this.domainObject);
});
+
+ this.viewActionsCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView);
+ this.showOrHideAvailableActions();
});
},
beforeDestroy() {
@@ -228,6 +240,22 @@ export default {
if (action) {
action.invoke(this.objectPath, this.currentView);
}
+ },
+ showOrHideAvailableActions() {
+ switch (this.timerState) {
+ case 'started':
+ this.viewActionsCollection.hide(['timer.start']);
+ this.viewActionsCollection.show(['timer.stop', 'timer.pause', 'timer.restart']);
+ break;
+ case 'paused':
+ this.viewActionsCollection.hide(['timer.pause']);
+ this.viewActionsCollection.show(['timer.stop', 'timer.start', 'timer.restart']);
+ break;
+ case 'stopped':
+ this.viewActionsCollection.hide(['timer.stop', 'timer.pause', 'timer.restart']);
+ this.viewActionsCollection.show(['timer.start']);
+ break;
+ }
}
}
};
diff --git a/src/plugins/viewDatumAction/pluginSpec.js b/src/plugins/viewDatumAction/pluginSpec.js
index 8133e8413..954172f9f 100644
--- a/src/plugins/viewDatumAction/pluginSpec.js
+++ b/src/plugins/viewDatumAction/pluginSpec.js
@@ -78,13 +78,15 @@ describe("the plugin", () => {
describe('when invoked', () => {
- beforeEach((done) => {
+ beforeEach(() => {
openmct.overlays.overlay = function (options) {};
spyOn(openmct.overlays, 'overlay');
viewDatumAction.invoke(mockObjectPath, mockView);
+ });
+ it('creates an overlay', () => {
expect(openmct.overlays.overlay).toHaveBeenCalled();
});
});
diff --git a/src/plugins/webPage/pluginSpec.js b/src/plugins/webPage/pluginSpec.js
new file mode 100644
index 000000000..eab1c968b
--- /dev/null
+++ b/src/plugins/webPage/pluginSpec.js
@@ -0,0 +1,106 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 { createOpenMct, resetApplicationState } from "utils/testing";
+import WebPagePlugin from "./plugin";
+
+function getView(openmct, domainObj, objectPath) {
+ const applicableViews = openmct.objectViews.get(domainObj, objectPath);
+ const webpageView = applicableViews.find((viewProvider) => viewProvider.key === 'webPage');
+
+ return webpageView.view(domainObj);
+}
+
+function destroyView(view) {
+ return view.destroy();
+}
+
+describe("The web page plugin", function () {
+ let mockDomainObject;
+ let mockDomainObjectPath;
+ let openmct;
+ let element;
+ let child;
+ let view;
+
+ beforeEach((done) => {
+ mockDomainObjectPath = [
+ {
+ name: 'mock webpage',
+ type: 'webpage',
+ identifier: {
+ key: 'mock-webpage',
+ namespace: ''
+ }
+ }
+ ];
+
+ mockDomainObject = {
+ displayFormat: "",
+ name: "Unnamed WebPage",
+ type: "webPage",
+ location: "f69c21ac-24ef-450c-8e2f-3d527087d285",
+ modified: 1627483839783,
+ url: "123",
+ displayText: "123",
+ persisted: 1627483839783,
+ id: "3d9c243d-dffb-446b-8474-d9931a99d679",
+ identifier: {
+ namespace: "",
+ key: "3d9c243d-dffb-446b-8474-d9931a99d679"
+ }
+ };
+
+ openmct = createOpenMct();
+ openmct.install(new WebPagePlugin());
+
+ element = document.createElement('div');
+ element.style.width = '640px';
+ element.style.height = '480px';
+ child = document.createElement('div');
+ child.style.width = '640px';
+ child.style.height = '480px';
+ element.appendChild(child);
+
+ openmct.on('start', done);
+ openmct.startHeadless();
+
+ });
+
+ afterEach(() => {
+ destroyView(view);
+
+ return resetApplicationState(openmct);
+ });
+
+ describe('the view', () => {
+ beforeEach(() => {
+ view = getView(openmct, mockDomainObject, mockDomainObjectPath);
+ view.show(child, true);
+ });
+
+ it('provides a view', () => {
+ expect(view).toBeDefined();
+ });
+ });
+
+});
diff --git a/src/styles/_constants-espresso.scss b/src/styles/_constants-espresso.scss
index 1aac3734b..684a3a7fe 100644
--- a/src/styles/_constants-espresso.scss
+++ b/src/styles/_constants-espresso.scss
@@ -94,7 +94,7 @@ $shellPanePad: $interiorMargin, 7px;
$drawerBg: lighten($colorBodyBg, 5%);
$drawerFg: lighten($colorBodyFg, 5%);
$sideBarBg: $drawerBg;
-$sideBarHeaderBg: rgba($colorBodyFg, 0.2);
+$sideBarHeaderBg: rgba($colorBodyFg, 0.1);
$sideBarHeaderFg: rgba($colorBodyFg, 0.7);
// Status colors, mainly used for messaging and item ancillary symbols
@@ -164,7 +164,7 @@ $borderMissing: 1px dashed $colorAlert !important;
$editUIColor: $uiColor; // Base color
$editUIColorBg: $editUIColor;
$editUIColorFg: #fff;
-$editUIColorHov: pullForward(saturate($uiColor, 10%), 20%); // Hover color when $editUIColor is applied as a base color
+$editUIColorHov: pullForward(saturate($uiColor, 10%), 10%); // Hover color when $editUIColor is applied as a base color
$editUIBaseColor: #344b8d; // Base color, toolbar bg
$editUIBaseColorHov: pullForward($editUIBaseColor, 20%);
$editUIBaseColorFg: #ffffff; // Toolbar button icon colors, etc.
@@ -178,11 +178,10 @@ $editFrameColor: $browseFrameColor; // Solid or dotted border applied to non-sel
$editFrameBorder: 1px dotted $editFrameColor;
$editFrameColorHov: $editUIColor; // Solid border hover on frames; hover should not be applied to selected objects
$editFrameBorderHov: 1px solid $editFrameColorHov; // Hover on selectable frames
-$editFrameColorSelected: #ccc; // Border of selected frames
+$editFrameColorSelected: #ffefc2; // Border of selected frames while editing
$editFrameColorHandleBg: $colorBodyBg; // Resize handle 'offset' color to make handle standout
$editFrameColorHandleFg: $editFrameColorSelected; // Resize handle main color
$editFrameSelectedShdw: rgba(black, 0.4) 0 1px 5px 1px;
-$editFrameSelectedBorder: 1px solid $editFrameColorHov; // Selected frame element
$editFrameMovebarColorBg: $editFrameColor; // Movebar bg color
$editFrameMovebarColorFg: pullForward($editFrameMovebarColorBg, 20%); // Grippy lines, container size text
$editFrameHovMovebarColorBg: pullForward($editFrameMovebarColorBg, 10%); // Hover style
@@ -191,6 +190,7 @@ $editFrameSelectedMovebarColorBg: pullForward($editFrameMovebarColorBg, 15%); //
$editFrameSelectedMovebarColorFg: pullForward($editFrameMovebarColorFg, 15%);
$editFrameMovebarH: 10px; // Height of move bar in layout frame
$editMarqueeBorder: 1px dashed $editFrameColorSelected;
+$editFrameSelectedBorder: $editMarqueeBorder; // Selected frame element
// Icons
$colorIconAlias: #4af6f3;
@@ -245,7 +245,8 @@ $colorMenuHovBg: rgba($colorKey, 0.5);
$colorMenuHovFg: $colorBodyFgEm;
$colorMenuHovIc: $colorMenuHovFg;
$colorMenuElementHilite: pullForward($colorMenuBg, 10%);
-$shdwMenu: rgba(black, 0.5) 0 1px 5px;
+$shdwMenu: rgba(black, 0.8) 0 2px 10px;
+$shdwMenuInner: inset 0 0 0 1px rgba(white, 0.2);
$shdwMenuText: none;
$menuItemPad: $interiorMargin, floor($interiorMargin * 1.25);
@@ -269,7 +270,6 @@ $colorFormSectionHeaderBg: rgba(#000, 0.1);
$colorFormSectionHeaderFg: rgba($colorBodyFg, 0.8);
$colorInputBg: rgba(black, 0.2);
$colorInputFg: $colorBodyFg;
-$colorInputPlaceholder: pushBack($colorBodyFg, 20%);
$colorFormText: pushBack($colorBodyFg, 10%);
$colorInputIcon: pushBack($colorBodyFg, 25%);
$colorFieldHint: pullForward($colorBodyFg, 40%);
diff --git a/src/styles/_constants-maelstrom.scss b/src/styles/_constants-maelstrom.scss
index 510239333..9077592ea 100644
--- a/src/styles/_constants-maelstrom.scss
+++ b/src/styles/_constants-maelstrom.scss
@@ -168,7 +168,7 @@ $borderMissing: 1px dashed $colorAlert !important;
$editUIColor: $uiColor; // Base color
$editUIColorBg: $editUIColor;
$editUIColorFg: #fff;
-$editUIColorHov: pullForward(saturate($uiColor, 10%), 20%); // Hover color when $editUIColor is applied as a base color
+$editUIColorHov: pullForward(saturate($uiColor, 10%), 10%); // Hover color when $editUIColor is applied as a base color
$editUIBaseColor: #344b8d; // Base color, toolbar bg
$editUIBaseColorHov: pullForward($editUIBaseColor, 20%);
$editUIBaseColorFg: #ffffff; // Toolbar button icon colors, etc.
@@ -182,11 +182,10 @@ $editFrameColor: $browseFrameColor; // Solid or dotted border applied to non-sel
$editFrameBorder: 1px dotted $editFrameColor;
$editFrameColorHov: $editUIColor; // Solid border hover on frames; hover should not be applied to selected objects
$editFrameBorderHov: 1px solid $editFrameColorHov; // Hover on selectable frames
-$editFrameColorSelected: #ccc; // Border of selected frames
+$editFrameColorSelected: #ffefc2; // Border of selected frames while editing
$editFrameColorHandleBg: $colorBodyBg; // Resize handle 'offset' color to make handle standout
$editFrameColorHandleFg: $editFrameColorSelected; // Resize handle main color
$editFrameSelectedShdw: rgba(black, 0.4) 0 1px 5px 1px;
-$editFrameSelectedBorder: 1px solid $editFrameColorHov; // Selected frame element
$editFrameMovebarColorBg: $editFrameColor; // Movebar bg color
$editFrameMovebarColorFg: pullForward($editFrameMovebarColorBg, 20%); // Grippy lines, container size text
$editFrameHovMovebarColorBg: pullForward($editFrameMovebarColorBg, 10%); // Hover style
@@ -195,6 +194,7 @@ $editFrameSelectedMovebarColorBg: pullForward($editFrameMovebarColorBg, 15%); //
$editFrameSelectedMovebarColorFg: pullForward($editFrameMovebarColorFg, 15%);
$editFrameMovebarH: 10px; // Height of move bar in layout frame
$editMarqueeBorder: 1px dashed $editFrameColorSelected;
+$editFrameSelectedBorder: $editMarqueeBorder; // Selected frame element
// Icons
$colorIconAlias: #4af6f3;
@@ -249,7 +249,8 @@ $colorMenuHovBg: rgba($colorKey, 0.5);
$colorMenuHovFg: $colorBodyFgEm;
$colorMenuHovIc: $colorMenuHovFg;
$colorMenuElementHilite: pullForward($colorMenuBg, 10%);
-$shdwMenu: rgba(black, 0.5) 0 1px 5px;
+$shdwMenu: rgba(black, 0.8) 0 2px 10px;
+$shdwMenuInner: inset 0 0 0 1px rgba(white, 0.2);
$shdwMenuText: none;
$menuItemPad: $interiorMargin, floor($interiorMargin * 1.25);
@@ -273,7 +274,6 @@ $colorFormSectionHeaderBg: rgba(#000, 0.1);
$colorFormSectionHeaderFg: rgba($colorBodyFg, 0.8);
$colorInputBg: rgba(black, 0.2);
$colorInputFg: $colorBodyFg;
-$colorInputPlaceholder: pushBack($colorBodyFg, 20%);
$colorFormText: pushBack($colorBodyFg, 10%);
$colorInputIcon: pushBack($colorBodyFg, 25%);
$colorFieldHint: pullForward($colorBodyFg, 40%);
diff --git a/src/styles/_constants-snow.scss b/src/styles/_constants-snow.scss
index 255a63748..1e88d4928 100644
--- a/src/styles/_constants-snow.scss
+++ b/src/styles/_constants-snow.scss
@@ -178,11 +178,10 @@ $editFrameColor: $browseFrameColor; // Solid or dotted border applied to non-sel
$editFrameBorder: 1px dotted $editFrameColor;
$editFrameColorHov: $editUIColor; // Solid border hover on frames; hover should not be applied to selected objects
$editFrameBorderHov: 1px solid $editFrameColorHov; // Hover on selectable frames
-$editFrameColorSelected: #333; // Border of selected frames
+$editFrameColorSelected: #ff7c00; // Border of selected frames
$editFrameColorHandleBg: $colorBodyBg; // Resize handle 'offset' color to make handle standout
$editFrameColorHandleFg: $editFrameColorSelected; // Resize handle main color
$editFrameSelectedShdw: rgba(black, 0.5) 0 1px 5px 2px;
-$editFrameSelectedBorder: 1px dashed $editFrameColorSelected; // Selected frame element
$editFrameMovebarColorBg: $editFrameColor; // Movebar bg color
$editFrameMovebarColorFg: pullForward($editFrameMovebarColorBg, 20%); // Grippy lines, container size text
$editFrameHovMovebarColorBg: pullForward($editFrameMovebarColorBg, 10%); // Hover style
@@ -191,6 +190,7 @@ $editFrameSelectedMovebarColorBg: pullForward($editFrameMovebarColorBg, 15%); //
$editFrameSelectedMovebarColorFg: pullForward($editFrameMovebarColorFg, 15%);
$editFrameMovebarH: 10px; // Height of move bar in layout frame
$editMarqueeBorder: 1px dashed $editFrameColorSelected;
+$editFrameSelectedBorder: 1px dashed $editMarqueeBorder; // Selected frame element
// Icons
$colorIconAlias: #4af6f3;
@@ -245,7 +245,8 @@ $colorMenuHovBg: $colorMenuIc;
$colorMenuHovFg: $colorMenuBg;
$colorMenuHovIc: $colorMenuBg;
$colorMenuElementHilite: darken($colorMenuBg, 10%);
-$shdwMenu: rgba(black, 0.5) 0 1px 5px;
+$shdwMenu: rgba(black, 0.8) 0 2px 10px;
+$shdwMenuInner: none;
$shdwMenuText: none;
$menuItemPad: $interiorMargin, floor($interiorMargin * 1.25);
@@ -269,7 +270,6 @@ $colorFormSectionHeaderBg: rgba(#000, 0.05);
$colorFormSectionHeaderFg: rgba($colorBodyFg, 0.5);
$colorInputBg: $colorGenBg;
$colorInputFg: $colorBodyFg;
-$colorInputPlaceholder: pushBack($colorBodyFg, 20%);
$colorFormText: pushBack($colorBodyFg, 10%);
$colorInputIcon: pushBack($colorBodyFg, 25%);
$colorFieldHint: pullForward($colorBodyFg, 40%);
diff --git a/src/styles/_constants.scss b/src/styles/_constants.scss
index 82ac76fcc..c6db00163 100755
--- a/src/styles/_constants.scss
+++ b/src/styles/_constants.scss
@@ -40,9 +40,10 @@ $inputTextP: $inputTextPTopBtm $inputTextPLeftRight;
$menuLineH: 1.5rem;
$treeItemIndent: 16px;
$treeTypeIconW: 18px;
-$overlayOuterMarginFullscreen: 0;
-$overlayOuterMarginLarge: 10px;
-$overlayOuterMarginDialog: 20%;
+$overlayOuterMarginFullscreen: (1%, 1%);
+$overlayOuterMarginLarge: (10px, 10px);
+$overlayOuterMarginSmall: (30%, 20%);
+$overlayOuterMarginDialog: (5%, 20%);
$overlayInnerMargin: 25px;
$mainViewPad: 0px;
$treeNavArrowD: 20px;
@@ -156,6 +157,13 @@ $glyph-icon-notebook-page: '\e92c';
$glyph-icon-unlocked: '\e92d';
$glyph-icon-circle: '\e92e';
$glyph-icon-draft: '\e92f';
+$glyph-icon-circle-slash: '\e930';
+$glyph-icon-question-mark: '\e931';
+$glyph-icon-status-poll-check: '\e932';
+$glyph-icon-status-poll-caution: '\e933';
+$glyph-icon-status-poll-circle-slash: '\e934';
+$glyph-icon-status-poll-question-mark: '\e935';
+$glyph-icon-status-poll-edit: '\e936';
$glyph-icon-arrows-right-left: '\ea00';
$glyph-icon-arrows-up-down: '\ea01';
$glyph-icon-bullet: '\ea02';
@@ -264,6 +272,8 @@ $glyph-icon-bar-chart: '\eb2c';
$glyph-icon-map: '\eb2d';
$glyph-icon-plan: '\eb2e';
$glyph-icon-timelist: '\eb2f';
+$glyph-icon-notebook-shift-log: '\eb31';
+$glyph-icon-plot-scatter: '\eb30';
/************************** GLYPHS AS DATA URI */
// Only objects have been converted, for use in Create menu and folder views
@@ -271,9 +281,6 @@ $bg-icon-alert-rect: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://
$bg-icon-alert-triangle: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M499.1 424.4L287.8 54.6c-17.5-30.6-46-30.6-63.5 0L12.9 424.4C-4.6 455 9.9 480 45.1 480h421.7c35.3 0 49.8-25 32.3-55.6zM288 448h-64v-64h64v64zm10.9-192L280 352h-48l-18.9-96V128H299v128z' fill='%23000000'/%3e%3c/svg%3e");
$bg-icon-bell: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cg fill='%23000000'%3e%3cpath d='M256 512c53 0 96-43 96-96H160c0 53 43 96 96 96zM448 224v-32C448 86 362 0 256 0S64 86 64 192v32c0 35.3-28.7 64-64 64v64h512v-64c-35.3 0-64-28.7-64-64z'/%3e%3c/g%3e%3c/svg%3e");
$bg-icon-info: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256 256-114.6 256-256S397.4 0 256 0zm0 64c35.3 0 64 28.7 64 64s-28.7 64-64 64-64-28.7-64-64 28.7-64 64-64zm96 352H160v-64h32V224h128v128h32v64z' fill='%23000000'/%3e%3c/svg%3e");
-$bg-icon-activity: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M288 32H160l160 160H174.872C152.74 153.742 111.377 128 64 128H0v256h64c47.377 0 88.74-25.742 110.872-64H320L160 480h128l224-224L288 32z'/%3e%3c/svg%3e");
-$bg-icon-activity-mode: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M256 0C148.6 0 56.6 66.2 18.6 160H64c28.4 0 54 12.4 71.5 32H256l-96-96h128l160 160-160 160H160l96-96H135.5C118 339.6 92.4 352 64 352H18.6c38 93.8 129.9 160 237.4 160 141.4 0 256-114.6 256-256S397.4 0 256 0z'/%3e%3c/svg%3e");
-$bg-icon-autoflow-tabular: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M96 0C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h32V0H96zM192 0h128v512H192zM416 0h-32v352h128V96c0-52.8-43.2-96-96-96z'/%3e%3c/svg%3e");
$bg-icon-plus: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M480,192H320V32A32.1,32.1,0,0,0,288,0H224a32.1,32.1,0,0,0-32,32V192H32A32.1,32.1,0,0,0,0,224v64a32.1,32.1,0,0,0,32,32H192V480a32.1,32.1,0,0,0,32,32h64a32.1,32.1,0,0,0,32-32V320H480a32.1,32.1,0,0,0,32-32V224A32.1,32.1,0,0,0,480,192Z' transform='translate(0)'/%3e%3c/svg%3e");
$bg-icon-grippy-ew: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M416 0v512h-64V0zM288 0v512h-64V0zM160 0v512H96V0z'/%3e%3c/svg%3e");
$bg-icon-chain-links: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M479.2 32.8C457.3 10.9 428.7 0 400 0c-28.7 0-57.3 10.9-79.2 32.8l-64 64c-37 37-42.7 93.5-17 136.5l-6.4 6.4C215.7 229.3 195.9 224 176 224c-28.7 0-57.3 10.9-79.2 32.8l-64 64c-43.7 43.7-43.7 114.7 0 158.4C54.7 501.1 83.3 512 112 512c28.7 0 57.3-10.9 79.2-32.8l64-64c37-37 42.7-93.5 17-136.5l6.4-6.4c17.6 10.5 37.5 15.8 57.3 15.8 28.7 0 57.3-10.9 79.2-32.8l64-64c43.8-43.8 43.8-114.8.1-158.5zM209.9 369.9l-64 64c-9 9.1-21.1 14.1-33.9 14.1-12.8 0-24.9-5-33.9-14.1-18.7-18.7-18.7-49.2 0-67.9l64-64c9.1-9.1 21.1-14.1 33.9-14.1 2.8 0 5.6.3 8.4.7l-27.8 27.8c-5.2 5.2-8.1 12.1-8.1 19.4s2.9 14.3 8.1 19.4c5.2 5.2 12.1 8.1 19.4 8.1s14.3-2.9 19.4-8.1l27.8-27.8c2.7 15.2-1.8 31.1-13.3 42.5zm224-224l-64 64c-9 9.1-21.1 14.1-33.9 14.1-2.8 0-5.6-.3-8.4-.7l27.8-27.8c5.2-5.2 8.1-12.1 8.1-19.4s-2.9-14.3-8.1-19.4c-5.2-5.2-12.1-8.1-19.4-8.1s-14.3 2.9-19.4 8.1l-27.8 27.8c-2.6-14.9 1.8-30.8 13.3-42.3l64-64C375.1 69 387.2 64 400 64s24.9 5 33.9 14.1c18.8 18.7 18.8 49.1 0 67.8z'/%3e%3c/svg%3e");
@@ -296,19 +303,17 @@ $bg-icon-session: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www
$bg-icon-tabular: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 0H64C28.8 0 0 28.8 0 64v384c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V64c0-35.2-28.8-64-64-64zM320 224H192v-96h128v96zm-128 32h128v96H192v-96zm-32 96H32v-96h128v96zm0-224v96H32v-96h128zM64 480c-8.5 0-16.5-3.3-22.6-9.4S32 456.5 32 448v-64h128v96H64zm128 0v-96h128v96H192zm288-32c0 8.5-3.3 16.5-9.4 22.6S456.5 480 448 480h-96v-96h128v64zm0-96H352v-96h128v96zm0-128H352v-96h128v96z'/%3e%3c/svg%3e");
$bg-icon-tabular-lad: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 0H64C28.7.1.1 28.7 0 64v384c.1 35.3 28.7 63.9 64 64h384c35.3-.1 63.9-28.7 64-64V64c-.1-35.3-28.7-63.9-64-64zM32 128h128v96H32v-96zm0 128h128v96H32v-96zm32 224c-17.6-.1-31.9-14.4-32-32v-64h128v96H64zm128 0v-96h128v96H192zm288-32c-.1 17.6-14.4 31.9-32 32h-96v-96h128v64zm0-192v96H192v-96h32v-32h-32v-96h288v96h-32v32h32z'/%3e%3cpath fill='%23000000' d='M391.2 273.7L336 246.1V160c0-8.8-7.2-16-16-16s-16 7.2-16 16v105.9l72.8 36.4c7.9 4 17.5.8 21.5-7.2 4-7.8.8-17.5-7.1-21.4z'/%3e%3c/svg%3e");
$bg-icon-tabular-lad-set: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M64 384V96c-35.3.1-63.9 28.7-64 64v288c.1 35.3 28.7 63.9 64 64h288c35.3-.1 63.9-28.7 64-64H128c-35.3-.1-63.9-28.7-64-64z'/%3e%3cpath fill='%23000000' d='M448 0H160c-35.3.1-63.9 28.7-64 64v288c.1 35.3 28.7 63.9 64 64h288c35.3-.1 63.9-28.7 64-64V64c-.1-35.3-28.7-63.9-64-64zM128 96h96v64h-96V96zm0 96h96v96h-96v-96zm32 192c-17.6-.1-31.9-14.4-32-32v-32h96v64h-64zm96 0v-64h96v64h-96zm224-32c-.1 17.6-14.4 31.9-32 32h-64v-64h96v32zm0-64H256V96h224v192z'/%3e%3cpath fill='%23000000' d='M416 240c8.8 0 16-7.2 16-16 0-6.9-4.4-13-10.9-15.2L384 196.5V144c0-8.8-7.2-16-16-16s-16 7.2-16 16v75.5l58.9 19.6c1.7.6 3.4.9 5.1.9z'/%3e%3c/svg%3e");
-$bg-icon-tabular-realtime: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M0 64v384c0 35.2 28.8 64 64 64h288c35.2 0 64-28.8 64-64V340c-19.8 7.8-41.4 12-64 12-35.4 0-68.4-10.5-96-28.6V352h-96v-96h35.3c-5.2-10.1-9.4-20.8-12.6-32H160v-96h22.7C203.6 54.2 271.6 0 352 0H64C28.8 0 0 28.8 0 64zm288 320h96v64c0 8.5-3.3 16.5-9.4 22.6S360.5 480 352 480h-64v-96zm-160 96H64c-8.5 0-16.5-3.3-22.6-9.4S32 456.5 32 448v-64h96v96zm0-128H32v-96h96v96zm32 32h96v96h-96v-96zm-32-160H32v-96h96v96z'/%3e%3cpath fill='%23000000' d='M192 160c0 88.4 71.6 160 160 160s160-71.6 160-160S440.4 0 352 0 192 71.6 192 160zm49.7 39.8L227 187.5c-1.4-6.4-2.3-12.9-2.7-19.6 15.1-.1 30.1-5 41.9-14.8l39.6-33c7.5-6.2 21.1-6.2 28.6 0l39.6 33c2.8 2.3 5.7 4.3 8.8 6.1-23-11.7-52.7-9.2-72.8 7.5l-39.6 33c-7.6 6.3-21.2 6.3-28.7.1zM352 288c-36.7 0-69.7-15.4-93-40.1 14.2-.6 28.1-5.5 39.2-14.7l39.6-33c7.5-6.2 21.1-6.2 28.6 0l39.6 33c11 9.2 25 14.1 39.2 14.7-23.5 24.7-56.5 40.1-93.2 40.1zm125.9-151.3c1.4 7.5 2.1 15.3 2.1 23.3 0 9.4-1 18.6-3 27.5l-14.7 12.3c-7.5 6.2-21.1 6.2-28.6 0l-39.6-33c-2.8-2.3-5.7-4.3-8.8-6.1 23 11.7 52.7 9.2 72.8-7.5l19.8-16.5zM352 32c46.4 0 87.1 24.7 109.5 61.7l-31.2 26c-7.5 6.2-21.1 6.2-28.6 0l-39.6-33c-23.6-19.7-60.6-19.7-84.3 0l-39.6 33c-2.5 2.1-5.7 3.5-9.1 4.2C244.7 70.8 293.8 32 352 32z'/%3e%3c/svg%3e");
-$bg-icon-tabular-scrolling: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M32 0C14.4 0 0 14.4 0 32v96h224V0H32zM512 128V32c0-17.6-14.4-32-32-32H288v128h224zM0 192v96c0 17.6 14.4 32 32 32h192V192H0zM480 320c17.6 0 32-14.4 32-32v-96H288v128h192zM256 512L128 384h256z'/%3e%3c/svg%3e");
+$bg-icon-tabular-scrolling: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M448 0H64A64.19 64.19 0 0 0 0 64v384a64.19 64.19 0 0 0 64 64h384a64.19 64.19 0 0 0 64-64V64a64.19 64.19 0 0 0-64-64Zm-64 128v96h-96v-96Zm-96 128h96v96h-96Zm-32 96h-96v-96h96Zm0-224v96h-96v-96Zm-224 0h96v96H32Zm0 128h96v96H32Zm32 224a32.2 32.2 0 0 1-32-32v-64h96v96Zm96 0v-96h96v96Zm192 0h-64v-96h96v96Zm118.57-9.43A31.74 31.74 0 0 1 448 480h-32v-32h64a31.74 31.74 0 0 1-9.43 22.57ZM480 384h-64V128h64Z'/%3e%3c/svg%3e");
$bg-icon-telemetry: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M16 315.83c7.14-2.81 27.22-23.77 46.48-73C83.71 188.64 120.64 124 176 124c26.2 0 50.71 14.58 72.85 43.34 18.67 24.25 32.42 54.46 40.67 75.54 19.26 49.19 39.34 70.15 46.48 73 7.14-2.81 27.22-23.77 46.48-73 18.7-47.75 49.57-103.57 94.47-116.23A255.87 255.87 0 0 0 256 0C114.62 0 0 114.62 0 256a257.18 257.18 0 0 0 5 50.52c4.77 5.39 8.61 8.37 11 9.31z'/%3e%3cpath fill='%23000000' d='M496 196.17c-7.14 2.81-27.22 23.76-46.48 73C428.29 323.36 391.36 388 336 388c-26.2 0-50.71-14.58-72.85-43.34-18.67-24.25-32.42-54.46-40.67-75.54-19.26-49.19-39.34-70.15-46.48-73-7.14 2.81-27.22 23.76-46.48 73-18.7 47.75-49.57 103.57-94.47 116.23A255.87 255.87 0 0 0 256 512c141.38 0 256-114.62 256-256a257.18 257.18 0 0 0-5-50.52c-4.77-5.39-8.61-8.37-11-9.31z'/%3e%3c/svg%3e");
$bg-icon-timeline: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M416 0H96C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V96c0-52.8-43.2-96-96-96ZM64 160V96h128v64Zm64 64h192v64H128Zm320 192H224v-64h224Zm0-128h-64v-64h64Zm0-128H256V96h192Z'/%3e%3c/svg%3e");
$bg-icon-timer: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M288 73.3V32.01a32 32 0 0 0-32-32h-64a32 32 0 0 0-32 32V73.3C67.48 100.84 0 186.54 0 288.01c0 123.71 100.29 224 224 224s224-100.29 224-224c0-101.48-67.5-187.2-160-214.71zm-54 224.71l-131.88 105.5A167.4 167.4 0 0 1 56 288.01c0-92.64 75.36-168 168-168 3.36 0 6.69.11 10 .31v177.69z'/%3e%3c/svg%3e");
-$bg-icon-topic: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M227.18 238.32l43.15-43.15a25.18 25.18 0 0 1 35.36 0l43.15 43.15a94.42 94.42 0 0 0 35.18 22.25V174.5l-28.82-28.82a95.11 95.11 0 0 0-134.35 0l-43.15 43.15a25.18 25.18 0 0 1-35.36 0L128 174.5v86.07a95.11 95.11 0 0 0 99.18-22.25z'/%3e%3cpath fill='%23000000' d='M252.82 273.68l-43.15 43.15a25.18 25.18 0 0 1-35.36 0l-43.15-43.15c-1-1-2.1-2-3.18-3v98.68a95.11 95.11 0 0 0 131.18-3l43.15-43.15a25.18 25.18 0 0 1 35.36 0l43.15 43.15c1 1 2.1 2 3.18 3v-98.68a95.11 95.11 0 0 0-131.18 3z'/%3e%3cpath fill='%23000000' d='M416 0h-64v96h63.83l.17.17v319.66l-.17.17H352v96h64c52.8 0 96-43.2 96-96V96c0-52.8-43.2-96-96-96zM160 416H96.17l-.17-.17V96.17l.17-.17H160V0H96C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h64v-96z'/%3e%3c/svg%3e");
$bg-icon-box-with-dashed-lines: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M0 192h64v128H0zM64 64.11l.11-.11H160V0H64A64.19 64.19 0 0 0 0 64v96h64V64.11zM64 447.89V352H0v96a64.19 64.19 0 0 0 64 64h96v-64H64.11zM192 0h128v64H192zM448 447.89l-.11.11H352v64h96a64.19 64.19 0 0 0 64-64v-96h-64v95.89zM448 0h-96v64h95.89l.11.11V160h64V64a64.19 64.19 0 0 0-64-64zM448 192h64v128h-64zM192 448h128v64H192zM128 128h256v256H128z'/%3e%3c/svg%3e");
-$bg-icon-summary-widget: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 0H64C28.8 0 0 28.8 0 64v384c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V64c0-35.2-28.8-64-64-64zm-24.1 305.2l-41.3 71.6-94.8-65.8 9.6 115h-82.7l9.6-115-94.8 65.8-41.3-71.6L192.5 256 88.1 206.8l41.3-71.6 94.8 65.8-9.6-115h82.7l-9.6 115 94.8-65.8 41.3 71.6L319.5 256l104.4 49.2z'/%3e%3c/svg%3e");
-$bg-icon-notebook: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 55.4c0-39.9-27.7-63.7-61.5-52.7L0 128h448V55.4zM448 160H0v288c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V224c0-35.2-28.8-64-64-64zm-32 256H224V256h192v160z'/%3e%3c/svg%3e");
+$bg-icon-summary-widget: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M416 0H96C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V96c0-52.8-43.2-96-96-96zM256 384L64 256l192-128 192 128z'/%3e%3c/svg%3e");
+$bg-icon-notebook: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' xml:space='preserve'%3e%3cpath d='M448 55.4c0-39.9-27.7-63.7-61.5-52.7L0 128h448V55.4zM448 160H0v288c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V224c0-35.2-28.8-64-64-64zm-32 256H224V256h192v160z'/%3e%3c/svg%3e");
$bg-icon-tabs-view: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M0 448a64.2 64.2 0 0 0 64 64h384a64.2 64.2 0 0 0 64-64V144H256L230.9 31.2C227.1 14.1 209.6 0 192 0H64A64.2 64.2 0 0 0 0 64zm416-64H96V256h320z'/%3e%3cpath d='M240 0c17.6 0 35.1 14.1 38.9 31.2l18 80.8H512V64a64.2 64.2 0 0 0-64-64z'/%3e%3c/svg%3e");
$bg-icon-flexible-layout: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M0 416c0 52.8 43.2 96 96 96h32V224H0zM0 96v64h128V0H96C43.2 0 0 43.2 0 96zM384 512h32c52.8 0 96-43.2 96-96v-64H384zM192 0h128v512H192zM416 0h-32v288h128V96c0-52.8-43.2-96-96-96z'/%3e%3c/svg%3e");
$bg-icon-generator-telemetry: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M76 236.9c5.4-2.1 20.4-17.8 34.9-54.7C126.8 141.5 154.5 93 196 93c19.7 0 38 10.9 54.6 32.5 14 18.2 24.4 40.8 30.5 56.7 14.5 36.9 29.5 52.6 34.9 54.7 5.4-2.1 20.4-17.8 34.9-54.7S388 104.5 421.7 95A192 192 0 0 0 256 0C150 0 64 86 64 192a197.2 197.2 0 0 0 3.7 37.9c3.6 4 6.5 6.3 8.3 7zM442.3 238.5A192.9 192.9 0 0 0 448 192a197.2 197.2 0 0 0-3.7-37.9c-3.6-4-6.5-6.3-8.3-7-5.4 2.1-20.4 17.8-34.9 54.7-10.9 27.9-27.3 59.5-50 76.6z'/%3e%3cpath d='M256 320l67.5-29.5a60.3 60.3 0 0 1-7.5.5c-19.7 0-38-10.9-54.6-32.5-14-18.2-24.4-40.8-30.5-56.7-14.5-36.9-29.5-52.6-34.9-54.7-5.4 2.1-20.4 17.8-34.9 54.7-8.2 21.1-19.6 44.2-34.4 61.6z'/%3e%3cpath d='M512 240L256 352 0 240v160l256 112 256-112V240z'/%3e%3c/svg%3e");
-$bg-icon-generator-events: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M160 96h192v32H160zM160 224h192v32H160zM160 160h160v32H160z'/%3e%3cpath d='M128 64.1h256V264l64-28V64a64.2 64.2 0 0 0-64-64H128a64.2 64.2 0 0 0-64 64v172l64 28zM329.1 288H182.9l73.1 32 73.1-32z'/%3e%3cpath d='M256 352L0 240v160l256 112 256-112V240L256 352z'/%3e%3c/svg%3e");
+$bg-icon-generator-events: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M160 96h192v32H160zM160 224h192v32H160zM160 160h160v32H160z'/%3e%3cpath d='M128 64.1h256V264l64-28V64a64.2 64.2 0 0 0-64-64H128a64.2 64.2 0 0 0-64 64v172l64 28zM329.1 288H182.9l73.1 32 73.1-32z'/%3e%3cpath d='M256 352L0 240v160l256 112 256-112V240L256 352z'/%3e%3c/svg%3e");
$bg-icon-gauge: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M256 0C114.6 0 0 114.6 0 256c0 113.2 73.5 209.2 175.3 243L304 256v251.5C422.4 485 512 381 512 256 512 114.6 397.4 0 256 0zm121.4 263.9a159.8 159.8 0 0 0-242.8 0l-73-62.5c4.3-5 8.7-9.8 13.4-14.4a255.9 255.9 0 0 1 362 0c4.7 4.6 9.1 9.4 13.4 14.4z'/%3e%3c/svg%3e");
$bg-icon-spectra: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M384 352H128l51.2-89.6L0 288v127c0 53.3 43.7 97 97 97h318c53.4 0 97-43.7 97-97v-31l-162.9-93.1zM415 0H97C43.7 0 0 43.6 0 97v159l200-30.1 56-97.9 54.9 96H512V97a97.2 97.2 0 00-97-97zM512 320v-32l-192-32 192 64z'/%3e%3c/svg%3e");
$bg-icon-spectra-telemetry: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M256 128l54.9 96H510C494.3 97.7 386.5 0 256 0 114.6 0 0 114.6 0 256l200-30.1zM384 352H128l51.2-89.6L2 287.7C17.6 414.1 125.4 512 256 512c100.8 0 188-58.3 229.8-143l-136.7-78.1zM320 256l192 64v-32l-192-32z'/%3e%3c/svg%3e");
@@ -319,3 +324,6 @@ $bg-icon-bar-chart: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://w
$bg-icon-map: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 32.7 384 64v448l64-31.3c35.2-17.21 64-60.1 64-95.3v-320c0-35.2-28.8-49.91-64-32.7ZM160 456l193.6 48.4v-448L160 8v448zM129.6.4 128 0 64 31.3C28.8 48.51 0 91.4 0 126.6v320c0 35.2 28.8 49.91 64 32.7l64-31.3 1.6.4Z'/%3e%3c/svg%3e");
$bg-icon-plan: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cg data-name='Layer 2'%3e%3cg data-name='Layer 1'%3e%3cpath fill='%23000000' d='M128 96V64a64.19 64.19 0 0 1 64-64h128a64.19 64.19 0 0 1 64 64v32Z'/%3e%3cpath fill='%23000000' d='M416 64v64H96V64c-52.8 0-96 43.2-96 96v256c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V160c0-52.8-43.2-96-96-96ZM64 288v-64h128v64Zm256 128H128v-64h192Zm128 0h-64v-64h64Zm0-128H256v-64h192Z'/%3e%3c/g%3e%3c/g%3e%3c/svg%3e");
$bg-icon-timelist: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cg data-name='Layer 2'%3e%3cpath d='M448 0H64A64.19 64.19 0 0 0 0 64v384a64.19 64.19 0 0 0 64 64h384a64.19 64.19 0 0 0 64-64V64a64.19 64.19 0 0 0-64-64ZM213.47 266.73a24 24 0 0 1-32.2 10.74L104 238.83V128a24 24 0 0 1 48 0v81.17l50.73 25.36a24 24 0 0 1 10.74 32.2ZM448 448H288v-64h160Zm0-96H288v-64h160Zm0-96H288v-64h160Zm0-96H288V96h160Z' data-name='Layer 1'/%3e%3c/g%3e%3c/svg%3e");
+$bg-icon-plot-scatter: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cg data-name='Layer 2'%3e%3cpath d='M96 0C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V96c0-52.8-43.2-96-96-96ZM64 176a48 48 0 1 1 48 48 48 48 0 0 1-48-48Zm80 240a48 48 0 1 1 48-48 48 48 0 0 1-48 48Zm128-96a48 48 0 1 1 48-48 48 48 0 0 1-48 48Zm0-160a48 48 0 1 1 48-48 48 48 0 0 1-48 48Zm128 256a48 48 0 1 1 48-48 48 48 0 0 1-48 48Z' data-name='Layer 1'/%3e%3c/g%3e%3c/svg%3e");
+$bg-icon-notebook-shift-log: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M448 55.36c0-39.95-27.69-63.66-61.54-52.68L0 128h448V55.36ZM448 160H0v288c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V224c0-35.2-28.8-64-64-64ZM128 416H64v-64h64v64Zm0-96H64v-64h64v64Zm320 96H192v-64h256v64Zm0-96H192v-64h256v64Z'/%3e%3c/svg%3e");
+$bg-icon-telemetry-aggregate: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cg data-name='Layer 2'%3e%3cg data-name='Layer 3'%3e%3cpath d='M39 197.72c7-20.72 18.74-50.4 34.6-74.18C92.91 94.65 114.79 80 138.67 80s45.75 14.65 65 43.54c15.86 23.78 27.57 53.46 34.6 74.18 15.44 45.48 31.56 67.49 39 73.27 7.47-5.78 23.6-27.79 39-73.27 7-20.72 18.74-50.4 34.61-74.18q13.9-20.85 29.56-31.75A207.78 207.78 0 0 0 208 0C93.12 0 0 93.12 0 208a208.14 208.14 0 0 0 7.39 55.09c8.39-10.87 20.2-31.67 31.61-65.37Z'/%3e%3cpath d='M377 218.28c-7 20.72-18.74 50.4-34.6 74.18-19.28 28.89-41.16 43.54-65 43.54s-45.75-14.65-65-43.54c-15.86-23.78-27.57-53.46-34.6-74.18-15.44-45.48-31.57-67.49-39-73.27-7.47 5.78-23.6 27.79-39 73.27-7.19 20.72-18.9 50.4-34.8 74.18q-13.9 20.85-29.56 31.75A207.78 207.78 0 0 0 208 416c114.88 0 208-93.12 208-208a208.14 208.14 0 0 0-7.39-55.09c-8.39 10.87-20.2 31.67-31.61 65.37Z'/%3e%3cpath d='M460.78 167.31A258.4 258.4 0 0 1 464 208a255.84 255.84 0 0 1-256 256 258.4 258.4 0 0 1-40.69-3.22A207.23 207.23 0 0 0 304 512c114.88 0 208-93.12 208-208a207.23 207.23 0 0 0-51.22-136.69Z'/%3e%3c/g%3e%3c/g%3e%3c/svg%3e");
diff --git a/src/styles/_controls.scss b/src/styles/_controls.scss
index 0e36cb6c7..318e58b58 100644
--- a/src/styles/_controls.scss
+++ b/src/styles/_controls.scss
@@ -22,6 +22,71 @@
@use 'sass:math';
+/******************************************************** CONTROL-SPECIFIC MIXINS */
+@mixin menuOuter() {
+ border-radius: $basicCr;
+ box-shadow: $shdwMenu;
+ @if $shdwMenuInner != none {
+ box-shadow: $shdwMenuInner, $shdwMenu;
+ }
+ background: $colorMenuBg;
+ color: $colorMenuFg;
+ text-shadow: $shdwMenuText;
+ padding: $interiorMarginSm;
+ display: flex;
+ flex-direction: column;
+ position: absolute;
+ z-index: 100;
+
+ > * {
+ flex: 0 0 auto;
+ }
+}
+
+@mixin menuPositioning() {
+ display: flex;
+ flex-direction: column;
+ position: absolute;
+ z-index: 100;
+
+ > * {
+ flex: 0 0 auto;
+ }
+}
+
+@mixin menuInner() {
+ li {
+ @include cControl();
+ justify-content: start;
+ cursor: pointer;
+ display: flex;
+ padding: nth($menuItemPad, 1) nth($menuItemPad, 2);
+ white-space: nowrap;
+
+ @include hover {
+ background: $colorMenuHovBg;
+ color: $colorMenuHovFg;
+ &:before {
+ color: $colorMenuHovIc !important;
+ }
+ }
+
+ &:not(.c-menu--no-icon &) {
+ &:before {
+ color: $colorMenuIc;
+ font-size: 1em;
+ margin-right: $interiorMargin;
+ min-width: 1em;
+ }
+
+ &:not([class*='icon']):before {
+ content: ''; // Enable :before so that menu items without an icon still indent properly
+ }
+
+ }
+ }
+}
+
/******************************************************** BUTTONS */
// Optionally can include icon in :before via markup
button {
@@ -277,16 +342,17 @@ input[type=password],
input[type=date],
textarea {
@include reactive-input();
- padding: $inputTextP;
&.numeric {
text-align: right;
}
}
-input[type=number]::-webkit-inner-spin-button,
-input[type=number]::-webkit-outer-spin-button {
- margin-right: -5px !important;
- margin-top: -1px !important;
+input[type=text],
+input[type=search],
+input[type=password],
+input[type=date],
+textarea {
+ padding: $inputTextP;
}
.c-input {
@@ -333,6 +399,47 @@ input[type=number]::-webkit-outer-spin-button {
// Small inputs, like small numerics
width: 40px;
}
+
+ &--autocomplete {
+ &__wrapper {
+ display: inline-flex;
+ flex-direction: row;
+ align-items: center;
+ }
+
+ &__input {
+ min-width: 100px;
+
+ // Fend off from afford-arrow
+ min-height: 2em;
+ padding-right: 2.5em !important;
+ }
+
+ &__options {
+ @include menuOuter();
+ @include menuInner();
+ display: flex;
+
+ ul {
+ flex: 1 1 auto;
+ overflow: auto;
+ }
+
+ li {
+ &:before {
+ color: var(--optionIconColor) !important;
+ font-size: 0.8em !important;
+ }
+ }
+ }
+
+ &__afford-arrow {
+ font-size: 0.8em;
+ position: absolute;
+ right: 2px;
+ z-index: 2;
+ }
+ }
}
input[type=number].c-input-number--no-spinners {
@@ -384,6 +491,10 @@ select {
&__row {
> * + * { margin-left: $interiorMargin; }
}
+
+ li {
+ white-space: nowrap;
+ }
}
/******************************************************** TABS */
@@ -470,61 +581,9 @@ select {
}
/******************************************************** MENUS */
-@mixin menuOuter() {
- border-radius: $basicCr;
- background: $colorMenuBg;
- filter: $filterMenu;
- text-shadow: $shdwMenuText;
- padding: $interiorMarginSm;
- box-shadow: $shdwMenu;
- display: flex;
- flex-direction: column;
- position: absolute;
- z-index: 100;
-
- > * {
- flex: 0 0 auto;
- }
-}
-
-@mixin menuInner() {
- li {
- @include cControl();
- justify-content: start;
- color: $colorMenuFg;
- cursor: pointer;
- display: flex;
- padding: nth($menuItemPad, 1) nth($menuItemPad, 2);
- transition: $transIn;
- white-space: nowrap;
-
- @include hover {
- background: $colorMenuHovBg;
- color: $colorMenuHovFg;
- &:before {
- color: $colorMenuHovIc;
- }
- }
-
- &:before {
- color: $colorMenuIc;
- font-size: 1em;
- margin-right: $interiorMargin;
- min-width: 1em;
- }
-
- &:not([class*='icon']):before {
- content: ''; // Enable :before so that menu items without an icon still indent properly
- }
-
- .menus-no-icon & {
- &:before { display: none; }
- }
- }
-}
-
.c-menu {
@include menuOuter();
+ @include menuPositioning();
@include menuInner();
&__section-hint {
@@ -548,6 +607,7 @@ select {
.c-super-menu {
// Two column layout, menu items on left with detail of hover element on right
@include menuOuter();
+ @include menuPositioning();
display: flex;
padding: $interiorMarginLg;
flex-direction: row;
@@ -993,6 +1053,14 @@ input[type="range"] {
display: inline-flex;
align-items: center;
}
+
+ [class*='--menus-aligned'] {
+ // Contains top level elements that hold dropdown menus
+ // Top level elements use display: contents to allow their menus to compactly align
+ // 03-18-22: used in ImageControls.vue
+ display: flex;
+ flex-direction: row;
+ }
}
.c-local-controls {
diff --git a/src/styles/_forms.scss b/src/styles/_forms.scss
index a0017a5cb..1cf33a6df 100644
--- a/src/styles/_forms.scss
+++ b/src/styles/_forms.scss
@@ -44,26 +44,33 @@
}
&__contents {
+ display: flex;
+ flex-direction: column;
flex: 1 1 auto;
overflow: auto;
padding-right: $interiorMargin;
}
&__section {
- display: inherit;
- flex-direction: column;
+ display: contents;
}
&__row {
display: flex;
padding: $formTBPad 0;
&:not(.first) { border-top: 1px solid $colorFormLines; }
+ flex: 0 0 auto;
+
+ &.grows {
+ flex: 1 1 auto;
+ }
}
&__section-header {
border-radius: $basicCr;
background: $colorFormSectionHeaderBg;
color: $colorFormSectionHeaderFg;
+ flex: 0 0 auto;
font-size: inherit;
font-weight: normal;
margin: $interiorMargin 0;
@@ -140,14 +147,16 @@
}
&--datetime {
- $size: min-content;
+ $size: max-content;
display: grid;
- grid-template-columns: repeat(5, min-content);
- grid-template-rows: auto;
+ grid-template-columns: repeat(5, $size);
+ grid-template-rows: $size;
grid-row-gap: 3px;
grid-column-gap: $interiorMargin;
+ align-items: stretch;
.hint {
+ align-self: center;
opacity: 0.7;
}
}
@@ -322,39 +331,6 @@
}
}
-.autocomplete {
- input {
- width: 226px;
- padding: 5px 0px 5px 7px;
- }
- .icon-arrow-down {
- position: absolute;
- top: 8px;
- left: 210px;
- font-size: 10px;
- cursor: pointer;
- }
- .autocompleteOptions {
- border: 1px solid $colorFormLines;
- border-radius: 5px;
- width: 224px;
- max-height: 170px;
- overflow-y: auto;
- overflow-x: hidden;
- li {
- border: 1px solid $colorFormLines;
- padding: 8px 0px 8px 5px;
- .optionText {
- cursor: pointer;
- }
- }
- .optionPreSelected {
- background-color: $colorInspectorSectionHeaderBg;
- color: $colorInspectorSectionHeaderFg;
- }
- }
-}
-
/********* COMPACT FORM */
// ul > li > label, control
// Make a new UL for each form section
diff --git a/src/styles/_global.scss b/src/styles/_global.scss
index 421f1fa17..46ab0ad66 100644
--- a/src/styles/_global.scss
+++ b/src/styles/_global.scss
@@ -256,7 +256,7 @@ body.desktop .has-local-controls {
}
::placeholder {
- opacity: 0.5;
+ opacity: 0.7;
font-style: italic;
}
@@ -349,3 +349,22 @@ body.desktop .has-local-controls {
pointer-events: none !important;
cursor: default !important;
}
+
+/******************************************************** RESPONSIVE CONTAINERS */
+@mixin responsiveContainerWidths($dimension) {
+ // 3/21/22: `--width-less-than*` classes set in ObjectView.vue
+ .--show-if-less-than-#{$dimension} {
+ // Hide anything that displays within a given width by default.
+ // `display` property must be set within a more specific class
+ // for the particular item to be displayed.
+ display: none !important
+ }
+
+ .--width-less-than-#{$dimension} {
+ .--hide-if-less-than-#{$dimension} { display: none; }
+ }
+}
+
+//.--hide-by-default { display: none !important; }
+@include responsiveContainerWidths('220');
+@include responsiveContainerWidths('600');
diff --git a/src/styles/_glyphs.scss b/src/styles/_glyphs.scss
index 267aeded6..d04205f71 100755
--- a/src/styles/_glyphs.scss
+++ b/src/styles/_glyphs.scss
@@ -87,6 +87,13 @@
.icon-unlocked { @include glyphBefore($glyph-icon-unlocked); }
.icon-circle { @include glyphBefore($glyph-icon-circle); }
.icon-draft { @include glyphBefore($glyph-icon-draft); }
+.icon-question-mark { @include glyphBefore($glyph-icon-question-mark); }
+.icon-circle-slash { @include glyphBefore($glyph-icon-circle-slash); }
+.icon-status-poll-check { @include glyphBefore($glyph-icon-status-poll-check); }
+.icon-status-poll-caution { @include glyphBefore($glyph-icon-status-poll-caution); }
+.icon-status-poll-circle-slash { @include glyphBefore($glyph-icon-status-poll-circle-slash); }
+.icon-status-poll-question-mark { @include glyphBefore($glyph-icon-status-poll-question-mark); }
+.icon-status-poll-edit { @include glyphBefore($glyph-icon-status-poll-edit); }
.icon-arrows-right-left { @include glyphBefore($glyph-icon-arrows-right-left); }
.icon-arrows-up-down { @include glyphBefore($glyph-icon-arrows-up-down); }
.icon-bullet { @include glyphBefore($glyph-icon-bullet); }
@@ -195,6 +202,8 @@
.icon-map { @include glyphBefore($glyph-icon-map); }
.icon-plan { @include glyphBefore($glyph-icon-plan); }
.icon-timelist { @include glyphBefore($glyph-icon-timelist); }
+.icon-notebook-shift-log { @include glyphBefore($glyph-icon-notebook-shift-log); }
+.icon-plot-scatter { @include glyphBefore($glyph-icon-plot-scatter); }
/************************** 12 PX CLASSES */
// TODO: sync with 16px redo as of 10/25/18
@@ -210,9 +219,6 @@
.bg-icon-alert-triangle { @include glyphBg($bg-icon-alert-triangle); }
.bg-icon-bell { @include glyphBg($bg-icon-bell); }
.bg-icon-info { @include glyphBg($bg-icon-info); }
-.bg-icon-activity { @include glyphBg($bg-icon-activity); }
-.bg-icon-activity-mode { @include glyphBg($bg-icon-activity-mode); }
-.bg-icon-autoflow-tabular { @include glyphBg($bg-icon-autoflow-tabular); }
.bg-icon-plus { @include glyphBg($bg-icon-plus); }
.bg-icon-grippy-ew { @include glyphBg($bg-icon-grippy-ew); }
.bg-icon-chain-links { @include glyphBg($bg-icon-chain-links); }
@@ -235,12 +241,10 @@
.bg-icon-tabular { @include glyphBg($bg-icon-tabular); }
.bg-icon-tabular-lad { @include glyphBg($bg-icon-tabular-lad); }
.bg-icon-tabular-lad-set { @include glyphBg($bg-icon-tabular-lad-set); }
-.bg-icon-tabular-realtime { @include glyphBg($bg-icon-tabular-realtime); }
.bg-icon-tabular-scrolling { @include glyphBg($bg-icon-tabular-scrolling); }
.bg-icon-telemetry { @include glyphBg($bg-icon-telemetry); }
.bg-icon-timeline { @include glyphBg($bg-icon-timeline); }
.bg-icon-timer { @include glyphBg($bg-icon-timer); }
-.bg-icon-topic { @include glyphBg($bg-icon-topic); }
.bg-icon-box-with-dashed-lines { @include glyphBg($bg-icon-box-with-dashed-lines); }
.bg-icon-summary-widget { @include glyphBg($bg-icon-summary-widget); }
.bg-icon-notebook { @include glyphBg($bg-icon-notebook); }
@@ -258,3 +262,6 @@
.bg-icon-map { @include glyphBg($bg-icon-map); }
.bg-icon-plan { @include glyphBg($bg-icon-plan); }
.bg-icon-timelist { @include glyphBg($bg-icon-timelist); }
+.bg-icon-plot-scatter { @include glyphBg($bg-icon-plot-scatter); }
+.bg-icon-notebook-shift-log { @include glyphBg($bg-icon-notebook-shift-log); }
+.bg-icon-telemetry-aggregate { @include glyphBg($bg-icon-telemetry-aggregate); }
diff --git a/src/styles/_legacy-plots.scss b/src/styles/_legacy-plots.scss
index 8b6e60a89..a5faf3ec5 100644
--- a/src/styles/_legacy-plots.scss
+++ b/src/styles/_legacy-plots.scss
@@ -65,7 +65,6 @@ mct-plot {
.c-plot {
@include abs($mainViewPad);
display: flex;
- flex-direction: column;
overflow: hidden;
min-height: $plotMinH;
@@ -83,11 +82,18 @@ mct-plot {
}
.c-plot--stacked-container {
+ border: 1px solid transparent;
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-height: $plotMinH;
overflow: hidden;
+
+ &[s-selected] {
+ .is-editing & {
+ border: $editMarqueeBorder;
+ }
+ }
}
;
@@ -118,7 +124,7 @@ mct-plot {
}
}
- .is-in-small-container & {
+ .--width-less-than-600 & {
.c-control-bar {
display: none;
}
@@ -498,7 +504,7 @@ mct-plot {
margin-bottom: $interiorMarginSm;
}
- .is-in-small-container & {
+ .--width-less-than-600 & {
&.is-legend-hidden {
display: none;
}
@@ -749,6 +755,12 @@ mct-plot {
overflow: hidden;
}
+/***************** SCATTER PLOTS */
+.c-scatter-chart {
+ flex: 1 1 auto;
+ overflow: hidden;
+}
+
/***************** CURSOR GUIDES */
[class*='c-cursor-guide'] {
box-shadow: $shdwCursorGuide;
diff --git a/src/styles/_legacy.scss b/src/styles/_legacy.scss
index 1f8d61879..3288aabad 100644
--- a/src/styles/_legacy.scss
+++ b/src/styles/_legacy.scss
@@ -36,10 +36,6 @@
align-items: center;
}
- &__value {
- color: $colorBodyFgEm;
- }
-
.c-frame & {
// When in a Display or Flexible Layout
@include abs();
@@ -49,6 +45,11 @@
.c-clock {
> * + * { margin-left: $interiorMargin; }
+
+ &__timezone-selection .c-menu {
+ // Menu for selecting timezones in properties dialog
+ max-height: 200px;
+ }
}
.c-timer {
@@ -66,7 +67,7 @@
}
&__direction {
- font-size: 0.9rem !important;
+ font-size: 0.7em !important;
margin-right: $interiorMargin;
}
diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss
index ea340e1c9..f15af2bdf 100644
--- a/src/styles/_mixins.scss
+++ b/src/styles/_mixins.scss
@@ -228,12 +228,12 @@
@mixin grippy($c: rgba(black, 0.5), $dir: 'x') {
$deg: 90deg;
- $bgSize: 2px 100%;
+ $bgSize: 3px 100%;
@if $dir != 'x' {
// Grippy texture runs 'vertically'
$deg: 0deg;
- $bgSize: 100% 2px;
+ $bgSize: 100% 3px;
}
background: linear-gradient($deg,
@@ -250,6 +250,12 @@
width: $plotSwatchD;
}
+@mixin dropDownArrowBg() {
+ background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10'%3e%3cpath fill='%23#{svgColorFromHex($colorSelectArw)}' d='M5 5l5-5H0z'/%3e%3c/svg%3e");
+ background-repeat: no-repeat, no-repeat;
+ background-position: right .4em top 80%, 0 0;
+}
+
@mixin noColor() {
// A "no fill/stroke" selection option. Used in palettes.
$c: red;
diff --git a/src/styles/_table.scss b/src/styles/_table.scss
index 12540ae93..d6c85206d 100644
--- a/src/styles/_table.scss
+++ b/src/styles/_table.scss
@@ -90,7 +90,7 @@ div.c-table {
flex: 1 1 auto;
}
- .is-in-small-container & {
+ .--width-less-than-600 & {
&:not(.is-paused) {
.c-table-control-bar {
display: none;
diff --git a/src/styles/fonts/Open MCT Symbols 16px.json b/src/styles/fonts/Open MCT Symbols 16px.json
index 932b9abff..c2df604e0 100644
--- a/src/styles/fonts/Open MCT Symbols 16px.json
+++ b/src/styles/fonts/Open MCT Symbols 16px.json
@@ -2,7 +2,7 @@
"metadata": {
"name": "Open MCT Symbols 16px",
"lastOpened": 0,
- "created": 1643998589907
+ "created": 1660771219523
},
"iconSets": [
{
@@ -392,12 +392,68 @@
"tempChar": ""
},
{
+ "order": 212,
+ "id": 183,
+ "name": "icon-circle-slash",
+ "prevSize": 16,
+ "code": 59696,
+ "tempChar": ""
+ },
+ {
+ "order": 213,
+ "id": 182,
+ "name": "icon-question-mark",
+ "prevSize": 16,
+ "code": 59697,
+ "tempChar": ""
+ },
+ {
+ "order": 206,
+ "id": 179,
+ "name": "icon-status-poll-check",
+ "prevSize": 16,
+ "code": 59698,
+ "tempChar": ""
+ },
+ {
+ "order": 207,
+ "id": 178,
+ "name": "icon-status-poll-caution",
+ "prevSize": 16,
+ "code": 59699,
+ "tempChar": ""
+ },
+ {
+ "order": 210,
+ "id": 180,
+ "name": "icon-status-poll-circle-slash",
+ "prevSize": 16,
+ "code": 59700,
+ "tempChar": ""
+ },
+ {
+ "order": 211,
+ "id": 181,
+ "name": "icon-status-poll-question-mark",
+ "prevSize": 16,
+ "code": 59701,
+ "tempChar": ""
+ },
+ {
+ "order": 209,
+ "id": 176,
+ "name": "icon-status-poll-edit",
+ "prevSize": 16,
+ "code": 59702,
+ "tempChar": ""
+ },
+ {
"order": 27,
"id": 105,
"name": "icon-arrows-right-left",
"prevSize": 16,
"code": 59904,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 26,
@@ -405,7 +461,7 @@
"name": "icon-arrows-up-down",
"prevSize": 16,
"code": 59905,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 68,
@@ -413,7 +469,7 @@
"name": "icon-bullet",
"prevSize": 16,
"code": 59906,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 150,
@@ -421,7 +477,7 @@
"prevSize": 16,
"code": 59907,
"name": "icon-calendar",
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 45,
@@ -429,7 +485,7 @@
"name": "icon-chain-links",
"prevSize": 16,
"code": 59908,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 73,
@@ -437,7 +493,7 @@
"name": "icon-download",
"prevSize": 16,
"code": 59909,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 39,
@@ -445,7 +501,7 @@
"name": "icon-duplicate",
"prevSize": 16,
"code": 59910,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 50,
@@ -453,7 +509,7 @@
"name": "icon-folder-new",
"prevSize": 16,
"code": 59911,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 138,
@@ -461,7 +517,7 @@
"name": "icon-fullscreen-collapse",
"prevSize": 16,
"code": 59912,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 139,
@@ -469,7 +525,7 @@
"name": "icon-fullscreen-expand",
"prevSize": 16,
"code": 59913,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 122,
@@ -477,7 +533,7 @@
"name": "icon-layers",
"prevSize": 16,
"code": 59914,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 151,
@@ -485,7 +541,7 @@
"name": "icon-line-horz",
"prevSize": 16,
"code": 59915,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 100,
@@ -493,7 +549,7 @@
"name": "icon-magnify",
"prevSize": 16,
"code": 59916,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 99,
@@ -501,7 +557,7 @@
"name": "icon-magnify-in",
"prevSize": 16,
"code": 59917,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 101,
@@ -509,7 +565,7 @@
"name": "icon-magnify-out-v2",
"prevSize": 16,
"code": 59918,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 103,
@@ -517,7 +573,7 @@
"name": "icon-menu",
"prevSize": 16,
"code": 59919,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 124,
@@ -525,7 +581,7 @@
"name": "icon-move",
"prevSize": 16,
"code": 59920,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 7,
@@ -533,7 +589,7 @@
"name": "icon-new-window",
"prevSize": 16,
"code": 59921,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 63,
@@ -541,7 +597,7 @@
"name": "icon-paint-bucket-v2",
"prevSize": 16,
"code": 59922,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 15,
@@ -549,7 +605,7 @@
"name": "icon-pencil",
"prevSize": 16,
"code": 59923,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 54,
@@ -557,7 +613,7 @@
"name": "icon-pencil-edit-in-place",
"prevSize": 16,
"code": 59924,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 40,
@@ -565,7 +621,7 @@
"name": "icon-play",
"prevSize": 16,
"code": 59925,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 125,
@@ -573,7 +629,7 @@
"name": "icon-pause",
"prevSize": 16,
"code": 59926,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 119,
@@ -581,7 +637,7 @@
"name": "icon-plot-resource",
"prevSize": 16,
"code": 59927,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 48,
@@ -589,7 +645,7 @@
"name": "icon-pointer-left",
"prevSize": 16,
"code": 59928,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 47,
@@ -597,7 +653,7 @@
"name": "icon-pointer-right",
"prevSize": 16,
"code": 59929,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 85,
@@ -605,7 +661,7 @@
"name": "icon-refresh",
"prevSize": 16,
"code": 59930,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 55,
@@ -613,7 +669,7 @@
"name": "icon-save",
"prevSize": 16,
"code": 59931,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 56,
@@ -621,7 +677,7 @@
"name": "icon-save-as",
"prevSize": 16,
"code": 59932,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 58,
@@ -629,7 +685,7 @@
"name": "icon-sine",
"prevSize": 16,
"code": 59933,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 113,
@@ -637,7 +693,7 @@
"name": "icon-font",
"prevSize": 16,
"code": 59934,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 41,
@@ -645,7 +701,7 @@
"name": "icon-thumbs-strip",
"prevSize": 16,
"code": 59935,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 146,
@@ -653,7 +709,7 @@
"name": "icon-two-parts-both",
"prevSize": 16,
"code": 59936,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 145,
@@ -661,7 +717,7 @@
"name": "icon-two-parts-one-only",
"prevSize": 16,
"code": 59937,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 82,
@@ -669,7 +725,7 @@
"name": "icon-resync",
"prevSize": 16,
"code": 59938,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 86,
@@ -677,7 +733,7 @@
"name": "icon-reset",
"prevSize": 16,
"code": 59939,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 61,
@@ -685,7 +741,7 @@
"name": "icon-x-in-circle",
"prevSize": 16,
"code": 59940,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 84,
@@ -693,7 +749,7 @@
"name": "icon-brightness",
"prevSize": 16,
"code": 59941,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 83,
@@ -701,7 +757,7 @@
"name": "icon-contrast",
"prevSize": 16,
"code": 59942,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 87,
@@ -709,7 +765,7 @@
"name": "icon-expand",
"prevSize": 16,
"code": 59943,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 89,
@@ -717,7 +773,7 @@
"name": "icon-list-view",
"prevSize": 16,
"code": 59944,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 133,
@@ -725,7 +781,7 @@
"name": "icon-grid-snap-to",
"prevSize": 16,
"code": 59945,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 132,
@@ -733,7 +789,7 @@
"name": "icon-grid-snap-no",
"prevSize": 16,
"code": 59946,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 94,
@@ -741,7 +797,7 @@
"name": "icon-frame-show",
"prevSize": 16,
"code": 59947,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 95,
@@ -749,7 +805,7 @@
"name": "icon-frame-hide",
"prevSize": 16,
"code": 59948,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 97,
@@ -757,7 +813,7 @@
"name": "icon-import",
"prevSize": 16,
"code": 59949,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 96,
@@ -765,7 +821,7 @@
"name": "icon-export",
"prevSize": 16,
"code": 59950,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 194,
@@ -773,7 +829,7 @@
"name": "icon-font-size",
"prevSize": 16,
"code": 59951,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 163,
@@ -781,7 +837,7 @@
"name": "icon-clear-data",
"prevSize": 16,
"code": 59952,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 173,
@@ -789,7 +845,7 @@
"name": "icon-history",
"prevSize": 16,
"code": 59953,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 181,
@@ -797,7 +853,7 @@
"name": "icon-arrow-up-to-parent",
"prevSize": 16,
"code": 59954,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 184,
@@ -805,7 +861,7 @@
"name": "icon-crosshair-in-circle",
"prevSize": 16,
"code": 59955,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 185,
@@ -813,7 +869,7 @@
"name": "icon-target",
"prevSize": 16,
"code": 59956,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 187,
@@ -821,7 +877,7 @@
"name": "icon-items-collapse",
"prevSize": 16,
"code": 59957,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 188,
@@ -829,7 +885,7 @@
"name": "icon-items-expand",
"prevSize": 16,
"code": 59958,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 190,
@@ -837,7 +893,7 @@
"name": "icon-3-dots",
"prevSize": 16,
"code": 59959,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 193,
@@ -845,7 +901,7 @@
"name": "icon-grid-on",
"prevSize": 16,
"code": 59960,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 192,
@@ -853,7 +909,7 @@
"name": "icon-grid-off",
"prevSize": 16,
"code": 59961,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 191,
@@ -861,7 +917,7 @@
"name": "icon-camera",
"prevSize": 16,
"code": 59962,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 196,
@@ -869,7 +925,7 @@
"name": "icon-folders-collapse",
"prevSize": 16,
"code": 59963,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 144,
@@ -877,7 +933,7 @@
"name": "icon-activity",
"prevSize": 16,
"code": 60160,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 104,
@@ -885,7 +941,7 @@
"name": "icon-activity-mode",
"prevSize": 16,
"code": 60161,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 137,
@@ -893,7 +949,7 @@
"name": "icon-autoflow-tabular",
"prevSize": 16,
"code": 60162,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 115,
@@ -901,7 +957,7 @@
"name": "icon-clock",
"prevSize": 16,
"code": 60163,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 2,
@@ -909,7 +965,7 @@
"name": "icon-database",
"prevSize": 16,
"code": 60164,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 3,
@@ -917,7 +973,7 @@
"name": "icon-database-query",
"prevSize": 16,
"code": 60165,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 67,
@@ -925,7 +981,7 @@
"name": "icon-dataset",
"prevSize": 16,
"code": 60166,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 59,
@@ -933,7 +989,7 @@
"name": "icon-datatable",
"prevSize": 16,
"code": 60167,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 136,
@@ -941,7 +997,7 @@
"name": "icon-dictionary",
"prevSize": 16,
"code": 60168,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 51,
@@ -949,7 +1005,7 @@
"name": "icon-folder",
"prevSize": 16,
"code": 60169,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 147,
@@ -957,7 +1013,7 @@
"name": "icon-image",
"prevSize": 16,
"code": 60170,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 4,
@@ -965,7 +1021,7 @@
"name": "icon-layout",
"prevSize": 16,
"code": 60171,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 24,
@@ -973,7 +1029,7 @@
"name": "icon-object",
"prevSize": 16,
"code": 60172,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 52,
@@ -981,7 +1037,7 @@
"name": "icon-object-unknown",
"prevSize": 16,
"code": 60173,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 105,
@@ -989,7 +1045,7 @@
"name": "icon-packet",
"prevSize": 16,
"code": 60174,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 126,
@@ -997,7 +1053,7 @@
"name": "icon-page",
"prevSize": 16,
"code": 60175,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 130,
@@ -1005,7 +1061,7 @@
"name": "icon-plot-overlay",
"prevSize": 16,
"code": 60176,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 80,
@@ -1013,7 +1069,7 @@
"name": "icon-plot-stacked",
"prevSize": 16,
"code": 60177,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 134,
@@ -1021,7 +1077,7 @@
"name": "icon-session",
"prevSize": 16,
"code": 60178,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 109,
@@ -1029,7 +1085,7 @@
"name": "icon-tabular",
"prevSize": 16,
"code": 60179,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 107,
@@ -1037,7 +1093,7 @@
"name": "icon-tabular-lad",
"prevSize": 16,
"code": 60180,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 106,
@@ -1045,7 +1101,7 @@
"name": "icon-tabular-lad-set",
"prevSize": 16,
"code": 60181,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 70,
@@ -1053,7 +1109,7 @@
"name": "icon-tabular-realtime",
"prevSize": 16,
"code": 60182,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 60,
@@ -1061,7 +1117,7 @@
"name": "icon-tabular-scrolling",
"prevSize": 16,
"code": 60183,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 131,
@@ -1069,7 +1125,7 @@
"name": "icon-telemetry",
"prevSize": 16,
"code": 60184,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 202,
@@ -1077,7 +1133,7 @@
"name": "icon-timeline",
"prevSize": 16,
"code": 60185,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 81,
@@ -1085,7 +1141,7 @@
"name": "icon-timer",
"prevSize": 16,
"code": 60186,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 69,
@@ -1093,7 +1149,7 @@
"name": "icon-topic",
"prevSize": 16,
"code": 60187,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 79,
@@ -1101,7 +1157,7 @@
"name": "icon-box-with-dashed-lines-v2",
"prevSize": 16,
"code": 60188,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 90,
@@ -1109,7 +1165,7 @@
"name": "icon-summary-widget",
"prevSize": 16,
"code": 60189,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 92,
@@ -1117,7 +1173,7 @@
"name": "icon-notebook",
"prevSize": 16,
"code": 60190,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 168,
@@ -1125,7 +1181,7 @@
"name": "icon-tabs-view",
"prevSize": 16,
"code": 60191,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 117,
@@ -1133,7 +1189,7 @@
"name": "icon-flexible-layout",
"prevSize": 16,
"code": 60192,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 166,
@@ -1141,7 +1197,7 @@
"name": "icon-generator-sine",
"prevSize": 16,
"code": 60193,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 167,
@@ -1149,7 +1205,7 @@
"name": "icon-generator-event",
"prevSize": 16,
"code": 60194,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 165,
@@ -1157,7 +1213,7 @@
"name": "icon-gauge-v2",
"prevSize": 16,
"code": 60195,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 170,
@@ -1165,7 +1221,7 @@
"name": "icon-spectra",
"prevSize": 16,
"code": 60196,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 171,
@@ -1173,7 +1229,7 @@
"name": "icon-telemetry-spectra",
"prevSize": 16,
"code": 60197,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 172,
@@ -1181,7 +1237,7 @@
"name": "icon-pushbutton",
"prevSize": 16,
"code": 60198,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 174,
@@ -1189,7 +1245,7 @@
"name": "icon-conditional",
"prevSize": 16,
"code": 60199,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 178,
@@ -1197,7 +1253,7 @@
"name": "icon-condition-widget",
"prevSize": 16,
"code": 60200,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 180,
@@ -1205,7 +1261,7 @@
"name": "icon-alphanumeric",
"prevSize": 16,
"code": 60201,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 183,
@@ -1213,7 +1269,7 @@
"name": "icon-image-telemetry",
"prevSize": 16,
"code": 60202,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 198,
@@ -1221,7 +1277,7 @@
"name": "icon-telemetry-aggregate",
"prevSize": 16,
"code": 60203,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 199,
@@ -1229,7 +1285,7 @@
"name": "icon-bar-graph",
"prevSize": 16,
"code": 60204,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 200,
@@ -1237,7 +1293,7 @@
"name": "icon-map",
"prevSize": 16,
"code": 60205,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 203,
@@ -1245,7 +1301,7 @@
"name": "icon-plan",
"prevSize": 16,
"code": 60206,
- "tempChar": ""
+ "tempChar": ""
},
{
"order": 204,
@@ -1253,7 +1309,23 @@
"name": "icon-timelist",
"prevSize": 16,
"code": 60207,
- "tempChar": ""
+ "tempChar": ""
+ },
+ {
+ "order": 214,
+ "id": 184,
+ "name": "icon-notebook-restricted",
+ "prevSize": 16,
+ "code": 60209,
+ "tempChar": ""
+ },
+ {
+ "order": 205,
+ "id": 176,
+ "name": "icon-plot-scatter",
+ "prevSize": 16,
+ "code": 60208,
+ "tempChar": ""
}
],
"id": 0,
@@ -2100,6 +2172,162 @@
}
},
{
+ "id": 183,
+ "paths": [
+ "M512 0c-282.78 0-512 229.22-512 512s229.22 512 512 512 512-229.22 512-512-229.22-512-512-512zM263.1 263.1c66.48-66.48 154.88-103.1 248.9-103.1 66.74 0 130.64 18.48 185.9 52.96l-484.94 484.94c-34.5-55.24-52.96-119.16-52.96-185.9 0-94.020 36.62-182.42 103.1-248.9zM760.9 760.9c-66.48 66.48-154.88 103.1-248.9 103.1-66.74 0-130.64-18.48-185.9-52.96l484.94-484.94c34.5 55.24 52.96 119.16 52.96 185.9 0 94.020-36.62 182.42-103.1 248.9z"
+ ],
+ "attrs": [
+ {}
+ ],
+ "isMulticolor": false,
+ "isMulticolor2": false,
+ "grid": 16,
+ "tags": [
+ "icon-circle-slash"
+ ],
+ "colorPermutations": {
+ "12552552551": [
+ {}
+ ]
+ }
+ },
+ {
+ "id": 182,
+ "paths": [
+ "M136.86 52.26c54.080-34.82 120.58-52.26 199.44-52.26 103.6 0 189.7 24.76 258.24 74.28s102.82 122.88 102.82 220.060c0 59.6-14.86 109.8-44.58 150.6-17.38 24.76-50.76 56.4-100.14 94.9l-48.68 37.82c-26.54 20.64-44.14 44.7-52.82 72.2-5.5 17.44-8.46 44.48-8.92 81.14h-186.4c2.74-77.48 10.060-131 21.94-160.58s42.5-63.62 91.88-102.12l50.060-39.2c16.46-12.38 29.72-25.9 39.78-40.58 18.28-25.2 27.42-52.96 27.42-83.22 0-34.84-10.18-66.6-30.52-95.24-20.36-28.64-57.52-42.98-111.48-42.98s-90.68 17.66-112.88 52.96c-22.18 35.32-33.26 71.98-33.26 110.040h-198.76c5.5-130.64 51.12-223.24 136.86-277.82zM251.020 825.24h205.62v198.74h-205.62v-198.74z"
+ ],
+ "attrs": [
+ {}
+ ],
+ "width": 697,
+ "isMulticolor": false,
+ "isMulticolor2": false,
+ "grid": 16,
+ "tags": [
+ "icon-question-mark"
+ ],
+ "colorPermutations": {
+ "12552552551": [
+ {}
+ ]
+ }
+ },
+ {
+ "id": 179,
+ "paths": [
+ "M512 0c-282.76 0-512 214.9-512 480 0 92.26 27.8 178.44 75.92 251.6l-75.92 292.4 313.5-101.42c61.040 24.1 128.12 37.42 198.5 37.42 282.76 0 512-214.9 512-480s-229.24-480-512-480zM768 448l-320 320-192-192v-192l192 192 320-320v192z"
+ ],
+ "attrs": [
+ {}
+ ],
+ "isMulticolor": false,
+ "isMulticolor2": false,
+ "grid": 16,
+ "tags": [
+ "icon-status-poll-check"
+ ],
+ "colorPermutations": {
+ "12552552551": [
+ {}
+ ]
+ }
+ },
+ {
+ "id": 178,
+ "paths": [
+ "M512 0c-282.76 0-512 214.9-512 480 0 92.26 27.8 178.44 75.92 251.6l-75.92 292.4 313.5-101.42c61.040 24.1 128.12 37.42 198.5 37.42 282.76 0 512-214.9 512-480s-229.24-480-512-480zM781.36 704h-538.72c-44.96 0-63.5-31.94-41.2-70.98l270-472.48c22.3-39.040 58.82-39.040 81.12 0l269.98 472.48c22.3 39.040 3.78 70.98-41.2 70.98z",
+ "M457.14 417.86l24.2 122.64h61.32l24.2-122.64v-163.5h-109.72v163.5z",
+ "M471.12 581.36h81.76v81.76h-81.76v-81.76z"
+ ],
+ "attrs": [
+ {},
+ {},
+ {}
+ ],
+ "isMulticolor": false,
+ "isMulticolor2": false,
+ "grid": 16,
+ "tags": [
+ "icon-status-poll-caution"
+ ],
+ "colorPermutations": {
+ "12552552551": [
+ {},
+ {},
+ {}
+ ]
+ }
+ },
+ {
+ "id": 180,
+ "paths": [
+ "M391.18 668.7c35.72 22.98 77.32 35.3 120.82 35.3 59.84 0 116.080-23.3 158.4-65.6 42.3-42.3 65.6-98.56 65.6-158.4 0-43.5-12.32-85.080-35.3-120.82l-309.52 309.52z",
+ "M512 256c-59.84 0-116.080 23.3-158.4 65.6-42.3 42.3-65.6 98.56-65.6 158.4 0 43.5 12.32 85.080 35.3 120.82l309.52-309.52c-35.72-22.98-77.32-35.3-120.82-35.3z",
+ "M512 0c-282.76 0-512 214.9-512 480 0 92.26 27.8 178.44 75.92 251.6l-75.92 292.4 313.5-101.42c61.040 24.1 128.12 37.42 198.5 37.42 282.76 0 512-214.9 512-480s-229.24-480-512-480zM512 800c-176.74 0-320-143.26-320-320s143.26-320 320-320 320 143.26 320 320-143.26 320-320 320z"
+ ],
+ "attrs": [
+ {},
+ {},
+ {}
+ ],
+ "isMulticolor": false,
+ "isMulticolor2": false,
+ "grid": 16,
+ "tags": [
+ "icon-status-poll-circle-slash"
+ ],
+ "colorPermutations": {
+ "12552552551": [
+ {},
+ {},
+ {}
+ ]
+ }
+ },
+ {
+ "id": 181,
+ "paths": [
+ "M512 0c-282.76 0-512 214.9-512 480 0 92.26 27.8 178.44 75.92 251.6l-75.92 292.4 313.5-101.42c61.040 24.1 128.12 37.42 198.5 37.42 282.76 0 512-214.9 512-480s-229.24-480-512-480zM579.020 832h-141.36v-136.64h141.36v136.64zM713.84 433.9c-11.94 17.020-34.9 38.78-68.84 65.24l-33.48 26c-18.24 14.18-30.34 30.74-36.32 49.64-3.78 11.98-5.82 30.58-6.14 55.8h-128.12c1.88-53.26 6.92-90.060 15.080-110.4 8.18-20.34 29.22-43.74 63.16-70.22l34.42-26.94c11.3-8.52 20.42-17.8 27.34-27.9 12.56-17.34 18.86-36.4 18.86-57.2 0-23.94-7-45.78-20.98-65.48-14-19.7-39.54-29.54-76.64-29.54s-62.34 12.14-77.6 36.4c-15.24 24.28-22.88 49.48-22.88 75.64h-136.64c3.78-89.84 35.14-153.5 94.080-191.020 37.18-23.94 82.9-35.94 137.12-35.94 71.22 0 130.42 17.020 177.54 51.060s70.68 84.48 70.68 151.3c0 40.98-10.22 75.5-30.66 103.54z"
+ ],
+ "attrs": [
+ {}
+ ],
+ "isMulticolor": false,
+ "isMulticolor2": false,
+ "grid": 16,
+ "tags": [
+ "icon-status-poll-question-mark"
+ ],
+ "colorPermutations": {
+ "12552552551": [
+ {}
+ ]
+ }
+ },
+ {
+ "id": 176,
+ "paths": [
+ "M1000.080 334.64l-336.6 336.76-20.52 6.88-450.96 153.72 160.68-471.52 332.34-332.34c-54.040-18.2-112.28-28.14-173.020-28.14-282.76 0-512 214.9-512 480 0 92.26 27.8 178.44 75.92 251.6l-75.92 292.4 313.5-101.42c61.040 24.1 128.12 37.42 198.5 37.42 282.76 0 512-214.9 512-480 0-50.68-8.4-99.5-23.92-145.36z",
+ "M408.42 395.24l-2.16 6.3-111.7 327.9 334.12-113.86 4.62-4.68 350.28-350.28c6.8-6.78 14.96-19.1 14.96-38.9 0-34.86-26.82-83.28-69.88-126.38-26.54-26.54-55.9-47.6-82.7-59.34-47.34-20.8-72.020-6.24-82.64 4.36l-354.9 354.88zM470.56 421.42h44v88h88v44l-4.7 12.72-139.68 47.54-47.94-47.94 47.6-139.72 12.72-4.6z"
+ ],
+ "attrs": [
+ {},
+ {}
+ ],
+ "isMulticolor": false,
+ "isMulticolor2": false,
+ "grid": 16,
+ "tags": [
+ "icon-status-poll-edit"
+ ],
+ "colorPermutations": {
+ "12552552551": [
+ {},
+ {}
+ ]
+ }
+ },
+ {
"id": 105,
"paths": [
"M1024 512l-448 512v-1024z",
@@ -3318,15 +3546,21 @@
{
"id": 76,
"paths": [
- "M510-2l-512 320v384l512 320 512-320v-384l-512-320zM585.4 859.2c-21.2 20.8-46 30.8-76 30.8-31.2 0-56.2-9.8-76.2-29.6-20-20-29.6-44.8-29.6-76.2 0-30.4 10.2-55.2 31-76.2s45.2-31.2 74.8-31.2c29.6 0 54.2 10.4 75.6 32s31.8 46.4 31.8 76c-0.2 29-10.8 54-31.4 74.4zM638.2 546.6c-23.6 11.8-37.4 22-43.4 32.4-3.6 6.2-6 14.8-7.4 26.8v41h-161.4v-44.2c0-40.2 4.4-69.8 13-88 8-17.2 22.6-30.2 44.8-40l34.8-15.4c32-14.2 48.2-35.2 48.2-62.8 0-16-6-30.4-17.2-41.8-11.2-11.2-25.6-17.2-41.6-17.2-24 0-54.4 10-62.8 57.4l-2.2 12.2h-147l1.4-16.2c4-44.6 17-82.4 38.8-112.2 19.6-27 45.6-48.6 77-64.6s64.6-24 98.2-24c60.6 0 110.2 19.4 151.4 59.6 41.2 40 61.2 88 61.2 147.2 0 70.8-28.8 121.4-85.8 149.8z"
+ "M511.98 0l-511.98 320v384l512 320 512-320v-384l-512.020-320zM586.22 896h-141.36v-136.64h141.36v136.64zM721.040 497.9c-11.94 17.020-34.9 38.78-68.84 65.24l-33.48 26c-18.24 14.18-30.34 30.74-36.32 49.64-3.78 11.98-5.82 30.58-6.14 55.8h-128.12c1.88-53.26 6.92-90.060 15.080-110.4 8.18-20.34 29.22-43.74 63.16-70.22l34.42-26.94c11.3-8.52 20.42-17.8 27.34-27.9 12.56-17.34 18.86-36.4 18.86-57.2 0-23.94-7-45.78-20.98-65.48-14-19.7-39.54-29.54-76.64-29.54s-62.34 12.14-77.6 36.4c-15.24 24.28-22.88 49.48-22.88 75.64h-136.64c3.78-89.84 35.14-153.5 94.080-191.020 37.18-23.94 82.9-35.94 137.12-35.94 71.22 0 130.42 17.020 177.54 51.060s70.68 84.48 70.68 151.3c0 40.98-10.22 75.5-30.66 103.54z"
+ ],
+ "attrs": [
+ {}
],
- "attrs": [],
"grid": 16,
"tags": [
"icon-object-unknown"
],
+ "isMulticolor": false,
+ "isMulticolor2": false,
"colorPermutations": {
- "12552552551": []
+ "12552552551": [
+ {}
+ ]
}
},
{
@@ -3468,19 +3702,21 @@
{
"id": 66,
"paths": [
- "M64 0c-35.2 0-64 28.8-64 64v192h448v-256h-384z",
- "M1024 256v-192c0-35.2-28.8-64-64-64h-384v256h448z",
- "M0 384v192c0 35.2 28.8 64 64 64h384v-256h-448z",
- "M960 640c35.2 0 64-28.8 64-64v-192h-448v256h384z",
- "M512 1024l-256-256h512z"
+ "M896 0h-768c-70.606 0.215-127.785 57.394-128 127.979l-0 0.021v768c0.215 70.606 57.394 127.785 127.979 128l0.021 0h768c70.606-0.215 127.785-57.394 128-127.979l0-0.021v-768c-0.215-70.606-57.394-127.785-127.979-128l-0.021-0zM768 256v192h-192v-192zM576 512h192v192h-192zM512 704h-192v-192h192zM512 256v192h-192v-192zM64 256h192v192h-192zM64 512h192v192h-192zM128 960c-35.255-0.225-63.775-28.745-64-63.978l-0-0.022v-128h192v192zM320 960v-192h192v192zM704 960h-128v-192h192v192zM941.14 941.14c-11.511 11.644-27.483 18.856-45.139 18.86l-64.001 0v-64h128c-0.004 17.657-7.216 33.629-18.854 45.134l-0.006 0.006zM960 768h-128v-512h128z"
+ ],
+ "attrs": [
+ {}
],
- "attrs": [],
"grid": 16,
"tags": [
"icon-tabular-scrolling"
],
+ "isMulticolor": false,
+ "isMulticolor2": false,
"colorPermutations": {
- "12552552551": []
+ "12552552551": [
+ {}
+ ]
}
},
{
@@ -4000,6 +4236,49 @@
{}
]
}
+ },
+ {
+ "id": 184,
+ "paths": [
+ "M896 110.72c0-79.9-55.38-127.32-123.080-105.36l-772.92 250.64h896v-145.28z",
+ "M896 320h-896v576c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v-448c0-70.4-57.6-128-128-128zM256 832h-128v-128h128v128zM256 640h-128v-128h128v128zM896 832h-512v-128h512v128zM896 640h-512v-128h512v128z"
+ ],
+ "attrs": [
+ {},
+ {}
+ ],
+ "isMulticolor": false,
+ "isMulticolor2": false,
+ "grid": 16,
+ "tags": [
+ "icon-notebook-restricted"
+ ],
+ "colorPermutations": {
+ "12552552551": [
+ {},
+ {}
+ ]
+ }
+ },
+ {
+ "id": 185,
+ "paths": [
+ "M192 0c-105.6 0-192 86.4-192 192v640c0 105.6 86.4 192 192 192h640c105.6 0 192-86.4 192-192v-640c0-105.6-86.4-192-192-192zM128 352c0-53.019 42.981-96 96-96s96 42.981 96 96c0 53.019-42.981 96-96 96v0c-53.019 0-96-42.981-96-96v0zM288 832c-53.019 0-96-42.981-96-96s42.981-96 96-96c53.019 0 96 42.981 96 96v0c0 53.019-42.981 96-96 96v0zM544 640c-53.019 0-96-42.981-96-96s42.981-96 96-96c53.019 0 96 42.981 96 96v0c0 53.019-42.981 96-96 96v0zM544 320c-53.019 0-96-42.981-96-96s42.981-96 96-96c53.019 0 96 42.981 96 96v0c0 53.019-42.981 96-96 96v0zM800 832c-53.019 0-96-42.981-96-96s42.981-96 96-96c53.019 0 96 42.981 96 96v0c0 53.019-42.981 96-96 96v0z"
+ ],
+ "attrs": [
+ {}
+ ],
+ "isMulticolor": false,
+ "isMulticolor2": false,
+ "grid": 16,
+ "tags": [
+ "icon-plot-scatter"
+ ],
+ "colorPermutations": {
+ "12552552551": [
+ {}
+ ]
+ }
}
],
"invisible": false,
@@ -4034,11 +4313,12 @@
"fontFamily": "Open-MCT-Symbols-16px",
"majorVersion": 5,
"minorVersion": 1,
- "designer": "Charles Hacskaylo"
+ "designer": "Charles Hacskaylo",
+ "description": "Change to 5% baseline height"
},
"metrics": {
"emSize": 1024,
- "baseline": 20,
+ "baseline": 10,
"whitespace": 0
},
"embed": false,
diff --git a/src/styles/fonts/Open-MCT-Symbols-16px.svg b/src/styles/fonts/Open-MCT-Symbols-16px.svg
index 3ebe2bfb1..bb2b35658 100644
--- a/src/styles/fonts/Open-MCT-Symbols-16px.svg
+++ b/src/styles/fonts/Open-MCT-Symbols-16px.svg
@@ -4,163 +4,172 @@
<metadata>Generated by IcoMoon</metadata>
<defs>
<font id="Open-MCT-Symbols-16px" horiz-adv-x="1024">
-<font-face units-per-em="1024" ascent="819.2" descent="-204.8" />
+<font-face units-per-em="1024" ascent="921.6" descent="-102.4" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="0" d="" />
-<glyph unicode="&#xe900;" glyph-name="icon-alert-rect-v2" d="M896 832h-768c-70.6-0.2-127.8-57.4-128-128v-768c0.2-70.6 57.4-127.8 128-128h768c70.6 0.2 127.8 57.4 128 128v768c-0.2 70.6-57.4 127.8-128 128zM576-64h-128v128h128v-128zM597.8 320l-37.8-192h-96l-37.8 192v384h171.8v-384z" />
-<glyph unicode="&#xe901;" glyph-name="icon-alert-triangle-v2" d="M998.2-16.8l-422.6 739.6c-35 61.2-92 61.2-127 0l-422.8-739.6c-35-61.2-6-111.2 64.4-111.2h843.4c70.6 0 99.6 50 64.6 111.2zM576-64h-128v128h128v-128zM597.8 320l-37.8-192h-96l-37.8 192v256h171.8v-256z" />
-<glyph unicode="&#xe902;" glyph-name="icon-arrow-up" d="M512 576l-512-512h1024z" />
-<glyph unicode="&#xe903;" glyph-name="icon-arrow-double-up" d="M510 322l512-512h-1024zM510 834l512-512h-1024z" />
-<glyph unicode="&#xe904;" glyph-name="icon-arrow-tall-up" d="M512 832l512-1024h-1024z" />
-<glyph unicode="&#xe905;" glyph-name="icon-arrow-right" d="M768 320l-512 512v-1024z" />
-<glyph unicode="&#xe906;" glyph-name="icon-arrow-right-equilateral" d="M962 320l-896-512v1024z" />
-<glyph unicode="&#xe907;" glyph-name="icon-arrow-down" d="M512 64l512 512h-1024z" />
-<glyph unicode="&#xe908;" glyph-name="icon-arrow-double-down" d="M510 322l-512 512h1024zM510-190l-512 512h1024z" />
-<glyph unicode="&#xe909;" glyph-name="icon-arrow-tall-down" d="M512-192l-512 1024h1024z" />
-<glyph unicode="&#xe90a;" glyph-name="icon-arrow-left" d="M256 320l512-512v1024z" />
-<glyph unicode="&#xe90b;" glyph-name="icon-asterisk" d="M1004.166 491.542l-97.522 168.916-330.534-229.414 33.414 400.956h-195.048l33.414-400.956-330.534 229.414-97.522-168.916 363.944-171.542-363.944-171.542 97.522-168.916 330.534 229.414-33.414-400.956h195.048l-33.414 400.956 330.534-229.414 97.522 168.916-363.944 171.542z" />
-<glyph unicode="&#xe90c;" glyph-name="icon-bell" d="M512-192c106 0 192 86 192 192h-384c0-106 86-192 192-192zM896 384v64c0 212-172 384-384 384s-384-172-384-384v-64c0-70.6-57.4-128-128-128v-128h1024v128c-70.6 0-128 57.4-128 128z" />
-<glyph unicode="&#xe90d;" glyph-name="icon-box-round-corners" d="M1024 0c0-105.6-86.4-192-192-192h-640c-105.6 0-192 86.4-192 192v640c0 105.6 86.4 192 192 192h640c105.6 0 192-86.4 192-192v-640z" />
-<glyph unicode="&#xe90e;" glyph-name="icon-box-with-arrow-cursor" d="M894 834h-768c-70.4 0-128-57.6-128-128v-768c0-70.4 57.6-128 128-128h400c-2.2 3.8-4 7.6-5.8 11.4l-255.2 576.8c-21.4 48.4-10.8 105 26.6 142.4 24.4 24.4 57.2 37.4 90.4 37.4 17.4 0 35.2-3.6 51.8-11l576.6-255.4c4-1.8 7.8-3.8 11.4-5.8v400.2c0.2 70.4-57.4 128-127.8 128zM958.6 194.6l-576.6 255.4 255.4-576.6 64.6 128.6 192-192 128 128-192 192z" />
-<glyph unicode="&#xe90f;" glyph-name="icon-check" d="M1024 832l-640-640-384 384v-384l384-384 640 640z" />
-<glyph unicode="&#xe910;" glyph-name="icon-connectivity" d="M704 256c0-70.4-57.6-128-128-128h-128c-70.4 0-128 57.6-128 128v128c0 70.4 57.6 128 128 128h128c70.4 0 128-57.6 128-128v-128zM1024 320l-192 320v-640zM0 320l192 320v-640z" />
-<glyph unicode="&#xe911;" glyph-name="icon-database-in-brackets" d="M768 480c0-53.019-114.615-96-256-96s-256 42.981-256 96c0 53.019 114.615 96 256 96s256-42.981 256-96zM768 160v256c0-53-114.6-96-256-96s-256 43-256 96v-256c0-53 114.6-96 256-96s256 43 256 96zM832 832h-128v-192h127.6c0.2 0 0.2-0.2 0.4-0.4v-639.4c0-0.2-0.2-0.2-0.4-0.4h-127.6v-192h128c105.6 0 192 86.4 192 192v640.2c0 105.6-86.4 192-192 192zM192 0.4v639.4c0 0.2 0.2 0.2 0.4 0.4h127.6v191.8h-128c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h128v192h-127.6c-0.2 0-0.4 0.2-0.4 0.4z" />
-<glyph unicode="&#xe912;" glyph-name="icon-eye-open" d="M512 715.6c-245.8 0-452.2-168-510.8-395.6 58.6-227.4 265-395.6 510.8-395.6s452.2 168 510.8 395.6c-58.6 227.4-265 395.6-510.8 395.6zM829.2 243.6c-22.6-34.4-50.6-64.8-83-90.4-32.8-25.8-69-45.6-108-59.4-40.4-14.2-82.8-21.4-126-21.4s-85.8 7.2-126 21.4c-39 13.8-75.4 33.8-108 59.4-32.4 25.6-60.4 55.8-83 90.4-15.8 24-28.8 49.6-38.6 76.4 10 26.8 23 52.4 38.6 76.4 22.6 34.4 50.6 64.8 83 90.4 32.8 25.8 69 45.6 108 59.4 40.4 14.2 82.8 21.4 126 21.4s85.8-7.2 126-21.4c39-13.8 75.4-33.8 108-59.4 32.4-25.6 60.4-55.8 83-90.4 15.8-24 28.8-49.6 38.6-76.4-9.8-26.8-22.8-52.4-38.6-76.4zM704 320c0-106.039-85.961-192-192-192s-192 85.961-192 192c0 106.039 85.961 192 192 192s192-85.961 192-192z" />
-<glyph unicode="&#xe913;" glyph-name="icon-gear" d="M1024 256v128l-140.976 35.244c-8.784 32.922-21.818 64.106-38.504 92.918l74.774 124.622-90.51 90.51-124.622-74.774c-28.812 16.686-59.996 29.72-92.918 38.504l-35.244 140.976h-128l-35.244-140.976c-32.922-8.784-64.106-21.818-92.918-38.504l-124.622 74.774-90.51-90.51 74.774-124.622c-16.686-28.812-29.72-59.996-38.504-92.918l-140.976-35.244v-128l140.976-35.244c8.784-32.922 21.818-64.106 38.504-92.918l-74.774-124.622 90.51-90.51 124.622 74.774c28.812-16.686 59.996-29.72 92.918-38.504l35.244-140.976h128l35.244 140.976c32.922 8.784 64.106 21.818 92.918 38.504l124.622-74.774 90.51 90.51-74.774 124.622c16.686 28.812 29.72 59.996 38.504 92.918l140.976 35.244zM704 320c0-106.038-85.962-192-192-192s-192 85.962-192 192 85.962 192 192 192 192-85.962 192-192z" />
-<glyph unicode="&#xe914;" glyph-name="icon-hourglass" d="M1024 832h-1024c0-282.8 229.2-512 512-512s512 229.2 512 512zM512 448c-102.6 0-199 40-271.6 112.4-41.2 41.2-72 90.2-90.8 143.6h724.6c-18.8-53.4-49.6-102.4-90.8-143.6-72.4-72.4-168.8-112.4-271.4-112.4zM512 320c-282.8 0-512-229.2-512-512h1024c0 282.8-229.2 512-512 512z" />
-<glyph unicode="&#xe915;" glyph-name="icon-info" d="M512 832c-282.8 0-512-229.2-512-512s229.2-512 512-512 512 229.2 512 512-229.2 512-512 512zM512 704c70.6 0 128-57.4 128-128s-57.4-128-128-128c-70.6 0-128 57.4-128 128s57.4 128 128 128zM704 0h-384v128h64v256h256v-256h64v-128z" />
-<glyph unicode="&#xe916;" glyph-name="icon-link" d="M1024 320l-512 512v-307.2l-512-204.8v-256h512v-256z" />
-<glyph unicode="&#xe917;" glyph-name="icon-lock" horiz-adv-x="768" d="M702 448h-62v128c0 141.385-114.615 256-256 256s-256-114.615-256-256v0-128h-64c-35.301-0.113-63.887-28.699-64-63.989v-512.011c0.113-35.301 28.699-63.887 63.989-64h638.011c35.301 0.113 63.887 28.699 64 63.989v512.011c-0.113 35.301-28.699 63.887-63.989 64h-0.011zM256 448v128c0 70.692 57.308 128 128 128s128-57.308 128-128v0-128z" />
-<glyph unicode="&#xe918;" glyph-name="icon-minus" d="M960 192c35.2 0 64 28.8 64 64v128c0 35.2-28.8 64-64 64h-896c-35.2 0-64-28.8-64-64v-128c0-35.2 28.8-64 64-64h896z" />
-<glyph unicode="&#xe919;" glyph-name="icon-people" d="M704 512h64c70.4 0 128 57.6 128 128v64c0 70.4-57.6 128-128 128h-64c-70.4 0-128-57.6-128-128v-64c0-70.4 57.6-128 128-128zM256 512h64c70.4 0 128 57.6 128 128v64c0 70.4-57.6 128-128 128h-64c-70.4 0-128-57.6-128-128v-64c0-70.4 57.6-128 128-128zM832 448h-192c-34.908 0-67.716-9.448-96-25.904 57.278-33.324 96-95.404 96-166.096v-448h384v448c0 105.6-86.4 192-192 192zM384 448h-192c-105.6 0-192-86.4-192-192v-448h576v448c0 105.6-86.4 192-192 192z" />
-<glyph unicode="&#xe91a;" glyph-name="icon-person" d="M768 576c0-105.6-86.4-192-192-192h-128c-105.6 0-192 86.4-192 192v64c0 105.6 86.4 192 192 192h128c105.6 0 192-86.4 192-192v-64zM64-192v192c0 140.8 115.2 256 256 256h384c140.8 0 256-115.2 256-256v-192z" />
-<glyph unicode="&#xe91b;" glyph-name="icon-plus" d="M960 448h-330v320c0 35.2-28.8 64-64 64h-108c-35.2 0-64-28.8-64-64v-320h-330c-35.2 0-64-28.8-64-64v-128c0-35.2 28.8-64 64-64h330v-320c0-35.2 28.8-64 64-64h108c35.2 0 64 28.8 64 64v320h330c35.2 0 64 28.8 64 64v128c0 35.2-28.8 64-64 64z" />
-<glyph unicode="&#xe91c;" glyph-name="icon-plus-in-rect" d="M830 832h-636c-106.6 0-194-87.2-194-194v-636c0-106.8 87.4-194 194-194h636c106.6 0 194 87.2 194 194v636c0 106.8-87.4 194-194 194zM896 224c0-17.673-14.327-32-32-32v0h-224v-224c0-17.673-14.327-32-32-32v0h-192c-17.673 0-32 14.327-32 32v0 224h-224c-17.673 0-32 14.327-32 32v0 192c0 17.673 14.327 32 32 32v0h224v224c0 17.673 14.327 32 32 32v0h192c17.673 0 32-14.327 32-32v0-224h224c17.673 0 32-14.327 32-32v0z" />
-<glyph unicode="&#xe91d;" glyph-name="icon-trash" d="M832 704h-192.36v64c0 35.2-28.8 64-64 64h-128c-35.2 0-64-28.8-64-64v-64h-191.64c-105.6 0-192-72-192-160s0-160 0-160h64v-384c0-105.6 86.4-192 192-192h512c105.6 0 192 86.4 192 192v384h64c0 0 0 72 0 160s-86.4 160-192 160zM320 0h-128v384h128v-384zM576 0h-128v384h128v-384zM832 0h-128v384h128v-384z" />
-<glyph unicode="&#xe91e;" glyph-name="icon-x-heavy" d="M704 320l301.332-301.332c24.89-24.89 24.89-65.62 0-90.51l-101.49-101.49c-24.89-24.89-65.62-24.89-90.51 0l-301.332 301.332c0 0-301.332-301.332-301.332-301.332-24.89-24.89-65.62-24.89-90.51 0l-101.49 101.49c-24.89 24.89-24.89 65.62 0 90.51l301.332 301.332c0 0-301.332 301.332-301.332 301.332-24.89 24.89-24.89 65.62 0 90.51l101.49 101.49c24.89 24.89 65.62 24.89 90.51 0l301.332-301.332c0 0 301.332 301.332 301.332 301.332 24.89 24.89 65.62 24.89 90.51 0l101.49-101.49c24.89-24.89 24.89-65.62 0-90.51 0 0-301.332-301.332-301.332-301.332z" />
-<glyph unicode="&#xe91f;" glyph-name="icon-brackets" d="M832 832h-192v-192h191.66l0.34-0.34v-639.32l-0.34-0.34h-191.66v-192h192c105.6 0 192 86.4 192 192v640c0 105.6-86.4 192-192 192zM384 0h-191.66l-0.34 0.34v639.32l0.34 0.34h191.66v192h-192c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h192v192z" />
-<glyph unicode="&#xe920;" glyph-name="icon-crosshair" d="M574 834h-128v-320h128v320zM1022 386h-320v-128h320v128zM574 130h-128v-320h128v320zM318 386h-320v-128h320v128z" />
-<glyph unicode="&#xe921;" glyph-name="icon-grippy" d="M365.4 649.2c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM365.4 429.8c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM365.4 210.2c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM365.4-9.2c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM584.8 758.8c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM584.8 539.4c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM584.8 320c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM584.8 100.6c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM584.8-118.8c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM804.2 649.2c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM804.2 429.8c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM804.2 210.2c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM804.2-9.2c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2z" />
-<glyph unicode="&#xe922;" glyph-name="icon-grid" d="M0 256v-256c0-105.6 86.4-192 192-192h256v448h-448zM448 832h-256c-105.6 0-192-86.4-192-192v-256h448v448zM832 832h-256v-448h448v256c0 105.6-86.4 192-192 192zM576-192h256c105.6 0 192 86.4 192 192v256h-448v-448z" />
-<glyph unicode="&#xe923;" glyph-name="icon-grippy-ew" d="M704 832h128v-1024h-128v1024zM448 832h128v-1024h-128v1024zM192 832h128v-1024h-128v1024z" />
-<glyph unicode="&#xe924;" glyph-name="icon-columns" d="M0 832h256v-1024h-256v1024zM384 832h256v-1024h-256v1024zM768 832h256v-1024h-256v1024z" />
-<glyph unicode="&#xe925;" glyph-name="icon-rows" d="M0 832h1024v-256h-1024v256zM0 448h1024v-256h-1024v256zM0 64h1024v-256h-1024v256z" />
-<glyph unicode="&#xe926;" glyph-name="icon-filter" d="M896 832h-768c-70.601-0.227-127.773-57.399-128-127.978v-768.022c0.227-70.601 57.399-127.773 127.978-128h256.022v512l-192 192h640l-192-192v-512h256c70.601 0.227 127.773 57.399 128 127.978v768.022c-0.227 70.601-57.399 127.773-127.978 128h-0.022z" />
-<glyph unicode="&#xe927;" glyph-name="icon-filter-outline" d="M896 832h-768c-70.601-0.227-127.773-57.399-128-127.978v-768.022c0.227-70.601 57.399-127.773 127.978-128h768.022c70.601 0.227 127.773 57.399 128 127.978v768.022c-0.227 70.601-57.399 127.773-127.978 128h-0.022zM896-63.8h-256v383.8l192 192h-640l192-192v-384h-256v767.8h768z" />
-<glyph unicode="&#xe928;" glyph-name="icon-suitcase" d="M768 704c-0.080 70.66-57.34 127.92-127.993 128h-256.007c-70.66-0.080-127.92-57.34-128-127.993v-128.007h-64v-768h640v768h-64zM384 703.88l0.12 0.12 255.88-0.12v-127.88h-256zM0 512v-640c0.102-35.305 28.695-63.898 63.99-64h64.010v768h-64c-35.305-0.102-63.898-28.695-64-63.99v-0.010zM960 576h-64v-768h64c35.305 0.102 63.898 28.695 64 63.99v640.010c-0.102 35.305-28.695 63.898-63.99 64h-0.010z" />
-<glyph unicode="&#xe929;" glyph-name="icon-cursor-locked" horiz-adv-x="768" d="M704 512h-64v64c0 141.385-114.615 256-256 256s-256-114.615-256-256v0-64h-64c-35.301-0.113-63.887-28.699-64-63.989v-576.011c0.113-35.301 28.699-63.887 63.989-64h640.011c35.301 0.113 63.887 28.699 64 63.989v576.011c-0.113 35.301-28.699 63.887-63.989 64h-0.011zM256 576c0 70.692 57.308 128 128 128s128-57.308 128-128v0-64h-256zM533.4-64l-128 128-43-85-170.4 383.6 383.6-170.2-85-43 128-128z" />
-<glyph unicode="&#xe92a;" glyph-name="icon-flag" d="M192 192h832l-192 320 192 320h-896c-70.606-0.215-127.785-57.394-128-127.979v-896.021h192z" />
-<glyph unicode="&#xe92b;" glyph-name="icon-eye-disabled" d="M209.46 223.32q-7.46 9.86-14.26 20.28c-14.737 21.984-27.741 47.184-37.759 73.847l-0.841 2.553c11.078 29.259 24.068 54.443 39.51 77.869l-0.91-1.469c23.221 34.963 50.705 64.8 82.207 89.793l0.793 0.607c57.663 45.719 130.179 75.053 209.311 79.947l1.069 0.053 114.48 140.88c-27.366 5.017-58.869 7.898-91.041 7.92h-0.019c-245.8 0-452.2-168-510.8-395.6 21.856-82.93 60.906-154.847 113.325-214.773l-0.525 0.613zM814.76 416.92q7.52-10 14.44-20.52c14.737-21.984 27.741-47.184 37.759-73.847l0.841-2.553c-10.859-29.216-23.863-54.416-39.447-77.748l0.847 1.348c-23.221-34.963-50.705-64.8-82.207-89.793l-0.793-0.607c-57.762-45.834-130.437-75.216-209.743-80.049l-1.057-0.051-114.46-140.86c27.346-4.988 58.817-7.84 90.955-7.84 0.037 0 0.074 0 0.111 0h-0.005c245.8 0 452.2 168 510.8 395.6-21.856 82.93-60.906 154.847-113.325 214.773l0.525-0.613zM832 832l-832-1024h192l832 1024h-192z" />
-<glyph unicode="&#xe92c;" glyph-name="icon-notebook-page" d="M830 770h-830l-4-702c0-106.6 87.4-194 194-194h640c106.6 0 194 87.4 194 194v508c0 106.8-87.4 194-194 194zM832 386l-384-384-192 192v256l192-192 384 384v-256z" />
-<glyph unicode="&#xe92d;" glyph-name="icon-unlocked" d="M768 832c-141.339-0.114-255.886-114.661-256-255.989v-128.011h-448c-35.301-0.113-63.887-28.699-64-63.989v-512.011c0.113-35.301 28.699-63.887 63.989-64h638.011c35.301 0.113 63.887 28.699 64 63.989v512.011c-0.113 35.301-28.699 63.887-63.989 64h-62.011v128c0 70.692 57.308 128 128 128s128-57.308 128-128v0-128h128v128c-0.114 141.339-114.661 255.886-255.989 256h-0.011z" />
-<glyph unicode="&#xe92e;" glyph-name="icon-circle" d="M1024 320c0-282.77-229.23-512-512-512s-512 229.23-512 512c0 282.77 229.23 512 512 512s512-229.23 512-512z" />
-<glyph unicode="&#xe92f;" glyph-name="icon-draft" d="M876.34 196.42l-49.9-49.88-19.26-19.5-26-8.7-423.040-144.2 144.2 423.28 8.84 25.78 150 149.88-85.6 149.78c-34.92 61.12-92 61.12-127 0l-422.78-739.72c-34.94-61.14-5.92-111.14 64.48-111.14h843.44c70.4 0 99.42 50 64.48 111.14zM973.18 589.16c-19.32 19.3-40.66 34.62-60.16 43.16-34.42 15.12-52.38 4.54-60.1-3.16l-258.12-258.12-82.8-243.040 243 82.8 3.36 3.4 254.76 254.76c4.94 4.94 10.88 13.88 10.88 28.3 0 25.34-19.5 60.56-50.82 91.9zM631 212.18l-34.88 34.86 34.64 101.6 9.24 3.36h32v-64h64v-32l-3.42-9.26z" />
-<glyph unicode="&#xea00;" glyph-name="icon-arrows-right-left" d="M1024 320l-448-512v1024zM448 832l-448-512 448-512z" />
-<glyph unicode="&#xea01;" glyph-name="icon-arrows-up-down" d="M512 832l512-448h-1024zM0 256l512-448 512 448z" />
-<glyph unicode="&#xea02;" glyph-name="icon-bullet" d="M832 80c0-44-36-80-80-80h-480c-44 0-80 36-80 80v480c0 44 36 80 80 80h480c44 0 80-36 80-80v-480z" />
-<glyph unicode="&#xea03;" glyph-name="icon-calendar" d="M896 832h-768c-70.4 0-128-57.6-128-128v-768c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v768c0 70.4-57.6 128-128 128zM640 384h-256v192h256v-192zM384 320h256v-192h-256v192zM320 128h-256v192h256v-192zM320 576v-192h-256v192h256zM128-128c-17 0-33 6.6-45.2 18.8s-18.8 28.2-18.8 45.2v128h256v-192h-192zM384-128v192h256v-192h-256zM960-64c0-17-6.6-33-18.8-45.2s-28.2-18.8-45.2-18.8h-192v192h256v-128zM960 128h-256v192h256v-192zM960 384h-256v192h256v-192z" />
-<glyph unicode="&#xea04;" glyph-name="icon-chain-links" d="M958.4 766.4c-43.8 43.8-101 65.6-158.4 65.6s-114.6-21.8-158.4-65.6l-128-128c-74-74-85.4-187-34-273l-12.8-12.8c-35.4 20.8-75 31.4-114.8 31.4-57.4 0-114.6-21.8-158.4-65.6l-128-128c-87.4-87.4-87.4-229.4 0-316.8 43.8-43.8 101-65.6 158.4-65.6s114.6 21.8 158.4 65.6l128 128c74 74 85.4 187 34 273l12.8 12.8c35.2-21 75-31.6 114.6-31.6 57.4 0 114.6 21.8 158.4 65.6l128 128c87.6 87.6 87.6 229.6 0.2 317zM419.8 92.2l-128-128c-18-18.2-42.2-28.2-67.8-28.2s-49.8 10-67.8 28.2c-37.4 37.4-37.4 98.4 0 135.8l128 128c18.2 18.2 42.2 28.2 67.8 28.2 5.6 0 11.2-0.6 16.8-1.4l-55.6-55.6c-10.4-10.4-16.2-24.2-16.2-38.8s5.8-28.6 16.2-38.8c10.4-10.4 24.2-16.2 38.8-16.2s28.6 5.8 38.8 16.2l55.6 55.6c5.4-30.4-3.6-62.2-26.6-85zM867.8 540.2l-128-128c-18-18.2-42.2-28.2-67.8-28.2-5.6 0-11.2 0.6-16.8 1.4l55.6 55.6c10.4 10.4 16.2 24.2 16.2 38.8s-5.8 28.6-16.2 38.8c-10.4 10.4-24.2 16.2-38.8 16.2s-28.6-5.8-38.8-16.2l-55.6-55.6c-5.2 29.8 3.6 61.6 26.6 84.6l128 128c18 18.4 42.2 28.4 67.8 28.4s49.8-10 67.8-28.2c37.6-37.4 37.6-98.2 0-135.6z" />
-<glyph unicode="&#xea05;" glyph-name="icon-download" d="M832 256v-255.66l-0.34-0.34-639.66 0.34v255.66h-192v-256c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v256h-192zM512 192l448 448h-256v192h-384v-192h-256l448-448z" />
-<glyph unicode="&#xea06;" glyph-name="icon-duplicate" d="M640 576v128c0 70.4-57.6 128-128 128h-384c-70.4 0-128-57.6-128-128v-384c0-70.4 57.6-128 128-128h128v139.6c0 134.8 109.6 244.4 244.4 244.4h139.6zM896 448h-384c-70.4 0-128-57.6-128-128v-384c0-70.4 57.6-128 128-128h384c70.4 0 128 57.6 128 128v384c0 70.4-57.6 128-128 128z" />
-<glyph unicode="&#xea07;" glyph-name="icon-folder-new" d="M896 640h-320c-16.4 16.4-96.8 96.8-109.2 109.2l-37.4 37.4c-25 25-74.2 45.4-109.4 45.4h-256c-35.2 0-64-28.8-64-64v-384c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v128c0 70.4-57.6 128-128 128zM896 384h-768c-70.4 0-128-57.6-128-128v-320c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v320c0 70.4-57.6 128-128 128zM704 32h-128v-128h-128v128h-128v128h128v128h128v-128h128v-128z" />
-<glyph unicode="&#xea08;" glyph-name="icon-fullscreen-collapse" d="M191.656 0c0.118-0.1 0.244-0.224 0.344-0.344v-191.656h192v192c0 105.6-86.4 192-192 192h-192v-192h191.656zM192 640.344c-0.1-0.118-0.224-0.244-0.344-0.344h-191.656v-192h192c105.6 0 192 86.4 192 192v192h-192v-191.656zM832 448h192v192h-191.656c-0.118 0.1-0.244 0.226-0.344 0.344v191.656h-192v-192c0-105.6 86.4-192 192-192zM832-0.344c0.1 0.118 0.224 0.244 0.344 0.344h191.656v192h-192c-105.6 0-192-86.4-192-192v-192h192v191.656z" />
-<glyph unicode="&#xea09;" glyph-name="icon-fullscreen-expand" d="M192.344 0c-0.118 0.1-0.244 0.224-0.344 0.344v191.656h-192v-192c0-105.6 86.4-192 192-192h192v192h-191.656zM192 639.656c0.1 0.118 0.224 0.244 0.344 0.344h191.656v192h-192c-105.6 0-192-86.4-192-192v-192h192v191.656zM832 832h-192v-192h191.656c0.118-0.1 0.244-0.226 0.344-0.344v-191.656h192v192c0 105.6-86.4 192-192 192zM832 0.344c-0.1-0.118-0.224-0.244-0.344-0.344h-191.656v-192h192c105.6 0 192 86.4 192 192v192h-192v-191.656z" />
-<glyph unicode="&#xea0a;" glyph-name="icon-layers" d="M1024 448l-512 384-512-384 512-384zM512-64l-426.666 320-85.334-64 512-384 512 384-85.334 64z" />
-<glyph unicode="&#xea0b;" glyph-name="icon-line-horz" d="M64 256c-35.346 0-64 28.654-64 64s28.654 64 64 64h896c35.346 0 64-28.654 64-64s-28.654-64-64-64h-896z" />
-<glyph unicode="&#xea0c;" glyph-name="icon-magnify" d="M1024-64l-256.8 256.8c42.4 66.6 65 144 64.8 223.2 0 229.8-186.2 416-416 416s-416-186.2-416-416 186.2-416 416-416c79-0.2 156.4 22.4 223.2 64.8l256.8-256.8 128 128zM212.4 212.4c-112.4 112.4-112.4 294.8 0 407.2s294.8 112.4 407.2 0 112.4-294.8 0-407.2c-54-54-127.2-84.4-203.6-84.4-76.4-0.2-149.8 30.2-203.6 84.4z" />
-<glyph unicode="&#xea0d;" glyph-name="icon-magnify-in" d="M1024-64l-256.86 256.86c40.681 62.963 64.861 139.898 64.861 222.481 0 0.232 0 0.464-0.001 0.696v-0.036c0 229.76-186.24 416-416 416s-416-186.24-416-416 186.24-416 416-416c0.196 0 0.427-0.001 0.659-0.001 82.583 0 159.518 24.18 224.112 65.846l-1.631-0.985 256.86-256.86zM212.36 212.36c-52.114 52.117-84.346 124.114-84.346 203.64 0 159.058 128.942 288 288 288s288-128.942 288-288c0-159.058-128.942-288-288-288-0.005 0-0.010 0-0.014 0h0.001c-0.242-0.001-0.529-0.001-0.815-0.001-79.271 0-151.010 32.251-202.811 84.348l-0.013 0.014zM224 480h384v-128h-384v128zM352 608h128v-384h-128v384z" />
-<glyph unicode="&#xea0e;" glyph-name="icon-magnify-out-v2" d="M767.2 192.8c42.4 66.6 65 144 64.8 223.2 0 229.8-186.2 416-416 416s-416-186.2-416-416 186.2-416 416-416c79-0.2 156.4 22.4 223.2 64.8l256.8-256.8 128 128-256.8 256.8zM619.6 212.4c-54-54-127.2-84.4-203.6-84.4-76.4-0.2-149.8 30.2-203.6 84.4-112.4 112.4-112.4 294.8 0 407.2s294.8 112.4 407.2 0c112.4-112.4 112.4-294.8 0-407.2zM224 480h384v-128h-384v128z" />
-<glyph unicode="&#xea0f;" glyph-name="icon-menu" d="M0 704h1024v-128h-1024v128zM0 384h1024v-128h-1024v128zM0 64h1024v-128h-1024v128z" />
-<glyph unicode="&#xea10;" glyph-name="icon-move" d="M293.4 320l218.6 218.6 256-256v421.4c0 70.4-57.6 128-128 128h-512c-70.4 0-128-57.6-128-128v-512c0-70.4 57.6-128 128-128h421.4l-256 256zM1024 384h-128v-320l-384 384-128-128 384-384h-320v-128h576z" />
-<glyph unicode="&#xea11;" glyph-name="icon-new-window" d="M448 832v-128h320l-384-384 128-128 384 384v-320h128v576zM576 157.726v-157.382c-0.1-0.118-0.226-0.244-0.344-0.344h-383.312c-0.118 0.1-0.244 0.226-0.344 0.344v383.312c0.1 0.118 0.226 0.244 0.344 0.344h157.382l192 192h-349.726c-105.6 0-192-86.4-192-192v-384c0-105.6 86.4-192 192-192h384c105.6 0 192 86.4 192 192v349.726l-192-192z" />
-<glyph unicode="&#xea12;" glyph-name="icon-paint-bucket-v2" d="M544 608v-224c0-88.4-71.6-160-160-160s-160 71.6-160 160v97.2l-197.4-196.4c-50-50-12.4-215.2 112.4-340s290-162.4 340-112.4l417 423.6-352 352zM896-192c70.6 0 128 57.4 128 128 0 108.6-128 192-128 192s-128-83.4-128-192c0-70.6 57.4-128 128-128zM384 320c-35.4 0-64 28.6-64 64v384c0 35.4 28.6 64 64 64s64-28.6 64-64v-384c0-35.4-28.6-64-64-64z" />
-<glyph unicode="&#xea13;" glyph-name="icon-pencil" d="M922.344 730.32c-38.612 38.596-81.306 69.232-120.304 86.324-68.848 30.25-104.77 9.078-120.194-6.344l-516.228-516.216-3.136-9.152-162.482-476.932 485.998 165.612 6.73 6.806 509.502 509.506c9.882 9.866 21.768 27.77 21.768 56.578 0.002 50.71-38.996 121.148-101.654 183.818zM237.982-23.66l-69.73 69.728 69.25 203.228 18.498 6.704h64v-128h128v-64l-6.846-18.506-203.172-69.154z" />
-<glyph unicode="&#xea14;" glyph-name="icon-pencil-edit-in-place" d="M922.4 730.4c-38.6 38.6-81.4 69.2-120.4 86.2-68.8 30.2-104.8 9-120.2-6.4l-516.2-516.2-3.2-9.2-162.4-476.8 486 165.6 516.2 516.4c9.8 9.8 21.8 27.8 21.8 56.6 0 50.6-39 121-101.6 183.8zM238-23.6l-69.8 69.6 69.2 203.2 18.4 6.8h64v-128h128v-64l-6.8-18.6-203-69zM0 832v-512l128 128v256h256l128 128zM1024-192v512l-128-128v-256h-256l-128-128z" />
-<glyph unicode="&#xea15;" glyph-name="icon-play" d="M1024 320l-1024-512v1024z" />
-<glyph unicode="&#xea16;" glyph-name="icon-pause" d="M126 834h256v-1024h-256v1024zM638 834h256v-1024h-256v1024z" />
-<glyph unicode="&#xea17;" glyph-name="icon-plot-resource" d="M255.8 128c0.2 0 0.2 0 0 0l0.2 128c0 70.6 57.4 128 128 128h255.8c0 0 0 0 0.2 0.2v127.8c0 70.6 57.4 128 128 128h143.6c-93.8 117-238 192-399.6 192-282.8 0-512-229.2-512-512 0-68 13.2-132.8 37.2-192h218.6zM768.2 512c-0.2 0-0.2 0 0 0l-0.2-128c0-70.6-57.4-128-128-128h-255.8c0 0 0 0-0.2-0.2v-127.8c0-70.6-57.4-128-128-128h-143.6c93.8-117 238-192 399.6-192 282.8 0 512 229.2 512 512 0 68-13.2 132.8-37.2 192h-218.6z" />
-<glyph unicode="&#xea18;" glyph-name="icon-pointer-left" d="M766-192l-256 512 256 512h-256l-256-512 256-512z" />
-<glyph unicode="&#xea19;" glyph-name="icon-pointer-right" d="M254 832l256-512-256-512h256l256 512-256 512z" />
-<glyph unicode="&#xea1a;" glyph-name="icon-refresh" d="M1024 371.2v460.8l-175.8-175.8c-85.2 69.6-190.8 107.6-302 107.6-127.6 0-247.6-49.8-338-140s-140-210.4-140-338 49.8-247.6 140-338 210.4-140 338-140 247.6 49.8 338 140c74 74 120.8 167.8 135 269.6h-138.6c-32-155.4-169.8-272.8-334.6-272.8-188.2 0-341.4 153.2-341.4 341.4s153.4 341.2 341.6 341.2c76.8 0 147.6-25.4 204.8-68.2l-187.8-187.8h460.8z" />
-<glyph unicode="&#xea1b;" glyph-name="icon-save" d="M192.2 256c-0.2 0-0.2 0 0 0l-0.2-448h640v447.8c0 0 0 0-0.2 0.2h-639.6zM978.8 621.2l-165.4 165.4c-25 25-74.2 45.4-109.4 45.4h-576c-70.4 0-128-57.6-128-128v-768c0-70.4 57.6-128 128-128v448c0 35.2 28.8 64 64 64h640c35.2 0 64-28.8 64-64v-448c70.4 0 128 57.6 128 128v576c0 35.2-20.4 84.4-45.2 109.2zM704 576c0-35.2-28.8-64-64-64h-448c-35.2 0-64 28.8-64 64v192h320v-192h128v192h128v-192z" />
-<glyph unicode="&#xea1c;" glyph-name="icon-save-as" d="M978.8 493.2l-64 64c24.8-24.8 45.2-74 45.2-109.2v-448c0-70.4-57.6-128-128-128h-640c-18.8 0-36.6 4.2-52.6 11.4 20.2-44.4 65-75.4 116.6-75.4h640c70.4 0 128 57.6 128 128v448c0 35.2-20.4 84.4-45.2 109.2zM704-64v319.8c0 0 0 0-0.2 0.2h-511.6l-0.2-320h512zM192 320h512c35.2 0 64-28.8 64-64v-320c70.4 0 128 57.6 128 128v448c0 35.2-20.4 84.4-45.2 109.2l-165.4 165.4c-25 25-74.2 45.4-109.4 45.4h-448c-70.4 0-128-57.6-128-128v-640c0-70.4 57.6-128 128-128v320c0 35.2 28.8 64 64 64zM128 768h192v-192h128v192h128v-192c0-35.2-28.8-64-64-64h-320c-35.2 0-64 28.8-64 64v192z" />
-<glyph unicode="&#xea1d;" glyph-name="icon-sine" d="M1024 320c-1.8 7.2-3.4 14.4-5.2 21.8-20.2 86.2-53.4 209.4-98.4 307.2-22.4 49-45.4 86.6-70.2 115.2-48.6 56-98.4 67.8-131.8 67.8-33.2 0-83.2-11.8-131.8-67.8-24.6-28.6-47.6-66.2-70-115.2-44.8-97.8-78.2-221-98.4-307.2-21.8-93-46.6-175.4-72-238.4-16.4-40.6-30.4-66.4-40.8-82.8-10.4 16.2-24.4 42.2-40.8 82.8-23.2 58-46.2 132.4-66.6 216.6h-198c1.8-7.2 3.4-14.4 5.2-21.8 20.2-86.2 53.4-209.4 98.4-307.2 22.4-49 45.4-86.6 70.2-115.2 48.6-56 98.6-67.8 131.8-67.8s83.2 11.8 131.8 67.8c24.8 28.6 47.6 66.2 70.2 115.2 44.8 97.8 78.2 221 98.4 307.2 21.8 93 46.6 175.4 72 238.4 16.4 40.6 30.4 66.4 40.8 82.8 10.4-16.2 24.4-42.2 40.8-82.8 23.4-57.8 46.4-132.4 66.8-216.4h197.6z" />
-<glyph unicode="&#xea1e;" glyph-name="icon-font" d="M800-192h224l-384 1024h-256l-384-1024h224l84 224h408zM380 224l132 352 132-352z" />
-<glyph unicode="&#xea1f;" glyph-name="icon-thumbs-strip" d="M448 450c0-35.2-28.8-64-64-64h-320c-35.2 0-64 28.8-64 64v320c0 35.2 28.8 64 64 64h320c35.2 0 64-28.8 64-64v-320zM1024 450c0-35.2-28.8-64-64-64h-320c-35.2 0-64 28.8-64 64v320c0 35.2 28.8 64 64 64h320c35.2 0 64-28.8 64-64v-320zM448-126c0-35.2-28.8-64-64-64h-320c-35.2 0-64 28.8-64 64v320c0 35.2 28.8 64 64 64h320c35.2 0 64-28.8 64-64v-320zM1024-126c0-35.2-28.8-64-64-64h-320c-35.2 0-64 28.8-64 64v320c0 35.2 28.8 64 64 64h320c35.2 0 64-28.8 64-64v-320z" />
-<glyph unicode="&#xea20;" glyph-name="icon-two-parts-both" d="M896 832h-768c-70.4 0-128-57.6-128-128v-768c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v768c0 70.4-57.6 128-128 128zM128 704h320v-768h-320v768zM896-64h-320v768h320v-768z" />
-<glyph unicode="&#xea21;" glyph-name="icon-two-parts-one-only" d="M896 832h-768c-70.4 0-128-57.6-128-128v-768c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v768c0 70.4-57.6 128-128 128zM896-64h-320v768h320v-768z" />
-<glyph unicode="&#xea22;" glyph-name="icon-resync" d="M795.2 667.2c-79.8 65.2-178.8 100.8-283.2 100.8-119.6 0-232.2-46.6-316.8-131.2-69.4-69.4-113.2-157.4-126.6-252.8h130c29.6 145.8 158.8 256 313.4 256 72 0 138.4-23.8 192-64l-176-176h432v432l-164.8-164.8zM512 0c-72 0-138.4 23.8-192 64l176 176h-432v-432l164.8 164.8c79.8-65.2 178.8-100.8 283.2-100.8 119.6 0 232.2 46.6 316.8 131.2 69.4 69.4 113.2 157.4 126.6 252.8h-130c-29.6-145.8-158.8-256-313.4-256z" />
-<glyph unicode="&#xea23;" glyph-name="icon-reset" d="M460.8 371.2l-187.8 187.8c57.2 42.8 128 68.2 204.8 68.2 188.2 0 341.6-153.2 341.6-341.4s-153.2-341.2-341.4-341.2c-165 0-302.8 117.6-334.6 273h-138.4c14.2-101.8 61-195.6 135-269.6 90.2-90.2 210.4-140 338-140s247.6 49.8 338 140 140 210.4 140 338-49.8 247.6-140 338-210.4 140-338 140c-111.4 0-217-38-302-107.6l-176 175.6v-460.8h460.8z" />
-<glyph unicode="&#xea24;" glyph-name="icon-x-in-circle" d="M512 832c-282.8 0-512-229.2-512-512s229.2-512 512-512 512 229.2 512 512-229.2 512-512 512zM832 128l-128-128-192 192-192-192-128 128 192 192-192 192 128 128 192-192 192 192 128-128-192-192 192-192z" />
-<glyph unicode="&#xea25;" glyph-name="icon-brightness" d="M253.414 513.939l-155.172 116.384c-50.233-66.209-85.127-146.713-97.91-234.39l-0.333-2.781 191.919-27.434c8.145 56.552 29.998 106.879 62.068 149.006l-0.573-0.784zM191.98 274.283l-191.919-27.434c13.115-90.459 48.009-170.963 99.174-238.453l-0.931 1.281 155.111 116.384c-31.476 41.347-53.309 91.675-61.231 146.504l-0.204 1.719zM466.283 640.020l-27.434 191.919c-90.459-13.115-170.963-48.009-238.453-99.174l1.281 0.931 116.384-155.111c41.347 31.476 91.675 53.309 146.504 61.231l1.719 0.204zM822.323 733.758c-66.209 50.233-146.713 85.127-234.39 97.91l-2.781 0.333-27.434-191.919c56.552-8.145 106.879-29.998 149.006-62.068l-0.784 0.573zM832.020 365.717l191.919 27.434c-13.115 90.459-48.009 170.963-99.174 238.453l0.931-1.281-155.111-116.384c31.476-41.347 53.309-91.675 61.231-146.504l0.204-1.719zM201.677-93.758c66.209-50.233 146.713-85.127 234.39-97.91l2.781-0.333 27.434 191.919c-56.552 8.145-106.879 29.998-149.006 62.068l0.784-0.573zM770.586 126.061l155.131-116.343c50.233 66.209 85.127 146.713 97.91 234.39l0.333 2.781-191.919 27.434c-8.125-56.564-29.966-106.906-62.028-149.049l0.574 0.786zM557.717-0.020l27.434-191.919c90.459 13.115 170.963 48.009 238.453 99.174l-1.281-0.931-116.384 155.111c-41.347-31.476-91.675-53.309-146.504-61.231l-1.719-0.204zM770.586 320c0-142.813-115.773-258.586-258.586-258.586s-258.586 115.773-258.586 258.586c0 142.813 115.773 258.586 258.586 258.586s258.586-115.773 258.586-258.586z" />
-<glyph unicode="&#xea26;" glyph-name="icon-contrast" d="M512 832c-282.78 0-512-229.24-512-512s229.22-512 512-512 512 229.24 512 512-229.22 512-512 512zM783.52 48.48c-69.111-69.481-164.785-112.481-270.502-112.481-0.358 0-0.716 0-1.074 0.001h0.055v768c212.070-0.010 383.982-171.929 383.982-384 0-106.034-42.977-202.031-112.462-271.52v0z" />
-<glyph unicode="&#xea27;" glyph-name="icon-expand" d="M960 832c0 0 0 0 0 0h-320v-128h165.4l-210.6-210.8c-25-25-25-65.6 0-90.6 12.4-12.4 28.8-18.8 45.2-18.8s32.8 6.2 45.2 18.8l210.8 210.8v-165.4h128v384h-64zM896 26.6l-210.8 210.6c-25 25-65.6 25-90.6 0s-25-65.6 0-90.6l210.8-210.6h-165.4v-128h384v384h-128v-165.4zM218.6 704h165.4v128h-320c0 0 0 0 0 0h-64v-384h128v165.4l210.8-210.8c12.4-12.4 28.8-18.8 45.2-18.8s32.8 6.2 45.2 18.8c25 25 25 65.6 0 90.6l-210.6 210.8zM338.8 237.2l-210.8-210.6v165.4h-128v-384h384v128h-165.4l210.8 210.8c25 25 25 65.6 0 90.6-25.2 24.8-65.6 24.8-90.6-0.2z" />
-<glyph unicode="&#xea28;" glyph-name="icon-list-view" d="M0 768h1024v-128h-1024v128zM0 512h1024v-128h-1024v128zM0 256h1024v-128h-1024v128zM0 0h1024v-128h-1024v128z" />
-<glyph unicode="&#xea29;" glyph-name="icon-grid-snap-to" d="M382 2h448v448h-448v-448zM510 322h192v-192h-192v192zM-2 258h320v-64h-320v64zM894 258h128v-64h-128v64zM574 834h64v-320h-64v320zM574-62h64v-128h-64v128zM574 258h64v-64h-64v64z" />
-<glyph unicode="&#xea2a;" glyph-name="icon-grid-snap-no" d="M768 256h192v-64h-192v64zM256 256h192v-64h-192v64zM0 256h192v-64h-192v64zM640 320h-64v-64h-64v-64h64v-64h64v64h64v64h-64zM576 576h64v-192h-64v192zM576 832h64v-192h-64v192zM576 64h64v-192h-64v192z" />
-<glyph unicode="&#xea2b;" glyph-name="icon-frame-show" d="M0 768v-896h1024v896h-1024zM896 0h-768v640h768v-640zM192 576h384v-128h-384v128z" />
-<glyph unicode="&#xea2c;" glyph-name="icon-frame-hide" d="M128 642h420l104 128h-652v-802.4l128 157.4zM896 2h-420l-104-128h652v802.4l-128-157.4zM832 834l-832-1024h192l832 1024zM392 450l104 128h-304v-128z" />
-<glyph unicode="&#xea2d;" glyph-name="icon-import" d="M832 639.6v-639.4c0-0.2-0.2-0.2-0.4-0.4h-319.6v-192h320c105.6 0 192 86.4 192 192v640.2c0 105.6-86.4 192-192 192h-320v-192h319.6c0.2 0 0.4-0.2 0.4-0.4zM192 128v-192l384 384-384 384v-192h-192v-384z" />
-<glyph unicode="&#xea2e;" glyph-name="icon-export" d="M192 0.34v639.32l0.34 0.34h319.66v192h-320c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h320v192h-319.66zM1024 320l-384 384v-192h-192v-384h192v-192l384 384z" />
-<glyph unicode="&#xea2f;" glyph-name="icon-font-size" horiz-adv-x="1504" d="M1226.4 512h-176l-76.22-203.24 77-205.34 87.22 232.58 90.74-242h-174.44l49.5-132h174.44l57.76-154h154l-264 704zM384 832l-384-1024h224l84 224h408l84-224h224l-384 1024zM380 224l132 352 132-352z" />
-<glyph unicode="&#xea30;" glyph-name="icon-clear-data" d="M632 520l-120-120-120 120-80-80 120-120-120-120 80-80 120 120 120-120 80 80-120 120 120 120-80 80zM512 832c-282.76 0-512-86-512-192v-640c0-106 229.24-192 512-192s512 86 512 192v640c0 106-229.24 192-512 192zM512 0c-176.731 0-320 143.269-320 320s143.269 320 320 320c176.731 0 320-143.269 320-320v0c0-176.731-143.269-320-320-320v0z" />
-<glyph unicode="&#xea31;" glyph-name="icon-history" d="M576 768c-247.4 0-448-200.6-448-448h-128l192-192 192 192h-128c0 85.4 33.2 165.8 93.8 226.2 60.4 60.6 140.8 93.8 226.2 93.8s165.8-33.2 226.2-93.8c60.6-60.4 93.8-140.8 93.8-226.2s-33.2-165.8-93.8-226.2c-60.4-60.6-140.8-93.8-226.2-93.8s-165.8 33.2-226.2 93.8l-90.6-90.6c81-81 193-131.2 316.8-131.2 247.4 0 448 200.6 448 448s-200.6 448-448 448zM576 560c-26.6 0-48-21.4-48-48v-211.8l142-142c9.4-9.4 21.6-14 34-14s24.6 4.6 34 14c18.8 18.8 18.8 49.2 0 67.8l-114 114v172c0 26.6-21.4 48-48 48z" />
-<glyph unicode="&#xea32;" glyph-name="icon-arrow-up-to-parent" horiz-adv-x="1056" d="M643.427 6.739c-81.955 0.697-148.179 67.065-148.642 149.010v395.872l296.871-247.393v197.914l-395.828 329.857-395.828-328.62v-197.502l296.871 246.156v-396.241c0-190.905 155.239-346.556 346.144-346.968l412.321-0.825 0.412 197.914z" />
-<glyph unicode="&#xea33;" glyph-name="icon-crosshair-in-circle" d="M512 832c-282.8 0-512-229.2-512-512s229.2-512 512-512 512 229.2 512 512-229.2 512-512 512zM783.6 48.4c-54.634-54.8-125.77-93.12-205.322-106.874l-2.278-0.326v250.8h-128v-250.8c-161.302 28.062-286.738 153.497-314.468 312.5l-0.332 2.3h250.8v128h-250.8c28.062 161.302 153.497 286.738 312.5 314.468l2.3 0.332v-250.8h128v250.8c161.302-28.062 286.738-153.497 314.468-312.5l0.332-2.3h-250.8v-128h250.8c-14.080-81.83-52.4-152.966-107.191-207.591l-0.009-0.009z" />
-<glyph unicode="&#xea34;" glyph-name="icon-target" d="M512 448c70.692 0 128-57.308 128-128s-57.308-128-128-128c-70.692 0-128 57.308-128 128v0c0.114 70.647 57.353 127.886 127.989 128h0.011zM512 576c-141.385 0-256-114.615-256-256s114.615-256 256-256c141.385 0 256 114.615 256 256v0c-0.114 141.339-114.661 255.886-255.989 256h-0.011zM512 704c211.87-0.128 383.575-171.912 383.575-383.8 0-211.967-171.833-383.8-383.8-383.8s-383.8 171.833-383.8 383.8c0 105.99 42.963 201.945 112.425 271.4v0c69.21 69.437 164.944 112.401 270.713 112.401 0.312 0 0.624 0 0.936-0.001h-0.048zM512 832c-282.8 0-512-229.2-512-512s229.2-512 512-512 512 229.2 512 512-229.2 512-512 512z" />
-<glyph unicode="&#xea35;" glyph-name="icon-items-collapse" d="M45.2 173.2h229.6l-274.8-274.6 90.6-90.6 274.6 274.8v-229.6h128v448h-448v-128zM1024 741.4l-90.6 90.6-274.6-274.8v229.6h-128v-448h448v128h-229.6l274.8 274.6z" />
-<glyph unicode="&#xea36;" glyph-name="icon-items-expand" d="M448-64h-229.4l274.6 274.8-90.4 90.4-274.8-274.6v229.4h-128v-448h448v128zM530.8 429.2l90.4-90.4 274.8 274.6v-229.4h128v448h-448v-128h229.4l-274.6-274.8z" />
-<glyph unicode="&#xea37;" glyph-name="icon-3-dots" d="M256 320c0-70.692-57.308-128-128-128s-128 57.308-128 128c0 70.692 57.308 128 128 128s128-57.308 128-128zM640 320c0-70.692-57.308-128-128-128s-128 57.308-128 128c0 70.692 57.308 128 128 128s128-57.308 128-128zM1024 320c0-70.692-57.308-128-128-128s-128 57.308-128 128c0 70.692 57.308 128 128 128s128-57.308 128-128z" />
-<glyph unicode="&#xea38;" glyph-name="icon-grid-on" d="M1024 448v128h-256v256h-128v-256h-256v256h-128v-256h-256v-128h256v-256h-256v-128h256v-256h128v256h256v-256h128v256h256v128h-256v256zM640 192h-256v256h256z" />
-<glyph unicode="&#xea39;" glyph-name="icon-grid-off" d="M256 280.6l128 157.6v9.8h8l104 128h-112v256h-128v-256h-256v-128h256v-167.4zM184 192h-184v-128h80l104 128zM768 359.4l-128-157.6v-9.8h-8l-104-128h112v-256h128v256h256v128h-256v167.4zM840 448h184v128h-80l-104-128zM832 832l-832-1024h192l832 1024h-192z" />
-<glyph unicode="&#xea3a;" glyph-name="icon-camera" d="M896 576h-128l-128 256h-256l-128-256h-128c-70.601-0.227-127.773-57.399-128-127.978v-512.022c0.227-70.601 57.399-127.773 127.978-128h768.022c70.601 0.227 127.773 57.399 128 127.978v512.022c-0.227 70.601-57.399 127.773-127.978 128h-0.022zM512-32c-141.385 0-256 114.615-256 256s114.615 256 256 256c141.385 0 256-114.615 256-256v0c0-141.385-114.615-256-256-256v0z" />
-<glyph unicode="&#xea3b;" glyph-name="icon-folders-collapse" d="M896 512v-448c-0.215-70.606-57.394-127.785-127.979-128h-576.021c0.215-70.606 57.394-127.785 127.979-128h576.021c70.606 0.215 127.785 57.394 128 127.979v448.021c-0.215 70.606-57.394 127.785-127.979 128h-0.021zM832 128v448c-0.215 70.606-57.394 127.785-127.979 128h-192.021l-101.5 82.74c-24.88 24.9-74.040 45.26-109.24 45.26h-237.26c-35.305-0.102-63.898-28.695-64-63.99v-640.010c0.215-70.606 57.394-127.785 127.979-128h576.021c70.606 0.215 127.785 57.394 128 127.979v0.021zM128 188v516l256-260z" />
-<glyph unicode="&#xeb00;" glyph-name="icon-activity" d="M576 768h-256l320-320h-290.256c-44.264 76.516-126.99 128-221.744 128h-128v-512h128c94.754 0 177.48 51.484 221.744 128h290.256l-320-320h256l448 448-448 448z" />
-<glyph unicode="&#xeb01;" glyph-name="icon-activity-mode" d="M512 832c-214.8 0-398.8-132.4-474.8-320h90.8c56.8 0 108-24.8 143-64h241l-192 192h256l320-320-320-320h-256l192 192h-241c-35-39.2-86.2-64-143-64h-90.8c76-187.6 259.8-320 474.8-320 282.8 0 512 229.2 512 512s-229.2 512-512 512z" />
-<glyph unicode="&#xeb02;" glyph-name="icon-autoflow-tabular" d="M192 832c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h64v1024h-64zM384 832h256v-1024h-256v1024zM832 832h-64v-704h256v512c0 105.6-86.4 192-192 192z" />
-<glyph unicode="&#xeb03;" glyph-name="icon-clock" d="M512 832c-282.8 0-512-229.2-512-512s229.2-512 512-512 512 229.2 512 512-229.2 512-512 512zM782 142c-12.8-22.2-36.6-36-62.4-36-12.6 0-25 3.4-36 9.6l-222 128.2c-0.8 0.4-1.6 1-2.4 1.4l-0.8 0.6-1.8 1.2-2.4 2-1.8 1.4-0.6 0.6c-0.8 0.6-1.4 1.2-2.2 1.8v0c-5 4.6-9.4 10-13 15.8-0.2 0.4-0.6 1-0.8 1.4s-0.6 1-0.8 1.4c-3.2 6-5.8 12.4-7.2 19.2v0.2c-0.2 1-0.4 1.8-0.6 2.8 0 0.2 0 0.6-0.2 0.8-0.2 0.6-0.2 1.4-0.2 2.2s-0.2 1-0.2 1.6 0 1-0.2 1.6-0.2 1.6-0.2 2.2c0 0.4 0 0.6 0 1 0 1 0 1.8 0 2.8 0 0 0 0.2 0 0.4v363.8c0 39.8 32.2 72 72 72s72-32.2 72-72v-322.4l185.8-107.2c34.2-20 45.8-64 26-98.4z" />
-<glyph unicode="&#xeb04;" glyph-name="icon-database" d="M1024 640c0-106.039-229.23-192-512-192s-512 85.961-512 192c0 106.039 229.23 192 512 192s512-85.961 512-192zM512 320c-282.77 0-512 85.962-512 192v-512c0-106.038 229.23-192 512-192s512 85.962 512 192v512c0-106.038-229.23-192-512-192z" />
-<glyph unicode="&#xeb05;" glyph-name="icon-database-query" d="M683.52 12.714c-50.782-28.456-109.284-44.714-171.52-44.714-194.094 0-352 157.906-352 352s157.906 352 352 352 352-157.906 352-352c0-62.236-16.258-120.738-44.714-171.52l191.692-191.692c8.516 13.89 13.022 28.354 13.022 43.212v640c0 106.038-229.23 192-512 192s-512-85.962-512-192v-640c0-106.038 229.23-192 512-192 126.11 0 241.548 17.108 330.776 45.46l-159.256 159.254zM352 320c0-88.224 71.776-160 160-160s160 71.776 160 160-71.776 160-160 160-160-71.776-160-160z" />
-<glyph unicode="&#xeb06;" glyph-name="icon-dataset" d="M896 640h-320c-16.4 16.4-96.8 96.8-109.2 109.2l-37.4 37.4c-25 25-74.2 45.4-109.4 45.4h-256c-35.2 0-64-28.8-64-64v-384c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v128c0 70.4-57.6 128-128 128zM896 384h-768c-70.4 0-128-57.6-128-128v-320c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v320c0 70.4-57.6 128-128 128zM320-64h-128v320h128v-320zM576-64h-128v320h128v-320zM832-64h-128v320h128v-320z" />
-<glyph unicode="&#xeb07;" glyph-name="icon-datatable" d="M1024 640c0-106.039-229.23-192-512-192s-512 85.961-512 192c0 106.039 229.23 192 512 192s512-85.961 512-192zM512 320c-282.8 0-512 86-512 192v-512c0-106 229.2-192 512-192s512 86 512 192v512c0-106-229.2-192-512-192zM896 257v-256c-36.6-15.6-79.8-28.8-128-39.4v256c48.2 10.6 91.4 23.8 128 39.4zM256 217.6v-256c-48.2 10.4-91.4 23.8-128 39.4v256c36.6-15.6 79.8-28.8 128-39.4zM384-58v256c41-4 83.8-6 128-6s87 2.2 128 6v-256c-41-4-83.8-6-128-6s-87 2.2-128 6z" />
-<glyph unicode="&#xeb08;" glyph-name="icon-dictionary" d="M832 192c105.6 0 192 86.4 192 192v256c0 105.6-86.4 192-192 192v-320l-128 64-128-64v320h-384c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v192c0-105.6-86.4-192-192-192h-640v192h640z" />
-<glyph unicode="&#xeb09;" glyph-name="icon-folder" d="M896 640h-320c-16.4 16.4-96.8 96.8-109.2 109.2l-37.4 37.4c-25 25-74.2 45.4-109.4 45.4h-256c-35.2 0-64-28.8-64-64v-384c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v128c0 70.4-57.6 128-128 128zM896 384h-768c-70.4 0-128-57.6-128-128v-320c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v320c0 70.4-57.6 128-128 128z" />
-<glyph unicode="&#xeb0a;" glyph-name="icon-image" d="M896 832h-768c-70.4 0-128-57.6-128-128v-768c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v768c0 70.4-57.6 128-128 128zM896-64h-768v768h768v-768zM320 576l-128-128v-448h640v320l-128 128-128-128z" />
-<glyph unicode="&#xeb0b;" glyph-name="icon-layout" d="M448 832h-256c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h256v1024zM832 832h-256v-577.664h448v385.664c0 105.6-86.4 192-192 192zM576-192h256c105.6 0 192 86.4 192 192v129.664h-448v-321.664z" />
-<glyph unicode="&#xeb0c;" glyph-name="icon-object" d="M512-192l512 320v384l-512.020 320-511.98-320v-384l512-320zM512 640l358.4-224-358.4-224-358.4 224 358.4 224z" />
-<glyph unicode="&#xeb0d;" glyph-name="icon-object-unknown" d="M510 834l-512-320v-384l512-320 512 320v384l-512 320zM585.4-27.2c-21.2-20.8-46-30.8-76-30.8-31.2 0-56.2 9.8-76.2 29.6-20 20-29.6 44.8-29.6 76.2 0 30.4 10.2 55.2 31 76.2s45.2 31.2 74.8 31.2c29.6 0 54.2-10.4 75.6-32s31.8-46.4 31.8-76c-0.2-29-10.8-54-31.4-74.4zM638.2 285.4c-23.6-11.8-37.4-22-43.4-32.4-3.6-6.2-6-14.8-7.4-26.8v-41h-161.4v44.2c0 40.2 4.4 69.8 13 88 8 17.2 22.6 30.2 44.8 40l34.8 15.4c32 14.2 48.2 35.2 48.2 62.8 0 16-6 30.4-17.2 41.8-11.2 11.2-25.6 17.2-41.6 17.2-24 0-54.4-10-62.8-57.4l-2.2-12.2h-147l1.4 16.2c4 44.6 17 82.4 38.8 112.2 19.6 27 45.6 48.6 77 64.6s64.6 24 98.2 24c60.6 0 110.2-19.4 151.4-59.6 41.2-40 61.2-88 61.2-147.2 0-70.8-28.8-121.4-85.8-149.8z" />
-<glyph unicode="&#xeb0e;" glyph-name="icon-packet" d="M512 832l-512-320v-512c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v512l-512 320zM512 640l358.4-224-358.4-224-358.4 224 358.4 224z" />
-<glyph unicode="&#xeb0f;" glyph-name="icon-page" d="M704 320c-105.6 0-192 86.4-192 192v320h-320c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v320h-320zM768 448h256l-384 384v-256c0-70.4 57.6-128 128-128z" />
-<glyph unicode="&#xeb10;" glyph-name="icon-plot-overlay" d="M830 832h-636c-106.7 0-194-87.3-194-194v-406.82c14.18-18.64 25.66-28.34 32-30.84 14.28 5.62 54.44 47.54 92.96 146 42.46 108.38 116.32 237.66 227.040 237.66 52.4 0 101.42-29.16 145.7-86.68 37.34-48.5 64.84-108.92 81.34-151.080 38.52-98.38 78.68-140.3 92.96-146 14.28 5.62 54.44 47.54 92.96 146 42.46 108.48 116.32 237.76 227.040 237.76 11.355-0.003 22.389-1.366 32.952-3.936l-0.952 0.196v57.74c0 106.7-87.3 194-194 194zM992 439.66c-14.28-5.62-54.44-47.52-92.96-146-42.46-108.38-116.32-237.66-227.040-237.66-52.4 0-101.42 29.16-145.7 86.68-37.34 48.5-64.84 108.92-81.34 151.080-38.52 98.38-78.68 140.3-92.96 146-14.28-5.62-54.44-47.52-92.96-146-42.46-108.48-116.32-237.76-227.040-237.76-11.355 0.003-22.389 1.367-32.952 3.936l0.952-0.196v-57.74c0-106.7 87.3-194 194-194h636c106.7 0 194 87.3 194 194v406.82c-14.18 18.64-25.66 28.34-32 30.84z" />
-<glyph unicode="&#xeb11;" glyph-name="icon-plot-stacked" d="M89.6 520c24.98 0 48.96 26.52 85.52 70.18 45.42 54.28 102 121.82 196 121.82 44.64 0 86.62-15.46 124.8-46 28.68-22.9 51.16-50.42 72.92-77.060 38.42-46.94 59.16-68.94 83.96-68.94h371.2v118c0 106.7-87.3 194-194 194h-636c-106.7 0-194-87.3-194-194v-118h89.6zM529.5 421.6c-28.24 22.64-50.52 50-72 76.28-35.5 43.48-58.76 70.12-86.3 70.12-25.060 0-49.080-26.54-85.66-70.24-45.4-54.24-102-121.76-196-121.76h-89.54v-112h371.2c44 0 85.54-15.34 123.3-45.6 28.24-22.64 50.52-50 72-76.28 35.5-43.48 58.76-70.12 86.3-70.12 25.060 0 49.080 26.54 85.66 70.24 45.4 54.24 102 121.76 196 121.76h89.54v112h-371.2c-44.060 0-85.54 15.34-123.3 45.6zM934.4 120c-24.98 0-48.96-26.52-85.52-70.18-45.42-54.28-102-121.82-196-121.82-44.64 0-86.62 15.46-124.8 46-28.68 22.9-51.16 50.42-72.92 77.060-38.42 46.94-59.16 68.94-83.96 68.94h-371.2v-118c0-106.7 87.3-194 194-194h636c106.7 0 194 87.3 194 194v118h-89.6z" />
-<glyph unicode="&#xeb12;" glyph-name="icon-session" d="M635.6 307.6c6.6-4.2 13.2-8.6 19.2-13.6l120.4-96.4c29.6-23.8 83.8-23.8 113.4 0l135.2 108c0.2 4.8 0.2 9.4 0.2 14.2 0 52.2-7.8 102.4-22.2 149.8l-154.8-123.6c-58.2-46.6-140.2-59.2-211.4-38.4zM248.6 197.8l120.4 96.4c58 46.4 140 59.2 211.2 38.4-6.6 4.2-13.2 8.6-19.2 13.6l-120.4 96.4c-29.6 23.8-83.8 23.8-113.4 0l-120.2-96.6c-40-32-91.4-48-143-48-21.6 0-43 2.8-63.8 8.4 0-0.6 0-1.2 0-1.6 5-3.4 10-6.8 14.6-10.6l120.4-96.4c29.8-23.8 83.8-23.8 113.4 0zM120.6 453.8l120.4 96.4c80.2 64.2 205.6 64.2 285.8 0l120.4-96.4c29.6-23.8 83.8-23.8 113.4 0l181 144.8c-91.2 140.4-249.6 233.4-429.6 233.4-238.6 0-439.2-163.2-496-384.2 30.8-17.6 77.8-15.6 104.6 6zM689 90l-120.4 96.4c-29.6 23.8-83.8 23.8-113.4 0l-120.2-96.4c-40-32-91.4-48-143-48-47.8 0-95.4 13.8-134.2 41.4 85.6-163.6 256.8-275.4 454.2-275.4s368.6 111.8 454.2 275.4c-80.4-57.4-199.8-55.2-277.2 6.6z" />
-<glyph unicode="&#xeb13;" glyph-name="icon-tabular" d="M896 832h-768c-70.4 0-128-57.6-128-128v-768c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v768c0 70.4-57.6 128-128 128zM640 384h-256v192h256v-192zM384 320h256v-192h-256v192zM320 128h-256v192h256v-192zM320 576v-192h-256v192h256zM128-128c-17 0-33 6.6-45.2 18.8s-18.8 28.2-18.8 45.2v128h256v-192h-192zM384-128v192h256v-192h-256zM960-64c0-17-6.6-33-18.8-45.2s-28.2-18.8-45.2-18.8h-192v192h256v-128zM960 128h-256v192h256v-192zM960 384h-256v192h256v-192z" />
-<glyph unicode="&#xeb14;" glyph-name="icon-tabular-lad" d="M896 832h-768c-70.6-0.2-127.8-57.4-128-128v-768c0.2-70.6 57.4-127.8 128-128h768c70.6 0.2 127.8 57.4 128 128v768c-0.2 70.6-57.4 127.8-128 128zM64 576h256v-192h-256v192zM64 320h256v-192h-256v192zM128-128c-35.2 0.2-63.8 28.8-64 64v128h256v-192h-192zM384-128v192h256v-192h-256zM960-64c-0.2-35.2-28.8-63.8-64-64h-192v192h256v-128zM960 320v-192h-576v192h64v64h-64v192h576v-192h-64v-64h64zM782.4 284.6l-110.4 55.2v172.2c0 17.6-14.4 32-32 32s-32-14.4-32-32v-211.8l145.6-72.8c15.8-8 35-1.6 43 14.4 8 15.6 1.6 35-14.2 42.8v0z" />
-<glyph unicode="&#xeb15;" glyph-name="icon-tabular-lad-set" d="M128 64v576c-70.6-0.2-127.8-57.4-128-128v-576c0.2-70.6 57.4-127.8 128-128h576c70.6 0.2 127.8 57.4 128 128h-576c-70.6 0.2-127.8 57.4-128 128zM896 832h-576c-70.6-0.2-127.8-57.4-128-128v-576c0.2-70.6 57.4-127.8 128-128h576c70.6 0.2 127.8 57.4 128 128v576c-0.2 70.6-57.4 127.8-128 128zM256 640h192v-128h-192v128zM256 448h192v-192h-192v192zM320 64c-35.2 0.2-63.8 28.8-64 64v64h192v-128h-128zM512 64v128h192v-128h-192zM960 128c-0.2-35.2-28.8-63.8-64-64h-128v128h192v-64zM960 256h-448v384h448v-384zM832 352c17.6 0 32 14.4 32 32 0 13.8-8.8 26-21.8 30.4l-74.2 24.6v105c0 17.6-14.4 32-32 32s-32-14.4-32-32v-151l117.8-39.2c3.4-1.2 6.8-1.8 10.2-1.8z" />
-<glyph unicode="&#xeb16;" glyph-name="icon-tabular-realtime" d="M896 832h-768c-70.606-0.215-127.785-57.394-128-127.979v-768.021c0.215-70.606 57.394-127.785 127.979-128h768.021c70.606 0.215 127.785 57.394 128 127.979v768.021c-0.215 70.606-57.394 127.785-127.979 128h-0.021zM448 540l25.060-25.32c7.916-7.922 18.856-12.822 30.94-12.822s23.023 4.9 30.94 12.822v0l75.5 76.3c29.97 30.338 71.571 49.128 117.56 49.128s87.59-18.79 117.544-49.112l0.016-0.016 50.44-50.98v-152.2c-24.111 8.83-44.678 22.255-61.542 39.342l-0.018 0.018-75.5 76.3c-7.916 7.922-18.856 12.822-30.94 12.822s-23.023-4.9-30.94-12.822v0l-75.5-76.3c-29.971-30.343-71.575-49.137-117.568-49.137-20.084 0-39.331 3.584-57.137 10.146l1.145-0.369v152.2zM320-128h-192c-35.26 0.214-63.786 28.74-64 63.98v128.020h256v-192zM320 128h-256v192h256v-192zM320 384h-256v192h256v-192zM640-128h-256v192h256v-192zM448 195.38v174.5c1.88-1.74 3.74-3.5 5.56-5.34l75.5-76.3c7.916-7.922 18.856-12.822 30.94-12.822s23.023 4.9 30.94 12.822v0l75.5 76.3c29.966 30.333 71.56 49.119 117.542 49.119 43.28 0 82.673-16.643 112.128-43.879l-0.11 0.1v-174.5c-1.88 1.74-3.74 3.5-5.56 5.34l-75.5 76.3c-7.916 7.922-18.856 12.822-30.94 12.822s-23.023-4.9-30.94-12.822v0l-75.5-76.3c-29.966-30.333-71.56-49.119-117.542-49.119-43.28 0-82.673 16.643-112.128 43.879l0.11-0.1zM960-64c-0.214-35.26-28.74-63.786-63.98-64h-192.020v192h256v-128z" />
-<glyph unicode="&#xeb17;" glyph-name="icon-tabular-scrolling" d="M64 832c-35.2 0-64-28.8-64-64v-192h448v256h-384zM1024 576v192c0 35.2-28.8 64-64 64h-384v-256h448zM0 448v-192c0-35.2 28.8-64 64-64h384v256h-448zM960 192c35.2 0 64 28.8 64 64v192h-448v-256h384zM512-192l-256 256h512z" />
-<glyph unicode="&#xeb18;" glyph-name="icon-telemetry" d="M32 200.34c14.28 5.62 54.44 47.54 92.96 146 42.46 108.38 116.32 237.66 227.040 237.66 52.4 0 101.42-29.16 145.7-86.68 37.34-48.5 64.84-108.92 81.34-151.080 38.52-98.38 78.68-140.3 92.96-146 14.28 5.62 54.44 47.54 92.96 146 37.4 95.5 99.14 207.14 188.94 232.46-90.462 152.598-254.314 253.3-441.686 253.3-0.075 0-0.15 0-0.225 0h0.011c-282.76 0-512-229.24-512-512 0-0.032 0-0.070 0-0.108 0-35.719 3.641-70.587 10.572-104.254l-0.572 3.323c9.54-10.78 17.22-16.74 22-18.62zM992 439.66c-14.28-5.62-54.44-47.52-92.96-146-42.46-108.38-116.32-237.66-227.040-237.66-52.4 0-101.42 29.16-145.7 86.68-37.34 48.5-64.84 108.92-81.34 151.080-38.52 98.38-78.68 140.3-92.96 146-14.28-5.62-54.44-47.52-92.96-146-37.4-95.5-99.14-207.14-188.94-232.46 90.462-152.598 254.314-253.3 441.686-253.3 0.075 0 0.15 0 0.225 0h-0.011c282.76 0 512 229.24 512 512 0 0.032 0 0.070 0 0.108 0 35.719-3.641 70.587-10.572 104.254l0.572-3.323c-9.54 10.78-17.22 16.74-22 18.62z" />
-<glyph unicode="&#xeb19;" glyph-name="icon-timeline" d="M832 832h-640c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v640c0 105.6-86.4 192-192 192zM128 512v128h256v-128zM256 384h384v-128h-384zM896 0h-448v128h448zM896 256h-128v128h128zM896 512h-384v128h384z" />
-<glyph unicode="&#xeb1a;" glyph-name="icon-timer" d="M640 685.4v82.58c0 35.346-28.654 64-64 64v0h-128c-35.346 0-64-28.654-64-64v0-82.58c-185.040-55.080-320-226.48-320-429.42 0-247.42 200.58-448 448-448s448 200.58 448 448c0 202.96-135 374.4-320 429.42zM532 235.98l-263.76-211c-57.105 59.935-92.24 141.25-92.24 230.772 0 0.080 0 0.16 0 0.24v-0.012c0 185.28 150.72 336 336 336 6.72 0 13.38-0.22 20-0.62v-355.38z" />
-<glyph unicode="&#xeb1b;" glyph-name="icon-topic" d="M454.36 355.36l86.3 86.3c9.088 8.965 21.577 14.502 35.36 14.502s26.272-5.537 35.366-14.507l86.294-86.294c19.328-19.358 42.832-34.541 69.047-44.082l1.313-0.418v172.14l-57.64 57.64c-34.408 34.33-81.9 55.558-134.35 55.558s-99.943-21.228-134.354-55.562l-86.296-86.296c-9.088-8.965-21.577-14.502-35.36-14.502s-26.272 5.537-35.366 14.507l-28.674 28.654v-172.14c19.045-7.022 41.040-11.084 63.984-11.084 52.463 0 99.966 21.239 134.379 55.587l-0.003-0.003zM505.64 284.64l-86.3-86.3c-9.088-8.965-21.577-14.502-35.36-14.502s-26.272 5.537-35.366 14.507l-86.294 86.294c-2 2-4.2 4-6.36 6v-197.36c33.664-30.721 78.65-49.537 128.031-49.537 52.44 0 99.923 21.22 134.333 55.541l86.296 86.296c9.088 8.965 21.577 14.502 35.36 14.502s26.272-5.537 35.366-14.507l86.294-86.294c2-2 4.2-4 6.36-6v197.36c-33.664 30.721-78.65 49.537-128.031 49.537-52.44 0-99.923-21.22-134.333-55.541l0.004 0.004zM832 832h-128v-192h127.66l0.34-0.34v-639.32l-0.34-0.34h-127.66v-192h128c105.6 0 192 86.4 192 192v640c0 105.6-86.4 192-192 192zM320 0h-127.66l-0.34 0.34v639.32l0.34 0.34h127.66v192h-128c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h128v192z" />
-<glyph unicode="&#xeb1c;" glyph-name="icon-box-with-dashed-lines-v2" d="M0 448h128v-256h-128v256zM128 703.78l0.22 0.22h191.78v128h-192c-70.606-0.215-127.785-57.394-128-127.979v-192.021h128v191.78zM128-63.78v191.78h-128v-192c0.215-70.606 57.394-127.785 127.979-128h192.021v128h-191.78zM384 832h256v-128h-256v128zM896-63.78l-0.22-0.22h-191.78v-128h192c70.606 0.215 127.785 57.394 128 127.979v192.021h-128v-191.78zM896 832h-192v-128h191.78l0.22-0.22v-191.78h128v192c-0.215 70.606-57.394 127.785-127.979 128h-0.021zM896 448h128v-256h-128v256zM384-64h256v-128h-256v128zM256 576h512v-512h-512v512z" />
-<glyph unicode="&#xeb1d;" glyph-name="icon-summary-widget" d="M896 832h-768c-70.4 0-128-57.6-128-128v-768c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v768c0 70.4-57.6 128-128 128zM847.8 221.6l-82.6-143.2-189.6 131.6 19.2-230h-165.4l19.2 230-189.6-131.6-82.6 143.2 208.6 98.4-208.8 98.4 82.6 143.2 189.6-131.6-19.2 230h165.4l-19.2-230 189.6 131.6 82.6-143.2-208.6-98.4 208.8-98.4z" />
-<glyph unicode="&#xeb1e;" glyph-name="icon-notebook" d="M896 721.2c0 79.8-55.4 127.4-123 105.4l-773-250.6h896v145.2zM896 512h-896v-576c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v448c0 70.4-57.6 128-128 128zM832 0h-384v320h384v-320z" />
-<glyph unicode="&#xeb1f;" glyph-name="icon-tabs-view" d="M0-64c0.227-70.601 57.399-127.773 127.978-128h768.022c70.601 0.227 127.773 57.399 128 127.978v608.022h-512l-50.2 225.6c-7.6 34.2-42.6 62.4-77.8 62.4h-256c-70.601-0.227-127.773-57.399-128-127.978v-0.022zM832 64h-640v256h640zM480 832c35.2 0 70.2-28.2 77.8-62.4l36-161.6h430.2v96c-0.227 70.601-57.399 127.773-127.978 128h-0.022z" />
-<glyph unicode="&#xeb20;" glyph-name="icon-flexible-layout" d="M0 0c0-105.6 86.4-192 192-192h64v576h-256zM0 640v-128h256v320h-64c-105.6 0-192-86.4-192-192zM768-192h64c105.6 0 192 86.4 192 192v128h-256zM384 832h256v-1024h-256v1024zM832 832h-64v-576h256v384c0 105.6-86.4 192-192 192z" />
-<glyph unicode="&#xeb21;" glyph-name="icon-generator-sine" d="M152 358.2c10.8 4.2 40.8 35.6 69.8 109.4 31.8 81.4 87.2 178.4 170.2 178.4 39.4 0 76-21.8 109.2-65 28-36.4 48.8-81.6 61-113.4 29-73.8 59-105.2 69.8-109.4 10.8 4.2 40.8 35.6 69.8 109.4s74.2 155.4 141.6 174.4c-67.89 114.467-190.82 190-331.391 190-0.003 0-0.007 0-0.010 0h0.001c-212 0-384-172-384-384 0.017-26.829 2.71-53.018 7.827-78.329l-0.427 2.529c7.2-8 13-12.6 16.6-14zM884.6 355c7.235 27.919 11.392 59.972 11.4 92.995v0.005c-0.017 26.829-2.71 53.018-7.827 78.329l0.427-2.529c-7.2 8-13 12.6-16.6 14-10.8-4.2-40.8-35.6-69.8-109.4-21.8-55.8-54.6-119-100-153.2zM512 192l135 59c-4.485-0.614-9.689-0.977-14.972-1h-0.028c-39.4 0-76 21.8-109.2 65-28 36.4-48.8 81.6-61 113.4-29 73.8-59 105.2-69.8 109.4-10.8-4.2-40.8-35.6-69.8-109.4-16.4-42.2-39.2-88.4-68.8-123.2zM1024 352l-512-224-512 224v-320l512-224 512 224v320z" />
-<glyph unicode="&#xeb22;" glyph-name="icon-generator-event" d="M320 640h384v-64h-384v64zM320 384h384v-64h-384v64zM320 512h320v-64h-320v64zM256 703.8h512v-399.8l128 56v344c-0.227 70.601-57.399 127.773-127.978 128h-512.022c-70.601-0.227-127.773-57.399-128-127.978v-344.022l128-56zM658.2 256h-292.4l146.2-64 146.2 64zM512 128l-512 224v-320l512-224 512 224v320l-512-224z" />
-<glyph unicode="&#xeb23;" glyph-name="icon-gauge-v2" d="M512 832c-282.8 0-512-229.2-512-512 0-226.4 147-418.4 350.6-486l257.4 486v-503c236.8 45 416 253 416 503 0 282.8-229.2 512-512 512zM754.8 304.2c-58.967 68.597-145.842 111.772-242.8 111.772s-183.833-43.176-242.445-111.35l-0.355-0.422-146 125c8.6 10 17.4 19.6 26.8 28.8 92.628 92.679 220.619 150.006 362 150.006s269.372-57.326 361.997-150.003l0.003-0.003c9.4-9.2 18.2-18.8 26.8-28.8z" />
-<glyph unicode="&#xeb24;" glyph-name="icon-spectra" d="M768 128h-512l102.4 179.2-358.4-51.2v-254c0-106.6 87.4-194 194-194h636c106.8 0 194 87.4 194 194v62l-325.8 186.2zM830 832h-636c-106.6 0-194-87.2-194-194v-318l400 60.2 112 195.8 109.8-192h402.2v254c-0.227 107.052-86.948 193.773-193.978 194h-0.022zM1024 192v64l-384 64 384-128z" />
-<glyph unicode="&#xeb25;" glyph-name="icon-telemetry-spectra" d="M512 576l109.8-192h398.2c-31.4 252.6-247 448-508 448-282.8 0-512-229.2-512-512l400 60.2zM768 128h-512l102.4 179.2-354.4-50.6c31.2-252.8 246.8-448.6 508-448.6 201.6 0 376 116.6 459.6 286l-273.4 156.2zM640 320l384-128v64l-384 64z" />
-<glyph unicode="&#xeb26;" glyph-name="icon-pushbutton" d="M370.2 372.6c9.326-8.53 19.666-16.261 30.729-22.914l0.871-0.486c-11.077 19.209-17.664 42.221-17.8 66.76v0.040c0 39.6 17.8 77.6 50.2 107.4 37 34 87.4 52.6 141.8 52.6 40.2 0 78.2-10.2 110.2-29.2-8.918 15.653-19.693 29.040-32.268 40.482l-0.132 0.118c-37 34-87.4 52.6-141.8 52.6s-104.8-18.6-141.8-52.6c-32.4-29.8-50.2-67.8-50.2-107.4s17.8-77.6 50.2-107.4zM885.4 562.4c-40.6 154.6-192.4 269.6-373.4 269.6s-332.8-115-373.4-269.6c-86-80-138.6-187.8-138.6-306.4 0-247.4 229.2-448 512-448s512 200.6 512 448c0 118.6-52.6 226.4-138.6 306.4zM512 704c141.2 0 256-100.4 256-224s-114.8-224-256-224-256 100.4-256 224 114.8 224 256 224zM512 0c-175.4 0-318.4 127.8-320 285.4 68.8-94.8 186.4-157.4 320-157.4s251.2 62.6 320 157.4c-1.6-157.6-144.6-285.4-320-285.4z" />
-<glyph unicode="&#xeb27;" glyph-name="icon-conditional" d="M512 832c-282.76 0-512-229.24-512-512s229.24-512 512-512 512 229.24 512 512-229.24 512-512 512zM512 64l-384 256 384 256 384-256z" />
-<glyph unicode="&#xeb28;" glyph-name="icon-condition-widget" d="M832 832h-640c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v640c0 105.6-86.4 192-192 192zM512 64l-384 256 384 256 384-256z" />
-<glyph unicode="&#xeb29;" glyph-name="icon-alphanumeric" d="M535.6 301.4c-8.4-1.6-17.2-3-26.2-4s-18.2-2.4-27.2-4c-10.196-1.861-18.808-4.010-27.21-6.633l1.61 0.433c-8.609-2.674-16.105-6.348-22.89-10.987l0.29 0.187c-6.693-4.517-12.283-10.107-16.663-16.585l-0.137-0.215c-4.6-6.8-7.4-15.6-8.8-26s-0.4-18.4 2.4-25.2c2.746-6.688 7.224-12.195 12.881-16.122l0.119-0.078c5.967-4.053 13.057-6.94 20.704-8.161l0.296-0.039c7.592-1.527 16.319-2.4 25.25-2.4 0.123 0 0.246 0 0.369 0h-0.019c22.2 0 39.6 3.6 52.6 11s23.2 16.2 30.2 26.4c6.273 8.873 11.271 19.191 14.426 30.285l0.174 0.715c1.853 6.809 3.601 15.41 4.855 24.169l0.145 1.231 5.2 41.6c-5.4-4.217-11.723-7.564-18.583-9.689l-0.417-0.111c-6.489-2.241-14.362-4.255-22.444-5.662l-0.956-0.138zM1024 448v192h-152l24 192h-192l-24-192h-256l24 192h-192l-24-192h-232v-192h208l-32-256h-176v-192h152l-24-192h192l24 192h256l-24-192h192l24 192h232v192h-208l32 256zM702.8 420.2l-26.4-211.8c-2.231-15.809-3.537-34.122-3.6-52.727v-0.073c0-16.8 2.2-29.4 6.4-37.8h-113.4c-1.342 5.556-2.338 12.122-2.781 18.84l-0.019 0.36c-0.261 3.524-0.409 7.634-0.409 11.778 0 2.962 0.076 5.907 0.226 8.832l-0.017-0.41c-18.663-17.401-41.395-30.694-66.597-38.289l-1.203-0.311c-22.627-6.956-48.639-10.974-75.586-11h-0.014c-0.764-0.011-1.666-0.018-2.569-0.018-18.098 0-35.598 2.563-52.156 7.345l1.325-0.328c-15.991 4.512-29.851 12.090-41.545 22.122l0.145-0.122c-11.233 9.982-19.792 22.733-24.624 37.192l-0.176 0.608c-5.2 15.2-6.4 33.4-3.8 54.4s9.4 42.2 19.4 57.2c9.524 14.399 21.535 26.346 35.532 35.512l0.468 0.288c13.387 8.662 28.922 15.533 45.512 19.765l1.088 0.235c13.436 3.792 30.801 7.554 48.47 10.41l2.93 0.39c17 2.6 33.8 4.6 50.4 6.2 16.628 1.527 31.69 4.070 46.349 7.643l-2.149-0.443c13 3 23.6 7.6 31.6 13.6s12.6 15 13.6 26.4 0.8 21.8-2.4 28.8c-2.849 6.902-7.542 12.56-13.468 16.517l-0.132 0.083c-6.217 4.011-13.604 6.78-21.543 7.774l-0.257 0.026c-7.897 1.277-17 2.007-26.274 2.007-0.537 0-1.073-0.002-1.609-0.007l0.082 0.001c-22 0-40-4.6-53.8-14.2s-23-25.2-28-47.2h-111.8c4.8 26.2 14.2 48 27.8 65.4 13.475 16.978 29.89 30.968 48.574 41.377l0.826 0.423c18.192 10.038 39.297 17.806 61.619 22.175l1.381 0.225c20.488 4.162 44.053 6.563 68.171 6.6h0.029c21.8-0.005 43.239-1.532 64.222-4.479l-2.422 0.279c20.641-2.809 39.324-8.783 56.401-17.461l-1.001 0.461c15.909-8.108 28.858-20.031 37.967-34.601l0.233-0.399c9-15 12.2-34.8 9-59.6z" />
-<glyph unicode="&#xeb2a;" glyph-name="icon-image-telemetry" d="M512 832c-282.8 0-512-229.2-512-512s229.2-512 512-512 512 229.2 512 512-229.2 512-512 512zM783.6 48.4c-69.581-69.675-165.757-112.776-272-112.776-212.298 0-384.4 172.102-384.4 384.4s172.102 384.4 384.4 384.4c212.298 0 384.4-172.102 384.4-384.4 0-0.008 0-0.017 0-0.025v0.001c0.001-0.264 0.001-0.575 0.001-0.887 0-105.769-42.964-201.503-112.391-270.703l-0.010-0.010zM704 448l-128-128-192 192-192-192c0-176.731 143.269-320 320-320s320 143.269 320 320v0z" />
-<glyph unicode="&#xeb2b;" glyph-name="icon-telemetry-aggregate" d="M78 436.56c14 41.44 37.48 100.8 69.2 148.36 38.62 57.78 82.38 87.080 130.14 87.080s91.5-29.3 130-87.080c31.72-47.56 55.14-106.92 69.2-148.36 30.88-90.96 63.12-134.98 78-146.54 14.94 11.56 47.2 55.58 78 146.54 14 41.44 37.48 100.8 69.22 148.36q27.8 41.7 59.12 63.5c-75.7 111.377-201.81 183.58-344.783 183.58-0.034 0-0.068 0-0.103 0h0.006c-229.76 0-416-186.24-416-416 0-0.071 0-0.156 0-0.24 0-39.119 5.396-76.977 15.484-112.871l-0.704 2.931c16.78 21.74 40.4 63.34 63.22 130.74zM754 395.44c-14-41.44-37.48-100.8-69.2-148.36-38.56-57.78-82.32-87.080-130-87.080s-91.5 29.3-130 87.080c-31.72 47.56-55.14 106.92-69.2 148.36-30.88 90.96-63.14 134.98-78 146.54-14.94-11.56-47.2-55.58-78-146.54-14.38-41.44-37.8-100.8-69.6-148.36q-27.8-41.7-59.12-63.5c75.7-111.378 201.81-183.58 344.783-183.58 0.119 0 0.237 0 0.356 0h-0.019c229.76 0 416 186.24 416 416 0 0.071 0 0.156 0 0.24 0 39.119-5.396 76.977-15.484 112.871l0.704-2.931c-16.78-21.74-40.4-63.34-63.22-130.74zM921.56 497.38c4.098-24.449 6.44-52.617 6.44-81.332 0-0.017 0-0.034 0-0.051v0.003c0-0.095 0-0.208 0-0.32 0-282.593-229.087-511.68-511.68-511.68-0.113 0-0.225 0-0.338 0h0.018c-0.014 0-0.031 0-0.048 0-28.716 0-56.884 2.342-84.325 6.845l2.993-0.405c72.483-63.623 168.109-102.44 272.802-102.44 0.203 0 0.406 0 0.61 0h-0.031c229.76 0 416 186.24 416 416 0 0.172 0 0.375 0 0.578 0 104.692-38.817 200.319-102.844 273.271l0.404-0.47z" />
-<glyph unicode="&#xeb2c;" glyph-name="icon-bar-graph" d="M832 832h-640c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v640c0 105.6-86.4 192-192 192zM267.64-64h-139.64v448h139.64zM477.1-64h-139.64v768h139.64zM686.54-64h-139.64v320h139.64zM896-64h-139.64v640h139.64z" />
-<glyph unicode="&#xeb2d;" glyph-name="icon-map" d="M896 766.6l-128-62.6v-896l128 62.6c70.4 34.42 128 120.2 128 190.6v640c0 70.4-57.6 99.82-128 65.4zM320-80l387.2-96.8v896l-387.2 96.8v-896zM259.2 831.2l-3.2 0.8-128-62.6c-70.4-34.42-128-120.2-128-190.6v-640c0-70.4 57.6-99.82 128-65.4l128 62.6 3.2-0.8z" />
-<glyph unicode="&#xeb2e;" glyph-name="icon-plan" d="M256 640v64c0.215 70.606 57.394 127.785 127.979 128h256.021c70.606-0.215 127.785-57.394 128-127.979v-64.021zM832 704v-128h-640v128c-105.6 0-192-86.4-192-192v-512c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v512c0 105.6-86.4 192-192 192zM128 256v128h256v-128zM640 0h-384v128h384zM896 0h-128v128h128zM896 256h-384v128h384z" />
-<glyph unicode="&#xeb2f;" glyph-name="icon-timelist" d="M896 832h-768c-70.606-0.215-127.785-57.394-128-127.979v-768.021c0.215-70.606 57.394-127.785 127.979-128h768.021c70.606 0.215 127.785 57.394 128 127.979v768.021c-0.215 70.606-57.394 127.785-127.979 128h-0.021zM426.94 298.54c-8.054-15.864-24.249-26.545-42.938-26.545-7.823 0-15.209 1.871-21.734 5.191l0.273-0.126-154.54 77.28v221.66c0 26.51 21.49 48 48 48s48-21.49 48-48v0-162.34l101.46-50.72c15.864-8.054 26.545-24.249 26.545-42.938 0-7.823-1.871-15.209-5.191-21.734l0.126 0.273zM896-64h-320v128h320zM896 128h-320v128h320zM896 320h-320v128h320zM896 512h-320v128h320z" />
+<glyph unicode="&#xe900;" glyph-name="icon-alert-rect-v2" d="M896 896h-768c-70.6-0.2-127.8-57.4-128-128v-768c0.2-70.6 57.4-127.8 128-128h768c70.6 0.2 127.8 57.4 128 128v768c-0.2 70.6-57.4 127.8-128 128zM576 0h-128v128h128v-128zM597.8 384l-37.8-192h-96l-37.8 192v384h171.8v-384z" />
+<glyph unicode="&#xe901;" glyph-name="icon-alert-triangle-v2" d="M998.2 47.2l-422.6 739.6c-35 61.2-92 61.2-127 0l-422.8-739.6c-35-61.2-6-111.2 64.4-111.2h843.4c70.6 0 99.6 50 64.6 111.2zM576 0h-128v128h128v-128zM597.8 384l-37.8-192h-96l-37.8 192v256h171.8v-256z" />
+<glyph unicode="&#xe902;" glyph-name="icon-arrow-up" d="M512 640l-512-512h1024z" />
+<glyph unicode="&#xe903;" glyph-name="icon-arrow-double-up" d="M510 386l512-512h-1024zM510 898l512-512h-1024z" />
+<glyph unicode="&#xe904;" glyph-name="icon-arrow-tall-up" d="M512 896l512-1024h-1024z" />
+<glyph unicode="&#xe905;" glyph-name="icon-arrow-right" d="M768 384l-512 512v-1024z" />
+<glyph unicode="&#xe906;" glyph-name="icon-arrow-right-equilateral" d="M962 384l-896-512v1024z" />
+<glyph unicode="&#xe907;" glyph-name="icon-arrow-down" d="M512 128l512 512h-1024z" />
+<glyph unicode="&#xe908;" glyph-name="icon-arrow-double-down" d="M510 386l-512 512h1024zM510-126l-512 512h1024z" />
+<glyph unicode="&#xe909;" glyph-name="icon-arrow-tall-down" d="M512-128l-512 1024h1024z" />
+<glyph unicode="&#xe90a;" glyph-name="icon-arrow-left" d="M256 384l512-512v1024z" />
+<glyph unicode="&#xe90b;" glyph-name="icon-asterisk" d="M1004.166 555.542l-97.522 168.916-330.534-229.414 33.414 400.956h-195.048l33.414-400.956-330.534 229.414-97.522-168.916 363.944-171.542-363.944-171.542 97.522-168.916 330.534 229.414-33.414-400.956h195.048l-33.414 400.956 330.534-229.414 97.522 168.916-363.944 171.542z" />
+<glyph unicode="&#xe90c;" glyph-name="icon-bell" d="M512-128c106 0 192 86 192 192h-384c0-106 86-192 192-192zM896 448v64c0 212-172 384-384 384s-384-172-384-384v-64c0-70.6-57.4-128-128-128v-128h1024v128c-70.6 0-128 57.4-128 128z" />
+<glyph unicode="&#xe90d;" glyph-name="icon-box-round-corners" d="M1024 64c0-105.6-86.4-192-192-192h-640c-105.6 0-192 86.4-192 192v640c0 105.6 86.4 192 192 192h640c105.6 0 192-86.4 192-192v-640z" />
+<glyph unicode="&#xe90e;" glyph-name="icon-box-with-arrow-cursor" d="M894 898h-768c-70.4 0-128-57.6-128-128v-768c0-70.4 57.6-128 128-128h400c-2.2 3.8-4 7.6-5.8 11.4l-255.2 576.8c-21.4 48.4-10.8 105 26.6 142.4 24.4 24.4 57.2 37.4 90.4 37.4 17.4 0 35.2-3.6 51.8-11l576.6-255.4c4-1.8 7.8-3.8 11.4-5.8v400.2c0.2 70.4-57.4 128-127.8 128zM958.6 258.6l-576.6 255.4 255.4-576.6 64.6 128.6 192-192 128 128-192 192z" />
+<glyph unicode="&#xe90f;" glyph-name="icon-check" d="M1024 896l-640-640-384 384v-384l384-384 640 640z" />
+<glyph unicode="&#xe910;" glyph-name="icon-connectivity" d="M704 320c0-70.4-57.6-128-128-128h-128c-70.4 0-128 57.6-128 128v128c0 70.4 57.6 128 128 128h128c70.4 0 128-57.6 128-128v-128zM1024 384l-192 320v-640zM0 384l192 320v-640z" />
+<glyph unicode="&#xe911;" glyph-name="icon-database-in-brackets" d="M768 544c0-53.019-114.615-96-256-96s-256 42.981-256 96c0 53.019 114.615 96 256 96s256-42.981 256-96zM768 224v256c0-53-114.6-96-256-96s-256 43-256 96v-256c0-53 114.6-96 256-96s256 43 256 96zM832 896h-128v-192h127.6c0.2 0 0.2-0.2 0.4-0.4v-639.4c0-0.2-0.2-0.2-0.4-0.4h-127.6v-192h128c105.6 0 192 86.4 192 192v640.2c0 105.6-86.4 192-192 192zM192 64.4v639.4c0 0.2 0.2 0.2 0.4 0.4h127.6v191.8h-128c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h128v192h-127.6c-0.2 0-0.4 0.2-0.4 0.4z" />
+<glyph unicode="&#xe912;" glyph-name="icon-eye-open" d="M512 779.6c-245.8 0-452.2-168-510.8-395.6 58.6-227.4 265-395.6 510.8-395.6s452.2 168 510.8 395.6c-58.6 227.4-265 395.6-510.8 395.6zM829.2 307.6c-22.6-34.4-50.6-64.8-83-90.4-32.8-25.8-69-45.6-108-59.4-40.4-14.2-82.8-21.4-126-21.4s-85.8 7.2-126 21.4c-39 13.8-75.4 33.8-108 59.4-32.4 25.6-60.4 55.8-83 90.4-15.8 24-28.8 49.6-38.6 76.4 10 26.8 23 52.4 38.6 76.4 22.6 34.4 50.6 64.8 83 90.4 32.8 25.8 69 45.6 108 59.4 40.4 14.2 82.8 21.4 126 21.4s85.8-7.2 126-21.4c39-13.8 75.4-33.8 108-59.4 32.4-25.6 60.4-55.8 83-90.4 15.8-24 28.8-49.6 38.6-76.4-9.8-26.8-22.8-52.4-38.6-76.4zM704 384c0-106.039-85.961-192-192-192s-192 85.961-192 192c0 106.039 85.961 192 192 192s192-85.961 192-192z" />
+<glyph unicode="&#xe913;" glyph-name="icon-gear" d="M1024 320v128l-140.976 35.244c-8.784 32.922-21.818 64.106-38.504 92.918l74.774 124.622-90.51 90.51-124.622-74.774c-28.812 16.686-59.996 29.72-92.918 38.504l-35.244 140.976h-128l-35.244-140.976c-32.922-8.784-64.106-21.818-92.918-38.504l-124.622 74.774-90.51-90.51 74.774-124.622c-16.686-28.812-29.72-59.996-38.504-92.918l-140.976-35.244v-128l140.976-35.244c8.784-32.922 21.818-64.106 38.504-92.918l-74.774-124.622 90.51-90.51 124.622 74.774c28.812-16.686 59.996-29.72 92.918-38.504l35.244-140.976h128l35.244 140.976c32.922 8.784 64.106 21.818 92.918 38.504l124.622-74.774 90.51 90.51-74.774 124.622c16.686 28.812 29.72 59.996 38.504 92.918l140.976 35.244zM704 384c0-106.038-85.962-192-192-192s-192 85.962-192 192 85.962 192 192 192 192-85.962 192-192z" />
+<glyph unicode="&#xe914;" glyph-name="icon-hourglass" d="M1024 896h-1024c0-282.8 229.2-512 512-512s512 229.2 512 512zM512 512c-102.6 0-199 40-271.6 112.4-41.2 41.2-72 90.2-90.8 143.6h724.6c-18.8-53.4-49.6-102.4-90.8-143.6-72.4-72.4-168.8-112.4-271.4-112.4zM512 384c-282.8 0-512-229.2-512-512h1024c0 282.8-229.2 512-512 512z" />
+<glyph unicode="&#xe915;" glyph-name="icon-info" d="M512 896c-282.8 0-512-229.2-512-512s229.2-512 512-512 512 229.2 512 512-229.2 512-512 512zM512 768c70.6 0 128-57.4 128-128s-57.4-128-128-128c-70.6 0-128 57.4-128 128s57.4 128 128 128zM704 64h-384v128h64v256h256v-256h64v-128z" />
+<glyph unicode="&#xe916;" glyph-name="icon-link" d="M1024 384l-512 512v-307.2l-512-204.8v-256h512v-256z" />
+<glyph unicode="&#xe917;" glyph-name="icon-lock" horiz-adv-x="768" d="M702 512h-62v128c0 141.385-114.615 256-256 256s-256-114.615-256-256v0-128h-64c-35.301-0.113-63.887-28.699-64-63.989v-512.011c0.113-35.301 28.699-63.887 63.989-64h638.011c35.301 0.113 63.887 28.699 64 63.989v512.011c-0.113 35.301-28.699 63.887-63.989 64h-0.011zM256 512v128c0 70.692 57.308 128 128 128s128-57.308 128-128v0-128z" />
+<glyph unicode="&#xe918;" glyph-name="icon-minus" d="M960 256c35.2 0 64 28.8 64 64v128c0 35.2-28.8 64-64 64h-896c-35.2 0-64-28.8-64-64v-128c0-35.2 28.8-64 64-64h896z" />
+<glyph unicode="&#xe919;" glyph-name="icon-people" d="M704 576h64c70.4 0 128 57.6 128 128v64c0 70.4-57.6 128-128 128h-64c-70.4 0-128-57.6-128-128v-64c0-70.4 57.6-128 128-128zM256 576h64c70.4 0 128 57.6 128 128v64c0 70.4-57.6 128-128 128h-64c-70.4 0-128-57.6-128-128v-64c0-70.4 57.6-128 128-128zM832 512h-192c-34.908 0-67.716-9.448-96-25.904 57.278-33.324 96-95.404 96-166.096v-448h384v448c0 105.6-86.4 192-192 192zM384 512h-192c-105.6 0-192-86.4-192-192v-448h576v448c0 105.6-86.4 192-192 192z" />
+<glyph unicode="&#xe91a;" glyph-name="icon-person" d="M768 640c0-105.6-86.4-192-192-192h-128c-105.6 0-192 86.4-192 192v64c0 105.6 86.4 192 192 192h128c105.6 0 192-86.4 192-192v-64zM64-128v192c0 140.8 115.2 256 256 256h384c140.8 0 256-115.2 256-256v-192z" />
+<glyph unicode="&#xe91b;" glyph-name="icon-plus" d="M960 512h-330v320c0 35.2-28.8 64-64 64h-108c-35.2 0-64-28.8-64-64v-320h-330c-35.2 0-64-28.8-64-64v-128c0-35.2 28.8-64 64-64h330v-320c0-35.2 28.8-64 64-64h108c35.2 0 64 28.8 64 64v320h330c35.2 0 64 28.8 64 64v128c0 35.2-28.8 64-64 64z" />
+<glyph unicode="&#xe91c;" glyph-name="icon-plus-in-rect" d="M830 896h-636c-106.6 0-194-87.2-194-194v-636c0-106.8 87.4-194 194-194h636c106.6 0 194 87.2 194 194v636c0 106.8-87.4 194-194 194zM896 288c0-17.673-14.327-32-32-32v0h-224v-224c0-17.673-14.327-32-32-32v0h-192c-17.673 0-32 14.327-32 32v0 224h-224c-17.673 0-32 14.327-32 32v0 192c0 17.673 14.327 32 32 32v0h224v224c0 17.673 14.327 32 32 32v0h192c17.673 0 32-14.327 32-32v0-224h224c17.673 0 32-14.327 32-32v0z" />
+<glyph unicode="&#xe91d;" glyph-name="icon-trash" d="M832 768h-192.36v64c0 35.2-28.8 64-64 64h-128c-35.2 0-64-28.8-64-64v-64h-191.64c-105.6 0-192-72-192-160s0-160 0-160h64v-384c0-105.6 86.4-192 192-192h512c105.6 0 192 86.4 192 192v384h64c0 0 0 72 0 160s-86.4 160-192 160zM320 64h-128v384h128v-384zM576 64h-128v384h128v-384zM832 64h-128v384h128v-384z" />
+<glyph unicode="&#xe91e;" glyph-name="icon-x-heavy" d="M704 384l301.332-301.332c24.89-24.89 24.89-65.62 0-90.51l-101.49-101.49c-24.89-24.89-65.62-24.89-90.51 0l-301.332 301.332c0 0-301.332-301.332-301.332-301.332-24.89-24.89-65.62-24.89-90.51 0l-101.49 101.49c-24.89 24.89-24.89 65.62 0 90.51l301.332 301.332c0 0-301.332 301.332-301.332 301.332-24.89 24.89-24.89 65.62 0 90.51l101.49 101.49c24.89 24.89 65.62 24.89 90.51 0l301.332-301.332c0 0 301.332 301.332 301.332 301.332 24.89 24.89 65.62 24.89 90.51 0l101.49-101.49c24.89-24.89 24.89-65.62 0-90.51 0 0-301.332-301.332-301.332-301.332z" />
+<glyph unicode="&#xe91f;" glyph-name="icon-brackets" d="M832 896h-192v-192h191.66l0.34-0.34v-639.32l-0.34-0.34h-191.66v-192h192c105.6 0 192 86.4 192 192v640c0 105.6-86.4 192-192 192zM384 64h-191.66l-0.34 0.34v639.32l0.34 0.34h191.66v192h-192c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h192v192z" />
+<glyph unicode="&#xe920;" glyph-name="icon-crosshair" d="M574 898h-128v-320h128v320zM1022 450h-320v-128h320v128zM574 194h-128v-320h128v320zM318 450h-320v-128h320v128z" />
+<glyph unicode="&#xe921;" glyph-name="icon-grippy" d="M365.4 713.2c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM365.4 493.8c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM365.4 274.2c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM365.4 54.8c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM584.8 822.8c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM584.8 603.4c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM584.8 384c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM584.8 164.6c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM584.8-54.8c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM804.2 713.2c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM804.2 493.8c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM804.2 274.2c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2zM804.2 54.8c0-40.427-32.773-73.2-73.2-73.2s-73.2 32.773-73.2 73.2c0 40.427 32.773 73.2 73.2 73.2s73.2-32.773 73.2-73.2z" />
+<glyph unicode="&#xe922;" glyph-name="icon-grid" d="M0 320v-256c0-105.6 86.4-192 192-192h256v448h-448zM448 896h-256c-105.6 0-192-86.4-192-192v-256h448v448zM832 896h-256v-448h448v256c0 105.6-86.4 192-192 192zM576-128h256c105.6 0 192 86.4 192 192v256h-448v-448z" />
+<glyph unicode="&#xe923;" glyph-name="icon-grippy-ew" d="M704 896h128v-1024h-128v1024zM448 896h128v-1024h-128v1024zM192 896h128v-1024h-128v1024z" />
+<glyph unicode="&#xe924;" glyph-name="icon-columns" d="M0 896h256v-1024h-256v1024zM384 896h256v-1024h-256v1024zM768 896h256v-1024h-256v1024z" />
+<glyph unicode="&#xe925;" glyph-name="icon-rows" d="M0 896h1024v-256h-1024v256zM0 512h1024v-256h-1024v256zM0 128h1024v-256h-1024v256z" />
+<glyph unicode="&#xe926;" glyph-name="icon-filter" d="M896 896h-768c-70.601-0.227-127.773-57.399-128-127.978v-768.022c0.227-70.601 57.399-127.773 127.978-128h256.022v512l-192 192h640l-192-192v-512h256c70.601 0.227 127.773 57.399 128 127.978v768.022c-0.227 70.601-57.399 127.773-127.978 128h-0.022z" />
+<glyph unicode="&#xe927;" glyph-name="icon-filter-outline" d="M896 896h-768c-70.601-0.227-127.773-57.399-128-127.978v-768.022c0.227-70.601 57.399-127.773 127.978-128h768.022c70.601 0.227 127.773 57.399 128 127.978v768.022c-0.227 70.601-57.399 127.773-127.978 128h-0.022zM896 0.2h-256v383.8l192 192h-640l192-192v-384h-256v767.8h768z" />
+<glyph unicode="&#xe928;" glyph-name="icon-suitcase" d="M768 768c-0.080 70.66-57.34 127.92-127.993 128h-256.007c-70.66-0.080-127.92-57.34-128-127.993v-128.007h-64v-768h640v768h-64zM384 767.88l0.12 0.12 255.88-0.12v-127.88h-256zM0 576v-640c0.102-35.305 28.695-63.898 63.99-64h64.010v768h-64c-35.305-0.102-63.898-28.695-64-63.99v-0.010zM960 640h-64v-768h64c35.305 0.102 63.898 28.695 64 63.99v640.010c-0.102 35.305-28.695 63.898-63.99 64h-0.010z" />
+<glyph unicode="&#xe929;" glyph-name="icon-cursor-locked" horiz-adv-x="768" d="M704 576h-64v64c0 141.385-114.615 256-256 256s-256-114.615-256-256v0-64h-64c-35.301-0.113-63.887-28.699-64-63.989v-576.011c0.113-35.301 28.699-63.887 63.989-64h640.011c35.301 0.113 63.887 28.699 64 63.989v576.011c-0.113 35.301-28.699 63.887-63.989 64h-0.011zM256 640c0 70.692 57.308 128 128 128s128-57.308 128-128v0-64h-256zM533.4 0l-128 128-43-85-170.4 383.6 383.6-170.2-85-43 128-128z" />
+<glyph unicode="&#xe92a;" glyph-name="icon-flag" d="M192 256h832l-192 320 192 320h-896c-70.606-0.215-127.785-57.394-128-127.979v-896.021h192z" />
+<glyph unicode="&#xe92b;" glyph-name="icon-eye-disabled" d="M209.46 287.32q-7.46 9.86-14.26 20.28c-14.737 21.984-27.741 47.184-37.759 73.847l-0.841 2.553c11.078 29.259 24.068 54.443 39.51 77.869l-0.91-1.469c23.221 34.963 50.705 64.8 82.207 89.793l0.793 0.607c57.663 45.719 130.179 75.053 209.311 79.947l1.069 0.053 114.48 140.88c-27.366 5.017-58.869 7.898-91.041 7.92h-0.019c-245.8 0-452.2-168-510.8-395.6 21.856-82.93 60.906-154.847 113.325-214.773l-0.525 0.613zM814.76 480.92q7.52-10 14.44-20.52c14.737-21.984 27.741-47.184 37.759-73.847l0.841-2.553c-10.859-29.216-23.863-54.416-39.447-77.748l0.847 1.348c-23.221-34.963-50.705-64.8-82.207-89.793l-0.793-0.607c-57.762-45.834-130.437-75.216-209.743-80.049l-1.057-0.051-114.46-140.86c27.346-4.988 58.817-7.84 90.955-7.84 0.037 0 0.074 0 0.111 0h-0.005c245.8 0 452.2 168 510.8 395.6-21.856 82.93-60.906 154.847-113.325 214.773l0.525-0.613zM832 896l-832-1024h192l832 1024h-192z" />
+<glyph unicode="&#xe92c;" glyph-name="icon-notebook-page" d="M830 834h-830l-4-702c0-106.6 87.4-194 194-194h640c106.6 0 194 87.4 194 194v508c0 106.8-87.4 194-194 194zM832 450l-384-384-192 192v256l192-192 384 384v-256z" />
+<glyph unicode="&#xe92d;" glyph-name="icon-unlocked" d="M768 896c-141.339-0.114-255.886-114.661-256-255.989v-128.011h-448c-35.301-0.113-63.887-28.699-64-63.989v-512.011c0.113-35.301 28.699-63.887 63.989-64h638.011c35.301 0.113 63.887 28.699 64 63.989v512.011c-0.113 35.301-28.699 63.887-63.989 64h-62.011v128c0 70.692 57.308 128 128 128s128-57.308 128-128v0-128h128v128c-0.114 141.339-114.661 255.886-255.989 256h-0.011z" />
+<glyph unicode="&#xe92e;" glyph-name="icon-circle" d="M1024 384c0-282.77-229.23-512-512-512s-512 229.23-512 512c0 282.77 229.23 512 512 512s512-229.23 512-512z" />
+<glyph unicode="&#xe92f;" glyph-name="icon-draft" d="M876.34 260.42l-49.9-49.88-19.26-19.5-26-8.7-423.040-144.2 144.2 423.28 8.84 25.78 150 149.88-85.6 149.78c-34.92 61.12-92 61.12-127 0l-422.78-739.72c-34.94-61.14-5.92-111.14 64.48-111.14h843.44c70.4 0 99.42 50 64.48 111.14zM973.18 653.16c-19.32 19.3-40.66 34.62-60.16 43.16-34.42 15.12-52.38 4.54-60.1-3.16l-258.12-258.12-82.8-243.040 243 82.8 3.36 3.4 254.76 254.76c4.94 4.94 10.88 13.88 10.88 28.3 0 25.34-19.5 60.56-50.82 91.9zM631 276.18l-34.88 34.86 34.64 101.6 9.24 3.36h32v-64h64v-32l-3.42-9.26z" />
+<glyph unicode="&#xe930;" glyph-name="icon-circle-slash" d="M512 896c-282.78 0-512-229.22-512-512s229.22-512 512-512 512 229.22 512 512-229.22 512-512 512zM263.1 632.9c66.48 66.48 154.88 103.1 248.9 103.1 66.74 0 130.64-18.48 185.9-52.96l-484.94-484.94c-34.5 55.24-52.96 119.16-52.96 185.9 0 94.020 36.62 182.42 103.1 248.9zM760.9 135.1c-66.48-66.48-154.88-103.1-248.9-103.1-66.74 0-130.64 18.48-185.9 52.96l484.94 484.94c34.5-55.24 52.96-119.16 52.96-185.9 0-94.020-36.62-182.42-103.1-248.9z" />
+<glyph unicode="&#xe931;" glyph-name="icon-question-mark" horiz-adv-x="697" d="M136.86 843.74c54.080 34.82 120.58 52.26 199.44 52.26 103.6 0 189.7-24.76 258.24-74.28s102.82-122.88 102.82-220.060c0-59.6-14.86-109.8-44.58-150.6-17.38-24.76-50.76-56.4-100.14-94.9l-48.68-37.82c-26.54-20.64-44.14-44.7-52.82-72.2-5.5-17.44-8.46-44.48-8.92-81.14h-186.4c2.74 77.48 10.060 131 21.94 160.58s42.5 63.62 91.88 102.12l50.060 39.2c16.46 12.38 29.72 25.9 39.78 40.58 18.28 25.2 27.42 52.96 27.42 83.22 0 34.84-10.18 66.6-30.52 95.24-20.36 28.64-57.52 42.98-111.48 42.98s-90.68-17.66-112.88-52.96c-22.18-35.32-33.26-71.98-33.26-110.040h-198.76c5.5 130.64 51.12 223.24 136.86 277.82zM251.020 70.76h205.62v-198.74h-205.62v198.74z" />
+<glyph unicode="&#xe932;" glyph-name="icon-status-poll-check" d="M512 896c-282.76 0-512-214.9-512-480 0-92.26 27.8-178.44 75.92-251.6l-75.92-292.4 313.5 101.42c61.040-24.1 128.12-37.42 198.5-37.42 282.76 0 512 214.9 512 480s-229.24 480-512 480zM768 448l-320-320-192 192v192l192-192 320 320v-192z" />
+<glyph unicode="&#xe933;" glyph-name="icon-status-poll-caution" d="M512 896c-282.76 0-512-214.9-512-480 0-92.26 27.8-178.44 75.92-251.6l-75.92-292.4 313.5 101.42c61.040-24.1 128.12-37.42 198.5-37.42 282.76 0 512 214.9 512 480s-229.24 480-512 480zM781.36 192h-538.72c-44.96 0-63.5 31.94-41.2 70.98l270 472.48c22.3 39.040 58.82 39.040 81.12 0l269.98-472.48c22.3-39.040 3.78-70.98-41.2-70.98zM457.14 478.14l24.2-122.64h61.32l24.2 122.64v163.5h-109.72v-163.5zM471.12 314.64h81.76v-81.76h-81.76v81.76z" />
+<glyph unicode="&#xe934;" glyph-name="icon-status-poll-circle-slash" d="M391.18 227.3c35.72-22.98 77.32-35.3 120.82-35.3 59.84 0 116.080 23.3 158.4 65.6 42.3 42.3 65.6 98.56 65.6 158.4 0 43.5-12.32 85.080-35.3 120.82l-309.52-309.52zM512 640c-59.84 0-116.080-23.3-158.4-65.6-42.3-42.3-65.6-98.56-65.6-158.4 0-43.5 12.32-85.080 35.3-120.82l309.52 309.52c-35.72 22.98-77.32 35.3-120.82 35.3zM512 896c-282.76 0-512-214.9-512-480 0-92.26 27.8-178.44 75.92-251.6l-75.92-292.4 313.5 101.42c61.040-24.1 128.12-37.42 198.5-37.42 282.76 0 512 214.9 512 480s-229.24 480-512 480zM512 96c-176.74 0-320 143.26-320 320s143.26 320 320 320 320-143.26 320-320-143.26-320-320-320z" />
+<glyph unicode="&#xe935;" glyph-name="icon-status-poll-question-mark" d="M512 896c-282.76 0-512-214.9-512-480 0-92.26 27.8-178.44 75.92-251.6l-75.92-292.4 313.5 101.42c61.040-24.1 128.12-37.42 198.5-37.42 282.76 0 512 214.9 512 480s-229.24 480-512 480zM579.020 64h-141.36v136.64h141.36v-136.64zM713.84 462.1c-11.94-17.020-34.9-38.78-68.84-65.24l-33.48-26c-18.24-14.18-30.34-30.74-36.32-49.64-3.78-11.98-5.82-30.58-6.14-55.8h-128.12c1.88 53.26 6.92 90.060 15.080 110.4 8.18 20.34 29.22 43.74 63.16 70.22l34.42 26.94c11.3 8.52 20.42 17.8 27.34 27.9 12.56 17.34 18.86 36.4 18.86 57.2 0 23.94-7 45.78-20.98 65.48-14 19.7-39.54 29.54-76.64 29.54s-62.34-12.14-77.6-36.4c-15.24-24.28-22.88-49.48-22.88-75.64h-136.64c3.78 89.84 35.14 153.5 94.080 191.020 37.18 23.94 82.9 35.94 137.12 35.94 71.22 0 130.42-17.020 177.54-51.060s70.68-84.48 70.68-151.3c0-40.98-10.22-75.5-30.66-103.54z" />
+<glyph unicode="&#xe936;" glyph-name="icon-status-poll-edit" d="M1000.080 561.36l-336.6-336.76-20.52-6.88-450.96-153.72 160.68 471.52 332.34 332.34c-54.040 18.2-112.28 28.14-173.020 28.14-282.76 0-512-214.9-512-480 0-92.26 27.8-178.44 75.92-251.6l-75.92-292.4 313.5 101.42c61.040-24.1 128.12-37.42 198.5-37.42 282.76 0 512 214.9 512 480 0 50.68-8.4 99.5-23.92 145.36zM408.42 500.76l-2.16-6.3-111.7-327.9 334.12 113.86 4.62 4.68 350.28 350.28c6.8 6.78 14.96 19.1 14.96 38.9 0 34.86-26.82 83.28-69.88 126.38-26.54 26.54-55.9 47.6-82.7 59.34-47.34 20.8-72.020 6.24-82.64-4.36l-354.9-354.88zM470.56 474.58h44v-88h88v-44l-4.7-12.72-139.68-47.54-47.94 47.94 47.6 139.72 12.72 4.6z" />
+<glyph unicode="&#xea00;" glyph-name="icon-arrows-right-left" d="M1024 384l-448-512v1024zM448 896l-448-512 448-512z" />
+<glyph unicode="&#xea01;" glyph-name="icon-arrows-up-down" d="M512 896l512-448h-1024zM0 320l512-448 512 448z" />
+<glyph unicode="&#xea02;" glyph-name="icon-bullet" d="M832 144c0-44-36-80-80-80h-480c-44 0-80 36-80 80v480c0 44 36 80 80 80h480c44 0 80-36 80-80v-480z" />
+<glyph unicode="&#xea03;" glyph-name="icon-calendar" d="M896 896h-768c-70.4 0-128-57.6-128-128v-768c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v768c0 70.4-57.6 128-128 128zM640 448h-256v192h256v-192zM384 384h256v-192h-256v192zM320 192h-256v192h256v-192zM320 640v-192h-256v192h256zM128-64c-17 0-33 6.6-45.2 18.8s-18.8 28.2-18.8 45.2v128h256v-192h-192zM384-64v192h256v-192h-256zM960 0c0-17-6.6-33-18.8-45.2s-28.2-18.8-45.2-18.8h-192v192h256v-128zM960 192h-256v192h256v-192zM960 448h-256v192h256v-192z" />
+<glyph unicode="&#xea04;" glyph-name="icon-chain-links" d="M958.4 830.4c-43.8 43.8-101 65.6-158.4 65.6s-114.6-21.8-158.4-65.6l-128-128c-74-74-85.4-187-34-273l-12.8-12.8c-35.4 20.8-75 31.4-114.8 31.4-57.4 0-114.6-21.8-158.4-65.6l-128-128c-87.4-87.4-87.4-229.4 0-316.8 43.8-43.8 101-65.6 158.4-65.6s114.6 21.8 158.4 65.6l128 128c74 74 85.4 187 34 273l12.8 12.8c35.2-21 75-31.6 114.6-31.6 57.4 0 114.6 21.8 158.4 65.6l128 128c87.6 87.6 87.6 229.6 0.2 317zM419.8 156.2l-128-128c-18-18.2-42.2-28.2-67.8-28.2s-49.8 10-67.8 28.2c-37.4 37.4-37.4 98.4 0 135.8l128 128c18.2 18.2 42.2 28.2 67.8 28.2 5.6 0 11.2-0.6 16.8-1.4l-55.6-55.6c-10.4-10.4-16.2-24.2-16.2-38.8s5.8-28.6 16.2-38.8c10.4-10.4 24.2-16.2 38.8-16.2s28.6 5.8 38.8 16.2l55.6 55.6c5.4-30.4-3.6-62.2-26.6-85zM867.8 604.2l-128-128c-18-18.2-42.2-28.2-67.8-28.2-5.6 0-11.2 0.6-16.8 1.4l55.6 55.6c10.4 10.4 16.2 24.2 16.2 38.8s-5.8 28.6-16.2 38.8c-10.4 10.4-24.2 16.2-38.8 16.2s-28.6-5.8-38.8-16.2l-55.6-55.6c-5.2 29.8 3.6 61.6 26.6 84.6l128 128c18 18.4 42.2 28.4 67.8 28.4s49.8-10 67.8-28.2c37.6-37.4 37.6-98.2 0-135.6z" />
+<glyph unicode="&#xea05;" glyph-name="icon-download" d="M832 320v-255.66l-0.34-0.34-639.66 0.34v255.66h-192v-256c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v256h-192zM512 256l448 448h-256v192h-384v-192h-256l448-448z" />
+<glyph unicode="&#xea06;" glyph-name="icon-duplicate" d="M640 640v128c0 70.4-57.6 128-128 128h-384c-70.4 0-128-57.6-128-128v-384c0-70.4 57.6-128 128-128h128v139.6c0 134.8 109.6 244.4 244.4 244.4h139.6zM896 512h-384c-70.4 0-128-57.6-128-128v-384c0-70.4 57.6-128 128-128h384c70.4 0 128 57.6 128 128v384c0 70.4-57.6 128-128 128z" />
+<glyph unicode="&#xea07;" glyph-name="icon-folder-new" d="M896 704h-320c-16.4 16.4-96.8 96.8-109.2 109.2l-37.4 37.4c-25 25-74.2 45.4-109.4 45.4h-256c-35.2 0-64-28.8-64-64v-384c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v128c0 70.4-57.6 128-128 128zM896 448h-768c-70.4 0-128-57.6-128-128v-320c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v320c0 70.4-57.6 128-128 128zM704 96h-128v-128h-128v128h-128v128h128v128h128v-128h128v-128z" />
+<glyph unicode="&#xea08;" glyph-name="icon-fullscreen-collapse" d="M191.656 64c0.118-0.1 0.244-0.224 0.344-0.344v-191.656h192v192c0 105.6-86.4 192-192 192h-192v-192h191.656zM192 704.344c-0.1-0.118-0.224-0.244-0.344-0.344h-191.656v-192h192c105.6 0 192 86.4 192 192v192h-192v-191.656zM832 512h192v192h-191.656c-0.118 0.1-0.244 0.226-0.344 0.344v191.656h-192v-192c0-105.6 86.4-192 192-192zM832 63.656c0.1 0.118 0.224 0.244 0.344 0.344h191.656v192h-192c-105.6 0-192-86.4-192-192v-192h192v191.656z" />
+<glyph unicode="&#xea09;" glyph-name="icon-fullscreen-expand" d="M192.344 64c-0.118 0.1-0.244 0.224-0.344 0.344v191.656h-192v-192c0-105.6 86.4-192 192-192h192v192h-191.656zM192 703.656c0.1 0.118 0.224 0.244 0.344 0.344h191.656v192h-192c-105.6 0-192-86.4-192-192v-192h192v191.656zM832 896h-192v-192h191.656c0.118-0.1 0.244-0.226 0.344-0.344v-191.656h192v192c0 105.6-86.4 192-192 192zM832 64.344c-0.1-0.118-0.224-0.244-0.344-0.344h-191.656v-192h192c105.6 0 192 86.4 192 192v192h-192v-191.656z" />
+<glyph unicode="&#xea0a;" glyph-name="icon-layers" d="M1024 512l-512 384-512-384 512-384zM512 0l-426.666 320-85.334-64 512-384 512 384-85.334 64z" />
+<glyph unicode="&#xea0b;" glyph-name="icon-line-horz" d="M64 320c-35.346 0-64 28.654-64 64s28.654 64 64 64h896c35.346 0 64-28.654 64-64s-28.654-64-64-64h-896z" />
+<glyph unicode="&#xea0c;" glyph-name="icon-magnify" d="M1024 0l-256.8 256.8c42.4 66.6 65 144 64.8 223.2 0 229.8-186.2 416-416 416s-416-186.2-416-416 186.2-416 416-416c79-0.2 156.4 22.4 223.2 64.8l256.8-256.8 128 128zM212.4 276.4c-112.4 112.4-112.4 294.8 0 407.2s294.8 112.4 407.2 0 112.4-294.8 0-407.2c-54-54-127.2-84.4-203.6-84.4-76.4-0.2-149.8 30.2-203.6 84.4z" />
+<glyph unicode="&#xea0d;" glyph-name="icon-magnify-in" d="M1024 0l-256.86 256.86c40.681 62.963 64.861 139.898 64.861 222.481 0 0.232 0 0.464-0.001 0.696v-0.036c0 229.76-186.24 416-416 416s-416-186.24-416-416 186.24-416 416-416c0.196 0 0.427-0.001 0.659-0.001 82.583 0 159.518 24.18 224.112 65.846l-1.631-0.985 256.86-256.86zM212.36 276.36c-52.114 52.117-84.346 124.114-84.346 203.64 0 159.058 128.942 288 288 288s288-128.942 288-288c0-159.058-128.942-288-288-288-0.005 0-0.010 0-0.014 0h0.001c-0.242-0.001-0.529-0.001-0.815-0.001-79.271 0-151.010 32.251-202.811 84.348l-0.013 0.014zM224 544h384v-128h-384v128zM352 672h128v-384h-128v384z" />
+<glyph unicode="&#xea0e;" glyph-name="icon-magnify-out-v2" d="M767.2 256.8c42.4 66.6 65 144 64.8 223.2 0 229.8-186.2 416-416 416s-416-186.2-416-416 186.2-416 416-416c79-0.2 156.4 22.4 223.2 64.8l256.8-256.8 128 128-256.8 256.8zM619.6 276.4c-54-54-127.2-84.4-203.6-84.4-76.4-0.2-149.8 30.2-203.6 84.4-112.4 112.4-112.4 294.8 0 407.2s294.8 112.4 407.2 0c112.4-112.4 112.4-294.8 0-407.2zM224 544h384v-128h-384v128z" />
+<glyph unicode="&#xea0f;" glyph-name="icon-menu" d="M0 768h1024v-128h-1024v128zM0 448h1024v-128h-1024v128zM0 128h1024v-128h-1024v128z" />
+<glyph unicode="&#xea10;" glyph-name="icon-move" d="M293.4 384l218.6 218.6 256-256v421.4c0 70.4-57.6 128-128 128h-512c-70.4 0-128-57.6-128-128v-512c0-70.4 57.6-128 128-128h421.4l-256 256zM1024 448h-128v-320l-384 384-128-128 384-384h-320v-128h576z" />
+<glyph unicode="&#xea11;" glyph-name="icon-new-window" d="M448 896v-128h320l-384-384 128-128 384 384v-320h128v576zM576 221.726v-157.382c-0.1-0.118-0.226-0.244-0.344-0.344h-383.312c-0.118 0.1-0.244 0.226-0.344 0.344v383.312c0.1 0.118 0.226 0.244 0.344 0.344h157.382l192 192h-349.726c-105.6 0-192-86.4-192-192v-384c0-105.6 86.4-192 192-192h384c105.6 0 192 86.4 192 192v349.726l-192-192z" />
+<glyph unicode="&#xea12;" glyph-name="icon-paint-bucket-v2" d="M544 672v-224c0-88.4-71.6-160-160-160s-160 71.6-160 160v97.2l-197.4-196.4c-50-50-12.4-215.2 112.4-340s290-162.4 340-112.4l417 423.6-352 352zM896-128c70.6 0 128 57.4 128 128 0 108.6-128 192-128 192s-128-83.4-128-192c0-70.6 57.4-128 128-128zM384 384c-35.4 0-64 28.6-64 64v384c0 35.4 28.6 64 64 64s64-28.6 64-64v-384c0-35.4-28.6-64-64-64z" />
+<glyph unicode="&#xea13;" glyph-name="icon-pencil" d="M922.344 794.32c-38.612 38.596-81.306 69.232-120.304 86.324-68.848 30.25-104.77 9.078-120.194-6.344l-516.228-516.216-3.136-9.152-162.482-476.932 485.998 165.612 6.73 6.806 509.502 509.506c9.882 9.866 21.768 27.77 21.768 56.578 0.002 50.71-38.996 121.148-101.654 183.818zM237.982 40.34l-69.73 69.728 69.25 203.228 18.498 6.704h64v-128h128v-64l-6.846-18.506-203.172-69.154z" />
+<glyph unicode="&#xea14;" glyph-name="icon-pencil-edit-in-place" d="M922.4 794.4c-38.6 38.6-81.4 69.2-120.4 86.2-68.8 30.2-104.8 9-120.2-6.4l-516.2-516.2-3.2-9.2-162.4-476.8 486 165.6 516.2 516.4c9.8 9.8 21.8 27.8 21.8 56.6 0 50.6-39 121-101.6 183.8zM238 40.4l-69.8 69.6 69.2 203.2 18.4 6.8h64v-128h128v-64l-6.8-18.6-203-69zM0 896v-512l128 128v256h256l128 128zM1024-128v512l-128-128v-256h-256l-128-128z" />
+<glyph unicode="&#xea15;" glyph-name="icon-play" d="M1024 384l-1024-512v1024z" />
+<glyph unicode="&#xea16;" glyph-name="icon-pause" d="M126 898h256v-1024h-256v1024zM638 898h256v-1024h-256v1024z" />
+<glyph unicode="&#xea17;" glyph-name="icon-plot-resource" d="M255.8 192c0.2 0 0.2 0 0 0l0.2 128c0 70.6 57.4 128 128 128h255.8c0 0 0 0 0.2 0.2v127.8c0 70.6 57.4 128 128 128h143.6c-93.8 117-238 192-399.6 192-282.8 0-512-229.2-512-512 0-68 13.2-132.8 37.2-192h218.6zM768.2 576c-0.2 0-0.2 0 0 0l-0.2-128c0-70.6-57.4-128-128-128h-255.8c0 0 0 0-0.2-0.2v-127.8c0-70.6-57.4-128-128-128h-143.6c93.8-117 238-192 399.6-192 282.8 0 512 229.2 512 512 0 68-13.2 132.8-37.2 192h-218.6z" />
+<glyph unicode="&#xea18;" glyph-name="icon-pointer-left" d="M766-128l-256 512 256 512h-256l-256-512 256-512z" />
+<glyph unicode="&#xea19;" glyph-name="icon-pointer-right" d="M254 896l256-512-256-512h256l256 512-256 512z" />
+<glyph unicode="&#xea1a;" glyph-name="icon-refresh" d="M1024 435.2v460.8l-175.8-175.8c-85.2 69.6-190.8 107.6-302 107.6-127.6 0-247.6-49.8-338-140s-140-210.4-140-338 49.8-247.6 140-338 210.4-140 338-140 247.6 49.8 338 140c74 74 120.8 167.8 135 269.6h-138.6c-32-155.4-169.8-272.8-334.6-272.8-188.2 0-341.4 153.2-341.4 341.4s153.4 341.2 341.6 341.2c76.8 0 147.6-25.4 204.8-68.2l-187.8-187.8h460.8z" />
+<glyph unicode="&#xea1b;" glyph-name="icon-save" d="M192.2 320c-0.2 0-0.2 0 0 0l-0.2-448h640v447.8c0 0 0 0-0.2 0.2h-639.6zM978.8 685.2l-165.4 165.4c-25 25-74.2 45.4-109.4 45.4h-576c-70.4 0-128-57.6-128-128v-768c0-70.4 57.6-128 128-128v448c0 35.2 28.8 64 64 64h640c35.2 0 64-28.8 64-64v-448c70.4 0 128 57.6 128 128v576c0 35.2-20.4 84.4-45.2 109.2zM704 640c0-35.2-28.8-64-64-64h-448c-35.2 0-64 28.8-64 64v192h320v-192h128v192h128v-192z" />
+<glyph unicode="&#xea1c;" glyph-name="icon-save-as" d="M978.8 557.2l-64 64c24.8-24.8 45.2-74 45.2-109.2v-448c0-70.4-57.6-128-128-128h-640c-18.8 0-36.6 4.2-52.6 11.4 20.2-44.4 65-75.4 116.6-75.4h640c70.4 0 128 57.6 128 128v448c0 35.2-20.4 84.4-45.2 109.2zM704 0v319.8c0 0 0 0-0.2 0.2h-511.6l-0.2-320h512zM192 384h512c35.2 0 64-28.8 64-64v-320c70.4 0 128 57.6 128 128v448c0 35.2-20.4 84.4-45.2 109.2l-165.4 165.4c-25 25-74.2 45.4-109.4 45.4h-448c-70.4 0-128-57.6-128-128v-640c0-70.4 57.6-128 128-128v320c0 35.2 28.8 64 64 64zM128 832h192v-192h128v192h128v-192c0-35.2-28.8-64-64-64h-320c-35.2 0-64 28.8-64 64v192z" />
+<glyph unicode="&#xea1d;" glyph-name="icon-sine" d="M1024 384c-1.8 7.2-3.4 14.4-5.2 21.8-20.2 86.2-53.4 209.4-98.4 307.2-22.4 49-45.4 86.6-70.2 115.2-48.6 56-98.4 67.8-131.8 67.8-33.2 0-83.2-11.8-131.8-67.8-24.6-28.6-47.6-66.2-70-115.2-44.8-97.8-78.2-221-98.4-307.2-21.8-93-46.6-175.4-72-238.4-16.4-40.6-30.4-66.4-40.8-82.8-10.4 16.2-24.4 42.2-40.8 82.8-23.2 58-46.2 132.4-66.6 216.6h-198c1.8-7.2 3.4-14.4 5.2-21.8 20.2-86.2 53.4-209.4 98.4-307.2 22.4-49 45.4-86.6 70.2-115.2 48.6-56 98.6-67.8 131.8-67.8s83.2 11.8 131.8 67.8c24.8 28.6 47.6 66.2 70.2 115.2 44.8 97.8 78.2 221 98.4 307.2 21.8 93 46.6 175.4 72 238.4 16.4 40.6 30.4 66.4 40.8 82.8 10.4-16.2 24.4-42.2 40.8-82.8 23.4-57.8 46.4-132.4 66.8-216.4h197.6z" />
+<glyph unicode="&#xea1e;" glyph-name="icon-font" d="M800-128h224l-384 1024h-256l-384-1024h224l84 224h408zM380 288l132 352 132-352z" />
+<glyph unicode="&#xea1f;" glyph-name="icon-thumbs-strip" d="M448 514c0-35.2-28.8-64-64-64h-320c-35.2 0-64 28.8-64 64v320c0 35.2 28.8 64 64 64h320c35.2 0 64-28.8 64-64v-320zM1024 514c0-35.2-28.8-64-64-64h-320c-35.2 0-64 28.8-64 64v320c0 35.2 28.8 64 64 64h320c35.2 0 64-28.8 64-64v-320zM448-62c0-35.2-28.8-64-64-64h-320c-35.2 0-64 28.8-64 64v320c0 35.2 28.8 64 64 64h320c35.2 0 64-28.8 64-64v-320zM1024-62c0-35.2-28.8-64-64-64h-320c-35.2 0-64 28.8-64 64v320c0 35.2 28.8 64 64 64h320c35.2 0 64-28.8 64-64v-320z" />
+<glyph unicode="&#xea20;" glyph-name="icon-two-parts-both" d="M896 896h-768c-70.4 0-128-57.6-128-128v-768c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v768c0 70.4-57.6 128-128 128zM128 768h320v-768h-320v768zM896 0h-320v768h320v-768z" />
+<glyph unicode="&#xea21;" glyph-name="icon-two-parts-one-only" d="M896 896h-768c-70.4 0-128-57.6-128-128v-768c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v768c0 70.4-57.6 128-128 128zM896 0h-320v768h320v-768z" />
+<glyph unicode="&#xea22;" glyph-name="icon-resync" d="M795.2 731.2c-79.8 65.2-178.8 100.8-283.2 100.8-119.6 0-232.2-46.6-316.8-131.2-69.4-69.4-113.2-157.4-126.6-252.8h130c29.6 145.8 158.8 256 313.4 256 72 0 138.4-23.8 192-64l-176-176h432v432l-164.8-164.8zM512 64c-72 0-138.4 23.8-192 64l176 176h-432v-432l164.8 164.8c79.8-65.2 178.8-100.8 283.2-100.8 119.6 0 232.2 46.6 316.8 131.2 69.4 69.4 113.2 157.4 126.6 252.8h-130c-29.6-145.8-158.8-256-313.4-256z" />
+<glyph unicode="&#xea23;" glyph-name="icon-reset" d="M460.8 435.2l-187.8 187.8c57.2 42.8 128 68.2 204.8 68.2 188.2 0 341.6-153.2 341.6-341.4s-153.2-341.2-341.4-341.2c-165 0-302.8 117.6-334.6 273h-138.4c14.2-101.8 61-195.6 135-269.6 90.2-90.2 210.4-140 338-140s247.6 49.8 338 140 140 210.4 140 338-49.8 247.6-140 338-210.4 140-338 140c-111.4 0-217-38-302-107.6l-176 175.6v-460.8h460.8z" />
+<glyph unicode="&#xea24;" glyph-name="icon-x-in-circle" d="M512 896c-282.8 0-512-229.2-512-512s229.2-512 512-512 512 229.2 512 512-229.2 512-512 512zM832 192l-128-128-192 192-192-192-128 128 192 192-192 192 128 128 192-192 192 192 128-128-192-192 192-192z" />
+<glyph unicode="&#xea25;" glyph-name="icon-brightness" d="M253.414 577.939l-155.172 116.384c-50.233-66.209-85.127-146.713-97.91-234.39l-0.333-2.781 191.919-27.434c8.145 56.552 29.998 106.879 62.068 149.006l-0.573-0.784zM191.98 338.283l-191.919-27.434c13.115-90.459 48.009-170.963 99.174-238.453l-0.931 1.281 155.111 116.384c-31.476 41.347-53.309 91.675-61.231 146.504l-0.204 1.719zM466.283 704.020l-27.434 191.919c-90.459-13.115-170.963-48.009-238.453-99.174l1.281 0.931 116.384-155.111c41.347 31.476 91.675 53.309 146.504 61.231l1.719 0.204zM822.323 797.758c-66.209 50.233-146.713 85.127-234.39 97.91l-2.781 0.333-27.434-191.919c56.552-8.145 106.879-29.998 149.006-62.068l-0.784 0.573zM832.020 429.717l191.919 27.434c-13.115 90.459-48.009 170.963-99.174 238.453l0.931-1.281-155.111-116.384c31.476-41.347 53.309-91.675 61.231-146.504l0.204-1.719zM201.677-29.758c66.209-50.233 146.713-85.127 234.39-97.91l2.781-0.333 27.434 191.919c-56.552 8.145-106.879 29.998-149.006 62.068l0.784-0.573zM770.586 190.061l155.131-116.343c50.233 66.209 85.127 146.713 97.91 234.39l0.333 2.781-191.919 27.434c-8.125-56.564-29.966-106.906-62.028-149.049l0.574 0.786zM557.717 63.98l27.434-191.919c90.459 13.115 170.963 48.009 238.453 99.174l-1.281-0.931-116.384 155.111c-41.347-31.476-91.675-53.309-146.504-61.231l-1.719-0.204zM770.586 384c0-142.813-115.773-258.586-258.586-258.586s-258.586 115.773-258.586 258.586c0 142.813 115.773 258.586 258.586 258.586s258.586-115.773 258.586-258.586z" />
+<glyph unicode="&#xea26;" glyph-name="icon-contrast" d="M512 896c-282.78 0-512-229.24-512-512s229.22-512 512-512 512 229.24 512 512-229.22 512-512 512zM783.52 112.48c-69.111-69.481-164.785-112.481-270.502-112.481-0.358 0-0.716 0-1.074 0.001h0.055v768c212.070-0.010 383.982-171.929 383.982-384 0-106.034-42.977-202.031-112.462-271.52v0z" />
+<glyph unicode="&#xea27;" glyph-name="icon-expand" d="M960 896c0 0 0 0 0 0h-320v-128h165.4l-210.6-210.8c-25-25-25-65.6 0-90.6 12.4-12.4 28.8-18.8 45.2-18.8s32.8 6.2 45.2 18.8l210.8 210.8v-165.4h128v384h-64zM896 90.6l-210.8 210.6c-25 25-65.6 25-90.6 0s-25-65.6 0-90.6l210.8-210.6h-165.4v-128h384v384h-128v-165.4zM218.6 768h165.4v128h-320c0 0 0 0 0 0h-64v-384h128v165.4l210.8-210.8c12.4-12.4 28.8-18.8 45.2-18.8s32.8 6.2 45.2 18.8c25 25 25 65.6 0 90.6l-210.6 210.8zM338.8 301.2l-210.8-210.6v165.4h-128v-384h384v128h-165.4l210.8 210.8c25 25 25 65.6 0 90.6-25.2 24.8-65.6 24.8-90.6-0.2z" />
+<glyph unicode="&#xea28;" glyph-name="icon-list-view" d="M0 832h1024v-128h-1024v128zM0 576h1024v-128h-1024v128zM0 320h1024v-128h-1024v128zM0 64h1024v-128h-1024v128z" />
+<glyph unicode="&#xea29;" glyph-name="icon-grid-snap-to" d="M382 66h448v448h-448v-448zM510 386h192v-192h-192v192zM-2 322h320v-64h-320v64zM894 322h128v-64h-128v64zM574 898h64v-320h-64v320zM574 2h64v-128h-64v128zM574 322h64v-64h-64v64z" />
+<glyph unicode="&#xea2a;" glyph-name="icon-grid-snap-no" d="M768 320h192v-64h-192v64zM256 320h192v-64h-192v64zM0 320h192v-64h-192v64zM640 384h-64v-64h-64v-64h64v-64h64v64h64v64h-64zM576 640h64v-192h-64v192zM576 896h64v-192h-64v192zM576 128h64v-192h-64v192z" />
+<glyph unicode="&#xea2b;" glyph-name="icon-frame-show" d="M0 832v-896h1024v896h-1024zM896 64h-768v640h768v-640zM192 640h384v-128h-384v128z" />
+<glyph unicode="&#xea2c;" glyph-name="icon-frame-hide" d="M128 706h420l104 128h-652v-802.4l128 157.4zM896 66h-420l-104-128h652v802.4l-128-157.4zM832 898l-832-1024h192l832 1024zM392 514l104 128h-304v-128z" />
+<glyph unicode="&#xea2d;" glyph-name="icon-import" d="M832 703.6v-639.4c0-0.2-0.2-0.2-0.4-0.4h-319.6v-192h320c105.6 0 192 86.4 192 192v640.2c0 105.6-86.4 192-192 192h-320v-192h319.6c0.2 0 0.4-0.2 0.4-0.4zM192 192v-192l384 384-384 384v-192h-192v-384z" />
+<glyph unicode="&#xea2e;" glyph-name="icon-export" d="M192 64.34v639.32l0.34 0.34h319.66v192h-320c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h320v192h-319.66zM1024 384l-384 384v-192h-192v-384h192v-192l384 384z" />
+<glyph unicode="&#xea2f;" glyph-name="icon-font-size" horiz-adv-x="1504" d="M1226.4 576h-176l-76.22-203.24 77-205.34 87.22 232.58 90.74-242h-174.44l49.5-132h174.44l57.76-154h154l-264 704zM384 896l-384-1024h224l84 224h408l84-224h224l-384 1024zM380 288l132 352 132-352z" />
+<glyph unicode="&#xea30;" glyph-name="icon-clear-data" d="M632 584l-120-120-120 120-80-80 120-120-120-120 80-80 120 120 120-120 80 80-120 120 120 120-80 80zM512 896c-282.76 0-512-86-512-192v-640c0-106 229.24-192 512-192s512 86 512 192v640c0 106-229.24 192-512 192zM512 64c-176.731 0-320 143.269-320 320s143.269 320 320 320c176.731 0 320-143.269 320-320v0c0-176.731-143.269-320-320-320v0z" />
+<glyph unicode="&#xea31;" glyph-name="icon-history" d="M576 832c-247.4 0-448-200.6-448-448h-128l192-192 192 192h-128c0 85.4 33.2 165.8 93.8 226.2 60.4 60.6 140.8 93.8 226.2 93.8s165.8-33.2 226.2-93.8c60.6-60.4 93.8-140.8 93.8-226.2s-33.2-165.8-93.8-226.2c-60.4-60.6-140.8-93.8-226.2-93.8s-165.8 33.2-226.2 93.8l-90.6-90.6c81-81 193-131.2 316.8-131.2 247.4 0 448 200.6 448 448s-200.6 448-448 448zM576 624c-26.6 0-48-21.4-48-48v-211.8l142-142c9.4-9.4 21.6-14 34-14s24.6 4.6 34 14c18.8 18.8 18.8 49.2 0 67.8l-114 114v172c0 26.6-21.4 48-48 48z" />
+<glyph unicode="&#xea32;" glyph-name="icon-arrow-up-to-parent" horiz-adv-x="1056" d="M643.427 70.739c-81.955 0.697-148.179 67.065-148.642 149.010v395.872l296.871-247.393v197.914l-395.828 329.857-395.828-328.62v-197.502l296.871 246.156v-396.241c0-190.905 155.239-346.556 346.144-346.968l412.321-0.825 0.412 197.914z" />
+<glyph unicode="&#xea33;" glyph-name="icon-crosshair-in-circle" d="M512 896c-282.8 0-512-229.2-512-512s229.2-512 512-512 512 229.2 512 512-229.2 512-512 512zM783.6 112.4c-54.634-54.8-125.77-93.12-205.322-106.874l-2.278-0.326v250.8h-128v-250.8c-161.302 28.062-286.738 153.497-314.468 312.5l-0.332 2.3h250.8v128h-250.8c28.062 161.302 153.497 286.738 312.5 314.468l2.3 0.332v-250.8h128v250.8c161.302-28.062 286.738-153.497 314.468-312.5l0.332-2.3h-250.8v-128h250.8c-14.080-81.83-52.4-152.966-107.191-207.591l-0.009-0.009z" />
+<glyph unicode="&#xea34;" glyph-name="icon-target" d="M512 512c70.692 0 128-57.308 128-128s-57.308-128-128-128c-70.692 0-128 57.308-128 128v0c0.114 70.647 57.353 127.886 127.989 128h0.011zM512 640c-141.385 0-256-114.615-256-256s114.615-256 256-256c141.385 0 256 114.615 256 256v0c-0.114 141.339-114.661 255.886-255.989 256h-0.011zM512 768c211.87-0.128 383.575-171.912 383.575-383.8 0-211.967-171.833-383.8-383.8-383.8s-383.8 171.833-383.8 383.8c0 105.99 42.963 201.945 112.425 271.4v0c69.21 69.437 164.944 112.401 270.713 112.401 0.312 0 0.624 0 0.936-0.001h-0.048zM512 896c-282.8 0-512-229.2-512-512s229.2-512 512-512 512 229.2 512 512-229.2 512-512 512z" />
+<glyph unicode="&#xea35;" glyph-name="icon-items-collapse" d="M45.2 237.2h229.6l-274.8-274.6 90.6-90.6 274.6 274.8v-229.6h128v448h-448v-128zM1024 805.4l-90.6 90.6-274.6-274.8v229.6h-128v-448h448v128h-229.6l274.8 274.6z" />
+<glyph unicode="&#xea36;" glyph-name="icon-items-expand" d="M448 0h-229.4l274.6 274.8-90.4 90.4-274.8-274.6v229.4h-128v-448h448v128zM530.8 493.2l90.4-90.4 274.8 274.6v-229.4h128v448h-448v-128h229.4l-274.6-274.8z" />
+<glyph unicode="&#xea37;" glyph-name="icon-3-dots" d="M256 384c0-70.692-57.308-128-128-128s-128 57.308-128 128c0 70.692 57.308 128 128 128s128-57.308 128-128zM640 384c0-70.692-57.308-128-128-128s-128 57.308-128 128c0 70.692 57.308 128 128 128s128-57.308 128-128zM1024 384c0-70.692-57.308-128-128-128s-128 57.308-128 128c0 70.692 57.308 128 128 128s128-57.308 128-128z" />
+<glyph unicode="&#xea38;" glyph-name="icon-grid-on" d="M1024 512v128h-256v256h-128v-256h-256v256h-128v-256h-256v-128h256v-256h-256v-128h256v-256h128v256h256v-256h128v256h256v128h-256v256zM640 256h-256v256h256z" />
+<glyph unicode="&#xea39;" glyph-name="icon-grid-off" d="M256 344.6l128 157.6v9.8h8l104 128h-112v256h-128v-256h-256v-128h256v-167.4zM184 256h-184v-128h80l104 128zM768 423.4l-128-157.6v-9.8h-8l-104-128h112v-256h128v256h256v128h-256v167.4zM840 512h184v128h-80l-104-128zM832 896l-832-1024h192l832 1024h-192z" />
+<glyph unicode="&#xea3a;" glyph-name="icon-camera" d="M896 640h-128l-128 256h-256l-128-256h-128c-70.601-0.227-127.773-57.399-128-127.978v-512.022c0.227-70.601 57.399-127.773 127.978-128h768.022c70.601 0.227 127.773 57.399 128 127.978v512.022c-0.227 70.601-57.399 127.773-127.978 128h-0.022zM512 32c-141.385 0-256 114.615-256 256s114.615 256 256 256c141.385 0 256-114.615 256-256v0c0-141.385-114.615-256-256-256v0z" />
+<glyph unicode="&#xea3b;" glyph-name="icon-folders-collapse" d="M896 576v-448c-0.215-70.606-57.394-127.785-127.979-128h-576.021c0.215-70.606 57.394-127.785 127.979-128h576.021c70.606 0.215 127.785 57.394 128 127.979v448.021c-0.215 70.606-57.394 127.785-127.979 128h-0.021zM832 192v448c-0.215 70.606-57.394 127.785-127.979 128h-192.021l-101.5 82.74c-24.88 24.9-74.040 45.26-109.24 45.26h-237.26c-35.305-0.102-63.898-28.695-64-63.99v-640.010c0.215-70.606 57.394-127.785 127.979-128h576.021c70.606 0.215 127.785 57.394 128 127.979v0.021zM128 252v516l256-260z" />
+<glyph unicode="&#xeb00;" glyph-name="icon-activity" d="M576 832h-256l320-320h-290.256c-44.264 76.516-126.99 128-221.744 128h-128v-512h128c94.754 0 177.48 51.484 221.744 128h290.256l-320-320h256l448 448-448 448z" />
+<glyph unicode="&#xeb01;" glyph-name="icon-activity-mode" d="M512 896c-214.8 0-398.8-132.4-474.8-320h90.8c56.8 0 108-24.8 143-64h241l-192 192h256l320-320-320-320h-256l192 192h-241c-35-39.2-86.2-64-143-64h-90.8c76-187.6 259.8-320 474.8-320 282.8 0 512 229.2 512 512s-229.2 512-512 512z" />
+<glyph unicode="&#xeb02;" glyph-name="icon-autoflow-tabular" d="M192 896c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h64v1024h-64zM384 896h256v-1024h-256v1024zM832 896h-64v-704h256v512c0 105.6-86.4 192-192 192z" />
+<glyph unicode="&#xeb03;" glyph-name="icon-clock" d="M512 896c-282.8 0-512-229.2-512-512s229.2-512 512-512 512 229.2 512 512-229.2 512-512 512zM782 206c-12.8-22.2-36.6-36-62.4-36-12.6 0-25 3.4-36 9.6l-222 128.2c-0.8 0.4-1.6 1-2.4 1.4l-0.8 0.6-1.8 1.2-2.4 2-1.8 1.4-0.6 0.6c-0.8 0.6-1.4 1.2-2.2 1.8v0c-5 4.6-9.4 10-13 15.8-0.2 0.4-0.6 1-0.8 1.4s-0.6 1-0.8 1.4c-3.2 6-5.8 12.4-7.2 19.2v0.2c-0.2 1-0.4 1.8-0.6 2.8 0 0.2 0 0.6-0.2 0.8-0.2 0.6-0.2 1.4-0.2 2.2s-0.2 1-0.2 1.6 0 1-0.2 1.6-0.2 1.6-0.2 2.2c0 0.4 0 0.6 0 1 0 1 0 1.8 0 2.8 0 0 0 0.2 0 0.4v363.8c0 39.8 32.2 72 72 72s72-32.2 72-72v-322.4l185.8-107.2c34.2-20 45.8-64 26-98.4z" />
+<glyph unicode="&#xeb04;" glyph-name="icon-database" d="M1024 704c0-106.039-229.23-192-512-192s-512 85.961-512 192c0 106.039 229.23 192 512 192s512-85.961 512-192zM512 384c-282.77 0-512 85.962-512 192v-512c0-106.038 229.23-192 512-192s512 85.962 512 192v512c0-106.038-229.23-192-512-192z" />
+<glyph unicode="&#xeb05;" glyph-name="icon-database-query" d="M683.52 76.714c-50.782-28.456-109.284-44.714-171.52-44.714-194.094 0-352 157.906-352 352s157.906 352 352 352 352-157.906 352-352c0-62.236-16.258-120.738-44.714-171.52l191.692-191.692c8.516 13.89 13.022 28.354 13.022 43.212v640c0 106.038-229.23 192-512 192s-512-85.962-512-192v-640c0-106.038 229.23-192 512-192 126.11 0 241.548 17.108 330.776 45.46l-159.256 159.254zM352 384c0-88.224 71.776-160 160-160s160 71.776 160 160-71.776 160-160 160-160-71.776-160-160z" />
+<glyph unicode="&#xeb06;" glyph-name="icon-dataset" d="M896 704h-320c-16.4 16.4-96.8 96.8-109.2 109.2l-37.4 37.4c-25 25-74.2 45.4-109.4 45.4h-256c-35.2 0-64-28.8-64-64v-384c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v128c0 70.4-57.6 128-128 128zM896 448h-768c-70.4 0-128-57.6-128-128v-320c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v320c0 70.4-57.6 128-128 128zM320 0h-128v320h128v-320zM576 0h-128v320h128v-320zM832 0h-128v320h128v-320z" />
+<glyph unicode="&#xeb07;" glyph-name="icon-datatable" d="M1024 704c0-106.039-229.23-192-512-192s-512 85.961-512 192c0 106.039 229.23 192 512 192s512-85.961 512-192zM512 384c-282.8 0-512 86-512 192v-512c0-106 229.2-192 512-192s512 86 512 192v512c0-106-229.2-192-512-192zM896 321v-256c-36.6-15.6-79.8-28.8-128-39.4v256c48.2 10.6 91.4 23.8 128 39.4zM256 281.6v-256c-48.2 10.4-91.4 23.8-128 39.4v256c36.6-15.6 79.8-28.8 128-39.4zM384 6v256c41-4 83.8-6 128-6s87 2.2 128 6v-256c-41-4-83.8-6-128-6s-87 2.2-128 6z" />
+<glyph unicode="&#xeb08;" glyph-name="icon-dictionary" d="M832 256c105.6 0 192 86.4 192 192v256c0 105.6-86.4 192-192 192v-320l-128 64-128-64v320h-384c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v192c0-105.6-86.4-192-192-192h-640v192h640z" />
+<glyph unicode="&#xeb09;" glyph-name="icon-folder" d="M896 704h-320c-16.4 16.4-96.8 96.8-109.2 109.2l-37.4 37.4c-25 25-74.2 45.4-109.4 45.4h-256c-35.2 0-64-28.8-64-64v-384c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v128c0 70.4-57.6 128-128 128zM896 448h-768c-70.4 0-128-57.6-128-128v-320c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v320c0 70.4-57.6 128-128 128z" />
+<glyph unicode="&#xeb0a;" glyph-name="icon-image" d="M896 896h-768c-70.4 0-128-57.6-128-128v-768c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v768c0 70.4-57.6 128-128 128zM896 0h-768v768h768v-768zM320 640l-128-128v-448h640v320l-128 128-128-128z" />
+<glyph unicode="&#xeb0b;" glyph-name="icon-layout" d="M448 896h-256c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h256v1024zM832 896h-256v-577.664h448v385.664c0 105.6-86.4 192-192 192zM576-128h256c105.6 0 192 86.4 192 192v129.664h-448v-321.664z" />
+<glyph unicode="&#xeb0c;" glyph-name="icon-object" d="M512-128l512 320v384l-512.020 320-511.98-320v-384l512-320zM512 704l358.4-224-358.4-224-358.4 224 358.4 224z" />
+<glyph unicode="&#xeb0d;" glyph-name="icon-object-unknown" d="M511.98 896l-511.98-320v-384l512-320 512 320v384l-512.020 320zM586.22 0h-141.36v136.64h141.36v-136.64zM721.040 398.1c-11.94-17.020-34.9-38.78-68.84-65.24l-33.48-26c-18.24-14.18-30.34-30.74-36.32-49.64-3.78-11.98-5.82-30.58-6.14-55.8h-128.12c1.88 53.26 6.92 90.060 15.080 110.4 8.18 20.34 29.22 43.74 63.16 70.22l34.42 26.94c11.3 8.52 20.42 17.8 27.34 27.9 12.56 17.34 18.86 36.4 18.86 57.2 0 23.94-7 45.78-20.98 65.48-14 19.7-39.54 29.54-76.64 29.54s-62.34-12.14-77.6-36.4c-15.24-24.28-22.88-49.48-22.88-75.64h-136.64c3.78 89.84 35.14 153.5 94.080 191.020 37.18 23.94 82.9 35.94 137.12 35.94 71.22 0 130.42-17.020 177.54-51.060s70.68-84.48 70.68-151.3c0-40.98-10.22-75.5-30.66-103.54z" />
+<glyph unicode="&#xeb0e;" glyph-name="icon-packet" d="M512 896l-512-320v-512c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v512l-512 320zM512 704l358.4-224-358.4-224-358.4 224 358.4 224z" />
+<glyph unicode="&#xeb0f;" glyph-name="icon-page" d="M704 384c-105.6 0-192 86.4-192 192v320h-320c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v320h-320zM768 512h256l-384 384v-256c0-70.4 57.6-128 128-128z" />
+<glyph unicode="&#xeb10;" glyph-name="icon-plot-overlay" d="M830 896h-636c-106.7 0-194-87.3-194-194v-406.82c14.18-18.64 25.66-28.34 32-30.84 14.28 5.62 54.44 47.54 92.96 146 42.46 108.38 116.32 237.66 227.040 237.66 52.4 0 101.42-29.16 145.7-86.68 37.34-48.5 64.84-108.92 81.34-151.080 38.52-98.38 78.68-140.3 92.96-146 14.28 5.62 54.44 47.54 92.96 146 42.46 108.48 116.32 237.76 227.040 237.76 11.355-0.003 22.389-1.366 32.952-3.936l-0.952 0.196v57.74c0 106.7-87.3 194-194 194zM992 503.66c-14.28-5.62-54.44-47.52-92.96-146-42.46-108.38-116.32-237.66-227.040-237.66-52.4 0-101.42 29.16-145.7 86.68-37.34 48.5-64.84 108.92-81.34 151.080-38.52 98.38-78.68 140.3-92.96 146-14.28-5.62-54.44-47.52-92.96-146-42.46-108.48-116.32-237.76-227.040-237.76-11.355 0.003-22.389 1.367-32.952 3.936l0.952-0.196v-57.74c0-106.7 87.3-194 194-194h636c106.7 0 194 87.3 194 194v406.82c-14.18 18.64-25.66 28.34-32 30.84z" />
+<glyph unicode="&#xeb11;" glyph-name="icon-plot-stacked" d="M89.6 584c24.98 0 48.96 26.52 85.52 70.18 45.42 54.28 102 121.82 196 121.82 44.64 0 86.62-15.46 124.8-46 28.68-22.9 51.16-50.42 72.92-77.060 38.42-46.94 59.16-68.94 83.96-68.94h371.2v118c0 106.7-87.3 194-194 194h-636c-106.7 0-194-87.3-194-194v-118h89.6zM529.5 485.6c-28.24 22.64-50.52 50-72 76.28-35.5 43.48-58.76 70.12-86.3 70.12-25.060 0-49.080-26.54-85.66-70.24-45.4-54.24-102-121.76-196-121.76h-89.54v-112h371.2c44 0 85.54-15.34 123.3-45.6 28.24-22.64 50.52-50 72-76.28 35.5-43.48 58.76-70.12 86.3-70.12 25.060 0 49.080 26.54 85.66 70.24 45.4 54.24 102 121.76 196 121.76h89.54v112h-371.2c-44.060 0-85.54 15.34-123.3 45.6zM934.4 184c-24.98 0-48.96-26.52-85.52-70.18-45.42-54.28-102-121.82-196-121.82-44.64 0-86.62 15.46-124.8 46-28.68 22.9-51.16 50.42-72.92 77.060-38.42 46.94-59.16 68.94-83.96 68.94h-371.2v-118c0-106.7 87.3-194 194-194h636c106.7 0 194 87.3 194 194v118h-89.6z" />
+<glyph unicode="&#xeb12;" glyph-name="icon-session" d="M635.6 371.6c6.6-4.2 13.2-8.6 19.2-13.6l120.4-96.4c29.6-23.8 83.8-23.8 113.4 0l135.2 108c0.2 4.8 0.2 9.4 0.2 14.2 0 52.2-7.8 102.4-22.2 149.8l-154.8-123.6c-58.2-46.6-140.2-59.2-211.4-38.4zM248.6 261.8l120.4 96.4c58 46.4 140 59.2 211.2 38.4-6.6 4.2-13.2 8.6-19.2 13.6l-120.4 96.4c-29.6 23.8-83.8 23.8-113.4 0l-120.2-96.6c-40-32-91.4-48-143-48-21.6 0-43 2.8-63.8 8.4 0-0.6 0-1.2 0-1.6 5-3.4 10-6.8 14.6-10.6l120.4-96.4c29.8-23.8 83.8-23.8 113.4 0zM120.6 517.8l120.4 96.4c80.2 64.2 205.6 64.2 285.8 0l120.4-96.4c29.6-23.8 83.8-23.8 113.4 0l181 144.8c-91.2 140.4-249.6 233.4-429.6 233.4-238.6 0-439.2-163.2-496-384.2 30.8-17.6 77.8-15.6 104.6 6zM689 154l-120.4 96.4c-29.6 23.8-83.8 23.8-113.4 0l-120.2-96.4c-40-32-91.4-48-143-48-47.8 0-95.4 13.8-134.2 41.4 85.6-163.6 256.8-275.4 454.2-275.4s368.6 111.8 454.2 275.4c-80.4-57.4-199.8-55.2-277.2 6.6z" />
+<glyph unicode="&#xeb13;" glyph-name="icon-tabular" d="M896 896h-768c-70.4 0-128-57.6-128-128v-768c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v768c0 70.4-57.6 128-128 128zM640 448h-256v192h256v-192zM384 384h256v-192h-256v192zM320 192h-256v192h256v-192zM320 640v-192h-256v192h256zM128-64c-17 0-33 6.6-45.2 18.8s-18.8 28.2-18.8 45.2v128h256v-192h-192zM384-64v192h256v-192h-256zM960 0c0-17-6.6-33-18.8-45.2s-28.2-18.8-45.2-18.8h-192v192h256v-128zM960 192h-256v192h256v-192zM960 448h-256v192h256v-192z" />
+<glyph unicode="&#xeb14;" glyph-name="icon-tabular-lad" d="M896 896h-768c-70.6-0.2-127.8-57.4-128-128v-768c0.2-70.6 57.4-127.8 128-128h768c70.6 0.2 127.8 57.4 128 128v768c-0.2 70.6-57.4 127.8-128 128zM64 640h256v-192h-256v192zM64 384h256v-192h-256v192zM128-64c-35.2 0.2-63.8 28.8-64 64v128h256v-192h-192zM384-64v192h256v-192h-256zM960 0c-0.2-35.2-28.8-63.8-64-64h-192v192h256v-128zM960 384v-192h-576v192h64v64h-64v192h576v-192h-64v-64h64zM782.4 348.6l-110.4 55.2v172.2c0 17.6-14.4 32-32 32s-32-14.4-32-32v-211.8l145.6-72.8c15.8-8 35-1.6 43 14.4 8 15.6 1.6 35-14.2 42.8v0z" />
+<glyph unicode="&#xeb15;" glyph-name="icon-tabular-lad-set" d="M128 128v576c-70.6-0.2-127.8-57.4-128-128v-576c0.2-70.6 57.4-127.8 128-128h576c70.6 0.2 127.8 57.4 128 128h-576c-70.6 0.2-127.8 57.4-128 128zM896 896h-576c-70.6-0.2-127.8-57.4-128-128v-576c0.2-70.6 57.4-127.8 128-128h576c70.6 0.2 127.8 57.4 128 128v576c-0.2 70.6-57.4 127.8-128 128zM256 704h192v-128h-192v128zM256 512h192v-192h-192v192zM320 128c-35.2 0.2-63.8 28.8-64 64v64h192v-128h-128zM512 128v128h192v-128h-192zM960 192c-0.2-35.2-28.8-63.8-64-64h-128v128h192v-64zM960 320h-448v384h448v-384zM832 416c17.6 0 32 14.4 32 32 0 13.8-8.8 26-21.8 30.4l-74.2 24.6v105c0 17.6-14.4 32-32 32s-32-14.4-32-32v-151l117.8-39.2c3.4-1.2 6.8-1.8 10.2-1.8z" />
+<glyph unicode="&#xeb16;" glyph-name="icon-tabular-realtime" d="M896 896h-768c-70.606-0.215-127.785-57.394-128-127.979v-768.021c0.215-70.606 57.394-127.785 127.979-128h768.021c70.606 0.215 127.785 57.394 128 127.979v768.021c-0.215 70.606-57.394 127.785-127.979 128h-0.021zM448 604l25.060-25.32c7.916-7.922 18.856-12.822 30.94-12.822s23.023 4.9 30.94 12.822v0l75.5 76.3c29.97 30.338 71.571 49.128 117.56 49.128s87.59-18.79 117.544-49.112l0.016-0.016 50.44-50.98v-152.2c-24.111 8.83-44.678 22.255-61.542 39.342l-0.018 0.018-75.5 76.3c-7.916 7.922-18.856 12.822-30.94 12.822s-23.023-4.9-30.94-12.822v0l-75.5-76.3c-29.971-30.343-71.575-49.137-117.568-49.137-20.084 0-39.331 3.584-57.137 10.146l1.145-0.369v152.2zM320-64h-192c-35.26 0.214-63.786 28.74-64 63.98v128.020h256v-192zM320 192h-256v192h256v-192zM320 448h-256v192h256v-192zM640-64h-256v192h256v-192zM448 259.38v174.5c1.88-1.74 3.74-3.5 5.56-5.34l75.5-76.3c7.916-7.922 18.856-12.822 30.94-12.822s23.023 4.9 30.94 12.822v0l75.5 76.3c29.966 30.333 71.56 49.119 117.542 49.119 43.28 0 82.673-16.643 112.128-43.879l-0.11 0.1v-174.5c-1.88 1.74-3.74 3.5-5.56 5.34l-75.5 76.3c-7.916 7.922-18.856 12.822-30.94 12.822s-23.023-4.9-30.94-12.822v0l-75.5-76.3c-29.966-30.333-71.56-49.119-117.542-49.119-43.28 0-82.673 16.643-112.128 43.879l0.11-0.1zM960 0c-0.214-35.26-28.74-63.786-63.98-64h-192.020v192h256v-128z" />
+<glyph unicode="&#xeb17;" glyph-name="icon-tabular-scrolling" d="M896 896h-768c-70.606-0.215-127.785-57.394-128-127.979v-768.021c0.215-70.606 57.394-127.785 127.979-128h768.021c70.606 0.215 127.785 57.394 128 127.979v768.021c-0.215 70.606-57.394 127.785-127.979 128h-0.021zM768 640v-192h-192v192zM576 384h192v-192h-192zM512 192h-192v192h192zM512 640v-192h-192v192zM64 640h192v-192h-192zM64 384h192v-192h-192zM128-64c-35.255 0.225-63.775 28.745-64 63.978v128.022h192v-192zM320-64v192h192v-192zM704-64h-128v192h192v-192zM941.14-45.14c-11.511-11.644-27.483-18.856-45.139-18.86h-64.001v64h128c-0.004-17.657-7.216-33.629-18.854-45.134l-0.006-0.006zM960 128h-128v512h128z" />
+<glyph unicode="&#xeb18;" glyph-name="icon-telemetry" d="M32 264.34c14.28 5.62 54.44 47.54 92.96 146 42.46 108.38 116.32 237.66 227.040 237.66 52.4 0 101.42-29.16 145.7-86.68 37.34-48.5 64.84-108.92 81.34-151.080 38.52-98.38 78.68-140.3 92.96-146 14.28 5.62 54.44 47.54 92.96 146 37.4 95.5 99.14 207.14 188.94 232.46-90.462 152.598-254.314 253.3-441.686 253.3-0.075 0-0.15 0-0.225 0h0.011c-282.76 0-512-229.24-512-512 0-0.032 0-0.070 0-0.108 0-35.719 3.641-70.587 10.572-104.254l-0.572 3.323c9.54-10.78 17.22-16.74 22-18.62zM992 503.66c-14.28-5.62-54.44-47.52-92.96-146-42.46-108.38-116.32-237.66-227.040-237.66-52.4 0-101.42 29.16-145.7 86.68-37.34 48.5-64.84 108.92-81.34 151.080-38.52 98.38-78.68 140.3-92.96 146-14.28-5.62-54.44-47.52-92.96-146-37.4-95.5-99.14-207.14-188.94-232.46 90.462-152.598 254.314-253.3 441.686-253.3 0.075 0 0.15 0 0.225 0h-0.011c282.76 0 512 229.24 512 512 0 0.032 0 0.070 0 0.108 0 35.719-3.641 70.587-10.572 104.254l0.572-3.323c-9.54 10.78-17.22 16.74-22 18.62z" />
+<glyph unicode="&#xeb19;" glyph-name="icon-timeline" d="M832 896h-640c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v640c0 105.6-86.4 192-192 192zM128 576v128h256v-128zM256 448h384v-128h-384zM896 64h-448v128h448zM896 320h-128v128h128zM896 576h-384v128h384z" />
+<glyph unicode="&#xeb1a;" glyph-name="icon-timer" d="M640 749.4v82.58c0 35.346-28.654 64-64 64v0h-128c-35.346 0-64-28.654-64-64v0-82.58c-185.040-55.080-320-226.48-320-429.42 0-247.42 200.58-448 448-448s448 200.58 448 448c0 202.96-135 374.4-320 429.42zM532 299.98l-263.76-211c-57.105 59.935-92.24 141.25-92.24 230.772 0 0.080 0 0.16 0 0.24v-0.012c0 185.28 150.72 336 336 336 6.72 0 13.38-0.22 20-0.62v-355.38z" />
+<glyph unicode="&#xeb1b;" glyph-name="icon-topic" d="M454.36 419.36l86.3 86.3c9.088 8.965 21.577 14.502 35.36 14.502s26.272-5.537 35.366-14.507l86.294-86.294c19.328-19.358 42.832-34.541 69.047-44.082l1.313-0.418v172.14l-57.64 57.64c-34.408 34.33-81.9 55.558-134.35 55.558s-99.943-21.228-134.354-55.562l-86.296-86.296c-9.088-8.965-21.577-14.502-35.36-14.502s-26.272 5.537-35.366 14.507l-28.674 28.654v-172.14c19.045-7.022 41.040-11.084 63.984-11.084 52.463 0 99.966 21.239 134.379 55.587l-0.003-0.003zM505.64 348.64l-86.3-86.3c-9.088-8.965-21.577-14.502-35.36-14.502s-26.272 5.537-35.366 14.507l-86.294 86.294c-2 2-4.2 4-6.36 6v-197.36c33.664-30.721 78.65-49.537 128.031-49.537 52.44 0 99.923 21.22 134.333 55.541l86.296 86.296c9.088 8.965 21.577 14.502 35.36 14.502s26.272-5.537 35.366-14.507l86.294-86.294c2-2 4.2-4 6.36-6v197.36c-33.664 30.721-78.65 49.537-128.031 49.537-52.44 0-99.923-21.22-134.333-55.541l0.004 0.004zM832 896h-128v-192h127.66l0.34-0.34v-639.32l-0.34-0.34h-127.66v-192h128c105.6 0 192 86.4 192 192v640c0 105.6-86.4 192-192 192zM320 64h-127.66l-0.34 0.34v639.32l0.34 0.34h127.66v192h-128c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h128v192z" />
+<glyph unicode="&#xeb1c;" glyph-name="icon-box-with-dashed-lines-v2" d="M0 512h128v-256h-128v256zM128 767.78l0.22 0.22h191.78v128h-192c-70.606-0.215-127.785-57.394-128-127.979v-192.021h128v191.78zM128 0.22v191.78h-128v-192c0.215-70.606 57.394-127.785 127.979-128h192.021v128h-191.78zM384 896h256v-128h-256v128zM896 0.22l-0.22-0.22h-191.78v-128h192c70.606 0.215 127.785 57.394 128 127.979v192.021h-128v-191.78zM896 896h-192v-128h191.78l0.22-0.22v-191.78h128v192c-0.215 70.606-57.394 127.785-127.979 128h-0.021zM896 512h128v-256h-128v256zM384 0h256v-128h-256v128zM256 640h512v-512h-512v512z" />
+<glyph unicode="&#xeb1d;" glyph-name="icon-summary-widget" d="M896 896h-768c-70.4 0-128-57.6-128-128v-768c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v768c0 70.4-57.6 128-128 128zM847.8 285.6l-82.6-143.2-189.6 131.6 19.2-230h-165.4l19.2 230-189.6-131.6-82.6 143.2 208.6 98.4-208.8 98.4 82.6 143.2 189.6-131.6-19.2 230h165.4l-19.2-230 189.6 131.6 82.6-143.2-208.6-98.4 208.8-98.4z" />
+<glyph unicode="&#xeb1e;" glyph-name="icon-notebook" d="M896 785.2c0 79.8-55.4 127.4-123 105.4l-773-250.6h896v145.2zM896 576h-896v-576c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v448c0 70.4-57.6 128-128 128zM832 64h-384v320h384v-320z" />
+<glyph unicode="&#xeb1f;" glyph-name="icon-tabs-view" d="M0 0c0.227-70.601 57.399-127.773 127.978-128h768.022c70.601 0.227 127.773 57.399 128 127.978v608.022h-512l-50.2 225.6c-7.6 34.2-42.6 62.4-77.8 62.4h-256c-70.601-0.227-127.773-57.399-128-127.978v-0.022zM832 128h-640v256h640zM480 896c35.2 0 70.2-28.2 77.8-62.4l36-161.6h430.2v96c-0.227 70.601-57.399 127.773-127.978 128h-0.022z" />
+<glyph unicode="&#xeb20;" glyph-name="icon-flexible-layout" d="M0 64c0-105.6 86.4-192 192-192h64v576h-256zM0 704v-128h256v320h-64c-105.6 0-192-86.4-192-192zM768-128h64c105.6 0 192 86.4 192 192v128h-256zM384 896h256v-1024h-256v1024zM832 896h-64v-576h256v384c0 105.6-86.4 192-192 192z" />
+<glyph unicode="&#xeb21;" glyph-name="icon-generator-sine" d="M152 422.2c10.8 4.2 40.8 35.6 69.8 109.4 31.8 81.4 87.2 178.4 170.2 178.4 39.4 0 76-21.8 109.2-65 28-36.4 48.8-81.6 61-113.4 29-73.8 59-105.2 69.8-109.4 10.8 4.2 40.8 35.6 69.8 109.4s74.2 155.4 141.6 174.4c-67.89 114.467-190.82 190-331.391 190-0.003 0-0.007 0-0.010 0h0.001c-212 0-384-172-384-384 0.017-26.829 2.71-53.018 7.827-78.329l-0.427 2.529c7.2-8 13-12.6 16.6-14zM884.6 419c7.235 27.919 11.392 59.972 11.4 92.995v0.005c-0.017 26.829-2.71 53.018-7.827 78.329l0.427-2.529c-7.2 8-13 12.6-16.6 14-10.8-4.2-40.8-35.6-69.8-109.4-21.8-55.8-54.6-119-100-153.2zM512 256l135 59c-4.485-0.614-9.689-0.977-14.972-1h-0.028c-39.4 0-76 21.8-109.2 65-28 36.4-48.8 81.6-61 113.4-29 73.8-59 105.2-69.8 109.4-10.8-4.2-40.8-35.6-69.8-109.4-16.4-42.2-39.2-88.4-68.8-123.2zM1024 416l-512-224-512 224v-320l512-224 512 224v320z" />
+<glyph unicode="&#xeb22;" glyph-name="icon-generator-event" d="M320 704h384v-64h-384v64zM320 448h384v-64h-384v64zM320 576h320v-64h-320v64zM256 767.8h512v-399.8l128 56v344c-0.227 70.601-57.399 127.773-127.978 128h-512.022c-70.601-0.227-127.773-57.399-128-127.978v-344.022l128-56zM658.2 320h-292.4l146.2-64 146.2 64zM512 192l-512 224v-320l512-224 512 224v320l-512-224z" />
+<glyph unicode="&#xeb23;" glyph-name="icon-gauge-v2" d="M512 896c-282.8 0-512-229.2-512-512 0-226.4 147-418.4 350.6-486l257.4 486v-503c236.8 45 416 253 416 503 0 282.8-229.2 512-512 512zM754.8 368.2c-58.967 68.597-145.842 111.772-242.8 111.772s-183.833-43.176-242.445-111.35l-0.355-0.422-146 125c8.6 10 17.4 19.6 26.8 28.8 92.628 92.679 220.619 150.006 362 150.006s269.372-57.326 361.997-150.003l0.003-0.003c9.4-9.2 18.2-18.8 26.8-28.8z" />
+<glyph unicode="&#xeb24;" glyph-name="icon-spectra" d="M768 192h-512l102.4 179.2-358.4-51.2v-254c0-106.6 87.4-194 194-194h636c106.8 0 194 87.4 194 194v62l-325.8 186.2zM830 896h-636c-106.6 0-194-87.2-194-194v-318l400 60.2 112 195.8 109.8-192h402.2v254c-0.227 107.052-86.948 193.773-193.978 194h-0.022zM1024 256v64l-384 64 384-128z" />
+<glyph unicode="&#xeb25;" glyph-name="icon-telemetry-spectra" d="M512 640l109.8-192h398.2c-31.4 252.6-247 448-508 448-282.8 0-512-229.2-512-512l400 60.2zM768 192h-512l102.4 179.2-354.4-50.6c31.2-252.8 246.8-448.6 508-448.6 201.6 0 376 116.6 459.6 286l-273.4 156.2zM640 384l384-128v64l-384 64z" />
+<glyph unicode="&#xeb26;" glyph-name="icon-pushbutton" d="M370.2 436.6c9.326-8.53 19.666-16.261 30.729-22.914l0.871-0.486c-11.077 19.209-17.664 42.221-17.8 66.76v0.040c0 39.6 17.8 77.6 50.2 107.4 37 34 87.4 52.6 141.8 52.6 40.2 0 78.2-10.2 110.2-29.2-8.918 15.653-19.693 29.040-32.268 40.482l-0.132 0.118c-37 34-87.4 52.6-141.8 52.6s-104.8-18.6-141.8-52.6c-32.4-29.8-50.2-67.8-50.2-107.4s17.8-77.6 50.2-107.4zM885.4 626.4c-40.6 154.6-192.4 269.6-373.4 269.6s-332.8-115-373.4-269.6c-86-80-138.6-187.8-138.6-306.4 0-247.4 229.2-448 512-448s512 200.6 512 448c0 118.6-52.6 226.4-138.6 306.4zM512 768c141.2 0 256-100.4 256-224s-114.8-224-256-224-256 100.4-256 224 114.8 224 256 224zM512 64c-175.4 0-318.4 127.8-320 285.4 68.8-94.8 186.4-157.4 320-157.4s251.2 62.6 320 157.4c-1.6-157.6-144.6-285.4-320-285.4z" />
+<glyph unicode="&#xeb27;" glyph-name="icon-conditional" d="M512 896c-282.76 0-512-229.24-512-512s229.24-512 512-512 512 229.24 512 512-229.24 512-512 512zM512 128l-384 256 384 256 384-256z" />
+<glyph unicode="&#xeb28;" glyph-name="icon-condition-widget" d="M832 896h-640c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v640c0 105.6-86.4 192-192 192zM512 128l-384 256 384 256 384-256z" />
+<glyph unicode="&#xeb29;" glyph-name="icon-alphanumeric" d="M535.6 365.4c-8.4-1.6-17.2-3-26.2-4s-18.2-2.4-27.2-4c-10.196-1.861-18.808-4.010-27.21-6.633l1.61 0.433c-8.609-2.674-16.105-6.348-22.89-10.987l0.29 0.187c-6.693-4.517-12.283-10.107-16.663-16.585l-0.137-0.215c-4.6-6.8-7.4-15.6-8.8-26s-0.4-18.4 2.4-25.2c2.746-6.688 7.224-12.195 12.881-16.122l0.119-0.078c5.967-4.053 13.057-6.94 20.704-8.161l0.296-0.039c7.592-1.527 16.319-2.4 25.25-2.4 0.123 0 0.246 0 0.369 0h-0.019c22.2 0 39.6 3.6 52.6 11s23.2 16.2 30.2 26.4c6.273 8.873 11.271 19.191 14.426 30.285l0.174 0.715c1.853 6.809 3.601 15.41 4.855 24.169l0.145 1.231 5.2 41.6c-5.4-4.217-11.723-7.564-18.583-9.689l-0.417-0.111c-6.489-2.241-14.362-4.255-22.444-5.662l-0.956-0.138zM1024 512v192h-152l24 192h-192l-24-192h-256l24 192h-192l-24-192h-232v-192h208l-32-256h-176v-192h152l-24-192h192l24 192h256l-24-192h192l24 192h232v192h-208l32 256zM702.8 484.2l-26.4-211.8c-2.231-15.809-3.537-34.122-3.6-52.727v-0.073c0-16.8 2.2-29.4 6.4-37.8h-113.4c-1.342 5.556-2.338 12.122-2.781 18.84l-0.019 0.36c-0.261 3.524-0.409 7.634-0.409 11.778 0 2.962 0.076 5.907 0.226 8.832l-0.017-0.41c-18.663-17.401-41.395-30.694-66.597-38.289l-1.203-0.311c-22.627-6.956-48.639-10.974-75.586-11h-0.014c-0.764-0.011-1.666-0.018-2.569-0.018-18.098 0-35.598 2.563-52.156 7.345l1.325-0.328c-15.991 4.512-29.851 12.090-41.545 22.122l0.145-0.122c-11.233 9.982-19.792 22.733-24.624 37.192l-0.176 0.608c-5.2 15.2-6.4 33.4-3.8 54.4s9.4 42.2 19.4 57.2c9.524 14.399 21.535 26.346 35.532 35.512l0.468 0.288c13.387 8.662 28.922 15.533 45.512 19.765l1.088 0.235c13.436 3.792 30.801 7.554 48.47 10.41l2.93 0.39c17 2.6 33.8 4.6 50.4 6.2 16.628 1.527 31.69 4.070 46.349 7.643l-2.149-0.443c13 3 23.6 7.6 31.6 13.6s12.6 15 13.6 26.4 0.8 21.8-2.4 28.8c-2.849 6.902-7.542 12.56-13.468 16.517l-0.132 0.083c-6.217 4.011-13.604 6.78-21.543 7.774l-0.257 0.026c-7.897 1.277-17 2.007-26.274 2.007-0.537 0-1.073-0.002-1.609-0.007l0.082 0.001c-22 0-40-4.6-53.8-14.2s-23-25.2-28-47.2h-111.8c4.8 26.2 14.2 48 27.8 65.4 13.475 16.978 29.89 30.968 48.574 41.377l0.826 0.423c18.192 10.038 39.297 17.806 61.619 22.175l1.381 0.225c20.488 4.162 44.053 6.563 68.171 6.6h0.029c21.8-0.005 43.239-1.532 64.222-4.479l-2.422 0.279c20.641-2.809 39.324-8.783 56.401-17.461l-1.001 0.461c15.909-8.108 28.858-20.031 37.967-34.601l0.233-0.399c9-15 12.2-34.8 9-59.6z" />
+<glyph unicode="&#xeb2a;" glyph-name="icon-image-telemetry" d="M512 896c-282.8 0-512-229.2-512-512s229.2-512 512-512 512 229.2 512 512-229.2 512-512 512zM783.6 112.4c-69.581-69.675-165.757-112.776-272-112.776-212.298 0-384.4 172.102-384.4 384.4s172.102 384.4 384.4 384.4c212.298 0 384.4-172.102 384.4-384.4 0-0.008 0-0.017 0-0.025v0.001c0.001-0.264 0.001-0.575 0.001-0.887 0-105.769-42.964-201.503-112.391-270.703l-0.010-0.010zM704 512l-128-128-192 192-192-192c0-176.731 143.269-320 320-320s320 143.269 320 320v0z" />
+<glyph unicode="&#xeb2b;" glyph-name="icon-telemetry-aggregate" d="M78 500.56c14 41.44 37.48 100.8 69.2 148.36 38.62 57.78 82.38 87.080 130.14 87.080s91.5-29.3 130-87.080c31.72-47.56 55.14-106.92 69.2-148.36 30.88-90.96 63.12-134.98 78-146.54 14.94 11.56 47.2 55.58 78 146.54 14 41.44 37.48 100.8 69.22 148.36q27.8 41.7 59.12 63.5c-75.7 111.377-201.81 183.58-344.783 183.58-0.034 0-0.068 0-0.103 0h0.006c-229.76 0-416-186.24-416-416 0-0.071 0-0.156 0-0.24 0-39.119 5.396-76.977 15.484-112.871l-0.704 2.931c16.78 21.74 40.4 63.34 63.22 130.74zM754 459.44c-14-41.44-37.48-100.8-69.2-148.36-38.56-57.78-82.32-87.080-130-87.080s-91.5 29.3-130 87.080c-31.72 47.56-55.14 106.92-69.2 148.36-30.88 90.96-63.14 134.98-78 146.54-14.94-11.56-47.2-55.58-78-146.54-14.38-41.44-37.8-100.8-69.6-148.36q-27.8-41.7-59.12-63.5c75.7-111.378 201.81-183.58 344.783-183.58 0.119 0 0.237 0 0.356 0h-0.019c229.76 0 416 186.24 416 416 0 0.071 0 0.156 0 0.24 0 39.119-5.396 76.977-15.484 112.871l0.704-2.931c-16.78-21.74-40.4-63.34-63.22-130.74zM921.56 561.38c4.098-24.449 6.44-52.617 6.44-81.332 0-0.017 0-0.034 0-0.051v0.003c0-0.095 0-0.208 0-0.32 0-282.593-229.087-511.68-511.68-511.68-0.113 0-0.225 0-0.338 0h0.018c-0.014 0-0.031 0-0.048 0-28.716 0-56.884 2.342-84.325 6.845l2.993-0.405c72.483-63.623 168.109-102.44 272.802-102.44 0.203 0 0.406 0 0.61 0h-0.031c229.76 0 416 186.24 416 416 0 0.172 0 0.375 0 0.578 0 104.692-38.817 200.319-102.844 273.271l0.404-0.47z" />
+<glyph unicode="&#xeb2c;" glyph-name="icon-bar-graph" d="M832 896h-640c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v640c0 105.6-86.4 192-192 192zM267.64 0h-139.64v448h139.64zM477.1 0h-139.64v768h139.64zM686.54 0h-139.64v320h139.64zM896 0h-139.64v640h139.64z" />
+<glyph unicode="&#xeb2d;" glyph-name="icon-map" d="M896 830.6l-128-62.6v-896l128 62.6c70.4 34.42 128 120.2 128 190.6v640c0 70.4-57.6 99.82-128 65.4zM320-16l387.2-96.8v896l-387.2 96.8v-896zM259.2 895.2l-3.2 0.8-128-62.6c-70.4-34.42-128-120.2-128-190.6v-640c0-70.4 57.6-99.82 128-65.4l128 62.6 3.2-0.8z" />
+<glyph unicode="&#xeb2e;" glyph-name="icon-plan" d="M256 704v64c0.215 70.606 57.394 127.785 127.979 128h256.021c70.606-0.215 127.785-57.394 128-127.979v-64.021zM832 768v-128h-640v128c-105.6 0-192-86.4-192-192v-512c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v512c0 105.6-86.4 192-192 192zM128 320v128h256v-128zM640 64h-384v128h384zM896 64h-128v128h128zM896 320h-384v128h384z" />
+<glyph unicode="&#xeb2f;" glyph-name="icon-timelist" d="M896 896h-768c-70.606-0.215-127.785-57.394-128-127.979v-768.021c0.215-70.606 57.394-127.785 127.979-128h768.021c70.606 0.215 127.785 57.394 128 127.979v768.021c-0.215 70.606-57.394 127.785-127.979 128h-0.021zM426.94 362.54c-8.054-15.864-24.249-26.545-42.938-26.545-7.823 0-15.209 1.871-21.734 5.191l0.273-0.126-154.54 77.28v221.66c0 26.51 21.49 48 48 48s48-21.49 48-48v0-162.34l101.46-50.72c15.864-8.054 26.545-24.249 26.545-42.938 0-7.823-1.871-15.209-5.191-21.734l0.126 0.273zM896 0h-320v128h320zM896 192h-320v128h320zM896 384h-320v128h320zM896 576h-320v128h320z" />
+<glyph unicode="&#xeb30;" glyph-name="icon-plot-scatter" d="M192 896c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v640c0 105.6-86.4 192-192 192zM128 544c0 53.019 42.981 96 96 96s96-42.981 96-96c0-53.019-42.981-96-96-96v0c-53.019 0-96 42.981-96 96v0zM288 64c-53.019 0-96 42.981-96 96s42.981 96 96 96c53.019 0 96-42.981 96-96v0c0-53.019-42.981-96-96-96v0zM544 256c-53.019 0-96 42.981-96 96s42.981 96 96 96c53.019 0 96-42.981 96-96v0c0-53.019-42.981-96-96-96v0zM544 576c-53.019 0-96 42.981-96 96s42.981 96 96 96c53.019 0 96-42.981 96-96v0c0-53.019-42.981-96-96-96v0zM800 64c-53.019 0-96 42.981-96 96s42.981 96 96 96c53.019 0 96-42.981 96-96v0c0-53.019-42.981-96-96-96v0z" />
+<glyph unicode="&#xeb31;" glyph-name="icon-notebook-restricted" d="M896 785.28c0 79.9-55.38 127.32-123.080 105.36l-772.92-250.64h896v145.28zM896 576h-896v-576c0-70.4 57.6-128 128-128h768c70.4 0 128 57.6 128 128v448c0 70.4-57.6 128-128 128zM256 64h-128v128h128v-128zM256 256h-128v128h128v-128zM896 64h-512v128h512v-128zM896 256h-512v128h512v-128z" />
</font></defs></svg> \ No newline at end of file
diff --git a/src/styles/fonts/Open-MCT-Symbols-16px.ttf b/src/styles/fonts/Open-MCT-Symbols-16px.ttf
index f4f69f5c7..8e495b78e 100644
--- a/src/styles/fonts/Open-MCT-Symbols-16px.ttf
+++ b/src/styles/fonts/Open-MCT-Symbols-16px.ttf
Binary files differ
diff --git a/src/styles/fonts/Open-MCT-Symbols-16px.woff b/src/styles/fonts/Open-MCT-Symbols-16px.woff
index 5268da210..6f9bdd0cc 100644
--- a/src/styles/fonts/Open-MCT-Symbols-16px.woff
+++ b/src/styles/fonts/Open-MCT-Symbols-16px.woff
Binary files differ
diff --git a/src/styles/notebook.scss b/src/styles/notebook.scss
index 44ddd5fc6..ca683142d 100644
--- a/src/styles/notebook.scss
+++ b/src/styles/notebook.scss
@@ -45,13 +45,13 @@
&__nav {
flex: 0 0 auto;
+
* {
overflow: hidden;
}
}
.c-sidebar {
- background: $sideBarBg;
.c-sidebar__pane {
flex-basis: 50%;
}
@@ -75,8 +75,10 @@
flex: 1 1 auto;
flex-direction: column;
width: 100%;
+
> * {
flex: 0 0 auto;
+
+ * {
margin-top: $interiorMargin;
}
@@ -111,18 +113,23 @@
flex: 1 1 auto;
}
+
+ &__page-locked,
&__drag-area {
- // TODO: recast this element to use c-drop-hint element
- background: rgba($colorKey, 0.1);
- border: 1px dashed rgba($colorKey, 0.7);
border-radius: $controlCr;
- color: rgba($colorBodyFg, 0.7);
padding: 10px;
- cursor: pointer;
&:before {
margin-right: 7px !important;
}
+ }
+
+ &__drag-area {
+ background: rgba($colorKey, 0.1);
+ border: 1px dashed rgba($colorKey, 0.7);
+ color: $colorKey;
+ cursor: pointer;
+ justify-content: center;
[class*="__label"] {
font-style: italic;
@@ -131,7 +138,7 @@
&:hover {
background: rgba($colorKey, 0.2);
- color: $colorBodyFg;
+ //color: $colorBodyFg;
}
&.drag-active,
@@ -151,6 +158,7 @@
display: flex;
flex-wrap: wrap; // Allows wrapping in mobile portrait and narrow placements
line-height: 220%;
+
> * {
flex: 0 0 auto;
}
@@ -162,9 +170,11 @@
overflow: hidden;
white-space: nowrap;
font-size: $headerFontSize;
+
> * {
// Section
flex: 0 0 auto;
+
+ * {
// Page
display: inline;
@@ -188,6 +198,13 @@
[class*="__entry"] + [class*="__entry"] {
margin-top: $interiorMarginSm;
}
+
+ .commit-button {
+ @include cButton();
+ position: absolute;
+ right: 5px;
+ bottom: 5px;
+ }
}
/***** SEARCH RESULTS */
@@ -218,6 +235,30 @@
}
}
}
+
+ /***** RESTRICTED NOTEBOOK */
+ &__page-locked {
+ background: rgba($colorAlert, 0.2);
+ display: flex;
+ padding: 5px;
+
+ > * + * {
+ margin-left: $interiorMargin;
+ }
+
+ [class*='icon'] {
+ flex: 0 0 auto;
+ }
+
+ [class*='__message'] {
+ flex: 1 1 auto;
+ }
+ }
+
+ &__commit-entries-control {
+ display: flex;
+ justify-content: flex-end;
+ }
}
.is-notebook-default,
@@ -243,40 +284,53 @@
display: flex;
padding: $interiorMarginSm $interiorMarginSm $interiorMarginSm $interiorMargin;
- &__time,
&__text,
&__local-controls {
padding-top: $p;
padding-bottom: $p;
}
- &__time,
+ &__creator,
&__embed__time {
opacity: 0.7;
}
+ &__time-and-creator,
+ &__input {
+ padding: $p;
+ }
+
+ &__creator [class*='icon'] {
+ font-size: 0.95em;
+ }
+
&__time-and-content {
- display: flex;
+ display: block;
flex: 1 1 auto;
- flex-wrap: wrap;
+
+ > * + * {
+ margin-top: $interiorMarginSm;
+ }
+
+ [class*='created-'] {
+ color: pullForward($colorBodyFg, 20%);
+ }
}
&__time {
- flex: 0 1 auto;
- min-width: 130px;
- margin-right: $interiorMarginLg;
-
* {
white-space: nowrap;
}
}
&__content {
+ display: flex;
+ flex-direction: column;
flex: 1 1 auto;
- > [class*="__"] + [class*="__"] {
+ > [class*="__"] + [class*="__"] {
margin-top: $interiorMarginSm;
- }
+ }
}
&__text {
@@ -294,11 +348,11 @@
&__input {
// Appended to __text element when Notebook is not in readOnly mode
@include inlineInput;
- padding-left: $inputTextPLeftRight;
- padding-right: $inputTextPLeftRight;
+ padding-left: $p;
+ padding-right: $p;
@include hover {
- &:not(:focus) {
+ &:not(:focus, .locked) {
background: rgba($colorBodyFg, 0.1);
}
}
@@ -399,6 +453,7 @@
@include snapThumb();
}
}
+
/****************************** SNAPSHOTTING */
// LEGACY: TODO: refactor these names
.t-contents,
@@ -413,7 +468,10 @@
color: $colorBodyFg;
padding: $interiorMarginSm !important; // Prevents items from going right to the edge of the image
- .l-sticky-headers .l-tabular-body { overflow: auto; }
+ .l-sticky-headers .l-tabular-body {
+ overflow: auto;
+ }
+
.l-browse-bar {
display: none; // Suppress browse-bar when snapshotting from view-large overlay
+ * {
@@ -457,6 +515,7 @@
> * {
flex: 1 1 auto;
+
&:first-child {
flex: 0 0 auto;
}
@@ -494,13 +553,19 @@
display: flex;
flex-direction: column;
position: absolute;
- top: $m; right: 0; bottom: $m; left: 0; // LEGACY, deal with .editor border-radius clipping stuff
+ top: $m;
+ right: 0;
+ bottom: $m;
+ left: 0; // LEGACY, deal with .editor border-radius clipping stuff
}
#snap-annotation-wrapper,
#snap-annotation-bar {
position: relative;
- top: auto; right: auto; bottom: auto; left: auto;
+ top: auto;
+ right: auto;
+ bottom: auto;
+ left: auto;
}
#snap-annotation-wrapper {
@@ -528,7 +593,9 @@
> div {
display: contents;
- > * + * { margin-left: $interiorMargin !important; }
+ > * + * {
+ margin-left: $interiorMargin !important;
+ }
}
.ptro-tool-controls {
@@ -600,7 +667,7 @@
}
.ptro-color-active-control {
- background: $colorBtnMajorBg !important;
+ background: $colorBtnMajorBg !important;
color: $colorBtnMajorFg !important;
}
@@ -618,7 +685,10 @@
/****************************** MOBILE */
body.mobile {
- .c-notebook__drag-area { display: none; }
+ .c-notebook__drag-area {
+ display: none;
+ }
+
.c-notebook__entry {
[class*="local-controls"] {
display: none;
@@ -651,3 +721,32 @@ body.mobile {
$c: $colorOk;
@include pulseProp($animName: flashSnapshot, $dur: 500ms, $iter: infinite, $prop: background, $valStart: rgba($c, 0.4), $valEnd: rgba($c, 0));
}
+
+/****************************** RESTRICTED NOTEBOOK / SHIFT LOG */
+.c-notebook--restricted {
+ .c-notebook__pages {
+ .c-list__item {
+ // Can display lock icon when a page is committed.
+ &:before {
+ $s: 0.8em;
+ color: $colorAlert;
+ display: block;
+ font-size: $s;
+ width: $s;
+ margin-right: $interiorMarginSm;
+ }
+
+ &:not([class*='lock']) {
+ &:before {
+ content: '';
+ }
+ }
+ }
+ }
+}
+
+.c-list__item {
+ &__name:focus {
+ text-overflow: clip;
+ }
+}
diff --git a/src/styles/vue-styles.scss b/src/styles/vue-styles.scss
index 8cfe9c699..bf172cef1 100644
--- a/src/styles/vue-styles.scss
+++ b/src/styles/vue-styles.scss
@@ -35,6 +35,7 @@
@import "../ui/components/progress-bar.scss";
@import "../ui/components/search.scss";
@import "../ui/components/swim-lane/swimlane.scss";
+@import "../ui/components/tags/tags.scss";
@import "../ui/components/toggle-switch.scss";
@import "../ui/components/timesystem-axis.scss";
@import "../ui/components/List/list-view.scss";
@@ -45,7 +46,7 @@
@import "../ui/layout/create-button.scss";
@import "../ui/layout/layout.scss";
@import "../ui/layout/mct-tree.scss";
-@import "../ui/layout/mct-search.scss";
+@import "../ui/layout/search/search.scss";
@import "../ui/layout/pane.scss";
@import "../ui/layout/status-bar/indicators.scss";
@import "../ui/layout/status-bar/notification-banner.scss";
@@ -54,6 +55,8 @@
@import "./notebook.scss";
@import "../plugins/notebook/components/sidebar.scss";
@import "../plugins/gauge/gauge.scss";
+@import "../plugins/faultManagement/fault-manager.scss";
+@import "../plugins/operatorStatus/operator-status";
#splash-screen {
display: none;
diff --git a/src/ui/color/ColorSwatch.vue b/src/ui/color/ColorSwatch.vue
index c07291f5d..e6de34c6c 100644
--- a/src/ui/color/ColorSwatch.vue
+++ b/src/ui/color/ColorSwatch.vue
@@ -20,11 +20,8 @@
at runtime from the About dialog for additional information.
-->
<template>
-<div class="u-contents">
- <div
- v-if="canEdit"
- class="grid-row"
- >
+<div class="grid-row grid-row--pad-label-for-button">
+ <template v-if="canEdit">
<div
class="grid-cell label"
:title="editTitle"
@@ -63,11 +60,8 @@
</div>
</div>
</div>
- </div>
- <div
- v-else
- class="grid-row"
- >
+ </template>
+ <template v-else>
<div
class="grid-cell label"
:title="viewTitle"
@@ -81,7 +75,7 @@
>
</span>
</div>
- </div>
+ </template>
</div>
</template>
diff --git a/src/ui/components/ObjectFrame.vue b/src/ui/components/ObjectFrame.vue
index cef893187..694ba2786 100644
--- a/src/ui/components/ObjectFrame.vue
+++ b/src/ui/components/ObjectFrame.vue
@@ -21,9 +21,11 @@
*****************************************************************************/
<template>
<div
+ ref="soView"
class="c-so-view js-notebook-snapshot-item-wrapper"
:class="[
statusClass,
+ widthClass,
'c-so-view--' + domainObject.type,
{
'c-so-view--no-frame': !hasFrame,
@@ -111,6 +113,7 @@ const SIMPLE_CONTENT_TYPES = [
'hyperlink',
'conditionWidget'
];
+const CSS_WIDTH_LESS_STR = '--width-less-than-';
export default {
components: {
@@ -150,6 +153,7 @@ export default {
return {
cssClass,
+ widthClass: '',
complexContent,
notebookEnabled: this.openmct.types.get('notebook'),
statusBarItems: [],
@@ -168,6 +172,11 @@ export default {
if (provider) {
this.$refs.objectView.show(this.domainObject, provider.key, false, this.objectPath);
}
+
+ if (this.$refs.soView) {
+ this.soViewResizeObserver = new ResizeObserver(this.resizeSoView);
+ this.soViewResizeObserver.observe(this.$refs.soView);
+ }
},
beforeDestroy() {
this.removeStatusListener();
@@ -175,6 +184,10 @@ export default {
if (this.actionCollection) {
this.unlistenToActionCollection();
}
+
+ if (this.soViewResizeObserver) {
+ this.soViewResizeObserver.disconnect();
+ }
},
methods: {
getSelectionContext() {
@@ -207,6 +220,19 @@ export default {
},
setStatus(status) {
this.status = status;
+ },
+ resizeSoView() {
+ let cW = this.$refs.soView.offsetWidth;
+ let widths = [220, 600];
+ let wClass = '';
+
+ for (let width of widths) {
+ if (cW < width) {
+ wClass = wClass.concat(' ', CSS_WIDTH_LESS_STR, width);
+ }
+ }
+
+ this.widthClass = wClass.trimStart();
}
}
};
diff --git a/src/ui/components/ObjectLabel.vue b/src/ui/components/ObjectLabel.vue
index af4cf1a09..a6247eacd 100644
--- a/src/ui/components/ObjectLabel.vue
+++ b/src/ui/components/ObjectLabel.vue
@@ -42,6 +42,13 @@ export default {
navigateToPath: {
type: String,
default: undefined
+ },
+ readOnly: {
+ type: Boolean,
+ required: false,
+ default() {
+ return false;
+ }
}
},
data() {
diff --git a/src/ui/components/ObjectPath.vue b/src/ui/components/ObjectPath.vue
new file mode 100644
index 000000000..65bb3b626
--- /dev/null
+++ b/src/ui/components/ObjectPath.vue
@@ -0,0 +1,104 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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>
+<ul
+ v-if="orderedOriginalPath.length"
+ class="c-location"
+>
+ <li
+ v-for="pathObject in orderedOriginalPath"
+ :key="pathObject.key"
+ class="c-location__item"
+ >
+ <object-label
+ :domain-object="pathObject.domainObject"
+ :object-path="pathObject.objectPath"
+ :read-only="readOnly"
+ />
+ </li>
+</ul>
+</template>
+
+<script>
+import ObjectLabel from './ObjectLabel.vue';
+
+export default {
+ components: {
+ ObjectLabel
+ },
+ inject: ['openmct'],
+ props: {
+ domainObject: {
+ type: Object,
+ required: true
+ },
+ readOnly: {
+ type: Boolean,
+ required: false,
+ default() {
+ return false;
+ }
+ },
+ showObjectItself: {
+ type: Boolean,
+ required: false,
+ default() {
+ return false;
+ }
+ }
+ },
+ data() {
+ return {
+ orderedOriginalPath: []
+ };
+ },
+ async mounted() {
+ const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
+
+ if (keyString && this.keyString !== keyString) {
+ this.keyString = keyString;
+ this.originalPath = [];
+
+ const rawOriginalPath = await this.openmct.objects.getOriginalPath(keyString);
+
+ const pathWithDomainObject = rawOriginalPath.map((domainObject, index, pathArray) => {
+ let key = this.openmct.objects.makeKeyString(domainObject.identifier);
+ const objectPath = pathArray.slice(index);
+
+ return {
+ domainObject,
+ key,
+ objectPath
+ };
+ });
+ if (this.showObjectItself) {
+ // remove ROOT only
+ this.orderedOriginalPath = pathWithDomainObject.slice(0, pathWithDomainObject.length - 1).reverse();
+ } else {
+ // remove ROOT and object itself from path
+ this.orderedOriginalPath = pathWithDomainObject.slice(1, pathWithDomainObject.length - 1).reverse();
+ }
+ }
+ }
+};
+</script>
diff --git a/src/ui/components/ObjectView.vue b/src/ui/components/ObjectView.vue
index fbc4d433f..1ab45a947 100644
--- a/src/ui/components/ObjectView.vue
+++ b/src/ui/components/ObjectView.vue
@@ -6,6 +6,7 @@
>
<independent-time-conductor
:domain-object="domainObject"
+ :object-path="path"
@stateChanged="updateIndependentTimeState"
@updated="saveTimeOptions"
/>
@@ -28,6 +29,7 @@ const SupportedViewTypes = [
'plot-stacked',
'plot-overlay',
'bar-graph.view',
+ 'scatter-plot.view',
'time-strip.view'
];
export default {
@@ -66,6 +68,9 @@ export default {
};
},
computed: {
+ path() {
+ return this.domainObject && (this.currentObjectPath || this.objectPath);
+ },
objectFontStyle() {
return this.domainObject && this.domainObject.configuration && this.domainObject.configuration.fontStyle;
},
diff --git a/src/ui/components/components.js b/src/ui/components/components.js
new file mode 100644
index 000000000..3a45f1bc7
--- /dev/null
+++ b/src/ui/components/components.js
@@ -0,0 +1,29 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 ObjectView from './ObjectView.vue';
+import StackedPlot from '../../plugins/plot/stackedPlot/StackedPlot.vue';
+
+export default {
+ ObjectView,
+ StackedPlot
+};
diff --git a/src/ui/components/componentsSpec.js b/src/ui/components/componentsSpec.js
new file mode 100644
index 000000000..be9a637f4
--- /dev/null
+++ b/src/ui/components/componentsSpec.js
@@ -0,0 +1,48 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 {
+ createOpenMct,
+ resetApplicationState
+} from 'utils/testing';
+
+describe('UI Components', () => {
+ let openmct;
+
+ beforeEach(done => {
+ openmct = createOpenMct();
+ openmct.on('start', done);
+ openmct.startHeadless();
+ });
+
+ afterEach(() => {
+ return resetApplicationState();
+ });
+
+ it('are exposed to users', () => {
+ expect(openmct.components).toBeDefined();
+ });
+
+ it('exposes the object view', () => {
+ expect(openmct.components.ObjectView).toBeDefined();
+ });
+});
diff --git a/src/ui/components/object-frame.scss b/src/ui/components/object-frame.scss
index 11f1a86ed..5c65f095d 100644
--- a/src/ui/components/object-frame.scss
+++ b/src/ui/components/object-frame.scss
@@ -11,6 +11,7 @@
margin-bottom: $interiorMarginSm;
overflow: hidden;
padding: 3px;
+ @include smallerControlButtons; // Make button in frame headers a bit smaller
.c-object-label {
font-size: 1.05em;
@@ -43,11 +44,11 @@
flex: 0 0 auto;
}
- .is-in-small-container &,
- .c-fl-frame & {
+ .--width-less-than-220 &,
+ .--width-less-than-600 & {
[class*="__label"] {
// button labels
- display: none;
+ display: none !important;
}
}
@@ -132,8 +133,6 @@
}
}
- @include smallerControlButtons;
-
&.has-complex-content {
> .c-so-view__view-large { display: block; }
}
@@ -141,6 +140,10 @@
&.is-status--missing {
border: $borderMissing;
}
+
+ // Leave for debugging
+ //&.--width-less-than-600 { background: rgba(orange, 0.2) !important; }
+ //&.--width-less-than-220 { background: rgba(red, 0.2) !important; }
}
.l-angular-ov-wrapper {
@@ -149,3 +152,5 @@
display: block;
height: 100%;
}
+
+
diff --git a/src/ui/components/object-label.scss b/src/ui/components/object-label.scss
index 3128eaba3..174b02277 100644
--- a/src/ui/components/object-label.scss
+++ b/src/ui/components/object-label.scss
@@ -2,7 +2,7 @@
// <a> tag and draggable element that holds type icon and name.
// Used mostly in trees and lists
display: flex;
- align-items: baseline; // Provides better vertical alignment than center
+ align-items: center;
flex: 0 1 auto;
overflow: hidden;
white-space: nowrap;
diff --git a/src/ui/components/search.vue b/src/ui/components/search.vue
index 46d069bff..57ea2d141 100644
--- a/src/ui/components/search.vue
+++ b/src/ui/components/search.vue
@@ -5,6 +5,7 @@
>
<input
class="c-search__input"
+ aria-label="Search Input"
tabindex="10000"
type="search"
v-bind="$attrs"
diff --git a/src/ui/components/tags/TagEditor.vue b/src/ui/components/tags/TagEditor.vue
new file mode 100644
index 000000000..2a4068009
--- /dev/null
+++ b/src/ui/components/tags/TagEditor.vue
@@ -0,0 +1,176 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+<template>
+<div class="c-tag-applier">
+ <TagSelection
+ v-for="(addedTag, index) in addedTags"
+ :key="index"
+ :selected-tag="addedTag.newTag ? null : addedTag"
+ :new-tag="addedTag.newTag"
+ :added-tags="addedTags"
+ @tagRemoved="tagRemoved"
+ @tagAdded="tagAdded"
+ />
+ <button
+ v-show="!userAddingTag && !maxTagsAdded"
+ class="c-tag-applier__add-btn c-icon-button c-icon-button--major icon-plus"
+ title="Add new tag"
+ @click="addTag"
+ >
+ <div class="c-icon-button__label">Add Tag</div>
+ </button>
+</div>
+</template>
+
+<script>
+import TagSelection from './TagSelection.vue';
+
+export default {
+ components: {
+ TagSelection
+ },
+ inject: ['openmct'],
+ props: {
+ annotationQuery: {
+ type: Object,
+ required: true
+ },
+ annotationType: {
+ type: String,
+ required: true
+ },
+ annotationSearchType: {
+ type: String,
+ required: true
+ },
+ targetSpecificDetails: {
+ type: Object,
+ required: true
+ },
+ domainObject: {
+ type: Object,
+ default() {
+ return null;
+ }
+ }
+ },
+ data() {
+ return {
+ annontation: null,
+ addedTags: [],
+ userAddingTag: false
+ };
+ },
+ computed: {
+ availableTags() {
+ return this.openmct.annotation.getAvailableTags();
+ },
+ maxTagsAdded() {
+ const availableTags = this.openmct.annotation.getAvailableTags();
+
+ return !(availableTags && availableTags.length && (this.addedTags.length < availableTags.length));
+ }
+ },
+ watch: {
+ annotation: {
+ handler() {
+ this.tagsChanged(this.annotation.tags);
+ },
+ deep: true
+ },
+ annotationQuery: {
+ handler() {
+ this.unloadAnnotation();
+ this.loadAnnotation();
+ },
+ deep: true
+ }
+ },
+ mounted() {
+ this.loadAnnotation();
+ },
+ destroyed() {
+ if (this.removeTagsListener) {
+ this.removeTagsListener();
+ }
+ },
+ methods: {
+ addAnnotationListener(annotation) {
+ if (annotation && !this.removeTagsListener) {
+ this.removeTagsListener = this.openmct.objects.observe(annotation, '*', (newAnnotation) => {
+ this.tagsChanged(newAnnotation.tags);
+ this.annotation = newAnnotation;
+ });
+ }
+ },
+ async loadAnnotation() {
+ this.annotation = await this.openmct.annotation.getAnnotation(this.annotationQuery, this.annotationSearchType);
+ this.addAnnotationListener(this.annotation);
+ if (this.annotation && this.annotation.tags) {
+ this.tagsChanged(this.annotation.tags);
+ }
+ },
+ unloadAnnotation() {
+ if (this.removeTagsListener) {
+ this.removeTagsListener();
+ this.removeTagsListener = undefined;
+ }
+ },
+ tagsChanged(newTags) {
+ if (newTags.length < this.addedTags.length) {
+ this.addedTags = this.addedTags.slice(0, newTags.length);
+ }
+
+ for (let index = 0; index < newTags.length; index += 1) {
+ this.$set(this.addedTags, index, newTags[index]);
+ }
+ },
+ addTag() {
+ const newTagValue = {
+ newTag: true
+ };
+ this.addedTags.push(newTagValue);
+ this.userAddingTag = true;
+ },
+ async tagRemoved(tagToRemove) {
+ const result = await this.openmct.annotation.removeAnnotationTag(this.annotation, tagToRemove);
+ this.$emit('tags-updated');
+
+ return result;
+ },
+ async tagAdded(newTag) {
+ const annotationWasCreated = this.annotation === null || this.annotation === undefined;
+ this.annotation = await this.openmct.annotation.addAnnotationTag(this.annotation,
+ this.domainObject, this.targetSpecificDetails, this.annotationType, newTag);
+ if (annotationWasCreated) {
+ this.addAnnotationListener(this.annotation);
+ }
+
+ this.tagsChanged(this.annotation.tags);
+ this.userAddingTag = false;
+
+ this.$emit('tags-updated');
+ }
+ }
+};
+</script>
diff --git a/src/ui/components/tags/TagSelection.vue b/src/ui/components/tags/TagSelection.vue
new file mode 100644
index 000000000..3163ae045
--- /dev/null
+++ b/src/ui/components/tags/TagSelection.vue
@@ -0,0 +1,152 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+<template>
+<div class="c-tag__parent">
+ <div class="c-tag_selection">
+ <AutoCompleteField
+ v-if="newTag"
+ ref="tagSelection"
+ :model="availableTagModel"
+ :place-holder-text="'Type to select tag'"
+ class="c-tag-selection"
+ :item-css-class="'icon-circle'"
+ @onChange="tagSelected"
+ />
+ <div
+ v-else
+ class="c-tag"
+ :style="{ background: selectedBackgroundColor, color: selectedForegroundColor }"
+ >
+ <div class="c-tag__label">{{ selectedTagLabel }} </div>
+ <button
+ class="c-completed-tag-deletion c-tag__remove-btn icon-x-in-circle"
+ @click="removeTag"
+ ></button>
+ </div>
+ </div>
+</div>
+</template>
+
+<script>
+
+import AutoCompleteField from '../../../api/forms/components/controls/AutoCompleteField.vue';
+
+export default {
+ components: {
+ AutoCompleteField
+ },
+ inject: ['openmct'],
+ props: {
+ addedTags: {
+ type: Array,
+ default() {
+ return [];
+ }
+ },
+ selectedTag: {
+ type: String,
+ default() {
+ return "";
+ }
+ },
+ newTag: {
+ type: Boolean,
+ default() {
+ return false;
+ }
+ }
+ },
+ data() {
+ return {
+ };
+ },
+ computed: {
+ availableTagModel() {
+ const availableTags = this.openmct.annotation.getAvailableTags().filter(tag => {
+ return (!this.addedTags.includes(tag.id));
+ }).map(tag => {
+ return {
+ name: tag.label,
+ color: tag.backgroundColor,
+ id: tag.id
+ };
+ });
+
+ return {
+ options: availableTags
+ };
+ },
+ selectedBackgroundColor() {
+ const selectedTag = this.getAvailableTagByID(this.selectedTag);
+ if (selectedTag) {
+ return selectedTag.backgroundColor;
+ } else {
+ // missing available tag color, use default
+ return '#00000';
+ }
+ },
+ selectedForegroundColor() {
+ const selectedTag = this.getAvailableTagByID(this.selectedTag);
+ if (selectedTag) {
+ return selectedTag.foregroundColor;
+ } else {
+ // missing available tag color, use default
+ return '#FFFFF';
+ }
+ },
+ selectedTagLabel() {
+ const selectedTag = this.getAvailableTagByID(this.selectedTag);
+ if (selectedTag) {
+ return selectedTag.label;
+ } else {
+ // missing available tag color, use default
+ return '¡UNKNOWN!';
+ }
+ }
+ },
+ mounted() {
+ },
+ methods: {
+ getAvailableTagByID(tagID) {
+ return this.openmct.annotation.getAvailableTags().find(tag => {
+ return tag.id === tagID;
+ });
+ },
+ removeTag() {
+ this.$emit('tagRemoved', this.selectedTag);
+ },
+ tagSelected(autoField) {
+ const tagAdded = autoField.model.options.find(option => {
+ if (option.name === autoField.value) {
+ return true;
+ }
+
+ return false;
+ });
+ if (tagAdded) {
+ this.$emit('tagAdded', tagAdded.id);
+ }
+ }
+ }
+};
+</script>
diff --git a/src/ui/components/tags/tags.scss b/src/ui/components/tags/tags.scss
new file mode 100644
index 000000000..ebd3e7a18
--- /dev/null
+++ b/src/ui/components/tags/tags.scss
@@ -0,0 +1,67 @@
+/******************************* TAGS */
+.c-tag {
+ border-radius: 10px; //TODO: convert to theme constant
+ display: inline-flex;
+ padding: 1px 10px; //TODO: convert to theme constant
+
+ > * + * {
+ margin-left: $interiorMargin;
+ }
+
+ &__remove-btn {
+ color: inherit !important;
+ display: none;
+ opacity: 0;
+ overflow: hidden;
+ padding: 1px !important;
+ transition: $transIn;
+ width: 0;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+
+ /* SEARCH RESULTS */
+ &.--is-not-search-match {
+ opacity: 0.5;
+ }
+}
+
+/******************************* TAG EDITOR */
+.c-tag-applier {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-items: center;
+
+ > * + * {
+ margin-left: $interiorMargin;
+ }
+
+ &__add-btn {
+ &:before { font-size: 0.9em; }
+ }
+
+ .c-tag {
+ flex-direction: row;
+ align-items: center;
+ padding-right: 3px !important;
+
+ &__remove-btn {
+ display: block;
+ }
+ }
+}
+
+/******************************* HOVERS */
+.has-tag-applier {
+ // Apply this class to all components that should trigger tag removal btn on hover
+ &:hover {
+ .c-tag__remove-btn {
+ width: 1.1em;
+ opacity: 0.7;
+ transition: $transOut;
+ }
+ }
+ }
diff --git a/src/ui/inspector/Location.vue b/src/ui/inspector/Location.vue
index 590b74ad1..9d661dfbc 100644
--- a/src/ui/inspector/Location.vue
+++ b/src/ui/inspector/Location.vue
@@ -21,7 +21,10 @@
*****************************************************************************/
<template>
-<div class="c-inspect-properties c-inspect-properties--location">
+<div
+ v-if="originalPath.length"
+ class="c-inspect-properties c-inspect-properties--location"
+>
<div
class="c-inspect-properties__header"
title="The location of this linked object."
diff --git a/src/ui/inspector/ObjectName.vue b/src/ui/inspector/ObjectName.vue
index 9703d4a91..57556e01f 100644
--- a/src/ui/inspector/ObjectName.vue
+++ b/src/ui/inspector/ObjectName.vue
@@ -63,6 +63,7 @@ export default {
return {
domainObject: {},
activity: undefined,
+ layoutItem: undefined,
keyString: undefined,
multiSelect: false,
itemsSelected: 0,
@@ -81,6 +82,12 @@ export default {
return 'icon-activity';
}
+ if (!this.domainObject && this.layoutItem) {
+ const layoutItemType = this.openmct.types.get(this.layoutItem.type);
+
+ return layoutItemType.definition.cssClass;
+ }
+
if (this.type.definition.cssClass === undefined) {
return 'icon-object';
}
@@ -132,6 +139,8 @@ export default {
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.status = this.openmct.status.get(this.keyString);
this.statusUnsubscribe = this.openmct.status.observe(this.keyString, this.updateStatus);
+ } else if (selection[0][0].context.layoutItem) {
+ this.layoutItem = selection[0][0].context.layoutItem;
}
}
},
diff --git a/src/ui/inspector/details/Properties.vue b/src/ui/inspector/details/Properties.vue
index 148a2d3dd..fe0edda9e 100644
--- a/src/ui/inspector/details/Properties.vue
+++ b/src/ui/inspector/details/Properties.vue
@@ -95,6 +95,7 @@ export default {
const timestampLabel = this.domainObject.modified ? 'Modified' : 'Created';
const timestamp = this.domainObject.modified ? this.domainObject.modified : this.domainObject.created;
const notes = this.domainObject.notes;
+ const version = this.domainObject.version;
const details = [
{
@@ -127,6 +128,13 @@ export default {
);
}
+ if (version) {
+ details.push({
+ name: 'Version',
+ value: version
+ });
+ }
+
return [...details, ...this.typeProperties];
},
context() {
diff --git a/src/ui/inspector/elements.scss b/src/ui/inspector/elements.scss
index a3d4363ca..14d90b570 100644
--- a/src/ui/inspector/elements.scss
+++ b/src/ui/inspector/elements.scss
@@ -18,7 +18,7 @@
}
.c-grippy {
- $d: 8px;
+ $d: 9px;
flex: 0 0 auto;
margin-right: $interiorMarginSm;
transform: translateY(-2px);
@@ -36,4 +36,4 @@
.js-last-place {
height: 10px;
-} \ No newline at end of file
+}
diff --git a/src/ui/inspector/inspector.scss b/src/ui/inspector/inspector.scss
index 93dc6a95d..3aafd98ba 100644
--- a/src/ui/inspector/inspector.scss
+++ b/src/ui/inspector/inspector.scss
@@ -173,12 +173,18 @@
grid-column: 1 / 3;
}
}
+}
- .is-editing & {
- .c-inspect-properties {
- &__value, &__label {
- line-height: 160%; // Prevent buttons/selects from overlapping when wrapping
- }
+.is-editing {
+ .c-inspect-properties {
+ &__value, &__label {
+ line-height: 160%; // Prevent buttons/selects from overlapping when wrapping
+ }
+ }
+ .grid-row--pad-label-for-button {
+ // Add extra space at the top of the label grid cell because there's a button to the right
+ [class*='label'] {
+ line-height: 1.8em;
}
}
}
@@ -197,17 +203,17 @@
}
}
- li.grid-row + li.grid-row {
+ .grid-row + .grid-row {
> * {
border-top: 1px solid $colorInspectorSectionHeaderBg;
}
}
- li.grid-row .label {
+ .grid-row .label {
color: $colorInspectorPropName;
}
- li.grid-row .value {
+ .grid-row .value {
color: $colorInspectorPropVal;
word-break: break-all;
&:first-child {
diff --git a/src/ui/layout/CreateButton.vue b/src/ui/layout/CreateButton.vue
index 4615aff3e..2443f6b66 100644
--- a/src/ui/layout/CreateButton.vue
+++ b/src/ui/layout/CreateButton.vue
@@ -19,32 +19,18 @@ import objectUtils from 'objectUtils';
export default {
inject: ['openmct'],
data: function () {
- let items = [];
-
- this.openmct.types.listKeys().forEach(key => {
- let menuItem = this.openmct.types.get(key).definition;
-
- if (menuItem.creatable) {
- let menuItemTemplate = {
- cssClass: menuItem.cssClass,
- name: menuItem.name,
- description: menuItem.description,
- onItemClicked: () => this.create(key)
- };
-
- items.push(menuItemTemplate);
- }
- });
return {
- items: items,
+ menuItems: {},
selectedMenuItem: {},
opened: false
};
},
computed: {
sortedItems() {
- return this.items.slice().sort((a, b) => {
+ let items = this.getItems();
+
+ return items.sort((a, b) => {
if (a.name < b.name) {
return -1;
} else if (a.name > b.name) {
@@ -56,6 +42,26 @@ export default {
}
},
methods: {
+ getItems() {
+ let keys = this.openmct.types.listKeys();
+
+ keys.forEach(key => {
+ if (!this.menuItems[key]) {
+ let typeDef = this.openmct.types.get(key).definition;
+
+ if (typeDef.creatable) {
+ this.menuItems[key] = {
+ cssClass: typeDef.cssClass,
+ name: typeDef.name,
+ description: typeDef.description,
+ onItemClicked: () => this.create(key)
+ };
+ }
+ }
+ });
+
+ return Object.values(this.menuItems);
+ },
showCreateMenu() {
const elementBoundingClientRect = this.$refs.createButton.getBoundingClientRect();
const x = elementBoundingClientRect.x;
diff --git a/src/ui/layout/Layout.vue b/src/ui/layout/Layout.vue
index 2ded6498d..87db0617c 100644
--- a/src/ui/layout/Layout.vue
+++ b/src/ui/layout/Layout.vue
@@ -18,6 +18,9 @@
}"
>
<CreateButton class="l-shell__create-button" />
+ <GrandSearch
+ ref="grand-search"
+ />
<indicators class="l-shell__head-section l-shell__indicators" />
<button
class="l-shell__head__collapse-button c-icon-button"
@@ -50,6 +53,7 @@
type="horizontal"
>
<pane
+ id="tree-pane"
class="l-shell__pane-tree"
handle="after"
label="Browse"
@@ -122,6 +126,7 @@ import Inspector from '../inspector/Inspector.vue';
import MctTree from './mct-tree.vue';
import ObjectView from '../components/ObjectView.vue';
import CreateButton from './CreateButton.vue';
+import GrandSearch from './search/GrandSearch.vue';
import multipane from './multipane.vue';
import pane from './pane.vue';
import BrowseBar from './BrowseBar.vue';
@@ -136,6 +141,7 @@ export default {
MctTree,
ObjectView,
CreateButton,
+ GrandSearch,
multipane,
pane,
BrowseBar,
diff --git a/src/ui/layout/MCTSearch.vue b/src/ui/layout/MCTSearch.vue
deleted file mode 100644
index 9208f0ca8..000000000
--- a/src/ui/layout/MCTSearch.vue
+++ /dev/null
@@ -1,13 +0,0 @@
-<template>
-<div class="c-search c-search--major">
- <input
- type="search"
- placeholder="Search"
- >
-</div>
-</template>
-
-<script>
-export default {
-};
-</script>
diff --git a/src/ui/layout/layout.scss b/src/ui/layout/layout.scss
index f708dbbed..f9dc4d8c1 100644
--- a/src/ui/layout/layout.scss
+++ b/src/ui/layout/layout.scss
@@ -392,6 +392,8 @@
&__nav-to-parent-button {
// This is an icon-button
+ margin-right: $interiorMargin;
+
.is-editing & {
display: none;
}
@@ -412,7 +414,6 @@
&__object-name--w {
@include headerFont(1.5em);
- margin-left: $interiorMarginLg;
min-width: 0;
.is-status__indicator {
diff --git a/src/ui/layout/mct-search.scss b/src/ui/layout/mct-search.scss
deleted file mode 100644
index a6b1a8f18..000000000
--- a/src/ui/layout/mct-search.scss
+++ /dev/null
@@ -1,10 +0,0 @@
-/******************************* SEARCH */
-.c-search {
- input[type=search] {
- width: 100%;
- }
-
- &--major {
- display: flex;
- }
-}
diff --git a/src/ui/layout/mct-tree.scss b/src/ui/layout/mct-tree.scss
index bab20a0c2..3dadf18c8 100644
--- a/src/ui/layout/mct-tree.scss
+++ b/src/ui/layout/mct-tree.scss
@@ -6,7 +6,9 @@
flex: 1 1 auto;
overflow: auto;
- > * + * { margin-top: $interiorMargin; }
+ > * + * {
+ margin-top: $interiorMargin;
+ }
&__search {
flex: 0 0 auto;
@@ -59,7 +61,6 @@
@include userSelectNone();
overflow-x: hidden;
overflow-y: auto;
- padding-right: $interiorMarginSm;
.icon-arrow-nav-to-parent {
visibility: hidden;
@@ -71,6 +72,7 @@
li {
position: relative;
+
&[class*="__item-h"] {
display: block;
width: 100%;
@@ -82,7 +84,6 @@
}
&__item {
- border-radius: $controlCr;
display: flex;
align-items: center;
cursor: pointer;
@@ -107,12 +108,14 @@
color: $colorItemTreeSelectedFg;
}
}
+
&.is-new {
animation-name: animTemporaryHighlight;
animation-timing-function: ease-out;
animation-duration: 3s;
animation-iteration-count: 1;
}
+
&.is-context-clicked {
box-shadow: inset $colorItemTreeSelectedBg 0 0 0 1px;
}
@@ -128,11 +131,15 @@
}
.c-tree {
+ padding-right: $interiorMarginSm;
+
.c-tree {
margin-left: 15px;
}
&__item {
+ border-radius: $smallCr;
+
[class*="view-control"] {
padding: 2px 10px;
}
@@ -161,6 +168,7 @@
@include button($bg: $colorMobilePaneLeftTreeItemBg, $fg: $colorMobilePaneLeftTreeItemFg);
height: $mobileTreeItemH;
margin-bottom: $interiorMarginSm;
+
[class*="view-control"] {
width: ceil($mobileTreeItemH * 0.5);
}
@@ -202,10 +210,11 @@
.c-tree {
&__item {
- body.mobile & {
+ body.mobile & {
@include button($bg: $colorMobilePaneLeftTreeItemBg, $fg: $colorMobilePaneLeftTreeItemFg);
height: $mobileTreeItemH;
margin-bottom: $interiorMarginSm;
+
[class*="view-control"] {
width: ceil($mobileTreeItemH * 0.5);
}
@@ -218,9 +227,9 @@
}
.c-list {
- padding-right: $interiorMargin;
-
&__item {
+ border-radius: $smallCr;
+
&__name {
$p: $interiorMarginSm;
@include ellipsize();
@@ -254,7 +263,8 @@
content: '';
display: block;
position: absolute;
- left: 50%; top: 50%;
+ left: 50%;
+ top: 50%;
height: $dimension;
width: $dimension;
}
@@ -277,8 +287,10 @@
.c-selector {
&.c-tree-and-search {
- border: 1px solid $colorFormLines;
- border-radius: $controlCr;
- padding: $interiorMargin;
+ background: rgba($colorFormLines, 0.1);
+ border-radius: $basicCr;
+ padding: 2px;
+ height: 100%;
+ min-height: 150px;
}
}
diff --git a/src/ui/layout/mct-tree.vue b/src/ui/layout/mct-tree.vue
index d78a7b8a1..34ed40929 100644
--- a/src/ui/layout/mct-tree.vue
+++ b/src/ui/layout/mct-tree.vue
@@ -5,13 +5,13 @@
:class="{
'c-selector': isSelectorTree
}"
- :style="treeHeight"
>
<div
ref="search"
class="c-tree-and-search__search"
>
<search
+ v-show="isSelectorTree"
ref="shell-search"
class="c-search"
:value="searchValue"
@@ -40,30 +40,30 @@
<div
ref="mainTree"
class="c-tree-and-search__tree c-tree"
+ role="tree"
+ aria-expanded="true"
>
- <div>
-
- <div
- ref="dummyItem"
- class="c-tree__item-h"
- style="left: -1000px; position: absolute; visibility: hidden"
- >
- <div class="c-tree__item">
- <span class="c-tree__item__view-control c-nav__up is-enabled"></span>
- <a
- class="c-tree__item__label c-object-label"
- draggable="true"
- href="#"
- >
- <div class="c-tree__item__type-icon c-object-label__type-icon icon-folder">
- <span title="Open MCT"></span>
- </div>
- <div class="c-tree__item__name c-object-label__name">
- Open MCT
- </div>
- </a>
- <span class="c-tree__item__view-control c-nav__down"></span>
- </div>
+
+ <div
+ ref="dummyItem"
+ class="c-tree__item-h"
+ style="left: -1000px; position: absolute; visibility: hidden"
+ >
+ <div class="c-tree__item">
+ <span class="c-tree__item__view-control c-nav__up is-enabled"></span>
+ <a
+ class="c-tree__item__label c-object-label"
+ draggable="true"
+ href="#"
+ >
+ <div class="c-tree__item__type-icon c-object-label__type-icon icon-folder">
+ <span title="Open MCT"></span>
+ </div>
+ <div class="c-tree__item__name c-object-label__name">
+ Open MCT
+ </div>
+ </a>
+ <span class="c-tree__item__view-control c-nav__down"></span>
</div>
</div>
@@ -174,7 +174,8 @@ export default {
itemOffset: 0,
activeSearch: false,
mainTreeTopMargin: undefined,
- selectedItem: {}
+ selectedItem: {},
+ observers: {}
};
},
computed: {
@@ -209,7 +210,7 @@ export default {
if (!this.isSelectorTree) {
return {};
} else {
- return { height: this.itemHeight * LOCATOR_ITEM_COUNT_HEIGHT + 'px' };
+ return { 'min-height': this.itemHeight * LOCATOR_ITEM_COUNT_HEIGHT + 'px' };
}
}
},
@@ -275,6 +276,8 @@ export default {
if (this.treeResizeObserver) {
this.treeResizeObserver.disconnect();
}
+
+ this.destroyObservers(this.observers);
},
methods: {
async initialize() {
@@ -444,7 +447,7 @@ export default {
},
scrollTo(navigationPath) {
- if (this.isItemInView(navigationPath)) {
+ if (!this.$refs.scrollable || this.isItemInView(navigationPath)) {
return;
}
@@ -463,6 +466,10 @@ export default {
}
},
scrollEndEvent() {
+ if (!this.$refs.scrollable) {
+ return;
+ }
+
this.$nextTick(() => {
if (this.scrollToPath) {
if (!this.isItemInView(this.scrollToPath)) {
@@ -549,6 +556,8 @@ export default {
}
return composition.map((object) => {
+ this.addTreeItemObserver(object, parentObjectPath);
+
return this.buildTreeItem(object, parentObjectPath);
});
},
@@ -565,6 +574,82 @@ export default {
navigationPath
};
},
+ addTreeItemObserver(domainObject, parentObjectPath) {
+ const objectPath = [domainObject].concat(parentObjectPath);
+ const navigationPath = this.buildNavigationPath(objectPath);
+
+ if (this.observers[navigationPath]) {
+ this.observers[navigationPath]();
+ }
+
+ this.observers[navigationPath] = this.openmct.objects.observe(
+ domainObject,
+ 'name',
+ this.sortTreeItems.bind(this, parentObjectPath)
+ );
+ },
+ async updateTreeItems(parentObjectPath) {
+ let children;
+
+ if (parentObjectPath.length) {
+ const parentItem = this.treeItems.find(item => item.objectPath === parentObjectPath);
+ const descendants = this.getChildrenInTreeFor(parentItem, true);
+ const parentIndex = this.treeItems.map(e => e.object).indexOf(parentObjectPath[0]);
+
+ children = await this.loadAndBuildTreeItemsFor(parentItem.object, parentItem.objectPath);
+
+ this.treeItems.splice(parentIndex + 1, descendants.length, ...children);
+ } else {
+ const root = await this.openmct.objects.get('ROOT');
+ children = await this.loadAndBuildTreeItemsFor(root, []);
+
+ this.treeItems = [...children];
+ }
+
+ for (let item of children) {
+ if (this.isTreeItemOpen(item)) {
+ this.openTreeItem(item);
+ }
+ }
+ },
+ sortTreeItems(parentObjectPath) {
+ const navigationPath = this.buildNavigationPath(parentObjectPath);
+ const parentItem = this.getTreeItemByPath(navigationPath);
+
+ // If the parent is not sortable, skip sorting
+ if (!this.isSortable(parentObjectPath)) {
+ return;
+ }
+
+ // Sort the renamed object and its siblings (direct descendants of the parent)
+ const directDescendants = this.getChildrenInTreeFor(parentItem, false);
+ directDescendants.sort(this.sortNameAscending);
+
+ // Take a copy of the sorted descendants array
+ const sortedTreeItems = directDescendants.slice();
+
+ directDescendants.forEach(descendant => {
+ const parent = this.getTreeItemByPath(descendant.navigationPath);
+
+ // If descendant is not open, skip
+ if (!this.isTreeItemOpen(parent)) {
+ return;
+ }
+
+ // If descendant is open but has no children, skip
+ const children = this.getChildrenInTreeFor(parent, true);
+ if (children.length === 0) {
+ return;
+ }
+
+ // Splice in the children of the descendant
+ const parentIndex = sortedTreeItems.map(item => item.navigationPath).indexOf(parent.navigationPath);
+ sortedTreeItems.splice(parentIndex + 1, 0, ...children);
+ });
+
+ // Splice in all of the sorted descendants
+ this.treeItems.splice(this.treeItems.indexOf(parentItem) + 1, sortedTreeItems.length, ...sortedTreeItems);
+ },
buildNavigationPath(objectPath) {
return '/browse/' + [...objectPath].reverse()
.map((object) => this.openmct.objects.makeKeyString(object.identifier))
@@ -577,6 +662,8 @@ export default {
const descendants = this.getChildrenInTreeFor(parentItem, true);
const directDescendants = this.getChildrenInTreeFor(parentItem);
+ this.addTreeItemObserver(domainObject, parentItem.objectPath);
+
if (directDescendants.length === 0) {
this.addItemToTreeAfter(newItem, parentItem);
@@ -611,6 +698,7 @@ export default {
let removeItem = directDescendants.find(item => item.id === removeKeyString);
this.removeItemFromTree(removeItem);
+ this.removeItemFromObservers(removeItem);
};
},
removeCompositionListenerFor(navigationPath) {
@@ -632,6 +720,13 @@ export default {
const removeIndex = this.getTreeItemIndex(item.navigationPath);
this.treeItems.splice(removeIndex, 1);
},
+ removeItemFromObservers(item) {
+ if (this.observers[item.id]) {
+ this.observers[item.id]();
+
+ delete this.observers[item.id];
+ }
+ },
addItemToTreeBefore(addItem, beforeItem) {
const addIndex = this.getTreeItemIndex(beforeItem.navigationPath);
@@ -863,6 +958,15 @@ export default {
},
handleTreeResize() {
this.calculateHeights();
+ },
+ destroyObservers(observers) {
+ Object.entries(observers).forEach(([keyString, unobserve]) => {
+ if (typeof unobserve === 'function') {
+ unobserve();
+ }
+
+ delete observers[keyString];
+ });
}
}
};
diff --git a/src/ui/layout/pane.vue b/src/ui/layout/pane.vue
index 270df6fb9..75c4353ce 100644
--- a/src/ui/layout/pane.vue
+++ b/src/ui/layout/pane.vue
@@ -23,7 +23,7 @@
>{{ label }}</span>
<slot name="controls"></slot>
<button
- v-if="collapsable"
+ v-if="isCollapsable"
class="l-pane__collapse-button c-icon-button"
@click="toggleCollapse"
></button>
@@ -69,8 +69,8 @@ export default {
};
},
computed: {
- collapsable() {
- return this.hideParam && this.hideParam.length;
+ isCollapsable() {
+ return this.hideParam && this.hideParam.length > 0;
}
},
beforeMount() {
@@ -80,7 +80,7 @@ export default {
async mounted() {
await this.$nextTick();
// Hide tree and/or inspector pane if specified in URL
- if (this.collapsable) {
+ if (this.isCollapsable) {
this.handleHideUrl();
}
},
@@ -145,7 +145,7 @@ export default {
updatePosition: function (event) {
let size = this.getNewSize(event);
let intSize = parseInt(size.substr(0, size.length - 2), 10);
- if (intSize < COLLAPSE_THRESHOLD_PX && this.collapsable === true) {
+ if (intSize < COLLAPSE_THRESHOLD_PX && this.isCollapsable === true) {
this.dragCollapse = true;
this.end();
this.toggleCollapse();
diff --git a/src/ui/layout/search/AnnotationSearchResult.vue b/src/ui/layout/search/AnnotationSearchResult.vue
new file mode 100644
index 000000000..e3678778f
--- /dev/null
+++ b/src/ui/layout/search/AnnotationSearchResult.vue
@@ -0,0 +1,148 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+<template>
+<div
+ class="c-gsearch-result c-gsearch-result--annotation"
+ aria-label="Search Result"
+ role="presentation"
+>
+ <div
+ class="c-gsearch-result__type-icon"
+ :class="resultTypeIcon"
+ ></div>
+ <div
+ class="c-gsearch-result__body"
+ aria-label="Annotation Search Result"
+ >
+ <div
+ class="c-gsearch-result__title"
+ @click="clickedResult"
+ >
+ {{ getResultName }}
+ </div>
+
+ <ObjectPath
+ :domain-object="domainObject"
+ :read-only="false"
+ :show-object-itself="true"
+ />
+
+ <div class="c-gsearch-result__tags">
+ <div
+ v-for="(tag, index) in result.fullTagModels"
+ :key="index"
+ class="c-tag"
+ :class="{ '--is-not-search-match': !isSearchMatched(tag) }"
+ :style="{ backgroundColor: tag.backgroundColor, color: tag.foregroundColor }"
+ >
+ {{ tag.label }}
+ </div>
+ </div>
+ </div>
+ <div class="c-gsearch-result__more-options-button">
+ <button class="c-icon-button icon-3-dots"></button>
+ </div>
+</div>
+</template>
+
+<script>
+import ObjectPath from '../../components/ObjectPath.vue';
+import objectPathToUrl from '../../../tools/url';
+
+export default {
+ name: 'AnnotationSearchResult',
+ components: {
+ ObjectPath
+ },
+ inject: ['openmct'],
+ props: {
+ result: {
+ type: Object,
+ required: true,
+ default() {
+ return {};
+ }
+ }
+ },
+ data() {
+ return {
+ };
+ },
+ computed: {
+ domainObject() {
+ return this.result.targetModels[0];
+ },
+ getResultName() {
+ if (this.result.annotationType === this.openmct.annotation.ANNOTATION_TYPES.NOTEBOOK) {
+ const targetID = Object.keys(this.result.targets)[0];
+ const entryIdToFind = this.result.targets[targetID].entryId;
+ const notebookModel = this.result.targetModels[0].configuration.entries;
+
+ const sections = Object.values(notebookModel);
+ for (const section of sections) {
+ const pages = Object.values(section);
+ for (const entries of pages) {
+ for (const entry of entries) {
+ if (entry.id === entryIdToFind) {
+ return entry.text;
+ }
+ }
+ }
+ }
+
+ return "Could not find any matching Notebook entries";
+ } else {
+ return this.result.targetModels[0].name;
+ }
+ },
+ resultTypeIcon() {
+ return this.openmct.types.get(this.result.type).definition.cssClass;
+ },
+ tagBackgroundColor() {
+ return this.result.fullTagModels[0].backgroundColor;
+ },
+ tagForegroundColor() {
+ return this.result.fullTagModels[0].foregroundColor;
+ }
+ },
+ methods: {
+ clickedResult() {
+ const objectPath = this.domainObject.originalPath;
+ let resultUrl = objectPathToUrl(this.openmct, objectPath);
+ // get rid of ROOT if extant
+ if (resultUrl.includes('/ROOT')) {
+ resultUrl = resultUrl.split('/ROOT').join('');
+ }
+
+ this.openmct.router.navigate(resultUrl);
+ },
+ isSearchMatched(tag) {
+ if (this.result.matchingTagKeys) {
+ return this.result.matchingTagKeys.includes(tag.tagID);
+ }
+
+ return false;
+ }
+ }
+};
+</script>
diff --git a/src/ui/layout/search/GrandSearch.vue b/src/ui/layout/search/GrandSearch.vue
new file mode 100644
index 000000000..f007fc5e8
--- /dev/null
+++ b/src/ui/layout/search/GrandSearch.vue
@@ -0,0 +1,173 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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="GrandSearch"
+ aria-label="OpenMCT Search"
+ class="c-gsearch"
+ role="searchbox"
+>
+ <search
+ ref="shell-search"
+ class="c-gsearch__input"
+ tabindex="0"
+ :value="searchValue"
+ @input="searchEverything"
+ @clear="searchEverything"
+ @click="showSearchResults"
+ />
+ <SearchResultsDropDown
+ ref="searchResultsDropDown"
+ />
+
+</div>
+</template>
+
+<script>
+import search from '../../components/search.vue';
+import SearchResultsDropDown from './SearchResultsDropDown.vue';
+
+export default {
+ name: 'GrandSearch',
+ components: {
+ search,
+ SearchResultsDropDown
+ },
+ inject: ['openmct'],
+ props: {
+ },
+ data() {
+ return {
+ searchValue: '',
+ searchLoading: false,
+ annotationSearchResults: [],
+ objectSearchResults: []
+ };
+ },
+ destroyed() {
+ document.body.removeEventListener('click', this.handleOutsideClick);
+ },
+ methods: {
+ async searchEverything(value) {
+ // if an abort controller exists, regardless of the value passed in,
+ // there is an active search that should be canceled
+ if (this.abortSearchController) {
+ this.abortSearchController.abort();
+ delete this.abortSearchController;
+ }
+
+ this.searchValue = value;
+ // clear any previous search results
+ this.annotationSearchResults = [];
+ this.objectSearchResults = [];
+
+ if (this.searchValue) {
+ await this.getSearchResults();
+ } else {
+ const dropdownOptions = {
+ searchLoading: this.searchLoading,
+ searchValue: this.searchValue,
+ annotationSearchResults: this.annotationSearchResults,
+ objectSearchResults: this.objectSearchResults
+ };
+ this.$refs.searchResultsDropDown.showResults(dropdownOptions);
+ }
+ },
+ getPathsForObjects(objectsNeedingPaths) {
+ return Promise.all(objectsNeedingPaths.map(async (domainObject) => {
+ if (!domainObject) {
+ // user interrupted search, return back
+ return null;
+ }
+
+ const keyStringForObject = this.openmct.objects.makeKeyString(domainObject.identifier);
+ const originalPathObjects = await this.openmct.objects.getOriginalPath(keyStringForObject);
+
+ return {
+ originalPath: originalPathObjects,
+ ...domainObject
+ };
+ }));
+ },
+ async getSearchResults() {
+ // an abort controller will be passed in that will be used
+ // to cancel an active searches if necessary
+ this.searchLoading = true;
+ this.$refs.searchResultsDropDown.showSearchStarted();
+ this.abortSearchController = new AbortController();
+ const abortSignal = this.abortSearchController.signal;
+ try {
+ this.annotationSearchResults = await this.openmct.annotation.searchForTags(this.searchValue, abortSignal);
+ const fullObjectSearchResults = await Promise.all(this.openmct.objects.search(this.searchValue, abortSignal));
+ const aggregatedObjectSearchResults = fullObjectSearchResults.flat();
+ const aggregatedObjectSearchResultsWithPaths = await this.getPathsForObjects(aggregatedObjectSearchResults);
+ const filterAnnotationsAndValidPaths = aggregatedObjectSearchResultsWithPaths.filter(result => {
+ if (this.openmct.annotation.isAnnotation(result)) {
+ return false;
+ }
+
+ return this.openmct.objects.isReachable(result?.originalPath);
+ });
+ this.objectSearchResults = filterAnnotationsAndValidPaths;
+ this.searchLoading = false;
+ this.showSearchResults();
+ } catch (error) {
+ this.searchLoading = false;
+
+ if (this.abortSearchController) {
+ delete this.abortSearchController;
+ }
+
+ // Is this coming from the AbortController?
+ // If so, we can swallow the error. If not, 🤮 it to console
+ if (error.name !== 'AbortError') {
+ console.error(`😞 Error searching`, error);
+ }
+ }
+ },
+ showSearchResults() {
+ const dropdownOptions = {
+ searchLoading: this.searchLoading,
+ searchValue: this.searchValue,
+ annotationSearchResults: this.annotationSearchResults,
+ objectSearchResults: this.objectSearchResults
+ };
+ this.$refs.searchResultsDropDown.showResults(dropdownOptions);
+ document.body.addEventListener('click', this.handleOutsideClick);
+ },
+ handleOutsideClick(event) {
+ // if click event is detected outside the dropdown while the
+ // dropdown is visible, this will collapse the dropdown.
+ if (this.$refs.GrandSearch) {
+ const clickedInsideDropdown = this.$refs.GrandSearch.contains(event.target);
+ const clickedPreviewClose = event.target.parentElement && event.target.parentElement.querySelector('.js-preview-window');
+ const searchResultsDropDown = this.$refs.searchResultsDropDown._data;
+ if (!clickedInsideDropdown && searchResultsDropDown.resultsShown && !searchResultsDropDown.previewVisible && !clickedPreviewClose) {
+ searchResultsDropDown.resultsShown = false;
+ document.body.removeEventListener('click', this.handleOutsideClick);
+ }
+ }
+ }
+ }
+};
+</script>
diff --git a/src/ui/layout/search/GrandSearchSpec.js b/src/ui/layout/search/GrandSearchSpec.js
new file mode 100644
index 000000000..5235fc2b3
--- /dev/null
+++ b/src/ui/layout/search/GrandSearchSpec.js
@@ -0,0 +1,285 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 {
+ createOpenMct,
+ resetApplicationState
+} from 'utils/testing';
+import Vue from 'vue';
+import GrandSearch from './GrandSearch.vue';
+import ExampleTagsPlugin from '../../../../example/exampleTags/plugin';
+import DisplayLayoutPlugin from '../../../plugins/displayLayout/plugin';
+
+describe("GrandSearch", () => {
+ let openmct;
+ let grandSearchComponent;
+ let viewContainer;
+ let parent;
+ let sharedWorkerToRestore;
+ let mockDomainObject;
+ let mockAnnotationObject;
+ let mockDisplayLayout;
+ let mockFolderObject;
+ let mockAnotherFolderObject;
+ let mockTopObject;
+ let originalRouterPath;
+ let mockNewObject;
+ let mockObjectProvider;
+
+ beforeEach((done) => {
+ openmct = createOpenMct();
+ originalRouterPath = openmct.router.path;
+ openmct.router.path = [mockDisplayLayout];
+ openmct.editor.edit();
+
+ openmct.install(new ExampleTagsPlugin());
+ openmct.install(new DisplayLayoutPlugin());
+ const availableTags = openmct.annotation.getAvailableTags();
+ mockDomainObject = {
+ type: 'notebook',
+ name: 'fooRabbitNotebook',
+ location: 'fooNameSpace:topObject',
+ identifier: {
+ key: 'some-object',
+ namespace: 'fooNameSpace'
+ },
+ configuration: {
+ entries: {
+ someSection: {
+ somePage: [
+ {
+ id: 'fooBarEntry',
+ text: 'Foo Bar Text'
+ }
+ ]
+ }
+ }
+ }
+ };
+ mockTopObject = {
+ type: 'root',
+ name: 'Top Folder',
+ composition: [],
+ identifier: {
+ key: 'topObject',
+ namespace: 'fooNameSpace'
+ }
+ };
+ mockAnotherFolderObject = {
+ type: 'folder',
+ name: 'Another Test Folder',
+ composition: [],
+ location: 'fooNameSpace:topObject',
+ identifier: {
+ key: 'someParent',
+ namespace: 'fooNameSpace'
+ }
+ };
+ mockFolderObject = {
+ type: 'folder',
+ name: 'Test Folder',
+ composition: [],
+ location: 'fooNameSpace:someParent',
+ identifier: {
+ key: 'someFolder',
+ namespace: 'fooNameSpace'
+ }
+ };
+ mockDisplayLayout = {
+ type: 'layout',
+ name: 'Bar Layout',
+ composition: [],
+ identifier: {
+ key: 'some-layout',
+ namespace: 'fooNameSpace'
+ },
+ configuration: {
+ items: [],
+ layoutGrid: [10, 10]
+ }
+ };
+ mockAnnotationObject = {
+ type: 'annotation',
+ name: 'Some Notebook Annotation',
+ annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
+ tags: [availableTags[0].id, availableTags[1].id],
+ identifier: {
+ key: 'anAnnotationKey',
+ namespace: 'fooNameSpace'
+ },
+ targets: {
+ 'fooNameSpace:some-object': {
+ entryId: 'fooBarEntry'
+ }
+ }
+ };
+ mockNewObject = {
+ type: 'folder',
+ name: 'New Apple Test Folder',
+ composition: [],
+ location: 'fooNameSpace:topObject',
+ identifier: {
+ key: 'newApple',
+ namespace: 'fooNameSpace'
+ }
+ };
+
+ openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(false);
+ mockObjectProvider = jasmine.createSpyObj("mock object provider", [
+ "create",
+ "update",
+ "get"
+ ]);
+ // eslint-disable-next-line require-await
+ mockObjectProvider.get = async (identifier) => {
+ if (identifier.key === mockDomainObject.identifier.key) {
+ return mockDomainObject;
+ } else if (identifier.key === mockAnnotationObject.identifier.key) {
+ return mockAnnotationObject;
+ } else if (identifier.key === mockDisplayLayout.identifier.key) {
+ return mockDisplayLayout;
+ } else if (identifier.key === mockFolderObject.identifier.key) {
+ return mockFolderObject;
+ } else if (identifier.key === mockAnotherFolderObject.identifier.key) {
+ return mockAnotherFolderObject;
+ } else if (identifier.key === mockTopObject.identifier.key) {
+ return mockTopObject;
+ } else if (identifier.key === mockNewObject.identifier.key) {
+ return mockNewObject;
+ } else {
+ return null;
+ }
+ };
+
+ mockObjectProvider.create.and.returnValue(Promise.resolve(true));
+ mockObjectProvider.update.and.returnValue(Promise.resolve(true));
+
+ openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
+
+ const mockViewProvider = jasmine.createSpyObj("mock view provider", [
+ "key",
+ "view",
+ "canView"
+ ]);
+
+ openmct.objectViews.addProvider(mockViewProvider);
+
+ openmct.on('start', async () => {
+ // use local worker
+ sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
+ openmct.objects.inMemorySearchProvider.worker = null;
+ await openmct.objects.inMemorySearchProvider.index(mockTopObject);
+ await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
+ await openmct.objects.inMemorySearchProvider.index(mockDisplayLayout);
+ await openmct.objects.inMemorySearchProvider.index(mockFolderObject);
+ await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject);
+ parent = document.createElement('div');
+ document.body.appendChild(parent);
+ viewContainer = document.createElement('div');
+ parent.append(viewContainer);
+ grandSearchComponent = new Vue({
+ el: viewContainer,
+ components: {
+ GrandSearch
+ },
+ provide: {
+ openmct
+ },
+ template: '<GrandSearch/>'
+ }).$mount();
+ await Vue.nextTick();
+ done();
+ });
+ openmct.startHeadless();
+ });
+
+ afterEach(() => {
+ openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
+ openmct.router.path = originalRouterPath;
+ grandSearchComponent.$destroy();
+ document.body.removeChild(parent);
+
+ return resetApplicationState(openmct);
+ });
+
+ it("should render an object search result", async () => {
+ await grandSearchComponent.$children[0].searchEverything('foo');
+ await Vue.nextTick();
+ const searchResults = document.querySelectorAll('[aria-label="fooRabbitNotebook notebook result"]');
+ expect(searchResults.length).toBe(1);
+ expect(searchResults[0].innerText).toContain('Rabbit');
+ });
+
+ it("should render an object search result if new object added", async () => {
+ const composition = openmct.composition.get(mockFolderObject);
+ composition.add(mockNewObject);
+ await grandSearchComponent.$children[0].searchEverything('apple');
+ await Vue.nextTick();
+ const searchResults = document.querySelectorAll('[aria-label="New Apple Test Folder folder result"]');
+ expect(searchResults.length).toBe(1);
+ expect(searchResults[0].innerText).toContain('Apple');
+ });
+
+ it("should not use InMemorySearch provider if object provider provides search", async () => {
+ // eslint-disable-next-line require-await
+ mockObjectProvider.search = async (query, abortSignal, searchType) => {
+ if (searchType === openmct.objects.SEARCH_TYPES.OBJECTS) {
+ return mockNewObject;
+ } else {
+ return [];
+ }
+ };
+
+ mockObjectProvider.supportsSearchType = (someType) => {
+ return true;
+ };
+
+ const composition = openmct.composition.get(mockFolderObject);
+ composition.add(mockNewObject);
+ await grandSearchComponent.$children[0].searchEverything('apple');
+ await Vue.nextTick();
+ const searchResults = document.querySelectorAll('[aria-label="New Apple Test Folder folder result"]');
+ // This will be of length 2 (doubles) if we're incorrectly searching with InMemorySearchProvider as well
+ expect(searchResults.length).toBe(1);
+ expect(searchResults[0].innerText).toContain('Apple');
+ });
+
+ it("should render an annotation search result", async () => {
+ await grandSearchComponent.$children[0].searchEverything('S');
+ await Vue.nextTick();
+ const annotationResults = document.querySelectorAll('[aria-label="Search Result"]');
+ expect(annotationResults.length).toBe(2);
+ expect(annotationResults[1].innerText).toContain('Driving');
+ });
+
+ it("should preview object search results in edit mode if object clicked", async () => {
+ await grandSearchComponent.$children[0].searchEverything('Folder');
+ grandSearchComponent._provided.openmct.router.path = [mockDisplayLayout];
+ await Vue.nextTick();
+ const searchResults = document.querySelectorAll('[name="Test Folder"]');
+ expect(searchResults.length).toBe(1);
+ expect(searchResults[0].innerText).toContain('Folder');
+ searchResults[0].click();
+ const previewWindow = document.querySelector('.js-preview-window');
+ expect(previewWindow.innerText).toContain('Snapshot');
+ });
+});
diff --git a/src/ui/layout/search/ObjectSearchResult.vue b/src/ui/layout/search/ObjectSearchResult.vue
new file mode 100644
index 000000000..5d4f6c8ff
--- /dev/null
+++ b/src/ui/layout/search/ObjectSearchResult.vue
@@ -0,0 +1,136 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+<template>
+<div
+ class="c-gsearch-result c-gsearch-result--object"
+ aria-label="Search Result"
+ role="presentation"
+>
+ <div
+ class="c-gsearch-result__type-icon"
+ :class="resultTypeIcon"
+ ></div>
+ <div
+ class="c-gsearch-result__body"
+ role="option"
+ :aria-label="`${resultName} ${resultType} result`"
+ >
+ <div
+ class="c-gsearch-result__title"
+ :name="resultName"
+ draggable="true"
+ @dragstart="dragStart"
+ @click="clickedResult"
+ >
+ {{ resultName }}
+ </div>
+
+ <ObjectPath
+ :read-only="false"
+ :domain-object="result"
+ />
+ </div>
+ <div class="c-gsearch-result__more-options-button">
+ <button class="c-icon-button icon-3-dots"></button>
+ </div>
+</div>
+</template>
+
+<script>
+import ObjectPath from '../../components/ObjectPath.vue';
+import objectPathToUrl from '../../../tools/url';
+import PreviewAction from '../../preview/PreviewAction';
+
+export default {
+ name: 'ObjectSearchResult',
+ components: {
+ ObjectPath
+ },
+ inject: ['openmct'],
+ props: {
+ result: {
+ type: Object,
+ required: true,
+ default() {
+ return {};
+ }
+ }
+ },
+ computed: {
+ resultName() {
+ return this.result.name;
+ },
+ resultTypeIcon() {
+ return this.openmct.types.get(this.result.type).definition.cssClass;
+ },
+ resultType() {
+ return this.result.type;
+ }
+ },
+ mounted() {
+ this.previewAction = new PreviewAction(this.openmct);
+ this.previewAction.on('isVisible', this.togglePreviewState);
+ },
+ destroyed() {
+ this.previewAction.off('isVisible', this.togglePreviewState);
+ },
+ methods: {
+ clickedResult(event) {
+ if (this.openmct.editor.isEditing()) {
+ event.preventDefault();
+ this.preview();
+ } else {
+ const objectPath = this.result.originalPath;
+ let resultUrl = objectPathToUrl(this.openmct, objectPath);
+ // get rid of ROOT if extant
+ if (resultUrl.includes('/ROOT')) {
+ resultUrl = resultUrl.split('/ROOT').join('');
+ }
+
+ this.openmct.router.navigate(resultUrl);
+ }
+ },
+ togglePreviewState(previewState) {
+ this.$emit('preview-changed', previewState);
+ },
+ preview() {
+ const objectPath = this.result.originalPath;
+ if (this.previewAction.appliesTo(objectPath)) {
+ this.previewAction.invoke(objectPath);
+ }
+ },
+ dragStart(event) {
+ const navigatedObject = this.openmct.router.path[0];
+ const objectPath = this.result.originalPath;
+ const serializedPath = JSON.stringify(objectPath);
+ const keyString = this.openmct.objects.makeKeyString(this.result.identifier);
+ if (this.openmct.composition.checkPolicy(navigatedObject, this.result)) {
+ event.dataTransfer.setData("openmct/composable-domain-object", JSON.stringify(this.result));
+ }
+
+ event.dataTransfer.setData("openmct/domain-object-path", serializedPath);
+ event.dataTransfer.setData(`openmct/domain-object/${keyString}`, this.result);
+ }
+ }
+};
+</script>
diff --git a/src/ui/layout/search/SearchResultsDropDown.vue b/src/ui/layout/search/SearchResultsDropDown.vue
new file mode 100644
index 000000000..71171a565
--- /dev/null
+++ b/src/ui/layout/search/SearchResultsDropDown.vue
@@ -0,0 +1,127 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
+<template>
+<div
+ class="c-gsearch__dropdown"
+>
+ <div
+ v-show="resultsShown"
+ class="c-gsearch__results-wrapper"
+ >
+ <div class="c-gsearch__results">
+ <div
+ v-if="objectResults && objectResults.length"
+ ref="objectResults"
+ class="c-gsearch__results-section"
+ role="listbox"
+ >
+ <div class="c-gsearch__results-section-title">Object Results</div>
+ <object-search-result
+ v-for="(objectResult) in objectResults"
+ :key="openmct.objects.makeKeyString(objectResult.identifier)"
+ :result="objectResult"
+ @preview-changed="previewChanged"
+ @click.native="selectedResult"
+ />
+ </div>
+ <div
+ v-if="annotationResults && annotationResults.length"
+ ref="annotationResults"
+ >
+ <div class="c-gsearch__results-section-title">Annotation Results</div>
+ <annotation-search-result
+ v-for="(annotationResult) in annotationResults"
+ :key="openmct.objects.makeKeyString(annotationResult.identifier)"
+ :result="annotationResult"
+ @click.native="selectedResult"
+ />
+ </div>
+ <div
+ v-if="searchLoading"
+ class="c-gsearch__result-pane-msg"
+ >
+ <div class="hint">Searching...</div>
+ <progress-bar :model="{ progressPerc: undefined }" />
+ </div>
+ <div
+ v-if="!searchLoading && (!annotationResults || !annotationResults.length) &&
+ (!objectResults || !objectResults.length)"
+ class="c-gsearch__result-pane-msg"
+ >
+ <div class="hint">No results found</div>
+ </div>
+ </div>
+ </div>
+</div></template>
+
+<script>
+import AnnotationSearchResult from './AnnotationSearchResult.vue';
+import ObjectSearchResult from './ObjectSearchResult.vue';
+import ProgressBar from '@/ui/components/ProgressBar.vue';
+
+export default {
+ name: 'SearchResultsDropDown',
+ components: {
+ AnnotationSearchResult,
+ ObjectSearchResult,
+ ProgressBar
+ },
+ inject: ['openmct'],
+ data() {
+ return {
+ resultsShown: false,
+ searchLoading: false,
+ annotationResults: [],
+ objectResults: [],
+ previewVisible: false
+ };
+ },
+ methods: {
+ selectedResult() {
+ if (!this.previewVisible) {
+ this.resultsShown = false;
+ }
+ },
+ previewChanged(changedPreviewState) {
+ this.previewVisible = changedPreviewState;
+ },
+ showSearchStarted() {
+ this.searchLoading = true;
+ this.resultsShown = true;
+ this.annotationResults = [];
+ this.objectResults = [];
+ },
+ showResults({searchLoading, searchValue, annotationSearchResults, objectSearchResults}) {
+ this.searchLoading = searchLoading;
+ this.annotationResults = annotationSearchResults;
+ this.objectResults = objectSearchResults;
+ if (searchValue?.length) {
+ this.resultsShown = true;
+ } else {
+ this.resultsShown = false;
+ }
+ }
+ },
+ template: 'Dropdown'
+};
+</script>
diff --git a/src/ui/layout/search/search.scss b/src/ui/layout/search/search.scss
new file mode 100644
index 000000000..e4499d83e
--- /dev/null
+++ b/src/ui/layout/search/search.scss
@@ -0,0 +1,144 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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.
+ *****************************************************************************/
+
+/******************************* EXPANDED SEARCH 2022 */
+.c-gsearch {
+ .l-shell__head & {
+ // Search input in the shell head
+ width: 20%;
+
+ .c-search {
+ background: rgba($colorHeadFg, 0.2);
+ box-shadow: none;
+ }
+ }
+
+ &__results-wrapper {
+ @include menuOuter();
+ display: flex;
+ flex-direction: column;
+ padding: $interiorMarginLg;
+ min-width: 500px;
+ max-height: 500px;
+ z-index: 60;
+ }
+
+ &__results,
+ &__results-section {
+ flex: 1 1 auto;
+ }
+
+ &__results {
+ // Holds n __results-sections
+ padding-right: $interiorMargin; // Fend off scrollbar
+ overflow-y: auto;
+
+ > * + * {
+ margin-top: $interiorMarginLg;
+ }
+ }
+
+ &__results-section {
+ > * + * {
+ margin-top: $interiorMarginSm;
+ }
+ }
+
+ &__results-section-title {
+ @include propertiesHeader();
+ }
+
+ &__result-pane-msg {
+ > * + * {
+ margin-top: $interiorMargin;
+ }
+ }
+}
+
+.c-gsearch-result {
+ display: flex;
+ padding: $interiorMargin $interiorMarginSm;
+
+ > * + * {
+ margin-left: $interiorMarginLg;
+ }
+
+ + .c-gsearch-result {
+ border-top: 1px solid $colorInteriorBorder;
+ }
+
+ &__type-icon,
+ &__more-options-button {
+ flex: 0 0 auto;
+ }
+
+ &__type-icon {
+ color: $colorItemTreeIcon;
+ font-size: 2.2em;
+
+ // TEMP: uses object-label component, hide label part
+ .c-object-label__name {
+ display: none;
+ }
+ }
+
+ &__more-options-button {
+ display: none; // TEMP until enabled
+ }
+
+ &__body {
+ flex: 1 1 auto;
+
+ > * + * {
+ margin-top: $interiorMarginSm;
+ }
+
+ .c-location {
+ font-size: 0.9em;
+ opacity: 0.8;
+ }
+ }
+
+ &__tags {
+ display: flex;
+
+ > * + * {
+ margin-left: $interiorMargin;
+ }
+ }
+
+ &__title {
+ border-radius: $basicCr;
+ color: pullForward($colorBodyFg, 30%);
+ cursor: pointer;
+ font-size: 1.15em;
+ padding: 3px $interiorMarginSm;
+
+ &:hover {
+ background-color: $colorItemTreeHoverBg;
+ }
+ }
+
+ .c-tag {
+ font-size: 0.9em;
+ }
+}
diff --git a/src/ui/layout/status-bar/Indicators.vue b/src/ui/layout/status-bar/Indicators.vue
index 7c9a975fc..fd7c3f122 100644
--- a/src/ui/layout/status-bar/Indicators.vue
+++ b/src/ui/layout/status-bar/Indicators.vue
@@ -24,10 +24,19 @@
export default {
inject: ['openmct'],
+ beforeDestroy() {
+ this.openmct.indicators.off('addIndicator', this.addIndicator);
+ },
mounted() {
- this.openmct.indicators.getIndicatorObjectsByPriority().forEach((indicator) => {
+ this.openmct.indicators.getIndicatorObjectsByPriority().forEach(this.addIndicator);
+
+ this.openmct.indicators.on('addIndicator', this.addIndicator);
+ },
+ methods: {
+ addIndicator(indicator) {
this.$el.appendChild(indicator.element);
- });
+ }
}
+
};
</script>
diff --git a/src/ui/layout/tree-item.vue b/src/ui/layout/tree-item.vue
index 7283e3bd3..5c0712f95 100644
--- a/src/ui/layout/tree-item.vue
+++ b/src/ui/layout/tree-item.vue
@@ -1,7 +1,9 @@
<template>
<div
- :style="treeItemStyles"
class="c-tree__item-h"
+ role="treeitem"
+ :style="treeItemStyles"
+ :aria-expanded="(!activeSearch && hasComposition) ? (isOpen || isLoading) ? 'true' : 'false' : undefined"
>
<div
class="c-tree__item"
diff --git a/src/ui/mixins/context-menu-gesture.js b/src/ui/mixins/context-menu-gesture.js
index 404015f38..ad7fa0de3 100644
--- a/src/ui/mixins/context-menu-gesture.js
+++ b/src/ui/mixins/context-menu-gesture.js
@@ -33,6 +33,10 @@ export default {
},
methods: {
showContextMenu(event) {
+ if (this.readOnly) {
+ return;
+ }
+
event.preventDefault();
event.stopPropagation();
diff --git a/src/ui/preview/PreviewAction.js b/src/ui/preview/PreviewAction.js
index 9618ac0a1..f077032da 100644
--- a/src/ui/preview/PreviewAction.js
+++ b/src/ui/preview/PreviewAction.js
@@ -21,9 +21,11 @@
*****************************************************************************/
import Preview from './Preview.vue';
import Vue from 'vue';
+import EventEmitter from 'EventEmitter';
-export default class PreviewAction {
+export default class PreviewAction extends EventEmitter {
constructor(openmct) {
+ super();
/**
* Metadata
*/
@@ -75,10 +77,12 @@ export default class PreviewAction {
onDestroy: () => {
PreviewAction.isVisible = false;
preview.$destroy();
+ this.emit('isVisible', false);
}
});
PreviewAction.isVisible = true;
+ this.emit('isVisible', true);
}
appliesTo(objectPath, view = {}) {
diff --git a/src/ui/router/ApplicationRouter.js b/src/ui/router/ApplicationRouter.js
index 2953449ab..35940058e 100644
--- a/src/ui/router/ApplicationRouter.js
+++ b/src/ui/router/ApplicationRouter.js
@@ -50,6 +50,10 @@ class ApplicationRouter extends EventEmitter {
this.started = false;
this.setHash = _.debounce(this.setHash.bind(this), 300);
+
+ openmct.once('destroy', () => {
+ this.destroy();
+ });
}
// Public Methods
diff --git a/src/ui/router/ApplicationRouterSpec.js b/src/ui/router/ApplicationRouterSpec.js
index d356d1854..4e6ca7ac3 100644
--- a/src/ui/router/ApplicationRouterSpec.js
+++ b/src/ui/router/ApplicationRouterSpec.js
@@ -28,6 +28,9 @@ describe('Application router utility functions', () => {
};
openmct.router.on('change:hash', resolveFunction);
+ // We have a debounce set to 300ms on setHash, so if we don't flush,
+ // the above resolve function sometimes doesn't fire due to a race condition.
+ openmct.router.setHash.flush();
openmct.router.setLocationFromUrl();
});
diff --git a/src/ui/router/Browse.js b/src/ui/router/Browse.js
index 05106815a..1c8f62245 100644
--- a/src/ui/router/Browse.js
+++ b/src/ui/router/Browse.js
@@ -133,9 +133,7 @@ define([
composition.load()
.then(children => {
let lastChild = children[children.length - 1];
- if (!lastChild) {
- console.debug('Unable to navigate to anything. No root objects found.');
- } else {
+ if (lastChild) {
let lastChildId = openmct.objects.makeKeyString(lastChild.identifier);
openmct.router.setPath(`#/browse/${lastChildId}`);
}
diff --git a/src/ui/toolbar/components/toolbar-toggle-button.vue b/src/ui/toolbar/components/toolbar-toggle-button.vue
index e3d37c4c5..27ca3d62f 100644
--- a/src/ui/toolbar/components/toolbar-toggle-button.vue
+++ b/src/ui/toolbar/components/toolbar-toggle-button.vue
@@ -5,9 +5,15 @@
:title="nextValue.title"
:class="[nextValue.icon, {'c-icon-button--mixed': nonSpecific}]"
@click="cycle"
- ></div>
-</div>
-</template>
+ >
+ <div
+ v-if="nextValue.label"
+ class="c-icon-button__label"
+ >
+ {{ nextValue.label }}
+ </div>
+ </div>
+</div></template>
<script>
export default {
diff --git a/src/utils/agent/Agent.js b/src/utils/agent/Agent.js
index 4706c2c94..f88f47eb3 100644
--- a/src/utils/agent/Agent.js
+++ b/src/utils/agent/Agent.js
@@ -89,7 +89,21 @@ export default class Agent {
* @returns {boolean} true in portrait mode
*/
isPortrait() {
- return this.window.innerWidth < this.window.innerHeight;
+ const { screen } = this.window;
+ const hasScreenOrientation = screen && Object.prototype.hasOwnProperty.call(screen, 'orientation');
+ const hasWindowOrientation = Object.prototype.hasOwnProperty.call(this.window, 'orientation');
+
+ if (hasScreenOrientation) {
+ return screen.orientation.type.includes('portrait');
+ } else if (hasWindowOrientation) {
+ // Use window.orientation API if available (e.g. Safari mobile)
+ // which returns [-90, 0, 90, 180] based on device orientation.
+ const { orientation } = this.window;
+
+ return Math.abs(orientation / 90) % 2 === 0;
+ } else {
+ return this.window.innerWidth < this.window.innerHeight;
+ }
}
/**
* Check if the user's device is in a landscape-style
diff --git a/src/utils/agent/AgentSpec.js b/src/utils/agent/AgentSpec.js
index 549772b40..d803fea4a 100644
--- a/src/utils/agent/AgentSpec.js
+++ b/src/utils/agent/AgentSpec.js
@@ -68,7 +68,7 @@ describe("The Agent", function () {
expect(agent.isTablet()).toBeTruthy();
});
- it("detects display orientation", function () {
+ it("detects display orientation by innerHeight and innerWidth", function () {
agent = new Agent(testWindow);
testWindow.innerWidth = 1024;
testWindow.innerHeight = 400;
@@ -80,6 +80,34 @@ describe("The Agent", function () {
expect(agent.isLandscape()).toBeFalsy();
});
+ it("detects display orientation by screen.orientation", function () {
+ agent = new Agent(testWindow);
+ testWindow.screen = {
+ orientation: {
+ type: "landscape-primary"
+ }
+ };
+ expect(agent.isPortrait()).toBeFalsy();
+ expect(agent.isLandscape()).toBeTruthy();
+ testWindow.screen = {
+ orientation: {
+ type: "portrait-primary"
+ }
+ };
+ expect(agent.isPortrait()).toBeTruthy();
+ expect(agent.isLandscape()).toBeFalsy();
+ });
+
+ it("detects display orientation by window.orientation", function () {
+ agent = new Agent(testWindow);
+ testWindow.orientation = 90;
+ expect(agent.isPortrait()).toBeFalsy();
+ expect(agent.isLandscape()).toBeTruthy();
+ testWindow.orientation = 0;
+ expect(agent.isPortrait()).toBeTruthy();
+ expect(agent.isLandscape()).toBeFalsy();
+ });
+
it("detects touch support", function () {
testWindow.ontouchstart = null;
expect(new Agent(testWindow).isTouch()).toBe(true);
diff --git a/src/utils/duration.js b/src/utils/duration.js
index 708d4b786..70b98378b 100644
--- a/src/utils/duration.js
+++ b/src/utils/duration.js
@@ -32,8 +32,16 @@ function normalizeAge(num) {
return isWhole ? hundredtized / 100 : num;
}
+function padLeadingZeros(num, numOfLeadingZeros) {
+ return num.toString().padStart(numOfLeadingZeros, '0');
+}
+
function toDoubleDigits(num) {
- return num >= 10 ? num : `0${num}`;
+ return padLeadingZeros(num, 2);
+}
+
+function toTripleDigits(num) {
+ return padLeadingZeros(num, 3);
}
function addTimeSuffix(value, suffix) {
@@ -46,7 +54,8 @@ export function millisecondsToDHMS(numericDuration) {
addTimeSuffix(Math.floor(normalizeAge(ms / ONE_DAY)), 'd'),
addTimeSuffix(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR)), 'h'),
addTimeSuffix(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE)), 'm'),
- addTimeSuffix(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)), 's')
+ addTimeSuffix(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)), 's'),
+ addTimeSuffix(Math.floor(normalizeAge(ms % ONE_SECOND)), "ms")
].filter(Boolean).join(' ');
return `${ dhms ? '+' : ''} ${dhms}`;
@@ -59,7 +68,8 @@ export function getPreciseDuration(value) {
toDoubleDigits(Math.floor(normalizeAge(ms / ONE_DAY))),
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR))),
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE))),
- toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)))
+ toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND))),
+ toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND)))
].join(":");
}
diff --git a/src/utils/raf.js b/src/utils/raf.js
new file mode 100644
index 000000000..d5c0c48fe
--- /dev/null
+++ b/src/utils/raf.js
@@ -0,0 +1,14 @@
+export default function raf(callback) {
+ let rendering = false;
+
+ return () => {
+ if (!rendering) {
+ rendering = true;
+
+ requestAnimationFrame(() => {
+ callback();
+ rendering = false;
+ });
+ }
+ };
+}
diff --git a/src/utils/rafSpec.js b/src/utils/rafSpec.js
new file mode 100644
index 000000000..0bf5ae9d9
--- /dev/null
+++ b/src/utils/rafSpec.js
@@ -0,0 +1,61 @@
+import raf from "./raf";
+
+describe('The raf utility function', () => {
+ it('Throttles function calls that arrive in quick succession using Request Animation Frame', () => {
+ const unthrottledFunction = jasmine.createSpy('unthrottledFunction');
+ const throttledCallback = jasmine.createSpy('throttledCallback');
+ const throttledFunction = raf(throttledCallback);
+
+ for (let i = 0; i < 10; i++) {
+ unthrottledFunction();
+ throttledFunction();
+ }
+
+ return new Promise((resolve) => {
+ requestAnimationFrame(resolve);
+ }).then(() => {
+ expect(unthrottledFunction).toHaveBeenCalledTimes(10);
+ expect(throttledCallback).toHaveBeenCalledTimes(1);
+ });
+ });
+ it('Only invokes callback once per animation frame', () => {
+ const throttledCallback = jasmine.createSpy('throttledCallback');
+ const throttledFunction = raf(throttledCallback);
+
+ for (let i = 0; i < 10; i++) {
+ throttledFunction();
+ }
+
+ return new Promise(resolve => {
+ requestAnimationFrame(resolve);
+ }).then(() => {
+ return new Promise(resolve => {
+ requestAnimationFrame(resolve);
+ });
+ }).then(() => {
+ expect(throttledCallback).toHaveBeenCalledTimes(1);
+ });
+ });
+ it('Invokes callback again if called in subsequent animation frame', () => {
+ const throttledCallback = jasmine.createSpy('throttledCallback');
+ const throttledFunction = raf(throttledCallback);
+
+ for (let i = 0; i < 10; i++) {
+ throttledFunction();
+ }
+
+ return new Promise(resolve => {
+ requestAnimationFrame(resolve);
+ }).then(() => {
+ for (let i = 0; i < 10; i++) {
+ throttledFunction();
+ }
+
+ return new Promise(resolve => {
+ requestAnimationFrame(resolve);
+ });
+ }).then(() => {
+ expect(throttledCallback).toHaveBeenCalledTimes(2);
+ });
+ });
+});
diff --git a/src/utils/template/templateHelpers.js b/src/utils/template/templateHelpers.js
new file mode 100644
index 000000000..70d381ce7
--- /dev/null
+++ b/src/utils/template/templateHelpers.js
@@ -0,0 +1,14 @@
+export function convertTemplateToHTML(templateString) {
+ const template = document.createElement('template');
+ template.innerHTML = templateString;
+
+ return template.content.cloneNode(true).children;
+}
+
+export function toggleClass(element, className) {
+ if (element.classList.contains(className)) {
+ element.classList.remove(className);
+ } else {
+ element.classList.add(className);
+ }
+}
diff --git a/src/utils/template/templateHelpersSpec.js b/src/utils/template/templateHelpersSpec.js
new file mode 100644
index 000000000..0790085f7
--- /dev/null
+++ b/src/utils/template/templateHelpersSpec.js
@@ -0,0 +1,106 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2022, 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 { toggleClass } from "@/utils/template/templateHelpers";
+
+const CLASS_AS_NON_EMPTY_STRING = 'class-to-toggle';
+const CLASS_AS_EMPTY_STRING = '';
+const CLASS_DEFAULT = CLASS_AS_NON_EMPTY_STRING;
+const CLASS_SECONDARY = 'another-class-to-toggle';
+const CLASS_TERTIARY = 'yet-another-class-to-toggle';
+
+const CLASS_TO_TOGGLE = CLASS_DEFAULT;
+
+describe('toggleClass', () => {
+ describe('type checking', () => {
+ const A_DOM_NODE = document.createElement('div');
+ const NOT_A_DOM_NODE = 'not-a-dom-node';
+ describe('errors', () => {
+ it('throws when "className" is an empty string', () => {
+ expect(() => toggleClass(A_DOM_NODE, CLASS_AS_EMPTY_STRING)).toThrow();
+ });
+ it('throws when "element" is not a DOM node', () => {
+ expect(() => toggleClass(NOT_A_DOM_NODE, CLASS_DEFAULT)).toThrow();
+ });
+ });
+ describe('success', () => {
+ it('does not throw when "className" is not an empty string', () => {
+ expect(() => toggleClass(A_DOM_NODE, CLASS_AS_NON_EMPTY_STRING)).not.toThrow();
+ });
+ it('does not throw when "element" is a DOM node', () => {
+ expect(() => toggleClass(A_DOM_NODE, CLASS_DEFAULT)).not.toThrow();
+ });
+ });
+ });
+ describe('adding a class', () => {
+ it('adds specified class to an element without any classes', () => {
+ // test case
+ const ELEMENT_WITHOUT_CLASS = document.createElement('div');
+ toggleClass(ELEMENT_WITHOUT_CLASS, CLASS_TO_TOGGLE);
+ // expected
+ const ELEMENT_WITHOUT_CLASS_EXPECTED = document.createElement('div');
+ ELEMENT_WITHOUT_CLASS_EXPECTED.classList.add(CLASS_TO_TOGGLE);
+ expect(ELEMENT_WITHOUT_CLASS).toEqual(ELEMENT_WITHOUT_CLASS_EXPECTED);
+ });
+ it('adds specified class to an element that already has another class', () => {
+ // test case
+ const ELEMENT_WITH_SINGLE_CLASS = document.createElement('div');
+ ELEMENT_WITH_SINGLE_CLASS.classList.add(CLASS_SECONDARY);
+ toggleClass(ELEMENT_WITH_SINGLE_CLASS, CLASS_TO_TOGGLE);
+ // expected
+ const ELEMENT_WITH_SINGLE_CLASS_EXPECTED = document.createElement('div');
+ ELEMENT_WITH_SINGLE_CLASS_EXPECTED.classList.add(CLASS_SECONDARY, CLASS_TO_TOGGLE);
+ expect(ELEMENT_WITH_SINGLE_CLASS).toEqual(ELEMENT_WITH_SINGLE_CLASS_EXPECTED);
+ });
+ it('adds specified class to an element that already has more than one other classes', () => {
+ // test case
+ const ELEMENT_WITH_MULTIPLE_CLASSES = document.createElement('div');
+ ELEMENT_WITH_MULTIPLE_CLASSES.classList.add(CLASS_TO_TOGGLE, CLASS_SECONDARY);
+ toggleClass(ELEMENT_WITH_MULTIPLE_CLASSES, CLASS_TO_TOGGLE);
+ // expected
+ const ELEMENT_WITH_MULTIPLE_CLASSES_EXPECTED = document.createElement('div');
+ ELEMENT_WITH_MULTIPLE_CLASSES_EXPECTED.classList.add(CLASS_SECONDARY);
+ expect(ELEMENT_WITH_MULTIPLE_CLASSES).toEqual(ELEMENT_WITH_MULTIPLE_CLASSES_EXPECTED);
+ });
+ });
+ describe('removing a class', () => {
+ it('removes specified class from an element that only has the specified class', () => {
+ // test case
+ const ELEMENT_WITH_ONLY_SPECIFIED_CLASS = document.createElement('div');
+ ELEMENT_WITH_ONLY_SPECIFIED_CLASS.classList.add(CLASS_TO_TOGGLE);
+ toggleClass(ELEMENT_WITH_ONLY_SPECIFIED_CLASS, CLASS_TO_TOGGLE);
+ // expected
+ const ELEMENT_WITH_ONLY_SPECIFIED_CLASS_EXPECTED = document.createElement('div');
+ ELEMENT_WITH_ONLY_SPECIFIED_CLASS_EXPECTED.className = '';
+ expect(ELEMENT_WITH_ONLY_SPECIFIED_CLASS).toEqual(ELEMENT_WITH_ONLY_SPECIFIED_CLASS_EXPECTED);
+ });
+ it('removes specified class from an element that has specified class, and others', () => {
+ // test case
+ const ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS = document.createElement('div');
+ ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS.classList.add(CLASS_TO_TOGGLE, CLASS_SECONDARY, CLASS_TERTIARY);
+ toggleClass(ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS, CLASS_TO_TOGGLE);
+ // expected
+ const ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS_EXPECTED = document.createElement('div');
+ ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS_EXPECTED.classList.add(CLASS_SECONDARY, CLASS_TERTIARY);
+ expect(ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS).toEqual(ELEMENT_WITH_SPECIFIED_CLASS_AND_OTHERS_EXPECTED);
+ });
+ });
+});
diff --git a/src/utils/textHighlight/TextHighlight.vue b/src/utils/textHighlight/TextHighlight.vue
index 18cdc1b35..0c1cbb49c 100644
--- a/src/utils/textHighlight/TextHighlight.vue
+++ b/src/utils/textHighlight/TextHighlight.vue
@@ -37,7 +37,7 @@
<script>
-import uuid from 'uuid';
+import { v4 as uuid } from 'uuid';
export default {
props: {
diff --git a/tsconfig.json b/tsconfig.json
index ee9b1e1e3..96eb34902 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -4,6 +4,7 @@
*/
{
"compilerOptions": {
+ "baseUrl": "./",
"allowJs": true,
"checkJs": false,
"strict": true,
@@ -11,10 +12,14 @@
"noImplicitOverride": true,
"module": "esnext",
"moduleResolution": "node",
-
+ "outDir": "dist",
"paths": {
// matches the alias in webpack config, so that types for those imports are visible.
"@/*": ["src/*"]
}
- }
+ },
+ "exclude": [
+ "node_modules",
+ "dist"
+ ]
}
diff --git a/webpack.common.js b/webpack.common.js
index 5b1de5da4..e48773f56 100644
--- a/webpack.common.js
+++ b/webpack.common.js
@@ -7,12 +7,19 @@ const webpack = require('webpack');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const {VueLoaderPlugin} = require('vue-loader');
-const gitRevision = require('child_process')
- .execSync('git rev-parse HEAD')
- .toString().trim();
-const gitBranch = require('child_process')
- .execSync('git rev-parse --abbrev-ref HEAD')
- .toString().trim();
+let gitRevision = 'error-retrieving-revision';
+let gitBranch = 'error-retrieving-branch';
+
+try {
+ gitRevision = require('child_process')
+ .execSync('git rev-parse HEAD')
+ .toString().trim();
+ gitBranch = require('child_process')
+ .execSync('git rev-parse --abbrev-ref HEAD')
+ .toString().trim();
+} catch (err) {
+ console.warn(err);
+}
/** @type {import('webpack').Configuration} */
const config = {
@@ -23,12 +30,12 @@ const config = {
inMemorySearchWorker: './src/api/objects/InMemorySearchWorker.js',
espressoTheme: './src/plugins/themes/espresso-theme.scss',
snowTheme: './src/plugins/themes/snow-theme.scss',
- maelstromTheme: './src/plugins/themes/maelstrom-theme.scss'
},
output: {
- globalObject: "this",
+ globalObject: 'this',
filename: '[name].js',
- library: '[name]',
+ path: path.resolve(__dirname, 'dist'),
+ library: 'openmct',
libraryTarget: 'umd',
publicPath: '',
hashFunction: 'xxhash64',
@@ -72,6 +79,10 @@ const config = {
transform: function (content) {
return content.toString().replace(/dist\//g, '');
}
+ },
+ {
+ from: 'src/plugins/imagery/layers',
+ to: 'imagery'
}
]
}),
@@ -89,8 +100,13 @@ const config = {
{
loader: 'css-loader'
},
- 'resolve-url-loader',
- 'sass-loader'
+ {
+ loader: 'resolve-url-loader'
+ },
+ {
+ loader: 'sass-loader',
+ options: {sourceMap: true }
+ }
]
},
{
@@ -102,13 +118,6 @@ const config = {
type: 'asset/source'
},
{
- test: /zepto/,
- use: [
- "imports-loader?this=>window",
- "exports-loader?Zepto"
- ]
- },
- {
test: /\.(jpg|jpeg|png|svg)$/,
type: 'asset/resource',
generator: {
diff --git a/webpack.coverage.js b/webpack.coverage.js
index 94766eb6c..9980e14ae 100644
--- a/webpack.coverage.js
+++ b/webpack.coverage.js
@@ -2,12 +2,11 @@
// instrumentation using babel-plugin-istanbul (see babel.coverage.js)
const config = require('./webpack.dev');
-
-const path = require('path');
-
-config.devtool = false;
-
const vueLoaderRule = config.module.rules.find(r => r.use === 'vue-loader');
+// eslint-disable-next-line no-undef
+const CI = process.env.CI === 'true';
+
+config.devtool = CI ? false : undefined;
vueLoaderRule.use = {
loader: 'vue-loader'
@@ -34,7 +33,11 @@ config.module.rules.push({
use: {
loader: 'babel-loader',
options: {
- configFile: path.resolve(process.cwd(), 'babel.coverage.js')
+ retainLines: true,
+ // eslint-disable-next-line no-undef
+ plugins: [['babel-plugin-istanbul', {
+ extension: ['.js', '.vue']
+ }]]
}
}
});
diff --git a/webpack.dev.js b/webpack.dev.js
index 21262682f..e7eeeb06c 100644
--- a/webpack.dev.js
+++ b/webpack.dev.js
@@ -6,6 +6,17 @@ const webpack = require('webpack');
module.exports = merge(common, {
mode: 'development',
+ watchOptions: {
+ // Since we use require.context, webpack is watching the entire directory.
+ // We need to exclude any files we don't want webpack to watch.
+ // See: https://webpack.js.org/configuration/watch/#watchoptions-exclude
+ ignored: [
+ '**/{node_modules,dist,docs,e2e}', // All files in node_modules, dist, docs, e2e,
+ '**/{*.yml,Procfile,webpack*.js,babel*.js,package*.json,tsconfig.json,jsdoc.json}', // Config files
+ '**/*.{sh,md,png,ttf,woff,svg}', // Non source files
+ '**/.*' // dotfiles and dotfolders
+ ]
+ },
resolve: {
alias: {
"vue": path.join(__dirname, "node_modules/vue/dist/vue.js")
diff --git a/webpack.prod.js b/webpack.prod.js
index a64d4ce54..362b4eb50 100644
--- a/webpack.prod.js
+++ b/webpack.prod.js
@@ -16,5 +16,5 @@ module.exports = merge(common, {
__OPENMCT_ROOT_RELATIVE__: '""'
})
],
- devtool: 'source-map'
+ devtool: 'eval-source-map'
});