From caa7bc6faebc204f67aedae3e35fb0d0d3ce27a7 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Thu, 18 May 2023 14:54:46 -0700 Subject: [PATCH 01/45] chore: add `prettier` (2/3): apply formatting, re-enable lint ci step (#6682) * style: apply prettier formatting * fix: re-enable lint ci check --- .circleci/config.yml | 58 +- .eslintrc.js | 322 +- .github/dependabot.yml | 57 +- .github/workflows/e2e-couchdb.yml | 8 +- .github/workflows/e2e-pr.yml | 2 +- .github/workflows/pr-platform.yml | 4 +- .github/workflows/prcop.yml | 2 +- .webpack/webpack.common.js | 293 +- .webpack/webpack.coverage.js | 36 +- .webpack/webpack.dev.js | 102 +- .webpack/webpack.prod.js | 34 +- codecov.yml | 6 +- e2e/.eslintrc.js | 24 +- e2e/appActions.js | 492 +-- e2e/baseFixtures.js | 209 +- e2e/helper/addInitExampleFaultProvider.js | 4 +- .../addInitExampleFaultProviderStatic.js | 6 +- e2e/helper/addInitExampleUser.js | 4 +- e2e/helper/addInitFaultManagementPlugin.js | 4 +- e2e/helper/addInitFileInputObject.js | 123 +- e2e/helper/addInitNotebookWithUrls.js | 4 +- e2e/helper/addInitOperatorStatus.js | 4 +- e2e/helper/addInitRestrictedNotebook.js | 4 +- e2e/helper/addNoneditableObject.js | 48 +- e2e/helper/faultUtils.js | 204 +- e2e/helper/notebookUtils.js | 40 +- e2e/helper/planningUtils.js | 87 +- e2e/helper/useSnowTheme.js | 4 +- e2e/playwright-ci.config.js | 133 +- e2e/playwright-local.config.js | 181 +- e2e/playwright-performance.config.js | 64 +- e2e/playwright-visual.config.js | 83 +- e2e/pluginFixtures.js | 57 +- e2e/test-data/ExampleLayouts.json | 51 +- e2e/test-data/PerformanceDisplayLayout.json | 91 +- e2e/test-data/PerformanceNotebook.json | 97 +- e2e/test-data/VisualTestData_storage.json | 2 +- .../examplePlans/ExamplePlan_Large.json | 2 +- .../examplePlans/ExamplePlan_Small1.json | 82 +- .../examplePlans/ExamplePlan_Small2.json | 66 +- e2e/test-data/recycled_local_storage.json | 2 +- e2e/tests/framework/appActions.e2e.spec.js | 270 +- e2e/tests/framework/baseFixtures.e2e.spec.js | 40 +- .../framework/exampleTemplate.e2e.spec.js | 156 +- .../generateVisualTestData.e2e.spec.js | 40 +- .../framework/pluginFixtures.e2e.spec.js | 26 +- e2e/tests/framework/testData.e2e.spec.js | 15 +- e2e/tests/functional/branding.e2e.spec.js | 58 +- e2e/tests/functional/couchdb.e2e.spec.js | 151 +- .../example/eventGenerator.e2e.spec.js | 35 +- .../sineWaveLimitProvider.e2e.spec.js | 156 +- e2e/tests/functional/forms.e2e.spec.js | 432 ++- e2e/tests/functional/menu.e2e.spec.js | 38 +- .../functional/moveAndLinkObjects.e2e.spec.js | 579 +-- e2e/tests/functional/notification.e2e.spec.js | 158 +- .../planning/ganttChart.e2e.spec.js | 168 +- .../functional/planning/plan.e2e.spec.js | 20 +- .../functional/planning/timestrip.e2e.spec.js | 308 +- .../plugins/clocks/clock.e2e.spec.js | 70 +- .../plugins/clocks/remoteClock.e2e.spec.js | 24 +- .../conditionSet/conditionSet.e2e.spec.js | 545 +-- .../displayLayout/displayLayout.e2e.spec.js | 355 +- .../faultManagement.e2e.spec.js | 315 +- .../flexibleLayout/flexibleLayout.e2e.spec.js | 248 +- .../plugins/gauge/gauge.e2e.spec.js | 180 +- .../imagery/exampleImagery.e2e.spec.js | 1048 ++--- .../exportAsJson.e2e.spec.js | 45 +- .../importAsJson.e2e.spec.js | 102 +- .../functional/plugins/lad/lad.e2e.spec.js | 372 +- .../plugins/notebook/notebook.e2e.spec.js | 711 ++-- .../notebook/notebookSnapshots.e2e.spec.js | 223 +- .../notebook/notebookWithCouchDB.e2e.spec.js | 370 +- .../notebook/restrictedNotebook.e2e.spec.js | 262 +- .../plugins/notebook/tags.e2e.spec.js | 372 +- .../operatorStatus/operatorStatus.e2e.spec.js | 193 +- .../plugins/plot/autoscale.e2e.spec.js | 245 +- .../plugins/plot/logPlot.e2e.spec.js | 341 +- .../plugins/plot/missingPlotObj.e2e.spec.js | 192 +- .../plugins/plot/overlayPlot.e2e.spec.js | 401 +- .../plugins/plot/plotRendering.e2e.spec.js | 88 +- .../plugins/plot/scatterPlot.e2e.spec.js | 134 +- .../plugins/plot/stackedPlot.e2e.spec.js | 341 +- .../plugins/plot/tagging.e2e.spec.js | 433 ++- .../telemetryTable/telemetryTable.e2e.spec.js | 101 +- .../timeConductor/timeConductor.e2e.spec.js | 272 +- .../plugins/timer/timer.e2e.spec.js | 139 +- .../functional/recentObjects.e2e.spec.js | 595 +-- e2e/tests/functional/search.e2e.spec.js | 430 ++- e2e/tests/functional/smoke.e2e.spec.js | 31 +- e2e/tests/functional/tree.e2e.spec.js | 317 +- e2e/tests/performance/imagery.perf.spec.js | 255 +- .../performance/memleak-imagery.perf.spec.js | 122 +- e2e/tests/performance/notebook.perf.spec.js | 219 +- e2e/tests/visual/addInit.visual.spec.js | 31 +- .../visual/components/tree.visual.spec.js | 126 +- .../visual/controlledClock.visual.spec.js | 36 +- e2e/tests/visual/default.visual.spec.js | 206 +- .../visual/faultManagement.visual.spec.js | 75 +- e2e/tests/visual/ladTable.visual.spec.js | 90 +- e2e/tests/visual/notebook.visual.spec.js | 41 +- e2e/tests/visual/notification.visual.spec.js | 57 +- e2e/tests/visual/planning.visual.spec.js | 120 +- e2e/tests/visual/search.visual.spec.js | 91 +- .../eventGenerator/EventMetadataProvider.js | 66 +- .../eventGenerator/EventTelemetryProvider.js | 128 +- example/eventGenerator/plugin.js | 32 +- example/eventGenerator/pluginSpec.js | 87 +- example/eventGenerator/transcript.json | 114 +- example/exampleTags/plugin.js | 18 +- example/exampleTags/tags.json | 32 +- example/exampleUser/ExampleUserProvider.js | 346 +- example/exampleUser/exampleUserCreator.js | 20 +- example/exampleUser/plugin.js | 24 +- example/exampleUser/pluginSpec.js | 37 +- example/faultManagement/exampleFaultSource.js | 62 +- example/faultManagement/pluginSpec.js | 35 +- example/faultManagement/utils.js | 107 +- .../generator/GeneratorMetadataProvider.js | 272 +- example/generator/GeneratorProvider.js | 129 +- example/generator/SinewaveLimitProvider.js | 272 +- .../generator/SinewaveStalenessProvider.js | 210 +- example/generator/StateGeneratorProvider.js | 94 +- example/generator/WorkerInterface.js | 149 +- example/generator/generatorWorker.js | 381 +- example/generator/plugin.js | 279 +- example/imagery/plugin.js | 443 +-- example/simpleVuePlugin/HelloWorld.vue | 12 +- example/simpleVuePlugin/plugin.js | 55 +- index.html | 392 +- karma.conf.js | 172 +- openmct.js | 10 +- src/MCT.js | 722 ++-- src/MCTSpec.js | 179 +- src/api/Branding.js | 8 +- src/api/Editor.js | 108 +- src/api/EditorSpec.js | 82 +- src/api/actions/ActionCollection.js | 277 +- src/api/actions/ActionCollectionSpec.js | 341 +- src/api/actions/ActionsAPI.js | 217 +- src/api/actions/ActionsAPISpec.js | 226 +- src/api/annotation/AnnotationAPI.js | 677 ++-- src/api/annotation/AnnotationAPISpec.js | 467 +-- src/api/api.js | 104 +- src/api/composition/CompositionAPI.js | 189 +- src/api/composition/CompositionAPISpec.js | 541 ++- src/api/composition/CompositionCollection.js | 540 ++- src/api/composition/CompositionProvider.js | 423 +- .../composition/DefaultCompositionProvider.js | 358 +- src/api/faultmanagement/FaultManagementAPI.js | 114 +- .../faultmanagement/FaultManagementAPISpec.js | 186 +- src/api/forms/FormController.js | 142 +- src/api/forms/FormsAPI.js | 338 +- src/api/forms/FormsAPISpec.js | 236 +- src/api/forms/components/FormProperties.vue | 253 +- src/api/forms/components/FormRow.vue | 218 +- .../components/controls/AutoCompleteField.vue | 457 ++- .../components/controls/CheckBoxField.vue | 37 +- .../controls/ClockDisplayFormatField.vue | 65 +- .../forms/components/controls/Composite.vue | 50 +- .../components/controls/CompositeItem.vue | 78 +- .../forms/components/controls/Datetime.vue | 258 +- .../forms/components/controls/FileInput.vue | 229 +- src/api/forms/components/controls/Locator.vue | 42 +- .../forms/components/controls/NumberField.vue | 73 +- .../forms/components/controls/SelectField.vue | 64 +- .../components/controls/TextAreaField.vue | 71 +- .../forms/components/controls/TextField.vue | 62 +- .../components/controls/ToggleSwitchField.vue | 51 +- src/api/forms/toggle-check-box-mixin.js | 28 +- src/api/indicators/IndicatorAPI.js | 91 +- src/api/indicators/IndicatorAPISpec.js | 106 +- src/api/indicators/SimpleIndicator.js | 136 +- .../indicators/res/indicator-template.html | 2 +- src/api/menu/MenuAPI.js | 139 +- src/api/menu/MenuAPISpec.js | 308 +- src/api/menu/components/Menu.vue | 89 +- src/api/menu/components/SuperMenu.vue | 139 +- src/api/menu/menu.js | 274 +- src/api/notifications/NotificationAPI.js | 565 ++- src/api/notifications/NotificationAPISpec.js | 262 +- src/api/objects/ConflictError.js | 3 +- src/api/objects/InMemorySearchProvider.js | 1032 ++--- src/api/objects/InMemorySearchWorker.js | 330 +- src/api/objects/InterceptorRegistry.js | 96 +- src/api/objects/MutableDomainObject.js | 186 +- src/api/objects/ObjectAPI.js | 1425 +++---- src/api/objects/ObjectAPISearchSpec.js | 416 +- src/api/objects/ObjectAPISpec.js | 1073 +++--- src/api/objects/RootObjectProvider.js | 56 +- src/api/objects/RootRegistry.js | 54 +- src/api/objects/Transaction.js | 96 +- src/api/objects/TransactionSpec.js | 179 +- src/api/objects/object-utils.js | 304 +- .../objects/test/RootObjectProviderSpec.js | 48 +- src/api/objects/test/RootRegistrySpec.js | 163 +- src/api/objects/test/object-utilsSpec.js | 281 +- src/api/overlays/Dialog.js | 49 +- src/api/overlays/Overlay.js | 112 +- src/api/overlays/OverlayAPI.js | 202 +- src/api/overlays/ProgressDialog.js | 81 +- .../overlays/components/DialogComponent.vue | 44 +- .../overlays/components/OverlayComponent.vue | 142 +- .../components/ProgressDialogComponent.vue | 24 +- .../overlays/components/dialog-component.scss | 134 +- .../components/overlay-component.scss | 285 +- src/api/priority/PriorityAPI.js | 6 +- src/api/status/StatusAPI.js | 60 +- src/api/status/StatusAPISpec.js | 153 +- src/api/telemetry/DefaultMetadataProvider.js | 194 +- src/api/telemetry/TelemetryAPI.js | 1235 +++--- src/api/telemetry/TelemetryAPISpec.js | 1178 +++--- src/api/telemetry/TelemetryCollection.js | 816 ++-- src/api/telemetry/TelemetryCollectionSpec.js | 125 +- src/api/telemetry/TelemetryMetadataManager.js | 224 +- .../telemetry/TelemetryRequestInterceptor.js | 81 +- src/api/telemetry/TelemetryValueFormatter.js | 242 +- src/api/telemetry/constants.js | 6 +- src/api/time/GlobalTimeContext.js | 138 +- src/api/time/IndependentTimeContext.js | 454 ++- src/api/time/TimeAPI.js | 341 +- src/api/time/TimeAPISpec.js | 451 ++- src/api/time/TimeContext.js | 642 ++-- src/api/time/independentTimeAPISpec.js | 457 +-- src/api/types/Type.js | 162 +- src/api/types/TypeRegistry.js | 114 +- src/api/types/TypeRegistrySpec.js | 46 +- src/api/user/StatusAPI.js | 420 +- src/api/user/StatusUserProvider.js | 114 +- src/api/user/User.js | 24 +- src/api/user/UserAPI.js | 215 +- src/api/user/UserAPISpec.js | 148 +- src/api/user/UserProvider.js | 26 +- src/api/user/UserStatusAPISpec.js | 136 +- src/exporters/CSVExporter.js | 17 +- src/exporters/ImageExporter.js | 277 +- src/exporters/ImageExporterSpec.js | 56 +- src/exporters/JSONExporter.js | 14 +- src/plugins/CouchDBSearchFolder/plugin.js | 69 +- src/plugins/CouchDBSearchFolder/pluginSpec.js | 126 +- src/plugins/DeviceClassifier/plugin.js | 16 +- .../DeviceClassifier/src/DeviceClassifier.js | 52 +- .../src/DeviceClassifierSpec.js | 122 +- .../DeviceClassifier/src/DeviceMatchers.js | 42 +- .../src/DeviceMatchersSpec.js | 74 +- src/plugins/ISOTimeFormat/ISOTimeFormat.js | 40 +- src/plugins/ISOTimeFormat/plugin.js | 6 +- src/plugins/ISOTimeFormat/pluginSpec.js | 51 +- .../LADTable/LADTableCompositionPolicy.js | 16 +- src/plugins/LADTable/LADTableConfiguration.js | 58 +- .../LADTableConfigurationViewProvider.js | 72 +- .../LADTable/LADTableSetViewProvider.js | 34 +- src/plugins/LADTable/LADTableView.js | 79 +- src/plugins/LADTable/LADTableViewProvider.js | 46 +- src/plugins/LADTable/LadTableSetView.js | 78 +- src/plugins/LADTable/ViewActions.js | 57 +- src/plugins/LADTable/components/LADRow.vue | 459 ++- src/plugins/LADTable/components/LADTable.vue | 423 +- .../components/LADTableConfiguration.vue | 413 +- .../LADTable/components/LadTableSet.vue | 476 +-- src/plugins/LADTable/plugin.js | 56 +- src/plugins/LADTable/pluginSpec.js | 709 ++-- .../URLIndicatorPlugin/URLIndicator.js | 167 +- .../URLIndicatorPlugin/URLIndicatorPlugin.js | 22 +- .../URLIndicatorPlugin/URLIndicatorSpec.js | 222 +- .../URLTimeSettingsSynchronizer.js | 369 +- .../URLTimeSettingsSynchronizer/plugin.js | 8 +- .../URLTimeSettingsSynchronizer/pluginSpec.js | 181 +- .../autoflow/AutoflowTabularConstants.js | 20 +- .../autoflow/AutoflowTabularController.js | 182 +- src/plugins/autoflow/AutoflowTabularPlugin.js | 43 +- .../autoflow/AutoflowTabularPluginSpec.js | 635 ++- .../autoflow/AutoflowTabularRowController.js | 120 +- src/plugins/autoflow/AutoflowTabularView.js | 178 +- src/plugins/autoflow/VueView.js | 16 +- src/plugins/autoflow/autoflow-tabular.html | 43 +- src/plugins/autoflow/dom-observer.js | 64 +- .../charts/bar/BarGraphCompositionPolicy.js | 42 +- src/plugins/charts/bar/BarGraphPlot.vue | 541 ++- src/plugins/charts/bar/BarGraphView.vue | 764 ++-- .../charts/bar/BarGraphViewProvider.js | 94 +- .../BarGraphInspectorViewProvider.js | 77 +- .../charts/bar/inspector/BarGraphOptions.vue | 711 ++-- .../charts/bar/inspector/SeriesOptions.vue | 248 +- src/plugins/charts/bar/plugin.js | 45 +- src/plugins/charts/bar/pluginSpec.js | 1141 +++--- .../scatter/ScatterPlotCompositionPolicy.js | 48 +- .../charts/scatter/ScatterPlotForm.vue | 208 +- .../charts/scatter/ScatterPlotView.vue | 637 +-- .../charts/scatter/ScatterPlotViewProvider.js | 88 +- .../scatter/ScatterPlotWithUnderlay.vue | 761 ++-- .../charts/scatter/inspector/PlotOptions.vue | 62 +- .../scatter/inspector/PlotOptionsBrowse.vue | 237 +- .../scatter/inspector/PlotOptionsEdit.vue | 449 +-- .../ScatterPlotInspectorViewProvider.js | 77 +- src/plugins/charts/scatter/plugin.js | 181 +- src/plugins/charts/scatter/pluginSpec.js | 766 ++-- src/plugins/clearData/ClearDataAction.js | 84 +- .../components/globalClearIndicator.vue | 16 +- src/plugins/clearData/plugin.js | 60 +- src/plugins/clearData/pluginSpec.js | 395 +- src/plugins/clock/ClockViewProvider.js | 60 +- src/plugins/clock/components/Clock.vue | 118 +- .../clock/components/ClockIndicator.vue | 54 +- src/plugins/clock/plugin.js | 227 +- src/plugins/clock/pluginSpec.js | 371 +- src/plugins/condition/Condition.js | 550 +-- src/plugins/condition/ConditionManager.js | 867 +++-- src/plugins/condition/ConditionManagerSpec.js | 367 +- .../ConditionSetCompositionPolicy.js | 16 +- .../ConditionSetCompositionPolicySpec.js | 113 +- .../condition/ConditionSetMetadataProvider.js | 97 +- .../ConditionSetTelemetryProvider.js | 120 +- .../condition/ConditionSetViewProvider.js | 118 +- src/plugins/condition/ConditionSpec.js | 288 +- src/plugins/condition/StyleRuleManager.js | 333 +- .../condition/components/Condition.vue | 679 ++-- .../components/ConditionCollection.vue | 510 +-- .../components/ConditionDescription.vue | 62 +- .../condition/components/ConditionError.vue | 94 +- .../condition/components/ConditionSet.vue | 169 +- .../condition/components/Criterion.vue | 567 ++- src/plugins/condition/components/TestData.vue | 413 +- .../condition/components/conditionals.scss | 462 +-- .../components/inspector/StyleEditor.vue | 425 +- .../components/inspector/StylesView.vue | 1728 +++++---- .../inspector/conditional-styles.scss | 210 +- .../criterion/AllTelemetryCriterion.js | 489 +-- .../condition/criterion/TelemetryCriterion.js | 541 +-- .../criterion/TelemetryCriterionSpec.js | 212 +- src/plugins/condition/plugin.js | 70 +- src/plugins/condition/pluginSpec.js | 1849 +++++---- src/plugins/condition/utils/constants.js | 50 +- src/plugins/condition/utils/evaluator.js | 60 +- src/plugins/condition/utils/evaluatorSpec.js | 338 +- src/plugins/condition/utils/operations.js | 580 +-- src/plugins/condition/utils/operationsSpec.js | 179 +- src/plugins/condition/utils/styleUtils.js | 267 +- src/plugins/condition/utils/time.js | 78 +- src/plugins/condition/utils/timeSpec.js | 69 +- .../ConditionWidgetViewProvider.js | 68 +- .../components/ConditionWidget.vue | 177 +- .../components/condition-widget.scss | 65 +- src/plugins/conditionWidget/plugin.js | 69 +- src/plugins/conditionWidget/pluginSpec.js | 337 +- src/plugins/defaultRootName/plugin.js | 8 +- src/plugins/defaultRootName/pluginSpec.js | 83 +- .../AlphanumericFormatViewProvider.js | 124 +- .../displayLayout/CustomStringFormatter.js | 56 +- .../CustomStringFormatterSpec.js | 118 +- .../displayLayout/DisplayLayoutToolbar.js | 1646 ++++---- .../displayLayout/DisplayLayoutType.js | 107 +- .../displayLayout/DrawingObjectTypes.js | 60 +- src/plugins/displayLayout/LayoutDrag.js | 176 +- .../actions/CopyToClipboardAction.js | 62 +- .../components/AlphanumericFormat.vue | 125 +- .../displayLayout/components/BoxView.vue | 160 +- .../components/DisplayLayout.vue | 1685 ++++---- .../components/DisplayLayoutGrid.vue | 63 +- .../displayLayout/components/EditMarquee.vue | 303 +- .../displayLayout/components/EllipseView.vue | 160 +- .../displayLayout/components/ImageView.vue | 176 +- .../displayLayout/components/LayoutFrame.vue | 190 +- .../displayLayout/components/LineView.vue | 633 ++- .../components/SubobjectView.vue | 241 +- .../components/TelemetryView.vue | 654 ++-- .../displayLayout/components/TextView.vue | 181 +- .../components/box-and-line-views.scss | 95 +- .../components/display-layout.scss | 153 +- .../components/edit-marquee.scss | 106 +- .../displayLayout/components/image-view.scss | 14 +- .../components/layout-frame.scss | 220 +- .../components/telemetry-view.scss | 70 +- .../displayLayout/components/text-view.scss | 16 +- .../mixins/objectStyles-mixin.js | 125 +- src/plugins/displayLayout/plugin.js | 179 +- src/plugins/displayLayout/pluginSpec.js | 785 ++-- src/plugins/duplicate/DuplicateAction.js | 257 +- src/plugins/duplicate/DuplicateTask.js | 421 +- src/plugins/duplicate/plugin.js | 8 +- src/plugins/duplicate/pluginSpec.js | 227 +- .../exportAsJSONAction/ExportAsJSONAction.js | 592 +-- .../ExportAsJSONActionSpec.js | 677 ++-- src/plugins/exportAsJSONAction/plugin.js | 6 +- .../FaultManagementInspector.vue | 169 +- .../FaultManagementInspectorViewProvider.js | 74 +- .../FaultManagementListHeader.vue | 126 +- .../FaultManagementListItem.vue | 337 +- .../FaultManagementListView.vue | 513 +-- .../FaultManagementObjectProvider.js | 60 +- .../faultManagement/FaultManagementPlugin.js | 25 +- .../faultManagement/FaultManagementSearch.vue | 99 +- .../FaultManagementToolbar.vue | 120 +- .../faultManagement/FaultManagementView.vue | 84 +- .../FaultManagementViewProvider.js | 72 +- src/plugins/faultManagement/constants.js | 163 +- .../faultManagement/fault-manager.scss | 367 +- src/plugins/faultManagement/pluginSpec.js | 136 +- .../filters/FiltersInspectorViewProvider.js | 93 +- .../filters/components/FilterField.vue | 229 +- .../filters/components/FilterObject.vue | 289 +- .../filters/components/FiltersView.vue | 453 +-- .../filters/components/GlobalFilters.vue | 198 +- .../filters/components/filters-view.scss | 24 +- .../filters/components/global-filters.scss | 48 +- src/plugins/filters/plugin.js | 16 +- .../flexibleLayout/components/container.vue | 350 +- .../flexibleLayout/components/dropHint.vue | 100 +- .../components/flexible-layout.scss | 543 +-- .../components/flexibleLayout.vue | 618 ++- .../flexibleLayout/components/frame.vue | 252 +- .../components/resizeHandle.vue | 140 +- .../flexibleLayoutViewProvider.js | 117 +- src/plugins/flexibleLayout/plugin.js | 52 +- src/plugins/flexibleLayout/pluginSpec.js | 343 +- src/plugins/flexibleLayout/toolbarProvider.js | 387 +- src/plugins/flexibleLayout/utils/container.js | 10 +- src/plugins/flexibleLayout/utils/frame.js | 12 +- src/plugins/folderView/FolderGridView.js | 84 +- src/plugins/folderView/FolderListView.js | 89 +- .../folderView/components/GridItem.vue | 82 +- .../folderView/components/GridView.vue | 19 +- .../folderView/components/ListItem.vue | 69 +- .../folderView/components/ListView.vue | 211 +- .../components/composition-loader.js | 95 +- .../folderView/components/grid-view.scss | 336 +- .../folderView/components/list-item.scss | 40 +- .../folderView/components/status-listener.js | 60 +- src/plugins/folderView/plugin.js | 39 +- src/plugins/folderView/pluginSpec.js | 227 +- src/plugins/formActions/CreateAction.js | 294 +- src/plugins/formActions/CreateActionSpec.js | 164 +- src/plugins/formActions/CreateWizard.js | 216 +- .../formActions/EditPropertiesAction.js | 125 +- src/plugins/formActions/PropertiesAction.js | 20 +- src/plugins/formActions/plugin.js | 6 +- src/plugins/formActions/pluginSpec.js | 350 +- src/plugins/gauge/GaugePlugin.js | 338 +- src/plugins/gauge/GaugePluginSpec.js | 1432 +++---- src/plugins/gauge/GaugeViewProvider.js | 74 +- src/plugins/gauge/components/Gauge.vue | 1371 ++++--- .../gauge/components/GaugeFormController.vue | 251 +- src/plugins/gauge/gauge-limit-util.js | 39 +- src/plugins/gauge/gauge.scss | 14 +- .../goToOriginalAction/goToOriginalAction.js | 66 +- src/plugins/goToOriginalAction/plugin.js | 6 +- src/plugins/goToOriginalAction/pluginSpec.js | 324 +- src/plugins/hyperlink/HyperlinkLayout.vue | 34 +- src/plugins/hyperlink/HyperlinkProvider.js | 57 +- src/plugins/hyperlink/plugin.js | 123 +- src/plugins/hyperlink/pluginSpec.js | 167 +- .../imagery/ImageryTimestripViewProvider.js | 91 +- src/plugins/imagery/ImageryView.js | 134 +- src/plugins/imagery/ImageryViewProvider.js | 43 +- .../imagery/components/Compass/Compass.vue | 135 +- .../imagery/components/Compass/CompassHUD.vue | 229 +- .../components/Compass/CompassRose.vue | 655 ++-- .../imagery/components/Compass/compass.scss | 295 +- .../imagery/components/Compass/pluginSpec.js | 106 +- .../imagery/components/Compass/utils.js | 36 +- .../imagery/components/FilterSettings.vue | 122 +- .../imagery/components/ImageControls.vue | 440 ++- .../imagery/components/ImageThumbnail.vue | 209 +- .../imagery/components/ImageryTimeView.vue | 718 ++-- .../imagery/components/ImageryView.vue | 2356 ++++++------ .../components/ImageryViewMenuSwitcher.vue | 99 +- .../imagery/components/LayerSettings.vue | 85 +- .../RelatedTelemetry/RelatedTelemetry.js | 285 +- .../imagery/components/ZoomSettings.vue | 136 +- .../imagery/components/imagery-view.scss | 865 +++-- src/plugins/imagery/lib/eventHelpers.js | 127 +- src/plugins/imagery/mixins/imageryData.js | 320 +- src/plugins/imagery/plugin.js | 9 +- src/plugins/imagery/pluginSpec.js | 1295 ++++--- .../ImportFromJSONAction.js | 494 +-- .../ImportFromJSONActionSpec.js | 163 +- src/plugins/importFromJSONAction/plugin.js | 6 +- .../annotations/AnnotationsInspectorView.vue | 342 +- .../annotations/AnnotationsViewProvider.js | 74 +- .../annotations/tags/TagEditor.vue | 360 +- .../annotations/tags/TagSelection.vue | 251 +- .../inspectorViews/annotations/tags/tags.scss | 103 +- .../inspectorViews/elements/ElementItem.vue | 143 +- .../elements/ElementItemGroup.vue | 112 +- .../inspectorViews/elements/ElementsPool.vue | 264 +- .../elements/ElementsViewProvider.js | 78 +- .../elements/PlotElementsPool.vue | 570 ++- .../elements/PlotElementsViewProvider.js | 74 +- .../inspectorViews/elements/elements.scss | 126 +- src/plugins/inspectorViews/plugin.js | 14 +- .../inspectorViews/properties/DetailText.vue | 18 +- .../inspectorViews/properties/Location.vue | 152 +- .../inspectorViews/properties/Properties.vue | 398 +- .../properties/PropertiesViewProvider.js | 62 +- .../inspectorViews/properties/location.scss | 68 +- .../inspectorViews/styles/FontStyleEditor.vue | 178 +- .../styles/SavedStyleSelector.vue | 274 +- .../styles/SavedStylesInspectorView.vue | 76 +- .../inspectorViews/styles/SavedStylesView.vue | 166 +- .../styles/StylesInspectorView.vue | 65 +- .../styles/StylesInspectorViewProvider.js | 98 +- .../inspectorViews/styles/StylesManager.js | 202 +- .../inspectorViews/styles/constants.js | 208 +- .../interceptors/missingObjectInterceptor.js | 34 +- src/plugins/interceptors/plugin.js | 8 +- src/plugins/interceptors/pluginSpec.js | 89 +- src/plugins/latestDataClock/LADClock.js | 32 +- src/plugins/latestDataClock/plugin.js | 14 +- src/plugins/licenses/Licenses.vue | 48 +- src/plugins/licenses/plugin.js | 22 +- .../licenses/third-party-licenses.json | 1 - src/plugins/linkAction/LinkAction.js | 236 +- src/plugins/linkAction/plugin.js | 8 +- src/plugins/linkAction/pluginSpec.js | 170 +- .../LocalStorageObjectProvider.js | 150 +- src/plugins/localStorage/plugin.js | 6 +- src/plugins/localStorage/pluginSpec.js | 112 +- .../localTimeSystem/LocalTimeFormat.js | 85 +- .../localTimeSystem/LocalTimeSystem.js | 36 +- src/plugins/localTimeSystem/plugin.js | 18 +- src/plugins/localTimeSystem/pluginSpec.js | 149 +- src/plugins/move/MoveAction.js | 344 +- src/plugins/move/plugin.js | 8 +- src/plugins/move/pluginSpec.js | 235 +- .../myItems/createMyItemsIdentifier.js | 8 +- src/plugins/myItems/myItemsInterceptor.js | 43 +- src/plugins/myItems/plugin.js | 26 +- src/plugins/myItems/pluginSpec.js | 165 +- .../newFolderAction/newFolderAction.js | 40 +- src/plugins/newFolderAction/plugin.js | 6 +- src/plugins/newFolderAction/pluginSpec.js | 161 +- src/plugins/notebook/NotebookType.js | 113 +- src/plugins/notebook/NotebookViewProvider.js | 88 +- .../notebook/actions/CopyToNotebookAction.js | 84 +- .../actions/ExportNotebookAsTextAction.js | 297 +- src/plugins/notebook/components/MenuItems.vue | 24 +- src/plugins/notebook/components/Notebook.vue | 1818 ++++----- .../notebook/components/NotebookEmbed.vue | 721 ++-- .../notebook/components/NotebookEntry.vue | 853 ++-- .../components/NotebookMenuSwitcher.vue | 205 +- .../components/NotebookSnapshotContainer.vue | 217 +- .../components/NotebookSnapshotIndicator.vue | 145 +- .../notebook/components/PageCollection.vue | 245 +- .../notebook/components/PageComponent.vue | 263 +- src/plugins/notebook/components/PopupMenu.vue | 162 +- .../notebook/components/SearchResults.vue | 94 +- .../notebook/components/SectionCollection.vue | 207 +- .../notebook/components/SectionComponent.vue | 243 +- src/plugins/notebook/components/Sidebar.vue | 465 ++- src/plugins/notebook/components/sidebar.scss | 164 +- .../components/snapshot-template.html | 80 +- .../monkeyPatchObjectAPIForNotebooks.js | 189 +- src/plugins/notebook/notebook-constants.js | 8 +- src/plugins/notebook/plugin.js | 174 +- src/plugins/notebook/pluginSpec.js | 708 ++-- src/plugins/notebook/snapshot-container.js | 131 +- src/plugins/notebook/snapshot.js | 213 +- .../notebook/utils/notebook-entries.js | 354 +- .../notebook/utils/notebook-entriesSpec.js | 308 +- src/plugins/notebook/utils/notebook-image.js | 111 +- .../notebook/utils/notebook-migration.js | 76 +- .../notebook/utils/notebook-snapshot-menu.js | 49 +- .../notebook/utils/notebook-storage.js | 136 +- .../notebook/utils/notebook-storageSpec.js | 248 +- .../notebook/utils/painterroInstance.js | 158 +- src/plugins/notebook/utils/removeDialog.js | 66 +- .../components/NotificationIndicator.vue | 111 +- .../components/NotificationMessage.vue | 171 +- .../components/NotificationsList.vue | 125 +- src/plugins/notificationIndicator/plugin.js | 34 +- .../notificationIndicator/pluginSpec.js | 67 +- src/plugins/objectMigration/Migrations.js | 479 +-- src/plugins/objectMigration/plugin.js | 47 +- .../openInNewTabAction/openInNewTabAction.js | 26 +- src/plugins/openInNewTabAction/plugin.js | 6 +- src/plugins/openInNewTabAction/pluginSpec.js | 91 +- .../operatorStatus/AbstractStatusIndicator.js | 144 +- .../operatorStatus/operator-status.scss | 218 +- .../operatorStatus/OperatorStatus.vue | 280 +- .../operatorStatus/OperatorStatusIndicator.js | 62 +- src/plugins/operatorStatus/plugin.js | 35 +- .../pollQuestion/PollQuestion.vue | 491 ++- .../pollQuestion/PollQuestionIndicator.js | 62 +- src/plugins/performanceIndicator/plugin.js | 66 +- .../performanceIndicator/pluginSpec.js | 86 +- .../persistence/couch/CouchChangesFeed.js | 223 +- .../persistence/couch/CouchDocument.js | 24 +- .../persistence/couch/CouchObjectProvider.js | 1282 ++++--- .../persistence/couch/CouchObjectQueue.js | 43 +- .../persistence/couch/CouchSearchProvider.js | 167 +- .../persistence/couch/CouchStatusIndicator.js | 64 +- .../persistence/couch/couchdb-compose.yaml | 6 +- src/plugins/persistence/couch/plugin.js | 30 +- src/plugins/persistence/couch/pluginSpec.js | 761 ++-- .../plan/GanttChartCompositionPolicy.js | 17 +- src/plugins/plan/PlanViewConfiguration.js | 144 +- src/plugins/plan/PlanViewProvider.js | 90 +- .../plan/components/ActivityTimeline.vue | 276 +- src/plugins/plan/components/Plan.vue | 1028 ++--- .../ActivityInspectorViewProvider.js | 79 +- .../GanttChartInspectorViewProvider.js | 74 +- .../inspector/components/ActivityProperty.vue | 35 +- .../components/PlanActivitiesView.vue | 336 +- .../inspector/components/PlanActivityView.vue | 90 +- .../components/PlanViewConfiguration.vue | 202 +- src/plugins/plan/plan.scss | 45 +- src/plugins/plan/plugin.js | 95 +- src/plugins/plan/pluginSpec.js | 515 +-- src/plugins/plan/util.js | 108 +- src/plugins/plot/LinearScale.js | 68 +- src/plugins/plot/MctPlot.vue | 3415 +++++++++-------- src/plugins/plot/MctTicks.vue | 490 ++- src/plugins/plot/Plot.vue | 405 +- src/plugins/plot/PlotViewProvider.js | 132 +- src/plugins/plot/actions/ViewActions.js | 47 +- src/plugins/plot/actions/utils.js | 2 +- src/plugins/plot/axis/XAxis.vue | 228 +- src/plugins/plot/axis/YAxis.vue | 466 +-- src/plugins/plot/chart/LimitLabel.vue | 78 +- src/plugins/plot/chart/LimitLine.vue | 58 +- .../plot/chart/MCTChartAlarmLineSet.js | 175 +- .../plot/chart/MCTChartAlarmPointSet.js | 68 +- src/plugins/plot/chart/MCTChartLineLinear.js | 9 +- .../plot/chart/MCTChartLineStepAfter.js | 81 +- src/plugins/plot/chart/MCTChartPointSet.js | 9 +- .../plot/chart/MCTChartSeriesElement.js | 229 +- src/plugins/plot/chart/MctChart.vue | 1755 ++++----- src/plugins/plot/chart/limitUtil.js | 52 +- src/plugins/plot/configuration/Collection.js | 151 +- src/plugins/plot/configuration/ConfigStore.js | 44 +- src/plugins/plot/configuration/LegendModel.js | 62 +- src/plugins/plot/configuration/Model.js | 188 +- .../configuration/PlotConfigurationModel.js | 278 +- src/plugins/plot/configuration/PlotSeries.js | 850 ++-- .../plot/configuration/SeriesCollection.js | 277 +- src/plugins/plot/configuration/XAxisModel.js | 158 +- src/plugins/plot/configuration/YAxisModel.js | 625 +-- src/plugins/plot/draw/Draw2D.js | 145 +- src/plugins/plot/draw/DrawLoader.js | 116 +- src/plugins/plot/draw/DrawWebGL.js | 238 +- src/plugins/plot/draw/MarkerShapes.js | 118 +- src/plugins/plot/inspector/PlotOptions.vue | 62 +- .../plot/inspector/PlotOptionsBrowse.vue | 513 ++- .../plot/inspector/PlotOptionsEdit.vue | 387 +- .../plot/inspector/PlotOptionsItem.vue | 320 +- .../inspector/PlotsInspectorViewProvider.js | 99 +- .../StackedPlotsInspectorViewProvider.js | 95 +- .../plot/inspector/forms/LegendForm.vue | 412 +- .../plot/inspector/forms/SeriesForm.vue | 694 ++-- .../plot/inspector/forms/YAxisForm.vue | 634 ++- src/plugins/plot/inspector/forms/formUtil.js | 18 +- src/plugins/plot/legend/PlotLegend.vue | 392 +- .../plot/legend/PlotLegendItemCollapsed.vue | 282 +- .../plot/legend/PlotLegendItemExpanded.vue | 324 +- src/plugins/plot/lib/eventHelpers.js | 129 +- src/plugins/plot/mathUtils.js | 14 +- .../OverlayPlotCompositionPolicy.js | 48 +- .../overlayPlot/OverlayPlotViewProvider.js | 102 +- src/plugins/plot/overlayPlot/pluginSpec.js | 941 ++--- src/plugins/plot/plugin.js | 97 +- src/plugins/plot/pluginSpec.js | 1743 +++++---- src/plugins/plot/stackedPlot/StackedPlot.vue | 565 +-- .../StackedPlotCompositionPolicy.js | 53 +- .../plot/stackedPlot/StackedPlotItem.vue | 683 ++-- .../stackedPlot/StackedPlotViewProvider.js | 102 +- .../stackedPlot/mixins/objectStyles-mixin.js | 245 +- src/plugins/plot/stackedPlot/pluginSpec.js | 1481 +++---- .../stackedPlotConfigurationInterceptor.js | 24 +- src/plugins/plot/tickUtils.js | 208 +- src/plugins/plugins.js | 402 +- src/plugins/remoteClock/RemoteClock.js | 241 +- src/plugins/remoteClock/RemoteClockSpec.js | 240 +- src/plugins/remoteClock/plugin.js | 8 +- src/plugins/remoteClock/requestInterceptor.js | 30 +- src/plugins/remove/RemoveAction.js | 237 +- src/plugins/remove/plugin.js | 8 +- src/plugins/remove/pluginSpec.js | 193 +- .../staticRootPlugin/StaticModelProvider.js | 282 +- .../StaticModelProviderSpec.js | 485 ++- src/plugins/staticRootPlugin/plugin.js | 60 +- .../static-provider-test-empty-namespace.json | 105 +- .../static-provider-test-foo-namespace.json | 121 +- .../summaryWidget/SummaryWidgetViewPolicy.js | 31 +- .../SummaryWidgetsCompositionPolicy.js | 32 +- src/plugins/summaryWidget/plugin.js | 194 +- .../summaryWidget/res/conditionTemplate.html | 18 +- .../res/input/paletteTemplate.html | 16 +- .../res/input/selectTemplate.html | 5 +- .../summaryWidget/res/ruleImageTemplate.html | 2 +- .../summaryWidget/res/ruleTemplate.html | 133 +- .../res/testDataItemTemplate.html | 34 +- .../summaryWidget/res/testDataTemplate.html | 29 +- .../summaryWidget/res/widgetTemplate.html | 70 +- src/plugins/summaryWidget/src/Condition.js | 437 +-- .../summaryWidget/src/ConditionEvaluator.js | 907 ++--- .../summaryWidget/src/ConditionManager.js | 730 ++-- src/plugins/summaryWidget/src/Rule.js | 975 ++--- .../summaryWidget/src/SummaryWidget.js | 794 ++-- src/plugins/summaryWidget/src/TestDataItem.js | 355 +- .../summaryWidget/src/TestDataManager.js | 357 +- src/plugins/summaryWidget/src/WidgetDnD.js | 291 +- src/plugins/summaryWidget/src/eventHelpers.js | 127 +- .../summaryWidget/src/input/ColorPalette.js | 176 +- .../summaryWidget/src/input/IconPalette.js | 144 +- .../summaryWidget/src/input/KeySelect.js | 170 +- .../summaryWidget/src/input/ObjectSelect.js | 161 +- .../src/input/OperationSelect.js | 214 +- .../summaryWidget/src/input/Palette.js | 325 +- src/plugins/summaryWidget/src/input/Select.js | 264 +- .../src/telemetry/EvaluatorPool.js | 67 +- .../src/telemetry/EvaluatorPoolSpec.js | 146 +- .../src/telemetry/SummaryWidgetCondition.js | 99 +- .../telemetry/SummaryWidgetConditionSpec.js | 215 +- .../src/telemetry/SummaryWidgetEvaluator.js | 478 ++- .../SummaryWidgetMetadataProvider.js | 170 +- .../src/telemetry/SummaryWidgetRule.js | 91 +- .../src/telemetry/SummaryWidgetRuleSpec.js | 264 +- .../SummaryWidgetTelemetryProvider.js | 66 +- .../SummaryWidgetTelemetryProviderSpec.js | 868 ++--- .../summaryWidget/src/telemetry/operations.js | 386 +- .../src/views/SummaryWidgetView.js | 179 +- .../src/views/SummaryWidgetViewProvider.js | 75 +- .../src/views/summary-widget.html | 6 +- .../test/ConditionEvaluatorSpec.js | 695 ++-- .../test/ConditionManagerSpec.js | 807 ++-- .../summaryWidget/test/ConditionSpec.js | 346 +- src/plugins/summaryWidget/test/RuleSpec.js | 549 +-- .../summaryWidget/test/SummaryWidgetSpec.js | 325 +- .../test/SummaryWidgetViewPolicySpec.js | 76 +- .../summaryWidget/test/TestDataItemSpec.js | 314 +- .../summaryWidget/test/TestDataManagerSpec.js | 458 +-- .../test/input/ColorPaletteSpec.js | 40 +- .../test/input/IconPaletteSpec.js | 40 +- .../summaryWidget/test/input/KeySelectSpec.js | 226 +- .../test/input/ObjectSelectSpec.js | 209 +- .../test/input/OperationSelectSpec.js | 255 +- .../summaryWidget/test/input/PaletteSpec.js | 80 +- .../summaryWidget/test/input/SelectSpec.js | 109 +- src/plugins/tabs/components/tabs.scss | 100 +- src/plugins/tabs/components/tabs.vue | 723 ++-- src/plugins/tabs/plugin.js | 72 +- src/plugins/tabs/pluginSpec.js | 351 +- src/plugins/tabs/tabs.js | 100 +- src/plugins/telemetryMean/plugin.js | 154 +- .../src/MeanTelemetryProvider.js | 197 +- .../src/MeanTelemetryProviderSpec.js | 1189 +++--- .../telemetryMean/src/MockTelemetryApi.js | 142 +- .../telemetryMean/src/TelemetryAverager.js | 155 +- .../TableConfigurationViewProvider.js | 106 +- src/plugins/telemetryTable/TelemetryTable.js | 794 ++-- .../telemetryTable/TelemetryTableColumn.js | 82 +- .../TelemetryTableConfiguration.js | 278 +- .../TelemetryTableNameColumn.js | 34 +- .../telemetryTable/TelemetryTableRow.js | 164 +- .../telemetryTable/TelemetryTableType.js | 27 +- .../TelemetryTableUnitColumn.js | 66 +- .../telemetryTable/TelemetryTableView.js | 115 +- .../TelemetryTableViewProvider.js | 51 +- src/plugins/telemetryTable/ViewActions.js | 152 +- .../collections/TableRowCollection.js | 560 ++- .../telemetryTable/components/sizing-row.vue | 94 +- .../telemetryTable/components/table-cell.vue | 97 +- .../components/table-column-header.vue | 264 +- .../components/table-configuration.vue | 258 +- .../components/table-footer-indicator.scss | 46 +- .../components/table-footer-indicator.vue | 281 +- .../telemetryTable/components/table-row.scss | 12 +- .../telemetryTable/components/table-row.vue | 348 +- .../telemetryTable/components/table.scss | 391 +- .../telemetryTable/components/table.vue | 2054 +++++----- src/plugins/telemetryTable/plugin.js | 30 +- src/plugins/telemetryTable/pluginSpec.js | 834 ++-- src/plugins/themes/espresso-theme.scss | 38 +- src/plugins/themes/espresso.js | 6 +- src/plugins/themes/installTheme.js | 22 +- src/plugins/themes/snow-theme.scss | 38 +- src/plugins/themes/snow.js | 6 +- src/plugins/timeConductor/Conductor.vue | 349 +- src/plugins/timeConductor/ConductorAxis.vue | 512 ++- .../timeConductor/ConductorHistory.vue | 547 +-- .../timeConductor/ConductorInputsFixed.vue | 545 ++- .../timeConductor/ConductorInputsRealtime.vue | 574 ++- src/plugins/timeConductor/ConductorMode.vue | 272 +- .../timeConductor/ConductorModeIcon.vue | 4 +- .../timeConductor/ConductorTimeSystem.vue | 189 +- src/plugins/timeConductor/DatePicker.vue | 441 +-- src/plugins/timeConductor/conductor-axis.scss | 108 +- .../timeConductor/conductor-mode-icon.scss | 239 +- src/plugins/timeConductor/conductor-mode.scss | 20 +- src/plugins/timeConductor/conductor.scss | 548 +-- src/plugins/timeConductor/date-picker.scss | 144 +- .../independent/IndependentTimeConductor.vue | 437 +-- .../timeConductor/independent/Mode.vue | 374 +- src/plugins/timeConductor/plugin.js | 145 +- src/plugins/timeConductor/pluginSpec.js | 245 +- src/plugins/timeConductor/timePopup.vue | 306 +- .../timeConductor/utcMultiTimeFormat.js | 101 +- .../timeline/TimelineCompositionPolicy.js | 80 +- src/plugins/timeline/TimelineObjectView.vue | 200 +- src/plugins/timeline/TimelineViewLayout.vue | 315 +- src/plugins/timeline/TimelineViewProvider.js | 71 +- src/plugins/timeline/plugin.js | 42 +- src/plugins/timeline/pluginSpec.js | 662 ++-- src/plugins/timeline/timeline.scss | 8 +- src/plugins/timeline/timelineInterceptor.js | 28 +- src/plugins/timelist/Timelist.vue | 910 ++--- .../timelist/TimelistCompositionPolicy.js | 18 +- src/plugins/timelist/TimelistViewProvider.js | 72 +- src/plugins/timelist/constants.js | 40 +- .../timelist/inspector/EventProperties.vue | 198 +- src/plugins/timelist/inspector/Filtering.vue | 151 +- .../TimeListInspectorViewProvider.js | 77 +- .../inspector/TimelistPropertiesView.vue | 195 +- src/plugins/timelist/plugin.js | 62 +- src/plugins/timelist/pluginSpec.js | 664 ++-- src/plugins/timelist/timelist.scss | 1 - src/plugins/timer/TimerViewProvider.js | 72 +- src/plugins/timer/actions/PauseTimerAction.js | 60 +- .../timer/actions/RestartTimerAction.js | 62 +- src/plugins/timer/actions/StartTimerAction.js | 96 +- src/plugins/timer/actions/StopTimerAction.js | 62 +- src/plugins/timer/components/Timer.vue | 447 ++- src/plugins/timer/plugin.js | 163 +- src/plugins/timer/pluginSpec.js | 626 +-- .../components/UserIndicator.vue | 41 +- src/plugins/userIndicator/plugin.js | 51 +- src/plugins/userIndicator/pluginSpec.js | 125 +- src/plugins/utcTimeSystem/DurationFormat.js | 31 +- src/plugins/utcTimeSystem/LocalClock.js | 51 +- src/plugins/utcTimeSystem/UTCTimeFormat.js | 101 +- src/plugins/utcTimeSystem/UTCTimeSystem.js | 33 +- src/plugins/utcTimeSystem/plugin.js | 14 +- src/plugins/utcTimeSystem/pluginSpec.js | 295 +- .../viewDatumAction/ViewDatumAction.js | 84 +- .../components/MetadataList.vue | 23 +- .../components/metadata-list.scss | 46 +- src/plugins/viewDatumAction/plugin.js | 6 +- src/plugins/viewDatumAction/pluginSpec.js | 118 +- src/plugins/viewLargeAction/plugin.js | 6 +- .../viewLargeAction/viewLargeAction.js | 119 +- src/plugins/webPage/WebPageViewProvider.js | 62 +- src/plugins/webPage/components/WebPage.vue | 26 +- src/plugins/webPage/plugin.js | 37 +- src/plugins/webPage/pluginSpec.js | 130 +- src/selection/Selection.js | 379 +- src/styles/_about.scss | 170 +- src/styles/_animations.scss | 170 +- src/styles/_constants-espresso.scss | 109 +- src/styles/_constants-maelstrom.scss | 127 +- src/styles/_constants-mobile.scss | 88 +- src/styles/_constants-snow.scss | 99 +- src/styles/_constants.scss | 2 +- src/styles/_controls.scss | 1690 ++++---- src/styles/_forms.scss | 692 ++-- src/styles/_global.scss | 434 ++- src/styles/_glyphs.scss | 924 +++-- src/styles/_legacy-messages.scss | 167 +- src/styles/_legacy-plots.scss | 1345 +++---- src/styles/_legacy.scss | 1828 ++++----- src/styles/_limits.scss | 384 +- src/styles/_mixins.scss | 1153 +++--- src/styles/_status.scss | 299 +- src/styles/_table.scss | 358 +- src/styles/fonts/Open MCT Symbols 12px.json | 26 +- src/styles/fonts/Open MCT Symbols 16px.json | 1311 ++----- src/styles/notebook.scss | 1275 +++--- src/styles/plotly.scss | 56 +- src/styles/vendor/normalize-min.scss | 241 +- src/styles/vue-styles.scss | 122 +- src/tools/url.js | 58 +- src/tools/urlSpec.js | 137 +- src/ui/color/Color.js | 59 +- src/ui/color/ColorHelper.js | 68 +- src/ui/color/ColorPalette.js | 56 +- src/ui/color/ColorSwatch.vue | 217 +- src/ui/components/List/ListHeader.vue | 79 +- src/ui/components/List/ListItem.vue | 75 +- src/ui/components/List/ListView.vue | 243 +- src/ui/components/List/list-view.scss | 54 +- src/ui/components/ObjectFrame.vue | 372 +- src/ui/components/ObjectLabel.vue | 189 +- src/ui/components/ObjectPath.vue | 176 +- src/ui/components/ObjectView.vue | 908 ++--- src/ui/components/ProgressBar.vue | 39 +- src/ui/components/TimeSystemAxis.vue | 271 +- src/ui/components/ToggleSwitch.vue | 68 +- src/ui/components/components.js | 6 +- src/ui/components/componentsSpec.js | 35 +- src/ui/components/contextMenuDropDown.vue | 18 +- src/ui/components/object-frame.scss | 291 +- src/ui/components/object-label.scss | 146 +- src/ui/components/progress-bar.scss | 40 +- src/ui/components/search.scss | 130 +- src/ui/components/search.vue | 105 +- src/ui/components/swim-lane/SwimLane.vue | 187 +- src/ui/components/swim-lane/swimlane.scss | 64 +- src/ui/components/timesystem-axis.scss | 76 +- src/ui/components/toggle-switch.scss | 109 +- src/ui/components/viewControl.vue | 50 +- src/ui/inspector/Inspector.vue | 77 +- src/ui/inspector/InspectorDetailsSpec.js | 262 +- src/ui/inspector/InspectorStylesSpec.js | 351 +- src/ui/inspector/InspectorStylesSpecMocks.js | 314 +- src/ui/inspector/InspectorTabs.vue | 153 +- src/ui/inspector/InspectorViews.vue | 105 +- src/ui/inspector/ObjectName.vue | 238 +- src/ui/inspector/inspector.scss | 451 +-- src/ui/layout/AboutDialog.vue | 94 +- src/ui/layout/AppLogo.vue | 50 +- src/ui/layout/BrowseBar.vue | 672 ++-- src/ui/layout/CreateButton.vue | 130 +- src/ui/layout/Layout.vue | 526 ++- src/ui/layout/LayoutSpec.js | 234 +- src/ui/layout/RecentObjectsList.vue | 439 +-- src/ui/layout/RecentObjectsListItem.vue | 185 +- src/ui/layout/ViewSwitcher.vue | 62 +- src/ui/layout/app-logo.scss | 10 +- src/ui/layout/create-button.scss | 34 +- src/ui/layout/layout.scss | 868 ++--- src/ui/layout/mct-tree.scss | 482 +-- src/ui/layout/mct-tree.vue | 2039 +++++----- src/ui/layout/multipane.vue | 26 +- src/ui/layout/pane.scss | 554 +-- src/ui/layout/pane.vue | 386 +- src/ui/layout/recent-objects.scss | 94 +- .../layout/search/AnnotationSearchResult.vue | 309 +- src/ui/layout/search/GrandSearch.vue | 307 +- src/ui/layout/search/GrandSearchSpec.js | 511 ++- src/ui/layout/search/ObjectSearchResult.vue | 180 +- .../layout/search/SearchResultsDropDown.vue | 184 +- src/ui/layout/search/search.scss | 192 +- src/ui/layout/status-bar/Indicators.vue | 27 +- .../layout/status-bar/NotificationBanner.vue | 280 +- src/ui/layout/status-bar/indicators.scss | 210 +- .../status-bar/notification-banner.scss | 116 +- src/ui/layout/tree-item.vue | 387 +- src/ui/mixins/context-menu-gesture.js | 122 +- src/ui/mixins/object-link.js | 46 +- src/ui/mixins/staleness-mixin.js | 81 +- src/ui/mixins/toggle-mixin.js | 50 +- src/ui/preview/Preview.vue | 375 +- src/ui/preview/PreviewAction.js | 136 +- src/ui/preview/ViewHistoricalDataAction.js | 29 +- src/ui/preview/plugin.js | 8 +- src/ui/preview/preview-header.vue | 279 +- src/ui/preview/preview.scss | 45 +- src/ui/registries/InspectorViewRegistry.js | 107 +- src/ui/registries/ToolbarRegistry.js | 179 +- src/ui/registries/ViewRegistry.js | 454 +-- src/ui/router/ApplicationRouter.js | 707 ++-- src/ui/router/ApplicationRouterSpec.js | 128 +- src/ui/router/Browse.js | 293 +- src/ui/toolbar/Toolbar.vue | 538 +-- src/ui/toolbar/components/toolbar-button.vue | 120 +- .../toolbar/components/toolbar-checkbox.scss | 70 +- .../toolbar/components/toolbar-checkbox.vue | 60 +- .../components/toolbar-color-picker.vue | 280 +- src/ui/toolbar/components/toolbar-input.vue | 91 +- src/ui/toolbar/components/toolbar-menu.vue | 77 +- .../components/toolbar-select-menu.vue | 104 +- .../toolbar/components/toolbar-separator.vue | 12 +- .../components/toolbar-toggle-button.vue | 74 +- src/utils/agent/Agent.js | 210 +- src/utils/agent/AgentSpec.js | 177 +- src/utils/clipboard.js | 16 +- src/utils/clock/DefaultClock.js | 97 +- src/utils/clock/Ticker.js | 106 +- src/utils/duration.js | 51 +- src/utils/raf.js | 20 +- src/utils/rafSpec.js | 108 +- src/utils/staleness.js | 92 +- src/utils/template/templateHelpers.js | 16 +- src/utils/template/templateHelpersSpec.js | 151 +- src/utils/testing.js | 459 +-- src/utils/testing/mockLocalStorage.js | 48 +- src/utils/textHighlight/TextHighlight.vue | 70 +- tsconfig.json | 50 +- 976 files changed, 115922 insertions(+), 114693 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index dfaef1c002..16110bae2f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,12 +13,12 @@ executors: docker_layer_caching: true parameters: BUST_CACHE: - description: "Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!" + description: 'Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!' default: false type: boolean commands: build_and_install: - description: "All steps used to build and install. Will use cache if found" + description: 'All steps used to build and install. Will use cache if found' parameters: node-version: type: string @@ -30,19 +30,19 @@ commands: node-version: << parameters.node-version >> - run: npm install --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" + 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: node-version: type: string steps: - when: condition: - equal: [false, << pipeline.parameters.BUST_CACHE >> ] + equal: [false, << pipeline.parameters.BUST_CACHE >>] steps: - restore_cache: key: deps--{{ arch }}--{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }} save_cache_cmd: - description: "Custom command for saving cache." + description: 'Custom command for saving cache.' parameters: node-version: type: string @@ -53,7 +53,7 @@ commands: - ~/.npm - node_modules generate_and_store_version_and_filesystem_artifacts: - description: "Track important packages and files" + description: 'Track important packages and files' steps: - run: | [[ $EUID -ne 0 ]] && (sudo mkdir -p /tmp/artifacts && sudo chmod 777 /tmp/artifacts) || (mkdir -p /tmp/artifacts && chmod 777 /tmp/artifacts) @@ -64,13 +64,13 @@ commands: - store_artifacts: path: /tmp/artifacts/ 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: + 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:<>:publish + steps: + - run: npm run cov:e2e:report || true + - run: npm run cov:e2e:<>:publish orbs: node: circleci/node@5.1.0 browser-tools: circleci/browser-tools@1.3.0 @@ -115,7 +115,7 @@ jobs: path: coverage - when: condition: - equal: [ 42, 42 ] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 + equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 steps: - generate_and_store_version_and_filesystem_artifacts e2e-test: @@ -131,16 +131,16 @@ jobs: node-version: <> - when: #Only install chrome-beta when running the 'full' suite to save $$$ condition: - equal: [ "full", <> ] + equal: ['full', <>] steps: - run: npx playwright install chrome-beta - run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL} - when: condition: - equal: [ 42, 42 ] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 + equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 steps: - - generate_e2e_code_cov_report: - suite: <> + - generate_e2e_code_cov_report: + suite: <> - store_test_results: path: test-results/results.xml - store_artifacts: @@ -151,7 +151,7 @@ jobs: path: html-test-results - when: condition: - equal: [ 42, 42 ] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 + equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 steps: - generate_and_store_version_and_filesystem_artifacts e2e-couchdb: @@ -168,14 +168,14 @@ jobs: docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach sleep 3 bash src/plugins/persistence/couch/setup-couchdb.sh - - run: sh src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh #Replace LocalStorage Plugin with CouchDB + - run: sh src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh #Replace LocalStorage Plugin with CouchDB - run: npm run test:e2e:couchdb - when: condition: - equal: [ 42, 42 ] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 + equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 steps: - - generate_e2e_code_cov_report: - suite: full #add to full suite + - generate_e2e_code_cov_report: + suite: full #add to full suite - store_test_results: path: test-results/results.xml - store_artifacts: @@ -186,7 +186,7 @@ jobs: path: html-test-results - when: condition: - equal: [ 42, 42 ] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 + equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 steps: - generate_and_store_version_and_filesystem_artifacts perf-test: @@ -206,7 +206,7 @@ jobs: path: html-test-results - when: condition: - equal: [ 42, 42 ] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 + equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 steps: - generate_and_store_version_and_filesystem_artifacts visual-test: @@ -226,15 +226,15 @@ jobs: path: html-test-results - when: condition: - equal: [ 42, 42 ] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 + equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 steps: - 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 + - lint: + name: node16-lint + node-version: lts/gallium - unit-test: name: node18-chrome node-version: lts/hydrogen @@ -246,7 +246,7 @@ workflows: node-version: lts/hydrogen - visual-test: node-version: lts/hydrogen - + the-nightly: #These jobs do not run on PRs, but against master at night jobs: - unit-test: @@ -269,7 +269,7 @@ workflows: node-version: lts/hydrogen triggers: - schedule: - cron: "0 0 * * *" + cron: '0 0 * * *' filters: branches: only: diff --git a/.eslintrc.js b/.eslintrc.js index e17a17ba3a..6ac5dc01cb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,166 +1,166 @@ -const LEGACY_FILES = ["example/**"]; +const LEGACY_FILES = ['example/**']; module.exports = { - "env": { - "browser": true, - "es6": true, - "jasmine": true, - "amd": true - }, - "globals": { - "_": "readonly" - }, - "plugins": ["prettier"], - "extends": [ - "eslint:recommended", - "plugin:compat/recommended", - "plugin:vue/recommended", - "plugin:you-dont-need-lodash-underscore/compatible", - "plugin:prettier/recommended" + env: { + browser: true, + es6: true, + jasmine: true, + amd: true + }, + globals: { + _: 'readonly' + }, + plugins: ['prettier'], + extends: [ + 'eslint:recommended', + 'plugin:compat/recommended', + 'plugin:vue/recommended', + 'plugin:you-dont-need-lodash-underscore/compatible', + 'plugin:prettier/recommended' + ], + parser: 'vue-eslint-parser', + parserOptions: { + parser: '@babel/eslint-parser', + requireConfigFile: false, + allowImportExportEverywhere: true, + ecmaVersion: 2015, + ecmaFeatures: { + impliedStrict: true + } + }, + rules: { + 'prettier/prettier': 'error', + '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', + 'guard-for-in': 'error', + 'no-extend-native': 'error', + 'no-inner-declarations': 'off', + 'no-use-before-define': ['error', 'nofunc'], + 'no-caller': 'error', + 'no-irregular-whitespace': 'error', + 'no-new': 'error', + 'no-shadow': 'error', + 'no-undef': 'error', + 'no-unused-vars': [ + 'error', + { + vars: 'all', + args: 'none' + } ], - "parser": "vue-eslint-parser", - "parserOptions": { - "parser": "@babel/eslint-parser", - "requireConfigFile": false, - "allowImportExportEverywhere": true, - "ecmaVersion": 2015, - "ecmaFeatures": { - "impliedStrict": true - } - }, - "rules": { - "prettier/prettier": "error", - "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", - "guard-for-in": "error", - "no-extend-native": "error", - "no-inner-declarations": "off", - "no-use-before-define": ["error", "nofunc"], - "no-caller": "error", - "no-irregular-whitespace": "error", - "no-new": "error", - "no-shadow": "error", - "no-undef": "error", - "no-unused-vars": [ - "error", - { - "vars": "all", - "args": "none" - } - ], - "no-console": "off", - "new-cap": [ - "error", - { - "capIsNew": false, - "properties": false - } - ], - "dot-notation": "error", + 'no-console': 'off', + 'new-cap': [ + 'error', + { + capIsNew: false, + properties: false + } + ], + 'dot-notation': 'error', - // https://eslint.org/docs/rules/no-case-declarations - "no-case-declarations": "error", - // https://eslint.org/docs/rules/max-classes-per-file - "max-classes-per-file": ["error", 1], - // https://eslint.org/docs/rules/no-eq-null - "no-eq-null": "error", - // https://eslint.org/docs/rules/no-eval - "no-eval": "error", - // https://eslint.org/docs/rules/no-implicit-globals - "no-implicit-globals": "error", - // https://eslint.org/docs/rules/no-implied-eval - "no-implied-eval": "error", - // https://eslint.org/docs/rules/no-lone-blocks - "no-lone-blocks": "error", - // https://eslint.org/docs/rules/no-loop-func - "no-loop-func": "error", - // https://eslint.org/docs/rules/no-new-func - "no-new-func": "error", - // https://eslint.org/docs/rules/no-new-wrappers - "no-new-wrappers": "error", - // https://eslint.org/docs/rules/no-octal-escape - "no-octal-escape": "error", - // https://eslint.org/docs/rules/no-proto - "no-proto": "error", - // https://eslint.org/docs/rules/no-return-await - "no-return-await": "error", - // https://eslint.org/docs/rules/no-script-url - "no-script-url": "error", - // https://eslint.org/docs/rules/no-self-compare - "no-self-compare": "error", - // https://eslint.org/docs/rules/no-sequences - "no-sequences": "error", - // https://eslint.org/docs/rules/no-unmodified-loop-condition - "no-unmodified-loop-condition": "error", - // https://eslint.org/docs/rules/no-useless-call - "no-useless-call": "error", - // https://eslint.org/docs/rules/no-nested-ternary - "no-nested-ternary": "error", - // https://eslint.org/docs/rules/no-useless-computed-key - "no-useless-computed-key": "error", - // https://eslint.org/docs/rules/no-var - "no-var": "error", - // https://eslint.org/docs/rules/one-var - "one-var": ["error", "never"], - // https://eslint.org/docs/rules/default-case-last - "default-case-last": "error", - // https://eslint.org/docs/rules/default-param-last - "default-param-last": "error", - // https://eslint.org/docs/rules/grouped-accessor-pairs - "grouped-accessor-pairs": "error", - // https://eslint.org/docs/rules/no-constructor-return - "no-constructor-return": "error", - // https://eslint.org/docs/rules/array-callback-return - "array-callback-return": "error", - // https://eslint.org/docs/rules/no-invalid-this - "no-invalid-this": "error", // Believe this one actually surfaces some bugs - // https://eslint.org/docs/rules/func-style - "func-style": ["error", "declaration"], - // https://eslint.org/docs/rules/no-unused-expressions - "no-unused-expressions": "error", - // https://eslint.org/docs/rules/no-useless-concat - "no-useless-concat": "error", - // https://eslint.org/docs/rules/radix - "radix": "error", - // https://eslint.org/docs/rules/require-await - "require-await": "error", - // https://eslint.org/docs/rules/no-alert - "no-alert": "error", - // https://eslint.org/docs/rules/no-useless-constructor - "no-useless-constructor": "error", - // https://eslint.org/docs/rules/no-duplicate-imports - "no-duplicate-imports": "error", + // https://eslint.org/docs/rules/no-case-declarations + 'no-case-declarations': 'error', + // https://eslint.org/docs/rules/max-classes-per-file + 'max-classes-per-file': ['error', 1], + // https://eslint.org/docs/rules/no-eq-null + 'no-eq-null': 'error', + // https://eslint.org/docs/rules/no-eval + 'no-eval': 'error', + // https://eslint.org/docs/rules/no-implicit-globals + 'no-implicit-globals': 'error', + // https://eslint.org/docs/rules/no-implied-eval + 'no-implied-eval': 'error', + // https://eslint.org/docs/rules/no-lone-blocks + 'no-lone-blocks': 'error', + // https://eslint.org/docs/rules/no-loop-func + 'no-loop-func': 'error', + // https://eslint.org/docs/rules/no-new-func + 'no-new-func': 'error', + // https://eslint.org/docs/rules/no-new-wrappers + 'no-new-wrappers': 'error', + // https://eslint.org/docs/rules/no-octal-escape + 'no-octal-escape': 'error', + // https://eslint.org/docs/rules/no-proto + 'no-proto': 'error', + // https://eslint.org/docs/rules/no-return-await + 'no-return-await': 'error', + // https://eslint.org/docs/rules/no-script-url + 'no-script-url': 'error', + // https://eslint.org/docs/rules/no-self-compare + 'no-self-compare': 'error', + // https://eslint.org/docs/rules/no-sequences + 'no-sequences': 'error', + // https://eslint.org/docs/rules/no-unmodified-loop-condition + 'no-unmodified-loop-condition': 'error', + // https://eslint.org/docs/rules/no-useless-call + 'no-useless-call': 'error', + // https://eslint.org/docs/rules/no-nested-ternary + 'no-nested-ternary': 'error', + // https://eslint.org/docs/rules/no-useless-computed-key + 'no-useless-computed-key': 'error', + // https://eslint.org/docs/rules/no-var + 'no-var': 'error', + // https://eslint.org/docs/rules/one-var + 'one-var': ['error', 'never'], + // https://eslint.org/docs/rules/default-case-last + 'default-case-last': 'error', + // https://eslint.org/docs/rules/default-param-last + 'default-param-last': 'error', + // https://eslint.org/docs/rules/grouped-accessor-pairs + 'grouped-accessor-pairs': 'error', + // https://eslint.org/docs/rules/no-constructor-return + 'no-constructor-return': 'error', + // https://eslint.org/docs/rules/array-callback-return + 'array-callback-return': 'error', + // https://eslint.org/docs/rules/no-invalid-this + 'no-invalid-this': 'error', // Believe this one actually surfaces some bugs + // https://eslint.org/docs/rules/func-style + 'func-style': ['error', 'declaration'], + // https://eslint.org/docs/rules/no-unused-expressions + 'no-unused-expressions': 'error', + // https://eslint.org/docs/rules/no-useless-concat + 'no-useless-concat': 'error', + // https://eslint.org/docs/rules/radix + radix: 'error', + // https://eslint.org/docs/rules/require-await + 'require-await': 'error', + // https://eslint.org/docs/rules/no-alert + 'no-alert': 'error', + // https://eslint.org/docs/rules/no-useless-constructor + 'no-useless-constructor': 'error', + // https://eslint.org/docs/rules/no-duplicate-imports + 'no-duplicate-imports': 'error', - // https://eslint.org/docs/rules/no-implicit-coercion - "no-implicit-coercion": "error", - //https://eslint.org/docs/rules/no-unneeded-ternary - "no-unneeded-ternary": "error", - "vue/first-attribute-linebreak": "error", - "vue/multiline-html-element-content-newline": "off", - "vue/singleline-html-element-content-newline": "off", - "vue/multi-word-component-names": "off", // TODO enable, align with conventions - "vue/no-mutating-props": "off" - }, - "overrides": [ - { - "files": LEGACY_FILES, - "rules": { - "no-unused-vars": [ - "warn", - { - "vars": "all", - "args": "none", - "varsIgnorePattern": "controller" - } - ], - "no-nested-ternary": "off", - "no-var": "off", - "one-var": "off" - } - } - ] + // https://eslint.org/docs/rules/no-implicit-coercion + 'no-implicit-coercion': 'error', + //https://eslint.org/docs/rules/no-unneeded-ternary + 'no-unneeded-ternary': 'error', + 'vue/first-attribute-linebreak': 'error', + 'vue/multiline-html-element-content-newline': 'off', + 'vue/singleline-html-element-content-newline': 'off', + 'vue/multi-word-component-names': 'off', // TODO enable, align with conventions + 'vue/no-mutating-props': 'off' + }, + overrides: [ + { + files: LEGACY_FILES, + rules: { + 'no-unused-vars': [ + 'warn', + { + vars: 'all', + args: 'none', + varsIgnorePattern: 'controller' + } + ], + 'no-nested-ternary': 'off', + 'no-var': 'off', + 'one-var': 'off' + } + } + ] }; diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8095366a17..5ff88acc4a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,39 +1,38 @@ - version: 2 updates: - - package-ecosystem: "npm" - directory: "/" + - package-ecosystem: 'npm' + directory: '/' schedule: - interval: "weekly" + interval: 'weekly' open-pull-requests-limit: 10 labels: - - "pr:daveit" - - "pr:e2e" - - "type:maintenance" - - "dependencies" - - "pr:platform" + - 'pr:daveit' + - 'pr:e2e' + - 'type:maintenance' + - 'dependencies' + - 'pr:platform' ignore: #We have to source the playwright container which is not detected by Dependabot - - dependency-name: "@playwright/test" - - dependency-name: "playwright-core" + - dependency-name: '@playwright/test' + - dependency-name: 'playwright-core' #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"] - - dependency-name: "babel-loader" - update-types: ["version-update:semver-patch"] - - dependency-name: "sinon" - update-types: ["version-update:semver-patch"] - - dependency-name: "moment-timezone" - update-types: ["version-update:semver-patch"] - - dependency-name: "@types/lodash" - update-types: ["version-update:semver-patch"] - - package-ecosystem: "github-actions" - directory: "/" + - dependency-name: '@babel/eslint-parser' + update-types: ['version-update:semver-patch'] + - dependency-name: 'eslint-plugin-vue' + update-types: ['version-update:semver-patch'] + - dependency-name: 'babel-loader' + update-types: ['version-update:semver-patch'] + - dependency-name: 'sinon' + update-types: ['version-update:semver-patch'] + - dependency-name: 'moment-timezone' + update-types: ['version-update:semver-patch'] + - dependency-name: '@types/lodash' + update-types: ['version-update:semver-patch'] + - package-ecosystem: 'github-actions' + directory: '/' schedule: - interval: "daily" + interval: 'daily' labels: - - "pr:daveit" - - "type:maintenance" - - "dependencies" + - 'pr:daveit' + - 'type:maintenance' + - 'dependencies' diff --git a/.github/workflows/e2e-couchdb.yml b/.github/workflows/e2e-couchdb.yml index 3eb62a75d8..e442800b72 100644 --- a/.github/workflows/e2e-couchdb.yml +++ b/.github/workflows/e2e-couchdb.yml @@ -1,4 +1,4 @@ -name: "e2e-couchdb" +name: 'e2e-couchdb' on: workflow_dispatch: pull_request: @@ -17,7 +17,7 @@ jobs: - run: npx playwright@1.32.3 install - run: npm install - name: Start CouchDB Docker Container and Init with Setup Scripts - run : | + run: | export $(cat src/plugins/persistence/couch/.env.ci | xargs) docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach sleep 3 @@ -25,10 +25,10 @@ jobs: bash src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh - name: Run CouchDB Tests and publish to deploysentinel env: - DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }} + DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }} run: npm run test:e2e:couchdb - name: Publish Results to Codecov.io - env: + env: SUPER_SECRET: ${{ secrets.CODECOV_TOKEN }} run: npm run cov:e2e:full:publish - name: Archive test results diff --git a/.github/workflows/e2e-pr.yml b/.github/workflows/e2e-pr.yml index 2822bada2c..62cd32dba9 100644 --- a/.github/workflows/e2e-pr.yml +++ b/.github/workflows/e2e-pr.yml @@ -1,4 +1,4 @@ -name: "e2e-pr" +name: 'e2e-pr' on: workflow_dispatch: pull_request: diff --git a/.github/workflows/pr-platform.yml b/.github/workflows/pr-platform.yml index 85f8334eb4..f56ff576c2 100644 --- a/.github/workflows/pr-platform.yml +++ b/.github/workflows/pr-platform.yml @@ -1,8 +1,8 @@ -name: "pr-platform" +name: 'pr-platform' on: workflow_dispatch: pull_request: - types: [ labeled ] + types: [labeled] jobs: e2e-full: diff --git a/.github/workflows/prcop.yml b/.github/workflows/prcop.yml index 4ee2c5569c..cc74f2c15c 100644 --- a/.github/workflows/prcop.yml +++ b/.github/workflows/prcop.yml @@ -22,5 +22,5 @@ jobs: - name: Linting Pull Request uses: makaroni4/prcop@v1.0.35 with: - config-file: ".github/workflows/prcop-config.json" + config-file: '.github/workflows/prcop-config.json' GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.webpack/webpack.common.js b/.webpack/webpack.common.js index 1b31af4bc7..797340753c 100644 --- a/.webpack/webpack.common.js +++ b/.webpack/webpack.common.js @@ -8,169 +8,156 @@ This is the OpenMCT common webpack file. It is imported by the other three webpa There are separate npm scripts to use these configurations, though simply running `npm install` will use the default production configuration. */ -const path = require("path"); -const packageDefinition = require("../package.json"); -const CopyWebpackPlugin = require("copy-webpack-plugin"); -const webpack = require("webpack"); -const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const path = require('path'); +const packageDefinition = require('../package.json'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); +const webpack = require('webpack'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const { VueLoaderPlugin } = require("vue-loader"); -let gitRevision = "error-retrieving-revision"; -let gitBranch = "error-retrieving-branch"; +const { VueLoaderPlugin } = require('vue-loader'); +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(); + 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); + console.warn(err); } -const projectRootDir = path.resolve(__dirname, ".."); +const projectRootDir = path.resolve(__dirname, '..'); /** @type {import('webpack').Configuration} */ const config = { - context: projectRootDir, - entry: { - openmct: "./openmct.js", - generatorWorker: "./example/generator/generatorWorker.js", - couchDBChangesFeed: - "./src/plugins/persistence/couch/CouchChangesFeed.js", - inMemorySearchWorker: "./src/api/objects/InMemorySearchWorker.js", - espressoTheme: "./src/plugins/themes/espresso-theme.scss", - snowTheme: "./src/plugins/themes/snow-theme.scss" - }, - output: { - globalObject: "this", - filename: "[name].js", - path: path.resolve(projectRootDir, "dist"), - library: "openmct", - libraryTarget: "umd", - publicPath: "", - hashFunction: "xxhash64", - clean: true - }, - resolve: { - alias: { - "@": path.join(projectRootDir, "src"), - legacyRegistry: path.join(projectRootDir, "src/legacyRegistry"), - saveAs: "file-saver/src/FileSaver.js", - csv: "comma-separated-values", - EventEmitter: "eventemitter3", - bourbon: "bourbon.scss", - "plotly-basic": "plotly.js-basic-dist", - "plotly-gl2d": "plotly.js-gl2d-dist", - "d3-scale": path.join( - projectRootDir, - "node_modules/d3-scale/dist/d3-scale.min.js" - ), - printj: path.join( - projectRootDir, - "node_modules/printj/dist/printj.min.js" - ), - styles: path.join(projectRootDir, "src/styles"), - MCT: path.join(projectRootDir, "src/MCT"), - testUtils: path.join(projectRootDir, "src/utils/testUtils.js"), - objectUtils: path.join( - projectRootDir, - "src/api/objects/object-utils.js" - ), - "kdbush": path.join(projectRootDir, "node_modules/kdbush/kdbush.min.js"), - utils: path.join(projectRootDir, "src/utils") - } - }, - plugins: [ - new webpack.DefinePlugin({ - __OPENMCT_VERSION__: `'${packageDefinition.version}'`, - __OPENMCT_BUILD_DATE__: `'${new Date()}'`, - __OPENMCT_REVISION__: `'${gitRevision}'`, - __OPENMCT_BUILD_BRANCH__: `'${gitBranch}'` - }), - new VueLoaderPlugin(), - new CopyWebpackPlugin({ - patterns: [ - { - from: "src/images/favicons", - to: "favicons" - }, - { - from: "./index.html", - transform: function (content) { - return content.toString().replace(/dist\//g, ""); - } - }, - { - from: "src/plugins/imagery/layers", - to: "imagery" - } - ] - }), - new MiniCssExtractPlugin({ - filename: "[name].css", - chunkFilename: "[name].css" - }) - ], - module: { - rules: [ - { - test: /\.(sc|sa|c)ss$/, - use: [ - MiniCssExtractPlugin.loader, - { - loader: "css-loader" - }, - { - loader: "resolve-url-loader" - }, - { - loader: "sass-loader", - options: { sourceMap: true } - } - ] - }, - { - test: /\.vue$/, - use: "vue-loader" - }, - { - test: /\.html$/, - type: "asset/source" - }, - { - test: /\.(jpg|jpeg|png|svg)$/, - type: "asset/resource", - generator: { - filename: "images/[name][ext]" - } - }, - { - test: /\.ico$/, - type: "asset/resource", - generator: { - filename: "icons/[name][ext]" - } - }, - { - test: /\.(woff|woff2?|eot|ttf)$/, - type: "asset/resource", - generator: { - filename: "fonts/[name][ext]" - } - } - ] - }, - stats: "errors-warnings", - performance: { - // We should eventually consider chunking to decrease - // these values - maxEntrypointSize: 27000000, - maxAssetSize: 27000000 + context: projectRootDir, + entry: { + openmct: './openmct.js', + generatorWorker: './example/generator/generatorWorker.js', + couchDBChangesFeed: './src/plugins/persistence/couch/CouchChangesFeed.js', + inMemorySearchWorker: './src/api/objects/InMemorySearchWorker.js', + espressoTheme: './src/plugins/themes/espresso-theme.scss', + snowTheme: './src/plugins/themes/snow-theme.scss' + }, + output: { + globalObject: 'this', + filename: '[name].js', + path: path.resolve(projectRootDir, 'dist'), + library: 'openmct', + libraryTarget: 'umd', + publicPath: '', + hashFunction: 'xxhash64', + clean: true + }, + resolve: { + alias: { + '@': path.join(projectRootDir, 'src'), + legacyRegistry: path.join(projectRootDir, 'src/legacyRegistry'), + saveAs: 'file-saver/src/FileSaver.js', + csv: 'comma-separated-values', + EventEmitter: 'eventemitter3', + bourbon: 'bourbon.scss', + 'plotly-basic': 'plotly.js-basic-dist', + 'plotly-gl2d': 'plotly.js-gl2d-dist', + 'd3-scale': path.join(projectRootDir, 'node_modules/d3-scale/dist/d3-scale.min.js'), + printj: path.join(projectRootDir, 'node_modules/printj/dist/printj.min.js'), + styles: path.join(projectRootDir, 'src/styles'), + MCT: path.join(projectRootDir, 'src/MCT'), + testUtils: path.join(projectRootDir, 'src/utils/testUtils.js'), + objectUtils: path.join(projectRootDir, 'src/api/objects/object-utils.js'), + kdbush: path.join(projectRootDir, 'node_modules/kdbush/kdbush.min.js'), + utils: path.join(projectRootDir, 'src/utils') } + }, + plugins: [ + new webpack.DefinePlugin({ + __OPENMCT_VERSION__: `'${packageDefinition.version}'`, + __OPENMCT_BUILD_DATE__: `'${new Date()}'`, + __OPENMCT_REVISION__: `'${gitRevision}'`, + __OPENMCT_BUILD_BRANCH__: `'${gitBranch}'` + }), + new VueLoaderPlugin(), + new CopyWebpackPlugin({ + patterns: [ + { + from: 'src/images/favicons', + to: 'favicons' + }, + { + from: './index.html', + transform: function (content) { + return content.toString().replace(/dist\//g, ''); + } + }, + { + from: 'src/plugins/imagery/layers', + to: 'imagery' + } + ] + }), + new MiniCssExtractPlugin({ + filename: '[name].css', + chunkFilename: '[name].css' + }) + ], + module: { + rules: [ + { + test: /\.(sc|sa|c)ss$/, + use: [ + MiniCssExtractPlugin.loader, + { + loader: 'css-loader' + }, + { + loader: 'resolve-url-loader' + }, + { + loader: 'sass-loader', + options: { sourceMap: true } + } + ] + }, + { + test: /\.vue$/, + use: 'vue-loader' + }, + { + test: /\.html$/, + type: 'asset/source' + }, + { + test: /\.(jpg|jpeg|png|svg)$/, + type: 'asset/resource', + generator: { + filename: 'images/[name][ext]' + } + }, + { + test: /\.ico$/, + type: 'asset/resource', + generator: { + filename: 'icons/[name][ext]' + } + }, + { + test: /\.(woff|woff2?|eot|ttf)$/, + type: 'asset/resource', + generator: { + filename: 'fonts/[name][ext]' + } + } + ] + }, + stats: 'errors-warnings', + performance: { + // We should eventually consider chunking to decrease + // these values + maxEntrypointSize: 27000000, + maxAssetSize: 27000000 + } }; module.exports = config; diff --git a/.webpack/webpack.coverage.js b/.webpack/webpack.coverage.js index b693ff6b18..5cfb1c059d 100644 --- a/.webpack/webpack.coverage.js +++ b/.webpack/webpack.coverage.js @@ -6,32 +6,32 @@ OpenMCT Continuous Integration servers use this configuration to add code covera information to pull requests. */ -const config = require("./webpack.dev"); +const config = require('./webpack.dev'); // eslint-disable-next-line no-undef -const CI = process.env.CI === "true"; +const CI = process.env.CI === 'true'; config.devtool = CI ? false : undefined; config.devServer.hot = false; config.module.rules.push({ - test: /\.js$/, - exclude: /(Spec\.js$)|(node_modules)/, - use: { - loader: "babel-loader", - options: { - retainLines: true, - // eslint-disable-next-line no-undef - plugins: [ - [ - "babel-plugin-istanbul", - { - extension: [".js", ".vue"] - } - ] - ] - } + test: /\.js$/, + exclude: /(Spec\.js$)|(node_modules)/, + use: { + loader: 'babel-loader', + options: { + retainLines: true, + // eslint-disable-next-line no-undef + plugins: [ + [ + 'babel-plugin-istanbul', + { + extension: ['.js', '.vue'] + } + ] + ] } + } }); module.exports = config; diff --git a/.webpack/webpack.dev.js b/.webpack/webpack.dev.js index d22b8a97d0..bca7117eaa 100644 --- a/.webpack/webpack.dev.js +++ b/.webpack/webpack.dev.js @@ -5,59 +5,59 @@ This configuration should be used for development purposes. It contains full sou devServer (which be invoked using by `npm start`), and a non-minified Vue.js distribution. If OpenMCT is to be used for a production server, use webpack.prod.js instead. */ -const path = require("path"); -const webpack = require("webpack"); -const { merge } = require("webpack-merge"); +const path = require('path'); +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); -const common = require("./webpack.common"); -const projectRootDir = path.resolve(__dirname, ".."); +const common = require('./webpack.common'); +const projectRootDir = path.resolve(__dirname, '..'); 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}", // Config files - "**/*.{sh,md,png,ttf,woff,svg}", // Non source files - "**/.*" // dotfiles and dotfolders - ] - }, - resolve: { - alias: { - vue: path.join(projectRootDir, "node_modules/vue/dist/vue.js") - } - }, - plugins: [ - new webpack.DefinePlugin({ - __OPENMCT_ROOT_RELATIVE__: '"dist/"' - }) - ], - devtool: "eval-source-map", - devServer: { - devMiddleware: { - writeToDisk: (filePathString) => { - const filePath = path.parse(filePathString); - const shouldWrite = !filePath.base.includes("hot-update"); - - return shouldWrite; - } - }, - watchFiles: ["**/*.css"], - static: { - directory: path.join(__dirname, "..", "/dist"), - publicPath: "/dist", - watch: false - }, - client: { - progress: true, - overlay: { - // Disable overlay for runtime errors. - // See: https://github.com/webpack/webpack-dev-server/issues/4771 - runtimeErrors: false - } - } + 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}', // Config files + '**/*.{sh,md,png,ttf,woff,svg}', // Non source files + '**/.*' // dotfiles and dotfolders + ] + }, + resolve: { + alias: { + vue: path.join(projectRootDir, 'node_modules/vue/dist/vue.js') } + }, + plugins: [ + new webpack.DefinePlugin({ + __OPENMCT_ROOT_RELATIVE__: '"dist/"' + }) + ], + devtool: 'eval-source-map', + devServer: { + devMiddleware: { + writeToDisk: (filePathString) => { + const filePath = path.parse(filePathString); + const shouldWrite = !filePath.base.includes('hot-update'); + + return shouldWrite; + } + }, + watchFiles: ['**/*.css'], + static: { + directory: path.join(__dirname, '..', '/dist'), + publicPath: '/dist', + watch: false + }, + client: { + progress: true, + overlay: { + // Disable overlay for runtime errors. + // See: https://github.com/webpack/webpack-dev-server/issues/4771 + runtimeErrors: false + } + } + } }); diff --git a/.webpack/webpack.prod.js b/.webpack/webpack.prod.js index d6f7f90a87..99032cb26b 100644 --- a/.webpack/webpack.prod.js +++ b/.webpack/webpack.prod.js @@ -4,24 +4,24 @@ This configuration should be used for production installs. It is the default webpack configuration. */ -const path = require("path"); -const webpack = require("webpack"); -const { merge } = require("webpack-merge"); +const path = require('path'); +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); -const common = require("./webpack.common"); -const projectRootDir = path.resolve(__dirname, ".."); +const common = require('./webpack.common'); +const projectRootDir = path.resolve(__dirname, '..'); module.exports = merge(common, { - mode: "production", - resolve: { - alias: { - vue: path.join(projectRootDir, "node_modules/vue/dist/vue.min.js") - } - }, - plugins: [ - new webpack.DefinePlugin({ - __OPENMCT_ROOT_RELATIVE__: '""' - }) - ], - devtool: "source-map" + mode: 'production', + resolve: { + alias: { + vue: path.join(projectRootDir, 'node_modules/vue/dist/vue.min.js') + } + }, + plugins: [ + new webpack.DefinePlugin({ + __OPENMCT_ROOT_RELATIVE__: '""' + }) + ], + devtool: 'source-map' }); diff --git a/codecov.yml b/codecov.yml index 7f1ed5e876..07da9fa4c3 100644 --- a/codecov.yml +++ b/codecov.yml @@ -11,7 +11,7 @@ coverage: informational: true precision: 2 round: down - range: "66...100" + range: '66...100' flags: unit: @@ -19,10 +19,10 @@ flags: e2e-stable: carryforward: false e2e-full: - carryforward: true + carryforward: true comment: - layout: "diff,flags,files,footer" + layout: 'diff,flags,files,footer' behavior: default require_changes: false show_carryforward_flags: true diff --git a/e2e/.eslintrc.js b/e2e/.eslintrc.js index 5d6b775817..9d378f77c0 100644 --- a/e2e/.eslintrc.js +++ b/e2e/.eslintrc.js @@ -1,15 +1,15 @@ /* eslint-disable no-undef */ module.exports = { - "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" - } - } - ] + 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/appActions.js b/e2e/appActions.js index 6eac4ad81f..c50566a149 100644 --- a/e2e/appActions.js +++ b/e2e/appActions.js @@ -66,58 +66,58 @@ const { expect } = require('@playwright/test'); * @returns {Promise} An object containing information about the newly created domain object. */ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) { - if (!name) { - name = `${type}:${genUuid()}`; - } + if (!name) { + name = `${type}:${genUuid()}`; + } - const parentUrl = await getHashUrlToDomainObject(page, parent); + 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`); + // 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 the Create button + await page.click('button:has-text("Create")'); - // Click the object specified by 'type' - await page.click(`li[role='menuitem']:text("${type}")`); + // Click the object specified by 'type' + await page.click(`li[role='menuitem']:text("${type}")`); - // Modify the name input field of the domain object to accept 'name' - const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]'); - await nameInput.fill(""); - await nameInput.fill(name); + // Modify the name input field of the domain object to accept 'name' + const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]'); + await nameInput.fill(''); + await nameInput.fill(name); - if (page.testNotes) { - // Fill the "Notes" section with information about the - // currently running test and its project. - const notesInput = page.locator('form[name="mctForm"] #notes-textarea'); - await notesInput.fill(page.testNotes); - } + if (page.testNotes) { + // Fill the "Notes" section with information about the + // currently running test and its project. + const notesInput = page.locator('form[name="mctForm"] #notes-textarea'); + await notesInput.fill(page.testNotes); + } - // 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') - ]); + // 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); + // 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(); - } + 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, - uuid, - url: objectUrl - }; + return { + name, + uuid, + url: objectUrl + }; } /** @@ -126,17 +126,17 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine * @param {CreateNotificationOptions} createNotificationOptions */ async function createNotification(page, createNotificationOptions) { - await page.evaluate((_createNotificationOptions) => { - const { message, severity, options } = _createNotificationOptions; - const notificationApi = window.openmct.notifications; - if (severity === 'info') { - notificationApi.info(message, options); - } else if (severity === 'alert') { - notificationApi.alert(message, options); - } else { - notificationApi.error(message, options); - } - }, createNotificationOptions); + await page.evaluate((_createNotificationOptions) => { + const { message, severity, options } = _createNotificationOptions; + const notificationApi = window.openmct.notifications; + if (severity === 'info') { + notificationApi.info(message, options); + } else if (severity === 'alert') { + notificationApi.alert(message, options); + } else { + notificationApi.error(message, options); + } + }, createNotificationOptions); } /** @@ -145,12 +145,12 @@ async function createNotification(page, createNotificationOptions) { * @param {string} name */ async function expandTreePaneItemByName(page, name) { - const treePane = page.getByRole('tree', { - name: 'Main Tree' - }); - const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`); - const expandTriangle = treeItem.locator('.c-disclosure-triangle'); - await expandTriangle.click(); + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); + const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`); + const expandTriangle = treeItem.locator('.c-disclosure-triangle'); + await expandTriangle.click(); } /** @@ -160,67 +160,67 @@ async function expandTreePaneItemByName(page, name) { * @returns {Promise} An object containing information about the newly created domain object. */ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) { - if (!name) { - name = `Plan:${genUuid()}`; - } + if (!name) { + name = `Plan:${genUuid()}`; + } - const parentUrl = await getHashUrlToDomainObject(page, parent); + 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`); + // 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 the Create button + await page.click('button:has-text("Create")'); - // Click 'Plan' menu option - await page.click(`li:text("Plan")`); + // Click 'Plan' menu option + await page.click(`li:text("Plan")`); - // Modify the name input field of the domain object to accept 'name' - const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]'); - await nameInput.fill(""); - await nameInput.fill(name); + // Modify the name input field of the domain object to accept '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)) - }); + // 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') - ]); + // 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); + // Wait until the URL is updated + await page.waitForURL(`**/${parent}/*`); + const uuid = await getFocusedObjectUuid(page); + const objectUrl = await getHashUrlToDomainObject(page, uuid); - return { - uuid, - name, - url: objectUrl - }; + 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 -*/ + * 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' - }); + await page.goto(url); + await page.click('button[title="Show selected item in tree"]'); + await page.locator('.is-navigated-object').click({ + button: 'right' + }); } /** @@ -228,23 +228,25 @@ async function openObjectTreeContextMenu(page, url) { * @param {import('@playwright/test').Page} page * @param {"Main Tree" | "Create Modal Tree"} [treeName="Main Tree"] */ -async function expandEntireTree(page, treeName = "Main Tree") { - const treeLocator = page.getByRole('tree', { - name: treeName - }); - const collapsedTreeItems = treeLocator.getByRole('treeitem', { - expanded: false - }).locator('span.c-disclosure-triangle.is-enabled'); +async function expandEntireTree(page, treeName = 'Main Tree') { + const treeLocator = page.getByRole('tree', { + name: treeName + }); + const collapsedTreeItems = treeLocator + .getByRole('treeitem', { + expanded: false + }) + .locator('span.c-disclosure-triangle.is-enabled'); - while (await collapsedTreeItems.count() > 0) { - await collapsedTreeItems.nth(0).click(); + while ((await collapsedTreeItems.count()) > 0) { + await collapsedTreeItems.nth(0).click(); - // FIXME: Replace hard wait with something event-driven. - // Without the wait, this fails periodically due to a race condition - // with Vue rendering (loop exits prematurely). - // eslint-disable-next-line playwright/no-wait-for-timeout - await page.waitForTimeout(200); - } + // FIXME: Replace hard wait with something event-driven. + // Without the wait, this fails periodically due to a race condition + // with Vue rendering (loop exits prematurely). + // eslint-disable-next-line playwright/no-wait-for-timeout + await page.waitForTimeout(200); + } } /** @@ -254,12 +256,12 @@ async function expandEntireTree(page, treeName = "Main Tree") { * @returns {Promise} 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); + 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; + return focusedObjectUuid; } /** @@ -273,22 +275,25 @@ async function getFocusedObjectUuid(page) { * @returns {Promise} the url of the object */ async function getHashUrlToDomainObject(page, uuid) { - await page.waitForLoadState('load'); //Add some determinism - 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('/'); + await page.waitForLoadState('load'); //Add some determinism + 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(''); - } + // Drop the vestigial '/ROOT' if it exists + if (url.includes('/ROOT')) { + url = url.split('/ROOT').join(''); + } - return url; - }, uuid); + return url; + }, uuid); - return hashUrl; + return hashUrl; } /** @@ -298,8 +303,8 @@ async function getHashUrlToDomainObject(page, uuid) { * @return {Promise} true if the Open MCT is in Edit Mode */ async function _isInEditMode(page, identifier) { - // eslint-disable-next-line no-return-await - return await page.evaluate(() => window.openmct.editor.isEditing()); + // eslint-disable-next-line no-return-await + return await page.evaluate(() => window.openmct.editor.isEditing()); } /** @@ -308,15 +313,15 @@ async function _isInEditMode(page, identifier) { * @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(); + // 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(); - } + // 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(); + } } /** @@ -324,7 +329,7 @@ async function setTimeConductorMode(page, isFixedTimespan = true) { * @param {import('@playwright/test').Page} page */ async function setFixedTimeMode(page) { - await setTimeConductorMode(page, true); + await setTimeConductorMode(page, true); } /** @@ -332,7 +337,7 @@ async function setFixedTimeMode(page) { * @param {import('@playwright/test').Page} page */ async function setRealTimeMode(page) { - await setTimeConductorMode(page, false); + await setTimeConductorMode(page, false); } /** @@ -348,23 +353,23 @@ async function setRealTimeMode(page) { * @param {OffsetValues} offset * @param {import('@playwright/test').Locator} offsetButton */ -async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) { - await offsetButton.click(); +async function setTimeConductorOffset(page, { hours, mins, secs }, offsetButton) { + await offsetButton.click(); - if (hours) { - await page.fill('.pr-time-controls__hrs', hours); - } + if (hours) { + await page.fill('.pr-time-controls__hrs', hours); + } - if (mins) { - await page.fill('.pr-time-controls__mins', mins); - } + if (mins) { + await page.fill('.pr-time-controls__mins', mins); + } - if (secs) { - await page.fill('.pr-time-controls__secs', secs); - } + if (secs) { + await page.fill('.pr-time-controls__secs', secs); + } - // Click the check button - await page.locator('.pr-time__buttons .icon-check').click(); + // Click the check button + await page.locator('.pr-time__buttons .icon-check').click(); } /** @@ -373,8 +378,8 @@ async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) { * @param {OffsetValues} offset */ async function setStartOffset(page, offset) { - const startOffsetButton = page.locator('data-testid=conductor-start-offset-button'); - await setTimeConductorOffset(page, offset, startOffsetButton); + const startOffsetButton = page.locator('data-testid=conductor-start-offset-button'); + await setTimeConductorOffset(page, offset, startOffsetButton); } /** @@ -383,8 +388,8 @@ async function setStartOffset(page, offset) { * @param {OffsetValues} offset */ async function setEndOffset(page, offset) { - const endOffsetButton = page.locator('data-testid=conductor-end-offset-button'); - await setTimeConductorOffset(page, offset, endOffsetButton); + const endOffsetButton = page.locator('data-testid=conductor-end-offset-button'); + await setTimeConductorOffset(page, offset, endOffsetButton); } /** @@ -394,34 +399,34 @@ async function setEndOffset(page, offset) { * @param {String} name the name of the tab */ async function selectInspectorTab(page, name) { - const inspectorTabs = page.getByRole('tablist'); - const inspectorTab = inspectorTabs.getByTitle(name); - const inspectorTabClass = await inspectorTab.getAttribute('class'); - const isSelectedInspectorTab = inspectorTabClass.includes('is-current'); + const inspectorTabs = page.getByRole('tablist'); + const inspectorTab = inspectorTabs.getByTitle(name); + const inspectorTabClass = await inspectorTab.getAttribute('class'); + const isSelectedInspectorTab = inspectorTabClass.includes('is-current'); - // do not click a tab that is already selected or it will timeout your test - // do to a { pointer-events: none; } on selected tabs - if (!isSelectedInspectorTab) { - await inspectorTab.click(); - } + // do not click a tab that is already selected or it will timeout your test + // do to a { pointer-events: none; } on selected tabs + if (!isSelectedInspectorTab) { + await inspectorTab.click(); + } } /** -* Waits and asserts that all plot series data on the page -* is loaded and drawn. -* -* In lieu of a better way to detect when a plot is done rendering, -* we [attach a class to the '.gl-plot' element](https://github.com/nasa/openmct/blob/5924d7ea95a0c2d4141c602a3c7d0665cb91095f/src/plugins/plot/MctPlot.vue#L27) -* once all pending series data has been loaded. The following appAction retrieves -* all plots on the page and waits up to the default timeout for the class to be -* attached to each plot. -* @param {import('@playwright/test').Page} page -*/ + * Waits and asserts that all plot series data on the page + * is loaded and drawn. + * + * In lieu of a better way to detect when a plot is done rendering, + * we [attach a class to the '.gl-plot' element](https://github.com/nasa/openmct/blob/5924d7ea95a0c2d4141c602a3c7d0665cb91095f/src/plugins/plot/MctPlot.vue#L27) + * once all pending series data has been loaded. The following appAction retrieves + * all plots on the page and waits up to the default timeout for the class to be + * attached to each plot. + * @param {import('@playwright/test').Page} page + */ async function waitForPlotsToRender(page) { - const plotLocator = page.locator('.gl-plot'); - for (const plot of await plotLocator.all()) { - await expect(plot).toHaveClass(/js-series-data-loaded/); - } + const plotLocator = page.locator('.gl-plot'); + for (const plot of await plotLocator.all()) { + await expect(plot).toHaveClass(/js-series-data-loaded/); + } } /** @@ -441,57 +446,70 @@ async function waitForPlotsToRender(page) { * @return {Promise} */ async function getCanvasPixels(page, canvasSelector) { - const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getCanvasValue', resolve)); - const canvasHandle = await page.evaluateHandle((canvas) => document.querySelector(canvas), canvasSelector); - const canvasContextHandle = await page.evaluateHandle(canvas => canvas.getContext('2d'), canvasHandle); + const getTelemValuePromise = new Promise((resolve) => + page.exposeFunction('getCanvasValue', resolve) + ); + const canvasHandle = await page.evaluateHandle( + (canvas) => document.querySelector(canvas), + canvasSelector + ); + const canvasContextHandle = await page.evaluateHandle( + (canvas) => canvas.getContext('2d'), + canvasHandle + ); - await waitForPlotsToRender(page); - await page.evaluate(([canvas, ctx]) => { - // The document canvas is where the plot points and lines are drawn. - // The only way to access the canvas is using document (using page.evaluate) - /** @type {ImageData} */ - const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data; - /** @type {number[]} */ - const imageDataValues = Object.values(data); - /** @type {PlotPixel[]} */ - const plotPixels = []; - // Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four. - // The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order. - for (let i = 0; i < imageDataValues.length;) { - if (imageDataValues[i] > 0) { - plotPixels.push({ - r: imageDataValues[i], - g: imageDataValues[i + 1], - b: imageDataValues[i + 2], - a: imageDataValues[i + 3], - strValue: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${imageDataValues[i + 2]}, ${imageDataValues[i + 3]})` - }); - } - - i = i + 4; + await waitForPlotsToRender(page); + await page.evaluate( + ([canvas, ctx]) => { + // The document canvas is where the plot points and lines are drawn. + // The only way to access the canvas is using document (using page.evaluate) + /** @type {ImageData} */ + const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data; + /** @type {number[]} */ + const imageDataValues = Object.values(data); + /** @type {PlotPixel[]} */ + const plotPixels = []; + // Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four. + // The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order. + for (let i = 0; i < imageDataValues.length; ) { + if (imageDataValues[i] > 0) { + plotPixels.push({ + r: imageDataValues[i], + g: imageDataValues[i + 1], + b: imageDataValues[i + 2], + a: imageDataValues[i + 3], + strValue: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${ + imageDataValues[i + 2] + }, ${imageDataValues[i + 3]})` + }); } - window.getCanvasValue(plotPixels); - }, [canvasHandle, canvasContextHandle]); + i = i + 4; + } - return getTelemValuePromise; + window.getCanvasValue(plotPixels); + }, + [canvasHandle, canvasContextHandle] + ); + + return getTelemValuePromise; } // eslint-disable-next-line no-undef module.exports = { - createDomainObjectWithDefaults, - createNotification, - createPlanFromJSON, - expandEntireTree, - expandTreePaneItemByName, - getCanvasPixels, - getHashUrlToDomainObject, - getFocusedObjectUuid, - openObjectTreeContextMenu, - setFixedTimeMode, - setRealTimeMode, - setStartOffset, - setEndOffset, - selectInspectorTab, - waitForPlotsToRender + createDomainObjectWithDefaults, + createNotification, + createPlanFromJSON, + expandEntireTree, + expandTreePaneItemByName, + getCanvasPixels, + getHashUrlToDomainObject, + getFocusedObjectUuid, + openObjectTreeContextMenu, + setFixedTimeMode, + setRealTimeMode, + setStartOffset, + setEndOffset, + selectInspectorTab, + waitForPlotsToRender }; diff --git a/e2e/baseFixtures.js b/e2e/baseFixtures.js index 532f000513..dbb8412121 100644 --- a/e2e/baseFixtures.js +++ b/e2e/baseFixtures.js @@ -43,9 +43,9 @@ const sinon = require('sinon'); * @returns {String} formatted string with message type, text, url, and line and column numbers */ function _consoleMessageToString(msg) { - const { url, lineNumber, columnNumber } = msg.location(); + const { url, lineNumber, columnNumber } = msg.location(); - return `[${msg.type()}] ${msg.text()} at (${url} ${lineNumber}:${columnNumber})`; + return `[${msg.type()}] ${msg.text()} at (${url} ${lineNumber}:${columnNumber})`; } /** @@ -56,12 +56,9 @@ function _consoleMessageToString(msg) { * @return {Promise} */ function waitForAnimations(locator) { - return locator - .evaluate((element) => - Promise.all( - element - .getAnimations({ subtree: true }) - .map((animation) => animation.finished))); + return locator.evaluate((element) => + Promise.all(element.getAnimations({ subtree: true }).map((animation) => animation.finished)) + ); } /** @@ -72,103 +69,113 @@ function waitForAnimations(locator) { 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); - } + /** + * 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); - for (const page of context.pages()) { - await page.evaluate(() => (window).collectIstanbulCoverage(JSON.stringify((window).__coverage__))); - } + await use(context); }, - /** - * 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); - } + { + 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; diff --git a/e2e/helper/addInitExampleFaultProvider.js b/e2e/helper/addInitExampleFaultProvider.js index 2658e6fb20..2819608fe2 100644 --- a/e2e/helper/addInitExampleFaultProvider.js +++ b/e2e/helper/addInitExampleFaultProvider.js @@ -23,6 +23,6 @@ // 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()); + const openmct = window.openmct; + openmct.install(openmct.plugins.example.ExampleFaultSource()); }); diff --git a/e2e/helper/addInitExampleFaultProviderStatic.js b/e2e/helper/addInitExampleFaultProviderStatic.js index 3024810887..a638acd7cd 100644 --- a/e2e/helper/addInitExampleFaultProviderStatic.js +++ b/e2e/helper/addInitExampleFaultProviderStatic.js @@ -23,8 +23,8 @@ // 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; + const openmct = window.openmct; + const staticFaults = true; - openmct.install(openmct.plugins.example.ExampleFaultSource(staticFaults)); + openmct.install(openmct.plugins.example.ExampleFaultSource(staticFaults)); }); diff --git a/e2e/helper/addInitExampleUser.js b/e2e/helper/addInitExampleUser.js index 7d8efbee5c..e7fb07f9ea 100644 --- a/e2e/helper/addInitExampleUser.js +++ b/e2e/helper/addInitExampleUser.js @@ -22,6 +22,6 @@ // This should be used to install the Example User document.addEventListener('DOMContentLoaded', () => { - const openmct = window.openmct; - openmct.install(openmct.plugins.example.ExampleUser()); + const openmct = window.openmct; + openmct.install(openmct.plugins.example.ExampleUser()); }); diff --git a/e2e/helper/addInitFaultManagementPlugin.js b/e2e/helper/addInitFaultManagementPlugin.js index 9d4654db92..4bb3b683bc 100644 --- a/e2e/helper/addInitFaultManagementPlugin.js +++ b/e2e/helper/addInitFaultManagementPlugin.js @@ -23,6 +23,6 @@ // 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()); + const openmct = window.openmct; + openmct.install(openmct.plugins.FaultManagement()); }); diff --git a/e2e/helper/addInitFileInputObject.js b/e2e/helper/addInitFileInputObject.js index 257492be74..fc923b5cf8 100644 --- a/e2e/helper/addInitFileInputObject.js +++ b/e2e/helper/addInitFileInputObject.js @@ -1,76 +1,71 @@ class DomainObjectViewProvider { - constructor(openmct) { - this.key = 'doViewProvider'; - this.name = 'Domain Object View Provider'; - this.openmct = openmct; - } + constructor(openmct) { + this.key = 'doViewProvider'; + this.name = 'Domain Object View Provider'; + this.openmct = openmct; + } - canView(domainObject) { - return domainObject.type === 'imageFileInput' - || domainObject.type === 'jsonFileInput'; - } + canView(domainObject) { + return domainObject.type === 'imageFileInput' || domainObject.type === 'jsonFileInput'; + } - view(domainObject, objectPath) { - let content; + view(domainObject, objectPath) { + let content; - return { - show: function (element) { - const body = domainObject.selectFile.body; - const type = typeof body; + return { + show: function (element) { + const body = domainObject.selectFile.body; + const type = typeof body; - content = document.createElement('div'); - content.id = 'file-input-type'; - content.textContent = JSON.stringify(type); - element.appendChild(content); - }, - destroy: function (element) { - element.removeChild(content); - content = undefined; - } - }; - } + content = document.createElement('div'); + content.id = 'file-input-type'; + content.textContent = JSON.stringify(type); + element.appendChild(content); + }, + destroy: function (element) { + element.removeChild(content); + content = undefined; + } + }; + } } document.addEventListener('DOMContentLoaded', () => { - const openmct = window.openmct; + const openmct = window.openmct; - openmct.types.addType('jsonFileInput', { - key: 'jsonFileInput', - name: "JSON File Input Object", - creatable: true, - form: [ - { - name: 'Upload File', - key: 'selectFile', - control: 'file-input', - required: true, - text: 'Select File...', - type: 'application/json', - property: [ - "selectFile" - ] - } - ] - }); + openmct.types.addType('jsonFileInput', { + key: 'jsonFileInput', + name: 'JSON File Input Object', + creatable: true, + form: [ + { + name: 'Upload File', + key: 'selectFile', + control: 'file-input', + required: true, + text: 'Select File...', + type: 'application/json', + property: ['selectFile'] + } + ] + }); - openmct.types.addType('imageFileInput', { - key: 'imageFileInput', - name: "Image File Input Object", - creatable: true, - form: [ - { - name: 'Upload File', - key: 'selectFile', - control: 'file-input', - required: true, - text: 'Select File...', - type: 'image/*', - property: [ - "selectFile" - ] - } - ] - }); + openmct.types.addType('imageFileInput', { + key: 'imageFileInput', + name: 'Image File Input Object', + creatable: true, + form: [ + { + name: 'Upload File', + key: 'selectFile', + control: 'file-input', + required: true, + text: 'Select File...', + type: 'image/*', + property: ['selectFile'] + } + ] + }); - openmct.objectViews.addProvider(new DomainObjectViewProvider(openmct)); + openmct.objectViews.addProvider(new DomainObjectViewProvider(openmct)); }); diff --git a/e2e/helper/addInitNotebookWithUrls.js b/e2e/helper/addInitNotebookWithUrls.js index 0c12ad0254..ee03046e98 100644 --- a/e2e/helper/addInitNotebookWithUrls.js +++ b/e2e/helper/addInitNotebookWithUrls.js @@ -27,6 +27,6 @@ const NOTEBOOK_NAME = 'Notebook'; const URL_WHITELIST = ['google.com']; document.addEventListener('DOMContentLoaded', () => { - const openmct = window.openmct; - openmct.install(openmct.plugins.Notebook(NOTEBOOK_NAME, URL_WHITELIST)); + const openmct = window.openmct; + openmct.install(openmct.plugins.Notebook(NOTEBOOK_NAME, URL_WHITELIST)); }); diff --git a/e2e/helper/addInitOperatorStatus.js b/e2e/helper/addInitOperatorStatus.js index cae93daf1d..d4fc15a05b 100644 --- a/e2e/helper/addInitOperatorStatus.js +++ b/e2e/helper/addInitOperatorStatus.js @@ -22,6 +22,6 @@ // This should be used to install the Operator Status document.addEventListener('DOMContentLoaded', () => { - const openmct = window.openmct; - openmct.install(openmct.plugins.OperatorStatus()); + const openmct = window.openmct; + openmct.install(openmct.plugins.OperatorStatus()); }); diff --git a/e2e/helper/addInitRestrictedNotebook.js b/e2e/helper/addInitRestrictedNotebook.js index 1907cb5bc4..c3b1aa05d2 100644 --- a/e2e/helper/addInitRestrictedNotebook.js +++ b/e2e/helper/addInitRestrictedNotebook.js @@ -25,6 +25,6 @@ // await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') }); document.addEventListener('DOMContentLoaded', () => { - const openmct = window.openmct; - openmct.install(openmct.plugins.RestrictedNotebook('CUSTOM_NAME')); + const openmct = window.openmct; + openmct.install(openmct.plugins.RestrictedNotebook('CUSTOM_NAME')); }); diff --git a/e2e/helper/addNoneditableObject.js b/e2e/helper/addNoneditableObject.js index 55da25358c..5098c47caf 100644 --- a/e2e/helper/addNoneditableObject.js +++ b/e2e/helper/addNoneditableObject.js @@ -1,27 +1,27 @@ (function () { - document.addEventListener('DOMContentLoaded', () => { - const PERSISTENCE_KEY = 'persistence-tests'; - const openmct = window.openmct; + document.addEventListener('DOMContentLoaded', () => { + const PERSISTENCE_KEY = 'persistence-tests'; + const openmct = window.openmct; - openmct.objects.addRoot({ - namespace: PERSISTENCE_KEY, - key: PERSISTENCE_KEY - }); - - openmct.objects.addProvider(PERSISTENCE_KEY, { - get(identifier) { - if (identifier.key !== PERSISTENCE_KEY) { - return undefined; - } else { - return Promise.resolve({ - identifier, - type: 'folder', - name: 'Persistence Testing', - location: 'ROOT', - composition: [] - }); - } - } - }); + openmct.objects.addRoot({ + namespace: PERSISTENCE_KEY, + key: PERSISTENCE_KEY }); -}()); + + openmct.objects.addProvider(PERSISTENCE_KEY, { + get(identifier) { + if (identifier.key !== PERSISTENCE_KEY) { + return undefined; + } else { + return Promise.resolve({ + identifier, + type: 'folder', + name: 'Persistence Testing', + location: 'ROOT', + composition: [] + }); + } + } + }); + }); +})(); diff --git a/e2e/helper/faultUtils.js b/e2e/helper/faultUtils.js index 9ad32b0ece..b5cd150478 100644 --- a/e2e/helper/faultUtils.js +++ b/e2e/helper/faultUtils.js @@ -26,254 +26,268 @@ const path = require('path'); * @param {import('@playwright/test').Page} page */ async function navigateToFaultManagementWithExample(page) { - await page.addInitScript({ path: path.join(__dirname, './', 'addInitExampleFaultProvider.js') }); + await page.addInitScript({ path: path.join(__dirname, './', 'addInitExampleFaultProvider.js') }); - await navigateToFaultItemInTree(page); + await navigateToFaultItemInTree(page); } /** * @param {import('@playwright/test').Page} page */ async function navigateToFaultManagementWithStaticExample(page) { - await page.addInitScript({ path: path.join(__dirname, './', 'addInitExampleFaultProviderStatic.js') }); + await page.addInitScript({ + path: path.join(__dirname, './', 'addInitExampleFaultProviderStatic.js') + }); - await navigateToFaultItemInTree(page); + await navigateToFaultItemInTree(page); } /** * @param {import('@playwright/test').Page} page */ async function navigateToFaultManagementWithoutExample(page) { - await page.addInitScript({ path: path.join(__dirname, './', 'addInitFaultManagementPlugin.js') }); + await page.addInitScript({ path: path.join(__dirname, './', 'addInitFaultManagementPlugin.js') }); - await navigateToFaultItemInTree(page); + await navigateToFaultItemInTree(page); } /** * @param {import('@playwright/test').Page} page */ async function navigateToFaultItemInTree(page) { - await page.goto('./', { waitUntil: 'networkidle' }); + await page.goto('./', { waitUntil: 'networkidle' }); - const faultManagementTreeItem = page.getByRole('tree', { - name: "Main Tree" - }).getByRole('treeitem', { - name: "Fault Management" + const faultManagementTreeItem = page + .getByRole('tree', { + name: 'Main Tree' + }) + .getByRole('treeitem', { + name: 'Fault Management' }); - // Navigate to "Fault Management" from the tree - await faultManagementTreeItem.click(); + // Navigate to "Fault Management" from the tree + await faultManagementTreeItem.click(); } /** * @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(); - + 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); + 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(); + 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); + 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(); + 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(); + 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); + 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); + 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); + 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, ''); + await enterSearchTerm(page, ''); } /** * @param {import('@playwright/test').Page} page */ async function selectFaultItem(page, rowNumber) { - await page.locator(`.c-fault-mgmt-item > input >> nth=${rowNumber - 1}`).check(); + await page.locator(`.c-fault-mgmt-item > input >> nth=${rowNumber - 1}`).check(); } /** * @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(); + 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'; - } + if (criticalCount > 0) { + return 'CRITICAL'; + } else if (warningCount > 0) { + return 'WARNING'; + } - return 'WATCH'; + 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(); + 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'; - } + if (watchCount > 0) { + return 'WATCH'; + } else if (warningCount > 0) { + return 'WARNING'; + } - return 'CRITICAL'; + 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(); + const count = await page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').count(); - return 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}`); + const fault = page.locator( + `.c-faults-list-view-item-body > .c-fault-mgmt__list >> nth=${rowNumber - 1}` + ); - return fault; + return fault; } /** * @param {import('@playwright/test').Page} page */ function getFaultByName(page, name) { - const fault = page.locator(`.c-fault-mgmt__list-faultname:has-text("${name}")`); + const fault = page.locator(`.c-fault-mgmt__list-faultname:has-text("${name}")`); - return fault; + 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(); + const faultName = await page + .locator(`.c-fault-mgmt__list-faultname >> nth=${rowNumber - 1}`) + .textContent(); - return faultName; + 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'); + const faultSeverity = await page + .locator(`.c-faults-list-view-item-body .c-fault-mgmt__list-severity >> nth=${rowNumber - 1}`) + .getAttribute('title'); - return faultSeverity; + 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(); + const faultNamespace = await page + .locator(`.c-fault-mgmt__list-path >> nth=${rowNumber - 1}`) + .textContent(); - return faultNamespace; + 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(); + const faultTriggerTime = await page + .locator(`.c-fault-mgmt__list-trigTime >> nth=${rowNumber - 1} >> .c-fault-mgmt-item__value`) + .textContent(); - return faultTriggerTime.toString().trim(); + 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(); - + // 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 + 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 index 36ab2e62b9..fa43d6505d 100644 --- a/e2e/helper/notebookUtils.js +++ b/e2e/helper/notebookUtils.js @@ -28,29 +28,29 @@ const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area'; * @param {import('@playwright/test').Page} page */ async function enterTextEntry(page, text) { - // Click the 'Add Notebook Entry' area - await page.locator(NOTEBOOK_DROP_AREA).click(); + // Click the 'Add Notebook Entry' area + await page.locator(NOTEBOOK_DROP_AREA).click(); - // enter text - await page.locator('[aria-label="Notebook Entry"].is-selected div.c-ne__text').fill(text); - await commitEntry(page); + // enter text + await page.locator('[aria-label="Notebook Entry"].is-selected div.c-ne__text').fill(text); + await commitEntry(page); } /** * @param {import('@playwright/test').Page} page */ async function dragAndDropEmbed(page, notebookObject) { - // Create example telemetry object - const swg = await createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator" - }); - // Navigate to notebook - await page.goto(notebookObject.url); - // Expand the tree to reveal the notebook - await page.click('button[title="Show selected item in tree"]'); - // Drag and drop the SWG into the notebook - await page.dragAndDrop(`text=${swg.name}`, NOTEBOOK_DROP_AREA); - await commitEntry(page); + // Create example telemetry object + const swg = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator' + }); + // Navigate to notebook + await page.goto(notebookObject.url); + // Expand the tree to reveal the notebook + await page.click('button[title="Show selected item in tree"]'); + // Drag and drop the SWG into the notebook + await page.dragAndDrop(`text=${swg.name}`, NOTEBOOK_DROP_AREA); + await commitEntry(page); } /** @@ -58,12 +58,12 @@ async function dragAndDropEmbed(page, notebookObject) { * @param {import('@playwright/test').Page} page */ async function commitEntry(page) { - //Click the Commit Entry button - await page.locator('.c-ne__save-button > button').click(); + //Click the Commit Entry button + await page.locator('.c-ne__save-button > button').click(); } // eslint-disable-next-line no-undef module.exports = { - enterTextEntry, - dragAndDropEmbed + enterTextEntry, + dragAndDropEmbed }; diff --git a/e2e/helper/planningUtils.js b/e2e/helper/planningUtils.js index 74075487fe..f86b4c98d3 100644 --- a/e2e/helper/planningUtils.js +++ b/e2e/helper/planningUtils.js @@ -32,46 +32,53 @@ import { expect } from '../pluginFixtures'; * @param {string} objectUrl The URL of the object to assert against (plan or gantt chart) */ export async function assertPlanActivities(page, plan, objectUrl) { - const groups = Object.keys(plan); - for (const group of groups) { - for (let i = 0; i < plan[group].length; i++) { - // Set the startBound to the start time of the first activity in the group - const startBound = plan[group][0].start; - // Set the endBound to the end time of the current activity - let endBound = plan[group][i].end; - if (endBound === startBound) { - // Prevent oddities with setting start and end bound equal - // via URL params - endBound += 1; - } + const groups = Object.keys(plan); + for (const group of groups) { + for (let i = 0; i < plan[group].length; i++) { + // Set the startBound to the start time of the first activity in the group + const startBound = plan[group][0].start; + // Set the endBound to the end time of the current activity + let endBound = plan[group][i].end; + if (endBound === startBound) { + // Prevent oddities with setting start and end bound equal + // via URL params + endBound += 1; + } - // Switch to fixed time mode with all plan events within the bounds - await page.goto(`${objectUrl}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=plan.view`); + // Switch to fixed time mode with all plan events within the bounds + await page.goto( + `${objectUrl}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=plan.view` + ); - // Assert that the number of activities in the plan view matches the number of - // activities in the plan data within the specified time bounds - const eventCount = await page.locator('.activity-bounds').count(); - expect(eventCount).toEqual(Object.values(plan) - .flat() - .filter(event => - activitiesWithinTimeBounds(event.start, event.end, startBound, endBound)).length); - } + // Assert that the number of activities in the plan view matches the number of + // activities in the plan data within the specified time bounds + const eventCount = await page.locator('.activity-bounds').count(); + expect(eventCount).toEqual( + Object.values(plan) + .flat() + .filter((event) => + activitiesWithinTimeBounds(event.start, event.end, startBound, endBound) + ).length + ); } + } } /** * Returns true if the activities time bounds overlap, false otherwise. -* @param {number} start1 the start time of the first activity -* @param {number} end1 the end time of the first activity -* @param {number} start2 the start time of the second activity -* @param {number} end2 the end time of the second activity -* @returns {boolean} true if the activities overlap, false otherwise -*/ + * @param {number} start1 the start time of the first activity + * @param {number} end1 the end time of the first activity + * @param {number} start2 the start time of the second activity + * @param {number} end2 the end time of the second activity + * @returns {boolean} true if the activities overlap, false otherwise + */ function activitiesWithinTimeBounds(start1, end1, start2, end2) { - return (start1 >= start2 && start1 <= end2) - || (end1 >= start2 && end1 <= end2) - || (start2 >= start1 && start2 <= end1) - || (end2 >= start1 && end2 <= end1); + return ( + (start1 >= start2 && start1 <= end2) || + (end1 >= start2 && end1 <= end2) || + (start2 >= start1 && start2 <= end1) || + (end2 >= start1 && end2 <= end1) + ); } /** @@ -82,11 +89,13 @@ function activitiesWithinTimeBounds(start1, end1, start2, end2) { * @param {string} planObjectUrl */ export async function setBoundsToSpanAllActivities(page, planJson, planObjectUrl) { - const activities = Object.values(planJson).flat(); - // Get the earliest start value - const start = Math.min(...activities.map(activity => activity.start)); - // Get the latest end value - const end = Math.max(...activities.map(activity => activity.end)); - // Set the start and end bounds to the earliest start and latest end - await page.goto(`${planObjectUrl}?tc.mode=fixed&tc.startBound=${start}&tc.endBound=${end}&tc.timeSystem=utc&view=plan.view`); + const activities = Object.values(planJson).flat(); + // Get the earliest start value + const start = Math.min(...activities.map((activity) => activity.start)); + // Get the latest end value + const end = Math.max(...activities.map((activity) => activity.end)); + // Set the start and end bounds to the earliest start and latest end + await page.goto( + `${planObjectUrl}?tc.mode=fixed&tc.startBound=${start}&tc.endBound=${end}&tc.timeSystem=utc&view=plan.view` + ); } diff --git a/e2e/helper/useSnowTheme.js b/e2e/helper/useSnowTheme.js index 2233034d49..30380c6c02 100644 --- a/e2e/helper/useSnowTheme.js +++ b/e2e/helper/useSnowTheme.js @@ -25,6 +25,6 @@ // await page.addInitScript({ path: path.join(__dirname, 'useSnowTheme.js') }); document.addEventListener('DOMContentLoaded', () => { - const openmct = window.openmct; - openmct.install(openmct.plugins.Snow()); + const openmct = window.openmct; + openmct.install(openmct.plugins.Snow()); }); diff --git a/e2e/playwright-ci.config.js b/e2e/playwright-ci.config.js index a87f516a35..5335bde352 100644 --- a/e2e/playwright-ci.config.js +++ b/e2e/playwright-ci.config.js @@ -9,74 +9,77 @@ const NUM_WORKERS = 2; /** @type {import('@playwright/test').PlaywrightTestConfig} */ const config = { - retries: 2, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=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:coverage', - url: 'http://localhost:8080/#', - timeout: 200 * 1000, - reuseExistingServer: false + retries: 2, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=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:coverage', + url: 'http://localhost:8080/#', + timeout: 200 * 1000, + reuseExistingServer: false + }, + 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: 'only-on-failure', + trace: 'on-first-retry', + video: 'off' + }, + projects: [ + { + name: 'chrome', + testMatch: '**/*.e2e.spec.js', // only run e2e tests + use: { + browserName: 'chromium' + } }, - 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: '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', - viewport: { - width: 2560, - height: 1440 - } - } - }, - { - name: 'firefox', - testMatch: '**/*.e2e.spec.js', // only run e2e tests - grepInvert: /@snapshot/, - use: { - 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' - } + { + name: 'MMOC', + testMatch: '**/*.e2e.spec.js', // only run e2e tests + grepInvert: /@snapshot/, + use: { + browserName: 'chromium', + viewport: { + width: 2560, + height: 1440 } + } + }, + { + name: 'firefox', + testMatch: '**/*.e2e.spec.js', // only run e2e tests + grepInvert: /@snapshot/, + use: { + 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 + } ], - 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' }], - ['github'], - ['@deploysentinel/playwright'] - ] + ['junit', { outputFile: '../test-results/results.xml' }], + ['github'], + ['@deploysentinel/playwright'] + ] }; module.exports = config; diff --git a/e2e/playwright-local.config.js b/e2e/playwright-local.config.js index 845cf68124..f3020ca0e3 100644 --- a/e2e/playwright-local.config.js +++ b/e2e/playwright-local.config.js @@ -7,98 +7,101 @@ const { devices } = require('@playwright/test'); /** @type {import('@playwright/test').PlaywrightTestConfig} */ const config = { - retries: 0, - testDir: 'tests', - testIgnore: '**/*.perf.spec.js', - timeout: 30 * 1000, - webServer: { - command: 'npm run start:coverage', - url: 'http://localhost:8080/#', - timeout: 120 * 1000, - reuseExistingServer: true + retries: 0, + testDir: 'tests', + testIgnore: '**/*.perf.spec.js', + timeout: 30 * 1000, + webServer: { + command: 'npm run start:coverage', + url: 'http://localhost:8080/#', + timeout: 120 * 1000, + reuseExistingServer: true + }, + workers: 1, + use: { + browserName: 'chromium', + baseURL: 'http://localhost:8080/', + headless: false, + ignoreHTTPSErrors: true, + screenshot: 'only-on-failure', + trace: 'retain-on-failure', + video: 'off' + }, + projects: [ + { + name: 'chrome', + use: { + browserName: 'chromium' + } }, - workers: 1, - use: { - browserName: "chromium", - baseURL: 'http://localhost:8080/', - headless: false, - ignoreHTTPSErrors: true, - screenshot: 'only-on-failure', - trace: 'retain-on-failure', - video: 'off' - }, - projects: [ - { - name: 'chrome', - use: { - browserName: 'chromium' - } - }, - { - name: 'MMOC', - testMatch: '**/*.e2e.spec.js', // only run e2e tests - grepInvert: /@snapshot/, - use: { - browserName: 'chromium', - viewport: { - width: 2560, - 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 - } + { + name: 'MMOC', + testMatch: '**/*.e2e.spec.js', // only run e2e tests + grepInvert: /@snapshot/, + use: { + browserName: 'chromium', + viewport: { + width: 2560, + height: 1440 } - ], - reporter: [ - ['list'], - ['html', { - open: 'on-failure', - outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840 - }] + } + }, + { + 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'], + [ + 'html', + { + open: 'on-failure', + outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840 + } ] + ] }; module.exports = config; diff --git a/e2e/playwright-performance.config.js b/e2e/playwright-performance.config.js index 7d71255bd3..1e38be3df2 100644 --- a/e2e/playwright-performance.config.js +++ b/e2e/playwright-performance.config.js @@ -6,38 +6,38 @@ const CI = process.env.CI === 'true'; /** @type {import('@playwright/test').PlaywrightTestConfig} */ const config = { - retries: 1, //Only for debugging purposes for trace: 'on-first-retry' - testDir: 'tests/performance/', - timeout: 60 * 1000, - workers: 1, //Only run in serial with 1 worker - webServer: { - command: 'npm run start', //coverage not generated - 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' }] - ] + retries: 1, //Only for debugging purposes for trace: 'on-first-retry' + testDir: 'tests/performance/', + timeout: 60 * 1000, + workers: 1, //Only run in serial with 1 worker + webServer: { + command: 'npm run start', //coverage not generated + 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 16ada0ef4e..fa776a01b8 100644 --- a/e2e/playwright-visual.config.js +++ b/e2e/playwright-visual.config.js @@ -4,48 +4,51 @@ /** @type {import('@playwright/test').PlaywrightTestConfig<{ theme: string }>} */ const config = { - retries: 0, // 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:coverage', - url: 'http://localhost:8080/#', - timeout: 200 * 1000, - reuseExistingServer: !process.env.CI + retries: 0, // 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:coverage', + url: 'http://localhost:8080/#', + timeout: 200 * 1000, + reuseExistingServer: !process.env.CI + }, + use: { + baseURL: 'http://localhost:8080/', + headless: true, // this needs to remain headless to avoid visual changes due to GPU rendering in headed browsers + ignoreHTTPSErrors: true, + screenshot: 'only-on-failure', + trace: 'on-first-retry', + video: 'off' + }, + projects: [ + { + name: 'chrome', + use: { + browserName: 'chromium' + } }, - use: { - baseURL: 'http://localhost:8080/', - headless: true, // this needs to remain headless to avoid visual changes due to GPU rendering in headed browsers - ignoreHTTPSErrors: true, - screenshot: 'only-on-failure', - trace: 'on-first-retry', - video: 'off' - }, - projects: [ - { - name: 'chrome', - use: { - browserName: 'chromium' - } - }, - { - name: 'chrome-snow-theme', //Runs the same visual tests but with snow-theme enabled - use: { - browserName: 'chromium', - theme: 'snow' - } - } - ], - reporter: [ - ['list'], - ['junit', { outputFile: '../test-results/results.xml' }], - ['html', { - open: 'on-failure', - outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840 - }] + { + name: 'chrome-snow-theme', //Runs the same visual tests but with snow-theme enabled + use: { + browserName: 'chromium', + theme: 'snow' + } + } + ], + reporter: [ + ['list'], + ['junit', { outputFile: '../test-results/results.xml' }], + [ + 'html', + { + open: 'on-failure', + outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840 + } ] + ] }; module.exports = config; diff --git a/e2e/pluginFixtures.js b/e2e/pluginFixtures.js index 375d411639..e30b5d8af4 100644 --- a/e2e/pluginFixtures.js +++ b/e2e/pluginFixtures.js @@ -120,34 +120,31 @@ const theme = 'espresso'; * * @type {string} */ -const myItemsFolderName = "My Items"; +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, testInfo) => { - // 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') }); - } - - // Attach info about the currently running test and its project. - // This will be used by appActions to fill in the created - // domain object's notes. - page.testNotes = [ - `${testInfo.titlePath.join('\n')}`, - `${testInfo.project.name}` - ].join('\n'); - - await use(page); - }, - myItemsFolderName: [myItemsFolderName, { option: true }], - // eslint-disable-next-line no-shadow - openmctConfig: async ({ myItemsFolderName }, use) => { - await use({ myItemsFolderName }); + // 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, testInfo) => { + // 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') }); } + + // Attach info about the currently running test and its project. + // This will be used by appActions to fill in the created + // domain object's notes. + page.testNotes = [`${testInfo.titlePath.join('\n')}`, `${testInfo.project.name}`].join('\n'); + + await use(page); + }, + myItemsFolderName: [myItemsFolderName, { option: true }], + // eslint-disable-next-line no-shadow + openmctConfig: async ({ myItemsFolderName }, use) => { + await use({ myItemsFolderName }); + } }); exports.expect = expect; @@ -157,10 +154,10 @@ exports.expect = expect; * @return {Promise} the stringified stream */ exports.streamToString = async function (readable) { - let result = ''; - for await (const chunk of readable) { - result += chunk; - } + let result = ''; + for await (const chunk of readable) { + result += chunk; + } - return result; + return result; }; diff --git a/e2e/test-data/ExampleLayouts.json b/e2e/test-data/ExampleLayouts.json index 0d5dc07f79..d914c913b4 100644 --- a/e2e/test-data/ExampleLayouts.json +++ b/e2e/test-data/ExampleLayouts.json @@ -274,10 +274,7 @@ "id": "ac0d7eb1-b485-458f-bd2a-a63aa87a3a8a" } ], - "layoutGrid": [ - 10, - 10 - ], + "layoutGrid": [10, 10], "objectStyles": { "ed63cc29-80e2-4e2b-a472-3d6d4adbf310": { "staticStyle": { @@ -1455,9 +1452,7 @@ "id": "64e49fe7-5b36-43db-8347-4550b910de4c", "telemetry": "any", "operation": "greaterThan", - "input": [ - "120" - ], + "input": ["120"], "metadata": "sin" } ] @@ -1475,10 +1470,7 @@ "id": "59f1c4bf-5d36-450c-9668-6546955fc066", "telemetry": "any", "operation": "between", - "input": [ - "120", - "-20" - ], + "input": ["120", "-20"], "metadata": "sin" } ] @@ -1496,9 +1488,7 @@ "id": "6707be12-6a6e-4535-bb97-ab5c86f99934", "telemetry": "any", "operation": "lessThan", - "input": [ - "-20" - ], + "input": ["-20"], "metadata": "sin" } ] @@ -1550,9 +1540,7 @@ "id": "64e49fe7-5b36-43db-8347-4550b910de4c", "telemetry": "any", "operation": "greaterThan", - "input": [ - "120" - ], + "input": ["120"], "metadata": "sin" } ] @@ -1570,10 +1558,7 @@ "id": "59f1c4bf-5d36-450c-9668-6546955fc066", "telemetry": "any", "operation": "between", - "input": [ - "120", - "-20" - ], + "input": ["120", "-20"], "metadata": "sin" } ] @@ -1591,9 +1576,7 @@ "id": "6707be12-6a6e-4535-bb97-ab5c86f99934", "telemetry": "any", "operation": "lessThan", - "input": [ - "-20" - ], + "input": ["-20"], "metadata": "sin" } ] @@ -1645,9 +1628,7 @@ "id": "64e49fe7-5b36-43db-8347-4550b910de4c", "telemetry": "any", "operation": "greaterThan", - "input": [ - "150" - ], + "input": ["150"], "metadata": "sin" } ] @@ -1665,10 +1646,7 @@ "id": "59f1c4bf-5d36-450c-9668-6546955fc066", "telemetry": "any", "operation": "between", - "input": [ - "50", - "-50" - ], + "input": ["50", "-50"], "metadata": "sin" } ] @@ -1720,9 +1698,7 @@ "id": "64e49fe7-5b36-43db-8347-4550b910de4c", "telemetry": "any", "operation": "greaterThan", - "input": [ - "150" - ], + "input": ["150"], "metadata": "sin" } ] @@ -1740,10 +1716,7 @@ "id": "59f1c4bf-5d36-450c-9668-6546955fc066", "telemetry": "any", "operation": "between", - "input": [ - "50", - "-50" - ], + "input": ["50", "-50"], "metadata": "sin" } ] @@ -2204,4 +2177,4 @@ } }, "rootId": "45b24009-dfed-4023-a30b-d31f5e3a2d87" -} \ No newline at end of file +} diff --git a/e2e/test-data/PerformanceDisplayLayout.json b/e2e/test-data/PerformanceDisplayLayout.json index de81d7b4ca..bf339ae5b4 100644 --- a/e2e/test-data/PerformanceDisplayLayout.json +++ b/e2e/test-data/PerformanceDisplayLayout.json @@ -1 +1,90 @@ -{"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 +{ + "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" +} diff --git a/e2e/test-data/PerformanceNotebook.json b/e2e/test-data/PerformanceNotebook.json index ae08431874..e4a3565d67 100644 --- a/e2e/test-data/PerformanceNotebook.json +++ b/e2e/test-data/PerformanceNotebook.json @@ -1 +1,96 @@ -{"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 +{ + "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" +} diff --git a/e2e/test-data/VisualTestData_storage.json b/e2e/test-data/VisualTestData_storage.json index 2ee2eaf6cf..02fe3cd82b 100644 --- a/e2e/test-data/VisualTestData_storage.json +++ b/e2e/test-data/VisualTestData_storage.json @@ -19,4 +19,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/e2e/test-data/examplePlans/ExamplePlan_Large.json b/e2e/test-data/examplePlans/ExamplePlan_Large.json index a81f800947..f01d465a07 100644 --- a/e2e/test-data/examplePlans/ExamplePlan_Large.json +++ b/e2e/test-data/examplePlans/ExamplePlan_Large.json @@ -1077,4 +1077,4 @@ "textColor": "#ffffff" } ] -} \ No newline at end of file +} diff --git a/e2e/test-data/examplePlans/ExamplePlan_Small1.json b/e2e/test-data/examplePlans/ExamplePlan_Small1.json index de32fba310..b41e51b141 100644 --- a/e2e/test-data/examplePlans/ExamplePlan_Small1.json +++ b/e2e/test-data/examplePlans/ExamplePlan_Small1.json @@ -1,44 +1,44 @@ { "Group 1": [ - { - "name": "Past event 1", - "start": 1660320408000, - "end": 1660343797000, - "type": "Group 1", - "color": "orange", - "textColor": "white" - }, - { - "name": "Past event 2", - "start": 1660406808000, - "end": 1660429160000, - "type": "Group 1", - "color": "orange", - "textColor": "white" - }, - { - "name": "Past event 3", - "start": 1660493208000, - "end": 1660503981000, - "type": "Group 1", - "color": "orange", - "textColor": "white" - }, - { - "name": "Past event 4", - "start": 1660579608000, - "end": 1660624108000, - "type": "Group 1", - "color": "orange", - "textColor": "white" - }, - { - "name": "Past event 5", - "start": 1660666008000, - "end": 1660681529000, - "type": "Group 1", - "color": "orange", - "textColor": "white" - } + { + "name": "Past event 1", + "start": 1660320408000, + "end": 1660343797000, + "type": "Group 1", + "color": "orange", + "textColor": "white" + }, + { + "name": "Past event 2", + "start": 1660406808000, + "end": 1660429160000, + "type": "Group 1", + "color": "orange", + "textColor": "white" + }, + { + "name": "Past event 3", + "start": 1660493208000, + "end": 1660503981000, + "type": "Group 1", + "color": "orange", + "textColor": "white" + }, + { + "name": "Past event 4", + "start": 1660579608000, + "end": 1660624108000, + "type": "Group 1", + "color": "orange", + "textColor": "white" + }, + { + "name": "Past event 5", + "start": 1660666008000, + "end": 1660681529000, + "type": "Group 1", + "color": "orange", + "textColor": "white" + } ] -} \ No newline at end of file +} diff --git a/e2e/test-data/examplePlans/ExamplePlan_Small2.json b/e2e/test-data/examplePlans/ExamplePlan_Small2.json index af744f4cc6..24ddb08a5f 100644 --- a/e2e/test-data/examplePlans/ExamplePlan_Small2.json +++ b/e2e/test-data/examplePlans/ExamplePlan_Small2.json @@ -1,38 +1,38 @@ { "Group 1": [ - { - "name": "Group 1 event 1", - "start": 1650320408000, - "end": 1660343797000, - "type": "Group 1", - "color": "orange", - "textColor": "white" - }, - { - "name": "Group 1 event 2", - "start": 1660005808000, - "end": 1660429160000, - "type": "Group 1", - "color": "yellow", - "textColor": "white" - } + { + "name": "Group 1 event 1", + "start": 1650320408000, + "end": 1660343797000, + "type": "Group 1", + "color": "orange", + "textColor": "white" + }, + { + "name": "Group 1 event 2", + "start": 1660005808000, + "end": 1660429160000, + "type": "Group 1", + "color": "yellow", + "textColor": "white" + } ], "Group 2": [ - { - "name": "Group 2 event 1", - "start": 1660320408000, - "end": 1660420408000, - "type": "Group 2", - "color": "green", - "textColor": "white" - }, - { - "name": "Group 2 event 2", - "start": 1660406808000, - "end": 1690429160000, - "type": "Group 2", - "color": "blue", - "textColor": "white" - } + { + "name": "Group 2 event 1", + "start": 1660320408000, + "end": 1660420408000, + "type": "Group 2", + "color": "green", + "textColor": "white" + }, + { + "name": "Group 2 event 2", + "start": 1660406808000, + "end": 1690429160000, + "type": "Group 2", + "color": "blue", + "textColor": "white" + } ] -} \ No newline at end of file +} diff --git a/e2e/test-data/recycled_local_storage.json b/e2e/test-data/recycled_local_storage.json index 9f816a2278..ab4608e7d8 100644 --- a/e2e/test-data/recycled_local_storage.json +++ b/e2e/test-data/recycled_local_storage.json @@ -19,4 +19,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/e2e/tests/framework/appActions.e2e.spec.js b/e2e/tests/framework/appActions.e2e.spec.js index baf9dda124..cbdba6a4e7 100644 --- a/e2e/tests/framework/appActions.e2e.spec.js +++ b/e2e/tests/framework/appActions.e2e.spec.js @@ -21,145 +21,149 @@ *****************************************************************************/ const { test, expect } = require('../../pluginFixtures.js'); -const { createDomainObjectWithDefaults, createNotification, expandEntireTree } = require('../../appActions.js'); +const { + createDomainObjectWithDefaults, + createNotification, + expandEntireTree +} = require('../../appActions.js'); test.describe('AppActions', () => { - test('createDomainObjectsWithDefaults', async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); + test('createDomainObjectsWithDefaults', async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); - 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); - await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer1.name); - await page.goto(timer2.url); - await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer2.name); - await page.goto(timer3.url); - await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer3.name); - }); - - 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); - await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder1.name); - await page.goto(folder2.url); - await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder2.name); - await page.goto(folder3.url); - await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder3.name); - - 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}`); - }); + const e2eFolder = await createDomainObjectWithDefaults(page, { + type: 'Folder', + name: 'e2e folder' }); - test("createNotification", async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); - await createNotification(page, { - message: 'Test info notification', - severity: 'info' - }); - await expect(page.locator('.c-message-banner__message')).toHaveText('Test info notification'); - await expect(page.locator('.c-message-banner')).toHaveClass(/info/); - await page.locator('[aria-label="Dismiss"]').click(); - await createNotification(page, { - message: 'Test alert notification', - severity: 'alert' - }); - await expect(page.locator('.c-message-banner__message')).toHaveText('Test alert notification'); - await expect(page.locator('.c-message-banner')).toHaveClass(/alert/); - await page.locator('[aria-label="Dismiss"]').click(); - await createNotification(page, { - message: 'Test error notification', - severity: 'error' - }); - await expect(page.locator('.c-message-banner__message')).toHaveText('Test error notification'); - await expect(page.locator('.c-message-banner')).toHaveClass(/error/); - await page.locator('[aria-label="Dismiss"]').click(); + + 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); + await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer1.name); + await page.goto(timer2.url); + await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer2.name); + await page.goto(timer3.url); + await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer3.name); }); - test('expandEntireTree', async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); - const rootFolder = await createDomainObjectWithDefaults(page, { - type: 'Folder' - }); - const folder1 = await createDomainObjectWithDefaults(page, { - type: 'Folder', - parent: rootFolder.uuid - }); + 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); + await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder1.name); + await page.goto(folder2.url); + await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder2.name); + await page.goto(folder3.url); + await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder3.name); - await createDomainObjectWithDefaults(page, { - type: 'Clock', - parent: folder1.uuid - }); - const folder2 = await createDomainObjectWithDefaults(page, { - type: 'Folder', - parent: folder1.uuid - }); - await createDomainObjectWithDefaults(page, { - type: 'Folder', - parent: folder1.uuid - }); - await createDomainObjectWithDefaults(page, { - type: 'Display Layout', - parent: folder2.uuid - }); - await createDomainObjectWithDefaults(page, { - type: 'Folder', - parent: folder2.uuid - }); - - await page.goto('./#/browse/mine'); - await expandEntireTree(page); - const treePane = page.getByRole('tree', { - name: "Main Tree" - }); - const treePaneCollapsedItems = treePane.getByRole('treeitem', { expanded: false }); - expect(await treePaneCollapsedItems.count()).toBe(0); - - await page.goto('./#/browse/mine'); - //Click the Create button - await page.click('button:has-text("Create")'); - - // Click the object specified by 'type' - await page.click(`li[role='menuitem']:text("Clock")`); - await expandEntireTree(page, "Create Modal Tree"); - const locatorTree = page.getByRole("tree", { - name: "Create Modal Tree" - }); - const locatorTreeCollapsedItems = locatorTree.locator('role=treeitem[expanded=false]'); - expect(await locatorTreeCollapsedItems.count()).toBe(0); + 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}`); }); + }); + test('createNotification', async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + await createNotification(page, { + message: 'Test info notification', + severity: 'info' + }); + await expect(page.locator('.c-message-banner__message')).toHaveText('Test info notification'); + await expect(page.locator('.c-message-banner')).toHaveClass(/info/); + await page.locator('[aria-label="Dismiss"]').click(); + await createNotification(page, { + message: 'Test alert notification', + severity: 'alert' + }); + await expect(page.locator('.c-message-banner__message')).toHaveText('Test alert notification'); + await expect(page.locator('.c-message-banner')).toHaveClass(/alert/); + await page.locator('[aria-label="Dismiss"]').click(); + await createNotification(page, { + message: 'Test error notification', + severity: 'error' + }); + await expect(page.locator('.c-message-banner__message')).toHaveText('Test error notification'); + await expect(page.locator('.c-message-banner')).toHaveClass(/error/); + await page.locator('[aria-label="Dismiss"]').click(); + }); + test('expandEntireTree', async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + const rootFolder = await createDomainObjectWithDefaults(page, { + type: 'Folder' + }); + const folder1 = await createDomainObjectWithDefaults(page, { + type: 'Folder', + parent: rootFolder.uuid + }); + + await createDomainObjectWithDefaults(page, { + type: 'Clock', + parent: folder1.uuid + }); + const folder2 = await createDomainObjectWithDefaults(page, { + type: 'Folder', + parent: folder1.uuid + }); + await createDomainObjectWithDefaults(page, { + type: 'Folder', + parent: folder1.uuid + }); + await createDomainObjectWithDefaults(page, { + type: 'Display Layout', + parent: folder2.uuid + }); + await createDomainObjectWithDefaults(page, { + type: 'Folder', + parent: folder2.uuid + }); + + await page.goto('./#/browse/mine'); + await expandEntireTree(page); + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); + const treePaneCollapsedItems = treePane.getByRole('treeitem', { expanded: false }); + expect(await treePaneCollapsedItems.count()).toBe(0); + + await page.goto('./#/browse/mine'); + //Click the Create button + await page.click('button:has-text("Create")'); + + // Click the object specified by 'type' + await page.click(`li[role='menuitem']:text("Clock")`); + await expandEntireTree(page, 'Create Modal Tree'); + const locatorTree = page.getByRole('tree', { + name: 'Create Modal Tree' + }); + const locatorTreeCollapsedItems = locatorTree.locator('role=treeitem[expanded=false]'); + expect(await locatorTreeCollapsedItems.count()).toBe(0); + }); }); diff --git a/e2e/tests/framework/baseFixtures.e2e.spec.js b/e2e/tests/framework/baseFixtures.e2e.spec.js index a592aff284..81b1697538 100644 --- a/e2e/tests/framework/baseFixtures.e2e.spec.js +++ b/e2e/tests/framework/baseFixtures.e2e.spec.js @@ -29,27 +29,25 @@ relates to how we've extended it (i.e. ./e2e/baseFixtures.js) and assumptions ma const { test } = require('../../baseFixtures.js'); test.describe('baseFixtures tests', () => { - test('Verify that tests fail if console.error is thrown', async ({ page }) => { - test.fail(); - //Go to baseURL - await page.goto('./', { waitUntil: 'domcontentloaded' }); + test('Verify that tests fail if console.error is thrown', async ({ page }) => { + test.fail(); + //Go to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); - //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! - ]); + //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: 'domcontentloaded' }); - }); - test('Verify that tests pass if console.warn is thrown', async ({ page }) => { - //Go to baseURL - await page.goto('./', { waitUntil: 'domcontentloaded' }); - - //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! - ]); - - }); + //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 index 0da83d46fe..9362fcd707 100644 --- a/e2e/tests/framework/exampleTemplate.e2e.spec.js +++ b/e2e/tests/framework/exampleTemplate.e2e.spec.js @@ -21,28 +21,28 @@ *****************************************************************************/ /* -* 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 -*/ + * 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('../../pluginFixtures'); @@ -58,63 +58,63 @@ const { createDomainObjectWithDefaults } = require('../../appActions'); * 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: 'domcontentloaded' }); + // 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: 'domcontentloaded' }); - // 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' }); + // 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); - }); + // 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"; + /** + * 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); + // 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); - }); + // 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"; + 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); + 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); + // 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); + // 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); - }); + // 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 - }); + /** + * 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 + }); }); /** @@ -131,18 +131,18 @@ test.describe('Renaming Timer Object', () => { * @param {string} newNameForTimer New name for object */ async function renameTimerFrom3DotMenu(page, timerUrl, newNameForTimer) { - // Navigate to the timer object - await page.goto(timerUrl); + // Navigate to the timer object + await page.goto(timerUrl); - // Click on 3 Dot Menu - await page.locator('button[title="More options"]').click(); + // Click on 3 Dot Menu + await page.locator('button[title="More options"]').click(); - // Click text=Edit Properties... - await page.locator('text=Edit Properties...').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); + // Rename the timer object + await page.locator('text=Properties Title Notes >> input[type="text"]').fill(newNameForTimer); - // Click Ok button to Save - await page.locator('button:has-text("OK")').click(); + // Click Ok button to Save + await page.locator('button:has-text("OK")').click(); } diff --git a/e2e/tests/framework/generateVisualTestData.e2e.spec.js b/e2e/tests/framework/generateVisualTestData.e2e.spec.js index 5d6fc3934d..9904176c3c 100644 --- a/e2e/tests/framework/generateVisualTestData.e2e.spec.js +++ b/e2e/tests/framework/generateVisualTestData.e2e.spec.js @@ -35,30 +35,30 @@ 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: 'domcontentloaded' }); - const overlayPlot = await createDomainObjectWithDefaults(page, { type: 'Overlay Plot' }); + //Go to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); + const overlayPlot = await createDomainObjectWithDefaults(page, { type: 'Overlay Plot' }); - // click create button - await page.locator('button:has-text("Create")').click(); + // click create button + await page.locator('button:has-text("Create")').click(); - // add sine wave generator with defaults - await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click(); + // add sine wave generator with defaults + await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click(); - //Add a 5000 ms Delay - await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000'); + //Add a 5000 ms Delay + await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000'); - await Promise.all([ - page.waitForNavigation(), - page.locator('button:has-text("OK")').click(), - //Wait for Save Banner to appear - page.waitForSelector('.c-message-banner__message') - ]); + await Promise.all([ + page.waitForNavigation(), + page.locator('button:has-text("OK")').click(), + //Wait for Save Banner to appear + page.waitForSelector('.c-message-banner__message') + ]); - // focus the overlay plot - await page.goto(overlayPlot.url); + // focus the overlay plot + await page.goto(overlayPlot.url); - await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlot.name); - //Save localStorage for future test execution - await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' }); + await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlot.name); + //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 index 91b23f2bf4..837fdcd7fc 100644 --- a/e2e/tests/framework/pluginFixtures.e2e.spec.js +++ b/e2e/tests/framework/pluginFixtures.e2e.spec.js @@ -29,18 +29,16 @@ 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); - // }); + // 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 index 2f77805826..a0ba50eb89 100644 --- a/e2e/tests/framework/testData.e2e.spec.js +++ b/e2e/tests/framework/testData.e2e.spec.js @@ -21,16 +21,15 @@ *****************************************************************************/ /* -* This test suite template is to be used when verifying Test Data files found in /e2e/test-data/ -*/ + * 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: 'domcontentloaded' }); - }); + //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: 'domcontentloaded' }); + }); }); - diff --git a/e2e/tests/functional/branding.e2e.spec.js b/e2e/tests/functional/branding.e2e.spec.js index f0e8e845b7..588af99940 100644 --- a/e2e/tests/functional/branding.e2e.spec.js +++ b/e2e/tests/functional/branding.e2e.spec.js @@ -27,37 +27,39 @@ This test suite is dedicated to tests which verify branding related components. 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: 'domcontentloaded' }); + test('About Modal launches with basic branding properties', async ({ page }) => { + // Go to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); - // Click About button - await page.click('.l-shell__app-logo'); + // Click About button + await page.click('.l-shell__app-logo'); - // Verify that the NASA Logo Appears - await expect(page.locator('.c-about__image')).toBeVisible(); + // Verify that the NASA Logo Appears + 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').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 @2p', async ({ page }) => { - // Go to baseURL - await page.goto('./', { waitUntil: 'domcontentloaded' }); + // Modify the Build information in 'about' Modal + 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 @2p', async ({ page }) => { + // Go to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); - // Click About button - await page.click('.l-shell__app-logo'); + // Click About button + await page.click('.l-shell__app-logo'); - // Verify that clicking on the third party licenses information opens up another tab on licenses url - const [page2] = await Promise.all([ - page.waitForEvent('popup'), - page.locator('text=click here for third party licensing information').click() - ]); - await page2.waitForLoadState('networkidle'); //Avoids timing issues with juggler/firefox - expect(page2.waitForURL('**/licenses**')).toBeTruthy(); - }); + // Verify that clicking on the third party licenses information opens up another tab on licenses url + const [page2] = await Promise.all([ + page.waitForEvent('popup'), + page.locator('text=click here for third party licensing information').click() + ]); + 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 index 1fcac15049..2ff9e97842 100644 --- a/e2e/tests/functional/couchdb.e2e.spec.js +++ b/e2e/tests/functional/couchdb.e2e.spec.js @@ -21,91 +21,98 @@ *****************************************************************************/ /* -* This test suite is meant to be executed against a couchdb container. More doc to come -* -*/ + * This test suite is meant to be executed against a couchdb container. More doc to come + * + */ const { test, expect } = require('../../pluginFixtures'); -test.describe("CouchDB Status Indicator with mocked responses @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.describe('CouchDB Status Indicator with mocked responses @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({}) + }); }); - 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(); + //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({}) + }); }); - 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(); + //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 with mocked responses @couchdb", () => { - test.use({ failOnConsoleError: false }); - test("'My Items' folder is created if it doesn't exist", async ({ page }) => { - const mockedMissingObjectResponsefromCouchDB = { - status: 404, - contentType: 'application/json', - body: JSON.stringify({}) - }; +test.describe('CouchDB initialization with mocked responses @couchdb', () => { + test.use({ failOnConsoleError: false }); + test("'My Items' folder is created if it doesn't exist", async ({ page }) => { + const mockedMissingObjectResponsefromCouchDB = { + status: 404, + contentType: 'application/json', + body: JSON.stringify({}) + }; - // Override the first request to GET openmct/mine to return a 404. - // This simulates the case of starting Open MCT with a fresh database - // and no "My Items" folder created yet. - await page.route('**/mine', route => { - route.fulfill(mockedMissingObjectResponsefromCouchDB); - }, { times: 1 }); + // Override the first request to GET openmct/mine to return a 404. + // This simulates the case of starting Open MCT with a fresh database + // and no "My Items" folder created yet. + await page.route( + '**/mine', + (route) => { + route.fulfill(mockedMissingObjectResponsefromCouchDB); + }, + { times: 1 } + ); - // Set up promise to verify that a PUT request to create "My Items" - // folder was made. - const putMineFolderRequest = page.waitForRequest(req => - req.url().endsWith('/mine') - && req.method() === 'PUT'); + // Set up promise to verify that a PUT request to create "My Items" + // folder was made. + const putMineFolderRequest = page.waitForRequest( + (req) => req.url().endsWith('/mine') && req.method() === 'PUT' + ); - // Set up promise to verify that a GET request to retrieve "My Items" - // folder was made. - const getMineFolderRequest = page.waitForRequest(req => - req.url().endsWith('/mine') - && req.method() === 'GET'); + // Set up promise to verify that a GET request to retrieve "My Items" + // folder was made. + const getMineFolderRequest = page.waitForRequest( + (req) => req.url().endsWith('/mine') && req.method() === 'GET' + ); - // Go to baseURL. - await page.goto('./', { waitUntil: 'domcontentloaded' }); + // Go to baseURL. + await page.goto('./', { waitUntil: 'domcontentloaded' }); - // Wait for both requests to resolve. - await Promise.all([ - putMineFolderRequest, - getMineFolderRequest - ]); - }); + // Wait for both requests to resolve. + await Promise.all([putMineFolderRequest, getMineFolderRequest]); + }); }); diff --git a/e2e/tests/functional/example/eventGenerator.e2e.spec.js b/e2e/tests/functional/example/eventGenerator.e2e.spec.js index 5d138890de..b8560787d0 100644 --- a/e2e/tests/functional/example/eventGenerator.e2e.spec.js +++ b/e2e/tests/functional/example/eventGenerator.e2e.spec.js @@ -28,32 +28,31 @@ const { test, expect } = require('../../../pluginFixtures'); const { createDomainObjectWithDefaults } = require('../../../appActions'); 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: 'domcontentloaded' }); + test('Can create a Test Event Generator and it results in the table View', async ({ page }) => { + //Go to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); - //Create a name for the object - const newObjectName = 'Test Event Generator'; + //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); + 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 }) => { + 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 - }); - test.fixme('telemetry is sorted by time ascending', async ({ page }) => { + }); + 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" - }); + }); }); diff --git a/e2e/tests/functional/example/generator/sineWaveLimitProvider.e2e.spec.js b/e2e/tests/functional/example/generator/sineWaveLimitProvider.e2e.spec.js index cc25659a84..e644d17a59 100644 --- a/e2e/tests/functional/example/generator/sineWaveLimitProvider.e2e.spec.js +++ b/e2e/tests/functional/example/generator/sineWaveLimitProvider.e2e.spec.js @@ -27,93 +27,113 @@ This test suite is dedicated to tests which verify the basic operations surround 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'); + 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: 'domcontentloaded' }); + //Go to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); - //Click the Create button - await page.click('button:has-text("Create")'); + //Click the Create button + await page.click('button:has-text("Create")'); - // Click Sine Wave Generator - await page.click('text=Sine Wave Generator'); + // 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 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'); + // 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/); + // 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/); + // 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/); + // 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/); + // 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/); + // 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/); + // 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 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 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 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 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'); + // 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'); + 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('button:has-text("OK")') - ]); + //Click text=OK + await Promise.all([page.waitForNavigation(), page.click('button:has-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 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 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]+/); - - }); + // 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 index ad4cbe29ef..793c859ed7 100644 --- a/e2e/tests/functional/forms.e2e.spec.js +++ b/e2e/tests/functional/forms.e2e.spec.js @@ -34,249 +34,265 @@ const jsonFilePath = 'e2e/test-data/ExampleLayouts.json'; const imageFilePath = 'e2e/test-data/rick.jpg'; 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: 'domcontentloaded' }); + test('Required Field indicators appear if title is empty and can be corrected', async ({ + page + }) => { + //Go to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); - await page.click('button:has-text("Create")'); - await page.click(':nth-match(:text("Folder"), 2)'); + 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'); + // 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('button:has-text("OK")')).toBeDisabled(); - await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/); + //Required Field Form Validation + await expect(page.locator('button:has-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'); + //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('button:has-text("OK")')).toBeEnabled(); - await expect(page.locator('.c-form-row__state-indicator').first()).not.toHaveClass(/invalid/); + //Required Field Form Validation is corrected + await expect(page.locator('button:has-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('button:has-text("OK")') - ]); + //Finish Creating Domain Object + await Promise.all([page.waitForNavigation(), page.click('button:has-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); - }); + //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('Form File Input Behavior', () => { - test.beforeEach(async ({ page }) => { - await page.addInitScript({ path: path.join(__dirname, '../../helper', 'addInitFileInputObject.js') }); + test.beforeEach(async ({ page }) => { + await page.addInitScript({ + path: path.join(__dirname, '../../helper', 'addInitFileInputObject.js') }); + }); - test('Can select a JSON file type', async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); + test('Can select a JSON file type', async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); - await page.getByRole('button', { name: ' Create ' }).click(); - await page.getByRole('menuitem', { name: 'JSON File Input Object' }).click(); + await page.getByRole('button', { name: ' Create ' }).click(); + await page.getByRole('menuitem', { name: 'JSON File Input Object' }).click(); - await page.setInputFiles('#fileElem', jsonFilePath); + await page.setInputFiles('#fileElem', jsonFilePath); - await page.getByRole('button', { name: 'Save' }).click(); + await page.getByRole('button', { name: 'Save' }).click(); - const type = await page.locator('#file-input-type').textContent(); - await expect(type).toBe(`"string"`); - }); + const type = await page.locator('#file-input-type').textContent(); + await expect(type).toBe(`"string"`); + }); - test('Can select an image file type', async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); + test('Can select an image file type', async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); - await page.getByRole('button', { name: ' Create ' }).click(); - await page.getByRole('menuitem', { name: 'Image File Input Object' }).click(); + await page.getByRole('button', { name: ' Create ' }).click(); + await page.getByRole('menuitem', { name: 'Image File Input Object' }).click(); - await page.setInputFiles('#fileElem', imageFilePath); + await page.setInputFiles('#fileElem', imageFilePath); - await page.getByRole('button', { name: 'Save' }).click(); + await page.getByRole('button', { name: 'Save' }).click(); - const type = await page.locator('#file-input-type').textContent(); - await expect(type).toBe(`"object"`); - }); + const type = await page.locator('#file-input-type').textContent(); + await expect(type).toBe(`"object"`); + }); }); test.describe('Persistence operations @addInit', () => { - // add non persistable root item - test.beforeEach(async ({ page }) => { - await page.addInitScript({ path: path.join(__dirname, '../../helper', 'addNoneditableObject.js') }); + // add non persistable root item + test.beforeEach(async ({ page }) => { + 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: 'domcontentloaded' }); - - 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('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: 'domcontentloaded' }); + + 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('Persistence operations @couchdb', () => { - test.use({ failOnConsoleError: false }); - test('Editing object properties should generate a single persistence operation', async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/5616' - }); - - await page.goto('./', { waitUntil: 'domcontentloaded' }); - - // Create a new 'Clock' object with default settings - const clock = await createDomainObjectWithDefaults(page, { - type: 'Clock' - }); - - // Count all persistence operations (PUT requests) for this specific object - let putRequestCount = 0; - page.on('request', req => { - if (req.method() === 'PUT' && req.url().endsWith(clock.uuid)) { - putRequestCount += 1; - } - }); - - // Open the edit form for the clock object - await page.click('button[title="More options"]'); - await page.click('li[title="Edit properties of this object."]'); - - // Modify the display format from default 12hr -> 24hr and click 'Save' - await page.locator('select[aria-label="12 or 24 hour clock"]').selectOption({ value: 'clock24' }); - await page.click('button[aria-label="Save"]'); - - await expect.poll(() => putRequestCount, { - message: 'Verify a single PUT request was made to persist the object', - timeout: 1000 - }).toEqual(1); + test.use({ failOnConsoleError: false }); + test('Editing object properties should generate a single persistence operation', async ({ + page + }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/5616' }); - test('Can create an object after a conflict error @couchdb @2p', async ({ page, openmctConfig }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/5982' - }); - const { myItemsFolderName } = openmctConfig; - // Instantiate a second page/tab - const page2 = await page.context().newPage(); - // Both pages: Go to baseURL - await Promise.all([ - page.goto('./', { waitUntil: 'networkidle' }), - page2.goto('./', { waitUntil: 'networkidle' }) - ]); + await page.goto('./', { waitUntil: 'domcontentloaded' }); - //Slow down the test a bit - await expect(page.getByRole('treeitem', { name: `  ${myItemsFolderName}` })).toBeVisible(); - await expect(page2.getByRole('treeitem', { name: `  ${myItemsFolderName}` })).toBeVisible(); - - // Both pages: Click the Create button - await Promise.all([ - page.click('button:has-text("Create")'), - page2.click('button:has-text("Create")') - ]); - - // Both pages: Click "Clock" in the Create menu - await Promise.all([ - page.click(`li[role='menuitem']:text("Clock")`), - page2.click(`li[role='menuitem']:text("Clock")`) - ]); - - // Generate unique names for both objects - const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]'); - const nameInput2 = page2.locator('form[name="mctForm"] .first input[type="text"]'); - - // Both pages: Fill in the 'Name' form field. - await Promise.all([ - nameInput.fill(""), - nameInput.fill(`Clock:${genUuid()}`), - nameInput2.fill(""), - nameInput2.fill(`Clock:${genUuid()}`) - ]); - - // Both pages: Fill the "Notes" section with information about the - // currently running test and its project. - const testNotes = page.testNotes; - const notesInput = page.locator('form[name="mctForm"] #notes-textarea'); - const notesInput2 = page2.locator('form[name="mctForm"] #notes-textarea'); - await Promise.all([ - notesInput.fill(testNotes), - notesInput2.fill(testNotes) - ]); - - // Page 2: Click "OK" to create the domain object and wait for navigation. - // This will update the composition of the parent folder, setting the - // conditions for a conflict error from the first page. - await Promise.all([ - page2.waitForLoadState(), - page2.click('[aria-label="Save"]'), - // Wait for Save Banner to appear - page2.waitForSelector('.c-message-banner__message') - ]); - - // Close Page 2, we're done with it. - await page2.close(); - - // Page 1: Click "OK" to create the domain object and wait for navigation. - // This will trigger a conflict error upon attempting to update - // the composition of the parent folder. - await Promise.all([ - page.waitForLoadState(), - page.click('[aria-label="Save"]'), - // Wait for Save Banner to appear - page.waitForSelector('.c-message-banner__message') - ]); - - // Page 1: Verify that the conflict has occurred and an error notification is displayed. - await expect(page.locator('.c-message-banner__message', { - hasText: "Conflict detected while saving mine" - })).toBeVisible(); - - // Page 1: Start logging console errors from this point on - let errors = []; - page.on('console', (msg) => { - if (msg.type() === 'error') { - errors.push(msg.text()); - } - }); - - // Page 1: Try to create a clock with the page that received the conflict. - const clockAfterConflict = await createDomainObjectWithDefaults(page, { - type: 'Clock' - }); - - // Page 1: Wait for save progress dialog to appear/disappear - await page.locator('.c-message-banner__message', { - hasText: 'Do not navigate away from this page or close this browser tab while this message is displayed.', - state: 'visible' - }).waitFor({ state: 'hidden' }); - - // Page 1: Navigate to 'My Items' and verify that the second clock was created - await page.goto('./#/browse/mine'); - await expect(page.locator(`.c-grid-item__name[title="${clockAfterConflict.name}"]`)).toBeVisible(); - - // Verify no console errors occurred - expect(errors).toHaveLength(0); + // Create a new 'Clock' object with default settings + const clock = await createDomainObjectWithDefaults(page, { + type: 'Clock' }); + + // Count all persistence operations (PUT requests) for this specific object + let putRequestCount = 0; + page.on('request', (req) => { + if (req.method() === 'PUT' && req.url().endsWith(clock.uuid)) { + putRequestCount += 1; + } + }); + + // Open the edit form for the clock object + await page.click('button[title="More options"]'); + await page.click('li[title="Edit properties of this object."]'); + + // Modify the display format from default 12hr -> 24hr and click 'Save' + await page + .locator('select[aria-label="12 or 24 hour clock"]') + .selectOption({ value: 'clock24' }); + await page.click('button[aria-label="Save"]'); + + await expect + .poll(() => putRequestCount, { + message: 'Verify a single PUT request was made to persist the object', + timeout: 1000 + }) + .toEqual(1); + }); + test('Can create an object after a conflict error @couchdb @2p', async ({ + page, + openmctConfig + }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/5982' + }); + const { myItemsFolderName } = openmctConfig; + // Instantiate a second page/tab + const page2 = await page.context().newPage(); + + // Both pages: Go to baseURL + await Promise.all([ + page.goto('./', { waitUntil: 'networkidle' }), + page2.goto('./', { waitUntil: 'networkidle' }) + ]); + + //Slow down the test a bit + await expect(page.getByRole('treeitem', { name: `  ${myItemsFolderName}` })).toBeVisible(); + await expect(page2.getByRole('treeitem', { name: `  ${myItemsFolderName}` })).toBeVisible(); + + // Both pages: Click the Create button + await Promise.all([ + page.click('button:has-text("Create")'), + page2.click('button:has-text("Create")') + ]); + + // Both pages: Click "Clock" in the Create menu + await Promise.all([ + page.click(`li[role='menuitem']:text("Clock")`), + page2.click(`li[role='menuitem']:text("Clock")`) + ]); + + // Generate unique names for both objects + const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]'); + const nameInput2 = page2.locator('form[name="mctForm"] .first input[type="text"]'); + + // Both pages: Fill in the 'Name' form field. + await Promise.all([ + nameInput.fill(''), + nameInput.fill(`Clock:${genUuid()}`), + nameInput2.fill(''), + nameInput2.fill(`Clock:${genUuid()}`) + ]); + + // Both pages: Fill the "Notes" section with information about the + // currently running test and its project. + const testNotes = page.testNotes; + const notesInput = page.locator('form[name="mctForm"] #notes-textarea'); + const notesInput2 = page2.locator('form[name="mctForm"] #notes-textarea'); + await Promise.all([notesInput.fill(testNotes), notesInput2.fill(testNotes)]); + + // Page 2: Click "OK" to create the domain object and wait for navigation. + // This will update the composition of the parent folder, setting the + // conditions for a conflict error from the first page. + await Promise.all([ + page2.waitForLoadState(), + page2.click('[aria-label="Save"]'), + // Wait for Save Banner to appear + page2.waitForSelector('.c-message-banner__message') + ]); + + // Close Page 2, we're done with it. + await page2.close(); + + // Page 1: Click "OK" to create the domain object and wait for navigation. + // This will trigger a conflict error upon attempting to update + // the composition of the parent folder. + await Promise.all([ + page.waitForLoadState(), + page.click('[aria-label="Save"]'), + // Wait for Save Banner to appear + page.waitForSelector('.c-message-banner__message') + ]); + + // Page 1: Verify that the conflict has occurred and an error notification is displayed. + await expect( + page.locator('.c-message-banner__message', { + hasText: 'Conflict detected while saving mine' + }) + ).toBeVisible(); + + // Page 1: Start logging console errors from this point on + let errors = []; + page.on('console', (msg) => { + if (msg.type() === 'error') { + errors.push(msg.text()); + } + }); + + // Page 1: Try to create a clock with the page that received the conflict. + const clockAfterConflict = await createDomainObjectWithDefaults(page, { + type: 'Clock' + }); + + // Page 1: Wait for save progress dialog to appear/disappear + await page + .locator('.c-message-banner__message', { + hasText: + 'Do not navigate away from this page or close this browser tab while this message is displayed.', + state: 'visible' + }) + .waitFor({ state: 'hidden' }); + + // Page 1: Navigate to 'My Items' and verify that the second clock was created + await page.goto('./#/browse/mine'); + await expect( + page.locator(`.c-grid-item__name[title="${clockAfterConflict.name}"]`) + ).toBeVisible(); + + // Verify no console errors occurred + expect(errors).toHaveLength(0); + }); }); 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}) => {}); + 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/functional/menu.e2e.spec.js b/e2e/tests/functional/menu.e2e.spec.js index 97077312e8..97b913bdc1 100644 --- a/e2e/tests/functional/menu.e2e.spec.js +++ b/e2e/tests/functional/menu.e2e.spec.js @@ -29,21 +29,31 @@ const { test, expect } = require('../../baseFixtures.js'); const path = require('path'); test.describe('Persistence operations @addInit', () => { - // add non persistable root item - test.beforeEach(async ({ page }) => { - await page.addInitScript({ path: path.join(__dirname, '../../helper', 'addNoneditableObject.js') }); + // add non persistable root item + test.beforeEach(async ({ page }) => { + await page.addInitScript({ + path: path.join(__dirname, '../../helper', 'addNoneditableObject.js') + }); + }); + + test('Non-persistable objects should not show persistence related actions', async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + await page.locator('text=Persistence Testing').first().click({ + button: 'right' }); - test('Non-persistable objects should not show persistence related actions', async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); + const menuOptions = page.locator('.c-menu li'); - await page.locator('text=Persistence Testing').first().click({ - button: 'right' - }); - - 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']); - }); + 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' + ]); + }); }); diff --git a/e2e/tests/functional/moveAndLinkObjects.e2e.spec.js b/e2e/tests/functional/moveAndLinkObjects.e2e.spec.js index 6ed01a880e..a1bb6610dd 100644 --- a/e2e/tests/functional/moveAndLinkObjects.e2e.spec.js +++ b/e2e/tests/functional/moveAndLinkObjects.e2e.spec.js @@ -1,276 +1,303 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2023, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -/* -This test suite is dedicated to tests which 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 - }); - const grandchildFolder = await createDomainObjectWithDefaults(page, { - type: 'Folder', - name: 'Grandchild Folder', - parent: childFolder.uuid - }); - - // Attempt to move parent to its own grandparent - await page.locator('button[title="Show selected item in tree"]').click(); - - const treePane = page.getByRole('tree', { - name: 'Main Tree' - }); - await treePane.getByRole('treeitem', { - name: 'Parent Folder' - }).click({ - button: 'right' - }); - - await page.getByRole('menuitem', { - name: /Move/ - }).click(); - - const createModalTree = page.getByRole('tree', { - name: "Create Modal Tree" - }); - const myItemsLocatorTreeItem = createModalTree.getByRole('treeitem', { - name: myItemsFolderName - }); - await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click(); - await myItemsLocatorTreeItem.click(); - - const parentFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { - name: parentFolder.name - }); - await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); - await parentFolderLocatorTreeItem.click(); - await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); - - const childFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { - name: new RegExp(childFolder.name) - }); - await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); - await childFolderLocatorTreeItem.click(); - await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); - - const grandchildFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { - name: grandchildFolder.name - }); - await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); - await grandchildFolderLocatorTreeItem.click(); - await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); - - await parentFolderLocatorTreeItem.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 treePane.getByRole('treeitem', { - name: new RegExp(childFolder.name) - }).click({ - button: 'right' - }); - await page.getByRole('menuitem', { - name: /Move/ - }).click(); - await myItemsLocatorTreeItem.click(); - - await page.locator('[aria-label="Save"]').click(); - const myItemsPaneTreeItem = treePane.getByRole('treeitem', { - name: myItemsFolderName - }); - - // Expect that Child Folder is in My Items, the root folder - expect(myItemsPaneTreeItem.locator('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[role="menuitem"]: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('button:has-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[role="menuitem"]: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 = 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('button:has-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 = 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 - }); - const grandchildFolder = await createDomainObjectWithDefaults(page, { - type: 'Folder', - name: 'Grandchild Folder', - parent: childFolder.uuid - }); - - // Attempt to move parent to its own grandparent - await page.locator('button[title="Show selected item in tree"]').click(); - - const treePane = page.getByRole('tree', { - name: 'Main Tree' - }); - await treePane.getByRole('treeitem', { - name: 'Parent Folder' - }).click({ - button: 'right' - }); - - await page.getByRole('menuitem', { - name: /Move/ - }).click(); - - const createModalTree = page.getByRole('tree', { - name: "Create Modal Tree" - }); - const myItemsLocatorTreeItem = createModalTree.getByRole('treeitem', { - name: myItemsFolderName - }); - await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click(); - await myItemsLocatorTreeItem.click(); - - const parentFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { - name: parentFolder.name - }); - await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); - await parentFolderLocatorTreeItem.click(); - await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); - - const childFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { - name: new RegExp(childFolder.name) - }); - await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); - await childFolderLocatorTreeItem.click(); - await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); - - const grandchildFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { - name: grandchildFolder.name - }); - await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); - await grandchildFolderLocatorTreeItem.click(); - await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); - - await parentFolderLocatorTreeItem.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 treePane.getByRole('treeitem', { - name: new RegExp(childFolder.name) - }).click({ - button: 'right' - }); - await page.getByRole('menuitem', { - name: /Link/ - }).click(); - await myItemsLocatorTreeItem.click(); - - await page.locator('[aria-label="Save"]').click(); - const myItemsPaneTreeItem = treePane.getByRole('treeitem', { - name: myItemsFolderName - }); - - // Expect that Child Folder is in My Items, the root folder - expect(myItemsPaneTreeItem.locator('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 -}); +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2023, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/* +This test suite is dedicated to tests which 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 + }); + const grandchildFolder = await createDomainObjectWithDefaults(page, { + type: 'Folder', + name: 'Grandchild Folder', + parent: childFolder.uuid + }); + + // Attempt to move parent to its own grandparent + await page.locator('button[title="Show selected item in tree"]').click(); + + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); + await treePane + .getByRole('treeitem', { + name: 'Parent Folder' + }) + .click({ + button: 'right' + }); + + await page + .getByRole('menuitem', { + name: /Move/ + }) + .click(); + + const createModalTree = page.getByRole('tree', { + name: 'Create Modal Tree' + }); + const myItemsLocatorTreeItem = createModalTree.getByRole('treeitem', { + name: myItemsFolderName + }); + await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click(); + await myItemsLocatorTreeItem.click(); + + const parentFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { + name: parentFolder.name + }); + await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); + await parentFolderLocatorTreeItem.click(); + await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); + + const childFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { + name: new RegExp(childFolder.name) + }); + await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); + await childFolderLocatorTreeItem.click(); + await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); + + const grandchildFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { + name: grandchildFolder.name + }); + await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); + await grandchildFolderLocatorTreeItem.click(); + await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); + + await parentFolderLocatorTreeItem.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 treePane + .getByRole('treeitem', { + name: new RegExp(childFolder.name) + }) + .click({ + button: 'right' + }); + await page + .getByRole('menuitem', { + name: /Move/ + }) + .click(); + await myItemsLocatorTreeItem.click(); + + await page.locator('[aria-label="Save"]').click(); + const myItemsPaneTreeItem = treePane.getByRole('treeitem', { + name: myItemsFolderName + }); + + // Expect that Child Folder is in My Items, the root folder + expect(myItemsPaneTreeItem.locator('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[role="menuitem"]: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('button:has-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[role="menuitem"]: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 = 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('button:has-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 = 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 + }); + const grandchildFolder = await createDomainObjectWithDefaults(page, { + type: 'Folder', + name: 'Grandchild Folder', + parent: childFolder.uuid + }); + + // Attempt to move parent to its own grandparent + await page.locator('button[title="Show selected item in tree"]').click(); + + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); + await treePane + .getByRole('treeitem', { + name: 'Parent Folder' + }) + .click({ + button: 'right' + }); + + await page + .getByRole('menuitem', { + name: /Move/ + }) + .click(); + + const createModalTree = page.getByRole('tree', { + name: 'Create Modal Tree' + }); + const myItemsLocatorTreeItem = createModalTree.getByRole('treeitem', { + name: myItemsFolderName + }); + await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click(); + await myItemsLocatorTreeItem.click(); + + const parentFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { + name: parentFolder.name + }); + await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); + await parentFolderLocatorTreeItem.click(); + await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); + + const childFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { + name: new RegExp(childFolder.name) + }); + await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); + await childFolderLocatorTreeItem.click(); + await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); + + const grandchildFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { + name: grandchildFolder.name + }); + await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); + await grandchildFolderLocatorTreeItem.click(); + await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); + + await parentFolderLocatorTreeItem.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 treePane + .getByRole('treeitem', { + name: new RegExp(childFolder.name) + }) + .click({ + button: 'right' + }); + await page + .getByRole('menuitem', { + name: /Link/ + }) + .click(); + await myItemsLocatorTreeItem.click(); + + await page.locator('[aria-label="Save"]').click(); + const myItemsPaneTreeItem = treePane.getByRole('treeitem', { + name: myItemsFolderName + }); + + // Expect that Child Folder is in My Items, the root folder + expect(myItemsPaneTreeItem.locator('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/notification.e2e.spec.js b/e2e/tests/functional/notification.e2e.spec.js index 472d87d11c..69719ea0b3 100644 --- a/e2e/tests/functional/notification.e2e.spec.js +++ b/e2e/tests/functional/notification.e2e.spec.js @@ -28,85 +28,91 @@ const { createDomainObjectWithDefaults, createNotification } = require('../../ap const { test, expect } = require('../../pluginFixtures'); test.describe('Notifications List', () => { - test('Notifications can be dismissed individually', async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/6122' - }); - - // Go to baseURL - await page.goto('./', { waitUntil: 'domcontentloaded' }); - - // Create an error notification with the message "Error message" - await createNotification(page, { - severity: 'error', - message: 'Error message' - }); - - // Create an alert notification with the message "Alert message" - await createNotification(page, { - severity: 'alert', - message: 'Alert message' - }); - - // Verify that there is a button with aria-label "Review 2 Notifications" - expect(await page.locator('button[aria-label="Review 2 Notifications"]').count()).toBe(1); - - // Click on button with aria-label "Review 2 Notifications" - await page.click('button[aria-label="Review 2 Notifications"]'); - - // Click on button with aria-label="Dismiss notification of Error message" - await page.click('button[aria-label="Dismiss notification of Error message"]'); - - // Verify there is no a notification (listitem) with the text "Error message" since it was dismissed - expect(await page.locator('div[role="dialog"] div[role="listitem"]').innerText()).not.toContain('Error message'); - - // Verify there is still a notification (listitem) with the text "Alert message" - expect(await page.locator('div[role="dialog"] div[role="listitem"]').innerText()).toContain('Alert message'); - - // Click on button with aria-label="Dismiss notification of Alert message" - await page.click('button[aria-label="Dismiss notification of Alert message"]'); - - // Verify that there is no dialog since the notification overlay was closed automatically after all notifications were dismissed - expect(await page.locator('div[role="dialog"]').count()).toBe(0); + test('Notifications can be dismissed individually', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/6122' }); + + // Go to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + // Create an error notification with the message "Error message" + await createNotification(page, { + severity: 'error', + message: 'Error message' + }); + + // Create an alert notification with the message "Alert message" + await createNotification(page, { + severity: 'alert', + message: 'Alert message' + }); + + // Verify that there is a button with aria-label "Review 2 Notifications" + expect(await page.locator('button[aria-label="Review 2 Notifications"]').count()).toBe(1); + + // Click on button with aria-label "Review 2 Notifications" + await page.click('button[aria-label="Review 2 Notifications"]'); + + // Click on button with aria-label="Dismiss notification of Error message" + await page.click('button[aria-label="Dismiss notification of Error message"]'); + + // Verify there is no a notification (listitem) with the text "Error message" since it was dismissed + expect(await page.locator('div[role="dialog"] div[role="listitem"]').innerText()).not.toContain( + 'Error message' + ); + + // Verify there is still a notification (listitem) with the text "Alert message" + expect(await page.locator('div[role="dialog"] div[role="listitem"]').innerText()).toContain( + 'Alert message' + ); + + // Click on button with aria-label="Dismiss notification of Alert message" + await page.click('button[aria-label="Dismiss notification of Alert message"]'); + + // Verify that there is no dialog since the notification overlay was closed automatically after all notifications were dismissed + expect(await page.locator('div[role="dialog"]').count()).toBe(0); + }); }); test.describe('Notification Overlay', () => { - test('Closing notification list after notification banner disappeared does not cause it to open automatically', async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/6130' - }); - - // Go to baseURL - await page.goto('./', { waitUntil: 'domcontentloaded' }); - - // Create a new Display Layout object - await createDomainObjectWithDefaults(page, { type: 'Display Layout' }); - - // Click on the button "Review 1 Notification" - await page.click('button[aria-label="Review 1 Notification"]'); - - // Verify that Notification List is open - expect(await page.locator('div[role="dialog"]').isVisible()).toBe(true); - - // Wait until there is no Notification Banner - await page.waitForSelector('div[role="alert"]', { state: 'detached'}); - - // Click on the "Close" button of the Notification List - await page.click('button[aria-label="Close"]'); - - // On the Display Layout object, click on the "Edit" button - await page.click('button[title="Edit"]'); - - // Click on the "Save" button - await page.click('button[title="Save"]'); - - // Click on the "Save and Finish Editing" option - await page.click('li[title="Save and Finish Editing"]'); - - // Verify that Notification List is NOT open - expect(await page.locator('div[role="dialog"]').isVisible()).toBe(false); + test('Closing notification list after notification banner disappeared does not cause it to open automatically', async ({ + page + }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/6130' }); + + // Go to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + // Create a new Display Layout object + await createDomainObjectWithDefaults(page, { type: 'Display Layout' }); + + // Click on the button "Review 1 Notification" + await page.click('button[aria-label="Review 1 Notification"]'); + + // Verify that Notification List is open + expect(await page.locator('div[role="dialog"]').isVisible()).toBe(true); + + // Wait until there is no Notification Banner + await page.waitForSelector('div[role="alert"]', { state: 'detached' }); + + // Click on the "Close" button of the Notification List + await page.click('button[aria-label="Close"]'); + + // On the Display Layout object, click on the "Edit" button + await page.click('button[title="Edit"]'); + + // Click on the "Save" button + await page.click('button[title="Save"]'); + + // Click on the "Save and Finish Editing" option + await page.click('li[title="Save and Finish Editing"]'); + + // Verify that Notification List is NOT open + expect(await page.locator('div[role="dialog"]').isVisible()).toBe(false); + }); }); diff --git a/e2e/tests/functional/planning/ganttChart.e2e.spec.js b/e2e/tests/functional/planning/ganttChart.e2e.spec.js index eb86802c9d..eb3b9408ef 100644 --- a/e2e/tests/functional/planning/ganttChart.e2e.spec.js +++ b/e2e/tests/functional/planning/ganttChart.e2e.spec.js @@ -20,84 +20,108 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ const { test, expect } = require('../../../pluginFixtures'); -const { createPlanFromJSON, createDomainObjectWithDefaults, selectInspectorTab } = require('../../../appActions'); +const { + createPlanFromJSON, + createDomainObjectWithDefaults, + selectInspectorTab +} = require('../../../appActions'); const testPlan1 = require('../../../test-data/examplePlans/ExamplePlan_Small1.json'); const testPlan2 = require('../../../test-data/examplePlans/ExamplePlan_Small2.json'); -const { assertPlanActivities, setBoundsToSpanAllActivities } = require('../../../helper/planningUtils'); +const { + assertPlanActivities, + setBoundsToSpanAllActivities +} = require('../../../helper/planningUtils'); const { getPreciseDuration } = require('../../../../src/utils/duration'); -test.describe("Gantt Chart", () => { - let ganttChart; - let plan; - test.beforeEach(async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); - ganttChart = await createDomainObjectWithDefaults(page, { - type: 'Gantt Chart' - }); - plan = await createPlanFromJSON(page, { - json: testPlan1, - parent: ganttChart.uuid - }); +test.describe('Gantt Chart', () => { + let ganttChart; + let plan; + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + ganttChart = await createDomainObjectWithDefaults(page, { + type: 'Gantt Chart' + }); + plan = await createPlanFromJSON(page, { + json: testPlan1, + parent: ganttChart.uuid + }); + }); + + test('Displays all plan events', async ({ page }) => { + await page.goto(ganttChart.url); + + await assertPlanActivities(page, testPlan1, ganttChart.url); + }); + test('Replaces a plan with a new plan', async ({ page }) => { + await assertPlanActivities(page, testPlan1, ganttChart.url); + await createPlanFromJSON(page, { + json: testPlan2, + parent: ganttChart.uuid + }); + const replaceModal = page + .getByRole('dialog') + .filter({ hasText: 'This action will replace the current Plan. Do you want to continue?' }); + await expect(replaceModal).toBeVisible(); + await page.getByRole('button', { name: 'OK' }).click(); + + await assertPlanActivities(page, testPlan2, ganttChart.url); + }); + test('Can select a single activity and display its details in the inspector', async ({ + page + }) => { + test.slow(); + await page.goto(ganttChart.url); + + await setBoundsToSpanAllActivities(page, testPlan1, ganttChart.url); + + const activities = Object.values(testPlan1).flat(); + const activity = activities[0]; + await page + .locator('g') + .filter({ hasText: new RegExp(activity.name) }) + .click(); + await selectInspectorTab(page, 'Activity'); + + const startDateTime = await page + .locator( + '.c-inspect-properties__label:has-text("Start DateTime")+.c-inspect-properties__value' + ) + .innerText(); + const endDateTime = await page + .locator('.c-inspect-properties__label:has-text("End DateTime")+.c-inspect-properties__value') + .innerText(); + const duration = await page + .locator('.c-inspect-properties__label:has-text("duration")+.c-inspect-properties__value') + .innerText(); + + const expectedStartDate = new Date(activity.start).toISOString(); + const actualStartDate = new Date(startDateTime).toISOString(); + const expectedEndDate = new Date(activity.end).toISOString(); + const actualEndDate = new Date(endDateTime).toISOString(); + const expectedDuration = getPreciseDuration(activity.end - activity.start); + const actualDuration = duration; + + expect(expectedStartDate).toEqual(actualStartDate); + expect(expectedEndDate).toEqual(actualEndDate); + expect(expectedDuration).toEqual(actualDuration); + }); + test("Displays a Plan's draft status", async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/6641' }); - test("Displays all plan events", async ({ page }) => { - await page.goto(ganttChart.url); + // Mark the Plan's status as draft in the OpenMCT API + await page.evaluate(async (planObject) => { + await window.openmct.status.set(planObject.uuid, 'draft'); + }, plan); - await assertPlanActivities(page, testPlan1, ganttChart.url); - }); - test("Replaces a plan with a new plan", async ({ page }) => { - await assertPlanActivities(page, testPlan1, ganttChart.url); - await createPlanFromJSON(page, { - json: testPlan2, - parent: ganttChart.uuid - }); - const replaceModal = page.getByRole('dialog').filter({ hasText: "This action will replace the current Plan. Do you want to continue?" }); - await expect(replaceModal).toBeVisible(); - await page.getByRole('button', { name: 'OK' }).click(); + // Navigate to the Gantt Chart + await page.goto(ganttChart.url); - await assertPlanActivities(page, testPlan2, ganttChart.url); - }); - test("Can select a single activity and display its details in the inspector", async ({ page }) => { - test.slow(); - await page.goto(ganttChart.url); - - await setBoundsToSpanAllActivities(page, testPlan1, ganttChart.url); - - const activities = Object.values(testPlan1).flat(); - const activity = activities[0]; - await page.locator('g').filter({ hasText: new RegExp(activity.name) }).click(); - await selectInspectorTab(page, 'Activity'); - - const startDateTime = await page.locator('.c-inspect-properties__label:has-text("Start DateTime")+.c-inspect-properties__value').innerText(); - const endDateTime = await page.locator('.c-inspect-properties__label:has-text("End DateTime")+.c-inspect-properties__value').innerText(); - const duration = await page.locator('.c-inspect-properties__label:has-text("duration")+.c-inspect-properties__value').innerText(); - - const expectedStartDate = new Date(activity.start).toISOString(); - const actualStartDate = new Date(startDateTime).toISOString(); - const expectedEndDate = new Date(activity.end).toISOString(); - const actualEndDate = new Date(endDateTime).toISOString(); - const expectedDuration = getPreciseDuration(activity.end - activity.start); - const actualDuration = duration; - - expect(expectedStartDate).toEqual(actualStartDate); - expect(expectedEndDate).toEqual(actualEndDate); - expect(expectedDuration).toEqual(actualDuration); - }); - test("Displays a Plan's draft status", async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/6641' - }); - - // Mark the Plan's status as draft in the OpenMCT API - await page.evaluate(async (planObject) => { - await window.openmct.status.set(planObject.uuid, 'draft'); - }, plan); - - // Navigate to the Gantt Chart - await page.goto(ganttChart.url); - - // Assert that the Plan's status is displayed as draft - expect(await page.locator('.u-contents.c-swimlane.is-status--draft').count()).toBe(Object.keys(testPlan1).length); - }); + // Assert that the Plan's status is displayed as draft + expect(await page.locator('.u-contents.c-swimlane.is-status--draft').count()).toBe( + Object.keys(testPlan1).length + ); + }); }); diff --git a/e2e/tests/functional/planning/plan.e2e.spec.js b/e2e/tests/functional/planning/plan.e2e.spec.js index b5a29623b1..c4a65ced3c 100644 --- a/e2e/tests/functional/planning/plan.e2e.spec.js +++ b/e2e/tests/functional/planning/plan.e2e.spec.js @@ -24,16 +24,16 @@ const { createPlanFromJSON } = require('../../../appActions'); const testPlan1 = require('../../../test-data/examplePlans/ExamplePlan_Small1.json'); const { assertPlanActivities } = require('../../../helper/planningUtils'); -test.describe("Plan", () => { - let plan; - test.beforeEach(async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); - plan = await createPlanFromJSON(page, { - json: testPlan1 - }); +test.describe('Plan', () => { + let plan; + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + plan = await createPlanFromJSON(page, { + json: testPlan1 }); + }); - test("Displays all plan events", async ({ page }) => { - await assertPlanActivities(page, testPlan1, plan.url); - }); + test('Displays all plan events', async ({ page }) => { + await assertPlanActivities(page, testPlan1, plan.url); + }); }); diff --git a/e2e/tests/functional/planning/timestrip.e2e.spec.js b/e2e/tests/functional/planning/timestrip.e2e.spec.js index eb197029d6..0bff22ffd4 100644 --- a/e2e/tests/functional/planning/timestrip.e2e.spec.js +++ b/e2e/tests/functional/planning/timestrip.e2e.spec.js @@ -24,158 +24,164 @@ 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_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: 'domcontentloaded' }); - - 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); - }); +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: 'domcontentloaded' }); + + 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/functional/plugins/clocks/clock.e2e.spec.js b/e2e/tests/functional/plugins/clocks/clock.e2e.spec.js index 4a4c6f7498..565da1e116 100644 --- a/e2e/tests/functional/plugins/clocks/clock.e2e.spec.js +++ b/e2e/tests/functional/plugins/clocks/clock.e2e.spec.js @@ -27,40 +27,40 @@ This test suite is dedicated to tests which verify the basic operations surround const { test, expect } = require('../../../../baseFixtures'); 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({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/4878' - }); - //Go to baseURL - await page.goto('./', { waitUntil: 'domcontentloaded' }); - - //Click the Create button - await page.click('button:has-text("Create")'); - - // Click Clock - await page.click('text=Clock'); - - // Click .icon-arrow-down - await page.locator('.icon-arrow-down').click(); - //verify if the autocomplete dropdown is visible - 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(".c-input--autocomplete__options")).toBeHidden(); - - // Click timezone input to open dropdown - await page.locator('.c-input--autocomplete__input').click(); - //verify if the autocomplete dropdown is visible - 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(".c-input--autocomplete__options")).toBeHidden(); - + test('Timezone dropdown will collapse when clicked outside or on dropdown icon again', async ({ + page + }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/4878' }); + //Go to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + //Click the Create button + await page.click('button:has-text("Create")'); + + // Click Clock + await page.click('text=Clock'); + + // Click .icon-arrow-down + await page.locator('.icon-arrow-down').click(); + //verify if the autocomplete dropdown is visible + 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('.c-input--autocomplete__options')).toBeHidden(); + + // Click timezone input to open dropdown + await page.locator('.c-input--autocomplete__input').click(); + //verify if the autocomplete dropdown is visible + 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('.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 index b91e0c5561..d34d0be52f 100644 --- a/e2e/tests/functional/plugins/clocks/remoteClock.e2e.spec.js +++ b/e2e/tests/functional/plugins/clocks/remoteClock.e2e.spec.js @@ -25,17 +25,17 @@ 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 + // 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/functional/plugins/conditionSet/conditionSet.e2e.spec.js b/e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js index 7a48f5e649..133f5cad1d 100644 --- a/e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js +++ b/e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js @@ -33,293 +33,336 @@ let conditionSetUrl; let getConditionSetIdentifierFromUrl; 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: 'domcontentloaded' }); - await page.click('button:has-text("Create")'); + test.beforeAll(async ({ browser }) => { + //TODO: This needs to be refactored + const context = await browser.newContext(); + const page = await context.newPage(); + await page.goto('./', { waitUntil: 'domcontentloaded' }); + await page.click('button:has-text("Create")'); - await page.locator('li[role="menuitem"]:has-text("Condition Set")').click(); + await page.locator('li[role="menuitem"]:has-text("Condition Set")').click(); - await Promise.all([ - page.waitForNavigation(), - page.click('button:has-text("OK")') - ]); + await Promise.all([page.waitForNavigation(), page.click('button:has-text("OK")')]); - //Save localStorage for future test execution - await context.storageState({ path: './e2e/test-data/recycled_local_storage.json' }); + //Save localStorage for future test execution + await context.storageState({ path: './e2e/test-data/recycled_local_storage.json' }); - //Set object identifier from url - conditionSetUrl = page.url(); + //Set object identifier from url + conditionSetUrl = page.url(); - getConditionSetIdentifierFromUrl = conditionSetUrl.split('/').pop().split('?')[0]; - console.debug(`getConditionSetIdentifierFromUrl: ${getConditionSetIdentifierFromUrl}`); - await page.close(); - }); + getConditionSetIdentifierFromUrl = conditionSetUrl.split('/').pop().split('?')[0]; + console.debug(`getConditionSetIdentifierFromUrl: ${getConditionSetIdentifierFromUrl}`); + await page.close(); + }); - //Load localStorage for subsequent tests - test.use({ storageState: './e2e/test-data/recycled_local_storage.json' }); + //Load localStorage for subsequent tests + 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 @localStorage', async ({ page }) => { - //Navigate to baseURL with injected localStorage - await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); + //Begin suite of tests again localStorage + 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. 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 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 - expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy(); + //Assertions on loaded Condition Set in Inspector + expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy(); - //Reload Page - await Promise.all([ - page.reload(), - page.waitForLoadState('networkidle') - ]); + //Reload Page + await Promise.all([page.reload(), page.waitForLoadState('networkidle')]); - //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 - expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy(); + //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 + expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy(); + }); + test('condition set object can be modified on @localStorage', async ({ page, openmctConfig }) => { + const { myItemsFolderName } = openmctConfig; - }); - test('condition set object can be modified on @localStorage', async ({ page, openmctConfig }) => { - const { myItemsFolderName } = openmctConfig; + await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); - await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); + //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 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 + // Click Edit Button + await page.locator('text=Conditions View Snapshot >> button').nth(3).click(); - //Update the Condition Set properties - // Click Edit Button - await page.locator('text=Conditions View Snapshot >> button').nth(3).click(); + //Edit Condition Set Name from main view + await page + .locator('.l-browse-bar__object-name') + .filter({ hasText: 'Unnamed Condition Set' }) + .first() + .fill('Renamed Condition Set'); + await page + .locator('.l-browse-bar__object-name') + .filter({ hasText: 'Renamed Condition Set' }) + .first() + .press('Enter'); + // Click Save Button + await page + .locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button') + .nth(1) + .click(); + // Click Save and Finish Editing Option + await page.locator('text=Save and Finish Editing').click(); - //Edit Condition Set Name from main view - await page.locator('.l-browse-bar__object-name').filter({ hasText: 'Unnamed Condition Set' }).first().fill('Renamed Condition Set'); - await page.locator('.l-browse-bar__object-name').filter({ hasText: 'Renamed Condition Set' }).first().press('Enter'); - // Click Save Button - await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); - // Click Save and Finish Editing Option - await page.locator('text=Save and Finish Editing').click(); + //Verify Main section reflects updated Name Property + await expect + .soft(page.locator('.l-browse-bar__object-name')) + .toContainText('Renamed Condition Set'); - //Verify Main section reflects updated Name Property - await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set'); + // Verify Inspector properties + // Verify Inspector has updated Name property + expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); + // Verify Inspector Details has updated Name property + expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy(); - // Verify Inspector properties - // Verify Inspector has updated Name property - expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); - // Verify Inspector Details has updated Name property - 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 ${myItemsFolderName} >> span >> nth=3`).click(); + // Verify Condition Set Object is renamed in Tree + expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); + // Verify Search Tree reflects renamed Name property + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed'); + expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); - // Verify Tree reflects updated Name proprety - // Expand Tree - await page.locator(`text=Open MCT ${myItemsFolderName} >> span >> nth=3`).click(); - // Verify Condition Set Object is renamed in Tree - expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); - // Verify Search Tree reflects renamed Name property - 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([page.reload(), page.waitForLoadState('networkidle')]); - //Reload Page - await Promise.all([ - page.reload(), - page.waitForLoadState('networkidle') - ]); + //Verify Main section reflects updated Name Property + await expect + .soft(page.locator('.l-browse-bar__object-name')) + .toContainText('Renamed Condition Set'); - //Verify Main section reflects updated Name Property - await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set'); + // Verify Inspector properties + // Verify Inspector has updated Name property + expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); + // Verify Inspector Details has updated Name property + expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy(); - // Verify Inspector properties - // Verify Inspector has updated Name property - expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); - // Verify Inspector Details has updated Name property - 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 ${myItemsFolderName} >> span >> nth=3`).click(); + // Verify Condition Set Object is renamed in Tree + expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); + // Verify Search Tree reflects renamed Name property + 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: 'domcontentloaded' }); - // Verify Tree reflects updated Name proprety - // Expand Tree - await page.locator(`text=Open MCT ${myItemsFolderName} >> span >> nth=3`).click(); - // Verify Condition Set Object is renamed in Tree - expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); - // Verify Search Tree reflects renamed Name property - 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: 'domcontentloaded' }); + //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(); - //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(); + const numberOfConditionSetsToStart = await page + .locator('a:has-text("Unnamed Condition Set Condition Set")') + .count(); - const numberOfConditionSetsToStart = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count(); + // Search for Unnamed Condition Set + 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(); - // Search for Unnamed Condition Set - 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 'Remove' and press OK + await page.locator('li[role="menuitem"]:has-text("Remove")').click(); + await page.locator('button:has-text("OK")').click(); - // Click 'Remove' and press OK - await page.locator('li[role="menuitem"]:has-text("Remove")').click(); - await page.locator('button:has-text("OK")').click(); + //Expect Unnamed Condition Set to be removed in Main View + const numberOfConditionSetsAtEnd = await page + .locator('a:has-text("Unnamed Condition Set Condition Set")') + .count(); - //Expect Unnamed Condition Set to be removed in Main View - const numberOfConditionSetsAtEnd = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count(); + expect(numberOfConditionSetsAtEnd).toEqual(numberOfConditionSetsToStart - 1); - expect(numberOfConditionSetsAtEnd).toEqual(numberOfConditionSetsToStart - 1); - - //Feature? - //Domain Object is still available by direct URL after delete - await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); - await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); - - }); + //Feature? + //Domain Object is still available by direct URL after delete + await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); + await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); + }); }); test.describe('Basic Condition Set Use', () => { - test.beforeEach(async ({ page }) => { - // Open a browser, navigate to the main page, and wait until all network events to resolve - await page.goto('./', { waitUntil: 'domcontentloaded' }); + test.beforeEach(async ({ page }) => { + // Open a browser, navigate to the main page, and wait until all network events to resolve + await page.goto('./', { waitUntil: 'domcontentloaded' }); + }); + test('Can add a condition', async ({ page }) => { + // Create a new condition set + await createDomainObjectWithDefaults(page, { + type: 'Condition Set', + name: 'Test Condition Set' }); - test('Can add a condition', async ({ page }) => { - // 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(); + // 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); + // 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); + }); + test('ConditionSet should display appropriate view options', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/5924' }); - test('ConditionSet should display appropriate view options', async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/5924' - }); - await createDomainObjectWithDefaults(page, { - type: 'Sine Wave Generator', - name: "Alpha Sine Wave Generator" - }); - await createDomainObjectWithDefaults(page, { - type: 'Sine Wave Generator', - name: "Beta Sine Wave Generator" - }); - const conditionSet1 = await createDomainObjectWithDefaults(page, { - type: 'Condition Set', - name: "Test Condition Set" - }); - - // Change the object to edit mode - await page.locator('[title="Edit"]').click(); - - // Expand the 'My Items' folder in the left tree - await page.goto(conditionSet1.url); - page.click('button[title="Show selected item in tree"]'); - // Add the Alpha & Beta Sine Wave Generator to the Condition Set and save changes - const treePane = page.getByRole('tree', { - name: 'Main Tree' - }); - const alphaGeneratorTreeItem = treePane.getByRole('treeitem', { name: "Alpha Sine Wave Generator"}); - const betaGeneratorTreeItem = treePane.getByRole('treeitem', { name: "Beta Sine Wave Generator"}); - const conditionCollection = page.locator('#conditionCollection'); - - await alphaGeneratorTreeItem.dragTo(conditionCollection); - await betaGeneratorTreeItem.dragTo(conditionCollection); - - const saveButtonLocator = page.locator('button[title="Save"]'); - await saveButtonLocator.click(); - await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); - await page.click('button[title="Change the current view"]'); - - await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden(); - await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible(); - await expect(page.getByRole('menuitem', { name: /Plot/ })).toBeVisible(); - await expect(page.getByRole('menuitem', { name: /Telemetry Table/ })).toBeVisible(); + await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: 'Alpha Sine Wave Generator' }); - test('ConditionSet should output blank instead of the default value', async ({ page }) => { - //Navigate to baseURL - await page.goto('./', { waitUntil: 'domcontentloaded' }); - - //Click the Create button - await page.click('button:has-text("Create")'); - - // Click the object specified by 'type' - await page.click(`li[role='menuitem']:text("Sine Wave Generator")`); - await page.getByRole('spinbutton', { name: 'Loading Delay (ms)' }).fill('8000'); - const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]'); - await nameInput.fill("Delayed Sine Wave Generator"); - - // Click OK button and wait for Navigate event - await Promise.all([ - page.waitForLoadState(), - page.click('[aria-label="Save"]'), - // Wait for Save Banner to appear - page.waitForSelector('.c-message-banner__message') - ]); - - // Create a new condition set - await createDomainObjectWithDefaults(page, { - type: 'Condition Set', - name: "Test Blank Output of Condition Set" - }); - // Change the object to edit mode - await page.locator('[title="Edit"]').click(); - - // Click Add Condition button twice - await page.locator('#addCondition').click(); - await page.locator('#addCondition').click(); - await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition'); - await page.locator('#conditionCollection').getByRole('textbox').nth(1).fill('Second Condition'); - - // Expand the 'My Items' folder in the left tree - await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click(); - // Add the Sine Wave Generator to the Condition Set and save changes - const treePane = page.getByRole('tree', { - name: 'Main Tree' - }); - const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { name: "Delayed Sine Wave Generator"}); - const conditionCollection = await page.locator('#conditionCollection'); - - await sineWaveGeneratorTreeItem.dragTo(conditionCollection); - - const firstCriterionTelemetry = await page.locator('[aria-label="Criterion Telemetry Selection"] >> nth=0'); - firstCriterionTelemetry.selectOption({ label: 'Delayed Sine Wave Generator' }); - - const secondCriterionTelemetry = await page.locator('[aria-label="Criterion Telemetry Selection"] >> nth=1'); - secondCriterionTelemetry.selectOption({ label: 'Delayed Sine Wave Generator' }); - - const firstCriterionMetadata = await page.locator('[aria-label="Criterion Metadata Selection"] >> nth=0'); - firstCriterionMetadata.selectOption({ label: 'Sine' }); - - const secondCriterionMetadata = await page.locator('[aria-label="Criterion Metadata Selection"] >> nth=1'); - secondCriterionMetadata.selectOption({ label: 'Sine' }); - - const firstCriterionComparison = await page.locator('[aria-label="Criterion Comparison Selection"] >> nth=0'); - firstCriterionComparison.selectOption({ label: 'is greater than or equal to' }); - - const secondCriterionComparison = await page.locator('[aria-label="Criterion Comparison Selection"] >> nth=1'); - secondCriterionComparison.selectOption({ label: 'is less than' }); - - const firstCriterionInput = await page.locator('[aria-label="Criterion Input"] >> nth=0'); - await firstCriterionInput.fill("0"); - - const secondCriterionInput = await page.locator('[aria-label="Criterion Input"] >> nth=1'); - await secondCriterionInput.fill("0"); - - const saveButtonLocator = page.locator('button[title="Save"]'); - await saveButtonLocator.click(); - await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); - - const outputValue = await page.locator('[aria-label="Current Output Value"]'); - await expect(outputValue).toHaveText('---'); + await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: 'Beta Sine Wave Generator' }); + const conditionSet1 = await createDomainObjectWithDefaults(page, { + type: 'Condition Set', + name: 'Test Condition Set' + }); + + // Change the object to edit mode + await page.locator('[title="Edit"]').click(); + + // Expand the 'My Items' folder in the left tree + await page.goto(conditionSet1.url); + page.click('button[title="Show selected item in tree"]'); + // Add the Alpha & Beta Sine Wave Generator to the Condition Set and save changes + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); + const alphaGeneratorTreeItem = treePane.getByRole('treeitem', { + name: 'Alpha Sine Wave Generator' + }); + const betaGeneratorTreeItem = treePane.getByRole('treeitem', { + name: 'Beta Sine Wave Generator' + }); + const conditionCollection = page.locator('#conditionCollection'); + + await alphaGeneratorTreeItem.dragTo(conditionCollection); + await betaGeneratorTreeItem.dragTo(conditionCollection); + + const saveButtonLocator = page.locator('button[title="Save"]'); + await saveButtonLocator.click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + await page.click('button[title="Change the current view"]'); + + await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden(); + await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: /Plot/ })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: /Telemetry Table/ })).toBeVisible(); + }); + test('ConditionSet should output blank instead of the default value', async ({ page }) => { + //Navigate to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + //Click the Create button + await page.click('button:has-text("Create")'); + + // Click the object specified by 'type' + await page.click(`li[role='menuitem']:text("Sine Wave Generator")`); + await page.getByRole('spinbutton', { name: 'Loading Delay (ms)' }).fill('8000'); + const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]'); + await nameInput.fill('Delayed Sine Wave Generator'); + + // Click OK button and wait for Navigate event + await Promise.all([ + page.waitForLoadState(), + page.click('[aria-label="Save"]'), + // Wait for Save Banner to appear + page.waitForSelector('.c-message-banner__message') + ]); + + // Create a new condition set + await createDomainObjectWithDefaults(page, { + type: 'Condition Set', + name: 'Test Blank Output of Condition Set' + }); + // Change the object to edit mode + await page.locator('[title="Edit"]').click(); + + // Click Add Condition button twice + await page.locator('#addCondition').click(); + await page.locator('#addCondition').click(); + await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition'); + await page.locator('#conditionCollection').getByRole('textbox').nth(1).fill('Second Condition'); + + // Expand the 'My Items' folder in the left tree + await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click(); + // Add the Sine Wave Generator to the Condition Set and save changes + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); + const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { + name: 'Delayed Sine Wave Generator' + }); + const conditionCollection = await page.locator('#conditionCollection'); + + await sineWaveGeneratorTreeItem.dragTo(conditionCollection); + + const firstCriterionTelemetry = await page.locator( + '[aria-label="Criterion Telemetry Selection"] >> nth=0' + ); + firstCriterionTelemetry.selectOption({ label: 'Delayed Sine Wave Generator' }); + + const secondCriterionTelemetry = await page.locator( + '[aria-label="Criterion Telemetry Selection"] >> nth=1' + ); + secondCriterionTelemetry.selectOption({ label: 'Delayed Sine Wave Generator' }); + + const firstCriterionMetadata = await page.locator( + '[aria-label="Criterion Metadata Selection"] >> nth=0' + ); + firstCriterionMetadata.selectOption({ label: 'Sine' }); + + const secondCriterionMetadata = await page.locator( + '[aria-label="Criterion Metadata Selection"] >> nth=1' + ); + secondCriterionMetadata.selectOption({ label: 'Sine' }); + + const firstCriterionComparison = await page.locator( + '[aria-label="Criterion Comparison Selection"] >> nth=0' + ); + firstCriterionComparison.selectOption({ label: 'is greater than or equal to' }); + + const secondCriterionComparison = await page.locator( + '[aria-label="Criterion Comparison Selection"] >> nth=1' + ); + secondCriterionComparison.selectOption({ label: 'is less than' }); + + const firstCriterionInput = await page.locator('[aria-label="Criterion Input"] >> nth=0'); + await firstCriterionInput.fill('0'); + + const secondCriterionInput = await page.locator('[aria-label="Criterion Input"] >> nth=1'); + await secondCriterionInput.fill('0'); + + const saveButtonLocator = page.locator('button[title="Save"]'); + await saveButtonLocator.click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + + const outputValue = await page.locator('[aria-label="Current Output Value"]'); + await expect(outputValue).toHaveText('---'); + }); }); diff --git a/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js b/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js index ed4bb8a691..37c413755b 100644 --- a/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js +++ b/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js @@ -21,173 +21,190 @@ *****************************************************************************/ const { test, expect } = require('../../../../pluginFixtures'); -const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions'); +const { + createDomainObjectWithDefaults, + setStartOffset, + setFixedTimeMode, + setRealTimeMode +} = require('../../../../appActions'); test.describe('Display Layout', () => { - /** @type {import('../../../../appActions').CreatedObjectInfo} */ - let sineWaveObject; - test.beforeEach(async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); - await setRealTimeMode(page); + /** @type {import('../../../../appActions').CreatedObjectInfo} */ + let sineWaveObject; + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + await setRealTimeMode(page); - // Create Sine Wave Generator - sineWaveObject = await createDomainObjectWithDefaults(page, { - type: 'Sine Wave Generator' - }); + // Create Sine Wave Generator + sineWaveObject = await createDomainObjectWithDefaults(page, { + type: '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 - const treePane = page.getByRole('tree', { - name: 'Main Tree' - }); - const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { - name: new RegExp(sineWaveObject.name) - }); - const layoutGridHolder = page.locator('.l-layout__grid-holder'); - await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder); - 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 = getTelemValuePromise; - const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`); - const displayLayoutValue = await displayLayoutValuePromise.textContent(); - const trimmedDisplayValue = displayLayoutValue.trim(); - - expect(trimmedDisplayValue).toBe(formattedTelemetryValue); + }); + 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' }); - 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(); + // Edit Display Layout + await page.locator('[title="Edit"]').click(); - // Expand the 'My Items' folder in the left tree - await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); - // Add the Sine Wave Generator to the Display Layout and save changes - const treePane = page.getByRole('tree', { - name: 'Main Tree' - }); - const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { - name: new RegExp(sineWaveObject.name) - }); - const layoutGridHolder = page.locator('.l-layout__grid-holder'); - await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder); - 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 = getTelemValuePromise; - const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`); - const displayLayoutValue = await displayLayoutValuePromise.textContent(); - const trimmedDisplayValue = displayLayoutValue.trim(); - - expect(trimmedDisplayValue).toBe(formattedTelemetryValue); + // Expand the 'My Items' folder in the left tree + await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); + // Add the Sine Wave Generator to the Display Layout and save changes + const treePane = page.getByRole('tree', { + name: 'Main Tree' }); - 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 - const treePane = page.getByRole('tree', { - name: 'Main Tree' - }); - const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { - name: new RegExp(sineWaveObject.name) - }); - const layoutGridHolder = page.locator('.l-layout__grid-holder'); - await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder); - 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 sineWaveGeneratorTreeItem.nth(1).click({ button: 'right' }); - await page.locator('li[role="menuitem"]:has-text("Remove")').click(); - await page.locator('button:has-text("OK")').click(); - - // delete - - expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0); + const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { + name: new RegExp(sineWaveObject.name) }); - test('items in a display layout can be removed with object tree context menu when viewing another item', async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/3117' - }); - // Create a Display Layout - const displayLayout = await createDomainObjectWithDefaults(page, { - type: 'Display Layout' - }); - // Edit Display Layout - await page.locator('[title="Edit"]').click(); + const layoutGridHolder = page.locator('.l-layout__grid-holder'); + await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder); + await page.locator('button[title="Save"]').click(); + await page.locator('text=Save and Finish Editing').click(); - // Expand the 'My Items' folder in the left tree - await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); - // Add the Sine Wave Generator to the Display Layout and save changes - const treePane = page.getByRole('tree', { - name: 'Main Tree' - }); - const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { - name: new RegExp(sineWaveObject.name) - }); - const layoutGridHolder = page.locator('.l-layout__grid-holder'); - await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder); - 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 = getTelemValuePromise; + const displayLayoutValuePromise = await page.waitForSelector( + `text="${formattedTelemetryValue}"` + ); + const displayLayoutValue = await displayLayoutValuePromise.textContent(); + const trimmedDisplayValue = displayLayoutValue.trim(); - 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(); - - // Go to the original Sine Wave Generator to navigate away from the Display Layout - await page.goto(sineWaveObject.url); - - // Bring up context menu and remove - await sineWaveGeneratorTreeItem.first().click({ button: 'right' }); - await page.locator('li[role="menuitem"]:has-text("Remove")').click(); - await page.locator('button:has-text("OK")').click(); - - // navigate back to the display layout to confirm it has been removed - await page.goto(displayLayout.url); - - expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0); + 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 + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); + const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { + name: new RegExp(sineWaveObject.name) + }); + const layoutGridHolder = page.locator('.l-layout__grid-holder'); + await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder); + 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 = getTelemValuePromise; + const displayLayoutValuePromise = await page.waitForSelector( + `text="${formattedTelemetryValue}"` + ); + const displayLayoutValue = await displayLayoutValuePromise.textContent(); + const trimmedDisplayValue = displayLayoutValue.trim(); + + 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 + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); + const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { + name: new RegExp(sineWaveObject.name) + }); + const layoutGridHolder = page.locator('.l-layout__grid-holder'); + await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder); + 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 sineWaveGeneratorTreeItem.nth(1).click({ button: 'right' }); + await page.locator('li[role="menuitem"]:has-text("Remove")').click(); + await page.locator('button:has-text("OK")').click(); + + // delete + + expect(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 + }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/3117' + }); + // Create a Display Layout + const displayLayout = await createDomainObjectWithDefaults(page, { + type: 'Display Layout' + }); + // Edit Display Layout + await page.locator('[title="Edit"]').click(); + + // Expand the 'My Items' folder in the left tree + await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); + // Add the Sine Wave Generator to the Display Layout and save changes + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); + const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { + name: new RegExp(sineWaveObject.name) + }); + const layoutGridHolder = page.locator('.l-layout__grid-holder'); + await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder); + 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(); + + // Go to the original Sine Wave Generator to navigate away from the Display Layout + await page.goto(sineWaveObject.url); + + // Bring up context menu and remove + await sineWaveGeneratorTreeItem.first().click({ button: 'right' }); + await page.locator('li[role="menuitem"]:has-text("Remove")').click(); + await page.locator('button:has-text("OK")').click(); + + // navigate back to the display layout to confirm it has been removed + await page.goto(displayLayout.url); + + expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0); + }); }); /** @@ -200,18 +217,20 @@ test.describe('Display Layout', () => { * @returns {Promise} the formatted sin telemetry value */ async function subscribeToTelemetry(page, objectIdentifier) { - const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getTelemValue', resolve)); + 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); + 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; + return getTelemValuePromise; } diff --git a/e2e/tests/functional/plugins/faultManagement/faultManagement.e2e.spec.js b/e2e/tests/functional/plugins/faultManagement/faultManagement.e2e.spec.js index b2f7d49f3b..c2fd649c33 100644 --- a/e2e/tests/functional/plugins/faultManagement/faultManagement.e2e.spec.js +++ b/e2e/tests/functional/plugins/faultManagement/faultManagement.e2e.spec.js @@ -25,216 +25,231 @@ const utils = require('../../../../helper/faultUtils'); const { selectInspectorTab } = require('../../../../appActions'); test.describe('The Fault Management Plugin using example faults', () => { - test.beforeEach(async ({ page }) => { - await utils.navigateToFaultManagementWithExample(page); - }); + 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(); + 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); - }); + 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); + 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); - await selectInspectorTab(page, 'Fault Management Configuration'); - 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 selectInspectorTab(page, 'Fault Management Configuration'); + 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); - }); + 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); + 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 selectedRows = page.locator( + '.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname' + ); + expect.soft(await selectedRows.count()).toEqual(2); - await selectInspectorTab(page, 'Fault Management Configuration'); - 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(); + await selectInspectorTab(page, 'Fault Management Configuration'); + 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); - }); + 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); + 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); + expect.soft(await beforeShelvedFault.count()).toBe(1); - await utils.shelveFault(page, 2); + 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); + // 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'); + await utils.changeViewTo(page, 'shelved'); - const shelvedViewFault = utils.getFaultByName(page, shelvedFaultName); + const shelvedViewFault = utils.getFaultByName(page, shelvedFaultName); - expect.soft(await shelvedViewFault.count()).toBe(1); - }); + expect.soft(await shelvedViewFault.count()).toBe(1); + }); - test('Allows you to acknowledge a fault @unstable', async ({ page }) => { - const acknowledgedFaultName = await utils.getFaultName(page, 3); + test('Allows you to acknowledge a fault @unstable', async ({ page }) => { + const acknowledgedFaultName = await utils.getFaultName(page, 3); - await utils.acknowledgeFault(page, 3); + await utils.acknowledgeFault(page, 3); - const fault = utils.getFault(page, 3); - await expect.soft(fault).toHaveClass(/is-acknowledged/); + const fault = utils.getFault(page, 3); + await expect.soft(fault).toHaveClass(/is-acknowledged/); - await utils.changeViewTo(page, 'acknowledged'); + await utils.changeViewTo(page, 'acknowledged'); - const acknowledgedViewFaultName = await utils.getFaultName(page, 1); - expect.soft(acknowledgedFaultName).toEqual(acknowledgedViewFaultName); - }); + 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); + 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); + 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); + expect.soft(await beforeShelvedFaultOne.count()).toBe(1); + expect.soft(await beforeShelvedFaultFour.count()).toBe(1); - await utils.shelveMultipleFaults(page, 1, 4); + 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); + // 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'); + await utils.changeViewTo(page, 'shelved'); - const shelvedViewFaultOne = utils.getFaultByName(page, shelvedFaultNameOne); - const shelvedViewFaultFour = utils.getFaultByName(page, shelvedFaultNameFour); + 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); - }); + 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); + 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); + await utils.acknowledgeMultipleFaults(page, 2, 5); - const faultTwo = utils.getFault(page, 2); - const faultFive = utils.getFault(page, 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/); + // check they have been acknowledged + await expect.soft(faultTwo).toHaveClass(/is-acknowledged/); + await expect.soft(faultFive).toHaveClass(/is-acknowledged/); - await utils.changeViewTo(page, 'acknowledged'); + await utils.changeViewTo(page, 'acknowledged'); - const acknowledgedViewFaultTwo = utils.getFaultByName(page, acknowledgedFaultNameTwo); - const acknowledgedViewFaultFive = utils.getFaultByName(page, acknowledgedFaultNameFive); + 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); - }); + 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); + 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); + // should be all faults (5) + let faultResultCount = await utils.getFaultResultCount(page); + expect.soft(faultResultCount).toEqual(5); - // search namespace - await utils.enterSearchTerm(page, faultThreeNamespace); + // 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); + 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); + // all faults + await utils.clearSearch(page); + faultResultCount = await utils.getFaultResultCount(page); + expect.soft(faultResultCount).toEqual(5); - // search name - await utils.enterSearchTerm(page, faultTwoName); + // 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); + 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); + // all faults + await utils.clearSearch(page); + faultResultCount = await utils.getFaultResultCount(page); + expect.soft(faultResultCount).toEqual(5); - // search triggerTime - await utils.enterSearchTerm(page, faultFiveTriggerTime); + // 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); - }); + 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); + 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); + expect.soft(firstFaultName).toEqual(faultOneName); - await utils.sortFaultsBy(page, 'oldest-first'); + await utils.sortFaultsBy(page, 'oldest-first'); - firstFaultName = await utils.getFaultName(page, 1); - expect.soft(firstFaultName).toEqual(faultFiveName); + 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); - }); + 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.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(); + 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); + 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, '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); - }); + 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'); + 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(); + const faultCount = await page.locator('c-fault-mgmt__list').count(); - expect.soft(faultCount).toEqual(0); - }); + 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 index 2aae29a798..940055bed7 100644 --- a/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js +++ b/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js @@ -24,130 +24,138 @@ const { test, expect } = require('../../../../pluginFixtures'); const { createDomainObjectWithDefaults } = require('../../../../appActions'); test.describe('Flexible Layout', () => { - let sineWaveObject; - let clockObject; - test.beforeEach(async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); + let sineWaveObject; + let clockObject; + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); - // Create Sine Wave Generator - sineWaveObject = await createDomainObjectWithDefaults(page, { - type: 'Sine Wave Generator' - }); - - // Create Clock Object - clockObject = await createDomainObjectWithDefaults(page, { - type: 'Clock' - }); + // Create Sine Wave Generator + sineWaveObject = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator' }); - test('panes have the appropriate draggable attribute while in Edit and Browse modes', async ({ page }) => { - const treePane = page.getByRole('tree', { - name: 'Main Tree' - }); - const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { - name: new RegExp(sineWaveObject.name) - }); - const clockTreeItem = treePane.getByRole('treeitem', { - name: new RegExp(clockObject.name) - }); - // Create a Flexible Layout - await createDomainObjectWithDefaults(page, { - type: '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 sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first()); - await clockTreeItem.dragTo(page.locator('.c-fl__container.is-empty')); - // Check that panes can be dragged while Flexible Layout is in Edit mode - let dragWrapper = 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 = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first(); - await expect(dragWrapper).toHaveAttribute('draggable', 'false'); + // Create Clock Object + clockObject = await createDomainObjectWithDefaults(page, { + type: 'Clock' }); - test('items in a flexible layout can be removed with object tree context menu when viewing the flexible layout', async ({ page }) => { - const treePane = page.getByRole('tree', { - name: 'Main Tree' - }); - const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { - name: new RegExp(sineWaveObject.name) - }); - // Create a Display Layout - await createDomainObjectWithDefaults(page, { - type: '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 to the Flexible Layout and save changes - await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first()); - await page.locator('button[title="Save"]').click(); - await page.locator('text=Save and Finish Editing').click(); - - expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1); - - // Expand the Flexible 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 sineWaveGeneratorTreeItem.first().click({ button: 'right' }); - await page.locator('li[role="menuitem"]:has-text("Remove")').click(); - await page.locator('button:has-text("OK")').click(); - - // Verify that the item has been removed from the layout - expect(await page.locator('.c-fl-container__frame').count()).toEqual(0); + }); + test('panes have the appropriate draggable attribute while in Edit and Browse modes', async ({ + page + }) => { + const treePane = page.getByRole('tree', { + name: 'Main Tree' }); - test('items in a flexible layout can be removed with object tree context menu when viewing another item', async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/3117' - }); - const treePane = page.getByRole('tree', { - name: 'Main Tree' - }); - const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { - name: new RegExp(sineWaveObject.name) - }); - - // Create a Flexible Layout - const flexibleLayout = await createDomainObjectWithDefaults(page, { - type: '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').click(); - // Add the Sine Wave Generator to the Flexible Layout and save changes - await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first()); - await page.locator('button[title="Save"]').click(); - await page.locator('text=Save and Finish Editing').click(); - - expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1); - - // Expand the Flexible Layout so we can remove the sine wave generator - await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click(); - - // Go to the original Sine Wave Generator to navigate away from the Flexible Layout - await page.goto(sineWaveObject.url); - - // Bring up context menu and remove - await sineWaveGeneratorTreeItem.first().click({ button: 'right' }); - await page.locator('li[role="menuitem"]:has-text("Remove")').click(); - await page.locator('button:has-text("OK")').click(); - - // navigate back to the display layout to confirm it has been removed - await page.goto(flexibleLayout.url); - - // Verify that the item has been removed from the layout - expect(await page.locator('.c-fl-container__frame').count()).toEqual(0); + const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { + name: new RegExp(sineWaveObject.name) }); + const clockTreeItem = treePane.getByRole('treeitem', { + name: new RegExp(clockObject.name) + }); + // Create a Flexible Layout + await createDomainObjectWithDefaults(page, { + type: '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 sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first()); + await clockTreeItem.dragTo(page.locator('.c-fl__container.is-empty')); + // Check that panes can be dragged while Flexible Layout is in Edit mode + let dragWrapper = 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 = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first(); + await expect(dragWrapper).toHaveAttribute('draggable', 'false'); + }); + test('items in a flexible layout can be removed with object tree context menu when viewing the flexible layout', async ({ + page + }) => { + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); + const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { + name: new RegExp(sineWaveObject.name) + }); + // Create a Display Layout + await createDomainObjectWithDefaults(page, { + type: '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 to the Flexible Layout and save changes + await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first()); + await page.locator('button[title="Save"]').click(); + await page.locator('text=Save and Finish Editing').click(); + + expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1); + + // Expand the Flexible 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 sineWaveGeneratorTreeItem.first().click({ button: 'right' }); + await page.locator('li[role="menuitem"]:has-text("Remove")').click(); + await page.locator('button:has-text("OK")').click(); + + // Verify that the item has been removed from the layout + expect(await page.locator('.c-fl-container__frame').count()).toEqual(0); + }); + test('items in a flexible layout can be removed with object tree context menu when viewing another item', async ({ + page + }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/3117' + }); + const treePane = page.getByRole('tree', { + name: 'Main Tree' + }); + const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { + name: new RegExp(sineWaveObject.name) + }); + + // Create a Flexible Layout + const flexibleLayout = await createDomainObjectWithDefaults(page, { + type: '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').click(); + // Add the Sine Wave Generator to the Flexible Layout and save changes + await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first()); + await page.locator('button[title="Save"]').click(); + await page.locator('text=Save and Finish Editing').click(); + + expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1); + + // Expand the Flexible Layout so we can remove the sine wave generator + await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click(); + + // Go to the original Sine Wave Generator to navigate away from the Flexible Layout + await page.goto(sineWaveObject.url); + + // Bring up context menu and remove + await sineWaveGeneratorTreeItem.first().click({ button: 'right' }); + await page.locator('li[role="menuitem"]:has-text("Remove")').click(); + await page.locator('button:has-text("OK")').click(); + + // navigate back to the display layout to confirm it has been removed + await page.goto(flexibleLayout.url); + + // Verify that the item has been removed from the layout + expect(await page.locator('.c-fl-container__frame').count()).toEqual(0); + }); }); diff --git a/e2e/tests/functional/plugins/gauge/gauge.e2e.spec.js b/e2e/tests/functional/plugins/gauge/gauge.e2e.spec.js index f2e83829be..bfc4464d50 100644 --- a/e2e/tests/functional/plugins/gauge/gauge.e2e.spec.js +++ b/e2e/tests/functional/plugins/gauge/gauge.e2e.spec.js @@ -21,104 +21,116 @@ *****************************************************************************/ /* -* This test suite is dedicated to testing the Gauge component. -*/ + * This test suite is dedicated to testing the Gauge component. + */ const { test, expect } = require('../../../../pluginFixtures'); const { createDomainObjectWithDefaults } = require('../../../../appActions'); const uuid = require('uuid').v4; test.describe('Gauge', () => { - test.beforeEach(async ({ page }) => { - // Open a browser, navigate to the main page, and wait until all networkevents to resolve - await page.goto('./', { waitUntil: 'domcontentloaded' }); + test.beforeEach(async ({ page }) => { + // Open a browser, navigate to the main page, and wait until all networkevents to resolve + await page.goto('./', { waitUntil: 'domcontentloaded' }); + }); + + test('Can add and remove telemetry sources @unstable', async ({ page }) => { + // Create the gauge with defaults + const gauge = await createDomainObjectWithDefaults(page, { type: 'Gauge' }); + const editButtonLocator = page.locator('button[title="Edit"]'); + const saveButtonLocator = page.locator('button[title="Save"]'); + + // Create a sine wave generator within the gauge + const swg1 = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: `swg-${uuid()}`, + parent: gauge.uuid }); - test('Can add and remove telemetry sources @unstable', async ({ page }) => { - // Create the gauge with defaults - const gauge = await createDomainObjectWithDefaults(page, { type: 'Gauge' }); - const editButtonLocator = page.locator('button[title="Edit"]'); - const saveButtonLocator = page.locator('button[title="Save"]'); + // Navigate to the gauge and verify that + // the SWG appears in the elements pool + await page.goto(gauge.url); + await editButtonLocator.click(); + await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible(); + await saveButtonLocator.click(); + await page.locator('li[title="Save and Finish Editing"]').click(); - // Create a sine wave generator within the gauge - const swg1 = await createDomainObjectWithDefaults(page, { - type: 'Sine Wave Generator', - name: `swg-${uuid()}`, - parent: gauge.uuid - }); - - // Navigate to the gauge and verify that - // the SWG appears in the elements pool - await page.goto(gauge.url); - await editButtonLocator.click(); - await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible(); - await saveButtonLocator.click(); - await page.locator('li[title="Save and Finish Editing"]').click(); - - // Create another sine wave generator within the gauge - const swg2 = await createDomainObjectWithDefaults(page, { - type: 'Sine Wave Generator', - name: `swg-${uuid()}`, - parent: gauge.uuid - }); - - // Verify that the 'Replace telemetry source' modal appears and accept it - await expect.soft(page.locator('text=This action will replace the current telemetry source. Do you want to continue?')).toBeVisible(); - await page.click('text=Ok'); - - // Navigate to the gauge and verify that the new SWG - // appears in the elements pool and the old one is gone - await page.goto(gauge.url); - await editButtonLocator.click(); - await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden(); - await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible(); - await saveButtonLocator.click(); - - // Right click on the new SWG in the elements pool and delete it - await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({ - button: 'right' - }); - await page.locator('li[title="Remove this object from its containing object."]').click(); - - // Verify that the 'Remove object' confirmation modal appears and accept it - await expect.soft(page.locator('text=Warning! This action will remove this object. Are you sure you want to continue?')).toBeVisible(); - await page.click('text=Ok'); - - // Verify that the elements pool shows no elements - await expect(page.locator('text="No contained elements"')).toBeVisible(); + // Create another sine wave generator within the gauge + const swg2 = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: `swg-${uuid()}`, + parent: gauge.uuid }); - test('Can create a non-default Gauge', async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/5356' - }); - //Click the Create button - await page.click('button:has-text("Create")'); - // Click the object specified by 'type' - await page.click(`li[role='menuitem']:text("Gauge")`); - // FIXME: We need better selectors for these custom form controls - const displayCurrentValueSwitch = page.locator('.c-toggle-switch__slider >> nth=0'); - await displayCurrentValueSwitch.setChecked(false); - await page.click('button[aria-label="Save"]'); + // Verify that the 'Replace telemetry source' modal appears and accept it + await expect + .soft( + page.locator( + 'text=This action will replace the current telemetry source. Do you want to continue?' + ) + ) + .toBeVisible(); + await page.click('text=Ok'); - // TODO: Verify changes in the UI + // Navigate to the gauge and verify that the new SWG + // appears in the elements pool and the old one is gone + await page.goto(gauge.url); + await editButtonLocator.click(); + await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden(); + await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible(); + await saveButtonLocator.click(); + + // Right click on the new SWG in the elements pool and delete it + await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({ + button: 'right' }); - test('Can edit a single Gauge-specific property', async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/5985' - }); + await page.locator('li[title="Remove this object from its containing object."]').click(); - // Create the gauge with defaults - await createDomainObjectWithDefaults(page, { type: 'Gauge' }); - await page.click('button[title="More options"]'); - await page.click('li[role="menuitem"]:has-text("Edit Properties")'); - // FIXME: We need better selectors for these custom form controls - const displayCurrentValueSwitch = page.locator('.c-toggle-switch__slider >> nth=0'); - await displayCurrentValueSwitch.setChecked(false); - await page.click('button[aria-label="Save"]'); + // Verify that the 'Remove object' confirmation modal appears and accept it + await expect + .soft( + page.locator( + 'text=Warning! This action will remove this object. Are you sure you want to continue?' + ) + ) + .toBeVisible(); + await page.click('text=Ok'); - // TODO: Verify changes in the UI + // Verify that the elements pool shows no elements + await expect(page.locator('text="No contained elements"')).toBeVisible(); + }); + test('Can create a non-default Gauge', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/5356' }); + //Click the Create button + await page.click('button:has-text("Create")'); + + // Click the object specified by 'type' + await page.click(`li[role='menuitem']:text("Gauge")`); + // FIXME: We need better selectors for these custom form controls + const displayCurrentValueSwitch = page.locator('.c-toggle-switch__slider >> nth=0'); + await displayCurrentValueSwitch.setChecked(false); + await page.click('button[aria-label="Save"]'); + + // TODO: Verify changes in the UI + }); + test('Can edit a single Gauge-specific property', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/5985' + }); + + // Create the gauge with defaults + await createDomainObjectWithDefaults(page, { type: 'Gauge' }); + await page.click('button[title="More options"]'); + await page.click('li[role="menuitem"]:has-text("Edit Properties")'); + // FIXME: We need better selectors for these custom form controls + const displayCurrentValueSwitch = page.locator('.c-toggle-switch__slider >> nth=0'); + await displayCurrentValueSwitch.setChecked(false); + await page.click('button[aria-label="Save"]'); + + // TODO: Verify changes in the UI + }); }); diff --git a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js index f523e74cf5..b759821c30 100644 --- a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js +++ b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js @@ -35,406 +35,418 @@ const thumbnailUrlParamsRegexp = /\?w=100&h=100/; //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: 'domcontentloaded' }); + test.beforeEach(async ({ page }) => { + //Go to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); - // Create a default 'Example Imagery' object - const exampleImagery = await createDomainObjectWithDefaults(page, { type: 'Example Imagery' }); + // Create a default 'Example Imagery' object + const exampleImagery = await createDomainObjectWithDefaults(page, { type: 'Example Imagery' }); - // Verify that the created object is focused - await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name); - await page.locator(backgroundImageSelector).hover({trial: true}); - }); + // Verify that the created object is focused + await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name); + 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); + 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); - }); + // 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(); + 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); - }); + // 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 + 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}); + 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 + // 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); + // 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); + //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 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 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 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); + // 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 + - 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(); - 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); - // 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); - // 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); + }); - // 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'); - 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(); - // 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/); - // 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/); + }); - // 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('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: 'domcontentloaded' }); + let displayLayout; + test.beforeEach(async ({ page }) => { + // Go to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); - displayLayout = await createDomainObjectWithDefaults(page, { type: 'Display Layout' }); - await page.goto(displayLayout.url); + 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")'); + /* 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('li[role="menuitem"]:has-text("Example Imagery")'); + // Click text=Example Imagery + await page.click('li[role="menuitem"]:has-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'); + // 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('button:has-text("OK")'), - //Wait for Save Banner to appear - page.waitForSelector('.c-message-banner__message') - ]); + // Click text=OK + await Promise.all([ + page.waitForNavigation({ waitUntil: 'networkidle' }), + page.click('button:has-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 expect(page.locator('.l-browse-bar__object-name')).toContainText( + 'Unnamed Example Imagery' + ); - await page.goto(displayLayout.url); + await page.goto(displayLayout.url); + }); + + test('View Large action pauses imagery when in realtime and returns to realtime', async ({ + page + }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/3647' }); - test('View Large action pauses imagery when in realtime and returns to realtime', async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/3647' - }); + // Click time conductor mode button + await page.locator('.c-mode-button').click(); - // Click time conductor mode button - await page.locator('.c-mode-button').click(); + // set realtime mode + await page.locator('[data-testid="conductor-modeOption-realtime"]').click(); - // set realtime mode - await page.locator('[data-testid="conductor-modeOption-realtime"]').click(); + // pause/play button + const pausePlayButton = await page.locator('.c-button.pause-play'); - // pause/play button - const pausePlayButton = await page.locator('.c-button.pause-play'); + await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/); - await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/); + // Open context menu and click view large menu item + await page.locator('button[title="View menu items"]').click(); + await page.locator('li[title="View Large"]').click(); + await expect(pausePlayButton).toHaveClass(/is-paused/); - // Open context menu and click view large menu item - await page.locator('button[title="View menu items"]').click(); - await page.locator('li[title="View Large"]').click(); - await expect(pausePlayButton).toHaveClass(/is-paused/); + await page.locator('[aria-label="Close"]').click(); + await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/); + }); - await page.locator('[aria-label="Close"]').click(); - await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/); + test('View Large action leaves keeps realtime mode paused', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/3647' }); - test('View Large action leaves keeps realtime mode paused', async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/3647' - }); + // Click time conductor mode button + await page.locator('.c-mode-button').click(); - // Click time conductor mode button - await page.locator('.c-mode-button').click(); + // set realtime mode + await page.locator('[data-testid="conductor-modeOption-realtime"]').click(); - // set realtime mode - await page.locator('[data-testid="conductor-modeOption-realtime"]').click(); + // pause/play button + const pausePlayButton = await page.locator('.c-button.pause-play'); + await pausePlayButton.click(); + await expect.soft(pausePlayButton).toHaveClass(/is-paused/); - // pause/play button - const pausePlayButton = await page.locator('.c-button.pause-play'); - await pausePlayButton.click(); - await expect.soft(pausePlayButton).toHaveClass(/is-paused/); + // Open context menu and click view large menu item + await page.locator('button[title="View menu items"]').click(); + await page.locator('li[title="View Large"]').click(); + await expect(pausePlayButton).toHaveClass(/is-paused/); - // Open context menu and click view large menu item - await page.locator('button[title="View menu items"]').click(); - await page.locator('li[title="View Large"]').click(); - await expect(pausePlayButton).toHaveClass(/is-paused/); + await page.locator('[aria-label="Close"]').click(); + await expect.soft(pausePlayButton).toHaveClass(/is-paused/); + }); - await page.locator('[aria-label="Close"]').click(); - await expect.soft(pausePlayButton).toHaveClass(/is-paused/); + test('Imagery View operations @unstable', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/5265' }); - 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"]'); - // Edit mode - await page.click('button[title="Edit"]'); + // Click on example imagery to expose toolbar + await page.locator('.c-so-view__header').click(); - // 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 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'); - // 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); + }); - 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"]'); - 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(); - // 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(); - // expect thumbnails not be visible when first added - expect.soft(thumbsWrapperLocator.isHidden()).toBeTruthy(); - - // Resize the example imagery vertically to change the thumbnail visibility - /* + // 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'); + 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/); + 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'); + // 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/); - }); + 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: 'domcontentloaded' }); + let flexibleLayout; + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); - flexibleLayout = await createDomainObjectWithDefaults(page, { type: 'Flexible Layout' }); - await page.goto(flexibleLayout.url); + 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")'); + /* 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('li[role="menuitem"]:has-text("Example Imagery")'); + // Click text=Example Imagery + await page.click('li[role="menuitem"]:has-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'); + // 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('button:has-text("OK")'), - //Wait for Save Banner to appear - page.waitForSelector('.c-message-banner__message') - ]); + // Click text=OK + await Promise.all([ + page.waitForNavigation({ waitUntil: 'networkidle' }), + page.click('button:has-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 expect(page.locator('.l-browse-bar__object-name')).toContainText( + 'Unnamed Example Imagery' + ); - await page.goto(flexibleLayout.url); + 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' }); - 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); - }); + await performImageryViewOperationsAndAssert(page); + }); }); test.describe('Example Imagery in Tabs View', () => { - let tabsView; - test.beforeEach(async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); + let tabsView; + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); - tabsView = await createDomainObjectWithDefaults(page, { type: 'Tabs View' }); - await page.goto(tabsView.url); + 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")'); + /* 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('li[role="menuitem"]:has-text("Example Imagery")'); + // Click text=Example Imagery + await page.click('li[role="menuitem"]:has-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'); + // 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('button:has-text("OK")'), - //Wait for Save Banner to appear - page.waitForSelector('.c-message-banner__message') - ]); + // Click text=OK + await Promise.all([ + page.waitForNavigation({ waitUntil: 'networkidle' }), + page.click('button:has-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 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); - }); + 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: 'domcontentloaded' }); - timeStripObject = await createDomainObjectWithDefaults(page, { - type: 'Time Strip' - }); - - await createDomainObjectWithDefaults(page, { - type: 'Example Imagery', - parent: timeStripObject.uuid - }); - // Navigate to timestrip - await page.goto(timeStripObject.url); + let timeStripObject; + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + timeStripObject = await createDomainObjectWithDefaults(page, { + type: 'Time Strip' }); - 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' - }); - // Hover over the timestrip to reveal a thumbnail image - await page.locator('.c-imagery-tsv-container').hover(); - - // Get the img src of the hovered image thumbnail - const hoveredThumbnailImg = page.locator('.c-imagery-tsv div.c-imagery-tsv__image-wrapper:hover img'); - const hoveredThumbnailImgSrc = await hoveredThumbnailImg.getAttribute('src'); - - // Verify that imagery timestrip view uses the thumbnailUrl as img src for thumbnails - expect(hoveredThumbnailImgSrc).toBeTruthy(); - expect(hoveredThumbnailImgSrc).toMatch(thumbnailUrlParamsRegexp); - - // Click on the hovered thumbnail to open "View Large" view - await page.locator('.c-imagery-tsv-container').click(); - - // Get the img src of the large view image - const viewLargeImg = page.locator('img.c-imagery__main-image__image'); - const viewLargeImgSrc = await viewLargeImg.getAttribute('src'); - expect(viewLargeImgSrc).toBeTruthy(); - - // Verify that the image in the large view is the same as the hovered thumbnail - expect(viewLargeImgSrc).toEqual(hoveredThumbnailImgSrc.split('?')[0]); + await createDomainObjectWithDefaults(page, { + type: 'Example Imagery', + 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' + }); + + // Hover over the timestrip to reveal a thumbnail image + await page.locator('.c-imagery-tsv-container').hover(); + + // Get the img src of the hovered image thumbnail + const hoveredThumbnailImg = page.locator( + '.c-imagery-tsv div.c-imagery-tsv__image-wrapper:hover img' + ); + const hoveredThumbnailImgSrc = await hoveredThumbnailImg.getAttribute('src'); + + // Verify that imagery timestrip view uses the thumbnailUrl as img src for thumbnails + expect(hoveredThumbnailImgSrc).toBeTruthy(); + expect(hoveredThumbnailImgSrc).toMatch(thumbnailUrlParamsRegexp); + + // Click on the hovered thumbnail to open "View Large" view + await page.locator('.c-imagery-tsv-container').click(); + + // Get the img src of the large view image + const viewLargeImg = page.locator('img.c-imagery__main-image__image'); + const viewLargeImgSrc = await viewLargeImg.getAttribute('src'); + expect(viewLargeImgSrc).toBeTruthy(); + + // Verify that the image in the large view is the same as the hovered thumbnail + expect(viewLargeImgSrc).toEqual(hoveredThumbnailImgSrc.split('?')[0]); + }); }); /** @@ -450,76 +462,81 @@ test.describe('Example Imagery in Time Strip', () => { * @param {import('@playwright/test').Page} page */ async function performImageryViewOperationsAndAssert(page) { - // Verify that imagery thumbnails use a thumbnail url - const thumbnailImages = page.locator('.c-thumb__image'); - const mainImage = page.locator('.c-imagery__main-image__image'); - await expect(thumbnailImages.first()).toHaveAttribute('src', thumbnailUrlParamsRegexp); - await expect(mainImage).not.toHaveAttribute('src', thumbnailUrlParamsRegexp); + // Verify that imagery thumbnails use a thumbnail url + const thumbnailImages = page.locator('.c-thumb__image'); + const mainImage = page.locator('.c-imagery__main-image__image'); + await expect(thumbnailImages.first()).toHaveAttribute('src', thumbnailUrlParamsRegexp); + await expect(mainImage).not.toHaveAttribute('src', thumbnailUrlParamsRegexp); - // Click previous image button - const previousImageButton = page.locator('.c-nav--prev'); - await previousImageButton.click(); + // 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(); + // 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 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 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 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); + // 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 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(); + // 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(); + // Select local clock mode + await page.locator('[data-testid=conductor-modeOption-realtime]').click(); - // Zoom in on next image - await mouseZoomOnImageAndAssert(page, 2); + // 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(); + // 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 () => { + // 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", + }, + { + message: 'verify that old images are discarded', timeout: 7 * 1000 - }).toBe(imageCount); + } + ) + .toBe(imageCount); - // Verify selected image is still displayed - await expect(selectedImage).toBeVisible(); + // Verify selected image is still displayed + await expect(selectedImage).toBeVisible(); - // Unpause imagery - await page.locator('.pause-play').click(); + // Unpause imagery + await page.locator('.pause-play').click(); - //Get background-image url from background-image css prop - await assertBackgroundImageUrlFromBackgroundCss(page); + //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(); + // 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 and contrast sliders around and assert filter values + await dragBrightnessSliderAndAssertFilterValues(page); + await dragContrastSliderAndAssertFilterValues(page); } /** @@ -527,20 +544,20 @@ async function performImageryViewOperationsAndAssert(page) { * @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; + 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(); + 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(); } /** @@ -548,20 +565,20 @@ async function dragBrightnessSliderAndAssertFilterValues(page) { * @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; + 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(); + 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(); } /** @@ -571,88 +588,99 @@ async function dragContrastSliderAndAssertFilterValues(page) { * @param {String} expected The expected brightness value */ async function assertBackgroundImageBrightness(page, expected) { - const backgroundImage = page.locator('.c-imagery__main-image__background-image'); + 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); + // 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); + 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 () => { + 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]; + 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", + }, + { + message: 'verify next image has updated', timeout: 7 * 1000 - }).not.toBe(backgroundImageUrl1); - console.log('backgroundImageUrl2 ' + backgroundImageUrl2); + } + ) + .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; + 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 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 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 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); + // 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); } /** @@ -660,31 +688,31 @@ async function panZoomAndAssertImageProperties(page) { * 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; + // 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); + // 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(); + // 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); - } + 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); + } } /** @@ -693,30 +721,30 @@ async function mouseZoomOnImageAndAssert(page, factor = 2) { * @param {import('@playwright/test').Page} page */ async function buttonZoomOnImageAndAssert(page) { - // Get initial image dimensions - const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); + // Get initial image dimensions + const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); - // Zoom in twice via button - await zoomIntoImageryByButton(page); - await zoomIntoImageryByButton(page); + // 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); + // 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); + // 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); + // 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); + // 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); } /** @@ -726,13 +754,13 @@ async function buttonZoomOnImageAndAssert(page) { * @param {String} expected The expected contrast value */ async function assertBackgroundImageContrast(page, expected) { - const backgroundImage = page.locator('.c-imagery__main-image__background-image'); + 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); + // 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); } /** @@ -741,15 +769,17 @@ async function assertBackgroundImageContrast(page, expected) { * @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}); - } + // 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); + await zoomInBtn.click(); + await waitForAnimations(backgroundImage); } /** @@ -758,15 +788,17 @@ async function zoomIntoImageryByButton(page) { * @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}); - } + // 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); + await zoomOutBtn.click(); + await waitForAnimations(backgroundImage); } /** @@ -775,13 +807,15 @@ async function zoomOutOfImageryByButton(page) { * @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}); - } + // 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); + await panZoomResetBtn.click(); + await waitForAnimations(backgroundImage); } diff --git a/e2e/tests/functional/plugins/importAndExportAsJSON/exportAsJson.e2e.spec.js b/e2e/tests/functional/plugins/importAndExportAsJSON/exportAsJson.e2e.spec.js index 0ad190e67b..aeab0424af 100644 --- a/e2e/tests/functional/plugins/importAndExportAsJSON/exportAsJson.e2e.spec.js +++ b/e2e/tests/functional/plugins/importAndExportAsJSON/exportAsJson.e2e.spec.js @@ -29,22 +29,31 @@ This test suite is dedicated to tests which verify the basic operations surround 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 }) => { - //Create domain object - //Save Domain Object - //Verify that the newly created domain object can be exported as JSON from the Tree - }); - test.fixme('Create a basic object and verify that it can be exported as JSON from 3 dot menu', async ({ page }) => { - //Create domain object - //Save Domain Object - //Verify that the newly created domain object can be exported as JSON from the 3 dot menu - }); - test.fixme('Verify that a nested Object can be exported as JSON', async ({ page }) => { - // Create 2 objects with hierarchy - // Export as JSON - // Verify Hiearchy - }); - test.fixme('Verify that the ExportAsJSON dropdown does not appear for the item X', async ({ page }) => { - // Other than non-persistible objects - }); + test.fixme( + 'Create a basic object and verify that it can be exported as JSON from Tree', + async ({ page }) => { + //Create domain object + //Save Domain Object + //Verify that the newly created domain object can be exported as JSON from the Tree + } + ); + test.fixme( + 'Create a basic object and verify that it can be exported as JSON from 3 dot menu', + async ({ page }) => { + //Create domain object + //Save Domain Object + //Verify that the newly created domain object can be exported as JSON from the 3 dot menu + } + ); + test.fixme('Verify that a nested Object can be exported as JSON', async ({ page }) => { + // Create 2 objects with hierarchy + // Export as JSON + // Verify Hiearchy + }); + test.fixme( + 'Verify that the ExportAsJSON dropdown does not appear for the item X', + async ({ page }) => { + // Other than non-persistible objects + } + ); }); diff --git a/e2e/tests/functional/plugins/importAndExportAsJSON/importAsJson.e2e.spec.js b/e2e/tests/functional/plugins/importAndExportAsJSON/importAsJson.e2e.spec.js index 6ab1793c4b..8ecdc22c90 100644 --- a/e2e/tests/functional/plugins/importAndExportAsJSON/importAsJson.e2e.spec.js +++ b/e2e/tests/functional/plugins/importAndExportAsJSON/importAsJson.e2e.spec.js @@ -1,48 +1,54 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2023, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -/* -This test suite is dedicated to tests which verify the basic operations surrounding importAsJSON. -*/ - -// 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 }) => { - //Verify that an testdata JSON file can be imported from Tree - //Verify correctness of imported domain object - }); - test.fixme('Verify that domain object can be importAsJSON from 3 dot menu on folder', async ({ page }) => { - //Verify that an testdata JSON file can be imported from 3 dot menu on folder domain object - //Verify correctness of imported domain object - }); - test.fixme('Verify that a nested Objects can be importAsJSON', async ({ page }) => { - // Testdata with hierarchy - // ImportAsJSON on Tree - // Verify Hierarchy - }); - test.fixme('Verify that the ImportAsJSON dropdown does not appear for the item X', async ({ page }) => { - // Other than non-persistible objects - }); -}); +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2023, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/* +This test suite is dedicated to tests which verify the basic operations surrounding importAsJSON. +*/ + +// 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 }) => { + //Verify that an testdata JSON file can be imported from Tree + //Verify correctness of imported domain object + }); + test.fixme( + 'Verify that domain object can be importAsJSON from 3 dot menu on folder', + async ({ page }) => { + //Verify that an testdata JSON file can be imported from 3 dot menu on folder domain object + //Verify correctness of imported domain object + } + ); + test.fixme('Verify that a nested Objects can be importAsJSON', async ({ page }) => { + // Testdata with hierarchy + // ImportAsJSON on Tree + // Verify Hierarchy + }); + test.fixme( + 'Verify that the ImportAsJSON dropdown does not appear for the item X', + async ({ page }) => { + // Other than non-persistible objects + } + ); +}); diff --git a/e2e/tests/functional/plugins/lad/lad.e2e.spec.js b/e2e/tests/functional/plugins/lad/lad.e2e.spec.js index 6dce8b5b9f..e546f4ab31 100644 --- a/e2e/tests/functional/plugins/lad/lad.e2e.spec.js +++ b/e2e/tests/functional/plugins/lad/lad.e2e.spec.js @@ -21,189 +21,201 @@ *****************************************************************************/ const { test, expect } = require('../../../../pluginFixtures'); -const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode, selectInspectorTab } = require('../../../../appActions'); +const { + createDomainObjectWithDefaults, + setStartOffset, + setFixedTimeMode, + setRealTimeMode, + selectInspectorTab +} = require('../../../../appActions'); test.describe('Testing LAD table configuration', () => { - test.beforeEach(async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); - // Create LAD table - const ladTable = await createDomainObjectWithDefaults(page, { - type: 'LAD Table', - name: "Test LAD Table" - }); - - // Create Sine Wave Generator - await createDomainObjectWithDefaults(page, { - type: 'Sine Wave Generator', - name: "Test Sine Wave Generator", - parent: ladTable.uuid - }); - - await page.goto(ladTable.url); - }); - test('in edit mode, LAD Tables provide ability to hide columns', async ({ page }) => { - // 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('role=treeitem[name=/Test Sine Wave Generator/]', '.c-lad-table-wrapper'); - // select configuration tab in inspector - await selectInspectorTab(page, 'LAD Table Configuration'); - - // make sure headers are visible initially - await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible(); - - // hide timestamp column - await page.getByLabel('Timestamp').uncheck(); - await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible(); - - // hide units & type column - await page.getByLabel('Units').uncheck(); - await page.getByLabel('Type').uncheck(); - await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden(); - - // save and reload and verify they columns are still hidden - await page.locator('button[title="Save"]').click(); - await page.locator('text=Save and Finish Editing').click(); - await page.reload(); - await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden(); - - // Edit LAD table - await page.locator('[title="Edit"]').click(); - await selectInspectorTab(page, 'LAD Table Configuration'); - - // show timestamp column - await page.getByLabel('Timestamp').check(); - await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible(); - - // save and reload and make sure only timestamp is still visible - await page.locator('button[title="Save"]').click(); - await page.locator('text=Save and Finish Editing').click(); - await page.reload(); - await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden(); - await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible(); - - // Edit LAD table - await page.locator('[title="Edit"]').click(); - await selectInspectorTab(page, 'LAD Table Configuration'); - - // show units and type columns - await page.getByLabel('Units').check(); - await page.getByLabel('Type').check(); - await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible(); - - // save and reload and make sure all columns are still visible - await page.locator('button[title="Save"]').click(); - await page.locator('text=Save and Finish Editing').click(); - await page.reload(); - await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible(); + // Create LAD table + const ladTable = await createDomainObjectWithDefaults(page, { + type: 'LAD Table', + name: 'Test LAD Table' }); - test('LAD Tables don\'t allow selection of rows but does show context click menus', async ({ page }) => { - const cell = await page.locator('.js-first-data'); - const userSelectable = await cell.evaluate((el) => { - return window.getComputedStyle(el).getPropertyValue('user-select'); - }); - - expect(userSelectable).toBe('none'); - // Right-click on the LAD table row - await cell.click({ - button: 'right' - }); - const menuOptions = page.locator('.c-menu ul'); - await expect.soft(menuOptions).toContainText('View Full Datum'); - await expect.soft(menuOptions).toContainText('View Historical Data'); - await expect.soft(menuOptions).toContainText('Remove'); - // await page.locator('li[title="Remove this object from its containing object."]').click(); + // Create Sine Wave Generator + await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: 'Test Sine Wave Generator', + parent: ladTable.uuid }); + + await page.goto(ladTable.url); + }); + test('in edit mode, LAD Tables provide ability to hide columns', async ({ page }) => { + // 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('role=treeitem[name=/Test Sine Wave Generator/]', '.c-lad-table-wrapper'); + // select configuration tab in inspector + await selectInspectorTab(page, 'LAD Table Configuration'); + + // make sure headers are visible initially + await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible(); + await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible(); + await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible(); + + // hide timestamp column + await page.getByLabel('Timestamp').uncheck(); + await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden(); + await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible(); + await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible(); + + // hide units & type column + await page.getByLabel('Units').uncheck(); + await page.getByLabel('Type').uncheck(); + await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden(); + await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden(); + await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden(); + + // save and reload and verify they columns are still hidden + await page.locator('button[title="Save"]').click(); + await page.locator('text=Save and Finish Editing').click(); + await page.reload(); + await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden(); + await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden(); + await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden(); + + // Edit LAD table + await page.locator('[title="Edit"]').click(); + await selectInspectorTab(page, 'LAD Table Configuration'); + + // show timestamp column + await page.getByLabel('Timestamp').check(); + await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden(); + await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden(); + await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible(); + + // save and reload and make sure only timestamp is still visible + await page.locator('button[title="Save"]').click(); + await page.locator('text=Save and Finish Editing').click(); + await page.reload(); + await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden(); + await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden(); + await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible(); + + // Edit LAD table + await page.locator('[title="Edit"]').click(); + await selectInspectorTab(page, 'LAD Table Configuration'); + + // show units and type columns + await page.getByLabel('Units').check(); + await page.getByLabel('Type').check(); + await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible(); + await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible(); + await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible(); + + // save and reload and make sure all columns are still visible + await page.locator('button[title="Save"]').click(); + await page.locator('text=Save and Finish Editing').click(); + await page.reload(); + await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible(); + await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible(); + await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible(); + }); + + test("LAD Tables don't allow selection of rows but does show context click menus", async ({ + page + }) => { + const cell = await page.locator('.js-first-data'); + const userSelectable = await cell.evaluate((el) => { + return window.getComputedStyle(el).getPropertyValue('user-select'); + }); + + expect(userSelectable).toBe('none'); + // Right-click on the LAD table row + await cell.click({ + button: 'right' + }); + const menuOptions = page.locator('.c-menu ul'); + await expect.soft(menuOptions).toContainText('View Full Datum'); + await expect.soft(menuOptions).toContainText('View Historical Data'); + await expect.soft(menuOptions).toContainText('Remove'); + // await page.locator('li[title="Remove this object from its containing object."]').click(); + }); }); test.describe('Testing LAD table @unstable', () => { - let sineWaveObject; - test.beforeEach(async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); - await setRealTimeMode(page); + let sineWaveObject; + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + await setRealTimeMode(page); - // Create Sine Wave Generator - sineWaveObject = await createDomainObjectWithDefaults(page, { - type: 'Sine Wave Generator', - name: "Test Sine Wave Generator" - }); + // 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 real time', async ({ + page + }) => { + // Create LAD table + await createDomainObjectWithDefaults(page, { + type: 'LAD Table', + name: 'Test LAD Table' }); - 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(); + // 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(); + // 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); + // 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(); - // 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); + 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); + }); }); /** @@ -216,18 +228,20 @@ test.describe('Testing LAD table @unstable', () => { * @returns {Promise} the formatted sin telemetry value */ async function subscribeToTelemetry(page, objectIdentifier) { - const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getTelemValue', resolve)); + 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); + 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; + return getTelemValuePromise; } diff --git a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js index 3ecd4a4f53..79aa68eaeb 100644 --- a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js @@ -32,417 +32,454 @@ const path = require('path'); const NOTEBOOK_NAME = 'Notebook'; 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.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 - }); + // 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: 'domcontentloaded' }); + //The following test cases are associated with Notebook Sections + test.beforeEach(async ({ page }) => { + //Navigate to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); - // Create Notebook - await createDomainObjectWithDefaults(page, { - type: NOTEBOOK_NAME - }); - }); - test('Default and new sections are automatically named Unnamed Section with Unnamed Page', async ({ page }) => { - const notebookSectionNames = page.locator('.c-notebook__sections .c-list__item__name'); - const notebookPageNames = page.locator('.c-notebook__pages .c-list__item__name'); - await expect(notebookSectionNames).toBeHidden(); - await expect(notebookPageNames).toBeHidden(); - // Expand sidebar - await page.locator('.c-notebook__toggle-nav-button').click(); - // Check that the default section and page are created and the name matches the defaults - const defaultSectionName = await notebookSectionNames.innerText(); - await expect(notebookSectionNames).toBeVisible(); - expect(defaultSectionName).toBe('Unnamed Section'); - const defaultPageName = await notebookPageNames.innerText(); - await expect(notebookPageNames).toBeVisible(); - expect(defaultPageName).toBe('Unnamed Page'); - - // Add a section - 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 notebookSectionNames.nth(1).innerText(); - await expect(notebookSectionNames.nth(1)).toBeVisible(); - expect(newSectionName).toBe('Unnamed Section'); - const newPageName = await notebookPageNames.innerText(); - await expect(notebookPageNames).toBeVisible(); - 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 + // Create Notebook + await createDomainObjectWithDefaults(page, { + type: NOTEBOOK_NAME }); + }); + test('Default and new sections are automatically named Unnamed Section with Unnamed Page', async ({ + page + }) => { + const notebookSectionNames = page.locator('.c-notebook__sections .c-list__item__name'); + const notebookPageNames = page.locator('.c-notebook__pages .c-list__item__name'); + await expect(notebookSectionNames).toBeHidden(); + await expect(notebookPageNames).toBeHidden(); + // Expand sidebar + await page.locator('.c-notebook__toggle-nav-button').click(); + // Check that the default section and page are created and the name matches the defaults + const defaultSectionName = await notebookSectionNames.innerText(); + await expect(notebookSectionNames).toBeVisible(); + expect(defaultSectionName).toBe('Unnamed Section'); + const defaultPageName = await notebookPageNames.innerText(); + await expect(notebookPageNames).toBeVisible(); + expect(defaultPageName).toBe('Unnamed Page'); + + // Add a section + 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 notebookSectionNames.nth(1).innerText(); + await expect(notebookSectionNames.nth(1)).toBeVisible(); + expect(newSectionName).toBe('Unnamed Section'); + const newPageName = await notebookPageNames.innerText(); + await expect(notebookPageNames).toBeVisible(); + 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: 'domcontentloaded' }); + //The following test cases are associated with Notebook Pages + test.beforeEach(async ({ page }) => { + //Navigate to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); - // Create Notebook - await createDomainObjectWithDefaults(page, { - type: NOTEBOOK_NAME - }); + // Create Notebook + await createDomainObjectWithDefaults(page, { + type: NOTEBOOK_NAME }); - //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(); + }); + //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 - }); + // 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 export tests', () => { - test.beforeEach(async ({ page }) => { - //Navigate to baseURL - await page.goto('./', { waitUntil: 'domcontentloaded' }); + test.beforeEach(async ({ page }) => { + //Navigate to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); - // Create Notebook - await createDomainObjectWithDefaults(page, { - type: NOTEBOOK_NAME - }); + // Create Notebook + await createDomainObjectWithDefaults(page, { + type: NOTEBOOK_NAME }); - test('can export notebook as text', async ({ page }) => { - await nbUtils.enterTextEntry(page, `Foo bar entry`); - // Click on 3 Dot Menu - await page.locator('button[title="More options"]').click(); - const downloadPromise = page.waitForEvent('download'); + }); + test('can export notebook as text', async ({ page }) => { + await nbUtils.enterTextEntry(page, `Foo bar entry`); + // Click on 3 Dot Menu + await page.locator('button[title="More options"]').click(); + const downloadPromise = page.waitForEvent('download'); - await page.getByRole('menuitem', { name: /Export Notebook as Text/ }).click(); + await page.getByRole('menuitem', { name: /Export Notebook as Text/ }).click(); - await page.getByRole('button', { name: 'Save' }).click(); - const download = await downloadPromise; - const readStream = await download.createReadStream(); - const exportedText = await streamToString(readStream); - expect(exportedText).toContain('Foo bar entry'); - }); - test.fixme('can export multiple notebook entries as text ', async ({ page }) => {}); - test.fixme('can export all notebook entry metdata', async ({ page }) => {}); - test.fixme('can export all notebook tags', async ({ page }) => {}); - test.fixme('can export all notebook snapshots', async ({ page }) => {}); + await page.getByRole('button', { name: 'Save' }).click(); + const download = await downloadPromise; + const readStream = await download.createReadStream(); + const exportedText = await streamToString(readStream); + expect(exportedText).toContain('Foo bar entry'); + }); + test.fixme('can export multiple notebook entries as text ', async ({ page }) => {}); + test.fixme('can export all notebook entry metdata', async ({ page }) => {}); + test.fixme('can export all notebook tags', async ({ page }) => {}); + test.fixme('can export all notebook snapshots', async ({ page }) => {}); }); 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.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', () => { - // Create Notebook with URL Whitelist - let notebookObject; - test.beforeEach(async ({ page }) => { - // eslint-disable-next-line no-undef - await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitNotebookWithUrls.js') }); - await page.goto('./', { waitUntil: 'domcontentloaded' }); - - notebookObject = await createDomainObjectWithDefaults(page, { - type: NOTEBOOK_NAME - }); + // Create Notebook with URL Whitelist + let notebookObject; + test.beforeEach(async ({ page }) => { + // eslint-disable-next-line no-undef + await page.addInitScript({ + path: path.join(__dirname, '../../../../helper/', 'addInitNotebookWithUrls.js') }); - test('When a new entry is created, it should be focused and selected', async ({ page }) => { - // Navigate to the notebook object - await page.goto(notebookObject.url); + await page.goto('./', { waitUntil: 'domcontentloaded' }); - // Click .c-notebook__drag-area - await page.locator('.c-notebook__drag-area').click(); - await expect(page.locator('[aria-label="Notebook Entry Input"]')).toBeVisible(); - await expect(page.locator('[aria-label="Notebook Entry"]')).toHaveClass(/is-selected/); + notebookObject = await createDomainObjectWithDefaults(page, { + type: NOTEBOOK_NAME }); - test('When an object is dropped into a notebook, a new entry is created and it should be focused @unstable', async ({ page }) => { - // Create Overlay Plot - const overlayPlot = await createDomainObjectWithDefaults(page, { - type: 'Overlay Plot' - }); + }); + test('When a new entry is created, it should be focused and selected', async ({ page }) => { + // Navigate to the notebook object + await page.goto(notebookObject.url); - // Navigate to the notebook object - await page.goto(notebookObject.url); - - // Reveal the notebook in the tree - await page.getByTitle('Show selected item in tree').click(); - - await page.getByRole('treeitem', { name: overlayPlot.name }).dragTo(page.locator('.c-notebook__drag-area')); - - const embed = page.locator('.c-ne__embed__link'); - const embedName = await embed.innerText(); - - await expect(embed).toHaveClass(/icon-plot-overlay/); - expect(embedName).toBe(overlayPlot.name); + // Click .c-notebook__drag-area + await page.locator('.c-notebook__drag-area').click(); + await expect(page.locator('[aria-label="Notebook Entry Input"]')).toBeVisible(); + await expect(page.locator('[aria-label="Notebook Entry"]')).toHaveClass(/is-selected/); + }); + test('When an object is dropped into a notebook, a new entry is created and it should be focused @unstable', async ({ + page + }) => { + // Create Overlay Plot + const overlayPlot = await createDomainObjectWithDefaults(page, { + type: 'Overlay Plot' }); - test('When an object is dropped into a notebooks existing entry, it should be focused @unstable', async ({ page }) => { - // Create Overlay Plot - const overlayPlot = await createDomainObjectWithDefaults(page, { - type: 'Overlay Plot' - }); - // Navigate to the notebook object - await page.goto(notebookObject.url); + // Navigate to the notebook object + await page.goto(notebookObject.url); - // Reveal the notebook in the tree - await page.getByTitle('Show selected item in tree').click(); + // Reveal the notebook in the tree + await page.getByTitle('Show selected item in tree').click(); - await nbUtils.enterTextEntry(page, 'Entry to drop into'); - await page.getByRole('treeitem', { name: overlayPlot.name }).dragTo(page.locator('text=Entry to drop into')); + await page + .getByRole('treeitem', { name: overlayPlot.name }) + .dragTo(page.locator('.c-notebook__drag-area')); - 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.innerText(); + const embed = page.locator('.c-ne__embed__link'); + const embedName = await embed.innerText(); - await expect(embed).toHaveClass(/icon-plot-overlay/); - expect(embedName).toBe(overlayPlot.name); + await expect(embed).toHaveClass(/icon-plot-overlay/); + expect(embedName).toBe(overlayPlot.name); + }); + test('When an object is dropped into a notebooks existing entry, it should be focused @unstable', async ({ + page + }) => { + // Create Overlay Plot + const overlayPlot = await createDomainObjectWithDefaults(page, { + type: 'Overlay Plot' }); - test.fixme('new entries persist through navigation events without save', async ({ page }) => {}); - test('previous and new entries can be deleted', async ({ page }) => { - // Navigate to the notebook object - await page.goto(notebookObject.url); - await nbUtils.enterTextEntry(page, 'First Entry'); - await page.hover('text="First Entry"'); - await page.click('button[title="Delete this entry"]'); - await page.getByRole('button', { name: 'Ok' }).filter({ hasText: 'Ok' }).click(); - await expect(page.locator('text="First Entry"')).toBeHidden(); - await nbUtils.enterTextEntry(page, 'Another First Entry'); - await nbUtils.enterTextEntry(page, 'Second Entry'); - await nbUtils.enterTextEntry(page, 'Third Entry'); - await page.hover('[aria-label="Notebook Entry"] >> nth=2'); - await page.click('button[title="Delete this entry"] >> nth=2'); - await page.getByRole('button', { name: 'Ok' }).filter({ hasText: 'Ok' }).click(); - await expect(page.locator('text="Third Entry"')).toBeHidden(); - await expect(page.locator('text="Another First Entry"')).toBeVisible(); - await expect(page.locator('text="Second Entry"')).toBeVisible(); + // Navigate to the notebook object + await page.goto(notebookObject.url); + + // Reveal the notebook in the tree + await page.getByTitle('Show selected item in tree').click(); + + await nbUtils.enterTextEntry(page, 'Entry to drop into'); + await page + .getByRole('treeitem', { name: overlayPlot.name }) + .dragTo(page.locator('text=Entry to drop into')); + + const existingEntry = page.locator('.c-ne__content', { + has: page.locator('text="Entry to drop into"') }); - test('when a valid link is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => { - const TEST_LINK = 'http://www.google.com'; + const embed = existingEntry.locator('.c-ne__embed__link'); + const embedName = await embed.innerText(); - // Navigate to the notebook object - await page.goto(notebookObject.url); + await expect(embed).toHaveClass(/icon-plot-overlay/); + expect(embedName).toBe(overlayPlot.name); + }); + test.fixme('new entries persist through navigation events without save', async ({ page }) => {}); + test('previous and new entries can be deleted', async ({ page }) => { + // Navigate to the notebook object + await page.goto(notebookObject.url); - // Reveal the notebook in the tree - await page.getByTitle('Show selected item in tree').click(); + await nbUtils.enterTextEntry(page, 'First Entry'); + await page.hover('text="First Entry"'); + await page.click('button[title="Delete this entry"]'); + await page.getByRole('button', { name: 'Ok' }).filter({ hasText: 'Ok' }).click(); + await expect(page.locator('text="First Entry"')).toBeHidden(); + await nbUtils.enterTextEntry(page, 'Another First Entry'); + await nbUtils.enterTextEntry(page, 'Second Entry'); + await nbUtils.enterTextEntry(page, 'Third Entry'); + await page.hover('[aria-label="Notebook Entry"] >> nth=2'); + await page.click('button[title="Delete this entry"] >> nth=2'); + await page.getByRole('button', { name: 'Ok' }).filter({ hasText: 'Ok' }).click(); + await expect(page.locator('text="Third Entry"')).toBeHidden(); + await expect(page.locator('text="Another First Entry"')).toBeVisible(); + await expect(page.locator('text="Second Entry"')).toBeVisible(); + }); + test('when a valid link is entered into a notebook entry, it becomes clickable when viewing', async ({ + page + }) => { + const TEST_LINK = 'http://www.google.com'; - await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`); + // Navigate to the notebook object + await page.goto(notebookObject.url); - const validLink = page.locator(`a[href="${TEST_LINK}"]`); + // Reveal the notebook in the tree + await page.getByTitle('Show selected item in tree').click(); - // Start waiting for popup before clicking. Note no await. - const popupPromise = page.waitForEvent('popup'); + await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`); - await validLink.click(); - const popup = await popupPromise; + const validLink = page.locator(`a[href="${TEST_LINK}"]`); - // Wait for the popup to load. - await popup.waitForLoadState(); - expect.soft(popup.url()).toContain('www.google.com'); + // Start waiting for popup before clicking. Note no await. + const popupPromise = page.waitForEvent('popup'); - expect(await validLink.count()).toBe(1); - }); - test('when an invalid link is entered into a notebook entry, it does not become clickable when viewing', async ({ page }) => { - const TEST_LINK = 'www.google.com'; + await validLink.click(); + const popup = await popupPromise; - // Navigate to the notebook object - await page.goto(notebookObject.url); + // Wait for the popup to load. + await popup.waitForLoadState(); + expect.soft(popup.url()).toContain('www.google.com'); - // Reveal the notebook in the tree - await page.getByTitle('Show selected item in tree').click(); + expect(await validLink.count()).toBe(1); + }); + test('when an invalid link is entered into a notebook entry, it does not become clickable when viewing', async ({ + page + }) => { + const TEST_LINK = 'www.google.com'; - await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`); + // Navigate to the notebook object + await page.goto(notebookObject.url); - const invalidLink = page.locator(`a[href="${TEST_LINK}"]`); + // Reveal the notebook in the tree + await page.getByTitle('Show selected item in tree').click(); - expect(await invalidLink.count()).toBe(0); - }); - test('when a link is entered, but it is not in the whitelisted urls, it does not become clickable when viewing', async ({ page }) => { - const TEST_LINK = 'http://www.bing.com'; + await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`); - // Navigate to the notebook object - await page.goto(notebookObject.url); + const invalidLink = page.locator(`a[href="${TEST_LINK}"]`); - // Reveal the notebook in the tree - await page.getByTitle('Show selected item in tree').click(); + expect(await invalidLink.count()).toBe(0); + }); + test('when a link is entered, but it is not in the whitelisted urls, it does not become clickable when viewing', async ({ + page + }) => { + const TEST_LINK = 'http://www.bing.com'; - await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`); + // Navigate to the notebook object + await page.goto(notebookObject.url); - const invalidLink = page.locator(`a[href="${TEST_LINK}"]`); + // Reveal the notebook in the tree + await page.getByTitle('Show selected item in tree').click(); - expect(await invalidLink.count()).toBe(0); - }); - test('when a valid link with a subdomain and a valid domain in the whitelisted urls is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => { - const INVALID_TEST_LINK = 'http://bing.google.com'; + await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`); - // Navigate to the notebook object - await page.goto(notebookObject.url); + const invalidLink = page.locator(`a[href="${TEST_LINK}"]`); - // Reveal the notebook in the tree - await page.getByTitle('Show selected item in tree').click(); + expect(await invalidLink.count()).toBe(0); + }); + test('when a valid link with a subdomain and a valid domain in the whitelisted urls is entered into a notebook entry, it becomes clickable when viewing', async ({ + page + }) => { + const INVALID_TEST_LINK = 'http://bing.google.com'; - await nbUtils.enterTextEntry(page, `This should be a link: ${INVALID_TEST_LINK} is it?`); + // Navigate to the notebook object + await page.goto(notebookObject.url); - const validLink = page.locator(`a[href="${INVALID_TEST_LINK}"]`); + // Reveal the notebook in the tree + await page.getByTitle('Show selected item in tree').click(); - expect(await validLink.count()).toBe(1); - }); - test('when a valid secure link is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => { - const TEST_LINK = 'https://www.google.com'; + await nbUtils.enterTextEntry(page, `This should be a link: ${INVALID_TEST_LINK} is it?`); - // Navigate to the notebook object - await page.goto(notebookObject.url); + const validLink = page.locator(`a[href="${INVALID_TEST_LINK}"]`); - // Reveal the notebook in the tree - await page.getByTitle('Show selected item in tree').click(); + expect(await validLink.count()).toBe(1); + }); + test('when a valid secure link is entered into a notebook entry, it becomes clickable when viewing', async ({ + page + }) => { + const TEST_LINK = 'https://www.google.com'; - await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`); + // Navigate to the notebook object + await page.goto(notebookObject.url); - const validLink = page.locator(`a[href="${TEST_LINK}"]`); + // Reveal the notebook in the tree + await page.getByTitle('Show selected item in tree').click(); - // Start waiting for popup before clicking. Note no await. - const popupPromise = page.waitForEvent('popup'); + await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`); - await validLink.click(); - const popup = await popupPromise; + const validLink = page.locator(`a[href="${TEST_LINK}"]`); - // Wait for the popup to load. - await popup.waitForLoadState(); - expect.soft(popup.url()).toContain('www.google.com'); + // Start waiting for popup before clicking. Note no await. + const popupPromise = page.waitForEvent('popup'); - expect(await validLink.count()).toBe(1); - }); - test('when a nefarious link is entered into a notebook entry, it is sanitized when viewing', async ({ page }) => { - const TEST_LINK = 'http://www.google.com?bad='; - const TEST_LINK_BAD = `http://www.google.com?bad=`; + await validLink.click(); + const popup = await popupPromise; - // Navigate to the notebook object - await page.goto(notebookObject.url); + // Wait for the popup to load. + await popup.waitForLoadState(); + expect.soft(popup.url()).toContain('www.google.com'); - // Reveal the notebook in the tree - await page.getByTitle('Show selected item in tree').click(); + expect(await validLink.count()).toBe(1); + }); + test('when a nefarious link is entered into a notebook entry, it is sanitized when viewing', async ({ + page + }) => { + const TEST_LINK = 'http://www.google.com?bad='; + const TEST_LINK_BAD = `http://www.google.com?bad=`; - await nbUtils.enterTextEntry(page, `This should be a link, BUT not a bad link: ${TEST_LINK_BAD} is it?`); + // Navigate to the notebook object + await page.goto(notebookObject.url); - const sanitizedLink = page.locator(`a[href="${TEST_LINK}"]`); - const unsanitizedLink = page.locator(`a[href="${TEST_LINK_BAD}"]`); + // Reveal the notebook in the tree + await page.getByTitle('Show selected item in tree').click(); - expect.soft(await sanitizedLink.count()).toBe(1); - expect(await unsanitizedLink.count()).toBe(0); - }); + await nbUtils.enterTextEntry( + page, + `This should be a link, BUT not a bad link: ${TEST_LINK_BAD} is it?` + ); + + const sanitizedLink = page.locator(`a[href="${TEST_LINK}"]`); + const unsanitizedLink = page.locator(`a[href="${TEST_LINK_BAD}"]`); + + expect.soft(await sanitizedLink.count()).toBe(1); + expect(await unsanitizedLink.count()).toBe(0); + }); }); diff --git a/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js index b347b8c05b..a8294b2e7a 100644 --- a/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebookSnapshots.e2e.spec.js @@ -29,106 +29,135 @@ const { test, expect } = require('../../../../pluginFixtures'); // const nbUtils = require('../../../../helper/notebookUtils'); 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.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.beforeEach(async ({ page }) => { - //Navigate to baseURL - await page.goto('./', { waitUntil: 'domcontentloaded' }); + test.beforeEach(async ({ page }) => { + //Navigate to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); - // Create Notebook - // const notebook = await createDomainObjectWithDefaults(page, { - // type: 'Notebook', - // name: "Test Notebook" - // }); - // // Create Overlay Plot - // const snapShotObject = await createDomainObjectWithDefaults(page, { - // type: 'Overlay Plot', - // name: "Dropped Overlay Plot" - // }); + // Create Notebook + // const notebook = await createDomainObjectWithDefaults(page, { + // type: 'Notebook', + // name: "Test Notebook" + // }); + // // Create Overlay Plot + // const snapShotObject = await createDomainObjectWithDefaults(page, { + // type: 'Overlay Plot', + // name: "Dropped Overlay Plot" + // }); - await page.getByRole('button', { name: ' Snapshot ' }).click(); - await page.getByRole('menuitem', { name: ' Save to Notebook Snapshots' }).click(); - await page.getByRole('button', { name: 'Show' }).click(); - - }); - 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 with 3 dot action menu', async ({ page }) => {}); - test.fixme('A snapshot can be Viewed, Annotated, display deleted, and saved from Container with 3 dot action menu', async ({ page }) => { - await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More options').click(); - await page.getByRole('menuitem', { name: ' View Snapshot' }).click(); - await expect(page.locator('.c-overlay__outer')).toBeVisible(); - await page.getByTitle('Annotate').click(); - await expect(page.locator('#snap-annotation-canvas')).toBeVisible(); - await page.getByRole('button', { name: '' }).click(); - // await expect(page.locator('#snap-annotation-canvas')).not.toBeVisible(); - await page.getByRole('button', { name: 'Save' }).click(); - await page.getByRole('button', { name: 'Done' }).click(); - //await expect(await page.locator) - }); - test('A snapshot can be Quick Viewed from Container with 3 dot action menu', async ({ page }) => { - await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More options').click(); - await page.getByRole('menuitem', { name: 'Quick View' }).click(); - await expect(page.locator('.c-overlay__outer')).toBeVisible(); - }); - test.fixme('A snapshot can be Navigated To from Container with 3 dot action menu', async ({ page }) => {}); - test.fixme('A snapshot can be Navigated To Item in Time from Container with 3 dot action menu', 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 - }); + await page.getByRole('button', { name: ' Snapshot ' }).click(); + await page.getByRole('menuitem', { name: ' Save to Notebook Snapshots' }).click(); + await page.getByRole('button', { name: 'Show' }).click(); + }); + 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 with 3 dot action menu', + async ({ page }) => {} + ); + test.fixme( + 'A snapshot can be Viewed, Annotated, display deleted, and saved from Container with 3 dot action menu', + async ({ page }) => { + await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More options').click(); + await page.getByRole('menuitem', { name: ' View Snapshot' }).click(); + await expect(page.locator('.c-overlay__outer')).toBeVisible(); + await page.getByTitle('Annotate').click(); + await expect(page.locator('#snap-annotation-canvas')).toBeVisible(); + await page.getByRole('button', { name: '' }).click(); + // await expect(page.locator('#snap-annotation-canvas')).not.toBeVisible(); + await page.getByRole('button', { name: 'Save' }).click(); + await page.getByRole('button', { name: 'Done' }).click(); + //await expect(await page.locator) + } + ); + test('A snapshot can be Quick Viewed from Container with 3 dot action menu', async ({ page }) => { + await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More options').click(); + await page.getByRole('menuitem', { name: 'Quick View' }).click(); + await expect(page.locator('.c-overlay__outer')).toBeVisible(); + }); + test.fixme( + 'A snapshot can be Navigated To from Container with 3 dot action menu', + async ({ page }) => {} + ); + test.fixme( + 'A snapshot can be Navigated To Item in Time from Container with 3 dot action menu', + 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/notebookWithCouchDB.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js index e60db7e0cb..377e6de07f 100644 --- a/e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js @@ -29,180 +29,184 @@ const { createDomainObjectWithDefaults } = require('../../../../appActions'); const nbUtils = require('../../../../helper/notebookUtils'); test.describe('Notebook Tests with CouchDB @couchdb', () => { - let testNotebook; + let testNotebook; - test.beforeEach(async ({ page }) => { - //Navigate to baseURL - await page.goto('./', { waitUntil: 'domcontentloaded' }); + test.beforeEach(async ({ page }) => { + //Navigate to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); - // Create Notebook - testNotebook = await createDomainObjectWithDefaults(page, {type: 'Notebook' }); - await page.goto(testNotebook.url, { waitUntil: 'networkidle'}); + // Create Notebook + testNotebook = await createDomainObjectWithDefaults(page, { type: 'Notebook' }); + await page.goto(testNotebook.url, { waitUntil: 'networkidle' }); + }); + + test('Inspect Notebook Entry Network Requests', async ({ page }) => { + //Ensure we're on the annotations Tab in the inspector + await page.getByText('Annotations').click(); + // Expand sidebar + await page.locator('.c-notebook__toggle-nav-button').click(); + + // Collect all request events to count and assert after notebook action + let notebookElementsRequests = []; + page.on('request', (request) => notebookElementsRequests.push(request)); + + //Clicking Add Page generates + let [notebookUrlRequest, allDocsRequest] = await Promise.all([ + // Waits for the next request with the specified url + page.waitForRequest(`**/openmct/${testNotebook.uuid}`), + page.waitForRequest('**/openmct/_all_docs?include_docs=true'), + // Triggers the request + page.click('[aria-label="Add Page"]') + ]); + // Ensures that there are no other network requests + await page.waitForLoadState('networkidle'); + + // Assert that only two requests are made + // Network Requests are: + // 1) The actual POST to create the page + // 2) The shared worker event from 👆 request + expect(notebookElementsRequests.length).toBe(2); + + // Assert on request object + expect(notebookUrlRequest.postDataJSON().metadata.name).toBe(testNotebook.name); + expect(notebookUrlRequest.postDataJSON().model.persisted).toBeGreaterThanOrEqual( + notebookUrlRequest.postDataJSON().model.modified + ); + expect(allDocsRequest.postDataJSON().keys).toContain(testNotebook.uuid); + + // Add an entry + // Network Requests are: + // 1) The actual POST to create the entry + // 2) The shared worker event from 👆 POST request + notebookElementsRequests = []; + await nbUtils.enterTextEntry(page, 'First Entry'); + await page.waitForLoadState('networkidle'); + expect(notebookElementsRequests.length).toBeLessThanOrEqual(2); + + // Add some tags + // Network Requests are for each tag creation are: + // 1) Getting the original path of the parent object + // 2) Getting the original path of the grandparent object (recursive call) + // 3) Creating the annotation/tag object + // 4) The shared worker event from 👆 POST request + // 5) Mutate notebook domain object's annotationModified property + // 6) The shared worker event from 👆 POST request + // 7) Notebooks fetching new annotations due to annotationModified changed + // 8) The update of the notebook domain's object's modified property + // 9) The shared worker event from 👆 POST request + // 10) Entry is timestamped + // 11) The shared worker event from 👆 POST request + + notebookElementsRequests = []; + await addTagAndAwaitNetwork(page, 'Driving'); + expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(11); + + notebookElementsRequests = []; + await addTagAndAwaitNetwork(page, 'Drilling'); + expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(11); + + notebookElementsRequests = []; + await addTagAndAwaitNetwork(page, 'Science'); + expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(11); + + // Delete all the tags + // Network requests are: + // 1) Send POST to mutate _delete property to true on annotation with tag + // 2) The shared worker event from 👆 POST request + // 3) Timestamp update on entry + // 4) The shared worker event from 👆 POST request + // This happens for 3 tags so 12 requests + notebookElementsRequests = []; + await removeTagAndAwaitNetwork(page, 'Driving'); + await removeTagAndAwaitNetwork(page, 'Drilling'); + await removeTagAndAwaitNetwork(page, 'Science'); + expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(12); + + // Add two more pages + await page.click('[aria-label="Add Page"]'); + await page.click('[aria-label="Add Page"]'); + + // Add three entries + await nbUtils.enterTextEntry(page, 'First Entry'); + await nbUtils.enterTextEntry(page, 'Second Entry'); + await nbUtils.enterTextEntry(page, 'Third Entry'); + + // Add three tags + await addTagAndAwaitNetwork(page, 'Science'); + await addTagAndAwaitNetwork(page, 'Drilling'); + await addTagAndAwaitNetwork(page, 'Driving'); + + // Add a fourth entry + // Network requests are: + // 1) Send POST to add new entry + // 2) The shared worker event from 👆 POST request + // 3) Timestamp update on entry + // 4) The shared worker event from 👆 POST request + notebookElementsRequests = []; + await nbUtils.enterTextEntry(page, 'Fourth Entry'); + page.waitForLoadState('networkidle'); + + expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4); + + // Add a fifth entry + // Network requests are: + // 1) Send POST to add new entry + // 2) The shared worker event from 👆 POST request + // 3) Timestamp update on entry + // 4) The shared worker event from 👆 POST request + notebookElementsRequests = []; + await nbUtils.enterTextEntry(page, 'Fifth Entry'); + page.waitForLoadState('networkidle'); + + expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4); + + // Add a sixth entry + // 1) Send POST to add new entry + // 2) The shared worker event from 👆 POST request + // 3) Timestamp update on entry + // 4) The shared worker event from 👆 POST request + notebookElementsRequests = []; + await nbUtils.enterTextEntry(page, 'Sixth Entry'); + page.waitForLoadState('networkidle'); + + expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4); + }); + + test('Search tests', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/akhenry/openmct-yamcs/issues/69' }); + await page.getByText('Annotations').click(); + await nbUtils.enterTextEntry(page, 'First Entry'); - test('Inspect Notebook Entry Network Requests', async ({ page }) => { - //Ensure we're on the annotations Tab in the inspector - await page.getByText('Annotations').click(); - // Expand sidebar - await page.locator('.c-notebook__toggle-nav-button').click(); + // Add three tags + await addTagAndAwaitNetwork(page, 'Science'); + await addTagAndAwaitNetwork(page, 'Drilling'); + await addTagAndAwaitNetwork(page, 'Driving'); - // Collect all request events to count and assert after notebook action - let notebookElementsRequests = []; - page.on('request', (request) => notebookElementsRequests.push(request)); + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); + //Partial match for "Science" should only return Science + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc'); + await expect(page.locator('[aria-label="Search Result"]').first()).toContainText('Science'); + await expect(page.locator('[aria-label="Search Result"]').first()).not.toContainText('Driving'); + await expect(page.locator('[aria-label="Search Result"]').first()).not.toContainText( + 'Drilling' + ); - //Clicking Add Page generates - let [notebookUrlRequest, allDocsRequest] = await Promise.all([ - // Waits for the next request with the specified url - page.waitForRequest(`**/openmct/${testNotebook.uuid}`), - page.waitForRequest('**/openmct/_all_docs?include_docs=true'), - // Triggers the request - page.click('[aria-label="Add Page"]') - ]); - // Ensures that there are no other network requests - await page.waitForLoadState('networkidle'); - - // Assert that only two requests are made - // Network Requests are: - // 1) The actual POST to create the page - // 2) The shared worker event from 👆 request - expect(notebookElementsRequests.length).toBe(2); - - // Assert on request object - expect(notebookUrlRequest.postDataJSON().metadata.name).toBe(testNotebook.name); - expect(notebookUrlRequest.postDataJSON().model.persisted).toBeGreaterThanOrEqual(notebookUrlRequest.postDataJSON().model.modified); - expect(allDocsRequest.postDataJSON().keys).toContain(testNotebook.uuid); - - // Add an entry - // Network Requests are: - // 1) The actual POST to create the entry - // 2) The shared worker event from 👆 POST request - notebookElementsRequests = []; - await nbUtils.enterTextEntry(page, 'First Entry'); - await page.waitForLoadState('networkidle'); - expect(notebookElementsRequests.length).toBeLessThanOrEqual(2); - - // Add some tags - // Network Requests are for each tag creation are: - // 1) Getting the original path of the parent object - // 2) Getting the original path of the grandparent object (recursive call) - // 3) Creating the annotation/tag object - // 4) The shared worker event from 👆 POST request - // 5) Mutate notebook domain object's annotationModified property - // 6) The shared worker event from 👆 POST request - // 7) Notebooks fetching new annotations due to annotationModified changed - // 8) The update of the notebook domain's object's modified property - // 9) The shared worker event from 👆 POST request - // 10) Entry is timestamped - // 11) The shared worker event from 👆 POST request - - notebookElementsRequests = []; - await addTagAndAwaitNetwork(page, 'Driving'); - expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(11); - - notebookElementsRequests = []; - await addTagAndAwaitNetwork(page, 'Drilling'); - expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(11); - - notebookElementsRequests = []; - await addTagAndAwaitNetwork(page, 'Science'); - expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(11); - - // Delete all the tags - // Network requests are: - // 1) Send POST to mutate _delete property to true on annotation with tag - // 2) The shared worker event from 👆 POST request - // 3) Timestamp update on entry - // 4) The shared worker event from 👆 POST request - // This happens for 3 tags so 12 requests - notebookElementsRequests = []; - await removeTagAndAwaitNetwork(page, 'Driving'); - await removeTagAndAwaitNetwork(page, 'Drilling'); - await removeTagAndAwaitNetwork(page, 'Science'); - expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(12); - - // Add two more pages - await page.click('[aria-label="Add Page"]'); - await page.click('[aria-label="Add Page"]'); - - // Add three entries - await nbUtils.enterTextEntry(page, 'First Entry'); - await nbUtils.enterTextEntry(page, 'Second Entry'); - await nbUtils.enterTextEntry(page, 'Third Entry'); - - // Add three tags - await addTagAndAwaitNetwork(page, 'Science'); - await addTagAndAwaitNetwork(page, 'Drilling'); - await addTagAndAwaitNetwork(page, 'Driving'); - - // Add a fourth entry - // Network requests are: - // 1) Send POST to add new entry - // 2) The shared worker event from 👆 POST request - // 3) Timestamp update on entry - // 4) The shared worker event from 👆 POST request - notebookElementsRequests = []; - await nbUtils.enterTextEntry(page, 'Fourth Entry'); - page.waitForLoadState('networkidle'); - - expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4); - - // Add a fifth entry - // Network requests are: - // 1) Send POST to add new entry - // 2) The shared worker event from 👆 POST request - // 3) Timestamp update on entry - // 4) The shared worker event from 👆 POST request - notebookElementsRequests = []; - await nbUtils.enterTextEntry(page, 'Fifth Entry'); - page.waitForLoadState('networkidle'); - - expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4); - - // Add a sixth entry - // 1) Send POST to add new entry - // 2) The shared worker event from 👆 POST request - // 3) Timestamp update on entry - // 4) The shared worker event from 👆 POST request - notebookElementsRequests = []; - await nbUtils.enterTextEntry(page, 'Sixth Entry'); - page.waitForLoadState('networkidle'); - - expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4); - }); - - test('Search tests', async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/akhenry/openmct-yamcs/issues/69' - }); - await page.getByText('Annotations').click(); - await nbUtils.enterTextEntry(page, 'First Entry'); - - // Add three tags - await addTagAndAwaitNetwork(page, 'Science'); - await addTagAndAwaitNetwork(page, 'Drilling'); - await addTagAndAwaitNetwork(page, 'Driving'); - - await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); - //Partial match for "Science" should only return Science - await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc'); - await expect(page.locator('[aria-label="Search Result"]').first()).toContainText("Science"); - await expect(page.locator('[aria-label="Search Result"]').first()).not.toContainText("Driving"); - await expect(page.locator('[aria-label="Search Result"]').first()).not.toContainText("Drilling"); - - //Searching for a tag which does not exist should return an empty result - await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); - await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq'); - await expect(page.locator('text=No results found')).toBeVisible(); - }); + //Searching for a tag which does not exist should return an empty result + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq'); + await expect(page.locator('text=No results found')).toBeVisible(); + }); }); // Try to reduce indeterminism of browser requests by only returning fetch requests. // Filter out preflight CORS, fetching stylesheets, page icons, etc. that can occur during tests function filterNonFetchRequests(requests) { - return requests.filter(request => { - return (request.resourceType() === 'fetch'); - }); + return requests.filter((request) => { + return request.resourceType() === 'fetch'; + }); } /** @@ -212,17 +216,17 @@ function filterNonFetchRequests(requests) { * @param {string} tagName */ async function addTagAndAwaitNetwork(page, tagName) { - await page.hover(`button:has-text("Add Tag")`); - await page.locator(`button:has-text("Add Tag")`).click(); - await page.locator('[placeholder="Type to select tag"]').click(); - await Promise.all([ - // Waits for the next request with the specified url - page.waitForRequest('**/openmct/_all_docs?include_docs=true'), - // Triggers the request - page.locator(`[aria-label="Autocomplete Options"] >> text=${tagName}`).click(), - expect(page.locator(`[aria-label="Tag"]:has-text("${tagName}")`)).toBeVisible() - ]); - await page.waitForLoadState('networkidle'); + await page.hover(`button:has-text("Add Tag")`); + await page.locator(`button:has-text("Add Tag")`).click(); + await page.locator('[placeholder="Type to select tag"]').click(); + await Promise.all([ + // Waits for the next request with the specified url + page.waitForRequest('**/openmct/_all_docs?include_docs=true'), + // Triggers the request + page.locator(`[aria-label="Autocomplete Options"] >> text=${tagName}`).click(), + expect(page.locator(`[aria-label="Tag"]:has-text("${tagName}")`)).toBeVisible() + ]); + await page.waitForLoadState('networkidle'); } /** @@ -232,12 +236,14 @@ async function addTagAndAwaitNetwork(page, tagName) { * @param {string} tagName */ async function removeTagAndAwaitNetwork(page, tagName) { - await page.hover(`[aria-label="Tag"]:has-text("${tagName}")`); - await Promise.all([ - page.locator(`[aria-label="Remove tag ${tagName}"]`).click(), - //With this pattern, we're awaiting the response but asserting on the request payload. - page.waitForResponse(resp => resp.request().postData().includes(`"_deleted":true`) && resp.status() === 201) - ]); - await expect(page.locator(`[aria-label="Tag"]:has-text("${tagName}")`)).toBeHidden(); - await page.waitForLoadState('networkidle'); + await page.hover(`[aria-label="Tag"]:has-text("${tagName}")`); + await Promise.all([ + page.locator(`[aria-label="Remove tag ${tagName}"]`).click(), + //With this pattern, we're awaiting the response but asserting on the request payload. + page.waitForResponse( + (resp) => resp.request().postData().includes(`"_deleted":true`) && resp.status() === 201 + ) + ]); + await expect(page.locator(`[aria-label="Tag"]:has-text("${tagName}")`)).toBeHidden(); + await page.waitForLoadState('networkidle'); } diff --git a/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js b/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js index 91a39c9d89..584a1c0401 100644 --- a/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js @@ -21,7 +21,10 @@ *****************************************************************************/ /* global __dirname */ const { test, expect, streamToString } = require('../../../../pluginFixtures'); -const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions'); +const { + openObjectTreeContextMenu, + createDomainObjectWithDefaults +} = require('../../../../appActions'); const path = require('path'); const nbUtils = require('../../../../helper/notebookUtils'); @@ -30,183 +33,186 @@ 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); - }); + 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(`${notebook.name}`); - }); + test('Can be renamed @addInit', async ({ page }) => { + await expect(page.locator('.l-browse-bar__object-name')).toContainText(`${notebook.name}`); + }); - test('Can be deleted if there are no locked pages @addInit', async ({ page }) => { - await openObjectTreeContextMenu(page, notebook.url); + test('Can be deleted if there are no locked pages @addInit', async ({ page }) => { + await openObjectTreeContextMenu(page, notebook.url); - const menuOptions = page.locator('.c-menu ul'); - await expect.soft(menuOptions).toContainText('Remove'); + const menuOptions = page.locator('.c-menu ul'); + await expect.soft(menuOptions).toContainText('Remove'); - const restrictedNotebookTreeObject = page.locator(`a:has-text("${notebook.name}")`); + const restrictedNotebookTreeObject = page.locator(`a:has-text("${notebook.name}")`); - // notebook tree object exists - expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1); + // notebook tree object exists + expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1); - // Click Remove Text - await page.locator('li[role="menuitem"]:has-text("Remove")').click(); + // Click Remove Text + await page.locator('li[role="menuitem"]:has-text("Remove")').click(); - // Click 'OK' on confirmation window and wait for save banner to appear - await Promise.all([ - page.waitForNavigation(), - page.locator('button:has-text("OK")').click(), - page.waitForSelector('.c-message-banner__message') - ]); + // Click 'OK' on confirmation window and wait for save banner to appear + await Promise.all([ + page.waitForNavigation(), + page.locator('button:has-text("OK")').click(), + page.waitForSelector('.c-message-banner__message') + ]); - // has been deleted - expect(await restrictedNotebookTreeObject.count()).toEqual(0); - }); + // 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('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); + 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(); - }); + // 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); + 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); + // 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'); + // 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'); - }); + await expect(menuOptions).not.toContainText('Remove'); + }); - test('Can still: add page, rename, add entry, delete unlocked pages @addInit', async ({ page }) => { - // Add a new page to the section - await page.getByRole('button', { name: 'Add Page' }).click(); - // Focus the new page by clicking it - await page.getByText('Unnamed Page').nth(1).click(); - // Rename the new page - await page.getByText('Unnamed Page').nth(1).fill(TEST_TEXT_NAME); + test('Can still: add page, rename, add entry, delete unlocked pages @addInit', async ({ + page + }) => { + // Add a new page to the section + await page.getByRole('button', { name: 'Add Page' }).click(); + // Focus the new page by clicking it + await page.getByText('Unnamed Page').nth(1).click(); + // Rename the new page + await page.getByText('Unnamed Page').nth(1).fill(TEST_TEXT_NAME); - // expect to be able to rename unlocked pages - const newPageElement = page.getByText(TEST_TEXT_NAME); - const newPageCount = await newPageElement.count(); - await newPageElement.press('Enter'); // exit contenteditable state - expect.soft(newPageCount).toEqual(1); + // expect to be able to rename unlocked pages + const newPageElement = page.getByText(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); + // enter test text + await nbUtils.enterTextEntry(page, TEST_TEXT); - // expect new page to be lockable - const commitButton = page.getByRole('button', { name: ' Commit Entries' }); - expect.soft(await commitButton.count()).toEqual(1); + // expect new page to be lockable + const commitButton = page.getByRole('button', { name: ' Commit Entries' }); + expect.soft(await commitButton.count()).toEqual(1); - // Click the context menu button for the new page - await page.getByTitle('Open context menu').click(); - // Delete the page - await page.getByRole('listitem', { name: 'Delete Page' }).click(); - // Click OK button - await page.getByRole('button', { name: 'Ok' }).click(); + // Click the context menu button for the new page + await page.getByTitle('Open context menu').click(); + // Delete the page + await page.getByRole('listitem', { name: 'Delete Page' }).click(); + // Click OK button + await page.getByRole('button', { name: 'Ok' }).click(); - // deleted page, should no longer exist - const deletedPageElement = page.getByText(TEST_TEXT_NAME); - expect(await deletedPageElement.count()).toEqual(0); - }); + // deleted page, should no longer exist + const deletedPageElement = page.getByText(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 }) => { + const notebook = await startAndAddRestrictedNotebookObject(page); + await nbUtils.dragAndDropEmbed(page, notebook); + }); - test.beforeEach(async ({ page }) => { - const notebook = await startAndAddRestrictedNotebookObject(page); - await nbUtils.dragAndDropEmbed(page, notebook); - }); + 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-icon-button').click(); // embed popup menu - 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-icon-button').click(); // embed popup menu + const embedMenu = page.locator('body >> .c-menu'); + await expect(embedMenu).toContainText('Remove This Embed'); + }); - 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-icon-button').click(); // embed popup menu - - const embedMenu = page.locator('body >> .c-menu'); - await expect(embedMenu).not.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-icon-button').click(); // embed popup menu + const embedMenu = page.locator('body >> .c-menu'); + await expect(embedMenu).not.toContainText('Remove This Embed'); + }); }); test.describe('can export restricted notebook as text', () => { - test.beforeEach(async ({ page }) => { - await startAndAddRestrictedNotebookObject(page); - }); + test.beforeEach(async ({ page }) => { + await startAndAddRestrictedNotebookObject(page); + }); - test('basic functionality ', async ({ page }) => { - await nbUtils.enterTextEntry(page, `Foo bar entry`); - // Click on 3 Dot Menu - await page.locator('button[title="More options"]').click(); - const downloadPromise = page.waitForEvent('download'); + test('basic functionality ', async ({ page }) => { + await nbUtils.enterTextEntry(page, `Foo bar entry`); + // Click on 3 Dot Menu + await page.locator('button[title="More options"]').click(); + const downloadPromise = page.waitForEvent('download'); - await page.getByRole('menuitem', { name: /Export Notebook as Text/ }).click(); + await page.getByRole('menuitem', { name: /Export Notebook as Text/ }).click(); - await page.getByRole('button', { name: 'Save' }).click(); - const download = await downloadPromise; - const readStream = await download.createReadStream(); - const exportedText = await streamToString(readStream); - expect(exportedText).toContain('Foo bar entry'); + await page.getByRole('button', { name: 'Save' }).click(); + const download = await downloadPromise; + const readStream = await download.createReadStream(); + const exportedText = await streamToString(readStream); + expect(exportedText).toContain('Foo bar entry'); + }); - }); - - test.fixme('can export multiple notebook entries as text ', async ({ page }) => {}); - test.fixme('can export all notebook entry metdata', async ({ page }) => {}); - test.fixme('can export all notebook tags', async ({ page }) => {}); - test.fixme('can export all notebook snapshots', async ({ page }) => {}); + test.fixme('can export multiple notebook entries as text ', async ({ page }) => {}); + test.fixme('can export all notebook entry metdata', async ({ page }) => {}); + test.fixme('can export all notebook tags', async ({ page }) => {}); + test.fixme('can export all notebook snapshots', async ({ page }) => {}); }); /** * @param {import('@playwright/test').Page} page */ async function startAndAddRestrictedNotebookObject(page) { - await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitRestrictedNotebook.js') }); - await page.goto('./', { waitUntil: 'domcontentloaded' }); + await page.addInitScript({ + path: path.join(__dirname, '../../../../helper/', 'addInitRestrictedNotebook.js') + }); + await page.goto('./', { waitUntil: 'domcontentloaded' }); - return createDomainObjectWithDefaults(page, { type: CUSTOM_NAME }); + 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(); + 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(); + //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 index 99afd1458d..14c0f8d113 100644 --- a/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js @@ -29,243 +29,247 @@ const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../.. const nbUtils = require('../../../../helper/notebookUtils'); /** - * 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 - */ + * 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) { - const notebook = createDomainObjectWithDefaults(page, { type: 'Notebook' }); + const notebook = createDomainObjectWithDefaults(page, { type: 'Notebook' }); - for (let iteration = 0; iteration < iterations; iteration++) { - await nbUtils.enterTextEntry(page, `Entry ${iteration}`); - } + for (let iteration = 0; iteration < iterations; iteration++) { + await nbUtils.enterTextEntry(page, `Entry ${iteration}`); + } - return notebook; + return notebook; } /** - * 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 - */ + * 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) { - const notebook = await createNotebookAndEntry(page, iterations); - await selectInspectorTab(page, 'Annotations'); + const notebook = await createNotebookAndEntry(page, iterations); + await selectInspectorTab(page, 'Annotations'); - 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.locator(`[aria-label="Notebook Entry"] >> nth = ${iteration}`).click(); - await page.hover(`button:has-text("Add Tag")`); - await page.locator(`button:has-text("Add Tag")`).click(); + 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.locator(`[aria-label="Notebook Entry"] >> nth = ${iteration}`).click(); + await page.hover(`button:has-text("Add Tag")`); + await page.locator(`button:has-text("Add Tag")`).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(); + // 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")`); - await page.locator(`button:has-text("Add Tag")`).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(); - } + // 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")`); + await page.locator(`button:has-text("Add Tag")`).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(); + } - return notebook; + return notebook; } test.describe('Tagging in Notebooks @addInit', () => { - test.beforeEach(async ({ page }) => { - //Go to baseURL - await page.goto('./', { waitUntil: 'domcontentloaded' }); - }); - test('Can load tags', async ({ page }) => { - await createNotebookAndEntry(page); + test.beforeEach(async ({ page }) => { + //Go to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); + }); + test('Can load tags', async ({ page }) => { + await createNotebookAndEntry(page); - await selectInspectorTab(page, 'Annotations'); + await selectInspectorTab(page, 'Annotations'); - await page.locator('button:has-text("Add Tag")').click(); + await page.locator('button:has-text("Add Tag")').click(); - await page.locator('[placeholder="Type to select tag"]').click(); + 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="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"); + await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText('Science'); + await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText('Driving'); - await page.locator('button:has-text("Add Tag")').click(); - await page.locator('[placeholder="Type to select tag"]').click(); + await page.locator('button:has-text("Add Tag")').click(); + 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 add tags with blank entry', async ({ page }) => { - await createDomainObjectWithDefaults(page, { type: 'Notebook' }); - await selectInspectorTab(page, 'Annotations'); + 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 add tags with blank entry', async ({ page }) => { + await createDomainObjectWithDefaults(page, { type: 'Notebook' }); + await selectInspectorTab(page, 'Annotations'); - await nbUtils.enterTextEntry(page, ''); - await page.hover(`button:has-text("Add Tag")`); - await page.locator(`button:has-text("Add Tag")`).click(); + await nbUtils.enterTextEntry(page, ''); + await page.hover(`button:has-text("Add Tag")`); + await page.locator(`button:has-text("Add Tag")`).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(); + // 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(); - await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving"); - }); - test('Can cancel adding tags', async ({ page }) => { - await createNotebookAndEntry(page); + await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText('Driving'); + }); + test('Can cancel adding tags', async ({ page }) => { + await createNotebookAndEntry(page); - await selectInspectorTab(page, 'Annotations'); + await selectInspectorTab(page, 'Annotations'); - // Test canceling adding a tag after we click "Type to select tag" - await page.locator('button:has-text("Add Tag")').click(); + // Test canceling adding a tag after we click "Type to select tag" + await page.locator('button:has-text("Add Tag")').click(); - await page.locator('[placeholder="Type to select tag"]').click(); + await page.locator('[placeholder="Type to select tag"]').click(); - await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); - await expect(page.locator('button:has-text("Add Tag")')).toBeVisible(); + await expect(page.locator('button:has-text("Add Tag")')).toBeVisible(); - // Test canceling adding a tag after we just click "Add Tag" - await page.locator('button:has-text("Add Tag")').click(); + // Test canceling adding a tag after we just click "Add Tag" + await page.locator('button:has-text("Add Tag")').click(); - await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); - await expect(page.locator('button:has-text("Add Tag")')).toBeVisible(); - }); - test('Can search for tags and preview works properly', async ({ page }) => { - await createNotebookEntryAndTags(page); - await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); - await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); - await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science"); - await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving"); + await expect(page.locator('button:has-text("Add Tag")')).toBeVisible(); + }); + test('Can search for tags and preview works properly', async ({ page }) => { + await createNotebookEntryAndTags(page); + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); + await expect(page.locator('[aria-label="Search Result"]')).toContainText('Science'); + await expect(page.locator('[aria-label="Search Result"]')).not.toContainText('Driving'); - await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); - await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc'); - await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science"); - await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving"); + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc'); + await expect(page.locator('[aria-label="Search Result"]')).toContainText('Science'); + await expect(page.locator('[aria-label="Search Result"]')).not.toContainText('Driving'); - await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); - await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq'); - await expect(page.locator('text=No results found')).toBeVisible(); + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq'); + await expect(page.locator('text=No results found')).toBeVisible(); - await createDomainObjectWithDefaults(page, { - type: 'Display Layout' - }); - - // Go back into edit mode for the display layout - await page.locator('button[title="Edit"]').click(); - - await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); - await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc'); - await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science"); - await page.getByText('Entry 0').click(); - await expect(page.locator('.js-preview-window')).toBeVisible(); + await createDomainObjectWithDefaults(page, { + type: 'Display Layout' }); - test('Can delete tags', async ({ page }) => { - await createNotebookEntryAndTags(page); - // Delete Driving - await page.hover('[aria-label="Tag"]:has-text("Driving")'); - await page.locator('[aria-label="Remove tag Driving"]').click(); + // Go back into edit mode for the display layout + await page.locator('button[title="Edit"]').click(); - await expect(page.locator('[aria-label="Tags Inspector"]')).toContainText("Science"); - await expect(page.locator('[aria-label="Tags Inspector"]')).not.toContainText("Driving"); + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc'); + await expect(page.locator('[aria-label="Search Result"]')).toContainText('Science'); + await page.getByText('Entry 0').click(); + await expect(page.locator('.js-preview-window')).toBeVisible(); + }); - 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 tags', async ({ page }) => { + await createNotebookEntryAndTags(page); + // Delete Driving + await page.hover('[aria-label="Tag"]:has-text("Driving")'); + await page.locator('[aria-label="Remove tag Driving"]').click(); + + await expect(page.locator('[aria-label="Tags Inspector"]')).toContainText('Science'); + await expect(page.locator('[aria-label="Tags Inspector"]')).not.toContainText('Driving'); + + 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 entries without tags', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/5823' }); - test('Can delete entries without tags', async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/5823' - }); + await createNotebookEntryAndTags(page); - await createNotebookEntryAndTags(page); + 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 = 1`; + await page.locator(entryLocator).click(); + await page.locator(entryLocator).fill(`An entry without tags`); + await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').press('Enter'); - 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 = 1`; - await page.locator(entryLocator).click(); - await page.locator(entryLocator).fill(`An entry without tags`); - await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').press('Enter'); + await page.hover('[aria-label="Notebook Entry Input"] >> nth=1'); + await page.locator('button[title="Delete this entry"]').last().click(); + await expect( + page.locator('text=This action will permanently delete this entry. Do you wish to continue?') + ).toBeVisible(); + await page.locator('button:has-text("Ok")').click(); + await expect( + page.locator('text=This action will permanently delete this entry. Do you wish to continue?') + ).toBeHidden(); + }); - await page.hover('[aria-label="Notebook Entry Input"] >> nth=1'); - await page.locator('button[title="Delete this entry"]').last().click(); - await expect(page.locator('text=This action will permanently delete this entry. Do you wish to continue?')).toBeVisible(); - await page.locator('button:has-text("Ok")').click(); - await expect(page.locator('text=This action will permanently delete this entry. Do you wish to continue?')).toBeHidden(); - }); + 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: 'domcontentloaded' }); - 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: 'domcontentloaded' }); + 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: 'domcontentloaded' }); - 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: 'domcontentloaded' }); + const ITERATIONS = 4; + const notebook = await createNotebookEntryAndTags(page, ITERATIONS); + await page.goto(notebook.url); - const ITERATIONS = 4; - const notebook = await createNotebookEntryAndTags(page, ITERATIONS); - await page.goto(notebook.url); + // Verify tags are present + 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'); + } - // Verify tags are present - 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 page.reload({ waitUntil: 'domcontentloaded' }); - //Reload Page - await page.reload({ waitUntil: 'domcontentloaded' }); + // Verify tags persist across reload + 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'); + } + }); + test('Can cancel adding a tag', async ({ page }) => { + await createNotebookAndEntry(page); - // Verify tags persist across reload - 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"); - } - }); - test('Can cancel adding a tag', async ({ page }) => { - await createNotebookAndEntry(page); + await selectInspectorTab(page, 'Annotations'); - await selectInspectorTab(page, 'Annotations'); + // Click on the "Add Tag" button + await page.locator('button:has-text("Add Tag")').click(); - // Click on the "Add Tag" button - await page.locator('button:has-text("Add Tag")').click(); + // Click inside the AutoComplete field + await page.locator('[placeholder="Type to select tag"]').click(); - // Click inside the AutoComplete field - await page.locator('[placeholder="Type to select tag"]').click(); + // Click on the "Tags" header (simulating a click outside the autocomplete) + await page.locator('div.c-inspect-properties__header:has-text("Tags")').click(); - // Click on the "Tags" header (simulating a click outside the autocomplete) - await page.locator('div.c-inspect-properties__header:has-text("Tags")').click(); + // Verify there is a button with text "Add Tag" + await expect(page.locator('button:has-text("Add Tag")')).toBeVisible(); - // Verify there is a button with text "Add Tag" - await expect(page.locator('button:has-text("Add Tag")')).toBeVisible(); - - // Verify the AutoComplete field is hidden - await expect(page.locator('[placeholder="Type to select tag"]')).toBeHidden(); - }); + // Verify the AutoComplete field is hidden + await expect(page.locator('[placeholder="Type to select tag"]')).toBeHidden(); + }); }); diff --git a/e2e/tests/functional/plugins/operatorStatus/operatorStatus.e2e.spec.js b/e2e/tests/functional/plugins/operatorStatus/operatorStatus.e2e.spec.js index 0673543591..b40dc2a67a 100644 --- a/e2e/tests/functional/plugins/operatorStatus/operatorStatus.e2e.spec.js +++ b/e2e/tests/functional/plugins/operatorStatus/operatorStatus.e2e.spec.js @@ -21,8 +21,8 @@ *****************************************************************************/ /* global __dirname */ /* -* This test suite is dedicated to testing the operator status plugin. -*/ + * This test suite is dedicated to testing the operator status plugin. + */ const path = require('path'); const { test, expect } = require('../../../../pluginFixtures'); @@ -38,117 +38,120 @@ STUB (test.fixme) Rolling through each */ test.describe('Operator Status', () => { - test.beforeEach(async ({ page }) => { - // FIXME: determine if plugins will be added to index.html or need to be injected - await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitExampleUser.js')}); - await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitOperatorStatus.js')}); - await page.goto('./', { waitUntil: 'domcontentloaded' }); + test.beforeEach(async ({ page }) => { + // FIXME: determine if plugins will be added to index.html or need to be injected + await page.addInitScript({ + path: path.join(__dirname, '../../../../helper/', 'addInitExampleUser.js') }); - - // verify that operator status is visible - test('operator status is visible and expands when clicked', async ({ page }) => { - await expect(page.locator('div[title="Set my operator status"]')).toBeVisible(); - await page.locator('div[title="Set my operator status"]').click(); - - // expect default status to be 'GO' - await expect(page.locator('.c-status-poll-panel')).toBeVisible(); + await page.addInitScript({ + path: path.join(__dirname, '../../../../helper/', 'addInitOperatorStatus.js') }); + await page.goto('./', { waitUntil: 'domcontentloaded' }); + }); - test('poll question indicator remains when blank poll set', async ({ page }) => { - await expect(page.locator('div[title="Set the current poll question"]')).toBeVisible(); - await page.locator('div[title="Set the current poll question"]').click(); - // set to blank - await page.getByRole('button', { name: 'Update' }).click(); + // verify that operator status is visible + test('operator status is visible and expands when clicked', async ({ page }) => { + await expect(page.locator('div[title="Set my operator status"]')).toBeVisible(); + await page.locator('div[title="Set my operator status"]').click(); - // should still be visible - await expect(page.locator('div[title="Set the current poll question"]')).toBeVisible(); - }); + // expect default status to be 'GO' + await expect(page.locator('.c-status-poll-panel')).toBeVisible(); + }); - // Verify that user 1 sees updates from user/role 2 (Not possible without openmct-yamcs implementation) - test('operator status table reflects answered values', async ({ page }) => { - // user navigates to operator status poll - const statusPollIndicator = page.locator('div[title="Set my operator status"]'); - await statusPollIndicator.click(); + test('poll question indicator remains when blank poll set', async ({ page }) => { + await expect(page.locator('div[title="Set the current poll question"]')).toBeVisible(); + await page.locator('div[title="Set the current poll question"]').click(); + // set to blank + await page.getByRole('button', { name: 'Update' }).click(); - // get user role value - const userRole = page.locator('.c-status-poll-panel__user-role'); - const userRoleText = await userRole.innerText(); + // should still be visible + await expect(page.locator('div[title="Set the current poll question"]')).toBeVisible(); + }); - // get selected status value - const selectStatus = page.locator('select[name="setStatus"]'); - await selectStatus.selectOption({ index: 1}); - const initialStatusValue = await selectStatus.inputValue(); + // Verify that user 1 sees updates from user/role 2 (Not possible without openmct-yamcs implementation) + test('operator status table reflects answered values', async ({ page }) => { + // user navigates to operator status poll + const statusPollIndicator = page.locator('div[title="Set my operator status"]'); + await statusPollIndicator.click(); - // open manage status poll - const manageStatusPollIndicator = page.locator('div[title="Set the current poll question"]'); - await manageStatusPollIndicator.click(); - // parse the table row values - const row = page.locator(`tr:has-text("${userRoleText}")`); - const rowValues = await row.innerText(); - const rowValuesArr = rowValues.split('\t'); - const COLUMN_STATUS_INDEX = 1; - // check initial set value matches status table - expect(rowValuesArr[COLUMN_STATUS_INDEX].toLowerCase()) - .toEqual(initialStatusValue.toLowerCase()); + // get user role value + const userRole = page.locator('.c-status-poll-panel__user-role'); + const userRoleText = await userRole.innerText(); - // change user status - await statusPollIndicator.click(); - // FIXME: might want to grab a dynamic option instead of arbitrary - await page.locator('select[name="setStatus"]').selectOption({ index: 2}); - const updatedStatusValue = await selectStatus.inputValue(); - // verify user status is reflected in table - await manageStatusPollIndicator.click(); + // get selected status value + const selectStatus = page.locator('select[name="setStatus"]'); + await selectStatus.selectOption({ index: 1 }); + const initialStatusValue = await selectStatus.inputValue(); - const updatedRow = page.locator(`tr:has-text("${userRoleText}")`); - const updatedRowValues = await updatedRow.innerText(); - const updatedRowValuesArr = updatedRowValues.split('\t'); + // open manage status poll + const manageStatusPollIndicator = page.locator('div[title="Set the current poll question"]'); + await manageStatusPollIndicator.click(); + // parse the table row values + const row = page.locator(`tr:has-text("${userRoleText}")`); + const rowValues = await row.innerText(); + const rowValuesArr = rowValues.split('\t'); + const COLUMN_STATUS_INDEX = 1; + // check initial set value matches status table + expect(rowValuesArr[COLUMN_STATUS_INDEX].toLowerCase()).toEqual( + initialStatusValue.toLowerCase() + ); - expect(updatedRowValuesArr[COLUMN_STATUS_INDEX].toLowerCase()) - .toEqual(updatedStatusValue.toLowerCase()); + // change user status + await statusPollIndicator.click(); + // FIXME: might want to grab a dynamic option instead of arbitrary + await page.locator('select[name="setStatus"]').selectOption({ index: 2 }); + const updatedStatusValue = await selectStatus.inputValue(); + // verify user status is reflected in table + await manageStatusPollIndicator.click(); - }); + const updatedRow = page.locator(`tr:has-text("${userRoleText}")`); + const updatedRowValues = await updatedRow.innerText(); + const updatedRowValuesArr = updatedRowValues.split('\t'); - test('clear poll button removes poll responses', async ({ page }) => { - // user navigates to operator status poll - const statusPollIndicator = page.locator('div[title="Set my operator status"]'); - await statusPollIndicator.click(); + expect(updatedRowValuesArr[COLUMN_STATUS_INDEX].toLowerCase()).toEqual( + updatedStatusValue.toLowerCase() + ); + }); - // get user role value - const userRole = page.locator('.c-status-poll-panel__user-role'); - const userRoleText = await userRole.innerText(); + test('clear poll button removes poll responses', async ({ page }) => { + // user navigates to operator status poll + const statusPollIndicator = page.locator('div[title="Set my operator status"]'); + await statusPollIndicator.click(); - // get selected status value - const selectStatus = page.locator('select[name="setStatus"]'); - // FIXME: might want to grab a dynamic option instead of arbitrary - await selectStatus.selectOption({ index: 1}); - const initialStatusValue = await selectStatus.inputValue(); + // get user role value + const userRole = page.locator('.c-status-poll-panel__user-role'); + const userRoleText = await userRole.innerText(); - // open manage status poll - const manageStatusPollIndicator = page.locator('div[title="Set the current poll question"]'); - await manageStatusPollIndicator.click(); - // parse the table row values - const row = page.locator(`tr:has-text("${userRoleText}")`); - const rowValues = await row.innerText(); - const rowValuesArr = rowValues.split('\t'); - const COLUMN_STATUS_INDEX = 1; - // check initial set value matches status table - expect(rowValuesArr[COLUMN_STATUS_INDEX].toLowerCase()) - .toEqual(initialStatusValue.toLowerCase()); + // get selected status value + const selectStatus = page.locator('select[name="setStatus"]'); + // FIXME: might want to grab a dynamic option instead of arbitrary + await selectStatus.selectOption({ index: 1 }); + const initialStatusValue = await selectStatus.inputValue(); - // clear the poll - await page.locator('button[title="Clear the previous poll question"]').click(); + // open manage status poll + const manageStatusPollIndicator = page.locator('div[title="Set the current poll question"]'); + await manageStatusPollIndicator.click(); + // parse the table row values + const row = page.locator(`tr:has-text("${userRoleText}")`); + const rowValues = await row.innerText(); + const rowValuesArr = rowValues.split('\t'); + const COLUMN_STATUS_INDEX = 1; + // check initial set value matches status table + expect(rowValuesArr[COLUMN_STATUS_INDEX].toLowerCase()).toEqual( + initialStatusValue.toLowerCase() + ); - const updatedRow = page.locator(`tr:has-text("${userRoleText}")`); - const updatedRowValues = await updatedRow.innerText(); - const updatedRowValuesArr = updatedRowValues.split('\t'); - const UNSET_VALUE_LABEL = 'Not set'; - expect(updatedRowValuesArr[COLUMN_STATUS_INDEX]) - .toEqual(UNSET_VALUE_LABEL); + // clear the poll + await page.locator('button[title="Clear the previous poll question"]').click(); - }); - - test.fixme('iterate through all possible response values', async ({ page }) => { - // test all possible respone values for the poll - }); + const updatedRow = page.locator(`tr:has-text("${userRoleText}")`); + const updatedRowValues = await updatedRow.innerText(); + const updatedRowValuesArr = updatedRowValues.split('\t'); + const UNSET_VALUE_LABEL = 'Not set'; + expect(updatedRowValuesArr[COLUMN_STATUS_INDEX]).toEqual(UNSET_VALUE_LABEL); + }); + test.fixme('iterate through all possible response values', async ({ page }) => { + // test all possible respone values for the poll + }); }); diff --git a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js index 669583d98f..821015e283 100644 --- a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js @@ -27,81 +27,95 @@ Testsuite for plot autoscale. const { selectInspectorTab } = require('../../../../appActions'); const { test, expect } = require('../../../../pluginFixtures'); test.use({ - viewport: { - width: 1280, - height: 720 - } + viewport: { + width: 1280, + height: 720 + } }); test.describe('Autoscale', () => { - test('User can set autoscale with a valid range @snapshot', async ({ page, openmctConfig }) => { - const { myItemsFolderName } = openmctConfig; + test('User can set autoscale with a valid range @snapshot', async ({ page, openmctConfig }) => { + const { myItemsFolderName } = openmctConfig; - //This is necessary due to the size of the test suite. - test.slow(); + //This is necessary due to the size of the test suite. + test.slow(); - await page.goto('./', { waitUntil: 'domcontentloaded' }); + await page.goto('./', { waitUntil: 'domcontentloaded' }); - await setTimeRange(page); + await setTimeRange(page); - await createSinewaveOverlayPlot(page, myItemsFolderName); + await createSinewaveOverlayPlot(page, myItemsFolderName); - await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']); + await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']); - // enter edit mode - await page.click('button[title="Edit"]'); + // enter edit mode + await page.click('button[title="Edit"]'); - await selectInspectorTab(page, 'Config'); - await turnOffAutoscale(page); + await selectInspectorTab(page, 'Config'); + await turnOffAutoscale(page); - await setUserDefinedMinAndMax(page, '-2', '2'); + await setUserDefinedMinAndMax(page, '-2', '2'); - // save - await page.click('button[title="Save"]'); - await Promise.all([ - page.locator('li[title = "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'}); + // save + await page.click('button[title="Save"]'); + await Promise.all([ + page.locator('li[title = "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' }); - // Make sure that after turning off autoscale, the user entered range values are reflexted in the ticks. - await testYTicks(page, ['-2.00', '-1.50', '-1.00', '-0.50', '0.00', '0.50', '1.00', '1.50', '2.00']); + // Make sure that after turning off autoscale, the user entered range values are reflexted in the ticks. + await testYTicks(page, [ + '-2.00', + '-1.50', + '-1.00', + '-0.50', + '0.00', + '0.50', + '1.00', + '1.50', + '2.00' + ]); - const canvas = page.locator('canvas').nth(1); + const canvas = page.locator('canvas').nth(1); - await canvas.hover({trial: true}); - await expect(page.locator('.js-series-data-loaded')).toBeVisible(); + await canvas.hover({ trial: true }); + await expect(page.locator('.js-series-data-loaded')).toBeVisible(); - expect.soft(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-prepan.png', { animations: 'disabled' }); + expect + .soft(await canvas.screenshot()) + .toMatchSnapshot('autoscale-canvas-prepan.png', { animations: 'disabled' }); - //Alt Drag Start - await page.keyboard.down('Alt'); + //Alt Drag Start + await page.keyboard.down('Alt'); - await canvas.dragTo(canvas, { - sourcePosition: { - x: 200, - y: 200 - }, - targetPosition: { - x: 400, - y: 400 - } - }); - - //Alt Drag End - await page.keyboard.up('Alt'); - - // Ensure the drag worked. - await testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00', '2.50', '3.00', '3.50']); - - //Wait for canvas to stablize. - await canvas.hover({trial: true}); - - expect.soft(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-panned.png', { animations: 'disabled' }); + await canvas.dragTo(canvas, { + sourcePosition: { + x: 200, + y: 200 + }, + targetPosition: { + x: 400, + y: 400 + } }); + + //Alt Drag End + await page.keyboard.up('Alt'); + + // Ensure the drag worked. + await testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00', '2.50', '3.00', '3.50']); + + //Wait for canvas to stablize. + await canvas.hover({ trial: true }); + + expect + .soft(await canvas.screenshot()) + .toMatchSnapshot('autoscale-canvas-panned.png', { animations: 'disabled' }); + }); }); /** @@ -109,16 +123,20 @@ test.describe('Autoscale', () => { * @param {string} start * @param {string} end */ -async function setTimeRange(page, start = '2022-03-29 22:00:00.000Z', end = '2022-03-29 22:00:30.000Z') { - // Set a specific time range for consistency, otherwise it will change - // on every test to a range based on the current time. +async function setTimeRange( + page, + start = '2022-03-29 22:00:00.000Z', + end = '2022-03-29 22:00:30.000Z' +) { + // Set a specific time range for consistency, otherwise it will change + // on every test to a range based on the current time. - const timeInputs = page.locator('input.c-input--datetime'); - await timeInputs.first().click(); - await timeInputs.first().fill(start); + const timeInputs = page.locator('input.c-input--datetime'); + await timeInputs.first().click(); + await timeInputs.first().fill(start); - await timeInputs.nth(1).click(); - await timeInputs.nth(1).fill(end); + await timeInputs.nth(1).click(); + await timeInputs.nth(1).fill(end); } /** @@ -126,54 +144,57 @@ async function setTimeRange(page, start = '2022-03-29 22:00:00.000Z', end = '202 * @param {string} myItemsFolderName */ async function createSinewaveOverlayPlot(page, myItemsFolderName) { - // click create button - await page.locator('button:has-text("Create")').click(); + // click create button + await page.locator('button:has-text("Create")').click(); - // add overlay plot with defaults - await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click(); - await Promise.all([ - page.waitForNavigation(), - page.locator('button:has-text("OK")').click(), - //Wait for Save Banner to appear1 - page.waitForSelector('.c-message-banner__message') - ]); - //Wait until Save Banner is gone - await page.locator('.c-message-banner__close-button').click(); - await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); + // add overlay plot with defaults + await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click(); + await Promise.all([ + page.waitForNavigation(), + page.locator('button:has-text("OK")').click(), + //Wait for Save Banner to appear1 + page.waitForSelector('.c-message-banner__message') + ]); + //Wait until Save Banner is gone + await page.locator('.c-message-banner__close-button').click(); + await page.waitForSelector('.c-message-banner__message', { state: 'detached' }); - // save (exit edit mode) - await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); - await page.locator('text=Save and Finish Editing').click(); + // save (exit edit mode) + await page + .locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button') + .nth(1) + .click(); + await page.locator('text=Save and Finish Editing').click(); - // click create button - await page.locator('button:has-text("Create")').click(); + // click create button + await page.locator('button:has-text("Create")').click(); - // add sine wave generator with defaults - await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click(); - await Promise.all([ - page.waitForNavigation(), - page.locator('button:has-text("OK")').click(), - //Wait for Save Banner to appear1 - page.waitForSelector('.c-message-banner__message') - ]); - //Wait until Save Banner is gone - await page.locator('.c-message-banner__close-button').click(); - await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); + // add sine wave generator with defaults + await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click(); + await Promise.all([ + page.waitForNavigation(), + page.locator('button:has-text("OK")').click(), + //Wait for Save Banner to appear1 + page.waitForSelector('.c-message-banner__message') + ]); + //Wait until Save Banner is gone + await page.locator('.c-message-banner__close-button').click(); + await page.waitForSelector('.c-message-banner__message', { state: 'detached' }); - // focus the overlay plot - await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click(); - await Promise.all([ - page.waitForNavigation(), - page.locator('text=Unnamed Overlay Plot').first().click() - ]); + // focus the overlay plot + await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click(); + await Promise.all([ + page.waitForNavigation(), + page.locator('text=Unnamed Overlay Plot').first().click() + ]); } /** * @param {import('@playwright/test').Page} page */ async function turnOffAutoscale(page) { - // uncheck autoscale - await page.getByRole('checkbox', { name: 'Auto scale' }).uncheck(); + // uncheck autoscale + await page.getByRole('checkbox', { name: 'Auto scale' }).uncheck(); } /** @@ -182,23 +203,23 @@ async function turnOffAutoscale(page) { * @param {string} max */ async function setUserDefinedMinAndMax(page, min, max) { - // set minimum value - await page.getByRole('spinbutton').first().fill(min); - // set maximum value - await page.getByRole('spinbutton').nth(1).fill(max); + // set minimum value + await page.getByRole('spinbutton').first().fill(min); + // set maximum value + await page.getByRole('spinbutton').nth(1).fill(max); } /** * @param {import('@playwright/test').Page} page */ async function testYTicks(page, values) { - const yTicks = page.locator('.gl-plot-y-tick-label'); - await page.locator('canvas >> nth=1').hover(); - let promises = [yTicks.count().then(c => expect(c).toBe(values.length))]; + const yTicks = page.locator('.gl-plot-y-tick-label'); + await page.locator('canvas >> nth=1').hover(); + let promises = [yTicks.count().then((c) => expect(c).toBe(values.length))]; - for (let i = 0, l = values.length; i < l; i += 1) { - promises.push(expect.soft(yTicks.nth(i)).toHaveText(values[i])); // eslint-disable-line - } + for (let i = 0, l = values.length; i < l; i += 1) { + promises.push(expect.soft(yTicks.nth(i)).toHaveText(values[i])); // eslint-disable-line + } - await Promise.all(promises); + await Promise.all(promises); } diff --git a/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js index 0fd82261da..ab498741b3 100644 --- a/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js @@ -29,44 +29,50 @@ const { test, expect } = require('../../../../pluginFixtures'); const { selectInspectorTab } = require('../../../../appActions'); test.describe('Log plot tests', () => { - test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ page, openmctConfig }) => { - const { myItemsFolderName } = openmctConfig; + 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(); + //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 selectInspectorTab(page, 'Config'); - await enableLogMode(page); - await testLogTicks(page); - await disableLogMode(page); - await testRegularTicks(page); - await enableLogMode(page); - await testLogTicks(page); - await saveOverlayPlot(page); - await testLogTicks(page); - }); + await makeOverlayPlot(page, myItemsFolderName); + await testRegularTicks(page); + await enableEditMode(page); + await selectInspectorTab(page, 'Config'); + await enableLogMode(page); + await testLogTicks(page); + await disableLogMode(page); + await testRegularTicks(page); + await enableLogMode(page); + await testLogTicks(page); + await saveOverlayPlot(page); + await testLogTicks(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; + // 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); + await makeOverlayPlot(page, myItemsFolderName); + await enableEditMode(page); + await enableLogMode(page); + await saveOverlayPlot(page); - // TODO ...export, delete the overlay, then import it... + // TODO ...export, delete the overlay, then import it... - //await testLogTicks(page); + //await testLogTicks(page); - // TODO, the plot is slightly at different position that in the other test, so this fails. - // ...We can fix it by copying all steps from the first test... - // await testLogPlotPixels(page); - }); + // TODO, the plot is slightly at different position that in the other test, so this fails. + // ...We can fix it by copying all steps from the first test... + // await testLogPlotPixels(page); + } + ); }); /** @@ -75,146 +81,149 @@ test.describe('Log plot tests', () => { * @param {string} myItemsFolderName */ 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: 'domcontentloaded' }); + // 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: 'domcontentloaded' }); - // Set a specific time range for consistency, otherwise it will change - // on every test to a range based on the current time. + // Set a specific time range for consistency, otherwise it will change + // on every test to a range based on the current time. - const timeInputs = page.locator('input.c-input--datetime'); - await timeInputs.first().click(); - await timeInputs.first().fill('2022-03-29 22:00:00.000Z'); + const timeInputs = page.locator('input.c-input--datetime'); + await timeInputs.first().click(); + await timeInputs.first().fill('2022-03-29 22:00:00.000Z'); - await timeInputs.nth(1).click(); - await timeInputs.nth(1).fill('2022-03-29 22:00:30.000Z'); + await timeInputs.nth(1).click(); + await timeInputs.nth(1).fill('2022-03-29 22:00:30.000Z'); - // create overlay plot + // create overlay plot - await page.locator('button.c-create-button').click(); - await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click(); - await Promise.all([ - page.waitForNavigation({ waitUntil: 'networkidle'}), - page.locator('button:has-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'}); + await page.locator('button.c-create-button').click(); + await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click(); + await Promise.all([ + page.waitForNavigation({ waitUntil: 'networkidle' }), + page.locator('button:has-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 + // save the overlay plot - await saveOverlayPlot(page); + await saveOverlayPlot(page); - // create a sinewave generator + // create a sinewave generator - await page.locator('button.c-create-button').click(); - await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click(); + await page.locator('button.c-create-button').click(); + await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click(); - // set amplitude to 6, offset 4, period 2 + // set amplitude to 6, offset 4, period 2 - 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(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) .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(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) .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'); + 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 + // Click OK to make generator - await Promise.all([ - page.waitForNavigation({ waitUntil: 'networkidle'}), - page.locator('button:has-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'}); + await Promise.all([ + page.waitForNavigation({ waitUntil: 'networkidle' }), + page.locator('button:has-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 + // click on overlay plot - await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click(); - await Promise.all([ - page.waitForNavigation(), - page.locator('text=Unnamed Overlay Plot').first().click() - ]); + await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click(); + await Promise.all([ + page.waitForNavigation(), + page.locator('text=Unnamed Overlay Plot').first().click() + ]); } /** * @param {import('@playwright/test').Page} page */ async function testRegularTicks(page) { - const yTicks = page.locator('.gl-plot-y-tick-label'); - expect(await yTicks.count()).toBe(7); - await expect(yTicks.nth(0)).toHaveText('-2'); - await expect(yTicks.nth(1)).toHaveText('0'); - await expect(yTicks.nth(2)).toHaveText('2'); - await expect(yTicks.nth(3)).toHaveText('4'); - await expect(yTicks.nth(4)).toHaveText('6'); - await expect(yTicks.nth(5)).toHaveText('8'); - await expect(yTicks.nth(6)).toHaveText('10'); + const yTicks = page.locator('.gl-plot-y-tick-label'); + expect(await yTicks.count()).toBe(7); + await expect(yTicks.nth(0)).toHaveText('-2'); + await expect(yTicks.nth(1)).toHaveText('0'); + await expect(yTicks.nth(2)).toHaveText('2'); + await expect(yTicks.nth(3)).toHaveText('4'); + await expect(yTicks.nth(4)).toHaveText('6'); + await expect(yTicks.nth(5)).toHaveText('8'); + await expect(yTicks.nth(6)).toHaveText('10'); } /** * @param {import('@playwright/test').Page} page */ async function testLogTicks(page) { - const yTicks = page.locator('.gl-plot-y-tick-label'); - expect(await yTicks.count()).toBe(9); - await expect(yTicks.nth(0)).toHaveText('-2.98'); - await expect(yTicks.nth(1)).toHaveText('-1.51'); - await expect(yTicks.nth(2)).toHaveText('-0.58'); - await expect(yTicks.nth(3)).toHaveText('-0.00'); - await expect(yTicks.nth(4)).toHaveText('0.58'); - await expect(yTicks.nth(5)).toHaveText('1.51'); - await expect(yTicks.nth(6)).toHaveText('2.98'); - await expect(yTicks.nth(7)).toHaveText('5.31'); - await expect(yTicks.nth(8)).toHaveText('9.00'); + const yTicks = page.locator('.gl-plot-y-tick-label'); + expect(await yTicks.count()).toBe(9); + await expect(yTicks.nth(0)).toHaveText('-2.98'); + await expect(yTicks.nth(1)).toHaveText('-1.51'); + await expect(yTicks.nth(2)).toHaveText('-0.58'); + await expect(yTicks.nth(3)).toHaveText('-0.00'); + await expect(yTicks.nth(4)).toHaveText('0.58'); + await expect(yTicks.nth(5)).toHaveText('1.51'); + await expect(yTicks.nth(6)).toHaveText('2.98'); + await expect(yTicks.nth(7)).toHaveText('5.31'); + await expect(yTicks.nth(8)).toHaveText('9.00'); } /** * @param {import('@playwright/test').Page} page */ async function enableEditMode(page) { - // turn on edit mode - await page.getByRole('button', { name: 'Edit' }).click(); - await expect(page.getByRole('button', { name: 'Save' })).toBeVisible(); + // turn on edit mode + await page.getByRole('button', { name: 'Edit' }).click(); + await expect(page.getByRole('button', { name: 'Save' })).toBeVisible(); } /** * @param {import('@playwright/test').Page} page */ async function enableLogMode(page) { - await expect(page.getByRole('checkbox', { name: 'Log mode' })).not.toBeChecked(); - await page.getByRole('checkbox', { name: 'Log mode' }).check(); + await expect(page.getByRole('checkbox', { name: 'Log mode' })).not.toBeChecked(); + await page.getByRole('checkbox', { name: 'Log mode' }).check(); } /** * @param {import('@playwright/test').Page} page */ async function disableLogMode(page) { - await expect(page.getByRole('checkbox', { name: 'Log mode' })).toBeChecked(); - await page.getByRole('checkbox', { name: 'Log mode' }).uncheck(); + await expect(page.getByRole('checkbox', { name: 'Log mode' })).toBeChecked(); + await page.getByRole('checkbox', { name: 'Log mode' }).uncheck(); } /** * @param {import('@playwright/test').Page} page */ async function saveOverlayPlot(page) { - // save overlay plot - await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); + // 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' }); + 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' }); } /** @@ -223,63 +232,63 @@ async function saveOverlayPlot(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. + 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. - await new Promise((r) => setTimeout(r, 5 * 1000)); + await new Promise((r) => setTimeout(r, 5 * 1000)); - // These are some pixels that should be blue points in the log plot. - // If the plot changes shape to an unexpected shape, this will - // likely fail, which is what we want. - // - // I found these pixels by pausing playwright in debug mode at this - // point, and using similar code as below to output the pixel data, then - // I logged those pixels here. - const expectedBluePixels = [ - // TODO these pixel sets only work with the first test, but not the second test. + // These are some pixels that should be blue points in the log plot. + // If the plot changes shape to an unexpected shape, this will + // likely fail, which is what we want. + // + // I found these pixels by pausing playwright in debug mode at this + // point, and using similar code as below to output the pixel data, then + // I logged those pixels here. + const expectedBluePixels = [ + // TODO these pixel sets only work with the first test, but not the second test. - // [60, 35], - // [121, 125], - // [156, 377], - // [264, 73], - // [372, 186], - // [576, 73], - // [659, 439], - // [675, 423] + // [60, 35], + // [121, 125], + // [156, 377], + // [264, 73], + // [372, 186], + // [576, 73], + // [659, 439], + // [675, 423] - [60, 35], - [120, 125], - [156, 375], - [264, 73], - [372, 185], - [575, 72], - [659, 437], - [675, 421] - ]; + [60, 35], + [120, 125], + [156, 375], + [264, 73], + [372, 185], + [575, 72], + [659, 437], + [675, 421] + ]; - // The first canvas in the DOM is the one that has the plot point - // icons (canvas 2d), which is the one we are testing. The second - // one in the DOM is the WebGL canvas with the line. (Why aren't - // they both WebGL?) - const canvas = document.querySelector('canvas'); + // The first canvas in the DOM is the one that has the plot point + // icons (canvas 2d), which is the one we are testing. The second + // one in the DOM is the WebGL canvas with the line. (Why aren't + // they both WebGL?) + const canvas = document.querySelector('canvas'); - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext('2d'); - for (const pixel of expectedBluePixels) { - // XXX Possible optimization: call getImageData only once with - // area including all pixels to be tested. - const data = ctx.getImageData(pixel[0], pixel[1], 1, 1).data; + for (const pixel of expectedBluePixels) { + // XXX Possible optimization: call getImageData only once with + // area including all pixels to be tested. + const data = ctx.getImageData(pixel[0], pixel[1], 1, 1).data; - // #43b0ffff <-- openmct cyanish-blue with 100% opacity - // if (data[0] !== 0x43 || data[1] !== 0xb0 || data[2] !== 0xff || data[3] !== 0xff) { - if (data[0] === 0 && data[1] === 0 && data[2] === 0 && data[3] === 0) { - // If any pixel is empty, it means we didn't hit a plot point. - return false; - } - } + // #43b0ffff <-- openmct cyanish-blue with 100% opacity + // if (data[0] !== 0x43 || data[1] !== 0xb0 || data[2] !== 0xff || data[3] !== 0xff) { + if (data[0] === 0 && data[1] === 0 && data[2] === 0 && data[3] === 0) { + // If any pixel is empty, it means we didn't hit a plot point. + return false; + } + } - return true; - }); + return true; + }); - expect(pixelsMatch).toBe(true); + expect(pixelsMatch).toBe(true); } diff --git a/e2e/tests/functional/plugins/plot/missingPlotObj.e2e.spec.js b/e2e/tests/functional/plugins/plot/missingPlotObj.e2e.spec.js index 789f8455f9..4e6e11eaae 100644 --- a/e2e/tests/functional/plugins/plot/missingPlotObj.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/missingPlotObj.e2e.spec.js @@ -27,55 +27,56 @@ 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'); + 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 = []; + 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); + 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); + }); }); /** @@ -83,42 +84,42 @@ test.describe('Handle missing object for plots', () => { * @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: 'domcontentloaded' }); + // 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: 'domcontentloaded' }); - // create stacked plot - await page.locator('button.c-create-button').click(); - await page.locator('li[role="menuitem"]:has-text("Stacked Plot")').click(); + // create stacked plot + await page.locator('button.c-create-button').click(); + await page.locator('li[role="menuitem"]:has-text("Stacked Plot")').click(); - await Promise.all([ - page.waitForNavigation({ waitUntil: 'networkidle'}), - page.locator('button:has-text("OK")').click(), - //Wait for Save Banner to appear - page.waitForSelector('.c-message-banner__message') - ]); + await Promise.all([ + page.waitForNavigation({ waitUntil: 'networkidle' }), + page.locator('button:has-text("OK")').click(), + //Wait for Save Banner to appear + page.waitForSelector('.c-message-banner__message') + ]); - // save the stacked plot - await saveStackedPlot(page); + // save the stacked plot + await saveStackedPlot(page); - // create a sinewave generator - await createSineWaveGenerator(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() - ]); + // 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); + // 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() - ]); + // 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() + ]); } /** @@ -126,17 +127,20 @@ async function makeStackedPlot(page, myItemsFolderName) { * @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(); + // 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' }); + 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' }); } /** @@ -144,14 +148,14 @@ async function saveStackedPlot(page) { * @private */ async function createSineWaveGenerator(page) { - //Create sine wave generator - await page.locator('button.c-create-button').click(); - await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click(); + //Create sine wave generator + await page.locator('button.c-create-button').click(); + await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click(); - await Promise.all([ - page.waitForNavigation({ waitUntil: 'networkidle'}), - page.locator('button:has-text("OK")').click(), - //Wait for Save Banner to appear - page.waitForSelector('.c-message-banner__message') - ]); + await Promise.all([ + page.waitForNavigation({ waitUntil: 'networkidle' }), + page.locator('button:has-text("OK")').click(), + //Wait for Save Banner to appear + page.waitForSelector('.c-message-banner__message') + ]); } diff --git a/e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js index 1aa1b848f8..8a1385b155 100644 --- a/e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js @@ -26,201 +26,230 @@ necessarily be used for reference when writing new tests in this area. */ const { test, expect } = require('../../../../pluginFixtures'); -const { createDomainObjectWithDefaults, getCanvasPixels, selectInspectorTab, waitForPlotsToRender } = require('../../../../appActions'); +const { + createDomainObjectWithDefaults, + getCanvasPixels, + selectInspectorTab, + waitForPlotsToRender +} = require('../../../../appActions'); test.describe('Overlay Plot', () => { - test.beforeEach(async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + }); + + test('Plot legend color is in sync with plot series color', async ({ page }) => { + const overlayPlot = await createDomainObjectWithDefaults(page, { + type: 'Overlay Plot' }); - test('Plot legend color is in sync with plot series color', async ({ page }) => { - const overlayPlot = await createDomainObjectWithDefaults(page, { - type: "Overlay Plot" - }); - - await createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator", - parent: overlayPlot.uuid - }); - - await page.goto(overlayPlot.url); - - await selectInspectorTab(page, 'Config'); - - // 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 seriesColorSwatch = page.locator('.gl-plot-y-label-swatch-container > .plot-series-color-swatch'); - await expect(seriesColorSwatch).toHaveCSS('background-color', 'rgb(255, 166, 61)'); + await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + parent: overlayPlot.uuid }); - test('Limit lines persist when series is moved to another Y Axis and on refresh', async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/6338' - }); - // Create an Overlay Plot with a default SWG - const overlayPlot = await createDomainObjectWithDefaults(page, { - type: "Overlay Plot" - }); + await page.goto(overlayPlot.url); - const swgA = await createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator", - parent: overlayPlot.uuid - }); + await selectInspectorTab(page, 'Config'); - await page.goto(overlayPlot.url); - - // Assert that no limit lines are shown by default - await page.waitForSelector('.js-limit-area', { state: 'attached' }); - expect(await page.locator('.c-plot-limit-line').count()).toBe(0); - - // Enter edit mode - await page.click('button[title="Edit"]'); - - // Expand the "Sine Wave Generator" plot series options and enable limit lines - await selectInspectorTab(page, 'Config'); - await page.getByRole('list', { name: 'Plot Series Properties' }).locator('span').first().click(); - await page.getByRole('list', { name: 'Plot Series Properties' }).locator('[title="Display limit lines"]~div input').check(); - - await assertLimitLinesExistAndAreVisible(page); - - // Save (exit edit mode) - await page.locator('button[title="Save"]').click(); - await page.locator('li[title="Save and Finish Editing"]').click(); - - await assertLimitLinesExistAndAreVisible(page); - - await page.reload(); - - await assertLimitLinesExistAndAreVisible(page); - - // Enter edit mode - await page.click('button[title="Edit"]'); - - await selectInspectorTab(page, 'Elements'); - - // Drag Sine Wave Generator series from Y Axis 1 into Y Axis 2 - await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); - - await assertLimitLinesExistAndAreVisible(page); - - // Save (exit edit mode) - await page.locator('button[title="Save"]').click(); - await page.locator('li[title="Save and Finish Editing"]').click(); - - await assertLimitLinesExistAndAreVisible(page); - - await page.reload(); - - await assertLimitLinesExistAndAreVisible(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 seriesColorSwatch = page.locator( + '.gl-plot-y-label-swatch-container > .plot-series-color-swatch' + ); + await expect(seriesColorSwatch).toHaveCSS('background-color', 'rgb(255, 166, 61)'); + }); + test('Limit lines persist when series is moved to another Y Axis and on refresh', async ({ + page + }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/6338' + }); + // Create an Overlay Plot with a default SWG + const overlayPlot = await createDomainObjectWithDefaults(page, { + type: 'Overlay Plot' }); - test('The elements pool supports dragging series into multiple y-axis buckets', async ({ page }) => { - const overlayPlot = await createDomainObjectWithDefaults(page, { - type: "Overlay Plot" - }); - - const swgA = await createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator", - parent: overlayPlot.uuid - }); - const swgB = await createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator", - parent: overlayPlot.uuid - }); - const swgC = await createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator", - parent: overlayPlot.uuid - }); - const swgD = await createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator", - parent: overlayPlot.uuid - }); - const swgE = await createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator", - parent: overlayPlot.uuid - }); - - await page.goto(overlayPlot.url); - await page.click('button[title="Edit"]'); - - await selectInspectorTab(page, 'Elements'); - - // Drag swg a, c, e into Y Axis 2 - await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); - await page.locator(`#inspector-elements-tree >> text=${swgC.name}`).dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); - await page.locator(`#inspector-elements-tree >> text=${swgE.name}`).dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); - - // Assert that Y Axis 1 and Y Axis 2 property groups are visible only - await selectInspectorTab(page, 'Config'); - - const yAxis1PropertyGroup = page.locator('[aria-label="Y Axis Properties"]'); - const yAxis2PropertyGroup = page.locator('[aria-label="Y Axis 2 Properties"]'); - const yAxis3PropertyGroup = page.locator('[aria-label="Y Axis 3 Properties"]'); - - await expect(yAxis1PropertyGroup).toBeVisible(); - await expect(yAxis2PropertyGroup).toBeVisible(); - await expect(yAxis3PropertyGroup).toBeHidden(); - - const yAxis1Group = page.getByLabel("Y Axis 1"); - const yAxis2Group = page.getByLabel("Y Axis 2"); - const yAxis3Group = page.getByLabel("Y Axis 3"); - - await selectInspectorTab(page, 'Elements'); - - // Drag swg b into Y Axis 3 - await page.locator(`#inspector-elements-tree >> text=${swgB.name}`).dragTo(page.locator('[aria-label="Element Item Group Y Axis 3"]')); - - // Assert that all Y Axis property groups are visible - await selectInspectorTab(page, 'Config'); - - await expect(yAxis1PropertyGroup).toBeVisible(); - await expect(yAxis2PropertyGroup).toBeVisible(); - await expect(yAxis3PropertyGroup).toBeVisible(); - - // Verify that the elements are in the correct buckets and in the correct order - await selectInspectorTab(page, 'Elements'); - - expect(yAxis1Group.getByRole('listitem', { name: swgD.name })).toBeTruthy(); - expect(yAxis1Group.getByRole('listitem').nth(0).getByText(swgD.name)).toBeTruthy(); - expect(yAxis2Group.getByRole('listitem', { name: swgE.name })).toBeTruthy(); - expect(yAxis2Group.getByRole('listitem').nth(0).getByText(swgE.name)).toBeTruthy(); - expect(yAxis2Group.getByRole('listitem', { name: swgC.name })).toBeTruthy(); - expect(yAxis2Group.getByRole('listitem').nth(1).getByText(swgC.name)).toBeTruthy(); - expect(yAxis2Group.getByRole('listitem', { name: swgA.name })).toBeTruthy(); - expect(yAxis2Group.getByRole('listitem').nth(2).getByText(swgA.name)).toBeTruthy(); - expect(yAxis3Group.getByRole('listitem', { name: swgB.name })).toBeTruthy(); - expect(yAxis3Group.getByRole('listitem').nth(0).getByText(swgB.name)).toBeTruthy(); + const swgA = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + parent: overlayPlot.uuid }); - test('Clicking on an item in the elements pool brings up the plot preview with data points', async ({ page }) => { - const overlayPlot = await createDomainObjectWithDefaults(page, { - type: "Overlay Plot" - }); + await page.goto(overlayPlot.url); - const swgA = await createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator", - parent: overlayPlot.uuid - }); + // Assert that no limit lines are shown by default + await page.waitForSelector('.js-limit-area', { state: 'attached' }); + expect(await page.locator('.c-plot-limit-line').count()).toBe(0); - await page.goto(overlayPlot.url); - // Wait for plot series data to load and be drawn - await waitForPlotsToRender(page); - await page.click('button[title="Edit"]'); + // Enter edit mode + await page.click('button[title="Edit"]'); - await selectInspectorTab(page, 'Elements'); + // Expand the "Sine Wave Generator" plot series options and enable limit lines + await selectInspectorTab(page, 'Config'); + await page + .getByRole('list', { name: 'Plot Series Properties' }) + .locator('span') + .first() + .click(); + await page + .getByRole('list', { name: 'Plot Series Properties' }) + .locator('[title="Display limit lines"]~div input') + .check(); - await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click(); + await assertLimitLinesExistAndAreVisible(page); - const plotPixels = await getCanvasPixels(page, '.js-overlay canvas'); - const plotPixelSize = plotPixels.length; - expect(plotPixelSize).toBeGreaterThan(0); + // Save (exit edit mode) + await page.locator('button[title="Save"]').click(); + await page.locator('li[title="Save and Finish Editing"]').click(); + + await assertLimitLinesExistAndAreVisible(page); + + await page.reload(); + + await assertLimitLinesExistAndAreVisible(page); + + // Enter edit mode + await page.click('button[title="Edit"]'); + + await selectInspectorTab(page, 'Elements'); + + // Drag Sine Wave Generator series from Y Axis 1 into Y Axis 2 + await page + .locator(`#inspector-elements-tree >> text=${swgA.name}`) + .dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); + + await assertLimitLinesExistAndAreVisible(page); + + // Save (exit edit mode) + await page.locator('button[title="Save"]').click(); + await page.locator('li[title="Save and Finish Editing"]').click(); + + await assertLimitLinesExistAndAreVisible(page); + + await page.reload(); + + await assertLimitLinesExistAndAreVisible(page); + }); + + test('The elements pool supports dragging series into multiple y-axis buckets', async ({ + page + }) => { + const overlayPlot = await createDomainObjectWithDefaults(page, { + type: 'Overlay Plot' }); + + const swgA = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + parent: overlayPlot.uuid + }); + const swgB = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + parent: overlayPlot.uuid + }); + const swgC = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + parent: overlayPlot.uuid + }); + const swgD = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + parent: overlayPlot.uuid + }); + const swgE = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + parent: overlayPlot.uuid + }); + + await page.goto(overlayPlot.url); + await page.click('button[title="Edit"]'); + + await selectInspectorTab(page, 'Elements'); + + // Drag swg a, c, e into Y Axis 2 + await page + .locator(`#inspector-elements-tree >> text=${swgA.name}`) + .dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); + await page + .locator(`#inspector-elements-tree >> text=${swgC.name}`) + .dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); + await page + .locator(`#inspector-elements-tree >> text=${swgE.name}`) + .dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); + + // Assert that Y Axis 1 and Y Axis 2 property groups are visible only + await selectInspectorTab(page, 'Config'); + + const yAxis1PropertyGroup = page.locator('[aria-label="Y Axis Properties"]'); + const yAxis2PropertyGroup = page.locator('[aria-label="Y Axis 2 Properties"]'); + const yAxis3PropertyGroup = page.locator('[aria-label="Y Axis 3 Properties"]'); + + await expect(yAxis1PropertyGroup).toBeVisible(); + await expect(yAxis2PropertyGroup).toBeVisible(); + await expect(yAxis3PropertyGroup).toBeHidden(); + + const yAxis1Group = page.getByLabel('Y Axis 1'); + const yAxis2Group = page.getByLabel('Y Axis 2'); + const yAxis3Group = page.getByLabel('Y Axis 3'); + + await selectInspectorTab(page, 'Elements'); + + // Drag swg b into Y Axis 3 + await page + .locator(`#inspector-elements-tree >> text=${swgB.name}`) + .dragTo(page.locator('[aria-label="Element Item Group Y Axis 3"]')); + + // Assert that all Y Axis property groups are visible + await selectInspectorTab(page, 'Config'); + + await expect(yAxis1PropertyGroup).toBeVisible(); + await expect(yAxis2PropertyGroup).toBeVisible(); + await expect(yAxis3PropertyGroup).toBeVisible(); + + // Verify that the elements are in the correct buckets and in the correct order + await selectInspectorTab(page, 'Elements'); + + expect(yAxis1Group.getByRole('listitem', { name: swgD.name })).toBeTruthy(); + expect(yAxis1Group.getByRole('listitem').nth(0).getByText(swgD.name)).toBeTruthy(); + expect(yAxis2Group.getByRole('listitem', { name: swgE.name })).toBeTruthy(); + expect(yAxis2Group.getByRole('listitem').nth(0).getByText(swgE.name)).toBeTruthy(); + expect(yAxis2Group.getByRole('listitem', { name: swgC.name })).toBeTruthy(); + expect(yAxis2Group.getByRole('listitem').nth(1).getByText(swgC.name)).toBeTruthy(); + expect(yAxis2Group.getByRole('listitem', { name: swgA.name })).toBeTruthy(); + expect(yAxis2Group.getByRole('listitem').nth(2).getByText(swgA.name)).toBeTruthy(); + expect(yAxis3Group.getByRole('listitem', { name: swgB.name })).toBeTruthy(); + expect(yAxis3Group.getByRole('listitem').nth(0).getByText(swgB.name)).toBeTruthy(); + }); + + test('Clicking on an item in the elements pool brings up the plot preview with data points', async ({ + page + }) => { + const overlayPlot = await createDomainObjectWithDefaults(page, { + type: 'Overlay Plot' + }); + + const swgA = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + parent: overlayPlot.uuid + }); + + await page.goto(overlayPlot.url); + // Wait for plot series data to load and be drawn + await waitForPlotsToRender(page); + await page.click('button[title="Edit"]'); + + await selectInspectorTab(page, 'Elements'); + + await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click(); + + const plotPixels = await getCanvasPixels(page, '.js-overlay canvas'); + const plotPixelSize = plotPixels.length; + expect(plotPixelSize).toBeGreaterThan(0); + }); }); /** @@ -228,14 +257,14 @@ test.describe('Overlay Plot', () => { * @param {import('@playwright/test').Page} page */ async function assertLimitLinesExistAndAreVisible(page) { - // Wait for plot series data to load - await waitForPlotsToRender(page); - // Wait for limit lines to be created - await page.waitForSelector('.js-limit-area', { state: 'attached' }); - const limitLineCount = await page.locator('.c-plot-limit-line').count(); - // There should be 10 limit lines created by default - expect(await page.locator('.c-plot-limit-line').count()).toBe(10); - for (let i = 0; i < limitLineCount; i++) { - await expect(page.locator('.c-plot-limit-line').nth(i)).toBeVisible(); - } + // Wait for plot series data to load + await waitForPlotsToRender(page); + // Wait for limit lines to be created + await page.waitForSelector('.js-limit-area', { state: 'attached' }); + const limitLineCount = await page.locator('.c-plot-limit-line').count(); + // There should be 10 limit lines created by default + expect(await page.locator('.c-plot-limit-line').count()).toBe(10); + for (let i = 0; i < limitLineCount; i++) { + await expect(page.locator('.c-plot-limit-line').nth(i)).toBeVisible(); + } } diff --git a/e2e/tests/functional/plugins/plot/plotRendering.e2e.spec.js b/e2e/tests/functional/plugins/plot/plotRendering.e2e.spec.js index a89c2f2d3b..767a6a1757 100644 --- a/e2e/tests/functional/plugins/plot/plotRendering.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/plotRendering.e2e.spec.js @@ -21,44 +21,46 @@ *****************************************************************************/ /* -* This test suite is dedicated to testing the rendering and interaction of plots. -* -*/ + * This test suite is dedicated to testing the rendering and interaction of plots. + * + */ const { test, expect } = require('../../../../pluginFixtures'); const { createDomainObjectWithDefaults, getCanvasPixels } = require('../../../../appActions'); test.describe('Plot Rendering', () => { - let sineWaveGeneratorObject; + let sineWaveGeneratorObject; - test.beforeEach(async ({ page }) => { - // Open a browser, navigate to the main page, and wait until all networkevents to resolve - await page.goto('./', { waitUntil: 'domcontentloaded' }); - sineWaveGeneratorObject = await createDomainObjectWithDefaults(page, { type: 'Sine Wave Generator' }); + test.beforeEach(async ({ page }) => { + // Open a browser, navigate to the main page, and wait until all networkevents to resolve + await page.goto('./', { waitUntil: 'domcontentloaded' }); + sineWaveGeneratorObject = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator' }); + }); - test('Plots do not re-request data when a plot is clicked', async ({ page }) => { - // Navigate to Sine Wave Generator - await page.goto(sineWaveGeneratorObject.url); - // Click on the plot canvas - await page.locator('canvas').nth(1).click(); - // No request was made to get historical data - const createMineFolderRequests = []; - page.on('request', req => { - createMineFolderRequests.push(req); - }); - expect(createMineFolderRequests.length).toEqual(0); + test('Plots do not re-request data when a plot is clicked', async ({ page }) => { + // Navigate to Sine Wave Generator + await page.goto(sineWaveGeneratorObject.url); + // Click on the plot canvas + await page.locator('canvas').nth(1).click(); + // No request was made to get historical data + const createMineFolderRequests = []; + page.on('request', (req) => { + createMineFolderRequests.push(req); }); + expect(createMineFolderRequests.length).toEqual(0); + }); - test('Plot is rendered when infinity values exist', async ({ page }) => { - // Edit Plot - await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject); + test('Plot is rendered when infinity values exist', async ({ page }) => { + // Edit Plot + await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject); - //Get pixel data from Canvas - const plotPixels = await getCanvasPixels(page, 'canvas'); - const plotPixelSize = plotPixels.length; - expect(plotPixelSize).toBeGreaterThan(0); - }); + //Get pixel data from Canvas + const plotPixels = await getCanvasPixels(page, 'canvas'); + const plotPixelSize = plotPixels.length; + expect(plotPixelSize).toBeGreaterThan(0); + }); }); /** @@ -69,20 +71,24 @@ test.describe('Plot Rendering', () => { * @returns {Promise} An object containing information about the edited domain object. */ async function editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject) { - await page.goto(sineWaveGeneratorObject.url); - // Edit SWG properties to include infinity values - await page.locator('[title="More options"]').click(); - await page.locator('[title="Edit properties of this object."]').click(); - await page.getByRole('switch', { - name: "Include Infinity Values" - }).check(); + await page.goto(sineWaveGeneratorObject.url); + // Edit SWG properties to include infinity values + await page.locator('[title="More options"]').click(); + await page.locator('[title="Edit properties of this object."]').click(); + await page + .getByRole('switch', { + name: 'Include Infinity Values' + }) + .check(); - await page.getByRole('button', { - name: 'Save' - }).click(); + await page + .getByRole('button', { + name: 'Save' + }) + .click(); - // FIXME: Changes to SWG properties should be reflected on save, but they're not? - // Thus, navigate away and back to the object. - await page.goto('./#/browse/mine'); - await page.goto(sineWaveGeneratorObject.url); + // FIXME: Changes to SWG properties should be reflected on save, but they're not? + // Thus, navigate away and back to the object. + await page.goto('./#/browse/mine'); + await page.goto(sineWaveGeneratorObject.url); } diff --git a/e2e/tests/functional/plugins/plot/scatterPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/scatterPlot.e2e.spec.js index 0e15764e1f..046dd6b81a 100644 --- a/e2e/tests/functional/plugins/plot/scatterPlot.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/scatterPlot.e2e.spec.js @@ -21,77 +21,89 @@ *****************************************************************************/ /* -* This test suite is dedicated to testing the Scatter Plot component. -*/ + * This test suite is dedicated to testing the Scatter Plot component. + */ const { test, expect } = require('../../../../pluginFixtures'); const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions'); const uuid = require('uuid').v4; test.describe('Scatter Plot', () => { - let scatterPlot; + let scatterPlot; - test.beforeEach(async ({ page }) => { - // Open a browser, navigate to the main page, and wait until all networkevents to resolve - await page.goto('./', { waitUntil: 'domcontentloaded' }); + test.beforeEach(async ({ page }) => { + // Open a browser, navigate to the main page, and wait until all networkevents to resolve + await page.goto('./', { waitUntil: 'domcontentloaded' }); - // Create the Scatter Plot - scatterPlot = await createDomainObjectWithDefaults(page, { type: 'Scatter Plot' }); + // Create the Scatter Plot + scatterPlot = await createDomainObjectWithDefaults(page, { type: 'Scatter Plot' }); + }); + + test('Can add and remove telemetry sources', async ({ page }) => { + const editButton = page.locator('button[title="Edit"]'); + const saveButton = page.locator('button[title="Save"]'); + + // Create a sine wave generator within the scatter plot + const swg1 = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: `swg-${uuid()}`, + parent: scatterPlot.uuid }); - test('Can add and remove telemetry sources', async ({ page }) => { - const editButton = page.locator('button[title="Edit"]'); - const saveButton = page.locator('button[title="Save"]'); + // Navigate to the scatter plot and verify that + // the SWG appears in the elements pool + await page.goto(scatterPlot.url); + await editButton.click(); + await selectInspectorTab(page, 'Elements'); + await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible(); + await saveButton.click(); + await page.locator('li[title="Save and Finish Editing"]').click(); - // Create a sine wave generator within the scatter plot - const swg1 = await createDomainObjectWithDefaults(page, { - type: 'Sine Wave Generator', - name: `swg-${uuid()}`, - parent: scatterPlot.uuid - }); - - // Navigate to the scatter plot and verify that - // the SWG appears in the elements pool - await page.goto(scatterPlot.url); - await editButton.click(); - await selectInspectorTab(page, 'Elements'); - await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible(); - await saveButton.click(); - await page.locator('li[title="Save and Finish Editing"]').click(); - - // Create another sine wave generator within the scatter plot - const swg2 = await createDomainObjectWithDefaults(page, { - type: 'Sine Wave Generator', - name: `swg-${uuid()}`, - parent: scatterPlot.uuid - }); - - // Verify that the 'Replace telemetry source' modal appears and accept it - await expect.soft(page.locator('text=This action will replace the current telemetry source. Do you want to continue?')).toBeVisible(); - await page.click('text=Ok'); - - // Navigate to the scatter plot and verify that the new SWG - // appears in the elements pool and the old one is gone - await page.goto(scatterPlot.url); - await editButton.click(); - - // Click the "Elements" tab - await selectInspectorTab(page, 'Elements'); - await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden(); - await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible(); - await saveButton.click(); - - // Right click on the new SWG in the elements pool and delete it - await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({ - button: 'right' - }); - await page.locator('li[title="Remove this object from its containing object."]').click(); - - // Verify that the 'Remove object' confirmation modal appears and accept it - await expect.soft(page.locator('text=Warning! This action will remove this object. Are you sure you want to continue?')).toBeVisible(); - await page.click('text=Ok'); - - // Verify that the elements pool shows no elements - await expect(page.locator('text="No contained elements"')).toBeVisible(); + // Create another sine wave generator within the scatter plot + const swg2 = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: `swg-${uuid()}`, + parent: scatterPlot.uuid }); + + // Verify that the 'Replace telemetry source' modal appears and accept it + await expect + .soft( + page.locator( + 'text=This action will replace the current telemetry source. Do you want to continue?' + ) + ) + .toBeVisible(); + await page.click('text=Ok'); + + // Navigate to the scatter plot and verify that the new SWG + // appears in the elements pool and the old one is gone + await page.goto(scatterPlot.url); + await editButton.click(); + + // Click the "Elements" tab + await selectInspectorTab(page, 'Elements'); + await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden(); + await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible(); + await saveButton.click(); + + // Right click on the new SWG in the elements pool and delete it + await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({ + button: 'right' + }); + await page.locator('li[title="Remove this object from its containing object."]').click(); + + // Verify that the 'Remove object' confirmation modal appears and accept it + await expect + .soft( + page.locator( + 'text=Warning! This action will remove this object. Are you sure you want to continue?' + ) + ) + .toBeVisible(); + await page.click('text=Ok'); + + // Verify that the elements pool shows no elements + await expect(page.locator('text="No contained elements"')).toBeVisible(); + }); }); diff --git a/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js index 401b75efac..c2e46566d4 100644 --- a/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js @@ -29,161 +29,202 @@ const { test, expect } = require('../../../../pluginFixtures'); const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions'); test.describe('Stacked Plot', () => { - let stackedPlot; - let swgA; - let swgB; - let swgC; + let stackedPlot; + let swgA; + let swgB; + let swgC; - test.beforeEach(async ({ page }) => { - // Open a browser, navigate to the main page, and wait until all networkevents to resolve - await page.goto('./', { waitUntil: 'domcontentloaded' }); + test.beforeEach(async ({ page }) => { + // Open a browser, navigate to the main page, and wait until all networkevents to resolve + await page.goto('./', { waitUntil: 'domcontentloaded' }); - stackedPlot = await createDomainObjectWithDefaults(page, { - type: "Stacked Plot" - }); - - swgA = await createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator", - parent: stackedPlot.uuid - }); - swgB = await createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator", - parent: stackedPlot.uuid - }); - swgC = await createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator", - parent: stackedPlot.uuid - }); + stackedPlot = await createDomainObjectWithDefaults(page, { + type: 'Stacked Plot' }); - test('Using the remove action removes the correct plot', async ({ page }) => { - const swgAElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgA.name }); - const swgBElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgB.name }); - const swgCElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgC.name }); - - await page.goto(stackedPlot.url); - - await page.click('button[title="Edit"]'); - - await selectInspectorTab(page, 'Elements'); - - await swgBElementsPoolItem.click({ button: 'right' }); - await page.getByRole('menuitem').filter({ hasText: /Remove/ }).click(); - await page.getByRole('button').filter({ hasText: "OK" }).click(); - - await expect(page.locator('#inspector-elements-tree .js-elements-pool__item')).toHaveCount(2); - - // Confirm that the elements pool contains the items we expect - await expect(swgAElementsPoolItem).toHaveCount(1); - await expect(swgBElementsPoolItem).toHaveCount(0); - await expect(swgCElementsPoolItem).toHaveCount(1); + swgA = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + parent: stackedPlot.uuid }); - - test('Can reorder Stacked Plot items', async ({ page }) => { - const swgAElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgA.name }); - const swgBElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgB.name }); - const swgCElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgC.name }); - - await page.goto(stackedPlot.url); - - await page.click('button[title="Edit"]'); - - await selectInspectorTab(page, 'Elements'); - - const stackedPlotItem1 = page.locator('.c-plot--stacked-container').nth(0); - const stackedPlotItem2 = page.locator('.c-plot--stacked-container').nth(1); - const stackedPlotItem3 = page.locator('.c-plot--stacked-container').nth(2); - - // assert initial plot order - [swgA, swgB, swgC] - await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`); - await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`); - await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`); - - // Drag and drop to reorder - [swgB, swgA, swgC] - await swgBElementsPoolItem.dragTo(swgAElementsPoolItem); - - // assert plot order after reorder - [swgB, swgA, swgC] - await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`); - await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`); - await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`); - - // Drag and drop to reorder - [swgB, swgC, swgA] - await swgCElementsPoolItem.dragTo(swgAElementsPoolItem); - - // assert plot order after second reorder - [swgB, swgC, swgA] - await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`); - await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`); - await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`); - - // collapse inspector - await page.locator('.l-shell__pane-inspector .l-pane__collapse-button').click(); - - // Save (exit edit mode) - await page.locator('button[title="Save"]').click(); - await page.locator('li[title="Save and Finish Editing"]').click(); - - // assert plot order persists after save - [swgB, swgC, swgA] - await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`); - await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`); - await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`); + swgB = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + parent: stackedPlot.uuid }); - - test('Selecting a child plot while in browse and edit modes shows its properties in the inspector', async ({ page }) => { - await page.goto(stackedPlot.url); - - await selectInspectorTab(page, 'Config'); - - // Click on the 1st plot - await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"] canvas`).nth(1).click(); - - // Assert that the inspector shows the Y Axis properties for swgA - await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series"); - await expect(page.getByRole('heading', { name: "Y Axis" })).toBeVisible(); - await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgA.name); - - // Click on the 2nd plot - await page.locator(`[aria-label="Stacked Plot Item ${swgB.name}"] canvas`).nth(1).click(); - - // Assert that the inspector shows the Y Axis properties for swgB - await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series"); - await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); - await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgB.name); - - // Click on the 3rd plot - await page.locator(`[aria-label="Stacked Plot Item ${swgC.name}"] canvas`).nth(1).click(); - - // Assert that the inspector shows the Y Axis properties for swgC - await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series"); - await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); - await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgC.name); - - // Go into edit mode - await page.click('button[title="Edit"]'); - - await selectInspectorTab(page, 'Config'); - - // Click on canvas for the 1st plot - await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"]`).click(); - - // Assert that the inspector shows the Y Axis properties for swgA - await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series"); - await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); - await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgA.name); - - //Click on canvas for the 2nd plot - await page.locator(`[aria-label="Stacked Plot Item ${swgB.name}"]`).click(); - - // Assert that the inspector shows the Y Axis properties for swgB - await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series"); - await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); - await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgB.name); - - //Click on canvas for the 3rd plot - await page.locator(`[aria-label="Stacked Plot Item ${swgC.name}"]`).click(); - - // Assert that the inspector shows the Y Axis properties for swgC - await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series"); - await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); - await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgC.name); + swgC = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + parent: stackedPlot.uuid }); + }); + + test('Using the remove action removes the correct plot', async ({ page }) => { + const swgAElementsPoolItem = page + .locator('#inspector-elements-tree') + .locator('.c-object-label', { hasText: swgA.name }); + const swgBElementsPoolItem = page + .locator('#inspector-elements-tree') + .locator('.c-object-label', { hasText: swgB.name }); + const swgCElementsPoolItem = page + .locator('#inspector-elements-tree') + .locator('.c-object-label', { hasText: swgC.name }); + + await page.goto(stackedPlot.url); + + await page.click('button[title="Edit"]'); + + await selectInspectorTab(page, 'Elements'); + + await swgBElementsPoolItem.click({ button: 'right' }); + await page + .getByRole('menuitem') + .filter({ hasText: /Remove/ }) + .click(); + await page.getByRole('button').filter({ hasText: 'OK' }).click(); + + await expect(page.locator('#inspector-elements-tree .js-elements-pool__item')).toHaveCount(2); + + // Confirm that the elements pool contains the items we expect + await expect(swgAElementsPoolItem).toHaveCount(1); + await expect(swgBElementsPoolItem).toHaveCount(0); + await expect(swgCElementsPoolItem).toHaveCount(1); + }); + + test('Can reorder Stacked Plot items', async ({ page }) => { + const swgAElementsPoolItem = page + .locator('#inspector-elements-tree') + .locator('.c-object-label', { hasText: swgA.name }); + const swgBElementsPoolItem = page + .locator('#inspector-elements-tree') + .locator('.c-object-label', { hasText: swgB.name }); + const swgCElementsPoolItem = page + .locator('#inspector-elements-tree') + .locator('.c-object-label', { hasText: swgC.name }); + + await page.goto(stackedPlot.url); + + await page.click('button[title="Edit"]'); + + await selectInspectorTab(page, 'Elements'); + + const stackedPlotItem1 = page.locator('.c-plot--stacked-container').nth(0); + const stackedPlotItem2 = page.locator('.c-plot--stacked-container').nth(1); + const stackedPlotItem3 = page.locator('.c-plot--stacked-container').nth(2); + + // assert initial plot order - [swgA, swgB, swgC] + await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`); + await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`); + await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`); + + // Drag and drop to reorder - [swgB, swgA, swgC] + await swgBElementsPoolItem.dragTo(swgAElementsPoolItem); + + // assert plot order after reorder - [swgB, swgA, swgC] + await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`); + await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`); + await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`); + + // Drag and drop to reorder - [swgB, swgC, swgA] + await swgCElementsPoolItem.dragTo(swgAElementsPoolItem); + + // assert plot order after second reorder - [swgB, swgC, swgA] + await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`); + await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`); + await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`); + + // collapse inspector + await page.locator('.l-shell__pane-inspector .l-pane__collapse-button').click(); + + // Save (exit edit mode) + await page.locator('button[title="Save"]').click(); + await page.locator('li[title="Save and Finish Editing"]').click(); + + // assert plot order persists after save - [swgB, swgC, swgA] + await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`); + await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`); + await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`); + }); + + test('Selecting a child plot while in browse and edit modes shows its properties in the inspector', async ({ + page + }) => { + await page.goto(stackedPlot.url); + + await selectInspectorTab(page, 'Config'); + + // Click on the 1st plot + await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"] canvas`).nth(1).click(); + + // Assert that the inspector shows the Y Axis properties for swgA + await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText( + 'Plot Series' + ); + await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); + await expect( + page.locator('[aria-label="Plot Series Properties"] .c-object-label') + ).toContainText(swgA.name); + + // Click on the 2nd plot + await page.locator(`[aria-label="Stacked Plot Item ${swgB.name}"] canvas`).nth(1).click(); + + // Assert that the inspector shows the Y Axis properties for swgB + await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText( + 'Plot Series' + ); + await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); + await expect( + page.locator('[aria-label="Plot Series Properties"] .c-object-label') + ).toContainText(swgB.name); + + // Click on the 3rd plot + await page.locator(`[aria-label="Stacked Plot Item ${swgC.name}"] canvas`).nth(1).click(); + + // Assert that the inspector shows the Y Axis properties for swgC + await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText( + 'Plot Series' + ); + await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); + await expect( + page.locator('[aria-label="Plot Series Properties"] .c-object-label') + ).toContainText(swgC.name); + + // Go into edit mode + await page.click('button[title="Edit"]'); + + await selectInspectorTab(page, 'Config'); + + // Click on canvas for the 1st plot + await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"]`).click(); + + // Assert that the inspector shows the Y Axis properties for swgA + await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText( + 'Plot Series' + ); + await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); + await expect( + page.locator('[aria-label="Plot Series Properties"] .c-object-label') + ).toContainText(swgA.name); + + //Click on canvas for the 2nd plot + await page.locator(`[aria-label="Stacked Plot Item ${swgB.name}"]`).click(); + + // Assert that the inspector shows the Y Axis properties for swgB + await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText( + 'Plot Series' + ); + await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); + await expect( + page.locator('[aria-label="Plot Series Properties"] .c-object-label') + ).toContainText(swgB.name); + + //Click on canvas for the 3rd plot + await page.locator(`[aria-label="Stacked Plot Item ${swgC.name}"]`).click(); + + // Assert that the inspector shows the Y Axis properties for swgC + await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText( + 'Plot Series' + ); + await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); + await expect( + page.locator('[aria-label="Plot Series Properties"] .c-object-label') + ).toContainText(swgC.name); + }); }); diff --git a/e2e/tests/functional/plugins/plot/tagging.e2e.spec.js b/e2e/tests/functional/plugins/plot/tagging.e2e.spec.js index c99a81ae38..bf49c1d407 100644 --- a/e2e/tests/functional/plugins/plot/tagging.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/tagging.e2e.spec.js @@ -25,237 +25,250 @@ Tests to verify plot tagging functionality. */ const { test, expect } = require('../../../../pluginFixtures'); -const { createDomainObjectWithDefaults, setRealTimeMode, setFixedTimeMode, waitForPlotsToRender } = require('../../../../appActions'); +const { + createDomainObjectWithDefaults, + setRealTimeMode, + setFixedTimeMode, + waitForPlotsToRender +} = require('../../../../appActions'); test.describe('Plot Tagging', () => { - /** - * Given a canvas and a set of points, tags the points on the canvas. - * @param {import('@playwright/test').Page} page - * @param {HTMLCanvasElement} canvas a telemetry item with a plot - * @param {Number} xEnd a telemetry item with a plot - * @param {Number} yEnd a telemetry item with a plot - * @returns {Promise} - */ - async function createTags({page, canvas, xEnd, yEnd}) { - await canvas.hover({trial: true}); + /** + * Given a canvas and a set of points, tags the points on the canvas. + * @param {import('@playwright/test').Page} page + * @param {HTMLCanvasElement} canvas a telemetry item with a plot + * @param {Number} xEnd a telemetry item with a plot + * @param {Number} yEnd a telemetry item with a plot + * @returns {Promise} + */ + async function createTags({ page, canvas, xEnd, yEnd }) { + await canvas.hover({ trial: true }); - //Alt+Shift Drag Start to select some points to tag - await page.keyboard.down('Alt'); - await page.keyboard.down('Shift'); + //Alt+Shift Drag Start to select some points to tag + await page.keyboard.down('Alt'); + await page.keyboard.down('Shift'); - await canvas.dragTo(canvas, { - sourcePosition: { - x: 1, - y: 1 - }, - targetPosition: { - x: xEnd, - y: yEnd - } - }); - - //Alt Drag End - await page.keyboard.up('Alt'); - await page.keyboard.up('Shift'); - - //Wait for canvas to stablize. - await canvas.hover({trial: true}); - - // add some tags - await page.getByText('Annotations').click(); - await page.getByRole('button', { name: /Add Tag/ }).click(); - await page.getByPlaceholder('Type to select tag').click(); - await page.getByText('Driving').click(); - - await page.getByRole('button', { name: /Add Tag/ }).click(); - await page.getByPlaceholder('Type to select tag').click(); - await page.getByText('Science').click(); - } - - /** - * Given a telemetry item (e.g., a Sine Wave Generator) with a plot, tests that the plot can be tagged. - * @param {import('@playwright/test').Page} page - * @param {import('../../../../appActions').CreatedObjectInfo} telemetryItem a telemetry item with a plot - * @returns {Promise} - */ - async function testTelemetryItem(page, telemetryItem) { - // Check that telemetry item also received the tag - await page.goto(telemetryItem.url); - - await expect(page.getByText('No tags to display for this item')).toBeVisible(); - - const canvas = page.locator('canvas').nth(1); - - //Wait for canvas to stablize. - await canvas.hover({trial: true}); - - // click on the tagged plot point - await canvas.click({ - position: { - x: 325, - y: 377 - } - }); - - await expect(page.getByText('Science')).toBeVisible(); - await expect(page.getByText('Driving')).toBeHidden(); - } - - /** - * Given a page, tests that tags are searchable, deletable, and persist across reloads. - * @param {import('@playwright/test').Page} page - * @returns {Promise} - */ - async function basicTagsTests(page) { - // Search for Driving - await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); - - // Clicking elsewhere should cause annotation selection to be cleared - await expect(page.getByText('No tags to display for this item')).toBeVisible(); - - await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv'); - // click on the search result - await page.getByRole('searchbox', { name: 'OpenMCT Search' }).getByText(/Sine Wave/).first().click(); - - // Delete Driving - await page.hover('[aria-label="Tag"]:has-text("Driving")'); - await page.locator('[aria-label="Remove tag Driving"]').click(); - - // Search for Science - await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); - await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); - await expect(page.locator('[aria-label="Search Result"]').nth(0)).toContainText("Science"); - await expect(page.locator('[aria-label="Search Result"]').nth(0)).not.toContainText("Drilling"); - - // Search for Driving - await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); - await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv'); - await expect(page.getByText('No results found')).toBeVisible(); - - //Reload Page - await page.reload({ waitUntil: 'domcontentloaded' }); - // wait for plots to load - await waitForPlotsToRender(page); - - await page.getByText('Annotations').click(); - await expect(page.getByText('No tags to display for this item')).toBeVisible(); - - const canvas = page.locator('canvas').nth(1); - // click on the tagged plot point - await canvas.click({ - position: { - x: 100, - y: 100 - } - }); - - await expect(page.getByText('Science')).toBeVisible(); - await expect(page.getByText('Driving')).toBeHidden(); - } - - test.beforeEach(async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); + await canvas.dragTo(canvas, { + sourcePosition: { + x: 1, + y: 1 + }, + targetPosition: { + x: xEnd, + y: yEnd + } }); - test('Tags work with Overlay Plots', async ({ page }) => { - //Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374 - test.slow(); + //Alt Drag End + await page.keyboard.up('Alt'); + await page.keyboard.up('Shift'); - const overlayPlot = await createDomainObjectWithDefaults(page, { - type: "Overlay Plot" - }); + //Wait for canvas to stablize. + await canvas.hover({ trial: true }); - const alphaSineWave = await createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator", - name: "Alpha Sine Wave", - parent: overlayPlot.uuid - }); + // add some tags + await page.getByText('Annotations').click(); + await page.getByRole('button', { name: /Add Tag/ }).click(); + await page.getByPlaceholder('Type to select tag').click(); + await page.getByText('Driving').click(); - await createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator", - name: "Beta Sine Wave", - parent: overlayPlot.uuid - }); + await page.getByRole('button', { name: /Add Tag/ }).click(); + await page.getByPlaceholder('Type to select tag').click(); + await page.getByText('Science').click(); + } - await page.goto(overlayPlot.url); + /** + * Given a telemetry item (e.g., a Sine Wave Generator) with a plot, tests that the plot can be tagged. + * @param {import('@playwright/test').Page} page + * @param {import('../../../../appActions').CreatedObjectInfo} telemetryItem a telemetry item with a plot + * @returns {Promise} + */ + async function testTelemetryItem(page, telemetryItem) { + // Check that telemetry item also received the tag + await page.goto(telemetryItem.url); - let canvas = page.locator('canvas').nth(1); + await expect(page.getByText('No tags to display for this item')).toBeVisible(); - // Switch to real-time mode - // Adding tags should pause the plot - await setRealTimeMode(page); + const canvas = page.locator('canvas').nth(1); - await createTags({ - page, - canvas, - xEnd: 700, - yEnd: 480 - }); + //Wait for canvas to stablize. + await canvas.hover({ trial: true }); - await setFixedTimeMode(page); - - await basicTagsTests(page); - await testTelemetryItem(page, alphaSineWave); - - // set to real time mode - await setRealTimeMode(page); - - // Search for Science - await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); - await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); - // click on the search result - await page.getByRole('searchbox', { name: 'OpenMCT Search' }).getByText('Alpha Sine Wave').first().click(); - // wait for plots to load - await expect(page.locator('.js-series-data-loaded')).toBeVisible(); - // expect plot to be paused - await expect(page.locator('[title="Resume displaying real-time data"]')).toBeVisible(); - - await setFixedTimeMode(page); + // click on the tagged plot point + await canvas.click({ + position: { + x: 325, + y: 377 + } }); - test('Tags work with Plot View of telemetry items', async ({ page }) => { - await createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator" - }); + await expect(page.getByText('Science')).toBeVisible(); + await expect(page.getByText('Driving')).toBeHidden(); + } - const canvas = page.locator('canvas').nth(1); - await createTags({ - page, - canvas, - xEnd: 700, - yEnd: 480 - }); - await basicTagsTests(page); + /** + * Given a page, tests that tags are searchable, deletable, and persist across reloads. + * @param {import('@playwright/test').Page} page + * @returns {Promise} + */ + async function basicTagsTests(page) { + // Search for Driving + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); + + // Clicking elsewhere should cause annotation selection to be cleared + await expect(page.getByText('No tags to display for this item')).toBeVisible(); + + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv'); + // click on the search result + await page + .getByRole('searchbox', { name: 'OpenMCT Search' }) + .getByText(/Sine Wave/) + .first() + .click(); + + // Delete Driving + await page.hover('[aria-label="Tag"]:has-text("Driving")'); + await page.locator('[aria-label="Remove tag Driving"]').click(); + + // Search for Science + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); + await expect(page.locator('[aria-label="Search Result"]').nth(0)).toContainText('Science'); + await expect(page.locator('[aria-label="Search Result"]').nth(0)).not.toContainText('Drilling'); + + // Search for Driving + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv'); + await expect(page.getByText('No results found')).toBeVisible(); + + //Reload Page + await page.reload({ waitUntil: 'domcontentloaded' }); + // wait for plots to load + await waitForPlotsToRender(page); + + await page.getByText('Annotations').click(); + await expect(page.getByText('No tags to display for this item')).toBeVisible(); + + const canvas = page.locator('canvas').nth(1); + // click on the tagged plot point + await canvas.click({ + position: { + x: 100, + y: 100 + } }); - test('Tags work with Stacked Plots', async ({ page }) => { - const stackedPlot = await createDomainObjectWithDefaults(page, { - type: "Stacked Plot" - }); + await expect(page.getByText('Science')).toBeVisible(); + await expect(page.getByText('Driving')).toBeHidden(); + } - const alphaSineWave = await createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator", - name: "Alpha Sine Wave", - parent: stackedPlot.uuid - }); + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + }); - await createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator", - name: "Beta Sine Wave", - parent: stackedPlot.uuid - }); + test('Tags work with Overlay Plots', async ({ page }) => { + //Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374 + test.slow(); - await page.goto(stackedPlot.url); - - const canvas = page.locator('canvas').nth(1); - - await createTags({ - page, - canvas, - xEnd: 700, - yEnd: 215 - }); - await basicTagsTests(page); - await testTelemetryItem(page, alphaSineWave); + const overlayPlot = await createDomainObjectWithDefaults(page, { + type: 'Overlay Plot' }); + + const alphaSineWave = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: 'Alpha Sine Wave', + parent: overlayPlot.uuid + }); + + await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: 'Beta Sine Wave', + parent: overlayPlot.uuid + }); + + await page.goto(overlayPlot.url); + + let canvas = page.locator('canvas').nth(1); + + // Switch to real-time mode + // Adding tags should pause the plot + await setRealTimeMode(page); + + await createTags({ + page, + canvas, + xEnd: 700, + yEnd: 480 + }); + + await setFixedTimeMode(page); + + await basicTagsTests(page); + await testTelemetryItem(page, alphaSineWave); + + // set to real time mode + await setRealTimeMode(page); + + // Search for Science + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); + await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); + // click on the search result + await page + .getByRole('searchbox', { name: 'OpenMCT Search' }) + .getByText('Alpha Sine Wave') + .first() + .click(); + // wait for plots to load + await expect(page.locator('.js-series-data-loaded')).toBeVisible(); + // expect plot to be paused + await expect(page.locator('[title="Resume displaying real-time data"]')).toBeVisible(); + + await setFixedTimeMode(page); + }); + + test('Tags work with Plot View of telemetry items', async ({ page }) => { + await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator' + }); + + const canvas = page.locator('canvas').nth(1); + await createTags({ + page, + canvas, + xEnd: 700, + yEnd: 480 + }); + await basicTagsTests(page); + }); + + test('Tags work with Stacked Plots', async ({ page }) => { + const stackedPlot = await createDomainObjectWithDefaults(page, { + type: 'Stacked Plot' + }); + + const alphaSineWave = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: 'Alpha Sine Wave', + parent: stackedPlot.uuid + }); + + await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: 'Beta Sine Wave', + parent: stackedPlot.uuid + }); + + await page.goto(stackedPlot.url); + + const canvas = page.locator('canvas').nth(1); + + await createTags({ + page, + canvas, + xEnd: 700, + yEnd: 215 + }); + await basicTagsTests(page); + await testTelemetryItem(page, alphaSineWave); + }); }); diff --git a/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js b/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js index f472838fcf..089c99c421 100644 --- a/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js +++ b/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js @@ -24,52 +24,59 @@ 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: 'domcontentloaded' }); - - 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); + 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: 'domcontentloaded' }); + + 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 index 0d1f394504..89fa1346d1 100644 --- a/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js +++ b/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js @@ -21,170 +21,200 @@ *****************************************************************************/ const { test, expect } = require('../../../../pluginFixtures'); -const { setFixedTimeMode, setRealTimeMode, setStartOffset, setEndOffset } = require('../../../../appActions'); +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: 'domcontentloaded' }); - const year = new Date().getFullYear(); + test('validate start time does not exceeds end time', async ({ page }) => { + // Go to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); + const year = new Date().getFullYear(); - let startDate = 'xxxx-01-01 01:00:00.000Z'; - startDate = year + startDate.substring(4); + 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); + 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); + const startTimeLocator = page.locator('input[type="text"]').first(); + const endTimeLocator = page.locator('input[type="text"]').nth(1); - // Click start time - await startTimeLocator.click(); + // Click start time + await startTimeLocator.click(); - // Click end time - await endTimeLocator.click(); + // Click end time + await endTimeLocator.click(); - await endTimeLocator.fill(endDate.toString()); - await startTimeLocator.fill(startDate.toString()); + 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(); + // 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(); + 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()); + // 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(); + // 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(); - }); + 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' - }; + test('validate input fields in real-time mode', async ({ page }) => { + const startOffset = { + secs: '23' + }; - const endOffset = { - secs: '31' - }; + const endOffset = { + secs: '31' + }; - // Go to baseURL - await page.goto('./', { waitUntil: 'domcontentloaded' }); + // Go to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); - // Switch to real-time mode - await setRealTimeMode(page); + // Switch to real-time mode + await setRealTimeMode(page); - // Set start time offset - await setStartOffset(page, startOffset); + // 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'); + // 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); + // 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 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' - }; + /** + * 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' - }; + const endOffset = { + secs: '01' + }; - // Convert offsets to milliseconds - const startDelta = (30 * 60 * 1000) + (23 * 1000); - const endDelta = (1 * 1000); + // Convert offsets to milliseconds + const startDelta = 30 * 60 * 1000 + 23 * 1000; + const endDelta = 1 * 1000; - // Go to baseURL - await page.goto('./', { waitUntil: 'domcontentloaded' }); + // Go to baseURL + await page.goto('./', { waitUntil: 'domcontentloaded' }); - // Switch to real-time mode - await setRealTimeMode(page); + // Switch to real-time mode + await setRealTimeMode(page); - // Set start time offset - await setStartOffset(page, startOffset); + // Set start time offset + await setStartOffset(page, startOffset); - // Set end time offset - await setEndOffset(page, endOffset); + // Set end time offset + await setEndOffset(page, endOffset); - // Switch to fixed timespan mode - await setFixedTimeMode(page); + // Switch to fixed timespan mode + await setFixedTimeMode(page); - // Switch back to real-time mode - await setRealTimeMode(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 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 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}`); - }); + // 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 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 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 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 - }); + 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 + }); }); test.describe('Time Conductor History', () => { - test("shows milliseconds on hover @unstable", async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/4386' - }); - // Navigate to Open MCT in Fixed Time Mode, UTC Time System - // with startBound at 2022-01-01 00:00:00.000Z - // and endBound at 2022-01-01 00:00:00.200Z - await page.goto('./#/browse/mine?view=grid&tc.mode=fixed&tc.startBound=1640995200000&tc.endBound=1640995200200&tc.timeSystem=utc&hideInspector=true', { waitUntil: 'networkidle' }); - await page.locator("[aria-label='Time Conductor History']").hover({ trial: true}); - await page.locator("[aria-label='Time Conductor History']").click(); - - // Validate history item format - const historyItem = page.locator('text="2022-01-01 00:00:00 + 200ms"'); - await expect(historyItem).toBeEnabled(); - await expect(historyItem).toHaveAttribute('title', '2022-01-01 00:00:00.000 - 2022-01-01 00:00:00.200'); + test('shows milliseconds on hover @unstable', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/4386' }); + // Navigate to Open MCT in Fixed Time Mode, UTC Time System + // with startBound at 2022-01-01 00:00:00.000Z + // and endBound at 2022-01-01 00:00:00.200Z + await page.goto( + './#/browse/mine?view=grid&tc.mode=fixed&tc.startBound=1640995200000&tc.endBound=1640995200200&tc.timeSystem=utc&hideInspector=true', + { waitUntil: 'networkidle' } + ); + await page.locator("[aria-label='Time Conductor History']").hover({ trial: true }); + await page.locator("[aria-label='Time Conductor History']").click(); + + // Validate history item format + const historyItem = page.locator('text="2022-01-01 00:00:00 + 200ms"'); + await expect(historyItem).toBeEnabled(); + await expect(historyItem).toHaveAttribute( + 'title', + '2022-01-01 00:00:00.000 - 2022-01-01 00:00:00.200' + ); + }); }); diff --git a/e2e/tests/functional/plugins/timer/timer.e2e.spec.js b/e2e/tests/functional/plugins/timer/timer.e2e.spec.js index 2c8c547ad4..6a7d4ad3cf 100644 --- a/e2e/tests/functional/plugins/timer/timer.e2e.spec.js +++ b/e2e/tests/functional/plugins/timer/timer.e2e.spec.js @@ -21,43 +21,46 @@ *****************************************************************************/ const { test, expect } = require('../../../../pluginFixtures'); -const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions'); +const { + openObjectTreeContextMenu, + createDomainObjectWithDefaults +} = require('../../../../appActions'); test.describe('Timer', () => { - let timer; - test.beforeEach(async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); - timer = await createDomainObjectWithDefaults(page, { type: 'timer' }); + let timer; + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + timer = await createDomainObjectWithDefaults(page, { type: 'timer' }); + }); + + test('Can perform actions on the Timer', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/4313' }); - test('Can perform actions on the Timer', async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/4313' - }); + const timerUrl = timer.url; - 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'); - }); + 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'); + }); + }); }); /** @@ -76,10 +79,10 @@ test.describe('Timer', () => { * @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); + const menuAction = `.c-menu ul li >> text="${action}"`; + await openObjectTreeContextMenu(page, timerUrl); + await page.locator(menuAction).click(); + assertTimerStateAfterAction(page, action); } /** @@ -88,21 +91,21 @@ async function triggerTimerContextMenuAction(page, timerUrl, action) { * @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++; - } + 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); + await page.locator(menuAction).click(); + assertTimerStateAfterAction(page, action); } /** @@ -111,10 +114,10 @@ async function triggerTimer3dotMenuAction(page, action) { * @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); + await page.locator('.c-timer').hover({ trial: true }); + const buttonTitle = buttonTitleFromAction(action); + await page.click(`button[title="${buttonTitle}"]`); + assertTimerStateAfterAction(page, action); } /** @@ -122,14 +125,14 @@ async function triggerTimerViewAction(page, action) { * @param {TimerViewAction} action */ function buttonTitleFromAction(action) { - switch (action) { + switch (action) { case 'Start': - return 'Start'; + return 'Start'; case 'Pause': - return 'Pause'; + return 'Pause'; case 'Restart at 0': - return 'Reset'; - } + return 'Reset'; + } } /** @@ -138,19 +141,19 @@ function buttonTitleFromAction(action) { * @param {TimerAction} action */ async function assertTimerStateAfterAction(page, action) { - let timerStateClass; - switch (action) { + let timerStateClass; + switch (action) { case 'Start': case 'Restart at 0': - timerStateClass = "is-started"; - break; + timerStateClass = 'is-started'; + break; case 'Stop': - timerStateClass = 'is-stopped'; - break; + timerStateClass = 'is-stopped'; + break; case 'Pause': - timerStateClass = 'is-paused'; - break; - } + timerStateClass = 'is-paused'; + break; + } - await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass)); + await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass)); } diff --git a/e2e/tests/functional/recentObjects.e2e.spec.js b/e2e/tests/functional/recentObjects.e2e.spec.js index 97072aa5fb..a97c32d88c 100644 --- a/e2e/tests/functional/recentObjects.e2e.spec.js +++ b/e2e/tests/functional/recentObjects.e2e.spec.js @@ -25,284 +25,327 @@ const { createDomainObjectWithDefaults } = require('../../appActions.js'); const { waitForAnimations } = require('../../baseFixtures.js'); test.describe('Recent Objects', () => { - /** @type {import('@playwright/test').Locator} */ - let recentObjectsList; - /** @type {import('@playwright/test').Locator} */ - let clock; - /** @type {import('@playwright/test').Locator} */ - let folderA; - test.beforeEach(async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); + /** @type {import('@playwright/test').Locator} */ + let recentObjectsList; + /** @type {import('@playwright/test').Locator} */ + let clock; + /** @type {import('@playwright/test').Locator} */ + let folderA; + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); - // Set Recent Objects List locator for subsequent tests - recentObjectsList = page.getByRole('list', { - name: 'Recent Objects' - }); - - // Create a folder and nest a Clock within it - folderA = await createDomainObjectWithDefaults(page, { - type: 'Folder' - }); - clock = await createDomainObjectWithDefaults(page, { - type: 'Clock', - parent: folderA.uuid - }); - - // Drag the Recent Objects panel up a bit - await page.locator('.l-pane.l-pane--vertical-handle-before', { - hasText: 'Recently Viewed' - }).locator('.l-pane__handle').hover(); - await page.mouse.down(); - await page.mouse.move(0, 100); - await page.mouse.up(); - }); - test('Navigated objects show up in recents, object renames and deletions are reflected', async ({ page }) => { - // Verify that both created objects appear in the list and are in the correct order - await assertInitialRecentObjectsListState(); - - // Navigate to the folder by clicking on the main object name in the recent objects list item - await page.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click(); - await page.waitForURL(`**/${folderA.uuid}?*`); - expect(recentObjectsList.getByRole('listitem').nth(0).getByText(folderA.name)).toBeTruthy(); - - // Rename - folderA.name = `${folderA.name}-NEW!`; - await page.locator('.l-browse-bar__object-name').fill(""); - await page.locator('.l-browse-bar__object-name').fill(folderA.name); - await page.keyboard.press('Enter'); - - // Verify rename has been applied in recent objects list item and objects paths - expect(await page.getByRole('navigation', { - name: clock.name - }).locator('a').filter({ - hasText: folderA.name - }).count()).toBeGreaterThan(0); - expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy(); - - // Delete - await page.click('button[title="Show selected item in tree"]'); - // Delete the folder via the left tree pane treeitem context menu - await page.getByRole('treeitem', { name: new RegExp(folderA.name) }).locator('a').click({ - button: 'right' - }); - await page.getByRole('menuitem', { name: /Remove/ }).click(); - await page.getByRole('button', { name: 'OK' }).click(); - - // Verify that the folder and clock are no longer in the recent objects list - await expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeHidden(); - await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeHidden(); - }); - test("Clicking on an object in the path of a recent object navigates to the object", async ({ page, openmctConfig }) => { - const { myItemsFolderName } = openmctConfig; - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/6151' - }); - await page.goto('./#/browse/mine'); - - // Navigate to the folder by clicking on its entry in the Clock's breadcrumb - const waitForFolderNavigation = page.waitForURL(`**/${folderA.uuid}?*`); - await page.getByRole('navigation', { - name: clock.name - }).locator('a').filter({ - hasText: folderA.name - }).click(); - - // Verify that the hash URL updates correctly - await waitForFolderNavigation; - expect(page.url()).toMatch(new RegExp(`.*${folderA.uuid}?.*`)); - - // Navigate to My Items by clicking on its entry in the Clock's breadcrumb - const waitForMyItemsNavigation = page.waitForURL(`**/mine?*`); - await page.getByRole('navigation', { - name: clock.name - }).locator('a').filter({ - hasText: myItemsFolderName - }).click(); - - // Verify that the hash URL updates correctly - await waitForMyItemsNavigation; - expect(page.url()).toMatch(new RegExp(`.*mine?.*`)); - }); - test("Clicking on the 'target button' scrolls the object into view in the tree and highlights it", async ({ page }) => { - const clockTreeItem = page.getByRole('tree', { name: 'Main Tree'}).getByRole('treeitem', { name: clock.name }); - const folderTreeItem = page.getByRole('tree', { name: 'Main Tree'}) - .getByRole('treeitem', { - name: folderA.name, - expanded: true - }); - - // Click the "Target" button for the Clock which is nested in a folder - await page.getByRole('button', { name: `Open and scroll to ${clock.name}`}).click(); - - // Assert that the Clock parent folder has expanded and the Clock is visible) - await expect(folderTreeItem.locator('.c-disclosure-triangle')).toHaveClass(/--expanded/); - await expect(clockTreeItem).toBeVisible(); - - // Assert that the Clock treeitem is highlighted - await expect(clockTreeItem.locator('.c-tree__item')).toHaveClass(/is-targeted-item/); - - // Wait for highlight animation to end - await waitForAnimations(clockTreeItem.locator('.c-tree__item')); - - // Assert that the Clock treeitem is no longer highlighted - await expect(clockTreeItem.locator('.c-tree__item')).not.toHaveClass(/is-targeted-item/); - }); - test("Persists on refresh", async ({ page }) => { - await assertInitialRecentObjectsListState(); - await page.reload(); - await assertInitialRecentObjectsListState(); - }); - test("Displays objects and aliases uniquely", async ({ page }) => { - const mainTree = page.getByRole('tree', { name: 'Main Tree'}); - - // Navigate to the clock and reveal it in the tree - await page.goto(clock.url); - await page.getByTitle('Show selected item in tree').click(); - - // Right click the clock and create an alias using the "link" context menu action - const clockTreeItem = page.getByRole('tree', { - name: 'Main Tree' - }).getByRole('treeitem', { - name: clock.name - }); - await clockTreeItem.click({ - button: 'right' - }); - await page.getByRole('menuitem', { - name: /Create Link/ - }).click(); - await page.getByRole('tree', { name: 'Create Modal Tree'}).getByRole('treeitem').first().click(); - await page.getByRole('button', { name: 'Save' }).click(); - - // Click the newly created object alias in the tree - await mainTree.getByRole('treeitem', { - name: new RegExp(clock.name) - }).filter({ - has: page.locator('.is-alias') - }).click(); - - // Assert that two recent objects are displayed and one of them is an alias - expect(await recentObjectsList.getByRole('listitem', { name: clock.name }).count()).toBe(2); - expect(await recentObjectsList.locator('.is-alias').count()).toBe(1); - - // Assert that the alias and the original's breadcrumbs are different - const clockBreadcrumbs = recentObjectsList.getByRole('listitem', {name: clock.name}).getByRole('navigation'); - expect(await clockBreadcrumbs.count()).toBe(2); - expect(await clockBreadcrumbs.nth(0).innerText()).not.toEqual(await clockBreadcrumbs.nth(1).innerText()); - }); - test("Enforces a limit of 20 recent objects and clears the recent objects", async ({ page }) => { - // Creating 21 objects takes a while, so increase the timeout - test.slow(); - - // Assert that the list initially contains 3 objects (clock, folder, my items) - expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(3); - - let lastFolder; - let lastClock; - // Create 19 more objects (3 in beforeEach() + 18 new = 21 total) - for (let i = 0; i < 9; i++) { - lastFolder = await createDomainObjectWithDefaults(page, { - type: "Folder", - parent: lastFolder?.uuid - }); - lastClock = await createDomainObjectWithDefaults(page, { - type: "Clock", - parent: lastFolder?.uuid - }); - } - - // Assert that the list contains 20 objects - expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(20); - - // Collapse the tree - await page.getByTitle("Collapse all tree items").click(); - const lastFolderTreeItem = page.getByRole('tree', { name: 'Main Tree'}) - .getByRole('treeitem', { - name: lastFolder.name, - expanded: true - }); - const lastClockTreeItem = page.getByRole('tree', { name: 'Main Tree'}) - .getByRole('treeitem', { - name: lastClock.name - }); - - // Test "Open and Scroll To" in a deeply nested tree, while we're here - await page.getByRole('button', { name: `Open and scroll to ${lastClock.name}`}).click(); - - // Assert that the Clock parent folder has expanded and the Clock is visible) - await expect(lastFolderTreeItem.locator('.c-disclosure-triangle')).toHaveClass(/--expanded/); - await expect(lastClockTreeItem).toBeVisible(); - - // Assert that the Clock treeitem is highlighted - await expect(lastClockTreeItem.locator('.c-tree__item')).toHaveClass(/is-targeted-item/); - - // Wait for highlight animation to end - await waitForAnimations(lastClockTreeItem.locator('.c-tree__item')); - - // Assert that the Clock treeitem is no longer highlighted - await expect(lastClockTreeItem.locator('.c-tree__item')).not.toHaveClass(/is-targeted-item/); - - // Click the aria-label="Clear Recently Viewed" button - await page.getByRole('button', { name: 'Clear Recently Viewed' }).click(); - - // Click on the "OK" button in the confirmation dialog - await page.getByRole('button', { name: 'OK' }).click(); - - // Assert that the list is empty - expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(0); - }); - test("Ensure clear recent objects button is active or inactive", async ({ page }) => { - // Assert that the list initially contains 3 objects (clock, folder, my items) - expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(3); - - // Assert that the button is enabled - expect( - await page - .getByRole("button", { name: "Clear Recently Viewed" }) - .isEnabled() - ).toBe(true); - - // Click the aria-label="Clear Recently Viewed" button - await page.getByRole("button", { name: "Clear Recently Viewed" }).click(); - - // Click on the "OK" button in the confirmation dialog - await page.getByRole("button", { name: "OK" }).click(); - - // Assert that the list is empty - expect( - await recentObjectsList.locator(".c-recentobjects-listitem").count() - ).toBe(0); - - // Assert that the button is disabled - expect( - await page - .getByRole("button", { name: "Clear Recently Viewed" }) - .isEnabled() - ).toBe(false); - - // Navigate to folder object - await page.goto(folderA.url); - - // Assert that the list contains 1 object - expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(1); - - // Assert that the button is enabled - expect( - await page - .getByRole("button", { name: "Clear Recently Viewed" }) - .isEnabled() - ).toBe(true); + // Set Recent Objects List locator for subsequent tests + recentObjectsList = page.getByRole('list', { + name: 'Recent Objects' }); - function assertInitialRecentObjectsListState() { - return Promise.all([ - expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeVisible(), - expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeVisible(), - expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeVisible(), - expect(recentObjectsList.getByRole('listitem').nth(0).getByText(clock.name)).toBeVisible(), - expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeVisible(), - expect(recentObjectsList.getByRole('listitem').nth(3).getByText(folderA.name)).toBeVisible() - ]); + // Create a folder and nest a Clock within it + folderA = await createDomainObjectWithDefaults(page, { + type: 'Folder' + }); + clock = await createDomainObjectWithDefaults(page, { + type: 'Clock', + parent: folderA.uuid + }); + + // Drag the Recent Objects panel up a bit + await page + .locator('.l-pane.l-pane--vertical-handle-before', { + hasText: 'Recently Viewed' + }) + .locator('.l-pane__handle') + .hover(); + await page.mouse.down(); + await page.mouse.move(0, 100); + await page.mouse.up(); + }); + test('Navigated objects show up in recents, object renames and deletions are reflected', async ({ + page + }) => { + // Verify that both created objects appear in the list and are in the correct order + await assertInitialRecentObjectsListState(); + + // Navigate to the folder by clicking on the main object name in the recent objects list item + await page.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click(); + await page.waitForURL(`**/${folderA.uuid}?*`); + expect(recentObjectsList.getByRole('listitem').nth(0).getByText(folderA.name)).toBeTruthy(); + + // Rename + folderA.name = `${folderA.name}-NEW!`; + await page.locator('.l-browse-bar__object-name').fill(''); + await page.locator('.l-browse-bar__object-name').fill(folderA.name); + await page.keyboard.press('Enter'); + + // Verify rename has been applied in recent objects list item and objects paths + expect( + await page + .getByRole('navigation', { + name: clock.name + }) + .locator('a') + .filter({ + hasText: folderA.name + }) + .count() + ).toBeGreaterThan(0); + expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy(); + + // Delete + await page.click('button[title="Show selected item in tree"]'); + // Delete the folder via the left tree pane treeitem context menu + await page + .getByRole('treeitem', { name: new RegExp(folderA.name) }) + .locator('a') + .click({ + button: 'right' + }); + await page.getByRole('menuitem', { name: /Remove/ }).click(); + await page.getByRole('button', { name: 'OK' }).click(); + + // Verify that the folder and clock are no longer in the recent objects list + await expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeHidden(); + await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeHidden(); + }); + test('Clicking on an object in the path of a recent object navigates to the object', async ({ + page, + openmctConfig + }) => { + const { myItemsFolderName } = openmctConfig; + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/6151' + }); + await page.goto('./#/browse/mine'); + + // Navigate to the folder by clicking on its entry in the Clock's breadcrumb + const waitForFolderNavigation = page.waitForURL(`**/${folderA.uuid}?*`); + await page + .getByRole('navigation', { + name: clock.name + }) + .locator('a') + .filter({ + hasText: folderA.name + }) + .click(); + + // Verify that the hash URL updates correctly + await waitForFolderNavigation; + expect(page.url()).toMatch(new RegExp(`.*${folderA.uuid}?.*`)); + + // Navigate to My Items by clicking on its entry in the Clock's breadcrumb + const waitForMyItemsNavigation = page.waitForURL(`**/mine?*`); + await page + .getByRole('navigation', { + name: clock.name + }) + .locator('a') + .filter({ + hasText: myItemsFolderName + }) + .click(); + + // Verify that the hash URL updates correctly + await waitForMyItemsNavigation; + expect(page.url()).toMatch(new RegExp(`.*mine?.*`)); + }); + test("Clicking on the 'target button' scrolls the object into view in the tree and highlights it", async ({ + page + }) => { + const clockTreeItem = page + .getByRole('tree', { name: 'Main Tree' }) + .getByRole('treeitem', { name: clock.name }); + const folderTreeItem = page.getByRole('tree', { name: 'Main Tree' }).getByRole('treeitem', { + name: folderA.name, + expanded: true + }); + + // Click the "Target" button for the Clock which is nested in a folder + await page.getByRole('button', { name: `Open and scroll to ${clock.name}` }).click(); + + // Assert that the Clock parent folder has expanded and the Clock is visible) + await expect(folderTreeItem.locator('.c-disclosure-triangle')).toHaveClass(/--expanded/); + await expect(clockTreeItem).toBeVisible(); + + // Assert that the Clock treeitem is highlighted + await expect(clockTreeItem.locator('.c-tree__item')).toHaveClass(/is-targeted-item/); + + // Wait for highlight animation to end + await waitForAnimations(clockTreeItem.locator('.c-tree__item')); + + // Assert that the Clock treeitem is no longer highlighted + await expect(clockTreeItem.locator('.c-tree__item')).not.toHaveClass(/is-targeted-item/); + }); + test('Persists on refresh', async ({ page }) => { + await assertInitialRecentObjectsListState(); + await page.reload(); + await assertInitialRecentObjectsListState(); + }); + test('Displays objects and aliases uniquely', async ({ page }) => { + const mainTree = page.getByRole('tree', { name: 'Main Tree' }); + + // Navigate to the clock and reveal it in the tree + await page.goto(clock.url); + await page.getByTitle('Show selected item in tree').click(); + + // Right click the clock and create an alias using the "link" context menu action + const clockTreeItem = page + .getByRole('tree', { + name: 'Main Tree' + }) + .getByRole('treeitem', { + name: clock.name + }); + await clockTreeItem.click({ + button: 'right' + }); + await page + .getByRole('menuitem', { + name: /Create Link/ + }) + .click(); + await page + .getByRole('tree', { name: 'Create Modal Tree' }) + .getByRole('treeitem') + .first() + .click(); + await page.getByRole('button', { name: 'Save' }).click(); + + // Click the newly created object alias in the tree + await mainTree + .getByRole('treeitem', { + name: new RegExp(clock.name) + }) + .filter({ + has: page.locator('.is-alias') + }) + .click(); + + // Assert that two recent objects are displayed and one of them is an alias + expect(await recentObjectsList.getByRole('listitem', { name: clock.name }).count()).toBe(2); + expect(await recentObjectsList.locator('.is-alias').count()).toBe(1); + + // Assert that the alias and the original's breadcrumbs are different + const clockBreadcrumbs = recentObjectsList + .getByRole('listitem', { name: clock.name }) + .getByRole('navigation'); + expect(await clockBreadcrumbs.count()).toBe(2); + expect(await clockBreadcrumbs.nth(0).innerText()).not.toEqual( + await clockBreadcrumbs.nth(1).innerText() + ); + }); + test('Enforces a limit of 20 recent objects and clears the recent objects', async ({ page }) => { + // Creating 21 objects takes a while, so increase the timeout + test.slow(); + + // Assert that the list initially contains 3 objects (clock, folder, my items) + expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(3); + + let lastFolder; + let lastClock; + // Create 19 more objects (3 in beforeEach() + 18 new = 21 total) + for (let i = 0; i < 9; i++) { + lastFolder = await createDomainObjectWithDefaults(page, { + type: 'Folder', + parent: lastFolder?.uuid + }); + lastClock = await createDomainObjectWithDefaults(page, { + type: 'Clock', + parent: lastFolder?.uuid + }); } + + // Assert that the list contains 20 objects + expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(20); + + // Collapse the tree + await page.getByTitle('Collapse all tree items').click(); + const lastFolderTreeItem = page.getByRole('tree', { name: 'Main Tree' }).getByRole('treeitem', { + name: lastFolder.name, + expanded: true + }); + const lastClockTreeItem = page.getByRole('tree', { name: 'Main Tree' }).getByRole('treeitem', { + name: lastClock.name + }); + + // Test "Open and Scroll To" in a deeply nested tree, while we're here + await page.getByRole('button', { name: `Open and scroll to ${lastClock.name}` }).click(); + + // Assert that the Clock parent folder has expanded and the Clock is visible) + await expect(lastFolderTreeItem.locator('.c-disclosure-triangle')).toHaveClass(/--expanded/); + await expect(lastClockTreeItem).toBeVisible(); + + // Assert that the Clock treeitem is highlighted + await expect(lastClockTreeItem.locator('.c-tree__item')).toHaveClass(/is-targeted-item/); + + // Wait for highlight animation to end + await waitForAnimations(lastClockTreeItem.locator('.c-tree__item')); + + // Assert that the Clock treeitem is no longer highlighted + await expect(lastClockTreeItem.locator('.c-tree__item')).not.toHaveClass(/is-targeted-item/); + + // Click the aria-label="Clear Recently Viewed" button + await page.getByRole('button', { name: 'Clear Recently Viewed' }).click(); + + // Click on the "OK" button in the confirmation dialog + await page.getByRole('button', { name: 'OK' }).click(); + + // Assert that the list is empty + expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(0); + }); + test('Ensure clear recent objects button is active or inactive', async ({ page }) => { + // Assert that the list initially contains 3 objects (clock, folder, my items) + expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(3); + + // Assert that the button is enabled + expect(await page.getByRole('button', { name: 'Clear Recently Viewed' }).isEnabled()).toBe( + true + ); + + // Click the aria-label="Clear Recently Viewed" button + await page.getByRole('button', { name: 'Clear Recently Viewed' }).click(); + + // Click on the "OK" button in the confirmation dialog + await page.getByRole('button', { name: 'OK' }).click(); + + // Assert that the list is empty + expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(0); + + // Assert that the button is disabled + expect(await page.getByRole('button', { name: 'Clear Recently Viewed' }).isEnabled()).toBe( + false + ); + + // Navigate to folder object + await page.goto(folderA.url); + + // Assert that the list contains 1 object + expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(1); + + // Assert that the button is enabled + expect(await page.getByRole('button', { name: 'Clear Recently Viewed' }).isEnabled()).toBe( + true + ); + }); + + function assertInitialRecentObjectsListState() { + return Promise.all([ + expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeVisible(), + expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeVisible(), + expect( + recentObjectsList + .getByRole('listitem', { name: clock.name }) + .locator('a') + .getByText(folderA.name) + ).toBeVisible(), + expect(recentObjectsList.getByRole('listitem').nth(0).getByText(clock.name)).toBeVisible(), + expect( + recentObjectsList + .getByRole('listitem', { name: clock.name }) + .locator('a') + .getByText(folderA.name) + ).toBeVisible(), + expect(recentObjectsList.getByRole('listitem').nth(3).getByText(folderA.name)).toBeVisible() + ]); + } }); diff --git a/e2e/tests/functional/search.e2e.spec.js b/e2e/tests/functional/search.e2e.spec.js index 656e86349b..846f8afda9 100644 --- a/e2e/tests/functional/search.e2e.spec.js +++ b/e2e/tests/functional/search.e2e.spec.js @@ -28,242 +28,270 @@ const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../ap const { v4: uuid } = require('uuid'); test.describe('Grand Search', () => { - const searchResultSelector = '.c-gsearch-result__title'; - const searchResultDropDownSelector = '.c-gsearch__results'; + const searchResultSelector = '.c-gsearch-result__title'; + const searchResultDropDownSelector = '.c-gsearch__results'; - test.beforeEach(async ({ page }) => { - // Go to baseURL - await page.goto("./", { waitUntil: "networkidle" }); + test.beforeEach(async ({ page }) => { + // Go to baseURL + await page.goto('./', { waitUntil: 'networkidle' }); + }); + + test('Can search for objects, and subsequent search dropdown behaves properly', async ({ + page, + openmctConfig + }) => { + const { myItemsFolderName } = openmctConfig; + + const createdObjects = await createObjectsForSearch(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('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 the Elements pool to dismiss the search menu + await selectInspectorTab(page, 'Elements'); + 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="Search Result"] >> nth=0').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('[aria-label="Clock A clock result"] >> 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( + createdObjects.displayLayout.name + ); + 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('Validate empty search result', async ({ page }) => { + // 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 = 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 }) => { + // Create a folder object + const folderName = uuid(); + await createDomainObjectWithDefaults(page, { + type: 'folder', + name: folderName }); - test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page, openmctConfig }) => { - const { myItemsFolderName } = openmctConfig; + // Full search for object + await page.type('input[type=search]', folderName); - const createdObjects = await createObjectsForSearch(page); + // Wait for search to complete + await waitForSearchCompletion(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('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 the Elements pool to dismiss the search menu - await selectInspectorTab(page, 'Elements'); - await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden(); + // Get the search results + const searchResults = page.locator(searchResultSelector); - 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(); + // Verify that one result is found + await expect(searchResults).toBeVisible(); + expect(await searchResults.count()).toBe(1); + await expect(searchResults).toHaveText(folderName); + }); - // 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`); + test('Search results are debounced @couchdb', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/6179' + }); + await createObjectsForSearch(page); - // Click [aria-label="OpenMCT Search"] a >> nth=0 - await page.locator('[aria-label="Search Result"] >> nth=0').click(); - await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden(); - - // 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('[aria-label="Clock A clock result"] >> 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(createdObjects.displayLayout.name); - 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`); + let networkRequests = []; + page.on('request', (request) => { + const searchRequest = request.url().endsWith('_find'); + const fetchRequest = request.resourceType() === 'fetch'; + if (searchRequest && fetchRequest) { + networkRequests.push(request); + } }); - test('Validate empty search result', async ({ page }) => { - // Invalid search for objects - await page.type("input[type=search]", 'not found'); + // Full search for object + await page.type('input[type=search]', 'Clock', { delay: 100 }); - // Wait for search to complete - await waitForSearchCompletion(page); + // Wait for search to finish + await waitForSearchCompletion(page); - // Get the search results - const searchResults = page.locator(searchResultSelector); + // Network requests for the composite telemetry with multiple items should be: + // 1. batched request for latest telemetry using the bulk API + expect(networkRequests.length).toBe(1); - // Verify that no results are found - expect(await searchResults.count()).toBe(0); + const searchResultDropDown = await page.locator(searchResultDropDownSelector); - // Verify proper message appears - await expect(page.locator('text=No results found')).toBeVisible(); + await expect(searchResultDropDown).toContainText('Clock A'); + }); + + 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' }); - test('Validate single object in search result @couchdb', async ({ page }) => { - // Create a folder object - const folderName = uuid(); - await createDomainObjectWithDefaults(page, { - type: 'folder', - name: folderName - }); + // Create folder objects + const folderName1 = 'e928a26e-e924-4ea0'; + const folderName2 = 'e928a26e-e924-4001'; - // 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 - await expect(searchResults).toBeVisible(); - expect(await searchResults.count()).toBe(1); - await expect(searchResults).toHaveText(folderName); + await createDomainObjectWithDefaults(page, { + type: 'Folder', + name: folderName1 }); - test('Search results are debounced @couchdb', async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/6179' - }); - await createObjectsForSearch(page); - - let networkRequests = []; - page.on('request', (request) => { - const searchRequest = request.url().endsWith('_find'); - const fetchRequest = request.resourceType() === 'fetch'; - if (searchRequest && fetchRequest) { - networkRequests.push(request); - } - }); - - // Full search for object - await page.type("input[type=search]", 'Clock', { delay: 100 }); - - // Wait for search to finish - await waitForSearchCompletion(page); - - // Network requests for the composite telemetry with multiple items should be: - // 1. batched request for latest telemetry using the bulk API - expect(networkRequests.length).toBe(1); - - const searchResultDropDown = await page.locator(searchResultDropDownSelector); - - await expect(searchResultDropDown).toContainText('Clock A'); + await createDomainObjectWithDefaults(page, { + type: 'Folder', + name: folderName2 }); - 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' - }); + // Partial search for objects + await page.type('input[type=search]', 'e928a26e'); - // Create folder objects - const folderName1 = "e928a26e-e924-4ea0"; - const folderName2 = "e928a26e-e924-4001"; + // Wait for search to finish + await waitForSearchCompletion(page); - await createDomainObjectWithDefaults(page, { - type: 'Folder', - name: folderName1 - }); + const searchResultDropDown = page.locator(searchResultDropDownSelector); - await createDomainObjectWithDefaults(page, { - type: 'Folder', - name: folderName2 - }); + // Verify that the search result/s correctly match the search query + await expect(searchResultDropDown).toContainText(folderName1); + await expect(searchResultDropDown).toContainText(folderName2); - // Partial search for objects - await page.type("input[type=search]", 'e928a26e'); - - // Wait for search to finish - await waitForSearchCompletion(page); - - const searchResultDropDown = page.locator(searchResultDropDownSelector); - - // Verify that the search result/s correctly match the search query - await expect(searchResultDropDown).toContainText(folderName1); - await expect(searchResultDropDown).toContainText(folderName2); - - // Get the search results - const searchResults = page.locator(searchResultSelector); - // Verify that two results are found - expect(await searchResults.count()).toBe(2); - }); + // Get the search results + const searchResults = page.locator(searchResultSelector); + // Verify that two results are found + expect(await searchResults.count()).toBe(2); + }); }); async function waitForSearchCompletion(page) { - // Wait loading spinner to disappear - await page.waitForSelector('.search-finished'); + // Wait loading spinner to disappear + await page.waitForSelector('.search-finished'); } /** - * Creates some domain objects for searching - * @param {import('@playwright/test').Page} page - */ + * Creates some domain objects for searching + * @param {import('@playwright/test').Page} page + */ async function createObjectsForSearch(page) { - const redFolder = await createDomainObjectWithDefaults(page, { - type: 'Folder', - name: 'Red Folder' - }); + const redFolder = await createDomainObjectWithDefaults(page, { + type: 'Folder', + name: 'Red Folder' + }); - const blueFolder = await createDomainObjectWithDefaults(page, { - type: 'Folder', - name: 'Blue Folder', - parent: redFolder.uuid - }); + const blueFolder = await createDomainObjectWithDefaults(page, { + type: 'Folder', + name: 'Blue Folder', + parent: redFolder.uuid + }); - const clockA = await createDomainObjectWithDefaults(page, { - type: 'Clock', - name: 'Clock A', - parent: blueFolder.uuid - }); - const clockB = await createDomainObjectWithDefaults(page, { - type: 'Clock', - name: 'Clock B', - parent: blueFolder.uuid - }); - const clockC = await createDomainObjectWithDefaults(page, { - type: 'Clock', - name: 'Clock C', - parent: blueFolder.uuid - }); - const clockD = await createDomainObjectWithDefaults(page, { - type: 'Clock', - name: 'Clock D', - parent: blueFolder.uuid - }); + const clockA = await createDomainObjectWithDefaults(page, { + type: 'Clock', + name: 'Clock A', + parent: blueFolder.uuid + }); + const clockB = await createDomainObjectWithDefaults(page, { + type: 'Clock', + name: 'Clock B', + parent: blueFolder.uuid + }); + const clockC = await createDomainObjectWithDefaults(page, { + type: 'Clock', + name: 'Clock C', + parent: blueFolder.uuid + }); + const clockD = await createDomainObjectWithDefaults(page, { + type: 'Clock', + name: 'Clock D', + parent: blueFolder.uuid + }); - const displayLayout = await createDomainObjectWithDefaults(page, { - type: 'Display Layout' - }); + const displayLayout = await createDomainObjectWithDefaults(page, { + type: 'Display Layout' + }); - // Go back into edit mode for the display layout - await page.locator('button[title="Edit"]').click(); + // Go back into edit mode for the display layout + await page.locator('button[title="Edit"]').click(); - return { - redFolder, - blueFolder, - clockA, - clockB, - clockC, - clockD, - displayLayout - }; + return { + redFolder, + blueFolder, + clockA, + clockB, + clockC, + clockD, + displayLayout + }; } diff --git a/e2e/tests/functional/smoke.e2e.spec.js b/e2e/tests/functional/smoke.e2e.spec.js index b6cfb7e5d8..375ec8ce57 100644 --- a/e2e/tests/functional/smoke.e2e.spec.js +++ b/e2e/tests/functional/smoke.e2e.spec.js @@ -35,25 +35,26 @@ Make no assumptions about the order that elements appear in the DOM. const { test, expect } = require('../../pluginFixtures'); -test('Verify that the create button appears and that the Folder Domain Object is available for selection', async ({ page }) => { +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: 'domcontentloaded' }); - //Go to baseURL - await page.goto('./', { waitUntil: 'domcontentloaded' }); + //Click the Create button + await page.click('button:has-text("Create")'); - //Click the Create button - await page.click('button:has-text("Create")'); - - // Verify that Create Folder appears in the dropdown - await expect(page.locator(':nth-match(:text("Folder"), 2)')).toBeEnabled(); + // Verify that Create Folder appears in the dropdown + 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('./'); + 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(); + //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 index cc7dcf1f71..efdd70b6d7 100644 --- a/e2e/tests/functional/tree.e2e.spec.js +++ b/e2e/tests/functional/tree.e2e.spec.js @@ -22,151 +22,158 @@ const { test, expect } = require('../../pluginFixtures.js'); const { - createDomainObjectWithDefaults, - openObjectTreeContextMenu + createDomainObjectWithDefaults, + openObjectTreeContextMenu } = require('../../appActions.js'); test.describe('Main Tree', () => { - test.beforeEach(async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + }); + + test('Creating a child object within a folder and immediately opening it shows the created object in the tree @couchdb', async ({ + page + }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/5975' }); - test('Creating a child object within a folder and immediately opening it shows the created object in the tree @couchdb', async ({ page }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/5975' - }); - - const folder = await createDomainObjectWithDefaults(page, { - type: 'Folder' - }); - - await page.getByTitle('Show selected item in tree').click(); - - const clock = await createDomainObjectWithDefaults(page, { - type: 'Clock', - parent: folder.uuid - }); - - await expandTreePaneItemByName(page, folder.name); - await assertTreeItemIsVisible(page, clock.name); + const folder = await createDomainObjectWithDefaults(page, { + type: 'Folder' }); - test('Creating a child object on one tab and expanding its parent on the other shows the correct composition @2p', async ({ page, openmctConfig }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/6391' - }); + await page.getByTitle('Show selected item in tree').click(); - const { myItemsFolderName } = openmctConfig; - const page2 = await page.context().newPage(); - - // Both pages: Go to baseURL - await Promise.all([ - page.goto('./', { waitUntil: 'networkidle' }), - page2.goto('./', { waitUntil: 'networkidle' }) - ]); - - const page1Folder = await createDomainObjectWithDefaults(page, { - type: 'Folder' - }); - - await expandTreePaneItemByName(page2, myItemsFolderName); - await assertTreeItemIsVisible(page2, page1Folder.name); + const clock = await createDomainObjectWithDefaults(page, { + type: 'Clock', + parent: folder.uuid }); - test('Creating a child object on one tab and expanding its parent on the other shows the correct composition @couchdb @2p', async ({ page, openmctConfig }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/6391' - }); + await expandTreePaneItemByName(page, folder.name); + await assertTreeItemIsVisible(page, clock.name); + }); - const { myItemsFolderName } = openmctConfig; - const page2 = await page.context().newPage(); - - // Both pages: Go to baseURL - await Promise.all([ - page.goto('./', { waitUntil: 'networkidle' }), - page2.goto('./', { waitUntil: 'networkidle' }) - ]); - - const page1Folder = await createDomainObjectWithDefaults(page, { - type: 'Folder' - }); - - await expandTreePaneItemByName(page2, myItemsFolderName); - await assertTreeItemIsVisible(page2, page1Folder.name); + test('Creating a child object on one tab and expanding its parent on the other shows the correct composition @2p', async ({ + page, + openmctConfig + }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/6391' }); - test('Renaming an object reorders the tree @unstable', async ({ page, openmctConfig }) => { - const { myItemsFolderName } = openmctConfig; + const { myItemsFolderName } = openmctConfig; + const page2 = await page.context().newPage(); - await createDomainObjectWithDefaults(page, { - type: 'Folder', - name: 'Foo' - }); + // Both pages: Go to baseURL + await Promise.all([ + page.goto('./', { waitUntil: 'networkidle' }), + page2.goto('./', { waitUntil: 'networkidle' }) + ]); - 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" - ]); - }); + const page1Folder = await createDomainObjectWithDefaults(page, { + type: 'Folder' }); + + await expandTreePaneItemByName(page2, myItemsFolderName); + await assertTreeItemIsVisible(page2, page1Folder.name); + }); + + test('Creating a child object on one tab and expanding its parent on the other shows the correct composition @couchdb @2p', async ({ + page, + openmctConfig + }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/6391' + }); + + const { myItemsFolderName } = openmctConfig; + const page2 = await page.context().newPage(); + + // Both pages: Go to baseURL + await Promise.all([ + page.goto('./', { waitUntil: 'networkidle' }), + page2.goto('./', { waitUntil: 'networkidle' }) + ]); + + const page1Folder = await createDomainObjectWithDefaults(page, { + type: 'Folder' + }); + + await expandTreePaneItemByName(page2, myItemsFolderName); + await assertTreeItemIsVisible(page2, page1Folder.name); + }); + + test('Renaming an object reorders the tree @unstable', async ({ page, openmctConfig }) => { + const { myItemsFolderName } = openmctConfig; + + 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' + ]); + }); + }); }); /** @@ -174,22 +181,22 @@ test.describe('Main Tree', () => { * @param {Array} 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); + 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); } async function assertTreeItemIsVisible(page, name) { - const mainTree = page.getByRole('tree', { - name: 'Main Tree' - }); - const treeItem = mainTree.getByRole('treeitem', { - name - }); + const mainTree = page.getByRole('tree', { + name: 'Main Tree' + }); + const treeItem = mainTree.getByRole('treeitem', { + name + }); - await expect(treeItem).toBeVisible(); + await expect(treeItem).toBeVisible(); } /** @@ -197,14 +204,14 @@ async function assertTreeItemIsVisible(page, name) { * @param {string} name */ async function expandTreePaneItemByName(page, name) { - const mainTree = page.getByRole('tree', { - name: 'Main Tree' - }); - const treeItem = mainTree.getByRole('treeitem', { - name, - expanded: false - }); - await treeItem.locator('.c-disclosure-triangle').click(); + const mainTree = page.getByRole('tree', { + name: 'Main Tree' + }); + const treeItem = mainTree.getByRole('treeitem', { + name, + expanded: false + }); + await treeItem.locator('.c-disclosure-triangle').click(); } /** @@ -214,10 +221,10 @@ async function expandTreePaneItemByName(page, name) { * @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"]'); + 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/performance/imagery.perf.spec.js b/e2e/tests/performance/imagery.perf.spec.js index 67b0873209..e47187c2d4 100644 --- a/e2e/tests/performance/imagery.perf.spec.js +++ b/e2e/tests/performance/imagery.perf.spec.js @@ -37,141 +37,154 @@ 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' }); + 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('button:has-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 - }); + // Click a:has-text("My Items") + await page.locator('a:has-text("My Items")').click({ + button: 'right' }); - test.afterEach(async ({ page, browser}) => { - console.log("\n==== Devtools: stopTracing ====\n"); - await browser.stopTracing(); - /* Measurement Section + // 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('button:has-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 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 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 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 + //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('./'); + 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")) - ]); + // 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); + //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 index ace6ebcbb3..2c7dd798f8 100644 --- a/e2e/tests/performance/memleak-imagery.perf.spec.js +++ b/e2e/tests/performance/memleak-imagery.perf.spec.js @@ -38,82 +38,84 @@ 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' }); + 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(); + // Click a:has-text("My Items") + await page.locator('a:has-text("My Items")').click({ + button: 'right' }); - test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => { + // Click text=Import from JSON + await page.locator('text=Import from JSON').click(); - await page.goto('./', {waitUntil: 'networkidle'}); + // Upload Performance Display Layout.json + await page.setInputFiles('#fileElem', filePath); - // 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() - ]); + // Click text=OK + await page.locator('text=OK').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 expect( + page.locator('a:has-text("Performance Display Layout Display Layout")') + ).toBeVisible(); + }); - 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'); + test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => { + await page.goto('./', { waitUntil: 'networkidle' }); - let performanceMetricsBefore = await client.send('Performance.getMetrics'); - console.log(performanceMetricsBefore.metrics); + // 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() + ]); - //await client.send('Performance.disable'); + //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' }); - //Open Large view - await page.locator('button:has-text("Large View")').click(); - await client.send('HeapProfiler.takeHeapSnapshot'); + 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'); - //Time to Imagery Rendered in Large Frame - await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'}); + let performanceMetricsBefore = await client.send('Performance.getMetrics'); + console.log(performanceMetricsBefore.metrics); - //Time to Example Imagery object loads - await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'}); + //await client.send('Performance.disable'); - // Click Close Icon - await page.locator('.c-click-icon').click(); + //Open Large view + await page.locator('button:has-text("Large View")').click(); + await client.send('HeapProfiler.takeHeapSnapshot'); - //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'}); + //Time to Imagery Rendered in Large Frame + await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' }); - await client.send('HeapProfiler.collectGarbage'); - //await client.send('Performance.enable'); + //Time to Example Imagery object loads + await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' }); - let performanceMetricsAfter = await client.send('Performance.getMetrics'); - console.log(performanceMetricsAfter.metrics); + // Click Close Icon + await page.locator('.c-click-icon').click(); - //await client.send('Performance.disable'); + //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 index 31c82c4bce..4402e6960e 100644 --- a/e2e/tests/performance/notebook.perf.spec.js +++ b/e2e/tests/performance/notebook.perf.spec.js @@ -36,124 +36,131 @@ 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' }); + 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 - }); + // Click a:has-text("My Items") + await page.locator('a:has-text("My Items")').click({ + button: 'right' }); - test.afterEach(async ({ page, browser}) => { - console.log("\n==== Devtools: stopTracing ====\n"); - await browser.stopTracing(); - /* Measurement Section + // 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); + 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 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 + //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('./'); + 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")) - ]); + // 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__input').hover(); - 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); + 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__input').hover(); + 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/visual/addInit.visual.spec.js b/e2e/tests/visual/addInit.visual.spec.js index ecca8e7ebb..007ce9904f 100644 --- a/e2e/tests/visual/addInit.visual.spec.js +++ b/e2e/tests/visual/addInit.visual.spec.js @@ -40,22 +40,23 @@ 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.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 }) => { + await page.addInitScript({ + path: path.join(__dirname, '../../helper', './addInitRestrictedNotebook.js') }); + //Go to baseURL + await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' }); - test('Restricted Notebook is visually correct @addInit @unstable', async ({ page, theme }) => { - 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 }); - 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}')`); - - }); + // 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 index fc029535e8..5d57f546d3 100644 --- a/e2e/tests/visual/components/tree.visual.spec.js +++ b/e2e/tests/visual/components/tree.visual.spec.js @@ -26,67 +26,67 @@ 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' }); + 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 = "[role=tree][aria-label='Main Tree']"; - - 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 - }); + 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 = "[role=tree][aria-label='Main Tree']"; + + 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 + }); + }); }); /** @@ -94,8 +94,8 @@ test.describe('Visual - Tree Pane', () => { * @param {string} name */ async function expandTreePaneItemByName(page, name) { - const treePane = page.getByTestId('tree-pane'); - const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`); - const expandTriangle = treeItem.locator('.c-disclosure-triangle'); - await expandTriangle.click(); + const treePane = page.getByTestId('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 index 1f9e5c9fb2..5a12fdf390 100644 --- a/e2e/tests/visual/controlledClock.visual.spec.js +++ b/e2e/tests/visual/controlledClock.visual.spec.js @@ -31,26 +31,26 @@ 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.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' }); + 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'); + 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}); + //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}')`); - }); + //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 bfcb267e9b..e733415567 100644 --- a/e2e/tests/visual/default.visual.spec.js +++ b/e2e/tests/visual/default.visual.spec.js @@ -37,132 +37,134 @@ const percySnapshot = require('@percy/playwright'); const { createDomainObjectWithDefaults } = require('../../appActions'); test.describe('Visual - Default', () => { - test.beforeEach(async ({ page }) => { - //Go to baseURL and Hide Tree - await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' }); - }); - test.use({ - clockOptions: { - now: 0, //Set browser clock to UNIX Epoch - shouldAdvanceTime: false //Don't advance the clock - } + test.beforeEach(async ({ page }) => { + //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 + } + }); + + test('Visual - Root and About', async ({ page, theme }) => { + // Verify that Create button is actionable + await expect(page.locator('button:has-text("Create")')).toBeEnabled(); + + // Take a snapshot of the Dashboard + await percySnapshot(page, `Root (theme: '${theme}')`); + + // 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 = + '
  • Version: visual-snapshot
  • Build Date: Mon Nov 15 2021 08:07:51 GMT-0800 (Pacific Standard Time)
  • Revision: 93049cdbc6c047697ca204893db9603b864b8c9f
  • Branch: master
  • ') + ); + + // Take a snapshot of the About modal + await percySnapshot(page, `About (theme: '${theme}')`); + }); + + test('Visual - Default Condition Set @unstable', async ({ page, theme }) => { + await createDomainObjectWithDefaults(page, { type: 'Condition Set' }); + + // Take a snapshot of the newly created Condition Set object + await percySnapshot(page, `Default Condition Set (theme: '${theme}')`); + }); + + test('Visual - Default Condition Widget @unstable', async ({ page, theme }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/5349' }); - test('Visual - Root and About', async ({ page, theme }) => { - // Verify that Create button is actionable - await expect(page.locator('button:has-text("Create")')).toBeEnabled(); + await createDomainObjectWithDefaults(page, { type: 'Condition Widget' }); - // Take a snapshot of the Dashboard - await percySnapshot(page, `Root (theme: '${theme}')`); + // Take a snapshot of the newly created Condition Widget object + await percySnapshot(page, `Default Condition Widget (theme: '${theme}')`); + }); - // Click About button - await page.click('.l-shell__app-logo'); + test('Visual - Time Conductor start time is less than end time', async ({ page, theme }) => { + const year = new Date().getFullYear(); - // 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 = '
  • Version: visual-snapshot
  • Build Date: Mon Nov 15 2021 08:07:51 GMT-0800 (Pacific Standard Time)
  • Revision: 93049cdbc6c047697ca204893db9603b864b8c9f
  • Branch: master
  • '); + let startDate = 'xxxx-01-01 01:00:00.000Z'; + startDate = year + startDate.substring(4); - // Take a snapshot of the About modal - await percySnapshot(page, `About (theme: '${theme}')`); - }); + let endDate = 'xxxx-01-01 02:00:00.000Z'; + endDate = year + endDate.substring(4); - test('Visual - Default Condition Set @unstable', async ({ page, theme }) => { + await page.locator('input[type="text"]').nth(1).fill(endDate.toString()); + await page.locator('input[type="text"]').first().fill(startDate.toString()); - await createDomainObjectWithDefaults(page, { type: 'Condition Set' }); + // verify no error msg + await percySnapshot(page, `Default Time conductor (theme: '${theme}')`); - // Take a snapshot of the newly created Condition Set object - await percySnapshot(page, `Default Condition Set (theme: '${theme}')`); - }); + 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(); - test('Visual - Default Condition Widget @unstable', async ({ page, theme }) => { - test.info().annotations.push({ - type: 'issue', - description: 'https://github.com/nasa/openmct/issues/5349' - }); + // verify error msg for start time (unable to capture snapshot of popup) + await percySnapshot(page, `Start time error (theme: '${theme}')`); - await createDomainObjectWithDefaults(page, { type: 'Condition Widget' }); + startDate = year - 1 + startDate.substring(4); + await page.locator('input[type="text"]').first().fill(startDate.toString()); - // Take a snapshot of the newly created Condition Widget object - await percySnapshot(page, `Default Condition Widget (theme: '${theme}')`); - }); + endDate = year - 2 + endDate.substring(4); + await page.locator('input[type="text"]').nth(1).fill(endDate.toString()); - test('Visual - Time Conductor start time is less than end time', async ({ page, theme }) => { - const year = new Date().getFullYear(); + await page.locator('input[type="text"]').first().click(); - let startDate = 'xxxx-01-01 01:00:00.000Z'; - startDate = year + startDate.substring(4); + // verify error msg for end time (unable to capture snapshot of popup) + await percySnapshot(page, `End time error (theme: '${theme}')`); + }); - let endDate = 'xxxx-01-01 02:00:00.000Z'; - endDate = year + endDate.substring(4); + test('Visual - Sine Wave Generator Form', async ({ page, theme }) => { + //Click the Create button + await page.click('button:has-text("Create")'); - await page.locator('input[type="text"]').nth(1).fill(endDate.toString()); - await page.locator('input[type="text"]').first().fill(startDate.toString()); + // Click text=Sine Wave Generator + await page.click('text=Sine Wave Generator'); - // verify no error msg - await percySnapshot(page, `Default Time conductor (theme: '${theme}')`); + await percySnapshot(page, `Default Sine Wave Generator Form (theme: '${theme}')`); - 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(); + 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 start time (unable to capture snapshot of popup) - await percySnapshot(page, `Start time error (theme: '${theme}')`); + // Validate red x mark + await percySnapshot(page, `removed amplitude property value (theme: '${theme}')`); + }); - startDate = (year - 1) + startDate.substring(4); - await page.locator('input[type="text"]').first().fill(startDate.toString()); + test('Visual - Save Successful Banner @unstable', async ({ page, theme }) => { + await createDomainObjectWithDefaults(page, { type: 'Timer' }); - endDate = (year - 2) + endDate.substring(4); - await page.locator('input[type="text"]').nth(1).fill(endDate.toString()); + await page.locator('.c-message-banner__message').hover({ trial: true }); + await percySnapshot(page, `Banner message shown (theme: '${theme}')`); - await page.locator('input[type="text"]').first().click(); + //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}')`); + }); - // verify error msg for end time (unable to capture snapshot of popup) - await percySnapshot(page, `End time error (theme: '${theme}')`); - }); + test('Visual - Display Layout Icon is correct', async ({ page, theme }) => { + //Click the Create button + await page.click('button:has-text("Create")'); - test('Visual - Sine Wave Generator Form', async ({ page, theme }) => { - //Click the Create button - await page.click('button:has-text("Create")'); + //Hover on Display Layout option. + await page.locator('text=Display Layout').hover(); + await percySnapshot(page, `Display Layout Create Menu (theme: '${theme}')`); + }); - // Click text=Sine Wave Generator - await page.click('text=Sine Wave Generator'); + test('Visual - Default Gauge is correct @unstable', async ({ page, theme }) => { + await createDomainObjectWithDefaults(page, { type: 'Gauge' }); - await percySnapshot(page, `Default Sine Wave Generator Form (theme: '${theme}')`); - - await page.locator('.field.control.l-input-sm input').first().click(); - await page.locator('.field.control.l-input-sm input').first().fill(''); - - // 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' }); - - await page.locator('.c-message-banner__message').hover({ trial: true }); - await percySnapshot(page, `Banner message shown (theme: '${theme}')`); - - //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")'); - - //Hover on Display Layout option. - await page.locator('text=Display Layout').hover(); - await percySnapshot(page, `Display Layout Create Menu (theme: '${theme}')`); - - }); - - test('Visual - Default Gauge is correct @unstable', async ({ page, theme }) => { - await createDomainObjectWithDefaults(page, { type: 'Gauge' }); - - // Take a snapshot of the newly created Gauge object - await percySnapshot(page, `Default Gauge (theme: '${theme}')`); - }); + // 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 index c59e25a2a0..ccb94e120e 100644 --- a/e2e/tests/visual/faultManagement.visual.spec.js +++ b/e2e/tests/visual/faultManagement.visual.spec.js @@ -27,51 +27,64 @@ const percySnapshot = require('@percy/playwright'); const utils = require('../../helper/faultUtils'); test.describe('The Fault Management Plugin Visual Test', () => { - - test('icon test', async ({ page, theme }) => { - 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('icon test', async ({ page, theme }) => { + await page.addInitScript({ + path: path.join(__dirname, '../../helper/', 'addInitFaultManagementPlugin.js') }); + await page.goto('./', { waitUntil: 'networkidle' }); - test('fault list and acknowledged faults', async ({ page, theme }) => { - await utils.navigateToFaultManagementWithStaticExample(page); + await percySnapshot(page, `Fault Management icon appears in tree (theme: '${theme}')`); + }); - await percySnapshot(page, `Shows a list of faults in the standard view (theme: '${theme}')`); + test('fault list and acknowledged faults', async ({ page, theme }) => { + await utils.navigateToFaultManagementWithStaticExample(page); - await utils.acknowledgeFault(page, 1); - await utils.changeViewTo(page, 'acknowledged'); + await percySnapshot(page, `Shows a list of faults in the standard view (theme: '${theme}')`); - await percySnapshot(page, `Acknowledged faults, have a checkmark on the fault icon and appear in the acknowldeged view (theme: '${theme}')`); - }); + await utils.acknowledgeFault(page, 1); + await utils.changeViewTo(page, 'acknowledged'); - test('shelved faults', async ({ page, theme }) => { - await utils.navigateToFaultManagementWithStaticExample(page); + await percySnapshot( + page, + `Acknowledged faults, have a checkmark on the fault icon and appear in the acknowldeged view (theme: '${theme}')` + ); + }); - await utils.shelveFault(page, 1); - await utils.changeViewTo(page, 'shelved'); + test('shelved faults', async ({ page, theme }) => { + await utils.navigateToFaultManagementWithStaticExample(page); - await percySnapshot(page, `Shelved faults appear in the shelved view (theme: '${theme}')`); + await utils.shelveFault(page, 1); + await utils.changeViewTo(page, 'shelved'); - await utils.openFaultRowMenu(page, 1); + await percySnapshot(page, `Shelved faults appear in the shelved view (theme: '${theme}')`); - await percySnapshot(page, `Shelved faults have a 3-dot menu with Unshelve option enabled (theme: '${theme}')`); - }); + await utils.openFaultRowMenu(page, 1); - test('3-dot menu for fault', async ({ page, theme }) => { - await utils.navigateToFaultManagementWithStaticExample(page); + await percySnapshot( + page, + `Shelved faults have a 3-dot menu with Unshelve option enabled (theme: '${theme}')` + ); + }); - await utils.openFaultRowMenu(page, 1); + test('3-dot menu for fault', async ({ page, theme }) => { + await utils.navigateToFaultManagementWithStaticExample(page); - await percySnapshot(page, `Faults have a 3-dot menu with Acknowledge, Shelve and Unshelve (Unshelve is disabled) options (theme: '${theme}')`); - }); + await utils.openFaultRowMenu(page, 1); - test('ability to acknowledge or shelve', async ({ page, theme }) => { - await utils.navigateToFaultManagementWithStaticExample(page); + await percySnapshot( + page, + `Faults have a 3-dot menu with Acknowledge, Shelve and Unshelve (Unshelve is disabled) options (theme: '${theme}')` + ); + }); - await utils.selectFaultItem(page, 1); + test('ability to acknowledge or shelve', async ({ page, theme }) => { + await utils.navigateToFaultManagementWithStaticExample(page); - await percySnapshot(page, `Selected faults highlight the ability to Acknowledge or Shelve above the fault list (theme: '${theme}')`); - }); + 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/ladTable.visual.spec.js b/e2e/tests/visual/ladTable.visual.spec.js index 53c40681d5..1d3a7f9151 100644 --- a/e2e/tests/visual/ladTable.visual.spec.js +++ b/e2e/tests/visual/ladTable.visual.spec.js @@ -25,50 +25,54 @@ const percySnapshot = require('@percy/playwright'); const { createDomainObjectWithDefaults } = require('../../appActions'); test.describe('Visual - LAD Table', () => { - /** @type {import('@playwright/test').Locator} */ - let ladTable; + /** @type {import('@playwright/test').Locator} */ + let ladTable; - test.beforeEach(async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); - // Create LAD Table - ladTable = await createDomainObjectWithDefaults(page, { - type: 'LAD Table', - name: 'LAD Table Test' - }); - // Create SWG inside of LAD Table - await createDomainObjectWithDefaults(page, { - type: 'Sine Wave Generator', - name: 'SWG4LAD Table Test', - parent: ladTable.uuid - }); - - //Modify SWG to create a really stable SWG - await page.locator('button[title="More options"]').click(); - - await page.getByRole('menuitem', { name: ' Edit Properties...' }).click(); - - //Forgive me, padre - await page.getByRole('spinbutton', { name: 'Data Rate (hz)' }).fill('0'); - await page.getByRole('spinbutton', { name: 'Period' }).fill('0'); - - await page.getByRole('button', { name: 'Save' }).click(); + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + // Create LAD Table + ladTable = await createDomainObjectWithDefaults(page, { + type: 'LAD Table', + name: 'LAD Table Test' }); - test('Toggled column widths behave accordingly', async ({ page, theme }) => { - - await page.goto(ladTable.url); - //Close panes for visual consistency - await page.getByTitle('Collapse Inspect Pane').click(); - await page.getByTitle('Collapse Browse Pane').click(); - - await expect(page.locator('button[title="Expand Columns"]')).toBeVisible(); - - await percySnapshot(page, `LAD Table w/ Sine Wave Generator columns autosized (theme: ${theme})`); - - await page.locator('button[title="Expand Columns"]').click(); - - await expect(page.locator('button[title="Autosize Columns"]')).toBeVisible(); - - await percySnapshot(page, `LAD Table w/ Sine Wave Generator columns expanded (theme: ${theme})`); - + // Create SWG inside of LAD Table + await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: 'SWG4LAD Table Test', + parent: ladTable.uuid }); + + //Modify SWG to create a really stable SWG + await page.locator('button[title="More options"]').click(); + + await page.getByRole('menuitem', { name: ' Edit Properties...' }).click(); + + //Forgive me, padre + await page.getByRole('spinbutton', { name: 'Data Rate (hz)' }).fill('0'); + await page.getByRole('spinbutton', { name: 'Period' }).fill('0'); + + await page.getByRole('button', { name: 'Save' }).click(); + }); + test('Toggled column widths behave accordingly', async ({ page, theme }) => { + await page.goto(ladTable.url); + //Close panes for visual consistency + await page.getByTitle('Collapse Inspect Pane').click(); + await page.getByTitle('Collapse Browse Pane').click(); + + await expect(page.locator('button[title="Expand Columns"]')).toBeVisible(); + + await percySnapshot( + page, + `LAD Table w/ Sine Wave Generator columns autosized (theme: ${theme})` + ); + + await page.locator('button[title="Expand Columns"]').click(); + + await expect(page.locator('button[title="Autosize Columns"]')).toBeVisible(); + + await percySnapshot( + page, + `LAD Table w/ Sine Wave Generator columns expanded (theme: ${theme})` + ); + }); }); diff --git a/e2e/tests/visual/notebook.visual.spec.js b/e2e/tests/visual/notebook.visual.spec.js index 04696a29de..cdade10488 100644 --- a/e2e/tests/visual/notebook.visual.spec.js +++ b/e2e/tests/visual/notebook.visual.spec.js @@ -25,27 +25,26 @@ 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})`); + 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/notification.visual.spec.js b/e2e/tests/visual/notification.visual.spec.js index 3b2280a44c..6bfc18bce9 100644 --- a/e2e/tests/visual/notification.visual.spec.js +++ b/e2e/tests/visual/notification.visual.spec.js @@ -28,31 +28,36 @@ const { test, expect } = require('../../pluginFixtures'); const percySnapshot = require('@percy/playwright'); const { createDomainObjectWithDefaults } = require('../../appActions'); -test.describe('Visual - Check Notification Info Banner of \'Save successful\'', () => { - test.beforeEach(async ({ page }) => { - // Go to baseURL and Hide Tree - await page.goto('./', { waitUntil: 'networkidle' }); - }); +test.describe("Visual - Check Notification Info Banner of 'Save successful'", () => { + test.beforeEach(async ({ page }) => { + // Go to baseURL and Hide Tree + await page.goto('./', { waitUntil: 'networkidle' }); + }); - test('Create a clock, click on \'Save successful\' banner and dismiss it', async ({ page, theme }) => { - // Create a clock domain object - await createDomainObjectWithDefaults(page, { type: 'Clock' }); - // Verify there is a button with aria-label="Review 1 Notification" - expect(await page.locator('button[aria-label="Review 1 Notification"]').isVisible()).toBe(true); - // Verify there is a button with aria-label="Clear all notifications" - expect(await page.locator('button[aria-label="Clear all notifications"]').isVisible()).toBe(true); - // Click on the div with role="alert" that has "Save successful" text - await page.locator('div[role="alert"]:has-text("Save successful")').click(); - // Verify there is a div with role="dialog" - expect(await page.locator('div[role="dialog"]').isVisible()).toBe(true); - // Verify the div with role="dialog" contains text "Save successful" - expect(await page.locator('div[role="dialog"]').innerText()).toContain('Save successful'); - await percySnapshot(page, `Notification banner - ${theme}`); - // Verify there is a button with text "Dismiss" - expect(await page.locator('button:has-text("Dismiss")').isVisible()).toBe(true); - // Click on button with text "Dismiss" - await page.locator('button:has-text("Dismiss")').click(); - // Verify there is no div with role="dialog" - expect(await page.locator('div[role="dialog"]').isVisible()).toBe(false); - }); + test("Create a clock, click on 'Save successful' banner and dismiss it", async ({ + page, + theme + }) => { + // Create a clock domain object + await createDomainObjectWithDefaults(page, { type: 'Clock' }); + // Verify there is a button with aria-label="Review 1 Notification" + expect(await page.locator('button[aria-label="Review 1 Notification"]').isVisible()).toBe(true); + // Verify there is a button with aria-label="Clear all notifications" + expect(await page.locator('button[aria-label="Clear all notifications"]').isVisible()).toBe( + true + ); + // Click on the div with role="alert" that has "Save successful" text + await page.locator('div[role="alert"]:has-text("Save successful")').click(); + // Verify there is a div with role="dialog" + expect(await page.locator('div[role="dialog"]').isVisible()).toBe(true); + // Verify the div with role="dialog" contains text "Save successful" + expect(await page.locator('div[role="dialog"]').innerText()).toContain('Save successful'); + await percySnapshot(page, `Notification banner - ${theme}`); + // Verify there is a button with text "Dismiss" + expect(await page.locator('button:has-text("Dismiss")').isVisible()).toBe(true); + // Click on button with text "Dismiss" + await page.locator('button:has-text("Dismiss")').click(); + // Verify there is no div with role="dialog" + expect(await page.locator('div[role="dialog"]').isVisible()).toBe(false); + }); }); diff --git a/e2e/tests/visual/planning.visual.spec.js b/e2e/tests/visual/planning.visual.spec.js index 120553e627..7f41398421 100644 --- a/e2e/tests/visual/planning.visual.spec.js +++ b/e2e/tests/visual/planning.visual.spec.js @@ -29,71 +29,71 @@ const examplePlanSmall = require('../../test-data/examplePlans/ExamplePlan_Small const snapshotScope = '.l-shell__pane-main .l-pane__contents'; test.describe('Visual - Planning', () => { - test.beforeEach(async ({ page }) => { - await page.goto('./', { waitUntil: 'domcontentloaded' }); + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + }); + + test('Plan View', async ({ page, theme }) => { + const plan = await createPlanFromJSON(page, { + name: 'Plan Visual Test', + json: examplePlanSmall }); - test('Plan View', async ({ page, theme }) => { - const plan = await createPlanFromJSON(page, { - name: 'Plan Visual Test', - json: examplePlanSmall - }); + await setBoundsToSpanAllActivities(page, examplePlanSmall, plan.url); + await percySnapshot(page, `Plan View (theme: ${theme})`, { + scope: snapshotScope + }); + }); - await setBoundsToSpanAllActivities(page, examplePlanSmall, plan.url); - await percySnapshot(page, `Plan View (theme: ${theme})`, { - scope: snapshotScope - }); + test('Plan View w/ draft status', async ({ page, theme }) => { + const plan = await createPlanFromJSON(page, { + name: 'Plan Visual Test (Draft)', + json: examplePlanSmall + }); + await page.goto('./#/browse/mine'); + + await setDraftStatusForPlan(page, plan); + + await setBoundsToSpanAllActivities(page, examplePlanSmall, plan.url); + await percySnapshot(page, `Plan View w/ draft status (theme: ${theme})`, { + scope: snapshotScope + }); + }); + + test('Gantt Chart View', async ({ page, theme }) => { + const ganttChart = await createDomainObjectWithDefaults(page, { + type: 'Gantt Chart', + name: 'Gantt Chart Visual Test' + }); + await createPlanFromJSON(page, { + json: examplePlanSmall, + parent: ganttChart.uuid + }); + await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url); + await percySnapshot(page, `Gantt Chart View (theme: ${theme})`, { + scope: snapshotScope + }); + }); + + test('Gantt Chart View w/ draft status', async ({ page, theme }) => { + const ganttChart = await createDomainObjectWithDefaults(page, { + type: 'Gantt Chart', + name: 'Gantt Chart Visual Test (Draft)' + }); + const plan = await createPlanFromJSON(page, { + json: examplePlanSmall, + parent: ganttChart.uuid }); - test('Plan View w/ draft status', async ({ page, theme }) => { - const plan = await createPlanFromJSON(page, { - name: 'Plan Visual Test (Draft)', - json: examplePlanSmall - }); - await page.goto('./#/browse/mine'); + await setDraftStatusForPlan(page, plan); - await setDraftStatusForPlan(page, plan); + await page.goto('./#/browse/mine'); - await setBoundsToSpanAllActivities(page, examplePlanSmall, plan.url); - await percySnapshot(page, `Plan View w/ draft status (theme: ${theme})`, { - scope: snapshotScope - }); - }); - - test('Gantt Chart View', async ({ page, theme }) => { - const ganttChart = await createDomainObjectWithDefaults(page, { - type: 'Gantt Chart', - name: 'Gantt Chart Visual Test' - }); - await createPlanFromJSON(page, { - json: examplePlanSmall, - parent: ganttChart.uuid - }); - await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url); - await percySnapshot(page, `Gantt Chart View (theme: ${theme})`, { - scope: snapshotScope - }); - }); - - test('Gantt Chart View w/ draft status', async ({ page, theme }) => { - const ganttChart = await createDomainObjectWithDefaults(page, { - type: 'Gantt Chart', - name: 'Gantt Chart Visual Test (Draft)' - }); - const plan = await createPlanFromJSON(page, { - json: examplePlanSmall, - parent: ganttChart.uuid - }); - - await setDraftStatusForPlan(page, plan); - - await page.goto('./#/browse/mine'); - - await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url); - await percySnapshot(page, `Gantt Chart View w/ draft status (theme: ${theme})`, { - scope: snapshotScope - }); + await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url); + await percySnapshot(page, `Gantt Chart View w/ draft status (theme: ${theme})`, { + scope: snapshotScope }); + }); }); /** @@ -102,7 +102,7 @@ test.describe('Visual - Planning', () => { * @param {import('../../appActions').CreatedObjectInfo} plan */ async function setDraftStatusForPlan(page, plan) { - await page.evaluate(async (planObject) => { - await window.openmct.status.set(planObject.uuid, 'draft'); - }, plan); + await page.evaluate(async (planObject) => { + await window.openmct.status.set(planObject.uuid, 'draft'); + }, plan); } diff --git a/e2e/tests/visual/search.visual.spec.js b/e2e/tests/visual/search.visual.spec.js index c44f967c31..9d07fe99ca 100644 --- a/e2e/tests/visual/search.visual.spec.js +++ b/e2e/tests/visual/search.visual.spec.js @@ -30,55 +30,62 @@ 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.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 }); - 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'); + // 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="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('[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=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('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"]').click(); - await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl'); + 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}')`); - - }); + 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/eventGenerator/EventMetadataProvider.js b/example/eventGenerator/EventMetadataProvider.js index 1b626537d0..e234062bfd 100644 --- a/example/eventGenerator/EventMetadataProvider.js +++ b/example/eventGenerator/EventMetadataProvider.js @@ -21,44 +21,40 @@ *****************************************************************************/ class EventMetadataProvider { - constructor() { - this.METADATA_BY_TYPE = { - 'eventGenerator': { - values: [ - { - key: "name", - name: "Name", - format: "string" - }, - { - key: "utc", - name: "Time", - format: "utc", - hints: { - domain: 1 - } - }, - { - key: "message", - name: "Message", - format: "string" - } - ] + constructor() { + this.METADATA_BY_TYPE = { + eventGenerator: { + values: [ + { + key: 'name', + name: 'Name', + format: 'string' + }, + { + key: 'utc', + name: 'Time', + format: 'utc', + hints: { + domain: 1 } - }; - } + }, + { + key: 'message', + name: 'Message', + format: 'string' + } + ] + } + }; + } - supportsMetadata(domainObject) { - return Object.prototype.hasOwnProperty.call(this.METADATA_BY_TYPE, domainObject.type); - } + supportsMetadata(domainObject) { + return Object.prototype.hasOwnProperty.call(this.METADATA_BY_TYPE, domainObject.type); + } - getMetadata(domainObject) { - return Object.assign( - {}, - domainObject.telemetry, - this.METADATA_BY_TYPE[domainObject.type] - ); - } + getMetadata(domainObject) { + return Object.assign({}, domainObject.telemetry, this.METADATA_BY_TYPE[domainObject.type]); + } } export default EventMetadataProvider; diff --git a/example/eventGenerator/EventTelemetryProvider.js b/example/eventGenerator/EventTelemetryProvider.js index 38caa7a808..701c32bcb6 100644 --- a/example/eventGenerator/EventTelemetryProvider.js +++ b/example/eventGenerator/EventTelemetryProvider.js @@ -27,70 +27,78 @@ import messages from './transcript.json'; class EventTelemetryProvider { - constructor() { - this.defaultSize = 25; + constructor() { + this.defaultSize = 25; + } + + generateData(firstObservedTime, count, startTime, duration, name) { + const millisecondsSinceStart = startTime - firstObservedTime; + const utc = startTime + count * duration; + const ind = count % messages.length; + const message = messages[ind] + ' - [' + millisecondsSinceStart + ']'; + + return { + name, + utc, + message + }; + } + + supportsRequest(domainObject) { + return domainObject.type === 'eventGenerator'; + } + + supportsSubscribe(domainObject) { + return domainObject.type === 'eventGenerator'; + } + + subscribe(domainObject, callback) { + const duration = domainObject.telemetry.duration * 1000; + const firstObservedTime = Date.now(); + let count = 0; + + const interval = setInterval(() => { + const startTime = Date.now(); + const datum = this.generateData( + firstObservedTime, + count, + startTime, + duration, + domainObject.name + ); + count += 1; + callback(datum); + }, duration); + + return function () { + clearInterval(interval); + }; + } + + request(domainObject, options) { + let start = options.start; + const end = Math.min(Date.now(), options.end); // no future values + const duration = domainObject.telemetry.duration * 1000; + const size = options.size ? options.size : this.defaultSize; + const data = []; + const firstObservedTime = options.start; + let count = 0; + + if (options.strategy === 'latest' || options.size === 1) { + start = end; } - generateData(firstObservedTime, count, startTime, duration, name) { - const millisecondsSinceStart = startTime - firstObservedTime; - const utc = startTime + (count * duration); - const ind = count % messages.length; - const message = messages[ind] + " - [" + millisecondsSinceStart + "]"; - - return { - name, - utc, - message - }; + while (start <= end && data.length < size) { + const startTime = options.start + count; + data.push( + this.generateData(firstObservedTime, count, startTime, duration, domainObject.name) + ); + start += duration; + count += 1; } - supportsRequest(domainObject) { - return domainObject.type === 'eventGenerator'; - } - - supportsSubscribe(domainObject) { - return domainObject.type === 'eventGenerator'; - } - - subscribe(domainObject, callback) { - const duration = domainObject.telemetry.duration * 1000; - const firstObservedTime = Date.now(); - let count = 0; - - const interval = setInterval(() => { - const startTime = Date.now(); - const datum = this.generateData(firstObservedTime, count, startTime, duration, domainObject.name); - count += 1; - callback(datum); - }, duration); - - return function () { - clearInterval(interval); - }; - } - - request(domainObject, options) { - let start = options.start; - const end = Math.min(Date.now(), options.end); // no future values - const duration = domainObject.telemetry.duration * 1000; - const size = options.size ? options.size : this.defaultSize; - const data = []; - const firstObservedTime = options.start; - let count = 0; - - if (options.strategy === 'latest' || options.size === 1) { - start = end; - } - - while (start <= end && data.length < size) { - const startTime = options.start + count; - data.push(this.generateData(firstObservedTime, count, startTime, duration, domainObject.name)); - start += duration; - count += 1; - } - - return Promise.resolve(data); - } + return Promise.resolve(data); + } } export default EventTelemetryProvider; diff --git a/example/eventGenerator/plugin.js b/example/eventGenerator/plugin.js index 8ec89584b1..23552fd63f 100644 --- a/example/eventGenerator/plugin.js +++ b/example/eventGenerator/plugin.js @@ -23,20 +23,20 @@ import EventTelmetryProvider from './EventTelemetryProvider'; import EventMetadataProvider from './EventMetadataProvider'; export default function EventGeneratorPlugin(options) { - return function install(openmct) { - openmct.types.addType("eventGenerator", { - name: "Event Message Generator", - description: "For development use. Creates sample event message data that mimics a live data stream.", - cssClass: "icon-generator-events", - creatable: true, - initialize: function (object) { - object.telemetry = { - duration: 5 - }; - } - }); - openmct.telemetry.addProvider(new EventTelmetryProvider()); - openmct.telemetry.addProvider(new EventMetadataProvider()); - - }; + return function install(openmct) { + openmct.types.addType('eventGenerator', { + name: 'Event Message Generator', + description: + 'For development use. Creates sample event message data that mimics a live data stream.', + cssClass: 'icon-generator-events', + creatable: true, + initialize: function (object) { + object.telemetry = { + duration: 5 + }; + } + }); + openmct.telemetry.addProvider(new EventTelmetryProvider()); + openmct.telemetry.addProvider(new EventMetadataProvider()); + }; } diff --git a/example/eventGenerator/pluginSpec.js b/example/eventGenerator/pluginSpec.js index e3cdb58a7b..f6f1ffeef4 100644 --- a/example/eventGenerator/pluginSpec.js +++ b/example/eventGenerator/pluginSpec.js @@ -20,57 +20,54 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ import EventMessageGeneratorPlugin from './plugin.js'; -import { - createOpenMct, - resetApplicationState -} from '../../src/utils/testing'; +import { createOpenMct, resetApplicationState } from '../../src/utils/testing'; describe('the plugin', () => { - let openmct; - const mockDomainObject = { - identifier: { - namespace: '', - key: 'some-value' - }, - telemetry: { - duration: 0 - }, - options: {}, - type: 'eventGenerator' - }; + let openmct; + const mockDomainObject = { + identifier: { + namespace: '', + key: 'some-value' + }, + telemetry: { + duration: 0 + }, + options: {}, + type: 'eventGenerator' + }; - beforeEach((done) => { - const options = {}; - openmct = createOpenMct(); - openmct.install(new EventMessageGeneratorPlugin(options)); - openmct.on('start', done); - openmct.startHeadless(); + beforeEach((done) => { + const options = {}; + openmct = createOpenMct(); + openmct.install(new EventMessageGeneratorPlugin(options)); + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach(async () => { + await resetApplicationState(openmct); + }); + + describe('the plugin', () => { + it('supports subscription', (done) => { + const unsubscribe = openmct.telemetry.subscribe(mockDomainObject, (telemetry) => { + expect(telemetry).not.toEqual(null); + expect(telemetry.message).toContain('CC: Eagle, Houston'); + expect(unsubscribe).not.toEqual(null); + unsubscribe(); + done(); + }); }); - afterEach(async () => { - await resetApplicationState(openmct); + it('supports requests without start/end defined', async () => { + const telemetry = await openmct.telemetry.request(mockDomainObject); + expect(telemetry[0].message).toContain('CC: Eagle, Houston'); }); - describe('the plugin', () => { - it("supports subscription", (done) => { - const unsubscribe = openmct.telemetry.subscribe(mockDomainObject, (telemetry) => { - expect(telemetry).not.toEqual(null); - expect(telemetry.message).toContain('CC: Eagle, Houston'); - expect(unsubscribe).not.toEqual(null); - unsubscribe(); - done(); - }); - }); - - it("supports requests without start/end defined", async () => { - const telemetry = await openmct.telemetry.request(mockDomainObject); - expect(telemetry[0].message).toContain('CC: Eagle, Houston'); - }); - - it("supports requests with arbitrary start time in the past", async () => { - mockDomainObject.options.start = 100000000000; // Mar 03 1973 - const telemetry = await openmct.telemetry.request(mockDomainObject); - expect(telemetry[0].message).toContain('CC: Eagle, Houston'); - }); + it('supports requests with arbitrary start time in the past', async () => { + mockDomainObject.options.start = 100000000000; // Mar 03 1973 + const telemetry = await openmct.telemetry.request(mockDomainObject); + expect(telemetry[0].message).toContain('CC: Eagle, Houston'); }); + }); }); diff --git a/example/eventGenerator/transcript.json b/example/eventGenerator/transcript.json index ce78735330..bb52ff94ee 100644 --- a/example/eventGenerator/transcript.json +++ b/example/eventGenerator/transcript.json @@ -1,58 +1,58 @@ [ - "CC: Eagle, Houston. You're GO for landing. Over.", - "LMP: Roger. Understand. GO for landing. 3000 feet. PROGRAM ALARM.", - "CC: Copy.", - "LMP: 1201", - "CDR: 1201.", - "CC: Roger. 1201 alarm. We're GO. Same type. We're GO.", - "LMP: 2000 feet. 2000 feet, Into the AGS, 47 degrees.", - "CC: Roger.", - "LMP: 47 degrees.", - "CC: Eagle, looking great. You're GO.", - "CC: Roger. 1202. We copy it.", - "O1: LMP 35 degrees. 35 degrees. 750. Coming down to 23.fl", - "LMP: 700 feet, 21 down, 33 degrees.", - "LMP: 600 feet, down at 19.", - "LMP: 540 feet, down at - 30. Down at 15.", - "LMP: At 400 feet, down at 9.", - "LMP: ...forward.", - "LMP: 350 feet, down at 4.", - "LMP: 30, ... one-half down.", - "LMP: We're pegged on horizontal velocity.", - "LMP: 300 feet, down 3 1/2, 47 forward.", - "LMP: ... up.", - "LMP: On 1 a minute, 1 1/2 down.", - "CDR: 70.", - "LMP: Watch your shadow out there.", - "LMP: 50, down at 2 1/2, 19 forward.", - "LMP: Altitude-velocity light.", - "LMP: 3 1/2 down s 220 feet, 13 forward.", - "LMP: 1t forward. Coming down nicely.", - "LMP: 200 feet, 4 1/2 down.", - "LMP: 5 1/2 down.", - "LMP: 160, 6 - 6 1/2 down.", - "LMP: 5 1/2 down, 9 forward. That's good.", - "LMP: 120 feet.", - "LMP: 100 feet, 3 1/2 down, 9 forward. Five percent.", - "LMP: ...", - "LMP: Okay. 75 feet. There's looking good. Down a half, 6 forward.", - "CC: 60 seconds.", - "LMP: Lights on. ...", - "LMP: Down 2 1/2. Forward. Forward. Good.", - "LMP: 40 feet, down 2 1/2. Kicking up some dust.", - "LMP: 30 feet, 2 1/2 down. Faint shadow.", - "LMP: 4 forward. 4 forward. Drifting to the right a little. Okay. Down a half.", - "CC: 30 seconds.", - "CDR: Forward drift?", - "LMP: Yes.", - "LMP: Okay.", - "LMP: CONTACT LIGHT.", - "LMP: Okay. ENGINE STOP.", - "LMP: ACA - out of DETENT.", - "CDR: Out of DETENT.", - "LMP: MODE CONTROL - both AUTO. DESCENT ENGINE COMMAND OVERRIDE - OFF. ENGINE ARM - OFF.", - "LMP: 413 is in.", - "CC: We copy you down, Eagle.", - "CDR: Houston, Tranquility Base here.", - "CDR: THE EAGLE HAS LANDED." -] \ No newline at end of file + "CC: Eagle, Houston. You're GO for landing. Over.", + "LMP: Roger. Understand. GO for landing. 3000 feet. PROGRAM ALARM.", + "CC: Copy.", + "LMP: 1201", + "CDR: 1201.", + "CC: Roger. 1201 alarm. We're GO. Same type. We're GO.", + "LMP: 2000 feet. 2000 feet, Into the AGS, 47 degrees.", + "CC: Roger.", + "LMP: 47 degrees.", + "CC: Eagle, looking great. You're GO.", + "CC: Roger. 1202. We copy it.", + "O1: LMP 35 degrees. 35 degrees. 750. Coming down to 23.fl", + "LMP: 700 feet, 21 down, 33 degrees.", + "LMP: 600 feet, down at 19.", + "LMP: 540 feet, down at - 30. Down at 15.", + "LMP: At 400 feet, down at 9.", + "LMP: ...forward.", + "LMP: 350 feet, down at 4.", + "LMP: 30, ... one-half down.", + "LMP: We're pegged on horizontal velocity.", + "LMP: 300 feet, down 3 1/2, 47 forward.", + "LMP: ... up.", + "LMP: On 1 a minute, 1 1/2 down.", + "CDR: 70.", + "LMP: Watch your shadow out there.", + "LMP: 50, down at 2 1/2, 19 forward.", + "LMP: Altitude-velocity light.", + "LMP: 3 1/2 down s 220 feet, 13 forward.", + "LMP: 1t forward. Coming down nicely.", + "LMP: 200 feet, 4 1/2 down.", + "LMP: 5 1/2 down.", + "LMP: 160, 6 - 6 1/2 down.", + "LMP: 5 1/2 down, 9 forward. That's good.", + "LMP: 120 feet.", + "LMP: 100 feet, 3 1/2 down, 9 forward. Five percent.", + "LMP: ...", + "LMP: Okay. 75 feet. There's looking good. Down a half, 6 forward.", + "CC: 60 seconds.", + "LMP: Lights on. ...", + "LMP: Down 2 1/2. Forward. Forward. Good.", + "LMP: 40 feet, down 2 1/2. Kicking up some dust.", + "LMP: 30 feet, 2 1/2 down. Faint shadow.", + "LMP: 4 forward. 4 forward. Drifting to the right a little. Okay. Down a half.", + "CC: 30 seconds.", + "CDR: Forward drift?", + "LMP: Yes.", + "LMP: Okay.", + "LMP: CONTACT LIGHT.", + "LMP: Okay. ENGINE STOP.", + "LMP: ACA - out of DETENT.", + "CDR: Out of DETENT.", + "LMP: MODE CONTROL - both AUTO. DESCENT ENGINE COMMAND OVERRIDE - OFF. ENGINE ARM - OFF.", + "LMP: 413 is in.", + "CC: We copy you down, Eagle.", + "CDR: Houston, Tranquility Base here.", + "CDR: THE EAGLE HAS LANDED." +] diff --git a/example/exampleTags/plugin.js b/example/exampleTags/plugin.js index a57633f537..10b72274c0 100644 --- a/example/exampleTags/plugin.js +++ b/example/exampleTags/plugin.js @@ -32,14 +32,14 @@ import availableTags from './tags.json'; * @returns {function} The plugin install function */ export default function exampleTagsPlugin(options) { - return function install(openmct) { - if (options?.namespaceToSaveAnnotations) { - openmct.annotation.setNamespaceToSaveAnnotations(options?.namespaceToSaveAnnotations); - } + return function install(openmct) { + if (options?.namespaceToSaveAnnotations) { + openmct.annotation.setNamespaceToSaveAnnotations(options?.namespaceToSaveAnnotations); + } - Object.keys(availableTags.tags).forEach(tagKey => { - const tagDefinition = availableTags.tags[tagKey]; - openmct.annotation.defineTag(tagKey, tagDefinition); - }); - }; + 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 index 31a1b823a9..093ffa80d3 100644 --- a/example/exampleTags/tags.json +++ b/example/exampleTags/tags.json @@ -1,19 +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" - } + "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 2317ccf829..683ddb1d4b 100644 --- a/example/exampleUser/ExampleUserProvider.js +++ b/example/exampleUser/ExampleUserProvider.js @@ -24,193 +24,199 @@ import EventEmitter from 'EventEmitter'; 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" -}]; +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, {defaultStatusRole} = {defaultStatusRole: undefined}) { - super(); + constructor(openmct, { defaultStatusRole } = { defaultStatusRole: undefined }) { + super(); - this.openmct = openmct; - this.user = undefined; - this.loggedIn = false; - this.autoLoginUser = undefined; - this.status = STATUSES[0]; - this.pollQuestion = undefined; - this.defaultStatusRole = defaultStatusRole; + this.openmct = openmct; + this.user = undefined; + this.loggedIn = false; + this.autoLoginUser = undefined; + this.status = STATUSES[0]; + this.pollQuestion = undefined; + this.defaultStatusRole = defaultStatusRole; - this.ExampleUser = createExampleUser(this.openmct.user.User); - this.loginPromise = undefined; + this.ExampleUser = createExampleUser(this.openmct.user.User); + this.loginPromise = undefined; + } + + isLoggedIn() { + return this.loggedIn; + } + + autoLogin(username) { + this.autoLoginUser = username; + } + + getCurrentUser() { + if (!this.loginPromise) { + this.loginPromise = this._login().then(() => this.user); } - isLoggedIn() { - return this.loggedIn; + return this.loginPromise; + } + + canProvideStatusForRole() { + return Promise.resolve(true); + } + + canSetPollQuestion() { + return Promise.resolve(true); + } + + hasRole(roleId) { + if (!this.loggedIn) { + Promise.resolve(undefined); } - autoLogin(username) { - this.autoLoginUser = username; + 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) { + status.timestamp = Date.now(); + this.status = status; + this.emit('statusChange', { + role, + status + }); + + return true; + } + + // eslint-disable-next-line require-await + async getPollQuestion() { + if (this.pollQuestion) { + return this.pollQuestion; + } else { + return undefined; + } + } + + setPollQuestion(pollQuestion) { + if (!pollQuestion) { + // If the poll question is undefined, set it to a blank string. + // This behavior better reflects how other telemetry systems + // deal with undefined poll questions. + pollQuestion = ''; } - getCurrentUser() { - if (!this.loginPromise) { - this.loginPromise = this._login().then(() => this.user); - } + this.pollQuestion = { + question: pollQuestion, + timestamp: Date.now() + }; + this.emit('pollQuestionChange', this.pollQuestion); - return this.loginPromise; + return true; + } + + getPossibleStatuses() { + return Promise.resolve(STATUSES); + } + + _login() { + const id = uuid(); + + // for testing purposes, this will skip the form, this wouldn't be used in + // a normal authentication process + if (this.autoLoginUser) { + this.user = new this.ExampleUser(id, this.autoLoginUser, ['example-role']); + this.loggedIn = true; + + return Promise.resolve(); } - canProvideStatusForRole() { - return Promise.resolve(true); - } - - canSetPollQuestion() { - return Promise.resolve(true); - } - - hasRole(roleId) { - if (!this.loggedIn) { - Promise.resolve(undefined); - } - - 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) { - status.timestamp = Date.now(); - this.status = status; - this.emit('statusChange', { - role, - status - }); - - return true; - } - - // eslint-disable-next-line require-await - async getPollQuestion() { - if (this.pollQuestion) { - return this.pollQuestion; - } else { - return undefined; - } - } - - setPollQuestion(pollQuestion) { - if (!pollQuestion) { - // If the poll question is undefined, set it to a blank string. - // This behavior better reflects how other telemetry systems - // deal with undefined poll questions. - pollQuestion = ''; - } - - this.pollQuestion = { - question: pollQuestion, - timestamp: Date.now() - }; - this.emit("pollQuestionChange", this.pollQuestion); - - return true; - } - - getPossibleStatuses() { - return Promise.resolve(STATUSES); - } - - _login() { - const id = uuid(); - - // for testing purposes, this will skip the form, this wouldn't be used in - // a normal authentication process - if (this.autoLoginUser) { - this.user = new this.ExampleUser(id, this.autoLoginUser, ['example-role']); - this.loggedIn = true; - - return Promise.resolve(); - } - - const formStructure = { - title: "Login", - sections: [ - { - rows: [ - { - key: "username", - control: "textfield", - name: "Username", - pattern: "\\S+", - required: true, - cssClass: "l-input-lg", - value: '' - } - ] - } - ], - buttons: { - submit: { - label: 'Login' - } + const formStructure = { + title: 'Login', + sections: [ + { + rows: [ + { + key: 'username', + control: 'textfield', + name: 'Username', + pattern: '\\S+', + required: true, + cssClass: 'l-input-lg', + value: '' } - }; + ] + } + ], + buttons: { + submit: { + label: 'Login' + } + } + }; - return this.openmct.forms.showForm(formStructure).then( - (info) => { - this.user = new this.ExampleUser(id, info.username, ['example-role']); - this.loggedIn = true; - }, - () => { // user canceled, setting a default username - this.user = new this.ExampleUser(id, 'Pat', ['example-role']); - this.loggedIn = true; - } - ); - } + return this.openmct.forms.showForm(formStructure).then( + (info) => { + this.user = new this.ExampleUser(id, info.username, ['example-role']); + this.loggedIn = true; + }, + () => { + // user canceled, setting a default username + this.user = new this.ExampleUser(id, 'Pat', ['example-role']); + this.loggedIn = true; + } + ); + } } /** * @typedef {import('@/api/user/StatusUserProvider').default} StatusUserProvider diff --git a/example/exampleUser/exampleUserCreator.js b/example/exampleUser/exampleUserCreator.js index 12e4d7975d..581ea10b7e 100644 --- a/example/exampleUser/exampleUserCreator.js +++ b/example/exampleUser/exampleUserCreator.js @@ -21,16 +21,16 @@ *****************************************************************************/ export default function createExampleUser(UserClass) { - return class ExampleUser extends UserClass { - constructor(id, name, roles) { - super(id, name); + return class ExampleUser extends UserClass { + constructor(id, name, roles) { + super(id, name); - this.roles = roles; - this.getRoles = this.getRoles.bind(this); - } + this.roles = roles; + this.getRoles = this.getRoles.bind(this); + } - getRoles() { - return this.roles; - } - }; + getRoles() { + return this.roles; + } + }; } diff --git a/example/exampleUser/plugin.js b/example/exampleUser/plugin.js index 64471466bb..25c00b7dc0 100644 --- a/example/exampleUser/plugin.js +++ b/example/exampleUser/plugin.js @@ -22,19 +22,21 @@ import ExampleUserProvider from './ExampleUserProvider'; -export default function ExampleUserPlugin({autoLoginUser, defaultStatusRole} = { +export default function ExampleUserPlugin( + { autoLoginUser, defaultStatusRole } = { autoLoginUser: 'guest', defaultStatusRole: 'test-role' -}) { - return function install(openmct) { - const userProvider = new ExampleUserProvider(openmct, { - defaultStatusRole - }); + } +) { + return function install(openmct) { + const userProvider = new ExampleUserProvider(openmct, { + defaultStatusRole + }); - if (autoLoginUser !== undefined) { - userProvider.autoLogin(autoLoginUser); - } + if (autoLoginUser !== undefined) { + userProvider.autoLogin(autoLoginUser); + } - openmct.user.setProvider(userProvider); - }; + openmct.user.setProvider(userProvider); + }; } diff --git a/example/exampleUser/pluginSpec.js b/example/exampleUser/pluginSpec.js index e5b94b4c29..0beddf32b9 100644 --- a/example/exampleUser/pluginSpec.js +++ b/example/exampleUser/pluginSpec.js @@ -20,31 +20,28 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { - createOpenMct, - resetApplicationState -} from '../../src/utils/testing'; +import { createOpenMct, resetApplicationState } from '../../src/utils/testing'; import ExampleUserProvider from './ExampleUserProvider'; -describe("The Example User Plugin", () => { - let openmct; +describe('The Example User Plugin', () => { + let openmct; - beforeEach(() => { - openmct = createOpenMct(); - }); + beforeEach(() => { + openmct = createOpenMct(); + }); - afterEach(() => { - return resetApplicationState(openmct); - }); + afterEach(() => { + return resetApplicationState(openmct); + }); - it('is not installed by default', () => { - expect(openmct.user.hasProvider()).toBeFalse(); - }); + it('is not installed by default', () => { + expect(openmct.user.hasProvider()).toBeFalse(); + }); - it('can be installed', () => { - openmct.user.on('providerAdded', (provider) => { - expect(provider).toBeInstanceOf(ExampleUserProvider); - }); - openmct.install(openmct.plugins.example.ExampleUser()); + it('can be installed', () => { + openmct.user.on('providerAdded', (provider) => { + expect(provider).toBeInstanceOf(ExampleUserProvider); }); + openmct.install(openmct.plugins.example.ExampleUser()); + }); }); diff --git a/example/faultManagement/exampleFaultSource.js b/example/faultManagement/exampleFaultSource.js index 70ac92af4d..20002b5221 100644 --- a/example/faultManagement/exampleFaultSource.js +++ b/example/faultManagement/exampleFaultSource.js @@ -23,40 +23,40 @@ import utils from './utils'; export default function (staticFaults = false) { - return function install(openmct) { - openmct.install(openmct.plugins.FaultManagement()); + return function install(openmct) { + openmct.install(openmct.plugins.FaultManagement()); - const faultsData = utils.randomFaults(staticFaults); + const faultsData = utils.randomFaults(staticFaults); - openmct.faults.addProvider({ - request(domainObject, options) { - return Promise.resolve(faultsData); - }, - subscribe(domainObject, callback) { - callback({ type: 'global-alarm-status' }); + openmct.faults.addProvider({ + request(domainObject, options) { + return Promise.resolve(faultsData); + }, + subscribe(domainObject, callback) { + callback({ type: 'global-alarm-status' }); - return () => {}; - }, - supportsRequest(domainObject) { - return domainObject.type === 'faultManagement'; - }, - supportsSubscribe(domainObject) { - return domainObject.type === 'faultManagement'; - }, - acknowledgeFault(fault, { comment = '' }) { - utils.acknowledgeFault(fault); + 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 - }); - } + 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 index 5cb2db00ca..fa5e698ea8 100644 --- a/example/faultManagement/pluginSpec.js +++ b/example/faultManagement/pluginSpec.js @@ -20,28 +20,25 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { - createOpenMct, - resetApplicationState -} from '../../src/utils/testing'; +import { createOpenMct, resetApplicationState } from '../../src/utils/testing'; -describe("The Example Fault Source Plugin", () => { - let openmct; +describe('The Example Fault Source Plugin', () => { + let openmct; - beforeEach(() => { - openmct = createOpenMct(); - }); + beforeEach(() => { + openmct = createOpenMct(); + }); - afterEach(() => { - return resetApplicationState(openmct); - }); + afterEach(() => { + return resetApplicationState(openmct); + }); - it('is not installed by default', () => { - expect(openmct.faults.provider).toBeUndefined(); - }); + 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(); - }); + 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 index 1287d570b4..4a38cd9e71 100644 --- a/example/faultManagement/utils.js +++ b/example/faultManagement/utils.js @@ -1,76 +1,79 @@ 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; + 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; + 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 - } - } - }; + 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 = { +function shelveFault( + fault, + opts = { shelved: true, comment: '', shelveDuration: 90000 -}) { - fault.shelved = true; + } +) { + fault.shelved = true; - setTimeout(() => { - fault.shelved = false; - }, opts.shelveDuration); + setTimeout(() => { + fault.shelved = false; + }, opts.shelveDuration); } function acknowledgeFault(fault) { - fault.acknowledged = true; + fault.acknowledged = true; } function randomFaults(staticFaults, count = 5) { - let faults = []; + let faults = []; - for (let x = 1, y = count + 1; x < y; x++) { - faults.push(getRandom.fault(x, staticFaults)); - } + for (let x = 1, y = count + 1; x < y; x++) { + faults.push(getRandom.fault(x, staticFaults)); + } - return faults; + return faults; } export default { - randomFaults, - shelveFault, - acknowledgeFault + randomFaults, + shelveFault, + acknowledgeFault }; diff --git a/example/generator/GeneratorMetadataProvider.js b/example/generator/GeneratorMetadataProvider.js index f274d2d53d..fd12a1a7d0 100644 --- a/example/generator/GeneratorMetadataProvider.js +++ b/example/generator/GeneratorMetadataProvider.js @@ -1,150 +1,138 @@ -define([ - 'lodash' -], function ( - _ -) { - - var METADATA_BY_TYPE = { - 'generator': { - values: [ - { - key: "name", - name: "Name", - format: "string" - }, - { - key: "utc", - name: "Time", - format: "utc", - hints: { - domain: 1 - } - }, - { - key: "yesterday", - name: "Yesterday", - format: "utc", - hints: { - domain: 2 - } - }, - { - key: "wavelengths", - name: "Wavelength", - unit: "nm", - format: 'string[]', - hints: { - range: 4 - } - }, - // Need to enable "LocalTimeSystem" plugin to make use of this - // { - // key: "local", - // name: "Time", - // format: "local-format", - // source: "utc", - // hints: { - // domain: 3 - // } - // }, - { - key: "sin", - name: "Sine", - unit: "Hz", - formatString: '%0.2f', - hints: { - range: 1 - } - }, - { - key: "cos", - name: "Cosine", - unit: "deg", - formatString: '%0.2f', - hints: { - range: 2 - } - }, - { - key: "intensities", - name: "Intensities", - format: 'number[]', - hints: { - range: 3 - } - } - ] +define(['lodash'], function (_) { + var METADATA_BY_TYPE = { + generator: { + values: [ + { + key: 'name', + name: 'Name', + format: 'string' }, - 'example.state-generator': { - values: [ - { - key: "name", - name: "Name", - format: "string" - }, - { - key: "utc", - name: "Time", - format: "utc", - hints: { - domain: 1 - } - }, - { - key: "local", - name: "Time", - format: "utc", - source: "utc", - hints: { - domain: 2 - } - }, - { - key: "state", - source: "value", - name: "State", - format: "enum", - enumerations: [ - { - value: 0, - string: "OFF" - }, - { - value: 1, - string: "ON" - } - ], - hints: { - range: 1 - } - }, - { - key: "value", - name: "Value", - hints: { - range: 2 - } - } - ] + { + key: 'utc', + name: 'Time', + format: 'utc', + hints: { + domain: 1 + } + }, + { + key: 'yesterday', + name: 'Yesterday', + format: 'utc', + hints: { + domain: 2 + } + }, + { + key: 'wavelengths', + name: 'Wavelength', + unit: 'nm', + format: 'string[]', + hints: { + range: 4 + } + }, + // Need to enable "LocalTimeSystem" plugin to make use of this + // { + // key: "local", + // name: "Time", + // format: "local-format", + // source: "utc", + // hints: { + // domain: 3 + // } + // }, + { + key: 'sin', + name: 'Sine', + unit: 'Hz', + formatString: '%0.2f', + hints: { + range: 1 + } + }, + { + key: 'cos', + name: 'Cosine', + unit: 'deg', + formatString: '%0.2f', + hints: { + range: 2 + } + }, + { + key: 'intensities', + name: 'Intensities', + format: 'number[]', + hints: { + range: 3 + } } - }; - - function GeneratorMetadataProvider() { - + ] + }, + 'example.state-generator': { + values: [ + { + key: 'name', + name: 'Name', + format: 'string' + }, + { + key: 'utc', + name: 'Time', + format: 'utc', + hints: { + domain: 1 + } + }, + { + key: 'local', + name: 'Time', + format: 'utc', + source: 'utc', + hints: { + domain: 2 + } + }, + { + key: 'state', + source: 'value', + name: 'State', + format: 'enum', + enumerations: [ + { + value: 0, + string: 'OFF' + }, + { + value: 1, + string: 'ON' + } + ], + hints: { + range: 1 + } + }, + { + key: 'value', + name: 'Value', + hints: { + range: 2 + } + } + ] } + }; - GeneratorMetadataProvider.prototype.supportsMetadata = function (domainObject) { - return Object.prototype.hasOwnProperty.call(METADATA_BY_TYPE, domainObject.type); - }; + function GeneratorMetadataProvider() {} - GeneratorMetadataProvider.prototype.getMetadata = function (domainObject) { - return Object.assign( - {}, - domainObject.telemetry, - METADATA_BY_TYPE[domainObject.type] - ); - }; + GeneratorMetadataProvider.prototype.supportsMetadata = function (domainObject) { + return Object.prototype.hasOwnProperty.call(METADATA_BY_TYPE, domainObject.type); + }; - return GeneratorMetadataProvider; + GeneratorMetadataProvider.prototype.getMetadata = function (domainObject) { + return Object.assign({}, domainObject.telemetry, METADATA_BY_TYPE[domainObject.type]); + }; + return GeneratorMetadataProvider; }); diff --git a/example/generator/GeneratorProvider.js b/example/generator/GeneratorProvider.js index 877a29f338..b74915ef12 100644 --- a/example/generator/GeneratorProvider.js +++ b/example/generator/GeneratorProvider.js @@ -20,87 +20,84 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './WorkerInterface' -], function ( - WorkerInterface -) { +define(['./WorkerInterface'], function (WorkerInterface) { + var REQUEST_DEFAULTS = { + amplitude: 1, + period: 10, + offset: 0, + dataRateInHz: 1, + randomness: 0, + phase: 0, + loadDelay: 0, + infinityValues: false + }; - var REQUEST_DEFAULTS = { - amplitude: 1, - period: 10, - offset: 0, - dataRateInHz: 1, - randomness: 0, - phase: 0, - loadDelay: 0, - infinityValues: false - }; + function GeneratorProvider(openmct, StalenessProvider) { + this.openmct = openmct; + this.workerInterface = new WorkerInterface(openmct, StalenessProvider); + } - function GeneratorProvider(openmct, StalenessProvider) { - this.openmct = openmct; - this.workerInterface = new WorkerInterface(openmct, StalenessProvider); - } + GeneratorProvider.prototype.canProvideTelemetry = function (domainObject) { + return domainObject.type === 'generator'; + }; - GeneratorProvider.prototype.canProvideTelemetry = function (domainObject) { - return domainObject.type === 'generator'; - }; + GeneratorProvider.prototype.supportsRequest = GeneratorProvider.prototype.supportsSubscribe = + GeneratorProvider.prototype.canProvideTelemetry; - GeneratorProvider.prototype.supportsRequest = - GeneratorProvider.prototype.supportsSubscribe = - GeneratorProvider.prototype.canProvideTelemetry; + GeneratorProvider.prototype.makeWorkerRequest = function (domainObject, request) { + var props = [ + 'amplitude', + 'period', + 'offset', + 'dataRateInHz', + 'randomness', + 'phase', + 'loadDelay', + 'infinityValues' + ]; - GeneratorProvider.prototype.makeWorkerRequest = function (domainObject, request) { - var props = [ - 'amplitude', - 'period', - 'offset', - 'dataRateInHz', - 'randomness', - 'phase', - 'loadDelay', - 'infinityValues' - ]; + request = request || {}; - request = request || {}; + var workerRequest = {}; - var workerRequest = {}; + props.forEach(function (prop) { + if ( + domainObject.telemetry && + Object.prototype.hasOwnProperty.call(domainObject.telemetry, prop) + ) { + workerRequest[prop] = domainObject.telemetry[prop]; + } - props.forEach(function (prop) { - if (domainObject.telemetry && Object.prototype.hasOwnProperty.call(domainObject.telemetry, prop)) { - workerRequest[prop] = domainObject.telemetry[prop]; - } + if (request && Object.prototype.hasOwnProperty.call(request, prop)) { + workerRequest[prop] = request[prop]; + } - if (request && Object.prototype.hasOwnProperty.call(request, prop)) { - workerRequest[prop] = request[prop]; - } + if (!Object.prototype.hasOwnProperty.call(workerRequest, prop)) { + workerRequest[prop] = REQUEST_DEFAULTS[prop]; + } - if (!Object.prototype.hasOwnProperty.call(workerRequest, prop)) { - workerRequest[prop] = REQUEST_DEFAULTS[prop]; - } + workerRequest[prop] = Number(workerRequest[prop]); + }); - workerRequest[prop] = Number(workerRequest[prop]); - }); + workerRequest.id = this.openmct.objects.makeKeyString(domainObject.identifier); + workerRequest.name = domainObject.name; - workerRequest.id = this.openmct.objects.makeKeyString(domainObject.identifier); - workerRequest.name = domainObject.name; + return workerRequest; + }; - return workerRequest; - }; + GeneratorProvider.prototype.request = function (domainObject, request) { + var workerRequest = this.makeWorkerRequest(domainObject, request); + workerRequest.start = request.start; + workerRequest.end = request.end; - GeneratorProvider.prototype.request = function (domainObject, request) { - var workerRequest = this.makeWorkerRequest(domainObject, request); - workerRequest.start = request.start; - workerRequest.end = request.end; + return this.workerInterface.request(workerRequest); + }; - return this.workerInterface.request(workerRequest); - }; + GeneratorProvider.prototype.subscribe = function (domainObject, callback) { + var workerRequest = this.makeWorkerRequest(domainObject, {}); - GeneratorProvider.prototype.subscribe = function (domainObject, callback) { - var workerRequest = this.makeWorkerRequest(domainObject, {}); + return this.workerInterface.subscribe(workerRequest, callback); + }; - return this.workerInterface.subscribe(workerRequest, callback); - }; - - return GeneratorProvider; + return GeneratorProvider; }); diff --git a/example/generator/SinewaveLimitProvider.js b/example/generator/SinewaveLimitProvider.js index c1bb22d23b..3f71747f8f 100644 --- a/example/generator/SinewaveLimitProvider.js +++ b/example/generator/SinewaveLimitProvider.js @@ -20,155 +20,147 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - -], function ( - -) { - - var PURPLE = { - sin: 2.2, - cos: 2.2 - }, - RED = { - sin: 0.9, - cos: 0.9 - }, - ORANGE = { - sin: 0.7, - cos: 0.7 - }, - YELLOW = { - sin: 0.5, - cos: 0.5 - }, - CYAN = { - sin: 0.45, - cos: 0.45 - }, - LIMITS = { - rh: { - cssClass: "is-limit--upr is-limit--red", - low: RED, - high: Number.POSITIVE_INFINITY, - name: "Red High" - }, - rl: { - cssClass: "is-limit--lwr is-limit--red", - high: -RED, - low: Number.NEGATIVE_INFINITY, - name: "Red Low" - }, - yh: { - cssClass: "is-limit--upr is-limit--yellow", - low: YELLOW, - high: RED, - name: "Yellow High" - }, - yl: { - cssClass: "is-limit--lwr is-limit--yellow", - low: -RED, - high: -YELLOW, - name: "Yellow Low" - } - }; - - function SinewaveLimitProvider() { - - } - - SinewaveLimitProvider.prototype.supportsLimits = function (domainObject) { - return domainObject.type === 'generator'; +define([], function () { + var PURPLE = { + sin: 2.2, + cos: 2.2 + }, + RED = { + sin: 0.9, + cos: 0.9 + }, + ORANGE = { + sin: 0.7, + cos: 0.7 + }, + YELLOW = { + sin: 0.5, + cos: 0.5 + }, + CYAN = { + sin: 0.45, + cos: 0.45 + }, + LIMITS = { + rh: { + cssClass: 'is-limit--upr is-limit--red', + low: RED, + high: Number.POSITIVE_INFINITY, + name: 'Red High' + }, + rl: { + cssClass: 'is-limit--lwr is-limit--red', + high: -RED, + low: Number.NEGATIVE_INFINITY, + name: 'Red Low' + }, + yh: { + cssClass: 'is-limit--upr is-limit--yellow', + low: YELLOW, + high: RED, + name: 'Yellow High' + }, + yl: { + cssClass: 'is-limit--lwr is-limit--yellow', + low: -RED, + high: -YELLOW, + name: 'Yellow Low' + } }; - SinewaveLimitProvider.prototype.getLimitEvaluator = function (domainObject) { - return { - evaluate: function (datum, valueMetadata) { - var range = valueMetadata && valueMetadata.key; + function SinewaveLimitProvider() {} - if (datum[range] > RED[range]) { - return LIMITS.rh; - } + SinewaveLimitProvider.prototype.supportsLimits = function (domainObject) { + return domainObject.type === 'generator'; + }; - if (datum[range] < -RED[range]) { - return LIMITS.rl; - } + SinewaveLimitProvider.prototype.getLimitEvaluator = function (domainObject) { + return { + evaluate: function (datum, valueMetadata) { + var range = valueMetadata && valueMetadata.key; - if (datum[range] > YELLOW[range]) { - return LIMITS.yh; - } + if (datum[range] > RED[range]) { + return LIMITS.rh; + } - if (datum[range] < -YELLOW[range]) { - return LIMITS.yl; - } - } - }; + if (datum[range] < -RED[range]) { + return LIMITS.rl; + } + + if (datum[range] > YELLOW[range]) { + return LIMITS.yh; + } + + if (datum[range] < -YELLOW[range]) { + return LIMITS.yl; + } + } }; + }; - SinewaveLimitProvider.prototype.getLimits = function (domainObject) { - - return { - limits: function () { - return Promise.resolve({ - WATCH: { - low: { - color: "cyan", - sin: -CYAN.sin, - cos: -CYAN.cos - }, - high: { - color: "cyan", - ...CYAN - } - }, - WARNING: { - low: { - color: "yellow", - sin: -YELLOW.sin, - cos: -YELLOW.cos - }, - high: { - color: "yellow", - ...YELLOW - } - }, - DISTRESS: { - low: { - color: "orange", - sin: -ORANGE.sin, - cos: -ORANGE.cos - }, - high: { - color: "orange", - ...ORANGE - } - }, - CRITICAL: { - low: { - color: "red", - sin: -RED.sin, - cos: -RED.cos - }, - high: { - color: "red", - ...RED - } - }, - SEVERE: { - low: { - color: "purple", - sin: -PURPLE.sin, - cos: -PURPLE.cos - }, - high: { - color: "purple", - ...PURPLE - } - } - }); + SinewaveLimitProvider.prototype.getLimits = function (domainObject) { + return { + limits: function () { + return Promise.resolve({ + WATCH: { + low: { + color: 'cyan', + sin: -CYAN.sin, + cos: -CYAN.cos + }, + high: { + color: 'cyan', + ...CYAN } - }; + }, + WARNING: { + low: { + color: 'yellow', + sin: -YELLOW.sin, + cos: -YELLOW.cos + }, + high: { + color: 'yellow', + ...YELLOW + } + }, + DISTRESS: { + low: { + color: 'orange', + sin: -ORANGE.sin, + cos: -ORANGE.cos + }, + high: { + color: 'orange', + ...ORANGE + } + }, + CRITICAL: { + low: { + color: 'red', + sin: -RED.sin, + cos: -RED.cos + }, + high: { + color: 'red', + ...RED + } + }, + SEVERE: { + low: { + color: 'purple', + sin: -PURPLE.sin, + cos: -PURPLE.cos + }, + high: { + color: 'purple', + ...PURPLE + } + } + }); + } }; + }; - return SinewaveLimitProvider; + return SinewaveLimitProvider; }); diff --git a/example/generator/SinewaveStalenessProvider.js b/example/generator/SinewaveStalenessProvider.js index 4bf1f0dc7b..9eda006fe1 100644 --- a/example/generator/SinewaveStalenessProvider.js +++ b/example/generator/SinewaveStalenessProvider.js @@ -23,135 +23,135 @@ import EventEmitter from 'EventEmitter'; export default class SinewaveLimitProvider extends EventEmitter { - #openmct; - #observingStaleness; - #watchingTheClock; - #isRealTime; + #openmct; + #observingStaleness; + #watchingTheClock; + #isRealTime; - constructor(openmct) { - super(); + constructor(openmct) { + super(); - this.#openmct = openmct; - this.#observingStaleness = {}; - this.#watchingTheClock = false; - this.#isRealTime = undefined; + this.#openmct = openmct; + this.#observingStaleness = {}; + this.#watchingTheClock = false; + this.#isRealTime = undefined; + } + + supportsStaleness(domainObject) { + return domainObject.type === 'generator'; + } + + isStale(domainObject, options) { + if (!this.#providingStaleness(domainObject)) { + return; } - supportsStaleness(domainObject) { - return domainObject.type === 'generator'; + const id = this.#getObjectKeyString(domainObject); + + if (!this.#observerExists(id)) { + this.#createObserver(id); } - isStale(domainObject, options) { - if (!this.#providingStaleness(domainObject)) { - return; - } + return Promise.resolve({ + isStale: this.#observingStaleness[id].isStale, + utc: Date.now() + }); + } - const id = this.#getObjectKeyString(domainObject); + subscribeToStaleness(domainObject, callback) { + const id = this.#getObjectKeyString(domainObject); - if (!this.#observerExists(id)) { - this.#createObserver(id); - } - - return Promise.resolve({ - isStale: this.#observingStaleness[id].isStale, - utc: Date.now() - }); + if (this.#isRealTime === undefined) { + this.#updateRealTime(this.#openmct.time.clock()); } - subscribeToStaleness(domainObject, callback) { - const id = this.#getObjectKeyString(domainObject); + this.#handleClockUpdate(); - if (this.#isRealTime === undefined) { - this.#updateRealTime(this.#openmct.time.clock()); - } - - this.#handleClockUpdate(); - - if (this.#observerExists(id)) { - this.#addCallbackToObserver(id, callback); - } else { - this.#createObserver(id, callback); - } - - const intervalId = setInterval(() => { - if (this.#providingStaleness(domainObject)) { - this.#updateStaleness(id, !this.#observingStaleness[id].isStale); - } - }, 10000); - - return () => { - clearInterval(intervalId); - this.#updateStaleness(id, false); - this.#handleClockUpdate(); - this.#destroyObserver(id); - }; + if (this.#observerExists(id)) { + this.#addCallbackToObserver(id, callback); + } else { + this.#createObserver(id, callback); } - #handleClockUpdate() { - let observers = Object.values(this.#observingStaleness).length > 0; + const intervalId = setInterval(() => { + if (this.#providingStaleness(domainObject)) { + this.#updateStaleness(id, !this.#observingStaleness[id].isStale); + } + }, 10000); - if (observers && !this.#watchingTheClock) { - this.#watchingTheClock = true; - this.#openmct.time.on('clock', this.#updateRealTime, this); - } else if (!observers && this.#watchingTheClock) { - this.#watchingTheClock = false; - this.#openmct.time.off('clock', this.#updateRealTime, this); - } + return () => { + clearInterval(intervalId); + this.#updateStaleness(id, false); + this.#handleClockUpdate(); + this.#destroyObserver(id); + }; + } + + #handleClockUpdate() { + let observers = Object.values(this.#observingStaleness).length > 0; + + if (observers && !this.#watchingTheClock) { + this.#watchingTheClock = true; + this.#openmct.time.on('clock', this.#updateRealTime, this); + } else if (!observers && this.#watchingTheClock) { + this.#watchingTheClock = false; + this.#openmct.time.off('clock', this.#updateRealTime, this); } + } - #updateRealTime(clock) { - this.#isRealTime = clock !== undefined; + #updateRealTime(clock) { + this.#isRealTime = clock !== undefined; - if (!this.#isRealTime) { - Object.keys(this.#observingStaleness).forEach((id) => { - this.#updateStaleness(id, false); - }); - } + if (!this.#isRealTime) { + Object.keys(this.#observingStaleness).forEach((id) => { + this.#updateStaleness(id, false); + }); } + } - #updateStaleness(id, isStale) { - this.#observingStaleness[id].isStale = isStale; - this.#observingStaleness[id].utc = Date.now(); - this.#observingStaleness[id].callback({ - isStale: this.#observingStaleness[id].isStale, - utc: this.#observingStaleness[id].utc - }); - this.emit('stalenessEvent', { - id, - isStale: this.#observingStaleness[id].isStale - }); + #updateStaleness(id, isStale) { + this.#observingStaleness[id].isStale = isStale; + this.#observingStaleness[id].utc = Date.now(); + this.#observingStaleness[id].callback({ + isStale: this.#observingStaleness[id].isStale, + utc: this.#observingStaleness[id].utc + }); + this.emit('stalenessEvent', { + id, + isStale: this.#observingStaleness[id].isStale + }); + } + + #createObserver(id, callback) { + this.#observingStaleness[id] = { + isStale: false, + utc: Date.now() + }; + + if (typeof callback === 'function') { + this.#addCallbackToObserver(id, callback); } + } - #createObserver(id, callback) { - this.#observingStaleness[id] = { - isStale: false, - utc: Date.now() - }; - - if (typeof callback === 'function') { - this.#addCallbackToObserver(id, callback); - } + #destroyObserver(id) { + if (this.#observingStaleness[id]) { + delete this.#observingStaleness[id]; } + } - #destroyObserver(id) { - if (this.#observingStaleness[id]) { - delete this.#observingStaleness[id]; - } - } + #providingStaleness(domainObject) { + return domainObject.telemetry?.staleness === true && this.#isRealTime; + } - #providingStaleness(domainObject) { - return domainObject.telemetry?.staleness === true && this.#isRealTime; - } + #getObjectKeyString(object) { + return this.#openmct.objects.makeKeyString(object.identifier); + } - #getObjectKeyString(object) { - return this.#openmct.objects.makeKeyString(object.identifier); - } + #addCallbackToObserver(id, callback) { + this.#observingStaleness[id].callback = callback; + } - #addCallbackToObserver(id, callback) { - this.#observingStaleness[id].callback = callback; - } - - #observerExists(id) { - return this.#observingStaleness?.[id]; - } + #observerExists(id) { + return this.#observingStaleness?.[id]; + } } diff --git a/example/generator/StateGeneratorProvider.js b/example/generator/StateGeneratorProvider.js index 3c1bd94380..c840e6bb99 100644 --- a/example/generator/StateGeneratorProvider.js +++ b/example/generator/StateGeneratorProvider.js @@ -20,64 +20,56 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ +define([], function () { + function StateGeneratorProvider() {} -], function ( + function pointForTimestamp(timestamp, duration, name) { + return { + name: name, + utc: Math.floor(timestamp / duration) * duration, + value: Math.floor(timestamp / duration) % 2 + }; + } -) { + StateGeneratorProvider.prototype.supportsSubscribe = function (domainObject) { + return domainObject.type === 'example.state-generator'; + }; - function StateGeneratorProvider() { + StateGeneratorProvider.prototype.subscribe = function (domainObject, callback) { + var duration = domainObject.telemetry.duration * 1000; + var interval = setInterval(function () { + var now = Date.now(); + var datum = pointForTimestamp(now, duration, domainObject.name); + datum.value = String(datum.value); + callback(datum); + }, duration); + + return function () { + clearInterval(interval); + }; + }; + + StateGeneratorProvider.prototype.supportsRequest = function (domainObject, options) { + return domainObject.type === 'example.state-generator'; + }; + + StateGeneratorProvider.prototype.request = function (domainObject, options) { + var start = options.start; + var end = Math.min(Date.now(), options.end); // no future values + var duration = domainObject.telemetry.duration * 1000; + if (options.strategy === 'latest' || options.size === 1) { + start = end; } - function pointForTimestamp(timestamp, duration, name) { - return { - name: name, - utc: Math.floor(timestamp / duration) * duration, - value: Math.floor(timestamp / duration) % 2 - }; + var data = []; + while (start <= end && data.length < 5000) { + data.push(pointForTimestamp(start, duration, domainObject.name)); + start += duration; } - StateGeneratorProvider.prototype.supportsSubscribe = function (domainObject) { - return domainObject.type === 'example.state-generator'; - }; - - StateGeneratorProvider.prototype.subscribe = function (domainObject, callback) { - var duration = domainObject.telemetry.duration * 1000; - - var interval = setInterval(function () { - var now = Date.now(); - var datum = pointForTimestamp(now, duration, domainObject.name); - datum.value = String(datum.value); - callback(datum); - }, duration); - - return function () { - clearInterval(interval); - }; - }; - - StateGeneratorProvider.prototype.supportsRequest = function (domainObject, options) { - return domainObject.type === 'example.state-generator'; - }; - - StateGeneratorProvider.prototype.request = function (domainObject, options) { - var start = options.start; - var end = Math.min(Date.now(), options.end); // no future values - var duration = domainObject.telemetry.duration * 1000; - if (options.strategy === 'latest' || options.size === 1) { - start = end; - } - - var data = []; - while (start <= end && data.length < 5000) { - data.push(pointForTimestamp(start, duration, domainObject.name)); - start += duration; - } - - return Promise.resolve(data); - }; - - return StateGeneratorProvider; + return Promise.resolve(data); + }; + return StateGeneratorProvider; }); diff --git a/example/generator/WorkerInterface.js b/example/generator/WorkerInterface.js index 7c8421813e..996add4f7f 100644 --- a/example/generator/WorkerInterface.js +++ b/example/generator/WorkerInterface.js @@ -20,93 +20,88 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - 'uuid' -], function ( - { v4: uuid } -) { - function WorkerInterface(openmct, StalenessProvider) { - // eslint-disable-next-line no-undef - const workerUrl = `${openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}generatorWorker.js`; - this.StalenessProvider = StalenessProvider; - this.worker = new Worker(workerUrl); - this.worker.onmessage = this.onMessage.bind(this); - this.callbacks = {}; - this.staleTelemetryIds = {}; +define(['uuid'], function ({ v4: uuid }) { + function WorkerInterface(openmct, StalenessProvider) { + // eslint-disable-next-line no-undef + const workerUrl = `${openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}generatorWorker.js`; + this.StalenessProvider = StalenessProvider; + this.worker = new Worker(workerUrl); + this.worker.onmessage = this.onMessage.bind(this); + this.callbacks = {}; + this.staleTelemetryIds = {}; - this.watchStaleness(); + this.watchStaleness(); + } + + WorkerInterface.prototype.watchStaleness = function () { + this.StalenessProvider.on('stalenessEvent', ({ id, isStale }) => { + this.staleTelemetryIds[id] = isStale; + }); + }; + + WorkerInterface.prototype.onMessage = function (message) { + message = message.data; + var callback = this.callbacks[message.id]; + if (callback) { + callback(message); + } + }; + + WorkerInterface.prototype.dispatch = function (request, data, callback) { + var message = { + request: request, + data: data, + id: uuid() + }; + + if (callback) { + this.callbacks[message.id] = callback; } - WorkerInterface.prototype.watchStaleness = function () { - this.StalenessProvider.on('stalenessEvent', ({ id, isStale}) => { - this.staleTelemetryIds[id] = isStale; - }); - }; + this.worker.postMessage(message); - WorkerInterface.prototype.onMessage = function (message) { - message = message.data; - var callback = this.callbacks[message.id]; - if (callback) { - callback(message); - } - }; + return message.id; + }; - WorkerInterface.prototype.dispatch = function (request, data, callback) { - var message = { - request: request, - data: data, - id: uuid() - }; + WorkerInterface.prototype.request = function (request) { + var deferred = {}; + var promise = new Promise(function (resolve, reject) { + deferred.resolve = resolve; + deferred.reject = reject; + }); + var messageId; - if (callback) { - this.callbacks[message.id] = callback; - } + let self = this; + function callback(message) { + if (message.error) { + deferred.reject(message.error); + } else { + deferred.resolve(message.data); + } - this.worker.postMessage(message); + delete self.callbacks[messageId]; + } - return message.id; - }; + messageId = this.dispatch('request', request, callback.bind(this)); - WorkerInterface.prototype.request = function (request) { - var deferred = {}; - var promise = new Promise(function (resolve, reject) { - deferred.resolve = resolve; - deferred.reject = reject; - }); - var messageId; + return promise; + }; - let self = this; - function callback(message) { - if (message.error) { - deferred.reject(message.error); - } else { - deferred.resolve(message.data); - } + WorkerInterface.prototype.subscribe = function (request, cb) { + const id = request.id; + const messageId = this.dispatch('subscribe', request, (message) => { + if (!this.staleTelemetryIds[id]) { + cb(message.data); + } + }); - delete self.callbacks[messageId]; + return function () { + this.dispatch('unsubscribe', { + id: messageId + }); + delete this.callbacks[messageId]; + }.bind(this); + }; - } - - messageId = this.dispatch('request', request, callback.bind(this)); - - return promise; - }; - - WorkerInterface.prototype.subscribe = function (request, cb) { - const id = request.id; - const messageId = this.dispatch('subscribe', request, (message) => { - if (!this.staleTelemetryIds[id]) { - cb(message.data); - } - }); - - return function () { - this.dispatch('unsubscribe', { - id: messageId - }); - delete this.callbacks[messageId]; - }.bind(this); - }; - - return WorkerInterface; + return WorkerInterface; }); diff --git a/example/generator/generatorWorker.js b/example/generator/generatorWorker.js index 21a44e015a..6a83fe2ba8 100644 --- a/example/generator/generatorWorker.js +++ b/example/generator/generatorWorker.js @@ -21,204 +21,229 @@ *****************************************************************************/ (function () { + var FIFTEEN_MINUTES = 15 * 60 * 1000; - var FIFTEEN_MINUTES = 15 * 60 * 1000; + var handlers = { + subscribe: onSubscribe, + unsubscribe: onUnsubscribe, + request: onRequest + }; - var handlers = { - subscribe: onSubscribe, - unsubscribe: onUnsubscribe, - request: onRequest - }; + var subscriptions = {}; - var subscriptions = {}; - - function workSubscriptions(timestamp) { - var now = Date.now(); - var nextWork = Math.min.apply(Math, Object.values(subscriptions).map(function (subscription) { - return subscription(now); - })); - var wait = nextWork - now; - if (wait < 0) { - wait = 0; - } - - if (Number.isFinite(wait)) { - setTimeout(workSubscriptions, wait); - } + function workSubscriptions(timestamp) { + var now = Date.now(); + var nextWork = Math.min.apply( + Math, + Object.values(subscriptions).map(function (subscription) { + return subscription(now); + }) + ); + var wait = nextWork - now; + if (wait < 0) { + wait = 0; } - function onSubscribe(message) { - var data = message.data; - - // Keep - var start = Date.now(); - var step = 1000 / data.dataRateInHz; - var nextStep = start - (start % step) + step; - let work; - if (data.spectra) { - work = function (now) { - while (nextStep < now) { - const messageCopy = Object.create(message); - message.data.start = nextStep - (60 * 1000); - message.data.end = nextStep; - onRequest(messageCopy); - nextStep += step; - } - - return nextStep; - }; - } else { - work = function (now) { - while (nextStep < now) { - self.postMessage({ - id: message.id, - data: { - name: data.name, - utc: nextStep, - yesterday: nextStep - 60 * 60 * 24 * 1000, - sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness, data.infinityValues), - wavelengths: wavelengths(), - intensities: intensities(), - cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness, data.infinityValues) - } - }); - nextStep += step; - } - - return nextStep; - }; - } - - subscriptions[message.id] = work; - workSubscriptions(); + if (Number.isFinite(wait)) { + setTimeout(workSubscriptions, wait); } + } - function onUnsubscribe(message) { - delete subscriptions[message.data.id]; - } + function onSubscribe(message) { + var data = message.data; - function onRequest(message) { - var request = message.data; - if (request.end === undefined) { - request.end = Date.now(); + // Keep + var start = Date.now(); + var step = 1000 / data.dataRateInHz; + var nextStep = start - (start % step) + step; + let work; + if (data.spectra) { + work = function (now) { + while (nextStep < now) { + const messageCopy = Object.create(message); + message.data.start = nextStep - 60 * 1000; + message.data.end = nextStep; + onRequest(messageCopy); + nextStep += step; } - if (request.start === undefined) { - request.start = request.end - FIFTEEN_MINUTES; - } - - var now = Date.now(); - var start = request.start; - var end = request.end > now ? now : request.end; - var amplitude = request.amplitude; - var period = request.period; - var offset = request.offset; - var dataRateInHz = request.dataRateInHz; - var phase = request.phase; - var randomness = request.randomness; - var loadDelay = Math.max(request.loadDelay, 0); - var infinityValues = request.infinityValues; - - var step = 1000 / dataRateInHz; - var nextStep = start - (start % step) + step; - - var data = []; - - for (; nextStep < end && data.length < 5000; nextStep += step) { - data.push({ - utc: nextStep, - yesterday: nextStep - 60 * 60 * 24 * 1000, - sin: sin(nextStep, period, amplitude, offset, phase, randomness, infinityValues), - wavelengths: wavelengths(), - intensities: intensities(), - cos: cos(nextStep, period, amplitude, offset, phase, randomness, infinityValues) - }); - } - - if (loadDelay === 0) { - postOnRequest(message, request, data); - } else { - setTimeout(() => postOnRequest(message, request, data), loadDelay); - } - } - - function postOnRequest(message, request, data) { - self.postMessage({ + return nextStep; + }; + } else { + work = function (now) { + while (nextStep < now) { + self.postMessage({ id: message.id, - data: request.spectra ? { - wavelength: data.map((item) => { - return item.wavelength; - }), - cos: data.map((item) => { - return item.cos; - }) - } : data - }); - } - - function cos(timestamp, period, amplitude, offset, phase, randomness, infinityValues) { - if (infinityValues && Math.random() > 0.5) { - return Number.POSITIVE_INFINITY; - } - - return amplitude - * Math.cos(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset; - } - - function sin(timestamp, period, amplitude, offset, phase, randomness, infinityValues) { - if (infinityValues && Math.random() > 0.5) { - return Number.POSITIVE_INFINITY; - } - - return amplitude - * Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset; - } - - function wavelengths() { - let values = []; - while (values.length < 5) { - const randomValue = Math.random() * 100; - if (!values.includes(randomValue)) { - values.push(String(randomValue)); + data: { + name: data.name, + utc: nextStep, + yesterday: nextStep - 60 * 60 * 24 * 1000, + sin: sin( + nextStep, + data.period, + data.amplitude, + data.offset, + data.phase, + data.randomness, + data.infinityValues + ), + wavelengths: wavelengths(), + intensities: intensities(), + cos: cos( + nextStep, + data.period, + data.amplitude, + data.offset, + data.phase, + data.randomness, + data.infinityValues + ) } + }); + nextStep += step; } - return values; + return nextStep; + }; } - function intensities() { - let values = []; - while (values.length < 5) { - const randomValue = Math.random() * 10; - if (!values.includes(randomValue)) { - values.push(String(randomValue)); - } - } + subscriptions[message.id] = work; + workSubscriptions(); + } - return values; + function onUnsubscribe(message) { + delete subscriptions[message.data.id]; + } + + function onRequest(message) { + var request = message.data; + if (request.end === undefined) { + request.end = Date.now(); } - function sendError(error, message) { - self.postMessage({ - error: error.name + ': ' + error.message, - message: message, - id: message.id - }); + if (request.start === undefined) { + request.start = request.end - FIFTEEN_MINUTES; } - self.onmessage = function handleMessage(event) { - var message = event.data; - var handler = handlers[message.request]; + var now = Date.now(); + var start = request.start; + var end = request.end > now ? now : request.end; + var amplitude = request.amplitude; + var period = request.period; + var offset = request.offset; + var dataRateInHz = request.dataRateInHz; + var phase = request.phase; + var randomness = request.randomness; + var loadDelay = Math.max(request.loadDelay, 0); + var infinityValues = request.infinityValues; - if (!handler) { - sendError(new Error('unknown message type'), message); - } else { - try { - handler(message); - } catch (e) { - sendError(e, message); - } - } - }; + var step = 1000 / dataRateInHz; + var nextStep = start - (start % step) + step; -}()); + var data = []; + + for (; nextStep < end && data.length < 5000; nextStep += step) { + data.push({ + utc: nextStep, + yesterday: nextStep - 60 * 60 * 24 * 1000, + sin: sin(nextStep, period, amplitude, offset, phase, randomness, infinityValues), + wavelengths: wavelengths(), + intensities: intensities(), + cos: cos(nextStep, period, amplitude, offset, phase, randomness, infinityValues) + }); + } + + 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 + ? { + wavelength: data.map((item) => { + return item.wavelength; + }), + cos: data.map((item) => { + return item.cos; + }) + } + : data + }); + } + + function cos(timestamp, period, amplitude, offset, phase, randomness, infinityValues) { + if (infinityValues && Math.random() > 0.5) { + return Number.POSITIVE_INFINITY; + } + + return ( + amplitude * Math.cos(phase + (timestamp / period / 1000) * Math.PI * 2) + + amplitude * Math.random() * randomness + + offset + ); + } + + function sin(timestamp, period, amplitude, offset, phase, randomness, infinityValues) { + if (infinityValues && Math.random() > 0.5) { + return Number.POSITIVE_INFINITY; + } + + return ( + amplitude * Math.sin(phase + (timestamp / period / 1000) * Math.PI * 2) + + amplitude * Math.random() * randomness + + offset + ); + } + + 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) { + self.postMessage({ + error: error.name + ': ' + error.message, + message: message, + id: message.id + }); + } + + self.onmessage = function handleMessage(event) { + var message = event.data; + var handler = handlers[message.request]; + + if (!handler) { + sendError(new Error('unknown message type'), message); + } else { + try { + handler(message); + } catch (e) { + sendError(e, message); + } + } + }; +})(); diff --git a/example/generator/plugin.js b/example/generator/plugin.js index 6f70e55bbf..97d2371c07 100644 --- a/example/generator/plugin.js +++ b/example/generator/plugin.js @@ -20,163 +20,134 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import GeneratorProvider from "./GeneratorProvider"; -import SinewaveLimitProvider from "./SinewaveLimitProvider"; -import SinewaveStalenessProvider from "./SinewaveStalenessProvider"; -import StateGeneratorProvider from "./StateGeneratorProvider"; -import GeneratorMetadataProvider from "./GeneratorMetadataProvider"; +import GeneratorProvider from './GeneratorProvider'; +import SinewaveLimitProvider from './SinewaveLimitProvider'; +import SinewaveStalenessProvider from './SinewaveStalenessProvider'; +import StateGeneratorProvider from './StateGeneratorProvider'; +import GeneratorMetadataProvider from './GeneratorMetadataProvider'; export default function (openmct) { + openmct.types.addType('example.state-generator', { + name: 'State Generator', + description: + 'For development use. Generates example enumerated telemetry by cycling through a given set of states.', + cssClass: 'icon-generator-telemetry', + creatable: true, + form: [ + { + name: 'State Duration (seconds)', + control: 'numberfield', + cssClass: 'l-input-sm l-numeric', + key: 'duration', + required: true, + property: ['telemetry', 'duration'] + } + ], + initialize: function (object) { + object.telemetry = { + duration: 5 + }; + } + }); - openmct.types.addType("example.state-generator", { - name: "State Generator", - description: "For development use. Generates example enumerated telemetry by cycling through a given set of states.", - cssClass: "icon-generator-telemetry", - creatable: true, - form: [ - { - name: "State Duration (seconds)", - control: "numberfield", - cssClass: "l-input-sm l-numeric", - key: "duration", - required: true, - property: [ - "telemetry", - "duration" - ] - } - ], - initialize: function (object) { - object.telemetry = { - duration: 5 - }; - } - }); + openmct.telemetry.addProvider(new StateGeneratorProvider()); - openmct.telemetry.addProvider(new StateGeneratorProvider()); + openmct.types.addType('generator', { + name: 'Sine Wave Generator', + description: + 'For development use. Generates example streaming telemetry data using a simple sine wave algorithm.', + cssClass: 'icon-generator-telemetry', + creatable: true, + form: [ + { + name: 'Period', + control: 'numberfield', + cssClass: 'l-input-sm l-numeric', + key: 'period', + required: true, + property: ['telemetry', 'period'] + }, + { + name: 'Amplitude', + control: 'numberfield', + cssClass: 'l-numeric', + key: 'amplitude', + required: true, + property: ['telemetry', 'amplitude'] + }, + { + name: 'Offset', + control: 'numberfield', + cssClass: 'l-numeric', + key: 'offset', + required: true, + property: ['telemetry', 'offset'] + }, + { + name: 'Data Rate (hz)', + control: 'numberfield', + cssClass: 'l-input-sm l-numeric', + key: 'dataRateInHz', + required: true, + property: ['telemetry', 'dataRateInHz'] + }, + { + name: 'Phase (radians)', + control: 'numberfield', + cssClass: 'l-input-sm l-numeric', + key: 'phase', + required: true, + property: ['telemetry', 'phase'] + }, + { + name: 'Randomness', + control: 'numberfield', + cssClass: 'l-input-sm l-numeric', + key: 'randomness', + required: true, + property: ['telemetry', 'randomness'] + }, + { + name: 'Loading Delay (ms)', + control: 'numberfield', + cssClass: 'l-input-sm l-numeric', + key: 'loadDelay', + required: true, + property: ['telemetry', 'loadDelay'] + }, + { + name: 'Include Infinity Values', + control: 'toggleSwitch', + cssClass: 'l-input', + key: 'infinityValues', + property: ['telemetry', 'infinityValues'] + }, + { + name: 'Provide Staleness Updates', + control: 'toggleSwitch', + cssClass: 'l-input', + key: 'staleness', + property: ['telemetry', 'staleness'] + } + ], + initialize: function (object) { + object.telemetry = { + period: 10, + amplitude: 1, + offset: 0, + dataRateInHz: 1, + phase: 0, + randomness: 0, + loadDelay: 0, + infinityValues: false, + staleness: false + }; + } + }); + const stalenessProvider = new SinewaveStalenessProvider(openmct); - openmct.types.addType("generator", { - name: "Sine Wave Generator", - description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.", - cssClass: "icon-generator-telemetry", - creatable: true, - form: [ - { - name: "Period", - control: "numberfield", - cssClass: "l-input-sm l-numeric", - key: "period", - required: true, - property: [ - "telemetry", - "period" - ] - }, - { - name: "Amplitude", - control: "numberfield", - cssClass: "l-numeric", - key: "amplitude", - required: true, - property: [ - "telemetry", - "amplitude" - ] - }, - { - name: "Offset", - control: "numberfield", - cssClass: "l-numeric", - key: "offset", - required: true, - property: [ - "telemetry", - "offset" - ] - }, - { - name: "Data Rate (hz)", - control: "numberfield", - cssClass: "l-input-sm l-numeric", - key: "dataRateInHz", - required: true, - property: [ - "telemetry", - "dataRateInHz" - ] - }, - { - name: "Phase (radians)", - control: "numberfield", - cssClass: "l-input-sm l-numeric", - key: "phase", - required: true, - property: [ - "telemetry", - "phase" - ] - }, - { - name: "Randomness", - control: "numberfield", - cssClass: "l-input-sm l-numeric", - key: "randomness", - required: true, - property: [ - "telemetry", - "randomness" - ] - }, - { - name: "Loading Delay (ms)", - control: "numberfield", - cssClass: "l-input-sm l-numeric", - key: "loadDelay", - required: true, - property: [ - "telemetry", - "loadDelay" - ] - }, - { - name: "Include Infinity Values", - control: "toggleSwitch", - cssClass: "l-input", - key: "infinityValues", - property: [ - "telemetry", - "infinityValues" - ] - }, - { - name: "Provide Staleness Updates", - control: "toggleSwitch", - cssClass: "l-input", - key: "staleness", - property: [ - "telemetry", - "staleness" - ] - } - ], - initialize: function (object) { - object.telemetry = { - period: 10, - amplitude: 1, - offset: 0, - dataRateInHz: 1, - phase: 0, - randomness: 0, - loadDelay: 0, - infinityValues: false, - staleness: false - }; - } - }); - const stalenessProvider = new SinewaveStalenessProvider(openmct); - - openmct.telemetry.addProvider(new GeneratorProvider(openmct, stalenessProvider)); - openmct.telemetry.addProvider(new GeneratorMetadataProvider()); - openmct.telemetry.addProvider(new SinewaveLimitProvider()); - openmct.telemetry.addProvider(stalenessProvider); + openmct.telemetry.addProvider(new GeneratorProvider(openmct, stalenessProvider)); + openmct.telemetry.addProvider(new GeneratorMetadataProvider()); + openmct.telemetry.addProvider(new SinewaveLimitProvider()); + openmct.telemetry.addProvider(stalenessProvider); } diff --git a/example/imagery/plugin.js b/example/imagery/plugin.js index 1a53adf689..573a4a6425 100644 --- a/example/imagery/plugin.js +++ b/example/imagery/plugin.js @@ -21,24 +21,24 @@ *****************************************************************************/ const DEFAULT_IMAGE_SAMPLES = [ - "https://www.hq.nasa.gov/alsj/a16/AS16-117-18731.jpg", - "https://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg", - "https://www.hq.nasa.gov/alsj/a16/AS16-117-18733.jpg", - "https://www.hq.nasa.gov/alsj/a16/AS16-117-18734.jpg", - "https://www.hq.nasa.gov/alsj/a16/AS16-117-18735.jpg", - "https://www.hq.nasa.gov/alsj/a16/AS16-117-18736.jpg", - "https://www.hq.nasa.gov/alsj/a16/AS16-117-18737.jpg", - "https://www.hq.nasa.gov/alsj/a16/AS16-117-18738.jpg", - "https://www.hq.nasa.gov/alsj/a16/AS16-117-18739.jpg", - "https://www.hq.nasa.gov/alsj/a16/AS16-117-18740.jpg", - "https://www.hq.nasa.gov/alsj/a16/AS16-117-18741.jpg", - "https://www.hq.nasa.gov/alsj/a16/AS16-117-18742.jpg", - "https://www.hq.nasa.gov/alsj/a16/AS16-117-18743.jpg", - "https://www.hq.nasa.gov/alsj/a16/AS16-117-18744.jpg", - "https://www.hq.nasa.gov/alsj/a16/AS16-117-18745.jpg", - "https://www.hq.nasa.gov/alsj/a16/AS16-117-18746.jpg", - "https://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg", - "https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg" + 'https://www.hq.nasa.gov/alsj/a16/AS16-117-18731.jpg', + 'https://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg', + 'https://www.hq.nasa.gov/alsj/a16/AS16-117-18733.jpg', + 'https://www.hq.nasa.gov/alsj/a16/AS16-117-18734.jpg', + 'https://www.hq.nasa.gov/alsj/a16/AS16-117-18735.jpg', + 'https://www.hq.nasa.gov/alsj/a16/AS16-117-18736.jpg', + 'https://www.hq.nasa.gov/alsj/a16/AS16-117-18737.jpg', + 'https://www.hq.nasa.gov/alsj/a16/AS16-117-18738.jpg', + 'https://www.hq.nasa.gov/alsj/a16/AS16-117-18739.jpg', + 'https://www.hq.nasa.gov/alsj/a16/AS16-117-18740.jpg', + 'https://www.hq.nasa.gov/alsj/a16/AS16-117-18741.jpg', + 'https://www.hq.nasa.gov/alsj/a16/AS16-117-18742.jpg', + 'https://www.hq.nasa.gov/alsj/a16/AS16-117-18743.jpg', + 'https://www.hq.nasa.gov/alsj/a16/AS16-117-18744.jpg', + 'https://www.hq.nasa.gov/alsj/a16/AS16-117-18745.jpg', + 'https://www.hq.nasa.gov/alsj/a16/AS16-117-18746.jpg', + 'https://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg', + 'https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg' ]; const DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS = 20000; const MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS = 5000; @@ -46,237 +46,252 @@ const MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS = 5000; let openmctInstance; export default function () { - return function install(openmct) { - openmctInstance = openmct; - openmct.types.addType('example.imagery', { - key: 'example.imagery', - name: 'Example Imagery', - cssClass: 'icon-image', - description: 'For development use. Creates example imagery data that mimics a live imagery stream.', - creatable: true, - initialize: (object) => { - object.configuration = { - imageLocation: '', - imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS, - imageSamples: [], - layers: [] - }; - - object.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 Thumbnail', - key: 'thumbnail-url', - format: 'thumbnail', - hints: { - thumbnail: 1 - }, - source: 'url' - }, - { - name: 'Image Download Name', - key: 'imageDownloadName', - format: 'imageDownloadName', - hints: { - imageDownloadName: 1 - } - } - ] - }; - }, - form: [ - { - key: 'imageLocation', - name: 'Images url list (comma separated)', - control: 'textarea', - cssClass: 'l-inline', - property: [ - "configuration", - "imageLocation" - ] - }, - { - key: 'imageLoadDelayInMilliSeconds', - name: 'Image load delay (milliseconds)', - control: 'numberfield', - required: true, - cssClass: 'l-inline', - property: [ - "configuration", - "imageLoadDelayInMilliSeconds" - ] - } - ] - }); - - const formatThumbnail = { - format: function (url) { - return `${url}?w=100&h=100`; - } + return function install(openmct) { + openmctInstance = openmct; + openmct.types.addType('example.imagery', { + key: 'example.imagery', + name: 'Example Imagery', + cssClass: 'icon-image', + description: + 'For development use. Creates example imagery data that mimics a live imagery stream.', + creatable: true, + initialize: (object) => { + object.configuration = { + imageLocation: '', + imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS, + imageSamples: [], + layers: [] }; - openmct.telemetry.addFormat({ - key: 'thumbnail', - ...formatThumbnail - }); - openmct.telemetry.addProvider(getRealtimeProvider()); - openmct.telemetry.addProvider(getHistoricalProvider()); - openmct.telemetry.addProvider(getLadProvider()); + object.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 Thumbnail', + key: 'thumbnail-url', + format: 'thumbnail', + hints: { + thumbnail: 1 + }, + source: 'url' + }, + { + name: 'Image Download Name', + key: 'imageDownloadName', + format: 'imageDownloadName', + hints: { + imageDownloadName: 1 + } + } + ] + }; + }, + form: [ + { + key: 'imageLocation', + name: 'Images url list (comma separated)', + control: 'textarea', + cssClass: 'l-inline', + property: ['configuration', 'imageLocation'] + }, + { + key: 'imageLoadDelayInMilliSeconds', + name: 'Image load delay (milliseconds)', + control: 'numberfield', + required: true, + cssClass: 'l-inline', + property: ['configuration', 'imageLoadDelayInMilliSeconds'] + } + ] + }); + + const formatThumbnail = { + format: function (url) { + return `${url}?w=100&h=100`; + } }; + + openmct.telemetry.addFormat({ + key: 'thumbnail', + ...formatThumbnail + }); + openmct.telemetry.addProvider(getRealtimeProvider()); + openmct.telemetry.addProvider(getHistoricalProvider()); + openmct.telemetry.addProvider(getLadProvider()); + }; } function getCompassValues(min, max) { - return min + Math.random() * (max - min); + return min + Math.random() * (max - min); } function getImageSamples(configuration) { - let imageSamples = DEFAULT_IMAGE_SAMPLES; + let imageSamples = DEFAULT_IMAGE_SAMPLES; - if (configuration.imageLocation && configuration.imageLocation.length) { - imageSamples = getImageUrlListFromConfig(configuration); - } + if (configuration.imageLocation && configuration.imageLocation.length) { + imageSamples = getImageUrlListFromConfig(configuration); + } - return imageSamples; + return imageSamples; } function getImageUrlListFromConfig(configuration) { - return configuration.imageLocation.split(','); + return configuration.imageLocation.split(','); } function getImageLoadDelay(domainObject) { - const imageLoadDelay = Math.trunc(Number(domainObject.configuration.imageLoadDelayInMilliSeconds)); - if (!imageLoadDelay) { - openmctInstance.objects.mutate(domainObject, 'configuration.imageLoadDelayInMilliSeconds', DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS); + const imageLoadDelay = Math.trunc( + Number(domainObject.configuration.imageLoadDelayInMilliSeconds) + ); + if (!imageLoadDelay) { + openmctInstance.objects.mutate( + domainObject, + 'configuration.imageLoadDelayInMilliSeconds', + DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS + ); - return DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS; - } + return DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS; + } - if (imageLoadDelay < MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS) { - openmctInstance.objects.mutate(domainObject, 'configuration.imageLoadDelayInMilliSeconds', MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS); + if (imageLoadDelay < MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS) { + openmctInstance.objects.mutate( + domainObject, + 'configuration.imageLoadDelayInMilliSeconds', + MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS + ); - return MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS; - } + return MIN_IMAGE_LOAD_DELAY_IN_MILISECONDS; + } - return imageLoadDelay; + return imageLoadDelay; } function getRealtimeProvider() { - return { - supportsSubscribe: domainObject => domainObject.type === 'example.imagery', - subscribe: (domainObject, callback) => { - const delay = getImageLoadDelay(domainObject); - const interval = setInterval(() => { - const imageSamples = getImageSamples(domainObject.configuration); - const datum = pointForTimestamp(Date.now(), domainObject.name, imageSamples, delay); - callback(datum); - }, delay); + return { + supportsSubscribe: (domainObject) => domainObject.type === 'example.imagery', + subscribe: (domainObject, callback) => { + const delay = getImageLoadDelay(domainObject); + const interval = setInterval(() => { + const imageSamples = getImageSamples(domainObject.configuration); + const datum = pointForTimestamp(Date.now(), domainObject.name, imageSamples, delay); + callback(datum); + }, delay); - return () => { - clearInterval(interval); - }; - } - }; + return () => { + clearInterval(interval); + }; + } + }; } function getHistoricalProvider() { - return { - supportsRequest: (domainObject, options) => { - return domainObject.type === 'example.imagery' - && options.strategy !== 'latest'; - }, - request: (domainObject, options) => { - const delay = getImageLoadDelay(domainObject); - let start = options.start; - const end = Math.min(options.end, Date.now()); - const data = []; - while (start <= end && data.length < delay) { - data.push(pointForTimestamp(start, domainObject.name, getImageSamples(domainObject.configuration), delay)); - start += delay; - } + return { + supportsRequest: (domainObject, options) => { + return domainObject.type === 'example.imagery' && options.strategy !== 'latest'; + }, + request: (domainObject, options) => { + const delay = getImageLoadDelay(domainObject); + let start = options.start; + const end = Math.min(options.end, Date.now()); + const data = []; + while (start <= end && data.length < delay) { + data.push( + pointForTimestamp( + start, + domainObject.name, + getImageSamples(domainObject.configuration), + delay + ) + ); + start += delay; + } - return Promise.resolve(data); - } - }; + return Promise.resolve(data); + } + }; } function getLadProvider() { - return { - supportsRequest: (domainObject, options) => { - return domainObject.type === 'example.imagery' - && options.strategy === 'latest'; - }, - request: (domainObject, options) => { - const delay = getImageLoadDelay(domainObject); - const datum = pointForTimestamp(Date.now(), domainObject.name, getImageSamples(domainObject.configuration), delay); + return { + supportsRequest: (domainObject, options) => { + return domainObject.type === 'example.imagery' && options.strategy === 'latest'; + }, + request: (domainObject, options) => { + const delay = getImageLoadDelay(domainObject); + const datum = pointForTimestamp( + Date.now(), + domainObject.name, + getImageSamples(domainObject.configuration), + delay + ); - return Promise.resolve([datum]); - } - }; + return Promise.resolve([datum]); + } + }; } function pointForTimestamp(timestamp, name, imageSamples, delay) { - const url = imageSamples[Math.floor(timestamp / delay) % imageSamples.length]; - const urlItems = url.split('/'); - const imageDownloadName = `example.imagery.${urlItems[urlItems.length - 1]}`; - const navCamTransformations = { - "translateX": 0, - "translateY": 18, - "rotation": 0, - "scale": 0.3, - "cameraAngleOfView": 70 - }; + const url = imageSamples[Math.floor(timestamp / delay) % imageSamples.length]; + const urlItems = url.split('/'); + const imageDownloadName = `example.imagery.${urlItems[urlItems.length - 1]}`; + const navCamTransformations = { + translateX: 0, + translateY: 18, + rotation: 0, + scale: 0.3, + cameraAngleOfView: 70 + }; - return { - name, - utc: Math.floor(timestamp / delay) * delay, - local: Math.floor(timestamp / delay) * delay, - url, - sunOrientation: getCompassValues(0, 360), - cameraAzimuth: getCompassValues(0, 360), - heading: getCompassValues(0, 360), - transformations: navCamTransformations, - imageDownloadName - }; + return { + name, + utc: Math.floor(timestamp / delay) * delay, + local: Math.floor(timestamp / delay) * delay, + url, + sunOrientation: getCompassValues(0, 360), + cameraAzimuth: getCompassValues(0, 360), + heading: getCompassValues(0, 360), + transformations: navCamTransformations, + imageDownloadName + }; } diff --git a/example/simpleVuePlugin/HelloWorld.vue b/example/simpleVuePlugin/HelloWorld.vue index 305d31183d..dc6b4272cc 100644 --- a/example/simpleVuePlugin/HelloWorld.vue +++ b/example/simpleVuePlugin/HelloWorld.vue @@ -1,14 +1,14 @@ diff --git a/example/simpleVuePlugin/plugin.js b/example/simpleVuePlugin/plugin.js index a1f29288da..78fc92ce06 100644 --- a/example/simpleVuePlugin/plugin.js +++ b/example/simpleVuePlugin/plugin.js @@ -2,35 +2,34 @@ import Vue from 'vue'; import HelloWorld from './HelloWorld.vue'; function SimpleVuePlugin() { - return function install(openmct) { - openmct.types.addType('hello-world', { - name: 'Hello World', - description: 'An introduction object', - creatable: true - }); - openmct.objectViews.addProvider({ - name: "demo-provider", - key: "hello-world", - cssClass: "icon-packet", - canView: function (d) { - return d.type === 'hello-world'; - }, - view: function (domainObject) { - var vm; + return function install(openmct) { + openmct.types.addType('hello-world', { + name: 'Hello World', + description: 'An introduction object', + creatable: true + }); + openmct.objectViews.addProvider({ + name: 'demo-provider', + key: 'hello-world', + cssClass: 'icon-packet', + canView: function (d) { + return d.type === 'hello-world'; + }, + view: function (domainObject) { + var vm; - return { - show: function (container) { - vm = new Vue(HelloWorld); - container.appendChild(vm.$mount().$el); - }, - destroy: function (container) { - vm.$destroy(); - } - }; - } - }); - - }; + return { + show: function (container) { + vm = new Vue(HelloWorld); + container.appendChild(vm.$mount().$el); + }, + destroy: function (container) { + vm.$destroy(); + } + }; + } + }); + }; } export default SimpleVuePlugin; diff --git a/index.html b/index.html index 4b052792c7..e8d460c35b 100644 --- a/index.html +++ b/index.html @@ -19,189 +19,227 @@ this source code distribution or the Licensing information page available at runtime from the About dialog for additional information. --> - + - - - - - - - - - - - - - - + } + ] + }) + ); + openmct.install(openmct.plugins.SummaryWidget()); + openmct.install(openmct.plugins.Notebook()); + openmct.install(openmct.plugins.LADTable()); + openmct.install(openmct.plugins.Filters(['table', 'telemetry.plot.overlay'])); + openmct.install(openmct.plugins.ObjectMigration()); + openmct.install( + openmct.plugins.ClearData( + ['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked', 'example.imagery'], + { 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()); + document.addEventListener('DOMContentLoaded', function () { + openmct.start(); + }); + diff --git a/karma.conf.js b/karma.conf.js index 0c2d76556a..587b3c6d70 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -23,93 +23,93 @@ /*global module,process*/ module.exports = (config) => { - let webpackConfig; - let browsers; - let singleRun; + let webpackConfig; + let browsers; + let singleRun; - if (process.env.KARMA_DEBUG) { - webpackConfig = require("./.webpack/webpack.dev.js"); - browsers = ["ChromeDebugging"]; - singleRun = false; - } else { - webpackConfig = require("./.webpack/webpack.coverage.js"); - browsers = ["ChromeHeadless"]; - singleRun = true; - } + if (process.env.KARMA_DEBUG) { + webpackConfig = require('./.webpack/webpack.dev.js'); + browsers = ['ChromeDebugging']; + singleRun = false; + } else { + webpackConfig = require('./.webpack/webpack.coverage.js'); + browsers = ['ChromeHeadless']; + singleRun = true; + } - delete webpackConfig.output; - // karma doesn't support webpack entry - delete webpackConfig.entry; + delete webpackConfig.output; + // karma doesn't support webpack entry + delete webpackConfig.entry; - config.set({ - basePath: "", - frameworks: ["jasmine", "webpack"], - files: [ - "indexTest.js", - // included means: should the files be included in the browser using diff --git a/src/api/forms/components/FormRow.vue b/src/api/forms/components/FormRow.vue index 0ccffd4636..f8eb900f63 100644 --- a/src/api/forms/components/FormRow.vue +++ b/src/api/forms/components/FormRow.vue @@ -21,133 +21,113 @@ --> diff --git a/src/api/forms/components/controls/AutoCompleteField.vue b/src/api/forms/components/controls/AutoCompleteField.vue index 32f6ec7ad3..5a8d2cc2f3 100644 --- a/src/api/forms/components/controls/AutoCompleteField.vue +++ b/src/api/forms/components/controls/AutoCompleteField.vue @@ -20,252 +20,243 @@ at runtime from the About dialog for additional information. --> diff --git a/src/api/forms/components/controls/CheckBoxField.vue b/src/api/forms/components/controls/CheckBoxField.vue index 916a5ae3b6..64dcf91b96 100644 --- a/src/api/forms/components/controls/CheckBoxField.vue +++ b/src/api/forms/components/controls/CheckBoxField.vue @@ -21,35 +21,28 @@ --> diff --git a/src/api/forms/components/controls/ClockDisplayFormatField.vue b/src/api/forms/components/controls/ClockDisplayFormatField.vue index 9ca8d760b0..960b15e1fa 100644 --- a/src/api/forms/components/controls/ClockDisplayFormatField.vue +++ b/src/api/forms/components/controls/ClockDisplayFormatField.vue @@ -21,47 +21,42 @@ --> diff --git a/src/api/forms/components/controls/Composite.vue b/src/api/forms/components/controls/Composite.vue index 7627a231ce..5ae11bce4e 100644 --- a/src/api/forms/components/controls/Composite.vue +++ b/src/api/forms/components/controls/Composite.vue @@ -21,38 +21,38 @@ --> diff --git a/src/api/forms/components/controls/CompositeItem.vue b/src/api/forms/components/controls/CompositeItem.vue index ff51130160..42ab07d025 100644 --- a/src/api/forms/components/controls/CompositeItem.vue +++ b/src/api/forms/components/controls/CompositeItem.vue @@ -21,56 +21,50 @@ --> diff --git a/src/api/forms/components/controls/Datetime.vue b/src/api/forms/components/controls/Datetime.vue index 2590741198..921ec03774 100644 --- a/src/api/forms/components/controls/Datetime.vue +++ b/src/api/forms/components/controls/Datetime.vue @@ -21,150 +21,144 @@ --> diff --git a/src/api/forms/components/controls/FileInput.vue b/src/api/forms/components/controls/FileInput.vue index 1f53866c4a..c7afaba6a4 100644 --- a/src/api/forms/components/controls/FileInput.vue +++ b/src/api/forms/components/controls/FileInput.vue @@ -21,129 +21,122 @@ --> diff --git a/src/api/forms/components/controls/Locator.vue b/src/api/forms/components/controls/Locator.vue index 8b1e12729a..95ce8f890c 100644 --- a/src/api/forms/components/controls/Locator.vue +++ b/src/api/forms/components/controls/Locator.vue @@ -21,36 +21,36 @@ --> diff --git a/src/api/forms/components/controls/NumberField.vue b/src/api/forms/components/controls/NumberField.vue index bdaab2f1c0..02cf8b92da 100644 --- a/src/api/forms/components/controls/NumberField.vue +++ b/src/api/forms/components/controls/NumberField.vue @@ -21,51 +21,48 @@ --> diff --git a/src/api/forms/components/controls/SelectField.vue b/src/api/forms/components/controls/SelectField.vue index 002c27e103..a170012f53 100644 --- a/src/api/forms/components/controls/SelectField.vue +++ b/src/api/forms/components/controls/SelectField.vue @@ -21,47 +21,43 @@ --> diff --git a/src/api/forms/components/controls/TextAreaField.vue b/src/api/forms/components/controls/TextAreaField.vue index a80043c5fe..5fc5f8028e 100644 --- a/src/api/forms/components/controls/TextAreaField.vue +++ b/src/api/forms/components/controls/TextAreaField.vue @@ -21,50 +21,47 @@ --> diff --git a/src/api/forms/components/controls/TextField.vue b/src/api/forms/components/controls/TextField.vue index 219fb84d6f..5878779e0f 100644 --- a/src/api/forms/components/controls/TextField.vue +++ b/src/api/forms/components/controls/TextField.vue @@ -21,48 +21,40 @@ --> diff --git a/src/api/forms/components/controls/ToggleSwitchField.vue b/src/api/forms/components/controls/ToggleSwitchField.vue index 61017179f2..3e42b92c64 100644 --- a/src/api/forms/components/controls/ToggleSwitchField.vue +++ b/src/api/forms/components/controls/ToggleSwitchField.vue @@ -21,19 +21,16 @@ --> diff --git a/src/api/forms/toggle-check-box-mixin.js b/src/api/forms/toggle-check-box-mixin.js index f15c344039..48714f76b3 100644 --- a/src/api/forms/toggle-check-box-mixin.js +++ b/src/api/forms/toggle-check-box-mixin.js @@ -1,19 +1,19 @@ export default { - data() { - return { - isChecked: false - }; - }, - methods: { - toggleCheckBox(event) { - this.isChecked = !this.isChecked; + data() { + return { + isChecked: false + }; + }, + methods: { + toggleCheckBox(event) { + this.isChecked = !this.isChecked; - const data = { - model: this.model, - value: this.isChecked - }; + const data = { + model: this.model, + value: this.isChecked + }; - this.$emit('onChange', data); - } + this.$emit('onChange', data); } + } }; diff --git a/src/api/indicators/IndicatorAPI.js b/src/api/indicators/IndicatorAPI.js index 8d72be0437..9aa68ddef5 100644 --- a/src/api/indicators/IndicatorAPI.js +++ b/src/api/indicators/IndicatorAPI.js @@ -20,58 +20,57 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import EventEmitter from "EventEmitter"; -import SimpleIndicator from "./SimpleIndicator"; +import EventEmitter from 'EventEmitter'; +import SimpleIndicator from './SimpleIndicator'; class IndicatorAPI extends EventEmitter { - constructor(openmct) { - super(); + constructor(openmct) { + super(); - this.openmct = openmct; - this.indicatorObjects = []; + this.openmct = openmct; + this.indicatorObjects = []; + } + + getIndicatorObjectsByPriority() { + const sortedIndicators = this.indicatorObjects.sort((a, b) => b.priority - a.priority); + + return sortedIndicators; + } + + simpleIndicator() { + return new SimpleIndicator(this.openmct); + } + + /** + * Accepts an indicator object, which is a simple object + * with a two attributes: 'element' which has an HTMLElement + * as its value, and 'priority' with an integer that specifies its order in the layout. + * The lower the priority, the further to the right the element is placed. + * If undefined, the priority will be assigned -1. + * + * We provide .simpleIndicator() as a convenience function + * which will create a default Open MCT indicator that can + * be passed to .add(indicator). This indicator also exposes + * functions for changing its appearance to support customization + * and dynamic behavior. + * + * Eg. + * const myIndicator = openmct.indicators.simpleIndicator(); + * openmct.indicators.add(myIndicator); + * + * myIndicator.text("Hello World!"); + * myIndicator.iconClass("icon-info"); + * + */ + add(indicator) { + if (!indicator.priority) { + indicator.priority = this.openmct.priority.DEFAULT; } - getIndicatorObjectsByPriority() { - const sortedIndicators = this.indicatorObjects.sort((a, b) => b.priority - a.priority); - - return sortedIndicators; - } - - simpleIndicator() { - return new SimpleIndicator(this.openmct); - } - - /** - * Accepts an indicator object, which is a simple object - * with a two attributes: 'element' which has an HTMLElement - * as its value, and 'priority' with an integer that specifies its order in the layout. - * The lower the priority, the further to the right the element is placed. - * If undefined, the priority will be assigned -1. - * - * We provide .simpleIndicator() as a convenience function - * which will create a default Open MCT indicator that can - * be passed to .add(indicator). This indicator also exposes - * functions for changing its appearance to support customization - * and dynamic behavior. - * - * Eg. - * const myIndicator = openmct.indicators.simpleIndicator(); - * openmct.indicators.add(myIndicator); - * - * myIndicator.text("Hello World!"); - * myIndicator.iconClass("icon-info"); - * - */ - add(indicator) { - if (!indicator.priority) { - indicator.priority = this.openmct.priority.DEFAULT; - } - - this.indicatorObjects.push(indicator); - - this.emit('addIndicator', indicator); - } + this.indicatorObjects.push(indicator); + this.emit('addIndicator', indicator); + } } export default IndicatorAPI; diff --git a/src/api/indicators/IndicatorAPISpec.js b/src/api/indicators/IndicatorAPISpec.js index 3560c1c3fa..6445922d34 100644 --- a/src/api/indicators/IndicatorAPISpec.js +++ b/src/api/indicators/IndicatorAPISpec.js @@ -22,61 +22,77 @@ import { createOpenMct, resetApplicationState } from '../../utils/testing'; import SimpleIndicator from './SimpleIndicator'; -describe("The Indicator API", () => { - let openmct; +describe('The Indicator API', () => { + let openmct; - beforeEach(() => { - openmct = createOpenMct(); - }); + beforeEach(() => { + openmct = createOpenMct(); + }); - afterEach(() => { - return resetApplicationState(openmct); - }); + afterEach(() => { + return resetApplicationState(openmct); + }); - function generateIndicator(className, label, priority) { - const element = document.createElement('div'); - element.classList.add(className); - const textNode = document.createTextNode(label); - element.appendChild(textNode); - const testIndicator = { - element, - priority - }; + function generateIndicator(className, label, priority) { + const element = document.createElement('div'); + element.classList.add(className); + const textNode = document.createTextNode(label); + element.appendChild(textNode); + const testIndicator = { + element, + priority + }; - return testIndicator; - } + return testIndicator; + } - it("can register an indicator", () => { - const testIndicator = generateIndicator('test-indicator', 'This is a test indicator', 2); - openmct.indicators.add(testIndicator); - expect(openmct.indicators.indicatorObjects).toBeDefined(); - // notifier indicator is installed by default - expect(openmct.indicators.indicatorObjects.length).toBe(2); - }); + it('can register an indicator', () => { + const testIndicator = generateIndicator('test-indicator', 'This is a test indicator', 2); + openmct.indicators.add(testIndicator); + expect(openmct.indicators.indicatorObjects).toBeDefined(); + // notifier indicator is installed by default + expect(openmct.indicators.indicatorObjects.length).toBe(2); + }); - it("can order indicators based on priority", () => { - const testIndicator1 = generateIndicator('test-indicator-1', 'This is a test indicator', openmct.priority.LOW); - openmct.indicators.add(testIndicator1); + it('can order indicators based on priority', () => { + const testIndicator1 = generateIndicator( + 'test-indicator-1', + 'This is a test indicator', + openmct.priority.LOW + ); + openmct.indicators.add(testIndicator1); - const testIndicator2 = generateIndicator('test-indicator-2', 'This is another test indicator', openmct.priority.DEFAULT); - openmct.indicators.add(testIndicator2); + const testIndicator2 = generateIndicator( + 'test-indicator-2', + 'This is another test indicator', + openmct.priority.DEFAULT + ); + openmct.indicators.add(testIndicator2); - const testIndicator3 = generateIndicator('test-indicator-3', 'This is yet another test indicator', openmct.priority.LOW); - openmct.indicators.add(testIndicator3); + const testIndicator3 = generateIndicator( + 'test-indicator-3', + 'This is yet another test indicator', + openmct.priority.LOW + ); + openmct.indicators.add(testIndicator3); - const testIndicator4 = generateIndicator('test-indicator-4', 'This is yet another test indicator', openmct.priority.HIGH); - openmct.indicators.add(testIndicator4); + const testIndicator4 = generateIndicator( + 'test-indicator-4', + 'This is yet another test indicator', + openmct.priority.HIGH + ); + openmct.indicators.add(testIndicator4); - expect(openmct.indicators.indicatorObjects.length).toBe(5); - const indicatorObjectsByPriority = openmct.indicators.getIndicatorObjectsByPriority(); - expect(indicatorObjectsByPriority.length).toBe(5); - expect(indicatorObjectsByPriority[2].priority).toBe(openmct.priority.DEFAULT); - }); + expect(openmct.indicators.indicatorObjects.length).toBe(5); + const indicatorObjectsByPriority = openmct.indicators.getIndicatorObjectsByPriority(); + expect(indicatorObjectsByPriority.length).toBe(5); + expect(indicatorObjectsByPriority[2].priority).toBe(openmct.priority.DEFAULT); + }); - it("the simple indicator can be added", () => { - const simpleIndicator = new SimpleIndicator(openmct); - openmct.indicators.add(simpleIndicator); + it('the simple indicator can be added', () => { + const simpleIndicator = new SimpleIndicator(openmct); + openmct.indicators.add(simpleIndicator); - expect(openmct.indicators.indicatorObjects.length).toBe(2); - }); + expect(openmct.indicators.indicatorObjects.length).toBe(2); + }); }); diff --git a/src/api/indicators/SimpleIndicator.js b/src/api/indicators/SimpleIndicator.js index 9ea8035202..dc8d28604a 100644 --- a/src/api/indicators/SimpleIndicator.js +++ b/src/api/indicators/SimpleIndicator.js @@ -27,94 +27,94 @@ import { convertTemplateToHTML } from '@/utils/template/templateHelpers'; const DEFAULT_ICON_CLASS = 'icon-info'; class SimpleIndicator extends EventEmitter { - constructor(openmct) { - super(); + constructor(openmct) { + super(); - this.openmct = openmct; - this.element = convertTemplateToHTML(indicatorTemplate)[0]; - this.priority = openmct.priority.DEFAULT; + this.openmct = openmct; + this.element = convertTemplateToHTML(indicatorTemplate)[0]; + this.priority = openmct.priority.DEFAULT; - this.textElement = this.element.querySelector('.js-indicator-text'); + this.textElement = this.element.querySelector('.js-indicator-text'); - //Set defaults - this.text('New Indicator'); - this.description(''); - this.iconClass(DEFAULT_ICON_CLASS); + //Set defaults + this.text('New Indicator'); + this.description(''); + this.iconClass(DEFAULT_ICON_CLASS); - this.click = this.click.bind(this); + this.click = this.click.bind(this); - this.element.addEventListener('click', this.click); - openmct.once('destroy', () => { - this.removeAllListeners(); - this.element.removeEventListener('click', this.click); - }); + 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; + + if (!text) { + this.element.classList.add('hidden'); + } else { + this.element.classList.remove('hidden'); + } } - text(text) { - if (text !== undefined && text !== this.textValue) { - this.textValue = text; - this.textElement.innerText = text; + return this.textValue; + } - if (!text) { - this.element.classList.add('hidden'); - } else { - this.element.classList.remove('hidden'); - } - } - - return this.textValue; + description(description) { + if (description !== undefined && description !== this.descriptionValue) { + this.descriptionValue = description; + this.element.title = description; } - description(description) { - if (description !== undefined && description !== this.descriptionValue) { - this.descriptionValue = description; - this.element.title = description; - } + return this.descriptionValue; + } - return this.descriptionValue; + 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); + } + + if (iconClass) { + this.element.classList.add(iconClass); + } + + 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); - } + statusClass(statusClass) { + if (arguments.length === 1 && statusClass !== this.statusClassValue) { + if (this.statusClassValue) { + this.element.classList.remove(this.statusClassValue); + } - this.iconClassValue = iconClass; - } + if (statusClass !== undefined) { + this.element.classList.add(statusClass); + } - return this.iconClassValue; + this.statusClassValue = statusClass; } - statusClass(statusClass) { - if (arguments.length === 1 && statusClass !== this.statusClassValue) { - if (this.statusClassValue) { - this.element.classList.remove(this.statusClassValue); - } + return this.statusClassValue; + } - if (statusClass !== undefined) { - this.element.classList.add(statusClass); - } + click(event) { + this.emit('click', event); + } - this.statusClassValue = statusClass; - } - - return this.statusClassValue; - } - - click(event) { - this.emit('click', event); - } - - getElement() { - return this.element; - } + getElement() { + return this.element; + } } export default SimpleIndicator; diff --git a/src/api/indicators/res/indicator-template.html b/src/api/indicators/res/indicator-template.html index fc0fb5dd9f..a6fb54b741 100644 --- a/src/api/indicators/res/indicator-template.html +++ b/src/api/indicators/res/indicator-template.html @@ -1,3 +1,3 @@
    - +
    diff --git a/src/api/menu/MenuAPI.js b/src/api/menu/MenuAPI.js index d316a1aa94..bb1c8215ae 100644 --- a/src/api/menu/MenuAPI.js +++ b/src/api/menu/MenuAPI.js @@ -48,83 +48,86 @@ import Menu, { MENU_PLACEMENT } from './menu.js'; */ class MenuAPI { - constructor(openmct) { - this.openmct = openmct; + constructor(openmct) { + this.openmct = openmct; - this.menuPlacement = MENU_PLACEMENT; - this.showMenu = this.showMenu.bind(this); - this.showSuperMenu = this.showSuperMenu.bind(this); + this.menuPlacement = MENU_PLACEMENT; + this.showMenu = this.showMenu.bind(this); + this.showSuperMenu = this.showSuperMenu.bind(this); - this._clearMenuComponent = this._clearMenuComponent.bind(this); - this._showObjectMenu = this._showObjectMenu.bind(this); - } + this._clearMenuComponent = this._clearMenuComponent.bind(this); + this._showObjectMenu = this._showObjectMenu.bind(this); + } - /** - * Show popup menu - * @param {number} x x-coordinates for popup - * @param {number} y x-coordinates for popup - * @param {Array.|Array.>} actions collection of actions{@link Action} or collection of groups of actions {@link Action} - * @param {MenuOptions} [menuOptions] [Optional] The {@link MenuOptions} options for Menu - */ - showMenu(x, y, items, menuOptions) { - this._createMenuComponent(x, y, items, menuOptions); + /** + * Show popup menu + * @param {number} x x-coordinates for popup + * @param {number} y x-coordinates for popup + * @param {Array.|Array.>} actions collection of actions{@link Action} or collection of groups of actions {@link Action} + * @param {MenuOptions} [menuOptions] [Optional] The {@link MenuOptions} options for Menu + */ + showMenu(x, y, items, menuOptions) { + this._createMenuComponent(x, y, items, menuOptions); - this.menuComponent.showMenu(); - } + this.menuComponent.showMenu(); + } - actionsToMenuItems(actions, objectPath, view) { - return actions.map(action => { - const isActionGroup = Array.isArray(action); - if (isActionGroup) { - action = this.actionsToMenuItems(action, objectPath, view); - } else { - action.onItemClicked = () => { - action.invoke(objectPath, view); - }; - } - - return action; - }); - } - - /** - * Show popup menu with description of item on hover - * @param {number} x x-coordinates for popup - * @param {number} y x-coordinates for popup - * @param {Array.|Array.>} actions collection of actions {@link Action} or collection of groups of actions {@link Action} - * @param {MenuOptions} [menuOptions] [Optional] The {@link MenuOptions} options for Menu - */ - showSuperMenu(x, y, actions, menuOptions) { - this._createMenuComponent(x, y, actions, menuOptions); - - this.menuComponent.showSuperMenu(); - } - - _clearMenuComponent() { - this.menuComponent = undefined; - delete this.menuComponent; - } - - _createMenuComponent(x, y, actions, menuOptions = {}) { - if (this.menuComponent) { - this.menuComponent.dismiss(); - } - - let options = { - x, - y, - actions, - ...menuOptions + actionsToMenuItems(actions, objectPath, view) { + return actions.map((action) => { + const isActionGroup = Array.isArray(action); + if (isActionGroup) { + action = this.actionsToMenuItems(action, objectPath, view); + } else { + action.onItemClicked = () => { + action.invoke(objectPath, view); }; + } - this.menuComponent = new Menu(options); - this.menuComponent.once('destroy', this._clearMenuComponent); + return action; + }); + } + + /** + * Show popup menu with description of item on hover + * @param {number} x x-coordinates for popup + * @param {number} y x-coordinates for popup + * @param {Array.|Array.>} actions collection of actions {@link Action} or collection of groups of actions {@link Action} + * @param {MenuOptions} [menuOptions] [Optional] The {@link MenuOptions} options for Menu + */ + showSuperMenu(x, y, actions, menuOptions) { + this._createMenuComponent(x, y, actions, menuOptions); + + this.menuComponent.showSuperMenu(); + } + + _clearMenuComponent() { + this.menuComponent = undefined; + delete this.menuComponent; + } + + _createMenuComponent(x, y, actions, menuOptions = {}) { + if (this.menuComponent) { + this.menuComponent.dismiss(); } - _showObjectMenu(objectPath, x, y, actionsToBeIncluded) { - let applicableActions = this.openmct.actions._groupedAndSortedObjectActions(objectPath, actionsToBeIncluded); + let options = { + x, + y, + actions, + ...menuOptions + }; - this.showMenu(x, y, applicableActions); - } + this.menuComponent = new Menu(options); + this.menuComponent.once('destroy', this._clearMenuComponent); + } + + _showObjectMenu(objectPath, x, y, actionsToBeIncluded) { + let applicableActions = this.openmct.actions._groupedAndSortedObjectActions( + objectPath, + actionsToBeIncluded + ); + + this.showMenu(x, y, applicableActions); + } } export default MenuAPI; diff --git a/src/api/menu/MenuAPISpec.js b/src/api/menu/MenuAPISpec.js index e4394849bb..68f6f3b04a 100644 --- a/src/api/menu/MenuAPISpec.js +++ b/src/api/menu/MenuAPISpec.js @@ -24,208 +24,208 @@ import MenuAPI from './MenuAPI'; import Menu from './menu'; import { createOpenMct, createMouseEvent, resetApplicationState } from '../../utils/testing'; -describe ('The Menu API', () => { - let openmct; - let appHolder; - let menuAPI; - let actionsArray; - let result; - let menuElement; +describe('The Menu API', () => { + let openmct; + let appHolder; + let menuAPI; + let actionsArray; + let result; + let menuElement; - const x = 8; - const y = 16; + const x = 8; + const y = 16; - const menuOptions = { - onDestroy: () => { - console.log('default onDestroy'); + const menuOptions = { + onDestroy: () => { + console.log('default onDestroy'); + } + }; + + beforeEach((done) => { + appHolder = document.createElement('div'); + appHolder.style.display = 'block'; + appHolder.style.width = '1920px'; + appHolder.style.height = '1080px'; + + openmct = createOpenMct(); + + openmct.on('start', done); + openmct.startHeadless(); + + menuAPI = new MenuAPI(openmct); + actionsArray = [ + { + key: 'test-css-class-1', + name: 'Test Action 1', + cssClass: 'icon-clock', + description: 'This is a test action 1', + onItemClicked: () => { + result = 'Test Action 1 Invoked'; } - }; + }, + { + key: 'test-css-class-2', + name: 'Test Action 2', + cssClass: 'icon-clock', + description: 'This is a test action 2', + onItemClicked: () => { + result = 'Test Action 2 Invoked'; + } + } + ]; + }); - beforeEach((done) => { - appHolder = document.createElement('div'); - appHolder.style.display = 'block'; - appHolder.style.width = '1920px'; - appHolder.style.height = '1080px'; + afterEach(() => { + return resetApplicationState(openmct); + }); - openmct = createOpenMct(); - - openmct.on('start', done); - openmct.startHeadless(); - - menuAPI = new MenuAPI(openmct); - actionsArray = [ - { - key: 'test-css-class-1', - name: 'Test Action 1', - cssClass: 'icon-clock', - description: 'This is a test action 1', - onItemClicked: () => { - result = 'Test Action 1 Invoked'; - } - }, - { - key: 'test-css-class-2', - name: 'Test Action 2', - cssClass: 'icon-clock', - description: 'This is a test action 2', - onItemClicked: () => { - result = 'Test Action 2 Invoked'; - } - } - ]; + describe('showMenu method', () => { + beforeAll(() => { + spyOn(menuOptions, 'onDestroy').and.callThrough(); }); - afterEach(() => { - return resetApplicationState(openmct); + it('creates an instance of Menu when invoked', (done) => { + menuOptions.onDestroy = done; + + menuAPI.showMenu(x, y, actionsArray, menuOptions); + + expect(menuAPI.menuComponent).toBeInstanceOf(Menu); + document.body.click(); }); - describe('showMenu method', () => { - beforeAll(() => { - spyOn(menuOptions, 'onDestroy').and.callThrough(); - }); + describe('creates a menu component', () => { + it('with all the actions passed in', (done) => { + menuOptions.onDestroy = done; - it('creates an instance of Menu when invoked', (done) => { - menuOptions.onDestroy = done; + menuAPI.showMenu(x, y, actionsArray, menuOptions); + menuElement = document.querySelector('.c-menu'); + expect(menuElement).toBeDefined(); - menuAPI.showMenu(x, y, actionsArray, menuOptions); + const listItems = menuElement.children[0].children; - expect(menuAPI.menuComponent).toBeInstanceOf(Menu); - document.body.click(); - }); + expect(listItems.length).toEqual(actionsArray.length); + document.body.click(); + }); - describe('creates a menu component', () => { - it('with all the actions passed in', (done) => { - menuOptions.onDestroy = done; + it('with click-able menu items, that will invoke the correct callBack', (done) => { + menuOptions.onDestroy = done; - menuAPI.showMenu(x, y, actionsArray, menuOptions); - menuElement = document.querySelector('.c-menu'); - expect(menuElement).toBeDefined(); + menuAPI.showMenu(x, y, actionsArray, menuOptions); - const listItems = menuElement.children[0].children; + menuElement = document.querySelector('.c-menu'); + const listItem1 = menuElement.children[0].children[0]; - expect(listItems.length).toEqual(actionsArray.length); - document.body.click(); - }); + listItem1.click(); - it('with click-able menu items, that will invoke the correct callBack', (done) => { - menuOptions.onDestroy = done; + expect(result).toEqual('Test Action 1 Invoked'); + }); - menuAPI.showMenu(x, y, actionsArray, menuOptions); + it('dismisses the menu when action is clicked on', (done) => { + menuOptions.onDestroy = done; - menuElement = document.querySelector('.c-menu'); - const listItem1 = menuElement.children[0].children[0]; + menuAPI.showMenu(x, y, actionsArray, menuOptions); - listItem1.click(); + menuElement = document.querySelector('.c-menu'); + const listItem1 = menuElement.children[0].children[0]; + listItem1.click(); - expect(result).toEqual('Test Action 1 Invoked'); - }); + menuElement = document.querySelector('.c-menu'); - it('dismisses the menu when action is clicked on', (done) => { - menuOptions.onDestroy = done; + expect(menuElement).toBeNull(); + }); - menuAPI.showMenu(x, y, actionsArray, menuOptions); + it('invokes the destroy method when menu is dismissed', (done) => { + menuOptions.onDestroy = done; - menuElement = document.querySelector('.c-menu'); - const listItem1 = menuElement.children[0].children[0]; - listItem1.click(); + menuAPI.showMenu(x, y, actionsArray, menuOptions); - menuElement = document.querySelector('.c-menu'); + const vueComponent = menuAPI.menuComponent.component; + spyOn(vueComponent, '$destroy'); - expect(menuElement).toBeNull(); - }); + document.body.click(); - it('invokes the destroy method when menu is dismissed', (done) => { - menuOptions.onDestroy = done; + expect(vueComponent.$destroy).toHaveBeenCalled(); + }); - menuAPI.showMenu(x, y, actionsArray, menuOptions); + it('invokes the onDestroy callback if passed in', (done) => { + let count = 0; + menuOptions.onDestroy = () => { + count++; + expect(count).toEqual(1); + done(); + }; - const vueComponent = menuAPI.menuComponent.component; - spyOn(vueComponent, '$destroy'); + menuAPI.showMenu(x, y, actionsArray, menuOptions); - document.body.click(); + document.body.click(); + }); + }); + }); - expect(vueComponent.$destroy).toHaveBeenCalled(); - }); + describe('superMenu method', () => { + it('creates a superMenu', (done) => { + menuOptions.onDestroy = done; - it('invokes the onDestroy callback if passed in', (done) => { - let count = 0; - menuOptions.onDestroy = () => { - count++; - expect(count).toEqual(1); - done(); - }; + menuAPI.showSuperMenu(x, y, actionsArray, menuOptions); + menuElement = document.querySelector('.c-super-menu__menu'); - menuAPI.showMenu(x, y, actionsArray, menuOptions); - - document.body.click(); - }); - }); + expect(menuElement).not.toBeNull(); + document.body.click(); }); - describe('superMenu method', () => { - it('creates a superMenu', (done) => { - menuOptions.onDestroy = done; + 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'); + menuAPI.showSuperMenu(x, y, actionsArray, menuOptions); + menuElement = document.querySelector('.c-super-menu__menu'); - expect(menuElement).not.toBeNull(); - document.body.click(); - }); + const superMenuItem = menuElement.querySelector('li'); + const mouseOverEvent = createMouseEvent('mouseover'); - it('Mouse over a superMenu shows correct description', (done) => { - menuOptions.onDestroy = done; + superMenuItem.dispatchEvent(mouseOverEvent); + const itemDescription = document.querySelector('.l-item-description__description'); - menuAPI.showSuperMenu(x, y, actionsArray, menuOptions); - menuElement = document.querySelector('.c-super-menu__menu'); + menuAPI.menuComponent.component.$nextTick(() => { + expect(menuElement).not.toBeNull(); + expect(itemDescription.innerText).toEqual(actionsArray[0].description); - const superMenuItem = menuElement.querySelector('li'); - const mouseOverEvent = createMouseEvent('mouseover'); + document.body.click(); + }); + }); + }); - superMenuItem.dispatchEvent(mouseOverEvent); - const itemDescription = document.querySelector('.l-item-description__description'); + describe('Menu Placements', () => { + it('default menu position BOTTOM_RIGHT', (done) => { + menuOptions.onDestroy = done; - menuAPI.menuComponent.component.$nextTick(() => { - expect(menuElement).not.toBeNull(); - expect(itemDescription.innerText).toEqual(actionsArray[0].description); + menuAPI.showMenu(x, y, actionsArray, menuOptions); + menuElement = document.querySelector('.c-menu'); - document.body.click(); - }); - }); + const boundingClientRect = menuElement.getBoundingClientRect(); + const left = boundingClientRect.left; + const top = boundingClientRect.top; + + expect(left).toEqual(x); + expect(top).toEqual(y); + + document.body.click(); }); - describe('Menu Placements', () => { - it('default menu position BOTTOM_RIGHT', (done) => { - menuOptions.onDestroy = done; + 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'); + menuAPI.showMenu(x, y, actionsArray, menuOptions); + menuElement = document.querySelector('.c-menu'); - const boundingClientRect = menuElement.getBoundingClientRect(); - const left = boundingClientRect.left; - const top = boundingClientRect.top; + const boundingClientRect = menuElement.getBoundingClientRect(); + const left = boundingClientRect.left; + const top = boundingClientRect.top; - expect(left).toEqual(x); - expect(top).toEqual(y); + expect(left).toEqual(x); + expect(top).toEqual(y); - document.body.click(); - }); - - 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 boundingClientRect = menuElement.getBoundingClientRect(); - const left = boundingClientRect.left; - const top = boundingClientRect.top; - - expect(left).toEqual(x); - expect(top).toEqual(y); - - document.body.click(); - }); + document.body.click(); }); + }); }); diff --git a/src/api/menu/components/Menu.vue b/src/api/menu/components/Menu.vue index f8df9d1365..62324bb9df 100644 --- a/src/api/menu/components/Menu.vue +++ b/src/api/menu/components/Menu.vue @@ -20,72 +20,51 @@ at runtime from the About dialog for additional information. --> diff --git a/src/api/menu/components/SuperMenu.vue b/src/api/menu/components/SuperMenu.vue index 665c160f8c..91d9970c33 100644 --- a/src/api/menu/components/SuperMenu.vue +++ b/src/api/menu/components/SuperMenu.vue @@ -20,104 +20,85 @@ at runtime from the About dialog for additional information. --> + + +
    -
    -
    - {{ hoveredItem.name }} -
    -
    - {{ hoveredItem.description }} -
    +
    +
    + {{ hoveredItem.name }} +
    +
    + {{ hoveredItem.description }} +
    - + diff --git a/src/api/menu/menu.js b/src/api/menu/menu.js index 874ebdbf79..9276630a17 100644 --- a/src/api/menu/menu.js +++ b/src/api/menu/menu.js @@ -25,165 +25,165 @@ import SuperMenuComponent from './components/SuperMenu.vue'; import Vue from 'vue'; export const MENU_PLACEMENT = { - TOP: 'top', - TOP_LEFT: 'top-left', - TOP_RIGHT: 'top-right', - BOTTOM: 'bottom', - BOTTOM_LEFT: 'bottom-left', - BOTTOM_RIGHT: 'bottom-right', - LEFT: 'left', - RIGHT: 'right' + TOP: 'top', + TOP_LEFT: 'top-left', + TOP_RIGHT: 'top-right', + BOTTOM: 'bottom', + BOTTOM_LEFT: 'bottom-left', + BOTTOM_RIGHT: 'bottom-right', + LEFT: 'left', + RIGHT: 'right' }; class Menu extends EventEmitter { - constructor(options) { - super(); + constructor(options) { + super(); - this.options = options; - if (options.onDestroy) { - this.once('destroy', options.onDestroy); - } - - this.dismiss = this.dismiss.bind(this); - this.show = this.show.bind(this); - this.showMenu = this.showMenu.bind(this); - this.showSuperMenu = this.showSuperMenu.bind(this); + this.options = options; + if (options.onDestroy) { + this.once('destroy', options.onDestroy); } - dismiss() { - this.emit('destroy'); - document.body.removeChild(this.component.$el); - document.removeEventListener('click', this.dismiss); - this.component.$destroy(); + this.dismiss = this.dismiss.bind(this); + this.show = this.show.bind(this); + this.showMenu = this.showMenu.bind(this); + this.showSuperMenu = this.showSuperMenu.bind(this); + } + + dismiss() { + this.emit('destroy'); + document.body.removeChild(this.component.$el); + document.removeEventListener('click', this.dismiss); + this.component.$destroy(); + } + + show() { + this.component.$mount(); + document.body.appendChild(this.component.$el); + + let position = this._calculatePopupPosition(this.component.$el); + + this.component.$el.style.left = `${position.x}px`; + this.component.$el.style.top = `${position.y}px`; + + document.addEventListener('click', this.dismiss); + } + + showMenu() { + this.component = new Vue({ + components: { + MenuComponent + }, + provide: { + options: this.options + }, + template: '' + }); + + this.show(); + } + + showSuperMenu() { + this.component = new Vue({ + components: { + SuperMenuComponent + }, + provide: { + options: this.options + }, + template: '' + }); + + this.show(); + } + + /** + * @private + */ + _calculatePopupPosition(menuElement) { + let menuDimensions = menuElement.getBoundingClientRect(); + + if (!this.options.placement) { + this.options.placement = MENU_PLACEMENT.BOTTOM_RIGHT; } - show() { - this.component.$mount(); - document.body.appendChild(this.component.$el); + const menuPosition = this._getMenuPositionBasedOnPlacement(menuDimensions); - let position = this._calculatePopupPosition(this.component.$el); + return this._preventMenuOverflow(menuPosition, menuDimensions); + } - this.component.$el.style.left = `${position.x}px`; - this.component.$el.style.top = `${position.y}px`; + /** + * @private + */ + _getMenuPositionBasedOnPlacement(menuDimensions) { + let eventPosX = this.options.x; + let eventPosY = this.options.y; - document.addEventListener('click', this.dismiss); + // Adjust popup menu based on placement + switch (this.options.placement) { + case MENU_PLACEMENT.TOP: + eventPosX = this.options.x - Math.floor(menuDimensions.width / 2); + eventPosY = this.options.y - menuDimensions.height; + break; + case MENU_PLACEMENT.BOTTOM: + eventPosX = this.options.x - Math.floor(menuDimensions.width / 2); + break; + case MENU_PLACEMENT.LEFT: + eventPosX = this.options.x - menuDimensions.width; + eventPosY = this.options.y - Math.floor(menuDimensions.height / 2); + break; + case MENU_PLACEMENT.RIGHT: + eventPosY = this.options.y - Math.floor(menuDimensions.height / 2); + break; + case MENU_PLACEMENT.TOP_LEFT: + eventPosX = this.options.x - menuDimensions.width; + eventPosY = this.options.y - menuDimensions.height; + break; + case MENU_PLACEMENT.TOP_RIGHT: + eventPosY = this.options.y - menuDimensions.height; + break; + case MENU_PLACEMENT.BOTTOM_LEFT: + eventPosX = this.options.x - menuDimensions.width; + break; + case MENU_PLACEMENT.BOTTOM_RIGHT: + break; } - showMenu() { - this.component = new Vue({ - components: { - MenuComponent - }, - provide: { - options: this.options - }, - template: '' - }); + return { + x: eventPosX, + y: eventPosY + }; + } - this.show(); + /** + * @private + */ + _preventMenuOverflow(menuPosition, menuDimensions) { + let { x: eventPosX, y: eventPosY } = menuPosition; + let overflowX = eventPosX + menuDimensions.width - document.body.clientWidth; + let overflowY = eventPosY + menuDimensions.height - document.body.clientHeight; + + if (overflowX > 0) { + eventPosX = eventPosX - overflowX; } - showSuperMenu() { - this.component = new Vue({ - components: { - SuperMenuComponent - }, - provide: { - options: this.options - }, - template: '' - }); - - this.show(); + if (overflowY > 0) { + eventPosY = eventPosY - overflowY; } - /** - * @private - */ - _calculatePopupPosition(menuElement) { - let menuDimensions = menuElement.getBoundingClientRect(); - - if (!this.options.placement) { - this.options.placement = MENU_PLACEMENT.BOTTOM_RIGHT; - } - - const menuPosition = this._getMenuPositionBasedOnPlacement(menuDimensions); - - return this._preventMenuOverflow(menuPosition, menuDimensions); + if (eventPosX < 0) { + eventPosX = 0; } - /** - * @private - */ - _getMenuPositionBasedOnPlacement(menuDimensions) { - let eventPosX = this.options.x; - let eventPosY = this.options.y; - - // Adjust popup menu based on placement - switch (this.options.placement) { - case MENU_PLACEMENT.TOP: - eventPosX = this.options.x - Math.floor(menuDimensions.width / 2); - eventPosY = this.options.y - menuDimensions.height; - break; - case MENU_PLACEMENT.BOTTOM: - eventPosX = this.options.x - Math.floor(menuDimensions.width / 2); - break; - case MENU_PLACEMENT.LEFT: - eventPosX = this.options.x - menuDimensions.width; - eventPosY = this.options.y - Math.floor(menuDimensions.height / 2); - break; - case MENU_PLACEMENT.RIGHT: - eventPosY = this.options.y - Math.floor(menuDimensions.height / 2); - break; - case MENU_PLACEMENT.TOP_LEFT: - eventPosX = this.options.x - menuDimensions.width; - eventPosY = this.options.y - menuDimensions.height; - break; - case MENU_PLACEMENT.TOP_RIGHT: - eventPosY = this.options.y - menuDimensions.height; - break; - case MENU_PLACEMENT.BOTTOM_LEFT: - eventPosX = this.options.x - menuDimensions.width; - break; - case MENU_PLACEMENT.BOTTOM_RIGHT: - break; - } - - return { - x: eventPosX, - y: eventPosY - }; + if (eventPosY < 0) { + eventPosY = 0; } - /** - * @private - */ - _preventMenuOverflow(menuPosition, menuDimensions) { - let { x: eventPosX, y: eventPosY } = menuPosition; - let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth; - let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight; - - if (overflowX > 0) { - eventPosX = eventPosX - overflowX; - } - - if (overflowY > 0) { - eventPosY = eventPosY - overflowY; - } - - if (eventPosX < 0) { - eventPosX = 0; - } - - if (eventPosY < 0) { - eventPosY = 0; - } - - return { - x: eventPosX, - y: eventPosY - }; - } + return { + x: eventPosX, + y: eventPosY + }; + } } export default Menu; diff --git a/src/api/notifications/NotificationAPI.js b/src/api/notifications/NotificationAPI.js index d46cfbd039..6162f09b5e 100644 --- a/src/api/notifications/NotificationAPI.js +++ b/src/api/notifications/NotificationAPI.js @@ -84,126 +84,126 @@ const MINIMIZE_ANIMATION_TIMEOUT = 300; /** * The notification service is responsible for informing the user of * events via the use of banner notifications. -*/ + */ export default class NotificationAPI extends EventEmitter { - constructor() { - super(); - /** @type {Notification[]} */ - this.notifications = []; - /** @type {{severity: "info" | "alert" | "error"}} */ - this.highest = { severity: "info" }; - - /** - * A context in which to hold the active notification and a - * handle to its timeout. - * @type {Notification | undefined} - */ - this.activeNotification = undefined; - } + constructor() { + super(); + /** @type {Notification[]} */ + this.notifications = []; + /** @type {{severity: "info" | "alert" | "error"}} */ + this.highest = { severity: 'info' }; /** - * Info notifications are low priority informational messages for the user. They will be auto-destroy after a brief - * period of time. - * @param {string} message The message to display to the user - * @param {NotificationOptions} [options] The notification options - * @returns {Notification} + * A context in which to hold the active notification and a + * handle to its timeout. + * @type {Notification | undefined} */ - info(message, options = {}) { - /** @type {NotificationModel} */ - const notificationModel = { - message: message, - autoDismiss: true, - severity: "info", - options - }; + this.activeNotification = undefined; + } - return this._notify(notificationModel); + /** + * Info notifications are low priority informational messages for the user. They will be auto-destroy after a brief + * period of time. + * @param {string} message The message to display to the user + * @param {NotificationOptions} [options] The notification options + * @returns {Notification} + */ + info(message, options = {}) { + /** @type {NotificationModel} */ + const notificationModel = { + message: message, + autoDismiss: true, + severity: 'info', + options + }; + + return this._notify(notificationModel); + } + + /** + * Present an alert to the user. + * @param {string} message The message to display to the user. + * @param {NotificationOptions} [options] object with following properties + * autoDismissTimeout: {number} in milliseconds to automatically dismisses notification + * link: {Object} Add a link to notifications for navigation + * onClick: callback function + * cssClass: css class name to add style on link + * text: text to display for link + * @returns {Notification} + */ + alert(message, options = {}) { + const notificationModel = { + message: message, + severity: 'alert', + options + }; + + return this._notify(notificationModel); + } + + /** + * Present an error message to the user + * @param {string} message + * @param {Object} [options] object with following properties + * autoDismissTimeout: {number} in milliseconds to automatically dismisses notification + * link: {Object} Add a link to notifications for navigation + * onClick: callback function + * cssClass: css class name to add style on link + * text: text to display for link + * @returns {Notification} + */ + error(message, options = {}) { + let notificationModel = { + message: message, + severity: 'error', + options + }; + + return this._notify(notificationModel); + } + + /** + * Create a new progress notification. These notifications will contain a progress bar. + * @param {string} message + * @param {number | 'unknown'} progressPerc A value between 0 and 100, or the string 'unknown'. + * @param {string} [progressText] Text description of progress (eg. "10 of 20 objects copied"). + */ + progress(message, progressPerc, progressText) { + let notificationModel = { + message: message, + progressPerc: progressPerc, + progressText: progressText, + severity: 'info', + options: {} + }; + + return this._notify(notificationModel); + } + + dismissAllNotifications() { + this.notifications = []; + this.emit('dismiss-all'); + } + + /** + * Minimize a notification. The notification will still be available + * from the notification list. Typically notifications with a + * severity of 'info' should not be minimized, but rather + * dismissed. + * + * @private + * @param {Notification | undefined} notification + */ + _minimize(notification) { + if (!notification) { + return; } - /** - * Present an alert to the user. - * @param {string} message The message to display to the user. - * @param {NotificationOptions} [options] object with following properties - * autoDismissTimeout: {number} in milliseconds to automatically dismisses notification - * link: {Object} Add a link to notifications for navigation - * onClick: callback function - * cssClass: css class name to add style on link - * text: text to display for link - * @returns {Notification} - */ - alert(message, options = {}) { - const notificationModel = { - message: message, - severity: "alert", - options - }; + //Check this is a known notification + let index = this.notifications.indexOf(notification); - return this._notify(notificationModel); - } - - /** - * Present an error message to the user - * @param {string} message - * @param {Object} [options] object with following properties - * autoDismissTimeout: {number} in milliseconds to automatically dismisses notification - * link: {Object} Add a link to notifications for navigation - * onClick: callback function - * cssClass: css class name to add style on link - * text: text to display for link - * @returns {Notification} - */ - error(message, options = {}) { - let notificationModel = { - message: message, - severity: "error", - options - }; - - return this._notify(notificationModel); - } - - /** - * Create a new progress notification. These notifications will contain a progress bar. - * @param {string} message - * @param {number | 'unknown'} progressPerc A value between 0 and 100, or the string 'unknown'. - * @param {string} [progressText] Text description of progress (eg. "10 of 20 objects copied"). - */ - progress(message, progressPerc, progressText) { - let notificationModel = { - message: message, - progressPerc: progressPerc, - progressText: progressText, - severity: "info", - options: {} - }; - - return this._notify(notificationModel); - } - - dismissAllNotifications() { - this.notifications = []; - this.emit('dismiss-all'); - } - - /** - * Minimize a notification. The notification will still be available - * from the notification list. Typically notifications with a - * severity of 'info' should not be minimized, but rather - * dismissed. - * - * @private - * @param {Notification | undefined} notification - */ - _minimize(notification) { - if (!notification) { - return; - } - - //Check this is a known notification - let index = this.notifications.indexOf(notification); - - if (this.activeTimeout) { - /* + if (this.activeTimeout) { + /* Method can be called manually (clicking dismiss) or automatically from an auto-timeout. this.activeTimeout acts as a semaphore to prevent race conditions. Cancel any @@ -211,127 +211,127 @@ export default class NotificationAPI extends EventEmitter { has shortcut an active auto-dismiss), and clear the semaphore. */ - clearTimeout(this.activeTimeout); - delete this.activeTimeout; - } - - if (index >= 0) { - notification.model.minimized = true; - notification.emit('minimized'); - //Add a brief timeout before showing the next notification - // in order to allow the minimize animation to run through. - setTimeout(() => { - notification.emit('destroy'); - this._setActiveNotification(this._selectNextNotification()); - }, MINIMIZE_ANIMATION_TIMEOUT); - } + clearTimeout(this.activeTimeout); + delete this.activeTimeout; } - /** - * Completely removes a notification. This will dismiss it from the - * message banner and remove it from the list of notifications. - * Typically only notifications with a severity of info should be - * dismissed. If you're not sure whether to dismiss or minimize a - * notification, use {@link Notification#dismissOrMinimize}. - * dismiss - * - * @private - * @param {Notification | undefined} notification - */ - _dismiss(notification) { - if (!notification) { - return; - } - - //Check this is a known notification - let index = this.notifications.indexOf(notification); - - if (this.activeTimeout) { - /* Method can be called manually (clicking dismiss) or - * automatically from an auto-timeout. this.activeTimeout - * acts as a semaphore to prevent race conditions. Cancel any - * timeout in progress (for the case where a manual dismiss - * has shortcut an active auto-dismiss), and clear the - * semaphore. - */ - - clearTimeout(this.activeTimeout); - delete this.activeTimeout; - } - - if (index >= 0) { - this.notifications.splice(index, 1); - } - - this._setActiveNotification(this._selectNextNotification()); - this._setHighestSeverity(); + if (index >= 0) { + notification.model.minimized = true; + notification.emit('minimized'); + //Add a brief timeout before showing the next notification + // in order to allow the minimize animation to run through. + setTimeout(() => { notification.emit('destroy'); + this._setActiveNotification(this._selectNextNotification()); + }, MINIMIZE_ANIMATION_TIMEOUT); + } + } + + /** + * Completely removes a notification. This will dismiss it from the + * message banner and remove it from the list of notifications. + * Typically only notifications with a severity of info should be + * dismissed. If you're not sure whether to dismiss or minimize a + * notification, use {@link Notification#dismissOrMinimize}. + * dismiss + * + * @private + * @param {Notification | undefined} notification + */ + _dismiss(notification) { + if (!notification) { + return; } - /** - * Depending on the severity of the notification will selectively - * dismiss or minimize where appropriate. - * - * @private - * @param {Notification | undefined} notification - */ - _dismissOrMinimize(notification) { - let model = notification?.model; - if (model?.severity === "info") { - this._dismiss(notification); - } else { - this._minimize(notification); - } + //Check this is a known notification + let index = this.notifications.indexOf(notification); + + if (this.activeTimeout) { + /* Method can be called manually (clicking dismiss) or + * automatically from an auto-timeout. this.activeTimeout + * acts as a semaphore to prevent race conditions. Cancel any + * timeout in progress (for the case where a manual dismiss + * has shortcut an active auto-dismiss), and clear the + * semaphore. + */ + + clearTimeout(this.activeTimeout); + delete this.activeTimeout; } - /** - * @private - */ - _setHighestSeverity() { - let severity = { - info: 1, - alert: 2, - error: 3 - }; - - this.highest.severity = this.notifications.reduce((previous, notification) => { - if (severity[notification.model.severity] > severity[previous]) { - return notification.model.severity; - } else { - return previous; - } - }, "info"); + if (index >= 0) { + this.notifications.splice(index, 1); } - /** - * Notifies the user of an event. If there is a banner notification - * already active, then it will be dismissed or minimized automatically, - * and the provided notification displayed in its place. - * - * @param {NotificationModel} notificationModel The notification to - * display - * @returns {Notification} the provided notification decorated with - * functions to {@link Notification#dismiss} or {@link Notification#minimize} - */ - _notify(notificationModel) { - let notification; - let activeNotification = this.activeNotification; + this._setActiveNotification(this._selectNextNotification()); + this._setHighestSeverity(); + notification.emit('destroy'); + } - notificationModel.severity = notificationModel.severity || "info"; - notificationModel.timestamp = moment.utc().format('YYYY-MM-DD hh:mm:ss.ms'); + /** + * Depending on the severity of the notification will selectively + * dismiss or minimize where appropriate. + * + * @private + * @param {Notification | undefined} notification + */ + _dismissOrMinimize(notification) { + let model = notification?.model; + if (model?.severity === 'info') { + this._dismiss(notification); + } else { + this._minimize(notification); + } + } - notification = this._createNotification(notificationModel); + /** + * @private + */ + _setHighestSeverity() { + let severity = { + info: 1, + alert: 2, + error: 3 + }; - this.notifications.push(notification); - this._setHighestSeverity(); + this.highest.severity = this.notifications.reduce((previous, notification) => { + if (severity[notification.model.severity] > severity[previous]) { + return notification.model.severity; + } else { + return previous; + } + }, 'info'); + } - /* + /** + * Notifies the user of an event. If there is a banner notification + * already active, then it will be dismissed or minimized automatically, + * and the provided notification displayed in its place. + * + * @param {NotificationModel} notificationModel The notification to + * display + * @returns {Notification} the provided notification decorated with + * functions to {@link Notification#dismiss} or {@link Notification#minimize} + */ + _notify(notificationModel) { + let notification; + let activeNotification = this.activeNotification; + + notificationModel.severity = notificationModel.severity || 'info'; + notificationModel.timestamp = moment.utc().format('YYYY-MM-DD hh:mm:ss.ms'); + + notification = this._createNotification(notificationModel); + + this.notifications.push(notification); + this._setHighestSeverity(); + + /* Check if there is already an active (ie. visible) notification */ - if (!this.activeNotification && !notification?.model?.options?.minimized) { - this._setActiveNotification(notification); - } else if (!this.activeTimeout) { - /* + if (!this.activeNotification && !notification?.model?.options?.minimized) { + this._setActiveNotification(notification); + } else if (!this.activeTimeout) { + /* If there is already an active notification, time it out. If it's already got a timeout in progress (either because it has had timeout forced because of a queue of messages, or it had an @@ -341,87 +341,86 @@ export default class NotificationAPI extends EventEmitter { This notification has been added to queue and will be serviced as soon as possible. */ - this.activeTimeout = setTimeout(() => { - this._dismissOrMinimize(activeNotification); - }, DEFAULT_AUTO_DISMISS_TIMEOUT); - } - - return notification; + this.activeTimeout = setTimeout(() => { + this._dismissOrMinimize(activeNotification); + }, DEFAULT_AUTO_DISMISS_TIMEOUT); } - /** - * @private - * @param {NotificationModel} notificationModel - * @returns {Notification} - */ - _createNotification(notificationModel) { - /** @type {Notification} */ - let notification = new EventEmitter(); - notification.model = notificationModel; - notification.dismiss = () => { - this._dismiss(notification); - }; + return notification; + } - if (Object.prototype.hasOwnProperty.call(notificationModel, 'progressPerc')) { - notification.progress = (progressPerc, progressText) => { - notification.model.progressPerc = progressPerc; - notification.model.progressText = progressText; - notification.emit('progress', progressPerc, progressText); - }; - } + /** + * @private + * @param {NotificationModel} notificationModel + * @returns {Notification} + */ + _createNotification(notificationModel) { + /** @type {Notification} */ + let notification = new EventEmitter(); + notification.model = notificationModel; + notification.dismiss = () => { + this._dismiss(notification); + }; - return notification; + if (Object.prototype.hasOwnProperty.call(notificationModel, 'progressPerc')) { + notification.progress = (progressPerc, progressText) => { + notification.model.progressPerc = progressPerc; + notification.model.progressText = progressText; + notification.emit('progress', progressPerc, progressText); + }; } - /** - * @private - * @param {Notification | undefined} notification - */ - _setActiveNotification(notification) { - this.activeNotification = notification; + return notification; + } - if (!notification) { - delete this.activeTimeout; + /** + * @private + * @param {Notification | undefined} notification + */ + _setActiveNotification(notification) { + this.activeNotification = notification; - return; - } + if (!notification) { + delete this.activeTimeout; - this.emit('notification', notification); - - if (notification.model.autoDismiss || this._selectNextNotification()) { - const autoDismissTimeout = notification.model.options.autoDismissTimeout - || DEFAULT_AUTO_DISMISS_TIMEOUT; - this.activeTimeout = setTimeout(() => { - this._dismissOrMinimize(notification); - }, autoDismissTimeout); - } else { - delete this.activeTimeout; - } + return; } - /** - * Used internally by the NotificationService - * - * @private - */ - _selectNextNotification() { - let notification; - let i = 0; + this.emit('notification', notification); - /* + if (notification.model.autoDismiss || this._selectNextNotification()) { + const autoDismissTimeout = + notification.model.options.autoDismissTimeout || DEFAULT_AUTO_DISMISS_TIMEOUT; + this.activeTimeout = setTimeout(() => { + this._dismissOrMinimize(notification); + }, autoDismissTimeout); + } else { + delete this.activeTimeout; + } + } + + /** + * Used internally by the NotificationService + * + * @private + */ + _selectNextNotification() { + let notification; + let i = 0; + + /* Loop through the notifications queue and find the first one that has not already been minimized (manually or otherwise). */ - for (; i < this.notifications.length; i++) { - notification = this.notifications[i]; + for (; i < this.notifications.length; i++) { + notification = this.notifications[i]; - const isNotificationMinimized = notification.model.minimized - || notification?.model?.options?.minimized; + const isNotificationMinimized = + notification.model.minimized || notification?.model?.options?.minimized; - if (!isNotificationMinimized - && notification !== this.activeNotification) { - return notification; - } - } + if (!isNotificationMinimized && notification !== this.activeNotification) { + return notification; + } } + } } diff --git a/src/api/notifications/NotificationAPISpec.js b/src/api/notifications/NotificationAPISpec.js index f98aeccf32..b3e93bebbd 100644 --- a/src/api/notifications/NotificationAPISpec.js +++ b/src/api/notifications/NotificationAPISpec.js @@ -23,150 +23,150 @@ import NotificationAPI from './NotificationAPI'; describe('The Notifiation API', () => { - let notificationAPIInstance; - let defaultTimeout = 4000; + let notificationAPIInstance; + let defaultTimeout = 4000; + + beforeAll(() => { + notificationAPIInstance = new NotificationAPI(); + }); + + describe('the info method', () => { + let message = 'Example Notification Message'; + let severity = 'info'; + let notificationModel; beforeAll(() => { - notificationAPIInstance = new NotificationAPI(); + notificationModel = notificationAPIInstance.info(message).model; }); - describe('the info method', () => { - let message = 'Example Notification Message'; - let severity = 'info'; - let notificationModel; - - beforeAll(() => { - notificationModel = notificationAPIInstance.info(message).model; - }); - - afterAll(() => { - notificationAPIInstance.dismissAllNotifications(); - }); - - it('shows a string message with info severity', () => { - expect(notificationModel.message).toEqual(message); - expect(notificationModel.severity).toEqual(severity); - }); - - it('auto dismisses the notification after a brief timeout', (done) => { - window.setTimeout(() => { - expect(notificationAPIInstance.notifications.length).toEqual(0); - done(); - }, defaultTimeout); - }); + afterAll(() => { + notificationAPIInstance.dismissAllNotifications(); }); - describe('the alert method', () => { - let message = 'Example alert message'; - let severity = 'alert'; - let notificationModel; - - beforeAll(() => { - notificationModel = notificationAPIInstance.alert(message).model; - }); - - afterAll(() => { - notificationAPIInstance.dismissAllNotifications(); - }); - - it('shows a string message, with alert severity', () => { - expect(notificationModel.message).toEqual(message); - expect(notificationModel.severity).toEqual(severity); - }); - - it('does not auto dismiss the notification', (done) => { - window.setTimeout(() => { - expect(notificationAPIInstance.notifications.length).toEqual(1); - done(); - }, defaultTimeout); - }); + it('shows a string message with info severity', () => { + expect(notificationModel.message).toEqual(message); + expect(notificationModel.severity).toEqual(severity); }); - describe('the error method', () => { - let message = 'Example error message'; - let severity = 'error'; - let notificationModel; + it('auto dismisses the notification after a brief timeout', (done) => { + window.setTimeout(() => { + expect(notificationAPIInstance.notifications.length).toEqual(0); + done(); + }, defaultTimeout); + }); + }); - beforeAll(() => { - notificationModel = notificationAPIInstance.error(message).model; - }); + describe('the alert method', () => { + let message = 'Example alert message'; + let severity = 'alert'; + let notificationModel; - afterAll(() => { - notificationAPIInstance.dismissAllNotifications(); - }); - - it('shows a string message, with severity error', () => { - expect(notificationModel.message).toEqual(message); - expect(notificationModel.severity).toEqual(severity); - }); - - it('does not auto dismiss the notification', (done) => { - window.setTimeout(() => { - expect(notificationAPIInstance.notifications.length).toEqual(1); - done(); - }, defaultTimeout); - }); + beforeAll(() => { + notificationModel = notificationAPIInstance.alert(message).model; }); - describe('the error method notificiation', () => { - let message = 'Minimized error message'; - - afterAll(() => { - notificationAPIInstance.dismissAllNotifications(); - }); - - it('is not shown if configured to show minimized', (done) => { - notificationAPIInstance.activeNotification = undefined; - notificationAPIInstance.error(message, { minimized: true }); - window.setTimeout(() => { - expect(notificationAPIInstance.notifications.length).toEqual(1); - expect(notificationAPIInstance.activeNotification).toEqual(undefined); - done(); - }, defaultTimeout); - }); + afterAll(() => { + notificationAPIInstance.dismissAllNotifications(); }); - describe('the progress method', () => { - let title = 'This is a progress notification'; - let message1 = 'Example progress message 1'; - let message2 = 'Example progress message 2'; - let percentage1 = 50; - let percentage2 = 99.9; - let severity = 'info'; - let notification; - let updatedPercentage; - let updatedMessage; - - beforeAll(() => { - notification = notificationAPIInstance.progress(title, percentage1, message1); - notification.on('progress', (percentage, text) => { - updatedPercentage = percentage; - updatedMessage = text; - }); - }); - - afterAll(() => { - notificationAPIInstance.dismissAllNotifications(); - }); - - it ('shows a notification with a message, progress message, percentage and info severity', () => { - expect(notification.model.message).toEqual(title); - expect(notification.model.severity).toEqual(severity); - expect(notification.model.progressText).toEqual(message1); - expect(notification.model.progressPerc).toEqual(percentage1); - }); - - it ('allows dynamically updating the progress attributes', () => { - notification.progress(percentage2, message2); - - expect(updatedPercentage).toEqual(percentage2); - expect(updatedMessage).toEqual(message2); - }); - - it ('allows dynamically dismissing of progress notification', () => { - notification.dismiss(); - - expect(notificationAPIInstance.notifications.length).toEqual(0); - }); + it('shows a string message, with alert severity', () => { + expect(notificationModel.message).toEqual(message); + expect(notificationModel.severity).toEqual(severity); }); + + it('does not auto dismiss the notification', (done) => { + window.setTimeout(() => { + expect(notificationAPIInstance.notifications.length).toEqual(1); + done(); + }, defaultTimeout); + }); + }); + + describe('the error method', () => { + let message = 'Example error message'; + let severity = 'error'; + let notificationModel; + + beforeAll(() => { + notificationModel = notificationAPIInstance.error(message).model; + }); + + afterAll(() => { + notificationAPIInstance.dismissAllNotifications(); + }); + + it('shows a string message, with severity error', () => { + expect(notificationModel.message).toEqual(message); + expect(notificationModel.severity).toEqual(severity); + }); + + it('does not auto dismiss the notification', (done) => { + window.setTimeout(() => { + expect(notificationAPIInstance.notifications.length).toEqual(1); + done(); + }, defaultTimeout); + }); + }); + + describe('the error method notificiation', () => { + let message = 'Minimized error message'; + + afterAll(() => { + notificationAPIInstance.dismissAllNotifications(); + }); + + it('is not shown if configured to show minimized', (done) => { + notificationAPIInstance.activeNotification = undefined; + notificationAPIInstance.error(message, { minimized: true }); + window.setTimeout(() => { + expect(notificationAPIInstance.notifications.length).toEqual(1); + expect(notificationAPIInstance.activeNotification).toEqual(undefined); + done(); + }, defaultTimeout); + }); + }); + + describe('the progress method', () => { + let title = 'This is a progress notification'; + let message1 = 'Example progress message 1'; + let message2 = 'Example progress message 2'; + let percentage1 = 50; + let percentage2 = 99.9; + let severity = 'info'; + let notification; + let updatedPercentage; + let updatedMessage; + + beforeAll(() => { + notification = notificationAPIInstance.progress(title, percentage1, message1); + notification.on('progress', (percentage, text) => { + updatedPercentage = percentage; + updatedMessage = text; + }); + }); + + afterAll(() => { + notificationAPIInstance.dismissAllNotifications(); + }); + + it('shows a notification with a message, progress message, percentage and info severity', () => { + expect(notification.model.message).toEqual(title); + expect(notification.model.severity).toEqual(severity); + expect(notification.model.progressText).toEqual(message1); + expect(notification.model.progressPerc).toEqual(percentage1); + }); + + it('allows dynamically updating the progress attributes', () => { + notification.progress(percentage2, message2); + + expect(updatedPercentage).toEqual(percentage2); + expect(updatedMessage).toEqual(message2); + }); + + it('allows dynamically dismissing of progress notification', () => { + notification.dismiss(); + + expect(notificationAPIInstance.notifications.length).toEqual(0); + }); + }); }); diff --git a/src/api/objects/ConflictError.js b/src/api/objects/ConflictError.js index a75f0e169b..bf996e96e6 100644 --- a/src/api/objects/ConflictError.js +++ b/src/api/objects/ConflictError.js @@ -1,2 +1 @@ -export default class ConflictError extends Error { -} +export default class ConflictError extends Error {} diff --git a/src/api/objects/InMemorySearchProvider.js b/src/api/objects/InMemorySearchProvider.js index dcf9f8ac32..3082ff9ffe 100644 --- a/src/api/objects/InMemorySearchProvider.js +++ b/src/api/objects/InMemorySearchProvider.js @@ -23,554 +23,560 @@ import { v4 as uuid } from 'uuid'; class InMemorySearchProvider { + /** + * A search service which searches through domain objects in + * the filetree without using external search implementations. + * + * @constructor + * @param {Object} openmct + */ + constructor(openmct) { /** - * A search service which searches through domain objects in - * the filetree without using external search implementations. - * - * @constructor - * @param {Object} openmct + * Maximum number of concurrent index requests to allow. */ - constructor(openmct) { - /** - * Maximum number of concurrent index requests to allow. - */ - this.MAX_CONCURRENT_REQUESTS = 100; - /** - * 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.idsToIndex = []; - this.pendingIndex = {}; - this.pendingRequests = 0; - this.worker = null; - - /** - * If we don't have SharedWorkers available (e.g., iOS) - */ - 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.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); - - this.openmct.on('start', this.startIndexing); - this.openmct.on('destroy', () => { - if (this.worker && this.worker.port) { - this.worker.onerror = null; - this.worker.port.onmessage = null; - this.worker.port.onmessageerror = null; - 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); - }); - } - - 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.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); - } - }); - } - } - }); - } + this.MAX_CONCURRENT_REQUESTS = 100; + /** + * 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.idsToIndex = []; + this.pendingIndex = {}; + this.pendingRequests = 0; + this.worker = null; /** - * @private + * If we don't have SharedWorkers available (e.g., iOS) */ - getIntermediateResponse() { - let intermediateResponse = {}; - intermediateResponse.promise = new Promise(function (resolve, reject) { - intermediateResponse.resolve = resolve; - intermediateResponse.reject = reject; - }); + this.localIndexedDomainObjects = {}; + this.localIndexedAnnotationsByDomainObject = {}; + this.localIndexedAnnotationsByTag = {}; - return intermediateResponse; - } + 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.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); - 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.#dispatchSearchToWorker(searchOptions); - } else { - 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.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. - * @private - */ - async onWorkerMessage(event) { - const pendingQuery = this.pendingQueries[event.data.queryId]; - const modelResults = { - total: event.data.total - }; - modelResults.hits = await Promise.all(event.data.results.map(async (hit) => { - if (hit && hit.keyString) { - const identifier = this.openmct.objects.parseKeyString(hit.keyString); - const domainObject = await this.openmct.objects.get(identifier); - - return domainObject; - } - })); - - pendingQuery.resolve(modelResults); - delete this.pendingQueries[event.data.queryId]; - } - - /** - * Handle error messages from the worker. - * @private - */ - onWorkerMessageError(event) { - console.error('⚙️ Error message from InMemorySearch worker ⚙️', event); - } - - /** - * Handle errors from the worker. - * @private - */ - onWorkerError(event) { - console.error('⚙️ Error with InMemorySearch worker ⚙️', event); - } - - /** - * @private - */ - startSharedWorker() { - // eslint-disable-next-line no-undef - const sharedWorkerURL = `${this.openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}inMemorySearchWorker.js`; - - const sharedWorker = new SharedWorker(sharedWorkerURL, 'InMemorySearch Shared Worker'); - sharedWorker.onerror = this.onWorkerError; - sharedWorker.port.onmessage = this.onWorkerMessage; - sharedWorker.port.onmessageerror = this.onWorkerMessageError; - sharedWorker.port.start(); - - return sharedWorker; - } - - /** - * Schedule an id to be indexed at a later date. If there are less - * 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. - */ - scheduleForIndexing(identifier) { - const keyString = this.openmct.objects.makeKeyString(identifier); - const objectProvider = this.openmct.objects.getProvider(identifier); - - if (objectProvider === undefined || objectProvider.search === undefined) { - if (!this.indexedIds[keyString] && !this.pendingIndex[keyString]) { - this.pendingIndex[keyString] = true; - this.idsToIndex.push(keyString); - } - } - - this.keepIndexing(); - } - - /** - * If there are less pending requests than concurrent requests, keep - * firing requests. - * - * @private - */ - keepIndexing() { - while (this.pendingRequests < this.MAX_CONCURRENT_REQUESTS - && this.idsToIndex.length - ) { - this.beginIndexRequest(); - } - } - - 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; - - domainObject.name = name; - 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]; - } + this.openmct.on('start', this.startIndexing); + this.openmct.on('destroy', () => { + if (this.worker && this.worker.port) { + this.worker.onerror = null; + this.worker.port.onmessage = null; + this.worker.port.onmessageerror = null; + this.worker.port.close(); + } + Object.keys(this.indexedCompositions).forEach((keyString) => { const composition = this.indexedCompositions[keyString]; - if (composition) { - composition.off('add', this.onCompositionAdded); - composition.off('remove', this.onCompositionRemoved); - delete this.indexedCompositions[keyString]; - } + composition.off('add', this.onCompositionAdded); + composition.off('remove', this.onCompositionRemoved); + }); + + this.destroyObservers(this.indexedIds); + this.destroyObservers(this.indexedCompositions); + }); + } + + 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.TAGS + ]; + + this.scheduleForIndexing(rootObject.identifier); + + this.indexAnnotations(); + + if (typeof SharedWorker !== 'undefined') { + this.worker = this.startSharedWorker(); + } else { + // we must be on iOS } - /** - * Pass a domainObject to the worker to be indexed. - * If the object has composition, schedule those ids for later indexing. - * Watch for object changes and re-index object and children if so - * - * @private - * @param domainObject a domainObject - */ - async index(domainObject) { - const provider = this; - const keyString = this.openmct.objects.makeKeyString(domainObject.identifier); - const composition = this.openmct.composition.get(domainObject); + this.openmct.annotation.on('annotationCreated', this.onAnnotationCreation); + } - if (!this.indexedIds[keyString]) { - this.indexedIds[keyString] = this.openmct.objects.observe( - domainObject, - 'name', - this.onNameMutation.bind(this, domainObject) - ); - if (composition) { - composition.on('add', this.onCompositionAdded); - composition.on('remove', this.onCompositionRemoved); - this.indexedCompositions[keyString] = composition; + 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); } + }); } + } + }); + } - if ((keyString !== 'ROOT')) { - if (this.worker) { - this.worker.port.postMessage({ - request: 'index', - model: domainObject, - keyString - }); - } else { - this.localIndexItem(keyString, domainObject); - } - } + /** + * @private + */ + getIntermediateResponse() { + let intermediateResponse = {}; + intermediateResponse.promise = new Promise(function (resolve, reject) { + intermediateResponse.resolve = resolve; + intermediateResponse.reject = reject; + }); - if (composition !== undefined) { - const children = await composition.load(); + return intermediateResponse; + } - children.forEach(child => provider.scheduleForIndexing(child.identifier)); - } + 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.#dispatchSearchToWorker(searchOptions); + } else { + this.#localQueryFallBack(searchOptions); } - /** - * Pulls an id from the indexing queue, loads it from the model service, - * and indexes it. Upon completion, tells the provider to keep - * indexing. - * - * @private - */ - async beginIndexRequest() { - const keyString = this.idsToIndex.shift(); - const provider = this; + return pendingQuery.promise; + } - this.pendingRequests += 1; - const domainObject = await this.openmct.objects.get(keyString); - delete provider.pendingIndex[keyString]; + #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.TAGS) { + return this.localSearchForTags(queryId, query, maxResults); + } else { + throw new Error(`🤷‍♂️ Unknown search type passed: ${searchType}`); + } + } - try { - if (domainObject) { - await provider.index(domainObject); - } - } catch (error) { - console.warn('Failed to index domain object ' + keyString, error); + supportsSearchType(searchType) { + return this.supportedSearchTypes.includes(searchType); + } + + /** + * Handle messages from the worker. + * @private + */ + async onWorkerMessage(event) { + const pendingQuery = this.pendingQueries[event.data.queryId]; + const modelResults = { + total: event.data.total + }; + modelResults.hits = await Promise.all( + event.data.results.map(async (hit) => { + if (hit && hit.keyString) { + const identifier = this.openmct.objects.parseKeyString(hit.keyString); + const domainObject = await this.openmct.objects.get(identifier); + + return domainObject; } + }) + ); - setTimeout(function () { - provider.pendingRequests -= 1; - provider.keepIndexing(); - }, 0); + pendingQuery.resolve(modelResults); + delete this.pendingQueries[event.data.queryId]; + } + + /** + * Handle error messages from the worker. + * @private + */ + onWorkerMessageError(event) { + console.error('⚙️ Error message from InMemorySearch worker ⚙️', event); + } + + /** + * Handle errors from the worker. + * @private + */ + onWorkerError(event) { + console.error('⚙️ Error with InMemorySearch worker ⚙️', event); + } + + /** + * @private + */ + startSharedWorker() { + // eslint-disable-next-line no-undef + const sharedWorkerURL = `${this.openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}inMemorySearchWorker.js`; + + const sharedWorker = new SharedWorker(sharedWorkerURL, 'InMemorySearch Shared Worker'); + sharedWorker.onerror = this.onWorkerError; + sharedWorker.port.onmessage = this.onWorkerMessage; + sharedWorker.port.onmessageerror = this.onWorkerMessageError; + sharedWorker.port.start(); + + return sharedWorker; + } + + /** + * Schedule an id to be indexed at a later date. If there are less + * 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. + */ + scheduleForIndexing(identifier) { + const keyString = this.openmct.objects.makeKeyString(identifier); + const objectProvider = this.openmct.objects.getProvider(identifier); + + if (objectProvider === undefined || objectProvider.search === undefined) { + if (!this.indexedIds[keyString] && !this.pendingIndex[keyString]) { + this.pendingIndex[keyString] = true; + this.idsToIndex.push(keyString); + } } - /** - * Dispatch a search query to the worker and return a queryId. - * - * @private - * @returns {String} a unique query Id for the query. - */ - #dispatchSearchToWorker({queryId, searchType, query, maxResults}) { - const message = { - request: searchType.toString(), - input: query, - maxResults, - queryId - }; - this.worker.port.postMessage(message); + this.keepIndexing(); + } + + /** + * If there are less pending requests than concurrent requests, keep + * firing requests. + * + * @private + */ + keepIndexing() { + while (this.pendingRequests < this.MAX_CONCURRENT_REQUESTS && this.idsToIndex.length) { + this.beginIndexRequest(); + } + } + + 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; + + domainObject.name = name; + 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]; } - localIndexTags(keyString, objectToIndex, model) { - // add new tags - model.tags.forEach(tagID => { - if (!this.localIndexedAnnotationsByTag[tagID]) { - this.localIndexedAnnotationsByTag[tagID] = []; - } + const composition = this.indexedCompositions[keyString]; + if (composition) { + composition.off('add', this.onCompositionAdded); + composition.off('remove', this.onCompositionRemoved); + delete this.indexedCompositions[keyString]; + } + } - const existsInIndex = this.localIndexedAnnotationsByTag[tagID].some(indexedObject => { - return indexedObject.keyString === objectToIndex.keyString; + /** + * Pass a domainObject to the worker to be indexed. + * If the object has composition, schedule those ids for later indexing. + * Watch for object changes and re-index object and children if so + * + * @private + * @param domainObject a domainObject + */ + 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( + domainObject, + 'name', + this.onNameMutation.bind(this, domainObject) + ); + if (composition) { + composition.on('add', this.onCompositionAdded); + composition.on('remove', this.onCompositionRemoved); + this.indexedCompositions[keyString] = composition; + } + } + + if (keyString !== 'ROOT') { + if (this.worker) { + this.worker.port.postMessage({ + request: 'index', + model: domainObject, + keyString + }); + } else { + this.localIndexItem(keyString, domainObject); + } + } + + if (composition !== undefined) { + const children = await composition.load(); + + children.forEach((child) => provider.scheduleForIndexing(child.identifier)); + } + } + + /** + * Pulls an id from the indexing queue, loads it from the model service, + * and indexes it. Upon completion, tells the provider to keep + * indexing. + * + * @private + */ + async beginIndexRequest() { + const keyString = this.idsToIndex.shift(); + const provider = this; + + this.pendingRequests += 1; + const domainObject = await this.openmct.objects.get(keyString); + delete provider.pendingIndex[keyString]; + + try { + if (domainObject) { + await provider.index(domainObject); + } + } catch (error) { + console.warn('Failed to index domain object ' + keyString, error); + } + + setTimeout(function () { + provider.pendingRequests -= 1; + provider.keepIndexing(); + }, 0); + } + + /** + * Dispatch a search query to the worker and return a queryId. + * + * @private + * @returns {String} a unique query Id for the query. + */ + #dispatchSearchToWorker({ queryId, searchType, query, maxResults }) { + const message = { + 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) { + 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; + } + } + + /** + * A local version of the same SharedWorker function + * if we don't have SharedWorkers available (e.g., iOS) + * + * Gets search results from the indexedItems based on provided search + * input. Returns matching results from indexedItems + */ + 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 = []; + const input = searchInput.trim().toLowerCase(); + const message = { + request: 'searchForObjects', + results: [], + total: 0, + queryId + }; + + 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 (!existsInIndex) { - this.localIndexedAnnotationsByTag[tagID].push(objectToIndex); + if (!existsInResults) { + results.push(matchingAnnotation); } - - }); - 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) { - 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; + }); } + }); } - /** - * A local version of the same SharedWorker function - * if we don't have SharedWorkers available (e.g., iOS) - * - * Gets search results from the indexedItems based on provided search - * input. Returns matching results from indexedItems - */ - 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 = []; - const input = searchInput.trim().toLowerCase(); - const message = { - request: 'searchForObjects', - results: [], - total: 0, - queryId - }; + message.total = results.length; + message.results = results.slice(0, maxResults); + const eventToReturn = { + data: message + }; + this.onWorkerMessage(eventToReturn); + } - results = Object.values(this.localIndexedDomainObjects).filter((indexedItem) => { - return indexedItem.name.toLowerCase().includes(input); - }) || []; + destroyObservers(observers) { + Object.entries(observers).forEach(([keyString, unobserve]) => { + if (typeof unobserve === 'function') { + unobserve(); + } - 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); - } - - destroyObservers(observers) { - Object.entries(observers).forEach(([keyString, unobserve]) => { - if (typeof unobserve === 'function') { - unobserve(); - } - - delete observers[keyString]; - }); - } + delete observers[keyString]; + }); + } } export default InMemorySearchProvider; diff --git a/src/api/objects/InMemorySearchWorker.js b/src/api/objects/InMemorySearchWorker.js index fd235a9c4c..121d3b1d26 100644 --- a/src/api/objects/InMemorySearchWorker.js +++ b/src/api/objects/InMemorySearchWorker.js @@ -24,182 +24,180 @@ * Module defining InMemorySearchWorker. Created by deeptailor on 10/03/2019. */ (function () { - // An object composed of domain object IDs and models - // {id: domainObject's ID, name: domainObject's name} - const indexedDomainObjects = {}; - const indexedAnnotationsByDomainObject = {}; - const indexedAnnotationsByTag = {}; + // An object composed of domain object IDs and models + // {id: domainObject's ID, name: domainObject's name} + const indexedDomainObjects = {}; + const indexedAnnotationsByDomainObject = {}; + const indexedAnnotationsByTag = {}; - self.onconnect = function (e) { - const port = e.ports[0]; - - port.onmessage = function (event) { - const requestType = event.data.request; - if (requestType === 'index') { - indexItem(event.data.keyString, event.data.model); - } 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 { - throw new Error(`Unknown request ${event.data.request}`); - } - }; - - port.start(); + self.onconnect = function (e) { + const port = e.ports[0]; + port.onmessage = function (event) { + const requestType = event.data.request; + if (requestType === 'index') { + indexItem(event.data.keyString, event.data.model); + } 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 { + throw new Error(`Unknown request ${event.data.request}`); + } }; - self.onerror = function (error) { - //do nothing - console.error('Error on feed', error); + port.start(); + }; + + self.onerror = function (error) { + //do nothing + 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) { + 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; + } + } + + /** + * Gets search results from the indexedItems based on provided search + * input. Returns matching results from indexedItems + * + * @param data An object which contains: + * * input: The original string which we are searching with + * * maxResults: The maximum number of search results desired + * * queryId: an id identifying this query, will be returned. + */ + function searchForObjects(data) { + let results = []; + const input = data.input.trim().toLowerCase(); + const message = { + request: 'searchForObjects', + results: [], + total: 0, + queryId: data.queryId }; - function indexAnnotation(objectToIndex, model) { - Object.keys(model.targets).forEach(targetID => { - if (!indexedAnnotationsByDomainObject[targetID]) { - indexedAnnotationsByDomainObject[targetID] = []; - } + results = + Object.values(indexedDomainObjects).filter((indexedItem) => { + return indexedItem.name.toLowerCase().includes(input); + }) || []; - objectToIndex.targets = model.targets; - objectToIndex.tags = model.tags; - const existsInIndex = indexedAnnotationsByDomainObject[targetID].some(indexedObject => { - return indexedObject.keyString === objectToIndex.keyString; + 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 (!existsInIndex) { - indexedAnnotationsByDomainObject[targetID].push(objectToIndex); + if (!existsInResults) { + results.push(matchingAnnotation); } - }); - } - - 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) { - 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; + }); } + }); } - /** - * Gets search results from the indexedItems based on provided search - * input. Returns matching results from indexedItems - * - * @param data An object which contains: - * * input: The original string which we are searching with - * * maxResults: The maximum number of search results desired - * * queryId: an id identifying this query, will be returned. - */ - function searchForObjects(data) { - let results = []; - const input = data.input.trim().toLowerCase(); - const message = { - request: 'searchForObjects', - results: [], - total: 0, - queryId: data.queryId - }; + message.total = results.length; + message.results = results.slice(0, data.maxResults); - 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; - } -}()); + return message; + } +})(); diff --git a/src/api/objects/InterceptorRegistry.js b/src/api/objects/InterceptorRegistry.js index e0e918f357..a7481693a3 100644 --- a/src/api/objects/InterceptorRegistry.js +++ b/src/api/objects/InterceptorRegistry.js @@ -21,54 +21,54 @@ *****************************************************************************/ const DEFAULT_INTERCEPTOR_PRIORITY = 0; export default class InterceptorRegistry { - /** - * A InterceptorRegistry maintains the definitions for different interceptors that may be invoked on domain objects. - * @interface InterceptorRegistry - * @memberof module:openmct - */ - constructor() { - this.interceptors = []; - } - - /** - * @interface InterceptorDef - * @property {function} appliesTo function that determines if this interceptor should be called for the given identifier/object - * @property {function} invoke function that transforms the provided domain object and returns the transformed domain object - * @property {function} priority the priority for this interceptor. A higher number returned has more weight than a lower number - * @memberof module:openmct InterceptorRegistry# - */ - - /** - * Register a new object interceptor. - * - * @param {module:openmct.InterceptorDef} interceptorDef the interceptor to add - * @method addInterceptor - * @memberof module:openmct.InterceptorRegistry# - */ - addInterceptor(interceptorDef) { - this.interceptors.push(interceptorDef); - } - - /** - * Retrieve all interceptors applicable to a domain object. - * @method getInterceptors - * @returns [module:openmct.InterceptorDef] the registered interceptors for this identifier/object - * @memberof module:openmct.InterceptorRegistry# - */ - getInterceptors(identifier, object) { - - function byPriority(interceptorA, interceptorB) { - let priorityA = interceptorA.priority ?? DEFAULT_INTERCEPTOR_PRIORITY; - let priorityB = interceptorB.priority ?? DEFAULT_INTERCEPTOR_PRIORITY; - - return priorityB - priorityA; - } - - return this.interceptors.filter(interceptor => { - return typeof interceptor.appliesTo === 'function' - && interceptor.appliesTo(identifier, object); - }).sort(byPriority); + /** + * A InterceptorRegistry maintains the definitions for different interceptors that may be invoked on domain objects. + * @interface InterceptorRegistry + * @memberof module:openmct + */ + constructor() { + this.interceptors = []; + } + + /** + * @interface InterceptorDef + * @property {function} appliesTo function that determines if this interceptor should be called for the given identifier/object + * @property {function} invoke function that transforms the provided domain object and returns the transformed domain object + * @property {function} priority the priority for this interceptor. A higher number returned has more weight than a lower number + * @memberof module:openmct InterceptorRegistry# + */ + + /** + * Register a new object interceptor. + * + * @param {module:openmct.InterceptorDef} interceptorDef the interceptor to add + * @method addInterceptor + * @memberof module:openmct.InterceptorRegistry# + */ + addInterceptor(interceptorDef) { + this.interceptors.push(interceptorDef); + } + + /** + * Retrieve all interceptors applicable to a domain object. + * @method getInterceptors + * @returns [module:openmct.InterceptorDef] the registered interceptors for this identifier/object + * @memberof module:openmct.InterceptorRegistry# + */ + getInterceptors(identifier, object) { + function byPriority(interceptorA, interceptorB) { + let priorityA = interceptorA.priority ?? DEFAULT_INTERCEPTOR_PRIORITY; + let priorityB = interceptorB.priority ?? DEFAULT_INTERCEPTOR_PRIORITY; + + return priorityB - priorityA; } + return this.interceptors + .filter((interceptor) => { + return ( + typeof interceptor.appliesTo === 'function' && interceptor.appliesTo(identifier, object) + ); + }) + .sort(byPriority); + } } - diff --git a/src/api/objects/MutableDomainObject.js b/src/api/objects/MutableDomainObject.js index 96be6edbed..d76ed5b5c2 100644 --- a/src/api/objects/MutableDomainObject.js +++ b/src/api/objects/MutableDomainObject.js @@ -40,112 +40,122 @@ const ANY_OBJECT_EVENT = 'mutation'; * @memberof module:openmct */ class MutableDomainObject { - constructor(eventEmitter) { - Object.defineProperties(this, { - _globalEventEmitter: { - value: eventEmitter, - // Property should not be serialized - enumerable: false - }, - _instanceEventEmitter: { - value: new EventEmitter(), - // Property should not be serialized - enumerable: false - }, - _observers: { - value: [], - // Property should not be serialized - enumerable: false - }, - isMutable: { - value: true, - // Property should not be serialized - enumerable: false - } - }); - } - $observe(path, callback) { - let fullPath = qualifiedEventName(this, path); - let eventOff = - this._globalEventEmitter.off.bind(this._globalEventEmitter, fullPath, callback); + constructor(eventEmitter) { + Object.defineProperties(this, { + _globalEventEmitter: { + value: eventEmitter, + // Property should not be serialized + enumerable: false + }, + _instanceEventEmitter: { + value: new EventEmitter(), + // Property should not be serialized + enumerable: false + }, + _observers: { + value: [], + // Property should not be serialized + enumerable: false + }, + isMutable: { + value: true, + // Property should not be serialized + enumerable: false + } + }); + } + $observe(path, callback) { + let fullPath = qualifiedEventName(this, path); + let eventOff = this._globalEventEmitter.off.bind(this._globalEventEmitter, fullPath, callback); - this._globalEventEmitter.on(fullPath, callback); - this._observers.push(eventOff); + this._globalEventEmitter.on(fullPath, callback); + this._observers.push(eventOff); - return eventOff; - } - $set(path, value) { - const oldModel = JSON.parse(JSON.stringify(this)); - const oldValue = _.get(oldModel, path); - MutableDomainObject.mutateObject(this, path, value); + return eventOff; + } + $set(path, value) { + const oldModel = JSON.parse(JSON.stringify(this)); + const oldValue = _.get(oldModel, path); + MutableDomainObject.mutateObject(this, path, value); - //Emit secret synchronization event first, so that all objects are in sync before subsequent events fired. - this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), this); + //Emit secret synchronization event first, so that all objects are in sync before subsequent events fired. + this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), this); - //Emit a general "any object" event - this._globalEventEmitter.emit(ANY_OBJECT_EVENT, this, oldModel); - //Emit wildcard event, with path so that callback knows what changed - this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this, path, value, oldModel, oldValue); + //Emit a general "any object" event + this._globalEventEmitter.emit(ANY_OBJECT_EVENT, this, oldModel); + //Emit wildcard event, with path so that callback knows what changed + this._globalEventEmitter.emit( + qualifiedEventName(this, '*'), + this, + path, + value, + oldModel, + oldValue + ); - //Emit events specific to properties affected - let parentPropertiesList = path.split('.'); - for (let index = parentPropertiesList.length; index > 0; index--) { - let parentPropertyPath = parentPropertiesList.slice(0, index).join('.'); - this._globalEventEmitter.emit(qualifiedEventName(this, parentPropertyPath), _.get(this, parentPropertyPath), _.get(oldModel, parentPropertyPath)); - } - - //TODO: Emit events for listeners of child properties when parent changes. - // Do it at observer time - also register observers for parent attribute path. + //Emit events specific to properties affected + let parentPropertiesList = path.split('.'); + for (let index = parentPropertiesList.length; index > 0; index--) { + let parentPropertyPath = parentPropertiesList.slice(0, index).join('.'); + this._globalEventEmitter.emit( + qualifiedEventName(this, parentPropertyPath), + _.get(this, parentPropertyPath), + _.get(oldModel, parentPropertyPath) + ); } - $refresh(model) { - //TODO: Currently we are updating the entire object. - // In the future we could update a specific property of the object using the 'path' parameter. - this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), model); + //TODO: Emit events for listeners of child properties when parent changes. + // Do it at observer time - also register observers for parent attribute path. + } - //Emit wildcard event, with path so that callback knows what changed - this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this); + $refresh(model) { + //TODO: Currently we are updating the entire object. + // In the future we could update a specific property of the object using the 'path' parameter. + this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), model); + + //Emit wildcard event, with path so that callback knows what changed + this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this); + } + + $on(event, callback) { + this._instanceEventEmitter.on(event, callback); + + return () => this._instanceEventEmitter.off(event, callback); + } + $destroy() { + while (this._observers.length > 0) { + const observer = this._observers.pop(); + observer(); } - $on(event, callback) { - this._instanceEventEmitter.on(event, callback); + this._instanceEventEmitter.emit('$_destroy'); + } - return () => this._instanceEventEmitter.off(event, callback); - } - $destroy() { - while (this._observers.length > 0) { - const observer = this._observers.pop(); - observer(); - } + static createMutable(object, mutationTopic) { + let mutable = Object.create(new MutableDomainObject(mutationTopic)); + Object.assign(mutable, object); - this._instanceEventEmitter.emit('$_destroy'); + mutable.$observe('$_synchronize_model', (updatedObject) => { + let clone = JSON.parse(JSON.stringify(updatedObject)); + utils.refresh(mutable, clone); + }); + + return mutable; + } + + static mutateObject(object, path, value) { + if (path !== 'persisted') { + _.set(object, 'modified', Date.now()); } - static createMutable(object, mutationTopic) { - let mutable = Object.create(new MutableDomainObject(mutationTopic)); - Object.assign(mutable, object); - - mutable.$observe('$_synchronize_model', (updatedObject) => { - let clone = JSON.parse(JSON.stringify(updatedObject)); - utils.refresh(mutable, clone); - }); - - return mutable; - } - - static mutateObject(object, path, value) { - if (path !== 'persisted') { - _.set(object, 'modified', Date.now()); - } - - _.set(object, path, value); - } + _.set(object, path, value); + } } function qualifiedEventName(object, eventName) { - let keystring = utils.makeKeyString(object.identifier); + let keystring = utils.makeKeyString(object.identifier); - return [keystring, eventName].join(':'); + return [keystring, eventName].join(':'); } export default MutableDomainObject; diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js index b4b0dbc338..cc3171b573 100644 --- a/src/api/objects/ObjectAPI.js +++ b/src/api/objects/ObjectAPI.js @@ -68,12 +68,12 @@ import InMemorySearchProvider from './InMemorySearchProvider'; */ /** - * @readonly - * @enum {string} SEARCH_TYPES - * @property {string} OBJECTS Search for objects - * @property {string} ANNOTATIONS Search for annotations - * @property {string} TAGS Search for tags - */ + * @readonly + * @enum {string} SEARCH_TYPES + * @property {string} OBJECTS Search for objects + * @property {string} ANNOTATIONS Search for annotations + * @property {string} TAGS Search for tags + */ /** * Utilities for loading, saving, and manipulating domain objects. @@ -81,718 +81,739 @@ import InMemorySearchProvider from './InMemorySearchProvider'; * @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', - TAGS: 'TAGS' - }); - this.eventEmitter = new EventEmitter(); - this.providers = {}; - this.rootRegistry = new RootRegistry(openmct); - this.inMemorySearchProvider = new InMemorySearchProvider(openmct); + constructor(typeRegistry, openmct) { + this.openmct = openmct; + this.typeRegistry = typeRegistry; + this.SEARCH_TYPES = Object.freeze({ + OBJECTS: 'OBJECTS', + ANNOTATIONS: 'ANNOTATIONS', + TAGS: 'TAGS' + }); + 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.rootProvider = new RootObjectProvider(this.rootRegistry); + this.cache = {}; + this.interceptorRegistry = new InterceptorRegistry(); - this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'restricted-notebook', 'plan', 'annotation']; + this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'restricted-notebook', 'plan', 'annotation']; - this.errors = { - Conflict: ConflictError - }; + this.errors = { + Conflict: ConflictError + }; + } + + /** + * Retrieve the provider for a given identifier. + */ + getProvider(identifier) { + if (identifier.key === 'ROOT') { + return this.rootProvider; } - /** - * Retrieve the provider for a given identifier. - */ - getProvider(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 + */ + getActiveTransaction() { + return this.transaction; + } + + /** + * Get the root-level object. + * @returns {Promise.} a promise for the root object + */ + getRoot() { + return this.rootProvider.get(); + } + + /** + * 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; + } + + /** + * 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. + * + * @param {string} key the key for the domain object to load + * @param {AbortSignal} abortSignal (optional) signal to abort fetch requests + * @param {boolean} [forceRemote=false] defaults to false. If true, will skip cached and + * dirty/in-transaction objects use and the provider.get method + * @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, forceRemote = false) { + let keystring = this.makeKeyString(identifier); + + if (!forceRemote) { + if (this.cache[keystring] !== undefined) { + return this.cache[keystring]; + } + + identifier = utils.parseKeyString(identifier); + + if (this.isTransactionActive()) { + let dirtyObject = this.transaction.getDirtyObject(identifier); + + if (dirtyObject) { + return Promise.resolve(dirtyObject); } - - return this.providers[identifier.namespace] || this.fallbackProvider; + } } - /** - * Get an active transaction instance - * @returns {Transaction} a transaction object - */ - getActiveTransaction() { - return this.transaction; + const provider = this.getProvider(identifier); + + if (!provider) { + throw new Error(`No Provider Matched for keyString "${this.makeKeyString(identifier)}}"`); } - /** - * Get the root-level object. - * @returns {Promise.} a promise for the root object - */ - getRoot() { - return this.rootProvider.get(); + if (!provider.get) { + throw new Error('Provider does not support get!'); } - /** - * 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; - } - - /** - * 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. - * - * @param {string} key the key for the domain object to load - * @param {AbortSignal} abortSignal (optional) signal to abort fetch requests - * @param {boolean} [forceRemote=false] defaults to false. If true, will skip cached and - * dirty/in-transaction objects use and the provider.get method - * @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, forceRemote = false) { - let keystring = this.makeKeyString(identifier); - - if (!forceRemote) { - if (this.cache[keystring] !== undefined) { - return this.cache[keystring]; - } - - identifier = utils.parseKeyString(identifier); - - if (this.isTransactionActive()) { - let dirtyObject = this.transaction.getDirtyObject(identifier); - - if (dirtyObject) { - return Promise.resolve(dirtyObject); - } - } - } - - const provider = this.getProvider(identifier); - - if (!provider) { - throw new Error(`No Provider Matched for keyString "${this.makeKeyString(identifier)}}"`); - } - - if (!provider.get) { - throw new Error('Provider does not support get!'); - } - - let objectPromise = provider.get(identifier, abortSignal).then(domainObject => { - delete this.cache[keystring]; - domainObject = this.applyGetInterceptors(identifier, domainObject); - - if (this.supportsMutation(identifier)) { - const mutableDomainObject = this.toMutable(domainObject); - mutableDomainObject.$refresh(domainObject); - this.destroyMutable(mutableDomainObject); - } - - return domainObject; - }).catch((error) => { - console.warn(`Failed to retrieve ${keystring}:`, error); - delete this.cache[keystring]; - const result = this.applyGetInterceptors(identifier); - - return result; - }); - - this.cache[keystring] = objectPromise; - - return objectPromise; - } - - /** - * 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.>} - * 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`); - } - - searchPromises.push(this.inMemorySearchProvider.search(query, searchType) - .then(results => results.hits - .map(hit => { - return hit; - }))); - - return searchPromises; - } - - /** - * 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.} 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.`); - } - - return this.get(identifier).then((object) => { - return this.toMutable(object); - }); - } - - /** - * 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"); - } - } - - delete() { - throw new Error('Delete not implemented'); - } - - isPersistable(idOrKeyString) { - let identifier = utils.parseKeyString(idOrKeyString); - let provider = this.getProvider(identifier); - - 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 - */ - async save(domainObject) { - const provider = this.getProvider(domainObject.identifier); - let result; - let lastPersistedTime; - - 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 username = await this.#getCurrentUsername(); - const isNewObject = domainObject.persisted === undefined; - let savedResolve; - let savedReject; - let savedObjectPromise; - - result = new Promise((resolve, reject) => { - savedResolve = resolve; - savedReject = reject; - }); - - this.#mutate(domainObject, 'modifiedBy', username); - - if (isNewObject) { - this.#mutate(domainObject, 'createdBy', username); - - const createdTime = Date.now(); - this.#mutate(domainObject, 'created', createdTime); - - const persistedTime = Date.now(); - this.#mutate(domainObject, 'persisted', persistedTime); - - savedObjectPromise = provider.create(domainObject); - } else { - lastPersistedTime = domainObject.persisted; - const persistedTime = Date.now(); - this.#mutate(domainObject, 'persisted', persistedTime); - savedObjectPromise = provider.update(domainObject); - } - - if (savedObjectPromise) { - savedObjectPromise.then(response => { - savedResolve(response); - }).catch((error) => { - if (!isNewObject) { - this.#mutate(domainObject, 'persisted', lastPersistedTime); - } - - savedReject(error); - }); - } else { - result = Promise.reject(`[ObjectAPI][save] Object provider returned ${savedObjectPromise} when ${isNewObject ? 'creating new' : 'updating'} object.`); - } - } - - return result.catch(async (error) => { - if (error instanceof this.errors.Conflict) { - // Synchronized objects will resolve their own conflicts - if (this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) { - this.openmct.notifications.info(`Conflict detected while saving "${this.makeKeyString(domainObject.name)}", attempting to resolve`); - } else { - this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`); - - if (this.isTransactionActive()) { - this.endTransaction(); - } - - await this.refresh(domainObject); - } - } - - throw error; - }); - } - - async #getCurrentUsername() { - const user = await this.openmct.user.getCurrentUser(); - let username; - - if (user !== undefined) { - username = user.getName(); - } - - return username; - } - - /** - * After entering into edit mode, creates a new instance of Transaction to keep track of changes in Objects - * - * @returns {Transaction} a new Transaction that was just created - */ - startTransaction() { - if (this.isTransactionActive()) { - throw new Error("Unable to start new Transaction: Previous Transaction is active"); - } - - this.transaction = new Transaction(this); - - return this.transaction; - } - - /** - * Clear instance of Transaction - */ - endTransaction() { - this.transaction = null; - } - - /** - * 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); - } - - /** - * 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); - } - - /** - * Retrieve the interceptors for a given domain object. - * @private - */ - #listGetInterceptors(identifier, object) { - return this.interceptorRegistry.getInterceptors(identifier, object); - } - - /** - * 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 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 - */ - getRelativePath(objectPath) { - return objectPath - .map(p => this.makeKeyString(p.identifier)) - .reverse() - .join('/'); - } - - /** - * Modify a domain object. Internal to ObjectAPI, won't call save after. - * @private - * - * @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 temporary mutable object, in the process informing any other mutable instances - mutableDomainObject.$set(path, value); - - //Destroy temporary mutable object - this.destroyMutable(mutableDomainObject); - } - } - - /** - * Modify a domain object and save. - * @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) { - this.#mutate(domainObject, path, value); - - if (this.isTransactionActive()) { - this.transaction.add(domainObject); - } else { - this.save(domainObject); - } - } - - /** - * Create a mutable domain object from an existing domain object - * @param {module:openmct.DomainObject} domainObject the object to make mutable - * @returns {MutableDomainObject} a mutable domain object that will automatically sync - * @method toMutable - * @memberof module:openmct.ObjectAPI# - */ - toMutable(domainObject) { - let mutableObject; - - if (domainObject.isMutable) { - mutableObject = domainObject; - } else { - mutableObject = MutableDomainObject.createMutable(domainObject, 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(domainObject.type)) { - let unobserve = provider.observe(identifier, (updatedModel) => { - // modified can sometimes be undefined, so make it 0 in this case - const mutableObjectModification = mutableObject.modified ?? Number.MIN_SAFE_INTEGER; - if (updatedModel.persisted > mutableObjectModification) { - //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(); - }); - } - } - - return mutableObject; - } - - /** - * 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); + let objectPromise = provider + .get(identifier, abortSignal) + .then((domainObject) => { + delete this.cache[keystring]; + domainObject = this.applyGetInterceptors(identifier, domainObject); + + if (this.supportsMutation(identifier)) { + const mutableDomainObject = this.toMutable(domainObject); + mutableDomainObject.$refresh(domainObject); + this.destroyMutable(mutableDomainObject); } return domainObject; - } - - /** - * @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); - } - - /** - * 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); - - return () => mutable.$destroy(); - } - } - - /** - * @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 {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); - } - - /** - * 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} 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 false; - } - - #pathContainsDomainObject(keyStringToCheck, path) { - if (!keyStringToCheck) { - return false; - } - - return path.some(pathElement => { - const identifierToCheck = utils.parseKeyString(keyStringToCheck); - - return this.areIdsEqual(identifierToCheck, pathElement.identifier); - }); - } - - /** - * Given an identifier, constructs the original path by walking up its parents - * @param {module:openmct.ObjectAPI~Identifier} identifier - * @param {Array} path an array of path objects - * @returns {Promise>} a promise containing an array of domain objects - */ - async getOriginalPath(identifier, path = []) { - const domainObject = await this.get(identifier); - path.push(domainObject); - 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; - } - } - - /** - * Parse and construct an `objectPath` from a `navigationPath`. - * - * A `navigationPath` is a string of the form `"/browse///..."` that is used - * by the Open MCT router to navigate to a specific object. - * - * Throws an error if the `navigationPath` is malformed. - * - * @param {string} navigationPath - * @returns {DomainObject[]} objectPath - */ - async getRelativeObjectPath(navigationPath) { - if (!navigationPath.startsWith('/browse/')) { - throw new Error(`Malformed navigation path: "${navigationPath}"`); - } - - navigationPath = navigationPath.replace('/browse/', ''); - - if (!navigationPath || navigationPath === '/') { - return []; - } - - // Remove any query params and split on '/' - const keyStrings = navigationPath.split('?')?.[0].split('/'); - - if (keyStrings[0] !== 'ROOT') { - keyStrings.unshift('ROOT'); - } - - const objectPath = (await Promise.all( - keyStrings.map( - keyString => this.supportsMutation(keyString) - ? this.getMutable(utils.parseKeyString(keyString)) - : this.get(utils.parseKeyString(keyString)) - ) - )).reverse(); - - return objectPath; - } - - isObjectPathToALink(domainObject, objectPath) { - return objectPath !== undefined - && objectPath.length > 1 - && domainObject.location !== this.makeKeyString(objectPath[1].identifier); - } - - isTransactionActive() { - return this.transaction !== undefined && this.transaction !== null; - } - - #hasAlreadyBeenPersisted(domainObject) { - // modified can sometimes be undefined, so make it 0 in this case - const modified = domainObject.modified ?? Number.MIN_SAFE_INTEGER; - const result = domainObject.persisted !== undefined - && domainObject.persisted >= modified; + }) + .catch((error) => { + console.warn(`Failed to retrieve ${keystring}:`, error); + delete this.cache[keystring]; + const result = this.applyGetInterceptors(identifier); return result; + }); + + this.cache[keystring] = objectPromise; + + return objectPromise; + } + + /** + * 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.>} + * 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`); + } + + searchPromises.push( + this.inMemorySearchProvider.search(query, searchType).then((results) => + results.hits.map((hit) => { + return hit; + }) + ) + ); + + return searchPromises; + } + + /** + * 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.} 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.`); + } + + return this.get(identifier).then((object) => { + return this.toMutable(object); + }); + } + + /** + * 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'); + } + } + + delete() { + throw new Error('Delete not implemented'); + } + + isPersistable(idOrKeyString) { + let identifier = utils.parseKeyString(idOrKeyString); + let provider = this.getProvider(identifier); + + 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 + */ + async save(domainObject) { + const provider = this.getProvider(domainObject.identifier); + let result; + let lastPersistedTime; + + 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 username = await this.#getCurrentUsername(); + const isNewObject = domainObject.persisted === undefined; + let savedResolve; + let savedReject; + let savedObjectPromise; + + result = new Promise((resolve, reject) => { + savedResolve = resolve; + savedReject = reject; + }); + + this.#mutate(domainObject, 'modifiedBy', username); + + if (isNewObject) { + this.#mutate(domainObject, 'createdBy', username); + + const createdTime = Date.now(); + this.#mutate(domainObject, 'created', createdTime); + + const persistedTime = Date.now(); + this.#mutate(domainObject, 'persisted', persistedTime); + + savedObjectPromise = provider.create(domainObject); + } else { + lastPersistedTime = domainObject.persisted; + const persistedTime = Date.now(); + this.#mutate(domainObject, 'persisted', persistedTime); + savedObjectPromise = provider.update(domainObject); + } + + if (savedObjectPromise) { + savedObjectPromise + .then((response) => { + savedResolve(response); + }) + .catch((error) => { + if (!isNewObject) { + this.#mutate(domainObject, 'persisted', lastPersistedTime); + } + + savedReject(error); + }); + } else { + result = Promise.reject( + `[ObjectAPI][save] Object provider returned ${savedObjectPromise} when ${ + isNewObject ? 'creating new' : 'updating' + } object.` + ); + } + } + + return result.catch(async (error) => { + if (error instanceof this.errors.Conflict) { + // Synchronized objects will resolve their own conflicts + if (this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) { + this.openmct.notifications.info( + `Conflict detected while saving "${this.makeKeyString( + domainObject.name + )}", attempting to resolve` + ); + } else { + this.openmct.notifications.error( + `Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}` + ); + + if (this.isTransactionActive()) { + this.endTransaction(); + } + + await this.refresh(domainObject); + } + } + + throw error; + }); + } + + async #getCurrentUsername() { + const user = await this.openmct.user.getCurrentUser(); + let username; + + if (user !== undefined) { + username = user.getName(); + } + + return username; + } + + /** + * After entering into edit mode, creates a new instance of Transaction to keep track of changes in Objects + * + * @returns {Transaction} a new Transaction that was just created + */ + startTransaction() { + if (this.isTransactionActive()) { + throw new Error('Unable to start new Transaction: Previous Transaction is active'); + } + + this.transaction = new Transaction(this); + + return this.transaction; + } + + /** + * Clear instance of Transaction + */ + endTransaction() { + this.transaction = null; + } + + /** + * 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); + } + + /** + * 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); + } + + /** + * Retrieve the interceptors for a given domain object. + * @private + */ + #listGetInterceptors(identifier, object) { + return this.interceptorRegistry.getInterceptors(identifier, object); + } + + /** + * 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 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 + */ + getRelativePath(objectPath) { + return objectPath + .map((p) => this.makeKeyString(p.identifier)) + .reverse() + .join('/'); + } + + /** + * Modify a domain object. Internal to ObjectAPI, won't call save after. + * @private + * + * @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 temporary mutable object, in the process informing any other mutable instances + mutableDomainObject.$set(path, value); + + //Destroy temporary mutable object + this.destroyMutable(mutableDomainObject); + } + } + + /** + * Modify a domain object and save. + * @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) { + this.#mutate(domainObject, path, value); + + if (this.isTransactionActive()) { + this.transaction.add(domainObject); + } else { + this.save(domainObject); + } + } + + /** + * Create a mutable domain object from an existing domain object + * @param {module:openmct.DomainObject} domainObject the object to make mutable + * @returns {MutableDomainObject} a mutable domain object that will automatically sync + * @method toMutable + * @memberof module:openmct.ObjectAPI# + */ + toMutable(domainObject) { + let mutableObject; + + if (domainObject.isMutable) { + mutableObject = domainObject; + } else { + mutableObject = MutableDomainObject.createMutable(domainObject, 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(domainObject.type) + ) { + let unobserve = provider.observe(identifier, (updatedModel) => { + // modified can sometimes be undefined, so make it 0 in this case + const mutableObjectModification = mutableObject.modified ?? Number.MIN_SAFE_INTEGER; + if (updatedModel.persisted > mutableObjectModification) { + //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(); + }); + } + } + + return mutableObject; + } + + /** + * 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; + } + + /** + * @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); + } + + /** + * 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); + + return () => mutable.$destroy(); + } + } + + /** + * @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 {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); + } + + /** + * 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} 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 false; + } + + #pathContainsDomainObject(keyStringToCheck, path) { + if (!keyStringToCheck) { + return false; + } + + return path.some((pathElement) => { + const identifierToCheck = utils.parseKeyString(keyStringToCheck); + + return this.areIdsEqual(identifierToCheck, pathElement.identifier); + }); + } + + /** + * Given an identifier, constructs the original path by walking up its parents + * @param {module:openmct.ObjectAPI~Identifier} identifier + * @param {Array} path an array of path objects + * @returns {Promise>} a promise containing an array of domain objects + */ + async getOriginalPath(identifier, path = []) { + const domainObject = await this.get(identifier); + path.push(domainObject); + 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; + } + } + + /** + * Parse and construct an `objectPath` from a `navigationPath`. + * + * A `navigationPath` is a string of the form `"/browse///..."` that is used + * by the Open MCT router to navigate to a specific object. + * + * Throws an error if the `navigationPath` is malformed. + * + * @param {string} navigationPath + * @returns {DomainObject[]} objectPath + */ + async getRelativeObjectPath(navigationPath) { + if (!navigationPath.startsWith('/browse/')) { + throw new Error(`Malformed navigation path: "${navigationPath}"`); + } + + navigationPath = navigationPath.replace('/browse/', ''); + + if (!navigationPath || navigationPath === '/') { + return []; + } + + // Remove any query params and split on '/' + const keyStrings = navigationPath.split('?')?.[0].split('/'); + + if (keyStrings[0] !== 'ROOT') { + keyStrings.unshift('ROOT'); + } + + const objectPath = ( + await Promise.all( + keyStrings.map((keyString) => + this.supportsMutation(keyString) + ? this.getMutable(utils.parseKeyString(keyString)) + : this.get(utils.parseKeyString(keyString)) + ) + ) + ).reverse(); + + return objectPath; + } + + isObjectPathToALink(domainObject, objectPath) { + return ( + objectPath !== undefined && + objectPath.length > 1 && + domainObject.location !== this.makeKeyString(objectPath[1].identifier) + ); + } + + isTransactionActive() { + return this.transaction !== undefined && this.transaction !== null; + } + + #hasAlreadyBeenPersisted(domainObject) { + // modified can sometimes be undefined, so make it 0 in this case + const modified = domainObject.modified ?? Number.MIN_SAFE_INTEGER; + const result = domainObject.persisted !== undefined && domainObject.persisted >= modified; + + return result; + } } diff --git a/src/api/objects/ObjectAPISearchSpec.js b/src/api/objects/ObjectAPISearchSpec.js index 1b25c63ae8..c282a9d507 100644 --- a/src/api/objects/ObjectAPISearchSpec.js +++ b/src/api/objects/ObjectAPISearchSpec.js @@ -1,223 +1,223 @@ import { createOpenMct, resetApplicationState } from '../../utils/testing'; -describe("The Object API Search Function", () => { - describe("The infrastructure", () => { - const MOCK_PROVIDER_KEY = 'mockProvider'; - const ANOTHER_MOCK_PROVIDER_KEY = 'anotherMockProvider'; - const MOCK_PROVIDER_SEARCH_DELAY = 15000; - const ANOTHER_MOCK_PROVIDER_SEARCH_DELAY = 20000; - const TOTAL_TIME_ELAPSED = 21000; - const BASE_TIME = new Date(2021, 0, 1); +describe('The Object API Search Function', () => { + describe('The infrastructure', () => { + const MOCK_PROVIDER_KEY = 'mockProvider'; + const ANOTHER_MOCK_PROVIDER_KEY = 'anotherMockProvider'; + const MOCK_PROVIDER_SEARCH_DELAY = 15000; + const ANOTHER_MOCK_PROVIDER_SEARCH_DELAY = 20000; + const TOTAL_TIME_ELAPSED = 21000; + const BASE_TIME = new Date(2021, 0, 1); - let mockObjectProvider; - let anotherMockObjectProvider; - let openmct; + let mockObjectProvider; + let anotherMockObjectProvider; + let openmct; - beforeEach((done) => { - openmct = createOpenMct(); + beforeEach((done) => { + openmct = createOpenMct(); - mockObjectProvider = jasmine.createSpyObj("mock object provider", [ - "search", "supportsSearchType" - ]); - anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [ - "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 = { - name: MOCK_PROVIDER_KEY, - start: new Date() - }; + mockObjectProvider = jasmine.createSpyObj('mock object provider', [ + 'search', + 'supportsSearchType' + ]); + anotherMockObjectProvider = jasmine.createSpyObj('another mock object provider', [ + '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 = { + name: MOCK_PROVIDER_KEY, + start: new Date() + }; - setTimeout(() => { - mockProviderSearch.end = new Date(); + setTimeout(() => { + mockProviderSearch.end = new Date(); - return resolve(mockProviderSearch); - }, MOCK_PROVIDER_SEARCH_DELAY); - }); - }); - anotherMockObjectProvider.supportsSearchType.and.callFake(() => { - return true; - }); - anotherMockObjectProvider.search.and.callFake(() => { - return new Promise(resolve => { - const anotherMockProviderSearch = { - name: ANOTHER_MOCK_PROVIDER_KEY, - start: new Date() - }; - - setTimeout(() => { - anotherMockProviderSearch.end = new Date(); - - return resolve(anotherMockProviderSearch); - }, ANOTHER_MOCK_PROVIDER_SEARCH_DELAY); - }); - }); - openmct.on('start', () => { - done(); - }); - openmct.startHeadless(); + return resolve(mockProviderSearch); + }, MOCK_PROVIDER_SEARCH_DELAY); }); - afterEach(async () => { - await resetApplicationState(openmct); - }); - it("uses each objects given provider's search function", () => { - openmct.objects.search('foo'); - expect(mockObjectProvider.search).toHaveBeenCalled(); - }); - it("provides each providers results as promises that resolve in parallel", async () => { - jasmine.clock().install(); - jasmine.clock().mockDate(BASE_TIME); - const resultsPromises = openmct.objects.search('foo'); - jasmine.clock().tick(TOTAL_TIME_ELAPSED); - const results = await Promise.all(resultsPromises); - const mockProviderResults = results.find( - result => result.name === MOCK_PROVIDER_KEY - ); - const anotherMockProviderResults = results.find( - result => result.name === ANOTHER_MOCK_PROVIDER_KEY - ); - const mockProviderStart = mockProviderResults.start.getTime(); - const mockProviderEnd = mockProviderResults.end.getTime(); - const anotherMockProviderStart = anotherMockProviderResults.start.getTime(); - const anotherMockProviderEnd = anotherMockProviderResults.end.getTime(); - const searchElapsedTime = Math.max(mockProviderEnd, anotherMockProviderEnd) - - Math.min(mockProviderEnd, anotherMockProviderEnd); + }); + anotherMockObjectProvider.supportsSearchType.and.callFake(() => { + return true; + }); + anotherMockObjectProvider.search.and.callFake(() => { + return new Promise((resolve) => { + const anotherMockProviderSearch = { + name: ANOTHER_MOCK_PROVIDER_KEY, + start: new Date() + }; - expect(mockProviderStart).toBeLessThan(anotherMockProviderEnd); - expect(anotherMockProviderStart).toBeLessThan(mockProviderEnd); - expect(searchElapsedTime).toBeLessThan( - MOCK_PROVIDER_SEARCH_DELAY - + ANOTHER_MOCK_PROVIDER_SEARCH_DELAY - ); + setTimeout(() => { + anotherMockProviderSearch.end = new Date(); - jasmine.clock().uninstall(); + return resolve(anotherMockProviderSearch); + }, ANOTHER_MOCK_PROVIDER_SEARCH_DELAY); }); + }); + openmct.on('start', () => { + done(); + }); + openmct.startHeadless(); + }); + afterEach(async () => { + await resetApplicationState(openmct); + }); + it("uses each objects given provider's search function", () => { + openmct.objects.search('foo'); + expect(mockObjectProvider.search).toHaveBeenCalled(); + }); + it('provides each providers results as promises that resolve in parallel', async () => { + jasmine.clock().install(); + jasmine.clock().mockDate(BASE_TIME); + const resultsPromises = openmct.objects.search('foo'); + jasmine.clock().tick(TOTAL_TIME_ELAPSED); + const results = await Promise.all(resultsPromises); + const mockProviderResults = results.find((result) => result.name === MOCK_PROVIDER_KEY); + const anotherMockProviderResults = results.find( + (result) => result.name === ANOTHER_MOCK_PROVIDER_KEY + ); + const mockProviderStart = mockProviderResults.start.getTime(); + const mockProviderEnd = mockProviderResults.end.getTime(); + const anotherMockProviderStart = anotherMockProviderResults.start.getTime(); + const anotherMockProviderEnd = anotherMockProviderResults.end.getTime(); + const searchElapsedTime = + Math.max(mockProviderEnd, anotherMockProviderEnd) - + Math.min(mockProviderEnd, anotherMockProviderEnd); + + expect(mockProviderStart).toBeLessThan(anotherMockProviderEnd); + expect(anotherMockProviderStart).toBeLessThan(mockProviderEnd); + expect(searchElapsedTime).toBeLessThan( + MOCK_PROVIDER_SEARCH_DELAY + ANOTHER_MOCK_PROVIDER_SEARCH_DELAY + ); + + jasmine.clock().uninstall(); + }); + }); + + describe('The in-memory search indexer', () => { + let openmct; + let mockDomainObject1; + let mockIdentifier1; + let mockDomainObject2; + let mockIdentifier2; + let mockDomainObject3; + let mockIdentifier3; + + beforeEach((done) => { + openmct = createOpenMct(); + const defaultObjectProvider = openmct.objects.getProvider({ + key: '', + namespace: '' + }); + openmct.objects.addProvider('foo', defaultObjectProvider); + spyOn(openmct.objects.inMemorySearchProvider, 'search').and.callThrough(); + spyOn(openmct.objects.inMemorySearchProvider, 'localSearchForObjects').and.callThrough(); + + openmct.on('start', async () => { + mockIdentifier1 = { + key: 'some-object', + namespace: 'foo' + }; + mockDomainObject1 = { + type: 'clock', + name: 'fooRabbit', + identifier: mockIdentifier1 + }; + mockIdentifier2 = { + key: 'some-other-object', + namespace: 'foo' + }; + mockDomainObject2 = { + type: 'clock', + name: 'fooBear', + identifier: mockIdentifier2 + }; + mockIdentifier3 = { + key: 'yet-another-object', + namespace: 'foo' + }; + mockDomainObject3 = { + type: 'clock', + name: 'redBear', + identifier: mockIdentifier3 + }; + await openmct.objects.inMemorySearchProvider.index(mockDomainObject1); + await openmct.objects.inMemorySearchProvider.index(mockDomainObject2); + await openmct.objects.inMemorySearchProvider.index(mockDomainObject3); + done(); + }); + openmct.startHeadless(); }); - describe("The in-memory search indexer", () => { - let openmct; - let mockDomainObject1; - let mockIdentifier1; - let mockDomainObject2; - let mockIdentifier2; - let mockDomainObject3; - let mockIdentifier3; - - beforeEach((done) => { - openmct = createOpenMct(); - const defaultObjectProvider = openmct.objects.getProvider({ - key: '', - namespace: '' - }); - openmct.objects.addProvider('foo', defaultObjectProvider); - spyOn(openmct.objects.inMemorySearchProvider, "search").and.callThrough(); - spyOn(openmct.objects.inMemorySearchProvider, "localSearchForObjects").and.callThrough(); - - openmct.on('start', async () => { - mockIdentifier1 = { - key: 'some-object', - namespace: 'foo' - }; - mockDomainObject1 = { - type: 'clock', - name: 'fooRabbit', - identifier: mockIdentifier1 - }; - mockIdentifier2 = { - key: 'some-other-object', - namespace: 'foo' - }; - mockDomainObject2 = { - type: 'clock', - name: 'fooBear', - identifier: mockIdentifier2 - }; - mockIdentifier3 = { - key: 'yet-another-object', - namespace: 'foo' - }; - mockDomainObject3 = { - type: 'clock', - name: 'redBear', - identifier: mockIdentifier3 - }; - await openmct.objects.inMemorySearchProvider.index(mockDomainObject1); - await openmct.objects.inMemorySearchProvider.index(mockDomainObject2); - await openmct.objects.inMemorySearchProvider.index(mockDomainObject3); - done(); - }); - openmct.startHeadless(); - }); - - afterEach(async () => { - await resetApplicationState(openmct); - }); - - it("can provide indexing without a provider", () => { - openmct.objects.search('foo'); - expect(openmct.objects.inMemorySearchProvider.search).toHaveBeenCalled(); - }); - - it("can do partial search", async () => { - const searchPromises = openmct.objects.search('foo'); - const searchResults = await Promise.all(searchPromises); - expect(searchResults[0].length).toBe(2); - }); - - it("returns nothing when appropriate", async () => { - const searchPromises = openmct.objects.search('laser'); - const searchResults = await Promise.all(searchPromises); - expect(searchResults[0].length).toBe(0); - }); - - it("returns exact matches", async () => { - const searchPromises = openmct.objects.search('redBear'); - const searchResults = await Promise.all(searchPromises); - expect(searchResults[0].length).toBe(1); - }); - - 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.localSearchForObjects).toHaveBeenCalled(); - }); - - it("can do partial search", async () => { - const searchPromises = openmct.objects.search('foo'); - const searchResults = await Promise.all(searchPromises); - expect(searchResults[0].length).toBe(2); - }); - - it("returns nothing when appropriate", async () => { - const searchPromises = openmct.objects.search('laser'); - const searchResults = await Promise.all(searchPromises); - expect(searchResults[0].length).toBe(0); - }); - - it("returns exact matches", async () => { - const searchPromises = openmct.objects.search('redBear'); - const searchResults = await Promise.all(searchPromises); - expect(searchResults[0].length).toBe(1); - }); - }); + afterEach(async () => { + await resetApplicationState(openmct); }); + + it('can provide indexing without a provider', () => { + openmct.objects.search('foo'); + expect(openmct.objects.inMemorySearchProvider.search).toHaveBeenCalled(); + }); + + it('can do partial search', async () => { + const searchPromises = openmct.objects.search('foo'); + const searchResults = await Promise.all(searchPromises); + expect(searchResults[0].length).toBe(2); + }); + + it('returns nothing when appropriate', async () => { + const searchPromises = openmct.objects.search('laser'); + const searchResults = await Promise.all(searchPromises); + expect(searchResults[0].length).toBe(0); + }); + + it('returns exact matches', async () => { + const searchPromises = openmct.objects.search('redBear'); + const searchResults = await Promise.all(searchPromises); + expect(searchResults[0].length).toBe(1); + }); + + 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.localSearchForObjects).toHaveBeenCalled(); + }); + + it('can do partial search', async () => { + const searchPromises = openmct.objects.search('foo'); + const searchResults = await Promise.all(searchPromises); + expect(searchResults[0].length).toBe(2); + }); + + it('returns nothing when appropriate', async () => { + const searchPromises = openmct.objects.search('laser'); + const searchResults = await Promise.all(searchPromises); + expect(searchResults[0].length).toBe(0); + }); + + it('returns exact matches', async () => { + const searchPromises = openmct.objects.search('redBear'); + const searchResults = await Promise.all(searchPromises); + expect(searchResults[0].length).toBe(1); + }); + }); + }); }); diff --git a/src/api/objects/ObjectAPISpec.js b/src/api/objects/ObjectAPISpec.js index dfe0e42114..9456440adf 100644 --- a/src/api/objects/ObjectAPISpec.js +++ b/src/api/objects/ObjectAPISpec.js @@ -1,556 +1,593 @@ import ObjectAPI from './ObjectAPI.js'; import { createOpenMct, resetApplicationState } from '../../utils/testing'; -describe("The Object API", () => { - let objectAPI; - let typeRegistry; - let openmct = {}; - let mockDomainObject; - const TEST_NAMESPACE = "test-namespace"; - const TEST_KEY = "test-key"; - const USERNAME = 'Joan Q Public'; - const FIFTEEN_MINUTES = 15 * 60 * 1000; +describe('The Object API', () => { + let objectAPI; + let typeRegistry; + let openmct = {}; + let mockDomainObject; + const TEST_NAMESPACE = 'test-namespace'; + const TEST_KEY = 'test-key'; + const USERNAME = 'Joan Q Public'; + const FIFTEEN_MINUTES = 15 * 60 * 1000; - beforeEach((done) => { - typeRegistry = jasmine.createSpyObj('typeRegistry', [ - 'get' + beforeEach((done) => { + typeRegistry = jasmine.createSpyObj('typeRegistry', ['get']); + const userProvider = { + isLoggedIn() { + return true; + }, + getCurrentUser() { + return Promise.resolve({ + getName() { + return USERNAME; + } + }); + } + }; + openmct = createOpenMct(); + openmct.user.setProvider(userProvider); + objectAPI = openmct.objects; + + openmct.editor = {}; + openmct.editor.isEditing = () => false; + + mockDomainObject = { + identifier: { + namespace: TEST_NAMESPACE, + key: TEST_KEY + }, + name: 'test object', + type: 'test-type' + }; + openmct.on('start', () => { + done(); + }); + openmct.startHeadless(); + }); + + afterEach(async () => { + await resetApplicationState(openmct); + }); + + describe('The save function', () => { + it('Rejects if no provider available', async () => { + let rejected = false; + objectAPI.providers = {}; + objectAPI.fallbackProvider = null; + + try { + await objectAPI.save(mockDomainObject); + } catch (error) { + rejected = true; + } + + expect(rejected).toBe(true); + }); + describe('when a provider is available', () => { + let mockProvider; + beforeEach(() => { + mockProvider = jasmine.createSpyObj('mock provider', ['create', 'update']); + mockProvider.create.and.returnValue(Promise.resolve(true)); + mockProvider.update.and.returnValue(Promise.resolve(true)); + objectAPI.addProvider(TEST_NAMESPACE, mockProvider); + }); + it("Adds a 'created' timestamp to new objects", async () => { + await objectAPI.save(mockDomainObject); + expect(mockDomainObject.created).not.toBeUndefined(); + }); + it("Calls 'create' on provider if object is new", async () => { + await objectAPI.save(mockDomainObject); + expect(mockProvider.create).toHaveBeenCalled(); + expect(mockProvider.update).not.toHaveBeenCalled(); + }); + it("Calls 'update' on provider if object is not new", async () => { + mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES; + mockDomainObject.modified = Date.now(); + + await objectAPI.save(mockDomainObject); + expect(mockProvider.create).not.toHaveBeenCalled(); + expect(mockProvider.update).toHaveBeenCalled(); + }); + describe('the persisted timestamp for existing objects', () => { + let persistedTimestamp; + beforeEach(() => { + persistedTimestamp = Date.now() - FIFTEEN_MINUTES; + mockDomainObject.persisted = persistedTimestamp; + mockDomainObject.modified = Date.now(); + }); + + it('is updated', async () => { + await objectAPI.save(mockDomainObject); + expect(mockDomainObject.persisted).toBeDefined(); + expect(mockDomainObject.persisted > persistedTimestamp).toBe(true); + }); + it('is >= modified timestamp', async () => { + await objectAPI.save(mockDomainObject); + expect(mockDomainObject.persisted >= mockDomainObject.modified).toBe(true); + }); + }); + describe('the persisted timestamp for new objects', () => { + it('is updated', async () => { + await objectAPI.save(mockDomainObject); + expect(mockDomainObject.persisted).toBeDefined(); + }); + it('is >= modified timestamp', async () => { + await objectAPI.save(mockDomainObject); + expect(mockDomainObject.persisted >= mockDomainObject.modified).toBe(true); + }); + }); + + it("Sets the current user for 'createdBy' on new objects", async () => { + await objectAPI.save(mockDomainObject); + expect(mockDomainObject.createdBy).toBe(USERNAME); + }); + it("Sets the current user for 'modifedBy' on existing objects", async () => { + mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES; + mockDomainObject.modified = Date.now(); + + await objectAPI.save(mockDomainObject); + expect(mockDomainObject.modifiedBy).toBe(USERNAME); + }); + + it('Does not persist if the object is unchanged', () => { + mockDomainObject.persisted = mockDomainObject.modified = Date.now(); + + objectAPI.save(mockDomainObject); + 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}` + ); + }); + }); + }); + }); + }); + + describe('The get function', () => { + describe('when a provider is available', () => { + let mockProvider; + let mockInterceptor; + let anotherMockInterceptor; + let notApplicableMockInterceptor; + beforeEach(() => { + mockProvider = jasmine.createSpyObj('mock provider', ['get']); + mockProvider.get.and.returnValue(Promise.resolve(mockDomainObject)); + + mockInterceptor = jasmine.createSpyObj('mock interceptor', ['appliesTo', 'invoke']); + mockInterceptor.appliesTo.and.returnValue(true); + mockInterceptor.invoke.and.callFake((identifier, object) => { + return Object.assign( + { + changed: true + }, + object + ); + }); + + anotherMockInterceptor = jasmine.createSpyObj('another mock interceptor', [ + 'appliesTo', + 'invoke' ]); - const userProvider = { - isLoggedIn() { - return true; + anotherMockInterceptor.appliesTo.and.returnValue(true); + anotherMockInterceptor.invoke.and.callFake((identifier, object) => { + return Object.assign( + { + alsoChanged: true }, - getCurrentUser() { - return Promise.resolve({ - getName() { - return USERNAME; - } - }); - } - }; - openmct = createOpenMct(); - openmct.user.setProvider(userProvider); - objectAPI = openmct.objects; + object + ); + }); - openmct.editor = {}; - openmct.editor.isEditing = () => false; - - mockDomainObject = { - identifier: { - namespace: TEST_NAMESPACE, - key: TEST_KEY + notApplicableMockInterceptor = jasmine.createSpyObj('not applicable mock interceptor', [ + 'appliesTo', + 'invoke' + ]); + notApplicableMockInterceptor.appliesTo.and.returnValue(false); + notApplicableMockInterceptor.invoke.and.callFake((identifier, object) => { + return Object.assign( + { + shouldNotBeChanged: true }, - name: "test object", - type: "test-type" - }; - openmct.on('start', () => { - done(); + object + ); }); - openmct.startHeadless(); + objectAPI.addProvider(TEST_NAMESPACE, mockProvider); + objectAPI.addGetInterceptor(mockInterceptor); + objectAPI.addGetInterceptor(anotherMockInterceptor); + objectAPI.addGetInterceptor(notApplicableMockInterceptor); + }); + + it('Caches multiple requests for the same object', () => { + const promises = []; + expect(mockProvider.get.calls.count()).toBe(0); + promises.push(objectAPI.get(mockDomainObject.identifier)); + expect(mockProvider.get.calls.count()).toBe(1); + 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(); + + 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}` + ); + }); + }); + }); + }); + + describe('the mutation API', () => { + let testObject; + let updatedTestObject; + let mutable; + let mockProvider; + let callbacks = []; + + beforeEach(function () { + objectAPI = new ObjectAPI(typeRegistry, openmct); + testObject = { + identifier: { + namespace: TEST_NAMESPACE, + key: TEST_KEY + }, + name: 'test object', + type: 'notebook', + otherAttribute: 'other-attribute-value', + modified: 0, + persisted: 0, + objectAttribute: { + embeddedObject: { + embeddedKey: 'embedded-value' + } + } + }; + updatedTestObject = Object.assign( + { + otherAttribute: 'changed-attribute-value' + }, + testObject + ); + updatedTestObject.modified = 1; + updatedTestObject.persisted = 1; + + mockProvider = jasmine.createSpyObj('mock provider', [ + 'get', + 'create', + 'update', + 'observe', + '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); + + return () => {}; + }); + mockProvider.observe.and.callFake((id, callback) => { + if (callbacks.length === 0) { + callbacks.push(callback); + } else { + callbacks[0] = callback; + } + + return () => {}; + }); + + objectAPI.addProvider(TEST_NAMESPACE, mockProvider); + + return objectAPI.getMutable(testObject.identifier).then((object) => { + mutable = object; + + return mutable; + }); }); - afterEach(async () => { - await resetApplicationState(openmct); + afterEach(() => { + mutable.$destroy(); }); - describe("The save function", () => { - it("Rejects if no provider available", async () => { - let rejected = false; - objectAPI.providers = {}; - objectAPI.fallbackProvider = null; + it('mutates the original object', () => { + const MUTATED_NAME = 'mutated name'; + objectAPI.mutate(testObject, 'name', MUTATED_NAME); + expect(testObject.name).toBe(MUTATED_NAME); + }); - try { - await objectAPI.save(mockDomainObject); - } catch (error) { - rejected = true; + it('Provides a way of refreshing an object from the persistence store', () => { + const modifiedTestObject = JSON.parse(JSON.stringify(testObject)); + const OTHER_ATTRIBUTE_VALUE = 'Modified value'; + const NEW_ATTRIBUTE_VALUE = 'A new attribute'; + modifiedTestObject.otherAttribute = OTHER_ATTRIBUTE_VALUE; + modifiedTestObject.newAttribute = NEW_ATTRIBUTE_VALUE; + delete modifiedTestObject.objectAttribute; + + spyOn(objectAPI, 'get'); + objectAPI.get.and.returnValue(Promise.resolve(modifiedTestObject)); + + expect(objectAPI.get).not.toHaveBeenCalled(); + + return objectAPI.refresh(testObject).then(() => { + expect(objectAPI.get).toHaveBeenCalledWith(testObject.identifier); + + expect(testObject.otherAttribute).toEqual(OTHER_ATTRIBUTE_VALUE); + expect(testObject.newAttribute).toEqual(NEW_ATTRIBUTE_VALUE); + expect(testObject.objectAttribute).not.toBeDefined(); + }); + }); + + describe('uses a MutableDomainObject', () => { + it('and retains properties of original object ', function () { + expect(hasOwnProperty(mutable, 'identifier')).toBe(true); + expect(hasOwnProperty(mutable, 'otherAttribute')).toBe(true); + expect(mutable.identifier).toEqual(testObject.identifier); + expect(mutable.otherAttribute).toEqual(testObject.otherAttribute); + }); + + it('that is identical to original object when serialized', function () { + expect(JSON.stringify(mutable)).toEqual(JSON.stringify(testObject)); + }); + + it('that observes for object changes', function () { + let mockListener = jasmine.createSpy('mockListener'); + objectAPI.observe(testObject, '*', mockListener); + mockProvider.observeObjectChanges(); + expect(mockListener).toHaveBeenCalled(); + }); + }); + + describe('uses events', function () { + let testObjectDuplicate; + let mutableSecondInstance; + + beforeEach(function () { + // Duplicate object to guarantee we are not sharing object instance, which would invalidate test + testObjectDuplicate = JSON.parse(JSON.stringify(testObject)); + mutableSecondInstance = objectAPI.toMutable(testObjectDuplicate); + }); + + afterEach(() => { + mutableSecondInstance.$destroy(); + }); + + it('to stay synchronized when mutated', function () { + objectAPI.mutate(mutable, 'otherAttribute', 'new-attribute-value'); + expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value'); + }); + + it('to indicate when a property changes', function () { + let mutationCallback = jasmine.createSpy('mutation-callback'); + let unlisten; + + return new Promise(function (resolve) { + mutationCallback.and.callFake(resolve); + unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback); + objectAPI.mutate(mutable, 'otherAttribute', 'some-new-value'); + }).then(function () { + expect(mutationCallback).toHaveBeenCalledWith('some-new-value', 'other-attribute-value'); + unlisten(); + }); + }); + + it('to indicate when a child property has changed', function () { + let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback'); + let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback'); + let objectAttributeCallback = jasmine.createSpy('objectAttribute'); + let listeners = []; + + return new Promise(function (resolve) { + objectAttributeCallback.and.callFake(resolve); + + listeners.push( + objectAPI.observe( + mutableSecondInstance, + 'objectAttribute.embeddedObject.embeddedKey', + embeddedKeyCallback + ) + ); + listeners.push( + objectAPI.observe( + mutableSecondInstance, + 'objectAttribute.embeddedObject', + embeddedObjectCallback + ) + ); + listeners.push( + objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback) + ); + + objectAPI.mutate( + mutable, + 'objectAttribute.embeddedObject.embeddedKey', + 'updated-embedded-value' + ); + }).then(function () { + expect(embeddedKeyCallback).toHaveBeenCalledWith( + 'updated-embedded-value', + 'embedded-value' + ); + expect(embeddedObjectCallback).toHaveBeenCalledWith( + { + embeddedKey: 'updated-embedded-value' + }, + { + embeddedKey: 'embedded-value' } + ); + expect(objectAttributeCallback).toHaveBeenCalledWith( + { + embeddedObject: { + embeddedKey: 'updated-embedded-value' + } + }, + { + embeddedObject: { + embeddedKey: 'embedded-value' + } + } + ); - expect(rejected).toBe(true); + listeners.forEach((listener) => listener()); }); - describe("when a provider is available", () => { - let mockProvider; - beforeEach(() => { - mockProvider = jasmine.createSpyObj("mock provider", [ - "create", - "update" - ]); - mockProvider.create.and.returnValue(Promise.resolve(true)); - mockProvider.update.and.returnValue(Promise.resolve(true)); - objectAPI.addProvider(TEST_NAMESPACE, mockProvider); - }); - it("Adds a 'created' timestamp to new objects", async () => { - await objectAPI.save(mockDomainObject); - expect(mockDomainObject.created).not.toBeUndefined(); - }); - it("Calls 'create' on provider if object is new", async () => { - await objectAPI.save(mockDomainObject); - expect(mockProvider.create).toHaveBeenCalled(); - expect(mockProvider.update).not.toHaveBeenCalled(); - }); - it("Calls 'update' on provider if object is not new", async () => { - mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES; - mockDomainObject.modified = Date.now(); + }); + }); + }); - await objectAPI.save(mockDomainObject); - expect(mockProvider.create).not.toHaveBeenCalled(); - expect(mockProvider.update).toHaveBeenCalled(); - }); - describe("the persisted timestamp for existing objects", () => { - let persistedTimestamp; - beforeEach(() => { - persistedTimestamp = Date.now() - FIFTEEN_MINUTES; - mockDomainObject.persisted = persistedTimestamp; - mockDomainObject.modified = Date.now(); - }); + describe('getOriginalPath', () => { + let mockGrandParentObject; + let mockParentObject; + let mockChildObject; - it("is updated", async () => { - await objectAPI.save(mockDomainObject); - expect(mockDomainObject.persisted).toBeDefined(); - expect(mockDomainObject.persisted > persistedTimestamp).toBe(true); - }); - it("is >= modified timestamp", async () => { - await objectAPI.save(mockDomainObject); - expect(mockDomainObject.persisted >= mockDomainObject.modified).toBe(true); - }); - }); - describe("the persisted timestamp for new objects", () => { - it("is updated", async () => { - await objectAPI.save(mockDomainObject); - expect(mockDomainObject.persisted).toBeDefined(); - }); - it("is >= modified timestamp", async () => { - await objectAPI.save(mockDomainObject); - expect(mockDomainObject.persisted >= mockDomainObject.modified).toBe(true); - }); - }); + beforeEach(() => { + const mockObjectProvider = jasmine.createSpyObj('mock object provider', [ + 'create', + 'update', + 'get' + ]); - it("Sets the current user for 'createdBy' on new objects", async () => { - await objectAPI.save(mockDomainObject); - expect(mockDomainObject.createdBy).toBe(USERNAME); - }); - it("Sets the current user for 'modifedBy' on existing objects", async () => { - mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES; - mockDomainObject.modified = Date.now(); + 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' + } + }; - await objectAPI.save(mockDomainObject); - expect(mockDomainObject.modifiedBy).toBe(USERNAME); - }); + // 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; + } + }; - it("Does not persist if the object is unchanged", () => { - mockDomainObject.persisted = - mockDomainObject.modified = Date.now(); + openmct.objects.addProvider('fooNameSpace', mockObjectProvider); - objectAPI.save(mockDomainObject); - expect(mockProvider.create).not.toHaveBeenCalled(); - expect(mockProvider.update).not.toHaveBeenCalled(); - }); + mockObjectProvider.create.and.returnValue(Promise.resolve(true)); + mockObjectProvider.update.and.returnValue(Promise.resolve(true)); - 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}`); - }); - }); - }); - }); + openmct.objects.addProvider('fooNameSpace', mockObjectProvider); }); - describe("The get function", () => { - describe("when a provider is available", () => { - let mockProvider; - let mockInterceptor; - let anotherMockInterceptor; - let notApplicableMockInterceptor; - beforeEach(() => { - mockProvider = jasmine.createSpyObj("mock provider", [ - "get" - ]); - mockProvider.get.and.returnValue(Promise.resolve(mockDomainObject)); + it('can construct paths even with cycles', async () => { + const objectPath = await objectAPI.getOriginalPath(mockChildObject.identifier); + expect(objectPath.length).toEqual(3); + }); + }); - mockInterceptor = jasmine.createSpyObj("mock interceptor", [ - "appliesTo", - "invoke" - ]); - mockInterceptor.appliesTo.and.returnValue(true); - mockInterceptor.invoke.and.callFake((identifier, object) => { - return Object.assign({ - changed: true - }, object); - }); - - anotherMockInterceptor = jasmine.createSpyObj("another mock interceptor", [ - "appliesTo", - "invoke" - ]); - anotherMockInterceptor.appliesTo.and.returnValue(true); - anotherMockInterceptor.invoke.and.callFake((identifier, object) => { - return Object.assign({ - alsoChanged: true - }, object); - }); - - notApplicableMockInterceptor = jasmine.createSpyObj("not applicable mock interceptor", [ - "appliesTo", - "invoke" - ]); - notApplicableMockInterceptor.appliesTo.and.returnValue(false); - notApplicableMockInterceptor.invoke.and.callFake((identifier, object) => { - return Object.assign({ - shouldNotBeChanged: true - }, object); - }); - objectAPI.addProvider(TEST_NAMESPACE, mockProvider); - objectAPI.addGetInterceptor(mockInterceptor); - objectAPI.addGetInterceptor(anotherMockInterceptor); - objectAPI.addGetInterceptor(notApplicableMockInterceptor); - }); - - it("Caches multiple requests for the same object", () => { - const promises = []; - expect(mockProvider.get.calls.count()).toBe(0); - promises.push(objectAPI.get(mockDomainObject.identifier)); - expect(mockProvider.get.calls.count()).toBe(1); - 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(); - - 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}`); - }); - }); - }); + describe('transactions', () => { + beforeEach(() => { + spyOn(openmct.editor, 'isEditing').and.returnValue(true); }); - describe("the mutation API", () => { - let testObject; - let updatedTestObject; - let mutable; - let mockProvider; - let callbacks = []; - - beforeEach(function () { - objectAPI = new ObjectAPI(typeRegistry, openmct); - testObject = { - identifier: { - namespace: TEST_NAMESPACE, - key: TEST_KEY - }, - name: 'test object', - type: 'notebook', - otherAttribute: 'other-attribute-value', - modified: 0, - persisted: 0, - objectAttribute: { - embeddedObject: { - embeddedKey: 'embedded-value' - } - } - }; - updatedTestObject = Object.assign({ - otherAttribute: 'changed-attribute-value' - }, testObject); - updatedTestObject.modified = 1; - updatedTestObject.persisted = 1; - - mockProvider = jasmine.createSpyObj("mock provider", [ - "get", - "create", - "update", - "observe", - "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); - - return () => {}; - }); - mockProvider.observe.and.callFake((id, callback) => { - if (callbacks.length === 0) { - callbacks.push(callback); - } else { - callbacks[0] = callback; - } - - return () => {}; - }); - - objectAPI.addProvider(TEST_NAMESPACE, mockProvider); - - return objectAPI.getMutable(testObject.identifier) - .then(object => { - mutable = object; - - return mutable; - }); - }); - - afterEach(() => { - mutable.$destroy(); - }); - - it('mutates the original object', () => { - const MUTATED_NAME = 'mutated name'; - objectAPI.mutate(testObject, 'name', MUTATED_NAME); - expect(testObject.name).toBe(MUTATED_NAME); - }); - - it('Provides a way of refreshing an object from the persistence store', () => { - const modifiedTestObject = JSON.parse(JSON.stringify(testObject)); - const OTHER_ATTRIBUTE_VALUE = 'Modified value'; - const NEW_ATTRIBUTE_VALUE = 'A new attribute'; - modifiedTestObject.otherAttribute = OTHER_ATTRIBUTE_VALUE; - modifiedTestObject.newAttribute = NEW_ATTRIBUTE_VALUE; - delete modifiedTestObject.objectAttribute; - - spyOn(objectAPI, 'get'); - objectAPI.get.and.returnValue(Promise.resolve(modifiedTestObject)); - - expect(objectAPI.get).not.toHaveBeenCalled(); - - return objectAPI.refresh(testObject).then(() => { - expect(objectAPI.get).toHaveBeenCalledWith(testObject.identifier); - - expect(testObject.otherAttribute).toEqual(OTHER_ATTRIBUTE_VALUE); - expect(testObject.newAttribute).toEqual(NEW_ATTRIBUTE_VALUE); - expect(testObject.objectAttribute).not.toBeDefined(); - }); - }); - - describe ('uses a MutableDomainObject', () => { - it('and retains properties of original object ', function () { - expect(hasOwnProperty(mutable, 'identifier')).toBe(true); - expect(hasOwnProperty(mutable, 'otherAttribute')).toBe(true); - expect(mutable.identifier).toEqual(testObject.identifier); - expect(mutable.otherAttribute).toEqual(testObject.otherAttribute); - }); - - it('that is identical to original object when serialized', function () { - expect(JSON.stringify(mutable)).toEqual(JSON.stringify(testObject)); - }); - - it('that observes for object changes', function () { - let mockListener = jasmine.createSpy('mockListener'); - objectAPI.observe(testObject, '*', mockListener); - mockProvider.observeObjectChanges(); - expect(mockListener).toHaveBeenCalled(); - }); - }); - - describe('uses events', function () { - let testObjectDuplicate; - let mutableSecondInstance; - - beforeEach(function () { - // Duplicate object to guarantee we are not sharing object instance, which would invalidate test - testObjectDuplicate = JSON.parse(JSON.stringify(testObject)); - mutableSecondInstance = objectAPI.toMutable(testObjectDuplicate); - }); - - afterEach(() => { - mutableSecondInstance.$destroy(); - }); - - it('to stay synchronized when mutated', function () { - objectAPI.mutate(mutable, 'otherAttribute', 'new-attribute-value'); - expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value'); - }); - - it('to indicate when a property changes', function () { - let mutationCallback = jasmine.createSpy('mutation-callback'); - let unlisten; - - return new Promise(function (resolve) { - mutationCallback.and.callFake(resolve); - unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback); - objectAPI.mutate(mutable, 'otherAttribute', 'some-new-value'); - }).then(function () { - expect(mutationCallback).toHaveBeenCalledWith('some-new-value', 'other-attribute-value'); - unlisten(); - }); - }); - - it('to indicate when a child property has changed', function () { - let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback'); - let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback'); - let objectAttributeCallback = jasmine.createSpy('objectAttribute'); - let listeners = []; - - return new Promise(function (resolve) { - objectAttributeCallback.and.callFake(resolve); - - listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback)); - listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback)); - listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback)); - - objectAPI.mutate(mutable, 'objectAttribute.embeddedObject.embeddedKey', 'updated-embedded-value'); - }).then(function () { - expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value', 'embedded-value'); - expect(embeddedObjectCallback).toHaveBeenCalledWith({ - embeddedKey: 'updated-embedded-value' - }, { - embeddedKey: 'embedded-value' - }); - expect(objectAttributeCallback).toHaveBeenCalledWith({ - embeddedObject: { - embeddedKey: 'updated-embedded-value' - } - }, { - embeddedObject: { - embeddedKey: 'embedded-value' - } - }); - - listeners.forEach(listener => listener()); - }); - }); - }); + it('there is no active transaction', () => { + expect(objectAPI.isTransactionActive()).toBe(false); }); - 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); - }); + it('start a transaction', () => { + objectAPI.startTransaction(); + expect(objectAPI.isTransactionActive()).toBe(true); }); - describe("transactions", () => { - beforeEach(() => { - spyOn(openmct.editor, 'isEditing').and.returnValue(true); - }); - - it('there is no active transaction', () => { - expect(objectAPI.isTransactionActive()).toBe(false); - }); - - it('start a transaction', () => { - objectAPI.startTransaction(); - expect(objectAPI.isTransactionActive()).toBe(true); - }); - - it('has active transaction', () => { - objectAPI.startTransaction(); - const activeTransaction = objectAPI.getActiveTransaction(); - expect(activeTransaction).not.toBe(null); - }); - - it('end a transaction', () => { - objectAPI.endTransaction(); - expect(objectAPI.isTransactionActive()).toBe(false); - }); - - it('returns dirty object on get', (done) => { - spyOn(objectAPI, 'supportsMutation').and.returnValue(true); - - objectAPI.startTransaction(); - objectAPI.mutate(mockDomainObject, 'name', 'dirty object'); - - const dirtyObject = objectAPI.transaction.getDirtyObject(mockDomainObject.identifier); - - objectAPI.get(mockDomainObject.identifier) - .then(object => { - const areEqual = JSON.stringify(object) === JSON.stringify(dirtyObject); - expect(areEqual).toBe(true); - }) - .finally(done); - }); + it('has active transaction', () => { + objectAPI.startTransaction(); + const activeTransaction = objectAPI.getActiveTransaction(); + expect(activeTransaction).not.toBe(null); }); + + it('end a transaction', () => { + objectAPI.endTransaction(); + expect(objectAPI.isTransactionActive()).toBe(false); + }); + + it('returns dirty object on get', (done) => { + spyOn(objectAPI, 'supportsMutation').and.returnValue(true); + + objectAPI.startTransaction(); + objectAPI.mutate(mockDomainObject, 'name', 'dirty object'); + + const dirtyObject = objectAPI.transaction.getDirtyObject(mockDomainObject.identifier); + + objectAPI + .get(mockDomainObject.identifier) + .then((object) => { + const areEqual = JSON.stringify(object) === JSON.stringify(dirtyObject); + expect(areEqual).toBe(true); + }) + .finally(done); + }); + }); }); function hasOwnProperty(object, property) { - return Object.prototype.hasOwnProperty.call(object, property); + return Object.prototype.hasOwnProperty.call(object, property); } diff --git a/src/api/objects/RootObjectProvider.js b/src/api/objects/RootObjectProvider.js index 7f90c2a372..1af453fcf6 100644 --- a/src/api/objects/RootObjectProvider.js +++ b/src/api/objects/RootObjectProvider.js @@ -21,41 +21,41 @@ *****************************************************************************/ class RootObjectProvider { - constructor(rootRegistry) { - if (!RootObjectProvider.instance) { - this.rootRegistry = rootRegistry; - this.rootObject = { - identifier: { - key: "ROOT", - namespace: "" - }, - name: 'Open MCT', - type: 'root', - composition: [] - }; - RootObjectProvider.instance = this; - } else if (rootRegistry) { - // if called twice, update instance rootRegistry - RootObjectProvider.instance.rootRegistry = rootRegistry; - } - - return RootObjectProvider.instance; // eslint-disable-line no-constructor-return + constructor(rootRegistry) { + if (!RootObjectProvider.instance) { + this.rootRegistry = rootRegistry; + this.rootObject = { + identifier: { + key: 'ROOT', + namespace: '' + }, + name: 'Open MCT', + type: 'root', + composition: [] + }; + RootObjectProvider.instance = this; + } else if (rootRegistry) { + // if called twice, update instance rootRegistry + RootObjectProvider.instance.rootRegistry = rootRegistry; } - updateName(name) { - this.rootObject.name = name; - } + return RootObjectProvider.instance; // eslint-disable-line no-constructor-return + } - async get() { - let roots = await this.rootRegistry.getRoots(); - this.rootObject.composition = roots; + updateName(name) { + this.rootObject.name = name; + } - return this.rootObject; - } + async get() { + let roots = await this.rootRegistry.getRoots(); + this.rootObject.composition = roots; + + return this.rootObject; + } } function instance(rootRegistry) { - return new RootObjectProvider(rootRegistry); + return new RootObjectProvider(rootRegistry); } export default instance; diff --git a/src/api/objects/RootRegistry.js b/src/api/objects/RootRegistry.js index d952aca79f..69a152be0e 100644 --- a/src/api/objects/RootRegistry.js +++ b/src/api/objects/RootRegistry.js @@ -23,40 +23,38 @@ import utils from './object-utils'; export default class RootRegistry { + constructor(openmct) { + this._rootItems = []; + this._openmct = openmct; + } - constructor(openmct) { - this._rootItems = []; - this._openmct = openmct; + getRoots() { + const sortedItems = this._rootItems.sort((a, b) => b.priority - a.priority); + const promises = sortedItems.map((rootItem) => rootItem.provider()); + + return Promise.all(promises).then((rootItems) => rootItems.flat()); + } + + addRoot(rootItem, priority) { + if (!this._isValid(rootItem)) { + return; } - getRoots() { - const sortedItems = this._rootItems.sort((a, b) => b.priority - a.priority); - const promises = sortedItems.map((rootItem) => rootItem.provider()); + this._rootItems.push({ + priority: priority || this._openmct.priority.DEFAULT, + provider: typeof rootItem === 'function' ? rootItem : () => rootItem + }); + } - return Promise.all(promises).then(rootItems => rootItems.flat()); + _isValid(rootItem) { + if (utils.isIdentifier(rootItem) || typeof rootItem === 'function') { + return true; } - addRoot(rootItem, priority) { - - if (!this._isValid(rootItem)) { - return; - } - - this._rootItems.push({ - priority: priority || this._openmct.priority.DEFAULT, - provider: typeof rootItem === 'function' ? rootItem : () => rootItem - }); + if (Array.isArray(rootItem)) { + return rootItem.every(utils.isIdentifier); } - _isValid(rootItem) { - if (utils.isIdentifier(rootItem) || typeof rootItem === 'function') { - return true; - } - - if (Array.isArray(rootItem)) { - return rootItem.every(utils.isIdentifier); - } - - return false; - } + return false; + } } diff --git a/src/api/objects/Transaction.js b/src/api/objects/Transaction.js index 3893882219..7eac5e6e7f 100644 --- a/src/api/objects/Transaction.js +++ b/src/api/objects/Transaction.js @@ -21,66 +21,66 @@ *****************************************************************************/ export default class Transaction { - constructor(objectAPI) { - this.dirtyObjects = {}; - this.objectAPI = objectAPI; - } + constructor(objectAPI) { + this.dirtyObjects = {}; + this.objectAPI = objectAPI; + } - add(object) { - const key = this.objectAPI.makeKeyString(object.identifier); + add(object) { + const key = this.objectAPI.makeKeyString(object.identifier); - this.dirtyObjects[key] = object; - } + this.dirtyObjects[key] = object; + } - cancel() { - return this._clear(); - } + cancel() { + return this._clear(); + } - commit() { - const promiseArray = []; - const save = this.objectAPI.save.bind(this.objectAPI); + commit() { + const promiseArray = []; + const save = this.objectAPI.save.bind(this.objectAPI); - Object.values(this.dirtyObjects).forEach(object => { - promiseArray.push(this.createDirtyObjectPromise(object, save)); - }); + Object.values(this.dirtyObjects).forEach((object) => { + promiseArray.push(this.createDirtyObjectPromise(object, save)); + }); - return Promise.all(promiseArray); - } + return Promise.all(promiseArray); + } - createDirtyObjectPromise(object, action) { - return new Promise((resolve, reject) => { - action(object) - .then((success) => { - const key = this.objectAPI.makeKeyString(object.identifier); + createDirtyObjectPromise(object, action) { + return new Promise((resolve, reject) => { + action(object) + .then((success) => { + const key = this.objectAPI.makeKeyString(object.identifier); - delete this.dirtyObjects[key]; - resolve(success); - }) - .catch(reject); - }); - } + delete this.dirtyObjects[key]; + resolve(success); + }) + .catch(reject); + }); + } - getDirtyObject(identifier) { - let dirtyObject; + getDirtyObject(identifier) { + let dirtyObject; - Object.values(this.dirtyObjects).forEach(object => { - const areIdsEqual = this.objectAPI.areIdsEqual(object.identifier, identifier); - if (areIdsEqual) { - dirtyObject = object; - } - }); + Object.values(this.dirtyObjects).forEach((object) => { + const areIdsEqual = this.objectAPI.areIdsEqual(object.identifier, identifier); + if (areIdsEqual) { + dirtyObject = object; + } + }); - return dirtyObject; - } + return dirtyObject; + } - _clear() { - const promiseArray = []; - const refresh = this.objectAPI.refresh.bind(this.objectAPI); + _clear() { + const promiseArray = []; + const refresh = this.objectAPI.refresh.bind(this.objectAPI); - Object.values(this.dirtyObjects).forEach(object => { - promiseArray.push(this.createDirtyObjectPromise(object, refresh)); - }); + Object.values(this.dirtyObjects).forEach((object) => { + promiseArray.push(this.createDirtyObjectPromise(object, refresh)); + }); - return Promise.all(promiseArray); - } + return Promise.all(promiseArray); + } } diff --git a/src/api/objects/TransactionSpec.js b/src/api/objects/TransactionSpec.js index 8d195f0fe2..08eb1cc21b 100644 --- a/src/api/objects/TransactionSpec.js +++ b/src/api/objects/TransactionSpec.js @@ -1,111 +1,116 @@ -import Transaction from "./Transaction"; +import Transaction from './Transaction'; import utils from 'objectUtils'; let openmct = {}; let objectAPI; let transaction; -describe("Transaction Class", () => { - beforeEach(() => { - objectAPI = { - makeKeyString: (identifier) => utils.makeKeyString(identifier), - save: () => Promise.resolve(true), - mutate: (object, prop, value) => { - object[prop] = value; +describe('Transaction Class', () => { + beforeEach(() => { + objectAPI = { + makeKeyString: (identifier) => utils.makeKeyString(identifier), + save: () => Promise.resolve(true), + mutate: (object, prop, value) => { + object[prop] = value; - return object; - }, - refresh: (object) => Promise.resolve(object), - areIdsEqual: (...identifiers) => { - return identifiers.map(utils.parseKeyString) - .every(identifier => { - return identifier === identifiers[0] - || (identifier.namespace === identifiers[0].namespace - && identifier.key === identifiers[0].key); - }); - } - }; + return object; + }, + refresh: (object) => Promise.resolve(object), + areIdsEqual: (...identifiers) => { + return identifiers.map(utils.parseKeyString).every((identifier) => { + return ( + identifier === identifiers[0] || + (identifier.namespace === identifiers[0].namespace && + identifier.key === identifiers[0].key) + ); + }); + } + }; - transaction = new Transaction(objectAPI); + transaction = new Transaction(objectAPI); - openmct.editor = { - isEditing: () => true - }; - }); + openmct.editor = { + isEditing: () => true + }; + }); - it('has no dirty objects', () => { + it('has no dirty objects', () => { + expect(Object.keys(transaction.dirtyObjects).length).toEqual(0); + }); + + it('add(), adds object to dirtyObjects', () => { + const mockDomainObjects = createMockDomainObjects(); + transaction.add(mockDomainObjects[0]); + expect(Object.keys(transaction.dirtyObjects).length).toEqual(1); + }); + + it('cancel(), clears all dirtyObjects', (done) => { + const mockDomainObjects = createMockDomainObjects(3); + mockDomainObjects.forEach(transaction.add.bind(transaction)); + + expect(Object.keys(transaction.dirtyObjects).length).toEqual(3); + + transaction + .cancel() + .then((success) => { expect(Object.keys(transaction.dirtyObjects).length).toEqual(0); - }); + }) + .finally(done); + }); - it('add(), adds object to dirtyObjects', () => { - const mockDomainObjects = createMockDomainObjects(); - transaction.add(mockDomainObjects[0]); - expect(Object.keys(transaction.dirtyObjects).length).toEqual(1); - }); + it('commit(), saves all dirtyObjects', (done) => { + const mockDomainObjects = createMockDomainObjects(3); + mockDomainObjects.forEach(transaction.add.bind(transaction)); - it('cancel(), clears all dirtyObjects', (done) => { - const mockDomainObjects = createMockDomainObjects(3); - mockDomainObjects.forEach(transaction.add.bind(transaction)); - - expect(Object.keys(transaction.dirtyObjects).length).toEqual(3); - - transaction.cancel() - .then(success => { - expect(Object.keys(transaction.dirtyObjects).length).toEqual(0); - }).finally(done); - }); - - it('commit(), saves all dirtyObjects', (done) => { - const mockDomainObjects = createMockDomainObjects(3); - mockDomainObjects.forEach(transaction.add.bind(transaction)); - - expect(Object.keys(transaction.dirtyObjects).length).toEqual(3); - spyOn(objectAPI, 'save').and.callThrough(); - - transaction.commit() - .then(success => { - expect(Object.keys(transaction.dirtyObjects).length).toEqual(0); - expect(objectAPI.save.calls.count()).toEqual(3); - }).finally(done); - }); - - it('getDirtyObject(), returns correct dirtyObject', () => { - const mockDomainObjects = createMockDomainObjects(); - transaction.add(mockDomainObjects[0]); - - expect(Object.keys(transaction.dirtyObjects).length).toEqual(1); - const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier); - - expect(dirtyObject).toEqual(mockDomainObjects[0]); - }); - - it('getDirtyObject(), returns empty dirtyObject for no active transaction', () => { - const mockDomainObjects = createMockDomainObjects(); + expect(Object.keys(transaction.dirtyObjects).length).toEqual(3); + spyOn(objectAPI, 'save').and.callThrough(); + transaction + .commit() + .then((success) => { expect(Object.keys(transaction.dirtyObjects).length).toEqual(0); - const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier); + expect(objectAPI.save.calls.count()).toEqual(3); + }) + .finally(done); + }); - expect(dirtyObject).toEqual(undefined); - }); + it('getDirtyObject(), returns correct dirtyObject', () => { + const mockDomainObjects = createMockDomainObjects(); + transaction.add(mockDomainObjects[0]); + + expect(Object.keys(transaction.dirtyObjects).length).toEqual(1); + const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier); + + expect(dirtyObject).toEqual(mockDomainObjects[0]); + }); + + it('getDirtyObject(), returns empty dirtyObject for no active transaction', () => { + const mockDomainObjects = createMockDomainObjects(); + + expect(Object.keys(transaction.dirtyObjects).length).toEqual(0); + const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier); + + expect(dirtyObject).toEqual(undefined); + }); }); function createMockDomainObjects(size = 1) { - const objects = []; + const objects = []; - while (size > 0) { - const mockDomainObject = { - identifier: { - namespace: 'test-namespace', - key: `test-key-${size}` - }, - name: `test object ${size}`, - type: 'test-type' - }; + while (size > 0) { + const mockDomainObject = { + identifier: { + namespace: 'test-namespace', + key: `test-key-${size}` + }, + name: `test object ${size}`, + type: 'test-type' + }; - objects.push(mockDomainObject); + objects.push(mockDomainObject); - size--; - } + size--; + } - return objects; + return objects; } diff --git a/src/api/objects/object-utils.js b/src/api/objects/object-utils.js index 28fe7693a8..08d2c01ffc 100644 --- a/src/api/objects/object-utils.js +++ b/src/api/objects/object-utils.js @@ -20,169 +20,163 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ +define([], function () { + /** + * Utility for checking if a thing is an Open MCT Identifier. + * @private + */ + function isIdentifier(thing) { + return ( + typeof thing === 'object' && + Object.prototype.hasOwnProperty.call(thing, 'key') && + Object.prototype.hasOwnProperty.call(thing, 'namespace') + ); + } -], function ( + /** + * Utility for checking if a thing is a key string. Not perfect. + * @private + */ + function isKeyString(thing) { + return typeof thing === 'string'; + } -) { - - /** - * Utility for checking if a thing is an Open MCT Identifier. - * @private - */ - function isIdentifier(thing) { - return typeof thing === 'object' - && Object.prototype.hasOwnProperty.call(thing, 'key') - && Object.prototype.hasOwnProperty.call(thing, 'namespace'); + /** + * Convert a keyString into an Open MCT Identifier, ex: + * 'scratch:root' ==> {namespace: 'scratch', key: 'root'} + * + * Idempotent. + * + * @param keyString + * @returns identifier + */ + function parseKeyString(keyString) { + if (isIdentifier(keyString)) { + return keyString; } - /** - * Utility for checking if a thing is a key string. Not perfect. - * @private - */ - function isKeyString(thing) { - return typeof thing === 'string'; + let namespace = ''; + let key = keyString; + for (let i = 0; i < key.length; i++) { + if (key[i] === '\\' && key[i + 1] === ':') { + i++; // skip escape character. + } else if (key[i] === ':') { + key = key.slice(i + 1); + break; + } + + namespace += key[i]; } - /** - * Convert a keyString into an Open MCT Identifier, ex: - * 'scratch:root' ==> {namespace: 'scratch', key: 'root'} - * - * Idempotent. - * - * @param keyString - * @returns identifier - */ - function parseKeyString(keyString) { - if (isIdentifier(keyString)) { - return keyString; - } - - let namespace = ''; - let key = keyString; - for (let i = 0; i < key.length; i++) { - if (key[i] === "\\" && key[i + 1] === ":") { - i++; // skip escape character. - } else if (key[i] === ":") { - key = key.slice(i + 1); - break; - } - - namespace += key[i]; - } - - if (keyString === namespace) { - namespace = ''; - } - - return { - namespace: namespace, - key: key - }; - } - - /** - * Convert an Open MCT Identifier into a keyString, ex: - * {namespace: 'scratch', key: 'root'} ==> 'scratch:root' - * - * Idempotent - * - * @param identifier - * @returns keyString - */ - function makeKeyString(identifier) { - if (!identifier) { - throw new Error("Cannot make key string from null identifier"); - } - - if (isKeyString(identifier)) { - return identifier; - } - - if (!identifier.namespace) { - return identifier.key; - } - - return [ - identifier.namespace.replace(/:/g, '\\:'), - identifier.key - ].join(':'); - } - - /** - * Convert a new domain object into an old format model, removing the - * identifier and converting the composition array from Open MCT Identifiers - * to old format keyStrings. - * - * @param domainObject - * @returns oldFormatModel - */ - function toOldFormat(model) { - model = JSON.parse(JSON.stringify(model)); - delete model.identifier; - if (model.composition) { - model.composition = model.composition.map(makeKeyString); - } - - return model; - } - - /** - * Convert an old format domain object model into a new format domain - * object. Adds an identifier using the provided keyString, and converts - * the composition array to utilize Open MCT Identifiers. - * - * @param model - * @param keyString - * @returns domainObject - */ - function toNewFormat(model, keyString) { - model = JSON.parse(JSON.stringify(model)); - model.identifier = parseKeyString(keyString); - if (model.composition) { - model.composition = model.composition.map(parseKeyString); - } - - return model; - } - - /** - * Compare two Open MCT Identifiers, returning true if they are equal. - * - * @param identifier - * @param otherIdentifier - * @returns Boolean true if identifiers are equal. - */ - function identifierEquals(a, b) { - return a.key === b.key && a.namespace === b.namespace; - } - - /** - * Compare two domain objects, return true if they're the same object. - * Equality is determined by identifier. - * - * @param domainObject - * @param otherDomainOBject - * @returns Boolean true if objects are equal. - */ - function objectEquals(a, b) { - return identifierEquals(a.identifier, b.identifier); - } - - function refresh(oldObject, newObject) { - let deleted = _.difference(Object.keys(oldObject), Object.keys(newObject)); - deleted.forEach((propertyName) => delete oldObject[propertyName]); - Object.assign(oldObject, newObject); + if (keyString === namespace) { + namespace = ''; } return { - isIdentifier: isIdentifier, - toOldFormat: toOldFormat, - toNewFormat: toNewFormat, - makeKeyString: makeKeyString, - parseKeyString: parseKeyString, - equals: objectEquals, - identifierEquals: identifierEquals, - refresh: refresh + namespace: namespace, + key: key }; + } + + /** + * Convert an Open MCT Identifier into a keyString, ex: + * {namespace: 'scratch', key: 'root'} ==> 'scratch:root' + * + * Idempotent + * + * @param identifier + * @returns keyString + */ + function makeKeyString(identifier) { + if (!identifier) { + throw new Error('Cannot make key string from null identifier'); + } + + if (isKeyString(identifier)) { + return identifier; + } + + if (!identifier.namespace) { + return identifier.key; + } + + return [identifier.namespace.replace(/:/g, '\\:'), identifier.key].join(':'); + } + + /** + * Convert a new domain object into an old format model, removing the + * identifier and converting the composition array from Open MCT Identifiers + * to old format keyStrings. + * + * @param domainObject + * @returns oldFormatModel + */ + function toOldFormat(model) { + model = JSON.parse(JSON.stringify(model)); + delete model.identifier; + if (model.composition) { + model.composition = model.composition.map(makeKeyString); + } + + return model; + } + + /** + * Convert an old format domain object model into a new format domain + * object. Adds an identifier using the provided keyString, and converts + * the composition array to utilize Open MCT Identifiers. + * + * @param model + * @param keyString + * @returns domainObject + */ + function toNewFormat(model, keyString) { + model = JSON.parse(JSON.stringify(model)); + model.identifier = parseKeyString(keyString); + if (model.composition) { + model.composition = model.composition.map(parseKeyString); + } + + return model; + } + + /** + * Compare two Open MCT Identifiers, returning true if they are equal. + * + * @param identifier + * @param otherIdentifier + * @returns Boolean true if identifiers are equal. + */ + function identifierEquals(a, b) { + return a.key === b.key && a.namespace === b.namespace; + } + + /** + * Compare two domain objects, return true if they're the same object. + * Equality is determined by identifier. + * + * @param domainObject + * @param otherDomainOBject + * @returns Boolean true if objects are equal. + */ + function objectEquals(a, b) { + return identifierEquals(a.identifier, b.identifier); + } + + function refresh(oldObject, newObject) { + let deleted = _.difference(Object.keys(oldObject), Object.keys(newObject)); + deleted.forEach((propertyName) => delete oldObject[propertyName]); + Object.assign(oldObject, newObject); + } + + return { + isIdentifier: isIdentifier, + toOldFormat: toOldFormat, + toNewFormat: toNewFormat, + makeKeyString: makeKeyString, + parseKeyString: parseKeyString, + equals: objectEquals, + identifierEquals: identifierEquals, + refresh: refresh + }; }); diff --git a/src/api/objects/test/RootObjectProviderSpec.js b/src/api/objects/test/RootObjectProviderSpec.js index dca4addc3b..62ebb2965f 100644 --- a/src/api/objects/test/RootObjectProviderSpec.js +++ b/src/api/objects/test/RootObjectProviderSpec.js @@ -22,30 +22,30 @@ import RootObjectProvider from '../RootObjectProvider'; describe('RootObjectProvider', function () { - const ROOT_NAME = 'Open MCT'; - let rootObjectProvider; - let roots = ['some root']; - let rootRegistry = { - getRoots: () => { - return Promise.resolve(roots); - } - }; + const ROOT_NAME = 'Open MCT'; + let rootObjectProvider; + let roots = ['some root']; + let rootRegistry = { + getRoots: () => { + return Promise.resolve(roots); + } + }; - beforeEach(function () { - rootObjectProvider = new RootObjectProvider(rootRegistry); - }); - - it('supports fetching root', async () => { - let root = await rootObjectProvider.get(); - - expect(root).toEqual({ - identifier: { - key: "ROOT", - namespace: "" - }, - name: ROOT_NAME, - type: 'root', - composition: ['some root'] - }); + beforeEach(function () { + rootObjectProvider = new RootObjectProvider(rootRegistry); + }); + + it('supports fetching root', async () => { + let root = await rootObjectProvider.get(); + + expect(root).toEqual({ + identifier: { + key: 'ROOT', + namespace: '' + }, + name: ROOT_NAME, + type: 'root', + composition: ['some root'] }); + }); }); diff --git a/src/api/objects/test/RootRegistrySpec.js b/src/api/objects/test/RootRegistrySpec.js index ce5ccdf410..e5485a869c 100644 --- a/src/api/objects/test/RootRegistrySpec.js +++ b/src/api/objects/test/RootRegistrySpec.js @@ -23,109 +23,102 @@ import { createOpenMct, resetApplicationState } from '../../../utils/testing'; describe('RootRegistry', () => { - let openmct; - let idA; - let idB; - let idC; - let idD; + let openmct; + let idA; + let idB; + let idC; + let idD; - beforeEach((done) => { - openmct = createOpenMct(); - idA = { - key: 'keyA', - namespace: 'something' - }; - idB = { - key: 'keyB', - namespace: 'something' - }; - idC = { - key: 'keyC', - namespace: 'something' - }; - idD = { - key: 'keyD', - namespace: 'something' - }; + beforeEach((done) => { + openmct = createOpenMct(); + idA = { + key: 'keyA', + namespace: 'something' + }; + idB = { + key: 'keyB', + namespace: 'something' + }; + idC = { + key: 'keyC', + namespace: 'something' + }; + idD = { + key: 'keyD', + namespace: 'something' + }; - openmct.on('start', done); - openmct.startHeadless(); + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach(async () => { + await resetApplicationState(openmct); + }); + + it('can register a root by identifier', () => { + openmct.objects.addRoot(idA); + + return openmct.objects.getRoot().then((rootObject) => { + expect(rootObject.composition).toEqual([idA]); }); + }); - afterEach(async () => { - await resetApplicationState(openmct); + it('can register multiple roots by identifier', () => { + openmct.objects.addRoot([idA, idB]); + + return openmct.objects.getRoot().then((rootObject) => { + expect(rootObject.composition).toEqual([idA, idB]); }); + }); - it('can register a root by identifier', () => { - openmct.objects.addRoot(idA); + it('can register an asynchronous root ', () => { + openmct.objects.addRoot(() => Promise.resolve(idA)); - return openmct.objects.getRoot() - .then((rootObject) => { - expect(rootObject.composition).toEqual([idA]); - }); + return openmct.objects.getRoot().then((rootObject) => { + expect(rootObject.composition).toEqual([idA]); }); + }); - it('can register multiple roots by identifier', () => { - openmct.objects.addRoot([idA, idB]); + it('can register multiple asynchronous roots', () => { + openmct.objects.addRoot(() => Promise.resolve([idA, idB])); - return openmct.objects.getRoot() - .then((rootObject) => { - expect(rootObject.composition).toEqual([idA, idB]); - }); + return openmct.objects.getRoot().then((rootObject) => { + expect(rootObject.composition).toEqual([idA, idB]); }); + }); - it('can register an asynchronous root ', () => { - openmct.objects.addRoot(() => Promise.resolve(idA)); + it('can combine different types of registration', () => { + openmct.objects.addRoot([idA, idB]); + openmct.objects.addRoot(() => Promise.resolve([idC])); - return openmct.objects.getRoot() - .then((rootObject) => { - expect(rootObject.composition).toEqual([idA]); - }); + return openmct.objects.getRoot().then((rootObject) => { + expect(rootObject.composition).toEqual([idA, idB, idC]); }); + }); - it('can register multiple asynchronous roots', () => { - openmct.objects.addRoot(() => Promise.resolve([idA, idB])); + it('supports priority ordering for identifiers', () => { + openmct.objects.addRoot(idA, openmct.priority.LOW); + openmct.objects.addRoot(idB, openmct.priority.HIGH); + openmct.objects.addRoot(idC); // DEFAULT - return openmct.objects.getRoot() - .then((rootObject) => { - expect(rootObject.composition).toEqual([idA, idB]); - }); + return openmct.objects.getRoot().then((rootObject) => { + expect(rootObject.composition[0]).toEqual(idB); + expect(rootObject.composition[1]).toEqual(idC); + expect(rootObject.composition[2]).toEqual(idA); }); + }); - it('can combine different types of registration', () => { - openmct.objects.addRoot([idA, idB]); - openmct.objects.addRoot(() => Promise.resolve([idC])); + it('supports priority ordering for different types of registration', () => { + openmct.objects.addRoot(() => Promise.resolve([idC]), openmct.priority.LOW); + openmct.objects.addRoot(idB, openmct.priority.HIGH); + openmct.objects.addRoot([idA, idD]); // default - return openmct.objects.getRoot() - .then((rootObject) => { - expect(rootObject.composition).toEqual([idA, idB, idC]); - }); - }); - - it('supports priority ordering for identifiers', () => { - openmct.objects.addRoot(idA, openmct.priority.LOW); - openmct.objects.addRoot(idB, openmct.priority.HIGH); - openmct.objects.addRoot(idC); // DEFAULT - - return openmct.objects.getRoot() - .then((rootObject) => { - expect(rootObject.composition[0]).toEqual(idB); - expect(rootObject.composition[1]).toEqual(idC); - expect(rootObject.composition[2]).toEqual(idA); - }); - }); - - it('supports priority ordering for different types of registration', () => { - openmct.objects.addRoot(() => Promise.resolve([idC]), openmct.priority.LOW); - openmct.objects.addRoot(idB, openmct.priority.HIGH); - openmct.objects.addRoot([idA, idD]); // default - - return openmct.objects.getRoot() - .then((rootObject) => { - expect(rootObject.composition[0]).toEqual(idB); - expect(rootObject.composition[1]).toEqual(idA); - expect(rootObject.composition[2]).toEqual(idD); - expect(rootObject.composition[3]).toEqual(idC); - }); + return openmct.objects.getRoot().then((rootObject) => { + expect(rootObject.composition[0]).toEqual(idB); + expect(rootObject.composition[1]).toEqual(idA); + expect(rootObject.composition[2]).toEqual(idD); + expect(rootObject.composition[3]).toEqual(idC); }); + }); }); diff --git a/src/api/objects/test/object-utilsSpec.js b/src/api/objects/test/object-utilsSpec.js index 25f6ad07e6..9555190e98 100644 --- a/src/api/objects/test/object-utilsSpec.js +++ b/src/api/objects/test/object-utilsSpec.js @@ -1,152 +1,147 @@ -define([ - 'objectUtils' -], function ( - objectUtils -) { - describe('objectUtils', function () { +define(['objectUtils'], function (objectUtils) { + describe('objectUtils', function () { + describe('keyString util', function () { + const EXPECTATIONS = { + ROOT: { + namespace: '', + key: 'ROOT' + }, + mine: { + namespace: '', + key: 'mine' + }, + 'extended:something:with:colons': { + key: 'something:with:colons', + namespace: 'extended' + }, + 'https\\://some/url:resourceId': { + key: 'resourceId', + namespace: 'https://some/url' + }, + 'scratch:root': { + namespace: 'scratch', + key: 'root' + }, + 'thingy\\:thing:abc123': { + namespace: 'thingy:thing', + key: 'abc123' + } + }; - describe('keyString util', function () { - const EXPECTATIONS = { - 'ROOT': { - namespace: '', - key: 'ROOT' - }, - 'mine': { - namespace: '', - key: 'mine' - }, - 'extended:something:with:colons': { - key: 'something:with:colons', - namespace: 'extended' - }, - 'https\\://some/url:resourceId': { - key: 'resourceId', - namespace: 'https://some/url' - }, - 'scratch:root': { - namespace: 'scratch', - key: 'root' - }, - 'thingy\\:thing:abc123': { - namespace: 'thingy:thing', - key: 'abc123' - } - }; - - Object.keys(EXPECTATIONS).forEach(function (keyString) { - it('parses "' + keyString + '".', function () { - expect(objectUtils.parseKeyString(keyString)) - .toEqual(EXPECTATIONS[keyString]); - }); - - it('parses and re-encodes "' + keyString + '"', function () { - const identifier = objectUtils.parseKeyString(keyString); - expect(objectUtils.makeKeyString(identifier)) - .toEqual(keyString); - }); - - it('is idempotent for "' + keyString + '".', function () { - const identifier = objectUtils.parseKeyString(keyString); - let again = objectUtils.parseKeyString(identifier); - expect(identifier).toEqual(again); - again = objectUtils.parseKeyString(again); - again = objectUtils.parseKeyString(again); - expect(identifier).toEqual(again); - - let againKeyString = objectUtils.makeKeyString(again); - expect(againKeyString).toEqual(keyString); - againKeyString = objectUtils.makeKeyString(againKeyString); - againKeyString = objectUtils.makeKeyString(againKeyString); - againKeyString = objectUtils.makeKeyString(againKeyString); - expect(againKeyString).toEqual(keyString); - }); - }); + Object.keys(EXPECTATIONS).forEach(function (keyString) { + it('parses "' + keyString + '".', function () { + expect(objectUtils.parseKeyString(keyString)).toEqual(EXPECTATIONS[keyString]); }); - describe('old object conversions', function () { - - it('translate ids', function () { - expect(objectUtils.toNewFormat({ - prop: 'someValue' - }, 'objId')) - .toEqual({ - prop: 'someValue', - identifier: { - namespace: '', - key: 'objId' - } - }); - }); - - it('translates composition', function () { - expect(objectUtils.toNewFormat({ - prop: 'someValue', - composition: [ - 'anotherObjectId', - 'scratch:anotherObjectId' - ] - }, 'objId')) - .toEqual({ - prop: 'someValue', - composition: [ - { - namespace: '', - key: 'anotherObjectId' - }, - { - namespace: 'scratch', - key: 'anotherObjectId' - } - ], - identifier: { - namespace: '', - key: 'objId' - } - }); - }); + it('parses and re-encodes "' + keyString + '"', function () { + const identifier = objectUtils.parseKeyString(keyString); + expect(objectUtils.makeKeyString(identifier)).toEqual(keyString); }); - describe('new object conversions', function () { + it('is idempotent for "' + keyString + '".', function () { + const identifier = objectUtils.parseKeyString(keyString); + let again = objectUtils.parseKeyString(identifier); + expect(identifier).toEqual(again); + again = objectUtils.parseKeyString(again); + again = objectUtils.parseKeyString(again); + expect(identifier).toEqual(again); - it('removes ids', function () { - expect(objectUtils.toOldFormat({ - prop: 'someValue', - identifier: { - namespace: '', - key: 'objId' - } - })) - .toEqual({ - prop: 'someValue' - }); - }); - - it('translates composition', function () { - expect(objectUtils.toOldFormat({ - prop: 'someValue', - composition: [ - { - namespace: '', - key: 'anotherObjectId' - }, - { - namespace: 'scratch', - key: 'anotherObjectId' - } - ], - identifier: { - namespace: '', - key: 'objId' - } - })) - .toEqual({ - prop: 'someValue', - composition: [ - 'anotherObjectId', - 'scratch:anotherObjectId' - ] - }); - }); + let againKeyString = objectUtils.makeKeyString(again); + expect(againKeyString).toEqual(keyString); + againKeyString = objectUtils.makeKeyString(againKeyString); + againKeyString = objectUtils.makeKeyString(againKeyString); + againKeyString = objectUtils.makeKeyString(againKeyString); + expect(againKeyString).toEqual(keyString); }); + }); }); + + describe('old object conversions', function () { + it('translate ids', function () { + expect( + objectUtils.toNewFormat( + { + prop: 'someValue' + }, + 'objId' + ) + ).toEqual({ + prop: 'someValue', + identifier: { + namespace: '', + key: 'objId' + } + }); + }); + + it('translates composition', function () { + expect( + objectUtils.toNewFormat( + { + prop: 'someValue', + composition: ['anotherObjectId', 'scratch:anotherObjectId'] + }, + 'objId' + ) + ).toEqual({ + prop: 'someValue', + composition: [ + { + namespace: '', + key: 'anotherObjectId' + }, + { + namespace: 'scratch', + key: 'anotherObjectId' + } + ], + identifier: { + namespace: '', + key: 'objId' + } + }); + }); + }); + + describe('new object conversions', function () { + it('removes ids', function () { + expect( + objectUtils.toOldFormat({ + prop: 'someValue', + identifier: { + namespace: '', + key: 'objId' + } + }) + ).toEqual({ + prop: 'someValue' + }); + }); + + it('translates composition', function () { + expect( + objectUtils.toOldFormat({ + prop: 'someValue', + composition: [ + { + namespace: '', + key: 'anotherObjectId' + }, + { + namespace: 'scratch', + key: 'anotherObjectId' + } + ], + identifier: { + namespace: '', + key: 'objId' + } + }) + ).toEqual({ + prop: 'someValue', + composition: ['anotherObjectId', 'scratch:anotherObjectId'] + }); + }); + }); + }); }); diff --git a/src/api/overlays/Dialog.js b/src/api/overlays/Dialog.js index 502446ec2e..a3e4acfcd9 100644 --- a/src/api/overlays/Dialog.js +++ b/src/api/overlays/Dialog.js @@ -3,33 +3,32 @@ import Overlay from './Overlay'; import Vue from 'vue'; class Dialog extends Overlay { - constructor({iconClass, message, title, hint, timestamp, ...options}) { + constructor({ iconClass, message, title, hint, timestamp, ...options }) { + let component = new Vue({ + components: { + DialogComponent: DialogComponent + }, + provide: { + iconClass, + message, + title, + hint, + timestamp + }, + template: '' + }).$mount(); - let component = new Vue({ - components: { - DialogComponent: DialogComponent - }, - provide: { - iconClass, - message, - title, - hint, - timestamp - }, - template: '' - }).$mount(); + super({ + element: component.$el, + size: 'fit', + dismissable: false, + ...options + }); - super({ - element: component.$el, - size: 'fit', - dismissable: false, - ...options - }); - - this.once('destroy', () => { - component.$destroy(); - }); - } + this.once('destroy', () => { + component.$destroy(); + }); + } } export default Dialog; diff --git a/src/api/overlays/Overlay.js b/src/api/overlays/Overlay.js index 632d881b1a..c990664ec6 100644 --- a/src/api/overlays/Overlay.js +++ b/src/api/overlays/Overlay.js @@ -3,72 +3,72 @@ import EventEmitter from 'EventEmitter'; import Vue from 'vue'; const cssClasses = { - large: 'l-overlay-large', - small: 'l-overlay-small', - fit: 'l-overlay-fit', - fullscreen: 'l-overlay-fullscreen', - dialog: 'l-overlay-dialog' + large: 'l-overlay-large', + small: 'l-overlay-small', + fit: 'l-overlay-fit', + fullscreen: 'l-overlay-fullscreen', + dialog: 'l-overlay-dialog' }; class Overlay extends EventEmitter { - constructor({ - buttons, - autoHide = true, - dismissable = true, + constructor({ + buttons, + autoHide = true, + dismissable = true, + element, + onDestroy, + onDismiss, + size + } = {}) { + super(); + + this.container = document.createElement('div'); + this.container.classList.add('l-overlay-wrapper', cssClasses[size]); + + this.autoHide = autoHide; + this.dismissable = dismissable !== false; + + this.component = new Vue({ + components: { + OverlayComponent: OverlayComponent + }, + provide: { + dismiss: this.notifyAndDismiss.bind(this), element, - onDestroy, - onDismiss, - size - } = {}) { - super(); + buttons, + dismissable: this.dismissable + }, + template: '' + }); - this.container = document.createElement('div'); - this.container.classList.add('l-overlay-wrapper', cssClasses[size]); - - this.autoHide = autoHide; - this.dismissable = dismissable !== false; - - this.component = new Vue({ - components: { - OverlayComponent: OverlayComponent - }, - provide: { - dismiss: this.notifyAndDismiss.bind(this), - element, - buttons, - dismissable: this.dismissable - }, - template: '' - }); - - if (onDestroy) { - this.once('destroy', onDestroy); - } - - if (onDismiss) { - this.once('dismiss', onDismiss); - } + if (onDestroy) { + this.once('destroy', onDestroy); } - dismiss() { - this.emit('destroy'); - document.body.removeChild(this.container); - this.component.$destroy(); + if (onDismiss) { + this.once('dismiss', onDismiss); } + } - //Ensures that any callers are notified that the overlay is dismissed - notifyAndDismiss() { - this.emit('dismiss'); - this.dismiss(); - } + dismiss() { + this.emit('destroy'); + document.body.removeChild(this.container); + this.component.$destroy(); + } - /** - * @private - **/ - show() { - document.body.appendChild(this.container); - this.container.appendChild(this.component.$mount().$el); - } + //Ensures that any callers are notified that the overlay is dismissed + notifyAndDismiss() { + this.emit('dismiss'); + this.dismiss(); + } + + /** + * @private + **/ + show() { + document.body.appendChild(this.container); + this.container.appendChild(this.component.$mount().$el); + } } export default Overlay; diff --git a/src/api/overlays/OverlayAPI.js b/src/api/overlays/OverlayAPI.js index 80c6238de2..f7033f6b52 100644 --- a/src/api/overlays/OverlayAPI.js +++ b/src/api/overlays/OverlayAPI.js @@ -9,129 +9,127 @@ import ProgressDialog from './ProgressDialog'; * * @memberof api/overlays * @constructor -*/ + */ class OverlayAPI { - constructor() { - this.activeOverlays = []; + constructor() { + this.activeOverlays = []; - this.dismissLastOverlay = this.dismissLastOverlay.bind(this); + this.dismissLastOverlay = this.dismissLastOverlay.bind(this); - document.addEventListener('keyup', (event) => { - if (event.key === 'Escape') { - this.dismissLastOverlay(); - } - }); + document.addEventListener('keyup', (event) => { + if (event.key === 'Escape') { + this.dismissLastOverlay(); + } + }); + } + /** + * private + */ + showOverlay(overlay) { + if (this.activeOverlays.length) { + const previousOverlay = this.activeOverlays[this.activeOverlays.length - 1]; + if (previousOverlay.autoHide) { + previousOverlay.container.classList.add('invisible'); + } } - /** - * private - */ - showOverlay(overlay) { - if (this.activeOverlays.length) { - const previousOverlay = this.activeOverlays[this.activeOverlays.length - 1]; - if (previousOverlay.autoHide) { - previousOverlay.container.classList.add('invisible'); - } - } + this.activeOverlays.push(overlay); - this.activeOverlays.push(overlay); + overlay.once('destroy', () => { + this.activeOverlays.splice(this.activeOverlays.indexOf(overlay), 1); - overlay.once('destroy', () => { - this.activeOverlays.splice(this.activeOverlays.indexOf(overlay), 1); + if (this.activeOverlays.length) { + this.activeOverlays[this.activeOverlays.length - 1].container.classList.remove('invisible'); + } + }); - if (this.activeOverlays.length) { - this.activeOverlays[this.activeOverlays.length - 1].container.classList.remove('invisible'); - } - }); + overlay.show(); + } - overlay.show(); + /** + * private + */ + dismissLastOverlay() { + let lastOverlay = this.activeOverlays[this.activeOverlays.length - 1]; + if (lastOverlay && lastOverlay.dismissable) { + lastOverlay.notifyAndDismiss(); } + } - /** - * private - */ - dismissLastOverlay() { - let lastOverlay = this.activeOverlays[this.activeOverlays.length - 1]; - if (lastOverlay && lastOverlay.dismissable) { - lastOverlay.notifyAndDismiss(); - } - } + /** + * A description of option properties that can be passed into the overlay + * @typedef options + * @property {object} element DOMElement that is to be inserted/shown on the overlay + * @property {string} size preferred size of the overlay (large, small, fit) + * @property {array} buttons optional button objects with label and callback properties + * @property {function} onDestroy callback to be called when overlay is destroyed + * @property {boolean} dismissable allow user to dismiss overlay by using esc, and clicking away + * from overlay. Unless set to false, all overlays will be dismissable by default. + */ + overlay(options) { + let overlay = new Overlay(options); - /** - * A description of option properties that can be passed into the overlay - * @typedef options - * @property {object} element DOMElement that is to be inserted/shown on the overlay - * @property {string} size preferred size of the overlay (large, small, fit) - * @property {array} buttons optional button objects with label and callback properties - * @property {function} onDestroy callback to be called when overlay is destroyed - * @property {boolean} dismissable allow user to dismiss overlay by using esc, and clicking away - * from overlay. Unless set to false, all overlays will be dismissable by default. - */ - overlay(options) { - let overlay = new Overlay(options); + this.showOverlay(overlay); - this.showOverlay(overlay); + return overlay; + } - return overlay; - } + /** + * Displays a blocking (modal) dialog. This dialog can be used for + * displaying messages that require the user's + * immediate attention. + * @param {model} options defines options for the dialog + * @returns {object} with an object with a dismiss function that can be called from the calling code + * to dismiss/destroy the dialog + * + * A description of the model options that may be passed to the + * dialog method. Note that the DialogModel described + * here is shared with the Notifications framework. + * @see NotificationService + * + * @typedef options + * @property {string} title the title to use for the dialog + * @property {string} iconClass class to apply to icon that is shown on dialog + * @property {string} message text that indicates a current message, + * @property {buttons[]} buttons a list of buttons with title and callback properties that will + * be added to the dialog. + */ + dialog(options) { + let dialog = new Dialog(options); - /** - * Displays a blocking (modal) dialog. This dialog can be used for - * displaying messages that require the user's - * immediate attention. - * @param {model} options defines options for the dialog - * @returns {object} with an object with a dismiss function that can be called from the calling code - * to dismiss/destroy the dialog - * - * A description of the model options that may be passed to the - * dialog method. Note that the DialogModel described - * here is shared with the Notifications framework. - * @see NotificationService - * - * @typedef options - * @property {string} title the title to use for the dialog - * @property {string} iconClass class to apply to icon that is shown on dialog - * @property {string} message text that indicates a current message, - * @property {buttons[]} buttons a list of buttons with title and callback properties that will - * be added to the dialog. - */ - dialog(options) { - let dialog = new Dialog(options); + this.showOverlay(dialog); - this.showOverlay(dialog); + return dialog; + } - return dialog; - } + /** + * Displays a blocking (modal) progress dialog. This dialog can be used for + * displaying messages that require the user's attention, and show progress + * @param {model} options defines options for the dialog + * @returns {object} with an object with a dismiss function that can be called from the calling code + * to dismiss/destroy the dialog and an updateProgress function that takes progressPercentage(Number 0-100) + * and progressText (string) + * + * A description of the model options that may be passed to the + * dialog method. Note that the DialogModel described + * here is shared with the Notifications framework. + * @see NotificationService + * + * @typedef options + * @property {number} progressPerc the initial progress value (0-100) or {string} 'unknown' for anonymous progress + * @property {string} progressText the initial text to be shown under the progress bar + * @property {buttons[]} buttons a list of buttons with title and callback properties that will + * be added to the dialog. + */ + progressDialog(options) { + let progressDialog = new ProgressDialog(options); - /** - * Displays a blocking (modal) progress dialog. This dialog can be used for - * displaying messages that require the user's attention, and show progress - * @param {model} options defines options for the dialog - * @returns {object} with an object with a dismiss function that can be called from the calling code - * to dismiss/destroy the dialog and an updateProgress function that takes progressPercentage(Number 0-100) - * and progressText (string) - * - * A description of the model options that may be passed to the - * dialog method. Note that the DialogModel described - * here is shared with the Notifications framework. - * @see NotificationService - * - * @typedef options - * @property {number} progressPerc the initial progress value (0-100) or {string} 'unknown' for anonymous progress - * @property {string} progressText the initial text to be shown under the progress bar - * @property {buttons[]} buttons a list of buttons with title and callback properties that will - * be added to the dialog. - */ - progressDialog(options) { - let progressDialog = new ProgressDialog(options); - - this.showOverlay(progressDialog); - - return progressDialog; - } + this.showOverlay(progressDialog); + return progressDialog; + } } export default OverlayAPI; diff --git a/src/api/overlays/ProgressDialog.js b/src/api/overlays/ProgressDialog.js index 937a15b1e2..2641acb8f7 100644 --- a/src/api/overlays/ProgressDialog.js +++ b/src/api/overlays/ProgressDialog.js @@ -5,45 +5,54 @@ import Vue from 'vue'; let component; class ProgressDialog extends Overlay { - constructor({progressPerc, progressText, iconClass, message, title, hint, timestamp, ...options}) { - component = new Vue({ - components: { - ProgressDialogComponent: ProgressDialogComponent - }, - provide: { - iconClass, - message, - title, - hint, - timestamp - }, - data() { - return { - model: { - progressPerc: progressPerc || 0, - progressText - } - }; - }, - template: '' - }).$mount(); + constructor({ + progressPerc, + progressText, + iconClass, + message, + title, + hint, + timestamp, + ...options + }) { + component = new Vue({ + components: { + ProgressDialogComponent: ProgressDialogComponent + }, + provide: { + iconClass, + message, + title, + hint, + timestamp + }, + data() { + return { + model: { + progressPerc: progressPerc || 0, + progressText + } + }; + }, + template: '' + }).$mount(); - super({ - element: component.$el, - size: 'fit', - dismissable: false, - ...options - }); + super({ + element: component.$el, + size: 'fit', + dismissable: false, + ...options + }); - this.once('destroy', () => { - component.$destroy(); - }); - } + this.once('destroy', () => { + component.$destroy(); + }); + } - updateProgress(progressPerc, progressText) { - component.model.progressPerc = progressPerc; - component.model.progressText = progressText; - } + updateProgress(progressPerc, progressText) { + component.model.progressPerc = progressPerc; + component.model.progressText = progressText; + } } export default ProgressDialog; diff --git a/src/api/overlays/components/DialogComponent.vue b/src/api/overlays/components/DialogComponent.vue index 01c8dcd807..c490463c31 100644 --- a/src/api/overlays/components/DialogComponent.vue +++ b/src/api/overlays/components/DialogComponent.vue @@ -20,42 +20,30 @@ at runtime from the About dialog for additional information. --> diff --git a/src/api/overlays/components/OverlayComponent.vue b/src/api/overlays/components/OverlayComponent.vue index 636d4ee9f0..e61ae7b0e0 100644 --- a/src/api/overlays/components/OverlayComponent.vue +++ b/src/api/overlays/components/OverlayComponent.vue @@ -20,92 +20,86 @@ at runtime from the About dialog for additional information. --> diff --git a/src/api/overlays/components/ProgressDialogComponent.vue b/src/api/overlays/components/ProgressDialogComponent.vue index b6b916ae3c..2bec219100 100644 --- a/src/api/overlays/components/ProgressDialogComponent.vue +++ b/src/api/overlays/components/ProgressDialogComponent.vue @@ -20,9 +20,9 @@ at runtime from the About dialog for additional information. --> diff --git a/src/api/overlays/components/dialog-component.scss b/src/api/overlays/components/dialog-component.scss index 71282ba08a..92edae42ac 100644 --- a/src/api/overlays/components/dialog-component.scss +++ b/src/api/overlays/components/dialog-component.scss @@ -1,81 +1,81 @@ @mixin legacyMessage() { - flex: 0 1 auto; - font-family: symbolsfont; - font-size: $messageIconD; // Singleton message in a dialog - margin-right: $interiorMarginLg; + flex: 0 1 auto; + font-family: symbolsfont; + font-size: $messageIconD; // Singleton message in a dialog + margin-right: $interiorMarginLg; } .c-message { + display: flex; + align-items: center; + + > * + * { + margin-left: $interiorMarginLg; + } + + &__icon { + // Holds a background SVG graphic + $s: 80px; + flex: 0 0 auto; + min-width: $s; + min-height: $s; + } + + &__text { display: flex; - align-items: center; + flex-direction: column; + flex: 1 1 auto; > * + * { - margin-left: $interiorMarginLg; + margin-top: $interiorMargin; + } + } + + // __text elements + &__action-text { + font-size: 1.2em; + } + + &__title { + font-size: 1.5em; + font-weight: bold; + } + + &--simple { + // Icon and text elements only + &:before { + font-size: 30px !important; } - &__icon { - // Holds a background SVG graphic - $s: 80px; - flex: 0 0 auto; - min-width: $s; - min-height: $s; + [class*='__text'] { + font-size: 1.25em; } + } - &__text { - display: flex; - flex-direction: column; - flex: 1 1 auto; + /************************** LEGACY */ + &.message-severity-info:before { + @include legacyMessage(); + content: $glyph-icon-info; + color: $colorInfo; + } - > * + * { - margin-top: $interiorMargin; - } - } - - // __text elements - &__action-text { - font-size: 1.2em; - } - - &__title { - font-size: 1.5em; - font-weight: bold; - } - - &--simple { - // Icon and text elements only - &:before { - font-size: 30px !important; - } - - [class*='__text'] { - font-size: 1.25em; - } - } - - /************************** LEGACY */ - &.message-severity-info:before { - @include legacyMessage(); - content: $glyph-icon-info; - color: $colorInfo; - } - - &.message-severity-alert:before { - @include legacyMessage(); - content: $glyph-icon-alert-rect; - color: $colorWarningLo; - } - - &.message-severity-error:before { - @include legacyMessage(); - content: $glyph-icon-alert-triangle; - color: $colorWarningHi; - } - - // Messages in a list - .c-overlay__messages & { - padding: $interiorMarginLg; - &:before { - font-size: $messageListIconD; - } + &.message-severity-alert:before { + @include legacyMessage(); + content: $glyph-icon-alert-rect; + color: $colorWarningLo; + } + + &.message-severity-error:before { + @include legacyMessage(); + content: $glyph-icon-alert-triangle; + color: $colorWarningHi; + } + + // Messages in a list + .c-overlay__messages & { + padding: $interiorMarginLg; + &:before { + font-size: $messageListIconD; } + } } diff --git a/src/api/overlays/components/overlay-component.scss b/src/api/overlays/components/overlay-component.scss index 3c05795328..be0a22b7c6 100644 --- a/src/api/overlays/components/overlay-component.scss +++ b/src/api/overlays/components/overlay-component.scss @@ -1,161 +1,168 @@ @mixin overlaySizing($marginTB: auto, $marginLR: auto, $width: auto, $height: auto) { - position: absolute; - top: $marginTB; right: $marginLR; bottom: $marginTB; left: $marginLR; - width: $width; - height: $height; + position: absolute; + top: $marginTB; + right: $marginLR; + bottom: $marginTB; + left: $marginLR; + width: $width; + height: $height; } .l-overlay-wrapper { - // Created by overlayService.js, contains this template. - // Acts as an anchor for one or more overlays. - display: contents; + // Created by overlayService.js, contains this template. + // Acts as an anchor for one or more overlays. + display: contents; } .c-overlay { + @include abs(); + z-index: 70; + + &__blocker { + display: none; // Mobile-first + } + + &__outer { @include abs(); - z-index: 70; + background: $colorBodyBg; + display: flex; + flex-direction: column; + padding: $overlayInnerMargin; + } - &__blocker { - display: none; // Mobile-first + &__close-button { + $p: $interiorMargin + 2px; + font-size: 1.5em; + position: absolute; + top: $p; + right: $p; + z-index: 99; + } + + &__contents { + flex: 1 1 auto; + display: flex; + flex-direction: column; + outline: none; + overflow: auto; + } + + &__top-bar { + flex: 0 0 auto; + flex-direction: column; + display: flex; + + > * { + flex: 0 0 auto; + margin-bottom: $interiorMargin; } + } - &__outer { - @include abs(); - background: $colorBodyBg; - display: flex; - flex-direction: column; - padding: $overlayInnerMargin; + &__dialog-title { + @include ellipsize(); + font-size: 1.5em; + line-height: 120%; + } + + &__contents-main { + display: flex; + flex-direction: column; + flex: 1 1 auto; + height: 0; // Chrome 73 overflow bug fix + overflow: auto; + padding-right: $interiorMargin; // fend off scroll bar + } + + &__button-bar { + flex: 0 0 auto; + display: flex; + justify-content: flex-end; + margin-top: $interiorMargin; + + > * + * { + margin-left: $interiorMargin; } + } - &__close-button { - $p: $interiorMargin + 2px; - font-size: 1.5em; - position: absolute; - top: $p; right: $p; - z-index: 99; - } - - &__contents { - flex: 1 1 auto; - display: flex; - flex-direction: column; - outline: none; - overflow: auto; - } - - &__top-bar { - flex: 0 0 auto; - flex-direction: column; - display: flex; - - > * { - flex: 0 0 auto; - margin-bottom: $interiorMargin; - } - } - - &__dialog-title { - @include ellipsize(); - font-size: 1.5em; - line-height: 120%; - } - - &__contents-main { - display: flex; - flex-direction: column; - flex: 1 1 auto; - height: 0; // Chrome 73 overflow bug fix - overflow: auto; - padding-right: $interiorMargin; // fend off scroll bar - } - - &__button-bar { - flex: 0 0 auto; - display: flex; - justify-content: flex-end; - margin-top: $interiorMargin; - - > * + * { - margin-left: $interiorMargin; - } - } - - .c-object-label__name { - color: $objectLabelNameColorFg; - } + .c-object-label__name { + color: $objectLabelNameColorFg; + } } body.desktop { + .c-overlay { + &__blocker { + @include abs(); + background: $colorOvrBlocker; + cursor: pointer; + display: block; + } + } + + // 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; + box-shadow: rgba(black, 0.5) 0 2px 25px; + } + } + + .l-overlay-fullscreen { + // Used by About > Licenses display + .c-overlay__outer { + @include overlaySizing( + nth($overlayOuterMarginFullscreen, 1), + nth($overlayOuterMarginFullscreen, 2) + ); + } + } + + .l-overlay-large { + // Default + $pad: $interiorMarginLg; + $tbPad: floor($pad * 0.8); + $lrPad: $pad; .c-overlay { - &__blocker { - @include abs(); - background: $colorOvrBlocker; - cursor: pointer; - display: block; - } + &__outer { + @include overlaySizing(nth($overlayOuterMarginLarge, 1), nth($overlayOuterMarginLarge, 2)); + padding: $tbPad $lrPad; + } + + &__close-button { + //top: $interiorMargin; + //right: $interiorMargin; + } } - // Overlay types, styling for desktop. Appended to .l-overlay-wrapper element. - .l-overlay-large, - .l-overlay-small, - .l-overlay-dialog, + .l-browse-bar { + margin-right: 50px; // Don't cover close button + margin-bottom: $interiorMargin; + } + } + + .l-overlay-small { + .c-overlay__outer { + @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 { - border-radius: $overlayCr; - box-shadow: rgba(black, 0.5) 0 2px 25px; - } - } - - .l-overlay-fullscreen { - // Used by About > Licenses display - .c-overlay__outer { - @include overlaySizing(nth($overlayOuterMarginFullscreen, 1), nth($overlayOuterMarginFullscreen, 2)); - } - } - - .l-overlay-large { - // Default - $pad: $interiorMarginLg; - $tbPad: floor($pad * 0.8); - $lrPad: $pad; - .c-overlay { - &__outer { - @include overlaySizing(nth($overlayOuterMarginLarge, 1), nth($overlayOuterMarginLarge, 2)); - padding: $tbPad $lrPad; - } - - &__close-button { - //top: $interiorMargin; - //right: $interiorMargin; - } - } - - .l-browse-bar { - margin-right: 50px; // Don't cover close button - margin-bottom: $interiorMargin; - } - } - - .l-overlay-small { - .c-overlay__outer { - @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, auto); - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - min-width: 20%; - } + .c-overlay__outer { + @include overlaySizing(auto, auto); + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + min-width: 20%; } + } } diff --git a/src/api/priority/PriorityAPI.js b/src/api/priority/PriorityAPI.js index fb0985ce28..ad8cea2e33 100644 --- a/src/api/priority/PriorityAPI.js +++ b/src/api/priority/PriorityAPI.js @@ -21,8 +21,8 @@ *****************************************************************************/ const PRIORITIES = Object.freeze({ - HIGH: 1000, - DEFAULT: 0, - LOW: -1000 + HIGH: 1000, + DEFAULT: 0, + LOW: -1000 }); export default PRIORITIES; diff --git a/src/api/status/StatusAPI.js b/src/api/status/StatusAPI.js index c7d3b7600d..7d948ebb0c 100644 --- a/src/api/status/StatusAPI.js +++ b/src/api/status/StatusAPI.js @@ -23,45 +23,45 @@ import EventEmitter from 'EventEmitter'; export default class StatusAPI extends EventEmitter { - constructor(openmct) { - super(); + constructor(openmct) { + super(); - this._openmct = openmct; - this._statusCache = {}; + this._openmct = openmct; + this._statusCache = {}; - this.get = this.get.bind(this); - this.set = this.set.bind(this); - this.observe = this.observe.bind(this); - } + this.get = this.get.bind(this); + this.set = this.set.bind(this); + this.observe = this.observe.bind(this); + } - get(identifier) { - let keyString = this._openmct.objects.makeKeyString(identifier); + get(identifier) { + let keyString = this._openmct.objects.makeKeyString(identifier); - return this._statusCache[keyString]; - } + return this._statusCache[keyString]; + } - set(identifier, value) { - let keyString = this._openmct.objects.makeKeyString(identifier); + set(identifier, value) { + let keyString = this._openmct.objects.makeKeyString(identifier); - this._statusCache[keyString] = value; - this.emit(keyString, value); - } + this._statusCache[keyString] = value; + this.emit(keyString, value); + } - delete(identifier) { - let keyString = this._openmct.objects.makeKeyString(identifier); + delete(identifier) { + let keyString = this._openmct.objects.makeKeyString(identifier); - this._statusCache[keyString] = undefined; - this.emit(keyString, undefined); - delete this._statusCache[keyString]; - } + this._statusCache[keyString] = undefined; + this.emit(keyString, undefined); + delete this._statusCache[keyString]; + } - observe(identifier, callback) { - let key = this._openmct.objects.makeKeyString(identifier); + observe(identifier, callback) { + let key = this._openmct.objects.makeKeyString(identifier); - this.on(key, callback); + this.on(key, callback); - return () => { - this.off(key, callback); - }; - } + return () => { + this.off(key, callback); + }; + } } diff --git a/src/api/status/StatusAPISpec.js b/src/api/status/StatusAPISpec.js index f61968de89..fe0fdd852c 100644 --- a/src/api/status/StatusAPISpec.js +++ b/src/api/status/StatusAPISpec.js @@ -1,85 +1,84 @@ import StatusAPI from './StatusAPI.js'; import { createOpenMct, resetApplicationState } from '../../utils/testing'; -describe("The Status API", () => { - let statusAPI; - let openmct; - let identifier; - let status; - let status2; - let callback; +describe('The Status API', () => { + let statusAPI; + let openmct; + let identifier; + let status; + let status2; + let callback; - beforeEach(() => { - openmct = createOpenMct(); - statusAPI = new StatusAPI(openmct); - identifier = { - namespace: "test-namespace", - key: "test-key" - }; - status = "test-status"; - status2 = 'test-status-deux'; - callback = jasmine.createSpy('callback', (statusUpdate) => statusUpdate); + beforeEach(() => { + openmct = createOpenMct(); + statusAPI = new StatusAPI(openmct); + identifier = { + namespace: 'test-namespace', + key: 'test-key' + }; + status = 'test-status'; + status2 = 'test-status-deux'; + callback = jasmine.createSpy('callback', (statusUpdate) => statusUpdate); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + describe('set function', () => { + it('sets status for identifier', () => { + statusAPI.set(identifier, status); + + let resultingStatus = statusAPI.get(identifier); + + expect(resultingStatus).toEqual(status); + }); + }); + + describe('get function', () => { + it('returns status for identifier', () => { + statusAPI.set(identifier, status2); + + let resultingStatus = statusAPI.get(identifier); + + expect(resultingStatus).toEqual(status2); + }); + }); + + describe('delete function', () => { + it('deletes status for identifier', () => { + statusAPI.set(identifier, status); + + let resultingStatus = statusAPI.get(identifier); + expect(resultingStatus).toEqual(status); + + statusAPI.delete(identifier); + resultingStatus = statusAPI.get(identifier); + + expect(resultingStatus).toBeUndefined(); + }); + }); + + describe('observe function', () => { + it('allows callbacks to be attached to status set and delete events', () => { + let unsubscribe = statusAPI.observe(identifier, callback); + statusAPI.set(identifier, status); + + expect(callback).toHaveBeenCalledWith(status); + + statusAPI.delete(identifier); + + expect(callback).toHaveBeenCalledWith(undefined); + unsubscribe(); }); - afterEach(() => { - return resetApplicationState(openmct); - }); - - describe("set function", () => { - it("sets status for identifier", () => { - statusAPI.set(identifier, status); - - let resultingStatus = statusAPI.get(identifier); - - expect(resultingStatus).toEqual(status); - }); - }); - - describe("get function", () => { - it("returns status for identifier", () => { - statusAPI.set(identifier, status2); - - let resultingStatus = statusAPI.get(identifier); - - expect(resultingStatus).toEqual(status2); - }); - }); - - describe("delete function", () => { - it("deletes status for identifier", () => { - statusAPI.set(identifier, status); - - let resultingStatus = statusAPI.get(identifier); - expect(resultingStatus).toEqual(status); - - statusAPI.delete(identifier); - resultingStatus = statusAPI.get(identifier); - - expect(resultingStatus).toBeUndefined(); - }); - }); - - describe("observe function", () => { - - it("allows callbacks to be attached to status set and delete events", () => { - let unsubscribe = statusAPI.observe(identifier, callback); - statusAPI.set(identifier, status); - - expect(callback).toHaveBeenCalledWith(status); - - statusAPI.delete(identifier); - - expect(callback).toHaveBeenCalledWith(undefined); - unsubscribe(); - }); - - it("returns a unsubscribe function", () => { - let unsubscribe = statusAPI.observe(identifier, callback); - unsubscribe(); - - statusAPI.set(identifier, status); - - expect(callback).toHaveBeenCalledTimes(0); - }); + it('returns a unsubscribe function', () => { + let unsubscribe = statusAPI.observe(identifier, callback); + unsubscribe(); + + statusAPI.set(identifier, status); + + expect(callback).toHaveBeenCalledTimes(0); }); + }); }); diff --git a/src/api/telemetry/DefaultMetadataProvider.js b/src/api/telemetry/DefaultMetadataProvider.js index e50b4324e6..15ca73c168 100644 --- a/src/api/telemetry/DefaultMetadataProvider.js +++ b/src/api/telemetry/DefaultMetadataProvider.js @@ -20,109 +20,105 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - 'lodash' -], function ( - _ -) { +define(['lodash'], function (_) { + /** + * This is the default metadata provider; for any object with a "telemetry" + * property, this provider will return the value of that property as the + * telemetry metadata. + * + * This provider also implements legacy support for telemetry metadata + * defined on the type. Telemetry metadata definitions on type will be + * depreciated in the future. + */ + function DefaultMetadataProvider(openmct) { + this.openmct = openmct; + } - /** - * This is the default metadata provider; for any object with a "telemetry" - * property, this provider will return the value of that property as the - * telemetry metadata. - * - * This provider also implements legacy support for telemetry metadata - * defined on the type. Telemetry metadata definitions on type will be - * depreciated in the future. - */ - function DefaultMetadataProvider(openmct) { - this.openmct = openmct; + /** + * Applies to any domain object with a telemetry property, or whose type + * definition has a telemetry property. + */ + DefaultMetadataProvider.prototype.supportsMetadata = function (domainObject) { + return Boolean(domainObject.telemetry) || Boolean(this.typeHasTelemetry(domainObject)); + }; + + /** + * Retrieves valueMetadata from legacy metadata. + * @private + */ + function valueMetadatasFromOldFormat(metadata) { + const valueMetadatas = []; + + valueMetadatas.push({ + key: 'name', + name: 'Name' + }); + + metadata.domains.forEach(function (domain, index) { + const valueMetadata = _.clone(domain); + valueMetadata.hints = { + domain: index + 1 + }; + valueMetadatas.push(valueMetadata); + }); + + metadata.ranges.forEach(function (range, index) { + const valueMetadata = _.clone(range); + valueMetadata.hints = { + range: index, + priority: index + metadata.domains.length + 1 + }; + + if (valueMetadata.type === 'enum') { + valueMetadata.key = 'enum'; + valueMetadata.hints.y -= 10; + valueMetadata.hints.range -= 10; + valueMetadata.enumerations = _.sortBy( + valueMetadata.enumerations.map(function (e) { + return { + string: e.string, + value: Number(e.value) + }; + }), + 'e.value' + ); + valueMetadata.values = valueMetadata.enumerations.map((e) => e.value); + valueMetadata.max = Math.max(valueMetadata.values); + valueMetadata.min = Math.min(valueMetadata.values); + } + + valueMetadatas.push(valueMetadata); + }); + + return valueMetadatas; + } + + /** + * Returns telemetry metadata for a given domain object. + */ + DefaultMetadataProvider.prototype.getMetadata = function (domainObject) { + const metadata = domainObject.telemetry || {}; + if (this.typeHasTelemetry(domainObject)) { + const typeMetadata = this.openmct.types.get(domainObject.type).definition.telemetry; + + Object.assign(metadata, typeMetadata); + + if (!metadata.values) { + metadata.values = valueMetadatasFromOldFormat(metadata); + } } - /** - * Applies to any domain object with a telemetry property, or whose type - * definition has a telemetry property. - */ - DefaultMetadataProvider.prototype.supportsMetadata = function (domainObject) { - return Boolean(domainObject.telemetry) || Boolean(this.typeHasTelemetry(domainObject)); - }; + return metadata; + }; - /** - * Retrieves valueMetadata from legacy metadata. - * @private - */ - function valueMetadatasFromOldFormat(metadata) { - const valueMetadatas = []; + /** + * @private + */ + DefaultMetadataProvider.prototype.typeHasTelemetry = function (domainObject) { + const type = this.openmct.types.get(domainObject.type); - valueMetadatas.push({ - key: 'name', - name: 'Name' - }); - - metadata.domains.forEach(function (domain, index) { - const valueMetadata = _.clone(domain); - valueMetadata.hints = { - domain: index + 1 - }; - valueMetadatas.push(valueMetadata); - }); - - metadata.ranges.forEach(function (range, index) { - const valueMetadata = _.clone(range); - valueMetadata.hints = { - range: index, - priority: index + metadata.domains.length + 1 - }; - - if (valueMetadata.type === 'enum') { - valueMetadata.key = 'enum'; - valueMetadata.hints.y -= 10; - valueMetadata.hints.range -= 10; - valueMetadata.enumerations = - _.sortBy(valueMetadata.enumerations.map(function (e) { - return { - string: e.string, - value: Number(e.value) - }; - }), 'e.value'); - valueMetadata.values = valueMetadata.enumerations.map(e => e.value); - valueMetadata.max = Math.max(valueMetadata.values); - valueMetadata.min = Math.min(valueMetadata.values); - } - - valueMetadatas.push(valueMetadata); - }); - - return valueMetadatas; - } - - /** - * Returns telemetry metadata for a given domain object. - */ - DefaultMetadataProvider.prototype.getMetadata = function (domainObject) { - const metadata = domainObject.telemetry || {}; - if (this.typeHasTelemetry(domainObject)) { - const typeMetadata = this.openmct.types.get(domainObject.type).definition.telemetry; - - Object.assign(metadata, typeMetadata); - - if (!metadata.values) { - metadata.values = valueMetadatasFromOldFormat(metadata); - } - } - - return metadata; - }; - - /** - * @private - */ - DefaultMetadataProvider.prototype.typeHasTelemetry = function (domainObject) { - const type = this.openmct.types.get(domainObject.type); - - return Boolean(type.definition.telemetry); - }; - - return DefaultMetadataProvider; + return Boolean(type.definition.telemetry); + }; + return DefaultMetadataProvider; }); diff --git a/src/api/telemetry/TelemetryAPI.js b/src/api/telemetry/TelemetryAPI.js index f247d150da..d016f06697 100644 --- a/src/api/telemetry/TelemetryAPI.js +++ b/src/api/telemetry/TelemetryAPI.js @@ -29,677 +29,686 @@ import DefaultMetadataProvider from './DefaultMetadataProvider'; import objectUtils from 'objectUtils'; export default class TelemetryAPI { - #isGreedyLAD; + #isGreedyLAD; - constructor(openmct) { - this.openmct = openmct; + constructor(openmct) { + this.openmct = openmct; - this.formatMapCache = new WeakMap(); - this.formatters = new Map(); - this.limitProviders = []; - this.stalenessProviders = []; - this.metadataCache = new WeakMap(); - this.metadataProviders = [new DefaultMetadataProvider(this.openmct)]; - this.noRequestProviderForAllObjects = false; - this.requestAbortControllers = new Set(); - this.requestProviders = []; - this.subscriptionProviders = []; - this.valueFormatterCache = new WeakMap(); - this.requestInterceptorRegistry = new TelemetryRequestInterceptorRegistry(); - this.#isGreedyLAD = true; + this.formatMapCache = new WeakMap(); + this.formatters = new Map(); + this.limitProviders = []; + this.stalenessProviders = []; + this.metadataCache = new WeakMap(); + this.metadataProviders = [new DefaultMetadataProvider(this.openmct)]; + this.noRequestProviderForAllObjects = false; + this.requestAbortControllers = new Set(); + this.requestProviders = []; + this.subscriptionProviders = []; + this.valueFormatterCache = new WeakMap(); + this.requestInterceptorRegistry = new TelemetryRequestInterceptorRegistry(); + this.#isGreedyLAD = true; + } + + abortAllRequests() { + this.requestAbortControllers.forEach((controller) => controller.abort()); + this.requestAbortControllers.clear(); + } + + /** + * Return Custom String Formatter + * + * @param {Object} valueMetadata valueMetadata for given telemetry object + * @param {string} format custom formatter string (eg: %.4f, <s etc.) + * @returns {CustomStringFormatter} + */ + customStringFormatter(valueMetadata, format) { + return new CustomStringFormatter(this.openmct, valueMetadata, format); + } + + /** + * Return true if the given domainObject is a telemetry object. A telemetry + * object is any object which has telemetry metadata-- regardless of whether + * the telemetry object has an available telemetry provider. + * + * @param {module:openmct.DomainObject} domainObject + * @returns {boolean} true if the object is a telemetry object. + */ + isTelemetryObject(domainObject) { + return Boolean(this.#findMetadataProvider(domainObject)); + } + + /** + * Check if this provider can supply telemetry data associated with + * this domain object. + * + * @method canProvideTelemetry + * @param {module:openmct.DomainObject} domainObject the object for + * which telemetry would be provided + * @returns {boolean} true if telemetry can be provided + * @memberof module:openmct.TelemetryAPI~TelemetryProvider# + */ + canProvideTelemetry(domainObject) { + return ( + Boolean(this.findSubscriptionProvider(domainObject)) || + Boolean(this.findRequestProvider(domainObject)) + ); + } + + /** + * Register a telemetry provider with the telemetry service. This + * allows you to connect alternative telemetry sources. + * @method addProvider + * @memberof module:openmct.TelemetryAPI# + * @param {module:openmct.TelemetryAPI~TelemetryProvider} provider the new + * telemetry provider + */ + addProvider(provider) { + if (provider.supportsRequest) { + this.requestProviders.unshift(provider); } - abortAllRequests() { - this.requestAbortControllers.forEach((controller) => controller.abort()); - this.requestAbortControllers.clear(); + if (provider.supportsSubscribe) { + this.subscriptionProviders.unshift(provider); } - /** - * Return Custom String Formatter - * - * @param {Object} valueMetadata valueMetadata for given telemetry object - * @param {string} format custom formatter string (eg: %.4f, <s etc.) - * @returns {CustomStringFormatter} - */ - customStringFormatter(valueMetadata, format) { - return new CustomStringFormatter(this.openmct, valueMetadata, format); + if (provider.supportsMetadata) { + this.metadataProviders.unshift(provider); } - /** - * Return true if the given domainObject is a telemetry object. A telemetry - * object is any object which has telemetry metadata-- regardless of whether - * the telemetry object has an available telemetry provider. - * - * @param {module:openmct.DomainObject} domainObject - * @returns {boolean} true if the object is a telemetry object. - */ - isTelemetryObject(domainObject) { - return Boolean(this.#findMetadataProvider(domainObject)); + if (provider.supportsLimits) { + this.limitProviders.unshift(provider); } - /** - * Check if this provider can supply telemetry data associated with - * this domain object. - * - * @method canProvideTelemetry - * @param {module:openmct.DomainObject} domainObject the object for - * which telemetry would be provided - * @returns {boolean} true if telemetry can be provided - * @memberof module:openmct.TelemetryAPI~TelemetryProvider# - */ - canProvideTelemetry(domainObject) { - return Boolean(this.findSubscriptionProvider(domainObject)) - || Boolean(this.findRequestProvider(domainObject)); + if (provider.supportsStaleness) { + this.stalenessProviders.unshift(provider); + } + } + + /** + * Returns a telemetry subscription provider that supports + * a given domain object and options. + */ + findSubscriptionProvider() { + const args = Array.prototype.slice.apply(arguments); + function supportsDomainObject(provider) { + return provider.supportsSubscribe.apply(provider, args); } - /** - * Register a telemetry provider with the telemetry service. This - * allows you to connect alternative telemetry sources. - * @method addProvider - * @memberof module:openmct.TelemetryAPI# - * @param {module:openmct.TelemetryAPI~TelemetryProvider} provider the new - * telemetry provider - */ - addProvider(provider) { - if (provider.supportsRequest) { - this.requestProviders.unshift(provider); - } + return this.subscriptionProviders.find(supportsDomainObject); + } - if (provider.supportsSubscribe) { - this.subscriptionProviders.unshift(provider); - } - - if (provider.supportsMetadata) { - this.metadataProviders.unshift(provider); - } - - if (provider.supportsLimits) { - this.limitProviders.unshift(provider); - } - - if (provider.supportsStaleness) { - this.stalenessProviders.unshift(provider); - } + /** + * Returns a telemetry request provider that supports + * a given domain object and options. + */ + findRequestProvider() { + const args = Array.prototype.slice.apply(arguments); + function supportsDomainObject(provider) { + return provider.supportsRequest.apply(provider, args); } - /** - * Returns a telemetry subscription provider that supports - * a given domain object and options. - */ - findSubscriptionProvider() { - const args = Array.prototype.slice.apply(arguments); - function supportsDomainObject(provider) { - return provider.supportsSubscribe.apply(provider, args); - } + return this.requestProviders.find(supportsDomainObject); + } - return this.subscriptionProviders.find(supportsDomainObject); + /** + * @private + */ + #findMetadataProvider(domainObject) { + return this.metadataProviders.find((provider) => { + return provider.supportsMetadata(domainObject); + }); + } + + /** + * @private + */ + #findLimitEvaluator(domainObject) { + return this.limitProviders.find((provider) => { + return provider.supportsLimits(domainObject); + }); + } + + /** + * @private + * Though used in TelemetryCollection as well + */ + standardizeRequestOptions(options) { + if (!Object.prototype.hasOwnProperty.call(options, 'start')) { + options.start = this.openmct.time.bounds().start; } - /** - * Returns a telemetry request provider that supports - * a given domain object and options. - */ - findRequestProvider() { - const args = Array.prototype.slice.apply(arguments); - function supportsDomainObject(provider) { - return provider.supportsRequest.apply(provider, args); - } - - return this.requestProviders.find(supportsDomainObject); + if (!Object.prototype.hasOwnProperty.call(options, 'end')) { + options.end = this.openmct.time.bounds().end; } - /** - * @private - */ - #findMetadataProvider(domainObject) { - return this.metadataProviders.find((provider) => { - return provider.supportsMetadata(domainObject); - }); + if (!Object.prototype.hasOwnProperty.call(options, 'domain')) { + options.domain = this.openmct.time.timeSystem().key; } - /** - * @private - */ - #findLimitEvaluator(domainObject) { - return this.limitProviders.find((provider) => { - return provider.supportsLimits(domainObject); - }); + if (!Object.prototype.hasOwnProperty.call(options, 'timeContext')) { + options.timeContext = this.openmct.time; + } + } + + /** + * 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; } - /** - * @private - * Though used in TelemetryCollection as well - */ - standardizeRequestOptions(options) { - if (!Object.prototype.hasOwnProperty.call(options, 'start')) { - options.start = this.openmct.time.bounds().start; - } + let modifiedRequest = { ...request }; - if (!Object.prototype.hasOwnProperty.call(options, 'end')) { - options.end = this.openmct.time.bounds().end; - } - - if (!Object.prototype.hasOwnProperty.call(options, 'domain')) { - options.domain = this.openmct.time.timeSystem().key; - } - - if (!Object.prototype.hasOwnProperty.call(options, 'timeContext')) { - options.timeContext = this.openmct.time; - } + for (let interceptor of interceptors) { + modifiedRequest = await interceptor.invoke(modifiedRequest); } - /** - * 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); + return modifiedRequest; + } + + /** + * Get or set greedy LAD. For stategy "latest" telemetry in + * realtime mode the start bound will be ignored if true and + * there is no new data to replace the existing data. + * defaults to true + * + * To turn off greedy LAD: + * openmct.telemetry.greedyLAD(false); + * + * @method greedyLAD + * @returns {boolean} if greedyLAD is active or not + * @memberof module:openmct.TelemetryAPI# + */ + greedyLAD(isGreedy) { + if (arguments.length > 0) { + if (isGreedy !== true && isGreedy !== false) { + throw new Error('Error setting greedyLAD. Greedy LAD only accepts true or false values'); + } + + this.#isGreedyLAD = isGreedy; } - /** - * Retrieve the request interceptors for a given domain object. - * @private - */ - #getInterceptorsForRequest(identifier, request) { - return this.requestInterceptorRegistry.getInterceptors(identifier, request); + return this.#isGreedyLAD; + } + + /** + * Request telemetry collection for a domain object. + * The `options` argument allows you to specify filters + * (start, end, etc.), sort order, and strategies for retrieving + * telemetry (aggregation, latest available, etc.). + * + * @method requestCollection + * @memberof module:openmct.TelemetryAPI~TelemetryProvider# + * @param {module:openmct.DomainObject} domainObject the object + * which has associated telemetry + * @param {module:openmct.TelemetryAPI~TelemetryRequest} options + * options for this telemetry collection request + * @returns {TelemetryCollection} a TelemetryCollection instance + */ + requestCollection(domainObject, options = {}) { + return new TelemetryCollection(this.openmct, domainObject, options); + } + + /** + * Request historical telemetry for a domain object. + * The `options` argument allows you to specify filters + * (start, end, etc.), sort order, time context, and strategies for retrieving + * telemetry (aggregation, latest available, etc.). + * + * @method request + * @memberof module:openmct.TelemetryAPI~TelemetryProvider# + * @param {module:openmct.DomainObject} domainObject the object + * which has associated telemetry + * @param {module:openmct.TelemetryAPI~TelemetryRequest} options + * options for this historical request + * @returns {Promise.} a promise for an array of + * telemetry data + */ + async request(domainObject) { + if (this.noRequestProviderForAllObjects || domainObject.type === 'unknown') { + return []; } - /** - * 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; + if (arguments.length === 1) { + arguments.length = 2; + arguments[1] = {}; } - /** - * Get or set greedy LAD. For stategy "latest" telemetry in - * realtime mode the start bound will be ignored if true and - * there is no new data to replace the existing data. - * defaults to true - * - * To turn off greedy LAD: - * openmct.telemetry.greedyLAD(false); - * - * @method greedyLAD - * @returns {boolean} if greedyLAD is active or not - * @memberof module:openmct.TelemetryAPI# - */ - greedyLAD(isGreedy) { - if (arguments.length > 0) { - if (isGreedy !== true && isGreedy !== false) { - throw new Error('Error setting greedyLAD. Greedy LAD only accepts true or false values'); - } + const abortController = new AbortController(); + arguments[1].signal = abortController.signal; + this.requestAbortControllers.add(abortController); - this.#isGreedyLAD = isGreedy; - } + this.standardizeRequestOptions(arguments[1]); - return this.#isGreedyLAD; + const provider = this.findRequestProvider.apply(this, arguments); + if (!provider) { + this.requestAbortControllers.delete(abortController); + + return this.#handleMissingRequestProvider(domainObject); } - /** - * Request telemetry collection for a domain object. - * The `options` argument allows you to specify filters - * (start, end, etc.), sort order, and strategies for retrieving - * telemetry (aggregation, latest available, etc.). - * - * @method requestCollection - * @memberof module:openmct.TelemetryAPI~TelemetryProvider# - * @param {module:openmct.DomainObject} domainObject the object - * which has associated telemetry - * @param {module:openmct.TelemetryAPI~TelemetryRequest} options - * options for this telemetry collection request - * @returns {TelemetryCollection} a TelemetryCollection instance - */ - requestCollection(domainObject, options = {}) { - return new TelemetryCollection( - this.openmct, - domainObject, - options + arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]); + try { + const telemetry = await provider.request(...arguments); + + return telemetry; + } catch (error) { + if (error.name !== 'AbortError') { + this.openmct.notifications.error( + 'Error requesting telemetry data, see console for details' ); + console.error(error); + } + + throw new Error(error); + } finally { + this.requestAbortControllers.delete(abortController); + } + } + + /** + * Subscribe to realtime telemetry for a specific domain object. + * The callback will be called whenever data is received from a + * realtime provider. + * + * @method subscribe + * @memberof module:openmct.TelemetryAPI~TelemetryProvider# + * @param {module:openmct.DomainObject} domainObject the object + * which has associated telemetry + * @param {Function} callback the callback to invoke with new data, as + * it becomes available + * @returns {Function} a function which may be called to terminate + * the subscription + */ + subscribe(domainObject, callback, options) { + if (domainObject.type === 'unknown') { + return () => {}; } - /** - * Request historical telemetry for a domain object. - * The `options` argument allows you to specify filters - * (start, end, etc.), sort order, time context, and strategies for retrieving - * telemetry (aggregation, latest available, etc.). - * - * @method request - * @memberof module:openmct.TelemetryAPI~TelemetryProvider# - * @param {module:openmct.DomainObject} domainObject the object - * which has associated telemetry - * @param {module:openmct.TelemetryAPI~TelemetryRequest} options - * options for this historical request - * @returns {Promise.} a promise for an array of - * telemetry data - */ - async request(domainObject) { - if (this.noRequestProviderForAllObjects || domainObject.type === 'unknown') { - return []; - } + const provider = this.findSubscriptionProvider(domainObject); - if (arguments.length === 1) { - arguments.length = 2; - arguments[1] = {}; - } - - const abortController = new AbortController(); - arguments[1].signal = abortController.signal; - this.requestAbortControllers.add(abortController); - - this.standardizeRequestOptions(arguments[1]); - - const provider = this.findRequestProvider.apply(this, arguments); - if (!provider) { - this.requestAbortControllers.delete(abortController); - - return this.#handleMissingRequestProvider(domainObject); - } - - arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]); - try { - const telemetry = await provider.request(...arguments); - - return telemetry; - } catch (error) { - if (error.name !== 'AbortError') { - this.openmct.notifications.error('Error requesting telemetry data, see console for details'); - console.error(error); - } - - throw new Error(error); - } finally { - this.requestAbortControllers.delete(abortController); - } + if (!this.subscribeCache) { + this.subscribeCache = {}; } - /** - * Subscribe to realtime telemetry for a specific domain object. - * The callback will be called whenever data is received from a - * realtime provider. - * - * @method subscribe - * @memberof module:openmct.TelemetryAPI~TelemetryProvider# - * @param {module:openmct.DomainObject} domainObject the object - * which has associated telemetry - * @param {Function} callback the callback to invoke with new data, as - * it becomes available - * @returns {Function} a function which may be called to terminate - * the subscription - */ - subscribe(domainObject, callback, options) { - if (domainObject.type === 'unknown') { - return () => {}; - } + const keyString = objectUtils.makeKeyString(domainObject.identifier); + let subscriber = this.subscribeCache[keyString]; - const provider = this.findSubscriptionProvider(domainObject); - - if (!this.subscribeCache) { - this.subscribeCache = {}; - } - - const keyString = objectUtils.makeKeyString(domainObject.identifier); - let subscriber = this.subscribeCache[keyString]; - - if (!subscriber) { - subscriber = this.subscribeCache[keyString] = { - callbacks: [callback] - }; - if (provider) { - subscriber.unsubscribe = provider - .subscribe(domainObject, function (value) { - subscriber.callbacks.forEach(function (cb) { - cb(value); - }); - }, options); - } else { - subscriber.unsubscribe = function () {}; - } - } else { - subscriber.callbacks.push(callback); - } - - return function unsubscribe() { - subscriber.callbacks = subscriber.callbacks.filter(function (cb) { - return cb !== callback; + if (!subscriber) { + subscriber = this.subscribeCache[keyString] = { + callbacks: [callback] + }; + if (provider) { + subscriber.unsubscribe = provider.subscribe( + domainObject, + function (value) { + subscriber.callbacks.forEach(function (cb) { + cb(value); }); - if (subscriber.callbacks.length === 0) { - subscriber.unsubscribe(); - delete this.subscribeCache[keyString]; - } - }.bind(this); + }, + options + ); + } else { + subscriber.unsubscribe = function () {}; + } + } else { + subscriber.callbacks.push(callback); } - /** - * Subscribe to staleness updates for a specific domain object. - * The callback will be called whenever staleness changes. - * - * @method subscribeToStaleness - * @memberof module:openmct.TelemetryAPI~StalenessProvider# - * @param {module:openmct.DomainObject} domainObject the object - * to watch for staleness updates - * @param {Function} callback the callback to invoke with staleness data, - * as it is received: ex. - * { - * isStale: , - * timestamp: - * } - * @returns {Function} a function which may be called to terminate - * the subscription to staleness updates - */ - subscribeToStaleness(domainObject, callback) { - const provider = this.#findStalenessProvider(domainObject); + return function unsubscribe() { + subscriber.callbacks = subscriber.callbacks.filter(function (cb) { + return cb !== callback; + }); + if (subscriber.callbacks.length === 0) { + subscriber.unsubscribe(); + delete this.subscribeCache[keyString]; + } + }.bind(this); + } - if (!this.stalenessSubscriberCache) { - this.stalenessSubscriberCache = {}; - } + /** + * Subscribe to staleness updates for a specific domain object. + * The callback will be called whenever staleness changes. + * + * @method subscribeToStaleness + * @memberof module:openmct.TelemetryAPI~StalenessProvider# + * @param {module:openmct.DomainObject} domainObject the object + * to watch for staleness updates + * @param {Function} callback the callback to invoke with staleness data, + * as it is received: ex. + * { + * isStale: , + * timestamp: + * } + * @returns {Function} a function which may be called to terminate + * the subscription to staleness updates + */ + subscribeToStaleness(domainObject, callback) { + const provider = this.#findStalenessProvider(domainObject); - const keyString = objectUtils.makeKeyString(domainObject.identifier); - let stalenessSubscriber = this.stalenessSubscriberCache[keyString]; + if (!this.stalenessSubscriberCache) { + this.stalenessSubscriberCache = {}; + } - if (!stalenessSubscriber) { - stalenessSubscriber = this.stalenessSubscriberCache[keyString] = { - callbacks: [callback] - }; - if (provider) { - stalenessSubscriber.unsubscribe = provider - .subscribeToStaleness(domainObject, (stalenessResponse) => { - stalenessSubscriber.callbacks.forEach((cb) => { - cb(stalenessResponse); - }); - }); - } else { - stalenessSubscriber.unsubscribe = () => {}; - } - } else { - stalenessSubscriber.callbacks.push(callback); - } + const keyString = objectUtils.makeKeyString(domainObject.identifier); + let stalenessSubscriber = this.stalenessSubscriberCache[keyString]; - return function unsubscribe() { - stalenessSubscriber.callbacks = stalenessSubscriber.callbacks.filter((cb) => { - return cb !== callback; + if (!stalenessSubscriber) { + stalenessSubscriber = this.stalenessSubscriberCache[keyString] = { + callbacks: [callback] + }; + if (provider) { + stalenessSubscriber.unsubscribe = provider.subscribeToStaleness( + domainObject, + (stalenessResponse) => { + stalenessSubscriber.callbacks.forEach((cb) => { + cb(stalenessResponse); }); - if (stalenessSubscriber.callbacks.length === 0) { - stalenessSubscriber.unsubscribe(); - delete this.stalenessSubscriberCache[keyString]; - } - }.bind(this); + } + ); + } else { + stalenessSubscriber.unsubscribe = () => {}; + } + } else { + stalenessSubscriber.callbacks.push(callback); } - /** - * Request telemetry staleness for a domain object. - * - * @method isStale - * @memberof module:openmct.TelemetryAPI~StalenessProvider# - * @param {module:openmct.DomainObject} domainObject the object - * which has associated telemetry staleness - * @returns {Promise.} a promise for a StalenessResponseObject - * or undefined if no provider exists - */ - async isStale(domainObject) { - const provider = this.#findStalenessProvider(domainObject); + return function unsubscribe() { + stalenessSubscriber.callbacks = stalenessSubscriber.callbacks.filter((cb) => { + return cb !== callback; + }); + if (stalenessSubscriber.callbacks.length === 0) { + stalenessSubscriber.unsubscribe(); + delete this.stalenessSubscriberCache[keyString]; + } + }.bind(this); + } - if (!provider) { - return; - } - - const abortController = new AbortController(); - const options = { signal: abortController.signal }; - this.requestAbortControllers.add(abortController); - - try { - const staleness = await provider.isStale(domainObject, options); - - return staleness; - } finally { - this.requestAbortControllers.delete(abortController); + /** + * Request telemetry staleness for a domain object. + * + * @method isStale + * @memberof module:openmct.TelemetryAPI~StalenessProvider# + * @param {module:openmct.DomainObject} domainObject the object + * which has associated telemetry staleness + * @returns {Promise.} a promise for a StalenessResponseObject + * or undefined if no provider exists + */ + async isStale(domainObject) { + const provider = this.#findStalenessProvider(domainObject); + + if (!provider) { + return; + } + + const abortController = new AbortController(); + const options = { signal: abortController.signal }; + this.requestAbortControllers.add(abortController); + + try { + const staleness = await provider.isStale(domainObject, options); + + return staleness; + } finally { + this.requestAbortControllers.delete(abortController); + } + } + + /** + * @private + */ + #findStalenessProvider(domainObject) { + return this.stalenessProviders.find((provider) => { + return provider.supportsStaleness(domainObject); + }); + } + + /** + * Get telemetry metadata for a given domain object. Returns a telemetry + * metadata manager which provides methods for interrogating telemetry + * metadata. + * + * @returns {TelemetryMetadataManager} + */ + getMetadata(domainObject) { + if (!this.metadataCache.has(domainObject)) { + const metadataProvider = this.#findMetadataProvider(domainObject); + if (!metadataProvider) { + return; + } + + const metadata = metadataProvider.getMetadata(domainObject); + + this.metadataCache.set(domainObject, new TelemetryMetadataManager(metadata)); + } + + return this.metadataCache.get(domainObject); + } + + /** + * Get a value formatter for a given valueMetadata. + * + * @returns {TelemetryValueFormatter} + */ + getValueFormatter(valueMetadata) { + if (!this.valueFormatterCache.has(valueMetadata)) { + this.valueFormatterCache.set( + valueMetadata, + new TelemetryValueFormatter(valueMetadata, this.formatters) + ); + } + + return this.valueFormatterCache.get(valueMetadata); + } + + /** + * Get a value formatter for a given key. + * @param {string} key + * + * @returns {Format} + */ + getFormatter(key) { + return this.formatters.get(key); + } + + /** + * Get a format map of all value formatters for a given piece of telemetry + * metadata. + * + * @returns {Object} + */ + getFormatMap(metadata) { + if (!metadata) { + return {}; + } + + if (!this.formatMapCache.has(metadata)) { + const formatMap = metadata.values().reduce( + function (map, valueMetadata) { + map[valueMetadata.key] = this.getValueFormatter(valueMetadata); + + return map; + }.bind(this), + {} + ); + this.formatMapCache.set(metadata, formatMap); + } + + return this.formatMapCache.get(metadata); + } + + /** + * Error Handling: Missing Request provider + * + * @returns Promise + */ + #handleMissingRequestProvider(domainObject) { + this.noRequestProviderForAllObjects = this.requestProviders.every((requestProvider) => { + const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments); + const hasRequestProvider = + Object.prototype.hasOwnProperty.call(requestProvider, 'request') && + typeof requestProvider.request === 'function'; + + return supportsRequest && hasRequestProvider; + }); + + let message = ''; + let detailMessage = ''; + if (this.noRequestProviderForAllObjects) { + message = 'Missing request providers, see console for details'; + detailMessage = 'Missing request provider for all request providers'; + } else { + message = 'Missing request provider, see console for details'; + const { name, identifier } = domainObject; + detailMessage = `Missing request provider for domainObject, name: ${name}, identifier: ${JSON.stringify( + identifier + )}`; + } + + this.openmct.notifications.error(message); + console.warn(detailMessage); + + return Promise.resolve([]); + } + + /** + * Register a new telemetry data formatter. + * @param {Format} format the + */ + addFormat(format) { + this.formatters.set(format.key, format); + } + + /** + * Get a limit evaluator for this domain object. + * Limit Evaluators help you evaluate limit and alarm status of individual + * telemetry datums for display purposes without having to interact directly + * with the Limit API. + * + * This method is optional. + * If a provider does not implement this method, it is presumed + * that no limits are defined for this domain object's telemetry. + * + * @param {module:openmct.DomainObject} domainObject the domain + * object for which to evaluate limits + * @returns {module:openmct.TelemetryAPI~LimitEvaluator} + * @method limitEvaluator + * @memberof module:openmct.TelemetryAPI~TelemetryProvider# + */ + limitEvaluator(domainObject) { + return this.getLimitEvaluator(domainObject); + } + + /** + * Get a limits for this domain object. + * Limits help you display limits and alarms of + * telemetry for display purposes without having to interact directly + * with the Limit API. + * + * This method is optional. + * If a provider does not implement this method, it is presumed + * that no limits are defined for this domain object's telemetry. + * + * @param {module:openmct.DomainObject} domainObject the domain + * object for which to get limits + * @returns {module:openmct.TelemetryAPI~LimitEvaluator} + * @method limits + * @memberof module:openmct.TelemetryAPI~TelemetryProvider# + */ + limitDefinition(domainObject) { + return this.getLimits(domainObject); + } + + /** + * Get a limit evaluator for this domain object. + * Limit Evaluators help you evaluate limit and alarm status of individual + * telemetry datums for display purposes without having to interact directly + * with the Limit API. + * + * This method is optional. + * If a provider does not implement this method, it is presumed + * that no limits are defined for this domain object's telemetry. + * + * @param {module:openmct.DomainObject} domainObject the domain + * object for which to evaluate limits + * @returns {module:openmct.TelemetryAPI~LimitEvaluator} + * @method limitEvaluator + * @memberof module:openmct.TelemetryAPI~TelemetryProvider# + */ + getLimitEvaluator(domainObject) { + const provider = this.#findLimitEvaluator(domainObject); + if (!provider) { + return { + evaluate: function () {} + }; + } + + return provider.getLimitEvaluator(domainObject); + } + + /** + * Get a limit definitions for this domain object. + * Limit Definitions help you indicate limits and alarms of + * telemetry for display purposes without having to interact directly + * with the Limit API. + * + * This method is optional. + * If a provider does not implement this method, it is presumed + * that no limits are defined for this domain object's telemetry. + * + * @param {module:openmct.DomainObject} domainObject the domain + * object for which to display limits + * @returns {module:openmct.TelemetryAPI~LimitEvaluator} + * @method limits returns a limits object of + * type { + * level1: { + * low: { key1: value1, key2: value2, color: }, + * high: { key1: value1, key2: value2, color: } + * }, + * level2: { + * low: { key1: value1, key2: value2 }, + * high: { key1: value1, key2: value2 } + * } + * } + * supported colors are purple, red, orange, yellow and cyan + * @memberof module:openmct.TelemetryAPI~TelemetryProvider# + */ + getLimits(domainObject) { + const provider = this.#findLimitEvaluator(domainObject); + if (!provider || !provider.getLimits) { + return { + limits: function () { + return Promise.resolve(undefined); } + }; } - /** - * @private - */ - #findStalenessProvider(domainObject) { - return this.stalenessProviders.find((provider) => { - return provider.supportsStaleness(domainObject); - }); - } - - /** - * Get telemetry metadata for a given domain object. Returns a telemetry - * metadata manager which provides methods for interrogating telemetry - * metadata. - * - * @returns {TelemetryMetadataManager} - */ - getMetadata(domainObject) { - if (!this.metadataCache.has(domainObject)) { - const metadataProvider = this.#findMetadataProvider(domainObject); - if (!metadataProvider) { - return; - } - - const metadata = metadataProvider.getMetadata(domainObject); - - this.metadataCache.set( - domainObject, - new TelemetryMetadataManager(metadata) - ); - } - - return this.metadataCache.get(domainObject); - } - - /** - * Get a value formatter for a given valueMetadata. - * - * @returns {TelemetryValueFormatter} - */ - getValueFormatter(valueMetadata) { - if (!this.valueFormatterCache.has(valueMetadata)) { - this.valueFormatterCache.set( - valueMetadata, - new TelemetryValueFormatter(valueMetadata, this.formatters) - ); - } - - return this.valueFormatterCache.get(valueMetadata); - } - - /** - * Get a value formatter for a given key. - * @param {string} key - * - * @returns {Format} - */ - getFormatter(key) { - return this.formatters.get(key); - } - - /** - * Get a format map of all value formatters for a given piece of telemetry - * metadata. - * - * @returns {Object} - */ - getFormatMap(metadata) { - if (!metadata) { - return {}; - } - - if (!this.formatMapCache.has(metadata)) { - const formatMap = metadata.values().reduce(function (map, valueMetadata) { - map[valueMetadata.key] = this.getValueFormatter(valueMetadata); - - return map; - }.bind(this), {}); - this.formatMapCache.set(metadata, formatMap); - } - - return this.formatMapCache.get(metadata); - } - - /** - * Error Handling: Missing Request provider - * - * @returns Promise - */ - #handleMissingRequestProvider(domainObject) { - this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => { - const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments); - const hasRequestProvider = Object.prototype.hasOwnProperty.call(requestProvider, 'request') && typeof requestProvider.request === 'function'; - - return supportsRequest && hasRequestProvider; - }); - - let message = ''; - let detailMessage = ''; - if (this.noRequestProviderForAllObjects) { - message = 'Missing request providers, see console for details'; - detailMessage = 'Missing request provider for all request providers'; - } else { - message = 'Missing request provider, see console for details'; - const { name, identifier } = domainObject; - detailMessage = `Missing request provider for domainObject, name: ${name}, identifier: ${JSON.stringify(identifier)}`; - } - - this.openmct.notifications.error(message); - console.warn(detailMessage); - - return Promise.resolve([]); - } - - /** - * Register a new telemetry data formatter. - * @param {Format} format the - */ - addFormat(format) { - this.formatters.set(format.key, format); - } - - /** - * Get a limit evaluator for this domain object. - * Limit Evaluators help you evaluate limit and alarm status of individual - * telemetry datums for display purposes without having to interact directly - * with the Limit API. - * - * This method is optional. - * If a provider does not implement this method, it is presumed - * that no limits are defined for this domain object's telemetry. - * - * @param {module:openmct.DomainObject} domainObject the domain - * object for which to evaluate limits - * @returns {module:openmct.TelemetryAPI~LimitEvaluator} - * @method limitEvaluator - * @memberof module:openmct.TelemetryAPI~TelemetryProvider# - */ - limitEvaluator(domainObject) { - return this.getLimitEvaluator(domainObject); - } - - /** - * Get a limits for this domain object. - * Limits help you display limits and alarms of - * telemetry for display purposes without having to interact directly - * with the Limit API. - * - * This method is optional. - * If a provider does not implement this method, it is presumed - * that no limits are defined for this domain object's telemetry. - * - * @param {module:openmct.DomainObject} domainObject the domain - * object for which to get limits - * @returns {module:openmct.TelemetryAPI~LimitEvaluator} - * @method limits - * @memberof module:openmct.TelemetryAPI~TelemetryProvider# - */ - limitDefinition(domainObject) { - return this.getLimits(domainObject); - } - - /** - * Get a limit evaluator for this domain object. - * Limit Evaluators help you evaluate limit and alarm status of individual - * telemetry datums for display purposes without having to interact directly - * with the Limit API. - * - * This method is optional. - * If a provider does not implement this method, it is presumed - * that no limits are defined for this domain object's telemetry. - * - * @param {module:openmct.DomainObject} domainObject the domain - * object for which to evaluate limits - * @returns {module:openmct.TelemetryAPI~LimitEvaluator} - * @method limitEvaluator - * @memberof module:openmct.TelemetryAPI~TelemetryProvider# - */ - getLimitEvaluator(domainObject) { - const provider = this.#findLimitEvaluator(domainObject); - if (!provider) { - return { - evaluate: function () {} - }; - } - - return provider.getLimitEvaluator(domainObject); - } - - /** - * Get a limit definitions for this domain object. - * Limit Definitions help you indicate limits and alarms of - * telemetry for display purposes without having to interact directly - * with the Limit API. - * - * This method is optional. - * If a provider does not implement this method, it is presumed - * that no limits are defined for this domain object's telemetry. - * - * @param {module:openmct.DomainObject} domainObject the domain - * object for which to display limits - * @returns {module:openmct.TelemetryAPI~LimitEvaluator} - * @method limits returns a limits object of - * type { - * level1: { - * low: { key1: value1, key2: value2, color: }, - * high: { key1: value1, key2: value2, color: } - * }, - * level2: { - * low: { key1: value1, key2: value2 }, - * high: { key1: value1, key2: value2 } - * } - * } - * supported colors are purple, red, orange, yellow and cyan - * @memberof module:openmct.TelemetryAPI~TelemetryProvider# - */ - getLimits(domainObject) { - const provider = this.#findLimitEvaluator(domainObject); - if (!provider || !provider.getLimits) { - return { - limits: function () { - return Promise.resolve(undefined); - } - }; - } - - return provider.getLimits(domainObject); - } + return provider.getLimits(domainObject); + } } /** diff --git a/src/api/telemetry/TelemetryAPISpec.js b/src/api/telemetry/TelemetryAPISpec.js index d7ab8902f0..a2550e978f 100644 --- a/src/api/telemetry/TelemetryAPISpec.js +++ b/src/api/telemetry/TelemetryAPISpec.js @@ -24,636 +24,602 @@ import TelemetryAPI from './TelemetryAPI'; import TelemetryCollection from './TelemetryCollection'; describe('Telemetry API', () => { - let openmct; - let telemetryAPI; + let openmct; + let telemetryAPI; + + beforeEach(() => { + openmct = { + time: jasmine.createSpyObj('timeAPI', ['timeSystem', 'bounds']), + types: jasmine.createSpyObj('typeRegistry', ['get']) + }; + + openmct.time.timeSystem.and.returnValue({ key: 'system' }); + openmct.time.bounds.and.returnValue({ + start: 0, + end: 1 + }); + telemetryAPI = new TelemetryAPI(openmct); + }); + + describe('telemetry providers', () => { + let telemetryProvider; + let domainObject; beforeEach(() => { - openmct = { - time: jasmine.createSpyObj('timeAPI', [ - 'timeSystem', - 'bounds' - ]), - types: jasmine.createSpyObj('typeRegistry', [ - 'get' - ]) - }; - - openmct.time.timeSystem.and.returnValue({key: 'system'}); - openmct.time.bounds.and.returnValue({ - start: 0, - end: 1 - }); - telemetryAPI = new TelemetryAPI(openmct); + telemetryProvider = jasmine.createSpyObj('telemetryProvider', [ + 'supportsSubscribe', + 'subscribe', + 'supportsRequest', + 'request' + ]); + domainObject = { + identifier: { + key: 'a', + namespace: 'b' + }, + type: 'sample-type' + }; + openmct.notifications = { + error: () => { + console.log('sample error notification'); + } + }; }); - describe('telemetry providers', () => { - let telemetryProvider; - let domainObject; + it('provides consistent results without providers', async () => { + const unsubscribe = telemetryAPI.subscribe(domainObject); - beforeEach(() => { - telemetryProvider = jasmine.createSpyObj('telemetryProvider', [ - 'supportsSubscribe', - 'subscribe', - 'supportsRequest', - 'request' - ]); - domainObject = { - identifier: { - key: 'a', - namespace: 'b' - }, - type: 'sample-type' - }; + expect(unsubscribe).toEqual(jasmine.any(Function)); - openmct.notifications = { - error: () => { - console.log('sample error notification'); - } - }; - }); - - it('provides consistent results without providers', async () => { - const unsubscribe = telemetryAPI.subscribe(domainObject); - - expect(unsubscribe).toEqual(jasmine.any(Function)); - - const data = await telemetryAPI.request(domainObject); - expect(data).toEqual([]); - }); - - it('skips providers that do not match', async () => { - telemetryProvider.supportsSubscribe.and.returnValue(false); - telemetryProvider.supportsRequest.and.returnValue(false); - telemetryProvider.request.and.returnValue(Promise.resolve([])); - telemetryAPI.addProvider(telemetryProvider); - - const callback = jasmine.createSpy('callback'); - const unsubscribe = telemetryAPI.subscribe(domainObject, callback); - expect(telemetryProvider.supportsSubscribe) - .toHaveBeenCalledWith(domainObject); - expect(telemetryProvider.subscribe).not.toHaveBeenCalled(); - expect(unsubscribe).toEqual(jasmine.any(Function)); - - await telemetryAPI.request(domainObject); - expect(telemetryProvider.supportsRequest) - .toHaveBeenCalledWith(domainObject, jasmine.any(Object)); - expect(telemetryProvider.request).not.toHaveBeenCalled(); - }); - - it('sends subscribe calls to matching providers', () => { - const unsubFunc = jasmine.createSpy('unsubscribe'); - telemetryProvider.subscribe.and.returnValue(unsubFunc); - telemetryProvider.supportsSubscribe.and.returnValue(true); - telemetryAPI.addProvider(telemetryProvider); - - const callback = jasmine.createSpy('callback'); - const unsubscribe = telemetryAPI.subscribe(domainObject, callback); - expect(telemetryProvider.supportsSubscribe.calls.count()).toBe(1); - expect(telemetryProvider.supportsSubscribe) - .toHaveBeenCalledWith(domainObject); - expect(telemetryProvider.subscribe.calls.count()).toBe(1); - expect(telemetryProvider.subscribe) - .toHaveBeenCalledWith(domainObject, jasmine.any(Function), undefined); - - const notify = telemetryProvider.subscribe.calls.mostRecent().args[1]; - notify('someValue'); - expect(callback).toHaveBeenCalledWith('someValue'); - - expect(unsubscribe).toEqual(jasmine.any(Function)); - expect(unsubFunc).not.toHaveBeenCalled(); - unsubscribe(); - expect(unsubFunc).toHaveBeenCalled(); - - notify('otherValue'); - expect(callback).not.toHaveBeenCalledWith('otherValue'); - }); - - it('subscribes once per object', () => { - const unsubFunc = jasmine.createSpy('unsubscribe'); - telemetryProvider.subscribe.and.returnValue(unsubFunc); - telemetryProvider.supportsSubscribe.and.returnValue(true); - telemetryAPI.addProvider(telemetryProvider); - - const callback = jasmine.createSpy('callback'); - const callbacktwo = jasmine.createSpy('callback two'); - const unsubscribe = telemetryAPI.subscribe(domainObject, callback); - const unsubscribetwo = telemetryAPI.subscribe(domainObject, callbacktwo); - - expect(telemetryProvider.subscribe.calls.count()).toBe(1); - - const notify = telemetryProvider.subscribe.calls.mostRecent().args[1]; - notify('someValue'); - expect(callback).toHaveBeenCalledWith('someValue'); - expect(callbacktwo).toHaveBeenCalledWith('someValue'); - - unsubscribe(); - expect(unsubFunc).not.toHaveBeenCalled(); - notify('otherValue'); - expect(callback).not.toHaveBeenCalledWith('otherValue'); - expect(callbacktwo).toHaveBeenCalledWith('otherValue'); - - unsubscribetwo(); - expect(unsubFunc).toHaveBeenCalled(); - notify('anotherValue'); - expect(callback).not.toHaveBeenCalledWith('anotherValue'); - expect(callbacktwo).not.toHaveBeenCalledWith('anotherValue'); - }); - - it('only deletes subscription cache when there are no more subscribers', () => { - const unsubFunc = jasmine.createSpy('unsubscribe'); - telemetryProvider.subscribe.and.returnValue(unsubFunc); - telemetryProvider.supportsSubscribe.and.returnValue(true); - telemetryAPI.addProvider(telemetryProvider); - - const callback = jasmine.createSpy('callback'); - const callbacktwo = jasmine.createSpy('callback two'); - const callbackThree = jasmine.createSpy('callback three'); - const unsubscribe = telemetryAPI.subscribe(domainObject, callback); - const unsubscribeTwo = telemetryAPI.subscribe(domainObject, callbacktwo); - - expect(telemetryProvider.subscribe.calls.count()).toBe(1); - unsubscribe(); - const unsubscribeThree = telemetryAPI.subscribe(domainObject, callbackThree); - // Regression test for where subscription cache was deleted on each unsubscribe, resulting in - // superfluous additional subscriptions. If the subscription cache is being deleted on each unsubscribe, - // then a subsequent subscribe will result in a new subscription at the provider. - expect(telemetryProvider.subscribe.calls.count()).toBe(1); - unsubscribeTwo(); - unsubscribeThree(); - }); - - it('does subscribe/unsubscribe', () => { - const unsubFunc = jasmine.createSpy('unsubscribe'); - telemetryProvider.subscribe.and.returnValue(unsubFunc); - telemetryProvider.supportsSubscribe.and.returnValue(true); - telemetryAPI.addProvider(telemetryProvider); - - const callback = jasmine.createSpy('callback'); - let unsubscribe = telemetryAPI.subscribe(domainObject, callback); - expect(telemetryProvider.subscribe.calls.count()).toBe(1); - unsubscribe(); - - unsubscribe = telemetryAPI.subscribe(domainObject, callback); - expect(telemetryProvider.subscribe.calls.count()).toBe(2); - unsubscribe(); - }); - - it('subscribes for different object', () => { - const unsubFuncs = []; - const notifiers = []; - telemetryProvider.supportsSubscribe.and.returnValue(true); - telemetryProvider.subscribe.and.callFake(function (obj, cb) { - const unsubFunc = jasmine.createSpy('unsubscribe ' + unsubFuncs.length); - unsubFuncs.push(unsubFunc); - notifiers.push(cb); - - return unsubFunc; - }); - telemetryAPI.addProvider(telemetryProvider); - - const otherDomainObject = JSON.parse(JSON.stringify(domainObject)); - otherDomainObject.identifier.namespace = 'other'; - - const callback = jasmine.createSpy('callback'); - const callbacktwo = jasmine.createSpy('callback two'); - - const unsubscribe = telemetryAPI.subscribe(domainObject, callback); - const unsubscribetwo = telemetryAPI.subscribe(otherDomainObject, callbacktwo); - - expect(telemetryProvider.subscribe.calls.count()).toBe(2); - - notifiers[0]('someValue'); - expect(callback).toHaveBeenCalledWith('someValue'); - expect(callbacktwo).not.toHaveBeenCalledWith('someValue'); - - notifiers[1]('anotherValue'); - expect(callback).not.toHaveBeenCalledWith('anotherValue'); - expect(callbacktwo).toHaveBeenCalledWith('anotherValue'); - - unsubscribe(); - expect(unsubFuncs[0]).toHaveBeenCalled(); - expect(unsubFuncs[1]).not.toHaveBeenCalled(); - - unsubscribetwo(); - expect(unsubFuncs[1]).toHaveBeenCalled(); - }); - - it('sends requests to matching providers', async () => { - const telemPromise = Promise.resolve([]); - telemetryProvider.supportsRequest.and.returnValue(true); - telemetryProvider.request.and.returnValue(telemPromise); - telemetryAPI.addProvider(telemetryProvider); - - await telemetryAPI.request(domainObject); - expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith( - domainObject, - jasmine.any(Object) - ); - expect(telemetryProvider.request).toHaveBeenCalledWith( - domainObject, - jasmine.any(Object) - ); - }); - - it('generates default request options', async () => { - telemetryProvider.supportsRequest.and.returnValue(true); - telemetryProvider.request.and.returnValue(Promise.resolve([])); - telemetryAPI.addProvider(telemetryProvider); - - await telemetryAPI.request(domainObject); - const { signal } = new AbortController(); - expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith( - jasmine.any(Object), - { - signal, - start: 0, - end: 1, - domain: 'system', - timeContext: jasmine.any(Object) - } - ); - - expect(telemetryProvider.request).toHaveBeenCalledWith( - jasmine.any(Object), - { - signal, - start: 0, - end: 1, - domain: 'system', - timeContext: jasmine.any(Object) - } - ); - - telemetryProvider.supportsRequest.calls.reset(); - telemetryProvider.request.calls.reset(); - - await telemetryAPI.request(domainObject, {}); - expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith( - jasmine.any(Object), - { - signal, - start: 0, - end: 1, - domain: 'system', - timeContext: jasmine.any(Object) - } - ); - - expect(telemetryProvider.request).toHaveBeenCalledWith( - jasmine.any(Object), - { - signal, - start: 0, - end: 1, - domain: 'system', - timeContext: jasmine.any(Object) - } - ); - }); - - it('do not overwrite existing request options', async () => { - telemetryProvider.supportsRequest.and.returnValue(true); - telemetryProvider.request.and.returnValue(Promise.resolve([])); - telemetryAPI.addProvider(telemetryProvider); - - await telemetryAPI.request(domainObject, { - start: 20, - end: 30, - domain: 'someDomain' - }); - const { signal } = new AbortController(); - expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith( - jasmine.any(Object), - { - start: 20, - end: 30, - domain: 'someDomain', - signal, - timeContext: jasmine.any(Object) - } - ); - - expect(telemetryProvider.request).toHaveBeenCalledWith( - jasmine.any(Object), - { - start: 20, - end: 30, - domain: 'someDomain', - signal, - timeContext: jasmine.any(Object) - } - ); - }); + const data = await telemetryAPI.request(domainObject); + expect(data).toEqual([]); }); - describe('metadata', () => { - let mockMetadata = {}; - let mockObjectType = { - definition: {} - }; - beforeEach(() => { - telemetryAPI.addProvider({ - key: 'mockMetadataProvider', - supportsMetadata() { - return true; - }, - getMetadata() { - return mockMetadata; - } - }); - openmct.types.get.and.returnValue(mockObjectType); - }); + it('skips providers that do not match', async () => { + telemetryProvider.supportsSubscribe.and.returnValue(false); + telemetryProvider.supportsRequest.and.returnValue(false); + telemetryProvider.request.and.returnValue(Promise.resolve([])); + telemetryAPI.addProvider(telemetryProvider); - it('respects explicit priority', () => { - mockMetadata.values = [ - { - key: "name", - name: "Name", - hints: { - priority: 2 - } + const callback = jasmine.createSpy('callback'); + const unsubscribe = telemetryAPI.subscribe(domainObject, callback); + expect(telemetryProvider.supportsSubscribe).toHaveBeenCalledWith(domainObject); + expect(telemetryProvider.subscribe).not.toHaveBeenCalled(); + expect(unsubscribe).toEqual(jasmine.any(Function)); - }, - { - key: "timestamp", - name: "Timestamp", - hints: { - priority: 1 - } - }, - { - key: "sin", - name: "Sine", - hints: { - priority: 4 - } - }, - { - key: "cos", - name: "Cosine", - hints: { - priority: 3 - } - } - ]; - let metadata = telemetryAPI.getMetadata({}); - let values = metadata.values(); - - values.forEach((value, index) => { - expect(value.hints.priority).toBe(index + 1); - }); - }); - it('if no explicit priority, defaults to order defined', () => { - mockMetadata.values = [ - { - key: "name", - name: "Name" - - }, - { - key: "timestamp", - name: "Timestamp" - }, - { - key: "sin", - name: "Sine" - }, - { - key: "cos", - name: "Cosine" - } - ]; - let metadata = telemetryAPI.getMetadata({}); - let values = metadata.values(); - - values.forEach((value, index) => { - expect(value.key).toBe(mockMetadata.values[index].key); - }); - }); - it('respects domain priority', () => { - mockMetadata.values = [ - { - key: "name", - name: "Name" - - }, - { - key: "timestamp-utc", - name: "Timestamp UTC", - hints: { - domain: 2 - } - }, - { - key: "timestamp-local", - name: "Timestamp Local", - hints: { - domain: 1 - } - }, - { - key: "sin", - name: "Sine", - hints: { - range: 2 - } - }, - { - key: "cos", - name: "Cosine", - hints: { - range: 1 - } - } - ]; - let metadata = telemetryAPI.getMetadata({}); - let values = metadata.valuesForHints(['domain']); - - expect(values[0].key).toBe('timestamp-local'); - expect(values[1].key).toBe('timestamp-utc'); - }); - it('respects range priority', () => { - mockMetadata.values = [ - { - key: "name", - name: "Name" - - }, - { - key: "timestamp-utc", - name: "Timestamp UTC", - hints: { - domain: 2 - } - }, - { - key: "timestamp-local", - name: "Timestamp Local", - hints: { - domain: 1 - } - }, - { - key: "sin", - name: "Sine", - hints: { - range: 2 - } - }, - { - key: "cos", - name: "Cosine", - hints: { - range: 1 - } - } - ]; - let metadata = telemetryAPI.getMetadata({}); - let values = metadata.valuesForHints(['range']); - - expect(values[0].key).toBe('cos'); - expect(values[1].key).toBe('sin'); - }); - it('respects priority and domain ordering', () => { - mockMetadata.values = [ - { - key: "id", - name: "ID", - hints: { - priority: 2 - } - }, - { - key: "name", - name: "Name", - hints: { - priority: 1 - } - - }, - { - key: "timestamp-utc", - name: "Timestamp UTC", - hints: { - domain: 2, - priority: 1 - } - }, - { - key: "timestamp-local", - name: "Timestamp Local", - hints: { - domain: 1, - priority: 2 - } - }, - { - key: "timestamp-pst", - name: "Timestamp PST", - hints: { - domain: 3, - priority: 2 - } - }, - { - key: "sin", - name: "Sine" - }, - { - key: "cos", - name: "Cosine" - } - ]; - let metadata = telemetryAPI.getMetadata({}); - let values = metadata.valuesForHints(['priority', 'domain']); - [ - 'timestamp-utc', - 'timestamp-local', - 'timestamp-pst' - ].forEach((key, index) => { - expect(values[index].key).toBe(key); - }); - }); + await telemetryAPI.request(domainObject); + expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith( + domainObject, + jasmine.any(Object) + ); + expect(telemetryProvider.request).not.toHaveBeenCalled(); }); - describe('telemetry collections', () => { - let domainObject; - let mockMetadata = {}; - let mockObjectType = { - definition: {} - }; + it('sends subscribe calls to matching providers', () => { + const unsubFunc = jasmine.createSpy('unsubscribe'); + telemetryProvider.subscribe.and.returnValue(unsubFunc); + telemetryProvider.supportsSubscribe.and.returnValue(true); + telemetryAPI.addProvider(telemetryProvider); - beforeEach(() => { - openmct.telemetry = telemetryAPI; - telemetryAPI.addProvider({ - key: 'mockMetadataProvider', - supportsMetadata() { - return true; - }, - getMetadata() { - return mockMetadata; - } - }); - openmct.types.get.and.returnValue(mockObjectType); - domainObject = { - identifier: { - key: 'a', - namespace: 'b' - }, - type: 'sample-type' - }; - }); + const callback = jasmine.createSpy('callback'); + const unsubscribe = telemetryAPI.subscribe(domainObject, callback); + expect(telemetryProvider.supportsSubscribe.calls.count()).toBe(1); + expect(telemetryProvider.supportsSubscribe).toHaveBeenCalledWith(domainObject); + expect(telemetryProvider.subscribe.calls.count()).toBe(1); + expect(telemetryProvider.subscribe).toHaveBeenCalledWith( + domainObject, + jasmine.any(Function), + undefined + ); - it('when requested, returns an instance of telemetry collection', () => { - const telemetryCollection = telemetryAPI.requestCollection(domainObject); + const notify = telemetryProvider.subscribe.calls.mostRecent().args[1]; + notify('someValue'); + expect(callback).toHaveBeenCalledWith('someValue'); - expect(telemetryCollection).toBeInstanceOf(TelemetryCollection); - }); + expect(unsubscribe).toEqual(jasmine.any(Function)); + expect(unsubFunc).not.toHaveBeenCalled(); + unsubscribe(); + expect(unsubFunc).toHaveBeenCalled(); + notify('otherValue'); + expect(callback).not.toHaveBeenCalledWith('otherValue'); }); + + it('subscribes once per object', () => { + const unsubFunc = jasmine.createSpy('unsubscribe'); + telemetryProvider.subscribe.and.returnValue(unsubFunc); + telemetryProvider.supportsSubscribe.and.returnValue(true); + telemetryAPI.addProvider(telemetryProvider); + + const callback = jasmine.createSpy('callback'); + const callbacktwo = jasmine.createSpy('callback two'); + const unsubscribe = telemetryAPI.subscribe(domainObject, callback); + const unsubscribetwo = telemetryAPI.subscribe(domainObject, callbacktwo); + + expect(telemetryProvider.subscribe.calls.count()).toBe(1); + + const notify = telemetryProvider.subscribe.calls.mostRecent().args[1]; + notify('someValue'); + expect(callback).toHaveBeenCalledWith('someValue'); + expect(callbacktwo).toHaveBeenCalledWith('someValue'); + + unsubscribe(); + expect(unsubFunc).not.toHaveBeenCalled(); + notify('otherValue'); + expect(callback).not.toHaveBeenCalledWith('otherValue'); + expect(callbacktwo).toHaveBeenCalledWith('otherValue'); + + unsubscribetwo(); + expect(unsubFunc).toHaveBeenCalled(); + notify('anotherValue'); + expect(callback).not.toHaveBeenCalledWith('anotherValue'); + expect(callbacktwo).not.toHaveBeenCalledWith('anotherValue'); + }); + + it('only deletes subscription cache when there are no more subscribers', () => { + const unsubFunc = jasmine.createSpy('unsubscribe'); + telemetryProvider.subscribe.and.returnValue(unsubFunc); + telemetryProvider.supportsSubscribe.and.returnValue(true); + telemetryAPI.addProvider(telemetryProvider); + + const callback = jasmine.createSpy('callback'); + const callbacktwo = jasmine.createSpy('callback two'); + const callbackThree = jasmine.createSpy('callback three'); + const unsubscribe = telemetryAPI.subscribe(domainObject, callback); + const unsubscribeTwo = telemetryAPI.subscribe(domainObject, callbacktwo); + + expect(telemetryProvider.subscribe.calls.count()).toBe(1); + unsubscribe(); + const unsubscribeThree = telemetryAPI.subscribe(domainObject, callbackThree); + // Regression test for where subscription cache was deleted on each unsubscribe, resulting in + // superfluous additional subscriptions. If the subscription cache is being deleted on each unsubscribe, + // then a subsequent subscribe will result in a new subscription at the provider. + expect(telemetryProvider.subscribe.calls.count()).toBe(1); + unsubscribeTwo(); + unsubscribeThree(); + }); + + it('does subscribe/unsubscribe', () => { + const unsubFunc = jasmine.createSpy('unsubscribe'); + telemetryProvider.subscribe.and.returnValue(unsubFunc); + telemetryProvider.supportsSubscribe.and.returnValue(true); + telemetryAPI.addProvider(telemetryProvider); + + const callback = jasmine.createSpy('callback'); + let unsubscribe = telemetryAPI.subscribe(domainObject, callback); + expect(telemetryProvider.subscribe.calls.count()).toBe(1); + unsubscribe(); + + unsubscribe = telemetryAPI.subscribe(domainObject, callback); + expect(telemetryProvider.subscribe.calls.count()).toBe(2); + unsubscribe(); + }); + + it('subscribes for different object', () => { + const unsubFuncs = []; + const notifiers = []; + telemetryProvider.supportsSubscribe.and.returnValue(true); + telemetryProvider.subscribe.and.callFake(function (obj, cb) { + const unsubFunc = jasmine.createSpy('unsubscribe ' + unsubFuncs.length); + unsubFuncs.push(unsubFunc); + notifiers.push(cb); + + return unsubFunc; + }); + telemetryAPI.addProvider(telemetryProvider); + + const otherDomainObject = JSON.parse(JSON.stringify(domainObject)); + otherDomainObject.identifier.namespace = 'other'; + + const callback = jasmine.createSpy('callback'); + const callbacktwo = jasmine.createSpy('callback two'); + + const unsubscribe = telemetryAPI.subscribe(domainObject, callback); + const unsubscribetwo = telemetryAPI.subscribe(otherDomainObject, callbacktwo); + + expect(telemetryProvider.subscribe.calls.count()).toBe(2); + + notifiers[0]('someValue'); + expect(callback).toHaveBeenCalledWith('someValue'); + expect(callbacktwo).not.toHaveBeenCalledWith('someValue'); + + notifiers[1]('anotherValue'); + expect(callback).not.toHaveBeenCalledWith('anotherValue'); + expect(callbacktwo).toHaveBeenCalledWith('anotherValue'); + + unsubscribe(); + expect(unsubFuncs[0]).toHaveBeenCalled(); + expect(unsubFuncs[1]).not.toHaveBeenCalled(); + + unsubscribetwo(); + expect(unsubFuncs[1]).toHaveBeenCalled(); + }); + + it('sends requests to matching providers', async () => { + const telemPromise = Promise.resolve([]); + telemetryProvider.supportsRequest.and.returnValue(true); + telemetryProvider.request.and.returnValue(telemPromise); + telemetryAPI.addProvider(telemetryProvider); + + await telemetryAPI.request(domainObject); + expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith( + domainObject, + jasmine.any(Object) + ); + expect(telemetryProvider.request).toHaveBeenCalledWith(domainObject, jasmine.any(Object)); + }); + + it('generates default request options', async () => { + telemetryProvider.supportsRequest.and.returnValue(true); + telemetryProvider.request.and.returnValue(Promise.resolve([])); + telemetryAPI.addProvider(telemetryProvider); + + await telemetryAPI.request(domainObject); + const { signal } = new AbortController(); + expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(jasmine.any(Object), { + signal, + start: 0, + end: 1, + domain: 'system', + timeContext: jasmine.any(Object) + }); + + expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), { + signal, + start: 0, + end: 1, + domain: 'system', + timeContext: jasmine.any(Object) + }); + + telemetryProvider.supportsRequest.calls.reset(); + telemetryProvider.request.calls.reset(); + + await telemetryAPI.request(domainObject, {}); + expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(jasmine.any(Object), { + signal, + start: 0, + end: 1, + domain: 'system', + timeContext: jasmine.any(Object) + }); + + expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), { + signal, + start: 0, + end: 1, + domain: 'system', + timeContext: jasmine.any(Object) + }); + }); + + it('do not overwrite existing request options', async () => { + telemetryProvider.supportsRequest.and.returnValue(true); + telemetryProvider.request.and.returnValue(Promise.resolve([])); + telemetryAPI.addProvider(telemetryProvider); + + await telemetryAPI.request(domainObject, { + start: 20, + end: 30, + domain: 'someDomain' + }); + const { signal } = new AbortController(); + expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(jasmine.any(Object), { + start: 20, + end: 30, + domain: 'someDomain', + signal, + timeContext: jasmine.any(Object) + }); + + expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), { + start: 20, + end: 30, + domain: 'someDomain', + signal, + timeContext: jasmine.any(Object) + }); + }); + }); + + describe('metadata', () => { + let mockMetadata = {}; + let mockObjectType = { + definition: {} + }; + beforeEach(() => { + telemetryAPI.addProvider({ + key: 'mockMetadataProvider', + supportsMetadata() { + return true; + }, + getMetadata() { + return mockMetadata; + } + }); + openmct.types.get.and.returnValue(mockObjectType); + }); + + it('respects explicit priority', () => { + mockMetadata.values = [ + { + key: 'name', + name: 'Name', + hints: { + priority: 2 + } + }, + { + key: 'timestamp', + name: 'Timestamp', + hints: { + priority: 1 + } + }, + { + key: 'sin', + name: 'Sine', + hints: { + priority: 4 + } + }, + { + key: 'cos', + name: 'Cosine', + hints: { + priority: 3 + } + } + ]; + let metadata = telemetryAPI.getMetadata({}); + let values = metadata.values(); + + values.forEach((value, index) => { + expect(value.hints.priority).toBe(index + 1); + }); + }); + it('if no explicit priority, defaults to order defined', () => { + mockMetadata.values = [ + { + key: 'name', + name: 'Name' + }, + { + key: 'timestamp', + name: 'Timestamp' + }, + { + key: 'sin', + name: 'Sine' + }, + { + key: 'cos', + name: 'Cosine' + } + ]; + let metadata = telemetryAPI.getMetadata({}); + let values = metadata.values(); + + values.forEach((value, index) => { + expect(value.key).toBe(mockMetadata.values[index].key); + }); + }); + it('respects domain priority', () => { + mockMetadata.values = [ + { + key: 'name', + name: 'Name' + }, + { + key: 'timestamp-utc', + name: 'Timestamp UTC', + hints: { + domain: 2 + } + }, + { + key: 'timestamp-local', + name: 'Timestamp Local', + hints: { + domain: 1 + } + }, + { + key: 'sin', + name: 'Sine', + hints: { + range: 2 + } + }, + { + key: 'cos', + name: 'Cosine', + hints: { + range: 1 + } + } + ]; + let metadata = telemetryAPI.getMetadata({}); + let values = metadata.valuesForHints(['domain']); + + expect(values[0].key).toBe('timestamp-local'); + expect(values[1].key).toBe('timestamp-utc'); + }); + it('respects range priority', () => { + mockMetadata.values = [ + { + key: 'name', + name: 'Name' + }, + { + key: 'timestamp-utc', + name: 'Timestamp UTC', + hints: { + domain: 2 + } + }, + { + key: 'timestamp-local', + name: 'Timestamp Local', + hints: { + domain: 1 + } + }, + { + key: 'sin', + name: 'Sine', + hints: { + range: 2 + } + }, + { + key: 'cos', + name: 'Cosine', + hints: { + range: 1 + } + } + ]; + let metadata = telemetryAPI.getMetadata({}); + let values = metadata.valuesForHints(['range']); + + expect(values[0].key).toBe('cos'); + expect(values[1].key).toBe('sin'); + }); + it('respects priority and domain ordering', () => { + mockMetadata.values = [ + { + key: 'id', + name: 'ID', + hints: { + priority: 2 + } + }, + { + key: 'name', + name: 'Name', + hints: { + priority: 1 + } + }, + { + key: 'timestamp-utc', + name: 'Timestamp UTC', + hints: { + domain: 2, + priority: 1 + } + }, + { + key: 'timestamp-local', + name: 'Timestamp Local', + hints: { + domain: 1, + priority: 2 + } + }, + { + key: 'timestamp-pst', + name: 'Timestamp PST', + hints: { + domain: 3, + priority: 2 + } + }, + { + key: 'sin', + name: 'Sine' + }, + { + key: 'cos', + name: 'Cosine' + } + ]; + let metadata = telemetryAPI.getMetadata({}); + let values = metadata.valuesForHints(['priority', 'domain']); + ['timestamp-utc', 'timestamp-local', 'timestamp-pst'].forEach((key, index) => { + expect(values[index].key).toBe(key); + }); + }); + }); + + describe('telemetry collections', () => { + let domainObject; + let mockMetadata = {}; + let mockObjectType = { + definition: {} + }; + + beforeEach(() => { + openmct.telemetry = telemetryAPI; + telemetryAPI.addProvider({ + key: 'mockMetadataProvider', + supportsMetadata() { + return true; + }, + getMetadata() { + return mockMetadata; + } + }); + openmct.types.get.and.returnValue(mockObjectType); + domainObject = { + identifier: { + key: 'a', + namespace: 'b' + }, + type: 'sample-type' + }; + }); + + it('when requested, returns an instance of telemetry collection', () => { + const telemetryCollection = telemetryAPI.requestCollection(domainObject); + + expect(telemetryCollection).toBeInstanceOf(TelemetryCollection); + }); + }); }); describe('Telemetery', () => { - let openmct; - let telemetryProvider; - let telemetryAPI; - let watchedSignal; + let openmct; + let telemetryProvider; + let telemetryAPI; + let watchedSignal; - beforeEach(() => { - openmct = createOpenMct(); - openmct.install(openmct.plugins.MyItems()); + beforeEach(() => { + openmct = createOpenMct(); + openmct.install(openmct.plugins.MyItems()); - telemetryAPI = openmct.telemetry; + telemetryAPI = openmct.telemetry; - telemetryProvider = { - request: (obj, options) => { - watchedSignal = options.signal; + telemetryProvider = { + request: (obj, options) => { + watchedSignal = options.signal; - return Promise.resolve(); - } - }; - spyOn(telemetryAPI, 'findRequestProvider').and.returnValue(telemetryProvider); - }); - - afterEach(() => { - return resetApplicationState(openmct); - }); - - it('should not abort request without navigation', async () => { - telemetryAPI.addProvider(telemetryProvider); - - await telemetryAPI.request({}); - expect(watchedSignal.aborted).toBe(false); - }); - - it('should abort request on navigation', (done) => { - telemetryAPI.addProvider(telemetryProvider); - - telemetryAPI.request({}).finally(() => { - expect(watchedSignal.aborted).toBe(true); - done(); - }); - openmct.router.doPathChange('newPath', 'oldPath'); + return Promise.resolve(); + } + }; + spyOn(telemetryAPI, 'findRequestProvider').and.returnValue(telemetryProvider); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('should not abort request without navigation', async () => { + telemetryAPI.addProvider(telemetryProvider); + + await telemetryAPI.request({}); + expect(watchedSignal.aborted).toBe(false); + }); + + it('should abort request on navigation', (done) => { + telemetryAPI.addProvider(telemetryProvider); + + telemetryAPI.request({}).finally(() => { + expect(watchedSignal.aborted).toBe(true); + done(); }); + openmct.router.doPathChange('newPath', 'oldPath'); + }); }); diff --git a/src/api/telemetry/TelemetryCollection.js b/src/api/telemetry/TelemetryCollection.js index 59fed19b9e..e052355a7a 100644 --- a/src/api/telemetry/TelemetryCollection.js +++ b/src/api/telemetry/TelemetryCollection.js @@ -27,462 +27,454 @@ import { LOADED_ERROR, TIMESYSTEM_KEY_NOTIFICATION, TIMESYSTEM_KEY_WARNING } fro /** Class representing a Telemetry Collection. */ export default class TelemetryCollection extends EventEmitter { - /** - * Creates a Telemetry Collection - * - * @param {OpenMCT} openmct - Open MCT - * @param {module:openmct.DomainObject} domainObject - Domain Object to use for telemetry collection - * @param {object} options - Any options passed in for request/subscribe - */ - constructor(openmct, domainObject, options) { - super(); + /** + * Creates a Telemetry Collection + * + * @param {OpenMCT} openmct - Open MCT + * @param {module:openmct.DomainObject} domainObject - Domain Object to use for telemetry collection + * @param {object} options - Any options passed in for request/subscribe + */ + constructor(openmct, domainObject, options) { + super(); - this.loaded = false; - this.openmct = openmct; - this.domainObject = domainObject; - this.boundedTelemetry = []; - this.futureBuffer = []; - this.parseTime = undefined; - this.metadata = this.openmct.telemetry.getMetadata(domainObject); - this.unsubscribe = undefined; - this.options = options; - this.pageState = undefined; - this.lastBounds = undefined; - this.requestAbort = undefined; - this.isStrategyLatest = this.options.strategy === 'latest'; - this.dataOutsideTimeBounds = false; + this.loaded = false; + this.openmct = openmct; + this.domainObject = domainObject; + this.boundedTelemetry = []; + this.futureBuffer = []; + this.parseTime = undefined; + this.metadata = this.openmct.telemetry.getMetadata(domainObject); + this.unsubscribe = undefined; + this.options = options; + this.pageState = undefined; + this.lastBounds = undefined; + this.requestAbort = undefined; + this.isStrategyLatest = this.options.strategy === 'latest'; + this.dataOutsideTimeBounds = false; + } + + /** + * This will start the requests for historical and realtime data, + * as well as setting up initial values and watchers + */ + load() { + if (this.loaded) { + this._error(LOADED_ERROR); } - /** - * This will start the requests for historical and realtime data, - * as well as setting up initial values and watchers - */ - load() { - if (this.loaded) { - this._error(LOADED_ERROR); - } + this._setTimeSystem(this.openmct.time.timeSystem()); + this.lastBounds = this.openmct.time.bounds(); - this._setTimeSystem(this.openmct.time.timeSystem()); - this.lastBounds = this.openmct.time.bounds(); + this._watchBounds(); + this._watchTimeSystem(); - this._watchBounds(); - this._watchTimeSystem(); + this._requestHistoricalTelemetry(); + this._initiateSubscriptionTelemetry(); - this._requestHistoricalTelemetry(); - this._initiateSubscriptionTelemetry(); + this.loaded = true; + } - this.loaded = true; + /** + * can/should be called by the requester of the telemetry collection + * to remove any listeners + */ + destroy() { + if (this.requestAbort) { + this.requestAbort.abort(); } - /** - * can/should be called by the requester of the telemetry collection - * to remove any listeners - */ - destroy() { - if (this.requestAbort) { - this.requestAbort.abort(); - } - - this._unwatchBounds(); - this._unwatchTimeSystem(); - if (this.unsubscribe) { - this.unsubscribe(); - } - - this.removeAllListeners(); + this._unwatchBounds(); + this._unwatchTimeSystem(); + if (this.unsubscribe) { + this.unsubscribe(); } - /** - * This will start the requests for historical and realtime data, - * as well as setting up initial values and watchers - */ - getAll() { - return this.boundedTelemetry; + this.removeAllListeners(); + } + + /** + * This will start the requests for historical and realtime data, + * as well as setting up initial values and watchers + */ + getAll() { + return this.boundedTelemetry; + } + + /** + * If a historical provider exists, then historical requests will be made + * @private + */ + async _requestHistoricalTelemetry() { + let options = { ...this.options }; + let historicalProvider; + + this.openmct.telemetry.standardizeRequestOptions(options); + historicalProvider = this.openmct.telemetry.findRequestProvider(this.domainObject, options); + + if (!historicalProvider) { + return; } - /** - * If a historical provider exists, then historical requests will be made - * @private - */ - async _requestHistoricalTelemetry() { - let options = { ...this.options }; - let historicalProvider; + let historicalData; - this.openmct.telemetry.standardizeRequestOptions(options); - historicalProvider = this.openmct.telemetry. - findRequestProvider(this.domainObject, options); + options.onPartialResponse = this._processNewTelemetry.bind(this); - if (!historicalProvider) { - return; - } - - let historicalData; - - options.onPartialResponse = this._processNewTelemetry.bind(this); - - try { - if (this.requestAbort) { - this.requestAbort.abort(); - } - - this.requestAbort = new AbortController(); - options.signal = this.requestAbort.signal; - this.emit('requestStarted'); - 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...'); - this._error(error); - } - } - - this.emit('requestEnded'); - this.requestAbort = undefined; - - if (!historicalData || !historicalData.length) { - return; - } - - this._processNewTelemetry(historicalData); + try { + if (this.requestAbort) { + this.requestAbort.abort(); + } + this.requestAbort = new AbortController(); + options.signal = this.requestAbort.signal; + this.emit('requestStarted'); + 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...'); + this._error(error); + } } - /** - * This uses the built in subscription function from Telemetry API - * @private - */ - _initiateSubscriptionTelemetry() { + this.emit('requestEnded'); + this.requestAbort = undefined; - if (this.unsubscribe) { - this.unsubscribe(); - } - - this.unsubscribe = this.openmct.telemetry - .subscribe( - this.domainObject, - datum => this._processNewTelemetry(datum), - this.options - ); + if (!historicalData || !historicalData.length) { + return; } - /** - * Filter any new telemetry (add/page, historical, subscription) based on - * time bounds and dupes - * - * @param {(Object|Object[])} telemetryData - telemetry data object or - * array of telemetry data objects - * @private - */ - _processNewTelemetry(telemetryData) { - if (telemetryData === undefined) { - return; - } + this._processNewTelemetry(historicalData); + } - let latestBoundedDatum = this.boundedTelemetry[this.boundedTelemetry.length - 1]; - let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData]; - let parsedValue; - let beforeStartOfBounds; - let afterEndOfBounds; - let added = []; - let addedIndices = []; - let hasDataBeforeStartBound = false; - - // loop through, sort and dedupe - for (let datum of data) { - parsedValue = this.parseTime(datum); - beforeStartOfBounds = parsedValue < this.lastBounds.start; - afterEndOfBounds = parsedValue > this.lastBounds.end; - - if (!afterEndOfBounds && (!beforeStartOfBounds || (this.isStrategyLatest && this.openmct.telemetry.greedyLAD()))) { - let isDuplicate = false; - let startIndex = this._sortedIndex(datum); - let endIndex = undefined; - - // dupe check - if (startIndex !== this.boundedTelemetry.length) { - endIndex = _.sortedLastIndexBy( - this.boundedTelemetry, - datum, - boundedDatum => this.parseTime(boundedDatum) - ); - - if (endIndex > startIndex) { - let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex); - isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, datum)); - } - } else if (startIndex === this.boundedTelemetry.length) { - isDuplicate = _.isEqual(datum, this.boundedTelemetry[this.boundedTelemetry.length - 1]); - } - - if (!isDuplicate) { - let index = endIndex || startIndex; - - this.boundedTelemetry.splice(index, 0, datum); - addedIndices.push(index); - added.push(datum); - - if (!hasDataBeforeStartBound && beforeStartOfBounds) { - hasDataBeforeStartBound = true; - } - } - - } else if (afterEndOfBounds) { - this.futureBuffer.push(datum); - } - } - - if (added.length) { - // if latest strategy is requested, we need to check if the value is the latest unemitted 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) { - if (hasDataBeforeStartBound) { - this._handleDataOutsideBounds(); - } else { - this._handleDataInsideBounds(); - } - - this.emit('add', this.boundedTelemetry); - } - } else { - this.emit('add', added, addedIndices); - } - } + /** + * This uses the built in subscription function from Telemetry API + * @private + */ + _initiateSubscriptionTelemetry() { + if (this.unsubscribe) { + this.unsubscribe(); } - /** - * Finds the correct insertion point for the given telemetry datum. - * Leverages lodash's `sortedIndexBy` function which implements a binary search. - * @private - */ - _sortedIndex(datum) { - if (this.boundedTelemetry.length === 0) { - return 0; + this.unsubscribe = this.openmct.telemetry.subscribe( + this.domainObject, + (datum) => this._processNewTelemetry(datum), + this.options + ); + } + + /** + * Filter any new telemetry (add/page, historical, subscription) based on + * time bounds and dupes + * + * @param {(Object|Object[])} telemetryData - telemetry data object or + * array of telemetry data objects + * @private + */ + _processNewTelemetry(telemetryData) { + 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 = []; + let addedIndices = []; + let hasDataBeforeStartBound = false; + + // loop through, sort and dedupe + for (let datum of data) { + parsedValue = this.parseTime(datum); + beforeStartOfBounds = parsedValue < this.lastBounds.start; + afterEndOfBounds = parsedValue > this.lastBounds.end; + + if ( + !afterEndOfBounds && + (!beforeStartOfBounds || (this.isStrategyLatest && this.openmct.telemetry.greedyLAD())) + ) { + let isDuplicate = false; + let startIndex = this._sortedIndex(datum); + let endIndex = undefined; + + // dupe check + if (startIndex !== this.boundedTelemetry.length) { + endIndex = _.sortedLastIndexBy(this.boundedTelemetry, datum, (boundedDatum) => + this.parseTime(boundedDatum) + ); + + if (endIndex > startIndex) { + let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex); + isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, datum)); + } + } else if (startIndex === this.boundedTelemetry.length) { + isDuplicate = _.isEqual(datum, this.boundedTelemetry[this.boundedTelemetry.length - 1]); } - let parsedValue = this.parseTime(datum); - let lastValue = this.parseTime(this.boundedTelemetry[this.boundedTelemetry.length - 1]); + if (!isDuplicate) { + let index = endIndex || startIndex; - if (parsedValue > lastValue || parsedValue === lastValue) { - return this.boundedTelemetry.length; + this.boundedTelemetry.splice(index, 0, datum); + addedIndices.push(index); + added.push(datum); + + if (!hasDataBeforeStartBound && beforeStartOfBounds) { + hasDataBeforeStartBound = true; + } + } + } else if (afterEndOfBounds) { + this.futureBuffer.push(datum); + } + } + + if (added.length) { + // if latest strategy is requested, we need to check if the value is the latest unemitted 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) { + if (hasDataBeforeStartBound) { + this._handleDataOutsideBounds(); + } else { + this._handleDataInsideBounds(); + } + + this.emit('add', this.boundedTelemetry); + } + } else { + this.emit('add', added, addedIndices); + } + } + } + + /** + * Finds the correct insertion point for the given telemetry datum. + * Leverages lodash's `sortedIndexBy` function which implements a binary search. + * @private + */ + _sortedIndex(datum) { + if (this.boundedTelemetry.length === 0) { + return 0; + } + + let parsedValue = this.parseTime(datum); + let lastValue = this.parseTime(this.boundedTelemetry[this.boundedTelemetry.length - 1]); + + if (parsedValue > lastValue || parsedValue === lastValue) { + return this.boundedTelemetry.length; + } else { + return _.sortedIndexBy(this.boundedTelemetry, datum, (boundedDatum) => + this.parseTime(boundedDatum) + ); + } + } + + /** + * when the start time, end time, or both have been updated. + * data could be added OR removed here we update the current + * bounded telemetry + * + * @param {TimeConductorBounds} bounds The newly updated bounds + * @param {boolean} [tick] `true` if the bounds update was due to + * a "tick" event (ie. was an automatic update), false otherwise. + * @private + */ + _bounds(bounds, isTick) { + let startChanged = this.lastBounds.start !== bounds.start; + let endChanged = this.lastBounds.end !== bounds.end; + + 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; + let endIndex = 0; + + let discarded = []; + let added = []; + let testDatum = {}; + + if (endChanged) { + testDatum[this.timeKey] = bounds.end; + // Calculate the new index of the last item in bounds + endIndex = _.sortedLastIndexBy(this.futureBuffer, testDatum, (datum) => + this.parseTime(datum) + ); + added = this.futureBuffer.splice(0, endIndex); + } + + if (startChanged) { + testDatum[this.timeKey] = bounds.start; + + // 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])) { + // if greedyLAD is active and there is no new data to replace, don't discard + const isGreedyLAD = this.openmct.telemetry.greedyLAD(); + const shouldRemove = !isGreedyLAD || (isGreedyLAD && added.length > 0); + + if (shouldRemove) { + discarded = this.boundedTelemetry; + this.boundedTelemetry = []; + // since it IS strategy latest, we can assume there will be at least 1 datum + // unless no data was returned in the first request, we need to account for that + } else if (this.boundedTelemetry.length === 1) { + this._handleDataOutsideBounds(); + } + } + } + + if (discarded.length > 0) { + this.emit('remove', discarded); + } + + if (added.length > 0) { + if (!this.isStrategyLatest) { + this.boundedTelemetry = [...this.boundedTelemetry, ...added]; } else { - return _.sortedIndexBy( - this.boundedTelemetry, - datum, - boundedDatum => this.parseTime(boundedDatum) - ); - } - } + this._handleDataInsideBounds(); - /** - * when the start time, end time, or both have been updated. - * data could be added OR removed here we update the current - * bounded telemetry - * - * @param {TimeConductorBounds} bounds The newly updated bounds - * @param {boolean} [tick] `true` if the bounds update was due to - * a "tick" event (ie. was an automatic update), false otherwise. - * @private - */ - _bounds(bounds, isTick) { - let startChanged = this.lastBounds.start !== bounds.start; - let endChanged = this.lastBounds.end !== bounds.end; - - 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; - let endIndex = 0; - - let discarded = []; - let added = []; - let testDatum = {}; - - if (endChanged) { - testDatum[this.timeKey] = bounds.end; - // Calculate the new index of the last item in bounds - endIndex = _.sortedLastIndexBy( - this.futureBuffer, - testDatum, - datum => this.parseTime(datum) - ); - added = this.futureBuffer.splice(0, endIndex); - } - - if (startChanged) { - testDatum[this.timeKey] = bounds.start; - - // 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])) { - // if greedyLAD is active and there is no new data to replace, don't discard - const isGreedyLAD = this.openmct.telemetry.greedyLAD(); - const shouldRemove = (!isGreedyLAD || (isGreedyLAD && added.length > 0)); - - if (shouldRemove) { - discarded = this.boundedTelemetry; - this.boundedTelemetry = []; - // since it IS strategy latest, we can assume there will be at least 1 datum - // unless no data was returned in the first request, we need to account for that - } else if (this.boundedTelemetry.length === 1) { - this._handleDataOutsideBounds(); - } - } - } - - if (discarded.length > 0) { - this.emit('remove', discarded); - } - - if (added.length > 0) { - if (!this.isStrategyLatest) { - this.boundedTelemetry = [...this.boundedTelemetry, ...added]; - } else { - this._handleDataInsideBounds(); - - added = [added[added.length - 1]]; - this.boundedTelemetry = added; - } - - // Assumption is that added will be of length 1 here, so just send the last index of the boundedTelemetry in the add event - this.emit('add', added, [this.boundedTelemetry.length]); - } - } else { - // user bounds change, reset - this._reset(); + added = [added[added.length - 1]]; + this.boundedTelemetry = added; } + // Assumption is that added will be of length 1 here, so just send the last index of the boundedTelemetry in the add event + this.emit('add', added, [this.boundedTelemetry.length]); + } + } else { + // user bounds change, reset + this._reset(); + } + } + + _handleDataInsideBounds() { + if (this.dataOutsideTimeBounds) { + this.dataOutsideTimeBounds = false; + this.emit('dataInsideTimeBounds'); + } + } + + _handleDataOutsideBounds() { + if (!this.dataOutsideTimeBounds) { + this.dataOutsideTimeBounds = true; + this.emit('dataOutsideTimeBounds'); + } + } + + /** + * whenever the time system is updated need to update related values in + * the Telemetry Collection and reset the telemetry collection + * + * @param {TimeSystem} timeSystem - the value of the currently applied + * Time System + * @private + */ + _setTimeSystem(timeSystem) { + let domains = []; + let metadataValue = { format: timeSystem.key }; + + if (this.metadata) { + domains = this.metadata.valuesForHints(['domain']); + metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key }; } - _handleDataInsideBounds() { - if (this.dataOutsideTimeBounds) { - this.dataOutsideTimeBounds = false; - this.emit('dataInsideTimeBounds'); - } + let domain = domains.find((d) => d.key === 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); } - _handleDataOutsideBounds() { - if (!this.dataOutsideTimeBounds) { - this.dataOutsideTimeBounds = true; - this.emit('dataOutsideTimeBounds'); - } - } + let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); - /** - * whenever the time system is updated need to update related values in - * the Telemetry Collection and reset the telemetry collection - * - * @param {TimeSystem} timeSystem - the value of the currently applied - * Time System - * @private - */ - _setTimeSystem(timeSystem) { - let domains = []; - let metadataValue = { format: timeSystem.key }; + this.parseTime = (datum) => { + return valueFormatter.parse(datum); + }; + } - if (this.metadata) { - domains = this.metadata.valuesForHints(['domain']); - metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key }; - } + _setTimeSystemAndFetchData(timeSystem) { + this._setTimeSystem(timeSystem); + this._reset(); + } - let domain = domains.find((d) => d.key === timeSystem.key); + /** + * Reset the telemetry data of the collection, and re-request + * historical telemetry + * @private + * + * @todo handle subscriptions more granually + */ + _reset() { + this.boundedTelemetry = []; + this.futureBuffer = []; - if (domain !== undefined) { - // timeKey is used to create a dummy datum used for sorting - this.timeKey = domain.source; - } else { - this.timeKey = undefined; + this.emit('clear'); - this._warn(TIMESYSTEM_KEY_WARNING); - this.openmct.notifications.alert(TIMESYSTEM_KEY_NOTIFICATION); - } + this._requestHistoricalTelemetry(); + } - let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); + /** + * adds the _bounds callback to the 'bounds' timeAPI listener + * @private + */ + _watchBounds() { + this.openmct.time.on('bounds', this._bounds, this); + } - this.parseTime = (datum) => { - return valueFormatter.parse(datum); - }; - } + /** + * removes the _bounds callback from the 'bounds' timeAPI listener + * @private + */ + _unwatchBounds() { + this.openmct.time.off('bounds', this._bounds, this); + } - _setTimeSystemAndFetchData(timeSystem) { - this._setTimeSystem(timeSystem); - this._reset(); - } + /** + * adds the _setTimeSystemAndFetchData callback to the 'timeSystem' timeAPI listener + * @private + */ + _watchTimeSystem() { + this.openmct.time.on('timeSystem', this._setTimeSystemAndFetchData, this); + } - /** - * Reset the telemetry data of the collection, and re-request - * historical telemetry - * @private - * - * @todo handle subscriptions more granually - */ - _reset() { - this.boundedTelemetry = []; - this.futureBuffer = []; + /** + * removes the _setTimeSystemAndFetchData callback from the 'timeSystem' timeAPI listener + * @private + */ + _unwatchTimeSystem() { + this.openmct.time.off('timeSystem', this._setTimeSystemAndFetchData, this); + } - this.emit('clear'); + /** + * will throw a new Error, for passed in message + * @param {string} message Message describing the error + * @private + */ + _error(message) { + throw new Error(message); + } - this._requestHistoricalTelemetry(); - } - - /** - * adds the _bounds callback to the 'bounds' timeAPI listener - * @private - */ - _watchBounds() { - this.openmct.time.on('bounds', this._bounds, this); - } - - /** - * removes the _bounds callback from the 'bounds' timeAPI listener - * @private - */ - _unwatchBounds() { - this.openmct.time.off('bounds', this._bounds, this); - } - - /** - * adds the _setTimeSystemAndFetchData callback to the 'timeSystem' timeAPI listener - * @private - */ - _watchTimeSystem() { - this.openmct.time.on('timeSystem', this._setTimeSystemAndFetchData, this); - } - - /** - * removes the _setTimeSystemAndFetchData callback from the 'timeSystem' timeAPI listener - * @private - */ - _unwatchTimeSystem() { - this.openmct.time.off('timeSystem', this._setTimeSystemAndFetchData, this); - } - - /** - * will throw a new Error, for passed in message - * @param {string} message Message describing the error - * @private - */ - _error(message) { - throw new Error(message); - } - - _warn(message) { - console.warn(message); - } + _warn(message) { + console.warn(message); + } } diff --git a/src/api/telemetry/TelemetryCollectionSpec.js b/src/api/telemetry/TelemetryCollectionSpec.js index ddae7a53ac..3990ae61c3 100644 --- a/src/api/telemetry/TelemetryCollectionSpec.js +++ b/src/api/telemetry/TelemetryCollectionSpec.js @@ -20,82 +20,79 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { - createOpenMct, - resetApplicationState -} from 'utils/testing'; +import { createOpenMct, resetApplicationState } from 'utils/testing'; import { TIMESYSTEM_KEY_WARNING } from './constants'; describe('Telemetry Collection', () => { - let openmct; - let mockMetadataProvider; - let mockMetadata = {}; - let domainObject; + let openmct; + let mockMetadataProvider; + let mockMetadata = {}; + let domainObject; - beforeEach(done => { - openmct = createOpenMct(); - openmct.on('start', done); + beforeEach((done) => { + openmct = createOpenMct(); + openmct.on('start', done); - domainObject = { - identifier: { - key: 'a', - namespace: 'b' - }, - type: 'sample-type' - }; + domainObject = { + identifier: { + key: 'a', + namespace: 'b' + }, + type: 'sample-type' + }; - mockMetadataProvider = { - key: 'mockMetadataProvider', - supportsMetadata() { - return true; - }, - getMetadata() { - return mockMetadata; - } - }; + mockMetadataProvider = { + key: 'mockMetadataProvider', + supportsMetadata() { + return true; + }, + getMetadata() { + return mockMetadata; + } + }; - openmct.telemetry.addProvider(mockMetadataProvider); - openmct.startHeadless(); - }); + openmct.telemetry.addProvider(mockMetadataProvider); + openmct.startHeadless(); + }); - afterEach(() => { - return resetApplicationState(); - }); + afterEach(() => { + return resetApplicationState(); + }); - it('Warns if telemetry metadata does not match the active timesystem', () => { - mockMetadata.values = [ - { - key: 'foo', - name: 'Bar', - hints: { - domain: 1 - } - } - ]; + 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(); + const telemetryCollection = openmct.telemetry.requestCollection(domainObject); + spyOn(telemetryCollection, '_warn'); + telemetryCollection.load(); - expect(telemetryCollection._warn).toHaveBeenCalledOnceWith(TIMESYSTEM_KEY_WARNING); - }); + 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 - } - } - ]; + 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(); + const telemetryCollection = openmct.telemetry.requestCollection(domainObject); + spyOn(telemetryCollection, '_warn'); + telemetryCollection.load(); - expect(telemetryCollection._warn).not.toHaveBeenCalled(); - }); + expect(telemetryCollection._warn).not.toHaveBeenCalled(); + }); }); diff --git a/src/api/telemetry/TelemetryMetadataManager.js b/src/api/telemetry/TelemetryMetadataManager.js index ec289a8f80..4247b24248 100644 --- a/src/api/telemetry/TelemetryMetadataManager.js +++ b/src/api/telemetry/TelemetryMetadataManager.js @@ -20,139 +20,135 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - 'lodash' -], function ( - _ -) { +define(['lodash'], function (_) { + function applyReasonableDefaults(valueMetadata, index) { + valueMetadata.source = valueMetadata.source || valueMetadata.key; + valueMetadata.hints = valueMetadata.hints || {}; - function applyReasonableDefaults(valueMetadata, index) { - valueMetadata.source = valueMetadata.source || valueMetadata.key; - valueMetadata.hints = valueMetadata.hints || {}; + if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'x')) { + if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'domain')) { + valueMetadata.hints.domain = valueMetadata.hints.x; + } - if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'x')) { - if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'domain')) { - valueMetadata.hints.domain = valueMetadata.hints.x; - } - - delete valueMetadata.hints.x; - } - - if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'y')) { - if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'range')) { - valueMetadata.hints.range = valueMetadata.hints.y; - } - - delete valueMetadata.hints.y; - } - - if (valueMetadata.format === 'enum') { - if (!valueMetadata.values) { - valueMetadata.values = valueMetadata.enumerations.map(e => e.value); - } - - if (!Object.prototype.hasOwnProperty.call(valueMetadata, 'max')) { - valueMetadata.max = Math.max(valueMetadata.values) + 1; - } - - if (!Object.prototype.hasOwnProperty.call(valueMetadata, 'min')) { - valueMetadata.min = Math.min(valueMetadata.values) - 1; - } - } - - if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'priority')) { - valueMetadata.hints.priority = index; - } - - return valueMetadata; + delete valueMetadata.hints.x; } - /** - * Utility class for handling and inspecting telemetry metadata. Applies - * reasonable defaults to simplify the task of providing metadata, while - * also providing methods for interrogating telemetry metadata. - */ - function TelemetryMetadataManager(metadata) { - this.metadata = metadata; + if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'y')) { + if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'range')) { + valueMetadata.hints.range = valueMetadata.hints.y; + } - this.valueMetadatas = this.metadata.values ? this.metadata.values.map(applyReasonableDefaults) : []; + delete valueMetadata.hints.y; } - /** - * Get value metadata for a single key. - */ - TelemetryMetadataManager.prototype.value = function (key) { - return this.valueMetadatas.filter(function (metadata) { - return metadata.key === key; - })[0]; - }; + if (valueMetadata.format === 'enum') { + if (!valueMetadata.values) { + valueMetadata.values = valueMetadata.enumerations.map((e) => e.value); + } - /** - * Returns all value metadatas, sorted by priority. - */ - TelemetryMetadataManager.prototype.values = function () { - return this.valuesForHints(['priority']); - }; + if (!Object.prototype.hasOwnProperty.call(valueMetadata, 'max')) { + valueMetadata.max = Math.max(valueMetadata.values) + 1; + } - /** - * Get an array of valueMetadatas that posess all hints requested. - * Array is sorted based on hint priority. - * - */ - TelemetryMetadataManager.prototype.valuesForHints = function ( - hints - ) { - function hasHint(hint) { - // eslint-disable-next-line no-invalid-this - return Object.prototype.hasOwnProperty.call(this.hints, hint); - } + if (!Object.prototype.hasOwnProperty.call(valueMetadata, 'min')) { + valueMetadata.min = Math.min(valueMetadata.values) - 1; + } + } - function hasHints(metadata) { - return hints.every(hasHint, metadata); - } + if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'priority')) { + valueMetadata.hints.priority = index; + } - const matchingMetadata = this.valueMetadatas.filter(hasHints); - let iteratees = hints.map(hint => { - return (metadata) => { - return metadata.hints[hint]; - }; - }); + return valueMetadata; + } - return _.sortBy(matchingMetadata, ...iteratees); - }; + /** + * Utility class for handling and inspecting telemetry metadata. Applies + * reasonable defaults to simplify the task of providing metadata, while + * also providing methods for interrogating telemetry metadata. + */ + function TelemetryMetadataManager(metadata) { + this.metadata = metadata; - /** - * check out of a given metadata has array values - */ - TelemetryMetadataManager.prototype.isArrayValue = function (metadata) { - const regex = /\[\]$/g; - if (!metadata.format && !metadata.formatString) { - return false; - } + this.valueMetadatas = this.metadata.values + ? this.metadata.values.map(applyReasonableDefaults) + : []; + } - return (metadata.format || metadata.formatString).match(regex) !== null; - }; + /** + * Get value metadata for a single key. + */ + TelemetryMetadataManager.prototype.value = function (key) { + return this.valueMetadatas.filter(function (metadata) { + return metadata.key === key; + })[0]; + }; - TelemetryMetadataManager.prototype.getFilterableValues = function () { - return this.valueMetadatas.filter(metadatum => metadatum.filters && metadatum.filters.length > 0); - }; + /** + * Returns all value metadatas, sorted by priority. + */ + TelemetryMetadataManager.prototype.values = function () { + return this.valuesForHints(['priority']); + }; - TelemetryMetadataManager.prototype.getDefaultDisplayValue = function () { - let valueMetadata = this.valuesForHints(['range'])[0]; + /** + * Get an array of valueMetadatas that posess all hints requested. + * Array is sorted based on hint priority. + * + */ + TelemetryMetadataManager.prototype.valuesForHints = function (hints) { + function hasHint(hint) { + // eslint-disable-next-line no-invalid-this + return Object.prototype.hasOwnProperty.call(this.hints, hint); + } - if (valueMetadata === undefined) { - valueMetadata = this.values().filter(values => { - return !(values.hints.domain); - })[0]; - } + function hasHints(metadata) { + return hints.every(hasHint, metadata); + } - if (valueMetadata === undefined) { - valueMetadata = this.values()[0]; - } + const matchingMetadata = this.valueMetadatas.filter(hasHints); + let iteratees = hints.map((hint) => { + return (metadata) => { + return metadata.hints[hint]; + }; + }); - return valueMetadata; - }; + return _.sortBy(matchingMetadata, ...iteratees); + }; - return TelemetryMetadataManager; + /** + * 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 + ); + }; + + TelemetryMetadataManager.prototype.getDefaultDisplayValue = function () { + let valueMetadata = this.valuesForHints(['range'])[0]; + + if (valueMetadata === undefined) { + valueMetadata = this.values().filter((values) => { + return !values.hints.domain; + })[0]; + } + + if (valueMetadata === undefined) { + valueMetadata = this.values()[0]; + } + + return valueMetadata; + }; + + return TelemetryMetadataManager; }); diff --git a/src/api/telemetry/TelemetryRequestInterceptor.js b/src/api/telemetry/TelemetryRequestInterceptor.js index 67b665910a..bdec628bfb 100644 --- a/src/api/telemetry/TelemetryRequestInterceptor.js +++ b/src/api/telemetry/TelemetryRequestInterceptor.js @@ -21,48 +21,47 @@ *****************************************************************************/ 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 = []; - } + /** + * 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# - */ + /** + * @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); - }); - } + /** + * 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 e07933ed2c..f97d650ebe 100644 --- a/src/api/telemetry/TelemetryValueFormatter.js +++ b/src/api/telemetry/TelemetryValueFormatter.js @@ -20,137 +20,133 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - 'lodash', - 'printj' -], function ( - _, - printj -) { +define(['lodash', 'printj'], function (_, printj) { + // TODO: needs reference to formatService; + function TelemetryValueFormatter(valueMetadata, formatMap) { + const numberFormatter = { + parse: function (x) { + return Number(x); + }, + format: function (x) { + return x; + }, + validate: function (x) { + return true; + } + }; - // TODO: needs reference to formatService; - function TelemetryValueFormatter(valueMetadata, formatMap) { - const numberFormatter = { - parse: function (x) { - return Number(x); - }, - format: function (x) { - return x; - }, - validate: function (x) { - return true; - } - }; + this.valueMetadata = valueMetadata; - this.valueMetadata = valueMetadata; + function getNonArrayValue(value) { + //metadata format could have array formats ex. string[]/number[] + const arrayRegex = /\[\]$/g; + if (value && value.match(arrayRegex)) { + return value.replace(arrayRegex, ''); + } - 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; - vm.byString[e.string] = e.value; - - return vm; - }, { - byValue: {}, - byString: {} - }); - this.formatter.format = function (value) { - if (Object.prototype.hasOwnProperty.call(this.enumerations.byValue, value)) { - return this.enumerations.byValue[value]; - } - - return value; - }.bind(this); - this.formatter.parse = function (string) { - if (typeof string === "string") { - if (Object.prototype.hasOwnProperty.call(this.enumerations.byString, string)) { - return this.enumerations.byString[string]; - } - } - - return Number(string); - }.bind(this); - } - - // Check for formatString support once instead of per format call. - if (valueMetadata.formatString) { - const baseFormat = this.formatter.format; - const formatString = getNonArrayValue(valueMetadata.formatString); - this.formatter.format = function (value) { - return printj.sprintf(formatString, baseFormat.call(this, value)); - }; - } - - if (valueMetadataFormat === 'string') { - this.formatter.parse = function (value) { - if (value === undefined) { - return ''; - } - - if (typeof value === 'string') { - return value; - } else { - return value.toString(); - } - }; - - this.formatter.format = function (value) { - return value; - }; - - this.formatter.validate = function (value) { - return typeof value === 'string'; - }; - } + return value; } - TelemetryValueFormatter.prototype.parse = function (datum) { - const isDatumArray = Array.isArray(datum); - if (_.isObject(datum)) { - const objectDatum = isDatumArray ? datum : datum[this.valueMetadata.source]; - if (Array.isArray(objectDatum)) { - return objectDatum.map((item) => { - return this.formatter.parse(item); - }); - } else { - return this.formatter.parse(objectDatum); - } + 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; + vm.byString[e.string] = e.value; + + return vm; + }, + { + byValue: {}, + byString: {} + } + ); + this.formatter.format = function (value) { + if (Object.prototype.hasOwnProperty.call(this.enumerations.byValue, value)) { + return this.enumerations.byValue[value]; } - return this.formatter.parse(datum); - }; - - TelemetryValueFormatter.prototype.format = function (datum) { - const isDatumArray = Array.isArray(datum); - if (_.isObject(datum)) { - const objectDatum = isDatumArray ? datum : datum[this.valueMetadata.source]; - if (Array.isArray(objectDatum)) { - return objectDatum.map((item) => { - return this.formatter.format(item); - }); - } else { - return this.formatter.format(objectDatum); - } + return value; + }.bind(this); + this.formatter.parse = function (string) { + if (typeof string === 'string') { + if (Object.prototype.hasOwnProperty.call(this.enumerations.byString, string)) { + return this.enumerations.byString[string]; + } } - return this.formatter.format(datum); - }; + return Number(string); + }.bind(this); + } - return TelemetryValueFormatter; + // Check for formatString support once instead of per format call. + if (valueMetadata.formatString) { + const baseFormat = this.formatter.format; + const formatString = getNonArrayValue(valueMetadata.formatString); + this.formatter.format = function (value) { + return printj.sprintf(formatString, baseFormat.call(this, value)); + }; + } + + if (valueMetadataFormat === 'string') { + this.formatter.parse = function (value) { + if (value === undefined) { + return ''; + } + + if (typeof value === 'string') { + return value; + } else { + return value.toString(); + } + }; + + this.formatter.format = function (value) { + return value; + }; + + this.formatter.validate = function (value) { + return typeof value === 'string'; + }; + } + } + + TelemetryValueFormatter.prototype.parse = function (datum) { + const isDatumArray = Array.isArray(datum); + if (_.isObject(datum)) { + const objectDatum = isDatumArray ? datum : 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); + }; + + TelemetryValueFormatter.prototype.format = function (datum) { + const isDatumArray = Array.isArray(datum); + if (_.isObject(datum)) { + const objectDatum = isDatumArray ? datum : 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); + }; + + return TelemetryValueFormatter; }); diff --git a/src/api/telemetry/constants.js b/src/api/telemetry/constants.js index 07e343c2f8..c732f61be8 100644 --- a/src/api/telemetry/constants.js +++ b/src/api/telemetry/constants.js @@ -20,6 +20,8 @@ * 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 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/GlobalTimeContext.js b/src/api/time/GlobalTimeContext.js index a7a721812c..2ba472c9b4 100644 --- a/src/api/time/GlobalTimeContext.js +++ b/src/api/time/GlobalTimeContext.js @@ -20,87 +20,87 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import TimeContext from "./TimeContext"; +import TimeContext from './TimeContext'; /** * The GlobalContext handles getting and setting time of the openmct application in general. * Views will use this context unless they specify an alternate/independent time context */ class GlobalTimeContext extends TimeContext { - constructor() { - super(); + constructor() { + super(); - //The Time Of Interest - this.toi = undefined; + //The Time Of Interest + this.toi = undefined; + } + + /** + * Get or set the start and end time of the time conductor. Basic validation + * of bounds is performed. + * + * @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds + * @throws {Error} Validation error + * @fires module:openmct.TimeAPI~bounds + * @returns {module:openmct.TimeAPI~TimeConductorBounds} + * @memberof module:openmct.TimeAPI# + * @method bounds + */ + bounds(newBounds) { + if (arguments.length > 0) { + super.bounds.call(this, ...arguments); + // If a bounds change results in a TOI outside of the current + // bounds, unset it + if (this.toi < newBounds.start || this.toi > newBounds.end) { + this.timeOfInterest(undefined); + } } - /** - * Get or set the start and end time of the time conductor. Basic validation - * of bounds is performed. - * - * @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds - * @throws {Error} Validation error - * @fires module:openmct.TimeAPI~bounds - * @returns {module:openmct.TimeAPI~TimeConductorBounds} - * @memberof module:openmct.TimeAPI# - * @method bounds - */ - bounds(newBounds) { - if (arguments.length > 0) { - super.bounds.call(this, ...arguments); - // If a bounds change results in a TOI outside of the current - // bounds, unset it - if (this.toi < newBounds.start || this.toi > newBounds.end) { - this.timeOfInterest(undefined); - } - } + //Return a copy to prevent direct mutation of time conductor bounds. + return JSON.parse(JSON.stringify(this.boundsVal)); + } - //Return a copy to prevent direct mutation of time conductor bounds. - return JSON.parse(JSON.stringify(this.boundsVal)); + /** + * Update bounds based on provided time and current offsets + * @private + * @param {number} timestamp A time from which bounds will be calculated + * using current offsets. + */ + tick(timestamp) { + super.tick.call(this, ...arguments); + + // If a bounds change results in a TOI outside of the current + // bounds, unset it + if (this.toi < this.boundsVal.start || this.toi > this.boundsVal.end) { + this.timeOfInterest(undefined); + } + } + + /** + * Get or set the Time of Interest. The Time of Interest is a single point + * in time, and constitutes the temporal focus of application views. It can + * be manipulated by the user from the time conductor or from other views. + * The time of interest can effectively be unset by assigning a value of + * 'undefined'. + * @fires module:openmct.TimeAPI~timeOfInterest + * @param newTOI + * @returns {number} the current time of interest + * @memberof module:openmct.TimeAPI# + * @method timeOfInterest + */ + timeOfInterest(newTOI) { + if (arguments.length > 0) { + this.toi = newTOI; + /** + * The Time of Interest has moved. + * @event timeOfInterest + * @memberof module:openmct.TimeAPI~ + * @property {number} Current time of interest + */ + this.emit('timeOfInterest', this.toi); } - /** - * Update bounds based on provided time and current offsets - * @private - * @param {number} timestamp A time from which bounds will be calculated - * using current offsets. - */ - tick(timestamp) { - super.tick.call(this, ...arguments); - - // If a bounds change results in a TOI outside of the current - // bounds, unset it - if (this.toi < this.boundsVal.start || this.toi > this.boundsVal.end) { - this.timeOfInterest(undefined); - } - } - - /** - * Get or set the Time of Interest. The Time of Interest is a single point - * in time, and constitutes the temporal focus of application views. It can - * be manipulated by the user from the time conductor or from other views. - * The time of interest can effectively be unset by assigning a value of - * 'undefined'. - * @fires module:openmct.TimeAPI~timeOfInterest - * @param newTOI - * @returns {number} the current time of interest - * @memberof module:openmct.TimeAPI# - * @method timeOfInterest - */ - timeOfInterest(newTOI) { - if (arguments.length > 0) { - this.toi = newTOI; - /** - * The Time of Interest has moved. - * @event timeOfInterest - * @memberof module:openmct.TimeAPI~ - * @property {number} Current time of interest - */ - this.emit('timeOfInterest', this.toi); - } - - return this.toi; - } + return this.toi; + } } export default GlobalTimeContext; diff --git a/src/api/time/IndependentTimeContext.js b/src/api/time/IndependentTimeContext.js index 50de217556..c118574209 100644 --- a/src/api/time/IndependentTimeContext.js +++ b/src/api/time/IndependentTimeContext.js @@ -20,252 +20,250 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import TimeContext, { TIME_CONTEXT_EVENTS } from "./TimeContext"; +import TimeContext, { TIME_CONTEXT_EVENTS } from './TimeContext'; /** * The IndependentTimeContext handles getting and setting time of the openmct application in general. * Views will use the GlobalTimeContext unless they specify an alternate/independent time context here. */ class IndependentTimeContext extends TimeContext { - constructor(openmct, globalTimeContext, objectPath) { - super(); - this.openmct = openmct; - this.unlisteners = []; - this.globalTimeContext = globalTimeContext; - // We always start with the global time context. - // This upstream context will be undefined when an independent time context is added later. - this.upstreamTimeContext = this.globalTimeContext; - this.objectPath = objectPath; - this.refreshContext = this.refreshContext.bind(this); - this.resetContext = this.resetContext.bind(this); - this.removeIndependentContext = this.removeIndependentContext.bind(this); + constructor(openmct, globalTimeContext, objectPath) { + super(); + this.openmct = openmct; + this.unlisteners = []; + this.globalTimeContext = globalTimeContext; + // We always start with the global time context. + // This upstream context will be undefined when an independent time context is added later. + this.upstreamTimeContext = this.globalTimeContext; + this.objectPath = objectPath; + this.refreshContext = this.refreshContext.bind(this); + this.resetContext = this.resetContext.bind(this); + this.removeIndependentContext = this.removeIndependentContext.bind(this); - this.refreshContext(); + this.refreshContext(); - this.globalTimeContext.on('refreshContext', this.refreshContext); - this.globalTimeContext.on('removeOwnContext', this.removeIndependentContext); + this.globalTimeContext.on('refreshContext', this.refreshContext); + this.globalTimeContext.on('removeOwnContext', this.removeIndependentContext); + } + + bounds(newBounds) { + if (this.upstreamTimeContext) { + return this.upstreamTimeContext.bounds(...arguments); + } else { + return super.bounds(...arguments); + } + } + + tick(timestamp) { + if (this.upstreamTimeContext) { + return this.upstreamTimeContext.tick(...arguments); + } else { + return super.tick(...arguments); + } + } + + clockOffsets(offsets) { + if (this.upstreamTimeContext) { + return this.upstreamTimeContext.clockOffsets(...arguments); + } else { + return super.clockOffsets(...arguments); + } + } + + stopClock() { + if (this.upstreamTimeContext) { + this.upstreamTimeContext.stopClock(); + } else { + super.stopClock(); + } + } + + timeOfInterest(newTOI) { + return this.globalTimeContext.timeOfInterest(...arguments); + } + + timeSystem(timeSystemOrKey, bounds) { + return this.globalTimeContext.timeSystem(...arguments); + } + + /** + * Set the active clock. Tick source will be immediately subscribed to + * and ticking will begin. Offsets from 'now' must also be provided. A clock + * can be unset by calling {@link stopClock}. + * + * @param {Clock || string} keyOrClock The clock to activate, or its key + * @param {ClockOffsets} offsets on each tick these will be used to calculate + * the start and end bounds. This maintains a sliding time window of a fixed + * width that automatically updates. + * @fires module:openmct.TimeAPI~clock + * @return {Clock} the currently active clock; + */ + clock(keyOrClock, offsets) { + if (this.upstreamTimeContext) { + return this.upstreamTimeContext.clock(...arguments); } - bounds(newBounds) { - if (this.upstreamTimeContext) { - return this.upstreamTimeContext.bounds(...arguments); - } else { - return super.bounds(...arguments); + if (arguments.length === 2) { + let clock; + + if (typeof keyOrClock === 'string') { + clock = this.globalTimeContext.clocks.get(keyOrClock); + if (clock === undefined) { + throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?"; } - } - - tick(timestamp) { - if (this.upstreamTimeContext) { - return this.upstreamTimeContext.tick(...arguments); - } else { - return super.tick(...arguments); + } else if (typeof keyOrClock === 'object') { + clock = keyOrClock; + if (!this.globalTimeContext.clocks.has(clock.key)) { + throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?"; } + } + + const previousClock = this.activeClock; + if (previousClock !== undefined) { + previousClock.off('tick', this.tick); + } + + this.activeClock = clock; + + /** + * The active clock has changed. Clock can be unset by calling {@link stopClock} + * @event clock + * @memberof module:openmct.TimeAPI~ + * @property {Clock} clock The newly activated clock, or undefined + * if the system is no longer following a clock source + */ + this.emit('clock', this.activeClock); + + if (this.activeClock !== undefined) { + this.clockOffsets(offsets); + this.activeClock.on('tick', this.tick); + } + } else if (arguments.length === 1) { + throw 'When setting the clock, clock offsets must also be provided'; } - clockOffsets(offsets) { - if (this.upstreamTimeContext) { - return this.upstreamTimeContext.clockOffsets(...arguments); - } else { - return super.clockOffsets(...arguments); - } - } + return this.activeClock; + } - stopClock() { - if (this.upstreamTimeContext) { - this.upstreamTimeContext.stopClock(); - } else { - super.stopClock(); - } - } - - timeOfInterest(newTOI) { - return this.globalTimeContext.timeOfInterest(...arguments); - } - - timeSystem(timeSystemOrKey, bounds) { - return this.globalTimeContext.timeSystem(...arguments); - } - - /** - * Set the active clock. Tick source will be immediately subscribed to - * and ticking will begin. Offsets from 'now' must also be provided. A clock - * can be unset by calling {@link stopClock}. - * - * @param {Clock || string} keyOrClock The clock to activate, or its key - * @param {ClockOffsets} offsets on each tick these will be used to calculate - * the start and end bounds. This maintains a sliding time window of a fixed - * width that automatically updates. - * @fires module:openmct.TimeAPI~clock - * @return {Clock} the currently active clock; - */ - clock(keyOrClock, offsets) { - if (this.upstreamTimeContext) { - return this.upstreamTimeContext.clock(...arguments); - } - - if (arguments.length === 2) { - let clock; - - if (typeof keyOrClock === 'string') { - clock = this.globalTimeContext.clocks.get(keyOrClock); - if (clock === undefined) { - throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?"; - } - } else if (typeof keyOrClock === 'object') { - clock = keyOrClock; - if (!this.globalTimeContext.clocks.has(clock.key)) { - throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?"; - } - } - - const previousClock = this.activeClock; - if (previousClock !== undefined) { - previousClock.off("tick", this.tick); - } - - this.activeClock = clock; - - /** - * The active clock has changed. Clock can be unset by calling {@link stopClock} - * @event clock - * @memberof module:openmct.TimeAPI~ - * @property {Clock} clock The newly activated clock, or undefined - * if the system is no longer following a clock source - */ - this.emit("clock", this.activeClock); - - if (this.activeClock !== undefined) { - this.clockOffsets(offsets); - this.activeClock.on("tick", this.tick); - } - - } else if (arguments.length === 1) { - throw "When setting the clock, clock offsets must also be provided"; - } - - return this.activeClock; - } - - /** - * Causes this time context to follow another time context (either the global context, or another upstream time context) - * This allows views to have their own time context which points to the appropriate upstream context as necessary, achieving nesting. - */ - followTimeContext() { - this.stopFollowingTimeContext(); - if (this.upstreamTimeContext) { - TIME_CONTEXT_EVENTS.forEach((eventName) => { - const thisTimeContext = this; - this.upstreamTimeContext.on(eventName, passthrough); - this.unlisteners.push(() => { - thisTimeContext.upstreamTimeContext.off(eventName, passthrough); - }); - function passthrough() { - thisTimeContext.emit(eventName, ...arguments); - } - }); - - } - } - - /** - * Stops following any upstream time context - */ - stopFollowingTimeContext() { - this.unlisteners.forEach(unlisten => unlisten()); - this.unlisteners = []; - } - - resetContext() { - if (this.upstreamTimeContext) { - this.stopFollowingTimeContext(); - this.upstreamTimeContext = undefined; - } - } - - /** - * Refresh the time context, following any upstream time contexts as necessary - */ - refreshContext(viewKey) { - const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier); - if (viewKey && key === viewKey) { - return; - } - - //this is necessary as the upstream context gets reassigned after this - this.stopFollowingTimeContext(); - - this.upstreamTimeContext = this.getUpstreamContext(); - this.followTimeContext(); - - // Emit bounds so that views that are changing context get the upstream bounds - this.emit('bounds', this.bounds()); - } - - hasOwnContext() { - return this.upstreamTimeContext === undefined; - } - - getUpstreamContext() { - // If a view has an independent context, don't return an upstream context - // Be aware that when a new independent time context is created, we assign the global context as default - if (this.hasOwnContext()) { - return undefined; - } - - let timeContext = this.globalTimeContext; - this.objectPath.some((item, index) => { - const key = this.openmct.objects.makeKeyString(item.identifier); - // we're only interested in parents, not self, so index > 0 - const itemContext = this.globalTimeContext.independentContexts.get(key); - if (index > 0 && itemContext && itemContext.hasOwnContext()) { - //upstream time context - timeContext = itemContext; - - return true; - } - - return false; + /** + * Causes this time context to follow another time context (either the global context, or another upstream time context) + * This allows views to have their own time context which points to the appropriate upstream context as necessary, achieving nesting. + */ + followTimeContext() { + this.stopFollowingTimeContext(); + if (this.upstreamTimeContext) { + TIME_CONTEXT_EVENTS.forEach((eventName) => { + const thisTimeContext = this; + this.upstreamTimeContext.on(eventName, passthrough); + this.unlisteners.push(() => { + thisTimeContext.upstreamTimeContext.off(eventName, passthrough); }); - - return timeContext; - } - - /** - * Set the time context of a view to follow any upstream time contexts as necessary (defaulting to the global context) - * This needs to be separate from refreshContext - */ - removeIndependentContext(viewKey) { - const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier); - if (viewKey && key === viewKey) { - //this is necessary as the upstream context gets reassigned after this - this.stopFollowingTimeContext(); - - let timeContext = this.globalTimeContext; - - this.objectPath.some((item, index) => { - const objectKey = this.openmct.objects.makeKeyString(item.identifier); - // we're only interested in any parents, not self, so index > 0 - const itemContext = this.globalTimeContext.independentContexts.get(objectKey); - if (index > 0 && itemContext && itemContext.hasOwnContext()) { - //upstream time context - timeContext = itemContext; - - return true; - } - - return false; - }); - - this.upstreamTimeContext = timeContext; - - this.followTimeContext(); - - // Emit bounds so that views that are changing context get the upstream bounds - this.emit('bounds', this.bounds()); - // now that the view's context is set, tell others to check theirs in case they were following this view's context. - this.globalTimeContext.emit('refreshContext', viewKey); + function passthrough() { + thisTimeContext.emit(eventName, ...arguments); } + }); } + } + + /** + * Stops following any upstream time context + */ + stopFollowingTimeContext() { + this.unlisteners.forEach((unlisten) => unlisten()); + this.unlisteners = []; + } + + resetContext() { + if (this.upstreamTimeContext) { + this.stopFollowingTimeContext(); + this.upstreamTimeContext = undefined; + } + } + + /** + * Refresh the time context, following any upstream time contexts as necessary + */ + refreshContext(viewKey) { + const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier); + if (viewKey && key === viewKey) { + return; + } + + //this is necessary as the upstream context gets reassigned after this + this.stopFollowingTimeContext(); + + this.upstreamTimeContext = this.getUpstreamContext(); + this.followTimeContext(); + + // Emit bounds so that views that are changing context get the upstream bounds + this.emit('bounds', this.bounds()); + } + + hasOwnContext() { + return this.upstreamTimeContext === undefined; + } + + getUpstreamContext() { + // If a view has an independent context, don't return an upstream context + // Be aware that when a new independent time context is created, we assign the global context as default + if (this.hasOwnContext()) { + return undefined; + } + + let timeContext = this.globalTimeContext; + this.objectPath.some((item, index) => { + const key = this.openmct.objects.makeKeyString(item.identifier); + // we're only interested in parents, not self, so index > 0 + const itemContext = this.globalTimeContext.independentContexts.get(key); + if (index > 0 && itemContext && itemContext.hasOwnContext()) { + //upstream time context + timeContext = itemContext; + + return true; + } + + return false; + }); + + return timeContext; + } + + /** + * Set the time context of a view to follow any upstream time contexts as necessary (defaulting to the global context) + * This needs to be separate from refreshContext + */ + removeIndependentContext(viewKey) { + const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier); + if (viewKey && key === viewKey) { + //this is necessary as the upstream context gets reassigned after this + this.stopFollowingTimeContext(); + + let timeContext = this.globalTimeContext; + + this.objectPath.some((item, index) => { + const objectKey = this.openmct.objects.makeKeyString(item.identifier); + // we're only interested in any parents, not self, so index > 0 + const itemContext = this.globalTimeContext.independentContexts.get(objectKey); + if (index > 0 && itemContext && itemContext.hasOwnContext()) { + //upstream time context + timeContext = itemContext; + + return true; + } + + return false; + }); + + this.upstreamTimeContext = timeContext; + + this.followTimeContext(); + + // Emit bounds so that views that are changing context get the upstream bounds + this.emit('bounds', this.bounds()); + // now that the view's context is set, tell others to check theirs in case they were following this view's context. + this.globalTimeContext.emit('refreshContext', viewKey); + } + } } export default IndependentTimeContext; diff --git a/src/api/time/TimeAPI.js b/src/api/time/TimeAPI.js index 0108982330..617954db3f 100644 --- a/src/api/time/TimeAPI.js +++ b/src/api/time/TimeAPI.js @@ -20,189 +20,190 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import GlobalTimeContext from "./GlobalTimeContext"; -import IndependentTimeContext from "@/api/time/IndependentTimeContext"; +import GlobalTimeContext from './GlobalTimeContext'; +import IndependentTimeContext from '@/api/time/IndependentTimeContext'; /** -* The public API for setting and querying the temporal state of the -* application. The concept of time is integral to Open MCT, and at least -* one {@link TimeSystem}, as well as some default time bounds must be -* registered and enabled via {@link TimeAPI.addTimeSystem} and -* {@link TimeAPI.timeSystem} respectively for Open MCT to work. -* -* Time-sensitive views will typically respond to changes to bounds or other -* properties of the time conductor and update the data displayed based on -* the temporal state of the application. The current time bounds are also -* used in queries for historical data. -* -* The TimeAPI extends the GlobalTimeContext which in turn extends the TimeContext/EventEmitter class. A number of events are -* fired when properties of the time conductor change, which are documented -* below. -* -* @interface -* @memberof module:openmct -*/ + * The public API for setting and querying the temporal state of the + * application. The concept of time is integral to Open MCT, and at least + * one {@link TimeSystem}, as well as some default time bounds must be + * registered and enabled via {@link TimeAPI.addTimeSystem} and + * {@link TimeAPI.timeSystem} respectively for Open MCT to work. + * + * Time-sensitive views will typically respond to changes to bounds or other + * properties of the time conductor and update the data displayed based on + * the temporal state of the application. The current time bounds are also + * used in queries for historical data. + * + * The TimeAPI extends the GlobalTimeContext which in turn extends the TimeContext/EventEmitter class. A number of events are + * fired when properties of the time conductor change, which are documented + * below. + * + * @interface + * @memberof module:openmct + */ class TimeAPI extends GlobalTimeContext { - constructor(openmct) { - super(); - this.openmct = openmct; - this.independentContexts = new Map(); + constructor(openmct) { + super(); + this.openmct = openmct; + this.independentContexts = new Map(); + } + + /** + * A TimeSystem provides meaning to the values returned by the TimeAPI. Open + * MCT supports multiple different types of time values, although all are + * intrinsically represented by numbers, the meaning of those numbers can + * differ depending on context. + * + * A default time system is provided by Open MCT in the form of the {@link UTCTimeSystem}, + * which represents integer values as ms in the Unix epoch. An example of + * another time system might be "sols" for a Martian mission. TimeSystems do + * not address the issue of converting between time systems. + * + * @typedef {object} TimeSystem + * @property {string} key A unique identifier + * @property {string} name A human-readable descriptor + * @property {string} [cssClass] Specify a css class defining an icon for + * this time system. This will be visible next to the time system in the + * menu in the Time Conductor + * @property {string} timeFormat The key of a format to use when displaying + * discrete timestamps from this time system + * @property {string} [durationFormat] The key of a format to use when + * displaying a duration or relative span of time in this time system. + */ + + /** + * Register a new time system. Once registered it can activated using + * {@link TimeAPI.timeSystem}, and can be referenced via its key in [Time Conductor configuration](@link https://github.com/nasa/openmct/blob/master/API.md#time-conductor). + * @memberof module:openmct.TimeAPI# + * @param {TimeSystem} timeSystem A time system object. + */ + addTimeSystem(timeSystem) { + this.timeSystems.set(timeSystem.key, timeSystem); + } + + /** + * @returns {TimeSystem[]} + */ + getAllTimeSystems() { + return Array.from(this.timeSystems.values()); + } + + /** + * Clocks provide a timing source that is used to + * automatically update the time bounds of the data displayed in Open MCT. + * + * @typedef {object} Clock + * @memberof openmct.timeAPI + * @property {string} key A unique identifier + * @property {string} name A human-readable name. The name will be used to + * represent this clock in the Time Conductor UI + * @property {string} description A longer description, ideally identifying + * what the clock ticks on. + * @property {function} currentValue Returns the last value generated by a tick, or a default value + * if no ticking has yet occurred + * @see {LocalClock} + */ + + /** + * Register a new Clock. + * @memberof module:openmct.TimeAPI# + * @param {Clock} clock + */ + addClock(clock) { + this.clocks.set(clock.key, clock); + } + + /** + * @memberof module:openmct.TimeAPI# + * @returns {Clock[]} + * @memberof module:openmct.TimeAPI# + */ + getAllClocks() { + return Array.from(this.clocks.values()); + } + + /** + * Get or set an independent time context which follows the TimeAPI timeSystem, + * but with different offsets for a given domain object + * @param {key | string} key The identifier key of the domain object these offsets are set for + * @param {ClockOffsets | TimeBounds} value This maintains a sliding time window of a fixed width that automatically updates + * @param {key | string} clockKey the real time clock key currently in use + * @memberof module:openmct.TimeAPI# + * @method addIndependentTimeContext + */ + addIndependentContext(key, value, clockKey) { + let timeContext = this.getIndependentContext(key); + //stop following upstream time context since the view has it's own + timeContext.resetContext(); + + if (clockKey) { + timeContext.clock(clockKey, value); + } else { + timeContext.stopClock(); + timeContext.bounds(value); } - /** - * A TimeSystem provides meaning to the values returned by the TimeAPI. Open - * MCT supports multiple different types of time values, although all are - * intrinsically represented by numbers, the meaning of those numbers can - * differ depending on context. - * - * A default time system is provided by Open MCT in the form of the {@link UTCTimeSystem}, - * which represents integer values as ms in the Unix epoch. An example of - * another time system might be "sols" for a Martian mission. TimeSystems do - * not address the issue of converting between time systems. - * - * @typedef {object} TimeSystem - * @property {string} key A unique identifier - * @property {string} name A human-readable descriptor - * @property {string} [cssClass] Specify a css class defining an icon for - * this time system. This will be visible next to the time system in the - * menu in the Time Conductor - * @property {string} timeFormat The key of a format to use when displaying - * discrete timestamps from this time system - * @property {string} [durationFormat] The key of a format to use when - * displaying a duration or relative span of time in this time system. - */ + // Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context + this.emit('refreshContext', key); - /** - * Register a new time system. Once registered it can activated using - * {@link TimeAPI.timeSystem}, and can be referenced via its key in [Time Conductor configuration](@link https://github.com/nasa/openmct/blob/master/API.md#time-conductor). - * @memberof module:openmct.TimeAPI# - * @param {TimeSystem} timeSystem A time system object. - */ - addTimeSystem(timeSystem) { - this.timeSystems.set(timeSystem.key, timeSystem); + return () => { + //follow any upstream time context + this.emit('removeOwnContext', key); + }; + } + + /** + * Get the independent time context which follows the TimeAPI timeSystem, + * but with different offsets. + * @param {key | string} key The identifier key of the domain object these offsets + * @memberof module:openmct.TimeAPI# + * @method getIndependentTimeContext + */ + getIndependentContext(key) { + return this.independentContexts.get(key); + } + + /** + * Get the a timeContext for a view based on it's objectPath. If there is any object in the objectPath with an independent time context, it will be returned. + * Otherwise, the global time context will be returned. + * @param { Array } objectPath The view's objectPath + * @memberof module:openmct.TimeAPI# + * @method getContextForView + */ + getContextForView(objectPath) { + if (!objectPath || !Array.isArray(objectPath)) { + throw new Error('No objectPath provided'); } - /** - * @returns {TimeSystem[]} - */ - getAllTimeSystems() { - return Array.from(this.timeSystems.values()); + const viewKey = + objectPath.length && this.openmct.objects.makeKeyString(objectPath[0].identifier); + + if (!viewKey) { + // Return the global time context + return this; } - /** - * Clocks provide a timing source that is used to - * automatically update the time bounds of the data displayed in Open MCT. - * - * @typedef {object} Clock - * @memberof openmct.timeAPI - * @property {string} key A unique identifier - * @property {string} name A human-readable name. The name will be used to - * represent this clock in the Time Conductor UI - * @property {string} description A longer description, ideally identifying - * what the clock ticks on. - * @property {function} currentValue Returns the last value generated by a tick, or a default value - * if no ticking has yet occurred - * @see {LocalClock} - */ + 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); - /** - * Register a new Clock. - * @memberof module:openmct.TimeAPI# - * @param {Clock} clock - */ - addClock(clock) { - this.clocks.set(clock.key, clock); + 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); + } } - /** - * @memberof module:openmct.TimeAPI# - * @returns {Clock[]} - * @memberof module:openmct.TimeAPI# - */ - getAllClocks() { - return Array.from(this.clocks.values()); - } - - /** - * Get or set an independent time context which follows the TimeAPI timeSystem, - * but with different offsets for a given domain object - * @param {key | string} key The identifier key of the domain object these offsets are set for - * @param {ClockOffsets | TimeBounds} value This maintains a sliding time window of a fixed width that automatically updates - * @param {key | string} clockKey the real time clock key currently in use - * @memberof module:openmct.TimeAPI# - * @method addIndependentTimeContext - */ - addIndependentContext(key, value, clockKey) { - let timeContext = this.getIndependentContext(key); - //stop following upstream time context since the view has it's own - timeContext.resetContext(); - - if (clockKey) { - timeContext.clock(clockKey, value); - } else { - timeContext.stopClock(); - timeContext.bounds(value); - } - - // Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context - this.emit('refreshContext', key); - - return () => { - //follow any upstream time context - this.emit('removeOwnContext', key); - }; - } - - /** - * Get the independent time context which follows the TimeAPI timeSystem, - * but with different offsets. - * @param {key | string} key The identifier key of the domain object these offsets - * @memberof module:openmct.TimeAPI# - * @method getIndependentTimeContext - */ - getIndependentContext(key) { - return this.independentContexts.get(key); - } - - /** - * Get the a timeContext for a view based on it's objectPath. If there is any object in the objectPath with an independent time context, it will be returned. - * Otherwise, the global time context will be returned. - * @param { Array } objectPath The view's objectPath - * @memberof module:openmct.TimeAPI# - * @method getContextForView - */ - 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) { - // Return the global time context - return this; - } - - 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); - - 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); - } - } - - return viewTimeContext; - } + return viewTimeContext; + } } export default TimeAPI; diff --git a/src/api/time/TimeAPISpec.js b/src/api/time/TimeAPISpec.js index 6006ba5f5b..97300157e2 100644 --- a/src/api/time/TimeAPISpec.js +++ b/src/api/time/TimeAPISpec.js @@ -19,262 +19,241 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import TimeAPI from "./TimeAPI"; -import {createOpenMct} from "utils/testing"; +import TimeAPI from './TimeAPI'; +import { createOpenMct } from 'utils/testing'; -describe("The Time API", function () { - let api; - let timeSystemKey; - let timeSystem; - let clockKey; - let clock; - let bounds; - let eventListener; - let toi; - let openmct; +describe('The Time API', function () { + let api; + let timeSystemKey; + let timeSystem; + let clockKey; + let clock; + let bounds; + let eventListener; + let toi; + let openmct; + + beforeEach(function () { + openmct = createOpenMct(); + api = new TimeAPI(openmct); + timeSystemKey = 'timeSystemKey'; + timeSystem = { key: timeSystemKey }; + clockKey = 'someClockKey'; + clock = jasmine.createSpyObj('clock', ['on', 'off', 'currentValue']); + clock.currentValue.and.returnValue(100); + clock.key = clockKey; + bounds = { + start: 0, + end: 1 + }; + eventListener = jasmine.createSpy('eventListener'); + toi = 111; + }); + + it('Supports setting and querying of time of interest', function () { + expect(api.timeOfInterest()).not.toBe(toi); + api.timeOfInterest(toi); + expect(api.timeOfInterest()).toBe(toi); + }); + + it('Allows setting of valid bounds', function () { + bounds = { + start: 0, + end: 1 + }; + expect(api.bounds()).not.toBe(bounds); + expect(api.bounds.bind(api, bounds)).not.toThrow(); + expect(api.bounds()).toEqual(bounds); + }); + + it('Disallows setting of invalid bounds', function () { + bounds = { + start: 1, + end: 0 + }; + expect(api.bounds()).not.toEqual(bounds); + expect(api.bounds.bind(api, bounds)).toThrow(); + expect(api.bounds()).not.toEqual(bounds); + + bounds = { start: 1 }; + expect(api.bounds()).not.toEqual(bounds); + expect(api.bounds.bind(api, bounds)).toThrow(); + expect(api.bounds()).not.toEqual(bounds); + }); + + it('Allows setting of previously registered time system with bounds', function () { + api.addTimeSystem(timeSystem); + expect(api.timeSystem()).not.toBe(timeSystem); + expect(function () { + api.timeSystem(timeSystem, bounds); + }).not.toThrow(); + expect(api.timeSystem()).toBe(timeSystem); + }); + + it('Disallows setting of time system without bounds', function () { + api.addTimeSystem(timeSystem); + expect(api.timeSystem()).not.toBe(timeSystem); + expect(function () { + api.timeSystem(timeSystemKey); + }).toThrow(); + expect(api.timeSystem()).not.toBe(timeSystem); + }); + + it('allows setting of timesystem without bounds with clock', function () { + api.addTimeSystem(timeSystem); + api.addClock(clock); + api.clock(clockKey, { + start: 0, + end: 1 + }); + expect(api.timeSystem()).not.toBe(timeSystem); + expect(function () { + api.timeSystem(timeSystemKey); + }).not.toThrow(); + expect(api.timeSystem()).toBe(timeSystem); + }); + + it('Emits an event when time system changes', function () { + api.addTimeSystem(timeSystem); + expect(eventListener).not.toHaveBeenCalled(); + api.on('timeSystem', eventListener); + api.timeSystem(timeSystemKey, bounds); + expect(eventListener).toHaveBeenCalledWith(timeSystem); + }); + + it('Emits an event when time of interest changes', function () { + expect(eventListener).not.toHaveBeenCalled(); + api.on('timeOfInterest', eventListener); + api.timeOfInterest(toi); + expect(eventListener).toHaveBeenCalledWith(toi); + }); + + it('Emits an event when bounds change', function () { + expect(eventListener).not.toHaveBeenCalled(); + api.on('bounds', eventListener); + api.bounds(bounds); + expect(eventListener).toHaveBeenCalledWith(bounds, false); + }); + + it('If bounds are set and TOI lies inside them, do not change TOI', function () { + api.timeOfInterest(6); + api.bounds({ + start: 1, + end: 10 + }); + expect(api.timeOfInterest()).toEqual(6); + }); + + it('If bounds are set and TOI lies outside them, reset TOI', function () { + api.timeOfInterest(11); + api.bounds({ + start: 1, + end: 10 + }); + expect(api.timeOfInterest()).toBeUndefined(); + }); + + it('Maintains delta during tick', function () {}); + + it('Allows registered time system to be activated', function () {}); + + it('Allows a registered tick source to be activated', function () { + const mockTickSource = jasmine.createSpyObj('mockTickSource', ['on', 'off', 'currentValue']); + mockTickSource.key = 'mockTickSource'; + }); + + describe(' when enabling a tick source', function () { + let mockTickSource; + let anotherMockTickSource; + const mockOffsets = { + start: 0, + end: 1 + }; beforeEach(function () { - openmct = createOpenMct(); - api = new TimeAPI(openmct); - timeSystemKey = "timeSystemKey"; - timeSystem = {key: timeSystemKey}; - clockKey = "someClockKey"; - clock = jasmine.createSpyObj("clock", [ - "on", - "off", - "currentValue" - ]); - clock.currentValue.and.returnValue(100); - clock.key = clockKey; - bounds = { - start: 0, - end: 1 - }; - eventListener = jasmine.createSpy("eventListener"); - toi = 111; + mockTickSource = jasmine.createSpyObj('clock', ['on', 'off', 'currentValue']); + mockTickSource.currentValue.and.returnValue(10); + mockTickSource.key = 'mts'; + + anotherMockTickSource = jasmine.createSpyObj('clock', ['on', 'off', 'currentValue']); + anotherMockTickSource.key = 'amts'; + anotherMockTickSource.currentValue.and.returnValue(10); + + api.addClock(mockTickSource); + api.addClock(anotherMockTickSource); }); - it("Supports setting and querying of time of interest", function () { - expect(api.timeOfInterest()).not.toBe(toi); - api.timeOfInterest(toi); - expect(api.timeOfInterest()).toBe(toi); + it('sets bounds based on current value', function () { + api.clock('mts', mockOffsets); + expect(api.bounds()).toEqual({ + start: 10, + end: 11 + }); }); - it("Allows setting of valid bounds", function () { - bounds = { - start: 0, - end: 1 - }; - expect(api.bounds()).not.toBe(bounds); - expect(api.bounds.bind(api, bounds)).not.toThrow(); - expect(api.bounds()).toEqual(bounds); + it('a new tick listener is registered', function () { + api.clock('mts', mockOffsets); + expect(mockTickSource.on).toHaveBeenCalledWith('tick', jasmine.any(Function)); }); - it("Disallows setting of invalid bounds", function () { - bounds = { - start: 1, - end: 0 - }; - expect(api.bounds()).not.toEqual(bounds); - expect(api.bounds.bind(api, bounds)).toThrow(); - expect(api.bounds()).not.toEqual(bounds); - - bounds = {start: 1}; - expect(api.bounds()).not.toEqual(bounds); - expect(api.bounds.bind(api, bounds)).toThrow(); - expect(api.bounds()).not.toEqual(bounds); + it('listener of existing tick source is reregistered', function () { + api.clock('mts', mockOffsets); + api.clock('amts', mockOffsets); + expect(mockTickSource.off).toHaveBeenCalledWith('tick', jasmine.any(Function)); }); - it("Allows setting of previously registered time system with bounds", function () { - api.addTimeSystem(timeSystem); - expect(api.timeSystem()).not.toBe(timeSystem); - expect(function () { - api.timeSystem(timeSystem, bounds); - }).not.toThrow(); - expect(api.timeSystem()).toBe(timeSystem); + it('Allows the active clock to be set and unset', function () { + expect(api.clock()).toBeUndefined(); + api.clock('mts', mockOffsets); + expect(api.clock()).toBeDefined(); + api.stopClock(); + expect(api.clock()).toBeUndefined(); }); - it("Disallows setting of time system without bounds", function () { - api.addTimeSystem(timeSystem); - expect(api.timeSystem()).not.toBe(timeSystem); - expect(function () { - api.timeSystem(timeSystemKey); - }).toThrow(); - expect(api.timeSystem()).not.toBe(timeSystem); + it('Provides a default time context', () => { + const timeContext = api.getContextForView([]); + expect(timeContext).not.toBe(null); }); - it("allows setting of timesystem without bounds with clock", function () { - api.addTimeSystem(timeSystem); - api.addClock(clock); - api.clock(clockKey, { - start: 0, - end: 1 - }); - expect(api.timeSystem()).not.toBe(timeSystem); - expect(function () { - api.timeSystem(timeSystemKey); - }).not.toThrow(); - expect(api.timeSystem()).toBe(timeSystem); - + it('Without a clock, is in fixed time mode', () => { + const timeContext = api.getContextForView([]); + expect(timeContext.isRealTime()).toBe(false); }); - it("Emits an event when time system changes", function () { - api.addTimeSystem(timeSystem); - expect(eventListener).not.toHaveBeenCalled(); - api.on("timeSystem", eventListener); - api.timeSystem(timeSystemKey, bounds); - expect(eventListener).toHaveBeenCalledWith(timeSystem); + it('Provided a clock, is in real-time mode', () => { + const timeContext = api.getContextForView([]); + timeContext.clock('mts', { + start: 0, + end: 1 + }); + expect(timeContext.isRealTime()).toBe(true); }); + }); - it("Emits an event when time of interest changes", function () { - expect(eventListener).not.toHaveBeenCalled(); - api.on("timeOfInterest", eventListener); - api.timeOfInterest(toi); - expect(eventListener).toHaveBeenCalledWith(toi); - }); + it('on tick, observes offsets, and indicates tick in bounds callback', function () { + const mockTickSource = jasmine.createSpyObj('clock', ['on', 'off', 'currentValue']); + mockTickSource.currentValue.and.returnValue(100); + let tickCallback; + const boundsCallback = jasmine.createSpy('boundsCallback'); + const clockOffsets = { + start: -100, + end: 100 + }; + mockTickSource.key = 'mts'; - it("Emits an event when bounds change", function () { - expect(eventListener).not.toHaveBeenCalled(); - api.on("bounds", eventListener); - api.bounds(bounds); - expect(eventListener).toHaveBeenCalledWith(bounds, false); - }); + api.addClock(mockTickSource); + api.clock('mts', clockOffsets); - it("If bounds are set and TOI lies inside them, do not change TOI", function () { - api.timeOfInterest(6); - api.bounds({ - start: 1, - end: 10 - }); - expect(api.timeOfInterest()).toEqual(6); - }); + api.on('bounds', boundsCallback); - it("If bounds are set and TOI lies outside them, reset TOI", function () { - api.timeOfInterest(11); - api.bounds({ - start: 1, - end: 10 - }); - expect(api.timeOfInterest()).toBeUndefined(); - }); - - it("Maintains delta during tick", function () { - }); - - it("Allows registered time system to be activated", function () { - }); - - it("Allows a registered tick source to be activated", function () { - const mockTickSource = jasmine.createSpyObj("mockTickSource", [ - "on", - "off", - "currentValue" - ]); - mockTickSource.key = 'mockTickSource'; - }); - - describe(" when enabling a tick source", function () { - let mockTickSource; - let anotherMockTickSource; - const mockOffsets = { - start: 0, - end: 1 - }; - - beforeEach(function () { - mockTickSource = jasmine.createSpyObj("clock", [ - "on", - "off", - "currentValue" - ]); - mockTickSource.currentValue.and.returnValue(10); - mockTickSource.key = "mts"; - - anotherMockTickSource = jasmine.createSpyObj("clock", [ - "on", - "off", - "currentValue" - ]); - anotherMockTickSource.key = "amts"; - anotherMockTickSource.currentValue.and.returnValue(10); - - api.addClock(mockTickSource); - api.addClock(anotherMockTickSource); - }); - - it("sets bounds based on current value", function () { - api.clock("mts", mockOffsets); - expect(api.bounds()).toEqual({ - start: 10, - end: 11 - }); - }); - - it("a new tick listener is registered", function () { - api.clock("mts", mockOffsets); - expect(mockTickSource.on).toHaveBeenCalledWith("tick", jasmine.any(Function)); - }); - - it("listener of existing tick source is reregistered", function () { - api.clock("mts", mockOffsets); - api.clock("amts", mockOffsets); - expect(mockTickSource.off).toHaveBeenCalledWith("tick", jasmine.any(Function)); - }); - - it("Allows the active clock to be set and unset", function () { - expect(api.clock()).toBeUndefined(); - api.clock("mts", mockOffsets); - expect(api.clock()).toBeDefined(); - api.stopClock(); - expect(api.clock()).toBeUndefined(); - }); - - it('Provides a default time context', () => { - const timeContext = api.getContextForView([]); - expect(timeContext).not.toBe(null); - }); - - it("Without a clock, is in fixed time mode", () => { - const timeContext = api.getContextForView([]); - expect(timeContext.isRealTime()).toBe(false); - }); - - it("Provided a clock, is in real-time mode", () => { - const timeContext = api.getContextForView([]); - timeContext.clock('mts', { - start: 0, - end: 1 - }); - expect(timeContext.isRealTime()).toBe(true); - }); - - }); - - it("on tick, observes offsets, and indicates tick in bounds callback", function () { - const mockTickSource = jasmine.createSpyObj("clock", [ - "on", - "off", - "currentValue" - ]); - mockTickSource.currentValue.and.returnValue(100); - let tickCallback; - const boundsCallback = jasmine.createSpy("boundsCallback"); - const clockOffsets = { - start: -100, - end: 100 - }; - mockTickSource.key = "mts"; - - api.addClock(mockTickSource); - api.clock("mts", clockOffsets); - - api.on("bounds", boundsCallback); - - tickCallback = mockTickSource.on.calls.mostRecent().args[1]; - tickCallback(1000); - expect(boundsCallback).toHaveBeenCalledWith({ - start: 900, - end: 1100 - }, true); - }); + tickCallback = mockTickSource.on.calls.mostRecent().args[1]; + tickCallback(1000); + expect(boundsCallback).toHaveBeenCalledWith( + { + start: 900, + end: 1100 + }, + true + ); + }); }); diff --git a/src/api/time/TimeContext.js b/src/api/time/TimeContext.js index 38c5c5e7a6..cef987e3da 100644 --- a/src/api/time/TimeContext.js +++ b/src/api/time/TimeContext.js @@ -22,357 +22,357 @@ import EventEmitter from 'EventEmitter'; -export const TIME_CONTEXT_EVENTS = [ - 'bounds', - 'clock', - 'timeSystem', - 'clockOffsets' -]; +export const TIME_CONTEXT_EVENTS = ['bounds', 'clock', 'timeSystem', 'clockOffsets']; class TimeContext extends EventEmitter { - constructor() { - super(); + constructor() { + super(); - //The Time System - this.timeSystems = new Map(); + //The Time System + this.timeSystems = new Map(); - this.system = undefined; + this.system = undefined; - this.clocks = new Map(); + this.clocks = new Map(); - this.boundsVal = { - start: undefined, - end: undefined - }; + this.boundsVal = { + start: undefined, + end: undefined + }; - this.activeClock = undefined; - this.offsets = undefined; + this.activeClock = undefined; + this.offsets = undefined; - this.tick = this.tick.bind(this); - } + this.tick = this.tick.bind(this); + } - /** - * Get or set the time system of the TimeAPI. - * @param {TimeSystem | string} timeSystemOrKey - * @param {module:openmct.TimeAPI~TimeConductorBounds} bounds - * @fires module:openmct.TimeAPI~timeSystem - * @returns {TimeSystem} The currently applied time system - * @memberof module:openmct.TimeAPI# - * @method timeSystem - */ - timeSystem(timeSystemOrKey, bounds) { - if (arguments.length >= 1) { - if (arguments.length === 1 && !this.activeClock) { - throw new Error( - "Must specify bounds when changing time system without an active clock." - ); - } + /** + * Get or set the time system of the TimeAPI. + * @param {TimeSystem | string} timeSystemOrKey + * @param {module:openmct.TimeAPI~TimeConductorBounds} bounds + * @fires module:openmct.TimeAPI~timeSystem + * @returns {TimeSystem} The currently applied time system + * @memberof module:openmct.TimeAPI# + * @method timeSystem + */ + timeSystem(timeSystemOrKey, bounds) { + if (arguments.length >= 1) { + if (arguments.length === 1 && !this.activeClock) { + throw new Error('Must specify bounds when changing time system without an active clock.'); + } - let timeSystem; + let timeSystem; - if (timeSystemOrKey === undefined) { - throw "Please provide a time system"; - } + if (timeSystemOrKey === undefined) { + throw 'Please provide a time system'; + } - if (typeof timeSystemOrKey === 'string') { - timeSystem = this.timeSystems.get(timeSystemOrKey); - - if (timeSystem === undefined) { - throw "Unknown time system " + timeSystemOrKey + ". Has it been registered with 'addTimeSystem'?"; - } - } else if (typeof timeSystemOrKey === 'object') { - timeSystem = timeSystemOrKey; - - if (!this.timeSystems.has(timeSystem.key)) { - throw "Unknown time system " + timeSystem.key + ". Has it been registered with 'addTimeSystem'?"; - } - } else { - throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key"; - } - - this.system = timeSystem; - - /** - * The time system used by the time - * conductor has changed. A change in Time System will always be - * followed by a bounds event specifying new query bounds. - * - * @event module:openmct.TimeAPI~timeSystem - * @property {TimeSystem} The value of the currently applied - * Time System - * */ - this.emit('timeSystem', this.system); - if (bounds) { - this.bounds(bounds); - } + if (typeof timeSystemOrKey === 'string') { + timeSystem = this.timeSystems.get(timeSystemOrKey); + if (timeSystem === undefined) { + throw ( + 'Unknown time system ' + + timeSystemOrKey + + ". Has it been registered with 'addTimeSystem'?" + ); } + } else if (typeof timeSystemOrKey === 'object') { + timeSystem = timeSystemOrKey; - return this.system; - } - - /** - * Clock offsets are used to calculate temporal bounds when the system is - * ticking on a clock source. - * - * @typedef {object} ValidationResult - * @property {boolean} valid Result of the validation - true or false. - * @property {string} message An error message if valid is false. - */ - /** - * Validate the given bounds. This can be used for pre-validation of bounds, - * for example by views validating user inputs. - * @param {TimeBounds} bounds The start and end time of the conductor. - * @returns {ValidationResult} A validation error, or true if valid - * @memberof module:openmct.TimeAPI# - * @method validateBounds - */ - validateBounds(bounds) { - if ((bounds.start === undefined) - || (bounds.end === undefined) - || isNaN(bounds.start) - || isNaN(bounds.end) - ) { - return { - valid: false, - message: "Start and end must be specified as integer values" - }; - } else if (bounds.start > bounds.end) { - return { - valid: false, - message: "Specified start date exceeds end bound" - }; + if (!this.timeSystems.has(timeSystem.key)) { + throw ( + 'Unknown time system ' + + timeSystem.key + + ". Has it been registered with 'addTimeSystem'?" + ); } + } else { + throw 'Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key'; + } - return { - valid: true, - message: '' - }; + this.system = timeSystem; + + /** + * The time system used by the time + * conductor has changed. A change in Time System will always be + * followed by a bounds event specifying new query bounds. + * + * @event module:openmct.TimeAPI~timeSystem + * @property {TimeSystem} The value of the currently applied + * Time System + * */ + this.emit('timeSystem', this.system); + if (bounds) { + this.bounds(bounds); + } } - /** - * Get or set the start and end time of the time conductor. Basic validation - * of bounds is performed. - * - * @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds - * @throws {Error} Validation error - * @fires module:openmct.TimeAPI~bounds - * @returns {module:openmct.TimeAPI~TimeConductorBounds} - * @memberof module:openmct.TimeAPI# - * @method bounds - */ - bounds(newBounds) { - if (arguments.length > 0) { - const validationResult = this.validateBounds(newBounds); - if (validationResult.valid !== true) { - throw new Error(validationResult.message); - } + return this.system; + } - //Create a copy to avoid direct mutation of conductor bounds - this.boundsVal = JSON.parse(JSON.stringify(newBounds)); - /** - * The start time, end time, or both have been updated. - * @event bounds - * @memberof module:openmct.TimeAPI~ - * @property {TimeConductorBounds} bounds The newly updated bounds - * @property {boolean} [tick] `true` if the bounds update was due to - * a "tick" event (ie. was an automatic update), false otherwise. - */ - this.emit('bounds', this.boundsVal, false); + /** + * Clock offsets are used to calculate temporal bounds when the system is + * ticking on a clock source. + * + * @typedef {object} ValidationResult + * @property {boolean} valid Result of the validation - true or false. + * @property {string} message An error message if valid is false. + */ + /** + * Validate the given bounds. This can be used for pre-validation of bounds, + * for example by views validating user inputs. + * @param {TimeBounds} bounds The start and end time of the conductor. + * @returns {ValidationResult} A validation error, or true if valid + * @memberof module:openmct.TimeAPI# + * @method validateBounds + */ + validateBounds(bounds) { + if ( + bounds.start === undefined || + bounds.end === undefined || + isNaN(bounds.start) || + isNaN(bounds.end) + ) { + return { + valid: false, + message: 'Start and end must be specified as integer values' + }; + } else if (bounds.start > bounds.end) { + return { + valid: false, + message: 'Specified start date exceeds end bound' + }; + } + + return { + valid: true, + message: '' + }; + } + + /** + * Get or set the start and end time of the time conductor. Basic validation + * of bounds is performed. + * + * @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds + * @throws {Error} Validation error + * @fires module:openmct.TimeAPI~bounds + * @returns {module:openmct.TimeAPI~TimeConductorBounds} + * @memberof module:openmct.TimeAPI# + * @method bounds + */ + bounds(newBounds) { + if (arguments.length > 0) { + const validationResult = this.validateBounds(newBounds); + if (validationResult.valid !== true) { + throw new Error(validationResult.message); + } + + //Create a copy to avoid direct mutation of conductor bounds + this.boundsVal = JSON.parse(JSON.stringify(newBounds)); + /** + * The start time, end time, or both have been updated. + * @event bounds + * @memberof module:openmct.TimeAPI~ + * @property {TimeConductorBounds} bounds The newly updated bounds + * @property {boolean} [tick] `true` if the bounds update was due to + * a "tick" event (ie. was an automatic update), false otherwise. + */ + this.emit('bounds', this.boundsVal, false); + } + + //Return a copy to prevent direct mutation of time conductor bounds. + return JSON.parse(JSON.stringify(this.boundsVal)); + } + + /** + * Validate the given offsets. This can be used for pre-validation of + * offsets, for example by views validating user inputs. + * @param {ClockOffsets} offsets The start and end offsets from a 'now' value. + * @returns { ValidationResult } A validation error, and true/false if valid or not + * @memberof module:openmct.TimeAPI# + * @method validateOffsets + */ + validateOffsets(offsets) { + if ( + offsets.start === undefined || + offsets.end === undefined || + isNaN(offsets.start) || + isNaN(offsets.end) + ) { + return { + valid: false, + message: 'Start and end offsets must be specified as integer values' + }; + } else if (offsets.start >= offsets.end) { + return { + valid: false, + message: 'Specified start offset must be < end offset' + }; + } + + return { + valid: true, + message: '' + }; + } + + /** + * @typedef {Object} TimeBounds + * @property {number} start The start time displayed by the time conductor + * in ms since epoch. Epoch determined by currently active time system + * @property {number} end The end time displayed by the time conductor in ms + * since epoch. + * @memberof module:openmct.TimeAPI~ + */ + + /** + * Clock offsets are used to calculate temporal bounds when the system is + * ticking on a clock source. + * + * @typedef {object} ClockOffsets + * @property {number} start A time span relative to the current value of the + * ticking clock, from which start bounds will be calculated. This value must + * be < 0. When a clock is active, bounds will be calculated automatically + * based on the value provided by the clock, and the defined clock offsets. + * @property {number} end A time span relative to the current value of the + * ticking clock, from which end bounds will be calculated. This value must + * be >= 0. + */ + /** + * Get or set the currently applied clock offsets. If no parameter is provided, + * the current value will be returned. If provided, the new value will be + * used as the new clock offsets. + * @param {ClockOffsets} offsets + * @returns {ClockOffsets} + */ + clockOffsets(offsets) { + if (arguments.length > 0) { + const validationResult = this.validateOffsets(offsets); + if (validationResult.valid !== true) { + throw new Error(validationResult.message); + } + + this.offsets = offsets; + + const currentValue = this.activeClock.currentValue(); + const newBounds = { + start: currentValue + offsets.start, + end: currentValue + offsets.end + }; + + this.bounds(newBounds); + + /** + * Event that is triggered when clock offsets change. + * @event clockOffsets + * @memberof module:openmct.TimeAPI~ + * @property {ClockOffsets} clockOffsets The newly activated clock + * offsets. + */ + this.emit('clockOffsets', offsets); + } + + return this.offsets; + } + + /** + * Stop the currently active clock from ticking, and unset it. This will + * revert all views to showing a static time frame defined by the current + * bounds. + */ + stopClock() { + if (this.activeClock) { + this.clock(undefined, undefined); + } + } + + /** + * Set the active clock. Tick source will be immediately subscribed to + * and ticking will begin. Offsets from 'now' must also be provided. A clock + * can be unset by calling {@link stopClock}. + * + * @param {Clock || string} keyOrClock The clock to activate, or its key + * @param {ClockOffsets} offsets on each tick these will be used to calculate + * the start and end bounds. This maintains a sliding time window of a fixed + * width that automatically updates. + * @fires module:openmct.TimeAPI~clock + * @return {Clock} the currently active clock; + */ + clock(keyOrClock, offsets) { + if (arguments.length === 2) { + let clock; + + if (typeof keyOrClock === 'string') { + clock = this.clocks.get(keyOrClock); + if (clock === undefined) { + throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?"; } - - //Return a copy to prevent direct mutation of time conductor bounds. - return JSON.parse(JSON.stringify(this.boundsVal)); - } - - /** - * Validate the given offsets. This can be used for pre-validation of - * offsets, for example by views validating user inputs. - * @param {ClockOffsets} offsets The start and end offsets from a 'now' value. - * @returns { ValidationResult } A validation error, and true/false if valid or not - * @memberof module:openmct.TimeAPI# - * @method validateOffsets - */ - validateOffsets(offsets) { - if ((offsets.start === undefined) - || (offsets.end === undefined) - || isNaN(offsets.start) - || isNaN(offsets.end) - ) { - return { - valid: false, - message: "Start and end offsets must be specified as integer values" - }; - } else if (offsets.start >= offsets.end) { - return { - valid: false, - message: "Specified start offset must be < end offset" - }; + } else if (typeof keyOrClock === 'object') { + clock = keyOrClock; + if (!this.clocks.has(clock.key)) { + throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?"; } + } - return { - valid: true, - message: '' - }; + const previousClock = this.activeClock; + if (previousClock !== undefined) { + previousClock.off('tick', this.tick); + } + + this.activeClock = clock; + + /** + * The active clock has changed. Clock can be unset by calling {@link stopClock} + * @event clock + * @memberof module:openmct.TimeAPI~ + * @property {Clock} clock The newly activated clock, or undefined + * if the system is no longer following a clock source + */ + this.emit('clock', this.activeClock); + + if (this.activeClock !== undefined) { + this.clockOffsets(offsets); + this.activeClock.on('tick', this.tick); + } + } else if (arguments.length === 1) { + throw 'When setting the clock, clock offsets must also be provided'; } - /** - * @typedef {Object} TimeBounds - * @property {number} start The start time displayed by the time conductor - * in ms since epoch. Epoch determined by currently active time system - * @property {number} end The end time displayed by the time conductor in ms - * since epoch. - * @memberof module:openmct.TimeAPI~ - */ + return this.activeClock; + } - /** - * Clock offsets are used to calculate temporal bounds when the system is - * ticking on a clock source. - * - * @typedef {object} ClockOffsets - * @property {number} start A time span relative to the current value of the - * ticking clock, from which start bounds will be calculated. This value must - * be < 0. When a clock is active, bounds will be calculated automatically - * based on the value provided by the clock, and the defined clock offsets. - * @property {number} end A time span relative to the current value of the - * ticking clock, from which end bounds will be calculated. This value must - * be >= 0. - */ - /** - * Get or set the currently applied clock offsets. If no parameter is provided, - * the current value will be returned. If provided, the new value will be - * used as the new clock offsets. - * @param {ClockOffsets} offsets - * @returns {ClockOffsets} - */ - clockOffsets(offsets) { - if (arguments.length > 0) { - - const validationResult = this.validateOffsets(offsets); - if (validationResult.valid !== true) { - throw new Error(validationResult.message); - } - - this.offsets = offsets; - - const currentValue = this.activeClock.currentValue(); - const newBounds = { - start: currentValue + offsets.start, - end: currentValue + offsets.end - }; - - this.bounds(newBounds); - - /** - * Event that is triggered when clock offsets change. - * @event clockOffsets - * @memberof module:openmct.TimeAPI~ - * @property {ClockOffsets} clockOffsets The newly activated clock - * offsets. - */ - this.emit("clockOffsets", offsets); - } - - return this.offsets; + /** + * Update bounds based on provided time and current offsets + * @param {number} timestamp A time from which bounds will be calculated + * using current offsets. + */ + tick(timestamp) { + if (!this.activeClock) { + return; } - /** - * Stop the currently active clock from ticking, and unset it. This will - * revert all views to showing a static time frame defined by the current - * bounds. - */ - stopClock() { - if (this.activeClock) { - this.clock(undefined, undefined); - } + const newBounds = { + start: timestamp + this.offsets.start, + end: timestamp + this.offsets.end + }; + + this.boundsVal = newBounds; + this.emit('bounds', this.boundsVal, true); + } + + /** + * Checks if this time context is in real-time mode or not. + * @returns {boolean} true if this context is in real-time mode, false if not + */ + isRealTime() { + if (this.clock()) { + return true; } - /** - * Set the active clock. Tick source will be immediately subscribed to - * and ticking will begin. Offsets from 'now' must also be provided. A clock - * can be unset by calling {@link stopClock}. - * - * @param {Clock || string} keyOrClock The clock to activate, or its key - * @param {ClockOffsets} offsets on each tick these will be used to calculate - * the start and end bounds. This maintains a sliding time window of a fixed - * width that automatically updates. - * @fires module:openmct.TimeAPI~clock - * @return {Clock} the currently active clock; - */ - clock(keyOrClock, offsets) { - if (arguments.length === 2) { - let clock; - - if (typeof keyOrClock === 'string') { - clock = this.clocks.get(keyOrClock); - if (clock === undefined) { - throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?"; - } - } else if (typeof keyOrClock === 'object') { - clock = keyOrClock; - if (!this.clocks.has(clock.key)) { - throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?"; - } - } - - const previousClock = this.activeClock; - if (previousClock !== undefined) { - previousClock.off("tick", this.tick); - } - - this.activeClock = clock; - - /** - * The active clock has changed. Clock can be unset by calling {@link stopClock} - * @event clock - * @memberof module:openmct.TimeAPI~ - * @property {Clock} clock The newly activated clock, or undefined - * if the system is no longer following a clock source - */ - this.emit("clock", this.activeClock); - - if (this.activeClock !== undefined) { - this.clockOffsets(offsets); - this.activeClock.on("tick", this.tick); - } - - } else if (arguments.length === 1) { - throw "When setting the clock, clock offsets must also be provided"; - } - - return this.activeClock; - } - - /** - * Update bounds based on provided time and current offsets - * @param {number} timestamp A time from which bounds will be calculated - * using current offsets. - */ - tick(timestamp) { - if (!this.activeClock) { - return; - } - - const newBounds = { - start: timestamp + this.offsets.start, - end: timestamp + this.offsets.end - }; - - this.boundsVal = newBounds; - this.emit('bounds', this.boundsVal, true); - } - - /** - * Checks if this time context is in real-time mode or not. - * @returns {boolean} true if this context is in real-time mode, false if not - */ - isRealTime() { - if (this.clock()) { - return true; - } - - return false; - } + return false; + } } export default TimeContext; diff --git a/src/api/time/independentTimeAPISpec.js b/src/api/time/independentTimeAPISpec.js index f3da532a7d..9d4489e22b 100644 --- a/src/api/time/independentTimeAPISpec.js +++ b/src/api/time/independentTimeAPISpec.js @@ -20,227 +20,242 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import TimeAPI from "./TimeAPI"; -import {createOpenMct} from "utils/testing"; -describe("The Independent Time API", function () { - let api; - let domainObjectKey; - let clockKey; - let clock; - let bounds; - let independentBounds; - let eventListener; - let openmct; - - beforeEach(function () { - openmct = createOpenMct(); - api = new TimeAPI(openmct); - clockKey = "someClockKey"; - clock = jasmine.createSpyObj("clock", [ - "on", - "off", - "currentValue" - ]); - clock.currentValue.and.returnValue(100); - clock.key = clockKey; - api.addClock(clock); - domainObjectKey = 'test-key'; - bounds = { - start: 0, - end: 1 - }; - api.bounds(bounds); - independentBounds = { - start: 10, - end: 11 - }; - eventListener = jasmine.createSpy("eventListener"); - }); - - it("Creates an independent time context", () => { - let timeContext = api.getContextForView([{ - identifier: { - namespace: '', - key: domainObjectKey - } - }]); - let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); - expect(timeContext.bounds()).toEqual(independentBounds); - destroyTimeContext(); - }); - - it("Gets an independent time context given the objectPath", () => { - let timeContext = api.getContextForView([{ identifier: domainObjectKey }, - { - identifier: { - namespace: '', - key: 'blah' - } - }]); - let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); - expect(timeContext.bounds()).toEqual(independentBounds); - destroyTimeContext(); - }); - - it("defaults to the global time context given the objectPath", () => { - let timeContext = api.getContextForView([{ - identifier: { - namespace: '', - key: 'blah' - } - }]); - expect(timeContext.bounds()).toEqual(bounds); - }); - - it("follows a parent time context given the objectPath", () => { - api.getContextForView([{ - identifier: { - namespace: '', - key: 'blah' - } - }]); - let destroyTimeContext = api.addIndependentContext('blah', independentBounds); - let timeContext = api.getContextForView([{ - identifier: { - namespace: '', - key: domainObjectKey - } - }, { - identifier: { - namespace: '', - key: 'blah' - } - }]); - expect(timeContext.bounds()).toEqual(independentBounds); - destroyTimeContext(); - expect(timeContext.bounds()).toEqual(bounds); - }); - - it("uses an object's independent time context if the parent doesn't have one", () => { - const domainObjectKey2 = `${domainObjectKey}-2`; - const domainObjectKey3 = `${domainObjectKey}-3`; - let timeContext = api.getContextForView([{ - identifier: { - namespace: '', - key: domainObjectKey - } - }]); - let timeContext2 = api.getContextForView([{ - identifier: { - namespace: '', - key: domainObjectKey2 - } - }]); - let timeContext3 = api.getContextForView([{ - identifier: { - namespace: '', - key: domainObjectKey3 - } - }]); - // all bounds follow global time context - expect(timeContext.bounds()).toEqual(bounds); - expect(timeContext2.bounds()).toEqual(bounds); - expect(timeContext3.bounds()).toEqual(bounds); - // only first item has own context - let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); - expect(timeContext.bounds()).toEqual(independentBounds); - expect(timeContext2.bounds()).toEqual(bounds); - expect(timeContext3.bounds()).toEqual(bounds); - // first and second item have own context - let destroyTimeContext2 = api.addIndependentContext(domainObjectKey2, independentBounds); - expect(timeContext.bounds()).toEqual(independentBounds); - expect(timeContext2.bounds()).toEqual(independentBounds); - expect(timeContext3.bounds()).toEqual(bounds); - // all items have own time context - let destroyTimeContext3 = api.addIndependentContext(domainObjectKey3, independentBounds); - expect(timeContext.bounds()).toEqual(independentBounds); - expect(timeContext2.bounds()).toEqual(independentBounds); - expect(timeContext3.bounds()).toEqual(independentBounds); - //remove own contexts one at a time - should revert to global time context - destroyTimeContext(); - expect(timeContext.bounds()).toEqual(bounds); - expect(timeContext2.bounds()).toEqual(independentBounds); - expect(timeContext3.bounds()).toEqual(independentBounds); - destroyTimeContext2(); - expect(timeContext.bounds()).toEqual(bounds); - expect(timeContext2.bounds()).toEqual(bounds); - expect(timeContext3.bounds()).toEqual(independentBounds); - destroyTimeContext3(); - expect(timeContext.bounds()).toEqual(bounds); - expect(timeContext2.bounds()).toEqual(bounds); - expect(timeContext3.bounds()).toEqual(bounds); - }); - - it("Allows setting of valid bounds", function () { - bounds = { - start: 0, - end: 1 - }; - let timeContext = api.getContextForView([{identifier: domainObjectKey}]); - let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); - expect(timeContext.bounds()).not.toEqual(bounds); - timeContext.bounds(bounds); - expect(timeContext.bounds()).toEqual(bounds); - destroyTimeContext(); - }); - - it("Disallows setting of invalid bounds", function () { - bounds = { - start: 1, - end: 0 - }; - - let timeContext = api.getContextForView([{identifier: domainObjectKey}]); - let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); - expect(timeContext.bounds()).not.toBe(bounds); - - expect(timeContext.bounds.bind(timeContext, bounds)).toThrow(); - expect(timeContext.bounds()).not.toEqual(bounds); - - bounds = {start: 1}; - expect(timeContext.bounds()).not.toEqual(bounds); - expect(timeContext.bounds.bind(timeContext, bounds)).toThrow(); - expect(timeContext.bounds()).not.toEqual(bounds); - destroyTimeContext(); - }); - - it("Emits an event when bounds change", function () { - let timeContext = api.getContextForView([{identifier: domainObjectKey}]); - let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); - expect(eventListener).not.toHaveBeenCalled(); - timeContext.on('bounds', eventListener); - timeContext.bounds(bounds); - expect(eventListener).toHaveBeenCalledWith(bounds, false); - destroyTimeContext(); - }); - - it("Emits an event when bounds change on the global context", function () { - let timeContext = api.getContextForView([{identifier: domainObjectKey}]); - expect(eventListener).not.toHaveBeenCalled(); - timeContext.on('bounds', eventListener); - timeContext.bounds(bounds); - expect(eventListener).toHaveBeenCalledWith(bounds, false); - }); - - describe(" when using real time clock", function () { - const mockOffsets = { - start: 10, - end: 11 - }; - - it("Emits an event when bounds change based on current value", function () { - let timeContext = api.getContextForView([{identifier: domainObjectKey}]); - let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); - expect(eventListener).not.toHaveBeenCalled(); - timeContext.clock('someClockKey', mockOffsets); - timeContext.on('bounds', eventListener); - timeContext.tick(10); - expect(eventListener).toHaveBeenCalledWith({ - start: 20, - end: 21 - }, true); - destroyTimeContext(); - }); +import TimeAPI from './TimeAPI'; +import { createOpenMct } from 'utils/testing'; +describe('The Independent Time API', function () { + let api; + let domainObjectKey; + let clockKey; + let clock; + let bounds; + let independentBounds; + let eventListener; + let openmct; + beforeEach(function () { + openmct = createOpenMct(); + api = new TimeAPI(openmct); + clockKey = 'someClockKey'; + clock = jasmine.createSpyObj('clock', ['on', 'off', 'currentValue']); + clock.currentValue.and.returnValue(100); + clock.key = clockKey; + api.addClock(clock); + domainObjectKey = 'test-key'; + bounds = { + start: 0, + end: 1 + }; + api.bounds(bounds); + independentBounds = { + start: 10, + end: 11 + }; + eventListener = jasmine.createSpy('eventListener'); + }); + + it('Creates an independent time context', () => { + let timeContext = api.getContextForView([ + { + identifier: { + namespace: '', + key: domainObjectKey + } + } + ]); + let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); + expect(timeContext.bounds()).toEqual(independentBounds); + destroyTimeContext(); + }); + + it('Gets an independent time context given the objectPath', () => { + let timeContext = api.getContextForView([ + { identifier: domainObjectKey }, + { + identifier: { + namespace: '', + key: 'blah' + } + } + ]); + let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); + expect(timeContext.bounds()).toEqual(independentBounds); + destroyTimeContext(); + }); + + it('defaults to the global time context given the objectPath', () => { + let timeContext = api.getContextForView([ + { + identifier: { + namespace: '', + key: 'blah' + } + } + ]); + expect(timeContext.bounds()).toEqual(bounds); + }); + + it('follows a parent time context given the objectPath', () => { + api.getContextForView([ + { + identifier: { + namespace: '', + key: 'blah' + } + } + ]); + let destroyTimeContext = api.addIndependentContext('blah', independentBounds); + let timeContext = api.getContextForView([ + { + identifier: { + namespace: '', + key: domainObjectKey + } + }, + { + identifier: { + namespace: '', + key: 'blah' + } + } + ]); + expect(timeContext.bounds()).toEqual(independentBounds); + destroyTimeContext(); + expect(timeContext.bounds()).toEqual(bounds); + }); + + it("uses an object's independent time context if the parent doesn't have one", () => { + const domainObjectKey2 = `${domainObjectKey}-2`; + const domainObjectKey3 = `${domainObjectKey}-3`; + let timeContext = api.getContextForView([ + { + identifier: { + namespace: '', + key: domainObjectKey + } + } + ]); + let timeContext2 = api.getContextForView([ + { + identifier: { + namespace: '', + key: domainObjectKey2 + } + } + ]); + let timeContext3 = api.getContextForView([ + { + identifier: { + namespace: '', + key: domainObjectKey3 + } + } + ]); + // all bounds follow global time context + expect(timeContext.bounds()).toEqual(bounds); + expect(timeContext2.bounds()).toEqual(bounds); + expect(timeContext3.bounds()).toEqual(bounds); + // only first item has own context + let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); + expect(timeContext.bounds()).toEqual(independentBounds); + expect(timeContext2.bounds()).toEqual(bounds); + expect(timeContext3.bounds()).toEqual(bounds); + // first and second item have own context + let destroyTimeContext2 = api.addIndependentContext(domainObjectKey2, independentBounds); + expect(timeContext.bounds()).toEqual(independentBounds); + expect(timeContext2.bounds()).toEqual(independentBounds); + expect(timeContext3.bounds()).toEqual(bounds); + // all items have own time context + let destroyTimeContext3 = api.addIndependentContext(domainObjectKey3, independentBounds); + expect(timeContext.bounds()).toEqual(independentBounds); + expect(timeContext2.bounds()).toEqual(independentBounds); + expect(timeContext3.bounds()).toEqual(independentBounds); + //remove own contexts one at a time - should revert to global time context + destroyTimeContext(); + expect(timeContext.bounds()).toEqual(bounds); + expect(timeContext2.bounds()).toEqual(independentBounds); + expect(timeContext3.bounds()).toEqual(independentBounds); + destroyTimeContext2(); + expect(timeContext.bounds()).toEqual(bounds); + expect(timeContext2.bounds()).toEqual(bounds); + expect(timeContext3.bounds()).toEqual(independentBounds); + destroyTimeContext3(); + expect(timeContext.bounds()).toEqual(bounds); + expect(timeContext2.bounds()).toEqual(bounds); + expect(timeContext3.bounds()).toEqual(bounds); + }); + + it('Allows setting of valid bounds', function () { + bounds = { + start: 0, + end: 1 + }; + let timeContext = api.getContextForView([{ identifier: domainObjectKey }]); + let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); + expect(timeContext.bounds()).not.toEqual(bounds); + timeContext.bounds(bounds); + expect(timeContext.bounds()).toEqual(bounds); + destroyTimeContext(); + }); + + it('Disallows setting of invalid bounds', function () { + bounds = { + start: 1, + end: 0 + }; + + let timeContext = api.getContextForView([{ identifier: domainObjectKey }]); + let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); + expect(timeContext.bounds()).not.toBe(bounds); + + expect(timeContext.bounds.bind(timeContext, bounds)).toThrow(); + expect(timeContext.bounds()).not.toEqual(bounds); + + bounds = { start: 1 }; + expect(timeContext.bounds()).not.toEqual(bounds); + expect(timeContext.bounds.bind(timeContext, bounds)).toThrow(); + expect(timeContext.bounds()).not.toEqual(bounds); + destroyTimeContext(); + }); + + it('Emits an event when bounds change', function () { + let timeContext = api.getContextForView([{ identifier: domainObjectKey }]); + let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); + expect(eventListener).not.toHaveBeenCalled(); + timeContext.on('bounds', eventListener); + timeContext.bounds(bounds); + expect(eventListener).toHaveBeenCalledWith(bounds, false); + destroyTimeContext(); + }); + + it('Emits an event when bounds change on the global context', function () { + let timeContext = api.getContextForView([{ identifier: domainObjectKey }]); + expect(eventListener).not.toHaveBeenCalled(); + timeContext.on('bounds', eventListener); + timeContext.bounds(bounds); + expect(eventListener).toHaveBeenCalledWith(bounds, false); + }); + + describe(' when using real time clock', function () { + const mockOffsets = { + start: 10, + end: 11 + }; + + it('Emits an event when bounds change based on current value', function () { + let timeContext = api.getContextForView([{ identifier: domainObjectKey }]); + let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds); + expect(eventListener).not.toHaveBeenCalled(); + timeContext.clock('someClockKey', mockOffsets); + timeContext.on('bounds', eventListener); + timeContext.tick(10); + expect(eventListener).toHaveBeenCalledWith( + { + start: 20, + end: 21 + }, + true + ); + destroyTimeContext(); }); + }); }); diff --git a/src/api/types/Type.js b/src/api/types/Type.js index 4c0f052684..f56a18abfc 100644 --- a/src/api/types/Type.js +++ b/src/api/types/Type.js @@ -29,93 +29,93 @@ * @memberof module:openmct */ export default class Type { - constructor(definition) { - this.definition = definition; - if (definition.key) { - this.key = definition.key; - } + constructor(definition) { + this.definition = definition; + if (definition.key) { + this.key = definition.key; } - /** - * Create a type definition from a legacy definition. - */ - static definitionFromLegacyDefinition(legacyDefinition) { - let definition = {}; - definition.name = legacyDefinition.name; - definition.cssClass = legacyDefinition.cssClass; - definition.description = legacyDefinition.description; - definition.form = legacyDefinition.properties; - if (legacyDefinition.telemetry !== undefined) { - let telemetry = { - values: [] - }; + } + /** + * Create a type definition from a legacy definition. + */ + static definitionFromLegacyDefinition(legacyDefinition) { + let definition = {}; + definition.name = legacyDefinition.name; + definition.cssClass = legacyDefinition.cssClass; + definition.description = legacyDefinition.description; + definition.form = legacyDefinition.properties; + if (legacyDefinition.telemetry !== undefined) { + let telemetry = { + values: [] + }; - if (legacyDefinition.telemetry.domains !== undefined) { - legacyDefinition.telemetry.domains.forEach((domain, index) => { - domain.hints = { - domain: index - }; - telemetry.values.push(domain); - }); - } + if (legacyDefinition.telemetry.domains !== undefined) { + legacyDefinition.telemetry.domains.forEach((domain, index) => { + domain.hints = { + domain: index + }; + telemetry.values.push(domain); + }); + } - if (legacyDefinition.telemetry.ranges !== undefined) { - legacyDefinition.telemetry.ranges.forEach((range, index) => { - range.hints = { - range: index - }; - telemetry.values.push(range); - }); - } + if (legacyDefinition.telemetry.ranges !== undefined) { + legacyDefinition.telemetry.ranges.forEach((range, index) => { + range.hints = { + range: index + }; + telemetry.values.push(range); + }); + } - definition.telemetry = telemetry; - } - - if (legacyDefinition.model) { - definition.initialize = function (model) { - for (let [k, v] of Object.entries(legacyDefinition.model)) { - model[k] = JSON.parse(JSON.stringify(v)); - } - }; - } - - if (legacyDefinition.features && legacyDefinition.features.includes("creation")) { - definition.creatable = true; - } - - return definition; + definition.telemetry = telemetry; } - /** - * Check if a domain object is an instance of this type. - * @param domainObject - * @returns {boolean} true if the domain object is of this type - * @memberof module:openmct.Type# - * @method check - */ - check(domainObject) { - // Depends on assignment from MCT. - return domainObject.type === this.key; - } - /** - * Get a definition for this type that can be registered using the - * legacy bundle format. - * @private - */ - toLegacyDefinition() { - const def = {}; - def.name = this.definition.name; - def.cssClass = this.definition.cssClass; - def.description = this.definition.description; - def.properties = this.definition.form; - if (this.definition.initialize) { - def.model = {}; - this.definition.initialize(def.model); + if (legacyDefinition.model) { + definition.initialize = function (model) { + for (let [k, v] of Object.entries(legacyDefinition.model)) { + model[k] = JSON.parse(JSON.stringify(v)); } - - if (this.definition.creatable) { - def.features = ['creation']; - } - - return def; + }; } + + if (legacyDefinition.features && legacyDefinition.features.includes('creation')) { + definition.creatable = true; + } + + return definition; + } + /** + * Check if a domain object is an instance of this type. + * @param domainObject + * @returns {boolean} true if the domain object is of this type + * @memberof module:openmct.Type# + * @method check + */ + check(domainObject) { + // Depends on assignment from MCT. + return domainObject.type === this.key; + } + /** + * Get a definition for this type that can be registered using the + * legacy bundle format. + * @private + */ + toLegacyDefinition() { + const def = {}; + def.name = this.definition.name; + def.cssClass = this.definition.cssClass; + def.description = this.definition.description; + def.properties = this.definition.form; + + if (this.definition.initialize) { + def.model = {}; + this.definition.initialize(def.model); + } + + if (this.definition.creatable) { + def.features = ['creation']; + } + + return def; + } } diff --git a/src/api/types/TypeRegistry.js b/src/api/types/TypeRegistry.js index a5b3731048..af25ad5029 100644 --- a/src/api/types/TypeRegistry.js +++ b/src/api/types/TypeRegistry.js @@ -22,9 +22,9 @@ import Type from './Type'; const UNKNOWN_TYPE = new Type({ - key: "unknown", - name: "Unknown Type", - cssClass: "icon-object-unknown" + key: 'unknown', + name: 'Unknown Type', + cssClass: 'icon-object-unknown' }); /** @@ -46,60 +46,60 @@ const UNKNOWN_TYPE = new Type({ * @memberof module:openmct */ export default class TypeRegistry { - constructor() { - this.types = {}; - } - /** - * Register a new object type. - * - * @param {string} typeKey a string identifier for this type - * @param {module:openmct.Type} type the type to add - * @method addType - * @memberof module:openmct.TypeRegistry# - */ - addType(typeKey, typeDef) { - this.standardizeType(typeDef); - this.types[typeKey] = new Type(typeDef); - } - /** - * Takes a typeDef, standardizes it, and logs warnings about unsupported - * usage. - * @private - */ - standardizeType(typeDef) { - if (Object.prototype.hasOwnProperty.call(typeDef, 'label')) { - if (!typeDef.name) { - typeDef.name = typeDef.label; - } + constructor() { + this.types = {}; + } + /** + * Register a new object type. + * + * @param {string} typeKey a string identifier for this type + * @param {module:openmct.Type} type the type to add + * @method addType + * @memberof module:openmct.TypeRegistry# + */ + addType(typeKey, typeDef) { + this.standardizeType(typeDef); + this.types[typeKey] = new Type(typeDef); + } + /** + * Takes a typeDef, standardizes it, and logs warnings about unsupported + * usage. + * @private + */ + standardizeType(typeDef) { + if (Object.prototype.hasOwnProperty.call(typeDef, 'label')) { + if (!typeDef.name) { + typeDef.name = typeDef.label; + } - delete typeDef.label; - } - } - /** - * List keys for all registered types. - * @method listKeys - * @memberof module:openmct.TypeRegistry# - * @returns {string[]} all registered type keys - */ - listKeys() { - return Object.keys(this.types); - } - /** - * Retrieve a registered type by its key. - * @method get - * @param {string} typeKey the key for this type - * @memberof module:openmct.TypeRegistry# - * @returns {module:openmct.Type} the registered type - */ - get(typeKey) { - return this.types[typeKey] || UNKNOWN_TYPE; - } - importLegacyTypes(types) { - types.filter((t) => this.get(t.key) === UNKNOWN_TYPE) - .forEach((type) => { - let def = Type.definitionFromLegacyDefinition(type); - this.addType(type.key, def); - }); + delete typeDef.label; } + } + /** + * List keys for all registered types. + * @method listKeys + * @memberof module:openmct.TypeRegistry# + * @returns {string[]} all registered type keys + */ + listKeys() { + return Object.keys(this.types); + } + /** + * Retrieve a registered type by its key. + * @method get + * @param {string} typeKey the key for this type + * @memberof module:openmct.TypeRegistry# + * @returns {module:openmct.Type} the registered type + */ + get(typeKey) { + return this.types[typeKey] || UNKNOWN_TYPE; + } + importLegacyTypes(types) { + types + .filter((t) => this.get(t.key) === UNKNOWN_TYPE) + .forEach((type) => { + let def = Type.definitionFromLegacyDefinition(type); + this.addType(type.key, def); + }); + } } - diff --git a/src/api/types/TypeRegistrySpec.js b/src/api/types/TypeRegistrySpec.js index 83ef11e116..cb4ce89aa7 100644 --- a/src/api/types/TypeRegistrySpec.js +++ b/src/api/types/TypeRegistrySpec.js @@ -23,33 +23,33 @@ import TypeRegistry from './TypeRegistry'; describe('The Type API', function () { - let typeRegistryInstance; + let typeRegistryInstance; - beforeEach(function () { - typeRegistryInstance = new TypeRegistry (); - typeRegistryInstance.addType('testType', { - name: 'Test Type', - description: 'This is a test type.', - creatable: true - }); + beforeEach(function () { + typeRegistryInstance = new TypeRegistry(); + typeRegistryInstance.addType('testType', { + name: 'Test Type', + description: 'This is a test type.', + creatable: true }); + }); - it('types can be standardized', function () { - typeRegistryInstance.addType('standardizationTestType', { - label: 'Test Type', - description: 'This is a test type.', - creatable: true - }); - typeRegistryInstance.standardizeType(typeRegistryInstance.types.standardizationTestType); - expect(typeRegistryInstance.get('standardizationTestType').definition.label).toBeUndefined(); - expect(typeRegistryInstance.get('standardizationTestType').definition.name).toBe('Test Type'); + it('types can be standardized', function () { + typeRegistryInstance.addType('standardizationTestType', { + label: 'Test Type', + description: 'This is a test type.', + creatable: true }); + typeRegistryInstance.standardizeType(typeRegistryInstance.types.standardizationTestType); + expect(typeRegistryInstance.get('standardizationTestType').definition.label).toBeUndefined(); + expect(typeRegistryInstance.get('standardizationTestType').definition.name).toBe('Test Type'); + }); - it('new types are registered successfully and can be retrieved', function () { - expect(typeRegistryInstance.get('testType').definition.name).toBe('Test Type'); - }); + it('new types are registered successfully and can be retrieved', function () { + expect(typeRegistryInstance.get('testType').definition.name).toBe('Test Type'); + }); - it('type registry contains new keys', function () { - expect(typeRegistryInstance.listKeys ()).toContain('testType'); - }); + it('type registry contains new keys', function () { + expect(typeRegistryInstance.listKeys()).toContain('testType'); + }); }); diff --git a/src/api/user/StatusAPI.js b/src/api/user/StatusAPI.js index 4c6f4845ed..159dcbb13b 100644 --- a/src/api/user/StatusAPI.js +++ b/src/api/user/StatusAPI.js @@ -19,259 +19,259 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import EventEmitter from "EventEmitter"; +import EventEmitter from 'EventEmitter'; export default class StatusAPI extends EventEmitter { - #userAPI; - #openmct; + #userAPI; + #openmct; - constructor(userAPI, openmct) { - super(); - this.#userAPI = userAPI; - this.#openmct = 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.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(); + this.#openmct.once('destroy', () => { + const provider = this.#userAPI.getProvider(); - if (typeof provider?.off === 'function') { - provider.off('statusChange', this.onProviderStatusChange); - provider.off('pollQuestionChange', this.onProviderPollQuestionChange); - } - }); + if (typeof provider?.off === 'function') { + provider.off('statusChange', this.onProviderStatusChange); + provider.off('pollQuestionChange', this.onProviderPollQuestionChange); + } + }); - this.#userAPI.on('providerAdded', this.listenToStatusEvents); + 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} + */ + getPollQuestion() { + const provider = this.#userAPI.getProvider(); + + if (provider.getPollQuestion) { + return provider.getPollQuestion(); + } else { + this.#userAPI.error('User provider does not support polling questions'); } + } - /** - * 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} - */ - getPollQuestion() { - const provider = this.#userAPI.getProvider(); + /** + * 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} true if operation was successful, otherwise false. + */ + async setPollQuestion(questionText) { + const canSetPollQuestion = await this.canSetPollQuestion(); - if (provider.getPollQuestion) { - return provider.getPollQuestion(); - } else { - this.#userAPI.error("User provider does not support polling questions"); - } + 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'); } + } - /** - * 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} true if operation was successful, otherwise false. - */ - async setPollQuestion(questionText) { - const canSetPollQuestion = await this.canSetPollQuestion(); + /** + * Can the currently logged in user set the operator status poll question. + * @returns {Promise} + */ + canSetPollQuestion() { + const provider = this.#userAPI.getProvider(); - 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"); - } + if (provider.canSetPollQuestion) { + return provider.canSetPollQuestion(); + } else { + return Promise.resolve(false); } + } - /** - * Can the currently logged in user set the operator status poll question. - * @returns {Promise} - */ - canSetPollQuestion() { - const provider = this.#userAPI.getProvider(); + /** + * @returns {Promise>} 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.canSetPollQuestion) { - return provider.canSetPollQuestion(); - } else { - return Promise.resolve(false); - } + if (provider.getPossibleStatuses) { + const possibleStatuses = (await provider.getPossibleStatuses()) || []; + + return possibleStatuses.map((status) => status); + } else { + this.#userAPI.error('User provider cannot provide statuses'); } + } - /** - * @returns {Promise>} the complete list of possible states that an operator can reply to a poll question with. - */ - async getPossibleStatuses() { - const provider = this.#userAPI.getProvider(); + /** + * @param {import("./UserAPI").Role} role The role to fetch the current status for. + * @returns {Promise} the current status of the provided role + */ + async getStatusForRole(role) { + const provider = this.#userAPI.getProvider(); - if (provider.getPossibleStatuses) { - const possibleStatuses = await provider.getPossibleStatuses() || []; + if (provider.getStatusForRole) { + const status = await provider.getStatusForRole(role); - return possibleStatuses.map(status => status); - } else { - this.#userAPI.error("User provider cannot provide statuses"); - } + return status; + } else { + this.#userAPI.error('User provider does not support role status'); } + } - /** - * @param {import("./UserAPI").Role} role The role to fetch the current status for. - * @returns {Promise} the current status of the provided role - */ - async getStatusForRole(role) { - const provider = this.#userAPI.getProvider(); + /** + * @param {import("./UserAPI").Role} role + * @returns {Promise} true if the configured UserProvider can provide status for the given role + * @see StatusUserProvider + */ + canProvideStatusForRole(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"); - } + if (provider.canProvideStatusForRole) { + return provider.canProvideStatusForRole(role); + } else { + return false; } + } - /** - * @param {import("./UserAPI").Role} role - * @returns {Promise} true if the configured UserProvider can provide status for the given role - * @see StatusUserProvider - */ - canProvideStatusForRole(role) { - const provider = this.#userAPI.getProvider(); + /** + * @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} true if operation was successful, otherwise false. + */ + setStatusForRole(role, status) { + const provider = this.#userAPI.getProvider(); - if (provider.canProvideStatusForRole) { - return provider.canProvideStatusForRole(role); - } else { - return false; - } + if (provider.setStatusForRole) { + return provider.setStatusForRole(role, status); + } else { + this.#userAPI.error('User provider does not support setting role status'); } + } - /** - * @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} true if operation was successful, otherwise false. - */ - setStatusForRole(role, status) { - const provider = this.#userAPI.getProvider(); + /** + * 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} 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, status); - } else { - this.#userAPI.error("User provider does not support setting role status"); - } + if (provider.setStatusForRole) { + return provider.setStatusForRole(role, defaultStatus); + } else { + this.#userAPI.error('User provider does not support resetting 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} true if operation was successful, otherwise false. - */ - async resetStatusForRole(role) { - const provider = this.#userAPI.getProvider(); - const defaultStatus = await this.getDefaultStatusForRole(role); + /** + * Resets the status of all operators to their default status + * @returns {Promise} true if operation was successful, otherwise false. + */ + async resetAllStatuses() { + const allStatusRoles = await this.getAllStatusRoles(); - if (provider.setStatusForRole) { - return provider.setStatusForRole(role, defaultStatus); - } else { - this.#userAPI.error("User provider does not support resetting role status"); - } + 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} 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>} 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'); } + } - /** - * Resets the status of all operators to their default status - * @returns {Promise} true if operation was successful, otherwise false. - */ - async resetAllStatuses() { - const allStatusRoles = await this.getAllStatusRoles(); + /** + * 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} the role for which the current user can provide status. + */ + getStatusRoleForCurrentUser() { + const provider = this.#userAPI.getProvider(); - return Promise.all(allStatusRoles.map(role => this.resetStatusForRole(role))); + if (provider.getStatusRoleForCurrentUser) { + return provider.getStatusRoleForCurrentUser(); + } else { + this.#userAPI.error('User provider cannot provide role status for this user'); } + } - /** - * 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} the default operator status if no other has been set. - */ - async getDefaultStatusForRole(role) { - const provider = this.#userAPI.getProvider(); - const defaultStatus = await provider.getDefaultStatusForRole(role); + /** + * @returns {Promise} 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(); - return defaultStatus; + if (provider.getStatusRoleForCurrentUser) { + const activeStatusRole = await this.#userAPI.getProvider().getStatusRoleForCurrentUser(); + const canProvideStatus = await this.canProvideStatusForRole(activeStatusRole); + + return canProvideStatus; + } else { + return false; } + } - /** - * 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>} 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"); - } + /** + * 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); } + } - /** - * 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} the role for which the current user can provide status. - */ - getStatusRoleForCurrentUser() { - const provider = this.#userAPI.getProvider(); + /** + * @private + */ + onProviderStatusChange(newStatus) { + this.emit('statusChange', newStatus); + } - if (provider.getStatusRoleForCurrentUser) { - return provider.getStatusRoleForCurrentUser(); - } else { - this.#userAPI.error("User provider cannot provide role status for this user"); - } - } - - /** - * @returns {Promise} 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); - } + /** + * @private + */ + onProviderPollQuestionChange(pollQuestion) { + this.emit('pollQuestionChange', pollQuestion); + } } /** diff --git a/src/api/user/StatusUserProvider.js b/src/api/user/StatusUserProvider.js index 7c8d5bb63a..85d132b52c 100644 --- a/src/api/user/StatusUserProvider.js +++ b/src/api/user/StatusUserProvider.js @@ -19,63 +19,63 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import UserProvider from "./UserProvider"; +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} true if operation was successful, otherwise false - */ - async setPollQuestion(pollQuestion) {} - /** - * @returns {Promise} true if the current user can set the poll question, otherwise false - */ - async canSetPollQuestion() {} - /** - * @returns {Promise>} a list of the possible statuses that an operator can be in - */ - async getPossibleStatuses() {} - /** - * @param {import("./UserAPI").Role} role - * @returns {Promise} true if operation was successful, otherwise false. - */ - async setStatusForRole(role, status) {} - /** - * @param {import("./UserAPI").Role} role - * @returns {Promise>} a list of all available status roles, if user permissions allow it. - */ - async getAllStatusRoles() {} - /** - * @returns {Promise} the active status role for the currently logged in user - */ - async getStatusRoleForCurrentUser() {} + /** + * @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} true if operation was successful, otherwise false + */ + async setPollQuestion(pollQuestion) {} + /** + * @returns {Promise} true if the current user can set the poll question, otherwise false + */ + async canSetPollQuestion() {} + /** + * @returns {Promise>} a list of the possible statuses that an operator can be in + */ + async getPossibleStatuses() {} + /** + * @param {import("./UserAPI").Role} role + * @returns {Promise} true if operation was successful, otherwise false. + */ + async setStatusForRole(role, status) {} + /** + * @param {import("./UserAPI").Role} role + * @returns {Promise>} a list of all available status roles, if user permissions allow it. + */ + async getAllStatusRoles() {} + /** + * @returns {Promise} the active status role for the currently logged in user + */ + async getStatusRoleForCurrentUser() {} } diff --git a/src/api/user/User.js b/src/api/user/User.js index 3259efc96f..9f0fa176c1 100644 --- a/src/api/user/User.js +++ b/src/api/user/User.js @@ -21,19 +21,19 @@ *****************************************************************************/ export default class User { - constructor(id, name) { - this.id = id; - this.name = name; + constructor(id, name) { + this.id = id; + this.name = name; - this.getId = this.getId.bind(this); - this.getName = this.getName.bind(this); - } + this.getId = this.getId.bind(this); + this.getName = this.getName.bind(this); + } - getId() { - return this.id; - } + getId() { + return this.id; + } - getName() { - return this.name; - } + getName() { + return this.name; + } } diff --git a/src/api/user/UserAPI.js b/src/api/user/UserAPI.js index de44084379..98f4322ac7 100644 --- a/src/api/user/UserAPI.js +++ b/src/api/user/UserAPI.js @@ -21,128 +21,125 @@ *****************************************************************************/ import EventEmitter from 'EventEmitter'; -import { - MULTIPLE_PROVIDER_ERROR, - NO_PROVIDER_ERROR -} from './constants'; +import { MULTIPLE_PROVIDER_ERROR, NO_PROVIDER_ERROR } from './constants'; import StatusAPI from './StatusAPI'; import User from './User'; class UserAPI extends EventEmitter { - /** - * @param {OpenMCT} openmct - * @param {UserAPIConfiguration} config - */ - constructor(openmct, config) { - super(); + /** + * @param {OpenMCT} openmct + * @param {UserAPIConfiguration} config + */ + constructor(openmct, config) { + super(); - this._openmct = openmct; - this._provider = undefined; + this._openmct = openmct; + this._provider = undefined; - this.User = User; - this.status = new StatusAPI(this, openmct, config); + this.User = User; + this.status = new StatusAPI(this, openmct, config); + } + + /** + * Set the user provider for the user API. This allows you + * to specifiy ONE user provider to be used with Open MCT. + * @method setProvider + * @memberof module:openmct.UserAPI# + * @param {module:openmct.UserAPI~UserProvider} provider the new + * user provider + */ + setProvider(provider) { + if (this.hasProvider()) { + this.error(MULTIPLE_PROVIDER_ERROR); } - /** - * Set the user provider for the user API. This allows you - * to specifiy ONE user provider to be used with Open MCT. - * @method setProvider - * @memberof module:openmct.UserAPI# - * @param {module:openmct.UserAPI~UserProvider} provider the new - * user provider - */ - setProvider(provider) { - if (this.hasProvider()) { - this.error(MULTIPLE_PROVIDER_ERROR); - } + this._provider = provider; + this.emit('providerAdded', this._provider); + } - this._provider = provider; - this.emit('providerAdded', this._provider); + getProvider() { + return this._provider; + } + + /** + * Return true if the user provider has been set. + * + * @memberof module:openmct.UserAPI# + * @returns {boolean} true if the user provider exists + */ + hasProvider() { + return this._provider !== undefined; + } + + /** + * If a user provider is set, it will return a copy of a user object from + * the provider. If the user is not logged in, it will return undefined; + * + * @memberof module:openmct.UserAPI# + * @returns {Function|Promise} user provider 'getCurrentUser' method + * @throws Will throw an error if no user provider is set + */ + getCurrentUser() { + if (!this.hasProvider()) { + return Promise.resolve(undefined); + } else { + return this._provider.getCurrentUser(); + } + } + + /** + * If a user provider is set, it will return the user provider's + * 'isLoggedIn' method + * + * @memberof module:openmct.UserAPI# + * @returns {Function|Boolean} user provider 'isLoggedIn' method + * @throws Will throw an error if no user provider is set + */ + isLoggedIn() { + if (!this.hasProvider()) { + return false; } - getProvider() { - return this._provider; + return this._provider.isLoggedIn(); + } + + /** + * If a user provider is set, it will return a call to it's + * 'hasRole' method + * + * @memberof module:openmct.UserAPI# + * @returns {Function|Boolean} user provider 'isLoggedIn' method + * @param {string} roleId id of role to check for + * @throws Will throw an error if no user provider is set + */ + hasRole(roleId) { + this.noProviderCheck(); + + return this._provider.hasRole(roleId); + } + + /** + * Checks if a provider is set and if not, will throw error + * + * @private + * @throws Will throw an error if no user provider is set + */ + noProviderCheck() { + if (!this.hasProvider()) { + this.error(NO_PROVIDER_ERROR); } + } - /** - * Return true if the user provider has been set. - * - * @memberof module:openmct.UserAPI# - * @returns {boolean} true if the user provider exists - */ - hasProvider() { - return this._provider !== undefined; - } - - /** - * If a user provider is set, it will return a copy of a user object from - * the provider. If the user is not logged in, it will return undefined; - * - * @memberof module:openmct.UserAPI# - * @returns {Function|Promise} user provider 'getCurrentUser' method - * @throws Will throw an error if no user provider is set - */ - getCurrentUser() { - if (!this.hasProvider()) { - return Promise.resolve(undefined); - } else { - return this._provider.getCurrentUser(); - } - } - - /** - * If a user provider is set, it will return the user provider's - * 'isLoggedIn' method - * - * @memberof module:openmct.UserAPI# - * @returns {Function|Boolean} user provider 'isLoggedIn' method - * @throws Will throw an error if no user provider is set - */ - isLoggedIn() { - if (!this.hasProvider()) { - return false; - } - - return this._provider.isLoggedIn(); - } - - /** - * If a user provider is set, it will return a call to it's - * 'hasRole' method - * - * @memberof module:openmct.UserAPI# - * @returns {Function|Boolean} user provider 'isLoggedIn' method - * @param {string} roleId id of role to check for - * @throws Will throw an error if no user provider is set - */ - hasRole(roleId) { - this.noProviderCheck(); - - return this._provider.hasRole(roleId); - } - - /** - * Checks if a provider is set and if not, will throw error - * - * @private - * @throws Will throw an error if no user provider is set - */ - noProviderCheck() { - if (!this.hasProvider()) { - this.error(NO_PROVIDER_ERROR); - } - } - - /** - * Utility function for throwing errors - * - * @private - * @param {string} error description of error - * @throws Will throw error passed in - */ - error(error) { - throw new Error(error); - } + /** + * Utility function for throwing errors + * + * @private + * @param {string} error description of error + * @throws Will throw error passed in + */ + error(error) { + throw new Error(error); + } } export default UserAPI; diff --git a/src/api/user/UserAPISpec.js b/src/api/user/UserAPISpec.js index 7ea4673ff8..0c5e0db947 100644 --- a/src/api/user/UserAPISpec.js +++ b/src/api/user/UserAPISpec.js @@ -20,95 +20,93 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { - createOpenMct, - resetApplicationState -} from '../../utils/testing'; -import { - MULTIPLE_PROVIDER_ERROR -} from './constants'; +import { createOpenMct, resetApplicationState } from '../../utils/testing'; +import { MULTIPLE_PROVIDER_ERROR } from './constants'; import ExampleUserProvider from '../../../example/exampleUser/ExampleUserProvider'; const USERNAME = 'Test User'; const EXAMPLE_ROLE = 'example-role'; -describe("The User API", () => { - let openmct; +describe('The User API', () => { + let openmct; + + beforeEach(() => { + openmct = createOpenMct(); + }); + + 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); + }); + openmct.user.setProvider(new ExampleUserProvider(openmct)); + }); + + it('prevents more than one user provider from being set', () => { + openmct.user.setProvider(new ExampleUserProvider(openmct)); + + expect(() => { + openmct.user.setProvider({}); + }).toThrow(new Error(MULTIPLE_PROVIDER_ERROR)); + }); + + it('provides a check for an existing user provider', () => { + expect(openmct.user.hasProvider()).toBeFalse(); + + openmct.user.setProvider(new ExampleUserProvider(openmct)); + + expect(openmct.user.hasProvider()).toBeTrue(); + }); + }); + + describe('provides the ability', () => { + let provider; beforeEach(() => { - openmct = createOpenMct(); + provider = new ExampleUserProvider(openmct); + provider.autoLogin(USERNAME); }); - afterEach(() => { - const activeOverlays = openmct.overlays.activeOverlays; - activeOverlays.forEach(overlay => overlay.dismiss()); + it('to check if a user (not specific) is loged in', (done) => { + expect(openmct.user.isLoggedIn()).toBeFalse(); - return resetApplicationState(openmct); + openmct.user.on('providerAdded', () => { + expect(openmct.user.isLoggedIn()).toBeTrue(); + done(); + }); + + // this will trigger the user indicator plugin, + // which will in turn login the user + openmct.user.setProvider(provider); }); - describe('with regard to user providers', () => { - it('allows you to specify a user provider', () => { - openmct.user.on('providerAdded', (provider) => { - expect(provider).toBeInstanceOf(ExampleUserProvider); - }); - openmct.user.setProvider(new ExampleUserProvider(openmct)); - }); - - it('prevents more than one user provider from being set', () => { - openmct.user.setProvider(new ExampleUserProvider(openmct)); - - expect(() => { - openmct.user.setProvider({}); - }).toThrow(new Error(MULTIPLE_PROVIDER_ERROR)); - }); - - it('provides a check for an existing user provider', () => { - expect(openmct.user.hasProvider()).toBeFalse(); - - openmct.user.setProvider(new ExampleUserProvider(openmct)); - - expect(openmct.user.hasProvider()).toBeTrue(); - }); + it('to get the current user', (done) => { + openmct.user.setProvider(provider); + openmct.user + .getCurrentUser() + .then((apiUser) => { + expect(apiUser.name).toEqual(USERNAME); + }) + .finally(done); }); - describe('provides the ability', () => { - let provider; + it('to check if a user has a specific role (by id)', (done) => { + openmct.user.setProvider(provider); + let junkIdCheckPromise = openmct.user.hasRole('junk-id').then((hasRole) => { + expect(hasRole).toBeFalse(); + }); + let realIdCheckPromise = openmct.user.hasRole(EXAMPLE_ROLE).then((hasRole) => { + expect(hasRole).toBeTrue(); + }); - beforeEach(() => { - provider = new ExampleUserProvider(openmct); - provider.autoLogin(USERNAME); - }); - - it('to check if a user (not specific) is loged in', (done) => { - expect(openmct.user.isLoggedIn()).toBeFalse(); - - openmct.user.on('providerAdded', () => { - expect(openmct.user.isLoggedIn()).toBeTrue(); - done(); - }); - - // this will trigger the user indicator plugin, - // which will in turn login the user - openmct.user.setProvider(provider); - }); - - it('to get the current user', (done) => { - openmct.user.setProvider(provider); - openmct.user.getCurrentUser().then((apiUser) => { - expect(apiUser.name).toEqual(USERNAME); - }).finally(done); - }); - - it('to check if a user has a specific role (by id)', (done) => { - openmct.user.setProvider(provider); - let junkIdCheckPromise = openmct.user.hasRole('junk-id').then((hasRole) => { - expect(hasRole).toBeFalse(); - }); - let realIdCheckPromise = openmct.user.hasRole(EXAMPLE_ROLE).then((hasRole) => { - expect(hasRole).toBeTrue(); - }); - - Promise.all([junkIdCheckPromise, realIdCheckPromise]).finally(done); - }); + Promise.all([junkIdCheckPromise, realIdCheckPromise]).finally(done); }); + }); }); diff --git a/src/api/user/UserProvider.js b/src/api/user/UserProvider.js index 9b2d5e3c55..7c185f88cf 100644 --- a/src/api/user/UserProvider.js +++ b/src/api/user/UserProvider.js @@ -20,17 +20,17 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ export default class UserProvider { - /** - * @returns {Promise} 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} true if the current user has the given role - */ - hasRole(role) {} + /** + * @returns {Promise} 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} 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 index 15d3e566b1..19c1efb2a4 100644 --- a/src/api/user/UserStatusAPISpec.js +++ b/src/api/user/UserStatusAPISpec.js @@ -20,84 +20,84 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { - createOpenMct, - resetApplicationState -} from '../../utils/testing'; +import { createOpenMct, resetApplicationState } from '../../utils/testing'; -describe("The User Status API", () => { - let openmct; - let userProvider; - let mockUser; +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); + 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; - afterEach(() => { - return resetApplicationState(openmct); - }); + // userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(true)); + // userProvider.on.and.callFake((eventName, listener) => { + // if (eventName === 'pollQuestionChange') { + // pollQuestionListener = listener; + // } + // }); - 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)); + // openmct.user.on('pollQuestionChange', pollQuestionChangeCallback); - 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; + // openmct.user.setProvider(userProvider); - // userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(true)); - // userProvider.on.and.callFake((eventName, listener) => { - // if (eventName === 'pollQuestionChange') { - // pollQuestionListener = listener; - // } - // }); + // return openmct.user.status.setPollQuestion('This is a poll question').then(() => { + // expect(pollQuestionListener).toBeDefined(); + // pollQuestionListener(); + // expect(pollQuestionChangeCallback).toHaveBeenCalled(); - // openmct.user.on('pollQuestionChange', pollQuestionChangeCallback); + // const pollQuestion = pollQuestionChangeCallback.calls.mostRecent().args[0]; + // expect(pollQuestion.question).toBe('This is a poll question'); - // openmct.user.setProvider(userProvider); + // 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').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(); - }); + 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/CSVExporter.js b/src/exporters/CSVExporter.js index 24f99ec8f6..e3ff409dc0 100644 --- a/src/exporters/CSVExporter.js +++ b/src/exporters/CSVExporter.js @@ -21,17 +21,16 @@ *****************************************************************************/ import CSV from 'comma-separated-values'; -import {saveAs} from 'saveAs'; +import { saveAs } from 'saveAs'; class CSVExporter { - export(rows, options) { - let headers = (options && options.headers) - || (Object.keys((rows[0] || {})).sort()); - let filename = (options && options.filename) || "export.csv"; - let csvText = new CSV(rows, { header: headers }).encode(); - let blob = new Blob([csvText], { type: "text/csv" }); - saveAs(blob, filename); - } + export(rows, options) { + let headers = (options && options.headers) || Object.keys(rows[0] || {}).sort(); + let filename = (options && options.filename) || 'export.csv'; + let csvText = new CSV(rows, { header: headers }).encode(); + let blob = new Blob([csvText], { type: 'text/csv' }); + saveAs(blob, filename); + } } export default CSVExporter; diff --git a/src/exporters/ImageExporter.js b/src/exporters/ImageExporter.js index 8d496ab589..559dcb9a5f 100644 --- a/src/exporters/ImageExporter.js +++ b/src/exporters/ImageExporter.js @@ -26,164 +26,169 @@ */ function replaceDotsWithUnderscores(filename) { - const regex = /\./gi; + const regex = /\./gi; - return filename.replace(regex, '_'); + return filename.replace(regex, '_'); } -import {saveAs} from 'saveAs'; +import { saveAs } from 'saveAs'; import html2canvas from 'html2canvas'; import { v4 as uuid } from 'uuid'; class ImageExporter { - constructor(openmct) { - this.openmct = openmct; - } - /** - * Converts an HTML element into a PNG or JPG Blob. - * @private - * @param {node} element that will be converted to an image - * @param {object} options Image options. - * @returns {promise} - */ - renderElement(element, { imageType, className, thumbnailSize }) { - const self = this; - const overlays = this.openmct.overlays; - const dialog = overlays.dialog({ - iconClass: 'info', - message: 'Capturing image, please wait...', - buttons: [ - { - label: 'Cancel', - emphasis: true, - callback: function () { - dialog.dismiss(); - } - } - ] - }); - - let mimeType = 'image/png'; - if (imageType === 'jpg') { - mimeType = 'image/jpeg'; + constructor(openmct) { + this.openmct = openmct; + } + /** + * Converts an HTML element into a PNG or JPG Blob. + * @private + * @param {node} element that will be converted to an image + * @param {object} options Image options. + * @returns {promise} + */ + renderElement(element, { imageType, className, thumbnailSize }) { + const self = this; + const overlays = this.openmct.overlays; + const dialog = overlays.dialog({ + iconClass: 'info', + message: 'Capturing image, please wait...', + buttons: [ + { + label: 'Cancel', + emphasis: true, + callback: function () { + dialog.dismiss(); + } } + ] + }); - let exportId = undefined; - let oldId = undefined; + let mimeType = 'image/png'; + if (imageType === 'jpg') { + mimeType = 'image/jpeg'; + } + + let exportId = undefined; + let oldId = undefined; + if (className) { + const newUUID = uuid(); + exportId = `$export-element-${newUUID}`; + oldId = element.id; + element.id = exportId; + } + + return html2canvas(element, { + useCORS: true, + allowTaint: true, + logging: false, + onclone: function (document) { if (className) { - const newUUID = uuid(); - exportId = `$export-element-${newUUID}`; - oldId = element.id; - element.id = exportId; + const clonedElement = document.getElementById(exportId); + clonedElement.classList.add(className); } - return html2canvas(element, { - useCORS: true, - allowTaint: true, - logging: false, - onclone: function (document) { - if (className) { - const clonedElement = document.getElementById(exportId); - clonedElement.classList.add(className); - } + element.id = oldId; + }, + removeContainer: true // Set to false to debug what html2canvas renders + }) + .then((canvas) => { + dialog.dismiss(); - element.id = oldId; - }, - removeContainer: true // Set to false to debug what html2canvas renders - }).then(canvas => { - dialog.dismiss(); + return new Promise(function (resolve, reject) { + if (thumbnailSize) { + const thumbnail = self.getThumbnail(canvas, mimeType, thumbnailSize); - return new Promise(function (resolve, reject) { - if (thumbnailSize) { - const thumbnail = self.getThumbnail(canvas, mimeType, thumbnailSize); + return canvas.toBlob( + (blob) => + resolve({ + blob, + thumbnail + }), + mimeType + ); + } - return canvas.toBlob(blob => resolve({ - blob, - thumbnail - }), mimeType); - } - - return canvas.toBlob(blob => resolve({ blob }), mimeType); - }); - }).catch(error => { - dialog.dismiss(); - - console.error('error capturing image', error); - const errorDialog = overlays.dialog({ - iconClass: 'error', - message: 'Image was not captured successfully!', - buttons: [ - { - label: "OK", - emphasis: true, - callback: function () { - errorDialog.dismiss(); - } - } - ] - }); + return canvas.toBlob((blob) => resolve({ blob }), mimeType); }); - } + }) + .catch((error) => { + dialog.dismiss(); - getThumbnail(canvas, mimeType, size) { - const thumbnailCanvas = document.createElement('canvas'); - thumbnailCanvas.setAttribute('width', size.width); - thumbnailCanvas.setAttribute('height', size.height); - const ctx = thumbnailCanvas.getContext('2d'); - ctx.globalCompositeOperation = "copy"; - ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height); - - return thumbnailCanvas.toDataURL(mimeType); - } - - /** - * Takes a screenshot of a DOM node and exports to JPG. - * @param {node} element to be exported - * @param {string} filename the exported image - * @param {string} className to be added to element before capturing (optional) - * @returns {promise} - */ - async exportJPG(element, filename, className) { - const processedFilename = replaceDotsWithUnderscores(filename); - - const img = await this.renderElement(element, { - imageType: 'jpg', - className + console.error('error capturing image', error); + const errorDialog = overlays.dialog({ + iconClass: 'error', + message: 'Image was not captured successfully!', + buttons: [ + { + label: 'OK', + emphasis: true, + callback: function () { + errorDialog.dismiss(); + } + } + ] }); - saveAs(img.blob, processedFilename); - } + }); + } - /** - * Takes a screenshot of a DOM node and exports to PNG. - * @param {node} element to be exported - * @param {string} filename the exported image - * @param {string} className to be added to element before capturing (optional) - * @returns {promise} - */ - async exportPNG(element, filename, className) { - const processedFilename = replaceDotsWithUnderscores(filename); + getThumbnail(canvas, mimeType, size) { + const thumbnailCanvas = document.createElement('canvas'); + thumbnailCanvas.setAttribute('width', size.width); + thumbnailCanvas.setAttribute('height', size.height); + const ctx = thumbnailCanvas.getContext('2d'); + ctx.globalCompositeOperation = 'copy'; + ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height); - const img = await this.renderElement(element, { - imageType: 'png', - className - }); - saveAs(img.blob, processedFilename); - } + return thumbnailCanvas.toDataURL(mimeType); + } - /** - * Takes a screenshot of a DOM node in PNG format. - * @param {node} element to be exported - * @param {string} filename the exported image - * @returns {promise} - */ + /** + * Takes a screenshot of a DOM node and exports to JPG. + * @param {node} element to be exported + * @param {string} filename the exported image + * @param {string} className to be added to element before capturing (optional) + * @returns {promise} + */ + async exportJPG(element, filename, className) { + const processedFilename = replaceDotsWithUnderscores(filename); - exportPNGtoSRC(element, options) { - return this.renderElement(element, { - imageType: 'png', - ...options - }); - } + const img = await this.renderElement(element, { + imageType: 'jpg', + className + }); + saveAs(img.blob, processedFilename); + } + + /** + * Takes a screenshot of a DOM node and exports to PNG. + * @param {node} element to be exported + * @param {string} filename the exported image + * @param {string} className to be added to element before capturing (optional) + * @returns {promise} + */ + async exportPNG(element, filename, className) { + const processedFilename = replaceDotsWithUnderscores(filename); + + const img = await this.renderElement(element, { + imageType: 'png', + className + }); + saveAs(img.blob, processedFilename); + } + + /** + * Takes a screenshot of a DOM node in PNG format. + * @param {node} element to be exported + * @param {string} filename the exported image + * @returns {promise} + */ + + exportPNGtoSRC(element, options) { + return this.renderElement(element, { + imageType: 'png', + ...options + }); + } } export default ImageExporter; - diff --git a/src/exporters/ImageExporterSpec.js b/src/exporters/ImageExporterSpec.js index 9fe62fba9d..f55e6eeafb 100644 --- a/src/exporters/ImageExporterSpec.js +++ b/src/exporters/ImageExporterSpec.js @@ -24,35 +24,35 @@ import ImageExporter from './ImageExporter'; import { createOpenMct, resetApplicationState } from '../utils/testing'; describe('The Image Exporter', () => { - let openmct; - let imageExporter; + let openmct; + let imageExporter; - beforeEach(() => { - openmct = createOpenMct(); + beforeEach(() => { + openmct = createOpenMct(); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + describe('basic instatation', () => { + it('can be instatiated', () => { + imageExporter = new ImageExporter(openmct); + + expect(imageExporter).not.toEqual(null); }); - - afterEach(() => { - return resetApplicationState(openmct); - }); - - describe("basic instatation", () => { - it("can be instatiated", () => { - imageExporter = new ImageExporter(openmct); - - expect(imageExporter).not.toEqual(null); - }); - it("can render an element to a blob", async () => { - const mockHeadElement = document.createElement("h1"); - const mockTextNode = document.createTextNode('foo bar'); - mockHeadElement.appendChild(mockTextNode); - document.body.appendChild(mockHeadElement); - imageExporter = new ImageExporter(openmct); - const returnedBlob = await imageExporter.renderElement(document.body, { - imageType: 'png' - }); - expect(returnedBlob).not.toEqual(null); - expect(returnedBlob.blob).not.toEqual(null); - expect(returnedBlob.blob).toBeInstanceOf(Blob); - }); + it('can render an element to a blob', async () => { + const mockHeadElement = document.createElement('h1'); + const mockTextNode = document.createTextNode('foo bar'); + mockHeadElement.appendChild(mockTextNode); + document.body.appendChild(mockHeadElement); + imageExporter = new ImageExporter(openmct); + const returnedBlob = await imageExporter.renderElement(document.body, { + imageType: 'png' + }); + expect(returnedBlob).not.toEqual(null); + expect(returnedBlob.blob).not.toEqual(null); + expect(returnedBlob.blob).toBeInstanceOf(Blob); }); + }); }); diff --git a/src/exporters/JSONExporter.js b/src/exporters/JSONExporter.js index 6f486c4296..0777829b5a 100644 --- a/src/exporters/JSONExporter.js +++ b/src/exporters/JSONExporter.js @@ -20,15 +20,15 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import {saveAs} from 'saveAs'; +import { saveAs } from 'saveAs'; class JSONExporter { - export(obj, options) { - let filename = (options && options.filename) || "test-export.json"; - let jsonText = JSON.stringify(obj); - let blob = new Blob([jsonText], {type: "application/json"}); - saveAs(blob, filename); - } + export(obj, options) { + let filename = (options && options.filename) || 'test-export.json'; + let jsonText = JSON.stringify(obj); + let blob = new Blob([jsonText], { type: 'application/json' }); + saveAs(blob, filename); + } } export default JSONExporter; diff --git a/src/plugins/CouchDBSearchFolder/plugin.js b/src/plugins/CouchDBSearchFolder/plugin.js index 7aee308293..002daf8d11 100644 --- a/src/plugins/CouchDBSearchFolder/plugin.js +++ b/src/plugins/CouchDBSearchFolder/plugin.js @@ -1,38 +1,39 @@ export default function (folderName, couchPlugin, searchFilter) { - return function install(openmct) { - const couchProvider = couchPlugin.couchProvider; + return function install(openmct) { + const couchProvider = couchPlugin.couchProvider; - openmct.objects.addRoot({ - namespace: 'couch-search', - key: 'couch-search' + openmct.objects.addRoot({ + namespace: 'couch-search', + key: 'couch-search' + }); + + openmct.objects.addProvider('couch-search', { + get(identifier) { + if (identifier.key !== 'couch-search') { + return undefined; + } else { + return Promise.resolve({ + identifier, + type: 'folder', + name: folderName || 'CouchDB Documents', + location: 'ROOT' + }); + } + } + }); + + openmct.composition.addProvider({ + appliesTo(domainObject) { + return ( + domainObject.identifier.namespace === 'couch-search' && + domainObject.identifier.key === 'couch-search' + ); + }, + load() { + return couchProvider.getObjectsByFilter(searchFilter).then((objects) => { + return objects.map((object) => object.identifier); }); - - openmct.objects.addProvider('couch-search', { - get(identifier) { - if (identifier.key !== 'couch-search') { - return undefined; - } else { - return Promise.resolve({ - identifier, - type: 'folder', - name: folderName || "CouchDB Documents", - location: 'ROOT' - }); - } - } - }); - - openmct.composition.addProvider({ - appliesTo(domainObject) { - return domainObject.identifier.namespace === 'couch-search' - && domainObject.identifier.key === 'couch-search'; - }, - load() { - return couchProvider.getObjectsByFilter(searchFilter).then(objects => { - return objects.map(object => object.identifier); - }); - } - }); - }; - + } + }); + }; } diff --git a/src/plugins/CouchDBSearchFolder/pluginSpec.js b/src/plugins/CouchDBSearchFolder/pluginSpec.js index 2a9798cf92..4f702ae2a5 100644 --- a/src/plugins/CouchDBSearchFolder/pluginSpec.js +++ b/src/plugins/CouchDBSearchFolder/pluginSpec.js @@ -20,81 +20,83 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import {createOpenMct, resetApplicationState} from "utils/testing"; +import { createOpenMct, resetApplicationState } from 'utils/testing'; import CouchDBSearchFolderPlugin from './plugin'; describe('the plugin', function () { - let identifier = { - namespace: 'couch-search', - key: "couch-search" - }; - let testPath = '/test/db'; - let openmct; - let composition; + let identifier = { + namespace: 'couch-search', + key: 'couch-search' + }; + let testPath = '/test/db'; + let openmct; + let composition; - beforeEach(() => { + beforeEach(() => { + openmct = createOpenMct(); - openmct = createOpenMct(); + let couchPlugin = openmct.plugins.CouchDB(testPath); + openmct.install(couchPlugin); - let couchPlugin = openmct.plugins.CouchDB(testPath); - openmct.install(couchPlugin); + openmct.install( + new CouchDBSearchFolderPlugin('CouchDB Documents', couchPlugin, { + selector: { + model: { + type: 'plan' + } + } + }) + ); - openmct.install(new CouchDBSearchFolderPlugin('CouchDB Documents', couchPlugin, { - "selector": { - "model": { - "type": "plan" - } - } - })); + spyOn(couchPlugin.couchProvider, 'getObjectsByFilter').and.returnValue( + Promise.resolve([ + { + identifier: { + key: '1', + namespace: 'mct' + } + }, + { + identifier: { + key: '2', + namespace: 'mct' + } + } + ]) + ); - spyOn(couchPlugin.couchProvider, 'getObjectsByFilter').and.returnValue(Promise.resolve([ - { - identifier: { - key: "1", - namespace: "mct" - } - }, - { - identifier: { - key: "2", - namespace: "mct" - } - } - ])); - - spyOn(couchPlugin.couchProvider, "get").and.callFake((id) => { - return Promise.resolve({ - identifier: id - }); - }); - - return new Promise((resolve) => { - openmct.once('start', resolve); - openmct.startHeadless(); - }).then(() => { - composition = openmct.composition.get({identifier}); - }); + spyOn(couchPlugin.couchProvider, 'get').and.callFake((id) => { + return Promise.resolve({ + identifier: id + }); }); - afterEach(() => { - return resetApplicationState(openmct); + return new Promise((resolve) => { + openmct.once('start', resolve); + openmct.startHeadless(); + }).then(() => { + composition = openmct.composition.get({ identifier }); }); + }); - it('provides a folder to hold plans', () => { - return openmct.objects.get(identifier).then((object) => { - expect(object).toEqual({ - identifier, - type: 'folder', - name: 'CouchDB Documents', - location: 'ROOT' - }); - }); + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('provides a folder to hold plans', () => { + return openmct.objects.get(identifier).then((object) => { + expect(object).toEqual({ + identifier, + type: 'folder', + name: 'CouchDB Documents', + location: 'ROOT' + }); }); + }); - it('provides composition for couch search folders', () => { - return composition.load().then((objects) => { - expect(objects.length).toEqual(2); - }); + it('provides composition for couch search folders', () => { + return composition.load().then((objects) => { + expect(objects.length).toEqual(2); }); - + }); }); diff --git a/src/plugins/DeviceClassifier/plugin.js b/src/plugins/DeviceClassifier/plugin.js index 76e72843ea..27816a752f 100644 --- a/src/plugins/DeviceClassifier/plugin.js +++ b/src/plugins/DeviceClassifier/plugin.js @@ -19,14 +19,14 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import Agent from "../../utils/agent/Agent"; -import DeviceClassifier from "./src/DeviceClassifier"; +import Agent from '../../utils/agent/Agent'; +import DeviceClassifier from './src/DeviceClassifier'; export default () => { - return (openmct) => { - openmct.on("start", () => { - const agent = new Agent(window); - DeviceClassifier(agent, window.document); - }); - }; + return (openmct) => { + openmct.on('start', () => { + const agent = new Agent(window); + DeviceClassifier(agent, window.document); + }); + }; }; diff --git a/src/plugins/DeviceClassifier/src/DeviceClassifier.js b/src/plugins/DeviceClassifier/src/DeviceClassifier.js index 197c365f4f..bd4cf44152 100644 --- a/src/plugins/DeviceClassifier/src/DeviceClassifier.js +++ b/src/plugins/DeviceClassifier/src/DeviceClassifier.js @@ -38,34 +38,34 @@ * @param document the HTML DOM document object * @constructor */ -import DeviceMatchers from "./DeviceMatchers"; +import DeviceMatchers from './DeviceMatchers'; export default (agent, document) => { - const body = document.body; + const body = document.body; - Object.keys(DeviceMatchers).forEach((key, index, array) => { - if (DeviceMatchers[key](agent)) { - body.classList.add(key); - } - }); - - if (agent.isMobile()) { - const mediaQuery = window.matchMedia("(orientation: landscape)"); - function eventHandler(event) { - if (event.matches) { - body.classList.remove("portrait"); - body.classList.add("landscape"); - } else { - body.classList.remove("landscape"); - body.classList.add("portrait"); - } - } - - if (mediaQuery.addEventListener) { - mediaQuery.addEventListener(`change`, eventHandler); - } else { - // Deprecated 'MediaQueryList' API, { + if (DeviceMatchers[key](agent)) { + body.classList.add(key); } + }); + + if (agent.isMobile()) { + const mediaQuery = window.matchMedia('(orientation: landscape)'); + function eventHandler(event) { + if (event.matches) { + body.classList.remove('portrait'); + body.classList.add('landscape'); + } else { + body.classList.remove('landscape'); + body.classList.add('portrait'); + } + } + + if (mediaQuery.addEventListener) { + mediaQuery.addEventListener(`change`, eventHandler); + } else { + // Deprecated 'MediaQueryList' API, { - const ISO_KEY = 'iso'; - const JUNK = "junk"; - const MOON_LANDING_TIMESTAMP = -14256000000; - const MOON_LANDING_DATESTRING = '1969-07-20T00:00:00.000Z'; - let isoFormatter; +describe('the plugin', () => { + const ISO_KEY = 'iso'; + const JUNK = 'junk'; + const MOON_LANDING_TIMESTAMP = -14256000000; + const MOON_LANDING_DATESTRING = '1969-07-20T00:00:00.000Z'; + let isoFormatter; - beforeEach(() => { - isoFormatter = new ISOTimeFormat(); + beforeEach(() => { + isoFormatter = new ISOTimeFormat(); + }); + + describe('creates a new ISO based formatter', function () { + it("with the key 'iso'", () => { + expect(isoFormatter.key).toBe(ISO_KEY); }); - describe("creates a new ISO based formatter", function () { - - it("with the key 'iso'", () => { - expect(isoFormatter.key).toBe(ISO_KEY); - }); - - it("that will format a timestamp in ISO standard format", () => { - expect(isoFormatter.format(MOON_LANDING_TIMESTAMP)).toBe(MOON_LANDING_DATESTRING); - }); - - it("that will parse an ISO Date String into milliseconds", () => { - expect(isoFormatter.parse(MOON_LANDING_DATESTRING)).toBe(MOON_LANDING_TIMESTAMP); - }); - - it("that will validate correctly", () => { - expect(isoFormatter.validate(MOON_LANDING_DATESTRING)).toBe(true); - expect(isoFormatter.validate(JUNK)).toBe(false); - }); + it('that will format a timestamp in ISO standard format', () => { + expect(isoFormatter.format(MOON_LANDING_TIMESTAMP)).toBe(MOON_LANDING_DATESTRING); }); + + it('that will parse an ISO Date String into milliseconds', () => { + expect(isoFormatter.parse(MOON_LANDING_DATESTRING)).toBe(MOON_LANDING_TIMESTAMP); + }); + + it('that will validate correctly', () => { + expect(isoFormatter.validate(MOON_LANDING_DATESTRING)).toBe(true); + expect(isoFormatter.validate(JUNK)).toBe(false); + }); + }); }); diff --git a/src/plugins/LADTable/LADTableCompositionPolicy.js b/src/plugins/LADTable/LADTableCompositionPolicy.js index 98f3c533ac..0365f78dc0 100644 --- a/src/plugins/LADTable/LADTableCompositionPolicy.js +++ b/src/plugins/LADTable/LADTableCompositionPolicy.js @@ -21,13 +21,13 @@ *****************************************************************************/ export default function ladTableCompositionPolicy(openmct) { - return function (parent, child) { - if (parent.type === 'LadTable') { - return openmct.telemetry.isTelemetryObject(child); - } else if (parent.type === 'LadTableSet') { - return child.type === 'LadTable'; - } + return function (parent, child) { + if (parent.type === 'LadTable') { + return openmct.telemetry.isTelemetryObject(child); + } else if (parent.type === 'LadTableSet') { + return child.type === 'LadTable'; + } - return true; - }; + return true; + }; } diff --git a/src/plugins/LADTable/LADTableConfiguration.js b/src/plugins/LADTable/LADTableConfiguration.js index 64c7fc6158..87f7cdc6f0 100644 --- a/src/plugins/LADTable/LADTableConfiguration.js +++ b/src/plugins/LADTable/LADTableConfiguration.js @@ -23,35 +23,39 @@ import EventEmitter from 'EventEmitter'; export default class LADTableConfiguration extends EventEmitter { - constructor(domainObject, openmct) { - super(); + constructor(domainObject, openmct) { + super(); - this.domainObject = domainObject; - this.openmct = openmct; + this.domainObject = domainObject; + this.openmct = openmct; - this.objectMutated = this.objectMutated.bind(this); - this.unlistenFromMutation = openmct.objects.observe(domainObject, 'configuration', this.objectMutated); + this.objectMutated = this.objectMutated.bind(this); + this.unlistenFromMutation = openmct.objects.observe( + domainObject, + 'configuration', + this.objectMutated + ); + } + + getConfiguration() { + const configuration = this.domainObject.configuration || {}; + configuration.hiddenColumns = configuration.hiddenColumns || {}; + configuration.isFixedLayout = configuration.isFixedLayout ?? true; + + return configuration; + } + + updateConfiguration(configuration) { + this.openmct.objects.mutate(this.domainObject, 'configuration', configuration); + } + + objectMutated(configuration) { + if (configuration !== undefined) { + this.emit('change', configuration); } + } - getConfiguration() { - const configuration = this.domainObject.configuration || {}; - configuration.hiddenColumns = configuration.hiddenColumns || {}; - configuration.isFixedLayout = configuration.isFixedLayout ?? true; - - return configuration; - } - - updateConfiguration(configuration) { - this.openmct.objects.mutate(this.domainObject, 'configuration', configuration); - } - - objectMutated(configuration) { - if (configuration !== undefined) { - this.emit('change', configuration); - } - } - - destroy() { - this.unlistenFromMutation(); - } + destroy() { + this.unlistenFromMutation(); + } } diff --git a/src/plugins/LADTable/LADTableConfigurationViewProvider.js b/src/plugins/LADTable/LADTableConfigurationViewProvider.js index 8eac4a2996..5b1b460ece 100644 --- a/src/plugins/LADTable/LADTableConfigurationViewProvider.js +++ b/src/plugins/LADTable/LADTableConfigurationViewProvider.js @@ -24,44 +24,44 @@ import LADTableConfigurationComponent from './components/LADTableConfiguration.v import Vue from 'vue'; export default function LADTableConfigurationViewProvider(openmct) { - return { - key: 'lad-table-configuration', - name: 'LAD Table Configuration', - canView(selection) { - if (selection.length !== 1 || selection[0].length === 0) { - return false; - } + return { + key: 'lad-table-configuration', + name: 'LAD Table Configuration', + canView(selection) { + if (selection.length !== 1 || selection[0].length === 0) { + return false; + } - const object = selection[0][0].context.item; + const object = selection[0][0].context.item; - return object?.type === 'LadTable' || object?.type === 'LadTableSet'; + return object?.type === 'LadTable' || object?.type === 'LadTableSet'; + }, + view(selection) { + let component; + + return { + show(element) { + component = new Vue({ + el: element, + components: { + LADTableConfiguration: LADTableConfigurationComponent + }, + provide: { + openmct + }, + template: '' + }); }, - view(selection) { - let component; - - return { - show(element) { - component = new Vue({ - el: element, - components: { - LADTableConfiguration: LADTableConfigurationComponent - }, - provide: { - openmct - }, - template: '' - }); - }, - priority() { - return 1; - }, - destroy() { - if (component) { - component.$destroy(); - component = undefined; - } - } - }; + priority() { + return 1; + }, + destroy() { + if (component) { + component.$destroy(); + component = undefined; + } } - }; + }; + } + }; } diff --git a/src/plugins/LADTable/LADTableSetViewProvider.js b/src/plugins/LADTable/LADTableSetViewProvider.js index e2e5ed2a86..d5259906f2 100644 --- a/src/plugins/LADTable/LADTableSetViewProvider.js +++ b/src/plugins/LADTable/LADTableSetViewProvider.js @@ -23,21 +23,21 @@ import LadTableSetView from './LadTableSetView'; export default function LADTableSetViewProvider(openmct) { - return { - key: 'LadTableSet', - name: 'LAD Table Set', - cssClass: 'icon-tabular-lad-set', - canView: function (domainObject) { - return domainObject.type === 'LadTableSet'; - }, - canEdit: function (domainObject) { - return domainObject.type === 'LadTableSet'; - }, - view: function (domainObject, objectPath) { - return new LadTableSetView(openmct, domainObject, objectPath); - }, - priority: function () { - return 1; - } - }; + return { + key: 'LadTableSet', + name: 'LAD Table Set', + cssClass: 'icon-tabular-lad-set', + canView: function (domainObject) { + return domainObject.type === 'LadTableSet'; + }, + canEdit: function (domainObject) { + return domainObject.type === 'LadTableSet'; + }, + view: function (domainObject, objectPath) { + return new LadTableSetView(openmct, domainObject, objectPath); + }, + priority: function () { + return 1; + } + }; } diff --git a/src/plugins/LADTable/LADTableView.js b/src/plugins/LADTable/LADTableView.js index 5ce760e819..56f58ee5dc 100644 --- a/src/plugins/LADTable/LADTableView.js +++ b/src/plugins/LADTable/LADTableView.js @@ -25,46 +25,47 @@ import LADTableConfiguration from './LADTableConfiguration'; import Vue from 'vue'; export default class LADTableView { - constructor(openmct, domainObject, objectPath) { - this.openmct = openmct; - this.domainObject = domainObject; - this.objectPath = objectPath; - this.component = undefined; + constructor(openmct, domainObject, objectPath) { + this.openmct = openmct; + this.domainObject = domainObject; + this.objectPath = objectPath; + this.component = undefined; + } + + show(element) { + let ladTableConfiguration = new LADTableConfiguration(this.domainObject, this.openmct); + + this.component = new Vue({ + el: element, + components: { + LadTable + }, + provide: { + openmct: this.openmct, + currentView: this, + ladTableConfiguration + }, + data: () => { + return { + domainObject: this.domainObject, + objectPath: this.objectPath + }; + }, + template: + '' + }); + } + + getViewContext() { + if (!this.component) { + return {}; } - show(element) { - let ladTableConfiguration = new LADTableConfiguration(this.domainObject, this.openmct); + return this.component.$refs.ladTable.getViewContext(); + } - this.component = new Vue({ - el: element, - components: { - LadTable - }, - provide: { - openmct: this.openmct, - currentView: this, - ladTableConfiguration - }, - data: () => { - return { - domainObject: this.domainObject, - objectPath: this.objectPath - }; - }, - template: '' - }); - } - - getViewContext() { - if (!this.component) { - return {}; - } - - return this.component.$refs.ladTable.getViewContext(); - } - - destroy(element) { - this.component.$destroy(); - this.component = undefined; - } + destroy(element) { + this.component.$destroy(); + this.component = undefined; + } } diff --git a/src/plugins/LADTable/LADTableViewProvider.js b/src/plugins/LADTable/LADTableViewProvider.js index d189cbe784..29b70127de 100644 --- a/src/plugins/LADTable/LADTableViewProvider.js +++ b/src/plugins/LADTable/LADTableViewProvider.js @@ -23,33 +23,31 @@ import LADTableView from './LADTableView'; export default class LADTableViewProvider { - constructor(openmct) { - this.openmct = openmct; - this.name = 'LAD Table'; - this.key = 'LadTable'; - this.cssClass = 'icon-tabular-lad'; - } + constructor(openmct) { + this.openmct = openmct; + this.name = 'LAD Table'; + this.key = 'LadTable'; + this.cssClass = 'icon-tabular-lad'; + } - canView(domainObject) { - const supportsComposition = this.openmct.composition.supportsComposition(domainObject); - const providesTelemetry = this.openmct.telemetry.isTelemetryObject(domainObject); - const isLadTable = domainObject.type === 'LadTable'; - const isConditionSet = domainObject.type === 'conditionSet'; + canView(domainObject) { + const supportsComposition = this.openmct.composition.supportsComposition(domainObject); + const providesTelemetry = this.openmct.telemetry.isTelemetryObject(domainObject); + const isLadTable = domainObject.type === 'LadTable'; + const isConditionSet = domainObject.type === 'conditionSet'; - return !isConditionSet - && (isLadTable - || (providesTelemetry && supportsComposition)); - } + return !isConditionSet && (isLadTable || (providesTelemetry && supportsComposition)); + } - canEdit(domainObject) { - return domainObject.type === 'LadTable'; - } + canEdit(domainObject) { + return domainObject.type === 'LadTable'; + } - view(domainObject, objectPath) { - return new LADTableView(this.openmct, domainObject, objectPath); - } + view(domainObject, objectPath) { + return new LADTableView(this.openmct, domainObject, objectPath); + } - priority(domainObject) { - return this.openmct.priority.HIGH; - } + priority(domainObject) { + return this.openmct.priority.HIGH; + } } diff --git a/src/plugins/LADTable/LadTableSetView.js b/src/plugins/LADTable/LadTableSetView.js index 165d9760f9..35de8b268d 100644 --- a/src/plugins/LADTable/LadTableSetView.js +++ b/src/plugins/LADTable/LadTableSetView.js @@ -25,46 +25,46 @@ import LADTableConfiguration from './LADTableConfiguration'; import Vue from 'vue'; export default class LadTableSetView { - constructor(openmct, domainObject, objectPath) { - this.openmct = openmct; - this.domainObject = domainObject; - this.objectPath = objectPath; - this.component = undefined; + constructor(openmct, domainObject, objectPath) { + this.openmct = openmct; + this.domainObject = domainObject; + this.objectPath = objectPath; + this.component = undefined; + } + + show(element) { + let ladTableConfiguration = new LADTableConfiguration(this.domainObject, this.openmct); + + this.component = new Vue({ + el: element, + components: { + LadTableSet + }, + provide: { + openmct: this.openmct, + objectPath: this.objectPath, + currentView: this, + ladTableConfiguration + }, + data: () => { + return { + domainObject: this.domainObject + }; + }, + template: '' + }); + } + + getViewContext() { + if (!this.component) { + return {}; } - show(element) { - let ladTableConfiguration = new LADTableConfiguration(this.domainObject, this.openmct); + return this.component.$refs.ladTableSet.getViewContext(); + } - this.component = new Vue({ - el: element, - components: { - LadTableSet - }, - provide: { - openmct: this.openmct, - objectPath: this.objectPath, - currentView: this, - ladTableConfiguration - }, - data: () => { - return { - domainObject: this.domainObject - }; - }, - template: '' - }); - } - - getViewContext() { - if (!this.component) { - return {}; - } - - return this.component.$refs.ladTableSet.getViewContext(); - } - - destroy(element) { - this.component.$destroy(); - this.component = undefined; - } + destroy(element) { + this.component.$destroy(); + this.component = undefined; + } } diff --git a/src/plugins/LADTable/ViewActions.js b/src/plugins/LADTable/ViewActions.js index f243b92e95..58462b193b 100644 --- a/src/plugins/LADTable/ViewActions.js +++ b/src/plugins/LADTable/ViewActions.js @@ -21,43 +21,40 @@ *****************************************************************************/ const expandColumns = { - name: 'Expand Columns', - key: 'lad-expand-columns', - description: "Increase column widths to fit currently available data.", - cssClass: 'icon-arrows-right-left labeled', - invoke: (objectPath, view) => { - view.getViewContext().toggleFixedLayout(); - }, - showInStatusBar: true, - group: 'view' + name: 'Expand Columns', + key: 'lad-expand-columns', + description: 'Increase column widths to fit currently available data.', + cssClass: 'icon-arrows-right-left labeled', + invoke: (objectPath, view) => { + view.getViewContext().toggleFixedLayout(); + }, + showInStatusBar: true, + group: 'view' }; const autosizeColumns = { - name: 'Autosize Columns', - key: 'lad-autosize-columns', - description: "Automatically size columns to fit the table into the available space.", - cssClass: 'icon-expand labeled', - invoke: (objectPath, view) => { - view.getViewContext().toggleFixedLayout(); - }, - showInStatusBar: true, - group: 'view' + name: 'Autosize Columns', + key: 'lad-autosize-columns', + description: 'Automatically size columns to fit the table into the available space.', + cssClass: 'icon-expand labeled', + invoke: (objectPath, view) => { + view.getViewContext().toggleFixedLayout(); + }, + showInStatusBar: true, + group: 'view' }; -const viewActions = [ - expandColumns, - autosizeColumns -]; +const viewActions = [expandColumns, autosizeColumns]; -viewActions.forEach(action => { - action.appliesTo = (objectPath, view = {}) => { - const viewContext = view.getViewContext && view.getViewContext(); - if (!viewContext) { - return false; - } +viewActions.forEach((action) => { + action.appliesTo = (objectPath, view = {}) => { + const viewContext = view.getViewContext && view.getViewContext(); + if (!viewContext) { + return false; + } - return viewContext.type === 'lad-table'; - }; + return viewContext.type === 'lad-table'; + }; }); export default viewActions; diff --git a/src/plugins/LADTable/components/LADRow.vue b/src/plugins/LADTable/components/LADRow.vue index 9eef1c8aa5..5f292d0f4c 100644 --- a/src/plugins/LADTable/components/LADRow.vue +++ b/src/plugins/LADTable/components/LADRow.vue @@ -21,260 +21,245 @@ --> diff --git a/src/plugins/LADTable/components/LADTable.vue b/src/plugins/LADTable/components/LADTable.vue index 96c5ef88bc..0bdbeb6cef 100644 --- a/src/plugins/LADTable/components/LADTable.vue +++ b/src/plugins/LADTable/components/LADTable.vue @@ -21,37 +21,31 @@ --> diff --git a/src/plugins/LADTable/components/LADTableConfiguration.vue b/src/plugins/LADTable/components/LADTableConfiguration.vue index bd144f6a97..1981b9d975 100644 --- a/src/plugins/LADTable/components/LADTableConfiguration.vue +++ b/src/plugins/LADTable/components/LADTableConfiguration.vue @@ -21,230 +21,219 @@ --> diff --git a/src/plugins/LADTable/components/LadTableSet.vue b/src/plugins/LADTable/components/LadTableSet.vue index f1c17549f4..e9ebb414ba 100644 --- a/src/plugins/LADTable/components/LadTableSet.vue +++ b/src/plugins/LADTable/components/LadTableSet.vue @@ -21,248 +21,258 @@ --> diff --git a/src/plugins/LADTable/plugin.js b/src/plugins/LADTable/plugin.js index e832fb44dd..c10757a3c7 100644 --- a/src/plugins/LADTable/plugin.js +++ b/src/plugins/LADTable/plugin.js @@ -26,36 +26,36 @@ import LADTableConfigurationViewProvider from './LADTableConfigurationViewProvid import LADTableViewActions from './ViewActions'; export default function plugin() { - return function install(openmct) { + return function install(openmct) { + openmct.objectViews.addProvider(new LADTableViewProvider(openmct)); + openmct.objectViews.addProvider(new LADTableSetViewProvider(openmct)); + openmct.inspectorViews.addProvider(new LADTableConfigurationViewProvider(openmct)); - openmct.objectViews.addProvider(new LADTableViewProvider(openmct)); - openmct.objectViews.addProvider(new LADTableSetViewProvider(openmct)); - openmct.inspectorViews.addProvider(new LADTableConfigurationViewProvider(openmct)); + openmct.types.addType('LadTable', { + name: 'LAD Table', + creatable: true, + 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 = []; + } + }); - openmct.types.addType('LadTable', { - name: "LAD Table", - creatable: true, - 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 = []; - } - }); + openmct.types.addType('LadTableSet', { + name: 'LAD Table Set', + creatable: true, + description: 'Group LAD Tables together into a single view with sub-headers.', + cssClass: 'icon-tabular-lad-set', + initialize(domainObject) { + domainObject.composition = []; + } + }); - openmct.types.addType('LadTableSet', { - name: "LAD Table Set", - creatable: true, - description: "Group LAD Tables together into a single view with sub-headers.", - cssClass: 'icon-tabular-lad-set', - initialize(domainObject) { - domainObject.composition = []; - } - }); + openmct.composition.addPolicy(ladTableCompositionPolicy(openmct)); - openmct.composition.addPolicy(ladTableCompositionPolicy(openmct)); - - LADTableViewActions.forEach(action => { - openmct.actions.register(action); - }); - }; + LADTableViewActions.forEach((action) => { + openmct.actions.register(action); + }); + }; } diff --git a/src/plugins/LADTable/pluginSpec.js b/src/plugins/LADTable/pluginSpec.js index 5cc595e6d5..a317d8b9cd 100644 --- a/src/plugins/LADTable/pluginSpec.js +++ b/src/plugins/LADTable/pluginSpec.js @@ -22,12 +22,12 @@ import LadPlugin from './plugin.js'; import Vue from 'vue'; import { - createOpenMct, - getMockObjects, - getMockTelemetry, - getLatestTelemetry, - spyOnBuiltins, - resetApplicationState + createOpenMct, + getMockObjects, + getMockTelemetry, + getLatestTelemetry, + spyOnBuiltins, + resetApplicationState } from 'utils/testing'; const TABLE_BODY_ROWS = '.js-lad-table__body__row'; @@ -38,388 +38,401 @@ const TABLE_BODY_FIRST_ROW_THIRD_DATA = TABLE_BODY_FIRST_ROW + ' .js-third-data' const LAD_SET_TABLE_HEADERS = '.js-lad-table-set__table-headers'; function utcTimeFormat(value) { - return new Date(value).toISOString().replace('T', ' '); + return new Date(value).toISOString().replace('T', ' '); } -describe("The LAD Table", () => { - const ladTableKey = 'LadTable'; +describe('The LAD Table', () => { + const ladTableKey = 'LadTable'; - let openmct; - let ladPlugin; - let historicalProvider; - let parent; - let child; - let telemetryCount = 3; - let timeFormat = 'utc'; - let mockTelemetry = getMockTelemetry({ - count: telemetryCount, - format: timeFormat - }); - let mockObj = getMockObjects({ - objectKeyStrings: ['ladTable', 'telemetry'], - format: timeFormat - }); - let bounds = { - start: 0, - end: 4 + let openmct; + let ladPlugin; + let historicalProvider; + let parent; + let child; + let telemetryCount = 3; + let timeFormat = 'utc'; + let mockTelemetry = getMockTelemetry({ + count: telemetryCount, + format: timeFormat + }); + let mockObj = getMockObjects({ + objectKeyStrings: ['ladTable', 'telemetry'], + format: timeFormat + }); + let bounds = { + start: 0, + end: 4 + }; + + // add telemetry object as composition in lad table + mockObj.ladTable.composition.push(mockObj.telemetry.identifier); + + // this setups up the app + beforeEach((done) => { + openmct = createOpenMct(); + + parent = document.createElement('div'); + child = document.createElement('div'); + parent.appendChild(child); + + spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([])); + + ladPlugin = new LadPlugin(); + openmct.install(ladPlugin); + + spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({})); + + historicalProvider = { + request: () => { + return Promise.resolve([]); + } }; + spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider); - // add telemetry object as composition in lad table - mockObj.ladTable.composition.push(mockObj.telemetry.identifier); - - // this setups up the app - beforeEach((done) => { - openmct = createOpenMct(); - - parent = document.createElement('div'); - child = document.createElement('div'); - parent.appendChild(child); - - spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([])); - - ladPlugin = new LadPlugin(); - openmct.install(ladPlugin); - - spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({})); - - historicalProvider = { - request: () => { - return Promise.resolve([]); - } - }; - spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider); - - openmct.time.bounds({ - start: bounds.start, - end: bounds.end - }); - - openmct.on('start', done); - openmct.startHeadless(); + openmct.time.bounds({ + start: bounds.start, + end: bounds.end }); - afterEach(() => { - return resetApplicationState(openmct); + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('should provide a table view only for lad table objects', () => { + let applicableViews = openmct.objectViews.get(mockObj.ladTable, []); + + let ladTableView = applicableViews.find((viewProvider) => viewProvider.key === ladTableKey); + + expect(applicableViews.length).toEqual(1); + expect(ladTableView).toBeDefined(); + }); + + describe('composition', () => { + let ladTableCompositionCollection; + + beforeEach(() => { + ladTableCompositionCollection = openmct.composition.get(mockObj.ladTable); + + return ladTableCompositionCollection.load(); }); - it("should provide a table view only for lad table objects", () => { - let applicableViews = openmct.objectViews.get(mockObj.ladTable, []); - - let ladTableView = applicableViews.find( - (viewProvider) => viewProvider.key === ladTableKey - ); - - expect(applicableViews.length).toEqual(1); - expect(ladTableView).toBeDefined(); + it('should accept telemetry producing objects', () => { + expect(() => { + ladTableCompositionCollection.add(mockObj.telemetry); + }).not.toThrow(); }); - describe('composition', () => { - let ladTableCompositionCollection; + it('should reject non-telemtry producing objects', () => { + expect(() => { + ladTableCompositionCollection.add(mockObj.ladTable); + }).toThrow(); + }); + }); - beforeEach(() => { - ladTableCompositionCollection = openmct.composition.get(mockObj.ladTable); + describe('table view', () => { + let applicableViews; + let ladTableViewProvider; + let ladTableView; + let anotherTelemetryObj = getMockObjects({ + objectKeyStrings: ['telemetry'], + overwrite: { + telemetry: { + name: 'New Telemetry Object', + identifier: { + namespace: '', + key: 'another-telemetry-object' + } + } + } + }).telemetry; - return ladTableCompositionCollection.load(); - }); + const aggregateTelemetryObj = getMockObjects({ + objectKeyStrings: ['telemetry'], + overwrite: { + telemetry: { + name: 'Aggregate Telemetry Object', + identifier: { + namespace: '', + key: 'aggregate-telemetry-object' + } + } + } + }).telemetry; - it("should accept telemetry producing objects", () => { - expect(() => { - ladTableCompositionCollection.add(mockObj.telemetry); - }).not.toThrow(); - }); + // add another aggregate telemetry object as composition in lad table to test multi rows + aggregateTelemetryObj.composition = [anotherTelemetryObj.identifier]; + mockObj.ladTable.composition.push(aggregateTelemetryObj.identifier); - it("should reject non-telemtry producing objects", () => { - expect(() => { - ladTableCompositionCollection.add(mockObj.ladTable); - }).toThrow(); - }); + beforeEach(async () => { + let telemetryRequestResolve; + let telemetryObjectResolve; + let anotherTelemetryObjectResolve; + let aggregateTelemetryObjectResolve; + const telemetryRequestPromise = new Promise((resolve) => { + telemetryRequestResolve = resolve; + }); + const telemetryObjectPromise = new Promise((resolve) => { + telemetryObjectResolve = resolve; + }); + const anotherTelemetryObjectPromise = new Promise((resolve) => { + anotherTelemetryObjectResolve = resolve; + }); + + const aggregateTelemetryObjectPromise = new Promise((resolve) => { + aggregateTelemetryObjectResolve = resolve; + }); + + spyOnBuiltins(['requestAnimationFrame']); + window.requestAnimationFrame.and.callFake((callBack) => { + callBack(); + }); + + historicalProvider.request = () => { + telemetryRequestResolve(mockTelemetry); + + return telemetryRequestPromise; + }; + + openmct.objects.get.and.callFake((obj) => { + if (obj.key === 'telemetry-object') { + telemetryObjectResolve(mockObj.telemetry); + + return telemetryObjectPromise; + } else if (obj.key === 'another-telemetry-object') { + anotherTelemetryObjectResolve(anotherTelemetryObj); + + return anotherTelemetryObjectPromise; + } else { + aggregateTelemetryObjectResolve(aggregateTelemetryObj); + + return aggregateTelemetryObjectPromise; + } + }); + + openmct.time.bounds({ + start: bounds.start, + end: bounds.end + }); + + applicableViews = openmct.objectViews.get(mockObj.ladTable, []); + ladTableViewProvider = applicableViews.find( + (viewProvider) => viewProvider.key === ladTableKey + ); + ladTableView = ladTableViewProvider.view(mockObj.ladTable, [mockObj.ladTable]); + ladTableView.show(child, true); + + await Promise.all([ + telemetryRequestPromise, + telemetryObjectPromise, + anotherTelemetryObjectPromise, + aggregateTelemetryObjectResolve + ]); + await Vue.nextTick(); }); - describe("table view", () => { - let applicableViews; - let ladTableViewProvider; - let ladTableView; - let anotherTelemetryObj = getMockObjects({ - objectKeyStrings: ['telemetry'], - overwrite: { - telemetry: { - name: "New Telemetry Object", - identifier: { - namespace: "", - key: "another-telemetry-object" - } - } - } - }).telemetry; - - const aggregateTelemetryObj = getMockObjects({ - objectKeyStrings: ['telemetry'], - overwrite: { - telemetry: { - name: "Aggregate Telemetry Object", - identifier: { - namespace: "", - key: "aggregate-telemetry-object" - } - } - } - }).telemetry; - - // add another aggregate telemetry object as composition in lad table to test multi rows - aggregateTelemetryObj.composition = [anotherTelemetryObj.identifier]; - mockObj.ladTable.composition.push(aggregateTelemetryObj.identifier); - - beforeEach(async () => { - let telemetryRequestResolve; - let telemetryObjectResolve; - let anotherTelemetryObjectResolve; - let aggregateTelemetryObjectResolve; - const telemetryRequestPromise = new Promise((resolve) => { - telemetryRequestResolve = resolve; - }); - const telemetryObjectPromise = new Promise((resolve) => { - telemetryObjectResolve = resolve; - }); - const anotherTelemetryObjectPromise = new Promise((resolve) => { - anotherTelemetryObjectResolve = resolve; - }); - - const aggregateTelemetryObjectPromise = new Promise((resolve) => { - aggregateTelemetryObjectResolve = resolve; - }); - - spyOnBuiltins(['requestAnimationFrame']); - window.requestAnimationFrame.and.callFake((callBack) => { - callBack(); - }); - - historicalProvider.request = () => { - telemetryRequestResolve(mockTelemetry); - - return telemetryRequestPromise; - }; - - openmct.objects.get.and.callFake((obj) => { - if (obj.key === 'telemetry-object') { - telemetryObjectResolve(mockObj.telemetry); - - return telemetryObjectPromise; - } else if (obj.key === 'another-telemetry-object') { - anotherTelemetryObjectResolve(anotherTelemetryObj); - - return anotherTelemetryObjectPromise; - } else { - aggregateTelemetryObjectResolve(aggregateTelemetryObj); - - return aggregateTelemetryObjectPromise; - } - }); - - openmct.time.bounds({ - start: bounds.start, - end: bounds.end - }); - - applicableViews = openmct.objectViews.get(mockObj.ladTable, []); - ladTableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === ladTableKey); - ladTableView = ladTableViewProvider.view(mockObj.ladTable, [mockObj.ladTable]); - ladTableView.show(child, true); - - await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise, aggregateTelemetryObjectResolve]); - await Vue.nextTick(); - }); - - it("should show one row per object in the composition", () => { - const rowCount = parent.querySelectorAll(TABLE_BODY_ROWS).length; - expect(rowCount).toBe(mockObj.ladTable.composition.length); - }); - - it("should show the most recent datum from the telemetry producing object", async () => { - const latestDatum = getLatestTelemetry(mockTelemetry, { timeFormat }); - const expectedDate = utcTimeFormat(latestDatum[timeFormat]); - await Vue.nextTick(); - const latestDate = parent.querySelector(TABLE_BODY_FIRST_ROW_SECOND_DATA).innerText; - expect(latestDate).toBe(expectedDate); - const dataType = parent.querySelector(TABLE_BODY_ROWS).querySelector('.js-type-data').innerText; - expect(dataType).toBe('Telemetry'); - }); - - it("should show aggregate telemetry type with blank data", async () => { - await Vue.nextTick(); - const lastestData = parent.querySelectorAll(TABLE_BODY_ROWS)[1].querySelectorAll('td')[2].innerText; - expect(lastestData).toBe('---'); - const dataType = parent.querySelectorAll(TABLE_BODY_ROWS)[1].querySelector('.js-type-data').innerText; - expect(dataType).toBe('Aggregate'); - }); - - it("should show the name provided for the the telemetry producing object", () => { - const rowName = parent.querySelector(TABLE_BODY_FIRST_ROW_FIRST_DATA).innerText; - - const expectedName = mockObj.telemetry.name; - expect(rowName).toBe(expectedName); - }); - - it("should show the correct values for the datum based on domain and range hints", async () => { - const range = mockObj.telemetry.telemetry.values.find((val) => { - return val.hints && val.hints.range !== undefined; - }).key; - const domain = mockObj.telemetry.telemetry.values.find((val) => { - return val.hints && val.hints.domain !== undefined; - }).key; - const mostRecentTelemetry = getLatestTelemetry(mockTelemetry, { timeFormat }); - const rangeValue = mostRecentTelemetry[range]; - const domainValue = utcTimeFormat(mostRecentTelemetry[domain]); - await Vue.nextTick(); - const actualDomainValue = parent.querySelector(TABLE_BODY_FIRST_ROW_SECOND_DATA).innerText; - const actualRangeValue = parent.querySelector(TABLE_BODY_FIRST_ROW_THIRD_DATA).innerText; - expect(actualRangeValue).toBe(rangeValue); - expect(actualDomainValue).toBe(domainValue); - }); + it('should show one row per object in the composition', () => { + const rowCount = parent.querySelectorAll(TABLE_BODY_ROWS).length; + expect(rowCount).toBe(mockObj.ladTable.composition.length); }); + + it('should show the most recent datum from the telemetry producing object', async () => { + const latestDatum = getLatestTelemetry(mockTelemetry, { timeFormat }); + const expectedDate = utcTimeFormat(latestDatum[timeFormat]); + await Vue.nextTick(); + const latestDate = parent.querySelector(TABLE_BODY_FIRST_ROW_SECOND_DATA).innerText; + expect(latestDate).toBe(expectedDate); + const dataType = parent + .querySelector(TABLE_BODY_ROWS) + .querySelector('.js-type-data').innerText; + expect(dataType).toBe('Telemetry'); + }); + + it('should show aggregate telemetry type with blank data', async () => { + await Vue.nextTick(); + const lastestData = parent + .querySelectorAll(TABLE_BODY_ROWS)[1] + .querySelectorAll('td')[2].innerText; + expect(lastestData).toBe('---'); + const dataType = parent + .querySelectorAll(TABLE_BODY_ROWS)[1] + .querySelector('.js-type-data').innerText; + expect(dataType).toBe('Aggregate'); + }); + + it('should show the name provided for the the telemetry producing object', () => { + const rowName = parent.querySelector(TABLE_BODY_FIRST_ROW_FIRST_DATA).innerText; + + const expectedName = mockObj.telemetry.name; + expect(rowName).toBe(expectedName); + }); + + it('should show the correct values for the datum based on domain and range hints', async () => { + const range = mockObj.telemetry.telemetry.values.find((val) => { + return val.hints && val.hints.range !== undefined; + }).key; + const domain = mockObj.telemetry.telemetry.values.find((val) => { + return val.hints && val.hints.domain !== undefined; + }).key; + const mostRecentTelemetry = getLatestTelemetry(mockTelemetry, { timeFormat }); + const rangeValue = mostRecentTelemetry[range]; + const domainValue = utcTimeFormat(mostRecentTelemetry[domain]); + await Vue.nextTick(); + const actualDomainValue = parent.querySelector(TABLE_BODY_FIRST_ROW_SECOND_DATA).innerText; + const actualRangeValue = parent.querySelector(TABLE_BODY_FIRST_ROW_THIRD_DATA).innerText; + expect(actualRangeValue).toBe(rangeValue); + expect(actualDomainValue).toBe(domainValue); + }); + }); }); -describe("The LAD Table Set", () => { - const ladTableSetKey = 'LadTableSet'; +describe('The LAD Table Set', () => { + const ladTableSetKey = 'LadTableSet'; - let openmct; - let ladPlugin; - let parent; - let child; + let openmct; + let ladPlugin; + let parent; + let child; - let mockObj = getMockObjects({ - objectKeyStrings: ['ladTable', 'ladTableSet', 'telemetry'] + let mockObj = getMockObjects({ + objectKeyStrings: ['ladTable', 'ladTableSet', 'telemetry'] + }); + + let bounds = { + start: 0, + end: 4 + }; + + // add mock telemetry to lad table and lad table to lad table set (composition) + mockObj.ladTable.composition.push(mockObj.telemetry.identifier); + mockObj.ladTableSet.composition.push(mockObj.ladTable.identifier); + + beforeEach((done) => { + openmct = createOpenMct(); + + parent = document.createElement('div'); + child = document.createElement('div'); + parent.appendChild(child); + + ladPlugin = new LadPlugin(); + openmct.install(ladPlugin); + + openmct.time.bounds({ + start: bounds.start, + end: bounds.end }); - let bounds = { - start: 0, - end: 4 - }; + openmct.on('start', done); + openmct.startHeadless(); + }); - // add mock telemetry to lad table and lad table to lad table set (composition) - mockObj.ladTable.composition.push(mockObj.telemetry.identifier); - mockObj.ladTableSet.composition.push(mockObj.ladTable.identifier); - - beforeEach((done) => { - openmct = createOpenMct(); - - parent = document.createElement('div'); - child = document.createElement('div'); - parent.appendChild(child); - - ladPlugin = new LadPlugin(); - openmct.install(ladPlugin); - - openmct.time.bounds({ - start: bounds.start, - end: bounds.end - }); - - openmct.on('start', done); - openmct.startHeadless(); + afterEach(() => { + openmct.time.timeSystem('utc', { + start: 0, + end: 1 }); - afterEach(() => { - openmct.time.timeSystem('utc', { - start: 0, - end: 1 - }); + return resetApplicationState(openmct); + }); - return resetApplicationState(openmct); + it('should provide a lad table set view only for lad table set objects', () => { + spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({})); + + let applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []); + + let ladTableSetView = applicableViews.find( + (viewProvider) => viewProvider.key === ladTableSetKey + ); + + expect(applicableViews.length).toEqual(1); + expect(ladTableSetView).toBeDefined(); + }); + + describe('composition', () => { + let ladTableSetCompositionCollection; + + beforeEach(() => { + spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({})); + + ladTableSetCompositionCollection = openmct.composition.get(mockObj.ladTableSet); + + return ladTableSetCompositionCollection.load(); }); - it("should provide a lad table set view only for lad table set objects", () => { - spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({})); - - let applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []); - - let ladTableSetView = applicableViews.find( - (viewProvider) => viewProvider.key === ladTableSetKey - ); - - expect(applicableViews.length).toEqual(1); - expect(ladTableSetView).toBeDefined(); + it('should accept lad table objects', () => { + expect(() => { + ladTableSetCompositionCollection.add(mockObj.ladTable); + }).not.toThrow(); }); - describe('composition', () => { - let ladTableSetCompositionCollection; + it('should reject non lad table objects', () => { + expect(() => { + ladTableSetCompositionCollection.add(mockObj.telemetry); + }).toThrow(); + }); + }); - beforeEach(() => { - spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({})); + describe('table view', () => { + let applicableViews; + let ladTableSetViewProvider; + let ladTableSetView; - ladTableSetCompositionCollection = openmct.composition.get(mockObj.ladTableSet); - - return ladTableSetCompositionCollection.load(); - }); - - it("should accept lad table objects", () => { - expect(() => { - ladTableSetCompositionCollection.add(mockObj.ladTable); - }).not.toThrow(); - }); - - it("should reject non lad table objects", () => { - expect(() => { - ladTableSetCompositionCollection.add(mockObj.telemetry); - }).toThrow(); - }); + let otherObj = getMockObjects({ + objectKeyStrings: ['ladTable'], + overwrite: { + ladTable: { + name: 'New LAD Table Object', + identifier: { + namespace: '', + key: 'another-lad-object' + } + } + } }); - describe("table view", () => { - let applicableViews; - let ladTableSetViewProvider; - let ladTableSetView; + // add another lad table (with telemetry object) object to the lad table set for multi row test + otherObj.ladTable.composition.push(mockObj.telemetry.identifier); + mockObj.ladTableSet.composition.push(otherObj.ladTable.identifier); - let otherObj = getMockObjects({ - objectKeyStrings: ['ladTable'], - overwrite: { - ladTable: { - name: "New LAD Table Object", - identifier: { - namespace: "", - key: "another-lad-object" - } - } - } - }); + beforeEach(() => { + spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([])); - // add another lad table (with telemetry object) object to the lad table set for multi row test - otherObj.ladTable.composition.push(mockObj.telemetry.identifier); - mockObj.ladTableSet.composition.push(otherObj.ladTable.identifier); + spyOn(openmct.objects, 'get').and.callFake((obj) => { + if (obj.key === 'lad-object') { + return Promise.resolve(mockObj.ladTable); + } else if (obj.key === 'another-lad-object') { + return Promise.resolve(otherObj.ladTable); + } else if (obj.key === 'telemetry-object') { + return Promise.resolve(mockObj.telemetry); + } + }); - beforeEach(() => { - spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([])); + openmct.time.bounds({ + start: bounds.start, + end: bounds.end + }); - spyOn(openmct.objects, 'get').and.callFake((obj) => { - if (obj.key === 'lad-object') { - return Promise.resolve(mockObj.ladTable); - } else if (obj.key === 'another-lad-object') { - return Promise.resolve(otherObj.ladTable); - } else if (obj.key === 'telemetry-object') { - return Promise.resolve(mockObj.telemetry); - } - }); + applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []); + ladTableSetViewProvider = applicableViews.find( + (viewProvider) => viewProvider.key === ladTableSetKey + ); + ladTableSetView = ladTableSetViewProvider.view(mockObj.ladTableSet, [mockObj.ladTableSet]); + ladTableSetView.show(child); - openmct.time.bounds({ - start: bounds.start, - end: bounds.end - }); - - applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []); - ladTableSetViewProvider = applicableViews.find((viewProvider) => viewProvider.key === ladTableSetKey); - ladTableSetView = ladTableSetViewProvider.view(mockObj.ladTableSet, [mockObj.ladTableSet]); - ladTableSetView.show(child); - - return Vue.nextTick(); - }); - - it("should show one row per lad table object in the composition", () => { - const ladTableSetCompositionCollection = openmct.composition.get(mockObj.ladTableSet); - - return ladTableSetCompositionCollection.load().then(() => { - const rowCount = parent.querySelectorAll(LAD_SET_TABLE_HEADERS).length; - - expect(rowCount).toBe(mockObj.ladTableSet.composition.length); - }); - }); + return Vue.nextTick(); }); + + it('should show one row per lad table object in the composition', () => { + const ladTableSetCompositionCollection = openmct.composition.get(mockObj.ladTableSet); + + return ladTableSetCompositionCollection.load().then(() => { + const rowCount = parent.querySelectorAll(LAD_SET_TABLE_HEADERS).length; + + expect(rowCount).toBe(mockObj.ladTableSet.composition.length); + }); + }); + }); }); diff --git a/src/plugins/URLIndicatorPlugin/URLIndicator.js b/src/plugins/URLIndicatorPlugin/URLIndicator.js index 12dfb50282..f8a3593235 100644 --- a/src/plugins/URLIndicatorPlugin/URLIndicator.js +++ b/src/plugins/URLIndicatorPlugin/URLIndicator.js @@ -20,93 +20,96 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -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. - // DISCONNECTED: HTTP failed; maybe misconfigured, disconnected. - // PENDING: Still trying to connect, and haven't failed yet. - const CONNECTED = { - statusClass: "s-status-on" - }; - const PENDING = { - statusClass: "s-status-warning-lo" - }; - const DISCONNECTED = { - statusClass: "s-status-warning-hi" - }; - function URLIndicator(options, simpleIndicator) { - this.bindMethods(); - this.count = 0; +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. + // DISCONNECTED: HTTP failed; maybe misconfigured, disconnected. + // PENDING: Still trying to connect, and haven't failed yet. + const CONNECTED = { + statusClass: 's-status-on' + }; + const PENDING = { + statusClass: 's-status-warning-lo' + }; + const DISCONNECTED = { + statusClass: 's-status-warning-hi' + }; + function URLIndicator(options, simpleIndicator) { + this.bindMethods(); + this.count = 0; - this.indicator = simpleIndicator; - this.setDefaultsFromOptions(options); - this.setIndicatorToState(PENDING); + this.indicator = simpleIndicator; + this.setDefaultsFromOptions(options); + this.setIndicatorToState(PENDING); - this.fetchUrl(); - setInterval(this.fetchUrl, this.interval); + this.fetchUrl(); + setInterval(this.fetchUrl, this.interval); + } + + URLIndicator.prototype.setIndicatorToState = function (state) { + switch (state) { + case CONNECTED: { + this.indicator.text(this.label + ' is connected'); + this.indicator.description( + this.label + ' is online, checking status every ' + this.interval + ' milliseconds.' + ); + break; + } + + case PENDING: { + this.indicator.text('Checking status of ' + this.label + ' please stand by...'); + this.indicator.description('Checking status of ' + this.label + ' please stand by...'); + break; + } + + case DISCONNECTED: { + this.indicator.text(this.label + ' is offline'); + this.indicator.description( + this.label + ' is offline, checking status every ' + this.interval + ' milliseconds' + ); + break; + } + } + + this.indicator.statusClass(state.statusClass); + }; + + URLIndicator.prototype.fetchUrl = function () { + fetch(this.URLpath) + .then((response) => { + if (response.ok) { + this.handleSuccess(); + } else { + this.handleError(); } + }) + .catch((error) => { + this.handleError(); + }); + }; - URLIndicator.prototype.setIndicatorToState = function (state) { - switch (state) { - case CONNECTED: { - this.indicator.text(this.label + " is connected"); - this.indicator.description(this.label + " is online, checking status every " + this.interval + " milliseconds."); - break; - } + URLIndicator.prototype.handleError = function (e) { + this.setIndicatorToState(DISCONNECTED); + }; - case PENDING: { - this.indicator.text("Checking status of " + this.label + " please stand by..."); - this.indicator.description("Checking status of " + this.label + " please stand by..."); - break; - } + URLIndicator.prototype.handleSuccess = function () { + this.setIndicatorToState(CONNECTED); + }; - case DISCONNECTED: { - this.indicator.text(this.label + " is offline"); - this.indicator.description(this.label + " is offline, checking status every " + this.interval + " milliseconds"); - break; - } - } + URLIndicator.prototype.setDefaultsFromOptions = function (options) { + this.URLpath = options.url; + this.label = options.label || options.url; + this.interval = options.interval || 10000; + this.indicator.iconClass(options.iconClass || 'icon-chain-links'); + }; - this.indicator.statusClass(state.statusClass); - }; + URLIndicator.prototype.bindMethods = function () { + this.fetchUrl = this.fetchUrl.bind(this); + this.handleSuccess = this.handleSuccess.bind(this); + this.handleError = this.handleError.bind(this); + this.setIndicatorToState = this.setIndicatorToState.bind(this); + }; - URLIndicator.prototype.fetchUrl = function () { - fetch(this.URLpath) - .then(response => { - if (response.ok) { - this.handleSuccess(); - } else { - this.handleError(); - } - }) - .catch(error => { - this.handleError(); - }); - }; - - URLIndicator.prototype.handleError = function (e) { - this.setIndicatorToState(DISCONNECTED); - }; - - URLIndicator.prototype.handleSuccess = function () { - this.setIndicatorToState(CONNECTED); - }; - - URLIndicator.prototype.setDefaultsFromOptions = function (options) { - this.URLpath = options.url; - this.label = options.label || options.url; - this.interval = options.interval || 10000; - this.indicator.iconClass(options.iconClass || 'icon-chain-links'); - }; - - URLIndicator.prototype.bindMethods = function () { - this.fetchUrl = this.fetchUrl.bind(this); - this.handleSuccess = this.handleSuccess.bind(this); - this.handleError = this.handleError.bind(this); - this.setIndicatorToState = this.setIndicatorToState.bind(this); - }; - - return URLIndicator; - }); + return URLIndicator; +}); diff --git a/src/plugins/URLIndicatorPlugin/URLIndicatorPlugin.js b/src/plugins/URLIndicatorPlugin/URLIndicatorPlugin.js index 1f77883f8f..de7ed0d262 100644 --- a/src/plugins/URLIndicatorPlugin/URLIndicatorPlugin.js +++ b/src/plugins/URLIndicatorPlugin/URLIndicatorPlugin.js @@ -19,17 +19,15 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -define(['./URLIndicator'], - function URLIndicatorPlugin(URLIndicator) { - return function (opts) { - return function install(openmct) { - const simpleIndicator = openmct.indicators.simpleIndicator(); - const urlIndicator = new URLIndicator(opts, simpleIndicator); +define(['./URLIndicator'], function URLIndicatorPlugin(URLIndicator) { + return function (opts) { + return function install(openmct) { + const simpleIndicator = openmct.indicators.simpleIndicator(); + const urlIndicator = new URLIndicator(opts, simpleIndicator); - openmct.indicators.add(simpleIndicator); + openmct.indicators.add(simpleIndicator); - return urlIndicator; - }; - }; - } -); + return urlIndicator; + }; + }; +}); diff --git a/src/plugins/URLIndicatorPlugin/URLIndicatorSpec.js b/src/plugins/URLIndicatorPlugin/URLIndicatorSpec.js index 6966bfc47e..9155033494 100644 --- a/src/plugins/URLIndicatorPlugin/URLIndicatorSpec.js +++ b/src/plugins/URLIndicatorPlugin/URLIndicatorSpec.js @@ -20,122 +20,118 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define( - [ - "utils/testing", - "./URLIndicator", - "./URLIndicatorPlugin", - "../../MCT" - ], - function ( - testingUtils, - URLIndicator, - URLIndicatorPlugin, - MCT - ) { - describe("The URLIndicator", function () { - let openmct; - let indicatorElement; - let pluginOptions; - let urlIndicator; // eslint-disable-line - let fetchSpy; +define(['utils/testing', './URLIndicator', './URLIndicatorPlugin', '../../MCT'], function ( + testingUtils, + URLIndicator, + URLIndicatorPlugin, + MCT +) { + describe('The URLIndicator', function () { + let openmct; + let indicatorElement; + let pluginOptions; + let urlIndicator; // eslint-disable-line + let fetchSpy; - beforeEach(function () { - jasmine.clock().install(); - openmct = new testingUtils.createOpenMct(); - spyOn(openmct.indicators, 'add'); - fetchSpy = spyOn(window, 'fetch').and.callFake(() => Promise.resolve({ - ok: true - })); - }); + beforeEach(function () { + jasmine.clock().install(); + openmct = new testingUtils.createOpenMct(); + spyOn(openmct.indicators, 'add'); + fetchSpy = spyOn(window, 'fetch').and.callFake(() => + Promise.resolve({ + ok: true + }) + ); + }); - afterEach(function () { - if (window.fetch.restore) { - window.fetch.restore(); - } + afterEach(function () { + if (window.fetch.restore) { + window.fetch.restore(); + } - jasmine.clock().uninstall(); + jasmine.clock().uninstall(); - return testingUtils.resetApplicationState(openmct); - }); + return testingUtils.resetApplicationState(openmct); + }); - describe("on initialization", function () { - describe("with default options", function () { - beforeEach(function () { - pluginOptions = { - url: "someURL" - }; - urlIndicator = URLIndicatorPlugin(pluginOptions)(openmct); - indicatorElement = openmct.indicators.add.calls.mostRecent().args[0].element; - }); - - it("has a default icon class if none supplied", function () { - expect(indicatorElement.classList.contains('icon-chain-links')).toBe(true); - }); - - it("defaults to the URL if no label supplied", function () { - expect(indicatorElement.textContent.indexOf(pluginOptions.url) >= 0).toBe(true); - }); - }); - - describe("with custom options", function () { - beforeEach(function () { - pluginOptions = { - url: "customURL", - interval: 1814, - iconClass: "iconClass-checked", - label: "custom label" - }; - urlIndicator = URLIndicatorPlugin(pluginOptions)(openmct); - indicatorElement = openmct.indicators.add.calls.mostRecent().args[0].element; - }); - - it("uses the custom iconClass", function () { - expect(indicatorElement.classList.contains('iconClass-checked')).toBe(true); - }); - it("uses custom interval", function () { - expect(window.fetch).toHaveBeenCalledTimes(1); - jasmine.clock().tick(1); - expect(window.fetch).toHaveBeenCalledTimes(1); - jasmine.clock().tick(pluginOptions.interval + 1); - expect(window.fetch).toHaveBeenCalledTimes(2); - }); - it("uses custom label if supplied in initialization", function () { - expect(indicatorElement.textContent.indexOf(pluginOptions.label) >= 0).toBe(true); - }); - }); - }); - - describe("when running", function () { - beforeEach(function () { - pluginOptions = { - url: "someURL", - interval: 100 - }; - urlIndicator = URLIndicatorPlugin(pluginOptions)(openmct); - indicatorElement = openmct.indicators.add.calls.mostRecent().args[0].element; - }); - - it("requests the provided URL", function () { - jasmine.clock().tick(pluginOptions.interval + 1); - expect(window.fetch).toHaveBeenCalledWith(pluginOptions.url); - }); - - it("indicates success if connection is nominal", async function () { - jasmine.clock().tick(pluginOptions.interval + 1); - await urlIndicator.fetchUrl(); - expect(indicatorElement.classList.contains('s-status-on')).toBe(true); - }); - - 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); - await urlIndicator.fetchUrl(); - expect(indicatorElement.classList.contains('s-status-warning-hi')).toBe(true); - }); - }); + describe('on initialization', function () { + describe('with default options', function () { + beforeEach(function () { + pluginOptions = { + url: 'someURL' + }; + urlIndicator = URLIndicatorPlugin(pluginOptions)(openmct); + indicatorElement = openmct.indicators.add.calls.mostRecent().args[0].element; }); - } -); + + it('has a default icon class if none supplied', function () { + expect(indicatorElement.classList.contains('icon-chain-links')).toBe(true); + }); + + it('defaults to the URL if no label supplied', function () { + expect(indicatorElement.textContent.indexOf(pluginOptions.url) >= 0).toBe(true); + }); + }); + + describe('with custom options', function () { + beforeEach(function () { + pluginOptions = { + url: 'customURL', + interval: 1814, + iconClass: 'iconClass-checked', + label: 'custom label' + }; + urlIndicator = URLIndicatorPlugin(pluginOptions)(openmct); + indicatorElement = openmct.indicators.add.calls.mostRecent().args[0].element; + }); + + it('uses the custom iconClass', function () { + expect(indicatorElement.classList.contains('iconClass-checked')).toBe(true); + }); + it('uses custom interval', function () { + expect(window.fetch).toHaveBeenCalledTimes(1); + jasmine.clock().tick(1); + expect(window.fetch).toHaveBeenCalledTimes(1); + jasmine.clock().tick(pluginOptions.interval + 1); + expect(window.fetch).toHaveBeenCalledTimes(2); + }); + it('uses custom label if supplied in initialization', function () { + expect(indicatorElement.textContent.indexOf(pluginOptions.label) >= 0).toBe(true); + }); + }); + }); + + describe('when running', function () { + beforeEach(function () { + pluginOptions = { + url: 'someURL', + interval: 100 + }; + urlIndicator = URLIndicatorPlugin(pluginOptions)(openmct); + indicatorElement = openmct.indicators.add.calls.mostRecent().args[0].element; + }); + + it('requests the provided URL', function () { + jasmine.clock().tick(pluginOptions.interval + 1); + expect(window.fetch).toHaveBeenCalledWith(pluginOptions.url); + }); + + it('indicates success if connection is nominal', async function () { + jasmine.clock().tick(pluginOptions.interval + 1); + await urlIndicator.fetchUrl(); + expect(indicatorElement.classList.contains('s-status-on')).toBe(true); + }); + + 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); + await urlIndicator.fetchUrl(); + expect(indicatorElement.classList.contains('s-status-warning-hi')).toBe(true); + }); + }); + }); +}); diff --git a/src/plugins/URLTimeSettingsSynchronizer/URLTimeSettingsSynchronizer.js b/src/plugins/URLTimeSettingsSynchronizer/URLTimeSettingsSynchronizer.js index 7e90e6d205..3355dc021a 100644 --- a/src/plugins/URLTimeSettingsSynchronizer/URLTimeSettingsSynchronizer.js +++ b/src/plugins/URLTimeSettingsSynchronizer/URLTimeSettingsSynchronizer.js @@ -30,205 +30,206 @@ const SEARCH_END_DELTA = 'tc.endDelta'; const MODE_FIXED = 'fixed'; export default class URLTimeSettingsSynchronizer { - constructor(openmct) { - this.openmct = openmct; - this.isUrlUpdateInProgress = false; + constructor(openmct) { + this.openmct = openmct; + this.isUrlUpdateInProgress = false; - this.initialize = this.initialize.bind(this); - this.destroy = this.destroy.bind(this); - this.updateTimeSettings = this.updateTimeSettings.bind(this); - this.setUrlFromTimeApi = this.setUrlFromTimeApi.bind(this); - this.updateBounds = this.updateBounds.bind(this); + this.initialize = this.initialize.bind(this); + this.destroy = this.destroy.bind(this); + this.updateTimeSettings = this.updateTimeSettings.bind(this); + this.setUrlFromTimeApi = this.setUrlFromTimeApi.bind(this); + this.updateBounds = this.updateBounds.bind(this); - openmct.on('start', this.initialize); - openmct.on('destroy', this.destroy); + openmct.on('start', this.initialize); + openmct.on('destroy', this.destroy); + } + + initialize() { + this.updateTimeSettings(); + this.openmct.router.on('change:params', this.updateTimeSettings); + + TIME_EVENTS.forEach((event) => { + this.openmct.time.on(event, this.setUrlFromTimeApi); + }); + this.openmct.time.on('bounds', this.updateBounds); + } + + destroy() { + this.openmct.router.off('change:params', this.updateTimeSettings); + + this.openmct.off('start', this.initialize); + this.openmct.off('destroy', this.destroy); + + TIME_EVENTS.forEach((event) => { + this.openmct.time.off(event, this.setUrlFromTimeApi); + }); + this.openmct.time.off('bounds', this.updateBounds); + } + + updateTimeSettings() { + let timeParameters = this.parseParametersFromUrl(); + + if (this.areTimeParametersValid(timeParameters)) { + this.setTimeApiFromUrl(timeParameters); + this.openmct.router.setLocationFromUrl(); + } else { + this.setUrlFromTimeApi(); + } + } + + parseParametersFromUrl() { + let searchParams = this.openmct.router.getAllSearchParams(); + + let mode = searchParams.get(SEARCH_MODE); + let timeSystem = searchParams.get(SEARCH_TIME_SYSTEM); + + let startBound = parseInt(searchParams.get(SEARCH_START_BOUND), 10); + let endBound = parseInt(searchParams.get(SEARCH_END_BOUND), 10); + let bounds = { + start: startBound, + end: endBound + }; + + let startOffset = parseInt(searchParams.get(SEARCH_START_DELTA), 10); + let endOffset = parseInt(searchParams.get(SEARCH_END_DELTA), 10); + let clockOffsets = { + start: 0 - startOffset, + end: endOffset + }; + + return { + mode, + timeSystem, + bounds, + clockOffsets + }; + } + + setTimeApiFromUrl(timeParameters) { + if (timeParameters.mode === 'fixed') { + if (this.openmct.time.timeSystem().key !== timeParameters.timeSystem) { + this.openmct.time.timeSystem(timeParameters.timeSystem, timeParameters.bounds); + } else if (!this.areStartAndEndEqual(this.openmct.time.bounds(), timeParameters.bounds)) { + this.openmct.time.bounds(timeParameters.bounds); + } + + if (this.openmct.time.clock()) { + this.openmct.time.stopClock(); + } + } else { + if (!this.openmct.time.clock() || this.openmct.time.clock().key !== timeParameters.mode) { + this.openmct.time.clock(timeParameters.mode, timeParameters.clockOffsets); + } else if ( + !this.areStartAndEndEqual(this.openmct.time.clockOffsets(), timeParameters.clockOffsets) + ) { + this.openmct.time.clockOffsets(timeParameters.clockOffsets); + } + + if ( + !this.openmct.time.timeSystem() || + this.openmct.time.timeSystem().key !== timeParameters.timeSystem + ) { + this.openmct.time.timeSystem(timeParameters.timeSystem); + } + } + } + + updateBounds(bounds, isTick) { + if (!isTick) { + this.setUrlFromTimeApi(); + } + } + + setUrlFromTimeApi() { + let searchParams = this.openmct.router.getAllSearchParams(); + let clock = this.openmct.time.clock(); + let bounds = this.openmct.time.bounds(); + let clockOffsets = this.openmct.time.clockOffsets(); + + if (clock === undefined) { + searchParams.set(SEARCH_MODE, MODE_FIXED); + searchParams.set(SEARCH_START_BOUND, bounds.start); + searchParams.set(SEARCH_END_BOUND, bounds.end); + + searchParams.delete(SEARCH_START_DELTA); + searchParams.delete(SEARCH_END_DELTA); + } else { + searchParams.set(SEARCH_MODE, clock.key); + + if (clockOffsets !== undefined) { + searchParams.set(SEARCH_START_DELTA, 0 - clockOffsets.start); + searchParams.set(SEARCH_END_DELTA, clockOffsets.end); + } else { + searchParams.delete(SEARCH_START_DELTA); + searchParams.delete(SEARCH_END_DELTA); + } + + searchParams.delete(SEARCH_START_BOUND); + searchParams.delete(SEARCH_END_BOUND); } - initialize() { - this.updateTimeSettings(); - this.openmct.router.on('change:params', this.updateTimeSettings); + searchParams.set(SEARCH_TIME_SYSTEM, this.openmct.time.timeSystem().key); + this.openmct.router.setAllSearchParams(searchParams); + } - TIME_EVENTS.forEach(event => { - this.openmct.time.on(event, this.setUrlFromTimeApi); - }); - this.openmct.time.on('bounds', this.updateBounds); + areTimeParametersValid(timeParameters) { + let isValid = false; + + if ( + this.isModeValid(timeParameters.mode) && + this.isTimeSystemValid(timeParameters.timeSystem) + ) { + if (timeParameters.mode === 'fixed') { + isValid = this.areStartAndEndValid(timeParameters.bounds); + } else { + isValid = this.areStartAndEndValid(timeParameters.clockOffsets); + } } - destroy() { - this.openmct.router.off('change:params', this.updateTimeSettings); + return isValid; + } - this.openmct.off('start', this.initialize); - this.openmct.off('destroy', this.destroy); + areStartAndEndValid(bounds) { + return ( + bounds !== undefined && + bounds.start !== undefined && + bounds.start !== null && + bounds.end !== undefined && + bounds.start !== null && + !isNaN(bounds.start) && + !isNaN(bounds.end) + ); + } - TIME_EVENTS.forEach(event => { - this.openmct.time.off(event, this.setUrlFromTimeApi); - }); - this.openmct.time.off('bounds', this.updateBounds); + isTimeSystemValid(timeSystem) { + let isValid = timeSystem !== undefined; + if (isValid) { + let timeSystemObject = this.openmct.time.timeSystems.get(timeSystem); + isValid = timeSystemObject !== undefined; } - updateTimeSettings() { - let timeParameters = this.parseParametersFromUrl(); + return isValid; + } - if (this.areTimeParametersValid(timeParameters)) { - this.setTimeApiFromUrl(timeParameters); - this.openmct.router.setLocationFromUrl(); - } else { - this.setUrlFromTimeApi(); - } + isModeValid(mode) { + let isValid = false; + + if (mode !== undefined && mode !== null) { + isValid = true; } - parseParametersFromUrl() { - let searchParams = this.openmct.router.getAllSearchParams(); - - let mode = searchParams.get(SEARCH_MODE); - let timeSystem = searchParams.get(SEARCH_TIME_SYSTEM); - - let startBound = parseInt(searchParams.get(SEARCH_START_BOUND), 10); - let endBound = parseInt(searchParams.get(SEARCH_END_BOUND), 10); - let bounds = { - start: startBound, - end: endBound - }; - - let startOffset = parseInt(searchParams.get(SEARCH_START_DELTA), 10); - let endOffset = parseInt(searchParams.get(SEARCH_END_DELTA), 10); - let clockOffsets = { - start: 0 - startOffset, - end: endOffset - }; - - return { - mode, - timeSystem, - bounds, - clockOffsets - }; + if (isValid) { + if (mode.toLowerCase() === MODE_FIXED) { + isValid = true; + } else { + isValid = this.openmct.time.clocks.get(mode) !== undefined; + } } - setTimeApiFromUrl(timeParameters) { - if (timeParameters.mode === 'fixed') { - if (this.openmct.time.timeSystem().key !== timeParameters.timeSystem) { - this.openmct.time.timeSystem( - timeParameters.timeSystem, - timeParameters.bounds - ); - } else if (!this.areStartAndEndEqual(this.openmct.time.bounds(), timeParameters.bounds)) { - this.openmct.time.bounds(timeParameters.bounds); - } + return isValid; + } - if (this.openmct.time.clock()) { - this.openmct.time.stopClock(); - } - } else { - if (!this.openmct.time.clock() - || this.openmct.time.clock().key !== timeParameters.mode) { - this.openmct.time.clock(timeParameters.mode, timeParameters.clockOffsets); - } else if (!this.areStartAndEndEqual(this.openmct.time.clockOffsets(), timeParameters.clockOffsets)) { - this.openmct.time.clockOffsets(timeParameters.clockOffsets); - } - - if (!this.openmct.time.timeSystem() - || this.openmct.time.timeSystem().key !== timeParameters.timeSystem) { - this.openmct.time.timeSystem(timeParameters.timeSystem); - } - } - } - - updateBounds(bounds, isTick) { - if (!isTick) { - this.setUrlFromTimeApi(); - } - } - - setUrlFromTimeApi() { - let searchParams = this.openmct.router.getAllSearchParams(); - let clock = this.openmct.time.clock(); - let bounds = this.openmct.time.bounds(); - let clockOffsets = this.openmct.time.clockOffsets(); - - if (clock === undefined) { - searchParams.set(SEARCH_MODE, MODE_FIXED); - searchParams.set(SEARCH_START_BOUND, bounds.start); - searchParams.set(SEARCH_END_BOUND, bounds.end); - - searchParams.delete(SEARCH_START_DELTA); - searchParams.delete(SEARCH_END_DELTA); - } else { - searchParams.set(SEARCH_MODE, clock.key); - - if (clockOffsets !== undefined) { - searchParams.set(SEARCH_START_DELTA, 0 - clockOffsets.start); - searchParams.set(SEARCH_END_DELTA, clockOffsets.end); - } else { - searchParams.delete(SEARCH_START_DELTA); - searchParams.delete(SEARCH_END_DELTA); - } - - searchParams.delete(SEARCH_START_BOUND); - searchParams.delete(SEARCH_END_BOUND); - } - - searchParams.set(SEARCH_TIME_SYSTEM, this.openmct.time.timeSystem().key); - this.openmct.router.setAllSearchParams(searchParams); - } - - areTimeParametersValid(timeParameters) { - let isValid = false; - - if (this.isModeValid(timeParameters.mode) - && this.isTimeSystemValid(timeParameters.timeSystem)) { - - if (timeParameters.mode === 'fixed') { - isValid = this.areStartAndEndValid(timeParameters.bounds); - } else { - isValid = this.areStartAndEndValid(timeParameters.clockOffsets); - } - } - - return isValid; - } - - areStartAndEndValid(bounds) { - return bounds !== undefined - && bounds.start !== undefined - && bounds.start !== null - && bounds.end !== undefined - && bounds.start !== null - && !isNaN(bounds.start) - && !isNaN(bounds.end); - } - - isTimeSystemValid(timeSystem) { - let isValid = timeSystem !== undefined; - if (isValid) { - let timeSystemObject = this.openmct.time.timeSystems.get(timeSystem); - isValid = timeSystemObject !== undefined; - } - - return isValid; - } - - isModeValid(mode) { - let isValid = false; - - if (mode !== undefined - && mode !== null) { - isValid = true; - } - - if (isValid) { - if (mode.toLowerCase() === MODE_FIXED) { - isValid = true; - } else { - isValid = this.openmct.time.clocks.get(mode) !== undefined; - } - } - - return isValid; - } - - areStartAndEndEqual(firstBounds, secondBounds) { - return firstBounds.start === secondBounds.start - && firstBounds.end === secondBounds.end; - } + areStartAndEndEqual(firstBounds, secondBounds) { + return firstBounds.start === secondBounds.start && firstBounds.end === secondBounds.end; + } } diff --git a/src/plugins/URLTimeSettingsSynchronizer/plugin.js b/src/plugins/URLTimeSettingsSynchronizer/plugin.js index a6ae3dd728..7c5049e21c 100644 --- a/src/plugins/URLTimeSettingsSynchronizer/plugin.js +++ b/src/plugins/URLTimeSettingsSynchronizer/plugin.js @@ -19,10 +19,10 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import URLTimeSettingsSynchronizer from "./URLTimeSettingsSynchronizer.js"; +import URLTimeSettingsSynchronizer from './URLTimeSettingsSynchronizer.js'; export default function () { - return function install(openmct) { - return new URLTimeSettingsSynchronizer(openmct); - }; + return function install(openmct) { + return new URLTimeSettingsSynchronizer(openmct); + }; } diff --git a/src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js b/src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js index 3bec3bf3d8..1dd6c18b87 100644 --- a/src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js +++ b/src/plugins/URLTimeSettingsSynchronizer/pluginSpec.js @@ -19,116 +19,113 @@ * 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 { createOpenMct, resetApplicationState } from 'utils/testing'; -describe("The URLTimeSettingsSynchronizer", () => { - let appHolder; - let openmct; - let resolveFunction; - let oldHash; +describe('The URLTimeSettingsSynchronizer', () => { + let appHolder; + let openmct; + let resolveFunction; + let oldHash; - beforeEach((done) => { - openmct = createOpenMct(); - openmct.install(openmct.plugins.MyItems()); - openmct.install(openmct.plugins.LocalTimeSystem()); - openmct.install(openmct.plugins.UTCTimeSystem()); + beforeEach((done) => { + openmct = createOpenMct(); + openmct.install(openmct.plugins.MyItems()); + openmct.install(openmct.plugins.LocalTimeSystem()); + openmct.install(openmct.plugins.UTCTimeSystem()); - openmct.on('start', done); + openmct.on('start', done); - appHolder = document.createElement("div"); - openmct.start(appHolder); - }); + appHolder = document.createElement('div'); + openmct.start(appHolder); + }); - afterEach(() => { - openmct.time.stopClock(); - openmct.router.removeListener('change:hash', resolveFunction); + afterEach(() => { + openmct.time.stopClock(); + openmct.router.removeListener('change:hash', resolveFunction); - appHolder = undefined; - openmct = undefined; - resolveFunction = undefined; + appHolder = undefined; + openmct = undefined; + resolveFunction = undefined; - return resetApplicationState(openmct); - }); + return resetApplicationState(openmct); + }); - it("initial clock is set to fixed is reflected in URL", (done) => { - resolveFunction = () => { - oldHash = window.location.hash; - expect(window.location.hash).toContain('tc.mode=fixed'); + it('initial clock is set to fixed is reflected in URL', (done) => { + resolveFunction = () => { + oldHash = window.location.hash; + expect(window.location.hash).toContain('tc.mode=fixed'); - openmct.router.removeListener('change:hash', resolveFunction); - done(); - }; + 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); - }); + // 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) => { - resolveFunction = () => { - openmct.time.clock('local', { - start: -2000, - end: 200 - }); - 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(); - }; + it('when the clock is set via the time API, it is reflected in the URL', (done) => { + resolveFunction = () => { + openmct.time.clock('local', { + start: -2000, + end: 200 + }); + 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); - }); + // 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) => { - resolveFunction = () => { - let hash = window.location.hash; - hash = hash.replace('tc.mode=fixed', 'tc.mode=local'); - window.location.hash = hash; + it('when the clock mode is set to local, it is reflected in the URL', (done) => { + resolveFunction = () => { + let hash = window.location.hash; + hash = hash.replace('tc.mode=fixed', 'tc.mode=local'); + window.location.hash = hash; - expect(window.location.hash).toContain('tc.mode=local'); - 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); - }); + // 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) => { - resolveFunction = () => { - let hash = window.location.hash; + it('when the clock mode is set to local, it is reflected in the URL', (done) => { + resolveFunction = () => { + let hash = window.location.hash; - hash = hash.replace('tc.mode=fixed', 'tc.mode=local'); - window.location.hash = hash; - expect(window.location.hash).toContain('tc.mode=local'); - done(); - }; + hash = hash.replace('tc.mode=fixed', 'tc.mode=local'); + window.location.hash = hash; + 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); - }); + // 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); + }); - // disabling due to test flakiness - xit("reset hash", (done) => { - window.location.hash = oldHash; - resolveFunction = () => { - expect(window.location.hash).toBe(oldHash); - done(); - }; + // disabling due to test flakiness + xit('reset hash', (done) => { + window.location.hash = oldHash; + resolveFunction = () => { + expect(window.location.hash).toBe(oldHash); + done(); + }; - openmct.router.on('change:hash', resolveFunction); - }); + openmct.router.on('change:hash', resolveFunction); + }); }); diff --git a/src/plugins/autoflow/AutoflowTabularConstants.js b/src/plugins/autoflow/AutoflowTabularConstants.js index fdec55fdbd..c16c55322e 100644 --- a/src/plugins/autoflow/AutoflowTabularConstants.js +++ b/src/plugins/autoflow/AutoflowTabularConstants.js @@ -21,14 +21,14 @@ *****************************************************************************/ define([], function () { - /** - * Constant values used by the Autoflow Tabular View. - */ - return { - ROW_HEIGHT: 16, - SLIDER_HEIGHT: 10, - INITIAL_COLUMN_WIDTH: 225, - MAX_COLUMN_WIDTH: 525, - COLUMN_WIDTH_STEP: 25 - }; + /** + * Constant values used by the Autoflow Tabular View. + */ + return { + ROW_HEIGHT: 16, + SLIDER_HEIGHT: 10, + INITIAL_COLUMN_WIDTH: 225, + MAX_COLUMN_WIDTH: 525, + COLUMN_WIDTH_STEP: 25 + }; }); diff --git a/src/plugins/autoflow/AutoflowTabularController.js b/src/plugins/autoflow/AutoflowTabularController.js index 295cbce637..e24a057bb2 100644 --- a/src/plugins/autoflow/AutoflowTabularController.js +++ b/src/plugins/autoflow/AutoflowTabularController.js @@ -20,102 +20,104 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './AutoflowTabularRowController' -], function (AutoflowTabularRowController) { - /** - * Controller for an Autoflow Tabular View. Subscribes to telemetry - * associated with children of the domain object and passes that - * information on to the view. - * - * @param {DomainObject} domainObject the object being viewed - * @param {*} data the view data - * @param openmct a reference to the openmct application - */ - function AutoflowTabularController(domainObject, data, openmct) { - this.composition = openmct.composition.get(domainObject); - this.data = data; - this.openmct = openmct; +define(['./AutoflowTabularRowController'], function (AutoflowTabularRowController) { + /** + * Controller for an Autoflow Tabular View. Subscribes to telemetry + * associated with children of the domain object and passes that + * information on to the view. + * + * @param {DomainObject} domainObject the object being viewed + * @param {*} data the view data + * @param openmct a reference to the openmct application + */ + function AutoflowTabularController(domainObject, data, openmct) { + this.composition = openmct.composition.get(domainObject); + this.data = data; + this.openmct = openmct; - this.rows = {}; - this.controllers = {}; + this.rows = {}; + this.controllers = {}; - this.addRow = this.addRow.bind(this); - this.removeRow = this.removeRow.bind(this); + this.addRow = this.addRow.bind(this); + this.removeRow = this.removeRow.bind(this); + } + + /** + * Set the "Last Updated" value to be displayed. + * @param {String} value the value to display + * @private + */ + AutoflowTabularController.prototype.trackLastUpdated = function (value) { + this.data.updated = value; + }; + + /** + * Respond to an `add` event from composition by adding a new row. + * @private + */ + AutoflowTabularController.prototype.addRow = function (childObject) { + const identifier = childObject.identifier; + const id = [identifier.namespace, identifier.key].join(':'); + + if (!this.rows[id]) { + this.rows[id] = { + classes: '', + name: childObject.name, + value: undefined + }; + this.controllers[id] = new AutoflowTabularRowController( + childObject, + this.rows[id], + this.openmct, + this.trackLastUpdated.bind(this) + ); + this.controllers[id].activate(); + this.data.items.push(this.rows[id]); } + }; - /** - * Set the "Last Updated" value to be displayed. - * @param {String} value the value to display - * @private - */ - AutoflowTabularController.prototype.trackLastUpdated = function (value) { - this.data.updated = value; - }; + /** + * Respond to an `remove` event from composition by removing any + * related row. + * @private + */ + AutoflowTabularController.prototype.removeRow = function (identifier) { + const id = [identifier.namespace, identifier.key].join(':'); - /** - * Respond to an `add` event from composition by adding a new row. - * @private - */ - AutoflowTabularController.prototype.addRow = function (childObject) { - const identifier = childObject.identifier; - const id = [identifier.namespace, identifier.key].join(":"); + if (this.rows[id]) { + this.data.items = this.data.items.filter( + function (item) { + return item !== this.rows[id]; + }.bind(this) + ); + this.controllers[id].destroy(); + delete this.controllers[id]; + delete this.rows[id]; + } + }; - if (!this.rows[id]) { - this.rows[id] = { - classes: "", - name: childObject.name, - value: undefined - }; - this.controllers[id] = new AutoflowTabularRowController( - childObject, - this.rows[id], - this.openmct, - this.trackLastUpdated.bind(this) - ); - this.controllers[id].activate(); - this.data.items.push(this.rows[id]); - } - }; + /** + * Activate this controller; begin listening for changes. + */ + AutoflowTabularController.prototype.activate = function () { + this.composition.on('add', this.addRow); + this.composition.on('remove', this.removeRow); + this.composition.load(); + }; - /** - * Respond to an `remove` event from composition by removing any - * related row. - * @private - */ - AutoflowTabularController.prototype.removeRow = function (identifier) { - const id = [identifier.namespace, identifier.key].join(":"); + /** + * Destroy this controller; detach any associated resources. + */ + AutoflowTabularController.prototype.destroy = function () { + Object.keys(this.controllers).forEach( + function (id) { + this.controllers[id].destroy(); + }.bind(this) + ); + this.controllers = {}; + this.composition.off('add', this.addRow); + this.composition.off('remove', this.removeRow); + }; - if (this.rows[id]) { - this.data.items = this.data.items.filter(function (item) { - return item !== this.rows[id]; - }.bind(this)); - this.controllers[id].destroy(); - delete this.controllers[id]; - delete this.rows[id]; - } - }; - - /** - * Activate this controller; begin listening for changes. - */ - AutoflowTabularController.prototype.activate = function () { - this.composition.on('add', this.addRow); - this.composition.on('remove', this.removeRow); - this.composition.load(); - }; - - /** - * Destroy this controller; detach any associated resources. - */ - AutoflowTabularController.prototype.destroy = function () { - Object.keys(this.controllers).forEach(function (id) { - this.controllers[id].destroy(); - }.bind(this)); - this.controllers = {}; - this.composition.off('add', this.addRow); - this.composition.off('remove', this.removeRow); - }; - - return AutoflowTabularController; + return AutoflowTabularController; }); diff --git a/src/plugins/autoflow/AutoflowTabularPlugin.js b/src/plugins/autoflow/AutoflowTabularPlugin.js index 8ed1aec855..c289e5c110 100644 --- a/src/plugins/autoflow/AutoflowTabularPlugin.js +++ b/src/plugins/autoflow/AutoflowTabularPlugin.js @@ -20,32 +20,23 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './AutoflowTabularView' -], function ( - AutoflowTabularView -) { - return function (options) { - return function (openmct) { - const views = (openmct.mainViews || openmct.objectViews); +define(['./AutoflowTabularView'], function (AutoflowTabularView) { + return function (options) { + return function (openmct) { + const views = openmct.mainViews || openmct.objectViews; - views.addProvider({ - name: "Autoflow Tabular", - key: "autoflow", - cssClass: "icon-packet", - description: "A tabular view of packet contents.", - canView: function (d) { - return !options || (options.type === d.type); - }, - view: function (domainObject) { - return new AutoflowTabularView( - domainObject, - openmct, - document - ); - } - }); - }; + views.addProvider({ + name: 'Autoflow Tabular', + key: 'autoflow', + cssClass: 'icon-packet', + description: 'A tabular view of packet contents.', + canView: function (d) { + return !options || options.type === d.type; + }, + view: function (domainObject) { + return new AutoflowTabularView(domainObject, openmct, document); + } + }); }; + }; }); - diff --git a/src/plugins/autoflow/AutoflowTabularPluginSpec.js b/src/plugins/autoflow/AutoflowTabularPluginSpec.js index c85c84159f..693702b713 100644 --- a/src/plugins/autoflow/AutoflowTabularPluginSpec.js +++ b/src/plugins/autoflow/AutoflowTabularPluginSpec.js @@ -22,351 +22,344 @@ import AutoflowTabularPlugin from './AutoflowTabularPlugin'; import AutoflowTabularConstants from './AutoflowTabularConstants'; import DOMObserver from './dom-observer'; -import { - createOpenMct, - resetApplicationState, - spyOnBuiltins -} from 'utils/testing'; +import { createOpenMct, resetApplicationState, spyOnBuiltins } from 'utils/testing'; import Vue from 'vue'; // TODO lots of its without expects -xdescribe("AutoflowTabularPlugin", () => { - let testType; - let testObject; - let mockmct; +xdescribe('AutoflowTabularPlugin', () => { + let testType; + let testObject; + let mockmct; + + beforeEach(() => { + testType = 'some-type'; + testObject = { type: testType }; + mockmct = createOpenMct(); + spyOn(mockmct.composition, 'get'); + spyOn(mockmct.objectViews, 'addProvider'); + spyOn(mockmct.telemetry, 'getMetadata'); + spyOn(mockmct.telemetry, 'getValueFormatter'); + spyOn(mockmct.telemetry, 'limitEvaluator'); + spyOn(mockmct.telemetry, 'request'); + spyOn(mockmct.telemetry, 'subscribe'); + + const plugin = new AutoflowTabularPlugin({ type: testType }); + plugin(mockmct); + }); + + afterEach(() => { + return resetApplicationState(mockmct); + }); + + it('installs a view provider', () => { + expect(mockmct.objectViews.addProvider).toHaveBeenCalled(); + }); + + describe('installs a view provider which', () => { + let provider; beforeEach(() => { - testType = "some-type"; - testObject = { type: testType }; - mockmct = createOpenMct(); - spyOn(mockmct.composition, 'get'); - spyOn(mockmct.objectViews, 'addProvider'); - spyOn(mockmct.telemetry, 'getMetadata'); - spyOn(mockmct.telemetry, 'getValueFormatter'); - spyOn(mockmct.telemetry, 'limitEvaluator'); - spyOn(mockmct.telemetry, 'request'); - spyOn(mockmct.telemetry, 'subscribe'); - - const plugin = new AutoflowTabularPlugin({ type: testType }); - plugin(mockmct); + provider = mockmct.objectViews.addProvider.calls.mostRecent().args[0]; }); - afterEach(() => { - return resetApplicationState(mockmct); + it('applies its view to the type from options', () => { + expect(provider.canView(testObject, [])).toBe(true); }); - it("installs a view provider", () => { - expect(mockmct.objectViews.addProvider).toHaveBeenCalled(); + it('does not apply to other types', () => { + expect(provider.canView({ type: 'foo' }, [])).toBe(false); }); - describe("installs a view provider which", () => { - let provider; + describe('provides a view which', () => { + let testKeys; + let testChildren; + let testContainer; + let testHistories; + let mockComposition; + let mockMetadata; + let mockEvaluator; + let mockUnsubscribes; + let callbacks; + let view; + let domObserver; - beforeEach(() => { - provider = - mockmct.objectViews.addProvider.calls.mostRecent().args[0]; + function waitsForChange() { + return new Promise(function (resolve) { + window.requestAnimationFrame(resolve); + }); + } + + function emitEvent(mockEmitter, type, event) { + mockEmitter.on.calls.all().forEach((call) => { + if (call.args[0] === type) { + call.args[1](event); + } + }); + } + + beforeEach(() => { + callbacks = {}; + + spyOnBuiltins(['requestAnimationFrame']); + window.requestAnimationFrame.and.callFake((callBack) => { + callBack(); }); - it("applies its view to the type from options", () => { - expect(provider.canView(testObject, [])).toBe(true); + testObject = { type: 'some-type' }; + testKeys = ['abc', 'def', 'xyz']; + testChildren = testKeys.map((key) => { + return { + identifier: { + namespace: 'test', + key: key + }, + name: 'Object ' + key + }; + }); + testContainer = document.createElement('div'); + domObserver = new DOMObserver(testContainer); + + testHistories = testKeys.reduce((histories, key, index) => { + histories[key] = { + key: key, + range: index + 10, + domain: key + index + }; + + return histories; + }, {}); + + mockComposition = jasmine.createSpyObj('composition', ['load', 'on', 'off']); + mockMetadata = jasmine.createSpyObj('metadata', ['valuesForHints']); + + mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']); + mockUnsubscribes = testKeys.reduce((map, key) => { + map[key] = jasmine.createSpy('unsubscribe-' + key); + + return map; + }, {}); + + mockmct.composition.get.and.returnValue(mockComposition); + mockComposition.load.and.callFake(() => { + testChildren.forEach(emitEvent.bind(null, mockComposition, 'add')); + + return Promise.resolve(testChildren); }); - it("does not apply to other types", () => { - expect(provider.canView({ type: 'foo' }, [])).toBe(false); + mockmct.telemetry.getMetadata.and.returnValue(mockMetadata); + mockmct.telemetry.getValueFormatter.and.callFake((metadatum) => { + const mockFormatter = jasmine.createSpyObj('formatter', ['format']); + mockFormatter.format.and.callFake((datum) => { + return datum[metadatum.hint]; + }); + + return mockFormatter; + }); + mockmct.telemetry.limitEvaluator.and.returnValue(mockEvaluator); + mockmct.telemetry.subscribe.and.callFake((obj, callback) => { + const key = obj.identifier.key; + callbacks[key] = callback; + + return mockUnsubscribes[key]; + }); + mockmct.telemetry.request.and.callFake((obj, request) => { + const key = obj.identifier.key; + + return Promise.resolve([testHistories[key]]); + }); + mockMetadata.valuesForHints.and.callFake((hints) => { + return [{ hint: hints[0] }]; }); - describe("provides a view which", () => { - let testKeys; - let testChildren; - let testContainer; - let testHistories; - let mockComposition; - let mockMetadata; - let mockEvaluator; - let mockUnsubscribes; - let callbacks; - let view; - let domObserver; + view = provider.view(testObject); + view.show(testContainer); - function waitsForChange() { - return new Promise(function (resolve) { - window.requestAnimationFrame(resolve); - }); - } + return Vue.nextTick(); + }); - function emitEvent(mockEmitter, type, event) { - mockEmitter.on.calls.all().forEach((call) => { - if (call.args[0] === type) { - call.args[1](event); - } - }); - } + afterEach(() => { + domObserver.destroy(); + }); - beforeEach(() => { - callbacks = {}; + it('populates its container', () => { + expect(testContainer.children.length > 0).toBe(true); + }); - spyOnBuiltins(['requestAnimationFrame']); - window.requestAnimationFrame.and.callFake((callBack) => { - callBack(); - }); + describe('when rows have been populated', () => { + function rowsMatch() { + const rows = testContainer.querySelectorAll('.l-autoflow-row').length; - testObject = { type: 'some-type' }; - testKeys = ['abc', 'def', 'xyz']; - testChildren = testKeys.map((key) => { - return { - identifier: { - namespace: "test", - key: key - }, - name: "Object " + key - }; - }); - testContainer = document.createElement('div'); - domObserver = new DOMObserver(testContainer); + return rows === testChildren.length; + } - testHistories = testKeys.reduce((histories, key, index) => { - histories[key] = { - key: key, - range: index + 10, - domain: key + index - }; - - return histories; - }, {}); - - mockComposition = - jasmine.createSpyObj('composition', ['load', 'on', 'off']); - mockMetadata = - jasmine.createSpyObj('metadata', ['valuesForHints']); - - mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']); - mockUnsubscribes = testKeys.reduce((map, key) => { - map[key] = jasmine.createSpy('unsubscribe-' + key); - - return map; - }, {}); - - mockmct.composition.get.and.returnValue(mockComposition); - mockComposition.load.and.callFake(() => { - testChildren.forEach(emitEvent.bind(null, mockComposition, 'add')); - - return Promise.resolve(testChildren); - }); - - mockmct.telemetry.getMetadata.and.returnValue(mockMetadata); - mockmct.telemetry.getValueFormatter.and.callFake((metadatum) => { - const mockFormatter = jasmine.createSpyObj('formatter', ['format']); - mockFormatter.format.and.callFake((datum) => { - return datum[metadatum.hint]; - }); - - return mockFormatter; - }); - mockmct.telemetry.limitEvaluator.and.returnValue(mockEvaluator); - mockmct.telemetry.subscribe.and.callFake((obj, callback) => { - const key = obj.identifier.key; - callbacks[key] = callback; - - return mockUnsubscribes[key]; - }); - mockmct.telemetry.request.and.callFake((obj, request) => { - const key = obj.identifier.key; - - return Promise.resolve([testHistories[key]]); - }); - mockMetadata.valuesForHints.and.callFake((hints) => { - return [{ hint: hints[0] }]; - }); - - view = provider.view(testObject); - view.show(testContainer); - - return Vue.nextTick(); - }); - - afterEach(() => { - domObserver.destroy(); - }); - - it("populates its container", () => { - expect(testContainer.children.length > 0).toBe(true); - }); - - describe("when rows have been populated", () => { - function rowsMatch() { - const rows = testContainer.querySelectorAll(".l-autoflow-row").length; - - return rows === testChildren.length; - } - - it("shows one row per child object", () => { - return domObserver.when(rowsMatch); - }); - - // it("adds rows on composition change", () => { - // const child = { - // identifier: { - // namespace: "test", - // key: "123" - // }, - // name: "Object 123" - // }; - // testChildren.push(child); - // emitEvent(mockComposition, 'add', child); - - // return domObserver.when(rowsMatch); - // }); - - it("removes rows on composition change", () => { - const child = testChildren.pop(); - emitEvent(mockComposition, 'remove', child.identifier); - - return domObserver.when(rowsMatch); - }); - }); - - it("removes subscriptions when destroyed", () => { - testKeys.forEach((key) => { - expect(mockUnsubscribes[key]).not.toHaveBeenCalled(); - }); - view.destroy(); - testKeys.forEach((key) => { - expect(mockUnsubscribes[key]).toHaveBeenCalled(); - }); - }); - - it("provides a button to change column width", () => { - const initialWidth = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH; - const nextWidth = - initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP; - - expect(testContainer.querySelector('.l-autoflow-col').css('width')) - .toEqual(initialWidth + 'px'); - - testContainer.querySelector('.change-column-width').click(); - - function widthHasChanged() { - const width = testContainer.querySelector('.l-autoflow-col').css('width'); - - return width !== initialWidth + 'px'; - } - - return domObserver.when(widthHasChanged) - .then(() => { - expect(testContainer.querySelector('.l-autoflow-col').css('width')) - .toEqual(nextWidth + 'px'); - }); - }); - - it("subscribes to all child objects", () => { - testKeys.forEach((key) => { - expect(callbacks[key]).toEqual(jasmine.any(Function)); - }); - }); - - it("displays historical telemetry", () => { - function rowTextDefined() { - 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.querySelector(".l-autoflow-row").eq(index).find(".r"); - expect($cell.text()).toEqual(String(datum.range)); - }); - }); - }); - - it("displays incoming telemetry", () => { - const testData = testKeys.map((key, index) => { - return { - key: key, - range: index * 100, - domain: key + index - }; - }); - - testData.forEach((datum) => { - callbacks[datum.key](datum); - }); - - return waitsForChange().then(() => { - testData.forEach((datum, index) => { - const $cell = testContainer.querySelector(".l-autoflow-row").eq(index).find(".r"); - expect($cell.text()).toEqual(String(datum.range)); - }); - }); - }); - - it("updates classes for limit violations", () => { - const testClass = "some-limit-violation"; - mockEvaluator.evaluate.and.returnValue({ cssClass: testClass }); - testKeys.forEach((key) => { - callbacks[key]({ - range: 'foo', - domain: 'bar' - }); - }); - - return waitsForChange().then(() => { - testKeys.forEach((datum, index) => { - const $cell = testContainer.querySelector(".l-autoflow-row").eq(index).find(".r"); - expect($cell.hasClass(testClass)).toBe(true); - }); - }); - }); - - it("automatically flows to new columns", () => { - const rowHeight = AutoflowTabularConstants.ROW_HEIGHT; - const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT; - const count = testKeys.length; - const $container = testContainer; - let promiseChain = Promise.resolve(); - - function columnsHaveAutoflowed() { - 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.querySelector('.l-autoflow-col').length === columns; - } - - $container.find('.abs').css({ - position: 'absolute', - left: '0px', - right: '0px', - top: '0px', - bottom: '0px' - }); - $container.css({ position: 'absolute' }); - - $container.appendTo(document.body); - - function setHeight(height) { - $container.css('height', height + 'px'); - - return domObserver.when(columnsHaveAutoflowed); - } - - for (let height = 0; height < rowHeight * count * 2; height += rowHeight / 2) { - // eslint-disable-next-line no-invalid-this - promiseChain = promiseChain.then(setHeight.bind(this, height)); - } - - return promiseChain.then(() => { - $container.remove(); - }); - }); - - it("loads composition exactly once", () => { - const testObj = testChildren.pop(); - emitEvent(mockComposition, 'remove', testObj.identifier); - testChildren.push(testObj); - emitEvent(mockComposition, 'add', testObj); - expect(mockComposition.load.calls.count()).toEqual(1); - }); + it('shows one row per child object', () => { + return domObserver.when(rowsMatch); }); + + // it("adds rows on composition change", () => { + // const child = { + // identifier: { + // namespace: "test", + // key: "123" + // }, + // name: "Object 123" + // }; + // testChildren.push(child); + // emitEvent(mockComposition, 'add', child); + + // return domObserver.when(rowsMatch); + // }); + + it('removes rows on composition change', () => { + const child = testChildren.pop(); + emitEvent(mockComposition, 'remove', child.identifier); + + return domObserver.when(rowsMatch); + }); + }); + + it('removes subscriptions when destroyed', () => { + testKeys.forEach((key) => { + expect(mockUnsubscribes[key]).not.toHaveBeenCalled(); + }); + view.destroy(); + testKeys.forEach((key) => { + expect(mockUnsubscribes[key]).toHaveBeenCalled(); + }); + }); + + it('provides a button to change column width', () => { + const initialWidth = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH; + const nextWidth = initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP; + + expect(testContainer.querySelector('.l-autoflow-col').css('width')).toEqual( + initialWidth + 'px' + ); + + testContainer.querySelector('.change-column-width').click(); + + function widthHasChanged() { + const width = testContainer.querySelector('.l-autoflow-col').css('width'); + + return width !== initialWidth + 'px'; + } + + return domObserver.when(widthHasChanged).then(() => { + expect(testContainer.querySelector('.l-autoflow-col').css('width')).toEqual( + nextWidth + 'px' + ); + }); + }); + + it('subscribes to all child objects', () => { + testKeys.forEach((key) => { + expect(callbacks[key]).toEqual(jasmine.any(Function)); + }); + }); + + it('displays historical telemetry', () => { + function rowTextDefined() { + 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.querySelector('.l-autoflow-row').eq(index).find('.r'); + expect($cell.text()).toEqual(String(datum.range)); + }); + }); + }); + + it('displays incoming telemetry', () => { + const testData = testKeys.map((key, index) => { + return { + key: key, + range: index * 100, + domain: key + index + }; + }); + + testData.forEach((datum) => { + callbacks[datum.key](datum); + }); + + return waitsForChange().then(() => { + testData.forEach((datum, index) => { + const $cell = testContainer.querySelector('.l-autoflow-row').eq(index).find('.r'); + expect($cell.text()).toEqual(String(datum.range)); + }); + }); + }); + + it('updates classes for limit violations', () => { + const testClass = 'some-limit-violation'; + mockEvaluator.evaluate.and.returnValue({ cssClass: testClass }); + testKeys.forEach((key) => { + callbacks[key]({ + range: 'foo', + domain: 'bar' + }); + }); + + return waitsForChange().then(() => { + testKeys.forEach((datum, index) => { + const $cell = testContainer.querySelector('.l-autoflow-row').eq(index).find('.r'); + expect($cell.hasClass(testClass)).toBe(true); + }); + }); + }); + + it('automatically flows to new columns', () => { + const rowHeight = AutoflowTabularConstants.ROW_HEIGHT; + const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT; + const count = testKeys.length; + const $container = testContainer; + let promiseChain = Promise.resolve(); + + function columnsHaveAutoflowed() { + 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.querySelector('.l-autoflow-col').length === columns; + } + + $container.find('.abs').css({ + position: 'absolute', + left: '0px', + right: '0px', + top: '0px', + bottom: '0px' + }); + $container.css({ position: 'absolute' }); + + $container.appendTo(document.body); + + function setHeight(height) { + $container.css('height', height + 'px'); + + return domObserver.when(columnsHaveAutoflowed); + } + + for (let height = 0; height < rowHeight * count * 2; height += rowHeight / 2) { + // eslint-disable-next-line no-invalid-this + promiseChain = promiseChain.then(setHeight.bind(this, height)); + } + + return promiseChain.then(() => { + $container.remove(); + }); + }); + + it('loads composition exactly once', () => { + const testObj = testChildren.pop(); + emitEvent(mockComposition, 'remove', testObj.identifier); + testChildren.push(testObj); + emitEvent(mockComposition, 'add', testObj); + expect(mockComposition.load.calls.count()).toEqual(1); + }); }); + }); }); diff --git a/src/plugins/autoflow/AutoflowTabularRowController.js b/src/plugins/autoflow/AutoflowTabularRowController.js index fe92a80c3e..77042d20f3 100644 --- a/src/plugins/autoflow/AutoflowTabularRowController.js +++ b/src/plugins/autoflow/AutoflowTabularRowController.js @@ -21,74 +21,70 @@ *****************************************************************************/ define([], function () { - /** - * Controller for individual rows of an Autoflow Tabular View. - * Subscribes to telemetry and updates row data. - * - * @param {DomainObject} domainObject the object being viewed - * @param {*} data the view data - * @param openmct a reference to the openmct application - * @param {Function} callback a callback to invoke with "last updated" timestamps - */ - function AutoflowTabularRowController(domainObject, data, openmct, callback) { - this.domainObject = domainObject; - this.data = data; - this.openmct = openmct; - this.callback = callback; + /** + * Controller for individual rows of an Autoflow Tabular View. + * Subscribes to telemetry and updates row data. + * + * @param {DomainObject} domainObject the object being viewed + * @param {*} data the view data + * @param openmct a reference to the openmct application + * @param {Function} callback a callback to invoke with "last updated" timestamps + */ + function AutoflowTabularRowController(domainObject, data, openmct, callback) { + this.domainObject = domainObject; + this.data = data; + this.openmct = openmct; + this.callback = callback; - this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); - this.ranges = this.metadata.valuesForHints(['range']); - this.domains = this.metadata.valuesForHints(['domain']); - this.rangeFormatter = - this.openmct.telemetry.getValueFormatter(this.ranges[0]); - this.domainFormatter = - this.openmct.telemetry.getValueFormatter(this.domains[0]); - this.evaluator = - this.openmct.telemetry.limitEvaluator(this.domainObject); + this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); + this.ranges = this.metadata.valuesForHints(['range']); + this.domains = this.metadata.valuesForHints(['domain']); + this.rangeFormatter = this.openmct.telemetry.getValueFormatter(this.ranges[0]); + this.domainFormatter = this.openmct.telemetry.getValueFormatter(this.domains[0]); + this.evaluator = this.openmct.telemetry.limitEvaluator(this.domainObject); - this.initialized = false; - } + this.initialized = false; + } - /** - * Update row to reflect incoming telemetry data. - * @private - */ - AutoflowTabularRowController.prototype.updateRowData = function (datum) { - const violations = this.evaluator.evaluate(datum, this.ranges[0]); + /** + * Update row to reflect incoming telemetry data. + * @private + */ + AutoflowTabularRowController.prototype.updateRowData = function (datum) { + const violations = this.evaluator.evaluate(datum, this.ranges[0]); - this.initialized = true; - this.data.classes = violations ? violations.cssClass : ""; - this.data.value = this.rangeFormatter.format(datum); - this.callback(this.domainFormatter.format(datum)); - }; + this.initialized = true; + this.data.classes = violations ? violations.cssClass : ''; + this.data.value = this.rangeFormatter.format(datum); + this.callback(this.domainFormatter.format(datum)); + }; - /** - * Activate this controller; begin listening for changes. - */ - AutoflowTabularRowController.prototype.activate = function () { - this.unsubscribe = this.openmct.telemetry.subscribe( - this.domainObject, - this.updateRowData.bind(this) - ); + /** + * Activate this controller; begin listening for changes. + */ + AutoflowTabularRowController.prototype.activate = function () { + this.unsubscribe = this.openmct.telemetry.subscribe( + this.domainObject, + this.updateRowData.bind(this) + ); - this.openmct.telemetry.request( - this.domainObject, - { size: 1 } - ).then(function (history) { - if (!this.initialized && history.length > 0) { - this.updateRowData(history[history.length - 1]); - } - }.bind(this)); - }; - - /** - * Destroy this controller; detach any associated resources. - */ - AutoflowTabularRowController.prototype.destroy = function () { - if (this.unsubscribe) { - this.unsubscribe(); + this.openmct.telemetry.request(this.domainObject, { size: 1 }).then( + function (history) { + if (!this.initialized && history.length > 0) { + this.updateRowData(history[history.length - 1]); } - }; + }.bind(this) + ); + }; - return AutoflowTabularRowController; + /** + * Destroy this controller; detach any associated resources. + */ + AutoflowTabularRowController.prototype.destroy = function () { + if (this.unsubscribe) { + this.unsubscribe(); + } + }; + + return AutoflowTabularRowController; }); diff --git a/src/plugins/autoflow/AutoflowTabularView.js b/src/plugins/autoflow/AutoflowTabularView.js index 066d82fe19..eb89c64456 100644 --- a/src/plugins/autoflow/AutoflowTabularView.js +++ b/src/plugins/autoflow/AutoflowTabularView.js @@ -21,105 +21,95 @@ *****************************************************************************/ define([ - './AutoflowTabularController', - './AutoflowTabularConstants', - './VueView', - './autoflow-tabular.html' -], function ( - AutoflowTabularController, - AutoflowTabularConstants, - VueView, - autoflowTemplate -) { - const ROW_HEIGHT = AutoflowTabularConstants.ROW_HEIGHT; - const SLIDER_HEIGHT = AutoflowTabularConstants.SLIDER_HEIGHT; - const INITIAL_COLUMN_WIDTH = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH; - const MAX_COLUMN_WIDTH = AutoflowTabularConstants.MAX_COLUMN_WIDTH; - const COLUMN_WIDTH_STEP = AutoflowTabularConstants.COLUMN_WIDTH_STEP; + './AutoflowTabularController', + './AutoflowTabularConstants', + './VueView', + './autoflow-tabular.html' +], function (AutoflowTabularController, AutoflowTabularConstants, VueView, autoflowTemplate) { + const ROW_HEIGHT = AutoflowTabularConstants.ROW_HEIGHT; + const SLIDER_HEIGHT = AutoflowTabularConstants.SLIDER_HEIGHT; + const INITIAL_COLUMN_WIDTH = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH; + const MAX_COLUMN_WIDTH = AutoflowTabularConstants.MAX_COLUMN_WIDTH; + const COLUMN_WIDTH_STEP = AutoflowTabularConstants.COLUMN_WIDTH_STEP; - /** - * Implements the Autoflow Tabular view of a domain object. - */ - function AutoflowTabularView(domainObject, openmct) { - const data = { - items: [], - columns: [], - width: INITIAL_COLUMN_WIDTH, - filter: "", - updated: "No updates", - rowCount: 1 - }; - const controller = - new AutoflowTabularController(domainObject, data, openmct); - let interval; + /** + * Implements the Autoflow Tabular view of a domain object. + */ + function AutoflowTabularView(domainObject, openmct) { + const data = { + items: [], + columns: [], + width: INITIAL_COLUMN_WIDTH, + filter: '', + updated: 'No updates', + rowCount: 1 + }; + const controller = new AutoflowTabularController(domainObject, data, openmct); + let interval; - VueView.call(this, { - data: data, - methods: { - increaseColumnWidth: function () { - data.width += COLUMN_WIDTH_STEP; - data.width = data.width > MAX_COLUMN_WIDTH - ? INITIAL_COLUMN_WIDTH : data.width; - }, - reflow: function () { - let column = []; - let index = 0; - const filteredItems = - data.items.filter(function (item) { - return item.name.toLowerCase() - .indexOf(data.filter.toLowerCase()) !== -1; - }); + VueView.call(this, { + data: data, + methods: { + increaseColumnWidth: function () { + data.width += COLUMN_WIDTH_STEP; + data.width = data.width > MAX_COLUMN_WIDTH ? INITIAL_COLUMN_WIDTH : data.width; + }, + reflow: function () { + let column = []; + let index = 0; + const filteredItems = data.items.filter(function (item) { + return item.name.toLowerCase().indexOf(data.filter.toLowerCase()) !== -1; + }); - data.columns = []; + data.columns = []; - while (index < filteredItems.length) { - if (column.length >= data.rowCount) { - data.columns.push(column); - column = []; - } - - column.push(filteredItems[index]); - index += 1; - } - - if (column.length > 0) { - data.columns.push(column); - } - } - }, - watch: { - filter: 'reflow', - items: 'reflow', - rowCount: 'reflow' - }, - template: autoflowTemplate, - destroyed: function () { - controller.destroy(); - - if (interval) { - clearInterval(interval); - interval = undefined; - } - }, - mounted: function () { - controller.activate(); - - const updateRowHeight = function () { - const tabularArea = this.$refs.autoflowItems; - const height = tabularArea ? tabularArea.clientHeight : 0; - const available = height - SLIDER_HEIGHT; - const rows = Math.max(1, Math.floor(available / ROW_HEIGHT)); - data.rowCount = rows; - }.bind(this); - - interval = setInterval(updateRowHeight, 50); - this.$nextTick(updateRowHeight); + while (index < filteredItems.length) { + if (column.length >= data.rowCount) { + data.columns.push(column); + column = []; } - }); - } - AutoflowTabularView.prototype = Object.create(VueView.prototype); + column.push(filteredItems[index]); + index += 1; + } - return AutoflowTabularView; + if (column.length > 0) { + data.columns.push(column); + } + } + }, + watch: { + filter: 'reflow', + items: 'reflow', + rowCount: 'reflow' + }, + template: autoflowTemplate, + destroyed: function () { + controller.destroy(); + + if (interval) { + clearInterval(interval); + interval = undefined; + } + }, + mounted: function () { + controller.activate(); + + const updateRowHeight = function () { + const tabularArea = this.$refs.autoflowItems; + const height = tabularArea ? tabularArea.clientHeight : 0; + const available = height - SLIDER_HEIGHT; + const rows = Math.max(1, Math.floor(available / ROW_HEIGHT)); + data.rowCount = rows; + }.bind(this); + + interval = setInterval(updateRowHeight, 50); + this.$nextTick(updateRowHeight); + } + }); + } + + AutoflowTabularView.prototype = Object.create(VueView.prototype); + + return AutoflowTabularView; }); - diff --git a/src/plugins/autoflow/VueView.js b/src/plugins/autoflow/VueView.js index c7f0866aa0..0d726c1743 100644 --- a/src/plugins/autoflow/VueView.js +++ b/src/plugins/autoflow/VueView.js @@ -21,14 +21,14 @@ *****************************************************************************/ define(['vue'], function (Vue) { - function VueView(options) { - const vm = new Vue(options); - this.show = function (container) { - container.appendChild(vm.$mount().$el); - }; + function VueView(options) { + const vm = new Vue(options); + this.show = function (container) { + container.appendChild(vm.$mount().$el); + }; - this.destroy = vm.$destroy.bind(vm); - } + this.destroy = vm.$destroy.bind(vm); + } - return VueView; + return VueView; }); diff --git a/src/plugins/autoflow/autoflow-tabular.html b/src/plugins/autoflow/autoflow-tabular.html index 91cd28981f..8db66103d3 100644 --- a/src/plugins/autoflow/autoflow-tabular.html +++ b/src/plugins/autoflow/autoflow-tabular.html @@ -20,23 +20,30 @@ at runtime from the About dialog for additional information. -->
    -
    - - - - +
    + + + + -
    {{updated}}
    - -
    -
    -
      -
    • - {{row.value}} - {{row.name}} -
    • -
    -
    +
    {{updated}}
    + +
    +
    +
      +
    • + {{row.value}} + {{row.name}} +
    • +
    +
    diff --git a/src/plugins/autoflow/dom-observer.js b/src/plugins/autoflow/dom-observer.js index a71f63e2be..98b600f285 100644 --- a/src/plugins/autoflow/dom-observer.js +++ b/src/plugins/autoflow/dom-observer.js @@ -21,39 +21,43 @@ *****************************************************************************/ define([], function () { - function DOMObserver(element) { - this.element = element; - this.observers = []; - } + function DOMObserver(element) { + this.element = element; + this.observers = []; + } - DOMObserver.prototype.when = function (latchFunction) { - return new Promise(function (resolve, reject) { - //Test latch function at least once + DOMObserver.prototype.when = function (latchFunction) { + return new Promise( + function (resolve, reject) { + //Test latch function at least once + if (latchFunction()) { + resolve(); + } else { + //Latch condition not true yet, create observer on DOM and test again on change. + const config = { + attributes: true, + childList: true, + subtree: true + }; + const observer = new MutationObserver(function () { if (latchFunction()) { - resolve(); - } else { - //Latch condition not true yet, create observer on DOM and test again on change. - const config = { - attributes: true, - childList: true, - subtree: true - }; - const observer = new MutationObserver(function () { - if (latchFunction()) { - resolve(); - } - }); - observer.observe(this.element, config); - this.observers.push(observer); + resolve(); } - }.bind(this)); - }; + }); + observer.observe(this.element, config); + this.observers.push(observer); + } + }.bind(this) + ); + }; - DOMObserver.prototype.destroy = function () { - this.observers.forEach(function (observer) { - observer.disconnect(); - }.bind(this)); - }; + DOMObserver.prototype.destroy = function () { + this.observers.forEach( + function (observer) { + observer.disconnect(); + }.bind(this) + ); + }; - return DOMObserver; + return DOMObserver; }); diff --git a/src/plugins/charts/bar/BarGraphCompositionPolicy.js b/src/plugins/charts/bar/BarGraphCompositionPolicy.js index cbaea5cc8f..a28963a3f3 100644 --- a/src/plugins/charts/bar/BarGraphCompositionPolicy.js +++ b/src/plugins/charts/bar/BarGraphCompositionPolicy.js @@ -23,31 +23,31 @@ import { BAR_GRAPH_KEY } from './BarGraphConstants'; export default function BarGraphCompositionPolicy(openmct) { - function hasRange(metadata) { - const rangeValues = metadata.valuesForHints(['range']); + function hasRange(metadata) { + const rangeValues = metadata.valuesForHints(['range']); - return rangeValues && rangeValues.length > 0; + return rangeValues && rangeValues.length > 0; + } + + function hasBarGraphTelemetry(domainObject) { + if (!openmct.telemetry.isTelemetryObject(domainObject)) { + return false; } - function hasBarGraphTelemetry(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 === BAR_GRAPH_KEY) { + if (child.type === 'conditionSet' || !hasBarGraphTelemetry(child)) { + return false; } + } - let metadata = openmct.telemetry.getMetadata(domainObject); - - return metadata.values().length > 0 && hasRange(metadata); + return true; } - - return { - allow: function (parent, child) { - if (parent.type === BAR_GRAPH_KEY) { - if ((child.type === 'conditionSet') || (!hasBarGraphTelemetry(child))) { - return false; - } - } - - return true; - } - }; + }; } diff --git a/src/plugins/charts/bar/BarGraphPlot.vue b/src/plugins/charts/bar/BarGraphPlot.vue index cff5ea512f..d82126eef3 100644 --- a/src/plugins/charts/bar/BarGraphPlot.vue +++ b/src/plugins/charts/bar/BarGraphPlot.vue @@ -20,297 +20,282 @@ at runtime from the About dialog for additional information. --> - diff --git a/src/plugins/charts/bar/BarGraphView.vue b/src/plugins/charts/bar/BarGraphView.vue index 58c2963577..177697f334 100644 --- a/src/plugins/charts/bar/BarGraphView.vue +++ b/src/plugins/charts/bar/BarGraphView.vue @@ -21,14 +21,14 @@ --> diff --git a/src/plugins/charts/bar/BarGraphViewProvider.js b/src/plugins/charts/bar/BarGraphViewProvider.js index 10f054b75e..b538f209d5 100644 --- a/src/plugins/charts/bar/BarGraphViewProvider.js +++ b/src/plugins/charts/bar/BarGraphViewProvider.js @@ -25,58 +25,58 @@ import { BAR_GRAPH_KEY, BAR_GRAPH_VIEW } from './BarGraphConstants'; import Vue from 'vue'; export default function BarGraphViewProvider(openmct) { - function isCompactView(objectPath) { - let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip'); + function isCompactView(objectPath) { + let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip'); - return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); - } + return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); + } - return { - key: BAR_GRAPH_VIEW, - name: 'Bar Graph', - cssClass: 'icon-telemetry', - canView(domainObject, objectPath) { - return domainObject && domainObject.type === BAR_GRAPH_KEY; - }, + return { + key: BAR_GRAPH_VIEW, + name: 'Bar Graph', + cssClass: 'icon-telemetry', + canView(domainObject, objectPath) { + return domainObject && domainObject.type === BAR_GRAPH_KEY; + }, - canEdit(domainObject, objectPath) { - return domainObject && domainObject.type === BAR_GRAPH_KEY; - }, + canEdit(domainObject, objectPath) { + return domainObject && domainObject.type === BAR_GRAPH_KEY; + }, - view: function (domainObject, objectPath) { - let component; + view: function (domainObject, objectPath) { + let component; - return { - show: function (element) { - let isCompact = isCompactView(objectPath); - component = new Vue({ - el: element, - components: { - BarGraphView - }, - provide: { - openmct, - domainObject, - path: objectPath - }, - data() { - return { - options: { - compact: isCompact - } - }; - }, - template: '' - }); - }, - destroy: function () { - component.$destroy(); - component = undefined; - }, - onClearData() { - component.$refs.graphComponent.refreshData(); + return { + show: function (element) { + let isCompact = isCompactView(objectPath); + component = new Vue({ + el: element, + components: { + BarGraphView + }, + provide: { + openmct, + domainObject, + path: objectPath + }, + data() { + return { + options: { + compact: isCompact } - }; + }; + }, + template: '' + }); + }, + destroy: function () { + component.$destroy(); + component = undefined; + }, + onClearData() { + component.$refs.graphComponent.refreshData(); } - }; + }; + } + }; } diff --git a/src/plugins/charts/bar/inspector/BarGraphInspectorViewProvider.js b/src/plugins/charts/bar/inspector/BarGraphInspectorViewProvider.js index 4bf8f0161b..81d13f49a2 100644 --- a/src/plugins/charts/bar/inspector/BarGraphInspectorViewProvider.js +++ b/src/plugins/charts/bar/inspector/BarGraphInspectorViewProvider.js @@ -1,48 +1,47 @@ import { BAR_GRAPH_INSPECTOR_KEY, BAR_GRAPH_KEY } from '../BarGraphConstants'; import Vue from 'vue'; -import BarGraphOptions from "./BarGraphOptions.vue"; +import BarGraphOptions from './BarGraphOptions.vue'; export default function BarGraphInspectorViewProvider(openmct) { - return { - key: BAR_GRAPH_INSPECTOR_KEY, - name: 'Bar Graph Configuration', - canView: function (selection) { - if (selection.length === 0 || selection[0].length === 0) { - return false; - } + return { + key: BAR_GRAPH_INSPECTOR_KEY, + name: 'Bar Graph Configuration', + canView: function (selection) { + if (selection.length === 0 || selection[0].length === 0) { + return false; + } - let object = selection[0][0].context.item; + let object = selection[0][0].context.item; - return object - && object.type === BAR_GRAPH_KEY; + return object && object.type === BAR_GRAPH_KEY; + }, + view: function (selection) { + let component; + + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + BarGraphOptions + }, + provide: { + openmct, + domainObject: selection[0][0].context.item + }, + template: '' + }); }, - view: function (selection) { - let component; - - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - BarGraphOptions - }, - provide: { - openmct, - domainObject: selection[0][0].context.item - }, - template: '' - }); - }, - priority: function () { - return openmct.priority.HIGH + 1; - }, - destroy: function () { - if (component) { - component.$destroy(); - component = undefined; - } - } - }; + priority: function () { + return openmct.priority.HIGH + 1; + }, + destroy: function () { + if (component) { + component.$destroy(); + component = undefined; + } } - }; + }; + } + }; } diff --git a/src/plugins/charts/bar/inspector/BarGraphOptions.vue b/src/plugins/charts/bar/inspector/BarGraphOptions.vue index 266d6bd657..aad2888510 100644 --- a/src/plugins/charts/bar/inspector/BarGraphOptions.vue +++ b/src/plugins/charts/bar/inspector/BarGraphOptions.vue @@ -20,380 +20,367 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/charts/bar/inspector/SeriesOptions.vue b/src/plugins/charts/bar/inspector/SeriesOptions.vue index bb109d7b59..8ffa1205b6 100644 --- a/src/plugins/charts/bar/inspector/SeriesOptions.vue +++ b/src/plugins/charts/bar/inspector/SeriesOptions.vue @@ -20,140 +20,142 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/charts/bar/plugin.js b/src/plugins/charts/bar/plugin.js index 2dad1b6556..27c91cb3c9 100644 --- a/src/plugins/charts/bar/plugin.js +++ b/src/plugins/charts/bar/plugin.js @@ -25,30 +25,29 @@ import BarGraphInspectorViewProvider from './inspector/BarGraphInspectorViewProv import BarGraphCompositionPolicy from './BarGraphCompositionPolicy'; export default function () { - return function install(openmct) { - openmct.types.addType(BAR_GRAPH_KEY, { - key: BAR_GRAPH_KEY, - name: "Graph", - cssClass: "icon-bar-chart", - description: "Visualize data as a bar or line graph.", - creatable: true, - initialize: function (domainObject) { - domainObject.composition = []; - domainObject.configuration = { - barStyles: { series: {} }, - axes: {}, - useInterpolation: 'linear', - useBar: true - }; - }, - priority: 891 - }); + return function install(openmct) { + openmct.types.addType(BAR_GRAPH_KEY, { + key: BAR_GRAPH_KEY, + name: 'Graph', + cssClass: 'icon-bar-chart', + description: 'Visualize data as a bar or line graph.', + creatable: true, + initialize: function (domainObject) { + domainObject.composition = []; + domainObject.configuration = { + barStyles: { series: {} }, + axes: {}, + useInterpolation: 'linear', + useBar: true + }; + }, + priority: 891 + }); - openmct.objectViews.addProvider(new BarGraphViewProvider(openmct)); + openmct.objectViews.addProvider(new BarGraphViewProvider(openmct)); - openmct.inspectorViews.addProvider(new BarGraphInspectorViewProvider(openmct)); + openmct.inspectorViews.addProvider(new BarGraphInspectorViewProvider(openmct)); - openmct.composition.addPolicy(new BarGraphCompositionPolicy(openmct).allow); - }; + openmct.composition.addPolicy(new BarGraphCompositionPolicy(openmct).allow); + }; } - diff --git a/src/plugins/charts/bar/pluginSpec.js b/src/plugins/charts/bar/pluginSpec.js index 1b9e317cfb..b496b8bce3 100644 --- a/src/plugins/charts/bar/pluginSpec.js +++ b/src/plugins/charts/bar/pluginSpec.js @@ -20,583 +20,614 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import {createOpenMct, resetApplicationState} from "utils/testing"; -import Vue from "vue"; -import BarGraphPlugin from "./plugin"; +import { createOpenMct, resetApplicationState } from 'utils/testing'; +import Vue from 'vue'; +import BarGraphPlugin from './plugin'; import BarGraph from './BarGraphPlot.vue'; -import EventEmitter from "EventEmitter"; +import EventEmitter from 'EventEmitter'; import { BAR_GRAPH_VIEW, BAR_GRAPH_KEY } from './BarGraphConstants'; -describe("the plugin", function () { - let element; - let child; - let openmct; - let telemetryPromise; - let telemetryPromiseResolve; - let mockObjectPath; +describe('the plugin', function () { + let element; + let child; + let openmct; + let telemetryPromise; + let telemetryPromiseResolve; + let mockObjectPath; - beforeEach((done) => { - mockObjectPath = [ + 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': ['1.3222'], + 'some-other-key': [1] + }, + { + utc: 2, + 'some-key': ['2.555'], + 'some-other-key': [2] + }, + { + utc: 3, + 'some-key': ['3.888'], + 'some-other-key': [3] + } + ]; + + openmct = createOpenMct(); + + telemetryPromise = new Promise((resolve) => { + telemetryPromiseResolve = resolve; + }); + + spyOn(openmct.telemetry, 'request').and.callFake(() => { + telemetryPromiseResolve(testTelemetry); + + return telemetryPromise; + }); + + openmct.install(new BarGraphPlugin()); + + 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 bar graph view', () => { + 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: {}, + useInterpolation: 'linear', + useBar: true + }, + type: 'telemetry.plot.bar-graph', + name: 'Test Bar Graph' + }; + + 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 + }, + provide: { + openmct: openmct, + domainObject: barGraphObject, + composition: openmct.composition.get(barGraphObject) + }, + template: '' + }); + + 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: [ { - name: 'mock folder', - type: 'fake-folder', - identifier: { - key: 'mock-folder', - namespace: '' - } + key: 'utc', + format: 'utc', + name: 'Time', + hints: { + domain: 1 + } }, { - name: 'mock parent folder', + key: 'some-key.foo.name.45', + name: 'Some dotful attribute', + hints: { + range: 1 + } + }, + { + key: 'some-other-key.bar.344.rad', + name: 'Another dotful attribute', + hints: { + range: 2 + } + } + ] + } + }; + + 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 = () => { + return []; + }; + + spyOn(openmct.composition, 'get').and.returnValue(mockComposition); + + let viewContainer = document.createElement('div'); + child.append(viewContainer); + component = new Vue({ + el: viewContainer, + components: { + BarGraph + }, + provide: { + openmct: openmct, + domainObject: barGraphObject, + composition: openmct.composition.get(barGraphObject) + }, + template: '' + }); + + await Vue.nextTick(); + }); + + it('Renders spectral plots', () => { + const dotFullTelemetryObject = { + identifier: { + namespace: 'someNamespace', + key: '~OpenMCT~outer.test-object.foo.bar' + }, + type: 'test-dotful-object', + name: 'A Dotful Object', + telemetry: { + values: [ + { + key: 'utc', + format: 'utc', + name: 'Time', + hints: { + domain: 1 + } + }, + { + key: 'some-key', + name: 'Some attribute', + formatString: '%0.2f[]', + hints: { + range: 1 + }, + source: 'some-key' + }, + { + 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(barGraphObject, [barGraphObject]); + barGraphView.show(child, true); + mockComposition.emit('add', dotFullTelemetryObject); + + return Vue.nextTick().then(() => { + const plotElement = element.querySelector('.cartesianlayer .scatterlayer .trace .lines'); + expect(plotElement).not.toBeNull(); + barGraphView.destroy(); + }); + }); + }); + + describe('the bar graph objects', () => { + const mockObject = { + name: 'A very nice bar graph', + key: BAR_GRAPH_KEY, + creatable: true + }; + + it('defines a bar graph object type with the correct key', () => { + const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition; + expect(objectDef.key).toEqual(mockObject.key); + }); + + it('is creatable', () => { + const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition; + expect(objectDef.creatable).toEqual(mockObject.creatable); + }); + }); + + describe('The bar graph composition policy', () => { + it('allows composition for telemetry that contain at least one range', () => { + const parent = { + composition: [], + configuration: {}, + name: 'Some Bar Graph', + type: 'telemetry.plot.bar-graph', + 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', + 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); + expect(() => { + composition.add(testTelemetryObject); + }).not.toThrow(); + expect(parent.composition.length).toBe(1); + }); + + it("disallows composition for telemetry that don't contain any range hints", () => { + const parent = { + composition: [], + configuration: {}, + name: 'Some Bar Graph', + type: 'telemetry.plot.bar-graph', + 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' + }, + { + key: 'some-other-key', + name: 'Another attribute' + } + ] + } + }; + const composition = openmct.composition.get(parent); + expect(() => { + composition.add(testTelemetryObject); + }).toThrow(); + expect(parent.composition.length).toBe(0); + }); + it('disallows composition for condition sets', () => { + const parent = { + composition: [], + configuration: {}, + name: 'Some Bar Graph', + type: 'telemetry.plot.bar-graph', + location: 'mine', + modified: 1631005183584, + persisted: 1631005183502, + identifier: { + namespace: '', + key: 'b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9' + } + }; + const testTelemetryObject = { + identifier: { + namespace: '', + key: 'test-object' + }, + type: 'conditionSet', + name: 'Test Object', + telemetry: { + values: [ + { + key: 'some-key', + name: 'Some attribute', + format: 'enum', + 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: '~Some~foo.bar' + }, + 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.bar-graph', + configuration: { + barStyles: { + series: { + '~Some~foo.bar': { + name: 'A telemetry object', + type: 'some-type', + isAlias: true + } + } + }, + axes: {}, + useInterpolation: 'linear', + useBar: true + }, + composition: [ + { + identifier: { + key: '~Some~foo.bar' + } + } + ] + } + } + }, + { + context: { + item: { type: 'time-strip', identifier: { - key: 'mock-parent-folder', - namespace: '' + key: 'some-other-key', + namespace: '' } + } } - ]; - const testTelemetry = [ - { - 'utc': 1, - 'some-key': ['1.3222'], - 'some-other-key': [1] - }, - { - 'utc': 2, - 'some-key': ['2.555'], - 'some-other-key': [2] - }, - { - 'utc': 3, - 'some-key': ['3.888'], - 'some-other-key': [3] - } - ]; + } + ] + ]; - openmct = createOpenMct(); + mockComposition = new EventEmitter(); + mockComposition.load = () => { + mockComposition.emit('add', testDomainObject); - telemetryPromise = new Promise((resolve) => { - telemetryPromiseResolve = resolve; - }); + return [testDomainObject]; + }; - spyOn(openmct.telemetry, 'request').and.callFake(() => { - telemetryPromiseResolve(testTelemetry); + spyOn(openmct.composition, 'get').and.returnValue(mockComposition); - return telemetryPromise; - }); + viewContainer = document.createElement('div'); + child.append(viewContainer); - openmct.install(new BarGraphPlugin()); + const applicableViews = openmct.inspectorViews.get(selection); + plotInspectorView = applicableViews.filter( + (view) => view.name === 'Bar Graph Configuration' + )[0]; + plotInspectorView.show(viewContainer); - 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(); + await Vue.nextTick(); + optionsElement = element.querySelector('.c-bar-graph-options'); }); - afterEach((done) => { - openmct.time.timeSystem('utc', { - start: 0, - end: 1 - }); - resetApplicationState(openmct).then(done).catch(done); + afterEach(() => { + plotInspectorView.destroy(); }); - describe("The bar graph view", () => { - 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: {}, - useInterpolation: 'linear', - useBar: true - }, - type: "telemetry.plot.bar-graph", - name: "Test Bar Graph" - }; - - 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 - }, - provide: { - openmct: openmct, - domainObject: barGraphObject, - composition: openmct.composition.get(barGraphObject) - }, - template: "" - }); - - 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", - format: "utc", - name: "Time", - hints: { - domain: 1 - } - }, { - key: "some-key.foo.name.45", - name: "Some dotful attribute", - hints: { - range: 1 - } - }, { - key: "some-other-key.bar.344.rad", - name: "Another dotful attribute", - hints: { - range: 2 - } - }] - } - }; - - 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(); - }); + it('it renders the options', () => { + expect(optionsElement).toBeDefined(); }); - 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 = () => { - return []; - }; - - spyOn(openmct.composition, 'get').and.returnValue(mockComposition); - - let viewContainer = document.createElement("div"); - child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - BarGraph - }, - provide: { - openmct: openmct, - domainObject: barGraphObject, - composition: openmct.composition.get(barGraphObject) - }, - template: "" - }); - - await Vue.nextTick(); - }); - - it("Renders spectral plots", () => { - const dotFullTelemetryObject = { - identifier: { - namespace: "someNamespace", - key: "~OpenMCT~outer.test-object.foo.bar" - }, - type: "test-dotful-object", - name: "A Dotful Object", - telemetry: { - values: [{ - key: "utc", - format: "utc", - name: "Time", - hints: { - domain: 1 - } - }, { - key: "some-key", - name: "Some attribute", - formatString: '%0.2f[]', - hints: { - range: 1 - }, - source: 'some-key' - }, { - 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(barGraphObject, [barGraphObject]); - barGraphView.show(child, true); - mockComposition.emit('add', dotFullTelemetryObject); - - return Vue.nextTick().then(() => { - const plotElement = element.querySelector('.cartesianlayer .scatterlayer .trace .lines'); - expect(plotElement).not.toBeNull(); - barGraphView.destroy(); - }); - }); - }); - - describe("the bar graph objects", () => { - const mockObject = { - name: 'A very nice bar graph', - key: BAR_GRAPH_KEY, - creatable: true - }; - - it('defines a bar graph object type with the correct key', () => { - const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition; - expect(objectDef.key).toEqual(mockObject.key); - }); - - it('is creatable', () => { - const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition; - expect(objectDef.creatable).toEqual(mockObject.creatable); - }); - }); - - describe("The bar graph composition policy", () => { - it("allows composition for telemetry that contain at least one range", () => { - const parent = { - "composition": [], - "configuration": {}, - "name": "Some Bar Graph", - "type": "telemetry.plot.bar-graph", - "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", - 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); - expect(() => { - composition.add(testTelemetryObject); - }).not.toThrow(); - expect(parent.composition.length).toBe(1); - }); - - it("disallows composition for telemetry that don't contain any range hints", () => { - const parent = { - "composition": [], - "configuration": {}, - "name": "Some Bar Graph", - "type": "telemetry.plot.bar-graph", - "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" - }, { - key: "some-other-key", - name: "Another attribute" - }] - } - }; - const composition = openmct.composition.get(parent); - expect(() => { - composition.add(testTelemetryObject); - }).toThrow(); - expect(parent.composition.length).toBe(0); - }); - it("disallows composition for condition sets", () => { - const parent = { - "composition": [], - "configuration": {}, - "name": "Some Bar Graph", - "type": "telemetry.plot.bar-graph", - "location": "mine", - "modified": 1631005183584, - "persisted": 1631005183502, - "identifier": { - "namespace": "", - "key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9" - } - }; - const testTelemetryObject = { - identifier: { - namespace: "", - key: "test-object" - }, - type: "conditionSet", - name: "Test Object", - telemetry: { - values: [{ - key: "some-key", - name: "Some attribute", - format: "enum", - 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: "~Some~foo.bar" - }, - 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.bar-graph", - configuration: { - barStyles: { - series: { - '~Some~foo.bar': { - name: 'A telemetry object', - type: 'some-type', - isAlias: true - } - } - }, - axes: {}, - useInterpolation: 'linear', - useBar: true - }, - composition: [ - { - identifier: { - key: '~Some~foo.bar' - } - } - ] - } - } - }, - { - context: { - item: { - type: 'time-strip', - identifier: { - key: 'some-other-key', - namespace: '' - } - } - } - } - ] - ]; - - 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.filter(view => view.name === 'Bar Graph Configuration')[0]; - plotInspectorView.show(viewContainer); - - await Vue.nextTick(); - optionsElement = element.querySelector('.c-bar-graph-options'); - }); - - afterEach(() => { - plotInspectorView.destroy(); - }); - - it('it renders the options', () => { - expect(optionsElement).toBeDefined(); - }); - - it('shows the name', () => { - const seriesEl = optionsElement.querySelector('.c-object-label__name'); - expect(seriesEl.innerHTML).toEqual('A telemetry object'); - }); + it('shows the name', () => { + const seriesEl = optionsElement.querySelector('.c-object-label__name'); + expect(seriesEl.innerHTML).toEqual('A telemetry object'); }); + }); }); diff --git a/src/plugins/charts/scatter/ScatterPlotCompositionPolicy.js b/src/plugins/charts/scatter/ScatterPlotCompositionPolicy.js index 1befba8185..37d69c376d 100644 --- a/src/plugins/charts/scatter/ScatterPlotCompositionPolicy.js +++ b/src/plugins/charts/scatter/ScatterPlotCompositionPolicy.js @@ -23,35 +23,35 @@ import { SCATTER_PLOT_KEY } from './scatterPlotConstants'; export default function ScatterPlotCompositionPolicy(openmct) { - function hasRange(metadata) { - const rangeValues = metadata.valuesForHints(['range']).map((value) => { - return value.source; - }); + function hasRange(metadata) { + const rangeValues = metadata.valuesForHints(['range']).map((value) => { + return value.source; + }); - const uniqueRangeValues = new Set(rangeValues); + const uniqueRangeValues = new Set(rangeValues); - return uniqueRangeValues && uniqueRangeValues.size > 1; + return uniqueRangeValues && uniqueRangeValues.size > 1; + } + + function hasScatterPlotTelemetry(domainObject) { + if (!openmct.telemetry.isTelemetryObject(domainObject)) { + return false; } - 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; } + } - let metadata = openmct.telemetry.getMetadata(domainObject); - - return metadata.values().length > 0 && hasRange(metadata); + return true; } - - 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 index a02a9205bf..a776be661f 100644 --- a/src/plugins/charts/scatter/ScatterPlotForm.vue +++ b/src/plugins/charts/scatter/ScatterPlotForm.vue @@ -21,126 +21,106 @@ --> diff --git a/src/plugins/charts/scatter/ScatterPlotView.vue b/src/plugins/charts/scatter/ScatterPlotView.vue index f6a52d02f7..cfba018cb6 100644 --- a/src/plugins/charts/scatter/ScatterPlotView.vue +++ b/src/plugins/charts/scatter/ScatterPlotView.vue @@ -21,13 +21,13 @@ --> diff --git a/src/plugins/charts/scatter/ScatterPlotViewProvider.js b/src/plugins/charts/scatter/ScatterPlotViewProvider.js index fa36513c27..3394a97f62 100644 --- a/src/plugins/charts/scatter/ScatterPlotViewProvider.js +++ b/src/plugins/charts/scatter/ScatterPlotViewProvider.js @@ -25,55 +25,55 @@ import { SCATTER_PLOT_KEY, SCATTER_PLOT_VIEW, TIME_STRIP_KEY } from './scatterPl import Vue from 'vue'; export default function ScatterPlotViewProvider(openmct) { - function isCompactView(objectPath) { - let isChildOfTimeStrip = objectPath.find(object => object.type === TIME_STRIP_KEY); + function isCompactView(objectPath) { + let isChildOfTimeStrip = objectPath.find((object) => object.type === TIME_STRIP_KEY); - return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); - } + 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; - }, + 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; - }, + canEdit(domainObject, objectPath) { + return domainObject && domainObject.type === SCATTER_PLOT_KEY; + }, - view: function (domainObject, objectPath) { - let component; + 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: '' - }); - }, - destroy: function () { - component.$destroy(); - component = undefined; + 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: '' + }); + }, + destroy: function () { + component.$destroy(); + component = undefined; } - }; + }; + } + }; } diff --git a/src/plugins/charts/scatter/ScatterPlotWithUnderlay.vue b/src/plugins/charts/scatter/ScatterPlotWithUnderlay.vue index beb6319e8c..463e3eb611 100644 --- a/src/plugins/charts/scatter/ScatterPlotWithUnderlay.vue +++ b/src/plugins/charts/scatter/ScatterPlotWithUnderlay.vue @@ -20,395 +20,412 @@ at runtime from the About dialog for additional information. --> - diff --git a/src/plugins/charts/scatter/inspector/PlotOptions.vue b/src/plugins/charts/scatter/inspector/PlotOptions.vue index 72fea823ac..961f072f09 100644 --- a/src/plugins/charts/scatter/inspector/PlotOptions.vue +++ b/src/plugins/charts/scatter/inspector/PlotOptions.vue @@ -20,45 +20,45 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue b/src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue index 23b690d695..a5e1653345 100644 --- a/src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue +++ b/src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue @@ -20,138 +20,143 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue b/src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue index ce21146570..d223c72571 100644 --- a/src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue +++ b/src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue @@ -20,243 +20,246 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/charts/scatter/inspector/ScatterPlotInspectorViewProvider.js b/src/plugins/charts/scatter/inspector/ScatterPlotInspectorViewProvider.js index bd3a9ef42f..bc3e096e42 100644 --- a/src/plugins/charts/scatter/inspector/ScatterPlotInspectorViewProvider.js +++ b/src/plugins/charts/scatter/inspector/ScatterPlotInspectorViewProvider.js @@ -1,48 +1,47 @@ import { SCATTER_PLOT_INSPECTOR_KEY, SCATTER_PLOT_KEY } from '../scatterPlotConstants'; import Vue from 'vue'; -import PlotOptions from "./PlotOptions.vue"; +import PlotOptions from './PlotOptions.vue'; export default function ScatterPlotInspectorViewProvider(openmct) { - return { - key: SCATTER_PLOT_INSPECTOR_KEY, - name: 'Config', - canView: function (selection) { - if (selection.length === 0 || selection[0].length === 0) { - return false; - } + return { + key: SCATTER_PLOT_INSPECTOR_KEY, + name: 'Config', + canView: function (selection) { + if (selection.length === 0 || selection[0].length === 0) { + return false; + } - let object = selection[0][0].context.item; + let object = selection[0][0].context.item; - return object - && object.type === SCATTER_PLOT_KEY; + 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: '' + }); }, - 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: '' - }); - }, - priority: function () { - return openmct.priority.HIGH + 1; - }, - destroy: function () { - if (component) { - component.$destroy(); - component = undefined; - } - } - }; + priority: function () { + return openmct.priority.HIGH + 1; + }, + destroy: function () { + if (component) { + component.$destroy(); + component = undefined; + } } - }; + }; + } + }; } diff --git a/src/plugins/charts/scatter/plugin.js b/src/plugins/charts/scatter/plugin.js index e2bcae1bb9..85457c63ce 100644 --- a/src/plugins/charts/scatter/plugin.js +++ b/src/plugins/charts/scatter/plugin.js @@ -23,105 +23,102 @@ 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"; +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)); + 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 - }; + 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); - } + if (callback) { + callback(valid); + } - const values = Object.values(valid); - const hasAllValues = values.every(rangeValue => rangeValue !== undefined); - const hasNoValues = values.every(rangeValue => rangeValue === undefined); + const values = Object.values(valid); + const hasAllValues = values.every((rangeValue) => rangeValue !== undefined); + const hasNoValues = values.every((rangeValue) => rangeValue === undefined); - return hasAllValues || hasNoValues; - } - } - ], - priority: 891 + 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: `` }); - openmct.objectViews.addProvider(new ScatterPlotViewProvider(openmct)); - - openmct.inspectorViews.addProvider(new ScatterPlotInspectorViewProvider(openmct)); - - openmct.composition.addPolicy(new ScatterPlotCompositionPolicy(openmct).allow); + return rowComponent; + } }; - - function getScatterPlotFormControl(openmct) { - return { - show(element, model, onChange) { - const rowComponent = new Vue({ - el: element, - components: { - ScatterPlotForm - }, - provide: { - openmct - }, - data() { - return { - model, - onChange - }; - }, - template: `` - }); - - return rowComponent; - } - }; - } + } } - diff --git a/src/plugins/charts/scatter/pluginSpec.js b/src/plugins/charts/scatter/pluginSpec.js index 2768666d00..7cd3b5f0df 100644 --- a/src/plugins/charts/scatter/pluginSpec.js +++ b/src/plugins/charts/scatter/pluginSpec.js @@ -20,402 +20,418 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import {createOpenMct, resetApplicationState} from "utils/testing"; -import Vue from "vue"; -import ScatterPlotPlugin from "./plugin"; +import { createOpenMct, resetApplicationState } from 'utils/testing'; +import Vue from 'vue'; +import ScatterPlotPlugin from './plugin'; import ScatterPlot from './ScatterPlotView.vue'; -import EventEmitter from "EventEmitter"; +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; +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' - } - ]; + 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(); + 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(); + telemetryPromise = new Promise((resolve) => { + telemetryPromiseResolve = resolve; }); - afterEach((done) => { - openmct.time.timeSystem('utc', { - start: 0, - end: 1 - }); - resetApplicationState(openmct).then(done).catch(done); + spyOn(openmct.telemetry, 'request').and.callFake(() => { + telemetryPromiseResolve(testTelemetry); + + return telemetryPromise; }); - describe("The scatter plot view", () => { - let testDomainObject; - let scatterPlotObject; - // eslint-disable-next-line no-unused-vars - let component; - let mockComposition; + openmct.install(new ScatterPlotPlugin()); - beforeEach(async () => { - scatterPlotObject = { + 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: '' + }); + + 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: { - namespace: "", - key: "test-plot" + key: 'test-object', + namespace: '' }, - type: "telemetry.plot.scatter-plot", - name: "Test Scatter Plot", + type: 'telemetry.plot.scatter-plot', configuration: { - axes: {}, - styles: {} - } - }; - - testDomainObject = { - identifier: { - namespace: "", - key: "test-object" + axes: {}, + styles: {} }, - 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: "" - }); - - 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' - } - ] - } - } - } + composition: [ + { + key: '~Some~foo.scatter' + } ] - ]; + } + } + } + ] + ]; - mockComposition = new EventEmitter(); - mockComposition.load = () => { - mockComposition.emit('add', testDomainObject); + mockComposition = new EventEmitter(); + mockComposition.load = () => { + mockComposition.emit('add', testDomainObject); - return [testDomainObject]; - }; + return [testDomainObject]; + }; - spyOn(openmct.composition, 'get').and.returnValue(mockComposition); + spyOn(openmct.composition, 'get').and.returnValue(mockComposition); - viewContainer = document.createElement('div'); - child.append(viewContainer); + viewContainer = document.createElement('div'); + child.append(viewContainer); - const applicableViews = openmct.inspectorViews.get(selection); - plotInspectorView = applicableViews[0]; - plotInspectorView.show(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(); - }); + 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/clearData/ClearDataAction.js b/src/plugins/clearData/ClearDataAction.js index 59459f7a34..438c8d8f1d 100644 --- a/src/plugins/clearData/ClearDataAction.js +++ b/src/plugins/clearData/ClearDataAction.js @@ -21,58 +21,58 @@ *****************************************************************************/ function inSelectionPath(openmct, domainObject) { - const domainObjectIdentifier = domainObject.identifier; + const domainObjectIdentifier = domainObject.identifier; - return openmct.selection.get().some(selectionPath => { - return selectionPath.some(objectInPath => { - const objectInPathIdentifier = objectInPath.context.item.identifier; + return openmct.selection.get().some((selectionPath) => { + return selectionPath.some((objectInPath) => { + const objectInPathIdentifier = objectInPath.context.item.identifier; - return openmct.objects.areIdsEqual(objectInPathIdentifier, domainObjectIdentifier); - }); + return openmct.objects.areIdsEqual(objectInPathIdentifier, domainObjectIdentifier); }); + }); } export default class ClearDataAction { - constructor(openmct, appliesToObjects) { - this.name = 'Clear Data for Object'; - this.key = 'clear-data-action'; - this.description = 'Clears current data for object, unsubscribes and resubscribes to data'; - this.cssClass = 'icon-clear-data'; + constructor(openmct, appliesToObjects) { + this.name = 'Clear Data for Object'; + this.key = 'clear-data-action'; + this.description = 'Clears current data for object, unsubscribes and resubscribes to data'; + this.cssClass = 'icon-clear-data'; - this._openmct = openmct; - this._appliesToObjects = appliesToObjects; + this._openmct = openmct; + this._appliesToObjects = appliesToObjects; + } + invoke(objectPath) { + let domainObject = null; + if (objectPath) { + domainObject = objectPath[0]; } - invoke(objectPath) { - let domainObject = null; - if (objectPath) { - domainObject = objectPath[0]; - } - this._openmct.objectViews.emit('clearData', domainObject); + this._openmct.objectViews.emit('clearData', domainObject); + } + appliesTo(objectPath) { + if (!objectPath) { + return false; } - appliesTo(objectPath) { - if (!objectPath) { - return false; - } - const contextualDomainObject = objectPath[0]; - // first check to see if this action applies to this sort of object at all - const appliesToThisObject = this._appliesToObjects.some(type => { - return contextualDomainObject.type === type; - }); - if (!appliesToThisObject) { - // we've selected something not applicable - return false; - } - - const objectInSelectionPath = inSelectionPath(this._openmct, contextualDomainObject); - if (objectInSelectionPath) { - return true; - } else { - // if this it doesn't match up, check to see if we're in a composition (i.e., layout) - const routerPath = this._openmct.router.path[0]; - - return routerPath.type === 'layout'; - } + const contextualDomainObject = objectPath[0]; + // first check to see if this action applies to this sort of object at all + const appliesToThisObject = this._appliesToObjects.some((type) => { + return contextualDomainObject.type === type; + }); + if (!appliesToThisObject) { + // we've selected something not applicable + return false; } + + const objectInSelectionPath = inSelectionPath(this._openmct, contextualDomainObject); + if (objectInSelectionPath) { + return true; + } else { + // if this it doesn't match up, check to see if we're in a composition (i.e., layout) + const routerPath = this._openmct.router.path[0]; + + return routerPath.type === 'layout'; + } + } } diff --git a/src/plugins/clearData/components/globalClearIndicator.vue b/src/plugins/clearData/components/globalClearIndicator.vue index a552d01ee8..bfa485d7c5 100644 --- a/src/plugins/clearData/components/globalClearIndicator.vue +++ b/src/plugins/clearData/components/globalClearIndicator.vue @@ -20,20 +20,20 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/clearData/plugin.js b/src/plugins/clearData/plugin.js index 2469677815..a862deb7bb 100644 --- a/src/plugins/clearData/plugin.js +++ b/src/plugins/clearData/plugin.js @@ -20,42 +20,38 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './components/globalClearIndicator.vue', - './ClearDataAction', - 'vue' -], function ( - GlobaClearIndicator, - ClearDataAction, - Vue +define(['./components/globalClearIndicator.vue', './ClearDataAction', 'vue'], function ( + GlobaClearIndicator, + ClearDataAction, + Vue ) { - return function plugin(appliesToObjects, options = {indicator: true}) { - let installIndicator = options.indicator; + return function plugin(appliesToObjects, options = { indicator: true }) { + let installIndicator = options.indicator; - appliesToObjects = appliesToObjects || []; + appliesToObjects = appliesToObjects || []; - return function install(openmct) { - if (installIndicator) { - let component = new Vue ({ - components: { - GlobalClearIndicator: GlobaClearIndicator.default - }, - provide: { - openmct - }, - template: '' - }); + return function install(openmct) { + if (installIndicator) { + let component = new Vue({ + components: { + GlobalClearIndicator: GlobaClearIndicator.default + }, + provide: { + openmct + }, + template: '' + }); - let indicator = { - element: component.$mount().$el, - key: 'global-clear-indicator', - priority: openmct.priority.DEFAULT - }; - - openmct.indicators.add(indicator); - } - - openmct.actions.register(new ClearDataAction.default(openmct, appliesToObjects)); + let indicator = { + element: component.$mount().$el, + key: 'global-clear-indicator', + priority: openmct.priority.DEFAULT }; + + openmct.indicators.add(indicator); + } + + openmct.actions.register(new ClearDataAction.default(openmct, appliesToObjects)); }; + }; }); diff --git a/src/plugins/clearData/pluginSpec.js b/src/plugins/clearData/pluginSpec.js index e2957b693a..caa4623c4a 100644 --- a/src/plugins/clearData/pluginSpec.js +++ b/src/plugins/clearData/pluginSpec.js @@ -25,208 +25,213 @@ import Vue from 'vue'; import { createOpenMct, resetApplicationState, createMouseEvent } from 'utils/testing'; describe('The Clear Data Plugin:', () => { - let clearDataPlugin; + let clearDataPlugin; - describe('The clear data action:', () => { - let openmct; - let selection; - let mockObjectPath; - let clearDataAction; - let testViewObject; - beforeEach((done) => { - openmct = createOpenMct(); + describe('The clear data action:', () => { + let openmct; + let selection; + let mockObjectPath; + let clearDataAction; + let testViewObject; + beforeEach((done) => { + openmct = createOpenMct(); - clearDataPlugin = new ClearDataPlugin( - ['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'], - {indicator: true} - ); - openmct.install(clearDataPlugin); + clearDataPlugin = new ClearDataPlugin( + ['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'], + { indicator: true } + ); + openmct.install(clearDataPlugin); - clearDataAction = openmct.actions.getAction('clear-data-action'); - testViewObject = [{ - identifier: { - key: "foo-table", - namespace: '' - }, - type: "table" - }]; - openmct.router.path = testViewObject; - mockObjectPath = [ - { - name: 'Mock Table', - type: 'table', - identifier: { - key: "foo-table", - namespace: '' - } - } - ]; - selection = [ - { - context: { - item: mockObjectPath[0] - } - } - ]; + clearDataAction = openmct.actions.getAction('clear-data-action'); + testViewObject = [ + { + identifier: { + key: 'foo-table', + namespace: '' + }, + type: 'table' + } + ]; + openmct.router.path = testViewObject; + mockObjectPath = [ + { + name: 'Mock Table', + type: 'table', + identifier: { + key: 'foo-table', + namespace: '' + } + } + ]; + selection = [ + { + context: { + item: mockObjectPath[0] + } + } + ]; - openmct.selection.select(selection); + openmct.selection.select(selection); - openmct.on('start', done); - openmct.startHeadless(); - }); - - afterEach(() => { - openmct.router.path = null; - - return resetApplicationState(openmct); - }); - it('is installed', () => { - expect(clearDataAction).toBeDefined(); - }); - - it('is applicable on applicable objects', () => { - const gatheredActions = openmct.actions.getActionsCollection(mockObjectPath); - expect(gatheredActions.applicableActions['clear-data-action']).toBeDefined(); - }); - - it('is not applicable on inapplicable objects', () => { - testViewObject = [{ - identifier: { - key: "foo-widget", - namespace: '' - }, - type: "widget" - }]; - mockObjectPath = [ - { - name: 'Mock Widget', - type: 'widget', - identifier: { - key: "foo-widget", - namespace: '' - } - } - ]; - selection = [ - { - context: { - item: mockObjectPath[0] - } - } - ]; - openmct.selection.select(selection); - const gatheredActions = openmct.actions.getActionsCollection(mockObjectPath); - expect(gatheredActions.applicableActions['clear-data-action']).toBeUndefined(); - }); - - it('is not applicable if object not in the selection path and not a layout', () => { - selection = [ - { - context: { - item: { - name: 'Some Random Widget', - type: 'not-in-path-widget', - identifier: { - key: "something-else-widget", - namespace: '' - } - } - } - } - ]; - openmct.selection.select(selection); - const gatheredActions = openmct.actions.getActionsCollection(mockObjectPath); - expect(gatheredActions.applicableActions['clear-data-action']).toBeUndefined(); - }); - - it('is applicable if object not in the selection path and is a layout', () => { - selection = [ - { - context: { - item: { - name: 'Some Random Widget', - type: 'not-in-path-widget', - identifier: { - key: "something-else-widget", - namespace: '' - } - } - } - } - ]; - - openmct.selection.select(selection); - - testViewObject = [{ - identifier: { - key: "foo-layout", - namespace: '' - }, - type: "layout" - }]; - openmct.router.path = testViewObject; - const gatheredActions = openmct.actions.getActionsCollection(mockObjectPath); - expect(gatheredActions.applicableActions['clear-data-action']).toBeDefined(); - }); - - it('fires an event upon invocation', (done) => { - openmct.objectViews.on('clearData', (domainObject) => { - expect(domainObject).toEqual(testViewObject[0]); - done(); - }); - clearDataAction.invoke(testViewObject); - }); + openmct.on('start', done); + openmct.startHeadless(); }); - describe('The clear data indicator:', () => { - let openmct; - let appHolder; + afterEach(() => { + openmct.router.path = null; - beforeEach((done) => { - openmct = createOpenMct(); - - clearDataPlugin = new ClearDataPlugin([ - 'table', - 'telemetry.plot.overlay', - 'telemetry.plot.stacked', - 'example.imagery' - ], { - indicator: true - }); - openmct.install(clearDataPlugin); - appHolder = document.createElement('div'); - document.body.appendChild(appHolder); - openmct.on('start', done); - openmct.start(appHolder); - }); - - it('installs', () => { - const globalClearIndicator = openmct.indicators.indicatorObjects - .find(indicator => indicator.key === 'global-clear-indicator').element; - expect(globalClearIndicator).toBeDefined(); - }); - - it("renders its major elements", async () => { - await Vue.nextTick(); - const indicatorClass = appHolder.querySelector('.c-indicator'); - const iconClass = appHolder.querySelector('.icon-clear-data'); - const indicatorLabel = appHolder.querySelector('.c-indicator__label'); - const buttonElement = indicatorLabel.querySelector('button'); - const hasMajorElements = Boolean(indicatorClass && iconClass && buttonElement); - - expect(hasMajorElements).toBe(true); - expect(buttonElement.innerText).toEqual('Clear Data'); - }); - - it("clicking the button fires the global clear", (done) => { - const indicatorLabel = appHolder.querySelector('.c-indicator__label'); - const buttonElement = indicatorLabel.querySelector('button'); - const clickEvent = createMouseEvent('click'); - openmct.objectViews.on('clearData', () => { - // when we click the button, this event should fire - done(); - }); - buttonElement.dispatchEvent(clickEvent); - }); + return resetApplicationState(openmct); }); + it('is installed', () => { + expect(clearDataAction).toBeDefined(); + }); + + it('is applicable on applicable objects', () => { + const gatheredActions = openmct.actions.getActionsCollection(mockObjectPath); + expect(gatheredActions.applicableActions['clear-data-action']).toBeDefined(); + }); + + it('is not applicable on inapplicable objects', () => { + testViewObject = [ + { + identifier: { + key: 'foo-widget', + namespace: '' + }, + type: 'widget' + } + ]; + mockObjectPath = [ + { + name: 'Mock Widget', + type: 'widget', + identifier: { + key: 'foo-widget', + namespace: '' + } + } + ]; + selection = [ + { + context: { + item: mockObjectPath[0] + } + } + ]; + openmct.selection.select(selection); + const gatheredActions = openmct.actions.getActionsCollection(mockObjectPath); + expect(gatheredActions.applicableActions['clear-data-action']).toBeUndefined(); + }); + + it('is not applicable if object not in the selection path and not a layout', () => { + selection = [ + { + context: { + item: { + name: 'Some Random Widget', + type: 'not-in-path-widget', + identifier: { + key: 'something-else-widget', + namespace: '' + } + } + } + } + ]; + openmct.selection.select(selection); + const gatheredActions = openmct.actions.getActionsCollection(mockObjectPath); + expect(gatheredActions.applicableActions['clear-data-action']).toBeUndefined(); + }); + + it('is applicable if object not in the selection path and is a layout', () => { + selection = [ + { + context: { + item: { + name: 'Some Random Widget', + type: 'not-in-path-widget', + identifier: { + key: 'something-else-widget', + namespace: '' + } + } + } + } + ]; + + openmct.selection.select(selection); + + testViewObject = [ + { + identifier: { + key: 'foo-layout', + namespace: '' + }, + type: 'layout' + } + ]; + openmct.router.path = testViewObject; + const gatheredActions = openmct.actions.getActionsCollection(mockObjectPath); + expect(gatheredActions.applicableActions['clear-data-action']).toBeDefined(); + }); + + it('fires an event upon invocation', (done) => { + openmct.objectViews.on('clearData', (domainObject) => { + expect(domainObject).toEqual(testViewObject[0]); + done(); + }); + clearDataAction.invoke(testViewObject); + }); + }); + + describe('The clear data indicator:', () => { + let openmct; + let appHolder; + + beforeEach((done) => { + openmct = createOpenMct(); + + clearDataPlugin = new ClearDataPlugin( + ['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked', 'example.imagery'], + { + indicator: true + } + ); + openmct.install(clearDataPlugin); + appHolder = document.createElement('div'); + document.body.appendChild(appHolder); + openmct.on('start', done); + openmct.start(appHolder); + }); + + it('installs', () => { + const globalClearIndicator = openmct.indicators.indicatorObjects.find( + (indicator) => indicator.key === 'global-clear-indicator' + ).element; + expect(globalClearIndicator).toBeDefined(); + }); + + it('renders its major elements', async () => { + await Vue.nextTick(); + const indicatorClass = appHolder.querySelector('.c-indicator'); + const iconClass = appHolder.querySelector('.icon-clear-data'); + const indicatorLabel = appHolder.querySelector('.c-indicator__label'); + const buttonElement = indicatorLabel.querySelector('button'); + const hasMajorElements = Boolean(indicatorClass && iconClass && buttonElement); + + expect(hasMajorElements).toBe(true); + expect(buttonElement.innerText).toEqual('Clear Data'); + }); + + it('clicking the button fires the global clear', (done) => { + const indicatorLabel = appHolder.querySelector('.c-indicator__label'); + const buttonElement = indicatorLabel.querySelector('button'); + const clickEvent = createMouseEvent('click'); + openmct.objectViews.on('clearData', () => { + // when we click the button, this event should fire + done(); + }); + buttonElement.dispatchEvent(clickEvent); + }); + }); }); diff --git a/src/plugins/clock/ClockViewProvider.js b/src/plugins/clock/ClockViewProvider.js index 094865cc90..c745d724e8 100644 --- a/src/plugins/clock/ClockViewProvider.js +++ b/src/plugins/clock/ClockViewProvider.js @@ -24,36 +24,36 @@ import Clock from './components/Clock.vue'; import Vue from 'vue'; export default function ClockViewProvider(openmct) { - return { - key: 'clock.view', - name: 'Clock', - cssClass: 'icon-clock', - canView(domainObject) { - return domainObject.type === 'clock'; + return { + key: 'clock.view', + name: 'Clock', + cssClass: 'icon-clock', + canView(domainObject) { + return domainObject.type === 'clock'; + }, + + view: function (domainObject) { + let component; + + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + Clock + }, + provide: { + openmct, + domainObject + }, + template: '' + }); }, - - view: function (domainObject) { - let component; - - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - Clock - }, - provide: { - openmct, - domainObject - }, - template: '' - }); - }, - destroy: function () { - component.$destroy(); - component = undefined; - } - }; + destroy: function () { + component.$destroy(); + component = undefined; } - }; + }; + } + }; } diff --git a/src/plugins/clock/components/Clock.vue b/src/plugins/clock/components/Clock.vue index 6b07f86cc6..bb5456e760 100644 --- a/src/plugins/clock/components/Clock.vue +++ b/src/plugins/clock/components/Clock.vue @@ -21,21 +21,21 @@ --> diff --git a/src/plugins/clock/components/ClockIndicator.vue b/src/plugins/clock/components/ClockIndicator.vue index d283096ce0..38f80ab4fe 100644 --- a/src/plugins/clock/components/ClockIndicator.vue +++ b/src/plugins/clock/components/ClockIndicator.vue @@ -21,11 +21,11 @@ --> diff --git a/src/plugins/clock/plugin.js b/src/plugins/clock/plugin.js index 919bd3de06..87f950d4b8 100644 --- a/src/plugins/clock/plugin.js +++ b/src/plugins/clock/plugin.js @@ -1,4 +1,3 @@ - /***************************************************************************** * Open MCT, Copyright (c) 2014-2023, United States Government * as represented by the Administrator of the National Aeronautics and Space @@ -28,130 +27,120 @@ import momentTimezone from 'moment-timezone'; import Vue from 'vue'; export default function ClockPlugin(options) { - return function install(openmct) { - const CLOCK_INDICATOR_FORMAT = 'YYYY/MM/DD HH:mm:ss'; - openmct.types.addType('clock', { - name: 'Clock', - 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) { - domainObject.configuration = { - baseFormat: 'YYYY/MM/DD hh:mm:ss', - use24: 'clock12', - timezone: 'UTC' - }; + return function install(openmct) { + const CLOCK_INDICATOR_FORMAT = 'YYYY/MM/DD HH:mm:ss'; + openmct.types.addType('clock', { + name: 'Clock', + 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) { + domainObject.configuration = { + baseFormat: 'YYYY/MM/DD hh:mm:ss', + use24: 'clock12', + timezone: 'UTC' + }; + }, + form: [ + { + key: 'displayFormat', + name: 'Display Format', + control: 'select', + options: [ + { + value: 'YYYY/MM/DD hh:mm:ss', + name: 'YYYY/MM/DD hh:mm:ss' }, - "form": [ - { - "key": "displayFormat", - "name": "Display Format", - control: 'select', - options: [ - { - value: 'YYYY/MM/DD hh:mm:ss', - name: 'YYYY/MM/DD hh:mm:ss' - }, - { - value: 'YYYY/DDD hh:mm:ss', - name: 'YYYY/DDD hh:mm:ss' - }, - { - value: 'hh:mm:ss', - name: 'hh:mm:ss' - } - ], - cssClass: 'l-inline', - property: [ - 'configuration', - 'baseFormat' - ] - }, - { - ariaLabel: "12 or 24 hour clock", - control: 'select', - options: [ - { - value: 'clock12', - name: '12hr' - }, - { - value: 'clock24', - name: '24hr' - } - ], - cssClass: 'l-inline', - property: [ - 'configuration', - 'use24' - ] - }, - { - "key": "timezone", - "name": "Timezone", - "control": "autocomplete", - "cssClass": "c-clock__timezone-selection c-menu--no-icon", - "options": momentTimezone.tz.names(), - property: [ - 'configuration', - 'timezone' - ] - } - ] - }); - openmct.objectViews.addProvider(new ClockViewProvider(openmct)); + { + value: 'YYYY/DDD hh:mm:ss', + name: 'YYYY/DDD hh:mm:ss' + }, + { + value: 'hh:mm:ss', + name: 'hh:mm:ss' + } + ], + cssClass: 'l-inline', + property: ['configuration', 'baseFormat'] + }, + { + ariaLabel: '12 or 24 hour clock', + control: 'select', + options: [ + { + value: 'clock12', + name: '12hr' + }, + { + value: 'clock24', + name: '24hr' + } + ], + cssClass: 'l-inline', + property: ['configuration', 'use24'] + }, + { + key: 'timezone', + name: 'Timezone', + control: 'autocomplete', + cssClass: 'c-clock__timezone-selection c-menu--no-icon', + options: momentTimezone.tz.names(), + property: ['configuration', 'timezone'] + } + ] + }); + openmct.objectViews.addProvider(new ClockViewProvider(openmct)); - if (options && options.enableClockIndicator === true) { - const clockIndicator = new Vue ({ - components: { - ClockIndicator - }, - provide: { - openmct - }, - data() { - return { - indicatorFormat: CLOCK_INDICATOR_FORMAT - }; - }, - template: '' - }); - const indicator = { - element: clockIndicator.$mount().$el, - key: 'clock-indicator', - priority: openmct.priority.LOW - }; + if (options && options.enableClockIndicator === true) { + const clockIndicator = new Vue({ + components: { + ClockIndicator + }, + provide: { + openmct + }, + data() { + return { + indicatorFormat: CLOCK_INDICATOR_FORMAT + }; + }, + template: '' + }); + const indicator = { + element: clockIndicator.$mount().$el, + key: 'clock-indicator', + priority: openmct.priority.LOW + }; - openmct.indicators.add(indicator); + openmct.indicators.add(indicator); + } + + openmct.objects.addGetInterceptor({ + appliesTo: (identifier, domainObject) => { + return domainObject && domainObject.type === 'clock'; + }, + invoke: (identifier, domainObject) => { + if (domainObject.configuration) { + return domainObject; } - openmct.objects.addGetInterceptor({ - appliesTo: (identifier, domainObject) => { - return domainObject && domainObject.type === 'clock'; - }, - invoke: (identifier, domainObject) => { - if (domainObject.configuration) { - return domainObject; - } + if (domainObject.clockFormat && domainObject.timezone) { + const baseFormat = domainObject.clockFormat[0]; + const use24 = domainObject.clockFormat[1]; + const timezone = domainObject.timezone; - if (domainObject.clockFormat - && domainObject.timezone) { - const baseFormat = domainObject.clockFormat[0]; - const use24 = domainObject.clockFormat[1]; - const timezone = domainObject.timezone; + domainObject.configuration = { + baseFormat, + use24, + timezone + }; - domainObject.configuration = { - baseFormat, - use24, - timezone - }; + openmct.objects.mutate(domainObject, 'configuration', domainObject.configuration); + } - openmct.objects.mutate(domainObject, 'configuration', domainObject.configuration); - } - - return domainObject; - } - }); - - }; + return domainObject; + } + }); + }; } diff --git a/src/plugins/clock/pluginSpec.js b/src/plugins/clock/pluginSpec.js index d014bccb63..4046e6ef0c 100644 --- a/src/plugins/clock/pluginSpec.js +++ b/src/plugins/clock/pluginSpec.js @@ -25,208 +25,211 @@ import clockPlugin from './plugin'; import Vue from 'vue'; -describe("Clock plugin:", () => { - let openmct; - let clockDefinition; - let element; - let child; - let appHolder; +describe('Clock plugin:', () => { + let openmct; + let clockDefinition; + let element; + let child; + let appHolder; - let clockDomainObject; + let clockDomainObject; - function setupClock(enableClockIndicator) { - return new Promise((resolve, reject) => { - clockDomainObject = { - identifier: { - key: 'clock', - namespace: 'test-namespace' - }, - type: 'clock' - }; + function setupClock(enableClockIndicator) { + return new Promise((resolve, reject) => { + clockDomainObject = { + identifier: { + key: 'clock', + namespace: 'test-namespace' + }, + type: 'clock' + }; - appHolder = document.createElement('div'); - appHolder.style.width = '640px'; - appHolder.style.height = '480px'; - document.body.appendChild(appHolder); + appHolder = document.createElement('div'); + appHolder.style.width = '640px'; + appHolder.style.height = '480px'; + document.body.appendChild(appHolder); - openmct = createOpenMct(); + openmct = createOpenMct(); - element = document.createElement('div'); - child = document.createElement('div'); - element.appendChild(child); + element = document.createElement('div'); + child = document.createElement('div'); + element.appendChild(child); - openmct.install(clockPlugin({ enableClockIndicator })); + openmct.install(clockPlugin({ enableClockIndicator })); - clockDefinition = openmct.types.get('clock').definition; - clockDefinition.initialize(clockDomainObject); + clockDefinition = openmct.types.get('clock').definition; + clockDefinition.initialize(clockDomainObject); - openmct.on('start', resolve); - openmct.start(appHolder); - }); - } + openmct.on('start', resolve); + openmct.start(appHolder); + }); + } - describe("Clock view:", () => { - let clockViewProvider; - let clockView; - let clockViewObject; - let mutableClockObject; + describe('Clock view:', () => { + let clockViewProvider; + let clockView; + let clockViewObject; + let mutableClockObject; - beforeEach(async () => { - await setupClock(true); + beforeEach(async () => { + await setupClock(true); - clockViewObject = { - ...clockDomainObject, - id: "test-object", - name: 'Clock', - configuration: { - baseFormat: 'YYYY/MM/DD hh:mm:ss', - use24: 'clock12', - timezone: 'UTC' - } - }; + clockViewObject = { + ...clockDomainObject, + id: 'test-object', + name: 'Clock', + configuration: { + baseFormat: 'YYYY/MM/DD hh:mm:ss', + use24: 'clock12', + timezone: 'UTC' + } + }; - spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(clockViewObject)); - spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true)); - spyOn(openmct.objects, 'supportsMutation').and.returnValue(true); + spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(clockViewObject)); + spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true)); + spyOn(openmct.objects, 'supportsMutation').and.returnValue(true); - const applicableViews = openmct.objectViews.get(clockViewObject, [clockViewObject]); - clockViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'clock.view'); + const applicableViews = openmct.objectViews.get(clockViewObject, [clockViewObject]); + clockViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'clock.view'); - mutableClockObject = await openmct.objects.getMutable(clockViewObject.identifier); + mutableClockObject = await openmct.objects.getMutable(clockViewObject.identifier); - clockView = clockViewProvider.view(mutableClockObject); - clockView.show(child); + clockView = clockViewProvider.view(mutableClockObject); + clockView.show(child); - await Vue.nextTick(); - }); - - afterEach(() => { - clockView.destroy(); - openmct.objects.destroyMutable(mutableClockObject); - if (appHolder) { - appHolder.remove(); - } - - return resetApplicationState(openmct); - }); - - it("has name as Clock", () => { - expect(clockDefinition.name).toEqual('Clock'); - }); - - it("is creatable", () => { - expect(clockDefinition.creatable).toEqual(true); - }); - - it("provides clock view", () => { - expect(clockViewProvider).toBeDefined(); - }); - - it("renders clock element", () => { - const clockElement = element.querySelectorAll('.c-clock'); - expect(clockElement.length).toBe(1); - }); - - it("renders major elements", () => { - const clockElement = element.querySelector('.c-clock'); - const timezone = clockElement.querySelector('.c-clock__timezone'); - const time = clockElement.querySelector('.c-clock__value'); - const amPm = clockElement.querySelector('.c-clock__ampm'); - const hasMajorElements = Boolean(timezone && time && amPm); - - expect(hasMajorElements).toBe(true); - }); - - it("renders time in UTC", () => { - const clockElement = element.querySelector('.c-clock'); - const timezone = clockElement.querySelector('.c-clock__timezone').textContent.trim(); - - expect(timezone).toBe('UTC'); - }); - - it("updates the 24 hour option in the configuration", (done) => { - expect(clockDomainObject.configuration.use24).toBe('clock12'); - const new24Option = 'clock24'; - - openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => { - expect(changedDomainObject.use24).toBe(new24Option); - done(); - }); - - openmct.objects.mutate(clockViewObject, 'configuration.use24', new24Option); - }); - - it("updates the timezone option in the configuration", (done) => { - expect(clockDomainObject.configuration.timezone).toBe('UTC'); - const newZone = 'CST6CDT'; - - openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => { - expect(changedDomainObject.timezone).toBe(newZone); - done(); - }); - - openmct.objects.mutate(clockViewObject, 'configuration.timezone', newZone); - }); - - it("updates the time format option in the configuration", (done) => { - expect(clockDomainObject.configuration.baseFormat).toBe('YYYY/MM/DD hh:mm:ss'); - const newFormat = 'hh:mm:ss'; - - openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => { - expect(changedDomainObject.baseFormat).toBe(newFormat); - done(); - }); - - openmct.objects.mutate(clockViewObject, 'configuration.baseFormat', newFormat); - }); + await Vue.nextTick(); }); - describe("Clock Indicator view:", () => { - let clockIndicator; + afterEach(() => { + clockView.destroy(); + openmct.objects.destroyMutable(mutableClockObject); + if (appHolder) { + appHolder.remove(); + } - afterEach(() => { - if (clockIndicator) { - clockIndicator.remove(); - } - - clockIndicator = undefined; - if (appHolder) { - appHolder.remove(); - } - - return resetApplicationState(openmct); - }); - - it("doesn't exist", async () => { - await setupClock(false); - - clockIndicator = openmct.indicators.indicatorObjects - .find(indicator => indicator.key === 'clock-indicator'); - - const clockIndicatorMissing = clockIndicator === null || clockIndicator === undefined; - expect(clockIndicatorMissing).toBe(true); - }); - - it("exists", async () => { - await setupClock(true); - - clockIndicator = openmct.indicators.indicatorObjects - .find(indicator => indicator.key === 'clock-indicator').element; - - const hasClockIndicator = clockIndicator !== null && clockIndicator !== undefined; - expect(hasClockIndicator).toBe(true); - }); - - it("contains text", async () => { - await setupClock(true); - - clockIndicator = openmct.indicators.indicatorObjects - .find(indicator => indicator.key === 'clock-indicator').element; - - const clockIndicatorText = clockIndicator.textContent.trim(); - const textIncludesUTC = clockIndicatorText.includes('UTC'); - - expect(textIncludesUTC).toBe(true); - }); + return resetApplicationState(openmct); }); + + it('has name as Clock', () => { + expect(clockDefinition.name).toEqual('Clock'); + }); + + it('is creatable', () => { + expect(clockDefinition.creatable).toEqual(true); + }); + + it('provides clock view', () => { + expect(clockViewProvider).toBeDefined(); + }); + + it('renders clock element', () => { + const clockElement = element.querySelectorAll('.c-clock'); + expect(clockElement.length).toBe(1); + }); + + it('renders major elements', () => { + const clockElement = element.querySelector('.c-clock'); + const timezone = clockElement.querySelector('.c-clock__timezone'); + const time = clockElement.querySelector('.c-clock__value'); + const amPm = clockElement.querySelector('.c-clock__ampm'); + const hasMajorElements = Boolean(timezone && time && amPm); + + expect(hasMajorElements).toBe(true); + }); + + it('renders time in UTC', () => { + const clockElement = element.querySelector('.c-clock'); + const timezone = clockElement.querySelector('.c-clock__timezone').textContent.trim(); + + expect(timezone).toBe('UTC'); + }); + + it('updates the 24 hour option in the configuration', (done) => { + expect(clockDomainObject.configuration.use24).toBe('clock12'); + const new24Option = 'clock24'; + + openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => { + expect(changedDomainObject.use24).toBe(new24Option); + done(); + }); + + openmct.objects.mutate(clockViewObject, 'configuration.use24', new24Option); + }); + + it('updates the timezone option in the configuration', (done) => { + expect(clockDomainObject.configuration.timezone).toBe('UTC'); + const newZone = 'CST6CDT'; + + openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => { + expect(changedDomainObject.timezone).toBe(newZone); + done(); + }); + + openmct.objects.mutate(clockViewObject, 'configuration.timezone', newZone); + }); + + it('updates the time format option in the configuration', (done) => { + expect(clockDomainObject.configuration.baseFormat).toBe('YYYY/MM/DD hh:mm:ss'); + const newFormat = 'hh:mm:ss'; + + openmct.objects.observe(clockViewObject, 'configuration', (changedDomainObject) => { + expect(changedDomainObject.baseFormat).toBe(newFormat); + done(); + }); + + openmct.objects.mutate(clockViewObject, 'configuration.baseFormat', newFormat); + }); + }); + + describe('Clock Indicator view:', () => { + let clockIndicator; + + afterEach(() => { + if (clockIndicator) { + clockIndicator.remove(); + } + + clockIndicator = undefined; + if (appHolder) { + appHolder.remove(); + } + + return resetApplicationState(openmct); + }); + + it("doesn't exist", async () => { + await setupClock(false); + + clockIndicator = openmct.indicators.indicatorObjects.find( + (indicator) => indicator.key === 'clock-indicator' + ); + + const clockIndicatorMissing = clockIndicator === null || clockIndicator === undefined; + expect(clockIndicatorMissing).toBe(true); + }); + + it('exists', async () => { + await setupClock(true); + + clockIndicator = openmct.indicators.indicatorObjects.find( + (indicator) => indicator.key === 'clock-indicator' + ).element; + + const hasClockIndicator = clockIndicator !== null && clockIndicator !== undefined; + expect(hasClockIndicator).toBe(true); + }); + + it('contains text', async () => { + await setupClock(true); + + clockIndicator = openmct.indicators.indicatorObjects.find( + (indicator) => indicator.key === 'clock-indicator' + ).element; + + const clockIndicatorText = clockIndicator.textContent.trim(); + const textIncludesUTC = clockIndicatorText.includes('UTC'); + + expect(textIncludesUTC).toBe(true); + }); + }); }); diff --git a/src/plugins/condition/Condition.js b/src/plugins/condition/Condition.js index a64577669a..57d522dbbb 100644 --- a/src/plugins/condition/Condition.js +++ b/src/plugins/condition/Condition.js @@ -22,303 +22,317 @@ import EventEmitter from 'EventEmitter'; import { v4 as uuid } from 'uuid'; -import TelemetryCriterion from "./criterion/TelemetryCriterion"; +import TelemetryCriterion from './criterion/TelemetryCriterion'; import { evaluateResults } from './utils/evaluator'; import { getLatestTimestamp } from './utils/time'; -import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion"; -import { TRIGGER_CONJUNCTION, TRIGGER_LABEL } from "./utils/constants"; +import AllTelemetryCriterion from './criterion/AllTelemetryCriterion'; +import { TRIGGER_CONJUNCTION, TRIGGER_LABEL } from './utils/constants'; /* -* conditionConfiguration = { -* id: uuid, -* trigger: 'any'/'all'/'not','xor', -* criteria: [ -* { -* telemetry: '', -* operation: '', -* input: [], -* metadata: '' -* } -* ] -* } -*/ + * conditionConfiguration = { + * id: uuid, + * trigger: 'any'/'all'/'not','xor', + * criteria: [ + * { + * telemetry: '', + * operation: '', + * input: [], + * metadata: '' + * } + * ] + * } + */ export default class Condition extends EventEmitter { + /** + * Manages criteria and emits the result of - true or false - based on criteria evaluated. + * @constructor + * @param conditionConfiguration: {id: uuid,trigger: enum, criteria: Array of {id: uuid, operation: enum, input: Array, metaDataKey: string, key: {domainObject.identifier} } + * @param openmct + * @param conditionManager + */ + constructor(conditionConfiguration, openmct, conditionManager) { + super(); - /** - * Manages criteria and emits the result of - true or false - based on criteria evaluated. - * @constructor - * @param conditionConfiguration: {id: uuid,trigger: enum, criteria: Array of {id: uuid, operation: enum, input: Array, metaDataKey: string, key: {domainObject.identifier} } - * @param openmct - * @param conditionManager - */ - constructor(conditionConfiguration, openmct, conditionManager) { - super(); + this.openmct = openmct; + this.conditionManager = conditionManager; + this.id = conditionConfiguration.id; + this.criteria = []; + this.result = undefined; + this.timeSystems = this.openmct.time.getAllTimeSystems(); + if (conditionConfiguration.configuration.criteria) { + this.createCriteria(conditionConfiguration.configuration.criteria); + } - this.openmct = openmct; - this.conditionManager = conditionManager; - this.id = conditionConfiguration.id; - this.criteria = []; - this.result = undefined; - this.timeSystems = this.openmct.time.getAllTimeSystems(); - if (conditionConfiguration.configuration.criteria) { - this.createCriteria(conditionConfiguration.configuration.criteria); + this.trigger = conditionConfiguration.configuration.trigger; + this.summary = ''; + } + + updateResult(datum) { + if (!datum || !datum.id) { + console.log('no data received'); + + return; + } + + // if all the criteria in this condition have no telemetry, we want to force the condition result to evaluate + if (this.hasNoTelemetry() || this.isTelemetryUsed(datum.id)) { + this.criteria.forEach((criterion) => { + if (this.isAnyOrAllTelemetry(criterion)) { + criterion.updateResult(datum, this.conditionManager.telemetryObjects); + } else { + if (criterion.usesTelemetry(datum.id)) { + criterion.updateResult(datum); + } } + }); - this.trigger = conditionConfiguration.configuration.trigger; - this.summary = ''; + this.result = evaluateResults( + this.criteria.map((criterion) => criterion.result), + this.trigger + ); + } + } + + isAnyOrAllTelemetry(criterion) { + return criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any'); + } + + hasNoTelemetry() { + return this.criteria.every((criterion) => { + return !this.isAnyOrAllTelemetry(criterion) && criterion.telemetry === ''; + }); + } + + isTelemetryUsed(id) { + return this.criteria.some((criterion) => { + return this.isAnyOrAllTelemetry(criterion) || criterion.usesTelemetry(id); + }); + } + + update(conditionConfiguration) { + this.updateTrigger(conditionConfiguration.configuration.trigger); + this.updateCriteria(conditionConfiguration.configuration.criteria); + } + + updateTrigger(trigger) { + if (this.trigger !== trigger) { + this.trigger = trigger; + } + } + + generateCriterion(criterionConfiguration) { + return { + id: criterionConfiguration.id || uuid(), + telemetry: criterionConfiguration.telemetry || '', + telemetryObjects: this.conditionManager.telemetryObjects, + operation: criterionConfiguration.operation || '', + input: criterionConfiguration.input === undefined ? [] : criterionConfiguration.input, + metadata: criterionConfiguration.metadata || '' + }; + } + + createCriteria(criterionConfigurations) { + criterionConfigurations.forEach((criterionConfiguration) => { + this.addCriterion(criterionConfiguration); + }); + } + + updateCriteria(criterionConfigurations) { + this.destroyCriteria(); + this.createCriteria(criterionConfigurations); + } + + updateTelemetryObjects() { + this.criteria.forEach((criterion) => { + criterion.updateTelemetryObjects(this.conditionManager.telemetryObjects); + }); + } + + /** + * adds criterion to the condition. + */ + addCriterion(criterionConfiguration) { + let criterion; + let criterionConfigurationWithId = this.generateCriterion(criterionConfiguration || null); + if ( + criterionConfiguration.telemetry && + (criterionConfiguration.telemetry === 'any' || criterionConfiguration.telemetry === 'all') + ) { + criterion = new AllTelemetryCriterion(criterionConfigurationWithId, this.openmct); + } else { + criterion = new TelemetryCriterion(criterionConfigurationWithId, this.openmct); } - updateResult(datum) { - if (!datum || !datum.id) { - console.log('no data received'); - - return; - } - - // if all the criteria in this condition have no telemetry, we want to force the condition result to evaluate - if (this.hasNoTelemetry() || this.isTelemetryUsed(datum.id)) { - - this.criteria.forEach(criterion => { - if (this.isAnyOrAllTelemetry(criterion)) { - criterion.updateResult(datum, this.conditionManager.telemetryObjects); - } else { - if (criterion.usesTelemetry(datum.id)) { - criterion.updateResult(datum); - } - } - }); - - this.result = evaluateResults(this.criteria.map(criterion => criterion.result), this.trigger); - } + criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); + criterion.on('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj)); + criterion.on('telemetryStaleness', () => this.handleTelemetryStaleness()); + if (!this.criteria) { + this.criteria = []; } - isAnyOrAllTelemetry(criterion) { - return (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any')); - } + this.criteria.push(criterion); - hasNoTelemetry() { - return this.criteria.every((criterion) => { - return !this.isAnyOrAllTelemetry(criterion) && criterion.telemetry === ''; - }); - } + return criterionConfigurationWithId.id; + } - isTelemetryUsed(id) { - return this.criteria.some(criterion => { - return this.isAnyOrAllTelemetry(criterion) || criterion.usesTelemetry(id); - }); - } + findCriterion(id) { + let criterion; - update(conditionConfiguration) { - this.updateTrigger(conditionConfiguration.configuration.trigger); - this.updateCriteria(conditionConfiguration.configuration.criteria); - } - - updateTrigger(trigger) { - if (this.trigger !== trigger) { - this.trigger = trigger; - } - } - - generateCriterion(criterionConfiguration) { - return { - id: criterionConfiguration.id || uuid(), - telemetry: criterionConfiguration.telemetry || '', - telemetryObjects: this.conditionManager.telemetryObjects, - operation: criterionConfiguration.operation || '', - input: criterionConfiguration.input === undefined ? [] : criterionConfiguration.input, - metadata: criterionConfiguration.metadata || '' + for (let i = 0, ii = this.criteria.length; i < ii; i++) { + if (this.criteria[i].id === id) { + criterion = { + item: this.criteria[i], + index: i }; + } } - createCriteria(criterionConfigurations) { - criterionConfigurations.forEach((criterionConfiguration) => { - this.addCriterion(criterionConfiguration); - }); + return criterion; + } + + updateCriterion(id, criterionConfiguration) { + let found = this.findCriterion(id); + if (found) { + const newCriterionConfiguration = this.generateCriterion(criterionConfiguration); + let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct); + newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); + newCriterion.on('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj)); + newCriterion.on('telemetryStaleness', () => this.handleTelemetryStaleness()); + + let criterion = found.item; + criterion.unsubscribe(); + criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); + criterion.off('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj)); + newCriterion.off('telemetryStaleness', () => this.handleTelemetryStaleness()); + this.criteria.splice(found.index, 1, newCriterion); + } + } + + destroyCriterion(id) { + let found = this.findCriterion(id); + if (found) { + let criterion = found.item; + criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); + criterion.off('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj)); + criterion.off('telemetryStaleness', () => this.handleTelemetryStaleness()); + criterion.destroy(); + this.criteria.splice(found.index, 1); + + return true; } - updateCriteria(criterionConfigurations) { - this.destroyCriteria(); - this.createCriteria(criterionConfigurations); - } + return false; + } - updateTelemetryObjects() { - this.criteria.forEach((criterion) => { - criterion.updateTelemetryObjects(this.conditionManager.telemetryObjects); - }); + handleCriterionUpdated(criterion) { + let found = this.findCriterion(criterion.id); + if (found) { + this.criteria[found.index] = criterion.data; } + } - /** - * adds criterion to the condition. - */ - addCriterion(criterionConfiguration) { - let criterion; - let criterionConfigurationWithId = this.generateCriterion(criterionConfiguration || null); - if (criterionConfiguration.telemetry && (criterionConfiguration.telemetry === 'any' || criterionConfiguration.telemetry === 'all')) { - criterion = new AllTelemetryCriterion(criterionConfigurationWithId, this.openmct); - } else { - criterion = new TelemetryCriterion(criterionConfigurationWithId, this.openmct); + handleOldTelemetryCriterion(updatedCriterion) { + this.result = evaluateResults( + this.criteria.map((criterion) => criterion.result), + this.trigger + ); + let latestTimestamp = {}; + latestTimestamp = getLatestTimestamp( + latestTimestamp, + updatedCriterion.data, + this.timeSystems, + this.openmct.time.timeSystem() + ); + this.conditionManager.updateCurrentCondition(latestTimestamp); + } + + handleTelemetryStaleness() { + this.result = evaluateResults( + this.criteria.map((criterion) => criterion.result), + this.trigger + ); + this.conditionManager.updateCurrentCondition(); + } + + updateDescription() { + const triggerDescription = this.getTriggerDescription(); + let description = ''; + this.criteria.forEach((criterion, index) => { + if (!index) { + description = `Match if ${triggerDescription.prefix}`; + } + + description = `${description} ${criterion.getDescription()} ${ + index < this.criteria.length - 1 ? triggerDescription.conjunction : '' + }`; + }); + this.summary = description; + } + + getTriggerDescription() { + if (this.trigger) { + return { + conjunction: TRIGGER_CONJUNCTION[this.trigger], + prefix: `${TRIGGER_LABEL[this.trigger]}: ` + }; + } else { + return { + conjunction: '', + prefix: '' + }; + } + } + + requestLADConditionResult(options) { + let latestTimestamp; + let criteriaResults = {}; + const criteriaRequests = this.criteria.map((criterion) => + criterion.requestLAD(this.conditionManager.telemetryObjects, options) + ); + + return Promise.all(criteriaRequests).then((results) => { + results.forEach((resultObj) => { + const { + id, + data, + data: { result } + } = resultObj; + if (this.findCriterion(id)) { + criteriaResults[id] = Boolean(result); } - criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); - criterion.on('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj)); - criterion.on('telemetryStaleness', () => this.handleTelemetryStaleness()); - if (!this.criteria) { - this.criteria = []; - } - - this.criteria.push(criterion); - - return criterionConfigurationWithId.id; - } - - findCriterion(id) { - let criterion; - - for (let i = 0, ii = this.criteria.length; i < ii; i++) { - if (this.criteria[i].id === id) { - criterion = { - item: this.criteria[i], - index: i - }; - } - } - - return criterion; - } - - updateCriterion(id, criterionConfiguration) { - let found = this.findCriterion(id); - if (found) { - const newCriterionConfiguration = this.generateCriterion(criterionConfiguration); - let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct); - newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); - newCriterion.on('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj)); - newCriterion.on('telemetryStaleness', () => this.handleTelemetryStaleness()); - - let criterion = found.item; - criterion.unsubscribe(); - criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); - criterion.off('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj)); - newCriterion.off('telemetryStaleness', () => this.handleTelemetryStaleness()); - this.criteria.splice(found.index, 1, newCriterion); - } - } - - destroyCriterion(id) { - let found = this.findCriterion(id); - if (found) { - let criterion = found.item; - criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); - criterion.off('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj)); - criterion.off('telemetryStaleness', () => this.handleTelemetryStaleness()); - criterion.destroy(); - this.criteria.splice(found.index, 1); - - return true; - } - - return false; - } - - handleCriterionUpdated(criterion) { - let found = this.findCriterion(criterion.id); - if (found) { - this.criteria[found.index] = criterion.data; - } - } - - handleOldTelemetryCriterion(updatedCriterion) { - this.result = evaluateResults(this.criteria.map(criterion => criterion.result), this.trigger); - let latestTimestamp = {}; latestTimestamp = getLatestTimestamp( - latestTimestamp, - updatedCriterion.data, - this.timeSystems, - this.openmct.time.timeSystem() + latestTimestamp, + data, + this.timeSystems, + this.openmct.time.timeSystem() ); - this.conditionManager.updateCurrentCondition(latestTimestamp); + }); + + return { + id: this.id, + data: Object.assign({}, latestTimestamp, { + result: evaluateResults(Object.values(criteriaResults), this.trigger) + }) + }; + }); + } + + getCriteria() { + return this.criteria; + } + + destroyCriteria() { + let success = true; + //looping through the array backwards since destroyCriterion modifies the criteria array + for (let i = this.criteria.length - 1; i >= 0; i--) { + success = success && this.destroyCriterion(this.criteria[i].id); } - handleTelemetryStaleness() { - this.result = evaluateResults(this.criteria.map(criterion => criterion.result), this.trigger); - this.conditionManager.updateCurrentCondition(); - } + return success; + } - updateDescription() { - const triggerDescription = this.getTriggerDescription(); - let description = ''; - this.criteria.forEach((criterion, index) => { - if (!index) { - description = `Match if ${triggerDescription.prefix}`; - } - - description = `${description} ${criterion.getDescription()} ${(index < this.criteria.length - 1) ? triggerDescription.conjunction : ''}`; - }); - this.summary = description; - } - - getTriggerDescription() { - if (this.trigger) { - return { - conjunction: TRIGGER_CONJUNCTION[this.trigger], - prefix: `${TRIGGER_LABEL[this.trigger]}: ` - }; - } else { - return { - conjunction: '', - prefix: '' - }; - } - } - - requestLADConditionResult(options) { - let latestTimestamp; - let criteriaResults = {}; - const criteriaRequests = this.criteria - .map(criterion => criterion.requestLAD(this.conditionManager.telemetryObjects, options)); - - return Promise.all(criteriaRequests) - .then(results => { - results.forEach(resultObj => { - const { id, data, data: { result } } = resultObj; - if (this.findCriterion(id)) { - criteriaResults[id] = Boolean(result); - } - - latestTimestamp = getLatestTimestamp( - latestTimestamp, - data, - this.timeSystems, - this.openmct.time.timeSystem() - ); - }); - - return { - id: this.id, - data: Object.assign( - {}, - latestTimestamp, - { result: evaluateResults(Object.values(criteriaResults), this.trigger) } - ) - }; - }); - } - - getCriteria() { - return this.criteria; - } - - destroyCriteria() { - let success = true; - //looping through the array backwards since destroyCriterion modifies the criteria array - for (let i = this.criteria.length - 1; i >= 0; i--) { - success = success && this.destroyCriterion(this.criteria[i].id); - } - - return success; - } - - destroy() { - this.destroyCriteria(); - } + destroy() { + this.destroyCriteria(); + } } diff --git a/src/plugins/condition/ConditionManager.js b/src/plugins/condition/ConditionManager.js index 0e4876d11c..99379f2938 100644 --- a/src/plugins/condition/ConditionManager.js +++ b/src/plugins/condition/ConditionManager.js @@ -20,445 +20,494 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import Condition from "./Condition"; +import Condition from './Condition'; import { getLatestTimestamp } from './utils/time'; import { v4 as uuid } from 'uuid'; import EventEmitter from 'EventEmitter'; export default class ConditionManager extends EventEmitter { - constructor(conditionSetDomainObject, openmct) { - super(); - this.openmct = openmct; - this.conditionSetDomainObject = conditionSetDomainObject; - this.timeSystems = this.openmct.time.getAllTimeSystems(); - this.composition = this.openmct.composition.get(conditionSetDomainObject); - this.composition.on('add', this.subscribeToTelemetry, this); - this.composition.on('remove', this.unsubscribeFromTelemetry, this); + constructor(conditionSetDomainObject, openmct) { + super(); + this.openmct = openmct; + this.conditionSetDomainObject = conditionSetDomainObject; + this.timeSystems = this.openmct.time.getAllTimeSystems(); + this.composition = this.openmct.composition.get(conditionSetDomainObject); + this.composition.on('add', this.subscribeToTelemetry, this); + this.composition.on('remove', this.unsubscribeFromTelemetry, this); - this.shouldEvaluateNewTelemetry = this.shouldEvaluateNewTelemetry.bind(this); + this.shouldEvaluateNewTelemetry = this.shouldEvaluateNewTelemetry.bind(this); - this.compositionLoad = this.composition.load(); - this.subscriptions = {}; - this.telemetryObjects = {}; - this.testData = { - conditionTestInputs: this.conditionSetDomainObject.configuration.conditionTestData, - applied: false - }; - this.initialize(); + this.compositionLoad = this.composition.load(); + this.subscriptions = {}; + this.telemetryObjects = {}; + this.testData = { + conditionTestInputs: this.conditionSetDomainObject.configuration.conditionTestData, + applied: false + }; + this.initialize(); - this.stopObservingForChanges = this.openmct.objects.observe(this.conditionSetDomainObject, '*', (newDomainObject) => { - this.conditionSetDomainObject = newDomainObject; + this.stopObservingForChanges = this.openmct.objects.observe( + this.conditionSetDomainObject, + '*', + (newDomainObject) => { + this.conditionSetDomainObject = newDomainObject; + } + ); + } + + subscribeToTelemetry(endpoint) { + const id = this.openmct.objects.makeKeyString(endpoint.identifier); + if (this.subscriptions[id]) { + console.log('subscription already exists'); + + return; + } + + const metadata = this.openmct.telemetry.getMetadata(endpoint); + + this.telemetryObjects[id] = Object.assign({}, endpoint, { + telemetryMetaData: metadata ? metadata.valueMetadatas : [] + }); + this.subscriptions[id] = this.openmct.telemetry.subscribe( + endpoint, + this.telemetryReceived.bind(this, endpoint) + ); + this.updateConditionTelemetryObjects(); + } + + unsubscribeFromTelemetry(endpointIdentifier) { + const id = this.openmct.objects.makeKeyString(endpointIdentifier); + if (!this.subscriptions[id]) { + console.log('no subscription to remove'); + + return; + } + + this.subscriptions[id](); + delete this.subscriptions[id]; + delete this.telemetryObjects[id]; + this.removeConditionTelemetryObjects(); + + //force re-computation of condition set result as we might be in a state where + // there is no telemetry datum coming in for a while or at all. + let latestTimestamp = getLatestTimestamp( + {}, + {}, + this.timeSystems, + this.openmct.time.timeSystem() + ); + this.updateConditionResults({ id: id }); + this.updateCurrentCondition(latestTimestamp); + + if (Object.keys(this.telemetryObjects).length === 0) { + // no telemetry objects + this.emit('noTelemetryObjects'); + } + } + + initialize() { + this.conditions = []; + if (this.conditionSetDomainObject.configuration.conditionCollection.length) { + this.conditionSetDomainObject.configuration.conditionCollection.forEach( + (conditionConfiguration, index) => { + this.initCondition(conditionConfiguration, index); + } + ); + } + + if (Object.keys(this.telemetryObjects).length === 0) { + // no telemetry objects + this.emit('noTelemetryObjects'); + } + } + + updateConditionTelemetryObjects() { + this.conditions.forEach((condition) => { + condition.updateTelemetryObjects(); + let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex( + (item) => item.id === condition.id + ); + if (index > -1) { + //Only assign the summary, don't mutate the domain object + this.conditionSetDomainObject.configuration.conditionCollection[index].summary = + this.updateConditionDescription(condition); + } + }); + } + + removeConditionTelemetryObjects() { + let conditionsChanged = false; + this.conditionSetDomainObject.configuration.conditionCollection.forEach( + (conditionConfiguration, conditionIndex) => { + let conditionChanged = false; + conditionConfiguration.configuration.criteria.forEach((criterion, index) => { + const isAnyAllTelemetry = + criterion.telemetry && (criterion.telemetry === 'any' || criterion.telemetry === 'all'); + if (!isAnyAllTelemetry) { + const found = Object.values(this.telemetryObjects).find((telemetryObject) => { + return this.openmct.objects.areIdsEqual( + telemetryObject.identifier, + criterion.telemetry + ); + }); + if (!found) { + criterion.telemetry = ''; + criterion.metadata = ''; + criterion.input = []; + criterion.operation = ''; + conditionChanged = true; + } + } else { + conditionChanged = true; + } }); + if (conditionChanged) { + this.updateCondition(conditionConfiguration, conditionIndex); + conditionsChanged = true; + } + } + ); + if (conditionsChanged) { + this.persistConditions(); + } + } + updateConditionDescription(condition) { + condition.updateDescription(); + + return condition.summary; + } + + updateCondition(conditionConfiguration) { + let condition = this.findConditionById(conditionConfiguration.id); + if (condition) { + condition.update(conditionConfiguration); + conditionConfiguration.summary = this.updateConditionDescription(condition); } - subscribeToTelemetry(endpoint) { - const id = this.openmct.objects.makeKeyString(endpoint.identifier); - if (this.subscriptions[id]) { - console.log('subscription already exists'); + let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex( + (item) => item.id === conditionConfiguration.id + ); + if (index > -1) { + this.conditionSetDomainObject.configuration.conditionCollection[index] = + conditionConfiguration; + this.persistConditions(); + } + } - return; + initCondition(conditionConfiguration, index) { + let condition = new Condition(conditionConfiguration, this.openmct, this); + conditionConfiguration.summary = this.updateConditionDescription(condition); + + if (index !== undefined) { + this.conditions.splice(index + 1, 0, condition); + } else { + this.conditions.unshift(condition); + } + } + + createCondition(conditionConfiguration) { + let conditionObj; + if (conditionConfiguration) { + conditionObj = { + ...conditionConfiguration, + id: uuid(), + configuration: { + ...conditionConfiguration.configuration, + name: `Copy of ${conditionConfiguration.configuration.name}` } - - const metadata = this.openmct.telemetry.getMetadata(endpoint); - - this.telemetryObjects[id] = Object.assign({}, endpoint, {telemetryMetaData: metadata ? metadata.valueMetadatas : []}); - this.subscriptions[id] = this.openmct.telemetry.subscribe( - endpoint, - this.telemetryReceived.bind(this, endpoint) - ); - this.updateConditionTelemetryObjects(); + }; + } else { + conditionObj = { + id: uuid(), + configuration: { + name: 'Unnamed Condition', + output: 'false', + trigger: 'all', + criteria: [ + { + id: uuid(), + telemetry: '', + operation: '', + input: [], + metadata: '' + } + ] + }, + summary: '' + }; } - unsubscribeFromTelemetry(endpointIdentifier) { - const id = this.openmct.objects.makeKeyString(endpointIdentifier); - if (!this.subscriptions[id]) { - console.log('no subscription to remove'); + return conditionObj; + } - return; - } + addCondition() { + this.createAndSaveCondition(); + } - this.subscriptions[id](); - delete this.subscriptions[id]; - delete this.telemetryObjects[id]; - this.removeConditionTelemetryObjects(); + cloneCondition(conditionConfiguration, index) { + let clonedConfig = JSON.parse(JSON.stringify(conditionConfiguration)); + clonedConfig.configuration.criteria.forEach((criterion) => (criterion.id = uuid())); + this.createAndSaveCondition(index, clonedConfig); + } - //force re-computation of condition set result as we might be in a state where - // there is no telemetry datum coming in for a while or at all. - let latestTimestamp = getLatestTimestamp( - {}, - {}, + createAndSaveCondition(index, conditionConfiguration) { + const newCondition = this.createCondition(conditionConfiguration); + if (index !== undefined) { + this.conditionSetDomainObject.configuration.conditionCollection.splice( + index + 1, + 0, + newCondition + ); + } else { + this.conditionSetDomainObject.configuration.conditionCollection.unshift(newCondition); + } + + this.initCondition(newCondition, index); + this.persistConditions(); + } + + removeCondition(id) { + let index = this.conditions.findIndex((item) => item.id === id); + if (index > -1) { + this.conditions[index].destroy(); + this.conditions.splice(index, 1); + } + + let conditionCollectionIndex = + this.conditionSetDomainObject.configuration.conditionCollection.findIndex( + (item) => item.id === id + ); + if (conditionCollectionIndex > -1) { + this.conditionSetDomainObject.configuration.conditionCollection.splice( + conditionCollectionIndex, + 1 + ); + this.persistConditions(); + } + } + + findConditionById(id) { + return this.conditions.find((condition) => condition.id === id); + } + + reorderConditions(reorderPlan) { + let oldConditions = Array.from(this.conditionSetDomainObject.configuration.conditionCollection); + let newCollection = []; + reorderPlan.forEach((reorderEvent) => { + let item = oldConditions[reorderEvent.oldIndex]; + newCollection.push(item); + }); + this.conditionSetDomainObject.configuration.conditionCollection = newCollection; + this.persistConditions(); + } + + getCurrentCondition() { + const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection; + let currentCondition = conditionCollection[conditionCollection.length - 1]; + + for (let i = 0; i < conditionCollection.length - 1; i++) { + const condition = this.findConditionById(conditionCollection[i].id); + if (condition.result) { + //first condition to be true wins + currentCondition = conditionCollection[i]; + break; + } + } + + return currentCondition; + } + + getCurrentConditionLAD(conditionResults) { + const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection; + let currentCondition = conditionCollection[conditionCollection.length - 1]; + + for (let i = 0; i < conditionCollection.length - 1; i++) { + if (conditionResults[conditionCollection[i].id]) { + //first condition to be true wins + currentCondition = conditionCollection[i]; + break; + } + } + + return currentCondition; + } + + requestLADConditionSetOutput(options) { + if (!this.conditions.length) { + return Promise.resolve([]); + } + + return this.compositionLoad.then(() => { + let latestTimestamp; + let conditionResults = {}; + let nextLegOptions = { ...options }; + delete nextLegOptions.onPartialResponse; + + const conditionRequests = this.conditions.map((condition) => + condition.requestLADConditionResult(nextLegOptions) + ); + + return Promise.all(conditionRequests).then((results) => { + results.forEach((resultObj) => { + const { + id, + data, + data: { result } + } = resultObj; + if (this.findConditionById(id)) { + conditionResults[id] = Boolean(result); + } + + latestTimestamp = getLatestTimestamp( + latestTimestamp, + data, this.timeSystems, this.openmct.time.timeSystem() + ); + }); + + if (!Object.values(latestTimestamp).some((timeSystem) => timeSystem)) { + return []; + } + + const currentCondition = this.getCurrentConditionLAD(conditionResults); + const currentOutput = Object.assign( + { + output: currentCondition.configuration.output, + id: this.conditionSetDomainObject.identifier, + conditionId: currentCondition.id + }, + latestTimestamp ); - this.updateConditionResults({id: id}); - this.updateCurrentCondition(latestTimestamp); - if (Object.keys(this.telemetryObjects).length === 0) { - // no telemetry objects - this.emit('noTelemetryObjects'); - } + return [currentOutput]; + }); + }); + } + + isTelemetryUsed(endpoint) { + const id = this.openmct.objects.makeKeyString(endpoint.identifier); + + for (let condition of this.conditions) { + if (condition.isTelemetryUsed(id)) { + return true; + } } - initialize() { - this.conditions = []; - if (this.conditionSetDomainObject.configuration.conditionCollection.length) { - this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, index) => { - this.initCondition(conditionConfiguration, index); - }); - } + return false; + } - if (Object.keys(this.telemetryObjects).length === 0) { - // no telemetry objects - this.emit('noTelemetryObjects'); - } + shouldEvaluateNewTelemetry(currentTimestamp) { + return this.openmct.time.bounds().end >= currentTimestamp; + } + + telemetryReceived(endpoint, datum) { + if (!this.isTelemetryUsed(endpoint)) { + return; } - updateConditionTelemetryObjects() { - this.conditions.forEach((condition) => { - condition.updateTelemetryObjects(); - let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(item => item.id === condition.id); - if (index > -1) { - //Only assign the summary, don't mutate the domain object - this.conditionSetDomainObject.configuration.conditionCollection[index].summary = this.updateConditionDescription(condition); - } - }); + const normalizedDatum = this.createNormalizedDatum(datum, endpoint); + const timeSystemKey = this.openmct.time.timeSystem().key; + let timestamp = {}; + const currentTimestamp = normalizedDatum[timeSystemKey]; + timestamp[timeSystemKey] = currentTimestamp; + if (this.shouldEvaluateNewTelemetry(currentTimestamp)) { + this.updateConditionResults(normalizedDatum); + this.updateCurrentCondition(timestamp); + } + } + + updateConditionResults(normalizedDatum) { + //We want to stop when the first condition evaluates to true. + this.conditions.some((condition) => { + condition.updateResult(normalizedDatum); + + return condition.result === true; + }); + } + + updateCurrentCondition(timestamp) { + const currentCondition = this.getCurrentCondition(); + + this.emit( + 'conditionSetResultUpdated', + Object.assign( + { + output: currentCondition.configuration.output, + id: this.conditionSetDomainObject.identifier, + conditionId: currentCondition.id + }, + timestamp + ) + ); + } + + getTestData(metadatum) { + let data = undefined; + if (this.testData.applied) { + const found = this.testData.conditionTestInputs.find( + (testInput) => testInput.metadata === metadatum.source + ); + if (found) { + data = found.value; + } } - removeConditionTelemetryObjects() { - let conditionsChanged = false; - this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, conditionIndex) => { - let conditionChanged = false; - conditionConfiguration.configuration.criteria.forEach((criterion, index) => { - const isAnyAllTelemetry = criterion.telemetry && (criterion.telemetry === 'any' || criterion.telemetry === 'all'); - if (!isAnyAllTelemetry) { - const found = Object.values(this.telemetryObjects).find((telemetryObject) => { - return this.openmct.objects.areIdsEqual(telemetryObject.identifier, criterion.telemetry); - }); - if (!found) { - criterion.telemetry = ''; - criterion.metadata = ''; - criterion.input = []; - criterion.operation = ''; - conditionChanged = true; - } - } else { - conditionChanged = true; - } - }); - if (conditionChanged) { - this.updateCondition(conditionConfiguration, conditionIndex); - conditionsChanged = true; - } - }); - if (conditionsChanged) { - this.persistConditions(); - } + return data; + } + + createNormalizedDatum(telemetryDatum, endpoint) { + const id = this.openmct.objects.makeKeyString(endpoint.identifier); + const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas; + + const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => { + const testValue = this.getTestData(metadatum); + const formatter = this.openmct.telemetry.getValueFormatter(metadatum); + datum[metadatum.key] = + testValue !== undefined + ? formatter.parse(testValue) + : formatter.parse(telemetryDatum[metadatum.source]); + + return datum; + }, {}); + + normalizedDatum.id = id; + + return normalizedDatum; + } + + updateTestData(testData) { + if (!_.isEqual(testData, this.testData)) { + this.testData = testData; + this.openmct.objects.mutate( + this.conditionSetDomainObject, + 'configuration.conditionTestData', + this.testData.conditionTestInputs + ); + } + } + + persistConditions() { + this.openmct.objects.mutate( + this.conditionSetDomainObject, + 'configuration.conditionCollection', + this.conditionSetDomainObject.configuration.conditionCollection + ); + } + + destroy() { + this.composition.off('add', this.subscribeToTelemetry, this); + this.composition.off('remove', this.unsubscribeFromTelemetry, this); + Object.values(this.subscriptions).forEach((unsubscribe) => unsubscribe()); + delete this.subscriptions; + + if (this.stopObservingForChanges) { + this.stopObservingForChanges(); } - updateConditionDescription(condition) { - condition.updateDescription(); - - return condition.summary; - } - - updateCondition(conditionConfiguration) { - let condition = this.findConditionById(conditionConfiguration.id); - if (condition) { - condition.update(conditionConfiguration); - conditionConfiguration.summary = this.updateConditionDescription(condition); - } - - let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(item => item.id === conditionConfiguration.id); - if (index > -1) { - this.conditionSetDomainObject.configuration.conditionCollection[index] = conditionConfiguration; - this.persistConditions(); - } - } - - initCondition(conditionConfiguration, index) { - let condition = new Condition(conditionConfiguration, this.openmct, this); - conditionConfiguration.summary = this.updateConditionDescription(condition); - - if (index !== undefined) { - this.conditions.splice(index + 1, 0, condition); - } else { - this.conditions.unshift(condition); - } - } - - createCondition(conditionConfiguration) { - let conditionObj; - if (conditionConfiguration) { - conditionObj = { - ...conditionConfiguration, - id: uuid(), - configuration: { - ...conditionConfiguration.configuration, - name: `Copy of ${conditionConfiguration.configuration.name}` - } - }; - } else { - conditionObj = { - id: uuid(), - configuration: { - name: 'Unnamed Condition', - output: 'false', - trigger: 'all', - criteria: [{ - id: uuid(), - telemetry: '', - operation: '', - input: [], - metadata: '' - }] - }, - summary: '' - }; - } - - return conditionObj; - } - - addCondition() { - this.createAndSaveCondition(); - } - - cloneCondition(conditionConfiguration, index) { - let clonedConfig = JSON.parse(JSON.stringify(conditionConfiguration)); - clonedConfig.configuration.criteria.forEach((criterion) => criterion.id = uuid()); - this.createAndSaveCondition(index, clonedConfig); - } - - createAndSaveCondition(index, conditionConfiguration) { - const newCondition = this.createCondition(conditionConfiguration); - if (index !== undefined) { - this.conditionSetDomainObject.configuration.conditionCollection.splice(index + 1, 0, newCondition); - } else { - this.conditionSetDomainObject.configuration.conditionCollection.unshift(newCondition); - } - - this.initCondition(newCondition, index); - this.persistConditions(); - } - - removeCondition(id) { - let index = this.conditions.findIndex(item => item.id === id); - if (index > -1) { - this.conditions[index].destroy(); - this.conditions.splice(index, 1); - } - - let conditionCollectionIndex = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(item => item.id === id); - if (conditionCollectionIndex > -1) { - this.conditionSetDomainObject.configuration.conditionCollection.splice(conditionCollectionIndex, 1); - this.persistConditions(); - } - } - - findConditionById(id) { - return this.conditions.find(condition => condition.id === id); - } - - reorderConditions(reorderPlan) { - let oldConditions = Array.from(this.conditionSetDomainObject.configuration.conditionCollection); - let newCollection = []; - reorderPlan.forEach((reorderEvent) => { - let item = oldConditions[reorderEvent.oldIndex]; - newCollection.push(item); - }); - this.conditionSetDomainObject.configuration.conditionCollection = newCollection; - this.persistConditions(); - } - - getCurrentCondition() { - const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection; - let currentCondition = conditionCollection[conditionCollection.length - 1]; - - for (let i = 0; i < conditionCollection.length - 1; i++) { - const condition = this.findConditionById(conditionCollection[i].id); - if (condition.result) { - //first condition to be true wins - currentCondition = conditionCollection[i]; - break; - } - } - - return currentCondition; - } - - getCurrentConditionLAD(conditionResults) { - const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection; - let currentCondition = conditionCollection[conditionCollection.length - 1]; - - for (let i = 0; i < conditionCollection.length - 1; i++) { - if (conditionResults[conditionCollection[i].id]) { - //first condition to be true wins - currentCondition = conditionCollection[i]; - break; - } - } - - return currentCondition; - } - - requestLADConditionSetOutput(options) { - if (!this.conditions.length) { - return Promise.resolve([]); - } - - return this.compositionLoad.then(() => { - let latestTimestamp; - let conditionResults = {}; - let nextLegOptions = {...options}; - delete nextLegOptions.onPartialResponse; - - const conditionRequests = this.conditions - .map(condition => condition.requestLADConditionResult(nextLegOptions)); - - return Promise.all(conditionRequests) - .then((results) => { - results.forEach(resultObj => { - const { id, data, data: { result } } = resultObj; - if (this.findConditionById(id)) { - conditionResults[id] = Boolean(result); - } - - latestTimestamp = getLatestTimestamp( - latestTimestamp, - data, - this.timeSystems, - this.openmct.time.timeSystem() - ); - }); - - if (!Object.values(latestTimestamp).some(timeSystem => timeSystem)) { - return []; - } - - const currentCondition = this.getCurrentConditionLAD(conditionResults); - const currentOutput = Object.assign( - { - output: currentCondition.configuration.output, - id: this.conditionSetDomainObject.identifier, - conditionId: currentCondition.id - }, - latestTimestamp - ); - - return [currentOutput]; - }); - }); - } - - isTelemetryUsed(endpoint) { - const id = this.openmct.objects.makeKeyString(endpoint.identifier); - - for (let condition of this.conditions) { - if (condition.isTelemetryUsed(id)) { - return true; - } - } - - return false; - } - - shouldEvaluateNewTelemetry(currentTimestamp) { - return this.openmct.time.bounds().end >= currentTimestamp; - } - - telemetryReceived(endpoint, datum) { - if (!this.isTelemetryUsed(endpoint)) { - return; - } - - const normalizedDatum = this.createNormalizedDatum(datum, endpoint); - const timeSystemKey = this.openmct.time.timeSystem().key; - let timestamp = {}; - const currentTimestamp = normalizedDatum[timeSystemKey]; - timestamp[timeSystemKey] = currentTimestamp; - if (this.shouldEvaluateNewTelemetry(currentTimestamp)) { - this.updateConditionResults(normalizedDatum); - this.updateCurrentCondition(timestamp); - } - } - - updateConditionResults(normalizedDatum) { - //We want to stop when the first condition evaluates to true. - this.conditions.some((condition) => { - condition.updateResult(normalizedDatum); - - return condition.result === true; - }); - } - - updateCurrentCondition(timestamp) { - const currentCondition = this.getCurrentCondition(); - - this.emit('conditionSetResultUpdated', - Object.assign( - { - output: currentCondition.configuration.output, - id: this.conditionSetDomainObject.identifier, - conditionId: currentCondition.id - }, - timestamp - ) - ); - } - - getTestData(metadatum) { - let data = undefined; - if (this.testData.applied) { - const found = this.testData.conditionTestInputs.find((testInput) => (testInput.metadata === metadatum.source)); - if (found) { - data = found.value; - } - } - - return data; - } - - createNormalizedDatum(telemetryDatum, endpoint) { - const id = this.openmct.objects.makeKeyString(endpoint.identifier); - const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas; - - const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => { - const testValue = this.getTestData(metadatum); - const formatter = this.openmct.telemetry.getValueFormatter(metadatum); - datum[metadatum.key] = testValue !== undefined ? formatter.parse(testValue) : formatter.parse(telemetryDatum[metadatum.source]); - - return datum; - }, {}); - - normalizedDatum.id = id; - - return normalizedDatum; - } - - updateTestData(testData) { - if (!_.isEqual(testData, this.testData)) { - this.testData = testData; - this.openmct.objects.mutate(this.conditionSetDomainObject, 'configuration.conditionTestData', this.testData.conditionTestInputs); - } - } - - persistConditions() { - this.openmct.objects.mutate(this.conditionSetDomainObject, 'configuration.conditionCollection', this.conditionSetDomainObject.configuration.conditionCollection); - } - - destroy() { - this.composition.off('add', this.subscribeToTelemetry, this); - this.composition.off('remove', this.unsubscribeFromTelemetry, this); - Object.values(this.subscriptions).forEach(unsubscribe => unsubscribe()); - delete this.subscriptions; - - if (this.stopObservingForChanges) { - this.stopObservingForChanges(); - } - - this.conditions.forEach((condition) => { - condition.destroy(); - }); - } + this.conditions.forEach((condition) => { + condition.destroy(); + }); + } } diff --git a/src/plugins/condition/ConditionManagerSpec.js b/src/plugins/condition/ConditionManagerSpec.js index cbbed22177..34777b1f1a 100644 --- a/src/plugins/condition/ConditionManagerSpec.js +++ b/src/plugins/condition/ConditionManagerSpec.js @@ -23,193 +23,206 @@ import ConditionManager from './ConditionManager'; describe('ConditionManager', () => { - - let conditionMgr; - let mockListener; - let openmct = {}; - let mockDefaultCondition = { - isDefault: true, - id: '1234-5678', - configuration: { - criteria: [] - } - }; - let mockCondition1 = { - id: '2345-6789', - configuration: { - criteria: [] - } - }; - let updatedMockCondition1 = { - id: '2345-6789', - configuration: { - trigger: 'xor', - criteria: [] - } - }; - let mockCondition2 = { - id: '3456-7890', - configuration: { - criteria: [] - } - }; - let conditionSetDomainObject = { - identifier: { - namespace: "", - key: "600a7372-8d48-4dc4-98b6-548611b1ff7e" - }, - type: "conditionSet", - location: "mine", - configuration: { - conditionCollection: [ - mockCondition1, - mockCondition2, - mockDefaultCondition - ] - } - }; - let mockComposition; - let loader; - let mockTimeSystems; - - function mockAngularComponents() { - let mockInjector = jasmine.createSpyObj('$injector', ['get']); - - let mockInstantiate = jasmine.createSpy('mockInstantiate'); - mockInstantiate.and.returnValue(mockInstantiate); - - let mockDomainObject = { - useCapability: function () { - return mockDefaultCondition; - } - }; - mockInstantiate.and.callFake(function () { - return mockDomainObject; - }); - mockInjector.get.and.callFake(function (service) { - return { - 'instantiate': mockInstantiate - }[service]; - }); - - openmct.$injector = mockInjector; + let conditionMgr; + let mockListener; + let openmct = {}; + let mockDefaultCondition = { + isDefault: true, + id: '1234-5678', + configuration: { + criteria: [] } + }; + let mockCondition1 = { + id: '2345-6789', + configuration: { + criteria: [] + } + }; + let updatedMockCondition1 = { + id: '2345-6789', + configuration: { + trigger: 'xor', + criteria: [] + } + }; + let mockCondition2 = { + id: '3456-7890', + configuration: { + criteria: [] + } + }; + let conditionSetDomainObject = { + identifier: { + namespace: '', + key: '600a7372-8d48-4dc4-98b6-548611b1ff7e' + }, + type: 'conditionSet', + location: 'mine', + configuration: { + conditionCollection: [mockCondition1, mockCondition2, mockDefaultCondition] + } + }; + let mockComposition; + let loader; + let mockTimeSystems; - beforeEach(function () { + function mockAngularComponents() { + let mockInjector = jasmine.createSpyObj('$injector', ['get']); - mockAngularComponents(); - mockListener = jasmine.createSpy('mockListener'); - loader = {}; - loader.promise = new Promise(function (resolve, reject) { - loader.resolve = resolve; - loader.reject = reject; - }); + let mockInstantiate = jasmine.createSpy('mockInstantiate'); + mockInstantiate.and.returnValue(mockInstantiate); - mockComposition = jasmine.createSpyObj('compositionCollection', [ - 'load', - 'on', - 'off' - ]); - mockComposition.load.and.callFake(() => { - setTimeout(() => { - loader.resolve(); - }); - - return loader.promise; - }); - mockComposition.on('add', mockListener); - mockComposition.on('remove', mockListener); - openmct.composition = jasmine.createSpyObj('compositionAPI', [ - 'get' - ]); - openmct.composition.get.and.returnValue(mockComposition); - - openmct.objects = jasmine.createSpyObj('objects', ['get', 'makeKeyString', 'observe', 'mutate']); - openmct.objects.get.and.returnValues(new Promise(function (resolve, reject) { - resolve(conditionSetDomainObject); - }), new Promise(function (resolve, reject) { - resolve(mockCondition1); - }), new Promise(function (resolve, reject) { - resolve(mockCondition2); - }), new Promise(function (resolve, reject) { - resolve(mockDefaultCondition); - })); - openmct.objects.makeKeyString.and.returnValue(conditionSetDomainObject.identifier.key); - openmct.objects.observe.and.returnValue(function () {}); - openmct.objects.mutate.and.returnValue(function () {}); - - mockTimeSystems = { - key: 'utc' - }; - openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems']); - openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]); - - conditionMgr = new ConditionManager(conditionSetDomainObject, openmct); - - conditionMgr.on('conditionSetResultUpdated', mockListener); - conditionMgr.on('telemetryReceived', mockListener); + let mockDomainObject = { + useCapability: function () { + return mockDefaultCondition; + } + }; + mockInstantiate.and.callFake(function () { + return mockDomainObject; + }); + mockInjector.get.and.callFake(function (service) { + return { + instantiate: mockInstantiate + }[service]; }); - it('creates a conditionCollection with a default condition', function () { - expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection.length).toEqual(3); - let defaultConditionId = conditionMgr.conditions[2].id; - expect(defaultConditionId).toEqual(mockDefaultCondition.id); + openmct.$injector = mockInjector; + } + + beforeEach(function () { + mockAngularComponents(); + mockListener = jasmine.createSpy('mockListener'); + loader = {}; + loader.promise = new Promise(function (resolve, reject) { + loader.resolve = resolve; + loader.reject = reject; }); - it('reorders a conditionCollection', function () { - let reorderPlan = [{ - oldIndex: 1, - newIndex: 0 - }, - { - oldIndex: 0, - newIndex: 1 - }, - { - oldIndex: 2, - newIndex: 2 - }]; - conditionMgr.reorderConditions(reorderPlan); - expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection.length).toEqual(3); - expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection[0].id).toEqual(mockCondition2.id); - expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection[1].id).toEqual(mockCondition1.id); - }); + mockComposition = jasmine.createSpyObj('compositionCollection', ['load', 'on', 'off']); + mockComposition.load.and.callFake(() => { + setTimeout(() => { + loader.resolve(); + }); - it('updates the right condition after reorder', function () { - let reorderPlan = [{ - oldIndex: 1, - newIndex: 0 - }, - { - oldIndex: 0, - newIndex: 1 - }, - { - oldIndex: 2, - newIndex: 2 - }]; - conditionMgr.reorderConditions(reorderPlan); - conditionMgr.updateCondition(updatedMockCondition1); - expect(conditionMgr.conditions[1].trigger).toEqual(updatedMockCondition1.configuration.trigger); + return loader.promise; }); + mockComposition.on('add', mockListener); + mockComposition.on('remove', mockListener); + openmct.composition = jasmine.createSpyObj('compositionAPI', ['get']); + openmct.composition.get.and.returnValue(mockComposition); - it('removes the right condition after reorder', function () { - let reorderPlan = [{ - oldIndex: 1, - newIndex: 0 - }, - { - oldIndex: 0, - newIndex: 1 - }, - { - oldIndex: 2, - newIndex: 2 - }]; - conditionMgr.reorderConditions(reorderPlan); - conditionMgr.removeCondition(mockCondition1.id); - expect(conditionMgr.conditions.length).toEqual(2); - expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection[0].id).toEqual(mockCondition2.id); - }); + openmct.objects = jasmine.createSpyObj('objects', [ + 'get', + 'makeKeyString', + 'observe', + 'mutate' + ]); + openmct.objects.get.and.returnValues( + new Promise(function (resolve, reject) { + resolve(conditionSetDomainObject); + }), + new Promise(function (resolve, reject) { + resolve(mockCondition1); + }), + new Promise(function (resolve, reject) { + resolve(mockCondition2); + }), + new Promise(function (resolve, reject) { + resolve(mockDefaultCondition); + }) + ); + openmct.objects.makeKeyString.and.returnValue(conditionSetDomainObject.identifier.key); + openmct.objects.observe.and.returnValue(function () {}); + openmct.objects.mutate.and.returnValue(function () {}); + mockTimeSystems = { + key: 'utc' + }; + openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems']); + openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]); + + conditionMgr = new ConditionManager(conditionSetDomainObject, openmct); + + conditionMgr.on('conditionSetResultUpdated', mockListener); + conditionMgr.on('telemetryReceived', mockListener); + }); + + it('creates a conditionCollection with a default condition', function () { + expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection.length).toEqual( + 3 + ); + let defaultConditionId = conditionMgr.conditions[2].id; + expect(defaultConditionId).toEqual(mockDefaultCondition.id); + }); + + it('reorders a conditionCollection', function () { + let reorderPlan = [ + { + oldIndex: 1, + newIndex: 0 + }, + { + oldIndex: 0, + newIndex: 1 + }, + { + oldIndex: 2, + newIndex: 2 + } + ]; + conditionMgr.reorderConditions(reorderPlan); + expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection.length).toEqual( + 3 + ); + expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection[0].id).toEqual( + mockCondition2.id + ); + expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection[1].id).toEqual( + mockCondition1.id + ); + }); + + it('updates the right condition after reorder', function () { + let reorderPlan = [ + { + oldIndex: 1, + newIndex: 0 + }, + { + oldIndex: 0, + newIndex: 1 + }, + { + oldIndex: 2, + newIndex: 2 + } + ]; + conditionMgr.reorderConditions(reorderPlan); + conditionMgr.updateCondition(updatedMockCondition1); + expect(conditionMgr.conditions[1].trigger).toEqual(updatedMockCondition1.configuration.trigger); + }); + + it('removes the right condition after reorder', function () { + let reorderPlan = [ + { + oldIndex: 1, + newIndex: 0 + }, + { + oldIndex: 0, + newIndex: 1 + }, + { + oldIndex: 2, + newIndex: 2 + } + ]; + conditionMgr.reorderConditions(reorderPlan); + conditionMgr.removeCondition(mockCondition1.id); + expect(conditionMgr.conditions.length).toEqual(2); + expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection[0].id).toEqual( + mockCondition2.id + ); + }); }); diff --git a/src/plugins/condition/ConditionSetCompositionPolicy.js b/src/plugins/condition/ConditionSetCompositionPolicy.js index 52baa6f2f6..51f7732365 100644 --- a/src/plugins/condition/ConditionSetCompositionPolicy.js +++ b/src/plugins/condition/ConditionSetCompositionPolicy.js @@ -21,13 +21,13 @@ *****************************************************************************/ export default function ConditionSetCompositionPolicy(openmct) { - return { - allow: function (parent, child) { - if (parent.type === 'conditionSet' && !openmct.telemetry.isTelemetryObject(child)) { - return false; - } + return { + allow: function (parent, child) { + if (parent.type === 'conditionSet' && !openmct.telemetry.isTelemetryObject(child)) { + return false; + } - return true; - } - }; + return true; + } + }; } diff --git a/src/plugins/condition/ConditionSetCompositionPolicySpec.js b/src/plugins/condition/ConditionSetCompositionPolicySpec.js index 4bbaf93789..3ed8954e40 100644 --- a/src/plugins/condition/ConditionSetCompositionPolicySpec.js +++ b/src/plugins/condition/ConditionSetCompositionPolicySpec.js @@ -23,66 +23,67 @@ import ConditionSetCompositionPolicy from './ConditionSetCompositionPolicy'; describe('ConditionSetCompositionPolicy', () => { + let policy; + let testTelemetryObject; + let openmct = {}; + let parentDomainObject; - let policy; - let testTelemetryObject; - let openmct = {}; - let parentDomainObject; - - beforeAll(function () { - 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 - } - }] + beforeAll(function () { + testTelemetryObject = { + identifier: { + namespace: '', + key: 'test-object' + }, + type: 'test-object', + name: 'Test Object', + telemetry: { + values: [ + { + key: 'some-key', + name: 'Some attribute', + hints: { + domain: 1 } - }; - openmct.objects = jasmine.createSpyObj('objects', ['get', 'makeKeyString']); - openmct.objects.get.and.returnValue(testTelemetryObject); - openmct.objects.makeKeyString.and.returnValue(testTelemetryObject.identifier.key); - openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject']); - policy = new ConditionSetCompositionPolicy(openmct); - parentDomainObject = {}; - }); + }, + { + key: 'some-other-key', + name: 'Another attribute', + hints: { + range: 1 + } + } + ] + } + }; + openmct.objects = jasmine.createSpyObj('objects', ['get', 'makeKeyString']); + openmct.objects.get.and.returnValue(testTelemetryObject); + openmct.objects.makeKeyString.and.returnValue(testTelemetryObject.identifier.key); + openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject']); + policy = new ConditionSetCompositionPolicy(openmct); + parentDomainObject = {}; + }); - it('returns true for object types that are not conditionSets', function () { - parentDomainObject.type = 'random'; - openmct.telemetry.isTelemetryObject.and.returnValue(false); - expect(policy.allow(parentDomainObject, {})).toBe(true); - }); + it('returns true for object types that are not conditionSets', function () { + parentDomainObject.type = 'random'; + openmct.telemetry.isTelemetryObject.and.returnValue(false); + expect(policy.allow(parentDomainObject, {})).toBe(true); + }); - it('returns false for object types that are not telemetry objects when parent is a conditionSet', function () { - parentDomainObject.type = 'conditionSet'; - openmct.telemetry.isTelemetryObject.and.returnValue(false); - expect(policy.allow(parentDomainObject, {})).toBe(false); - }); + it('returns false for object types that are not telemetry objects when parent is a conditionSet', function () { + parentDomainObject.type = 'conditionSet'; + openmct.telemetry.isTelemetryObject.and.returnValue(false); + expect(policy.allow(parentDomainObject, {})).toBe(false); + }); - it('returns true for object types that are telemetry objects when parent is a conditionSet', function () { - parentDomainObject.type = 'conditionSet'; - openmct.telemetry.isTelemetryObject.and.returnValue(true); - expect(policy.allow(parentDomainObject, testTelemetryObject)).toBe(true); - }); - - it('returns true for object types that are telemetry objects when parent is not a conditionSet', function () { - parentDomainObject.type = 'random'; - openmct.telemetry.isTelemetryObject.and.returnValue(true); - expect(policy.allow(parentDomainObject, testTelemetryObject)).toBe(true); - }); + it('returns true for object types that are telemetry objects when parent is a conditionSet', function () { + parentDomainObject.type = 'conditionSet'; + openmct.telemetry.isTelemetryObject.and.returnValue(true); + expect(policy.allow(parentDomainObject, testTelemetryObject)).toBe(true); + }); + it('returns true for object types that are telemetry objects when parent is not a conditionSet', function () { + parentDomainObject.type = 'random'; + openmct.telemetry.isTelemetryObject.and.returnValue(true); + expect(policy.allow(parentDomainObject, testTelemetryObject)).toBe(true); + }); }); diff --git a/src/plugins/condition/ConditionSetMetadataProvider.js b/src/plugins/condition/ConditionSetMetadataProvider.js index 9a854b83c8..8e2cb9e712 100644 --- a/src/plugins/condition/ConditionSetMetadataProvider.js +++ b/src/plugins/condition/ConditionSetMetadataProvider.js @@ -21,57 +21,56 @@ *****************************************************************************/ export default class ConditionSetMetadataProvider { - constructor(openmct) { - this.openmct = openmct; - } + constructor(openmct) { + this.openmct = openmct; + } - supportsMetadata(domainObject) { - return domainObject.type === 'conditionSet'; - } + supportsMetadata(domainObject) { + return domainObject.type === 'conditionSet'; + } - getDomains(domainObject) { - return this.openmct.time.getAllTimeSystems().map(function (ts, i) { - return { - key: ts.key, - name: ts.name, - format: ts.timeFormat, - hints: { - domain: i - } - }; - }); - } + getDomains(domainObject) { + return this.openmct.time.getAllTimeSystems().map(function (ts, i) { + return { + key: ts.key, + name: ts.name, + format: ts.timeFormat, + hints: { + domain: i + } + }; + }); + } - getMetadata(domainObject) { - const enumerations = domainObject.configuration.conditionCollection - .map((condition, index) => { - return { - string: condition.configuration.output, - value: index - }; - }); + getMetadata(domainObject) { + const enumerations = domainObject.configuration.conditionCollection.map((condition, index) => { + return { + string: condition.configuration.output, + value: index + }; + }); - return { - values: this.getDomains().concat([ - { - key: "state", - source: "output", - name: "State", - format: "enum", - enumerations: enumerations, - hints: { - range: 1 - } - }, - { - key: "output", - name: "Value", - format: "string", - hints: { - range: 2 - } - } - ]) - }; - } + return { + values: this.getDomains().concat([ + { + key: 'state', + source: 'output', + name: 'State', + format: 'enum', + enumerations: enumerations, + hints: { + range: 1 + } + }, + { + key: 'output', + name: 'Value', + format: 'string', + hints: { + range: 2 + } + } + ]) + }; + } } diff --git a/src/plugins/condition/ConditionSetTelemetryProvider.js b/src/plugins/condition/ConditionSetTelemetryProvider.js index ad8147df22..b1ddb633b9 100644 --- a/src/plugins/condition/ConditionSetTelemetryProvider.js +++ b/src/plugins/condition/ConditionSetTelemetryProvider.js @@ -23,66 +23,68 @@ import ConditionManager from './ConditionManager'; export default class ConditionSetTelemetryProvider { - constructor(openmct) { - this.openmct = openmct; - this.conditionManagerPool = {}; + constructor(openmct) { + this.openmct = openmct; + this.conditionManagerPool = {}; + } + + isTelemetryObject(domainObject) { + return domainObject.type === 'conditionSet'; + } + + supportsRequest(domainObject) { + return domainObject.type === 'conditionSet'; + } + + supportsSubscribe(domainObject) { + return domainObject.type === 'conditionSet'; + } + + request(domainObject, options) { + let conditionManager = this.getConditionManager(domainObject); + + return conditionManager.requestLADConditionSetOutput(options).then((latestOutput) => { + return latestOutput; + }); + } + + subscribe(domainObject, callback) { + let conditionManager = this.getConditionManager(domainObject); + + conditionManager.on('conditionSetResultUpdated', (data) => { + callback(data); + }); + + return this.destroyConditionManager.bind( + this, + this.openmct.objects.makeKeyString(domainObject.identifier) + ); + } + + /** + * returns conditionManager instance for corresponding domain object + * creates the instance if it is not yet created + * @private + */ + getConditionManager(domainObject) { + const id = this.openmct.objects.makeKeyString(domainObject.identifier); + + if (!this.conditionManagerPool[id]) { + this.conditionManagerPool[id] = new ConditionManager(domainObject, this.openmct); } - isTelemetryObject(domainObject) { - return domainObject.type === 'conditionSet'; - } - - supportsRequest(domainObject) { - return domainObject.type === 'conditionSet'; - } - - supportsSubscribe(domainObject) { - return domainObject.type === 'conditionSet'; - } - - request(domainObject, options) { - let conditionManager = this.getConditionManager(domainObject); - - return conditionManager.requestLADConditionSetOutput(options) - .then(latestOutput => { - return latestOutput; - }); - } - - subscribe(domainObject, callback) { - let conditionManager = this.getConditionManager(domainObject); - - conditionManager.on('conditionSetResultUpdated', (data) => { - callback(data); - }); - - return this.destroyConditionManager.bind(this, this.openmct.objects.makeKeyString(domainObject.identifier)); - } - - /** - * returns conditionManager instance for corresponding domain object - * creates the instance if it is not yet created - * @private - */ - getConditionManager(domainObject) { - const id = this.openmct.objects.makeKeyString(domainObject.identifier); - - if (!this.conditionManagerPool[id]) { - this.conditionManagerPool[id] = new ConditionManager(domainObject, this.openmct); - } - - return this.conditionManagerPool[id]; - } - - /** - * cleans up and destroys conditionManager instance for corresponding domain object id - * can be called manually for views that only request but do not subscribe to data - */ - destroyConditionManager(id) { - if (this.conditionManagerPool[id]) { - this.conditionManagerPool[id].off('conditionSetResultUpdated'); - this.conditionManagerPool[id].destroy(); - delete this.conditionManagerPool[id]; - } + return this.conditionManagerPool[id]; + } + + /** + * cleans up and destroys conditionManager instance for corresponding domain object id + * can be called manually for views that only request but do not subscribe to data + */ + destroyConditionManager(id) { + if (this.conditionManagerPool[id]) { + this.conditionManagerPool[id].off('conditionSetResultUpdated'); + this.conditionManagerPool[id].destroy(); + delete this.conditionManagerPool[id]; } + } } diff --git a/src/plugins/condition/ConditionSetViewProvider.js b/src/plugins/condition/ConditionSetViewProvider.js index 62c88fa281..a31ef98e34 100644 --- a/src/plugins/condition/ConditionSetViewProvider.js +++ b/src/plugins/condition/ConditionSetViewProvider.js @@ -26,64 +26,64 @@ import Vue from 'vue'; const DEFAULT_VIEW_PRIORITY = 100; export default class ConditionSetViewProvider { - constructor(openmct) { - this.openmct = openmct; - this.name = 'Conditions View'; - this.key = 'conditionSet.view'; - this.cssClass = 'icon-conditional'; - } - - canView(domainObject, objectPath) { - const isConditionSet = domainObject.type === 'conditionSet'; - - return isConditionSet && this.openmct.router.isNavigatedObject(objectPath); - } - - canEdit(domainObject, objectPath) { - const isConditionSet = domainObject.type === 'conditionSet'; - - return isConditionSet && this.openmct.router.isNavigatedObject(objectPath); - } - - view(domainObject, objectPath) { - let component; - const openmct = this.openmct; - - return { - show: (container, isEditing) => { - component = new Vue({ - el: container, - components: { - ConditionSet - }, - provide: { - openmct, - domainObject, - objectPath - }, - data() { - return { - isEditing - }; - }, - template: '' - }); - }, - onEditModeChange: (isEditing) => { - component.isEditing = isEditing; - }, - destroy: () => { - component.$destroy(); - component = undefined; - } - }; - } - - priority(domainObject) { - if (domainObject.type === 'conditionSet') { - return Number.MAX_VALUE; - } else { - return DEFAULT_VIEW_PRIORITY; - } + constructor(openmct) { + this.openmct = openmct; + this.name = 'Conditions View'; + this.key = 'conditionSet.view'; + this.cssClass = 'icon-conditional'; + } + + canView(domainObject, objectPath) { + const isConditionSet = domainObject.type === 'conditionSet'; + + return isConditionSet && this.openmct.router.isNavigatedObject(objectPath); + } + + canEdit(domainObject, objectPath) { + const isConditionSet = domainObject.type === 'conditionSet'; + + return isConditionSet && this.openmct.router.isNavigatedObject(objectPath); + } + + view(domainObject, objectPath) { + let component; + const openmct = this.openmct; + + return { + show: (container, isEditing) => { + component = new Vue({ + el: container, + components: { + ConditionSet + }, + provide: { + openmct, + domainObject, + objectPath + }, + data() { + return { + isEditing + }; + }, + template: '' + }); + }, + onEditModeChange: (isEditing) => { + component.isEditing = isEditing; + }, + destroy: () => { + component.$destroy(); + component = undefined; + } + }; + } + + priority(domainObject) { + if (domainObject.type === 'conditionSet') { + return Number.MAX_VALUE; + } else { + return DEFAULT_VIEW_PRIORITY; } + } } diff --git a/src/plugins/condition/ConditionSpec.js b/src/plugins/condition/ConditionSpec.js index 0862dcbb25..faea60636a 100644 --- a/src/plugins/condition/ConditionSpec.js +++ b/src/plugins/condition/ConditionSpec.js @@ -20,9 +20,9 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import Condition from "./Condition"; -import {TRIGGER} from "./utils/constants"; -import TelemetryCriterion from "./criterion/TelemetryCriterion"; +import Condition from './Condition'; +import { TRIGGER } from './utils/constants'; +import TelemetryCriterion from './criterion/TelemetryCriterion'; let openmct = {}; let testConditionDefinition; @@ -32,153 +32,159 @@ let conditionManager; let mockTelemetryReceived; let mockTimeSystems; -describe("The condition", function () { +describe('The condition', function () { + beforeEach(() => { + conditionManager = jasmine.createSpyObj('conditionManager', [ + 'on', + 'updateConditionDescription' + ]); + mockTelemetryReceived = jasmine.createSpy('listener'); + conditionManager.on('telemetryReceived', mockTelemetryReceived); + conditionManager.updateConditionDescription.and.returnValue(function () {}); - beforeEach (() => { - conditionManager = jasmine.createSpyObj('conditionManager', - ['on', 'updateConditionDescription'] - ); - mockTelemetryReceived = jasmine.createSpy('listener'); - conditionManager.on('telemetryReceived', mockTelemetryReceived); - conditionManager.updateConditionDescription.and.returnValue(function () {}); - - testTelemetryObject = { - identifier: { - namespace: "", - key: "test-object" - }, - type: "test-object", - name: "Test Object", - telemetry: { - valueMetadatas: [{ - key: "some-key", - name: "Some attribute", - hints: { - range: 2 - } - }, - { - key: "utc", - name: "Time", - format: "utc", - hints: { - domain: 1 - } - }, { - key: "testSource", - source: "value", - name: "Test", - format: "string" - }] + testTelemetryObject = { + identifier: { + namespace: '', + key: 'test-object' + }, + type: 'test-object', + name: 'Test Object', + telemetry: { + valueMetadatas: [ + { + key: 'some-key', + name: 'Some attribute', + hints: { + range: 2 } - }; - conditionManager.telemetryObjects = { - "test-object": testTelemetryObject - }; - openmct.objects = jasmine.createSpyObj('objects', ['get', 'makeKeyString']); - openmct.objects.get.and.returnValue(new Promise(function (resolve, reject) { - resolve(testTelemetryObject); - })); openmct.objects.makeKeyString.and.returnValue(testTelemetryObject.identifier.key); - openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject', 'subscribe', 'getMetadata']); - openmct.telemetry.isTelemetryObject.and.returnValue(true); - openmct.telemetry.subscribe.and.returnValue(function () {}); - openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry); - - mockTimeSystems = { - key: 'utc' - }; - openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems']); - openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]); - - testConditionDefinition = { - id: '123-456', - configuration: { - name: 'mock condition', - output: 'mock output', - trigger: TRIGGER.ANY, - criteria: [ - { - id: '1234-5678-9999-0000', - operation: 'equalTo', - input: ['0'], - metadata: 'value', - telemetry: testTelemetryObject.identifier - } - ] + }, + { + key: 'utc', + name: 'Time', + format: 'utc', + hints: { + domain: 1 } - }; + }, + { + key: 'testSource', + source: 'value', + name: 'Test', + format: 'string' + } + ] + } + }; + conditionManager.telemetryObjects = { + 'test-object': testTelemetryObject + }; + openmct.objects = jasmine.createSpyObj('objects', ['get', 'makeKeyString']); + openmct.objects.get.and.returnValue( + new Promise(function (resolve, reject) { + resolve(testTelemetryObject); + }) + ); + openmct.objects.makeKeyString.and.returnValue(testTelemetryObject.identifier.key); + openmct.telemetry = jasmine.createSpyObj('telemetry', [ + 'isTelemetryObject', + 'subscribe', + 'getMetadata' + ]); + openmct.telemetry.isTelemetryObject.and.returnValue(true); + openmct.telemetry.subscribe.and.returnValue(function () {}); + openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry); - conditionObj = new Condition( - testConditionDefinition, - openmct, - conditionManager - ); + mockTimeSystems = { + key: 'utc' + }; + openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems']); + openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]); + + testConditionDefinition = { + id: '123-456', + configuration: { + name: 'mock condition', + output: 'mock output', + trigger: TRIGGER.ANY, + criteria: [ + { + id: '1234-5678-9999-0000', + operation: 'equalTo', + input: ['0'], + metadata: 'value', + telemetry: testTelemetryObject.identifier + } + ] + } + }; + + conditionObj = new Condition(testConditionDefinition, openmct, conditionManager); + }); + + it('generates criteria with the correct properties', function () { + const testCriterion = testConditionDefinition.configuration.criteria[0]; + let criterion = conditionObj.generateCriterion(testCriterion); + expect(criterion.id).toBeDefined(); + expect(criterion.operation).toEqual(testCriterion.operation); + expect(criterion.input).toEqual(testCriterion.input); + expect(criterion.metadata).toEqual(testCriterion.metadata); + expect(criterion.telemetry).toEqual(testCriterion.telemetry); + }); + + it('initializes with an id', function () { + expect(conditionObj.id).toBeDefined(); + }); + + it('initializes with criteria from the condition definition', function () { + expect(conditionObj.criteria.length).toEqual(1); + let criterion = conditionObj.criteria[0]; + expect(criterion instanceof TelemetryCriterion).toBeTrue(); + expect(criterion.operator).toEqual(testConditionDefinition.configuration.criteria[0].operator); + expect(criterion.input).toEqual(testConditionDefinition.configuration.criteria[0].input); + expect(criterion.metadata).toEqual(testConditionDefinition.configuration.criteria[0].metadata); + }); + + it('initializes with the trigger from the condition definition', function () { + expect(conditionObj.trigger).toEqual(testConditionDefinition.configuration.trigger); + }); + + it('destroys all criteria for a condition', function () { + const result = conditionObj.destroyCriteria(); + expect(result).toBeTrue(); + expect(conditionObj.criteria.length).toEqual(0); + }); + + it('gets the result of a condition when new telemetry data is received', function () { + conditionObj.updateResult({ + value: '0', + utc: 'Hi', + id: testTelemetryObject.identifier.key }); + expect(conditionObj.result).toBeTrue(); + }); - it("generates criteria with the correct properties", function () { - const testCriterion = testConditionDefinition.configuration.criteria[0]; - let criterion = conditionObj.generateCriterion(testCriterion); - expect(criterion.id).toBeDefined(); - expect(criterion.operation).toEqual(testCriterion.operation); - expect(criterion.input).toEqual(testCriterion.input); - expect(criterion.metadata).toEqual(testCriterion.metadata); - expect(criterion.telemetry).toEqual(testCriterion.telemetry); + it('gets the result of a condition when new telemetry data is received', function () { + conditionObj.updateResult({ + value: '1', + utc: 'Hi', + id: testTelemetryObject.identifier.key }); + expect(conditionObj.result).toBeFalse(); + }); - it("initializes with an id", function () { - expect(conditionObj.id).toBeDefined(); + it('keeps the old result new telemetry data is not used by it', function () { + conditionObj.updateResult({ + value: '0', + utc: 'Hi', + id: testTelemetryObject.identifier.key }); + expect(conditionObj.result).toBeTrue(); - it("initializes with criteria from the condition definition", function () { - expect(conditionObj.criteria.length).toEqual(1); - let criterion = conditionObj.criteria[0]; - expect(criterion instanceof TelemetryCriterion).toBeTrue(); - expect(criterion.operator).toEqual(testConditionDefinition.configuration.criteria[0].operator); - expect(criterion.input).toEqual(testConditionDefinition.configuration.criteria[0].input); - expect(criterion.metadata).toEqual(testConditionDefinition.configuration.criteria[0].metadata); - }); - - it("initializes with the trigger from the condition definition", function () { - expect(conditionObj.trigger).toEqual(testConditionDefinition.configuration.trigger); - }); - - it("destroys all criteria for a condition", function () { - const result = conditionObj.destroyCriteria(); - expect(result).toBeTrue(); - expect(conditionObj.criteria.length).toEqual(0); - }); - - it("gets the result of a condition when new telemetry data is received", function () { - conditionObj.updateResult({ - value: '0', - utc: 'Hi', - id: testTelemetryObject.identifier.key - }); - expect(conditionObj.result).toBeTrue(); - }); - - it("gets the result of a condition when new telemetry data is received", function () { - conditionObj.updateResult({ - value: '1', - utc: 'Hi', - id: testTelemetryObject.identifier.key - }); - expect(conditionObj.result).toBeFalse(); - }); - - it("keeps the old result new telemetry data is not used by it", function () { - conditionObj.updateResult({ - value: '0', - utc: 'Hi', - id: testTelemetryObject.identifier.key - }); - expect(conditionObj.result).toBeTrue(); - - conditionObj.updateResult({ - value: '1', - utc: 'Hi', - id: '1234' - }); - expect(conditionObj.result).toBeTrue(); + conditionObj.updateResult({ + value: '1', + utc: 'Hi', + id: '1234' }); + expect(conditionObj.result).toBeTrue(); + }); }); diff --git a/src/plugins/condition/StyleRuleManager.js b/src/plugins/condition/StyleRuleManager.js index 2e2270ef65..3c1376c7fb 100644 --- a/src/plugins/condition/StyleRuleManager.js +++ b/src/plugins/condition/StyleRuleManager.js @@ -23,184 +23,203 @@ import EventEmitter from 'EventEmitter'; export default class StyleRuleManager extends EventEmitter { - constructor(styleConfiguration, openmct, callback, suppressSubscriptionOnEdit) { - super(); - this.openmct = openmct; - this.callback = callback; - this.refreshData = this.refreshData.bind(this); - this.toggleSubscription = this.toggleSubscription.bind(this); - if (suppressSubscriptionOnEdit) { - this.openmct.editor.on('isEditing', this.toggleSubscription); - this.isEditing = this.openmct.editor.editing; - } - - if (styleConfiguration) { - // We don't set the selectedConditionId here because we want condition set computation to happen before we apply any selected style - const styleConfigurationWithNoSelection = Object.assign(styleConfiguration, {selectedConditionId: ''}); - this.initialize(styleConfigurationWithNoSelection); - if (styleConfiguration.conditionSetIdentifier) { - this.openmct.time.on("bounds", this.refreshData); - this.subscribeToConditionSet(); - } else { - this.applyStaticStyle(); - } - } + constructor(styleConfiguration, openmct, callback, suppressSubscriptionOnEdit) { + super(); + this.openmct = openmct; + this.callback = callback; + this.refreshData = this.refreshData.bind(this); + this.toggleSubscription = this.toggleSubscription.bind(this); + if (suppressSubscriptionOnEdit) { + this.openmct.editor.on('isEditing', this.toggleSubscription); + this.isEditing = this.openmct.editor.editing; } - toggleSubscription(isEditing) { - this.isEditing = isEditing; - if (this.isEditing) { - if (this.stopProvidingTelemetry) { - this.stopProvidingTelemetry(); - delete this.stopProvidingTelemetry; - } + if (styleConfiguration) { + // We don't set the selectedConditionId here because we want condition set computation to happen before we apply any selected style + const styleConfigurationWithNoSelection = Object.assign(styleConfiguration, { + selectedConditionId: '' + }); + this.initialize(styleConfigurationWithNoSelection); + if (styleConfiguration.conditionSetIdentifier) { + this.openmct.time.on('bounds', this.refreshData); + this.subscribeToConditionSet(); + } else { + this.applyStaticStyle(); + } + } + } - if (this.conditionSetIdentifier) { - this.applySelectedConditionStyle(); - } - } else if (this.conditionSetIdentifier) { - //reset the selected style and let the condition set output determine what it should be - this.selectedConditionId = undefined; - this.currentStyle = undefined; - this.updateDomainObjectStyle(); - this.subscribeToConditionSet(); - } + toggleSubscription(isEditing) { + this.isEditing = isEditing; + if (this.isEditing) { + if (this.stopProvidingTelemetry) { + this.stopProvidingTelemetry(); + delete this.stopProvidingTelemetry; + } + + if (this.conditionSetIdentifier) { + this.applySelectedConditionStyle(); + } + } else if (this.conditionSetIdentifier) { + //reset the selected style and let the condition set output determine what it should be + this.selectedConditionId = undefined; + this.currentStyle = undefined; + this.updateDomainObjectStyle(); + this.subscribeToConditionSet(); + } + } + + initialize(styleConfiguration) { + this.conditionSetIdentifier = styleConfiguration.conditionSetIdentifier; + this.selectedConditionId = styleConfiguration.selectedConditionId; + this.staticStyle = styleConfiguration.staticStyle; + this.defaultConditionId = styleConfiguration.defaultConditionId; + this.updateConditionStylesMap(styleConfiguration.styles || []); + } + + subscribeToConditionSet() { + if (this.stopProvidingTelemetry) { + this.stopProvidingTelemetry(); + delete this.stopProvidingTelemetry; } - initialize(styleConfiguration) { - this.conditionSetIdentifier = styleConfiguration.conditionSetIdentifier; - this.selectedConditionId = styleConfiguration.selectedConditionId; - this.staticStyle = styleConfiguration.staticStyle; - this.defaultConditionId = styleConfiguration.defaultConditionId; - this.updateConditionStylesMap(styleConfiguration.styles || []); - } - - subscribeToConditionSet() { - if (this.stopProvidingTelemetry) { - this.stopProvidingTelemetry(); - delete this.stopProvidingTelemetry; + this.openmct.objects.get(this.conditionSetIdentifier).then((conditionSetDomainObject) => { + this.openmct.telemetry.request(conditionSetDomainObject).then((output) => { + if ( + output && + output.length && + this.conditionSetIdentifier && + this.openmct.objects.areIdsEqual( + conditionSetDomainObject.identifier, + this.conditionSetIdentifier + ) + ) { + this.handleConditionSetResultUpdated(output[0]); } + }); + if ( + this.conditionSetIdentifier && + this.openmct.objects.areIdsEqual( + conditionSetDomainObject.identifier, + this.conditionSetIdentifier + ) + ) { + this.stopProvidingTelemetry = this.openmct.telemetry.subscribe( + conditionSetDomainObject, + this.handleConditionSetResultUpdated.bind(this) + ); + } + }); + } - this.openmct.objects.get(this.conditionSetIdentifier).then((conditionSetDomainObject) => { - this.openmct.telemetry.request(conditionSetDomainObject) - .then(output => { - if (output && output.length && (this.conditionSetIdentifier && this.openmct.objects.areIdsEqual(conditionSetDomainObject.identifier, this.conditionSetIdentifier))) { - this.handleConditionSetResultUpdated(output[0]); - } - }); - if (this.conditionSetIdentifier && this.openmct.objects.areIdsEqual(conditionSetDomainObject.identifier, this.conditionSetIdentifier)) { - this.stopProvidingTelemetry = this.openmct.telemetry.subscribe(conditionSetDomainObject, this.handleConditionSetResultUpdated.bind(this)); - } + refreshData(bounds, isTick) { + if (!isTick) { + let options = { + start: bounds.start, + end: bounds.end, + size: 1, + strategy: 'latest' + }; + this.openmct.objects.get(this.conditionSetIdentifier).then((conditionSetDomainObject) => { + this.openmct.telemetry.request(conditionSetDomainObject, options).then((output) => { + if (output && output.length) { + this.handleConditionSetResultUpdated(output[0]); + } }); + }); } + } - refreshData(bounds, isTick) { - if (!isTick) { - let options = { - start: bounds.start, - end: bounds.end, - size: 1, - strategy: 'latest' - }; - this.openmct.objects.get(this.conditionSetIdentifier).then((conditionSetDomainObject) => { - this.openmct.telemetry.request(conditionSetDomainObject, options) - .then(output => { - if (output && output.length) { - this.handleConditionSetResultUpdated(output[0]); - } - }); - }); + updateObjectStyleConfig(styleConfiguration) { + if (!styleConfiguration || !styleConfiguration.conditionSetIdentifier) { + this.initialize(styleConfiguration || {}); + this.applyStaticStyle(); + this.destroy(true); + } else { + let isNewConditionSet = + !this.conditionSetIdentifier || + !this.openmct.objects.areIdsEqual( + this.conditionSetIdentifier, + styleConfiguration.conditionSetIdentifier + ); + this.initialize(styleConfiguration); + if (this.isEditing) { + this.applySelectedConditionStyle(); + } else { + //Only resubscribe if the conditionSet has changed. + if (isNewConditionSet) { + this.subscribeToConditionSet(); } + } } + } - updateObjectStyleConfig(styleConfiguration) { - if (!styleConfiguration || !styleConfiguration.conditionSetIdentifier) { - this.initialize(styleConfiguration || {}); - this.applyStaticStyle(); - this.destroy(true); - } else { - let isNewConditionSet = !this.conditionSetIdentifier - || !this.openmct.objects.areIdsEqual(this.conditionSetIdentifier, styleConfiguration.conditionSetIdentifier); - this.initialize(styleConfiguration); - if (this.isEditing) { - this.applySelectedConditionStyle(); - } else { - //Only resubscribe if the conditionSet has changed. - if (isNewConditionSet) { - this.subscribeToConditionSet(); - } - } - } + updateConditionStylesMap(conditionStyles) { + let conditionStyleMap = {}; + conditionStyles.forEach((conditionStyle) => { + if (conditionStyle.conditionId) { + conditionStyleMap[conditionStyle.conditionId] = conditionStyle.style; + } else { + conditionStyleMap.static = conditionStyle.style; + } + }); + this.conditionalStyleMap = conditionStyleMap; + } + + handleConditionSetResultUpdated(resultData) { + let foundStyle = this.conditionalStyleMap[resultData.conditionId]; + if (foundStyle) { + if (foundStyle !== this.currentStyle) { + this.currentStyle = foundStyle; + } + + this.updateDomainObjectStyle(); + } else { + this.applyStaticStyle(); } + } - updateConditionStylesMap(conditionStyles) { - let conditionStyleMap = {}; - conditionStyles.forEach((conditionStyle) => { - if (conditionStyle.conditionId) { - conditionStyleMap[conditionStyle.conditionId] = conditionStyle.style; - } else { - conditionStyleMap.static = conditionStyle.style; - } + updateDomainObjectStyle() { + if (this.callback) { + this.callback(Object.assign({}, this.currentStyle)); + } + } + + applySelectedConditionStyle() { + const conditionId = this.selectedConditionId || this.defaultConditionId; + if (!conditionId) { + this.applyStaticStyle(); + } else if (this.conditionalStyleMap[conditionId]) { + this.currentStyle = this.conditionalStyleMap[conditionId]; + this.updateDomainObjectStyle(); + } + } + + applyStaticStyle() { + if (this.staticStyle) { + this.currentStyle = this.staticStyle.style; + } else { + if (this.currentStyle) { + Object.keys(this.currentStyle).forEach((key) => { + this.currentStyle[key] = '__no_value'; }); - this.conditionalStyleMap = conditionStyleMap; + } } - handleConditionSetResultUpdated(resultData) { - let foundStyle = this.conditionalStyleMap[resultData.conditionId]; - if (foundStyle) { - if (foundStyle !== this.currentStyle) { - this.currentStyle = foundStyle; - } + this.updateDomainObjectStyle(); + } - this.updateDomainObjectStyle(); - } else { - this.applyStaticStyle(); - } + destroy(skipEventListeners) { + if (this.stopProvidingTelemetry) { + this.stopProvidingTelemetry(); + delete this.stopProvidingTelemetry; } - updateDomainObjectStyle() { - if (this.callback) { - this.callback(Object.assign({}, this.currentStyle)); - } - } - - applySelectedConditionStyle() { - const conditionId = this.selectedConditionId || this.defaultConditionId; - if (!conditionId) { - this.applyStaticStyle(); - } else if (this.conditionalStyleMap[conditionId]) { - this.currentStyle = this.conditionalStyleMap[conditionId]; - this.updateDomainObjectStyle(); - } - } - - applyStaticStyle() { - if (this.staticStyle) { - this.currentStyle = this.staticStyle.style; - } else { - if (this.currentStyle) { - Object.keys(this.currentStyle).forEach(key => { - this.currentStyle[key] = '__no_value'; - }); - } - } - - this.updateDomainObjectStyle(); - } - - destroy(skipEventListeners) { - if (this.stopProvidingTelemetry) { - - this.stopProvidingTelemetry(); - delete this.stopProvidingTelemetry; - } - - if (!skipEventListeners) { - this.openmct.time.off("bounds", this.refreshData); - this.openmct.editor.off('isEditing', this.toggleSubscription); - } - - this.conditionSetIdentifier = undefined; + if (!skipEventListeners) { + this.openmct.time.off('bounds', this.refreshData); + this.openmct.editor.off('isEditing', this.toggleSubscription); } + this.conditionSetIdentifier = undefined; + } } diff --git a/src/plugins/condition/components/Condition.vue b/src/plugins/condition/components/Condition.vue index 69f2c95076..7ca46eddbb 100644 --- a/src/plugins/condition/components/Condition.vue +++ b/src/plugins/condition/components/Condition.vue @@ -21,395 +21,370 @@ --> diff --git a/src/plugins/condition/components/ConditionCollection.vue b/src/plugins/condition/components/ConditionCollection.vue index bc916040ff..51848f2b2f 100644 --- a/src/plugins/condition/components/ConditionCollection.vue +++ b/src/plugins/condition/components/ConditionCollection.vue @@ -21,272 +21,282 @@ --> diff --git a/src/plugins/condition/components/ConditionDescription.vue b/src/plugins/condition/components/ConditionDescription.vue index eb2049fabc..7fa498075c 100644 --- a/src/plugins/condition/components/ConditionDescription.vue +++ b/src/plugins/condition/components/ConditionDescription.vue @@ -21,51 +21,39 @@ --> diff --git a/src/plugins/condition/components/ConditionError.vue b/src/plugins/condition/components/ConditionError.vue index ff37f9a1d8..34dbe5ee4f 100644 --- a/src/plugins/condition/components/ConditionError.vue +++ b/src/plugins/condition/components/ConditionError.vue @@ -21,62 +21,58 @@ --> diff --git a/src/plugins/condition/components/ConditionSet.vue b/src/plugins/condition/components/ConditionSet.vue index 0eb90d3691..5f5cb090e7 100644 --- a/src/plugins/condition/components/ConditionSet.vue +++ b/src/plugins/condition/components/ConditionSet.vue @@ -21,45 +21,37 @@ --> - diff --git a/src/plugins/condition/components/Criterion.vue b/src/plugins/condition/components/Criterion.vue index 60bfc93a4a..657e5aacd2 100644 --- a/src/plugins/condition/components/Criterion.vue +++ b/src/plugins/condition/components/Criterion.vue @@ -21,310 +21,307 @@ --> diff --git a/src/plugins/condition/components/TestData.vue b/src/plugins/condition/components/TestData.vue index 7d283d320b..ca55c39c16 100644 --- a/src/plugins/condition/components/TestData.vue +++ b/src/plugins/condition/components/TestData.vue @@ -21,236 +21,215 @@ --> diff --git a/src/plugins/condition/components/conditionals.scss b/src/plugins/condition/components/conditionals.scss index dc838e04ba..cfe1766859 100644 --- a/src/plugins/condition/components/conditionals.scss +++ b/src/plugins/condition/components/conditionals.scss @@ -21,303 +21,303 @@ *****************************************************************************/ /***************************** DRAGGING */ .is-active-dragging { - .c-condition-h__drop-target { - height: 3px; - margin-bottom: $interiorMarginSm; - } + .c-condition-h__drop-target { + height: 3px; + margin-bottom: $interiorMarginSm; + } } .c-condition-h { - &__drop-target { - border-radius: $controlCr; - height: 0; - min-height: 0; - transition: background-color, height; - transition-duration: 150ms; + &__drop-target { + border-radius: $controlCr; + height: 0; + min-height: 0; + transition: background-color, height; + transition-duration: 150ms; + } + + &.is-drag-target { + .c-condition > * { + pointer-events: none; // Keeps the JS drop handler from being intercepted by internal elements } - &.is-drag-target { - .c-condition > * { - pointer-events: none; // Keeps the JS drop handler from being intercepted by internal elements - } - - .c-condition-h__drop-target { - background-color: rgba($colorKey, 0.7); - } + .c-condition-h__drop-target { + background-color: rgba($colorKey, 0.7); } + } } .c-cs { + display: flex; + flex-direction: column; + flex: 1 1 auto; + height: 100%; + overflow: hidden; + + &.is-stale { + @include isStaleHolder(); + } + + /************************** CONDITION SET LAYOUT */ + &__current-output { + flex: 0 0 auto; + } + + &__test-data-and-conditions-w { display: flex; flex-direction: column; flex: 1 1 auto; height: 100%; overflow: hidden; + } - &.is-stale { - @include isStaleHolder(); + &__test-data, + &__conditions { + flex: 0 0 auto; + overflow: hidden; + } + + &__test-data { + flex: 0 0 auto; + max-height: 50%; + + &.is-expanded { + margin-bottom: $interiorMargin * 4; + } + } + + &__conditions { + flex: 1 1 auto; + + > * + * { + margin-top: $interiorMarginSm; + } + } + + &__content { + display: flex; + flex-direction: column; + flex: 0 1 auto; + overflow: hidden; + + > * { + flex: 0 0 auto; + overflow: hidden; + + * { + margin-top: $interiorMarginSm; + } } - /************************** CONDITION SET LAYOUT */ - &__current-output { - flex: 0 0 auto; + .c-button { + align-self: start; + } + } + + .is-editing & { + // Add some space to kick away from blue editing border indication + padding: $interiorMargin; + } + + section { + display: flex; + flex-direction: column; + overflow: hidden; + } + + &__conditions-h { + display: flex; + flex-direction: column; + flex: 1 1 auto; + overflow: auto; + padding-right: $interiorMarginSm; + + > * + * { + margin-top: $interiorMarginSm; + } + } + + .hint { + padding: $interiorMarginSm; + } + + /************************** SPECIFIC ITEMS */ + &__current-output-value { + flex-direction: row; + align-items: baseline; + padding: 0 $interiorMargin $interiorMarginLg $interiorMargin; + + > * { + padding: $interiorMargin 0; // Must do this to align label and value } - &__test-data-and-conditions-w { - display: flex; - flex-direction: column; - flex: 1 1 auto; - height: 100%; - overflow: hidden; + &__label { + color: $colorInspectorSectionHeaderFg; + opacity: 0.9; + text-transform: uppercase; } - &__test-data, - &__conditions { - flex: 0 0 auto; - overflow: hidden; - } - - &__test-data { - flex: 0 0 auto; - max-height: 50%; - - &.is-expanded { - margin-bottom: $interiorMargin * 4; - } - } - - &__conditions { - flex: 1 1 auto; - - > * + * { - margin-top: $interiorMarginSm; - } - } - - &__content { - display: flex; - flex-direction: column; - flex: 0 1 auto; - overflow: hidden; - - > * { - flex: 0 0 auto; - overflow: hidden; - + * { - margin-top: $interiorMarginSm; - } - } - - .c-button { - align-self: start; - } - } - - .is-editing & { - // Add some space to kick away from blue editing border indication - padding: $interiorMargin; - } - - section { - display: flex; - flex-direction: column; - overflow: hidden; - } - - &__conditions-h { - display: flex; - flex-direction: column; - flex: 1 1 auto; - overflow: auto; - padding-right: $interiorMarginSm; - - > * + * { - margin-top: $interiorMarginSm; - } - } - - .hint { - padding: $interiorMarginSm; - } - - /************************** SPECIFIC ITEMS */ - &__current-output-value { - flex-direction: row; - align-items: baseline; - padding: 0 $interiorMargin $interiorMarginLg $interiorMargin; - - > * { - padding: $interiorMargin 0; // Must do this to align label and value - } - - &__label { - color: $colorInspectorSectionHeaderFg; - opacity: 0.9; - text-transform: uppercase; - } - - &__value { - $p: $interiorMargin * 3; - font-size: 1.25em; - margin-left: $interiorMargin; - padding-left: $p; - padding-right: $p; - background: rgba(black, 0.2); - border-radius: 5px; - } + &__value { + $p: $interiorMargin * 3; + font-size: 1.25em; + margin-left: $interiorMargin; + padding-left: $p; + padding-right: $p; + background: rgba(black, 0.2); + border-radius: 5px; } + } } /***************************** CONDITIONS AND TEST DATUM ELEMENTS */ .c-condition, .c-test-datum { - @include discreteItem(); - display: flex; - padding: $interiorMargin; - line-height: 170%; // Aligns text with controls like selects + @include discreteItem(); + display: flex; + padding: $interiorMargin; + line-height: 170%; // Aligns text with controls like selects } .c-cdef, .c-cs-test { - &__controls { - display: flex; - flex: 1 1 auto; - flex-wrap: wrap; + &__controls { + display: flex; + flex: 1 1 auto; + flex-wrap: wrap; - > * > * { - margin-right: $interiorMarginSm; - } + > * > * { + margin-right: $interiorMarginSm; } + } - &__buttons { - white-space: nowrap; - } + &__buttons { + white-space: nowrap; + } } .c-condition { - border: 1px solid transparent; - flex-direction: column; - min-width: 400px; + border: 1px solid transparent; + flex-direction: column; + min-width: 400px; - > * + * { - margin-top: $interiorMarginSm; - } - &--browse { - .c-condition__summary { - border-top: 1px solid $colorInteriorBorder; - padding-top: $interiorMargin; - } + > * + * { + margin-top: $interiorMarginSm; + } + &--browse { + .c-condition__summary { + border-top: 1px solid $colorInteriorBorder; + padding-top: $interiorMargin; } + } - /***************************** HEADER */ - &__header { - $h: 22px; - display: flex; - align-items: start; - align-content: stretch; - overflow: hidden; - min-height: $h; - line-height: $h; + /***************************** HEADER */ + &__header { + $h: 22px; + display: flex; + align-items: start; + align-content: stretch; + overflow: hidden; + min-height: $h; + line-height: $h; - > * { - flex: 0 0 auto; - + * { - margin-left: $interiorMarginSm; - } - } + > * { + flex: 0 0 auto; + + * { + margin-left: $interiorMarginSm; + } } + } - &__drag-grippy { - transform: translateY(50%); - } + &__drag-grippy { + transform: translateY(50%); + } - &__name { - font-weight: bold; - align-self: baseline; // Fixes bold line-height offset problem - } + &__name { + font-weight: bold; + align-self: baseline; // Fixes bold line-height offset problem + } - &__output, - &__summary { - flex: 1 1 auto; - } + &__output, + &__summary { + flex: 1 1 auto; + } - &.is-current { - $c: $colorBodyFg; - border-color: rgba($c, 0.2); - background: rgba($c, 0.2); - } + &.is-current { + $c: $colorBodyFg; + border-color: rgba($c, 0.2); + background: rgba($c, 0.2); + } } /***************************** CONDITION DEFINITION, EDITING */ .c-cdef { - display: grid; - grid-row-gap: $interiorMarginSm; - grid-column-gap: $interiorMargin; - grid-auto-columns: min-content 1fr max-content; - align-items: start; - min-width: 150px; - margin-left: 29px; - overflow: hidden; + display: grid; + grid-row-gap: $interiorMarginSm; + grid-column-gap: $interiorMargin; + grid-auto-columns: min-content 1fr max-content; + align-items: start; + min-width: 150px; + margin-left: 29px; + overflow: hidden; - &__criteria, - &__match-and-criteria { - display: contents; + &__criteria, + &__match-and-criteria { + display: contents; + } + + &__label { + grid-column: 1; + text-align: right; + white-space: nowrap; + } + + &__separator { + grid-column: 1 / span 3; + } + + &__controls { + align-items: flex-start; + grid-column: 2; + + > * > * { + margin-right: $interiorMarginSm; } + } - &__label { - grid-column: 1; - text-align: right; - white-space: nowrap; - } - - &__separator { - grid-column: 1 / span 3; - } - - &__controls { - align-items: flex-start; - grid-column: 2; - - > * > * { - margin-right: $interiorMarginSm; - } - } - - &__buttons { - grid-column: 3; - } + &__buttons { + grid-column: 3; + } } .c-c__drag-ghost { - width: 100%; - min-height: $interiorMarginSm; + width: 100%; + min-height: $interiorMarginSm; - &.dragging { - min-height: 5em; - background-color: lightblue; - border-radius: 2px; - } + &.dragging { + min-height: 5em; + background-color: lightblue; + border-radius: 2px; + } } /***************************** TEST DATA */ .c-cs__test-data { - &__controls { - flex: 0 0 auto; - } + &__controls { + flex: 0 0 auto; + } } .c-cs-tests { - flex: 0 1 auto; - overflow: auto; - padding-right: $interiorMarginSm; + flex: 0 1 auto; + overflow: auto; + padding-right: $interiorMarginSm; - > * + * { - margin-top: $interiorMarginSm; - } + > * + * { + margin-top: $interiorMarginSm; + } } .c-cs-test { - > * + * { - margin-left: $interiorMargin; - } + > * + * { + margin-left: $interiorMargin; + } } diff --git a/src/plugins/condition/components/inspector/StyleEditor.vue b/src/plugins/condition/components/inspector/StyleEditor.vue index 13e14cc30f..5800397aaa 100644 --- a/src/plugins/condition/components/inspector/StyleEditor.vue +++ b/src/plugins/condition/components/inspector/StyleEditor.vue @@ -21,233 +21,236 @@ --> diff --git a/src/plugins/condition/components/inspector/StylesView.vue b/src/plugins/condition/components/inspector/StylesView.vue index 5c623da29b..aac40bdf79 100644 --- a/src/plugins/condition/components/inspector/StylesView.vue +++ b/src/plugins/condition/components/inspector/StylesView.vue @@ -21,867 +21,931 @@ --> diff --git a/src/plugins/condition/components/inspector/conditional-styles.scss b/src/plugins/condition/components/inspector/conditional-styles.scss index ca5500912e..64bb2596f3 100644 --- a/src/plugins/condition/components/inspector/conditional-styles.scss +++ b/src/plugins/condition/components/inspector/conditional-styles.scss @@ -22,134 +22,134 @@ /********************************************* INSPECTOR STYLES TAB */ .c-inspect-styles { + > * + * { + margin-top: $interiorMargin; + } + + &__content, + &__conditions, + &__condition { > * + * { - margin-top: $interiorMargin; + margin-top: $interiorMargin; + } + } + + &__content { + display: flex; + flex-direction: column; + } + + &__elem { + border-bottom: 1px solid $colorInteriorBorder; + padding-bottom: $interiorMargin; + } + + &__condition-set { + align-items: baseline; + display: flex; + flex-direction: row; + + .c-object-label { + flex: 1 1 auto; } - &__content, - &__conditions, - &__condition { - > * + * { - margin-top: $interiorMargin; - } + .c-button { + flex: 0 0 auto; + } + } + + &__style { + padding-bottom: $interiorMargin; + } + + &__condition { + padding: $interiorMargin; + } + + &__condition { + @include discreteItem(); + border: 1px solid transparent; + pointer-events: none; // Prevent selecting when the object isn't being edited + + &.is-current { + $c: $colorBodyFg; + border-color: rgba($c, 0.2); + background: rgba($c, 0.2); } - &__content { - display: flex; - flex-direction: column; + .is-editing & { + cursor: pointer; + pointer-events: initial; + + &:hover { + background: rgba($colorBodyFg, 0.1); + } + + &.is-current { + $c: $editUIColorBg; + border-color: $c; + background: rgba($c, 0.1); + } } + } - &__elem { - border-bottom: 1px solid $colorInteriorBorder; - padding-bottom: $interiorMargin; - } - - &__condition-set { - align-items: baseline; - display: flex; - flex-direction: row; - - .c-object-label { - flex: 1 1 auto; - } - - .c-button { - flex: 0 0 auto; - } - } - - &__style { - padding-bottom: $interiorMargin; - } - - &__condition { - padding: $interiorMargin; - } - - &__condition { - @include discreteItem(); - border: 1px solid transparent; - pointer-events: none; // Prevent selecting when the object isn't being edited - - &.is-current { - $c: $colorBodyFg; - border-color: rgba($c, 0.2); - background: rgba($c, 0.2); - } - - .is-editing & { - cursor: pointer; - pointer-events: initial; - - &:hover { - background: rgba($colorBodyFg, 0.1); - } - - &.is-current { - $c: $editUIColorBg; - border-color: $c; - background: rgba($c, 0.1); - } - } - } - - .c-style { - padding: 2px; // Allow a bit of room for thumb box-shadow - - &__condition-desc { - @include ellipsize(); - } + .c-style { + padding: 2px; // Allow a bit of room for thumb box-shadow + + &__condition-desc { + @include ellipsize(); } + } } .c-inspect-styles__style { - .is-editing & { - border-bottom: 1px solid $colorInteriorBorder; - } + .is-editing & { + border-bottom: 1px solid $colorInteriorBorder; + } } .l-shell:not(.is-editing) .c-inspect-styles { - .c-toolbar { - // Disabled-look toolbar when not editing - pointer-events: none; - cursor: inherit; + .c-toolbar { + // Disabled-look toolbar when not editing + pointer-events: none; + cursor: inherit; - // Hide control buttons, like image URL - [class*='--image-url'] { - display: none; - } - - // Make buttons look disabled by knocking back icon, not swatch element - .c-icon-button { - &:before { - opacity: $controlDisabledOpacity; - } - } + // Hide control buttons, like image URL + [class*='--image-url'] { + display: none; } + + // Make buttons look disabled by knocking back icon, not swatch element + .c-icon-button { + &:before { + opacity: $controlDisabledOpacity; + } + } + } } .c-toggle-styling-button { - display: none; + display: none; - .is-editing & { - display: block; - align-self: flex-end; - } + .is-editing & { + display: block; + align-self: flex-end; + } } .is-style-invisible { - display: none !important; + display: none !important; - .is-editing & { - display: block !important; - opacity: 0.2; - } + .is-editing & { + display: block !important; + opacity: 0.2; + } - &.c-style-thumb { - display: block !important; - background-color: transparent !important; - border-color: transparent !important; - @include bgCheckerboard($size: 10px, $imp: true); - opacity: 1; - } + &.c-style-thumb { + display: block !important; + background-color: transparent !important; + border-color: transparent !important; + @include bgCheckerboard($size: 10px, $imp: true); + opacity: 1; + } } diff --git a/src/plugins/condition/criterion/AllTelemetryCriterion.js b/src/plugins/condition/criterion/AllTelemetryCriterion.js index 90ff1272bd..94738f1bcc 100644 --- a/src/plugins/condition/criterion/AllTelemetryCriterion.js +++ b/src/plugins/condition/criterion/AllTelemetryCriterion.js @@ -22,270 +22,275 @@ import TelemetryCriterion from './TelemetryCriterion'; import StalenessUtils from '@/utils/staleness'; -import { evaluateResults } from "../utils/evaluator"; +import { evaluateResults } from '../utils/evaluator'; import { getLatestTimestamp, checkIfOld } from '../utils/time'; -import { getOperatorText } from "@/plugins/condition/utils/operations"; +import { getOperatorText } from '@/plugins/condition/utils/operations'; export default class AllTelemetryCriterion extends TelemetryCriterion { + /** + * Subscribes/Unsubscribes to telemetry and emits the result + * of operations performed on the telemetry data returned and a given input value. + * @constructor + * @param telemetryDomainObjectDefinition {id: uuid, operation: enum, input: Array, metadata: string, key: {domainObject.identifier} } + * @param openmct + */ - /** - * Subscribes/Unsubscribes to telemetry and emits the result - * of operations performed on the telemetry data returned and a given input value. - * @constructor - * @param telemetryDomainObjectDefinition {id: uuid, operation: enum, input: Array, metadata: string, key: {domainObject.identifier} } - * @param openmct - */ + initialize() { + this.telemetryObjects = { ...this.telemetryDomainObjectDefinition.telemetryObjects }; + this.telemetryDataCache = {}; - initialize() { - this.telemetryObjects = { ...this.telemetryDomainObjectDefinition.telemetryObjects }; - this.telemetryDataCache = {}; - - if (this.isValid() && this.isOldCheck() && this.isValidInput()) { - this.checkForOldData(this.telemetryObjects || {}); - } - - if (this.isValid() && this.isStalenessCheck()) { - this.subscribeToStaleness(this.telemetryObjects || {}); - } + if (this.isValid() && this.isOldCheck() && this.isValidInput()) { + this.checkForOldData(this.telemetryObjects || {}); } - checkForOldData(telemetryObjects) { - if (!this.ageCheck) { - this.ageCheck = {}; - } + if (this.isValid() && this.isStalenessCheck()) { + this.subscribeToStaleness(this.telemetryObjects || {}); + } + } - Object.values(telemetryObjects).forEach((telemetryObject) => { - const id = this.openmct.objects.makeKeyString(telemetryObject.identifier); - if (!this.ageCheck[id]) { - this.ageCheck[id] = checkIfOld((data) => { - this.handleOldTelemetry(id, data); - }, this.input[0] * 1000); - } - }); - } - - handleOldTelemetry(id, data) { - if (this.telemetryDataCache) { - this.telemetryDataCache[id] = true; - this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry); - } - - this.emitEvent('telemetryIsOld', data); - } - - subscribeToStaleness(telemetryObjects) { - if (!this.stalenessSubscription) { - this.stalenessSubscription = {}; - } - - Object.values(telemetryObjects).forEach((telemetryObject) => { - const id = this.openmct.objects.makeKeyString(telemetryObject.identifier); - if (!this.stalenessSubscription[id]) { - this.stalenessSubscription[id] = {}; - this.stalenessSubscription[id].stalenessUtils = new StalenessUtils(this.openmct, telemetryObject); - this.openmct.telemetry.isStale(telemetryObject).then((stalenessResponse) => { - if (stalenessResponse !== undefined) { - this.handleStaleTelemetry(id, stalenessResponse); - } - }); - this.stalenessSubscription[id].unsubscribe = this.openmct.telemetry.subscribeToStaleness( - telemetryObject, - (stalenessResponse) => { - this.handleStaleTelemetry(id, stalenessResponse); - } - ); - } - }); - } - - handleStaleTelemetry(id, stalenessResponse) { - if (this.telemetryDataCache) { - if (this.stalenessSubscription[id].stalenessUtils.shouldUpdateStaleness(stalenessResponse)) { - this.telemetryDataCache[id] = stalenessResponse.isStale; - this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry); - - this.emitEvent('telemetryStaleness'); - } - } - } - - isValid() { - return (this.telemetry === 'any' || this.telemetry === 'all') && this.metadata && this.operation; - } - - updateTelemetryObjects(telemetryObjects) { - this.telemetryObjects = { ...telemetryObjects }; - this.removeTelemetryDataCache(); - - if (this.isValid() && this.isOldCheck() && this.isValidInput()) { - this.checkForOldData(this.telemetryObjects || {}); - } - - if (this.isValid() && this.isStalenessCheck()) { - this.subscribeToStaleness(this.telemetryObjects || {}); - } - } - - removeTelemetryDataCache() { - const telemetryCacheIds = Object.keys(this.telemetryDataCache); - Object.values(this.telemetryObjects).forEach(telemetryObject => { - const id = this.openmct.objects.makeKeyString(telemetryObject.identifier); - const foundIndex = telemetryCacheIds.indexOf(id); - if (foundIndex > -1) { - telemetryCacheIds.splice(foundIndex, 1); - } - }); - telemetryCacheIds.forEach(id => { - delete (this.telemetryDataCache[id]); - delete (this.ageCheck[id]); - this.stalenessSubscription[id].unsubscribe(); - this.stalenessSubscription[id].stalenessUtils.destroy(); - delete (this.stalenessSubscription[id]); - }); - } - - formatData(data, telemetryObjects) { - if (data) { - this.telemetryDataCache[data.id] = this.computeResult(data); - } - - let keys = Object.keys(telemetryObjects); - keys.forEach((key) => { - let telemetryObject = telemetryObjects[key]; - const id = this.openmct.objects.makeKeyString(telemetryObject.identifier); - if (this.telemetryDataCache[id] === undefined) { - this.telemetryDataCache[id] = false; - } - }); - - const datum = { - result: evaluateResults(Object.values(this.telemetryDataCache), this.telemetry) - }; - - if (data) { - this.openmct.time.getAllTimeSystems().forEach(timeSystem => { - datum[timeSystem.key] = data[timeSystem.key]; - }); - } - - return datum; - } - - updateResult(data, telemetryObjects) { - const validatedData = this.isValid() ? data : {}; - - if (validatedData && !this.isStalenessCheck()) { - if (this.isOldCheck()) { - if (this.ageCheck?.[validatedData.id]) { - this.ageCheck[validatedData.id].update(validatedData); - } - - this.telemetryDataCache[validatedData.id] = false; - } else { - this.telemetryDataCache[validatedData.id] = this.computeResult(validatedData); - } - } - - Object.values(telemetryObjects).forEach(telemetryObject => { - const id = this.openmct.objects.makeKeyString(telemetryObject.identifier); - if (this.telemetryDataCache[id] === undefined) { - this.telemetryDataCache[id] = false; - } + checkForOldData(telemetryObjects) { + if (!this.ageCheck) { + this.ageCheck = {}; + } + + Object.values(telemetryObjects).forEach((telemetryObject) => { + const id = this.openmct.objects.makeKeyString(telemetryObject.identifier); + if (!this.ageCheck[id]) { + this.ageCheck[id] = checkIfOld((data) => { + this.handleOldTelemetry(id, data); + }, this.input[0] * 1000); + } + }); + } + + handleOldTelemetry(id, data) { + if (this.telemetryDataCache) { + this.telemetryDataCache[id] = true; + this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry); + } + + this.emitEvent('telemetryIsOld', data); + } + + subscribeToStaleness(telemetryObjects) { + if (!this.stalenessSubscription) { + this.stalenessSubscription = {}; + } + + Object.values(telemetryObjects).forEach((telemetryObject) => { + const id = this.openmct.objects.makeKeyString(telemetryObject.identifier); + if (!this.stalenessSubscription[id]) { + this.stalenessSubscription[id] = {}; + this.stalenessSubscription[id].stalenessUtils = new StalenessUtils( + this.openmct, + telemetryObject + ); + this.openmct.telemetry.isStale(telemetryObject).then((stalenessResponse) => { + if (stalenessResponse !== undefined) { + this.handleStaleTelemetry(id, stalenessResponse); + } }); + this.stalenessSubscription[id].unsubscribe = this.openmct.telemetry.subscribeToStaleness( + telemetryObject, + (stalenessResponse) => { + this.handleStaleTelemetry(id, stalenessResponse); + } + ); + } + }); + } + handleStaleTelemetry(id, stalenessResponse) { + if (this.telemetryDataCache) { + if (this.stalenessSubscription[id].stalenessUtils.shouldUpdateStaleness(stalenessResponse)) { + this.telemetryDataCache[id] = stalenessResponse.isStale; this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry); + + this.emitEvent('telemetryStaleness'); + } + } + } + + isValid() { + return ( + (this.telemetry === 'any' || this.telemetry === 'all') && this.metadata && this.operation + ); + } + + updateTelemetryObjects(telemetryObjects) { + this.telemetryObjects = { ...telemetryObjects }; + this.removeTelemetryDataCache(); + + if (this.isValid() && this.isOldCheck() && this.isValidInput()) { + this.checkForOldData(this.telemetryObjects || {}); } - requestLAD(telemetryObjects, requestOptions) { - let options = { - strategy: 'latest', - size: 1 - }; + if (this.isValid() && this.isStalenessCheck()) { + this.subscribeToStaleness(this.telemetryObjects || {}); + } + } - if (requestOptions !== undefined) { - options = Object.assign(options, requestOptions); - } + removeTelemetryDataCache() { + const telemetryCacheIds = Object.keys(this.telemetryDataCache); + Object.values(this.telemetryObjects).forEach((telemetryObject) => { + const id = this.openmct.objects.makeKeyString(telemetryObject.identifier); + const foundIndex = telemetryCacheIds.indexOf(id); + if (foundIndex > -1) { + telemetryCacheIds.splice(foundIndex, 1); + } + }); + telemetryCacheIds.forEach((id) => { + delete this.telemetryDataCache[id]; + delete this.ageCheck[id]; + this.stalenessSubscription[id].unsubscribe(); + this.stalenessSubscription[id].stalenessUtils.destroy(); + delete this.stalenessSubscription[id]; + }); + } - if (!this.isValid()) { - return this.formatData({}, telemetryObjects); - } - - let keys = Object.keys(Object.assign({}, telemetryObjects)); - const telemetryRequests = keys - .map(key => this.openmct.telemetry.request( - telemetryObjects[key], - options - )); - - let telemetryDataCache = {}; - - return Promise.all(telemetryRequests) - .then(telemetryRequestsResults => { - let latestTimestamp; - const timeSystems = this.openmct.time.getAllTimeSystems(); - const timeSystem = this.openmct.time.timeSystem(); - - telemetryRequestsResults.forEach((results, index) => { - const latestDatum = (Array.isArray(results) && results.length) ? results[results.length - 1] : {}; - const datumId = keys[index]; - const normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObjects[datumId]); - - telemetryDataCache[datumId] = this.computeResult(normalizedDatum); - - latestTimestamp = getLatestTimestamp( - latestTimestamp, - normalizedDatum, - timeSystems, - timeSystem - ); - }); - - const datum = { - result: evaluateResults(Object.values(telemetryDataCache), this.telemetry), - ...latestTimestamp - }; - - return { - id: this.id, - data: datum - }; - }); + formatData(data, telemetryObjects) { + if (data) { + this.telemetryDataCache[data.id] = this.computeResult(data); } - getDescription() { - const telemetryDescription = this.telemetry === 'all' ? 'all telemetry' : 'any telemetry'; - let metadataValue = (this.metadata === 'dataReceived' ? '' : this.metadata); - let inputValue = this.input; - if (this.metadata) { - const telemetryObjects = Object.values(this.telemetryObjects); - for (let i = 0; i < telemetryObjects.length; i++) { - const telemetryObject = telemetryObjects[i]; - const metadataObject = this.getMetaDataObject(telemetryObject, this.metadata); - if (metadataObject) { - metadataValue = this.getMetadataValueFromMetaData(metadataObject) || this.metadata; - inputValue = this.getInputValueFromMetaData(metadataObject, this.input) || this.input; - break; - } - } - } + let keys = Object.keys(telemetryObjects); + keys.forEach((key) => { + let telemetryObject = telemetryObjects[key]; + const id = this.openmct.objects.makeKeyString(telemetryObject.identifier); + if (this.telemetryDataCache[id] === undefined) { + this.telemetryDataCache[id] = false; + } + }); - return `${telemetryDescription} ${metadataValue} ${getOperatorText(this.operation, inputValue)}`; + const datum = { + result: evaluateResults(Object.values(this.telemetryDataCache), this.telemetry) + }; + + if (data) { + this.openmct.time.getAllTimeSystems().forEach((timeSystem) => { + datum[timeSystem.key] = data[timeSystem.key]; + }); } - destroy() { - delete this.telemetryObjects; - delete this.telemetryDataCache; + return datum; + } - if (this.ageCheck) { - Object.values(this.ageCheck).forEach((subscription) => subscription.clear); - delete this.ageCheck; + updateResult(data, telemetryObjects) { + const validatedData = this.isValid() ? data : {}; + + if (validatedData && !this.isStalenessCheck()) { + if (this.isOldCheck()) { + if (this.ageCheck?.[validatedData.id]) { + this.ageCheck[validatedData.id].update(validatedData); } - if (this.stalenessSubscription) { - Object.values(this.stalenessSubscription).forEach(subscription => { - subscription.unsubscribe(); - subscription.stalenessUtils.destroy(); - }); - } + this.telemetryDataCache[validatedData.id] = false; + } else { + this.telemetryDataCache[validatedData.id] = this.computeResult(validatedData); + } } + + Object.values(telemetryObjects).forEach((telemetryObject) => { + const id = this.openmct.objects.makeKeyString(telemetryObject.identifier); + if (this.telemetryDataCache[id] === undefined) { + this.telemetryDataCache[id] = false; + } + }); + + this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry); + } + + requestLAD(telemetryObjects, requestOptions) { + let options = { + strategy: 'latest', + size: 1 + }; + + if (requestOptions !== undefined) { + options = Object.assign(options, requestOptions); + } + + if (!this.isValid()) { + return this.formatData({}, telemetryObjects); + } + + let keys = Object.keys(Object.assign({}, telemetryObjects)); + const telemetryRequests = keys.map((key) => + this.openmct.telemetry.request(telemetryObjects[key], options) + ); + + let telemetryDataCache = {}; + + return Promise.all(telemetryRequests).then((telemetryRequestsResults) => { + let latestTimestamp; + const timeSystems = this.openmct.time.getAllTimeSystems(); + const timeSystem = this.openmct.time.timeSystem(); + + telemetryRequestsResults.forEach((results, index) => { + const latestDatum = + Array.isArray(results) && results.length ? results[results.length - 1] : {}; + const datumId = keys[index]; + const normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObjects[datumId]); + + telemetryDataCache[datumId] = this.computeResult(normalizedDatum); + + latestTimestamp = getLatestTimestamp( + latestTimestamp, + normalizedDatum, + timeSystems, + timeSystem + ); + }); + + const datum = { + result: evaluateResults(Object.values(telemetryDataCache), this.telemetry), + ...latestTimestamp + }; + + return { + id: this.id, + data: datum + }; + }); + } + + getDescription() { + const telemetryDescription = this.telemetry === 'all' ? 'all telemetry' : 'any telemetry'; + let metadataValue = this.metadata === 'dataReceived' ? '' : this.metadata; + let inputValue = this.input; + if (this.metadata) { + const telemetryObjects = Object.values(this.telemetryObjects); + for (let i = 0; i < telemetryObjects.length; i++) { + const telemetryObject = telemetryObjects[i]; + const metadataObject = this.getMetaDataObject(telemetryObject, this.metadata); + if (metadataObject) { + metadataValue = this.getMetadataValueFromMetaData(metadataObject) || this.metadata; + inputValue = this.getInputValueFromMetaData(metadataObject, this.input) || this.input; + break; + } + } + } + + return `${telemetryDescription} ${metadataValue} ${getOperatorText( + this.operation, + inputValue + )}`; + } + + destroy() { + delete this.telemetryObjects; + delete this.telemetryDataCache; + + if (this.ageCheck) { + Object.values(this.ageCheck).forEach((subscription) => subscription.clear); + delete this.ageCheck; + } + + if (this.stalenessSubscription) { + Object.values(this.stalenessSubscription).forEach((subscription) => { + subscription.unsubscribe(); + subscription.stalenessUtils.destroy(); + }); + } + } } diff --git a/src/plugins/condition/criterion/TelemetryCriterion.js b/src/plugins/condition/criterion/TelemetryCriterion.js index 32106b5550..72df33fd5a 100644 --- a/src/plugins/condition/criterion/TelemetryCriterion.js +++ b/src/plugins/condition/criterion/TelemetryCriterion.js @@ -24,304 +24,317 @@ import EventEmitter from 'EventEmitter'; import StalenessUtils from '@/utils/staleness'; import { IS_OLD_KEY, IS_STALE_KEY } from '../utils/constants'; import { OPERATIONS, getOperatorText } from '../utils/operations'; -import { checkIfOld } from "../utils/time"; +import { checkIfOld } from '../utils/time'; export default class TelemetryCriterion extends EventEmitter { + /** + * Subscribes/Unsubscribes to telemetry and emits the result + * of operations performed on the telemetry data returned and a given input value. + * @constructor + * @param telemetryDomainObjectDefinition {id: uuid, operation: enum, input: Array, metadata: string, key: {domainObject.identifier} } + * @param openmct + */ + constructor(telemetryDomainObjectDefinition, openmct) { + super(); - /** - * Subscribes/Unsubscribes to telemetry and emits the result - * of operations performed on the telemetry data returned and a given input value. - * @constructor - * @param telemetryDomainObjectDefinition {id: uuid, operation: enum, input: Array, metadata: string, key: {domainObject.identifier} } - * @param openmct - */ - constructor(telemetryDomainObjectDefinition, openmct) { - super(); + this.openmct = openmct; + this.telemetryDomainObjectDefinition = telemetryDomainObjectDefinition; + this.id = telemetryDomainObjectDefinition.id; + this.telemetry = telemetryDomainObjectDefinition.telemetry; + this.operation = telemetryDomainObjectDefinition.operation; + this.input = telemetryDomainObjectDefinition.input; + this.metadata = telemetryDomainObjectDefinition.metadata; + this.result = undefined; + this.ageCheck = undefined; + this.unsubscribeFromStaleness = undefined; - this.openmct = openmct; - this.telemetryDomainObjectDefinition = telemetryDomainObjectDefinition; - this.id = telemetryDomainObjectDefinition.id; - this.telemetry = telemetryDomainObjectDefinition.telemetry; - this.operation = telemetryDomainObjectDefinition.operation; - this.input = telemetryDomainObjectDefinition.input; - this.metadata = telemetryDomainObjectDefinition.metadata; - this.result = undefined; - this.ageCheck = undefined; - this.unsubscribeFromStaleness = undefined; + this.initialize(); + this.emitEvent('criterionUpdated', this); + } - this.initialize(); - this.emitEvent('criterionUpdated', this); + initialize() { + this.telemetryObjectIdAsString = ''; + if (![undefined, null, ''].includes(this.telemetryDomainObjectDefinition?.telemetry)) { + this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString( + this.telemetryDomainObjectDefinition.telemetry + ); } - initialize() { - this.telemetryObjectIdAsString = ""; - if (![undefined, null, ""].includes(this.telemetryDomainObjectDefinition?.telemetry)) { - this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry); - } + this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects); - this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects); - - if (this.isValid() && this.isOldCheck() && this.isValidInput()) { - this.checkForOldData(); - } - - if (this.isValid() && this.isStalenessCheck()) { - this.subscribeToStaleness(); - } + if (this.isValid() && this.isOldCheck() && this.isValidInput()) { + this.checkForOldData(); } - usesTelemetry(id) { - return this.telemetryObjectIdAsString && (this.telemetryObjectIdAsString === id); + if (this.isValid() && this.isStalenessCheck()) { + this.subscribeToStaleness(); + } + } + + usesTelemetry(id) { + return this.telemetryObjectIdAsString && this.telemetryObjectIdAsString === id; + } + + checkForOldData() { + if (this.ageCheck) { + this.ageCheck.clear(); } - checkForOldData() { + this.ageCheck = checkIfOld(this.handleOldTelemetry.bind(this), this.input[0] * 1000); + } + + handleOldTelemetry(data) { + this.result = true; + this.emitEvent('telemetryIsOld', data); + } + + subscribeToStaleness() { + if (this.unsubscribeFromStaleness) { + this.unsubscribeFromStaleness(); + } + + if (!this.stalenessUtils) { + this.stalenessUtils = new StalenessUtils(this.openmct, this.telemetryObject); + } + + this.openmct.telemetry.isStale(this.telemetryObject).then(this.handleStaleTelemetry.bind(this)); + this.unsubscribeFromStaleness = this.openmct.telemetry.subscribeToStaleness( + this.telemetryObject, + this.handleStaleTelemetry.bind(this) + ); + } + + handleStaleTelemetry(stalenessResponse) { + if ( + stalenessResponse !== undefined && + this.stalenessUtils.shouldUpdateStaleness(stalenessResponse) + ) { + this.result = stalenessResponse.isStale; + this.emitEvent('telemetryStaleness'); + } + } + + isValid() { + return this.telemetryObject && this.metadata && this.operation; + } + + isOldCheck() { + return this.metadata && this.metadata === 'dataReceived' && this.operation === IS_OLD_KEY; + } + + isStalenessCheck() { + return this.metadata && this.metadata === 'dataReceived' && this.operation === IS_STALE_KEY; + } + + isValidInput() { + return this.input instanceof Array && this.input.length; + } + + updateTelemetryObjects(telemetryObjects) { + this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString]; + + if (this.isValid() && this.isOldCheck() && this.isValidInput()) { + this.checkForOldData(); + } + + if (this.isValid() && this.isStalenessCheck()) { + this.subscribeToStaleness(); + } + } + + createNormalizedDatum(telemetryDatum, endpoint) { + const id = this.openmct.objects.makeKeyString(endpoint.identifier); + const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas; + + const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => { + const formatter = this.openmct.telemetry.getValueFormatter(metadatum); + datum[metadatum.key] = formatter.parse(telemetryDatum[metadatum.source]); + + return datum; + }, {}); + + normalizedDatum.id = id; + + return normalizedDatum; + } + + formatData(data) { + const datum = { + result: this.computeResult(data) + }; + + if (data) { + this.openmct.time.getAllTimeSystems().forEach((timeSystem) => { + datum[timeSystem.key] = data[timeSystem.key]; + }); + } + + return datum; + } + + updateResult(data) { + const validatedData = this.isValid() ? data : {}; + + if (!this.isStalenessCheck()) { + if (this.isOldCheck()) { if (this.ageCheck) { - this.ageCheck.clear(); + this.ageCheck.update(validatedData); } - this.ageCheck = checkIfOld(this.handleOldTelemetry.bind(this), this.input[0] * 1000); + this.result = false; + } else { + this.result = this.computeResult(validatedData); + } + } + } + + requestLAD(telemetryObjects, requestOptions) { + let options = { + strategy: 'latest', + size: 1 + }; + + if (requestOptions !== undefined) { + options = Object.assign(options, requestOptions); } - handleOldTelemetry(data) { - this.result = true; - this.emitEvent('telemetryIsOld', data); + if (!this.isValid()) { + return { + id: this.id, + data: this.formatData({}) + }; } - subscribeToStaleness() { - if (this.unsubscribeFromStaleness) { - this.unsubscribeFromStaleness(); - } + let telemetryObject = this.telemetryObject; - if (!this.stalenessUtils) { - this.stalenessUtils = new StalenessUtils(this.openmct, this.telemetryObject); - } + return this.openmct.telemetry + .request(this.telemetryObject, options) + .then((results) => { + const latestDatum = results.length ? results[results.length - 1] : {}; + const normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObject); - this.openmct.telemetry.isStale(this.telemetryObject).then(this.handleStaleTelemetry.bind(this)); - this.unsubscribeFromStaleness = this.openmct.telemetry.subscribeToStaleness( - this.telemetryObject, - this.handleStaleTelemetry.bind(this) + return { + id: this.id, + data: this.formatData(normalizedDatum) + }; + }) + .catch((error) => { + return { + id: this.id, + data: this.formatData() + }; + }); + } + + findOperation(operation) { + for (let i = 0, ii = OPERATIONS.length; i < ii; i++) { + if (operation === OPERATIONS[i].name) { + return OPERATIONS[i].operation; + } + } + + return null; + } + + computeResult(data) { + let result = false; + if (data) { + let comparator = this.findOperation(this.operation); + let params = []; + params.push(data[this.metadata]); + if (this.isValidInput()) { + this.input.forEach((input) => params.push(input)); + } + + if (typeof comparator === 'function') { + result = Boolean(comparator(params)); + } + } + + return result; + } + + emitEvent(eventName, data) { + this.emit(eventName, { + id: this.id, + data: data + }); + } + + getMetaDataObject(telemetryObject, metadata) { + let metadataObject; + if (metadata) { + const telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject); + if (telemetryMetadata) { + metadataObject = telemetryMetadata.valueMetadatas.find( + (valueMetadata) => valueMetadata.key === metadata ); + } } - handleStaleTelemetry(stalenessResponse) { - if (stalenessResponse !== undefined && this.stalenessUtils.shouldUpdateStaleness(stalenessResponse)) { - this.result = stalenessResponse.isStale; - this.emitEvent('telemetryStaleness'); + return metadataObject; + } + + getInputValueFromMetaData(metadataObject, input) { + let inputValue; + if (metadataObject) { + if (metadataObject.enumerations && input.length) { + const enumeration = metadataObject.enumerations.find( + (item) => item.value.toString() === input[0].toString() + ); + if (enumeration !== undefined && enumeration.string) { + inputValue = [enumeration.string]; } + } } - isValid() { - return this.telemetryObject && this.metadata && this.operation; + return inputValue; + } + + getMetadataValueFromMetaData(metadataObject) { + let metadataValue; + if (metadataObject) { + if (metadataObject.name) { + metadataValue = metadataObject.name; + } } - isOldCheck() { - return this.metadata && this.metadata === 'dataReceived' && this.operation === IS_OLD_KEY; + return metadataValue; + } + + getDescription(criterion, index) { + let description; + if (!this.telemetry || !this.telemetryObject || this.telemetryObject.type === 'unknown') { + description = `Unknown ${this.metadata} ${getOperatorText(this.operation, this.input)}`; + } else { + const metadataObject = this.getMetaDataObject(this.telemetryObject, this.metadata); + const metadataValue = + this.getMetadataValueFromMetaData(metadataObject) || + (this.metadata === 'dataReceived' ? '' : this.metadata); + const inputValue = this.getInputValueFromMetaData(metadataObject, this.input) || this.input; + description = `${this.telemetryObject.name} ${metadataValue} ${getOperatorText( + this.operation, + inputValue + )}`; } - isStalenessCheck() { - return this.metadata && this.metadata === 'dataReceived' && this.operation === IS_STALE_KEY; + return description; + } + + destroy() { + delete this.telemetryObject; + delete this.telemetryObjectIdAsString; + + if (this.ageCheck) { + delete this.ageCheck; } - isValidInput() { - return this.input instanceof Array && this.input.length; + if (this.stalenessUtils) { + this.stalenessUtils.destroy(); } - updateTelemetryObjects(telemetryObjects) { - this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString]; - - if (this.isValid() && this.isOldCheck() && this.isValidInput()) { - this.checkForOldData(); - } - - if (this.isValid() && this.isStalenessCheck()) { - this.subscribeToStaleness(); - } - } - - createNormalizedDatum(telemetryDatum, endpoint) { - const id = this.openmct.objects.makeKeyString(endpoint.identifier); - const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas; - - const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => { - const formatter = this.openmct.telemetry.getValueFormatter(metadatum); - datum[metadatum.key] = formatter.parse(telemetryDatum[metadatum.source]); - - return datum; - }, {}); - - normalizedDatum.id = id; - - return normalizedDatum; - } - - formatData(data) { - const datum = { - result: this.computeResult(data) - }; - - if (data) { - this.openmct.time.getAllTimeSystems().forEach(timeSystem => { - datum[timeSystem.key] = data[timeSystem.key]; - }); - } - - return datum; - } - - updateResult(data) { - const validatedData = this.isValid() ? data : {}; - - if (!this.isStalenessCheck()) { - if (this.isOldCheck()) { - if (this.ageCheck) { - this.ageCheck.update(validatedData); - } - - this.result = false; - } else { - this.result = this.computeResult(validatedData); - } - } - } - - requestLAD(telemetryObjects, requestOptions) { - let options = { - strategy: 'latest', - size: 1 - }; - - if (requestOptions !== undefined) { - options = Object.assign(options, requestOptions); - } - - if (!this.isValid()) { - return { - id: this.id, - data: this.formatData({}) - }; - } - - let telemetryObject = this.telemetryObject; - - return this.openmct.telemetry.request( - this.telemetryObject, - options - ).then(results => { - const latestDatum = results.length ? results[results.length - 1] : {}; - const normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObject); - - return { - id: this.id, - data: this.formatData(normalizedDatum) - }; - }).catch((error) => { - return { - id: this.id, - data: this.formatData() - }; - }); - } - - findOperation(operation) { - for (let i = 0, ii = OPERATIONS.length; i < ii; i++) { - if (operation === OPERATIONS[i].name) { - return OPERATIONS[i].operation; - } - } - - return null; - } - - computeResult(data) { - let result = false; - if (data) { - let comparator = this.findOperation(this.operation); - let params = []; - params.push(data[this.metadata]); - if (this.isValidInput()) { - this.input.forEach(input => params.push(input)); - } - - if (typeof comparator === 'function') { - result = Boolean(comparator(params)); - } - } - - return result; - } - - emitEvent(eventName, data) { - this.emit(eventName, { - id: this.id, - data: data - }); - } - - getMetaDataObject(telemetryObject, metadata) { - let metadataObject; - if (metadata) { - const telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject); - if (telemetryMetadata) { - metadataObject = telemetryMetadata.valueMetadatas.find((valueMetadata) => valueMetadata.key === metadata); - } - } - - return metadataObject; - } - - getInputValueFromMetaData(metadataObject, input) { - let inputValue; - if (metadataObject) { - if (metadataObject.enumerations && input.length) { - const enumeration = metadataObject.enumerations.find((item) => item.value.toString() === input[0].toString()); - if (enumeration !== undefined && enumeration.string) { - inputValue = [enumeration.string]; - } - } - } - - return inputValue; - } - - getMetadataValueFromMetaData(metadataObject) { - let metadataValue; - if (metadataObject) { - if (metadataObject.name) { - metadataValue = metadataObject.name; - } - } - - return metadataValue; - } - - getDescription(criterion, index) { - let description; - if (!this.telemetry || !this.telemetryObject || (this.telemetryObject.type === 'unknown')) { - description = `Unknown ${this.metadata} ${getOperatorText(this.operation, this.input)}`; - } else { - const metadataObject = this.getMetaDataObject(this.telemetryObject, this.metadata); - const metadataValue = this.getMetadataValueFromMetaData(metadataObject) || (this.metadata === 'dataReceived' ? '' : this.metadata); - const inputValue = this.getInputValueFromMetaData(metadataObject, this.input) || this.input; - description = `${this.telemetryObject.name} ${metadataValue} ${getOperatorText(this.operation, inputValue)}`; - } - - return description; - } - - destroy() { - delete this.telemetryObject; - delete this.telemetryObjectIdAsString; - - if (this.ageCheck) { - delete this.ageCheck; - } - - if (this.stalenessUtils) { - this.stalenessUtils.destroy(); - } - - if (this.unsubscribeFromStaleness) { - this.unsubscribeFromStaleness(); - } + if (this.unsubscribeFromStaleness) { + this.unsubscribeFromStaleness(); } + } } diff --git a/src/plugins/condition/criterion/TelemetryCriterionSpec.js b/src/plugins/condition/criterion/TelemetryCriterionSpec.js index a31a2133cb..0438b76ad5 100644 --- a/src/plugins/condition/criterion/TelemetryCriterionSpec.js +++ b/src/plugins/condition/criterion/TelemetryCriterionSpec.js @@ -20,8 +20,8 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import TelemetryCriterion from "./TelemetryCriterion"; -import { getMockTelemetry } from "utils/testing"; +import TelemetryCriterion from './TelemetryCriterion'; +import { getMockTelemetry } from 'utils/testing'; let openmct = {}; let mockListener; @@ -30,118 +30,122 @@ let testTelemetryObject; let telemetryCriterion; let mockTelemetry = getMockTelemetry(); -describe("The telemetry criterion", function () { - - beforeEach (() => { - testTelemetryObject = { - identifier: { - namespace: "", - key: "test-object" - }, - type: "test-object", - name: "Test Object", - telemetry: { - valueMetadatas: [{ - key: "value", - name: "Value", - hints: { - range: 2 - } - }, - { - key: "utc", - name: "Time", - format: "utc", - hints: { - domain: 1 - } - }, { - key: "testSource", - source: "value", - name: "Test", - format: "string" - }] +describe('The telemetry criterion', function () { + beforeEach(() => { + testTelemetryObject = { + identifier: { + namespace: '', + key: 'test-object' + }, + type: 'test-object', + name: 'Test Object', + telemetry: { + valueMetadatas: [ + { + key: 'value', + name: 'Value', + hints: { + range: 2 } - }; - openmct.objects = jasmine.createSpyObj('objects', ['get', 'makeKeyString']); - openmct.objects.makeKeyString.and.returnValue(testTelemetryObject.identifier.key); - openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject', "subscribe", "getMetadata", "getValueFormatter", "request"]); - openmct.telemetry.isTelemetryObject.and.returnValue(true); - openmct.telemetry.subscribe.and.returnValue(function () {}); - openmct.telemetry.getValueFormatter.and.returnValue({ - parse: function (value) { - return value; + }, + { + key: 'utc', + name: 'Time', + format: 'utc', + hints: { + domain: 1 } - }); - openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry); + }, + { + key: 'testSource', + source: 'value', + name: 'Test', + format: 'string' + } + ] + } + }; + openmct.objects = jasmine.createSpyObj('objects', ['get', 'makeKeyString']); + openmct.objects.makeKeyString.and.returnValue(testTelemetryObject.identifier.key); + openmct.telemetry = jasmine.createSpyObj('telemetry', [ + 'isTelemetryObject', + 'subscribe', + 'getMetadata', + 'getValueFormatter', + 'request' + ]); + openmct.telemetry.isTelemetryObject.and.returnValue(true); + openmct.telemetry.subscribe.and.returnValue(function () {}); + openmct.telemetry.getValueFormatter.and.returnValue({ + parse: function (value) { + return value; + } + }); + openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry); - openmct.time = jasmine.createSpyObj('timeAPI', - ['timeSystem', 'bounds', 'getAllTimeSystems'] - ); - openmct.time.timeSystem.and.returnValue({key: 'system'}); - openmct.time.bounds.and.returnValue({ - start: 0, - end: 1 - }); - openmct.time.getAllTimeSystems.and.returnValue([{key: 'system'}]); + openmct.time = jasmine.createSpyObj('timeAPI', ['timeSystem', 'bounds', 'getAllTimeSystems']); + openmct.time.timeSystem.and.returnValue({ key: 'system' }); + openmct.time.bounds.and.returnValue({ + start: 0, + end: 1 + }); + openmct.time.getAllTimeSystems.and.returnValue([{ key: 'system' }]); - testCriterionDefinition = { - id: 'test-criterion-id', - telemetry: openmct.objects.makeKeyString(testTelemetryObject.identifier), - operation: 'textContains', - metadata: 'value', - input: ['Hell'], - telemetryObjects: {[testTelemetryObject.identifier.key]: testTelemetryObject} - }; + testCriterionDefinition = { + id: 'test-criterion-id', + telemetry: openmct.objects.makeKeyString(testTelemetryObject.identifier), + operation: 'textContains', + metadata: 'value', + input: ['Hell'], + telemetryObjects: { [testTelemetryObject.identifier.key]: testTelemetryObject } + }; - mockListener = jasmine.createSpy('listener'); + mockListener = jasmine.createSpy('listener'); - telemetryCriterion = new TelemetryCriterion( - testCriterionDefinition, - openmct - ); + telemetryCriterion = new TelemetryCriterion(testCriterionDefinition, openmct); - telemetryCriterion.on('criterionResultUpdated', mockListener); + telemetryCriterion.on('criterionResultUpdated', mockListener); + }); + it('initializes with a telemetry objectId as string', function () { + expect(telemetryCriterion.telemetryObjectIdAsString).toEqual( + testTelemetryObject.identifier.key + ); + }); + + it('returns a result on new data from relevant telemetry providers', function () { + telemetryCriterion.updateResult({ + value: 'Hello', + utc: 'Hi', + id: testTelemetryObject.identifier.key + }); + expect(telemetryCriterion.result).toBeTrue(); + }); + + describe('the LAD request', () => { + beforeEach(() => { + let telemetryRequestResolve; + let telemetryRequestPromise = new Promise((resolve) => { + telemetryRequestResolve = resolve; + }); + openmct.telemetry.request.and.callFake(() => { + setTimeout(() => { + telemetryRequestResolve(mockTelemetry); + }, 100); + + return telemetryRequestPromise; + }); }); - it("initializes with a telemetry objectId as string", function () { - expect(telemetryCriterion.telemetryObjectIdAsString).toEqual(testTelemetryObject.identifier.key); - }); - - it("returns a result on new data from relevant telemetry providers", function () { - telemetryCriterion.updateResult({ - value: 'Hello', - utc: 'Hi', - id: testTelemetryObject.identifier.key - }); - expect(telemetryCriterion.result).toBeTrue(); - }); - - describe('the LAD request', () => { - beforeEach(() => { - let telemetryRequestResolve; - let telemetryRequestPromise = new Promise((resolve) => { - telemetryRequestResolve = resolve; - }); - openmct.telemetry.request.and.callFake(() => { - setTimeout(() => { - telemetryRequestResolve(mockTelemetry); - }, 100); - - return telemetryRequestPromise; - }); - }); - - it("returns results for slow LAD requests", function () { - const criteriaRequest = telemetryCriterion.requestLAD(); - telemetryCriterion.destroy(); - expect(telemetryCriterion.telemetryObject).toBeUndefined(); - setTimeout(() => { - criteriaRequest.then((result) => { - expect(result).toBeDefined(); - }); - }, 300); + it('returns results for slow LAD requests', function () { + const criteriaRequest = telemetryCriterion.requestLAD(); + telemetryCriterion.destroy(); + expect(telemetryCriterion.telemetryObject).toBeUndefined(); + setTimeout(() => { + criteriaRequest.then((result) => { + expect(result).toBeDefined(); }); + }, 300); }); + }); }); diff --git a/src/plugins/condition/plugin.js b/src/plugins/condition/plugin.js index e0c7868878..d424c71ea2 100644 --- a/src/plugins/condition/plugin.js +++ b/src/plugins/condition/plugin.js @@ -20,45 +20,45 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ import ConditionSetViewProvider from './ConditionSetViewProvider.js'; -import ConditionSetCompositionPolicy from "./ConditionSetCompositionPolicy"; +import ConditionSetCompositionPolicy from './ConditionSetCompositionPolicy'; import ConditionSetMetadataProvider from './ConditionSetMetadataProvider'; import ConditionSetTelemetryProvider from './ConditionSetTelemetryProvider'; import { v4 as uuid } from 'uuid'; export default function ConditionPlugin() { - - return function install(openmct) { - - openmct.types.addType('conditionSet', { - name: 'Condition Set', - key: 'conditionSet', - description: 'Monitor and evaluate telemetry values in real-time with a wide variety of criteria. Use to control the styling of many objects in Open MCT.', - creatable: true, - cssClass: 'icon-conditional', - initialize: function (domainObject) { - domainObject.configuration = { - conditionTestData: [], - conditionCollection: [{ - isDefault: true, - id: uuid(), - configuration: { - name: 'Default', - output: 'Default', - trigger: 'all', - criteria: [] - }, - summary: 'Default condition' - }] - }; - domainObject.composition = []; - domainObject.telemetry = {}; + return function install(openmct) { + openmct.types.addType('conditionSet', { + name: 'Condition Set', + key: 'conditionSet', + description: + 'Monitor and evaluate telemetry values in real-time with a wide variety of criteria. Use to control the styling of many objects in Open MCT.', + creatable: true, + cssClass: 'icon-conditional', + initialize: function (domainObject) { + domainObject.configuration = { + conditionTestData: [], + conditionCollection: [ + { + isDefault: true, + id: uuid(), + configuration: { + name: 'Default', + output: 'Default', + trigger: 'all', + criteria: [] + }, + summary: 'Default condition' } - }); - let compositionPolicy = new ConditionSetCompositionPolicy(openmct); - openmct.composition.addPolicy(compositionPolicy.allow.bind(compositionPolicy)); - openmct.telemetry.addProvider(new ConditionSetMetadataProvider(openmct)); - openmct.telemetry.addProvider(new ConditionSetTelemetryProvider(openmct)); - openmct.objectViews.addProvider(new ConditionSetViewProvider(openmct)); - - }; + ] + }; + domainObject.composition = []; + domainObject.telemetry = {}; + } + }); + let compositionPolicy = new ConditionSetCompositionPolicy(openmct); + openmct.composition.addPolicy(compositionPolicy.allow.bind(compositionPolicy)); + openmct.telemetry.addProvider(new ConditionSetMetadataProvider(openmct)); + openmct.telemetry.addProvider(new ConditionSetTelemetryProvider(openmct)); + openmct.objectViews.addProvider(new ConditionSetViewProvider(openmct)); + }; } diff --git a/src/plugins/condition/pluginSpec.js b/src/plugins/condition/pluginSpec.js index c73ff79d11..de2c4694b1 100644 --- a/src/plugins/condition/pluginSpec.js +++ b/src/plugins/condition/pluginSpec.js @@ -20,968 +20,965 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { createOpenMct, resetApplicationState } from "utils/testing"; -import ConditionPlugin from "./plugin"; +import { createOpenMct, resetApplicationState } from 'utils/testing'; +import ConditionPlugin from './plugin'; import stylesManager from '../inspectorViews/styles/StylesManager'; -import StylesView from "./components/inspector/StylesView.vue"; +import StylesView from './components/inspector/StylesView.vue'; import Vue from 'vue'; -import {getApplicableStylesForItem} from "./utils/styleUtils"; -import ConditionManager from "@/plugins/condition/ConditionManager"; -import StyleRuleManager from "./StyleRuleManager"; -import { IS_OLD_KEY } from "./utils/constants"; +import { getApplicableStylesForItem } from './utils/styleUtils'; +import ConditionManager from '@/plugins/condition/ConditionManager'; +import StyleRuleManager from './StyleRuleManager'; +import { IS_OLD_KEY } from './utils/constants'; describe('the plugin', function () { - let conditionSetDefinition; - let mockConditionSetDomainObject; - let mockListener; - let element; - let child; - let openmct; - let testTelemetryObject; + let conditionSetDefinition; + let mockConditionSetDomainObject; + let mockListener; + let element; + let child; + let openmct; + let testTelemetryObject; - beforeEach((done) => { - testTelemetryObject = { - identifier: { - namespace: "", - key: "test-object" - }, - type: "test-object", - name: "Test Object", - telemetry: { - values: [{ - key: "some-key2", - source: "some-key2", - name: "Some attribute", - hints: { - range: 2 - } - }, - { - key: "utc", - name: "Time", - format: "utc", - hints: { - domain: 1 - } - }, { - key: "testSource", - source: "value", - name: "Test", - format: "string" - }, - { - key: "some-key", - source: "some-key", - hints: { - domain: 1 - } - }] + beforeEach((done) => { + testTelemetryObject = { + identifier: { + namespace: '', + key: 'test-object' + }, + type: 'test-object', + name: 'Test Object', + telemetry: { + values: [ + { + key: 'some-key2', + source: 'some-key2', + name: 'Some attribute', + hints: { + range: 2 } - }; + }, + { + key: 'utc', + name: 'Time', + format: 'utc', + hints: { + domain: 1 + } + }, + { + key: 'testSource', + source: 'value', + name: 'Test', + format: 'string' + }, + { + key: 'some-key', + source: 'some-key', + hints: { + domain: 1 + } + } + ] + } + }; - openmct = createOpenMct(); - openmct.install(new ConditionPlugin()); + openmct = createOpenMct(); + openmct.install(new ConditionPlugin()); - conditionSetDefinition = openmct.types.get('conditionSet').definition; + conditionSetDefinition = openmct.types.get('conditionSet').definition; - element = document.createElement('div'); - child = document.createElement('div'); - element.appendChild(child); + element = document.createElement('div'); + child = document.createElement('div'); + element.appendChild(child); - mockConditionSetDomainObject = { - identifier: { - key: 'testConditionSetKey', - namespace: '' + mockConditionSetDomainObject = { + identifier: { + key: 'testConditionSetKey', + namespace: '' + }, + type: 'conditionSet' + }; + + mockListener = jasmine.createSpy('mockListener'); + + openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true); + + conditionSetDefinition.initialize(mockConditionSetDomainObject); + + spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true)); + + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + let mockConditionSetObject = { + name: 'Condition Set', + key: 'conditionSet', + creatable: true + }; + + it('defines a conditionSet object type with the correct key', () => { + expect(conditionSetDefinition.key).toEqual(mockConditionSetObject.key); + }); + + describe('the conditionSet object', () => { + it('is creatable', () => { + expect(conditionSetDefinition.creatable).toEqual(mockConditionSetObject.creatable); + }); + + it('initializes with an empty composition list', () => { + expect(mockConditionSetDomainObject.composition instanceof Array).toBeTrue(); + expect(mockConditionSetDomainObject.composition.length).toEqual(0); + }); + }); + + describe('the condition set usage for condition widgets', () => { + let conditionWidgetItem; + let selection; + let component; + let styleViewComponentObject; + const conditionSetDomainObject = { + configuration: { + conditionTestData: [ + { + telemetry: '', + metadata: '', + input: '' + } + ], + conditionCollection: [ + { + id: '39584410-cbf9-499e-96dc-76f27e69885d', + configuration: { + name: 'Unnamed Condition', + output: 'Sine > 0', + trigger: 'all', + criteria: [ + { + id: '85fbb2f7-7595-42bd-9767-a932266c5225', + telemetry: { + namespace: '', + key: 'be0ba97f-b510-4f40-a18d-4ff121d5ea1a' + }, + operation: 'greaterThan', + input: ['0'], + metadata: 'sin' + }, + { + id: '35400132-63b0-425c-ac30-8197df7d5862', + telemetry: 'any', + operation: 'enumValueIs', + input: ['0'], + metadata: 'state' + } + ] }, - type: 'conditionSet' - }; + summary: + 'Match if all criteria are met: Sine Wave Generator Sine > 0 and any telemetry State is OFF ' + }, + { + isDefault: true, + id: '2532d90a-e0d6-4935-b546-3123522da2de', + configuration: { + name: 'Default', + output: 'Default', + trigger: 'all', + criteria: [] + }, + summary: '' + } + ] + }, + composition: [ + { + namespace: '', + key: 'be0ba97f-b510-4f40-a18d-4ff121d5ea1a' + }, + { + namespace: '', + key: '077ffa67-e78f-4e99-80e0-522ac33a3888' + } + ], + telemetry: {}, + name: 'Condition Set', + type: 'conditionSet', + identifier: { + namespace: '', + key: '863012c1-f6ca-4ab0-aed7-fd43d5e4cd12' + } + }; - mockListener = jasmine.createSpy('mockListener'); + beforeEach(() => { + conditionWidgetItem = { + label: 'Condition Widget', + conditionalLabel: '', + configuration: {}, + name: 'Condition Widget', + type: 'conditionWidget', + identifier: { + namespace: '', + key: 'c5e636c1-6771-4c9c-b933-8665cab189b3' + } + }; + selection = [ + [ + { + context: { + item: conditionWidgetItem, + supportsMultiSelect: false + } + } + ] + ]; + let viewContainer = document.createElement('div'); + child.append(viewContainer); + component = new Vue({ + el: viewContainer, + components: { + StylesView + }, + provide: { + openmct: openmct, + selection: selection, + stylesManager + }, + template: '' + }); - openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true); - - conditionSetDefinition.initialize(mockConditionSetDomainObject); - - spyOn(openmct.objects, "save").and.returnValue(Promise.resolve(true)); - - openmct.on('start', done); - openmct.startHeadless(); + return Vue.nextTick().then(() => { + styleViewComponentObject = component.$root.$children[0]; + styleViewComponentObject.setEditState(true); + }); }); afterEach(() => { - return resetApplicationState(openmct); + component.$destroy(); }); - let mockConditionSetObject = { - name: 'Condition Set', - key: 'conditionSet', - creatable: true + it('does not include the output label when the flag is disabled', () => { + styleViewComponentObject.conditionSetDomainObject = conditionSetDomainObject; + styleViewComponentObject.conditionalStyles = []; + styleViewComponentObject.initializeConditionalStyles(); + expect(styleViewComponentObject.conditionalStyles.length).toBe(2); + + return Vue.nextTick().then(() => { + const hasNoOutput = + styleViewComponentObject.domainObject.configuration.objectStyles.styles.every((style) => { + return style.style.output === '' || style.style.output === undefined; + }); + + expect(hasNoOutput).toBeTrue(); + }); + }); + + it('includes the output label when the flag is enabled', () => { + styleViewComponentObject.conditionSetDomainObject = conditionSetDomainObject; + styleViewComponentObject.conditionalStyles = []; + styleViewComponentObject.initializeConditionalStyles(); + expect(styleViewComponentObject.conditionalStyles.length).toBe(2); + + styleViewComponentObject.useConditionSetOutputAsLabel = true; + styleViewComponentObject.persistLabelConfiguration(); + + return Vue.nextTick().then(() => { + const outputs = styleViewComponentObject.domainObject.configuration.objectStyles.styles.map( + (style) => { + return style.style.output; + } + ); + expect(outputs.join(',')).toEqual('Sine > 0,Default'); + }); + }); + }); + + describe('the condition set usage for multiple display layout items', () => { + let displayLayoutItem; + let lineLayoutItem; + let boxLayoutItem; + let notCreatableObjectItem; + let notCreatableObject; + let selection; + let component; + let styleViewComponentObject; + const conditionSetDomainObject = { + configuration: { + conditionTestData: [ + { + telemetry: '', + metadata: '', + input: '' + } + ], + conditionCollection: [ + { + id: '39584410-cbf9-499e-96dc-76f27e69885d', + configuration: { + name: 'Unnamed Condition', + output: 'Sine > 0', + trigger: 'all', + criteria: [ + { + id: '85fbb2f7-7595-42bd-9767-a932266c5225', + telemetry: { + namespace: '', + key: 'be0ba97f-b510-4f40-a18d-4ff121d5ea1a' + }, + operation: 'greaterThan', + input: ['0'], + metadata: 'sin' + }, + { + id: '35400132-63b0-425c-ac30-8197df7d5862', + telemetry: 'any', + operation: 'enumValueIs', + input: ['0'], + metadata: 'state' + } + ] + }, + summary: + 'Match if all criteria are met: Sine Wave Generator Sine > 0 and any telemetry State is OFF ' + }, + { + isDefault: true, + id: '2532d90a-e0d6-4935-b546-3123522da2de', + configuration: { + name: 'Default', + output: 'Default', + trigger: 'all', + criteria: [] + }, + summary: '' + } + ] + }, + composition: [ + { + namespace: '', + key: 'be0ba97f-b510-4f40-a18d-4ff121d5ea1a' + }, + { + namespace: '', + key: '077ffa67-e78f-4e99-80e0-522ac33a3888' + } + ], + telemetry: {}, + name: 'Condition Set', + type: 'conditionSet', + identifier: { + namespace: '', + key: '863012c1-f6ca-4ab0-aed7-fd43d5e4cd12' + } + }; + const staticStyle = { + style: { + backgroundColor: '#666666', + border: '1px solid #00ffff' + } + }; + const conditionalStyle = { + conditionId: '39584410-cbf9-499e-96dc-76f27e69885d', + style: { + isStyleInvisible: '', + backgroundColor: '#666666', + border: '1px solid #ffff00' + } }; - it('defines a conditionSet object type with the correct key', () => { - expect(conditionSetDefinition.key).toEqual(mockConditionSetObject.key); + beforeEach(() => { + displayLayoutItem = { + composition: [], + configuration: { + items: [ + { + fill: '#666666', + stroke: '', + x: 1, + y: 1, + width: 10, + height: 5, + type: 'box-view', + id: '89b88746-d325-487b-aec4-11b79afff9e8' + }, + { + fill: '#666666', + stroke: '', + x: 1, + y: 1, + width: 10, + height: 5, + type: 'ellipse-view', + id: '19b88746-d325-487b-aec4-11b79afff9z8' + }, + { + x: 18, + y: 9, + x2: 23, + y2: 4, + stroke: '#666666', + type: 'line-view', + id: '57d49a28-7863-43bd-9593-6570758916f0' + }, + { + width: 32, + height: 18, + x: 36, + y: 8, + identifier: { + key: '~TEST~image', + namespace: 'test-space' + }, + hasFrame: true, + type: 'subobject-view', + id: '6d9fe81b-a3ce-4e59-b404-a4a0be1a5d85' + } + ], + layoutGrid: [10, 10] + }, + name: 'Display Layout', + type: 'layout', + identifier: { + namespace: '', + key: 'c5e636c1-6771-4c9c-b933-8665cab189b3' + } + }; + lineLayoutItem = { + x: 18, + y: 9, + x2: 23, + y2: 4, + stroke: '#666666', + type: 'line-view', + id: '57d49a28-7863-43bd-9593-6570758916f0' + }; + boxLayoutItem = { + fill: '#666666', + stroke: '', + x: 1, + y: 1, + width: 10, + height: 5, + type: 'box-view', + id: '89b88746-d325-487b-aec4-11b79afff9e8' + }; + notCreatableObjectItem = { + width: 32, + height: 18, + x: 36, + y: 8, + identifier: { + key: '~TEST~image', + namespace: 'test-space' + }, + hasFrame: true, + type: 'subobject-view', + id: '6d9fe81b-a3ce-4e59-b404-a4a0be1a5d85' + }; + notCreatableObject = { + identifier: { + key: '~TEST~image', + namespace: 'test-space' + }, + name: 'test~image', + location: 'test-space:~TEST', + type: 'test.image', + telemetry: { + values: [ + { + key: 'value', + name: 'Value', + hints: { + image: 1, + priority: 0 + }, + format: 'image', + source: 'value' + }, + { + key: 'utc', + source: 'timestamp', + name: 'Timestamp', + format: 'iso', + hints: { + domain: 1, + priority: 1 + } + } + ] + } + }; + selection = [ + [ + { + context: { + layoutItem: lineLayoutItem, + index: 1 + } + }, + { + context: { + item: displayLayoutItem, + supportsMultiSelect: true + } + } + ], + [ + { + context: { + layoutItem: boxLayoutItem, + index: 0 + } + }, + { + context: { + item: displayLayoutItem, + supportsMultiSelect: true + } + } + ], + [ + { + context: { + item: notCreatableObject, + layoutItem: notCreatableObjectItem, + index: 2 + } + }, + { + context: { + item: displayLayoutItem, + supportsMultiSelect: true + } + } + ] + ]; + let viewContainer = document.createElement('div'); + child.append(viewContainer); + component = new Vue({ + el: viewContainer, + components: { + StylesView + }, + provide: { + openmct: openmct, + selection: selection, + stylesManager + }, + template: '' + }); + + return Vue.nextTick().then(() => { + styleViewComponentObject = component.$root.$children[0]; + styleViewComponentObject.setEditState(true); + }); }); - describe('the conditionSet object', () => { - - it('is creatable', () => { - expect(conditionSetDefinition.creatable).toEqual(mockConditionSetObject.creatable); - }); - - it('initializes with an empty composition list', () => { - expect(mockConditionSetDomainObject.composition instanceof Array).toBeTrue(); - expect(mockConditionSetDomainObject.composition.length).toEqual(0); - }); + it('initializes the items in the view', () => { + expect(styleViewComponentObject.items.length).toBe(3); }); - describe('the condition set usage for condition widgets', () => { - let conditionWidgetItem; - let selection; - let component; - let styleViewComponentObject; - const conditionSetDomainObject = { - "configuration": { - "conditionTestData": [ - { - "telemetry": "", - "metadata": "", - "input": "" - } - ], - "conditionCollection": [ - { - "id": "39584410-cbf9-499e-96dc-76f27e69885d", - "configuration": { - "name": "Unnamed Condition", - "output": "Sine > 0", - "trigger": "all", - "criteria": [ - { - "id": "85fbb2f7-7595-42bd-9767-a932266c5225", - "telemetry": { - "namespace": "", - "key": "be0ba97f-b510-4f40-a18d-4ff121d5ea1a" - }, - "operation": "greaterThan", - "input": [ - "0" - ], - "metadata": "sin" - }, - { - "id": "35400132-63b0-425c-ac30-8197df7d5862", - "telemetry": "any", - "operation": "enumValueIs", - "input": [ - "0" - ], - "metadata": "state" - } - ] - }, - "summary": "Match if all criteria are met: Sine Wave Generator Sine > 0 and any telemetry State is OFF " - }, - { - "isDefault": true, - "id": "2532d90a-e0d6-4935-b546-3123522da2de", - "configuration": { - "name": "Default", - "output": "Default", - "trigger": "all", - "criteria": [ - ] - }, - "summary": "" - } + it('initializes conditional styles', () => { + styleViewComponentObject.conditionSetDomainObject = conditionSetDomainObject; + styleViewComponentObject.conditionalStyles = []; + styleViewComponentObject.initializeConditionalStyles(); + expect(styleViewComponentObject.conditionalStyles.length).toBe(2); + }); + + it('updates applicable conditional styles', () => { + styleViewComponentObject.conditionSetDomainObject = conditionSetDomainObject; + styleViewComponentObject.conditionalStyles = []; + styleViewComponentObject.initializeConditionalStyles(); + expect(styleViewComponentObject.conditionalStyles.length).toBe(2); + styleViewComponentObject.updateConditionalStyle(conditionalStyle, 'border'); + + return Vue.nextTick().then(() => { + expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined(); + [boxLayoutItem, lineLayoutItem, notCreatableObjectItem].forEach((item) => { + const itemStyles = + styleViewComponentObject.domainObject.configuration.objectStyles[item.id].styles; + expect(itemStyles.length).toBe(2); + const foundStyle = itemStyles.find((style) => { + return style.conditionId === conditionalStyle.conditionId; + }); + expect(foundStyle).toBeDefined(); + const applicableStyles = getApplicableStylesForItem( + styleViewComponentObject.domainObject, + item + ); + const applicableStylesKeys = Object.keys(applicableStyles).concat(['isStyleInvisible']); + Object.keys(foundStyle.style).forEach((key) => { + if (key === 'output') { + return; + } + + expect(applicableStylesKeys.indexOf(key)).toBeGreaterThan(-1); + expect(foundStyle.style[key]).toEqual(conditionalStyle.style[key]); + }); + }); + }); + }); + + it('updates applicable static styles', () => { + styleViewComponentObject.updateStaticStyle(staticStyle, 'border'); + + return Vue.nextTick().then(() => { + expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined(); + [boxLayoutItem, lineLayoutItem, notCreatableObjectItem].forEach((item) => { + const itemStyle = + styleViewComponentObject.domainObject.configuration.objectStyles[item.id].staticStyle; + expect(itemStyle).toBeDefined(); + const applicableStyles = getApplicableStylesForItem( + styleViewComponentObject.domainObject, + item + ); + const applicableStylesKeys = Object.keys(applicableStyles).concat(['isStyleInvisible']); + Object.keys(itemStyle.style).forEach((key) => { + expect(applicableStylesKeys.indexOf(key)).toBeGreaterThan(-1); + expect(itemStyle.style[key]).toEqual(staticStyle.style[key]); + }); + }); + }); + }); + }); + + describe('the condition check if old', () => { + let conditionSetDomainObject; + + beforeEach(() => { + conditionSetDomainObject = { + configuration: { + conditionTestData: [ + { + telemetry: '', + metadata: '', + input: '' + } + ], + conditionCollection: [ + { + id: '39584410-cbf9-499e-96dc-76f27e69885d', + configuration: { + name: 'Unnamed Condition', + output: 'Any old telemetry', + trigger: 'all', + criteria: [ + { + id: '35400132-63b0-425c-ac30-8197df7d5862', + telemetry: 'any', + operation: IS_OLD_KEY, + input: ['0.2'], + metadata: 'dataReceived' + } ] + }, + summary: 'Match if all criteria are met: Any telemetry is old after 5 seconds' }, - "composition": [ - { - "namespace": "", - "key": "be0ba97f-b510-4f40-a18d-4ff121d5ea1a" - }, - { - "namespace": "", - "key": "077ffa67-e78f-4e99-80e0-522ac33a3888" - } - ], - "telemetry": { - }, - "name": "Condition Set", - "type": "conditionSet", - "identifier": { - "namespace": "", - "key": "863012c1-f6ca-4ab0-aed7-fd43d5e4cd12" + { + isDefault: true, + id: '2532d90a-e0d6-4935-b546-3123522da2de', + configuration: { + name: 'Default', + output: 'Default', + trigger: 'all', + criteria: [] + }, + summary: '' } - - }; - - beforeEach(() => { - conditionWidgetItem = { - "label": "Condition Widget", - "conditionalLabel": "", - "configuration": { - }, - "name": "Condition Widget", - "type": "conditionWidget", - "identifier": { - "namespace": "", - "key": "c5e636c1-6771-4c9c-b933-8665cab189b3" - } - }; - selection = [ - [{ - context: { - "item": conditionWidgetItem, - "supportsMultiSelect": false - } - }] - ]; - let viewContainer = document.createElement('div'); - child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - StylesView - }, - provide: { - openmct: openmct, - selection: selection, - stylesManager - }, - template: '' - }); - - return Vue.nextTick().then(() => { - styleViewComponentObject = component.$root.$children[0]; - styleViewComponentObject.setEditState(true); - }); - }); - - afterEach(() => { - component.$destroy(); - }); - - it('does not include the output label when the flag is disabled', () => { - styleViewComponentObject.conditionSetDomainObject = conditionSetDomainObject; - styleViewComponentObject.conditionalStyles = []; - styleViewComponentObject.initializeConditionalStyles(); - expect(styleViewComponentObject.conditionalStyles.length).toBe(2); - - return Vue.nextTick().then(() => { - const hasNoOutput = styleViewComponentObject.domainObject.configuration.objectStyles.styles.every((style) => { - return style.style.output === '' || style.style.output === undefined; - }); - - expect(hasNoOutput).toBeTrue(); - }); - }); - - it('includes the output label when the flag is enabled', () => { - styleViewComponentObject.conditionSetDomainObject = conditionSetDomainObject; - styleViewComponentObject.conditionalStyles = []; - styleViewComponentObject.initializeConditionalStyles(); - expect(styleViewComponentObject.conditionalStyles.length).toBe(2); - - styleViewComponentObject.useConditionSetOutputAsLabel = true; - styleViewComponentObject.persistLabelConfiguration(); - - return Vue.nextTick().then(() => { - const outputs = styleViewComponentObject.domainObject.configuration.objectStyles.styles.map((style) => { - return style.style.output; - }); - expect(outputs.join(',')).toEqual('Sine > 0,Default'); - }); - }); - + ] + }, + composition: [ + { + namespace: '', + key: 'test-object' + } + ], + telemetry: {}, + name: 'Condition Set', + type: 'conditionSet', + identifier: { + namespace: '', + key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9' + } + }; }); - describe('the condition set usage for multiple display layout items', () => { - let displayLayoutItem; - let lineLayoutItem; - let boxLayoutItem; - let notCreatableObjectItem; - let notCreatableObject; - let selection; - let component; - let styleViewComponentObject; - const conditionSetDomainObject = { - "configuration": { - "conditionTestData": [ - { - "telemetry": "", - "metadata": "", - "input": "" - } - ], - "conditionCollection": [ - { - "id": "39584410-cbf9-499e-96dc-76f27e69885d", - "configuration": { - "name": "Unnamed Condition", - "output": "Sine > 0", - "trigger": "all", - "criteria": [ - { - "id": "85fbb2f7-7595-42bd-9767-a932266c5225", - "telemetry": { - "namespace": "", - "key": "be0ba97f-b510-4f40-a18d-4ff121d5ea1a" - }, - "operation": "greaterThan", - "input": [ - "0" - ], - "metadata": "sin" - }, - { - "id": "35400132-63b0-425c-ac30-8197df7d5862", - "telemetry": "any", - "operation": "enumValueIs", - "input": [ - "0" - ], - "metadata": "state" - } - ] - }, - "summary": "Match if all criteria are met: Sine Wave Generator Sine > 0 and any telemetry State is OFF " - }, - { - "isDefault": true, - "id": "2532d90a-e0d6-4935-b546-3123522da2de", - "configuration": { - "name": "Default", - "output": "Default", - "trigger": "all", - "criteria": [ - ] - }, - "summary": "" - } + it('should evaluate as old when telemetry is not received in the allotted time', (done) => { + let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct); + conditionMgr.on('conditionSetResultUpdated', mockListener); + conditionMgr.telemetryObjects = { + 'test-object': testTelemetryObject + }; + conditionMgr.updateConditionTelemetryObjects(); + setTimeout(() => { + expect(mockListener).toHaveBeenCalledWith({ + output: 'Any old telemetry', + id: { + namespace: '', + key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9' + }, + conditionId: '39584410-cbf9-499e-96dc-76f27e69885d', + utc: undefined + }); + done(); + }, 400); + }); + + it('should not evaluate as old when telemetry is received in the allotted time', (done) => { + const date = 1; + conditionSetDomainObject.configuration.conditionCollection[0].configuration.criteria[0].input = + ['0.4']; + let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct); + conditionMgr.on('conditionSetResultUpdated', mockListener); + conditionMgr.telemetryObjects = { + 'test-object': testTelemetryObject + }; + conditionMgr.updateConditionTelemetryObjects(); + conditionMgr.telemetryReceived(testTelemetryObject, { + utc: date + }); + setTimeout(() => { + expect(mockListener).toHaveBeenCalledWith({ + output: 'Default', + id: { + namespace: '', + key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9' + }, + conditionId: '2532d90a-e0d6-4935-b546-3123522da2de', + utc: date + }); + done(); + }, 300); + }); + }); + + describe('the condition evaluation', () => { + let conditionSetDomainObject; + + beforeEach(() => { + conditionSetDomainObject = { + configuration: { + conditionTestData: [ + { + telemetry: '', + metadata: '', + input: '' + } + ], + conditionCollection: [ + { + id: '39584410-cbf9-499e-96dc-76f27e69885f', + configuration: { + name: 'Unnamed Condition0', + output: 'Any telemetry less than 0', + trigger: 'all', + criteria: [ + { + id: '35400132-63b0-425c-ac30-8197df7d5864', + telemetry: 'any', + operation: 'lessThan', + input: ['0'], + metadata: 'some-key' + } ] + }, + summary: 'Match if all criteria are met: Any telemetry value is less than 0' }, - "composition": [ - { - "namespace": "", - "key": "be0ba97f-b510-4f40-a18d-4ff121d5ea1a" - }, - { - "namespace": "", - "key": "077ffa67-e78f-4e99-80e0-522ac33a3888" - } - ], - "telemetry": { + { + id: '39584410-cbf9-499e-96dc-76f27e69885d', + configuration: { + name: 'Unnamed Condition', + output: 'Any telemetry greater than 0', + trigger: 'all', + criteria: [ + { + id: '35400132-63b0-425c-ac30-8197df7d5862', + telemetry: 'any', + operation: 'greaterThan', + input: ['0'], + metadata: 'some-key' + } + ] + }, + summary: 'Match if all criteria are met: Any telemetry value is greater than 0' }, - "name": "Condition Set", - "type": "conditionSet", - "identifier": { - "namespace": "", - "key": "863012c1-f6ca-4ab0-aed7-fd43d5e4cd12" + { + id: '39584410-cbf9-499e-96dc-76f27e69885e', + configuration: { + name: 'Unnamed Condition1', + output: 'Any telemetry greater than 1', + trigger: 'all', + criteria: [ + { + id: '35400132-63b0-425c-ac30-8197df7d5863', + telemetry: 'any', + operation: 'greaterThan', + input: ['1'], + metadata: 'some-key' + } + ] + }, + summary: 'Match if all criteria are met: Any telemetry value is greater than 1' + }, + { + isDefault: true, + id: '2532d90a-e0d6-4935-b546-3123522da2de', + configuration: { + name: 'Default', + output: 'Default', + trigger: 'all', + criteria: [] + }, + summary: '' } + ] + }, + composition: [ + { + namespace: '', + key: 'test-object' + } + ], + telemetry: {}, + name: 'Condition Set', + type: 'conditionSet', + identifier: { + namespace: '', + key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9' + } + }; + }); - }; - const staticStyle = { - "style": { - "backgroundColor": "#666666", - "border": "1px solid #00ffff" + it('should stop evaluating conditions when a condition evaluates to true', () => { + const date = Date.now(); + let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct); + conditionMgr.on('conditionSetResultUpdated', mockListener); + conditionMgr.telemetryObjects = { + 'test-object': testTelemetryObject + }; + conditionMgr.updateConditionTelemetryObjects(); + conditionMgr.telemetryReceived(testTelemetryObject, { + 'some-key': 2, + utc: date + }); + let result = conditionMgr.conditions.map((condition) => condition.result); + expect(result[2]).toBeUndefined(); + }); + }); + + describe('canView of ConditionSetViewProvider', () => { + let conditionSetView; + const testViewObject = { + id: 'test-object', + type: 'conditionSet', + configuration: { + conditionCollection: [] + } + }; + + beforeEach(() => { + const applicableViews = openmct.objectViews.get(testViewObject, []); + conditionSetView = applicableViews.find( + (viewProvider) => viewProvider.key === 'conditionSet.view' + ); + }); + + it('provides a view', () => { + expect(conditionSetView).toBeDefined(); + }); + + it('returns true for type `conditionSet` and is a navigated Object', () => { + openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true); + + const isCanView = conditionSetView.canView(testViewObject, []); + + expect(isCanView).toBe(true); + }); + + it('returns false for type `conditionSet` and is not a navigated Object', () => { + openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(false); + + const isCanView = conditionSetView.canView(testViewObject, []); + + expect(isCanView).toBe(false); + }); + + it('returns false for type `notConditionSet` and is a navigated Object', () => { + openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true); + testViewObject.type = 'notConditionSet'; + const isCanView = conditionSetView.canView(testViewObject, []); + + expect(isCanView).toBe(false); + }); + }); + + describe('The Style Rule Manager', () => { + it('should subscribe to the conditionSet after the editor saves', async () => { + const stylesObject = { + styles: [ + { + conditionId: 'a8bf7d1a-c1bb-4fc7-936a-62056a51b5cd', + style: { + backgroundColor: '#38761d', + border: '', + color: '#073763', + isStyleInvisible: '' } - }; - const conditionalStyle = { - "conditionId": "39584410-cbf9-499e-96dc-76f27e69885d", - "style": { - "isStyleInvisible": "", - "backgroundColor": "#666666", - "border": "1px solid #ffff00" + }, + { + conditionId: '0558fa77-9bdc-4142-9f9a-7a28fe95182e', + style: { + backgroundColor: '#980000', + border: '', + color: '#ff9900', + isStyleInvisible: '' } - }; - - beforeEach(() => { - displayLayoutItem = { - "composition": [ - ], - "configuration": { - "items": [ - { - "fill": "#666666", - "stroke": "", - "x": 1, - "y": 1, - "width": 10, - "height": 5, - "type": "box-view", - "id": "89b88746-d325-487b-aec4-11b79afff9e8" - }, - { - "fill": "#666666", - "stroke": "", - "x": 1, - "y": 1, - "width": 10, - "height": 5, - "type": "ellipse-view", - "id": "19b88746-d325-487b-aec4-11b79afff9z8" - }, - { - "x": 18, - "y": 9, - "x2": 23, - "y2": 4, - "stroke": "#666666", - "type": "line-view", - "id": "57d49a28-7863-43bd-9593-6570758916f0" - }, - { - "width": 32, - "height": 18, - "x": 36, - "y": 8, - "identifier": { - "key": "~TEST~image", - "namespace": "test-space" - }, - "hasFrame": true, - "type": "subobject-view", - "id": "6d9fe81b-a3ce-4e59-b404-a4a0be1a5d85" - } - ], - "layoutGrid": [ - 10, - 10 - ] - }, - "name": "Display Layout", - "type": "layout", - "identifier": { - "namespace": "", - "key": "c5e636c1-6771-4c9c-b933-8665cab189b3" - } - }; - lineLayoutItem = { - "x": 18, - "y": 9, - "x2": 23, - "y2": 4, - "stroke": "#666666", - "type": "line-view", - "id": "57d49a28-7863-43bd-9593-6570758916f0" - }; - boxLayoutItem = { - "fill": "#666666", - "stroke": "", - "x": 1, - "y": 1, - "width": 10, - "height": 5, - "type": "box-view", - "id": "89b88746-d325-487b-aec4-11b79afff9e8" - }; - notCreatableObjectItem = { - "width": 32, - "height": 18, - "x": 36, - "y": 8, - "identifier": { - "key": "~TEST~image", - "namespace": "test-space" - }, - "hasFrame": true, - "type": "subobject-view", - "id": "6d9fe81b-a3ce-4e59-b404-a4a0be1a5d85" - }; - notCreatableObject = { - "identifier": { - "key": "~TEST~image", - "namespace": "test-space" - }, - "name": "test~image", - "location": "test-space:~TEST", - "type": "test.image", - "telemetry": { - "values": [ - { - "key": "value", - "name": "Value", - "hints": { - "image": 1, - "priority": 0 - }, - "format": "image", - "source": "value" - }, - { - "key": "utc", - "source": "timestamp", - "name": "Timestamp", - "format": "iso", - "hints": { - "domain": 1, - "priority": 1 - } - } - ] - } - }; - selection = [ - [{ - context: { - "layoutItem": lineLayoutItem, - "index": 1 - } - }, - { - context: { - "item": displayLayoutItem, - "supportsMultiSelect": true - } - }], - [{ - context: { - "layoutItem": boxLayoutItem, - "index": 0 - } - }, - { - context: { - item: displayLayoutItem, - "supportsMultiSelect": true - } - }], - [{ - context: { - "item": notCreatableObject, - "layoutItem": notCreatableObjectItem, - "index": 2 - } - }, - { - context: { - item: displayLayoutItem, - "supportsMultiSelect": true - } - }] - ]; - let viewContainer = document.createElement('div'); - child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - StylesView - }, - provide: { - openmct: openmct, - selection: selection, - stylesManager - }, - template: '' - }); - - return Vue.nextTick().then(() => { - styleViewComponentObject = component.$root.$children[0]; - styleViewComponentObject.setEditState(true); - }); - }); - - it('initializes the items in the view', () => { - expect(styleViewComponentObject.items.length).toBe(3); - }); - - it('initializes conditional styles', () => { - styleViewComponentObject.conditionSetDomainObject = conditionSetDomainObject; - styleViewComponentObject.conditionalStyles = []; - styleViewComponentObject.initializeConditionalStyles(); - expect(styleViewComponentObject.conditionalStyles.length).toBe(2); - }); - - it('updates applicable conditional styles', () => { - styleViewComponentObject.conditionSetDomainObject = conditionSetDomainObject; - styleViewComponentObject.conditionalStyles = []; - styleViewComponentObject.initializeConditionalStyles(); - expect(styleViewComponentObject.conditionalStyles.length).toBe(2); - styleViewComponentObject.updateConditionalStyle(conditionalStyle, 'border'); - - return Vue.nextTick().then(() => { - expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined(); - [boxLayoutItem, lineLayoutItem, notCreatableObjectItem].forEach((item) => { - const itemStyles = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].styles; - expect(itemStyles.length).toBe(2); - const foundStyle = itemStyles.find((style) => { - return style.conditionId === conditionalStyle.conditionId; - }); - expect(foundStyle).toBeDefined(); - const applicableStyles = getApplicableStylesForItem(styleViewComponentObject.domainObject, item); - const applicableStylesKeys = Object.keys(applicableStyles).concat(['isStyleInvisible']); - Object.keys(foundStyle.style).forEach((key) => { - if (key === 'output') { - return; - } - - expect(applicableStylesKeys.indexOf(key)).toBeGreaterThan(-1); - expect(foundStyle.style[key]).toEqual(conditionalStyle.style[key]); - }); - }); - }); - }); - - it('updates applicable static styles', () => { - styleViewComponentObject.updateStaticStyle(staticStyle, 'border'); - - return Vue.nextTick().then(() => { - expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined(); - [boxLayoutItem, lineLayoutItem, notCreatableObjectItem].forEach((item) => { - const itemStyle = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].staticStyle; - expect(itemStyle).toBeDefined(); - const applicableStyles = getApplicableStylesForItem(styleViewComponentObject.domainObject, item); - const applicableStylesKeys = Object.keys(applicableStyles).concat(['isStyleInvisible']); - Object.keys(itemStyle.style).forEach((key) => { - expect(applicableStylesKeys.indexOf(key)).toBeGreaterThan(-1); - expect(itemStyle.style[key]).toEqual(staticStyle.style[key]); - }); - }); - }); - }); + } + ], + staticStyle: { + style: { + backgroundColor: '', + border: '', + color: '' + } + }, + selectedConditionId: '0558fa77-9bdc-4142-9f9a-7a28fe95182e', + defaultConditionId: '0558fa77-9bdc-4142-9f9a-7a28fe95182e', + conditionSetIdentifier: { + namespace: '', + key: '035c589c-d98f-429e-8b89-d76bd8d22b29' + } + }; + openmct.$injector = jasmine.createSpyObj('$injector', ['get']); + // const mockTransactionService = jasmine.createSpyObj( + // 'transactionService', + // ['commit'] + // ); + openmct.telemetry = jasmine.createSpyObj('telemetry', [ + 'isTelemetryObject', + 'subscribe', + 'getMetadata', + 'getValueFormatter', + 'request' + ]); + openmct.telemetry.isTelemetryObject.and.returnValue(true); + openmct.telemetry.subscribe.and.returnValue(function () {}); + openmct.telemetry.getValueFormatter.and.returnValue({ + parse: function (value) { + return value; + } + }); + openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry); + openmct.telemetry.request.and.returnValue(Promise.resolve([])); + const styleRuleManger = new StyleRuleManager(stylesObject, openmct, null, true); + spyOn(styleRuleManger, 'subscribeToConditionSet'); + openmct.editor.edit(); + await openmct.editor.save(); + expect(styleRuleManger.subscribeToConditionSet).toHaveBeenCalledTimes(1); }); - - describe('the condition check if old', () => { - let conditionSetDomainObject; - - beforeEach(() => { - conditionSetDomainObject = { - "configuration": { - "conditionTestData": [ - { - "telemetry": "", - "metadata": "", - "input": "" - } - ], - "conditionCollection": [ - { - "id": "39584410-cbf9-499e-96dc-76f27e69885d", - "configuration": { - "name": "Unnamed Condition", - "output": "Any old telemetry", - "trigger": "all", - "criteria": [ - { - "id": "35400132-63b0-425c-ac30-8197df7d5862", - "telemetry": "any", - "operation": IS_OLD_KEY, - "input": [ - "0.2" - ], - "metadata": "dataReceived" - } - ] - }, - "summary": "Match if all criteria are met: Any telemetry is old after 5 seconds" - }, - { - "isDefault": true, - "id": "2532d90a-e0d6-4935-b546-3123522da2de", - "configuration": { - "name": "Default", - "output": "Default", - "trigger": "all", - "criteria": [ - ] - }, - "summary": "" - } - ] - }, - "composition": [ - { - "namespace": "", - "key": "test-object" - } - ], - "telemetry": { - }, - "name": "Condition Set", - "type": "conditionSet", - "identifier": { - "namespace": "", - "key": "cf4456a9-296a-4e6b-b182-62ed29cd15b9" - } - - }; - }); - - it('should evaluate as old when telemetry is not received in the allotted time', (done) => { - let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct); - conditionMgr.on('conditionSetResultUpdated', mockListener); - conditionMgr.telemetryObjects = { - "test-object": testTelemetryObject - }; - conditionMgr.updateConditionTelemetryObjects(); - setTimeout(() => { - expect(mockListener).toHaveBeenCalledWith({ - output: 'Any old telemetry', - id: { - namespace: '', - key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9' - }, - conditionId: '39584410-cbf9-499e-96dc-76f27e69885d', - utc: undefined - }); - done(); - }, 400); - }); - - it('should not evaluate as old when telemetry is received in the allotted time', (done) => { - const date = 1; - conditionSetDomainObject.configuration.conditionCollection[0].configuration.criteria[0].input = ["0.4"]; - let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct); - conditionMgr.on('conditionSetResultUpdated', mockListener); - conditionMgr.telemetryObjects = { - "test-object": testTelemetryObject - }; - conditionMgr.updateConditionTelemetryObjects(); - conditionMgr.telemetryReceived(testTelemetryObject, { - utc: date - }); - setTimeout(() => { - expect(mockListener).toHaveBeenCalledWith({ - output: 'Default', - id: { - namespace: '', - key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9' - }, - conditionId: '2532d90a-e0d6-4935-b546-3123522da2de', - utc: date - }); - done(); - }, 300); - }); - }); - - describe('the condition evaluation', () => { - let conditionSetDomainObject; - - beforeEach(() => { - conditionSetDomainObject = { - "configuration": { - "conditionTestData": [ - { - "telemetry": "", - "metadata": "", - "input": "" - } - ], - "conditionCollection": [ - { - "id": "39584410-cbf9-499e-96dc-76f27e69885f", - "configuration": { - "name": "Unnamed Condition0", - "output": "Any telemetry less than 0", - "trigger": "all", - "criteria": [ - { - "id": "35400132-63b0-425c-ac30-8197df7d5864", - "telemetry": "any", - "operation": "lessThan", - "input": [ - "0" - ], - "metadata": "some-key" - } - ] - }, - "summary": "Match if all criteria are met: Any telemetry value is less than 0" - }, - { - "id": "39584410-cbf9-499e-96dc-76f27e69885d", - "configuration": { - "name": "Unnamed Condition", - "output": "Any telemetry greater than 0", - "trigger": "all", - "criteria": [ - { - "id": "35400132-63b0-425c-ac30-8197df7d5862", - "telemetry": "any", - "operation": "greaterThan", - "input": [ - "0" - ], - "metadata": "some-key" - } - ] - }, - "summary": "Match if all criteria are met: Any telemetry value is greater than 0" - }, - { - "id": "39584410-cbf9-499e-96dc-76f27e69885e", - "configuration": { - "name": "Unnamed Condition1", - "output": "Any telemetry greater than 1", - "trigger": "all", - "criteria": [ - { - "id": "35400132-63b0-425c-ac30-8197df7d5863", - "telemetry": "any", - "operation": "greaterThan", - "input": [ - "1" - ], - "metadata": "some-key" - } - ] - }, - "summary": "Match if all criteria are met: Any telemetry value is greater than 1" - }, - { - "isDefault": true, - "id": "2532d90a-e0d6-4935-b546-3123522da2de", - "configuration": { - "name": "Default", - "output": "Default", - "trigger": "all", - "criteria": [ - ] - }, - "summary": "" - } - ] - }, - "composition": [ - { - "namespace": "", - "key": "test-object" - } - ], - "telemetry": { - }, - "name": "Condition Set", - "type": "conditionSet", - "identifier": { - "namespace": "", - "key": "cf4456a9-296a-4e6b-b182-62ed29cd15b9" - } - - }; - }); - - it('should stop evaluating conditions when a condition evaluates to true', () => { - const date = Date.now(); - let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct); - conditionMgr.on('conditionSetResultUpdated', mockListener); - conditionMgr.telemetryObjects = { - "test-object": testTelemetryObject - }; - conditionMgr.updateConditionTelemetryObjects(); - conditionMgr.telemetryReceived(testTelemetryObject, { - "some-key": 2, - utc: date - }); - let result = conditionMgr.conditions.map(condition => condition.result); - expect(result[2]).toBeUndefined(); - }); - }); - - describe('canView of ConditionSetViewProvider', () => { - let conditionSetView; - const testViewObject = { - id: "test-object", - type: "conditionSet", - configuration: { - conditionCollection: [] - } - }; - - beforeEach(() => { - const applicableViews = openmct.objectViews.get(testViewObject, []); - conditionSetView = applicableViews.find((viewProvider) => viewProvider.key === 'conditionSet.view'); - }); - - it('provides a view', () => { - expect(conditionSetView).toBeDefined(); - }); - - it('returns true for type `conditionSet` and is a navigated Object', () => { - openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true); - - const isCanView = conditionSetView.canView(testViewObject, []); - - expect(isCanView).toBe(true); - }); - - it('returns false for type `conditionSet` and is not a navigated Object', () => { - openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(false); - - const isCanView = conditionSetView.canView(testViewObject, []); - - expect(isCanView).toBe(false); - }); - - it('returns false for type `notConditionSet` and is a navigated Object', () => { - openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true); - testViewObject.type = 'notConditionSet'; - const isCanView = conditionSetView.canView(testViewObject, []); - - expect(isCanView).toBe(false); - }); - }); - - describe('The Style Rule Manager', () => { - it('should subscribe to the conditionSet after the editor saves', async () => { - const stylesObject = { - "styles": [ - { - "conditionId": "a8bf7d1a-c1bb-4fc7-936a-62056a51b5cd", - "style": { - "backgroundColor": "#38761d", - "border": "", - "color": "#073763", - "isStyleInvisible": "" - } - }, - { - "conditionId": "0558fa77-9bdc-4142-9f9a-7a28fe95182e", - "style": { - "backgroundColor": "#980000", - "border": "", - "color": "#ff9900", - "isStyleInvisible": "" - } - } - ], - "staticStyle": { - "style": { - "backgroundColor": "", - "border": "", - "color": "" - } - }, - "selectedConditionId": "0558fa77-9bdc-4142-9f9a-7a28fe95182e", - "defaultConditionId": "0558fa77-9bdc-4142-9f9a-7a28fe95182e", - "conditionSetIdentifier": { - "namespace": "", - "key": "035c589c-d98f-429e-8b89-d76bd8d22b29" - } - }; - openmct.$injector = jasmine.createSpyObj('$injector', ['get']); - // const mockTransactionService = jasmine.createSpyObj( - // 'transactionService', - // ['commit'] - // ); - openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject', "subscribe", "getMetadata", "getValueFormatter", "request"]); - openmct.telemetry.isTelemetryObject.and.returnValue(true); - openmct.telemetry.subscribe.and.returnValue(function () {}); - openmct.telemetry.getValueFormatter.and.returnValue({ - parse: function (value) { - return value; - } - }); - openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry); - openmct.telemetry.request.and.returnValue(Promise.resolve([])); - - const styleRuleManger = new StyleRuleManager(stylesObject, openmct, null, true); - spyOn(styleRuleManger, 'subscribeToConditionSet'); - openmct.editor.edit(); - await openmct.editor.save(); - expect(styleRuleManger.subscribeToConditionSet).toHaveBeenCalledTimes(1); - }); - }); + }); }); diff --git a/src/plugins/condition/utils/constants.js b/src/plugins/condition/utils/constants.js index 8fd72b2069..ac2c60d1c7 100644 --- a/src/plugins/condition/utils/constants.js +++ b/src/plugins/condition/utils/constants.js @@ -21,43 +21,43 @@ *****************************************************************************/ export const TRIGGER = { - ANY: 'any', - ALL: 'all', - NOT: 'not', - XOR: 'xor' + ANY: 'any', + ALL: 'all', + NOT: 'not', + XOR: 'xor' }; export const TRIGGER_LABEL = { - 'any': 'any criteria are met', - 'all': 'all criteria are met', - 'not': 'no criteria are met', - 'xor': 'only one criterion is met' + any: 'any criteria are met', + all: 'all criteria are met', + not: 'no criteria are met', + xor: 'only one criterion is met' }; export const TRIGGER_CONJUNCTION = { - 'any': 'or', - 'all': 'and', - 'not': 'and', - 'xor': 'or' + any: 'or', + all: 'and', + not: 'and', + xor: 'or' }; export const STYLE_CONSTANTS = { - isStyleInvisible: 'is-style-invisible', - borderColorTitle: 'Set border color', - textColorTitle: 'Set text color', - backgroundColorTitle: 'Set background color', - imagePropertiesTitle: 'Edit image properties', - visibilityHidden: 'Hidden', - visibilityVisible: 'Visible' + isStyleInvisible: 'is-style-invisible', + borderColorTitle: 'Set border color', + textColorTitle: 'Set text color', + backgroundColorTitle: 'Set background color', + imagePropertiesTitle: 'Edit image properties', + visibilityHidden: 'Hidden', + visibilityVisible: 'Visible' }; export const ERROR = { - 'TELEMETRY_NOT_FOUND': { - errorText: 'Telemetry not found for criterion' - }, - 'CONDITION_NOT_FOUND': { - errorText: 'Condition not found' - } + TELEMETRY_NOT_FOUND: { + errorText: 'Telemetry not found for criterion' + }, + CONDITION_NOT_FOUND: { + errorText: 'Condition not found' + } }; export const IS_OLD_KEY = 'isStale'; diff --git a/src/plugins/condition/utils/evaluator.js b/src/plugins/condition/utils/evaluator.js index 0087ef09aa..c5785d9083 100644 --- a/src/plugins/condition/utils/evaluator.js +++ b/src/plugins/condition/utils/evaluator.js @@ -19,51 +19,51 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import { TRIGGER } from "./constants"; +import { TRIGGER } from './constants'; export function evaluateResults(results, trigger) { - if (trigger && trigger === TRIGGER.XOR) { - return matchExact(results, 1); - } else if (trigger && trigger === TRIGGER.NOT) { - return matchExact(results, 0); - } else if (trigger && trigger === TRIGGER.ALL) { - return matchAll(results); - } else { - return matchAny(results); - } + if (trigger && trigger === TRIGGER.XOR) { + return matchExact(results, 1); + } else if (trigger && trigger === TRIGGER.NOT) { + return matchExact(results, 0); + } else if (trigger && trigger === TRIGGER.ALL) { + return matchAll(results); + } else { + return matchAny(results); + } } function matchAll(results) { - for (let result of results) { - if (result !== true) { - return false; - } + for (let result of results) { + if (result !== true) { + return false; } + } - return true; + return true; } function matchAny(results) { - for (let result of results) { - if (result === true) { - return true; - } + for (let result of results) { + if (result === true) { + return true; } + } - return false; + return false; } function matchExact(results, target) { - let matches = 0; - for (let result of results) { - if (result === true) { - matches++; - } - - if (matches > target) { - return false; - } + let matches = 0; + for (let result of results) { + if (result === true) { + matches++; } - return matches === target; + if (matches > target) { + return false; + } + } + + return matches === target; } diff --git a/src/plugins/condition/utils/evaluatorSpec.js b/src/plugins/condition/utils/evaluatorSpec.js index a8bf243148..ef938ddec9 100644 --- a/src/plugins/condition/utils/evaluatorSpec.js +++ b/src/plugins/condition/utils/evaluatorSpec.js @@ -24,181 +24,191 @@ import { evaluateResults } from './evaluator'; import { TRIGGER } from './constants'; describe('evaluate results', () => { - // const allTrue = [true, true, true, true, true]; - // const oneTrue = [false, false, false, false, true]; - // const multipleTrue = [false, true, false, true, false]; - // const noneTrue = [false, false, false, false, false]; - // const allTrueWithUndefined = [true, true, true, undefined, true]; - // const oneTrueWithUndefined = [undefined, undefined, undefined, undefined, true]; - // const multipleTrueWithUndefined = [true, undefined, true, undefined, true]; - // const allUndefined = [undefined, undefined, undefined, undefined, undefined]; - // const singleTrue = [true]; - // const singleFalse = [false]; - // const singleUndefined = [undefined]; - // const empty = []; + // const allTrue = [true, true, true, true, true]; + // const oneTrue = [false, false, false, false, true]; + // const multipleTrue = [false, true, false, true, false]; + // const noneTrue = [false, false, false, false, false]; + // const allTrueWithUndefined = [true, true, true, undefined, true]; + // const oneTrueWithUndefined = [undefined, undefined, undefined, undefined, true]; + // const multipleTrueWithUndefined = [true, undefined, true, undefined, true]; + // const allUndefined = [undefined, undefined, undefined, undefined, undefined]; + // const singleTrue = [true]; + // const singleFalse = [false]; + // const singleUndefined = [undefined]; + // const empty = []; - const tests = [ - { - name: 'allTrue', - values: [true, true, true, true, true], - any: true, - all: true, - not: false, - xor: false - }, { - name: 'oneTrue', - values: [false, false, false, false, true], - any: true, - all: false, - not: false, - xor: true - }, { - name: 'multipleTrue', - values: [false, true, false, true, false], - any: true, - all: false, - not: false, - xor: false - }, { - name: 'noneTrue', - values: [false, false, false, false, false], - any: false, - all: false, - not: true, - xor: false - }, { - name: 'allTrueWithUndefined', - values: [true, true, true, undefined, true], - any: true, - all: false, - not: false, - xor: false - }, { - name: 'oneTrueWithUndefined', - values: [undefined, undefined, undefined, undefined, true], - any: true, - all: false, - not: false, - xor: true - }, { - name: 'multipleTrueWithUndefined', - values: [true, undefined, true, undefined, true], - any: true, - all: false, - not: false, - xor: false - }, { - name: 'allUndefined', - values: [undefined, undefined, undefined, undefined, undefined], - any: false, - all: false, - not: true, - xor: false - }, { - name: 'singleTrue', - values: [true], - any: true, - all: true, - not: false, - xor: true - }, { - name: 'singleFalse', - values: [false], - any: false, - all: false, - not: true, - xor: false - }, { - name: 'singleUndefined', - values: [undefined], - any: false, - all: false, - not: true, - xor: false - } - // , { - // name: 'empty', - // values: [], - // any: false, - // all: false, - // not: true, - // xor: false - // } - ]; + const tests = [ + { + name: 'allTrue', + values: [true, true, true, true, true], + any: true, + all: true, + not: false, + xor: false + }, + { + name: 'oneTrue', + values: [false, false, false, false, true], + any: true, + all: false, + not: false, + xor: true + }, + { + name: 'multipleTrue', + values: [false, true, false, true, false], + any: true, + all: false, + not: false, + xor: false + }, + { + name: 'noneTrue', + values: [false, false, false, false, false], + any: false, + all: false, + not: true, + xor: false + }, + { + name: 'allTrueWithUndefined', + values: [true, true, true, undefined, true], + any: true, + all: false, + not: false, + xor: false + }, + { + name: 'oneTrueWithUndefined', + values: [undefined, undefined, undefined, undefined, true], + any: true, + all: false, + not: false, + xor: true + }, + { + name: 'multipleTrueWithUndefined', + values: [true, undefined, true, undefined, true], + any: true, + all: false, + not: false, + xor: false + }, + { + name: 'allUndefined', + values: [undefined, undefined, undefined, undefined, undefined], + any: false, + all: false, + not: true, + xor: false + }, + { + name: 'singleTrue', + values: [true], + any: true, + all: true, + not: false, + xor: true + }, + { + name: 'singleFalse', + values: [false], + any: false, + all: false, + not: true, + xor: false + }, + { + name: 'singleUndefined', + values: [undefined], + any: false, + all: false, + not: true, + xor: false + } + // , { + // name: 'empty', + // values: [], + // any: false, + // all: false, + // not: true, + // xor: false + // } + ]; - describe(`based on trigger ${TRIGGER.ANY}`, () => { - it('should evaluate to expected result', () => { - tests.forEach(test => { - const result = evaluateResults(test.values, TRIGGER.ANY); - expect(result).toEqual(test[TRIGGER.ANY]); - }); - }); + describe(`based on trigger ${TRIGGER.ANY}`, () => { + it('should evaluate to expected result', () => { + tests.forEach((test) => { + const result = evaluateResults(test.values, TRIGGER.ANY); + expect(result).toEqual(test[TRIGGER.ANY]); + }); }); + }); - describe(`based on trigger ${TRIGGER.ALL}`, () => { - it('should evaluate to expected result', () => { - tests.forEach(test => { - const result = evaluateResults(test.values, TRIGGER.ALL); - expect(result).toEqual(test[TRIGGER.ALL]); - }); - }); + describe(`based on trigger ${TRIGGER.ALL}`, () => { + it('should evaluate to expected result', () => { + tests.forEach((test) => { + const result = evaluateResults(test.values, TRIGGER.ALL); + expect(result).toEqual(test[TRIGGER.ALL]); + }); }); + }); - describe(`based on trigger ${TRIGGER.NOT}`, () => { - it('should evaluate to expected result', () => { - tests.forEach(test => { - const result = evaluateResults(test.values, TRIGGER.NOT); - expect(result).toEqual(test[TRIGGER.NOT]); - }); - }); + describe(`based on trigger ${TRIGGER.NOT}`, () => { + it('should evaluate to expected result', () => { + tests.forEach((test) => { + const result = evaluateResults(test.values, TRIGGER.NOT); + expect(result).toEqual(test[TRIGGER.NOT]); + }); }); + }); - describe(`based on trigger ${TRIGGER.XOR}`, () => { - it('should evaluate to expected result', () => { - tests.forEach(test => { - const result = evaluateResults(test.values, TRIGGER.XOR); - expect(result).toEqual(test[TRIGGER.XOR]); - }); - }); + describe(`based on trigger ${TRIGGER.XOR}`, () => { + it('should evaluate to expected result', () => { + tests.forEach((test) => { + const result = evaluateResults(test.values, TRIGGER.XOR); + expect(result).toEqual(test[TRIGGER.XOR]); + }); }); + }); - // it('should evaluate to true if trigger is NOT', () => { - // const results = { - // result: false, - // result1: false, - // result2: false - // }; - // const result = computeConditionByLimit(results, 0); - // expect(result).toBeTrue(); - // }); + // it('should evaluate to true if trigger is NOT', () => { + // const results = { + // result: false, + // result1: false, + // result2: false + // }; + // const result = computeConditionByLimit(results, 0); + // expect(result).toBeTrue(); + // }); - // it('should evaluate to false if trigger is NOT', () => { - // const results = { - // result: true, - // result1: false, - // result2: false - // }; - // const result = computeConditionByLimit(results, 0); - // expect(result).toBeFalse(); - // }); + // it('should evaluate to false if trigger is NOT', () => { + // const results = { + // result: true, + // result1: false, + // result2: false + // }; + // const result = computeConditionByLimit(results, 0); + // expect(result).toBeFalse(); + // }); - // it('should evaluate to true if trigger is XOR', () => { - // const results = { - // result: false, - // result1: true, - // result2: false - // }; - // const result = computeConditionByLimit(results, 1); - // expect(result).toBeTrue(); - // }); + // it('should evaluate to true if trigger is XOR', () => { + // const results = { + // result: false, + // result1: true, + // result2: false + // }; + // const result = computeConditionByLimit(results, 1); + // expect(result).toBeTrue(); + // }); - // it('should evaluate to false if trigger is XOR', () => { - // const results = { - // result: false, - // result1: true, - // result2: true - // }; - // const result = computeConditionByLimit(results, 1); - // expect(result).toBeFalse(); - // }); + // it('should evaluate to false if trigger is XOR', () => { + // const results = { + // result: false, + // result1: true, + // result2: true + // }; + // const result = computeConditionByLimit(results, 1); + // expect(result).toBeFalse(); + // }); }); diff --git a/src/plugins/condition/utils/operations.js b/src/plugins/condition/utils/operations.js index 7a9235141e..ecd7695e08 100644 --- a/src/plugins/condition/utils/operations.js +++ b/src/plugins/condition/utils/operations.js @@ -20,315 +20,317 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { IS_OLD_KEY, IS_STALE_KEY } from "./constants"; +import { IS_OLD_KEY, IS_STALE_KEY } from './constants'; function convertToNumbers(input) { - let numberInputs = []; - input.forEach(inputValue => numberInputs.push(Number(inputValue))); + let numberInputs = []; + input.forEach((inputValue) => numberInputs.push(Number(inputValue))); - return numberInputs; + return numberInputs; } function convertToStrings(input) { - let stringInputs = []; - input.forEach(inputValue => stringInputs.push(inputValue !== undefined ? inputValue.toString() : '')); + let stringInputs = []; + input.forEach((inputValue) => + stringInputs.push(inputValue !== undefined ? inputValue.toString() : '') + ); - return stringInputs; + return stringInputs; } function joinValues(values, length) { - return values.slice(0, length).join(', '); + return values.slice(0, length).join(', '); } export const OPERATIONS = [ - { - name: 'equalTo', - operation: function (input) { - return Number(input[0]) === Number(input[1]); - }, - text: 'is equal to', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' is ' + joinValues(values, 1); - } + { + name: 'equalTo', + operation: function (input) { + return Number(input[0]) === Number(input[1]); }, - { - name: 'notEqualTo', - operation: function (input) { - return Number(input[0]) !== Number(input[1]); - }, - text: 'is not equal to', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' is not ' + joinValues(values, 1); - } - }, - { - name: 'greaterThan', - operation: function (input) { - return Number(input[0]) > Number(input[1]); - }, - text: 'is greater than', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' > ' + joinValues(values, 1); - } - }, - { - name: 'lessThan', - operation: function (input) { - return Number(input[0]) < Number(input[1]); - }, - text: 'is less than', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' < ' + joinValues(values, 1); - } - }, - { - name: 'greaterThanOrEq', - operation: function (input) { - return Number(input[0]) >= Number(input[1]); - }, - text: 'is greater than or equal to', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' >= ' + joinValues(values, 1); - } - }, - { - name: 'lessThanOrEq', - operation: function (input) { - return Number(input[0]) <= Number(input[1]); - }, - text: 'is less than or equal to', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' <= ' + joinValues(values, 1); - } - }, - { - name: 'between', - operation: function (input) { - let numberInputs = convertToNumbers(input); - let larger = Math.max(...numberInputs.slice(1, 3)); - let smaller = Math.min(...numberInputs.slice(1, 3)); - - return (numberInputs[0] > smaller) && (numberInputs[0] < larger); - }, - text: 'is between', - appliesTo: ['number'], - inputCount: 2, - getDescription: function (values) { - return ' is between ' + values[0] + ' and ' + values[1]; - } - }, - { - name: 'notBetween', - operation: function (input) { - let numberInputs = convertToNumbers(input); - let larger = Math.max(...numberInputs.slice(1, 3)); - let smaller = Math.min(...numberInputs.slice(1, 3)); - - return (numberInputs[0] < smaller) || (numberInputs[0] > larger); - }, - text: 'is not between', - appliesTo: ['number'], - inputCount: 2, - getDescription: function (values) { - return ' is not between ' + values[0] + ' and ' + values[1]; - } - }, - { - name: 'textContains', - operation: function (input) { - return input[0] && input[1] && input[0].includes(input[1]); - }, - text: 'text contains', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' contains ' + joinValues(values, 1); - } - }, - { - name: 'textDoesNotContain', - operation: function (input) { - return input[0] && input[1] && !input[0].includes(input[1]); - }, - text: 'text does not contain', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' does not contain ' + joinValues(values, 1); - } - }, - { - name: 'textStartsWith', - operation: function (input) { - return input[0].startsWith(input[1]); - }, - text: 'text starts with', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' starts with ' + joinValues(values, 1); - } - }, - { - name: 'textEndsWith', - operation: function (input) { - return input[0].endsWith(input[1]); - }, - text: 'text ends with', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' ends with ' + joinValues(values, 1); - } - }, - { - name: 'textIsExactly', - operation: function (input) { - return input[0] === input[1]; - }, - text: 'text is exactly', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' is exactly ' + joinValues(values, 1); - } - }, - { - name: 'isUndefined', - operation: function (input) { - return typeof input[0] === 'undefined'; - }, - text: 'is undefined', - appliesTo: ['string', 'number', 'enum'], - inputCount: 0, - getDescription: function () { - return ' is undefined'; - } - }, - { - name: 'isDefined', - operation: function (input) { - return typeof input[0] !== 'undefined'; - }, - text: 'is defined', - appliesTo: ['string', 'number', 'enum'], - inputCount: 0, - getDescription: function () { - return ' is defined'; - } - }, - { - name: 'enumValueIs', - operation: function (input) { - let stringInputs = convertToStrings(input); - - return stringInputs[0] === stringInputs[1]; - }, - text: 'is', - appliesTo: ['enum'], - inputCount: 1, - getDescription: function (values) { - return ' is ' + joinValues(values, 1); - } - }, - { - name: 'enumValueIsNot', - operation: function (input) { - let stringInputs = convertToStrings(input); - - return stringInputs[0] !== stringInputs[1]; - }, - text: 'is not', - appliesTo: ['enum'], - inputCount: 1, - getDescription: function (values) { - return ' is not ' + joinValues(values, 1); - } - }, - { - name: 'isOneOf', - operation: function (input) { - const lhsValue = input[0] !== undefined ? input[0].toString() : ''; - if (input[1]) { - const values = input[1].split(','); - - return values.some((value) => lhsValue === value.toString().trim()); - } - - return false; - }, - text: 'is one of', - appliesTo: ["string", "number"], - inputCount: 1, - getDescription: function (values) { - return ' is one of ' + values[0]; - } - }, - { - name: 'isNotOneOf', - operation: function (input) { - const lhsValue = input[0] !== undefined ? input[0].toString() : ''; - if (input[1]) { - const values = input[1].split(','); - const found = values.some((value) => lhsValue === value.toString().trim()); - - return !found; - } - - return false; - }, - text: 'is not one of', - appliesTo: ["string", "number"], - inputCount: 1, - getDescription: function (values) { - return ' is not one of ' + values[0]; - } - }, - { - name: IS_OLD_KEY, - operation: function () { - return false; - }, - text: 'is older than', - appliesTo: ["number"], - inputCount: 1, - getDescription: function (values) { - return ` is older than ${values[0] || ''} seconds`; - } - }, - { - name: IS_STALE_KEY, - operation: function () { - return false; - }, - text: 'is stale', - appliesTo: ["number"], - inputCount: 0, - getDescription: function () { - return ' is stale'; - } + text: 'is equal to', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' is ' + joinValues(values, 1); } + }, + { + name: 'notEqualTo', + operation: function (input) { + return Number(input[0]) !== Number(input[1]); + }, + text: 'is not equal to', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' is not ' + joinValues(values, 1); + } + }, + { + name: 'greaterThan', + operation: function (input) { + return Number(input[0]) > Number(input[1]); + }, + text: 'is greater than', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' > ' + joinValues(values, 1); + } + }, + { + name: 'lessThan', + operation: function (input) { + return Number(input[0]) < Number(input[1]); + }, + text: 'is less than', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' < ' + joinValues(values, 1); + } + }, + { + name: 'greaterThanOrEq', + operation: function (input) { + return Number(input[0]) >= Number(input[1]); + }, + text: 'is greater than or equal to', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' >= ' + joinValues(values, 1); + } + }, + { + name: 'lessThanOrEq', + operation: function (input) { + return Number(input[0]) <= Number(input[1]); + }, + text: 'is less than or equal to', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' <= ' + joinValues(values, 1); + } + }, + { + name: 'between', + operation: function (input) { + let numberInputs = convertToNumbers(input); + let larger = Math.max(...numberInputs.slice(1, 3)); + let smaller = Math.min(...numberInputs.slice(1, 3)); + + return numberInputs[0] > smaller && numberInputs[0] < larger; + }, + text: 'is between', + appliesTo: ['number'], + inputCount: 2, + getDescription: function (values) { + return ' is between ' + values[0] + ' and ' + values[1]; + } + }, + { + name: 'notBetween', + operation: function (input) { + let numberInputs = convertToNumbers(input); + let larger = Math.max(...numberInputs.slice(1, 3)); + let smaller = Math.min(...numberInputs.slice(1, 3)); + + return numberInputs[0] < smaller || numberInputs[0] > larger; + }, + text: 'is not between', + appliesTo: ['number'], + inputCount: 2, + getDescription: function (values) { + return ' is not between ' + values[0] + ' and ' + values[1]; + } + }, + { + name: 'textContains', + operation: function (input) { + return input[0] && input[1] && input[0].includes(input[1]); + }, + text: 'text contains', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' contains ' + joinValues(values, 1); + } + }, + { + name: 'textDoesNotContain', + operation: function (input) { + return input[0] && input[1] && !input[0].includes(input[1]); + }, + text: 'text does not contain', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' does not contain ' + joinValues(values, 1); + } + }, + { + name: 'textStartsWith', + operation: function (input) { + return input[0].startsWith(input[1]); + }, + text: 'text starts with', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' starts with ' + joinValues(values, 1); + } + }, + { + name: 'textEndsWith', + operation: function (input) { + return input[0].endsWith(input[1]); + }, + text: 'text ends with', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' ends with ' + joinValues(values, 1); + } + }, + { + name: 'textIsExactly', + operation: function (input) { + return input[0] === input[1]; + }, + text: 'text is exactly', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' is exactly ' + joinValues(values, 1); + } + }, + { + name: 'isUndefined', + operation: function (input) { + return typeof input[0] === 'undefined'; + }, + text: 'is undefined', + appliesTo: ['string', 'number', 'enum'], + inputCount: 0, + getDescription: function () { + return ' is undefined'; + } + }, + { + name: 'isDefined', + operation: function (input) { + return typeof input[0] !== 'undefined'; + }, + text: 'is defined', + appliesTo: ['string', 'number', 'enum'], + inputCount: 0, + getDescription: function () { + return ' is defined'; + } + }, + { + name: 'enumValueIs', + operation: function (input) { + let stringInputs = convertToStrings(input); + + return stringInputs[0] === stringInputs[1]; + }, + text: 'is', + appliesTo: ['enum'], + inputCount: 1, + getDescription: function (values) { + return ' is ' + joinValues(values, 1); + } + }, + { + name: 'enumValueIsNot', + operation: function (input) { + let stringInputs = convertToStrings(input); + + return stringInputs[0] !== stringInputs[1]; + }, + text: 'is not', + appliesTo: ['enum'], + inputCount: 1, + getDescription: function (values) { + return ' is not ' + joinValues(values, 1); + } + }, + { + name: 'isOneOf', + operation: function (input) { + const lhsValue = input[0] !== undefined ? input[0].toString() : ''; + if (input[1]) { + const values = input[1].split(','); + + return values.some((value) => lhsValue === value.toString().trim()); + } + + return false; + }, + text: 'is one of', + appliesTo: ['string', 'number'], + inputCount: 1, + getDescription: function (values) { + return ' is one of ' + values[0]; + } + }, + { + name: 'isNotOneOf', + operation: function (input) { + const lhsValue = input[0] !== undefined ? input[0].toString() : ''; + if (input[1]) { + const values = input[1].split(','); + const found = values.some((value) => lhsValue === value.toString().trim()); + + return !found; + } + + return false; + }, + text: 'is not one of', + appliesTo: ['string', 'number'], + inputCount: 1, + getDescription: function (values) { + return ' is not one of ' + values[0]; + } + }, + { + name: IS_OLD_KEY, + operation: function () { + return false; + }, + text: 'is older than', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ` is older than ${values[0] || ''} seconds`; + } + }, + { + name: IS_STALE_KEY, + operation: function () { + return false; + }, + text: 'is stale', + appliesTo: ['number'], + inputCount: 0, + getDescription: function () { + return ' is stale'; + } + } ]; export const INPUT_TYPES = { - 'string': 'text', - 'number': 'number' + string: 'text', + number: 'number' }; export function getOperatorText(operationName, values) { - const found = OPERATIONS.find((operation) => operation.name === operationName); + const found = OPERATIONS.find((operation) => operation.name === operationName); - return found?.getDescription(values) ?? ''; + return found?.getDescription(values) ?? ''; } diff --git a/src/plugins/condition/utils/operationsSpec.js b/src/plugins/condition/utils/operationsSpec.js index b25f9d91a2..d4f6b6e34e 100644 --- a/src/plugins/condition/utils/operationsSpec.js +++ b/src/plugins/condition/utils/operationsSpec.js @@ -20,7 +20,7 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { OPERATIONS } from "./operations"; +import { OPERATIONS } from './operations'; let isOneOfOperation = OPERATIONS.find((operation) => operation.name === 'isOneOf'); let isNotOneOfOperation = OPERATIONS.find((operation) => operation.name === 'isNotOneOf'); let isBetween = OPERATIONS.find((operation) => operation.name === 'between'); @@ -29,114 +29,113 @@ let enumIsOperation = OPERATIONS.find((operation) => operation.name === 'enumVal let enumIsNotOperation = OPERATIONS.find((operation) => operation.name === 'enumValueIsNot'); describe('operations', function () { + it('should evaluate isOneOf to true for number inputs', () => { + const inputs = [45, '5,6,45,8']; + expect(Boolean(isOneOfOperation.operation(inputs))).toBeTrue(); + }); - it('should evaluate isOneOf to true for number inputs', () => { - const inputs = [45, "5,6,45,8"]; - expect(Boolean(isOneOfOperation.operation(inputs))).toBeTrue(); - }); + it('should evaluate isOneOf to true for string inputs', () => { + const inputs = ['45', ' 45, 645, 4,8 ']; + expect(Boolean(isOneOfOperation.operation(inputs))).toBeTrue(); + }); - it('should evaluate isOneOf to true for string inputs', () => { - const inputs = ["45", " 45, 645, 4,8 "]; - expect(Boolean(isOneOfOperation.operation(inputs))).toBeTrue(); - }); + it('should evaluate isNotOneOf to true for number inputs', () => { + const inputs = [45, '5,6,4,8']; + expect(Boolean(isNotOneOfOperation.operation(inputs))).toBeTrue(); + }); - it('should evaluate isNotOneOf to true for number inputs', () => { - const inputs = [45, "5,6,4,8"]; - expect(Boolean(isNotOneOfOperation.operation(inputs))).toBeTrue(); - }); + it('should evaluate isNotOneOf to true for string inputs', () => { + const inputs = ['45', ' 5,645, 4,8 ']; + expect(Boolean(isNotOneOfOperation.operation(inputs))).toBeTrue(); + }); - it('should evaluate isNotOneOf to true for string inputs', () => { - const inputs = ["45", " 5,645, 4,8 "]; - expect(Boolean(isNotOneOfOperation.operation(inputs))).toBeTrue(); - }); + it('should evaluate isOneOf to false for number inputs', () => { + const inputs = [4, '5, 6, 7, 8']; + expect(Boolean(isOneOfOperation.operation(inputs))).toBeFalse(); + }); - it('should evaluate isOneOf to false for number inputs', () => { - const inputs = [4, "5, 6, 7, 8"]; - expect(Boolean(isOneOfOperation.operation(inputs))).toBeFalse(); - }); + it('should evaluate isOneOf to false for string inputs', () => { + const inputs = ['4', '5,645 ,7,8']; + expect(Boolean(isOneOfOperation.operation(inputs))).toBeFalse(); + }); - it('should evaluate isOneOf to false for string inputs', () => { - const inputs = ["4", "5,645 ,7,8"]; - expect(Boolean(isOneOfOperation.operation(inputs))).toBeFalse(); - }); + it('should evaluate isNotOneOf to false for number inputs', () => { + const inputs = [4, '5,4, 7,8']; + expect(Boolean(isNotOneOfOperation.operation(inputs))).toBeFalse(); + }); - it('should evaluate isNotOneOf to false for number inputs', () => { - const inputs = [4, "5,4, 7,8"]; - expect(Boolean(isNotOneOfOperation.operation(inputs))).toBeFalse(); - }); + it('should evaluate isNotOneOf to false for string inputs', () => { + const inputs = ['4', '5,46,4,8']; + expect(Boolean(isNotOneOfOperation.operation(inputs))).toBeFalse(); + }); - it('should evaluate isNotOneOf to false for string inputs', () => { - const inputs = ["4", "5,46,4,8"]; - expect(Boolean(isNotOneOfOperation.operation(inputs))).toBeFalse(); - }); + it('should evaluate isBetween to true', () => { + const inputs = ['4', '3', '89']; + expect(Boolean(isBetween.operation(inputs))).toBeTrue(); + }); - it('should evaluate isBetween to true', () => { - const inputs = ["4", "3", "89"]; - expect(Boolean(isBetween.operation(inputs))).toBeTrue(); - }); + it('should evaluate isNotBetween to true', () => { + const inputs = ['45', '100', '89']; + expect(Boolean(isNotBetween.operation(inputs))).toBeTrue(); + }); - it('should evaluate isNotBetween to true', () => { - const inputs = ["45", "100", "89"]; - expect(Boolean(isNotBetween.operation(inputs))).toBeTrue(); - }); + it('should evaluate isBetween to false', () => { + const inputs = ['4', '100', '89']; + expect(Boolean(isBetween.operation(inputs))).toBeFalse(); + }); - it('should evaluate isBetween to false', () => { - const inputs = ["4", "100", "89"]; - expect(Boolean(isBetween.operation(inputs))).toBeFalse(); - }); + it('should evaluate isNotBetween to false', () => { + const inputs = ['45', '30', '50']; + expect(Boolean(isNotBetween.operation(inputs))).toBeFalse(); + }); - it('should evaluate isNotBetween to false', () => { - const inputs = ["45", "30", "50"]; - expect(Boolean(isNotBetween.operation(inputs))).toBeFalse(); - }); + it('should evaluate enumValueIs to true for number inputs', () => { + const inputs = [1, '1']; + expect(Boolean(enumIsOperation.operation(inputs))).toBeTrue(); + }); - it('should evaluate enumValueIs to true for number inputs', () => { - const inputs = [1, "1"]; - expect(Boolean(enumIsOperation.operation(inputs))).toBeTrue(); - }); + it('should evaluate enumValueIs to true for string inputs', () => { + const inputs = ['45', '45']; + expect(Boolean(enumIsOperation.operation(inputs))).toBeTrue(); + }); - it('should evaluate enumValueIs to true for string inputs', () => { - const inputs = ["45", "45"]; - expect(Boolean(enumIsOperation.operation(inputs))).toBeTrue(); - }); + it('should evaluate enumValueIsNot to true for number inputs', () => { + const inputs = [45, '46']; + expect(Boolean(enumIsNotOperation.operation(inputs))).toBeTrue(); + }); - it('should evaluate enumValueIsNot to true for number inputs', () => { - const inputs = [45, "46"]; - expect(Boolean(enumIsNotOperation.operation(inputs))).toBeTrue(); - }); + it('should evaluate enumValueIsNot to true for string inputs', () => { + const inputs = ['45', '46']; + expect(Boolean(enumIsNotOperation.operation(inputs))).toBeTrue(); + }); - it('should evaluate enumValueIsNot to true for string inputs', () => { - const inputs = ["45", "46"]; - expect(Boolean(enumIsNotOperation.operation(inputs))).toBeTrue(); - }); + it('should evaluate enumValueIs to false for number inputs', () => { + const inputs = [1, '2']; + expect(Boolean(enumIsOperation.operation(inputs))).toBeFalse(); + }); - it('should evaluate enumValueIs to false for number inputs', () => { - const inputs = [1, "2"]; - expect(Boolean(enumIsOperation.operation(inputs))).toBeFalse(); - }); + it('should evaluate enumValueIs to false for string inputs', () => { + const inputs = ['45', '46']; + expect(Boolean(enumIsOperation.operation(inputs))).toBeFalse(); + }); - it('should evaluate enumValueIs to false for string inputs', () => { - const inputs = ["45", "46"]; - expect(Boolean(enumIsOperation.operation(inputs))).toBeFalse(); - }); + it('should evaluate enumValueIsNot to false for number inputs', () => { + const inputs = [45, '45']; + expect(Boolean(enumIsNotOperation.operation(inputs))).toBeFalse(); + }); - it('should evaluate enumValueIsNot to false for number inputs', () => { - const inputs = [45, "45"]; - expect(Boolean(enumIsNotOperation.operation(inputs))).toBeFalse(); - }); + it('should evaluate enumValueIsNot to false for string inputs', () => { + const inputs = ['45', '45']; + expect(Boolean(enumIsNotOperation.operation(inputs))).toBeFalse(); + }); - it('should evaluate enumValueIsNot to false for string inputs', () => { - const inputs = ["45", "45"]; - expect(Boolean(enumIsNotOperation.operation(inputs))).toBeFalse(); - }); + it('should evaluate enumValueIs to false for undefined input', () => { + const inputs = [undefined, '45']; + expect(Boolean(enumIsOperation.operation(inputs))).toBeFalse(); + }); - it('should evaluate enumValueIs to false for undefined input', () => { - const inputs = [undefined, "45"]; - expect(Boolean(enumIsOperation.operation(inputs))).toBeFalse(); - }); - - it('should evaluate enumValueIsNot to true for undefined input', () => { - const inputs = [undefined, "45"]; - expect(Boolean(enumIsNotOperation.operation(inputs))).toBeTrue(); - }); + it('should evaluate enumValueIsNot to true for undefined input', () => { + const inputs = [undefined, '45']; + expect(Boolean(enumIsNotOperation.operation(inputs))).toBeTrue(); + }); }); diff --git a/src/plugins/condition/utils/styleUtils.js b/src/plugins/condition/utils/styleUtils.js index 5346e3d029..7c046fda8a 100644 --- a/src/plugins/condition/utils/styleUtils.js +++ b/src/plugins/condition/utils/styleUtils.js @@ -24,172 +24,179 @@ import isEmpty from 'lodash/isEmpty'; const NONE_VALUE = '__no_value'; const styleProps = { - backgroundColor: { - svgProperty: 'fill', - noneValue: NONE_VALUE, - applicableForType: type => { - return !type ? true : (type === 'text-view' - || type === 'telemetry-view' - || type === 'box-view' - || type === 'ellipse-view' - || type === 'subobject-view'); - } - }, - border: { - svgProperty: 'stroke', - noneValue: NONE_VALUE, - applicableForType: type => { - return !type ? true : (type === 'text-view' - || type === 'telemetry-view' - || type === 'box-view' - || type === 'ellipse-view' - || type === 'image-view' - || type === 'line-view' - || type === 'subobject-view'); - } - }, - color: { - svgProperty: 'color', - noneValue: NONE_VALUE, - applicableForType: type => { - return !type ? true : (type === 'text-view' - || type === 'telemetry-view' - || type === 'subobject-view'); - } - }, - imageUrl: { - svgProperty: 'url', - noneValue: '', - applicableForType: type => { - return !type ? false : type === 'image-view'; - } + backgroundColor: { + svgProperty: 'fill', + noneValue: NONE_VALUE, + applicableForType: (type) => { + return !type + ? true + : type === 'text-view' || + type === 'telemetry-view' || + type === 'box-view' || + type === 'ellipse-view' || + type === 'subobject-view'; } + }, + border: { + svgProperty: 'stroke', + noneValue: NONE_VALUE, + applicableForType: (type) => { + return !type + ? true + : type === 'text-view' || + type === 'telemetry-view' || + type === 'box-view' || + type === 'ellipse-view' || + type === 'image-view' || + type === 'line-view' || + type === 'subobject-view'; + } + }, + color: { + svgProperty: 'color', + noneValue: NONE_VALUE, + applicableForType: (type) => { + return !type + ? true + : type === 'text-view' || type === 'telemetry-view' || type === 'subobject-view'; + } + }, + imageUrl: { + svgProperty: 'url', + noneValue: '', + applicableForType: (type) => { + return !type ? false : type === 'image-view'; + } + } }; function aggregateStyleValues(accumulator, currentStyle) { - const styleKeys = Object.keys(currentStyle); - const properties = Object.keys(styleProps); - properties.forEach((property) => { - if (!accumulator[property]) { - accumulator[property] = []; - } + const styleKeys = Object.keys(currentStyle); + const properties = Object.keys(styleProps); + properties.forEach((property) => { + if (!accumulator[property]) { + accumulator[property] = []; + } - const found = styleKeys.find(key => key === property); - if (found) { - accumulator[property].push(currentStyle[found]); - } - }); + const found = styleKeys.find((key) => key === property); + if (found) { + accumulator[property].push(currentStyle[found]); + } + }); - return accumulator; + return accumulator; } function getStaticStyleForItem(domainObject, id) { - let domainObjectStyles = domainObject && domainObject.configuration && domainObject.configuration.objectStyles; - if (domainObjectStyles) { - if (id) { - if (domainObjectStyles[id] && domainObjectStyles[id].staticStyle) { - return domainObjectStyles[id].staticStyle.style; - } - } else if (domainObjectStyles.staticStyle) { - return domainObjectStyles.staticStyle.style; - } + let domainObjectStyles = + domainObject && domainObject.configuration && domainObject.configuration.objectStyles; + if (domainObjectStyles) { + if (id) { + if (domainObjectStyles[id] && domainObjectStyles[id].staticStyle) { + return domainObjectStyles[id].staticStyle.style; + } + } else if (domainObjectStyles.staticStyle) { + return domainObjectStyles.staticStyle.style; } + } } // Returns a union of styles used by multiple items. // Styles that are common to all items but don't have the same value are added to the mixedStyles list export function getConsolidatedStyleValues(multipleItemStyles) { - let aggregatedStyleValues = multipleItemStyles.reduce(aggregateStyleValues, {}); + let aggregatedStyleValues = multipleItemStyles.reduce(aggregateStyleValues, {}); - let styleValues = {}; - let mixedStyles = []; - const properties = Object.keys(styleProps); - properties.forEach((property) => { - const values = aggregatedStyleValues[property]; - if (values && values.length) { - if (values.every(value => value === values[0])) { - styleValues[property] = values[0]; - } else { - styleValues[property] = ''; - mixedStyles.push(property); - } - } - }); + let styleValues = {}; + let mixedStyles = []; + const properties = Object.keys(styleProps); + properties.forEach((property) => { + const values = aggregatedStyleValues[property]; + if (values && values.length) { + if (values.every((value) => value === values[0])) { + styleValues[property] = values[0]; + } else { + styleValues[property] = ''; + mixedStyles.push(property); + } + } + }); - return { - styles: styleValues, - mixedStyles - }; + return { + styles: styleValues, + mixedStyles + }; } export function getConditionalStyleForItem(domainObject, id) { - let domainObjectStyles = domainObject && domainObject.configuration && domainObject.configuration.objectStyles; - if (domainObjectStyles) { - if (id) { - if (domainObjectStyles[id] && domainObjectStyles[id].conditionSetIdentifier) { - return domainObjectStyles[id].styles; - } - } else if (domainObjectStyles.conditionSetIdentifier) { - return domainObjectStyles.styles; - } + let domainObjectStyles = + domainObject && domainObject.configuration && domainObject.configuration.objectStyles; + if (domainObjectStyles) { + if (id) { + if (domainObjectStyles[id] && domainObjectStyles[id].conditionSetIdentifier) { + return domainObjectStyles[id].styles; + } + } else if (domainObjectStyles.conditionSetIdentifier) { + return domainObjectStyles.styles; } + } } export function getConditionSetIdentifierForItem(domainObject, id) { - let domainObjectStyles = domainObject && domainObject.configuration && domainObject.configuration.objectStyles; - if (domainObjectStyles) { - if (id) { - if (domainObjectStyles[id] && domainObjectStyles[id].conditionSetIdentifier) { - return domainObjectStyles[id].conditionSetIdentifier; - } - } else if (domainObjectStyles.conditionSetIdentifier) { - return domainObjectStyles.conditionSetIdentifier; - } + let domainObjectStyles = + domainObject && domainObject.configuration && domainObject.configuration.objectStyles; + if (domainObjectStyles) { + if (id) { + if (domainObjectStyles[id] && domainObjectStyles[id].conditionSetIdentifier) { + return domainObjectStyles[id].conditionSetIdentifier; + } + } else if (domainObjectStyles.conditionSetIdentifier) { + return domainObjectStyles.conditionSetIdentifier; } + } } //Returns either existing static styles or uses SVG defaults if available export function getApplicableStylesForItem(domainObject, item) { - const type = item && item.type; - const id = item && item.id; - let style = {}; + const type = item && item.type; + const id = item && item.id; + let style = {}; - let staticStyle = getStaticStyleForItem(domainObject, id); + let staticStyle = getStaticStyleForItem(domainObject, id); - const properties = Object.keys(styleProps); - properties.forEach(property => { - const styleProp = styleProps[property]; - if (styleProp.applicableForType(type)) { - let defaultValue; - if (staticStyle) { - defaultValue = staticStyle[property]; - } else if (item) { - defaultValue = item[styleProp.svgProperty]; - } + const properties = Object.keys(styleProps); + properties.forEach((property) => { + const styleProp = styleProps[property]; + if (styleProp.applicableForType(type)) { + let defaultValue; + if (staticStyle) { + defaultValue = staticStyle[property]; + } else if (item) { + defaultValue = item[styleProp.svgProperty]; + } - style[property] = defaultValue === undefined ? styleProp.noneValue : defaultValue; - } - }); + style[property] = defaultValue === undefined ? styleProp.noneValue : defaultValue; + } + }); - return style; + return style; } export function getStylesWithoutNoneValue(style) { - if (isEmpty(style) || !style) { - return; + if (isEmpty(style) || !style) { + return; + } + + let styleObj = {}; + const keys = Object.keys(style); + keys.forEach((key) => { + if (typeof style[key] === 'string') { + if (style[key].indexOf('__no_value') > -1) { + style[key] = ''; + } else { + styleObj[key] = style[key]; + } } + }); - let styleObj = {}; - const keys = Object.keys(style); - keys.forEach(key => { - if ((typeof style[key] === 'string')) { - if (style[key].indexOf('__no_value') > -1) { - style[key] = ''; - } else { - styleObj[key] = style[key]; - } - } - }); - - return styleObj; + return styleObj; } diff --git a/src/plugins/condition/utils/time.js b/src/plugins/condition/utils/time.js index 177f36924f..4c8028eb80 100644 --- a/src/plugins/condition/utils/time.js +++ b/src/plugins/condition/utils/time.js @@ -21,57 +21,57 @@ *****************************************************************************/ function updateLatestTimeStamp(timestamp, timeSystems) { - let latest = {}; + let latest = {}; - timeSystems.forEach(timeSystem => { - latest[timeSystem.key] = timestamp[timeSystem.key]; - }); + timeSystems.forEach((timeSystem) => { + latest[timeSystem.key] = timestamp[timeSystem.key]; + }); - return latest; + return latest; } export function getLatestTimestamp( - currentTimestamp, - compareTimestamp, - timeSystems, - currentTimeSystem + currentTimestamp, + compareTimestamp, + timeSystems, + currentTimeSystem ) { - let latest = { ...currentTimestamp }; - const compare = { ...compareTimestamp }; - const key = currentTimeSystem.key; + let latest = { ...currentTimestamp }; + const compare = { ...compareTimestamp }; + const key = currentTimeSystem.key; - if (!latest || !latest[key]) { - latest = updateLatestTimeStamp(compare, timeSystems); - } + if (!latest || !latest[key]) { + latest = updateLatestTimeStamp(compare, timeSystems); + } - if (compare[key] > latest[key]) { - latest = updateLatestTimeStamp(compare, timeSystems); - } + if (compare[key] > latest[key]) { + latest = updateLatestTimeStamp(compare, timeSystems); + } - return latest; + return latest; } export function checkIfOld(callback, timeout) { - let oldCheckTimer = setTimeout(() => { + let oldCheckTimer = setTimeout(() => { + clearTimeout(oldCheckTimer); + callback(); + }, timeout); + + return { + update: (data) => { + if (oldCheckTimer) { clearTimeout(oldCheckTimer); - callback(); - }, timeout); + } - return { - update: (data) => { - if (oldCheckTimer) { - clearTimeout(oldCheckTimer); - } - - oldCheckTimer = setTimeout(() => { - clearTimeout(oldCheckTimer); - callback(data); - }, timeout); - }, - clear: () => { - if (oldCheckTimer) { - clearTimeout(oldCheckTimer); - } - } - }; + oldCheckTimer = setTimeout(() => { + clearTimeout(oldCheckTimer); + callback(data); + }, timeout); + }, + clear: () => { + if (oldCheckTimer) { + clearTimeout(oldCheckTimer); + } + } + }; } diff --git a/src/plugins/condition/utils/timeSpec.js b/src/plugins/condition/utils/timeSpec.js index 5b00976371..33ec1e42a7 100644 --- a/src/plugins/condition/utils/timeSpec.js +++ b/src/plugins/condition/utils/timeSpec.js @@ -19,47 +19,46 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import { checkIfOld } from "./time"; +import { checkIfOld } from './time'; describe('time related utils', () => { - let subscription; - let mockListener; + let subscription; + let mockListener; - beforeEach(() => { - mockListener = jasmine.createSpy('listener'); - subscription = checkIfOld(mockListener, 100); + beforeEach(() => { + mockListener = jasmine.createSpy('listener'); + subscription = checkIfOld(mockListener, 100); + }); + + describe('check if old', () => { + it('should call listeners when old', (done) => { + setTimeout(() => { + expect(mockListener).toHaveBeenCalled(); + done(); + }, 200); }); - describe('check if old', () => { - it('should call listeners when old', (done) => { - setTimeout(() => { - expect(mockListener).toHaveBeenCalled(); - done(); - }, 200); - }); + it('should update the subscription', (done) => { + function updated() { + setTimeout(() => { + expect(mockListener).not.toHaveBeenCalled(); + done(); + }, 50); + } - it('should update the subscription', (done) => { - function updated() { - setTimeout(() => { - expect(mockListener).not.toHaveBeenCalled(); - done(); - }, 50); - } - - setTimeout(() => { - subscription.update(); - updated(); - }, 50); - }); - - it('should clear the subscription', (done) => { - subscription.clear(); - - setTimeout(() => { - expect(mockListener).not.toHaveBeenCalled(); - done(); - }, 200); - }); + setTimeout(() => { + subscription.update(); + updated(); + }, 50); }); + it('should clear the subscription', (done) => { + subscription.clear(); + + setTimeout(() => { + expect(mockListener).not.toHaveBeenCalled(); + done(); + }, 200); + }); + }); }); diff --git a/src/plugins/conditionWidget/ConditionWidgetViewProvider.js b/src/plugins/conditionWidget/ConditionWidgetViewProvider.js index 2e7f113ff1..9224c1ea05 100644 --- a/src/plugins/conditionWidget/ConditionWidgetViewProvider.js +++ b/src/plugins/conditionWidget/ConditionWidgetViewProvider.js @@ -24,41 +24,41 @@ import ConditionWidgetComponent from './components/ConditionWidget.vue'; import Vue from 'vue'; export default function ConditionWidget(openmct) { - return { - key: 'conditionWidget', - name: 'Condition Widget', - cssClass: 'icon-condition-widget', - canView: function (domainObject) { - return domainObject.type === 'conditionWidget'; - }, - canEdit: function (domainObject) { - return domainObject.type === 'conditionWidget'; - }, - view: function (domainObject) { - let component; + return { + key: 'conditionWidget', + name: 'Condition Widget', + cssClass: 'icon-condition-widget', + canView: function (domainObject) { + return domainObject.type === 'conditionWidget'; + }, + canEdit: function (domainObject) { + return domainObject.type === 'conditionWidget'; + }, + view: function (domainObject) { + let component; - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - ConditionWidgetComponent: ConditionWidgetComponent - }, - provide: { - openmct, - domainObject - }, - template: '' - }); - }, - destroy: function (element) { - component.$destroy(); - component = undefined; - } - }; + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + ConditionWidgetComponent: ConditionWidgetComponent + }, + provide: { + openmct, + domainObject + }, + template: '' + }); }, - priority: function () { - return 1; + destroy: function (element) { + component.$destroy(); + component = undefined; } - }; + }; + }, + priority: function () { + return 1; + } + }; } diff --git a/src/plugins/conditionWidget/components/ConditionWidget.vue b/src/plugins/conditionWidget/components/ConditionWidget.vue index 7f088ebef7..86d157baa4 100644 --- a/src/plugins/conditionWidget/components/ConditionWidget.vue +++ b/src/plugins/conditionWidget/components/ConditionWidget.vue @@ -21,107 +21,102 @@ --> diff --git a/src/plugins/conditionWidget/components/condition-widget.scss b/src/plugins/conditionWidget/components/condition-widget.scss index bc727d4c32..ef4b72903e 100644 --- a/src/plugins/conditionWidget/components/condition-widget.scss +++ b/src/plugins/conditionWidget/components/condition-widget.scss @@ -21,54 +21,59 @@ *****************************************************************************/ .c-condition-widget { - $shdwSize: 3px; - @include userSelectNone(); - background-color: rgba($colorBodyFg, 0.1); // Give a little presence if the user hasn't defined a fill color - border-radius: $basicCr; - border: 1px solid transparent; - display: block; - max-width: max-content; + $shdwSize: 3px; + @include userSelectNone(); + background-color: rgba( + $colorBodyFg, + 0.1 + ); // Give a little presence if the user hasn't defined a fill color + border-radius: $basicCr; + border: 1px solid transparent; + display: block; + max-width: max-content; - a { - display: block; - color: inherit; - } + a { + display: block; + color: inherit; + } } .c-condition-widget__label { - // Either a
    or an tag - padding: $interiorMargin $interiorMargin * 1.5; - text-align: center; - white-space: normal; + // Either a
    or an tag + padding: $interiorMargin $interiorMargin * 1.5; + text-align: center; + white-space: normal; } // Make Condition Widget expand when in a hidden frame Layout context // For both static and Flexible Layouts .c-so-view--conditionWidget.c-so-view--no-frame { - .c-condition-widget { - @include abs(); - max-width: unset; + .c-condition-widget { + @include abs(); + max-width: unset; - &__label-wrapper { - @include abs(); - display: flex; - align-items: center; - justify-content: center; - } + &__label-wrapper { + @include abs(); + display: flex; + align-items: center; + justify-content: center; } + } - .c-so-view__frame-controls { display: none; } + .c-so-view__frame-controls { + display: none; + } } // Add some margin when a Condition Widget is in a Flexible Layout .c-fl .c-so-view--no-frame .c-condition-widget { - @include abs(1px); + @include abs(1px); } // When the widget is in the main view, center it in the space .l-shell__main-container > * > .c-condition-widget { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); } diff --git a/src/plugins/conditionWidget/plugin.js b/src/plugins/conditionWidget/plugin.js index 1e1b640f3e..25999f18ed 100644 --- a/src/plugins/conditionWidget/plugin.js +++ b/src/plugins/conditionWidget/plugin.js @@ -23,40 +23,39 @@ import ConditionWidgetViewProvider from './ConditionWidgetViewProvider.js'; export default function plugin() { - return function install(openmct) { - openmct.objectViews.addProvider(new ConditionWidgetViewProvider(openmct)); + return function install(openmct) { + openmct.objectViews.addProvider(new ConditionWidgetViewProvider(openmct)); - openmct.types.addType('conditionWidget', { - key: 'conditionWidget', - name: "Condition Widget", - description: "A button that can be used on its own, or dynamically styled with a Condition Set.", - creatable: true, - cssClass: 'icon-condition-widget', - initialize(domainObject) { - domainObject.configuration = {}; - domainObject.label = 'Condition Widget'; - domainObject.conditionalLabel = ''; - domainObject.url = ''; - }, - form: [ - { - "key": "label", - "name": "Label", - "control": "textfield", - property: [ - "label" - ], - "required": true, - "cssClass": "l-input" - }, - { - "key": "url", - "name": "URL", - "control": "textfield", - "required": false, - "cssClass": "l-input-lg" - } - ] - }); - }; + openmct.types.addType('conditionWidget', { + key: 'conditionWidget', + name: 'Condition Widget', + description: + 'A button that can be used on its own, or dynamically styled with a Condition Set.', + creatable: true, + cssClass: 'icon-condition-widget', + initialize(domainObject) { + domainObject.configuration = {}; + domainObject.label = 'Condition Widget'; + domainObject.conditionalLabel = ''; + domainObject.url = ''; + }, + form: [ + { + key: 'label', + name: 'Label', + control: 'textfield', + property: ['label'], + required: true, + cssClass: 'l-input' + }, + { + key: 'url', + name: 'URL', + control: 'textfield', + required: false, + cssClass: 'l-input-lg' + } + ] + }); + }; } diff --git a/src/plugins/conditionWidget/pluginSpec.js b/src/plugins/conditionWidget/pluginSpec.js index 033516f138..d1291cf22d 100644 --- a/src/plugins/conditionWidget/pluginSpec.js +++ b/src/plugins/conditionWidget/pluginSpec.js @@ -1,195 +1,200 @@ -import { createOpenMct, resetApplicationState } from "utils/testing"; -import ConditionWidgetPlugin from "./plugin"; +import { createOpenMct, resetApplicationState } from 'utils/testing'; +import ConditionWidgetPlugin from './plugin'; import Vue from 'vue'; describe('the plugin', function () { - const CONDITION_WIDGET_KEY = 'conditionWidget'; - let objectDef; - let element; - let child; - let openmct; - let mockConditionObjectDefinition; - let mockConditionObject; - let mockConditionObjectPath; + const CONDITION_WIDGET_KEY = 'conditionWidget'; + let objectDef; + let element; + let child; + let openmct; + let mockConditionObjectDefinition; + let mockConditionObject; + let mockConditionObjectPath; - beforeEach((done) => { - mockConditionObjectPath = [ + beforeEach((done) => { + mockConditionObjectPath = [ + { + name: 'mock folder', + type: 'fake-folder', + identifier: { + key: 'mock-folder', + namespace: '' + } + }, + { + name: 'mock parent folder', + type: 'conditionWidget', + identifier: { + key: 'mock-parent-folder', + namespace: '' + } + } + ]; + + mockConditionObjectDefinition = { + name: 'Condition Widget', + key: 'conditionWidget', + creatable: true + }; + + mockConditionObject = { + conditionWidget: { + identifier: { + namespace: '', + key: 'condition-widget-object' + }, + url: 'https://nasa.github.io/openmct/', + label: 'Foo Widget', + type: 'conditionWidget', + composition: [] + }, + telemetry: { + identifier: { + namespace: '', + key: 'telemetry-object' + }, + type: 'test-telemetry-object', + name: 'Test Telemetry Object', + telemetry: { + values: [ { - name: 'mock folder', - type: 'fake-folder', - identifier: { - key: 'mock-folder', - namespace: '' - } + key: 'name', + name: 'Name', + format: 'string' }, { - name: 'mock parent folder', - type: 'conditionWidget', - identifier: { - key: 'mock-parent-folder', - namespace: '' - } - } - ]; - - mockConditionObjectDefinition = { - name: 'Condition Widget', - key: 'conditionWidget', - creatable: true - }; - - mockConditionObject = { - "conditionWidget": { - "identifier": { - "namespace": "", - "key": "condition-widget-object" - }, - "url": "https://nasa.github.io/openmct/", - "label": "Foo Widget", - "type": "conditionWidget", - "composition": [] + key: 'utc', + name: 'Time', + format: 'utc', + hints: { + domain: 1 + } }, - "telemetry": { - "identifier": { - "namespace": "", - "key": "telemetry-object" - }, - "type": "test-telemetry-object", - "name": "Test Telemetry Object", - "telemetry": { - "values": [ - { - "key": "name", - "name": "Name", - "format": "string" - }, - { - "key": "utc", - "name": "Time", - "format": "utc", - "hints": { - "domain": 1 - } - }, - { - "name": "Some attribute 1", - "key": "some-key-1", - "hints": { - "range": 1 - } - }, - { - "name": "Some attribute 2", - "key": "some-key-2" - } - ] - } + { + name: 'Some attribute 1', + key: 'some-key-1', + hints: { + range: 1 + } + }, + { + name: 'Some attribute 2', + key: 'some-key-2' } - }; + ] + } + } + }; - const timeSystem = { - timeSystemKey: 'utc', - bounds: { - start: 1597160002854, - end: 1597181232854 - } - }; + const timeSystem = { + timeSystemKey: 'utc', + bounds: { + start: 1597160002854, + end: 1597181232854 + } + }; - openmct = createOpenMct(timeSystem); - openmct.install(new ConditionWidgetPlugin()); + openmct = createOpenMct(timeSystem); + openmct.install(new ConditionWidgetPlugin()); - objectDef = openmct.types.get('conditionWidget').definition; + objectDef = openmct.types.get('conditionWidget').definition; - 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); + 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(); + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('defines a conditionWidget object type with the correct key', () => { + expect(objectDef.key).toEqual(mockConditionObjectDefinition.key); + }); + + describe('the conditionWidget object', () => { + it('is creatable', () => { + expect(objectDef.creatable).toEqual(mockConditionObjectDefinition.creatable); + }); + }); + + describe('the view', () => { + let conditionWidgetView; + let testViewObject; + + beforeEach(() => { + testViewObject = { + id: 'test-object', + identifier: { + key: 'test-object', + namespace: '' + }, + type: 'conditionWidget' + }; + + const applicableViews = openmct.objectViews.get(testViewObject, mockConditionObjectPath); + conditionWidgetView = applicableViews.find( + (viewProvider) => viewProvider.key === 'conditionWidget' + ); + let view = conditionWidgetView.view(testViewObject, element); + view.show(child, true); + + return Vue.nextTick(); }); - afterEach(() => { - return resetApplicationState(openmct); + it('provides a view', () => { + expect(conditionWidgetView).toBeDefined(); }); + }); - it('defines a conditionWidget object type with the correct key', () => { - expect(objectDef.key).toEqual(mockConditionObjectDefinition.key); - }); + it('should have a view provider for condition widget objects', () => { + const applicableViews = openmct.objectViews.get(mockConditionObject[CONDITION_WIDGET_KEY], []); - describe('the conditionWidget object', () => { - it('is creatable', () => { - expect(objectDef.creatable).toEqual(mockConditionObjectDefinition.creatable); - }); - }); + const conditionWidgetViewProvider = applicableViews.find( + (viewProvider) => viewProvider.key === CONDITION_WIDGET_KEY + ); - describe('the view', () => { - let conditionWidgetView; - let testViewObject; + expect(applicableViews.length).toEqual(1); + expect(conditionWidgetViewProvider).toBeDefined(); + }); - beforeEach(() => { - testViewObject = { - id: "test-object", - identifier: { - key: "test-object", - namespace: '' - }, - type: "conditionWidget" - }; + it('should render a view with a URL and label', async () => { + const urlParent = document.createElement('div'); + const urlChild = document.createElement('div'); + urlParent.appendChild(urlChild); - const applicableViews = openmct.objectViews.get(testViewObject, mockConditionObjectPath); - conditionWidgetView = applicableViews.find((viewProvider) => viewProvider.key === 'conditionWidget'); - let view = conditionWidgetView.view(testViewObject, element); - view.show(child, true); + const applicableViews = openmct.objectViews.get(mockConditionObject[CONDITION_WIDGET_KEY], []); - return Vue.nextTick(); - }); + const conditionWidgetViewProvider = applicableViews.find( + (viewProvider) => viewProvider.key === CONDITION_WIDGET_KEY + ); - it('provides a view', () => { - expect(conditionWidgetView).toBeDefined(); - }); - }); + const conditionWidgetView = conditionWidgetViewProvider.view( + mockConditionObject[CONDITION_WIDGET_KEY], + [mockConditionObject[CONDITION_WIDGET_KEY]] + ); + conditionWidgetView.show(urlChild); - it("should have a view provider for condition widget objects", () => { - const applicableViews = openmct.objectViews.get(mockConditionObject[CONDITION_WIDGET_KEY], []); + await Vue.nextTick(); - const conditionWidgetViewProvider = applicableViews.find( - (viewProvider) => viewProvider.key === CONDITION_WIDGET_KEY - ); + const domainUrl = mockConditionObject[CONDITION_WIDGET_KEY].url; + expect(urlParent.innerHTML).toContain(`'); - it("should render a view with a URL and label", async () => { - const urlParent = document.createElement('div'); - const urlChild = document.createElement('div'); - urlParent.appendChild(urlChild); - - const applicableViews = openmct.objectViews.get(mockConditionObject[CONDITION_WIDGET_KEY], []); - - const conditionWidgetViewProvider = applicableViews.find( - (viewProvider) => viewProvider.key === CONDITION_WIDGET_KEY - ); - - const conditionWidgetView = conditionWidgetViewProvider.view(mockConditionObject[CONDITION_WIDGET_KEY], [mockConditionObject[CONDITION_WIDGET_KEY]]); - conditionWidgetView.show(urlChild); - - await Vue.nextTick(); - - const domainUrl = mockConditionObject[CONDITION_WIDGET_KEY].url; - expect(urlParent.innerHTML).toContain(`'); - - const conditionWidgetLabel = conditionWidgetRender.querySelector('.c-condition-widget__label'); - expect(conditionWidgetLabel).toBeDefined(); - const domainLabel = mockConditionObject[CONDITION_WIDGET_KEY].label; - expect(conditionWidgetLabel.textContent).toContain(domainLabel); - }); + const conditionWidgetLabel = conditionWidgetRender.querySelector('.c-condition-widget__label'); + expect(conditionWidgetLabel).toBeDefined(); + const domainLabel = mockConditionObject[CONDITION_WIDGET_KEY].label; + expect(conditionWidgetLabel.textContent).toContain(domainLabel); + }); }); diff --git a/src/plugins/defaultRootName/plugin.js b/src/plugins/defaultRootName/plugin.js index 693f39eb20..cc35c7e40a 100644 --- a/src/plugins/defaultRootName/plugin.js +++ b/src/plugins/defaultRootName/plugin.js @@ -22,8 +22,8 @@ import RootObjectProvider from '../../api/objects/RootObjectProvider.js'; export default function (name) { - return function (openmct) { - let rootObjectProvider = new RootObjectProvider(); - rootObjectProvider.updateName(name); - }; + return function (openmct) { + let rootObjectProvider = new RootObjectProvider(); + rootObjectProvider.updateName(name); + }; } diff --git a/src/plugins/defaultRootName/pluginSpec.js b/src/plugins/defaultRootName/pluginSpec.js index ae05b96fb6..6d84a48618 100644 --- a/src/plugins/defaultRootName/pluginSpec.js +++ b/src/plugins/defaultRootName/pluginSpec.js @@ -19,10 +19,7 @@ * 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 { createOpenMct, resetApplicationState } from 'utils/testing'; const OLD_ROOT_NAME = 'Open MCT'; const NEW_ROOT_NAME = 'not_a_root'; @@ -30,48 +27,46 @@ const NEW_ROOT_NAME = 'not_a_root'; let openmct; describe('the DefaultRootNamePlugin', () => { - describe('without DefaultRootNamePlugin', () => { - beforeEach((done) => { - openmct = createOpenMct(); + describe('without DefaultRootNamePlugin', () => { + beforeEach((done) => { + openmct = createOpenMct(); - openmct.on('start', done); - openmct.startHeadless(); - }); - - afterEach(() => { - return resetApplicationState(openmct); - }); - - it('does not changes root name', (done) => { - openmct.objects.getRoot() - .then(object => { - expect(object.name).toEqual(OLD_ROOT_NAME); - - done(); - }); - }); + openmct.on('start', done); + openmct.startHeadless(); }); - describe('with DefaultRootNamePlugin', () => { - beforeEach((done) => { - openmct = createOpenMct(); - - openmct.install(openmct.plugins.DefaultRootName(NEW_ROOT_NAME)); - openmct.on('start', done); - openmct.startHeadless(); - }); - - afterEach(() => { - return resetApplicationState(openmct); - }); - - it('changes root name', (done) => { - openmct.objects.getRoot() - .then(object => { - expect(object.name).toEqual(NEW_ROOT_NAME); - - done(); - }); - }); + afterEach(() => { + return resetApplicationState(openmct); }); + + it('does not changes root name', (done) => { + openmct.objects.getRoot().then((object) => { + expect(object.name).toEqual(OLD_ROOT_NAME); + + done(); + }); + }); + }); + + describe('with DefaultRootNamePlugin', () => { + beforeEach((done) => { + openmct = createOpenMct(); + + openmct.install(openmct.plugins.DefaultRootName(NEW_ROOT_NAME)); + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('changes root name', (done) => { + openmct.objects.getRoot().then((object) => { + expect(object.name).toEqual(NEW_ROOT_NAME); + + done(); + }); + }); + }); }); diff --git a/src/plugins/displayLayout/AlphanumericFormatViewProvider.js b/src/plugins/displayLayout/AlphanumericFormatViewProvider.js index 7d48254a08..4a1d66ab54 100644 --- a/src/plugins/displayLayout/AlphanumericFormatViewProvider.js +++ b/src/plugins/displayLayout/AlphanumericFormatViewProvider.js @@ -25,74 +25,76 @@ import AlphanumericFormat from './components/AlphanumericFormat.vue'; import Vue from 'vue'; class AlphanumericFormatView { - constructor(openmct, domainObject, objectPath) { - this.openmct = openmct; - this.domainObject = domainObject; - this.objectPath = objectPath; - this.component = undefined; + constructor(openmct, domainObject, objectPath) { + this.openmct = openmct; + this.domainObject = domainObject; + this.objectPath = objectPath; + this.component = undefined; + } + + show(element) { + this.component = new Vue({ + el: element, + name: 'AlphanumericFormat', + components: { + AlphanumericFormat + }, + provide: { + openmct: this.openmct, + objectPath: this.objectPath, + currentView: this + }, + template: '' + }); + } + + getViewContext() { + if (this.component) { + return {}; } - show(element) { - this.component = new Vue({ - el: element, - name: 'AlphanumericFormat', - components: { - AlphanumericFormat - }, - provide: { - openmct: this.openmct, - objectPath: this.objectPath, - currentView: this - }, - template: '' - }); - } + return this.component.$refs.alphanumericFormat.getViewContext(); + } - getViewContext() { - if (this.component) { - return {}; - } + priority() { + return 1; + } - return this.component.$refs.alphanumericFormat.getViewContext(); - } - - priority() { - return 1; - } - - destroy() { - this.component.$destroy(); - this.component = undefined; - } + destroy() { + this.component.$destroy(); + this.component = undefined; + } } export default function AlphanumericFormatViewProvider(openmct, options) { - function isTelemetryObject(selectionPath) { - let selectedObject = selectionPath[0].context.item; - let parentObject = selectionPath[1].context.item; - let selectedLayoutItem = selectionPath[0].context.layoutItem; + function isTelemetryObject(selectionPath) { + let selectedObject = selectionPath[0].context.item; + let parentObject = selectionPath[1].context.item; + let selectedLayoutItem = selectionPath[0].context.layoutItem; - return parentObject - && parentObject.type === 'layout' - && selectedObject - && selectedLayoutItem - && selectedLayoutItem.type === 'telemetry-view' - && openmct.telemetry.isTelemetryObject(selectedObject) - && !options.showAsView.includes(selectedObject.type); + return ( + parentObject && + parentObject.type === 'layout' && + selectedObject && + selectedLayoutItem && + selectedLayoutItem.type === 'telemetry-view' && + openmct.telemetry.isTelemetryObject(selectedObject) && + !options.showAsView.includes(selectedObject.type) + ); + } + + return { + key: 'alphanumeric-format', + name: 'Format', + canView: function (selection) { + if (selection.length === 0 || selection[0].length === 1) { + return false; + } + + return selection.every(isTelemetryObject); + }, + view: function (domainObject, objectPath) { + return new AlphanumericFormatView(openmct, domainObject, objectPath); } - - return { - key: 'alphanumeric-format', - name: 'Format', - canView: function (selection) { - if (selection.length === 0 || selection[0].length === 1) { - return false; - } - - return selection.every(isTelemetryObject); - }, - view: function (domainObject, objectPath) { - return new AlphanumericFormatView(openmct, domainObject, objectPath); - } - }; + }; } diff --git a/src/plugins/displayLayout/CustomStringFormatter.js b/src/plugins/displayLayout/CustomStringFormatter.js index 445cfd1edf..7e49280541 100644 --- a/src/plugins/displayLayout/CustomStringFormatter.js +++ b/src/plugins/displayLayout/CustomStringFormatter.js @@ -1,38 +1,38 @@ import printj from 'printj'; export default class CustomStringFormatter { - constructor(openmct, valueMetadata, itemFormat) { - this.openmct = openmct; + constructor(openmct, valueMetadata, itemFormat) { + this.openmct = openmct; - this.itemFormat = itemFormat; - this.valueMetadata = valueMetadata; + this.itemFormat = itemFormat; + this.valueMetadata = valueMetadata; + } + + format(datum) { + if (!this.itemFormat) { + return; } - format(datum) { - if (!this.itemFormat) { - return; - } - - if (!this.itemFormat.startsWith('&')) { - return printj.sprintf(this.itemFormat, datum[this.valueMetadata.key]); - } - - try { - const key = this.itemFormat.slice(1); - const customFormatter = this.openmct.telemetry.getFormatter(key); - if (!customFormatter) { - throw new Error('Custom Formatter not found'); - } - - return customFormatter.format(datum[this.valueMetadata.key]); - } catch (e) { - console.error(e); - - return datum[this.valueMetadata.key]; - } + if (!this.itemFormat.startsWith('&')) { + return printj.sprintf(this.itemFormat, datum[this.valueMetadata.key]); } - setFormat(itemFormat) { - this.itemFormat = itemFormat; + try { + const key = this.itemFormat.slice(1); + const customFormatter = this.openmct.telemetry.getFormatter(key); + if (!customFormatter) { + throw new Error('Custom Formatter not found'); + } + + return customFormatter.format(datum[this.valueMetadata.key]); + } catch (e) { + console.error(e); + + return datum[this.valueMetadata.key]; } + } + + setFormat(itemFormat) { + this.itemFormat = itemFormat; + } } diff --git a/src/plugins/displayLayout/CustomStringFormatterSpec.js b/src/plugins/displayLayout/CustomStringFormatterSpec.js index e4be84bbcd..83a2803865 100644 --- a/src/plugins/displayLayout/CustomStringFormatterSpec.js +++ b/src/plugins/displayLayout/CustomStringFormatterSpec.js @@ -2,80 +2,80 @@ import CustomStringFormatter from './CustomStringFormatter'; import { createOpenMct, resetApplicationState } from 'utils/testing'; const CUSTOM_FORMATS = [ - { - key: 'sclk', - format: (value) => 2 * value - }, - { - key: 'lts', - format: (value) => 3 * value - } + { + key: 'sclk', + format: (value) => 2 * value + }, + { + key: 'lts', + format: (value) => 3 * value + } ]; const valueMetadata = { - key: "sin", - name: "Sine", - unit: "Hz", - formatString: "%0.2f", - hints: { - range: 1, - priority: 3 - }, - source: "sin" + key: 'sin', + name: 'Sine', + unit: 'Hz', + formatString: '%0.2f', + hints: { + range: 1, + priority: 3 + }, + source: 'sin' }; const datum = { - name: "1 Sine Wave Generator", - utc: 1603930354000, - yesterday: 1603843954000, - sin: 0.587785209686822, - cos: -0.8090170253297632 + name: '1 Sine Wave Generator', + utc: 1603930354000, + yesterday: 1603843954000, + sin: 0.587785209686822, + cos: -0.8090170253297632 }; describe('CustomStringFormatter', function () { - let element; - let child; - let openmct; - let customStringFormatter; + let element; + let child; + let openmct; + let customStringFormatter; - beforeEach((done) => { - openmct = createOpenMct(); + beforeEach((done) => { + openmct = createOpenMct(); - element = document.createElement('div'); - child = document.createElement('div'); - element.appendChild(child); - CUSTOM_FORMATS.forEach((formatter) => { - openmct.telemetry.addFormat(formatter); - }); - openmct.on('start', done); - openmct.startHeadless(); - - customStringFormatter = new CustomStringFormatter(openmct, valueMetadata); + element = document.createElement('div'); + child = document.createElement('div'); + element.appendChild(child); + CUSTOM_FORMATS.forEach((formatter) => { + openmct.telemetry.addFormat(formatter); }); + openmct.on('start', done); + openmct.startHeadless(); - afterEach(() => { - return resetApplicationState(openmct); - }); + customStringFormatter = new CustomStringFormatter(openmct, valueMetadata); + }); - it('adds custom format sclk', () => { - const format = openmct.telemetry.getFormatter('sclk'); - expect(format.key).toEqual('sclk'); - }); + afterEach(() => { + return resetApplicationState(openmct); + }); - it('adds custom format lts', () => { - const format = openmct.telemetry.getFormatter('lts'); - expect(format.key).toEqual('lts'); - }); + it('adds custom format sclk', () => { + const format = openmct.telemetry.getFormatter('sclk'); + expect(format.key).toEqual('sclk'); + }); - it('returns correct value for custom format sclk', () => { - customStringFormatter.setFormat('&sclk'); - const value = customStringFormatter.format(datum, valueMetadata); - expect(datum.sin * 2).toEqual(value); - }); + it('adds custom format lts', () => { + const format = openmct.telemetry.getFormatter('lts'); + expect(format.key).toEqual('lts'); + }); - it('returns correct value for custom format lts', () => { - customStringFormatter.setFormat('<s'); - const value = customStringFormatter.format(datum, valueMetadata); - expect(datum.sin * 3).toEqual(value); - }); + it('returns correct value for custom format sclk', () => { + customStringFormatter.setFormat('&sclk'); + const value = customStringFormatter.format(datum, valueMetadata); + expect(datum.sin * 2).toEqual(value); + }); + + it('returns correct value for custom format lts', () => { + customStringFormatter.setFormat('<s'); + const value = customStringFormatter.format(datum, valueMetadata); + expect(datum.sin * 3).toEqual(value); + }); }); diff --git a/src/plugins/displayLayout/DisplayLayoutToolbar.js b/src/plugins/displayLayout/DisplayLayoutToolbar.js index 396538894b..5e7d4f315f 100644 --- a/src/plugins/displayLayout/DisplayLayoutToolbar.js +++ b/src/plugins/displayLayout/DisplayLayoutToolbar.js @@ -21,823 +21,835 @@ *****************************************************************************/ define(['lodash'], function (_) { - function DisplayLayoutToolbar(openmct) { - return { - name: "Display Layout Toolbar", - key: "layout", - description: "A toolbar for objects inside a display layout.", - forSelection: function (selection) { - if (!selection || selection.length === 0) { - return false; - } - - let selectionPath = selection[0]; - let selectedObject = selectionPath[0]; - let selectedParent = selectionPath[1]; - - // Apply the layout toolbar if the selected object is inside a layout, or the main layout is selected. - return (selectedParent && selectedParent.context.item && selectedParent.context.item.type === 'layout') - || (selectedObject.context.item && selectedObject.context.item.type === 'layout'); - }, - toolbar: function (selectedObjects) { - const DIALOG_FORM = { - 'text': { - title: "Text Element Properties", - sections: [ - { - rows: [ - { - key: "text", - control: "textfield", - name: "Text", - required: true, - cssClass: "l-input-lg" - } - ] - } - ] - }, - 'image': { - title: "Image Properties", - sections: [ - { - rows: [ - { - key: "url", - control: "textfield", - name: "Image URL", - cssClass: "l-input-lg", - required: true - } - ] - } - ] - } - }; - const VIEW_TYPES = { - 'telemetry-view': { - value: 'telemetry-view', - name: 'Alphanumeric', - class: 'icon-alphanumeric' - }, - 'telemetry.plot.overlay': { - value: 'telemetry.plot.overlay', - name: 'Overlay Plot', - class: "icon-plot-overlay" - }, - 'telemetry.plot.stacked': { - value: "telemetry.plot.stacked", - name: "Stacked Plot", - class: "icon-plot-stacked" - }, - 'table': { - value: 'table', - name: 'Table', - class: 'icon-tabular-scrolling' - } - }; - const APPLICABLE_VIEWS = { - 'telemetry-view': [ - VIEW_TYPES['telemetry.plot.overlay'], - VIEW_TYPES['telemetry.plot.stacked'], - VIEW_TYPES.table - ], - 'telemetry.plot.overlay': [ - VIEW_TYPES['telemetry.plot.stacked'], - VIEW_TYPES.table, - VIEW_TYPES['telemetry-view'] - ], - 'telemetry.plot.stacked': [ - VIEW_TYPES['telemetry.plot.overlay'], - VIEW_TYPES.table, - VIEW_TYPES['telemetry-view'] - ], - 'table': [ - VIEW_TYPES['telemetry.plot.overlay'], - VIEW_TYPES['telemetry.plot.stacked'], - VIEW_TYPES['telemetry-view'] - ], - 'telemetry-view-multi': [ - VIEW_TYPES['telemetry.plot.overlay'], - VIEW_TYPES['telemetry.plot.stacked'], - VIEW_TYPES.table - ], - 'telemetry.plot.overlay-multi': [ - VIEW_TYPES['telemetry.plot.stacked'] - ] - }; - - function getPath(selectionPath) { - return `configuration.items[${selectionPath[0].context.index}]`; - } - - function getAllOfType(selection, specificType) { - return selection.filter(selectionPath => { - let type = selectionPath[0].context.layoutItem.type; - - return type === specificType; - }); - } - - function getAllTypes(selection) { - return selection.filter(selectionPath => { - let type = selectionPath[0].context.layoutItem.type; - - return type === 'text-view' - || type === 'telemetry-view' - || type === 'box-view' - || type === 'ellipse-view' - || type === 'image-view' - || type === 'line-view' - || type === 'subobject-view'; - }); - } - - function getAddButton(selection, selectionPath) { - if (selection.length === 1) { - selectionPath = selectionPath || selection[0]; - - return { - control: "menu", - domainObject: selectionPath[0].context.item, - method: function (option) { - let name = option.name.toLowerCase(); - let form = DIALOG_FORM[name]; - if (form) { - showForm(form, name, selectionPath); - } else { - selectionPath[0].context.addElement(name); - } - }, - key: "add", - icon: "icon-plus", - label: "Add", - options: [ - { - "name": "Box", - "class": "icon-box-round-corners" - }, - { - "name": "Ellipse", - "class": "icon-circle" - }, - { - "name": "Line", - "class": "icon-line-horz" - }, - { - "name": "Text", - "class": "icon-font" - }, - { - "name": "Image", - "class": "icon-image" - } - ] - }; - } - } - - function getToggleFrameButton(selectedParent, selection) { - return { - control: "toggle-button", - domainObject: selectedParent, - applicableSelectedItems: selection.filter(selectionPath => - selectionPath[0].context.layoutItem.type === 'subobject-view' - ), - property: function (selectionPath) { - return getPath(selectionPath) + ".hasFrame"; - }, - options: [ - { - value: false, - icon: 'icon-frame-hide', - title: "Frame visible", - label: 'Hide frame' - }, - { - value: true, - icon: 'icon-frame-show', - title: "Frame hidden", - label: 'Show frame' - } - ] - }; - } - - function getRemoveButton(selectedParent, selectionPath, selection) { - return { - control: "button", - domainObject: selectedParent, - icon: "icon-trash", - title: "Delete the selected object", - method: function () { - let removeItem = selectionPath[1].context.removeItem; - let prompt = openmct.overlays.dialog({ - iconClass: 'alert', - message: `Warning! This action will remove this item from the Display Layout. Do you want to continue?`, - buttons: [ - { - label: 'OK', - emphasis: 'true', - callback: function () { - removeItem(getAllTypes(selection)); - prompt.dismiss(); - } - }, - { - label: 'Cancel', - callback: function () { - prompt.dismiss(); - } - } - ] - }); - } - }; - } - - function getStackOrder(selectedParent, selectionPath) { - return { - control: "menu", - domainObject: selectedParent, - icon: "icon-layers", - title: "Move the selected object above or below other objects", - options: [ - { - name: "Move to Top", - value: "top", - class: "icon-arrow-double-up" - }, - { - name: "Move Up", - value: "up", - class: "icon-arrow-up" - }, - { - name: "Move Down", - value: "down", - class: "icon-arrow-down" - }, - { - name: "Move to Bottom", - value: "bottom", - class: "icon-arrow-double-down" - } - ], - method: function (option) { - selectionPath[1].context.orderItem(option.value, getAllTypes(selectedObjects)); - } - }; - } - - function getXInput(selectedParent, selection) { - if (selection.length === 1) { - return { - control: "input", - type: "number", - domainObject: selectedParent, - applicableSelectedItems: getAllTypes(selection), - property: function (selectionPath) { - return getPath(selectionPath) + ".x"; - }, - label: "X:", - title: "X position" - }; - } - } - - function getYInput(selectedParent, selection) { - if (selection.length === 1) { - return { - control: "input", - type: "number", - domainObject: selectedParent, - applicableSelectedItems: getAllTypes(selection), - property: function (selectionPath) { - return getPath(selectionPath) + ".y"; - }, - label: "Y:", - title: "Y position" - }; - } - } - - function getWidthInput(selectedParent, selection) { - if (selection.length === 1) { - return { - control: 'input', - type: 'number', - domainObject: selectedParent, - applicableSelectedItems: getAllTypes(selection), - property: function (selectionPath) { - return getPath(selectionPath) + ".width"; - }, - label: 'W:', - title: 'Resize object width' - }; - } - } - - function getHeightInput(selectedParent, selection) { - if (selection.length === 1) { - return { - control: 'input', - type: 'number', - domainObject: selectedParent, - applicableSelectedItems: getAllTypes(selection), - property: function (selectionPath) { - return getPath(selectionPath) + ".height"; - }, - label: 'H:', - title: 'Resize object height' - }; - } - } - - function getX2Input(selectedParent, selection) { - if (selection.length === 1) { - return { - control: "input", - type: "number", - domainObject: selectedParent, - applicableSelectedItems: selection.filter(selectionPath => { - return selectionPath[0].context.layoutItem.type === 'line-view'; - }), - property: function (selectionPath) { - return getPath(selectionPath) + ".x2"; - }, - label: "X2:", - title: "X2 position" - }; - } - } - - function getY2Input(selectedParent, selection) { - if (selection.length === 1) { - return { - control: "input", - type: "number", - domainObject: selectedParent, - applicableSelectedItems: selection.filter(selectionPath => { - return selectionPath[0].context.layoutItem.type === 'line-view'; - }), - property: function (selectionPath) { - return getPath(selectionPath) + ".y2"; - }, - label: "Y2:", - title: "Y2 position" - }; - } - } - - function getTextButton(selectedParent, selection) { - return { - control: "button", - domainObject: selectedParent, - applicableSelectedItems: selection.filter(selectionPath => { - return selectionPath[0].context.layoutItem.type === 'text-view'; - }), - property: function (selectionPath) { - return getPath(selectionPath); - }, - icon: "icon-pencil", - title: "Edit text properties", - label: "Edit text", - dialog: DIALOG_FORM.text - }; - } - - function getTelemetryValueMenu(selectionPath, selection) { - if (selection.length === 1) { - return { - control: "select-menu", - domainObject: selectionPath[1].context.item, - applicableSelectedItems: selection.filter(path => { - return path[0].context.layoutItem.type === 'telemetry-view'; - }), - property: function (path) { - return getPath(path) + ".value"; - }, - title: "Set value", - options: openmct.telemetry.getMetadata(selectionPath[0].context.item).values().map(value => { - return { - name: value.name, - value: value.key - }; - }) - }; - } - } - - function getDisplayModeMenu(selectedParent, selection) { - if (selection.length === 1) { - return { - control: "select-menu", - domainObject: selectedParent, - applicableSelectedItems: selection.filter(selectionPath => { - return selectionPath[0].context.layoutItem.type === 'telemetry-view'; - }), - property: function (selectionPath) { - return getPath(selectionPath) + ".displayMode"; - }, - title: "Set display mode", - options: [ - { - name: 'Label + Value', - value: 'all' - }, - { - name: "Label only", - value: "label" - }, - { - name: "Value only", - value: "value" - } - ] - }; - } - } - - function getDuplicateButton(selectedParent, selectionPath, selection) { - return { - control: "button", - domainObject: selectedParent, - icon: "icon-duplicate", - title: "Duplicate the selected object", - method: function () { - let duplicateItem = selectionPath[1].context.duplicateItem; - - duplicateItem(selection); - } - }; - } - - function getPropertyFromPath(object, path) { - let splitPath = path.split('.'); - let property = Object.assign({}, object); - - while (splitPath.length && property) { - property = property[splitPath.shift()]; - } - - return property; - } - - function areAllViews(type, path, selection) { - let allTelemetry = true; - - selection.forEach(selectedItem => { - let selectedItemContext = selectedItem[0].context; - - if (getPropertyFromPath(selectedItemContext, path) !== type) { - allTelemetry = false; - } - }); - - return allTelemetry; - } - - function getToggleUnitsButton(selectedParent, selection) { - let applicableItems = getAllOfType(selection, 'telemetry-view'); - applicableItems = unitsOnly(applicableItems); - if (!applicableItems.length) { - return; - } - - return { - control: "toggle-button", - domainObject: selectedParent, - applicableSelectedItems: applicableItems, - property: function (selectionPath) { - return getPath(selectionPath) + '.showUnits'; - }, - options: [ - { - value: true, - icon: 'icon-eye-open', - title: "Show units", - label: "Show units" - }, - { - value: false, - icon: 'icon-eye-disabled', - title: "Hide units", - label: "Hide units" - } - ] - }; - } - - function unitsOnly(items) { - let results = items.filter((item) => { - let currentItem = item[0]; - let metadata = openmct.telemetry.getMetadata(currentItem.context.item); - if (!metadata) { - return false; - } - - let hasUnits = metadata - .valueMetadatas - .filter((metadatum) => metadatum.unit) - .length; - - return hasUnits > 0; - }); - - return results; - } - - function getViewSwitcherMenu(selectedParent, selectionPath, selection) { - if (selection.length === 1) { - let displayLayoutContext = selectionPath[1].context; - let selectedItemContext = selectionPath[0].context; - let selectedItemType = selectedItemContext.item.type; - - if (selectedItemContext.layoutItem.type === 'telemetry-view') { - selectedItemType = 'telemetry-view'; - } - - let viewOptions = APPLICABLE_VIEWS[selectedItemType]; - - if (viewOptions) { - return { - control: "menu", - 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); - } - }; - } - } else if (selection.length > 1) { - if (areAllViews('telemetry-view', 'layoutItem.type', selection)) { - let displayLayoutContext = selectionPath[1].context; - - return { - control: "menu", - domainObject: selectedParent, - icon: "icon-object", - title: "Merge into a telemetry table or plot", - label: "View type", - options: APPLICABLE_VIEWS['telemetry-view-multi'], - method: function (option) { - displayLayoutContext.mergeMultipleTelemetryViews(selection, option.value); - } - }; - } else if (areAllViews('telemetry.plot.overlay', 'item.type', selection)) { - let displayLayoutContext = selectionPath[1].context; - - return { - control: "menu", - domainObject: selectedParent, - icon: "icon-object", - title: "Merge into a stacked plot", - options: APPLICABLE_VIEWS['telemetry.plot.overlay-multi'], - method: function (option) { - displayLayoutContext.mergeMultipleOverlayPlots(selection, option.value); - } - }; - } - } - } - - function getToggleGridButton(selection, selectionPath) { - const ICON_GRID_SHOW = 'icon-grid-on'; - const ICON_GRID_HIDE = 'icon-grid-off'; - - let displayLayoutContext; - - if (selection.length === 1 && selectionPath === undefined) { - displayLayoutContext = selection[0][0].context; - } else { - displayLayoutContext = selectionPath[1].context; - } - - return { - control: "button", - domainObject: displayLayoutContext.item, - icon: ICON_GRID_SHOW, - method: function () { - displayLayoutContext.toggleGrid(); - - this.icon = this.icon === ICON_GRID_SHOW - ? ICON_GRID_HIDE - : ICON_GRID_SHOW; - }, - secondary: true - }; - } - - function getSeparator() { - return { - control: "separator" - }; - } - - function isMainLayoutSelected(selectionPath) { - let selectedObject = selectionPath[0].context.item; - - return selectedObject && selectedObject.type === 'layout' - && !selectionPath[0].context.layoutItem; - } - - function showForm(formStructure, name, selectionPath) { - openmct.forms.showForm(formStructure) - .then(changes => { - selectionPath[0].context.addElement(name, changes); - }); - } - - if (isMainLayoutSelected(selectedObjects[0])) { - return [ - getToggleGridButton(selectedObjects), - getAddButton(selectedObjects) - ]; - } - - let toolbar = { - 'add-menu': [], - 'text': [], - 'url': [], - 'viewSwitcher': [], - 'toggle-frame': [], - 'display-mode': [], - 'telemetry-value': [], - 'style': [], - 'unit-toggle': [], - 'position': [], - 'duplicate': [], - 'remove': [], - 'toggle-grid': [] - }; - - selectedObjects.forEach(selectionPath => { - let selectedParent = selectionPath[1].context.item; - let layoutItem = selectionPath[0].context.layoutItem; - - if (!layoutItem || selectedParent.locked) { - return; - } - - if (layoutItem.type === 'subobject-view') { - if (toolbar['add-menu'].length === 0 && selectionPath[0].context.item.type === 'layout') { - toolbar['add-menu'] = [getAddButton(selectedObjects, selectionPath)]; - } - - if (toolbar['toggle-frame'].length === 0) { - toolbar['toggle-frame'] = [getToggleFrameButton(selectedParent, selectedObjects)]; - } - - if (toolbar.position.length === 0) { - toolbar.position = [ - getStackOrder(selectedParent, selectionPath), - getSeparator(), - getXInput(selectedParent, selectedObjects), - getYInput(selectedParent, selectedObjects), - getHeightInput(selectedParent, selectedObjects), - getWidthInput(selectedParent, selectedObjects) - ]; - } - - if (toolbar.remove.length === 0) { - toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; - } - - if (toolbar.viewSwitcher.length === 0) { - toolbar.viewSwitcher = [getViewSwitcherMenu(selectedParent, selectionPath, selectedObjects)]; - } - } else if (layoutItem.type === 'telemetry-view') { - if (toolbar['display-mode'].length === 0) { - toolbar['display-mode'] = [getDisplayModeMenu(selectedParent, selectedObjects)]; - } - - if (toolbar['telemetry-value'].length === 0) { - 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), - getWidthInput(selectedParent, selectedObjects) - ]; - } - - if (toolbar.remove.length === 0) { - toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; - } - - if (toolbar.viewSwitcher.length === 0) { - toolbar.viewSwitcher = [getViewSwitcherMenu(selectedParent, selectionPath, selectedObjects)]; - } - } 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), - getWidthInput(selectedParent, selectedObjects) - ]; - } - - if (toolbar.text.length === 0) { - toolbar.text = [getTextButton(selectedParent, selectedObjects)]; - } - - if (toolbar.remove.length === 0) { - toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; - } - } else if (layoutItem.type === 'box-view' || layoutItem.type === 'ellipse-view') { - if (toolbar.position.length === 0) { - toolbar.position = [ - getStackOrder(selectedParent, selectionPath), - getSeparator(), - getXInput(selectedParent, selectedObjects), - getYInput(selectedParent, selectedObjects), - getHeightInput(selectedParent, selectedObjects), - getWidthInput(selectedParent, selectedObjects) - ]; - } - - if (toolbar.remove.length === 0) { - toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; - } - } else if (layoutItem.type === 'image-view') { - if (toolbar.position.length === 0) { - toolbar.position = [ - getStackOrder(selectedParent, selectionPath), - getSeparator(), - getXInput(selectedParent, selectedObjects), - getYInput(selectedParent, selectedObjects), - getHeightInput(selectedParent, selectedObjects), - getWidthInput(selectedParent, selectedObjects) - ]; - } - - if (toolbar.remove.length === 0) { - toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; - } - } else if (layoutItem.type === 'line-view') { - if (toolbar.position.length === 0) { - toolbar.position = [ - getStackOrder(selectedParent, selectionPath), - getSeparator(), - getXInput(selectedParent, selectedObjects), - getYInput(selectedParent, selectedObjects), - getX2Input(selectedParent, selectedObjects), - getY2Input(selectedParent, selectedObjects) - ]; - } - - if (toolbar.remove.length === 0) { - toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; - } - } - - if (toolbar.duplicate.length === 0) { - toolbar.duplicate = [getDuplicateButton(selectedParent, selectionPath, selectedObjects)]; - } - - if (toolbar['toggle-grid'].length === 0) { - toolbar['toggle-grid'] = [getToggleGridButton(selectedObjects, selectionPath)]; - } - }); - - let toolbarArray = Object.values(toolbar); - - return _.flatten(toolbarArray.reduce((accumulator, group, index) => { - group = group.filter(control => control !== undefined); - - if (group.length > 0) { - accumulator.push(group); - - if (index < toolbarArray.length - 1) { - accumulator.push(getSeparator()); - } - } - - return accumulator; - }, [])); - } + function DisplayLayoutToolbar(openmct) { + return { + name: 'Display Layout Toolbar', + key: 'layout', + description: 'A toolbar for objects inside a display layout.', + forSelection: function (selection) { + if (!selection || selection.length === 0) { + return false; + } + + let selectionPath = selection[0]; + let selectedObject = selectionPath[0]; + let selectedParent = selectionPath[1]; + + // Apply the layout toolbar if the selected object is inside a layout, or the main layout is selected. + return ( + (selectedParent && + selectedParent.context.item && + selectedParent.context.item.type === 'layout') || + (selectedObject.context.item && selectedObject.context.item.type === 'layout') + ); + }, + toolbar: function (selectedObjects) { + const DIALOG_FORM = { + text: { + title: 'Text Element Properties', + sections: [ + { + rows: [ + { + key: 'text', + control: 'textfield', + name: 'Text', + required: true, + cssClass: 'l-input-lg' + } + ] + } + ] + }, + image: { + title: 'Image Properties', + sections: [ + { + rows: [ + { + key: 'url', + control: 'textfield', + name: 'Image URL', + cssClass: 'l-input-lg', + required: true + } + ] + } + ] + } + }; + const VIEW_TYPES = { + 'telemetry-view': { + value: 'telemetry-view', + name: 'Alphanumeric', + class: 'icon-alphanumeric' + }, + 'telemetry.plot.overlay': { + value: 'telemetry.plot.overlay', + name: 'Overlay Plot', + class: 'icon-plot-overlay' + }, + 'telemetry.plot.stacked': { + value: 'telemetry.plot.stacked', + name: 'Stacked Plot', + class: 'icon-plot-stacked' + }, + table: { + value: 'table', + name: 'Table', + class: 'icon-tabular-scrolling' + } + }; + const APPLICABLE_VIEWS = { + 'telemetry-view': [ + VIEW_TYPES['telemetry.plot.overlay'], + VIEW_TYPES['telemetry.plot.stacked'], + VIEW_TYPES.table + ], + 'telemetry.plot.overlay': [ + VIEW_TYPES['telemetry.plot.stacked'], + VIEW_TYPES.table, + VIEW_TYPES['telemetry-view'] + ], + 'telemetry.plot.stacked': [ + VIEW_TYPES['telemetry.plot.overlay'], + VIEW_TYPES.table, + VIEW_TYPES['telemetry-view'] + ], + table: [ + VIEW_TYPES['telemetry.plot.overlay'], + VIEW_TYPES['telemetry.plot.stacked'], + VIEW_TYPES['telemetry-view'] + ], + 'telemetry-view-multi': [ + VIEW_TYPES['telemetry.plot.overlay'], + VIEW_TYPES['telemetry.plot.stacked'], + VIEW_TYPES.table + ], + 'telemetry.plot.overlay-multi': [VIEW_TYPES['telemetry.plot.stacked']] }; - } - return DisplayLayoutToolbar; + function getPath(selectionPath) { + return `configuration.items[${selectionPath[0].context.index}]`; + } + + function getAllOfType(selection, specificType) { + return selection.filter((selectionPath) => { + let type = selectionPath[0].context.layoutItem.type; + + return type === specificType; + }); + } + + function getAllTypes(selection) { + return selection.filter((selectionPath) => { + let type = selectionPath[0].context.layoutItem.type; + + return ( + type === 'text-view' || + type === 'telemetry-view' || + type === 'box-view' || + type === 'ellipse-view' || + type === 'image-view' || + type === 'line-view' || + type === 'subobject-view' + ); + }); + } + + function getAddButton(selection, selectionPath) { + if (selection.length === 1) { + selectionPath = selectionPath || selection[0]; + + return { + control: 'menu', + domainObject: selectionPath[0].context.item, + method: function (option) { + let name = option.name.toLowerCase(); + let form = DIALOG_FORM[name]; + if (form) { + showForm(form, name, selectionPath); + } else { + selectionPath[0].context.addElement(name); + } + }, + key: 'add', + icon: 'icon-plus', + label: 'Add', + options: [ + { + name: 'Box', + class: 'icon-box-round-corners' + }, + { + name: 'Ellipse', + class: 'icon-circle' + }, + { + name: 'Line', + class: 'icon-line-horz' + }, + { + name: 'Text', + class: 'icon-font' + }, + { + name: 'Image', + class: 'icon-image' + } + ] + }; + } + } + + function getToggleFrameButton(selectedParent, selection) { + return { + control: 'toggle-button', + domainObject: selectedParent, + applicableSelectedItems: selection.filter( + (selectionPath) => selectionPath[0].context.layoutItem.type === 'subobject-view' + ), + property: function (selectionPath) { + return getPath(selectionPath) + '.hasFrame'; + }, + options: [ + { + value: false, + icon: 'icon-frame-hide', + title: 'Frame visible', + label: 'Hide frame' + }, + { + value: true, + icon: 'icon-frame-show', + title: 'Frame hidden', + label: 'Show frame' + } + ] + }; + } + + function getRemoveButton(selectedParent, selectionPath, selection) { + return { + control: 'button', + domainObject: selectedParent, + icon: 'icon-trash', + title: 'Delete the selected object', + method: function () { + let removeItem = selectionPath[1].context.removeItem; + let prompt = openmct.overlays.dialog({ + iconClass: 'alert', + message: `Warning! This action will remove this item from the Display Layout. Do you want to continue?`, + buttons: [ + { + label: 'OK', + emphasis: 'true', + callback: function () { + removeItem(getAllTypes(selection)); + prompt.dismiss(); + } + }, + { + label: 'Cancel', + callback: function () { + prompt.dismiss(); + } + } + ] + }); + } + }; + } + + function getStackOrder(selectedParent, selectionPath) { + return { + control: 'menu', + domainObject: selectedParent, + icon: 'icon-layers', + title: 'Move the selected object above or below other objects', + options: [ + { + name: 'Move to Top', + value: 'top', + class: 'icon-arrow-double-up' + }, + { + name: 'Move Up', + value: 'up', + class: 'icon-arrow-up' + }, + { + name: 'Move Down', + value: 'down', + class: 'icon-arrow-down' + }, + { + name: 'Move to Bottom', + value: 'bottom', + class: 'icon-arrow-double-down' + } + ], + method: function (option) { + selectionPath[1].context.orderItem(option.value, getAllTypes(selectedObjects)); + } + }; + } + + function getXInput(selectedParent, selection) { + if (selection.length === 1) { + return { + control: 'input', + type: 'number', + domainObject: selectedParent, + applicableSelectedItems: getAllTypes(selection), + property: function (selectionPath) { + return getPath(selectionPath) + '.x'; + }, + label: 'X:', + title: 'X position' + }; + } + } + + function getYInput(selectedParent, selection) { + if (selection.length === 1) { + return { + control: 'input', + type: 'number', + domainObject: selectedParent, + applicableSelectedItems: getAllTypes(selection), + property: function (selectionPath) { + return getPath(selectionPath) + '.y'; + }, + label: 'Y:', + title: 'Y position' + }; + } + } + + function getWidthInput(selectedParent, selection) { + if (selection.length === 1) { + return { + control: 'input', + type: 'number', + domainObject: selectedParent, + applicableSelectedItems: getAllTypes(selection), + property: function (selectionPath) { + return getPath(selectionPath) + '.width'; + }, + label: 'W:', + title: 'Resize object width' + }; + } + } + + function getHeightInput(selectedParent, selection) { + if (selection.length === 1) { + return { + control: 'input', + type: 'number', + domainObject: selectedParent, + applicableSelectedItems: getAllTypes(selection), + property: function (selectionPath) { + return getPath(selectionPath) + '.height'; + }, + label: 'H:', + title: 'Resize object height' + }; + } + } + + function getX2Input(selectedParent, selection) { + if (selection.length === 1) { + return { + control: 'input', + type: 'number', + domainObject: selectedParent, + applicableSelectedItems: selection.filter((selectionPath) => { + return selectionPath[0].context.layoutItem.type === 'line-view'; + }), + property: function (selectionPath) { + return getPath(selectionPath) + '.x2'; + }, + label: 'X2:', + title: 'X2 position' + }; + } + } + + function getY2Input(selectedParent, selection) { + if (selection.length === 1) { + return { + control: 'input', + type: 'number', + domainObject: selectedParent, + applicableSelectedItems: selection.filter((selectionPath) => { + return selectionPath[0].context.layoutItem.type === 'line-view'; + }), + property: function (selectionPath) { + return getPath(selectionPath) + '.y2'; + }, + label: 'Y2:', + title: 'Y2 position' + }; + } + } + + function getTextButton(selectedParent, selection) { + return { + control: 'button', + domainObject: selectedParent, + applicableSelectedItems: selection.filter((selectionPath) => { + return selectionPath[0].context.layoutItem.type === 'text-view'; + }), + property: function (selectionPath) { + return getPath(selectionPath); + }, + icon: 'icon-pencil', + title: 'Edit text properties', + label: 'Edit text', + dialog: DIALOG_FORM.text + }; + } + + function getTelemetryValueMenu(selectionPath, selection) { + if (selection.length === 1) { + return { + control: 'select-menu', + domainObject: selectionPath[1].context.item, + applicableSelectedItems: selection.filter((path) => { + return path[0].context.layoutItem.type === 'telemetry-view'; + }), + property: function (path) { + return getPath(path) + '.value'; + }, + title: 'Set value', + options: openmct.telemetry + .getMetadata(selectionPath[0].context.item) + .values() + .map((value) => { + return { + name: value.name, + value: value.key + }; + }) + }; + } + } + + function getDisplayModeMenu(selectedParent, selection) { + if (selection.length === 1) { + return { + control: 'select-menu', + domainObject: selectedParent, + applicableSelectedItems: selection.filter((selectionPath) => { + return selectionPath[0].context.layoutItem.type === 'telemetry-view'; + }), + property: function (selectionPath) { + return getPath(selectionPath) + '.displayMode'; + }, + title: 'Set display mode', + options: [ + { + name: 'Label + Value', + value: 'all' + }, + { + name: 'Label only', + value: 'label' + }, + { + name: 'Value only', + value: 'value' + } + ] + }; + } + } + + function getDuplicateButton(selectedParent, selectionPath, selection) { + return { + control: 'button', + domainObject: selectedParent, + icon: 'icon-duplicate', + title: 'Duplicate the selected object', + method: function () { + let duplicateItem = selectionPath[1].context.duplicateItem; + + duplicateItem(selection); + } + }; + } + + function getPropertyFromPath(object, path) { + let splitPath = path.split('.'); + let property = Object.assign({}, object); + + while (splitPath.length && property) { + property = property[splitPath.shift()]; + } + + return property; + } + + function areAllViews(type, path, selection) { + let allTelemetry = true; + + selection.forEach((selectedItem) => { + let selectedItemContext = selectedItem[0].context; + + if (getPropertyFromPath(selectedItemContext, path) !== type) { + allTelemetry = false; + } + }); + + return allTelemetry; + } + + function getToggleUnitsButton(selectedParent, selection) { + let applicableItems = getAllOfType(selection, 'telemetry-view'); + applicableItems = unitsOnly(applicableItems); + if (!applicableItems.length) { + return; + } + + return { + control: 'toggle-button', + domainObject: selectedParent, + applicableSelectedItems: applicableItems, + property: function (selectionPath) { + return getPath(selectionPath) + '.showUnits'; + }, + options: [ + { + value: true, + icon: 'icon-eye-open', + title: 'Show units', + label: 'Show units' + }, + { + value: false, + icon: 'icon-eye-disabled', + title: 'Hide units', + label: 'Hide units' + } + ] + }; + } + + function unitsOnly(items) { + let results = items.filter((item) => { + let currentItem = item[0]; + let metadata = openmct.telemetry.getMetadata(currentItem.context.item); + if (!metadata) { + return false; + } + + let hasUnits = metadata.valueMetadatas.filter((metadatum) => metadatum.unit).length; + + return hasUnits > 0; + }); + + return results; + } + + function getViewSwitcherMenu(selectedParent, selectionPath, selection) { + if (selection.length === 1) { + let displayLayoutContext = selectionPath[1].context; + let selectedItemContext = selectionPath[0].context; + let selectedItemType = selectedItemContext.item.type; + + if (selectedItemContext.layoutItem.type === 'telemetry-view') { + selectedItemType = 'telemetry-view'; + } + + let viewOptions = APPLICABLE_VIEWS[selectedItemType]; + + if (viewOptions) { + return { + control: 'menu', + 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); + } + }; + } + } else if (selection.length > 1) { + if (areAllViews('telemetry-view', 'layoutItem.type', selection)) { + let displayLayoutContext = selectionPath[1].context; + + return { + control: 'menu', + domainObject: selectedParent, + icon: 'icon-object', + title: 'Merge into a telemetry table or plot', + label: 'View type', + options: APPLICABLE_VIEWS['telemetry-view-multi'], + method: function (option) { + displayLayoutContext.mergeMultipleTelemetryViews(selection, option.value); + } + }; + } else if (areAllViews('telemetry.plot.overlay', 'item.type', selection)) { + let displayLayoutContext = selectionPath[1].context; + + return { + control: 'menu', + domainObject: selectedParent, + icon: 'icon-object', + title: 'Merge into a stacked plot', + options: APPLICABLE_VIEWS['telemetry.plot.overlay-multi'], + method: function (option) { + displayLayoutContext.mergeMultipleOverlayPlots(selection, option.value); + } + }; + } + } + } + + function getToggleGridButton(selection, selectionPath) { + const ICON_GRID_SHOW = 'icon-grid-on'; + const ICON_GRID_HIDE = 'icon-grid-off'; + + let displayLayoutContext; + + if (selection.length === 1 && selectionPath === undefined) { + displayLayoutContext = selection[0][0].context; + } else { + displayLayoutContext = selectionPath[1].context; + } + + return { + control: 'button', + domainObject: displayLayoutContext.item, + icon: ICON_GRID_SHOW, + method: function () { + displayLayoutContext.toggleGrid(); + + this.icon = this.icon === ICON_GRID_SHOW ? ICON_GRID_HIDE : ICON_GRID_SHOW; + }, + secondary: true + }; + } + + function getSeparator() { + return { + control: 'separator' + }; + } + + function isMainLayoutSelected(selectionPath) { + let selectedObject = selectionPath[0].context.item; + + return ( + selectedObject && + selectedObject.type === 'layout' && + !selectionPath[0].context.layoutItem + ); + } + + function showForm(formStructure, name, selectionPath) { + openmct.forms.showForm(formStructure).then((changes) => { + selectionPath[0].context.addElement(name, changes); + }); + } + + if (isMainLayoutSelected(selectedObjects[0])) { + return [getToggleGridButton(selectedObjects), getAddButton(selectedObjects)]; + } + + let toolbar = { + 'add-menu': [], + text: [], + url: [], + viewSwitcher: [], + 'toggle-frame': [], + 'display-mode': [], + 'telemetry-value': [], + style: [], + 'unit-toggle': [], + position: [], + duplicate: [], + remove: [], + 'toggle-grid': [] + }; + + selectedObjects.forEach((selectionPath) => { + let selectedParent = selectionPath[1].context.item; + let layoutItem = selectionPath[0].context.layoutItem; + + if (!layoutItem || selectedParent.locked) { + return; + } + + if (layoutItem.type === 'subobject-view') { + if ( + toolbar['add-menu'].length === 0 && + selectionPath[0].context.item.type === 'layout' + ) { + toolbar['add-menu'] = [getAddButton(selectedObjects, selectionPath)]; + } + + if (toolbar['toggle-frame'].length === 0) { + toolbar['toggle-frame'] = [getToggleFrameButton(selectedParent, selectedObjects)]; + } + + if (toolbar.position.length === 0) { + toolbar.position = [ + getStackOrder(selectedParent, selectionPath), + getSeparator(), + getXInput(selectedParent, selectedObjects), + getYInput(selectedParent, selectedObjects), + getHeightInput(selectedParent, selectedObjects), + getWidthInput(selectedParent, selectedObjects) + ]; + } + + if (toolbar.remove.length === 0) { + toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; + } + + if (toolbar.viewSwitcher.length === 0) { + toolbar.viewSwitcher = [ + getViewSwitcherMenu(selectedParent, selectionPath, selectedObjects) + ]; + } + } else if (layoutItem.type === 'telemetry-view') { + if (toolbar['display-mode'].length === 0) { + toolbar['display-mode'] = [getDisplayModeMenu(selectedParent, selectedObjects)]; + } + + if (toolbar['telemetry-value'].length === 0) { + 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), + getWidthInput(selectedParent, selectedObjects) + ]; + } + + if (toolbar.remove.length === 0) { + toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; + } + + if (toolbar.viewSwitcher.length === 0) { + toolbar.viewSwitcher = [ + getViewSwitcherMenu(selectedParent, selectionPath, selectedObjects) + ]; + } + } 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), + getWidthInput(selectedParent, selectedObjects) + ]; + } + + if (toolbar.text.length === 0) { + toolbar.text = [getTextButton(selectedParent, selectedObjects)]; + } + + if (toolbar.remove.length === 0) { + toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; + } + } else if (layoutItem.type === 'box-view' || layoutItem.type === 'ellipse-view') { + if (toolbar.position.length === 0) { + toolbar.position = [ + getStackOrder(selectedParent, selectionPath), + getSeparator(), + getXInput(selectedParent, selectedObjects), + getYInput(selectedParent, selectedObjects), + getHeightInput(selectedParent, selectedObjects), + getWidthInput(selectedParent, selectedObjects) + ]; + } + + if (toolbar.remove.length === 0) { + toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; + } + } else if (layoutItem.type === 'image-view') { + if (toolbar.position.length === 0) { + toolbar.position = [ + getStackOrder(selectedParent, selectionPath), + getSeparator(), + getXInput(selectedParent, selectedObjects), + getYInput(selectedParent, selectedObjects), + getHeightInput(selectedParent, selectedObjects), + getWidthInput(selectedParent, selectedObjects) + ]; + } + + if (toolbar.remove.length === 0) { + toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; + } + } else if (layoutItem.type === 'line-view') { + if (toolbar.position.length === 0) { + toolbar.position = [ + getStackOrder(selectedParent, selectionPath), + getSeparator(), + getXInput(selectedParent, selectedObjects), + getYInput(selectedParent, selectedObjects), + getX2Input(selectedParent, selectedObjects), + getY2Input(selectedParent, selectedObjects) + ]; + } + + if (toolbar.remove.length === 0) { + toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; + } + } + + if (toolbar.duplicate.length === 0) { + toolbar.duplicate = [ + getDuplicateButton(selectedParent, selectionPath, selectedObjects) + ]; + } + + if (toolbar['toggle-grid'].length === 0) { + toolbar['toggle-grid'] = [getToggleGridButton(selectedObjects, selectionPath)]; + } + }); + + let toolbarArray = Object.values(toolbar); + + return _.flatten( + toolbarArray.reduce((accumulator, group, index) => { + group = group.filter((control) => control !== undefined); + + if (group.length > 0) { + accumulator.push(group); + + if (index < toolbarArray.length - 1) { + accumulator.push(getSeparator()); + } + } + + return accumulator; + }, []) + ); + } + }; + } + + return DisplayLayoutToolbar; }); diff --git a/src/plugins/displayLayout/DisplayLayoutType.js b/src/plugins/displayLayout/DisplayLayoutType.js index a697c84026..bf631fdf5f 100644 --- a/src/plugins/displayLayout/DisplayLayoutType.js +++ b/src/plugins/displayLayout/DisplayLayoutType.js @@ -21,67 +21,52 @@ *****************************************************************************/ define(function () { - function DisplayLayoutType() { - return { - name: "Display Layout", - creatable: true, - description: 'Assemble other objects and components together into a reusable screen layout. Simply drag in the objects you want, position and size them. Save your design and view or edit it at any time.', - cssClass: 'icon-layout', - initialize(domainObject) { - domainObject.composition = []; - domainObject.configuration = { - items: [], - layoutGrid: [10, 10] - }; - }, - form: [ - { - name: "Horizontal grid (px)", - control: "numberfield", - cssClass: "l-input-sm l-numeric", - property: [ - "configuration", - "layoutGrid", - 0 - ], - required: true - }, - { - name: "Vertical grid (px)", - control: "numberfield", - cssClass: "l-input-sm l-numeric", - property: [ - "configuration", - "layoutGrid", - 1 - ], - required: true - }, - { - name: "Horizontal size (px)", - control: "numberfield", - cssClass: "l-input-sm l-numeric", - property: [ - "configuration", - "layoutDimensions", - 0 - ], - required: false - }, - { - name: "Vertical size (px)", - control: "numberfield", - cssClass: "l-input-sm l-numeric", - property: [ - "configuration", - "layoutDimensions", - 1 - ], - required: false - } - ] + function DisplayLayoutType() { + return { + name: 'Display Layout', + creatable: true, + description: + 'Assemble other objects and components together into a reusable screen layout. Simply drag in the objects you want, position and size them. Save your design and view or edit it at any time.', + cssClass: 'icon-layout', + initialize(domainObject) { + domainObject.composition = []; + domainObject.configuration = { + items: [], + layoutGrid: [10, 10] }; - } + }, + form: [ + { + name: 'Horizontal grid (px)', + control: 'numberfield', + cssClass: 'l-input-sm l-numeric', + property: ['configuration', 'layoutGrid', 0], + required: true + }, + { + name: 'Vertical grid (px)', + control: 'numberfield', + cssClass: 'l-input-sm l-numeric', + property: ['configuration', 'layoutGrid', 1], + required: true + }, + { + name: 'Horizontal size (px)', + control: 'numberfield', + cssClass: 'l-input-sm l-numeric', + property: ['configuration', 'layoutDimensions', 0], + required: false + }, + { + name: 'Vertical size (px)', + control: 'numberfield', + cssClass: 'l-input-sm l-numeric', + property: ['configuration', 'layoutDimensions', 1], + required: false + } + ] + }; + } - return DisplayLayoutType; + return DisplayLayoutType; }); diff --git a/src/plugins/displayLayout/DrawingObjectTypes.js b/src/plugins/displayLayout/DrawingObjectTypes.js index 9b34e808c9..60956bab9a 100644 --- a/src/plugins/displayLayout/DrawingObjectTypes.js +++ b/src/plugins/displayLayout/DrawingObjectTypes.js @@ -1,34 +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' - } + '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/LayoutDrag.js b/src/plugins/displayLayout/LayoutDrag.js index b45e3c3124..afbb464d34 100644 --- a/src/plugins/displayLayout/LayoutDrag.js +++ b/src/plugins/displayLayout/LayoutDrag.js @@ -20,107 +20,93 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define( - [], - function () { +define([], function () { + /** + * Handles drag interactions on frames in layouts. This will + * provides new positions/dimensions for frames based on + * relative pixel positions provided; these will take into account + * the grid size (in a snap-to sense) and will enforce some minimums + * on both position and dimensions. + * + * The provided position and dimensions factors will determine + * whether this is a move or a resize, and what type of resize it + * will be. For instance, a position factor of [1, 1] + * will move a frame along with the mouse as the drag + * proceeds, while a dimension factor of [0, 0] will leave + * dimensions unchanged. Combining these in different + * ways results in different handles; a position factor of + * [1, 0] and a dimensions factor of [-1, 0] will implement + * a left-edge resize, as the horizontal position will move + * with the mouse while the horizontal dimensions shrink in + * kind (and vertical properties remain unmodified.) + * + * @param {object} rawPosition the initial position/dimensions + * of the frame being interacted with + * @param {number[]} posFactor the position factor + * @param {number[]} dimFactor the dimensions factor + * @param {number[]} the size of each grid element, in pixels + * @constructor + * @memberof platform/features/layout + */ + function LayoutDrag(rawPosition, posFactor, dimFactor, gridSize) { + this.rawPosition = rawPosition; + this.posFactor = posFactor; + this.dimFactor = dimFactor; + this.gridSize = gridSize; + } - /** - * Handles drag interactions on frames in layouts. This will - * provides new positions/dimensions for frames based on - * relative pixel positions provided; these will take into account - * the grid size (in a snap-to sense) and will enforce some minimums - * on both position and dimensions. - * - * The provided position and dimensions factors will determine - * whether this is a move or a resize, and what type of resize it - * will be. For instance, a position factor of [1, 1] - * will move a frame along with the mouse as the drag - * proceeds, while a dimension factor of [0, 0] will leave - * dimensions unchanged. Combining these in different - * ways results in different handles; a position factor of - * [1, 0] and a dimensions factor of [-1, 0] will implement - * a left-edge resize, as the horizontal position will move - * with the mouse while the horizontal dimensions shrink in - * kind (and vertical properties remain unmodified.) - * - * @param {object} rawPosition the initial position/dimensions - * of the frame being interacted with - * @param {number[]} posFactor the position factor - * @param {number[]} dimFactor the dimensions factor - * @param {number[]} the size of each grid element, in pixels - * @constructor - * @memberof platform/features/layout - */ - function LayoutDrag(rawPosition, posFactor, dimFactor, gridSize) { - this.rawPosition = rawPosition; - this.posFactor = posFactor; - this.dimFactor = dimFactor; - this.gridSize = gridSize; - } + // Convert a delta from pixel coordinates to grid coordinates, + // rounding to whole-number grid coordinates. + function toGridDelta(gridSize, pixelDelta) { + return pixelDelta.map(function (v, i) { + return Math.round(v / gridSize[i]); + }); + } - // Convert a delta from pixel coordinates to grid coordinates, - // rounding to whole-number grid coordinates. - function toGridDelta(gridSize, pixelDelta) { - return pixelDelta.map(function (v, i) { - return Math.round(v / gridSize[i]); - }); - } + // Utility function to perform element-by-element multiplication + function multiply(array, factors) { + return array.map(function (v, i) { + return v * factors[i]; + }); + } - // Utility function to perform element-by-element multiplication - function multiply(array, factors) { - return array.map(function (v, i) { - return v * factors[i]; - }); - } + // Utility function to perform element-by-element addition + function add(array, other) { + return array.map(function (v, i) { + return v + other[i]; + }); + } - // Utility function to perform element-by-element addition - function add(array, other) { - return array.map(function (v, i) { - return v + other[i]; - }); - } + // Utility function to perform element-by-element max-choosing + function max(array, other) { + return array.map(function (v, i) { + return Math.max(v, other[i]); + }); + } - // Utility function to perform element-by-element max-choosing - function max(array, other) { - return array.map(function (v, i) { - return Math.max(v, other[i]); - }); - } + /** + * Get a new position object in grid coordinates, with + * position and dimensions both offset appropriately + * according to the factors supplied in the constructor. + * @param {number[]} pixelDelta the offset from the + * original position, in pixels + */ + LayoutDrag.prototype.getAdjustedPositionAndDimensions = function (pixelDelta) { + const gridDelta = toGridDelta(this.gridSize, pixelDelta); - /** - * Get a new position object in grid coordinates, with - * position and dimensions both offset appropriately - * according to the factors supplied in the constructor. - * @param {number[]} pixelDelta the offset from the - * original position, in pixels - */ - LayoutDrag.prototype.getAdjustedPositionAndDimensions = function (pixelDelta) { - const gridDelta = toGridDelta(this.gridSize, pixelDelta); + return { + position: max(add(this.rawPosition.position, multiply(gridDelta, this.posFactor)), [0, 0]), + dimensions: max(add(this.rawPosition.dimensions, multiply(gridDelta, this.dimFactor)), [1, 1]) + }; + }; - return { - position: max(add( - this.rawPosition.position, - multiply(gridDelta, this.posFactor) - ), [0, 0]), - dimensions: max(add( - this.rawPosition.dimensions, - multiply(gridDelta, this.dimFactor) - ), [1, 1]) - }; - }; + LayoutDrag.prototype.getAdjustedPosition = function (pixelDelta) { + const gridDelta = toGridDelta(this.gridSize, pixelDelta); - LayoutDrag.prototype.getAdjustedPosition = function (pixelDelta) { - const gridDelta = toGridDelta(this.gridSize, pixelDelta); + return { + position: max(add(this.rawPosition.position, multiply(gridDelta, this.posFactor)), [0, 0]) + }; + }; - return { - position: max(add( - this.rawPosition.position, - multiply(gridDelta, this.posFactor) - ), [0, 0]) - }; - }; - - return LayoutDrag; - - } -); + return LayoutDrag; +}); diff --git a/src/plugins/displayLayout/actions/CopyToClipboardAction.js b/src/plugins/displayLayout/actions/CopyToClipboardAction.js index 5fdaf81037..c9e708dbcf 100644 --- a/src/plugins/displayLayout/actions/CopyToClipboardAction.js +++ b/src/plugins/displayLayout/actions/CopyToClipboardAction.js @@ -1,38 +1,38 @@ import clipboard from '@/utils/clipboard'; export default class CopyToClipboardAction { - constructor(openmct) { - this.openmct = openmct; + constructor(openmct) { + this.openmct = openmct; - this.cssClass = 'icon-duplicate'; - this.description = 'Copy value to clipboard'; - this.group = "action"; - this.key = 'copyToClipboard'; - this.name = 'Copy to Clipboard'; - this.priority = 1; + this.cssClass = 'icon-duplicate'; + this.description = 'Copy value to clipboard'; + this.group = 'action'; + this.key = 'copyToClipboard'; + this.name = 'Copy to Clipboard'; + this.priority = 1; + } + + invoke(objectPath, view = {}) { + const viewContext = view.getViewContext && view.getViewContext(); + const formattedValue = viewContext.row.formattedValueForCopy(); + + clipboard + .updateClipboard(formattedValue) + .then(() => { + this.openmct.notifications.info(`Success : copied '${formattedValue}' to clipboard `); + }) + .catch(() => { + this.openmct.notifications.error(`Failed : to copy '${formattedValue}' to clipboard `); + }); + } + + appliesTo(objectPath, view = {}) { + const viewContext = view.getViewContext && view.getViewContext(); + const row = viewContext && viewContext.row; + if (!row) { + return false; } - invoke(objectPath, view = {}) { - const viewContext = view.getViewContext && view.getViewContext(); - const formattedValue = viewContext.row.formattedValueForCopy(); - - clipboard.updateClipboard(formattedValue) - .then(() => { - this.openmct.notifications.info(`Success : copied '${formattedValue}' to clipboard `); - }) - .catch(() => { - this.openmct.notifications.error(`Failed : to copy '${formattedValue}' to clipboard `); - }); - } - - appliesTo(objectPath, view = {}) { - const viewContext = view.getViewContext && view.getViewContext(); - const row = viewContext && viewContext.row; - if (!row) { - return false; - } - - return row.formattedValueForCopy - && typeof row.formattedValueForCopy === 'function'; - } + return row.formattedValueForCopy && typeof row.formattedValueForCopy === 'function'; + } } diff --git a/src/plugins/displayLayout/components/AlphanumericFormat.vue b/src/plugins/displayLayout/components/AlphanumericFormat.vue index 3c49b4c49d..9f1dc6180d 100644 --- a/src/plugins/displayLayout/components/AlphanumericFormat.vue +++ b/src/plugins/displayLayout/components/AlphanumericFormat.vue @@ -21,82 +21,79 @@ --> diff --git a/src/plugins/displayLayout/components/BoxView.vue b/src/plugins/displayLayout/components/BoxView.vue index 2b6bc5b232..008297fda6 100644 --- a/src/plugins/displayLayout/components/BoxView.vue +++ b/src/plugins/displayLayout/components/BoxView.vue @@ -21,19 +21,19 @@ --> diff --git a/src/plugins/displayLayout/components/DisplayLayout.vue b/src/plugins/displayLayout/components/DisplayLayout.vue index 32df4d09a3..81be71d378 100644 --- a/src/plugins/displayLayout/components/DisplayLayout.vue +++ b/src/plugins/displayLayout/components/DisplayLayout.vue @@ -21,55 +21,55 @@ --> diff --git a/src/plugins/displayLayout/components/DisplayLayoutGrid.vue b/src/plugins/displayLayout/components/DisplayLayoutGrid.vue index cad05d456a..26f612c3d3 100644 --- a/src/plugins/displayLayout/components/DisplayLayoutGrid.vue +++ b/src/plugins/displayLayout/components/DisplayLayoutGrid.vue @@ -20,42 +20,49 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/displayLayout/components/EditMarquee.vue b/src/plugins/displayLayout/components/EditMarquee.vue index 7d18881f48..e0f28a52b9 100644 --- a/src/plugins/displayLayout/components/EditMarquee.vue +++ b/src/plugins/displayLayout/components/EditMarquee.vue @@ -21,171 +21,174 @@ --> diff --git a/src/plugins/displayLayout/components/EllipseView.vue b/src/plugins/displayLayout/components/EllipseView.vue index 846b09e147..df0df529c2 100644 --- a/src/plugins/displayLayout/components/EllipseView.vue +++ b/src/plugins/displayLayout/components/EllipseView.vue @@ -21,19 +21,19 @@ --> diff --git a/src/plugins/displayLayout/components/ImageView.vue b/src/plugins/displayLayout/components/ImageView.vue index 339083b1e9..a3e7359739 100644 --- a/src/plugins/displayLayout/components/ImageView.vue +++ b/src/plugins/displayLayout/components/ImageView.vue @@ -21,109 +21,107 @@ --> diff --git a/src/plugins/displayLayout/components/LayoutFrame.vue b/src/plugins/displayLayout/components/LayoutFrame.vue index 41eb4d7847..d81b6b4f02 100644 --- a/src/plugins/displayLayout/components/LayoutFrame.vue +++ b/src/plugins/displayLayout/components/LayoutFrame.vue @@ -21,20 +21,17 @@ --> diff --git a/src/plugins/displayLayout/components/LineView.vue b/src/plugins/displayLayout/components/LineView.vue index bcc01a67ab..d79ee35604 100644 --- a/src/plugins/displayLayout/components/LineView.vue +++ b/src/plugins/displayLayout/components/LineView.vue @@ -21,356 +21,347 @@ --> diff --git a/src/plugins/displayLayout/components/SubobjectView.vue b/src/plugins/displayLayout/components/SubobjectView.vue index f2b1b29242..952ebf8403 100644 --- a/src/plugins/displayLayout/components/SubobjectView.vue +++ b/src/plugins/displayLayout/components/SubobjectView.vue @@ -20,25 +20,25 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/displayLayout/components/TelemetryView.vue b/src/plugins/displayLayout/components/TelemetryView.vue index 474cc0ab36..68159458fb 100644 --- a/src/plugins/displayLayout/components/TelemetryView.vue +++ b/src/plugins/displayLayout/components/TelemetryView.vue @@ -21,353 +21,365 @@ --> diff --git a/src/plugins/displayLayout/components/TextView.vue b/src/plugins/displayLayout/components/TextView.vue index 8b2e50f926..9c0f1cb449 100644 --- a/src/plugins/displayLayout/components/TextView.vue +++ b/src/plugins/displayLayout/components/TextView.vue @@ -21,111 +21,116 @@ --> diff --git a/src/plugins/displayLayout/components/box-and-line-views.scss b/src/plugins/displayLayout/components/box-and-line-views.scss index 13c5a2316e..5b0c4dda5b 100644 --- a/src/plugins/displayLayout/components/box-and-line-views.scss +++ b/src/plugins/displayLayout/components/box-and-line-views.scss @@ -1,65 +1,66 @@ .c-box-view, .c-ellipse-view { - border-width: $drawingObjBorderW !important; - display: flex; - align-items: stretch; + border-width: $drawingObjBorderW !important; + display: flex; + align-items: stretch; - .c-frame & { - @include abs(); - } + .c-frame & { + @include abs(); + } } .c-ellipse-view { - border-radius: 50%; + border-radius: 50%; } .c-line-view { - &.c-frame { - box-shadow: none !important; - } + &.c-frame { + box-shadow: none !important; + } - .c-frame-edit { - border: none; - } + .c-frame-edit { + border: none; + } - .c-handle-info { - background: rgba(#999, 0.2); - padding: 2px; - position: absolute; - top: 5px; left: 5px; - white-space: nowrap; - } + .c-handle-info { + background: rgba(#999, 0.2); + padding: 2px; + position: absolute; + top: 5px; + left: 5px; + white-space: nowrap; + } - svg { - // Prevent clipping when line is horizontal and vertical - min-height: 1px; - min-width: 1px; - // Must use !important to counteract setting in normalize.min.css - overflow: visible; - } + svg { + // Prevent clipping when line is horizontal and vertical + min-height: 1px; + min-width: 1px; + // Must use !important to counteract setting in normalize.min.css + overflow: visible; + } - &__line { - stroke-linecap: round; - stroke-width: $drawingObjBorderW; - } + &__line { + stroke-linecap: round; + stroke-width: $drawingObjBorderW; + } - &__hover-indicator { - display: none; - opacity: 0.5; - stroke: $editFrameColorHov; - stroke-width: $drawingObjBorderW + 4; - } + &__hover-indicator { + display: none; + opacity: 0.5; + stroke: $editFrameColorHov; + stroke-width: $drawingObjBorderW + 4; + } - .is-editing & { - // Needed to allow line to be moved - $w: 4px; - min-width: $w; - min-height: $w; + .is-editing & { + // Needed to allow line to be moved + $w: 4px; + min-width: $w; + min-height: $w; - &:hover { - [class*='__hover-indicator'] { - display: inline; - } - } + &:hover { + [class*='__hover-indicator'] { + display: inline; + } } + } } diff --git a/src/plugins/displayLayout/components/display-layout.scss b/src/plugins/displayLayout/components/display-layout.scss index 0bc0b9b9b3..69f5a05552 100644 --- a/src/plugins/displayLayout/components/display-layout.scss +++ b/src/plugins/displayLayout/components/display-layout.scss @@ -1,96 +1,97 @@ @mixin displayMarquee($c) { - > .c-frame-edit { - // All other frames - //@include test($c, 0.4); - display: block; - } - > .c-frame > .c-frame-edit { - // Line object frame - //@include test($c, 0.4); - display: block; - } + > .c-frame-edit { + // All other frames + //@include test($c, 0.4); + display: block; + } + > .c-frame > .c-frame-edit { + // Line object frame + //@include test($c, 0.4); + display: block; + } } .l-layout { - @include abs(); - display: flex; - flex-direction: column; - overflow: auto; + @include abs(); + display: flex; + flex-direction: column; + overflow: auto; - &__grid-holder, - &__dimensions { - display: none; + &__grid-holder, + &__dimensions { + display: none; + } + + &__dimensions { + $b: 1px dashed $editDimensionsColor; + border-right: $b; + border-bottom: $b; + pointer-events: none; + position: absolute; + + &-vals { + $p: 2px; + color: $editDimensionsColor; + display: inline-block; + font-style: italic; + position: absolute; + bottom: $p; + right: $p; + opacity: 0.7; } + } - &__dimensions { - $b: 1px dashed $editDimensionsColor; - border-right: $b; - border-bottom: $b; - pointer-events: none; - position: absolute; - - &-vals { - $p: 2px; - color: $editDimensionsColor; - display: inline-block; - font-style: italic; - position: absolute; - bottom: $p; right: $p; - opacity: 0.7; - } - } - - &__frame { - position: absolute; - } + &__frame { + position: absolute; + } } .is-editing { - .l-shell__main-container { - [s-selected], - [s-selected-parent] { - // Display grid and allow edit marquee to display in main layout holder when editing - > .l-layout { - background: $editUIGridColorBg; + .l-shell__main-container { + [s-selected], + [s-selected-parent] { + // Display grid and allow edit marquee to display in main layout holder when editing + > .l-layout { + background: $editUIGridColorBg; - > [class*="__dimensions"] { - display: block; - } - - > [class*="__grid-holder"] { - display: block; - } - } + > [class*='__dimensions'] { + display: block; } + + > [class*='__grid-holder'] { + display: block; + } + } } + } - .l-layout__frame { - &[s-selected]:not([multi-select="true"]), - &[s-selected-parent] { - // Display grid and allow edit marquee to display in nested layouts when editing - > * > * > .l-layout.allow-editing { - box-shadow: inset $editUIGridColorFg 0 0 2px 1px; + .l-layout__frame { + &[s-selected]:not([multi-select='true']), + &[s-selected-parent] { + // Display grid and allow edit marquee to display in nested layouts when editing + > * > * > .l-layout.allow-editing { + box-shadow: inset $editUIGridColorFg 0 0 2px 1px; - > [class*="__dimensions"] { - display: block; - } - - > [class*='grid-holder'] { - display: block; - } - } + > [class*='__dimensions'] { + display: block; } + + > [class*='grid-holder'] { + display: block; + } + } } + } - /*********************** EDIT MARQUEE CONTROL */ - *[s-selected-parent] { - > .l-layout { - // When main shell layout is the parent - @include displayMarquee(deeppink); // TEMP - } - > * > * > * { - // When a sub-layout is the parent - @include displayMarquee(blue); - } + /*********************** EDIT MARQUEE CONTROL */ + *[s-selected-parent] { + > .l-layout { + // When main shell layout is the parent + @include displayMarquee(deeppink); // TEMP } + > * > * > * { + // When a sub-layout is the parent + @include displayMarquee(blue); + } + } } diff --git a/src/plugins/displayLayout/components/edit-marquee.scss b/src/plugins/displayLayout/components/edit-marquee.scss index 1c7da0ddf9..ea244c8ebc 100644 --- a/src/plugins/displayLayout/components/edit-marquee.scss +++ b/src/plugins/displayLayout/components/edit-marquee.scss @@ -1,54 +1,62 @@ .c-frame-edit { - // In Layouts, this is the editing rect and handles - display: none; // Set to display: block in DisplayLayout.vue - pointer-events: none; - @include abs(); - border: $editMarqueeBorder; + // In Layouts, this is the editing rect and handles + display: none; // Set to display: block in DisplayLayout.vue + pointer-events: none; + @include abs(); + border: $editMarqueeBorder; - &__handle { - $d: 6px; - $o: floor($d * -0.5); - background: $editFrameColorHandleFg; - box-shadow: $editFrameColorHandleBg 0 0 0 2px; - pointer-events: all; - position: absolute; - width: $d; height: $d; - top: auto; right: auto; bottom: auto; left: auto; + &__handle { + $d: 6px; + $o: floor($d * -0.5); + background: $editFrameColorHandleFg; + box-shadow: $editFrameColorHandleBg 0 0 0 2px; + pointer-events: all; + position: absolute; + width: $d; + height: $d; + top: auto; + right: auto; + bottom: auto; + left: auto; - &:before { - // Extended hit area - @include abs(-10px); - content: ''; - display: block; - z-index: 0; - } - - &:hover { - background: $editUIColor; - } - - &--nwse { - cursor: nwse-resize; - } - - &--nw { - cursor: nw-resize; - left: $o; top: $o; - } - - &--ne { - cursor: ne-resize; - right: $o; top: $o; - } - - &--se { - cursor: se-resize; - right: $o; bottom: $o; - } - - &--sw { - cursor: sw-resize; - left: $o; bottom: $o; - } + &:before { + // Extended hit area + @include abs(-10px); + content: ''; + display: block; + z-index: 0; } + + &:hover { + background: $editUIColor; + } + + &--nwse { + cursor: nwse-resize; + } + + &--nw { + cursor: nw-resize; + left: $o; + top: $o; + } + + &--ne { + cursor: ne-resize; + right: $o; + top: $o; + } + + &--se { + cursor: se-resize; + right: $o; + bottom: $o; + } + + &--sw { + cursor: sw-resize; + left: $o; + bottom: $o; + } + } } diff --git a/src/plugins/displayLayout/components/image-view.scss b/src/plugins/displayLayout/components/image-view.scss index a2c4c97dda..e1e4354b67 100644 --- a/src/plugins/displayLayout/components/image-view.scss +++ b/src/plugins/displayLayout/components/image-view.scss @@ -1,10 +1,10 @@ .c-image-view { - background-size: cover; - background-repeat: no-repeat; - background-position: center; + background-size: cover; + background-repeat: no-repeat; + background-position: center; - .c-frame & { - @include abs(); - border: 1px solid transparent; - } + .c-frame & { + @include abs(); + border: 1px solid transparent; + } } diff --git a/src/plugins/displayLayout/components/layout-frame.scss b/src/plugins/displayLayout/components/layout-frame.scss index b35d4e2fa9..a6fbc562e9 100644 --- a/src/plugins/displayLayout/components/layout-frame.scss +++ b/src/plugins/displayLayout/components/layout-frame.scss @@ -2,134 +2,134 @@ /******************* FRAME */ .c-frame { - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; - // Whatever is placed into the slot, make it fill the entirety of the space, obeying padding - > *:first-child { - flex: 1 1 auto; - } + // Whatever is placed into the slot, make it fill the entirety of the space, obeying padding + > *:first-child { + flex: 1 1 auto; + } } .c-frame__move-bar { - display: none; + display: none; } .is-editing { - /******************* STYLES FOR C-FRAME WHILE EDITING */ - .c-frame { - border: 1px solid rgba($editFrameColorHov, 0.3); + /******************* STYLES FOR C-FRAME WHILE EDITING */ + .c-frame { + border: 1px solid rgba($editFrameColorHov, 0.3); - &:not([s-selected]) { - &:hover { - border: $editFrameBorderHov; - } - } - - &[s-selected] { - // All frames selected while editing - box-shadow: $editFrameSelectedShdw; - - .c-frame__move-bar { - cursor: move; - } - } + &:not([s-selected]) { + &:hover { + border: $editFrameBorderHov; + } } - /******************* DEFAULT STYLES FOR -EDIT__MOVE */ - // All object types - .c-frame__move-bar { - @include abs(); - display: block; + &[s-selected] { + // All frames selected while editing + box-shadow: $editFrameSelectedShdw; + + .c-frame__move-bar { + cursor: move; + } + } + } + + /******************* DEFAULT STYLES FOR -EDIT__MOVE */ + // All object types + .c-frame__move-bar { + @include abs(); + display: block; + } + + // Has-complex-content objects + .c-so-view.has-complex-content { + @include transition($prop: transform, $dur: $transOutTime, $delay: $moveBarOutDelay); + + > .c-so-view__local-controls { + @include transition($prop: transform, $dur: 250ms, $delay: $moveBarOutDelay); } - // Has-complex-content objects - .c-so-view.has-complex-content { - @include transition($prop: transform, $dur: $transOutTime, $delay: $moveBarOutDelay); + + .c-frame__move-bar { + display: none; + } + } - > .c-so-view__local-controls { - @include transition($prop: transform, $dur: 250ms, $delay: $moveBarOutDelay); + .l-layout { + /******************* 0 - 1 ITEM SELECTED */ + &:not(.is-multi-selected) { + > .l-layout__frame { + > .c-so-view.has-complex-content { + > .c-so-view__local-controls { + @include transition($prop: transform, $dur: $transOutTime, $delay: $moveBarOutDelay); + } + + + .c-frame__move-bar { + @include transition($prop: height, $delay: $moveBarOutDelay); + @include userSelectNone(); + background: $editFrameMovebarColorBg; + box-shadow: rgba(black, 0.3) 0 2px; + bottom: auto; + display: block; + height: 0; // Height is set on hover below + opacity: 0.9; + max-height: 100%; + overflow: hidden; + text-align: center; + z-index: 10; + + &:before { + // Grippy + $h: 4px; + $tbOffset: math.div($editFrameMovebarH - $h, 2); + $lrOffset: 25%; + @include grippy($editFrameMovebarColorFg); + content: ''; + display: none; + position: absolute; + top: $tbOffset; + right: $lrOffset; + bottom: $tbOffset; + left: $lrOffset; + } + } } - + .c-frame__move-bar { - display: none; + &:hover { + > .c-so-view.has-complex-content { + transition: $transInTransform; + transition-delay: 0s; + + > .c-so-view__local-controls { + transform: translateY($editFrameMovebarH); + @include transition(height, $transOutTime); + transition-delay: 0s; + } + + + .c-frame__move-bar { + @include transition(height); + height: $editFrameMovebarH; + } + } } + } + > .l-layout__frame[s-selected] { + > .c-so-view.has-complex-content { + + .c-frame__move-bar:before { + display: block; + } + } + } } - .l-layout { - /******************* 0 - 1 ITEM SELECTED */ - &:not(.is-multi-selected) { - > .l-layout__frame { - > .c-so-view.has-complex-content { - > .c-so-view__local-controls { - @include transition($prop: transform, $dur: $transOutTime, $delay: $moveBarOutDelay); - } - - + .c-frame__move-bar { - @include transition($prop: height, $delay: $moveBarOutDelay); - @include userSelectNone(); - background: $editFrameMovebarColorBg; - box-shadow: rgba(black, 0.3) 0 2px; - bottom: auto; - display: block; - height: 0; // Height is set on hover below - opacity: 0.9; - max-height: 100%; - overflow: hidden; - text-align: center; - z-index: 10; - - &:before { - // Grippy - $h: 4px; - $tbOffset: math.div($editFrameMovebarH - $h, 2); - $lrOffset: 25%; - @include grippy($editFrameMovebarColorFg); - content: ''; - display: none; - position: absolute; - top: $tbOffset; - right: $lrOffset; - bottom: $tbOffset; - left: $lrOffset; - } - } - } - - &:hover { - > .c-so-view.has-complex-content { - transition: $transInTransform; - transition-delay: 0s; - - > .c-so-view__local-controls { - transform: translateY($editFrameMovebarH); - @include transition(height, $transOutTime); - transition-delay: 0s; - } - - + .c-frame__move-bar { - @include transition(height); - height: $editFrameMovebarH; - } - } - } - } - > .l-layout__frame[s-selected] { - > .c-so-view.has-complex-content { - + .c-frame__move-bar:before { - display: block; - } - } - } - } - - /******************* > 1 ITEMS SELECTED */ - &.is-multi-selected { - .l-layout__frame[s-selected] { - > .c-so-view.has-complex-content + .c-frame__move-bar { - display: block; - } - } + /******************* > 1 ITEMS SELECTED */ + &.is-multi-selected { + .l-layout__frame[s-selected] { + > .c-so-view.has-complex-content + .c-frame__move-bar { + display: block; } + } } + } } diff --git a/src/plugins/displayLayout/components/telemetry-view.scss b/src/plugins/displayLayout/components/telemetry-view.scss index 1b82bfb2e3..e1ffcca9fd 100644 --- a/src/plugins/displayLayout/components/telemetry-view.scss +++ b/src/plugins/displayLayout/components/telemetry-view.scss @@ -1,48 +1,48 @@ .c-telemetry-view { + display: flex; + align-items: stretch; + + > * { + // Label and value holders + flex: 1 1 50%; display: flex; - align-items: stretch; + flex-direction: row; + align-items: center; + overflow: hidden; + padding: $interiorMargin; > * { - // Label and value holders - flex: 1 1 50%; - display: flex; - flex-direction: row; - align-items: center; - overflow: hidden; - padding: $interiorMargin; - - > * { - // Text elements - @include ellipsize(); - } + // Text elements + @include ellipsize(); } + } - &__value { - @include isLimit(); - } + &__value { + @include isLimit(); + } - &__label { - margin-right: $interiorMargin; - } + &__label { + margin-right: $interiorMargin; + } - &.is-stale { - .c-telemetry-view__value { - @include isStaleElement(); - } + &.is-stale { + .c-telemetry-view__value { + @include isStaleElement(); } + } - .c-frame & { - @include abs(); - border: 1px solid transparent; - } + .c-frame & { + @include abs(); + border: 1px solid transparent; + } - .is-status__indicator { - position: absolute; - top: 0; - left: 0; - } + .is-status__indicator { + position: absolute; + top: 0; + left: 0; + } - &[class*='is-status'] { - border: $borderMissing; - } + &[class*='is-status'] { + border: $borderMissing; + } } diff --git a/src/plugins/displayLayout/components/text-view.scss b/src/plugins/displayLayout/components/text-view.scss index 0ea717e7dd..043035b5b6 100644 --- a/src/plugins/displayLayout/components/text-view.scss +++ b/src/plugins/displayLayout/components/text-view.scss @@ -1,11 +1,11 @@ .c-text-view { - display: flex; - align-items: center; // Vertically center text - overflow: hidden; - padding: $interiorMargin; + display: flex; + align-items: center; // Vertically center text + overflow: hidden; + padding: $interiorMargin; - .c-frame & { - @include abs(); - border: 1px solid transparent; - } + .c-frame & { + @include abs(); + border: 1px solid transparent; + } } diff --git a/src/plugins/displayLayout/mixins/objectStyles-mixin.js b/src/plugins/displayLayout/mixins/objectStyles-mixin.js index 67b48b34fa..bbfb0999d8 100644 --- a/src/plugins/displayLayout/mixins/objectStyles-mixin.js +++ b/src/plugins/displayLayout/mixins/objectStyles-mixin.js @@ -20,64 +20,75 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import StyleRuleManager from "@/plugins/condition/StyleRuleManager"; -import {getStylesWithoutNoneValue} from "@/plugins/condition/utils/styleUtils"; +import StyleRuleManager from '@/plugins/condition/StyleRuleManager'; +import { getStylesWithoutNoneValue } from '@/plugins/condition/utils/styleUtils'; export default { - inject: ['openmct'], - data() { - return { - objectStyle: undefined, - itemStyle: undefined, - styleClass: '' - }; - }, - mounted() { - this.parentDomainObject = this.$parent.domainObject; - this.itemId = this.item.id; - this.objectStyle = this.getObjectStyleForItem(this.parentDomainObject.configuration.objectStyles); - this.initObjectStyles(); - }, - beforeDestroy() { - if (this.stopListeningObjectStyles) { - this.stopListeningObjectStyles(); - } - - if (this.styleRuleManager) { - this.styleRuleManager.destroy(); - } - }, - methods: { - getObjectStyleForItem(objectStyle) { - if (objectStyle) { - return objectStyle[this.itemId] ? Object.assign({}, objectStyle[this.itemId]) : undefined; - } else { - return undefined; - } - }, - initObjectStyles() { - if (!this.styleRuleManager) { - this.styleRuleManager = new StyleRuleManager(this.objectStyle, this.openmct, this.updateStyle.bind(this), true); - } else { - this.styleRuleManager.updateObjectStyleConfig(this.objectStyle); - } - - if (this.stopListeningObjectStyles) { - this.stopListeningObjectStyles(); - } - - this.stopListeningObjectStyles = this.openmct.objects.observe(this.parentDomainObject, 'configuration.objectStyles', (newObjectStyle) => { - //Updating object styles in the inspector view will trigger this so that the changes are reflected immediately - let newItemObjectStyle = this.getObjectStyleForItem(newObjectStyle); - if (this.objectStyle !== newItemObjectStyle) { - this.objectStyle = newItemObjectStyle; - this.styleRuleManager.updateObjectStyleConfig(this.objectStyle); - } - }); - }, - updateStyle(style) { - this.itemStyle = getStylesWithoutNoneValue(style); - this.styleClass = this.itemStyle && this.itemStyle.isStyleInvisible; - } + inject: ['openmct'], + data() { + return { + objectStyle: undefined, + itemStyle: undefined, + styleClass: '' + }; + }, + mounted() { + this.parentDomainObject = this.$parent.domainObject; + this.itemId = this.item.id; + this.objectStyle = this.getObjectStyleForItem( + this.parentDomainObject.configuration.objectStyles + ); + this.initObjectStyles(); + }, + beforeDestroy() { + if (this.stopListeningObjectStyles) { + this.stopListeningObjectStyles(); } + + if (this.styleRuleManager) { + this.styleRuleManager.destroy(); + } + }, + methods: { + getObjectStyleForItem(objectStyle) { + if (objectStyle) { + return objectStyle[this.itemId] ? Object.assign({}, objectStyle[this.itemId]) : undefined; + } else { + return undefined; + } + }, + initObjectStyles() { + if (!this.styleRuleManager) { + this.styleRuleManager = new StyleRuleManager( + this.objectStyle, + this.openmct, + this.updateStyle.bind(this), + true + ); + } else { + this.styleRuleManager.updateObjectStyleConfig(this.objectStyle); + } + + if (this.stopListeningObjectStyles) { + this.stopListeningObjectStyles(); + } + + this.stopListeningObjectStyles = this.openmct.objects.observe( + this.parentDomainObject, + 'configuration.objectStyles', + (newObjectStyle) => { + //Updating object styles in the inspector view will trigger this so that the changes are reflected immediately + let newItemObjectStyle = this.getObjectStyleForItem(newObjectStyle); + if (this.objectStyle !== newItemObjectStyle) { + this.objectStyle = newItemObjectStyle; + this.styleRuleManager.updateObjectStyleConfig(this.objectStyle); + } + } + ); + }, + updateStyle(style) { + this.itemStyle = getStylesWithoutNoneValue(style); + this.styleClass = this.itemStyle && this.itemStyle.isStyleInvisible; + } + } }; diff --git a/src/plugins/displayLayout/plugin.js b/src/plugins/displayLayout/plugin.js index 22adaed9c5..5515bfb1d5 100644 --- a/src/plugins/displayLayout/plugin.js +++ b/src/plugins/displayLayout/plugin.js @@ -32,105 +32,108 @@ import objectUtils from 'objectUtils'; import Vue from 'vue'; class DisplayLayoutView { - constructor(openmct, domainObject, objectPath, options) { - this.openmct = openmct; - this.domainObject = domainObject; - this.objectPath = objectPath; - this.options = options; + constructor(openmct, domainObject, objectPath, options) { + this.openmct = openmct; + this.domainObject = domainObject; + this.objectPath = objectPath; + this.options = options; - this.component = undefined; - } + this.component = undefined; + } - show(container, isEditing) { - this.component = new Vue({ - el: container, - components: { - DisplayLayout - }, - provide: { - openmct: this.openmct, - objectPath: this.objectPath, - options: this.options, - objectUtils, - currentView: this - }, - data: () => { - return { - domainObject: this.domainObject, - isEditing - }; - }, - template: '' - }); - } - - getViewContext() { - if (!this.component) { - return {}; - } - - return this.component.$refs.displayLayout.getViewContext(); - } - - getSelectionContext() { + show(container, isEditing) { + this.component = new Vue({ + el: container, + components: { + DisplayLayout + }, + provide: { + openmct: this.openmct, + objectPath: this.objectPath, + options: this.options, + objectUtils, + currentView: this + }, + data: () => { return { - item: this.domainObject, - supportsMultiSelect: true, - addElement: this.component && this.component.$refs.displayLayout.addElement, - removeItem: this.component && this.component.$refs.displayLayout.removeItem, - orderItem: this.component && this.component.$refs.displayLayout.orderItem, - duplicateItem: this.component && this.component.$refs.displayLayout.duplicateItem, - switchViewType: this.component && this.component.$refs.displayLayout.switchViewType, - mergeMultipleTelemetryViews: this.component && this.component.$refs.displayLayout.mergeMultipleTelemetryViews, - mergeMultipleOverlayPlots: this.component && this.component.$refs.displayLayout.mergeMultipleOverlayPlots, - toggleGrid: this.component && this.component.$refs.displayLayout.toggleGrid + domainObject: this.domainObject, + isEditing }; + }, + template: + '' + }); + } + + getViewContext() { + if (!this.component) { + return {}; } - onEditModeChange(isEditing) { - this.component.isEditing = isEditing; - } + return this.component.$refs.displayLayout.getViewContext(); + } - destroy() { - this.component.$destroy(); - this.component = undefined; - } + getSelectionContext() { + return { + item: this.domainObject, + supportsMultiSelect: true, + addElement: this.component && this.component.$refs.displayLayout.addElement, + removeItem: this.component && this.component.$refs.displayLayout.removeItem, + orderItem: this.component && this.component.$refs.displayLayout.orderItem, + duplicateItem: this.component && this.component.$refs.displayLayout.duplicateItem, + switchViewType: this.component && this.component.$refs.displayLayout.switchViewType, + mergeMultipleTelemetryViews: + this.component && this.component.$refs.displayLayout.mergeMultipleTelemetryViews, + mergeMultipleOverlayPlots: + this.component && this.component.$refs.displayLayout.mergeMultipleOverlayPlots, + toggleGrid: this.component && this.component.$refs.displayLayout.toggleGrid + }; + } + + onEditModeChange(isEditing) { + this.component.isEditing = isEditing; + } + + destroy() { + this.component.$destroy(); + this.component = undefined; + } } export default function DisplayLayoutPlugin(options) { - return function (openmct) { - openmct.actions.register(new CopyToClipboardAction(openmct)); + return function (openmct) { + openmct.actions.register(new CopyToClipboardAction(openmct)); - openmct.objectViews.addProvider({ - key: 'layout.view', - canView: function (domainObject) { - return domainObject.type === 'layout'; - }, - canEdit: function (domainObject) { - return domainObject.type === 'layout'; - }, - view: function (domainObject, objectPath) { - return new DisplayLayoutView(openmct, domainObject, objectPath, options); - }, - priority() { - return 100; - } - }); - openmct.types.addType('layout', DisplayLayoutType()); - openmct.toolbars.addProvider(new DisplayLayoutToolbar(openmct)); - openmct.inspectorViews.addProvider(new AlphaNumericFormatViewProvider(openmct, options)); - openmct.composition.addPolicy((parent, child) => { - if (parent.type === 'layout' && child.type === 'folder') { - return false; - } else { - return true; - } - }); + openmct.objectViews.addProvider({ + key: 'layout.view', + canView: function (domainObject) { + return domainObject.type === 'layout'; + }, + canEdit: function (domainObject) { + return domainObject.type === 'layout'; + }, + view: function (domainObject, objectPath) { + return new DisplayLayoutView(openmct, domainObject, objectPath, options); + }, + priority() { + return 100; + } + }); + openmct.types.addType('layout', DisplayLayoutType()); + openmct.toolbars.addProvider(new DisplayLayoutToolbar(openmct)); + openmct.inspectorViews.addProvider(new AlphaNumericFormatViewProvider(openmct, options)); + openmct.composition.addPolicy((parent, child) => { + if (parent.type === 'layout' && child.type === 'folder') { + return false; + } else { + return true; + } + }); - for (const [type, definition] of Object.entries(DisplayLayoutDrawingObjectTypes)) { - openmct.types.addType(type, definition); - } + for (const [type, definition] of Object.entries(DisplayLayoutDrawingObjectTypes)) { + openmct.types.addType(type, definition); + } - DisplayLayoutPlugin._installed = true; - }; + DisplayLayoutPlugin._installed = true; + }; } diff --git a/src/plugins/displayLayout/pluginSpec.js b/src/plugins/displayLayout/pluginSpec.js index a4cac2e724..476fc906c1 100644 --- a/src/plugins/displayLayout/pluginSpec.js +++ b/src/plugins/displayLayout/pluginSpec.js @@ -25,416 +25,407 @@ import Vue from 'vue'; import DisplayLayoutPlugin from './plugin'; describe('the plugin', function () { - let element; - let child; - let openmct; - let displayLayoutDefinition; + let element; + let child; + let openmct; + let displayLayoutDefinition; + + beforeEach((done) => { + openmct = createOpenMct(); + openmct.install( + new DisplayLayoutPlugin({ + showAsView: [] + }) + ); + displayLayoutDefinition = openmct.types.get('layout'); + + element = document.createElement('div'); + child = document.createElement('div'); + element.appendChild(child); + + openmct.on('start', done); + openmct.start(child); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('defines a display layout object type with the correct key', () => { + expect(displayLayoutDefinition.definition.name).toEqual('Display Layout'); + }); + + it('provides a view', () => { + const testViewObject = { + id: 'test-object', + type: 'layout', + configuration: { + items: [ + { + identifier: { + namespace: '', + key: '55122607-e65e-44d5-9c9d-9c31a914ca89' + }, + x: 8, + y: 3, + width: 10, + height: 5, + displayMode: 'all', + value: 'sin', + stroke: '', + fill: '', + color: '', + size: '13px', + type: 'telemetry-view', + id: 'deb9f839-80ad-4ccf-a152-5c763ceb7d7e' + } + ], + layoutGrid: [10, 10] + } + }; + + const applicableViews = openmct.objectViews.get(testViewObject, []); + let displayLayoutViewProvider = applicableViews.find( + (viewProvider) => viewProvider.key === 'layout.view' + ); + 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) => { - openmct = createOpenMct(); - openmct.install(new DisplayLayoutPlugin({ - showAsView: [] - })); - displayLayoutDefinition = openmct.types.get('layout'); - - element = document.createElement('div'); - child = document.createElement('div'); - element.appendChild(child); - - openmct.on('start', done); - openmct.start(child); - }); - - afterEach(() => { - return resetApplicationState(openmct); - }); - - it('defines a display layout object type with the correct key', () => { - expect(displayLayoutDefinition.definition.name).toEqual('Display Layout'); - }); - - it('provides a view', () => { - const testViewObject = { - id: 'test-object', - type: 'layout', - configuration: { - items: [ - { - - 'identifier': { - 'namespace': '', - 'key': '55122607-e65e-44d5-9c9d-9c31a914ca89' - }, - 'x': 8, - 'y': 3, - 'width': 10, - 'height': 5, - 'displayMode': 'all', - 'value': 'sin', - 'stroke': '', - 'fill': '', - 'color': '', - 'size': '13px', - 'type': 'telemetry-view', - 'id': 'deb9f839-80ad-4ccf-a152-5c763ceb7d7e' - - } - ], - layoutGrid: [10, 10] - } - }; - - const applicableViews = openmct.objectViews.get(testViewObject, []); - let displayLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'layout.view'); - 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; + 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' } + }; - expect(error).toBeUndefined(); + 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); }); - describe('on load', () => { - let displayLayoutItem; - let item; + it('will sync compostion and layout items', () => { + expect(displayLayoutItem.configuration.items.length).toBe(0); + }); + }); - 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' + describe('the alpha numeric format view', () => { + let displayLayoutItem; + let telemetryItem; + let selection; - }; - displayLayoutItem = { - 'composition': [ - // no item in compostion, but item in configuration items + beforeEach(() => { + displayLayoutItem = { + composition: [], + configuration: { + items: [ + { + identifier: { + namespace: '', + key: '55122607-e65e-44d5-9c9d-9c31a914ca89' + }, + x: 8, + y: 3, + width: 10, + height: 5, + displayMode: 'all', + value: 'sin', + stroke: '', + fill: '', + color: '', + size: '13px', + type: 'telemetry-view', + id: 'deb9f839-80ad-4ccf-a152-5c763ceb7d7e' + } + ], + layoutGrid: [10, 10] + }, + name: 'Display Layout', + type: 'layout', + identifier: { + namespace: '', + key: 'c5e636c1-6771-4c9c-b933-8665cab189b3' + } + }; + telemetryItem = { + telemetry: { + period: 5, + amplitude: 5, + offset: 5, + dataRateInHz: 5, + phase: 5, + randomness: 0 + }, + name: 'Sine Wave Generator', + type: 'generator', + modified: 1592851063871, + location: 'mine', + persisted: 1592851063871, + id: '55122607-e65e-44d5-9c9d-9c31a914ca89', + identifier: { + namespace: '', + key: '55122607-e65e-44d5-9c9d-9c31a914ca89' + } + }; + selection = [ + [ + { + context: { + layoutItem: displayLayoutItem.configuration.items[0], + item: telemetryItem, + index: 1 + } + }, + { + context: { + item: displayLayoutItem, + supportsMultiSelect: true + } + } + ] + ]; + }); + + it('provides an alphanumeric format view', () => { + const displayLayoutAlphaNumFormatView = openmct.inspectorViews.get(selection); + expect(displayLayoutAlphaNumFormatView.length).toBeDefined(); + }); + }); + + describe('the toolbar', () => { + let displayLayoutItem; + let selection; + + beforeEach(() => { + displayLayoutItem = { + composition: [], + configuration: { + items: [ + { + fill: '#666666', + stroke: '', + x: 1, + y: 1, + width: 10, + height: 5, + type: 'box-view', + id: '89b88746-d325-487b-aec4-11b79afff9e8' + }, + { + fill: '#666666', + stroke: '', + x: 1, + y: 1, + width: 10, + height: 10, + type: 'ellipse-view', + id: '19b88746-d325-487b-aec4-11b79afff9z8' + }, + { + x: 18, + y: 9, + x2: 23, + y2: 4, + stroke: '#666666', + type: 'line-view', + id: '57d49a28-7863-43bd-9593-6570758916f0' + }, + { + identifier: { + namespace: '', + key: '55122607-e65e-44d5-9c9d-9c31a914ca89' + }, + x: 8, + y: 3, + width: 10, + height: 5, + displayMode: 'all', + value: 'sin', + stroke: '', + fill: '', + color: '', + size: '13px', + type: 'telemetry-view', + id: 'deb9f839-80ad-4ccf-a152-5c763ceb7d7e' + }, + { + width: 32, + height: 18, + x: 78, + y: 8, + identifier: { + namespace: '', + key: 'bdeb91ab-3a7e-4a71-9dd2-39d73644e136' + }, + hasFrame: true, + type: 'subobject-view', + id: 'c0ff485a-344c-4e70-8d83-a9d9998a69fc' + } + ], + layoutGrid: [10, 10] + }, + name: 'Display Layout', + type: 'layout', + identifier: { + namespace: '', + key: 'c5e636c1-6771-4c9c-b933-8665cab189b3' + } + }; + selection = [ + [ + { + context: { + layoutItem: displayLayoutItem.configuration.items[1], + index: 1 + } + }, + { + context: { + item: displayLayoutItem, + supportsMultiSelect: true + } + } + ], + [ + { + context: { + layoutItem: displayLayoutItem.configuration.items[0], + index: 0 + } + }, + { + context: { + item: displayLayoutItem, + supportsMultiSelect: true + } + } + ], + [ + { + context: { + layoutItem: displayLayoutItem.configuration.items[2], + item: displayLayoutItem.configuration.items[2], + index: 2 + } + }, + { + context: { + item: displayLayoutItem, + supportsMultiSelect: true + } + } + ], + [ + { + context: { + item: { + composition: [ + { + namespace: '', + key: '55122607-e65e-44d5-9c9d-9c31a914ca89' + } ], - 'configuration': { - 'items': [ - item - ], - 'layoutGrid': [ - 10, - 10 - ] + configuration: { + series: [ + { + identifier: { + namespace: '', + key: '55122607-e65e-44d5-9c9d-9c31a914ca89' + } + } + ], + yAxis: {}, + xAxis: {} }, - '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); - }); + name: 'Unnamed Overlay Plot', + type: 'telemetry.plot.overlay', + modified: 1594142141929, + location: 'mine', + identifier: { + namespace: '', + key: 'bdeb91ab-3a7e-4a71-9dd2-39d73644e136' + }, + persisted: 1594142141929 + }, + layoutItem: displayLayoutItem.configuration.items[3], + index: 3 + } + }, + { + context: { + item: displayLayoutItem, + supportsMultiSelect: true + } + } + ] + ]; }); - describe('the alpha numeric format view', () => { - let displayLayoutItem; - let telemetryItem; - let selection; + it('provides controls including separators', () => { + const displayLayoutToolbar = openmct.toolbars.get(selection); - beforeEach(() => { - displayLayoutItem = { - 'composition': [ - ], - 'configuration': { - 'items': [ - { - - 'identifier': { - 'namespace': '', - 'key': '55122607-e65e-44d5-9c9d-9c31a914ca89' - }, - 'x': 8, - 'y': 3, - 'width': 10, - 'height': 5, - 'displayMode': 'all', - 'value': 'sin', - 'stroke': '', - 'fill': '', - 'color': '', - 'size': '13px', - 'type': 'telemetry-view', - 'id': 'deb9f839-80ad-4ccf-a152-5c763ceb7d7e' - - } - ], - 'layoutGrid': [ - 10, - 10 - ] - }, - 'name': 'Display Layout', - 'type': 'layout', - 'identifier': { - 'namespace': '', - 'key': 'c5e636c1-6771-4c9c-b933-8665cab189b3' - } - }; - telemetryItem = { - 'telemetry': { - 'period': 5, - 'amplitude': 5, - 'offset': 5, - 'dataRateInHz': 5, - 'phase': 5, - 'randomness': 0 - }, - 'name': 'Sine Wave Generator', - 'type': 'generator', - 'modified': 1592851063871, - 'location': 'mine', - 'persisted': 1592851063871, - 'id': '55122607-e65e-44d5-9c9d-9c31a914ca89', - 'identifier': { - 'namespace': '', - 'key': '55122607-e65e-44d5-9c9d-9c31a914ca89' - } - }; - selection = [ - [{ - context: { - 'layoutItem': displayLayoutItem.configuration.items[0], - 'item': telemetryItem, - 'index': 1 - } - }, - { - context: { - 'item': displayLayoutItem, - 'supportsMultiSelect': true - } - }] - ]; - }); - - it('provides an alphanumeric format view', () => { - const displayLayoutAlphaNumFormatView = openmct.inspectorViews.get(selection); - expect(displayLayoutAlphaNumFormatView.length).toBeDefined(); - }); - }); - - describe('the toolbar', () => { - let displayLayoutItem; - let selection; - - beforeEach(() => { - displayLayoutItem = { - 'composition': [ - ], - 'configuration': { - 'items': [ - { - 'fill': '#666666', - 'stroke': '', - 'x': 1, - 'y': 1, - 'width': 10, - 'height': 5, - 'type': 'box-view', - 'id': '89b88746-d325-487b-aec4-11b79afff9e8' - }, - { - 'fill': '#666666', - 'stroke': '', - 'x': 1, - 'y': 1, - 'width': 10, - 'height': 10, - 'type': 'ellipse-view', - 'id': '19b88746-d325-487b-aec4-11b79afff9z8' - }, - { - 'x': 18, - 'y': 9, - 'x2': 23, - 'y2': 4, - 'stroke': '#666666', - 'type': 'line-view', - 'id': '57d49a28-7863-43bd-9593-6570758916f0' - }, - { - - 'identifier': { - 'namespace': '', - 'key': '55122607-e65e-44d5-9c9d-9c31a914ca89' - }, - 'x': 8, - 'y': 3, - 'width': 10, - 'height': 5, - 'displayMode': 'all', - 'value': 'sin', - 'stroke': '', - 'fill': '', - 'color': '', - 'size': '13px', - 'type': 'telemetry-view', - 'id': 'deb9f839-80ad-4ccf-a152-5c763ceb7d7e' - - }, - { - - 'width': 32, - 'height': 18, - 'x': 78, - 'y': 8, - 'identifier': { - 'namespace': '', - 'key': 'bdeb91ab-3a7e-4a71-9dd2-39d73644e136' - }, - 'hasFrame': true, - 'type': 'subobject-view', - 'id': 'c0ff485a-344c-4e70-8d83-a9d9998a69fc' - - } - ], - 'layoutGrid': [ - 10, - 10 - ] - }, - 'name': 'Display Layout', - 'type': 'layout', - 'identifier': { - 'namespace': '', - 'key': 'c5e636c1-6771-4c9c-b933-8665cab189b3' - } - }; - selection = [ - [{ - context: { - 'layoutItem': displayLayoutItem.configuration.items[1], - 'index': 1 - } - }, - { - context: { - 'item': displayLayoutItem, - 'supportsMultiSelect': true - } - }], - [{ - context: { - 'layoutItem': displayLayoutItem.configuration.items[0], - 'index': 0 - } - }, - { - context: { - item: displayLayoutItem, - 'supportsMultiSelect': true - } - }], - [{ - context: { - 'layoutItem': displayLayoutItem.configuration.items[2], - 'item': displayLayoutItem.configuration.items[2], - 'index': 2 - } - }, - { - context: { - item: displayLayoutItem, - 'supportsMultiSelect': true - } - }], - [{ - context: { - 'item': { - - 'composition': [ - { - 'namespace': '', - 'key': '55122607-e65e-44d5-9c9d-9c31a914ca89' - } - ], - 'configuration': { - 'series': [ - { - 'identifier': { - 'namespace': '', - 'key': '55122607-e65e-44d5-9c9d-9c31a914ca89' - } - } - ], - 'yAxis': { - }, - 'xAxis': { - } - }, - 'name': 'Unnamed Overlay Plot', - 'type': 'telemetry.plot.overlay', - 'modified': 1594142141929, - 'location': 'mine', - 'identifier': { - 'namespace': '', - 'key': 'bdeb91ab-3a7e-4a71-9dd2-39d73644e136' - }, - 'persisted': 1594142141929 - - }, - 'layoutItem': displayLayoutItem.configuration.items[3], - 'index': 3 - } - }, - { - context: { - item: displayLayoutItem, - 'supportsMultiSelect': true - } - }] - ]; - }); - - it('provides controls including separators', () => { - const displayLayoutToolbar = openmct.toolbars.get(selection); - - expect(displayLayoutToolbar.length).toBe(8); - }); + expect(displayLayoutToolbar.length).toBe(8); }); + }); }); diff --git a/src/plugins/duplicate/DuplicateAction.js b/src/plugins/duplicate/DuplicateAction.js index 56845a7c8b..00cddf6df7 100644 --- a/src/plugins/duplicate/DuplicateAction.js +++ b/src/plugins/duplicate/DuplicateAction.js @@ -22,145 +22,150 @@ import DuplicateTask from './DuplicateTask'; export default class DuplicateAction { - constructor(openmct) { - this.name = 'Duplicate'; - this.key = 'duplicate'; - this.description = 'Duplicate this object.'; - this.cssClass = "icon-duplicate"; - this.group = "action"; - this.priority = 7; + constructor(openmct) { + this.name = 'Duplicate'; + this.key = 'duplicate'; + this.description = 'Duplicate this object.'; + this.cssClass = 'icon-duplicate'; + this.group = 'action'; + this.priority = 7; - this.openmct = openmct; - this.transaction = null; + this.openmct = openmct; + this.transaction = null; + } + + invoke(objectPath) { + this.object = objectPath[0]; + this.parent = objectPath[1]; + + this.showForm(this.object, this.parent); + } + + inNavigationPath() { + return this.openmct.router.path.some((objectInPath) => + this.openmct.objects.areIdsEqual(objectInPath.identifier, this.object.identifier) + ); + } + + async onSave(changes) { + this.startTransaction(); + + let inNavigationPath = this.inNavigationPath(); + if (inNavigationPath && this.openmct.editor.isEditing()) { + this.openmct.editor.save(); } - invoke(objectPath) { - this.object = objectPath[0]; - this.parent = objectPath[1]; - - this.showForm(this.object, this.parent); + let duplicationTask = new DuplicateTask(this.openmct); + if (changes.name && changes.name !== this.object.name) { + duplicationTask.changeName(changes.name); } - inNavigationPath() { - return this.openmct.router.path - .some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, this.object.identifier)); - } + const parentDomainObjectpath = changes.location || [this.parent]; + const parent = parentDomainObjectpath[0]; - async onSave(changes) { - this.startTransaction(); + await duplicationTask.duplicate(this.object, parent); - let inNavigationPath = this.inNavigationPath(); - if (inNavigationPath && this.openmct.editor.isEditing()) { - this.openmct.editor.save(); - } + return this.saveTransaction(); + } - let duplicationTask = new DuplicateTask(this.openmct); - if (changes.name && (changes.name !== this.object.name)) { - duplicationTask.changeName(changes.name); - } - - const parentDomainObjectpath = changes.location || [this.parent]; - const parent = parentDomainObjectpath[0]; - - await duplicationTask.duplicate(this.object, parent); - - return this.saveTransaction(); - } - - showForm(domainObject, parentDomainObject) { - const formStructure = { - title: "Duplicate Item", - sections: [ - { - rows: [ - { - key: "name", - control: "textfield", - name: "Title", - pattern: "\\S+", - required: true, - cssClass: "l-input-lg", - value: domainObject.name - }, - { - name: "Location", - cssClass: "grows", - control: "locator", - required: true, - parent: parentDomainObject, - validate: this.validate(parentDomainObject), - key: 'location' - } - ] - } - ] - }; - - this.openmct.forms.showForm(formStructure) - .then(this.onSave.bind(this)); - } - - validate(currentParent) { - return (data) => { - const parentCandidate = data.value[0]; - - 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 (!this.openmct.objects.isPersistable(parentCandidate.identifier)) { - return false; + showForm(domainObject, parentDomainObject) { + const formStructure = { + title: 'Duplicate Item', + sections: [ + { + rows: [ + { + key: 'name', + control: 'textfield', + name: 'Title', + pattern: '\\S+', + required: true, + cssClass: 'l-input-lg', + value: domainObject.name + }, + { + name: 'Location', + cssClass: 'grows', + control: 'locator', + required: true, + parent: parentDomainObject, + validate: this.validate(parentDomainObject), + key: 'location' } - - if (!parentCandidateKeystring || !currentParentKeystring) { - return false; - } - - if (parentCandidateKeystring === objectKeystring) { - return false; - } - - const parentCandidateComposition = parentCandidate.composition; - if (parentCandidateComposition && parentCandidateComposition.indexOf(objectKeystring) !== -1) { - return false; - } - - return parentCandidate && this.openmct.composition.checkPolicy(parentCandidate, this.object); - }; - } - - appliesTo(objectPath) { - const parent = objectPath[1]; - const parentType = parent && this.openmct.types.get(parent.type); - const child = objectPath[0]; - const childType = child && this.openmct.types.get(child.type); - const locked = child.locked ? child.locked : parent && parent.locked; - const isPersistable = this.openmct.objects.isPersistable(child.identifier); - - if (locked || !isPersistable) { - return false; + ] } + ] + }; - return childType - && childType.definition.creatable - && parentType - && parentType.definition.creatable - && Array.isArray(parent.composition); + this.openmct.forms.showForm(formStructure).then(this.onSave.bind(this)); + } + + validate(currentParent) { + return (data) => { + const parentCandidate = data.value[0]; + + 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 (!this.openmct.objects.isPersistable(parentCandidate.identifier)) { + return false; + } + + if (!parentCandidateKeystring || !currentParentKeystring) { + return false; + } + + if (parentCandidateKeystring === objectKeystring) { + return false; + } + + const parentCandidateComposition = parentCandidate.composition; + if ( + parentCandidateComposition && + parentCandidateComposition.indexOf(objectKeystring) !== -1 + ) { + return false; + } + + return parentCandidate && this.openmct.composition.checkPolicy(parentCandidate, this.object); + }; + } + + appliesTo(objectPath) { + const parent = objectPath[1]; + const parentType = parent && this.openmct.types.get(parent.type); + const child = objectPath[0]; + const childType = child && this.openmct.types.get(child.type); + const locked = child.locked ? child.locked : parent && parent.locked; + const isPersistable = this.openmct.objects.isPersistable(child.identifier); + + if (locked || !isPersistable) { + return false; } - startTransaction() { - if (!this.openmct.objects.isTransactionActive()) { - this.transaction = this.openmct.objects.startTransaction(); - } + return ( + childType && + childType.definition.creatable && + parentType && + parentType.definition.creatable && + Array.isArray(parent.composition) + ); + } + + startTransaction() { + if (!this.openmct.objects.isTransactionActive()) { + this.transaction = this.openmct.objects.startTransaction(); + } + } + + async saveTransaction() { + if (!this.transaction) { + return; } - async saveTransaction() { - if (!this.transaction) { - return; - } - - await this.transaction.commit(); - this.openmct.objects.endTransaction(); - this.transaction = null; - } + await this.transaction.commit(); + this.openmct.objects.endTransaction(); + this.transaction = null; + } } diff --git a/src/plugins/duplicate/DuplicateTask.js b/src/plugins/duplicate/DuplicateTask.js index 7c62c9aef6..f79dc0bb79 100644 --- a/src/plugins/duplicate/DuplicateTask.js +++ b/src/plugins/duplicate/DuplicateTask.js @@ -34,247 +34,254 @@ import { v4 as uuid } from 'uuid'; * @constructor */ export default class DuplicateTask { - constructor(openmct) { - this.domainObject = undefined; - this.parent = undefined; - this.firstClone = undefined; - this.filter = undefined; - this.persisted = 0; - this.clones = []; - this.idMap = {}; - this.name = undefined; + constructor(openmct) { + this.domainObject = undefined; + this.parent = undefined; + this.firstClone = undefined; + this.filter = undefined; + this.persisted = 0; + this.clones = []; + this.idMap = {}; + this.name = undefined; - this.openmct = openmct; + this.openmct = openmct; + } + + changeName(name) { + this.name = name; + } + + /** + * Execute the duplicate/copy task with the objects provided. + * @returns {promise} Which will resolve with a clone of the object + * once complete. + */ + async duplicate(domainObject, parent, filter) { + this.domainObject = domainObject; + this.parent = parent; + this.namespace = parent.identifier.namespace; + this.filter = filter || this.isCreatable; + + await this.buildDuplicationPlan(); + await this.persistObjects(); + await this.addClonesToParent(); + + return this.firstClone; + } + + /** + * Will build a graph of an object and all of its child objects in + * memory + * @private + * @param domainObject The original object to be copied + * @param parent The parent of the original object to be copied + * @returns {Promise} resolved with an array of clones of the models + * of the object tree being copied. Duplicating is done in a bottom-up + * fashion, so that the last member in the array is a clone of the model + * object being copied. The clones are all full composed with + * references to their own children. + */ + async buildDuplicationPlan() { + let domainObjectClone = await this.duplicateObject(this.domainObject); + if (domainObjectClone !== this.domainObject) { + domainObjectClone.location = this.getKeyString(this.parent); } - changeName(name) { - this.name = name; + if (this.name) { + domainObjectClone.name = this.name; } - /** - * Execute the duplicate/copy task with the objects provided. - * @returns {promise} Which will resolve with a clone of the object - * once complete. - */ - async duplicate(domainObject, parent, filter) { - this.domainObject = domainObject; - this.parent = parent; - this.namespace = parent.identifier.namespace; - this.filter = filter || this.isCreatable; + this.firstClone = domainObjectClone; - await this.buildDuplicationPlan(); - await this.persistObjects(); - await this.addClonesToParent(); + return; + } - return this.firstClone; + /** + * Will persist a list of {@link objectClones}. It will persist all + * simultaneously, irrespective of order in the list. This may + * result in automatic request batching by the browser. + */ + async persistObjects() { + let initialCount = this.clones.length; + let dialog = this.openmct.overlays.progressDialog({ + progressPerc: 0, + message: `Duplicating ${initialCount} objects.`, + iconClass: 'info', + title: 'Duplicating' + }); + + let clonesDone = Promise.all( + this.clones.map((clone) => { + let percentPersisted = Math.ceil(100 * (++this.persisted / initialCount)); + let message = `Duplicating ${initialCount - this.persisted} objects.`; + + dialog.updateProgress(percentPersisted, message); + + return this.openmct.objects.save(clone); + }) + ); + + await clonesDone; + + dialog.dismiss(); + this.openmct.notifications.info(`Duplicated ${this.persisted} objects.`); + + return; + } + + /** + * Will add a list of clones to the specified parent's composition + */ + async addClonesToParent() { + let parentComposition = this.openmct.composition.get(this.parent); + await parentComposition.load(); + parentComposition.add(this.firstClone); + + return; + } + + /** + * A recursive function that will perform a bottom-up duplicate of + * the object tree with originalObject at the root. Recurses to + * the farthest leaf, then works its way back up again, + * cloning objects, and composing them with their child clones + * as it goes + * @private + * @returns {DomainObject} If the type of the original object allows for + * duplication, then a duplicate of the object, otherwise the object + * itself (to allow linking to non duplicatable objects). + */ + async duplicateObject(originalObject) { + // Check if the creatable (or other passed in filter). + if (this.filter(originalObject)) { + let clone = this.cloneObjectModel(originalObject); + let composeesCollection = this.openmct.composition.get(originalObject); + let composees; + + if (composeesCollection) { + composees = await composeesCollection.load(); + } + + return this.duplicateComposees(clone, composees); } - /** - * Will build a graph of an object and all of its child objects in - * memory - * @private - * @param domainObject The original object to be copied - * @param parent The parent of the original object to be copied - * @returns {Promise} resolved with an array of clones of the models - * of the object tree being copied. Duplicating is done in a bottom-up - * fashion, so that the last member in the array is a clone of the model - * object being copied. The clones are all full composed with - * references to their own children. - */ - async buildDuplicationPlan() { - let domainObjectClone = await this.duplicateObject(this.domainObject); - if (domainObjectClone !== this.domainObject) { - domainObjectClone.location = this.getKeyString(this.parent); - } + // Not creatable, creating a link, no need to iterate children + return originalObject; + } - if (this.name) { - domainObjectClone.name = this.name; - } + /** + * Given an array of objects composed by a parent, clone them, then + * add them to the parent. + * @private + * @returns {*} + */ + async duplicateComposees(clonedParent, composees = []) { + let idMappings = []; + let allComposeesDuplicated = composees.reduce(async (previousPromise, nextComposee) => { + await previousPromise; - this.firstClone = domainObjectClone; + let clonedComposee = await this.duplicateObject(nextComposee); - return; - } - - /** - * Will persist a list of {@link objectClones}. It will persist all - * simultaneously, irrespective of order in the list. This may - * result in automatic request batching by the browser. - */ - async persistObjects() { - let initialCount = this.clones.length; - let dialog = this.openmct.overlays.progressDialog({ - progressPerc: 0, - message: `Duplicating ${initialCount} objects.`, - iconClass: 'info', - title: 'Duplicating' + if (clonedComposee) { + idMappings.push({ + newId: clonedComposee.identifier, + oldId: nextComposee.identifier }); + this.composeChild(clonedComposee, clonedParent, clonedComposee !== nextComposee); + } - let clonesDone = Promise.all(this.clones.map((clone) => { - let percentPersisted = Math.ceil(100 * (++this.persisted / initialCount)); - let message = `Duplicating ${initialCount - this.persisted} objects.`; + return; + }, Promise.resolve()); - dialog.updateProgress(percentPersisted, message); + await allComposeesDuplicated; - return this.openmct.objects.save(clone); - })); + clonedParent = this.rewriteIdentifiers(clonedParent, idMappings); + this.clones.push(clonedParent); - await clonesDone; + return clonedParent; + } - dialog.dismiss(); - this.openmct.notifications.info(`Duplicated ${this.persisted} objects.`); + /** + * Update identifiers in a cloned object model (or part of + * a cloned object model) to reflect new identifiers after + * duplicating. + * @private + */ + rewriteIdentifiers(clonedParent, childIdMappings) { + for (let { newId, oldId } of childIdMappings) { + let newIdKeyString = this.openmct.objects.makeKeyString(newId); + let oldIdKeyString = this.openmct.objects.makeKeyString(oldId); - return; - } + // regex replace keystrings + clonedParent = JSON.stringify(clonedParent).replace( + new RegExp(oldIdKeyString, 'g'), + newIdKeyString + ); - /** - * Will add a list of clones to the specified parent's composition - */ - async addClonesToParent() { - let parentComposition = this.openmct.composition.get(this.parent); - await parentComposition.load(); - parentComposition.add(this.firstClone); - - return; - } - - /** - * A recursive function that will perform a bottom-up duplicate of - * the object tree with originalObject at the root. Recurses to - * the farthest leaf, then works its way back up again, - * cloning objects, and composing them with their child clones - * as it goes - * @private - * @returns {DomainObject} If the type of the original object allows for - * duplication, then a duplicate of the object, otherwise the object - * itself (to allow linking to non duplicatable objects). - */ - async duplicateObject(originalObject) { - // Check if the creatable (or other passed in filter). - if (this.filter(originalObject)) { - let clone = this.cloneObjectModel(originalObject); - let composeesCollection = this.openmct.composition.get(originalObject); - let composees; - - if (composeesCollection) { - composees = await composeesCollection.load(); - } - - return this.duplicateComposees(clone, composees); + // parse reviver to replace identifiers + clonedParent = JSON.parse(clonedParent, (key, value) => { + if ( + Object.prototype.hasOwnProperty.call(value, 'key') && + Object.prototype.hasOwnProperty.call(value, 'namespace') && + value.key === oldId.key && + value.namespace === oldId.namespace + ) { + return newId; + } else { + return value; } - - // Not creatable, creating a link, no need to iterate children - return originalObject; + }); } - /** - * Given an array of objects composed by a parent, clone them, then - * add them to the parent. - * @private - * @returns {*} - */ - async duplicateComposees(clonedParent, composees = []) { - let idMappings = []; - let allComposeesDuplicated = composees.reduce(async (previousPromise, nextComposee) => { - await previousPromise; + return clonedParent; + } - let clonedComposee = await this.duplicateObject(nextComposee); + composeChild(child, parent, setLocation) { + parent.composition.push(child.identifier); - if (clonedComposee) { - idMappings.push({ - newId: clonedComposee.identifier, - oldId: nextComposee.identifier - }); - this.composeChild(clonedComposee, clonedParent, clonedComposee !== nextComposee); - } + //If a location is not specified, set it. + if (setLocation && child.location === undefined) { + let parentKeyString = this.getKeyString(parent); + child.location = parentKeyString; + } + } - return; - }, Promise.resolve()); + getTypeDefinition(domainObject, definition) { + let typeDefinitions = this.openmct.types.get(domainObject.type).definition; - await allComposeesDuplicated; + return typeDefinitions[definition] || false; + } - clonedParent = this.rewriteIdentifiers(clonedParent, idMappings); - this.clones.push(clonedParent); + cloneObjectModel(domainObject) { + let clone = JSON.parse(JSON.stringify(domainObject)); + let identifier = { + key: uuid(), + namespace: this.namespace // set to NEW parent's namespace + }; - return clonedParent; + if (clone.modified || clone.persisted || clone.location) { + clone.modified = undefined; + clone.persisted = undefined; + clone.location = undefined; + delete clone.modified; + delete clone.persisted; + delete clone.location; } - /** - * Update identifiers in a cloned object model (or part of - * a cloned object model) to reflect new identifiers after - * duplicating. - * @private - */ - rewriteIdentifiers(clonedParent, childIdMappings) { - for (let { newId, oldId } of childIdMappings) { - let newIdKeyString = this.openmct.objects.makeKeyString(newId); - let oldIdKeyString = this.openmct.objects.makeKeyString(oldId); - - // regex replace keystrings - clonedParent = JSON.stringify(clonedParent).replace(new RegExp(oldIdKeyString, 'g'), newIdKeyString); - - // parse reviver to replace identifiers - clonedParent = JSON.parse(clonedParent, (key, value) => { - if (Object.prototype.hasOwnProperty.call(value, 'key') - && Object.prototype.hasOwnProperty.call(value, 'namespace') - && value.key === oldId.key - && value.namespace === oldId.namespace) { - return newId; - } else { - return value; - } - }); - } - - return clonedParent; + if (clone.composition) { + clone.composition = []; } - composeChild(child, parent, setLocation) { - parent.composition.push(child.identifier); + clone.identifier = identifier; - //If a location is not specified, set it. - if (setLocation && child.location === undefined) { - let parentKeyString = this.getKeyString(parent); - child.location = parentKeyString; - } - } + return clone; + } - getTypeDefinition(domainObject, definition) { - let typeDefinitions = this.openmct.types.get(domainObject.type).definition; + getKeyString(domainObject) { + return this.openmct.objects.makeKeyString(domainObject.identifier); + } - return typeDefinitions[definition] || false; - } - - cloneObjectModel(domainObject) { - let clone = JSON.parse(JSON.stringify(domainObject)); - let identifier = { - key: uuid(), - namespace: this.namespace // set to NEW parent's namespace - }; - - if (clone.modified || clone.persisted || clone.location) { - clone.modified = undefined; - clone.persisted = undefined; - clone.location = undefined; - delete clone.modified; - delete clone.persisted; - delete clone.location; - } - - if (clone.composition) { - clone.composition = []; - } - - clone.identifier = identifier; - - return clone; - } - - getKeyString(domainObject) { - return this.openmct.objects.makeKeyString(domainObject.identifier); - } - - isCreatable(domainObject) { - return this.getTypeDefinition(domainObject, 'creatable'); - } + isCreatable(domainObject) { + return this.getTypeDefinition(domainObject, 'creatable'); + } } diff --git a/src/plugins/duplicate/plugin.js b/src/plugins/duplicate/plugin.js index 0d7d1db3b9..4112b97681 100644 --- a/src/plugins/duplicate/plugin.js +++ b/src/plugins/duplicate/plugin.js @@ -19,10 +19,10 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import DuplicateAction from "./DuplicateAction"; +import DuplicateAction from './DuplicateAction'; export default function () { - return function (openmct) { - openmct.actions.register(new DuplicateAction(openmct)); - }; + return function (openmct) { + openmct.actions.register(new DuplicateAction(openmct)); + }; } diff --git a/src/plugins/duplicate/pluginSpec.js b/src/plugins/duplicate/pluginSpec.js index 4ebef83421..d1f43dab9e 100644 --- a/src/plugins/duplicate/pluginSpec.js +++ b/src/plugins/duplicate/pluginSpec.js @@ -21,135 +21,134 @@ *****************************************************************************/ import DuplicateActionPlugin from './plugin.js'; import DuplicateTask from './DuplicateTask.js'; -import { - createOpenMct, - resetApplicationState, - getMockObjects -} from 'utils/testing'; +import { createOpenMct, resetApplicationState, getMockObjects } from 'utils/testing'; -describe("The Duplicate Action plugin", () => { - let openmct; - let duplicateTask; - let childObject; - let parentObject; - let anotherParentObject; +describe('The Duplicate Action plugin', () => { + let openmct; + let duplicateTask; + let childObject; + let parentObject; + let anotherParentObject; - // this setups up the app - beforeEach((done) => { - openmct = createOpenMct(); + // this setups up the app + beforeEach((done) => { + openmct = createOpenMct(); - childObject = getMockObjects({ - objectKeyStrings: ['folder'], - overwrite: { - folder: { - name: "Child Folder", - identifier: { - namespace: "", - key: "child-folder-object" - } - } + childObject = getMockObjects({ + objectKeyStrings: ['folder'], + overwrite: { + folder: { + name: 'Child Folder', + identifier: { + namespace: '', + key: 'child-folder-object' + } + } + } + }).folder; + + parentObject = getMockObjects({ + objectKeyStrings: ['folder'], + overwrite: { + folder: { + name: 'Parent Folder', + type: 'folder', + composition: [childObject.identifier] + } + } + }).folder; + + anotherParentObject = getMockObjects({ + objectKeyStrings: ['folder'], + overwrite: { + folder: { + name: 'Another Parent Folder' + } + } + }).folder; + + let objectGet = openmct.objects.get.bind(openmct.objects); + spyOn(openmct.objects, 'get').and.callFake((identifier) => { + let obj = [childObject, parentObject, anotherParentObject].find( + (ob) => ob.identifier.key === identifier.key + ); + + if (!obj) { + // not one of the mocked objs, callthrough basically + return objectGet(identifier); + } + + return Promise.resolve(obj); + }); + + spyOn(openmct.composition, 'get').and.callFake((domainObject) => { + return { + load: async () => { + let obj = [childObject, parentObject, anotherParentObject].find( + (ob) => ob.identifier.key === domainObject.identifier.key + ); + let children = []; + + if (obj) { + for (let i = 0; i < obj.composition.length; i++) { + children.push(await openmct.objects.get(obj.composition[i])); } - }).folder; + } - parentObject = getMockObjects({ - objectKeyStrings: ['folder'], - overwrite: { - folder: { - name: "Parent Folder", - type: "folder", - composition: [childObject.identifier] - } - } - }).folder; - - anotherParentObject = getMockObjects({ - objectKeyStrings: ['folder'], - overwrite: { - folder: { - name: "Another Parent Folder" - } - } - }).folder; - - let objectGet = openmct.objects.get.bind(openmct.objects); - spyOn(openmct.objects, 'get').and.callFake((identifier) => { - let obj = [childObject, parentObject, anotherParentObject].find((ob) => ob.identifier.key === identifier.key); - - if (!obj) { - // not one of the mocked objs, callthrough basically - return objectGet(identifier); - } - - return Promise.resolve(obj); - }); - - spyOn(openmct.composition, 'get').and.callFake((domainObject) => { - return { - load: async () => { - let obj = [childObject, parentObject, anotherParentObject].find((ob) => ob.identifier.key === domainObject.identifier.key); - let children = []; - - if (obj) { - for (let i = 0; i < obj.composition.length; i++) { - children.push(await openmct.objects.get(obj.composition[i])); - } - } - - return Promise.resolve(children); - }, - add: (child) => { - domainObject.composition.push(child.identifier); - } - }; - }); - - // already installed by default, but never hurts, just adds to context menu - openmct.install(DuplicateActionPlugin()); - openmct.types.addType('folder', {creatable: true}); - - openmct.on('start', done); - openmct.startHeadless(); + return Promise.resolve(children); + }, + add: (child) => { + domainObject.composition.push(child.identifier); + } + }; }); - afterEach(() => { - return resetApplicationState(openmct); + // already installed by default, but never hurts, just adds to context menu + openmct.install(DuplicateActionPlugin()); + openmct.types.addType('folder', { creatable: true }); + + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('should be defined', () => { + expect(DuplicateActionPlugin).toBeDefined(); + }); + + describe('when moving an object to a new parent', () => { + beforeEach(async () => { + duplicateTask = new DuplicateTask(openmct); + await duplicateTask.duplicate(parentObject, anotherParentObject); }); - it("should be defined", () => { - expect(DuplicateActionPlugin).toBeDefined(); + it("the duplicate child object's name (when not changing) should be the same as the original object", async () => { + let duplicatedObjectIdentifier = anotherParentObject.composition[0]; + let duplicatedObject = await openmct.objects.get(duplicatedObjectIdentifier); + let duplicateObjectName = duplicatedObject.name; + + expect(duplicateObjectName).toEqual(parentObject.name); }); - describe("when moving an object to a new parent", () => { - beforeEach(async () => { - duplicateTask = new DuplicateTask(openmct); - await duplicateTask.duplicate(parentObject, anotherParentObject); - }); + it("the duplicate child object's identifier should be new", () => { + let duplicatedObjectIdentifier = anotherParentObject.composition[0]; - it("the duplicate child object's name (when not changing) should be the same as the original object", async () => { - let duplicatedObjectIdentifier = anotherParentObject.composition[0]; - let duplicatedObject = await openmct.objects.get(duplicatedObjectIdentifier); - let duplicateObjectName = duplicatedObject.name; - - expect(duplicateObjectName).toEqual(parentObject.name); - }); - - it("the duplicate child object's identifier should be new", () => { - let duplicatedObjectIdentifier = anotherParentObject.composition[0]; - - expect(duplicatedObjectIdentifier.key).not.toEqual(parentObject.identifier.key); - }); + expect(duplicatedObjectIdentifier.key).not.toEqual(parentObject.identifier.key); }); + }); - describe("when a new name is provided for the duplicated object", () => { - it("the name is updated", async () => { - const NEW_NAME = 'New Name'; + describe('when a new name is provided for the duplicated object', () => { + it('the name is updated', async () => { + const NEW_NAME = 'New Name'; - duplicateTask = new DuplicateTask(openmct); - duplicateTask.changeName(NEW_NAME); - const child = await duplicateTask.duplicate(childObject, anotherParentObject); + duplicateTask = new DuplicateTask(openmct); + duplicateTask.changeName(NEW_NAME); + const child = await duplicateTask.duplicate(childObject, anotherParentObject); - expect(child.name).toEqual(NEW_NAME); - }); + expect(child.name).toEqual(NEW_NAME); }); - + }); }); diff --git a/src/plugins/exportAsJSONAction/ExportAsJSONAction.js b/src/plugins/exportAsJSONAction/ExportAsJSONAction.js index 5cf2ca0508..0f72ab2f9c 100644 --- a/src/plugins/exportAsJSONAction/ExportAsJSONAction.js +++ b/src/plugins/exportAsJSONAction/ExportAsJSONAction.js @@ -23,323 +23,323 @@ import JSONExporter from '/src/exporters/JSONExporter.js'; import { v4 as uuid } from 'uuid'; export default class ExportAsJSONAction { - constructor(openmct) { - this.openmct = openmct; + constructor(openmct) { + this.openmct = openmct; - this.name = 'Export as JSON'; - this.key = 'export.JSON'; - this.description = ''; - this.cssClass = "icon-export"; - this.group = "export"; - this.priority = 1; + this.name = 'Export as JSON'; + this.key = 'export.JSON'; + this.description = ''; + this.cssClass = 'icon-export'; + this.group = 'export'; + this.priority = 1; - this.tree = null; - this.calls = null; - this.idMap = null; + this.tree = null; + this.calls = null; + this.idMap = null; - this.JSONExportService = new JSONExporter(); + this.JSONExportService = new JSONExporter(); + } + + // Public + /** + * + * @param {object} objectPath + * @returns {boolean} + */ + appliesTo(objectPath) { + let domainObject = objectPath[0]; + + return this._isCreatableAndPersistable(domainObject); + } + /** + * + * @param {object} objectpath + */ + invoke(objectpath) { + this.tree = {}; + this.calls = 0; + this.idMap = {}; + + const root = objectpath[0]; + this.root = this._copy(root); + + const rootId = this._getKeystring(this.root); + this.tree[rootId] = this.root; + + this._write(this.root); + } + + /** + * @private + * @param {object} parent + */ + async _write(parent) { + this.calls++; + + //conditional object styles are not saved on the composition, so we need to check for them + const conditionSetIdentifier = this._getConditionSetIdentifier(parent); + const hasItemConditionSetIdentifiers = this._hasItemConditionSetIdentifiers(parent); + const composition = this.openmct.composition.get(parent); + + if (composition) { + const children = await composition.load(); + + children.forEach((child) => { + this._exportObject(child, parent); + }); } - // Public - /** - * - * @param {object} objectPath - * @returns {boolean} - */ - appliesTo(objectPath) { - let domainObject = objectPath[0]; + if (!conditionSetIdentifier && !hasItemConditionSetIdentifiers) { + this._decrementCallsAndSave(); + } else { + const conditionSetObjects = []; - return this._isCreatableAndPersistable(domainObject); - } - /** - * - * @param {object} objectpath - */ - invoke(objectpath) { - this.tree = {}; - this.calls = 0; - this.idMap = {}; + // conditionSetIdentifiers directly in objectStyles object + if (conditionSetIdentifier) { + conditionSetObjects.push(await this.openmct.objects.get(conditionSetIdentifier)); + } - const root = objectpath[0]; - this.root = this._copy(root); + // conditionSetIdentifiers stored on item ids in the objectStyles object + if (hasItemConditionSetIdentifiers) { + const itemConditionSetIdentifiers = this._getItemConditionSetIdentifiers(parent); - const rootId = this._getKeystring(this.root); - this.tree[rootId] = this.root; - - this._write(this.root); - } - - /** - * @private - * @param {object} parent - */ - async _write(parent) { - this.calls++; - - //conditional object styles are not saved on the composition, so we need to check for them - const conditionSetIdentifier = this._getConditionSetIdentifier(parent); - const hasItemConditionSetIdentifiers = this._hasItemConditionSetIdentifiers(parent); - const composition = this.openmct.composition.get(parent); - - if (composition) { - const children = await composition.load(); - - children.forEach((child) => { - this._exportObject(child, parent); - }); + for (const itemConditionSetIdentifier of itemConditionSetIdentifiers) { + conditionSetObjects.push(await this.openmct.objects.get(itemConditionSetIdentifier)); } + } - if (!conditionSetIdentifier && !hasItemConditionSetIdentifiers) { - this._decrementCallsAndSave(); - } else { - const conditionSetObjects = []; + for (const conditionSetObject of conditionSetObjects) { + this._exportObject(conditionSetObject, parent); + } - // conditionSetIdentifiers directly in objectStyles object - if (conditionSetIdentifier) { - conditionSetObjects.push(await this.openmct.objects.get(conditionSetIdentifier)); - } + this._decrementCallsAndSave(); + } + } - // conditionSetIdentifiers stored on item ids in the objectStyles object - if (hasItemConditionSetIdentifiers) { - const itemConditionSetIdentifiers = this._getItemConditionSetIdentifiers(parent); + _exportObject(child, parent) { + const originalKeyString = this._getKeystring(child); + const createable = this._isCreatableAndPersistable(child); + const isNotInfinite = !Object.prototype.hasOwnProperty.call(this.tree, originalKeyString); - for (const itemConditionSetIdentifier of itemConditionSetIdentifiers) { - conditionSetObjects.push(await this.openmct.objects.get(itemConditionSetIdentifier)); - } - } + if (createable && isNotInfinite) { + // for external or linked objects we generate new keys, if they don't exist already + if (this._isLinkedObject(child, parent)) { + child = this._rewriteLink(child, parent); + } else { + this.tree[originalKeyString] = child; + } - for (const conditionSetObject of conditionSetObjects) { - this._exportObject(conditionSetObject, parent); - } + this._write(child); + } + } - this._decrementCallsAndSave(); - } + /** + * @private + * @param {object} child + * @param {object} parent + * @returns {object} + */ + _rewriteLink(child, parent) { + const originalKeyString = this._getKeystring(child); + const parentKeyString = this._getKeystring(parent); + const conditionSetIdentifier = this._getConditionSetIdentifier(parent); + const hasItemConditionSetIdentifiers = this._hasItemConditionSetIdentifiers(parent); + const existingMappedKeyString = this.idMap[originalKeyString]; + let copy; + + if (!existingMappedKeyString) { + copy = this._copy(child); + copy.identifier.key = uuid(); + + if (!conditionSetIdentifier && !hasItemConditionSetIdentifiers) { + copy.location = parentKeyString; + } + + let newKeyString = this._getKeystring(copy); + this.idMap[originalKeyString] = newKeyString; + this.tree[newKeyString] = copy; + } else { + copy = this.tree[existingMappedKeyString]; } - _exportObject(child, parent) { - const originalKeyString = this._getKeystring(child); - const createable = this._isCreatableAndPersistable(child); - const isNotInfinite = !Object.prototype.hasOwnProperty.call(this.tree, originalKeyString); - - if (createable && isNotInfinite) { - // for external or linked objects we generate new keys, if they don't exist already - if (this._isLinkedObject(child, parent)) { - child = this._rewriteLink(child, parent); - } else { - this.tree[originalKeyString] = child; - } - - this._write(child); - } - } - - /** - * @private - * @param {object} child - * @param {object} parent - * @returns {object} - */ - _rewriteLink(child, parent) { - const originalKeyString = this._getKeystring(child); - const parentKeyString = this._getKeystring(parent); - const conditionSetIdentifier = this._getConditionSetIdentifier(parent); - const hasItemConditionSetIdentifiers = this._hasItemConditionSetIdentifiers(parent); - const existingMappedKeyString = this.idMap[originalKeyString]; - let copy; - - if (!existingMappedKeyString) { - copy = this._copy(child); - copy.identifier.key = uuid(); - - if (!conditionSetIdentifier && !hasItemConditionSetIdentifiers) { - copy.location = parentKeyString; - } - - let newKeyString = this._getKeystring(copy); - this.idMap[originalKeyString] = newKeyString; - this.tree[newKeyString] = copy; - } else { - copy = this.tree[existingMappedKeyString]; - } - - if (conditionSetIdentifier || hasItemConditionSetIdentifiers) { - - // update objectStyle object - if (conditionSetIdentifier) { - const directObjectStylesIdentifier = this.openmct.objects.areIdsEqual( - parent.configuration.objectStyles.conditionSetIdentifier, - child.identifier - ); - - if (directObjectStylesIdentifier) { - parent.configuration.objectStyles.conditionSetIdentifier = copy.identifier; - this.tree[parentKeyString].configuration.objectStyles.conditionSetIdentifier = copy.identifier; - } - } - - // update per item id on objectStyle object - if (hasItemConditionSetIdentifiers) { - for (const itemId in parent.configuration.objectStyles) { - if (parent.configuration.objectStyles[itemId]) { - const itemConditionSetIdentifier = parent.configuration.objectStyles[itemId].conditionSetIdentifier; - - if ( - itemConditionSetIdentifier - && this.openmct.objects.areIdsEqual(itemConditionSetIdentifier, child.identifier) - ) { - parent.configuration.objectStyles[itemId].conditionSetIdentifier = copy.identifier; - this.tree[parentKeyString].configuration.objectStyles[itemId].conditionSetIdentifier = copy.identifier; - } - } - } - } - } else { - // just update parent - const index = parent.composition.findIndex(identifier => { - return this.openmct.objects.areIdsEqual(child.identifier, identifier); - }); - - parent.composition[index] = copy.identifier; - this.tree[parentKeyString].composition[index] = copy.identifier; - } - - return copy; - } - - /** - * @private - * @param {object} domainObject - * @returns {string} A string representation of the given identifier, including namespace and key - */ - _getKeystring(domainObject) { - return this.openmct.objects.makeKeyString(domainObject.identifier); - } - - /** - * @private - * @param {object} domainObject - * @returns {boolean} - */ - _isCreatableAndPersistable(domainObject) { - const type = this.openmct.types.get(domainObject.type); - const isPersistable = this.openmct.objects.isPersistable(domainObject.identifier); - - return type && type.definition.creatable && isPersistable; - } - - /** - * @private - * @param {object} child - * @param {object} parent - * @returns {boolean} - */ - _isLinkedObject(child, parent) { - const rootKeyString = this._getKeystring(this.root); - const childKeyString = this._getKeystring(child); - const parentKeyString = this._getKeystring(parent); - - return (child.location !== parentKeyString - && !Object.keys(this.tree).includes(child.location) - && childKeyString !== rootKeyString) - || this.idMap[childKeyString] !== undefined; - } - - _getConditionSetIdentifier(object) { - return object.configuration?.objectStyles?.conditionSetIdentifier; - } - - _hasItemConditionSetIdentifiers(parent) { - const objectStyles = parent.configuration?.objectStyles; - - for (const itemId in objectStyles) { - if (Object.prototype.hasOwnProperty.call(objectStyles[itemId], 'conditionSetIdentifier')) { - return true; - } - } - - return false; - } - - _getItemConditionSetIdentifiers(parent) { - const objectStyles = parent.configuration?.objectStyles; - let identifiers = new Set(); - - if (objectStyles) { - Object.keys(objectStyles).forEach(itemId => { - if (objectStyles[itemId].conditionSetIdentifier) { - identifiers.add(objectStyles[itemId].conditionSetIdentifier); - } - }); - } - - return Array.from(identifiers); - } - - /** - * @private - */ - _rewriteReferences() { - const oldKeyStrings = Object.keys(this.idMap); - let treeString = JSON.stringify(this.tree); - - oldKeyStrings.forEach((oldKeyString) => { - // this will cover keyStrings, identifiers and identifiers created - // by hand that may be structured differently from those created with 'makeKeyString' - const newKeyString = this.idMap[oldKeyString]; - const newIdentifier = JSON.stringify(this.openmct.objects.parseKeyString(newKeyString)); - const oldIdentifier = this.openmct.objects.parseKeyString(oldKeyString); - const oldIdentifierNamespaceFirst = JSON.stringify(oldIdentifier); - const oldIdentifierKeyFirst = JSON.stringify({ - key: oldIdentifier.key, - namespace: oldIdentifier.namespace - }); - - // replace keyStrings - treeString = treeString.split(oldKeyString).join(newKeyString); - - // check for namespace first identifiers, replace if necessary - if (treeString.includes(oldIdentifierNamespaceFirst)) { - treeString = treeString.split(oldIdentifierNamespaceFirst).join(newIdentifier); - } - - // check for key first identifiers, replace if necessary - if (treeString.includes(oldIdentifierKeyFirst)) { - treeString = treeString.split(oldIdentifierKeyFirst).join(newIdentifier); - } - - }); - this.tree = JSON.parse(treeString); - } - /** - * @private - * @param {object} completedTree - */ - _saveAs(completedTree) { - this.JSONExportService.export( - completedTree, - { filename: this.root.name + '.json' } + if (conditionSetIdentifier || hasItemConditionSetIdentifiers) { + // update objectStyle object + if (conditionSetIdentifier) { + const directObjectStylesIdentifier = this.openmct.objects.areIdsEqual( + parent.configuration.objectStyles.conditionSetIdentifier, + child.identifier ); - } - /** - * @private - * @returns {object} - */ - _wrapTree() { - return { - "openmct": this.tree, - "rootId": this._getKeystring(this.root) - }; - } - _decrementCallsAndSave() { - this.calls--; - if (this.calls === 0) { - this._rewriteReferences(); - this._saveAs(this._wrapTree()); + if (directObjectStylesIdentifier) { + parent.configuration.objectStyles.conditionSetIdentifier = copy.identifier; + this.tree[parentKeyString].configuration.objectStyles.conditionSetIdentifier = + copy.identifier; } + } + + // update per item id on objectStyle object + if (hasItemConditionSetIdentifiers) { + for (const itemId in parent.configuration.objectStyles) { + if (parent.configuration.objectStyles[itemId]) { + const itemConditionSetIdentifier = + parent.configuration.objectStyles[itemId].conditionSetIdentifier; + + if ( + itemConditionSetIdentifier && + this.openmct.objects.areIdsEqual(itemConditionSetIdentifier, child.identifier) + ) { + parent.configuration.objectStyles[itemId].conditionSetIdentifier = copy.identifier; + this.tree[parentKeyString].configuration.objectStyles[itemId].conditionSetIdentifier = + copy.identifier; + } + } + } + } + } else { + // just update parent + const index = parent.composition.findIndex((identifier) => { + return this.openmct.objects.areIdsEqual(child.identifier, identifier); + }); + + parent.composition[index] = copy.identifier; + this.tree[parentKeyString].composition[index] = copy.identifier; } - _copy(object) { - return JSON.parse(JSON.stringify(object)); + return copy; + } + + /** + * @private + * @param {object} domainObject + * @returns {string} A string representation of the given identifier, including namespace and key + */ + _getKeystring(domainObject) { + return this.openmct.objects.makeKeyString(domainObject.identifier); + } + + /** + * @private + * @param {object} domainObject + * @returns {boolean} + */ + _isCreatableAndPersistable(domainObject) { + const type = this.openmct.types.get(domainObject.type); + const isPersistable = this.openmct.objects.isPersistable(domainObject.identifier); + + return type && type.definition.creatable && isPersistable; + } + + /** + * @private + * @param {object} child + * @param {object} parent + * @returns {boolean} + */ + _isLinkedObject(child, parent) { + const rootKeyString = this._getKeystring(this.root); + const childKeyString = this._getKeystring(child); + const parentKeyString = this._getKeystring(parent); + + return ( + (child.location !== parentKeyString && + !Object.keys(this.tree).includes(child.location) && + childKeyString !== rootKeyString) || + this.idMap[childKeyString] !== undefined + ); + } + + _getConditionSetIdentifier(object) { + return object.configuration?.objectStyles?.conditionSetIdentifier; + } + + _hasItemConditionSetIdentifiers(parent) { + const objectStyles = parent.configuration?.objectStyles; + + for (const itemId in objectStyles) { + if (Object.prototype.hasOwnProperty.call(objectStyles[itemId], 'conditionSetIdentifier')) { + return true; + } } + + return false; + } + + _getItemConditionSetIdentifiers(parent) { + const objectStyles = parent.configuration?.objectStyles; + let identifiers = new Set(); + + if (objectStyles) { + Object.keys(objectStyles).forEach((itemId) => { + if (objectStyles[itemId].conditionSetIdentifier) { + identifiers.add(objectStyles[itemId].conditionSetIdentifier); + } + }); + } + + return Array.from(identifiers); + } + + /** + * @private + */ + _rewriteReferences() { + const oldKeyStrings = Object.keys(this.idMap); + let treeString = JSON.stringify(this.tree); + + oldKeyStrings.forEach((oldKeyString) => { + // this will cover keyStrings, identifiers and identifiers created + // by hand that may be structured differently from those created with 'makeKeyString' + const newKeyString = this.idMap[oldKeyString]; + const newIdentifier = JSON.stringify(this.openmct.objects.parseKeyString(newKeyString)); + const oldIdentifier = this.openmct.objects.parseKeyString(oldKeyString); + const oldIdentifierNamespaceFirst = JSON.stringify(oldIdentifier); + const oldIdentifierKeyFirst = JSON.stringify({ + key: oldIdentifier.key, + namespace: oldIdentifier.namespace + }); + + // replace keyStrings + treeString = treeString.split(oldKeyString).join(newKeyString); + + // check for namespace first identifiers, replace if necessary + if (treeString.includes(oldIdentifierNamespaceFirst)) { + treeString = treeString.split(oldIdentifierNamespaceFirst).join(newIdentifier); + } + + // check for key first identifiers, replace if necessary + if (treeString.includes(oldIdentifierKeyFirst)) { + treeString = treeString.split(oldIdentifierKeyFirst).join(newIdentifier); + } + }); + this.tree = JSON.parse(treeString); + } + /** + * @private + * @param {object} completedTree + */ + _saveAs(completedTree) { + this.JSONExportService.export(completedTree, { filename: this.root.name + '.json' }); + } + /** + * @private + * @returns {object} + */ + _wrapTree() { + return { + openmct: this.tree, + rootId: this._getKeystring(this.root) + }; + } + + _decrementCallsAndSave() { + this.calls--; + if (this.calls === 0) { + this._rewriteReferences(); + this._saveAs(this._wrapTree()); + } + } + + _copy(object) { + return JSON.parse(JSON.stringify(object)); + } } diff --git a/src/plugins/exportAsJSONAction/ExportAsJSONActionSpec.js b/src/plugins/exportAsJSONAction/ExportAsJSONActionSpec.js index 7ecb3b9637..3e4f17aa36 100644 --- a/src/plugins/exportAsJSONAction/ExportAsJSONActionSpec.js +++ b/src/plugins/exportAsJSONAction/ExportAsJSONActionSpec.js @@ -1,378 +1,389 @@ -import { - createOpenMct, - resetApplicationState -} from 'utils/testing'; +import { createOpenMct, resetApplicationState } from 'utils/testing'; describe('Export as JSON plugin', () => { - const ACTION_KEY = 'export.JSON'; + const ACTION_KEY = 'export.JSON'; - let openmct; - let domainObject; - let exportAsJSONAction; + let openmct; + let domainObject; + let exportAsJSONAction; - beforeEach((done) => { - openmct = createOpenMct(); + beforeEach((done) => { + openmct = createOpenMct(); - openmct.on('start', done); - openmct.startHeadless(); + openmct.on('start', done); + openmct.startHeadless(); - exportAsJSONAction = openmct.actions.getAction(ACTION_KEY); + exportAsJSONAction = openmct.actions.getAction(ACTION_KEY); + }); + + afterEach(() => resetApplicationState(openmct)); + + it('Export as JSON action exist', () => { + expect(exportAsJSONAction.key).toEqual(ACTION_KEY); + }); + + it('ExportAsJSONAction applies to folder', () => { + domainObject = { + identifier: { + key: 'export-testing', + namespace: '' + }, + composition: [], + location: 'mine', + modified: 1640115501237, + name: 'Unnamed Folder', + persisted: 1640115501237, + type: 'folder' + }; + + expect(exportAsJSONAction.appliesTo([domainObject])).toEqual(true); + }); + + it('ExportAsJSONAction applies to telemetry.plot.overlay', () => { + domainObject = { + identifier: { + key: 'export-testing', + namespace: '' + }, + composition: [], + location: 'mine', + modified: 1640115501237, + name: 'Unnamed Plot', + persisted: 1640115501237, + type: 'telemetry.plot.overlay' + }; + + expect(exportAsJSONAction.appliesTo([domainObject])).toEqual(true); + }); + + it('ExportAsJSONAction applies to telemetry.plot.stacked', () => { + domainObject = { + identifier: { + key: 'export-testing', + namespace: '' + }, + composition: [], + location: 'mine', + modified: 1640115501237, + name: 'Unnamed Plot', + persisted: 1640115501237, + type: 'telemetry.plot.stacked' + }; + + expect(exportAsJSONAction.appliesTo([domainObject])).toEqual(true); + }); + + it('ExportAsJSONAction does not applie to non-persistable objects', () => { + domainObject = { + identifier: { + key: 'export-testing', + namespace: '' + }, + composition: [], + location: 'mine', + modified: 1640115501237, + name: 'Non Editable Folder', + persisted: 1640115501237, + type: 'folder' + }; + + spyOn(openmct.objects, 'getProvider').and.callFake(() => { + return { get: () => domainObject }; }); - afterEach(() => resetApplicationState(openmct)); + expect(exportAsJSONAction.appliesTo([domainObject])).toEqual(false); + }); - it('Export as JSON action exist', () => { - expect(exportAsJSONAction.key).toEqual(ACTION_KEY); + it('ExportAsJSONAction exports object from tree', (done) => { + const parent = { + composition: [ + { + 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: 'parent', + persisted: 1503598132428 + }; + + spyOn(openmct.composition, 'get').and.callFake((object) => { + return { + load: () => { + if (object.name === 'Parent') { + return Promise.resolve([child]); + } + + return Promise.resolve([]); + } + }; }); - it('ExportAsJSONAction applies to folder', () => { - domainObject = { - identifier: { - key: 'export-testing', - namespace: '' - }, - composition: [], - location: 'mine', - modified: 1640115501237, - name: 'Unnamed Folder', - persisted: 1640115501237, - type: 'folder' - }; + spyOn(exportAsJSONAction, '_saveAs').and.callFake((completedTree) => { + expect(Object.keys(completedTree).length).toBe(2); + 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(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'child')).toBeTruthy(); - expect(exportAsJSONAction.appliesTo([domainObject])).toEqual(true); + done(); }); - it('ExportAsJSONAction applies to telemetry.plot.overlay', () => { - domainObject = { - identifier: { - key: 'export-testing', - namespace: '' - }, - composition: [], - location: 'mine', - modified: 1640115501237, - name: 'Unnamed Plot', - persisted: 1640115501237, - type: 'telemetry.plot.overlay' - }; + exportAsJSONAction.invoke([parent]); + }); - expect(exportAsJSONAction.appliesTo([domainObject])).toEqual(true); + it('ExportAsJSONAction skips non-creatable objects from tree', (done) => { + const parent = { + composition: [ + { + key: 'child', + namespace: '' + } + ], + identifier: { + key: 'parent', + namespace: '' + }, + name: 'Parent of Non Editable Child Folder', + type: 'folder', + modified: 1503598129176, + location: 'mine', + persisted: 1503598129176 + }; + + const child = { + composition: [], + identifier: { + key: 'child', + namespace: '' + }, + name: 'Non Editable Child Folder', + type: 'noneditable.folder', + modified: 1503598132428, + location: 'parent', + persisted: 1503598132428 + }; + + spyOn(openmct.composition, 'get').and.callFake((object) => { + return { + load: () => { + if (object.identifier.key === 'parent') { + return Promise.resolve([child]); + } + + return Promise.resolve([]); + } + }; }); - it('ExportAsJSONAction applies to telemetry.plot.stacked', () => { - domainObject = { - identifier: { - key: 'export-testing', - namespace: '' - }, - composition: [], - location: 'mine', - modified: 1640115501237, - name: 'Unnamed Plot', - persisted: 1640115501237, - type: 'telemetry.plot.stacked' - }; + spyOn(exportAsJSONAction, '_saveAs').and.callFake((completedTree) => { + expect(Object.keys(completedTree).length).toBe(2); + 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(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'child')).not.toBeTruthy(); - expect(exportAsJSONAction.appliesTo([domainObject])).toEqual(true); + done(); }); - it('ExportAsJSONAction does not applie to non-persistable objects', () => { - domainObject = { - identifier: { - key: 'export-testing', - namespace: '' - }, - composition: [], - location: 'mine', - modified: 1640115501237, - name: 'Non Editable Folder', - persisted: 1640115501237, - type: 'folder' - }; + exportAsJSONAction.invoke([parent]); + }); - spyOn(openmct.objects, 'getProvider').and.callFake(() => { - return { get: () => domainObject }; - }); + it('can export self-containing objects', (done) => { + const parent = { + composition: [ + { + key: 'infinteChild', + namespace: '' + } + ], + identifier: { + key: 'infiniteParent', + namespace: '' + }, + name: 'parent', + type: 'folder', + modified: 1503598129176, + location: 'mine', + persisted: 1503598129176 + }; - expect(exportAsJSONAction.appliesTo([domainObject])).toEqual(false); + const child = { + composition: [ + { + key: 'infiniteParent', + namespace: '' + } + ], + identifier: { + key: 'infinteChild', + namespace: '' + }, + name: 'child', + type: 'folder', + modified: 1503598132428, + location: 'infiniteParent', + persisted: 1503598132428 + }; + + spyOn(openmct.composition, 'get').and.callFake((object) => { + return { + load: () => { + if (object.name === 'parent') { + return Promise.resolve([child]); + } + + return Promise.resolve([]); + } + }; }); - it('ExportAsJSONAction exports object from tree', (done) => { - const parent = { - composition: [{ - key: 'child', - namespace: '' - }], - identifier: { - key: 'parent', - namespace: '' - }, - name: 'Parent', - type: 'folder', - modified: 1503598129176, - location: 'mine', - persisted: 1503598129176 - }; + spyOn(exportAsJSONAction, '_saveAs').and.callFake((completedTree) => { + expect(Object.keys(completedTree).length).toBe(2); + expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy(); + expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy(); + expect( + Object.prototype.hasOwnProperty.call(completedTree.openmct, 'infiniteParent') + ).toBeTruthy(); + expect( + Object.prototype.hasOwnProperty.call(completedTree.openmct, 'infinteChild') + ).toBeTruthy(); - const child = { - composition: [], - identifier: { - key: 'child', - namespace: '' - }, - name: 'Child', - type: 'folder', - modified: 1503598132428, - location: 'parent', - persisted: 1503598132428 - }; - - spyOn(openmct.composition, 'get').and.callFake(object => { - return { - load: () => { - if (object.name === 'Parent') { - return Promise.resolve([child]); - } - - return Promise.resolve([]); - } - }; - }); - - spyOn(exportAsJSONAction, '_saveAs').and.callFake(completedTree => { - expect(Object.keys(completedTree).length).toBe(2); - 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(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'child')).toBeTruthy(); - - done(); - }); - - exportAsJSONAction.invoke([parent]); + done(); }); - it('ExportAsJSONAction skips non-creatable objects from tree', (done) => { - const parent = { - composition: [{ - key: 'child', - namespace: '' - }], - identifier: { - key: 'parent', - namespace: '' - }, - name: 'Parent of Non Editable Child Folder', - type: 'folder', - modified: 1503598129176, - location: 'mine', - persisted: 1503598129176 - }; + exportAsJSONAction.invoke([parent]); + }); - const child = { - composition: [], - identifier: { - key: 'child', - namespace: '' - }, - name: 'Non Editable Child Folder', - type: 'noneditable.folder', - modified: 1503598132428, - location: 'parent', - persisted: 1503598132428 - }; + it('exports links to external objects as new objects', function (done) { + const parent = { + composition: [ + { + key: 'child', + namespace: '' + } + ], + identifier: { + key: 'parent', + namespace: '' + }, + name: 'Parent', + type: 'folder', + modified: 1503598129176, + location: 'mine', + persisted: 1503598129176 + }; - spyOn(openmct.composition, 'get').and.callFake(object => { - return { - load: () => { - if (object.identifier.key === 'parent') { - return Promise.resolve([child]); - } + const child = { + composition: [], + identifier: { + key: 'child', + namespace: '' + }, + name: 'Child', + type: 'folder', + modified: 1503598132428, + location: 'outsideOfTree', + persisted: 1503598132428 + }; - return Promise.resolve([]); - } - }; - }); + spyOn(openmct.composition, 'get').and.callFake((object) => { + return { + load: () => { + if (object.name === 'Parent') { + return Promise.resolve([child]); + } - spyOn(exportAsJSONAction, '_saveAs').and.callFake(completedTree => { - expect(Object.keys(completedTree).length).toBe(2); - 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(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'child')).not.toBeTruthy(); - - done(); - }); - - exportAsJSONAction.invoke([parent]); + return Promise.resolve([]); + } + }; }); - it('can export self-containing objects', (done) => { - const parent = { - composition: [{ - key: 'infinteChild', - namespace: '' - }], - identifier: { - key: 'infiniteParent', - namespace: '' - }, - name: 'parent', - type: 'folder', - modified: 1503598129176, - location: 'mine', - persisted: 1503598129176 - }; + spyOn(exportAsJSONAction, '_saveAs').and.callFake((completedTree) => { + expect(Object.keys(completedTree).length).toBe(2); + 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(); - const child = { - composition: [{ - key: 'infiniteParent', - namespace: '' - }], - identifier: { - key: 'infinteChild', - namespace: '' - }, - name: 'child', - type: 'folder', - modified: 1503598132428, - location: 'infiniteParent', - persisted: 1503598132428 - }; + // parent and child objects as part of openmct but child with new id/key + expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'child')).not.toBeTruthy(); + expect(Object.keys(completedTree.openmct).length).toBe(2); - spyOn(openmct.composition, 'get').and.callFake(object => { - return { - load: () => { - if (object.name === 'parent') { - return Promise.resolve([child]); - } - - return Promise.resolve([]); - } - }; - }); - - spyOn(exportAsJSONAction, '_saveAs').and.callFake(completedTree => { - expect(Object.keys(completedTree).length).toBe(2); - expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy(); - expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy(); - expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'infiniteParent')).toBeTruthy(); - expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'infinteChild')).toBeTruthy(); - - done(); - }); - - exportAsJSONAction.invoke([parent]); + done(); }); - it('exports links to external objects as new objects', function (done) { - const parent = { - composition: [{ - key: 'child', - namespace: '' - }], - identifier: { - key: 'parent', - namespace: '' - }, - name: 'Parent', - type: 'folder', - modified: 1503598129176, - location: 'mine', - persisted: 1503598129176 - }; + exportAsJSONAction.invoke([parent]); + }); - const child = { - composition: [], - identifier: { - key: 'child', - namespace: '' - }, - name: 'Child', - type: 'folder', - modified: 1503598132428, - location: 'outsideOfTree', - persisted: 1503598132428 - }; + 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 + }; - spyOn(openmct.composition, 'get').and.callFake(object => { - return { - load: () => { - if (object.name === 'Parent') { - return Promise.resolve([child]); - } + const child = { + composition: [], + identifier: { + key: 'child', + namespace: '' + }, + name: 'Child', + type: 'folder', + modified: 1503598132428, + location: null, + persisted: 1503598132428 + }; - return Promise.resolve([]); - } - }; - }); - - spyOn(exportAsJSONAction, '_saveAs').and.callFake(completedTree => { - expect(Object.keys(completedTree).length).toBe(2); - 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(); - - // parent and child objects as part of openmct but child with new id/key - expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'child')).not.toBeTruthy(); - expect(Object.keys(completedTree.openmct).length).toBe(2); - - done(); - }); - - exportAsJSONAction.invoke([parent]); + spyOn(openmct.objects, 'get').and.callFake((object) => { + return Promise.resolve(child); }); - 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 - }; + 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'); - 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]); + done(); }); + + exportAsJSONAction.invoke([parent]); + }); }); diff --git a/src/plugins/exportAsJSONAction/plugin.js b/src/plugins/exportAsJSONAction/plugin.js index a6fa6c0adb..20e64fa88d 100644 --- a/src/plugins/exportAsJSONAction/plugin.js +++ b/src/plugins/exportAsJSONAction/plugin.js @@ -22,7 +22,7 @@ import ExportAsJSONAction from './ExportAsJSONAction'; export default function () { - return function (openmct) { - openmct.actions.register(new ExportAsJSONAction(openmct)); - }; + return function (openmct) { + openmct.actions.register(new ExportAsJSONAction(openmct)); + }; } diff --git a/src/plugins/faultManagement/FaultManagementInspector.vue b/src/plugins/faultManagement/FaultManagementInspector.vue index 465c9252cc..ba41e665b6 100644 --- a/src/plugins/faultManagement/FaultManagementInspector.vue +++ b/src/plugins/faultManagement/FaultManagementInspector.vue @@ -21,109 +21,102 @@ --> diff --git a/src/plugins/faultManagement/FaultManagementInspectorViewProvider.js b/src/plugins/faultManagement/FaultManagementInspectorViewProvider.js index 3b5479fbce..33f08a93c4 100644 --- a/src/plugins/faultManagement/FaultManagementInspectorViewProvider.js +++ b/src/plugins/faultManagement/FaultManagementInspectorViewProvider.js @@ -27,45 +27,45 @@ 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 Configuration', - canView: (selection) => { - if (selection.length !== 1 || selection[0].length === 0) { - return false; - } + return { + openmct: openmct, + key: FAULT_MANAGEMENT_INSPECTOR, + name: 'Fault Management Configuration', + canView: (selection) => { + if (selection.length !== 1 || selection[0].length === 0) { + return false; + } - let object = selection[0][0].context.item; + let object = selection[0][0].context.item; - return object && object.type === FAULT_MANAGEMENT_TYPE; + 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: '' + }); }, - view: (selection) => { - let component; - - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - FaultManagementInspector - }, - provide: { - openmct - }, - template: '' - }); - }, - priority: function () { - return openmct.priority.HIGH + 1; - }, - destroy: function () { - if (component) { - component.$destroy(); - component = undefined; - } - } - }; + priority: function () { + return openmct.priority.HIGH + 1; + }, + destroy: function () { + if (component) { + component.$destroy(); + component = undefined; + } } - }; + }; + } + }; } diff --git a/src/plugins/faultManagement/FaultManagementListHeader.vue b/src/plugins/faultManagement/FaultManagementListHeader.vue index fdddcc06a7..9b84900025 100644 --- a/src/plugins/faultManagement/FaultManagementListHeader.vue +++ b/src/plugins/faultManagement/FaultManagementListHeader.vue @@ -21,35 +21,33 @@ --> diff --git a/src/plugins/faultManagement/FaultManagementListItem.vue b/src/plugins/faultManagement/FaultManagementListItem.vue index e867ca7e7d..21cdce53aa 100644 --- a/src/plugins/faultManagement/FaultManagementListItem.vue +++ b/src/plugins/faultManagement/FaultManagementListItem.vue @@ -21,203 +21,186 @@ --> diff --git a/src/plugins/faultManagement/FaultManagementListView.vue b/src/plugins/faultManagement/FaultManagementListView.vue index f3514a350e..2a9dd4bb17 100644 --- a/src/plugins/faultManagement/FaultManagementListView.vue +++ b/src/plugins/faultManagement/FaultManagementListView.vue @@ -21,46 +21,46 @@ --> diff --git a/src/plugins/faultManagement/FaultManagementObjectProvider.js b/src/plugins/faultManagement/FaultManagementObjectProvider.js index c3cec8f09d..03028e57dc 100644 --- a/src/plugins/faultManagement/FaultManagementObjectProvider.js +++ b/src/plugins/faultManagement/FaultManagementObjectProvider.js @@ -20,37 +20,41 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { FAULT_MANAGEMENT_TYPE, FAULT_MANAGEMENT_VIEW, FAULT_MANAGEMENT_NAMESPACE } from './constants'; +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 = {}; + constructor(openmct) { + this.openmct = openmct; + this.namespace = FAULT_MANAGEMENT_NAMESPACE; + this.key = FAULT_MANAGEMENT_VIEW; + this.objects = {}; - this.createFaultManagementRootObject(); + 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); } - 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(); - } + return Promise.reject(); + } } diff --git a/src/plugins/faultManagement/FaultManagementPlugin.js b/src/plugins/faultManagement/FaultManagementPlugin.js index 4a113db76e..d10f991384 100644 --- a/src/plugins/faultManagement/FaultManagementPlugin.js +++ b/src/plugins/faultManagement/FaultManagementPlugin.js @@ -27,16 +27,19 @@ import FaultManagementInspectorViewProvider from './FaultManagementInspectorView 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' - }); + 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)); - }; + 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 index af67833fcc..a424e78b93 100644 --- a/src/plugins/faultManagement/FaultManagementSearch.vue +++ b/src/plugins/faultManagement/FaultManagementSearch.vue @@ -21,21 +21,21 @@ --> diff --git a/src/plugins/faultManagement/FaultManagementToolbar.vue b/src/plugins/faultManagement/FaultManagementToolbar.vue index 0deb7a25eb..68cfd58b27 100644 --- a/src/plugins/faultManagement/FaultManagementToolbar.vue +++ b/src/plugins/faultManagement/FaultManagementToolbar.vue @@ -21,82 +21,72 @@ --> diff --git a/src/plugins/faultManagement/FaultManagementView.vue b/src/plugins/faultManagement/FaultManagementView.vue index d803353d87..9babe82705 100644 --- a/src/plugins/faultManagement/FaultManagementView.vue +++ b/src/plugins/faultManagement/FaultManagementView.vue @@ -21,58 +21,52 @@ --> diff --git a/src/plugins/faultManagement/FaultManagementViewProvider.js b/src/plugins/faultManagement/FaultManagementViewProvider.js index 9045d5edf8..2687b3818b 100644 --- a/src/plugins/faultManagement/FaultManagementViewProvider.js +++ b/src/plugins/faultManagement/FaultManagementViewProvider.js @@ -25,45 +25,45 @@ 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; - } + constructor(openmct) { + this.openmct = openmct; + this.key = FAULT_MANAGEMENT_VIEW; + } - canView(domainObject) { - return domainObject.type === FAULT_MANAGEMENT_TYPE; - } + canView(domainObject) { + return domainObject.type === FAULT_MANAGEMENT_TYPE; + } - canEdit(domainObject) { - return false; - } + canEdit(domainObject) { + return false; + } - view(domainObject) { - let component; - const openmct = this.openmct; + view(domainObject) { + let component; + const openmct = this.openmct; - return { - show: (element) => { - component = new Vue({ - el: element, - components: { - FaultManagementView - }, - provide: { - openmct, - domainObject - }, - template: '' - }); - }, - destroy: () => { - if (!component) { - return; - } + return { + show: (element) => { + component = new Vue({ + el: element, + components: { + FaultManagementView + }, + provide: { + openmct, + domainObject + }, + template: '' + }); + }, + destroy: () => { + if (!component) { + return; + } - component.$destroy(); - component = undefined; - } - }; - } + component.$destroy(); + component = undefined; + } + }; + } } diff --git a/src/plugins/faultManagement/constants.js b/src/plugins/faultManagement/constants.js index 01889c96ae..0e27bcca11 100644 --- a/src/plugins/faultManagement/constants.js +++ b/src/plugins/faultManagement/constants.js @@ -21,21 +21,21 @@ *****************************************************************************/ const FAULT_SEVERITY = { - 'CRITICAL': { - name: 'CRITICAL', - value: 'critical', - priority: 0 - }, - 'WARNING': { - name: 'WARNING', - value: 'warning', - priority: 1 - }, - 'WATCH': { - name: 'WATCH', - value: 'watch', - priority: 2 - } + 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'; @@ -43,80 +43,75 @@ 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 - } + { + 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 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; - } + '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; - } + 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; - } + 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 index 4d01e4a8bb..435211fbf4 100644 --- a/src/plugins/faultManagement/fault-manager.scss +++ b/src/plugins/faultManagement/fault-manager.scss @@ -25,244 +25,243 @@ $colorFaultItemBg: pullForward($colorBodyBg, 5%); /*********************************************** SEARCH */ .c-fault-mgmt__search-row { - display: flex; - align-items: center; - flex: 0 0 auto; - > * + * { - margin-left: 10px; - float: right; - } + display: flex; + align-items: center; + flex: 0 0 auto; + > * + * { + margin-left: 10px; + float: right; + } } .c-fault-mgmt-search { - width: 95%; + width: 95%; } /*********************************************** TOOLBAR */ .c-fault-mgmt__toolbar { - display: flex; - justify-content: center; - flex: 0 0 auto; - > * + * { - margin-left: $interiorMargin; - } + 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%; + display: flex; + flex-direction: column; + height: 100%; - > * + * { - margin-top: $interiorMargin; - } + > * + * { + 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; + 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; - } + &-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; - } + .--width-less-than-600 & { + grid-template-columns: max-content max-content 1fr 1fr max-content; + } } .c-faults-list-view-item-body { - display: contents; + 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; - color: $colorFaultItemFg; - &-checkbox{ - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; + .--width-less-than-600 & { + display: flex; + flex-wrap: wrap; + grid-column: span 2; } + } - &-severity { - font-size: 2em; + &-pathname { + padding-right: $interiorMarginLg; + overflow-wrap: anywhere; + min-width: 100px; + } + &-path { + font-size: 0.85em; + margin-left: $interiorMargin; + } - &.is-severity-critical { - @include glyphBefore($glyph-icon-alert-triangle); - color: $colorStatusError; - } + &-faultname { + font-size: 1.3em; + margin-left: $interiorMargin; + } - &.is-severity-warning { - @include glyphBefore($glyph-icon-alert-rect); - color: $colorStatusAlert; - } + &-content-right { + display: contents; + } - &.is-severity-watch { - @include glyphBefore($glyph-icon-info); - color: $colorCommand; - } + &-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); } + } - &-content { - display: contents; + &.is-acknowledged, + &.is-shelved { + .c-fault-mgmt__list-severity { + &:before { + opacity: 60%; + //font-size: 1.5em; + } - .--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 { + &:after { color: $colorFaultItemFgEmphasis; - .c-fault-mgmt__list-severity { - @include pulse($animName: severityAnim, $dur: 200ms); - } + 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-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-shelved { - .c-fault-mgmt__list-pathname { - font-style: italic; - } - } + &.is-acknowledged .c-fault-mgmt__list-severity:after { + content: $glyph-icon-check; + } - &.is-acknowledged .c-fault-mgmt__list-severity:after { - content: $glyph-icon-check; - } - - &.is-shelved .c-fault-mgmt__list-severity:after { - content: $glyph-icon-timer; - } + &.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; + display: contents; + border-radius: $controlCr; + align-items: center; - * { - margin: 0px; - border-radius: 0px; + * { + 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 & { - .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-column: 4 / span 2; } + } } /*********************************************** GRID ITEM */ .c-fault-mgmt-item { - $p: $interiorMargin; + $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; - background: $colorFaultItemBg; - white-space: nowrap; + position: sticky; + top: 0; + z-index: 2; + } - &-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; + } - &__value { - @include isLimit(); - background: rgba($colorBodyFg, 0.1); - padding: $p; - border-radius: $controlCr; - display: inline-flex; - } - - .is-selected & { - background: $colorSelectedBg; - } + .is-selected & { + background: $colorSelectedBg; + } } diff --git a/src/plugins/faultManagement/pluginSpec.js b/src/plugins/faultManagement/pluginSpec.js index 495679c984..e282a7dba3 100644 --- a/src/plugins/faultManagement/pluginSpec.js +++ b/src/plugins/faultManagement/pluginSpec.js @@ -20,85 +20,87 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ +import { createOpenMct, resetApplicationState } from '../../utils/testing'; import { - createOpenMct, - resetApplicationState -} from '../../utils/testing'; -import { - FAULT_MANAGEMENT_TYPE, - FAULT_MANAGEMENT_VIEW, - FAULT_MANAGEMENT_NAMESPACE + 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' - } - }; +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 = createOpenMct(); + openmct.install(openmct.plugins.FaultManagement()); }); - afterEach(() => { - return resetApplicationState(openmct); + 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('is not installed by default', () => { - const typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition; + it('provides an inspector view for fault management types', () => { + const faultDomainObjectSelection = [ + [ + { + context: { + item: faultDomainObject + } + } + ] + ]; + const applicableInspectorViews = openmct.inspectorViews.get(faultDomainObjectSelection); + const faultManagementInspectorView = applicableInspectorViews.filter( + (view) => view.name === 'Fault Management Configuration' + ); - expect(typeDef.name).toBe('Unknown Type'); + expect(faultManagementInspectorView.length).toEqual(1); }); - 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); - const faultManagementInspectorView = applicableInspectorViews.filter(view => view.name === 'Fault Management Configuration'); - - expect(faultManagementInspectorView.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(); - }); + 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/filters/FiltersInspectorViewProvider.js b/src/plugins/filters/FiltersInspectorViewProvider.js index 6d568cc81b..d71fbd2fa1 100644 --- a/src/plugins/filters/FiltersInspectorViewProvider.js +++ b/src/plugins/filters/FiltersInspectorViewProvider.js @@ -20,60 +20,53 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './components/FiltersView.vue', - 'vue' -], function ( - FiltersView, - Vue -) { +define(['./components/FiltersView.vue', 'vue'], function (FiltersView, Vue) { + function FiltersInspectorViewProvider(openmct, supportedObjectTypesArray) { + return { + key: 'filters-inspector', + name: 'Filters', + canView: function (selection) { + const domainObject = selection?.[0]?.[0]?.context?.item; + + return domainObject && supportedObjectTypesArray.some((type) => domainObject.type === type); + }, + view: function (selection) { + let component; + + const domainObject = selection?.[0]?.[0]?.context?.item; - function FiltersInspectorViewProvider(openmct, supportedObjectTypesArray) { return { - key: 'filters-inspector', - name: 'Filters', - canView: function (selection) { - const domainObject = selection?.[0]?.[0]?.context?.item; + show: function (element) { + component = new Vue({ + el: element, + components: { + FiltersView: FiltersView.default + }, + provide: { + openmct + }, + template: '' + }); + }, + showTab: function (isEditing) { + const hasPersistedFilters = Boolean(domainObject?.configuration?.filters); + const hasGlobalFilters = Boolean(domainObject?.configuration?.globalFilters); - return domainObject && supportedObjectTypesArray.some(type => domainObject.type === type); - }, - view: function (selection) { - let component; - - const domainObject = selection?.[0]?.[0]?.context?.item; - - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - FiltersView: FiltersView.default - }, - provide: { - openmct - }, - template: '' - }); - }, - showTab: function (isEditing) { - const hasPersistedFilters = Boolean(domainObject?.configuration?.filters); - const hasGlobalFilters = Boolean(domainObject?.configuration?.globalFilters); - - return hasPersistedFilters || hasGlobalFilters; - }, - priority: function () { - return openmct.priority.DEFAULT; - }, - destroy: function () { - if (component) { - component.$destroy(); - component = undefined; - } - } - }; + return hasPersistedFilters || hasGlobalFilters; + }, + priority: function () { + return openmct.priority.DEFAULT; + }, + destroy: function () { + if (component) { + component.$destroy(); + component = undefined; } + } }; - } + } + }; + } - return FiltersInspectorViewProvider; + return FiltersInspectorViewProvider; }); diff --git a/src/plugins/filters/components/FilterField.vue b/src/plugins/filters/components/FilterField.vue index 315e6ce354..d8edc53fff 100644 --- a/src/plugins/filters/components/FilterField.vue +++ b/src/plugins/filters/components/FilterField.vue @@ -20,133 +20,132 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/filters/components/FilterObject.vue b/src/plugins/filters/components/FilterObject.vue index 87cd45878d..bb5881496d 100644 --- a/src/plugins/filters/components/FilterObject.vue +++ b/src/plugins/filters/components/FilterObject.vue @@ -20,64 +20,55 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/filters/components/FiltersView.vue b/src/plugins/filters/components/FiltersView.vue index 2e77232f19..978cb10a8f 100644 --- a/src/plugins/filters/components/FiltersView.vue +++ b/src/plugins/filters/components/FiltersView.vue @@ -20,30 +20,24 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/filters/components/GlobalFilters.vue b/src/plugins/filters/components/GlobalFilters.vue index 0fbf640dd6..20711d67ef 100644 --- a/src/plugins/filters/components/GlobalFilters.vue +++ b/src/plugins/filters/components/GlobalFilters.vue @@ -20,119 +20,111 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/filters/components/filters-view.scss b/src/plugins/filters/components/filters-view.scss index 261c269bdb..40b32bc4d9 100644 --- a/src/plugins/filters/components/filters-view.scss +++ b/src/plugins/filters/components/filters-view.scss @@ -1,15 +1,15 @@ .c-inspector { - .c-filter-indication { - border-radius: $smallCr; - font-size: inherit; - padding: $interiorMarginSm $interiorMargin; - text-transform: inherit; - } - .c-filter-tree { - // Filters UI uses a tree-based structure - .c-inspect-properties { - // Add extra margin to account for filter-indicator - margin-left: 38px; - } + .c-filter-indication { + border-radius: $smallCr; + font-size: inherit; + padding: $interiorMarginSm $interiorMargin; + text-transform: inherit; + } + .c-filter-tree { + // Filters UI uses a tree-based structure + .c-inspect-properties { + // Add extra margin to account for filter-indicator + margin-left: 38px; } + } } diff --git a/src/plugins/filters/components/global-filters.scss b/src/plugins/filters/components/global-filters.scss index 8839eae53a..3e1335665d 100644 --- a/src/plugins/filters/components/global-filters.scss +++ b/src/plugins/filters/components/global-filters.scss @@ -1,34 +1,34 @@ .c-filter-indication { - // Appears as a block element beneath tables - @include userSelectNone(); - background: $colorFilterBg; - color: $colorFilterFg; + // Appears as a block element beneath tables + @include userSelectNone(); + background: $colorFilterBg; + color: $colorFilterFg; - &:before { - font-family: symbolsfont-12px; - content: $glyph-icon-filter; - margin-right: $interiorMarginSm; - } + &:before { + font-family: symbolsfont-12px; + content: $glyph-icon-filter; + margin-right: $interiorMarginSm; + } - &--mixed { - .c-filter-indication__mixed { - font-style: italic; - } + &--mixed { + .c-filter-indication__mixed { + font-style: italic; } + } - &__label { - + .c-filter-indication__label { - &:before { - content: ', '; - } - } + &__label { + + .c-filter-indication__label { + &:before { + content: ', '; + } } + } } .c-filter-tree-item { - &__filter-indicator { - color: $colorFilter; - width: 1.2em; // Set width explicitly for layout reasons: will either have class icon-filter, or none. - flex: 0 0 auto; - } + &__filter-indicator { + color: $colorFilter; + width: 1.2em; // Set width explicitly for layout reasons: will either have class icon-filter, or none. + flex: 0 0 auto; + } } diff --git a/src/plugins/filters/plugin.js b/src/plugins/filters/plugin.js index 0759517459..2c2fa90b38 100644 --- a/src/plugins/filters/plugin.js +++ b/src/plugins/filters/plugin.js @@ -20,14 +20,12 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './FiltersInspectorViewProvider' -], function ( - FiltersInspectorViewProvider -) { - return function plugin(supportedObjectTypesArray) { - return function install(openmct) { - openmct.inspectorViews.addProvider(new FiltersInspectorViewProvider(openmct, supportedObjectTypesArray)); - }; +define(['./FiltersInspectorViewProvider'], function (FiltersInspectorViewProvider) { + return function plugin(supportedObjectTypesArray) { + return function install(openmct) { + openmct.inspectorViews.addProvider( + new FiltersInspectorViewProvider(openmct, supportedObjectTypesArray) + ); }; + }; }); diff --git a/src/plugins/flexibleLayout/components/container.vue b/src/plugins/flexibleLayout/components/container.vue index fd0aa7171d..66eebb5e67 100644 --- a/src/plugins/flexibleLayout/components/container.vue +++ b/src/plugins/flexibleLayout/components/container.vue @@ -21,62 +21,60 @@ --> diff --git a/src/plugins/flexibleLayout/components/dropHint.vue b/src/plugins/flexibleLayout/components/dropHint.vue index 395637baff..ba4870fbf5 100644 --- a/src/plugins/flexibleLayout/components/dropHint.vue +++ b/src/plugins/flexibleLayout/components/dropHint.vue @@ -21,63 +21,63 @@ --> diff --git a/src/plugins/flexibleLayout/components/flexible-layout.scss b/src/plugins/flexibleLayout/components/flexible-layout.scss index 04789cd816..7054472904 100644 --- a/src/plugins/flexibleLayout/components/flexible-layout.scss +++ b/src/plugins/flexibleLayout/components/flexible-layout.scss @@ -1,320 +1,329 @@ @use 'sass:math'; @mixin containerGrippy($headerSize, $dir) { - position: absolute; - $h: 6px; - $minorOffset: math.div($headerSize - $h, 2); - $majorOffset: 35%; - content: ''; - display: block; - position: absolute; - @include grippy($c: $editFrameSelectedMovebarColorFg, $dir: $dir); - @if $dir == 'x' { - top: $minorOffset; right: $majorOffset; bottom: $minorOffset; left: $majorOffset; - } @else { - top: $majorOffset; right: $minorOffset; bottom: $majorOffset; left: $minorOffset; - } + position: absolute; + $h: 6px; + $minorOffset: math.div($headerSize - $h, 2); + $majorOffset: 35%; + content: ''; + display: block; + position: absolute; + @include grippy($c: $editFrameSelectedMovebarColorFg, $dir: $dir); + @if $dir == 'x' { + top: $minorOffset; + right: $majorOffset; + bottom: $minorOffset; + left: $majorOffset; + } @else { + top: $majorOffset; + right: $minorOffset; + bottom: $majorOffset; + left: $minorOffset; + } } .c-fl { - @include abs(); + @include abs(); + display: flex; + + .temp-toolbar { + flex: 0 0 auto; + } + + &__container-holder { display: flex; + flex: 1 1 100%; // Must be 100% to work + overflow: auto; - .temp-toolbar { - flex: 0 0 auto; + // Columns by default + flex-direction: row; + > * + * { + margin-left: 1px; } - &__container-holder { - display: flex; - flex: 1 1 100%; // Must be 100% to work - overflow: auto; - - // Columns by default - flex-direction: row; - > * + * { margin-left: 1px; } - - &[class*='--rows'] { - flex-direction: column; - > * + * { - margin-left: 0; - margin-top: 1px; - } - } + &[class*='--rows'] { + flex-direction: column; + > * + * { + margin-left: 0; + margin-top: 1px; + } } + } - &__empty { - @include abs(); - background: rgba($colorBodyFg, 0.1); - display: flex; - align-items: center; - justify-content: center; - text-align: center; + &__empty { + @include abs(); + background: rgba($colorBodyFg, 0.1); + display: flex; + align-items: center; + justify-content: center; + text-align: center; - > * { - font-style: italic; - opacity: 0.5; - } + > * { + font-style: italic; + opacity: 0.5; } + } - &__drag-ghost{ - background: $colorItemTreeHoverBg; - color: $colorItemTreeHoverFg; - border-radius: $basicCr; - display: flex; - align-items: center; - padding: $interiorMarginLg $interiorMarginLg * 2; - position: absolute; - top: -10000px; - z-index: 2; + &__drag-ghost { + background: $colorItemTreeHoverBg; + color: $colorItemTreeHoverFg; + border-radius: $basicCr; + display: flex; + align-items: center; + padding: $interiorMarginLg $interiorMarginLg * 2; + position: absolute; + top: -10000px; + z-index: 2; - &:before { - color: $colorKey; - margin-right: $interiorMarginSm; - } + &:before { + color: $colorKey; + margin-right: $interiorMarginSm; } + } } .c-fl-container { - /***************************************************** CONTAINERS */ - $headerSize: 16px; + /***************************************************** CONTAINERS */ + $headerSize: 16px; + display: flex; + flex-direction: column; + overflow: auto; + + // flex-basis is set with inline style in code, controls size + flex-grow: 1; + flex-shrink: 1; + + &__header { + // Only displayed when editing, controlled via JS + background: $editFrameMovebarColorBg; + color: $editFrameMovebarColorFg; + cursor: move; display: flex; - flex-direction: column; - overflow: auto; + align-items: center; + flex: 0 0 $headerSize; - // flex-basis is set with inline style in code, controls size - flex-grow: 1; - flex-shrink: 1; + &:before { + // Drag grippy + @include containerGrippy($headerSize, 'x'); + opacity: 0.5; + } + } - &__header { - // Only displayed when editing, controlled via JS - background: $editFrameMovebarColorBg; - color: $editFrameMovebarColorFg; - cursor: move; - display: flex; - align-items: center; - flex: 0 0 $headerSize; + &__size-indicator { + position: absolute; + display: inline-block; + right: $interiorMargin; + } + + &__frames-holder { + display: flex; + flex: 1 1 100%; // Must be 100% to work + flex-direction: column; // Default + align-content: stretch; + align-items: stretch; + overflow: hidden; // This sucks, but doing in the short-term + } + + .is-editing & { + &:hover { + .c-fl-container__header { + background: $editFrameHovMovebarColorBg; + color: $editFrameHovMovebarColorFg; &:before { - // Drag grippy - @include containerGrippy($headerSize, 'x'); - opacity: 0.5; + opacity: 0.75; } + } + } + + &[s-selected] { + border: $editFrameSelectedBorder; + + .c-fl-container__header { + background: $editFrameSelectedMovebarColorBg; + color: $editFrameSelectedMovebarColorFg; + &:before { + // Grippy + opacity: 1; + } + } + } + + [s-selected].c-fl-frame__drag-wrapper { + border: $editFrameSelectedBorder; + } + } + + /****** THEIR FRAMES */ + // Frames get styled here because this is particular to their presence in this layout type + .c-fl-frame { + @include browserPrefix(margin-collapse, collapse); + } + + /****** ROWS LAYOUT */ + .c-fl--rows & { + // Layout is rows + flex-direction: row; + + &__header { + flex-basis: $headerSize; + overflow: hidden; + + &:before { + // Grippy + @include containerGrippy($headerSize, 'y'); + } } &__size-indicator { - position: absolute; - display: inline-block; - right: $interiorMargin; + right: 0; + top: $interiorMargin; + transform-origin: top right; + transform: rotate(-90deg) translateY(-100%); } &__frames-holder { - display: flex; - flex: 1 1 100%; // Must be 100% to work - flex-direction: column; // Default - align-content: stretch; - align-items: stretch; - overflow: hidden; // This sucks, but doing in the short-term - } - - .is-editing & { - &:hover { - .c-fl-container__header { - background: $editFrameHovMovebarColorBg; - color: $editFrameHovMovebarColorFg; - - &:before { - opacity: .75; - } - } - } - - &[s-selected] { - border: $editFrameSelectedBorder; - - .c-fl-container__header { - background:$editFrameSelectedMovebarColorBg; - color: $editFrameSelectedMovebarColorFg; - &:before { - // Grippy - opacity: 1; - } - } - } - - [s-selected].c-fl-frame__drag-wrapper { - border: $editFrameSelectedBorder; - } - } - - /****** THEIR FRAMES */ - // Frames get styled here because this is particular to their presence in this layout type - .c-fl-frame { - @include browserPrefix(margin-collapse, collapse); - } - - /****** ROWS LAYOUT */ - .c-fl--rows & { - // Layout is rows - flex-direction: row; - - &__header { - flex-basis: $headerSize; - overflow: hidden; - - &:before { - // Grippy - @include containerGrippy($headerSize, 'y'); - } - } - - &__size-indicator { - right: 0; - top: $interiorMargin; - transform-origin: top right; - transform: rotate(-90deg) translateY(-100%); - } - - &__frames-holder { - flex-direction: row; - } + flex-direction: row; } + } } .c-fl-frame { - /***************************************************** CONTAINER FRAMES */ - $sizeIndicatorM: 16px; - $dropHintSize: 15px; + /***************************************************** CONTAINER FRAMES */ + $sizeIndicatorM: 16px; + $dropHintSize: 15px; + + display: flex; + flex: 1 1; + flex-direction: column; + overflow: hidden; // Needed to allow frames to collapse when sized down + + &__drag-wrapper { + flex: 1 1 auto; + overflow: auto; + + .is-editing & { + > * { + pointer-events: none; + } + } + } + + &__header { + flex: 0 0 auto; + margin-bottom: $interiorMargin; + } + + &__size-indicator { + $size: 35px; + + @include ellipsize(); + background: $colorBtnBg; + border-top-left-radius: $controlCr; + color: $colorBtnFg; + display: inline-block; + padding: $interiorMarginSm 0; + position: absolute; + pointer-events: none; + text-align: center; + width: $size; + + // Changed when layout is different, see below + border-top-right-radius: $controlCr; + bottom: 1px; + right: $sizeIndicatorM; + } + + &__drop-hint { + flex: 0 0 $dropHintSize; + .c-drop-hint { + border-radius: $smallCr; + } + } + + &__resize-handle { + $size: 2px; + $margin: 3px; + $marginHov: 0; + $grippyThickness: $size + 6; + $grippyLen: $grippyThickness * 2; display: flex; - flex: 1 1; flex-direction: column; - overflow: hidden; // Needed to allow frames to collapse when sized down + flex: 0 0 ($margin * 2) + $size; - &__drag-wrapper { - flex: 1 1 auto; - overflow: auto; - - .is-editing & { - > * { - pointer-events: none; - } - } + &:before { + // The visible resize line + background-color: $editUIColor; + content: ''; + display: block; + flex: 1 1 auto; + min-height: $size; + min-width: $size; } - &__header { - flex: 0 0 auto; - margin-bottom: $interiorMargin; + &.vertical { + padding: $margin $size; + &:hover { + cursor: row-resize; + } } + &.horizontal { + padding: $size $margin; + &:hover { + cursor: col-resize; + } + } + + &:hover { + &:before { + // The visible resize line + background-color: $editUIColorHov; + } + } + } + + // Hide the resize-handles in first and last c-fl-frame elements + &:first-child, + &:last-child { + .c-fl-frame__resize-handle { + display: none; + } + } + + .c-fl--rows & { + flex-direction: row; + &__size-indicator { - $size: 35px; + border-bottom-left-radius: $controlCr; + border-top-right-radius: 0; + bottom: $sizeIndicatorM; + right: 1px; + } + } - @include ellipsize(); - background: $colorBtnBg; - border-top-left-radius: $controlCr; - color: $colorBtnFg; - display: inline-block; - padding: $interiorMarginSm 0; - position: absolute; - pointer-events: none; - text-align: center; - width: $size; + &--first-in-container { + border: none; + flex: 0 0 0; + .c-fl-frame__drag-wrapper { + display: none; + } - // Changed when layout is different, see below - border-top-right-radius: $controlCr; - bottom: 1px; - right: $sizeIndicatorM; + &.is-dragging { + flex-basis: $dropHintSize; + } + } + + .is-empty & { + &.c-fl-frame--first-in-container { + flex: 1 1 auto; } &__drop-hint { - flex: 0 0 $dropHintSize; - .c-drop-hint { - border-radius: $smallCr; - } - } - - &__resize-handle { - $size: 2px; - $margin: 3px; - $marginHov: 0; - $grippyThickness: $size + 6; - $grippyLen: $grippyThickness * 2; - - display: flex; - flex-direction: column; - flex: 0 0 ($margin * 2) + $size; - - &:before { - // The visible resize line - background-color: $editUIColor; - content: ''; - display: block; - flex: 1 1 auto; - min-height: $size; min-width: $size; - } - - &.vertical { - padding: $margin $size; - &:hover{ - cursor: row-resize; - } - } - - &.horizontal { - padding: $size $margin; - &:hover{ - cursor: col-resize; - } - } - - &:hover { - &:before { - // The visible resize line - background-color: $editUIColorHov; - } - } - } - - // Hide the resize-handles in first and last c-fl-frame elements - &:first-child, - &:last-child { - .c-fl-frame__resize-handle { - display: none; - } - } - - .c-fl--rows & { - flex-direction: row; - - &__size-indicator { - border-bottom-left-radius: $controlCr; - border-top-right-radius: 0; - bottom: $sizeIndicatorM; - right: 1px; - } - } - - &--first-in-container { - border: none; - flex: 0 0 0; - .c-fl-frame__drag-wrapper { - display: none; - } - - &.is-dragging { - flex-basis: $dropHintSize; - } - } - - .is-empty & { - &.c-fl-frame--first-in-container { - flex: 1 1 auto; - } - - &__drop-hint { - flex: 1 0 100%; - margin: 0; - } + flex: 1 0 100%; + margin: 0; } + } } diff --git a/src/plugins/flexibleLayout/components/flexibleLayout.vue b/src/plugins/flexibleLayout/components/flexibleLayout.vue index 0cbf2d225b..d6af10252f 100644 --- a/src/plugins/flexibleLayout/components/flexibleLayout.vue +++ b/src/plugins/flexibleLayout/components/flexibleLayout.vue @@ -21,71 +21,65 @@ --> diff --git a/src/plugins/flexibleLayout/components/frame.vue b/src/plugins/flexibleLayout/components/frame.vue index 5ffac84a9c..1b84869129 100644 --- a/src/plugins/flexibleLayout/components/frame.vue +++ b/src/plugins/flexibleLayout/components/frame.vue @@ -1,4 +1,3 @@ - diff --git a/src/plugins/flexibleLayout/components/resizeHandle.vue b/src/plugins/flexibleLayout/components/resizeHandle.vue index 11484b314b..975bd1ca47 100644 --- a/src/plugins/flexibleLayout/components/resizeHandle.vue +++ b/src/plugins/flexibleLayout/components/resizeHandle.vue @@ -21,86 +21,86 @@ --> diff --git a/src/plugins/flexibleLayout/flexibleLayoutViewProvider.js b/src/plugins/flexibleLayout/flexibleLayoutViewProvider.js index cf0baf18d6..8082cd95c2 100644 --- a/src/plugins/flexibleLayout/flexibleLayoutViewProvider.js +++ b/src/plugins/flexibleLayout/flexibleLayoutViewProvider.js @@ -20,70 +20,65 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './components/flexibleLayout.vue', - 'vue' -], function ( - FlexibleLayoutComponent, - Vue -) { - function FlexibleLayoutViewProvider(openmct) { +define(['./components/flexibleLayout.vue', 'vue'], function (FlexibleLayoutComponent, Vue) { + function FlexibleLayoutViewProvider(openmct) { + return { + key: 'flexible-layout', + name: 'FlexibleLayout', + cssClass: 'icon-layout-view', + canView: function (domainObject) { + return domainObject.type === 'flexible-layout'; + }, + canEdit: function (domainObject) { + return domainObject.type === 'flexible-layout'; + }, + view: function (domainObject, objectPath) { + let component; + return { - key: 'flexible-layout', - name: 'FlexibleLayout', - cssClass: 'icon-layout-view', - canView: function (domainObject) { - return domainObject.type === 'flexible-layout'; - }, - canEdit: function (domainObject) { - return domainObject.type === 'flexible-layout'; - }, - view: function (domainObject, objectPath) { - let component; - + show: function (element, isEditing) { + component = new Vue({ + el: element, + components: { + FlexibleLayoutComponent: FlexibleLayoutComponent.default + }, + provide: { + openmct, + objectPath, + layoutObject: domainObject + }, + data() { return { - show: function (element, isEditing) { - component = new Vue({ - el: element, - components: { - FlexibleLayoutComponent: FlexibleLayoutComponent.default - }, - provide: { - openmct, - objectPath, - layoutObject: domainObject - }, - data() { - return { - isEditing: isEditing - }; - }, - template: '' - }); - }, - getSelectionContext: function () { - return { - item: domainObject, - addContainer: component.$refs.flexibleLayout.addContainer, - deleteContainer: component.$refs.flexibleLayout.deleteContainer, - deleteFrame: component.$refs.flexibleLayout.deleteFrame, - type: 'flexible-layout' - }; - }, - onEditModeChange: function (isEditing) { - component.isEditing = isEditing; - }, - destroy: function (element) { - component.$destroy(); - component = undefined; - } + isEditing: isEditing }; - }, - priority: function () { - return 1; - } + }, + template: + '' + }); + }, + getSelectionContext: function () { + return { + item: domainObject, + addContainer: component.$refs.flexibleLayout.addContainer, + deleteContainer: component.$refs.flexibleLayout.deleteContainer, + deleteFrame: component.$refs.flexibleLayout.deleteFrame, + type: 'flexible-layout' + }; + }, + onEditModeChange: function (isEditing) { + component.isEditing = isEditing; + }, + destroy: function (element) { + component.$destroy(); + component = undefined; + } }; - } + }, + priority: function () { + return 1; + } + }; + } - return FlexibleLayoutViewProvider; + return FlexibleLayoutViewProvider; }); diff --git a/src/plugins/flexibleLayout/plugin.js b/src/plugins/flexibleLayout/plugin.js index 53c514838c..0f17d177d8 100644 --- a/src/plugins/flexibleLayout/plugin.js +++ b/src/plugins/flexibleLayout/plugin.js @@ -20,37 +20,33 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './flexibleLayoutViewProvider', - './utils/container', - './toolbarProvider' -], function ( - FlexibleLayoutViewProvider, - Container, - ToolBarProvider +define(['./flexibleLayoutViewProvider', './utils/container', './toolbarProvider'], function ( + FlexibleLayoutViewProvider, + Container, + ToolBarProvider ) { - return function plugin() { + return function plugin() { + return function install(openmct) { + openmct.objectViews.addProvider(new FlexibleLayoutViewProvider(openmct)); - return function install(openmct) { - openmct.objectViews.addProvider(new FlexibleLayoutViewProvider(openmct)); + openmct.types.addType('flexible-layout', { + name: 'Flexible Layout', + creatable: true, + description: + 'A fluid, flexible layout canvas that can display multiple objects in rows or columns.', + cssClass: 'icon-flexible-layout', + initialize: function (domainObject) { + domainObject.configuration = { + containers: [new Container.default(50), new Container.default(50)], + rowsLayout: false + }; + domainObject.composition = []; + } + }); - openmct.types.addType('flexible-layout', { - name: "Flexible Layout", - creatable: true, - description: "A fluid, flexible layout canvas that can display multiple objects in rows or columns.", - cssClass: 'icon-flexible-layout', - initialize: function (domainObject) { - domainObject.configuration = { - containers: [new Container.default(50), new Container.default(50)], - rowsLayout: false - }; - domainObject.composition = []; - } - }); + let toolbar = ToolBarProvider.default(openmct); - let toolbar = ToolBarProvider.default(openmct); - - openmct.toolbars.addProvider(toolbar); - }; + openmct.toolbars.addProvider(toolbar); }; + }; }); diff --git a/src/plugins/flexibleLayout/pluginSpec.js b/src/plugins/flexibleLayout/pluginSpec.js index 3a8650d64c..1b659b664e 100644 --- a/src/plugins/flexibleLayout/pluginSpec.js +++ b/src/plugins/flexibleLayout/pluginSpec.js @@ -25,184 +25,187 @@ import FlexibleLayout from './plugin'; import Vue from 'vue'; describe('the plugin', function () { - let element; - let child; - let openmct; - let flexibleLayoutDefinition; - const testViewObject = { + let element; + let child; + let openmct; + let flexibleLayoutDefinition; + const testViewObject = { + id: 'test-object', + type: 'flexible-layout', + configuration: { + rowsLayout: false, + containers: [ + { + id: 'deb9f839-80ad-4ccf-a152-5c763ceb7d7e', + frames: [], + size: 50 + }, + { + id: 'deb9f839-80ad-4ccf-a152-5c763ceb7d7f', + frames: [], + size: 50 + } + ] + }, + composition: [] + }; + + beforeEach((done) => { + openmct = createOpenMct(); + openmct.install(new FlexibleLayout()); + flexibleLayoutDefinition = openmct.types.get('flexible-layout'); + + element = document.createElement('div'); + child = document.createElement('div'); + element.appendChild(child); + + openmct.on('start', done); + openmct.start(child); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('defines a flexible layout object type with the correct key', () => { + expect(flexibleLayoutDefinition.definition.name).toEqual('Flexible Layout'); + }); + + describe('the view', function () { + let flexibleLayoutViewProvider; + + beforeEach(() => { + const applicableViews = openmct.objectViews.get(testViewObject, []); + flexibleLayoutViewProvider = applicableViews.find( + (viewProvider) => viewProvider.key === 'flexible-layout' + ); + }); + + 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', () => { + let flexibleLayoutItem; + let flexibleLayoutToolbar; + let telemetryItem; + let selection; + + beforeEach(() => { + flexibleLayoutItem = { id: 'test-object', type: 'flexible-layout', configuration: { - rowsLayout: false, - containers: [ + rowsLayout: true, + containers: [ + { + id: 'deb9f839-80ad-4ccf-a152-5c763ceb7d7e', + frames: [ { - 'id': 'deb9f839-80ad-4ccf-a152-5c763ceb7d7e', - frames: [], - size: 50 - - }, - { - 'id': 'deb9f839-80ad-4ccf-a152-5c763ceb7d7f', - frames: [], - size: 50 - + id: '329bf482-d0dc-486a-aae0-6176276bd315', + domainObjectIdentifier: { + namespace: '', + key: '55122607-e65e-44d5-9c9d-9c31a914ca89' + }, + size: 100, + noFrame: false } - ] + ], + size: 61 + }, + { + id: 'deb9f839-80ad-4ccf-a152-5c763ceb7d7f', + frames: [ + { + id: '329bf482-d0dc-486a-aae0-6176276bd316', + domainObjectIdentifier: { + namespace: '', + key: '55122607-e65e-44d5-9c9d-9c31a914ca90' + }, + size: 100, + noFrame: false + } + ], + size: 39 + } + ] }, - composition: [] - }; + composition: [ + { + identifier: { + namespace: '', + key: '55122607-e65e-44d5-9c9d-9c31a914ca89' + } + }, + { + identifier: { + namespace: '', + key: '55122607-e65e-44d5-9c9d-9c31a914ca90' + } + } + ] + }; + telemetryItem = { + telemetry: { + period: 5, + amplitude: 5, + offset: 5, + dataRateInHz: 5, + phase: 5, + randomness: 0 + }, + name: 'Sine Wave Generator', + type: 'generator', + modified: 1592851063871, + location: 'mine', + persisted: 1592851063871, + id: '55122607-e65e-44d5-9c9d-9c31a914ca89', + identifier: { + namespace: '', + key: '55122607-e65e-44d5-9c9d-9c31a914ca89' + } + }; + selection = [ + [ + { + context: { + frameId: '329bf482-d0dc-486a-aae0-6176276bd315', + item: telemetryItem, + type: 'frame' + } + }, + { + context: { + containerId: 'deb9f839-80ad-4ccf-a152-5c763ceb7d7e', + item: flexibleLayoutItem, + type: 'container' + } + }, + { + context: { + item: flexibleLayoutItem, + type: 'flexible-layout' + } + } + ] + ]; - beforeEach((done) => { - openmct = createOpenMct(); - openmct.install(new FlexibleLayout()); - flexibleLayoutDefinition = openmct.types.get('flexible-layout'); - - element = document.createElement('div'); - child = document.createElement('div'); - element.appendChild(child); - - openmct.on('start', done); - openmct.start(child); + flexibleLayoutToolbar = openmct.toolbars.get(selection); }); - afterEach(() => { - return resetApplicationState(openmct); - }); - - it('defines a flexible layout object type with the correct key', () => { - expect(flexibleLayoutDefinition.definition.name).toEqual('Flexible Layout'); - }); - - describe('the view', function () { - let flexibleLayoutViewProvider; - - beforeEach(() => { - const applicableViews = openmct.objectViews.get(testViewObject, []); - flexibleLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'flexible-layout'); - }); - - 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', () => { - let flexibleLayoutItem; - let flexibleLayoutToolbar; - let telemetryItem; - let selection; - - beforeEach(() => { - flexibleLayoutItem = { - id: 'test-object', - type: 'flexible-layout', - configuration: { - rowsLayout: true, - containers: [ - { - 'id': 'deb9f839-80ad-4ccf-a152-5c763ceb7d7e', - frames: [{ - id: "329bf482-d0dc-486a-aae0-6176276bd315", - 'domainObjectIdentifier': { - 'namespace': '', - 'key': '55122607-e65e-44d5-9c9d-9c31a914ca89' - }, - size: 100, - noFrame: false - }], - size: 61 - - }, - { - 'id': 'deb9f839-80ad-4ccf-a152-5c763ceb7d7f', - frames: [{ - id: "329bf482-d0dc-486a-aae0-6176276bd316", - 'domainObjectIdentifier': { - 'namespace': '', - 'key': '55122607-e65e-44d5-9c9d-9c31a914ca90' - }, - size: 100, - noFrame: false - }], - size: 39 - - } - ] - }, - composition: [ - { - 'identifier': { - 'namespace': '', - 'key': '55122607-e65e-44d5-9c9d-9c31a914ca89' - } - }, - { - 'identifier': { - 'namespace': '', - 'key': '55122607-e65e-44d5-9c9d-9c31a914ca90' - } - } - ] - }; - telemetryItem = { - 'telemetry': { - 'period': 5, - 'amplitude': 5, - 'offset': 5, - 'dataRateInHz': 5, - 'phase': 5, - 'randomness': 0 - }, - 'name': 'Sine Wave Generator', - 'type': 'generator', - 'modified': 1592851063871, - 'location': 'mine', - 'persisted': 1592851063871, - 'id': '55122607-e65e-44d5-9c9d-9c31a914ca89', - 'identifier': { - 'namespace': '', - 'key': '55122607-e65e-44d5-9c9d-9c31a914ca89' - } - }; - selection = [ - [{ - context: { - frameId: "329bf482-d0dc-486a-aae0-6176276bd315", - item: telemetryItem, - type: "frame" - } - }, - { - context: { - containerId: "deb9f839-80ad-4ccf-a152-5c763ceb7d7e", - 'item': flexibleLayoutItem, - type: "container" - } - }, - { - context: { - item: flexibleLayoutItem, - type: "flexible-layout" - } - - }] - ]; - - flexibleLayoutToolbar = openmct.toolbars.get(selection); - }); - - it('provides controls including separators', () => { - expect(flexibleLayoutToolbar.length).toBe(6); - }); + it('provides controls including separators', () => { + expect(flexibleLayoutToolbar.length).toBe(6); }); + }); }); diff --git a/src/plugins/flexibleLayout/toolbarProvider.js b/src/plugins/flexibleLayout/toolbarProvider.js index feb03a42d1..3771191bba 100644 --- a/src/plugins/flexibleLayout/toolbarProvider.js +++ b/src/plugins/flexibleLayout/toolbarProvider.js @@ -21,206 +21,203 @@ *****************************************************************************/ function ToolbarProvider(openmct) { + return { + name: 'Flexible Layout Toolbar', + key: 'flex-layout', + description: 'A toolbar for objects inside a Flexible Layout.', + forSelection: function (selection) { + let context = selection[0][0].context; - return { - name: "Flexible Layout Toolbar", - key: "flex-layout", - description: "A toolbar for objects inside a Flexible Layout.", - forSelection: function (selection) { - let context = selection[0][0].context; + return ( + context && + context.type && + (context.type === 'flexible-layout' || + context.type === 'container' || + context.type === 'frame') + ); + }, + toolbar: function (selection) { + let selectionPath = selection[0]; + let primary = selectionPath[0]; + let secondary = selectionPath[1]; + let tertiary = selectionPath[2]; + let deleteFrame; + let toggleContainer; + let deleteContainer; + let addContainer; + let toggleFrame; - return (context && context.type - && (context.type === 'flexible-layout' || context.type === 'container' || context.type === 'frame')); - }, - toolbar: function (selection) { - let selectionPath = selection[0]; - let primary = selectionPath[0]; - let secondary = selectionPath[1]; - let tertiary = selectionPath[2]; - let deleteFrame; - let toggleContainer; - let deleteContainer; - let addContainer; - let toggleFrame; + toggleContainer = { + control: 'toggle-button', + key: 'toggle-layout', + domainObject: primary.context.item, + property: 'configuration.rowsLayout', + options: [ + { + value: true, + icon: 'icon-columns', + title: 'Columns layout' + }, + { + value: false, + icon: 'icon-rows', + title: 'Rows layout' + } + ] + }; - toggleContainer = { - control: 'toggle-button', - key: 'toggle-layout', - domainObject: primary.context.item, - property: 'configuration.rowsLayout', - options: [ - { - value: true, - icon: 'icon-columns', - title: 'Columns layout' - }, - { - value: false, - icon: 'icon-rows', - title: 'Rows layout' - } - ] - }; + function getSeparator() { + return { + control: 'separator' + }; + } - function getSeparator() { - return { - control: "separator" - }; - } - - if (primary.context.type === 'frame') { - if (secondary.context.item.locked) { - return []; - } - - let frameId = primary.context.frameId; - let layoutObject = tertiary.context.item; - let containers = layoutObject - .configuration - .containers; - let container = containers - .filter(c => c.frames.some(f => f.id === frameId))[0]; - let containerIndex = containers.indexOf(container); - let frame = container && container - .frames - .filter((f => f.id === frameId))[0]; - let frameIndex = container && container.frames.indexOf(frame); - - deleteFrame = { - control: "button", - domainObject: primary.context.item, - method: function () { - let deleteFrameAction = tertiary.context.deleteFrame; - - let prompt = openmct.overlays.dialog({ - iconClass: 'alert', - message: `This action will remove this frame from this Flexible Layout. Do you want to continue?`, - buttons: [ - { - label: 'OK', - emphasis: 'true', - callback: function () { - deleteFrameAction(primary.context.frameId); - prompt.dismiss(); - } - }, - { - label: 'Cancel', - callback: function () { - prompt.dismiss(); - } - } - ] - }); - }, - key: "remove", - icon: "icon-trash", - title: "Remove Frame" - }; - toggleFrame = { - control: "toggle-button", - domainObject: secondary.context.item, - property: `configuration.containers[${containerIndex}].frames[${frameIndex}].noFrame`, - options: [ - { - value: false, - icon: 'icon-frame-hide', - title: "Frame hidden" - }, - { - value: true, - icon: 'icon-frame-show', - title: "Frame visible" - } - ] - }; - addContainer = { - control: "button", - domainObject: tertiary.context.item, - method: tertiary.context.addContainer, - key: "add", - icon: "icon-plus-in-rect", - title: 'Add Container' - }; - - toggleContainer.domainObject = secondary.context.item; - - } else if (primary.context.type === 'container') { - if (primary.context.item.locked) { - return []; - } - - deleteContainer = { - control: "button", - domainObject: primary.context.item, - method: function () { - let removeContainer = secondary.context.deleteContainer; - let containerId = primary.context.containerId; - - let prompt = openmct.overlays.dialog({ - iconClass: 'alert', - message: 'This action will permanently delete this container from this Flexible Layout. Do you want to continue?', - buttons: [ - { - label: 'OK', - emphasis: 'true', - callback: function () { - removeContainer(containerId); - prompt.dismiss(); - } - }, - { - label: 'Cancel', - callback: function () { - prompt.dismiss(); - } - } - ] - }); - }, - key: "remove", - icon: "icon-trash", - title: "Remove Container" - }; - - addContainer = { - control: "button", - domainObject: secondary.context.item, - method: secondary.context.addContainer, - key: "add", - icon: "icon-plus-in-rect", - title: 'Add Container' - }; - - } else if (primary.context.type === 'flexible-layout') { - if (primary.context.item.locked) { - return []; - } - - addContainer = { - control: "button", - domainObject: primary.context.item, - method: primary.context.addContainer, - key: "add", - icon: "icon-plus-in-rect", - title: 'Add Container' - }; - - } - - let toolbar = [ - toggleContainer, - addContainer, - toggleFrame ? getSeparator() : undefined, - toggleFrame, - deleteFrame || deleteContainer ? getSeparator() : undefined, - deleteFrame, - deleteContainer - ]; - - return toolbar.filter(button => button !== undefined); + if (primary.context.type === 'frame') { + if (secondary.context.item.locked) { + return []; } - }; + + let frameId = primary.context.frameId; + let layoutObject = tertiary.context.item; + let containers = layoutObject.configuration.containers; + let container = containers.filter((c) => c.frames.some((f) => f.id === frameId))[0]; + let containerIndex = containers.indexOf(container); + let frame = container && container.frames.filter((f) => f.id === frameId)[0]; + let frameIndex = container && container.frames.indexOf(frame); + + deleteFrame = { + control: 'button', + domainObject: primary.context.item, + method: function () { + let deleteFrameAction = tertiary.context.deleteFrame; + + let prompt = openmct.overlays.dialog({ + iconClass: 'alert', + message: `This action will remove this frame from this Flexible Layout. Do you want to continue?`, + buttons: [ + { + label: 'OK', + emphasis: 'true', + callback: function () { + deleteFrameAction(primary.context.frameId); + prompt.dismiss(); + } + }, + { + label: 'Cancel', + callback: function () { + prompt.dismiss(); + } + } + ] + }); + }, + key: 'remove', + icon: 'icon-trash', + title: 'Remove Frame' + }; + toggleFrame = { + control: 'toggle-button', + domainObject: secondary.context.item, + property: `configuration.containers[${containerIndex}].frames[${frameIndex}].noFrame`, + options: [ + { + value: false, + icon: 'icon-frame-hide', + title: 'Frame hidden' + }, + { + value: true, + icon: 'icon-frame-show', + title: 'Frame visible' + } + ] + }; + addContainer = { + control: 'button', + domainObject: tertiary.context.item, + method: tertiary.context.addContainer, + key: 'add', + icon: 'icon-plus-in-rect', + title: 'Add Container' + }; + + toggleContainer.domainObject = secondary.context.item; + } else if (primary.context.type === 'container') { + if (primary.context.item.locked) { + return []; + } + + deleteContainer = { + control: 'button', + domainObject: primary.context.item, + method: function () { + let removeContainer = secondary.context.deleteContainer; + let containerId = primary.context.containerId; + + let prompt = openmct.overlays.dialog({ + iconClass: 'alert', + message: + 'This action will permanently delete this container from this Flexible Layout. Do you want to continue?', + buttons: [ + { + label: 'OK', + emphasis: 'true', + callback: function () { + removeContainer(containerId); + prompt.dismiss(); + } + }, + { + label: 'Cancel', + callback: function () { + prompt.dismiss(); + } + } + ] + }); + }, + key: 'remove', + icon: 'icon-trash', + title: 'Remove Container' + }; + + addContainer = { + control: 'button', + domainObject: secondary.context.item, + method: secondary.context.addContainer, + key: 'add', + icon: 'icon-plus-in-rect', + title: 'Add Container' + }; + } else if (primary.context.type === 'flexible-layout') { + if (primary.context.item.locked) { + return []; + } + + addContainer = { + control: 'button', + domainObject: primary.context.item, + method: primary.context.addContainer, + key: 'add', + icon: 'icon-plus-in-rect', + title: 'Add Container' + }; + } + + let toolbar = [ + toggleContainer, + addContainer, + toggleFrame ? getSeparator() : undefined, + toggleFrame, + deleteFrame || deleteContainer ? getSeparator() : undefined, + deleteFrame, + deleteContainer + ]; + + return toolbar.filter((button) => button !== undefined); + } + }; } export default ToolbarProvider; diff --git a/src/plugins/flexibleLayout/utils/container.js b/src/plugins/flexibleLayout/utils/container.js index a26bf08add..c4889cb885 100644 --- a/src/plugins/flexibleLayout/utils/container.js +++ b/src/plugins/flexibleLayout/utils/container.js @@ -1,11 +1,11 @@ import { v4 as uuid } from 'uuid'; class Container { - constructor(size) { - this.id = uuid(); - this.frames = []; - this.size = size; - } + constructor(size) { + this.id = uuid(); + this.frames = []; + this.size = size; + } } export default Container; diff --git a/src/plugins/flexibleLayout/utils/frame.js b/src/plugins/flexibleLayout/utils/frame.js index 767464419e..f7736aa199 100644 --- a/src/plugins/flexibleLayout/utils/frame.js +++ b/src/plugins/flexibleLayout/utils/frame.js @@ -1,13 +1,13 @@ import { v4 as uuid } from 'uuid'; class Frame { - constructor(domainObjectIdentifier, size) { - this.id = uuid(); - this.domainObjectIdentifier = domainObjectIdentifier; - this.size = size; + constructor(domainObjectIdentifier, size) { + this.id = uuid(); + this.domainObjectIdentifier = domainObjectIdentifier; + this.size = size; - this.noFrame = false; - } + this.noFrame = false; + } } export default Frame; diff --git a/src/plugins/folderView/FolderGridView.js b/src/plugins/folderView/FolderGridView.js index 6aacbaf832..e676d24163 100644 --- a/src/plugins/folderView/FolderGridView.js +++ b/src/plugins/folderView/FolderGridView.js @@ -20,53 +20,49 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './components/GridView.vue', - './constants.js', - 'vue' -], function ( - GridViewComponent, - constants, - Vue +define(['./components/GridView.vue', './constants.js', 'vue'], function ( + GridViewComponent, + constants, + Vue ) { - function FolderGridView(openmct) { - const ALLOWED_FOLDER_TYPES = constants.ALLOWED_FOLDER_TYPES; + function FolderGridView(openmct) { + const ALLOWED_FOLDER_TYPES = constants.ALLOWED_FOLDER_TYPES; + + return { + key: 'grid', + name: 'Grid View', + cssClass: 'icon-thumbs-strip', + canView: function (domainObject) { + return ALLOWED_FOLDER_TYPES.includes(domainObject.type); + }, + view: function (domainObject) { + let component; return { - key: 'grid', - name: 'Grid View', - cssClass: 'icon-thumbs-strip', - canView: function (domainObject) { - return ALLOWED_FOLDER_TYPES.includes(domainObject.type); - }, - view: function (domainObject) { - let component; - - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - gridViewComponent: GridViewComponent.default - }, - provide: { - openmct, - domainObject - }, - template: '' - }); - }, - destroy: function (element) { - component.$destroy(); - component = undefined; - } - }; - }, - priority: function () { - return 1; - } + show: function (element) { + component = new Vue({ + el: element, + components: { + gridViewComponent: GridViewComponent.default + }, + provide: { + openmct, + domainObject + }, + template: '' + }); + }, + destroy: function (element) { + component.$destroy(); + component = undefined; + } }; - } + }, + priority: function () { + return 1; + } + }; + } - return FolderGridView; + return FolderGridView; }); diff --git a/src/plugins/folderView/FolderListView.js b/src/plugins/folderView/FolderListView.js index 80da130ce1..b1391520aa 100644 --- a/src/plugins/folderView/FolderListView.js +++ b/src/plugins/folderView/FolderListView.js @@ -20,56 +20,51 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './components/ListView.vue', - './constants.js', - 'vue', - 'moment' -], function ( - ListViewComponent, - constants, - Vue, - Moment +define(['./components/ListView.vue', './constants.js', 'vue', 'moment'], function ( + ListViewComponent, + constants, + Vue, + Moment ) { - function FolderListView(openmct) { - const ALLOWED_FOLDER_TYPES = constants.ALLOWED_FOLDER_TYPES; + function FolderListView(openmct) { + const ALLOWED_FOLDER_TYPES = constants.ALLOWED_FOLDER_TYPES; + + return { + key: 'list-view', + name: 'List View', + cssClass: 'icon-list-view', + canView: function (domainObject) { + return ALLOWED_FOLDER_TYPES.includes(domainObject.type); + }, + view: function (domainObject) { + let component; return { - key: 'list-view', - name: 'List View', - cssClass: 'icon-list-view', - canView: function (domainObject) { - return ALLOWED_FOLDER_TYPES.includes(domainObject.type); - }, - view: function (domainObject) { - let component; - - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - listViewComponent: ListViewComponent.default - }, - provide: { - openmct, - domainObject, - Moment - }, - template: '' - }); - }, - destroy: function (element) { - component.$destroy(); - component = undefined; - } - }; - }, - priority: function () { - return 1; - } + show: function (element) { + component = new Vue({ + el: element, + components: { + listViewComponent: ListViewComponent.default + }, + provide: { + openmct, + domainObject, + Moment + }, + template: '' + }); + }, + destroy: function (element) { + component.$destroy(); + component = undefined; + } }; - } + }, + priority: function () { + return 1; + } + }; + } - return FolderListView; + return FolderListView; }); diff --git a/src/plugins/folderView/components/GridItem.vue b/src/plugins/folderView/components/GridItem.vue index 1e36532ce2..9258d51fd8 100644 --- a/src/plugins/folderView/components/GridItem.vue +++ b/src/plugins/folderView/components/GridItem.vue @@ -20,48 +20,38 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/folderView/components/GridView.vue b/src/plugins/folderView/components/GridView.vue index 0783f4e16f..3c6d91f188 100644 --- a/src/plugins/folderView/components/GridView.vue +++ b/src/plugins/folderView/components/GridView.vue @@ -20,24 +20,23 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/folderView/components/ListItem.vue b/src/plugins/folderView/components/ListItem.vue index 4a2a4a7cd2..b461b1143d 100644 --- a/src/plugins/folderView/components/ListItem.vue +++ b/src/plugins/folderView/components/ListItem.vue @@ -20,67 +20,58 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/folderView/components/ListView.vue b/src/plugins/folderView/components/ListView.vue index eb16fde3cf..9eaf795097 100644 --- a/src/plugins/folderView/components/ListView.vue +++ b/src/plugins/folderView/components/ListView.vue @@ -20,66 +20,68 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/folderView/components/composition-loader.js b/src/plugins/folderView/components/composition-loader.js index 33aab1a658..b2ec742a76 100644 --- a/src/plugins/folderView/components/composition-loader.js +++ b/src/plugins/folderView/components/composition-loader.js @@ -1,53 +1,54 @@ const unknownObjectType = { - definition: { - cssClass: 'icon-object-unknown', - name: 'Unknown Type' - } + definition: { + cssClass: 'icon-object-unknown', + name: 'Unknown Type' + } }; export default { - inject: ['openmct', 'domainObject'], - data() { - return { - items: [] - }; - }, - mounted() { - this.composition = this.openmct.composition.get(this.domainObject); - this.keystring = this.openmct.objects.makeKeyString(this.domainObject.identifier); - if (!this.composition) { - return; - } - - this.composition.on('add', this.add); - this.composition.on('remove', this.remove); - this.composition.load(); - }, - destroyed() { - if (!this.composition) { - return; - } - - this.composition.off('add', this.add); - this.composition.off('remove', this.remove); - }, - methods: { - add(child, index, anything) { - const type = this.openmct.types.get(child.type) || unknownObjectType; - this.items.push({ - model: child, - type: type.definition, - isAlias: this.keystring !== child.location, - objectPath: [child].concat(this.openmct.router.path), - objectKeyString: this.openmct.objects.makeKeyString(child.identifier) - }); - }, - remove(identifier) { - this.items = this.items - .filter((i) => { - return i.model.identifier.key !== identifier.key - || i.model.identifier.namespace !== identifier.namespace; - }); - } + inject: ['openmct', 'domainObject'], + data() { + return { + items: [] + }; + }, + mounted() { + this.composition = this.openmct.composition.get(this.domainObject); + this.keystring = this.openmct.objects.makeKeyString(this.domainObject.identifier); + if (!this.composition) { + return; } + + this.composition.on('add', this.add); + this.composition.on('remove', this.remove); + this.composition.load(); + }, + destroyed() { + if (!this.composition) { + return; + } + + this.composition.off('add', this.add); + this.composition.off('remove', this.remove); + }, + methods: { + add(child, index, anything) { + const type = this.openmct.types.get(child.type) || unknownObjectType; + this.items.push({ + model: child, + type: type.definition, + isAlias: this.keystring !== child.location, + objectPath: [child].concat(this.openmct.router.path), + objectKeyString: this.openmct.objects.makeKeyString(child.identifier) + }); + }, + remove(identifier) { + this.items = this.items.filter((i) => { + return ( + i.model.identifier.key !== identifier.key || + i.model.identifier.namespace !== identifier.namespace + ); + }); + } + } }; diff --git a/src/plugins/folderView/components/grid-view.scss b/src/plugins/folderView/components/grid-view.scss index 7ac1ea1cb0..42f74971a3 100644 --- a/src/plugins/folderView/components/grid-view.scss +++ b/src/plugins/folderView/components/grid-view.scss @@ -2,191 +2,195 @@ /******************************* GRID VIEW */ .l-grid-view { - display: flex; - flex-flow: column nowrap; - overflow: auto; - height: 100%; + display: flex; + flex-flow: column nowrap; + overflow: auto; + height: 100%; + + &__item { + flex: 0 0 auto; + + .l-grid-view__item { + margin-top: $interiorMargin; + } + } + + body.desktop & { + flex-flow: row wrap; + align-content: flex-start; &__item { - flex: 0 0 auto; - + .l-grid-view__item { margin-top: $interiorMargin; } + height: $gridItemDesk; + width: $gridItemDesk; + margin: 0 $interiorMargin $interiorMargin 0; } + } - body.desktop & { - flex-flow: row wrap; - align-content: flex-start; + body.mobile & { + flex: 1 0 auto; + } - &__item { - height: $gridItemDesk; - width: $gridItemDesk; - margin: 0 $interiorMargin $interiorMargin 0; - } - } - - body.mobile & { - flex: 1 0 auto; - } - - [class*='l-overlay'] & { - // When this view is in an overlay, prevent navigation - pointer-events: none; - } + [class*='l-overlay'] & { + // When this view is in an overlay, prevent navigation + pointer-events: none; + } } /******************************* GRID ITEMS */ .c-grid-item { - // Mobile-first - @include button($bg: $colorItemBg, $fg: $colorItemFg); - @include cControlHov(); - cursor: pointer; + // Mobile-first + @include button($bg: $colorItemBg, $fg: $colorItemFg); + @include cControlHov(); + cursor: pointer; + display: flex; + padding: $interiorMarginLg; + + &__type-icon { + filter: $colorKeyFilter; + flex: 0 0 $gridItemMobile; + font-size: floor(math.div($gridItemMobile, 2)); + margin-right: $interiorMarginLg; + } + + &.is-alias { + // Object is an alias to an original. + [class*='__type-icon'] { + @include isAlias(); + } + } + + &.is-status--notebook-default { + .is-status__indicator { + display: block; + + &:before { + color: $colorFilter; + content: $glyph-icon-notebook-page; + font-family: symbolsfont; + } + } + } + + &.is-status--current { + .is-status__indicator { + display: block; + + &:before { + color: $colorFilter; + content: $glyph-icon-asterisk; + font-family: symbolsfont; + } + } + } + + &.is-status--draft { + .is-status__indicator { + display: block; + + &:before { + color: $colorStatusAlert; + content: $glyph-icon-draft; + font-family: symbolsfont; + } + } + } + + &[class*='is-status--missing'], + &[class*='is-status--suspect'] { + [class*='__type-icon'], + [class*='__details'] { + opacity: $opacityMissing; + } + } + + &__details { display: flex; - padding: $interiorMarginLg; + flex-flow: column nowrap; + flex: 1 1 auto; + } - &__type-icon { - filter: $colorKeyFilter; - flex: 0 0 $gridItemMobile; - font-size: floor(math.div($gridItemMobile, 2)); - margin-right: $interiorMarginLg; - } + &__name { + @include ellipsize(); + color: $colorItemFg; + font-size: 1.2em; + font-weight: 400; + margin-bottom: $interiorMarginSm; + } - &.is-alias { - // Object is an alias to an original. - [class*='__type-icon'] { - @include isAlias(); + &__metadata { + color: $colorItemFgDetails; + font-size: 0.9em; + + body.mobile & { + [class*='__item-count'] { + &:before { + content: ' - '; } + } + } + } + + &__controls { + color: $colorItemFgDetails; + flex: 0 0 64px; + font-size: 1.2em; + display: flex; + align-items: center; + justify-content: flex-end; + + > * + * { + margin-left: $interiorMargin; + } + } + + body.desktop & { + $transOutMs: 300ms; + flex-flow: column nowrap; + + &:hover { + .c-grid-item__type-icon { + transform: scale(1.1); + } } - &.is-status--notebook-default { - .is-status__indicator { - display: block; - - &:before { - color: $colorFilter; - content: $glyph-icon-notebook-page; - font-family: symbolsfont; - } - } - } - - &.is-status--current { - .is-status__indicator { - display: block; - - &:before { - color: $colorFilter; - content: $glyph-icon-asterisk; - font-family: symbolsfont; - } - } - } - - &.is-status--draft { - .is-status__indicator { - display: block; - - &:before { - color: $colorStatusAlert; - content: $glyph-icon-draft; - font-family: symbolsfont; - } - } - } - - &[class*='is-status--missing'], - &[class*='is-status--suspect']{ - [class*='__type-icon'], - [class*='__details'] { - opacity: $opacityMissing; - } - } - - &__details { - display: flex; - flex-flow: column nowrap; - flex: 1 1 auto; - } - - &__name { - @include ellipsize(); - color: $colorItemFg; - font-size: 1.2em; - font-weight: 400; - margin-bottom: $interiorMarginSm; - } - - &__metadata { - color: $colorItemFgDetails; - font-size: 0.9em; - - body.mobile & { - [class*='__item-count'] { - &:before { - content: ' - '; - } - } - } + > * { + margin: 0; // Reset from mobile } &__controls { - color: $colorItemFgDetails; - flex: 0 0 64px; - font-size: 1.2em; - display: flex; - align-items: center; - justify-content: flex-end; - - > * + * { - margin-left: $interiorMargin; - } + align-items: baseline; + flex: 0 0 auto; + order: 1; + .c-info-button, + .c-pointer-icon { + display: none; + } } - body.desktop & { - $transOutMs: 300ms; - flex-flow: column nowrap; - - &:hover { - .c-grid-item__type-icon { - transform: scale(1.1); - } - } - - > * { - margin: 0; // Reset from mobile - } - - &__controls { - align-items: baseline; - flex: 0 0 auto; - order: 1; - .c-info-button, - .c-pointer-icon { display: none; } - } - - &__type-icon { - flex: 1 1 auto; - font-size: floor(math.div($gridItemDesk, 3)); - margin: $interiorMargin 22.5% $interiorMargin * 3 22.5%; - order: 2; - } - - &__details { - flex: 0 0 auto; - justify-content: flex-end; - order: 3; - } - - &__metadata { - display: flex; - - &__type { - flex: 1 1 auto; - @include ellipsize(); - } - - &__item-count { - opacity: 0.7; - flex: 0 0 auto; - } - } + &__type-icon { + flex: 1 1 auto; + font-size: floor(math.div($gridItemDesk, 3)); + margin: $interiorMargin 22.5% $interiorMargin * 3 22.5%; + order: 2; } + + &__details { + flex: 0 0 auto; + justify-content: flex-end; + order: 3; + } + + &__metadata { + display: flex; + + &__type { + flex: 1 1 auto; + @include ellipsize(); + } + + &__item-count { + opacity: 0.7; + flex: 0 0 auto; + } + } + } } diff --git a/src/plugins/folderView/components/list-item.scss b/src/plugins/folderView/components/list-item.scss index 8e99c4a64e..4c5ccece87 100644 --- a/src/plugins/folderView/components/list-item.scss +++ b/src/plugins/folderView/components/list-item.scss @@ -1,30 +1,30 @@ /******************************* LIST ITEM */ .c-list-item { - &__name__type-icon { - color: $colorItemTreeIcon; - } + &__name__type-icon { + color: $colorItemTreeIcon; + } - &__name__name { - @include ellipsize(); + &__name__name { + @include ellipsize(); - a & { - color: $colorItemFg; - } + a & { + color: $colorItemFg; } + } - &:not(.c-list-item__name) { - color: $colorItemFgDetails; - } + &:not(.c-list-item__name) { + color: $colorItemFgDetails; + } - &.is-alias { - // Object is an alias to an original. - [class*='__type-icon'] { - @include isAlias(); - } + &.is-alias { + // Object is an alias to an original. + [class*='__type-icon'] { + @include isAlias(); } + } - [class*='l-overlay'] & { - // When this view is in an overlay, prevent navigation - pointer-events: none; - } + [class*='l-overlay'] & { + // When this view is in an overlay, prevent navigation + pointer-events: none; + } } diff --git a/src/plugins/folderView/components/status-listener.js b/src/plugins/folderView/components/status-listener.js index 7ee7eee8d4..8ed070c155 100644 --- a/src/plugins/folderView/components/status-listener.js +++ b/src/plugins/folderView/components/status-listener.js @@ -1,33 +1,33 @@ export default { - inject: ['openmct'], - props: { - item: { - type: Object, - required: true - } - }, - computed: { - statusClass() { - return (this.status) ? `is-status--${this.status}` : ''; - } - }, - data() { - return { - status: '' - }; - }, - methods: { - setStatus(status) { - this.status = status; - } - }, - mounted() { - let identifier = this.item.model.identifier; - - this.status = this.openmct.status.get(identifier); - this.removeStatusListener = this.openmct.status.observe(identifier, this.setStatus); - }, - destroyed() { - this.removeStatusListener(); + inject: ['openmct'], + props: { + item: { + type: Object, + required: true } + }, + computed: { + statusClass() { + return this.status ? `is-status--${this.status}` : ''; + } + }, + data() { + return { + status: '' + }; + }, + methods: { + setStatus(status) { + this.status = status; + } + }, + mounted() { + let identifier = this.item.model.identifier; + + this.status = this.openmct.status.get(identifier); + this.removeStatusListener = this.openmct.status.observe(identifier, this.setStatus); + }, + destroyed() { + this.removeStatusListener(); + } }; diff --git a/src/plugins/folderView/plugin.js b/src/plugins/folderView/plugin.js index ec98ea5cd4..c711720f5a 100644 --- a/src/plugins/folderView/plugin.js +++ b/src/plugins/folderView/plugin.js @@ -20,28 +20,23 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './FolderGridView', - './FolderListView' -], function ( - FolderGridView, - FolderListView -) { - return function plugin() { - return function install(openmct) { - openmct.types.addType('folder', { - name: "Folder", - key: "folder", - description: "Create folders to organize other objects or links to objects without the ability to edit it's properties.", - cssClass: "icon-folder", - creatable: true, - initialize: function (domainObject) { - domainObject.composition = []; - } - }); +define(['./FolderGridView', './FolderListView'], function (FolderGridView, FolderListView) { + return function plugin() { + return function install(openmct) { + openmct.types.addType('folder', { + name: 'Folder', + key: 'folder', + description: + "Create folders to organize other objects or links to objects without the ability to edit it's properties.", + cssClass: 'icon-folder', + creatable: true, + initialize: function (domainObject) { + domainObject.composition = []; + } + }); - openmct.objectViews.addProvider(new FolderGridView(openmct)); - openmct.objectViews.addProvider(new FolderListView(openmct)); - }; + openmct.objectViews.addProvider(new FolderGridView(openmct)); + openmct.objectViews.addProvider(new FolderListView(openmct)); }; + }; }); diff --git a/src/plugins/folderView/pluginSpec.js b/src/plugins/folderView/pluginSpec.js index 7ea067c19e..f8a4bf9f41 100644 --- a/src/plugins/folderView/pluginSpec.js +++ b/src/plugins/folderView/pluginSpec.js @@ -21,133 +21,136 @@ *****************************************************************************/ import FolderPlugin from './plugin.js'; import Vue from 'vue'; -import { - createOpenMct, - resetApplicationState -} from 'utils/testing'; +import { createOpenMct, resetApplicationState } from 'utils/testing'; -describe("The folder plugin", () => { - let openmct; - let folderPlugin; +describe('The folder plugin', () => { + let openmct; + let folderPlugin; - beforeEach((done) => { - openmct = createOpenMct(); - folderPlugin = new FolderPlugin(); - openmct.install(folderPlugin); + beforeEach((done) => { + openmct = createOpenMct(); + folderPlugin = new FolderPlugin(); + openmct.install(folderPlugin); - openmct.on('start', done); - openmct.startHeadless(); + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + describe('the folder object type', () => { + let folderType; + + beforeEach(() => { + folderType = openmct.types.get('folder'); }); - afterEach(() => { - return resetApplicationState(openmct); + it('is installed by the plugin', () => { + expect(folderType).toBeDefined(); }); - describe("the folder object type", () => { - let folderType; + it('is user creatable', () => { + expect(folderType.definition.creatable).toBe(true); + }); + }); - beforeEach(() => { - folderType = openmct.types.get('folder'); - }); + describe('the folder grid view', () => { + let gridViewProvider; + let listViewProvider; + let folderObject; + let addCallback; + let parentDiv; + let childDiv; - it("is installed by the plugin", () => { - expect(folderType).toBeDefined(); - }); + beforeEach(() => { + parentDiv = document.createElement('div'); + childDiv = document.createElement('div'); + parentDiv.appendChild(childDiv); - it("is user creatable", () => { - expect(folderType.definition.creatable).toBe(true); + folderObject = { + identifier: { + namespace: 'test-namespace', + key: 'folder-object' + }, + name: 'A folder!', + type: 'folder', + composition: [ + { + namespace: 'test-namespace', + key: 'child-object-1' + }, + { + namespace: 'test-namespace', + key: 'child-object-2' + }, + { + namespace: 'test-namespace', + key: 'child-object-3' + }, + { + namespace: 'test-namespace', + key: 'child-object-4' + } + ] + }; + + gridViewProvider = openmct.objectViews + .get(folderObject, [folderObject]) + .find((view) => view.key === 'grid'); + listViewProvider = openmct.objectViews + .get(folderObject, [folderObject]) + .find((view) => view.key === 'list-view'); + + const fakeCompositionCollection = jasmine.createSpyObj('compositionCollection', [ + 'on', + 'load' + ]); + fakeCompositionCollection.on.and.callFake((eventName, callback) => { + if (eventName === 'add') { + addCallback = callback; + } + }); + fakeCompositionCollection.load.and.callFake(() => { + folderObject.composition.forEach((identifier) => { + addCallback({ + identifier, + type: 'folder' + }); }); + }); + spyOn(openmct.composition, 'get').and.returnValue(fakeCompositionCollection); }); - describe("the folder grid view", () => { - let gridViewProvider; - let listViewProvider; - let folderObject; - let addCallback; - let parentDiv; - let childDiv; + describe('the grid view', () => { + it('is installed by the plugin and is applicable to the folder type', () => { + expect(gridViewProvider).toBeDefined(); + }); + it("renders each item contained in the folder's composition", async () => { + let folderView = gridViewProvider.view(folderObject, [folderObject]); + folderView.show(childDiv, true); - beforeEach(() => { - parentDiv = document.createElement("div"); - childDiv = document.createElement("div"); - parentDiv.appendChild(childDiv); + await Vue.nextTick(); - folderObject = { - identifier: { - namespace: 'test-namespace', - key: 'folder-object' - }, - name: "A folder!", - type: "folder", - composition: [ - { - namespace: 'test-namespace', - key: 'child-object-1' - }, { - namespace: 'test-namespace', - key: 'child-object-2' - }, { - namespace: 'test-namespace', - key: 'child-object-3' - }, { - namespace: 'test-namespace', - key: 'child-object-4' - } - ] - }; - - gridViewProvider = openmct.objectViews.get(folderObject, [folderObject]).find((view) => view.key === 'grid'); - listViewProvider = openmct.objectViews.get(folderObject, [folderObject]).find((view) => view.key === 'list-view'); - - const fakeCompositionCollection = jasmine.createSpyObj('compositionCollection', [ - 'on', - 'load' - ]); - fakeCompositionCollection.on.and.callFake((eventName, callback) => { - if (eventName === "add") { - addCallback = callback; - } - }); - fakeCompositionCollection.load.and.callFake(() => { - folderObject.composition.forEach((identifier) => { - addCallback({ - identifier, - type: "folder" - }); - }); - }); - spyOn(openmct.composition, "get").and.returnValue(fakeCompositionCollection); - }); - - describe("the grid view", () => { - it("is installed by the plugin and is applicable to the folder type", () => { - expect(gridViewProvider).toBeDefined(); - }); - it("renders each item contained in the folder's composition", async () => { - let folderView = gridViewProvider.view(folderObject, [folderObject]); - folderView.show(childDiv, true); - - await Vue.nextTick(); - - let children = parentDiv.getElementsByClassName("js-folder-child"); - expect(children.length).toBe(folderObject.composition.length); - }); - }); - - describe("the list view", () => { - it("installs a list view for the folder type", () => { - expect(listViewProvider).toBeDefined(); - }); - it("renders each item contained in the folder's composition", async () => { - let folderView = listViewProvider.view(folderObject, [folderObject]); - folderView.show(childDiv, true); - - await Vue.nextTick(); - - let children = parentDiv.getElementsByClassName("js-folder-child"); - expect(children.length).toBe(folderObject.composition.length); - }); - }); + let children = parentDiv.getElementsByClassName('js-folder-child'); + expect(children.length).toBe(folderObject.composition.length); + }); }); + describe('the list view', () => { + it('installs a list view for the folder type', () => { + expect(listViewProvider).toBeDefined(); + }); + it("renders each item contained in the folder's composition", async () => { + let folderView = listViewProvider.view(folderObject, [folderObject]); + folderView.show(childDiv, true); + + await Vue.nextTick(); + + let children = parentDiv.getElementsByClassName('js-folder-child'); + expect(children.length).toBe(folderObject.composition.length); + }); + }); + }); }); diff --git a/src/plugins/formActions/CreateAction.js b/src/plugins/formActions/CreateAction.js index 950987898b..4ad579e822 100644 --- a/src/plugins/formActions/CreateAction.js +++ b/src/plugins/formActions/CreateAction.js @@ -27,163 +27,167 @@ import { v4 as uuid } from 'uuid'; import _ from 'lodash'; export default class CreateAction extends PropertiesAction { - #transaction; + #transaction; - constructor(openmct, type, parentDomainObject) { - super(openmct); + constructor(openmct, type, parentDomainObject) { + super(openmct); - this.type = type; - this.parentDomainObject = parentDomainObject; - this.#transaction = null; + this.type = type; + this.parentDomainObject = parentDomainObject; + this.#transaction = null; + } + + invoke() { + this._showCreateForm(this.type); + } + + /** + * @private + */ + async _onSave(changes) { + let parentDomainObjectPath; + + Object.entries(changes).forEach(([key, value]) => { + if (key === 'location') { + parentDomainObjectPath = value; + + return; + } + + const existingValue = this.domainObject[key]; + if (!(existingValue instanceof Array) && typeof existingValue === 'object') { + value = _.merge(existingValue, value); + } + + _.set(this.domainObject, key, value); + }); + + const parentDomainObject = this.openmct.objects.toMutable(parentDomainObjectPath[0]); + + this.domainObject.modified = Date.now(); + this.domainObject.location = this.openmct.objects.makeKeyString(parentDomainObject.identifier); + this.domainObject.identifier.namespace = parentDomainObject.identifier.namespace; + + // Show saving progress dialog + let dialog = this.openmct.overlays.progressDialog({ + progressPerc: 'unknown', + message: + 'Do not navigate away from this page or close this browser tab while this message is displayed.', + iconClass: 'info', + title: 'Saving' + }); + + try { + await this.openmct.objects.save(this.domainObject); + const compositionCollection = await this.openmct.composition.get(parentDomainObject); + compositionCollection.add(this.domainObject); + await this.saveTransaction(); + + this._navigateAndEdit(this.domainObject, parentDomainObjectPath); + + this.openmct.notifications.info('Save successful'); + } catch (err) { + console.error(err); + this.openmct.notifications.error(`Error saving objects: ${err}`); + } finally { + this.openmct.objects.destroyMutable(parentDomainObject); + dialog.dismiss(); + } + } + + /** + * @private + */ + _onCancel() { + this.#transaction.cancel().then(() => { + this.openmct.objects.endTransaction(); + this.#transaction = null; + }); + } + + /** + * @private + */ + async _navigateAndEdit(domainObject, parentDomainObjectpath) { + let objectPath; + let self = this; + if (parentDomainObjectpath) { + objectPath = parentDomainObjectpath && [domainObject].concat(parentDomainObjectpath); + } else { + objectPath = await this.openmct.objects.getOriginalPath(domainObject.identifier); } - invoke() { - this._showCreateForm(this.type); + const url = + '#/browse/' + + objectPath + .map((object) => object && this.openmct.objects.makeKeyString(object.identifier)) + .reverse() + .join('/'); + + function editObject() { + const objectView = self.openmct.objectViews.get(domainObject, objectPath)[0]; + const canEdit = + objectView && objectView.canEdit && objectView.canEdit(domainObject, objectPath); + + if (canEdit) { + self.openmct.editor.edit(); + } } - /** - * @private - */ - async _onSave(changes) { - let parentDomainObjectPath; + this.openmct.router.once('afterNavigation', editObject); - Object.entries(changes).forEach(([key, value]) => { - if (key === 'location') { - parentDomainObjectPath = value; + this.openmct.router.navigate(url); + } - return; - } + /** + * @private + */ + _showCreateForm(type) { + const typeDefinition = this.openmct.types.get(type); + const definition = typeDefinition.definition; + const domainObject = { + name: `Unnamed ${definition.name}`, + type, + identifier: { + key: uuid(), + namespace: this.parentDomainObject.identifier.namespace + } + }; - const existingValue = this.domainObject[key]; - if (!(existingValue instanceof Array) && (typeof existingValue === 'object')) { - value = _.merge(existingValue, value); - } - - _.set(this.domainObject, key, value); - }); - - const parentDomainObject = this.openmct.objects.toMutable(parentDomainObjectPath[0]); - - this.domainObject.modified = Date.now(); - this.domainObject.location = this.openmct.objects.makeKeyString(parentDomainObject.identifier); - this.domainObject.identifier.namespace = parentDomainObject.identifier.namespace; - - // Show saving progress dialog - let dialog = this.openmct.overlays.progressDialog({ - progressPerc: 'unknown', - message: 'Do not navigate away from this page or close this browser tab while this message is displayed.', - iconClass: 'info', - title: 'Saving' - }); - - try { - await this.openmct.objects.save(this.domainObject); - const compositionCollection = await this.openmct.composition.get(parentDomainObject); - compositionCollection.add(this.domainObject); - await this.saveTransaction(); - - this._navigateAndEdit(this.domainObject, parentDomainObjectPath); - - this.openmct.notifications.info('Save successful'); - } catch (err) { - console.error(err); - this.openmct.notifications.error(`Error saving objects: ${err}`); - } finally { - this.openmct.objects.destroyMutable(parentDomainObject); - dialog.dismiss(); - } + this.domainObject = this.openmct.objects.toMutable(domainObject); + if (definition.initialize) { + definition.initialize(this.domainObject); } - /** - * @private - */ - _onCancel() { - this.#transaction.cancel().then(() => { - this.openmct.objects.endTransaction(); - this.#transaction = null; - }); + const createWizard = new CreateWizard(this.openmct, this.domainObject, this.parentDomainObject); + const formStructure = createWizard.getFormStructure(true); + formStructure.title = 'Create a New ' + definition.name; + + this.startTransaction(); + + this.openmct.forms + .showForm(formStructure) + .then(this._onSave.bind(this)) + .catch(this._onCancel.bind(this)) + .finally(() => { + this.openmct.objects.destroyMutable(this.domainObject); + }); + } + + startTransaction() { + if (!this.openmct.objects.isTransactionActive()) { + this.#transaction = this.openmct.objects.startTransaction(); + } + } + + async saveTransaction() { + if (!this.#transaction) { + return; } - /** - * @private - */ - async _navigateAndEdit(domainObject, parentDomainObjectpath) { - let objectPath; - let self = this; - if (parentDomainObjectpath) { - objectPath = parentDomainObjectpath && [domainObject].concat(parentDomainObjectpath); - } else { - objectPath = await this.openmct.objects.getOriginalPath(domainObject.identifier); - } - - const url = '#/browse/' + objectPath - .map(object => object && this.openmct.objects.makeKeyString(object.identifier)) - .reverse() - .join('/'); - - function editObject() { - const objectView = self.openmct.objectViews.get(domainObject, objectPath)[0]; - const canEdit = objectView && objectView.canEdit && objectView.canEdit(domainObject, objectPath); - - if (canEdit) { - self.openmct.editor.edit(); - } - } - - this.openmct.router.once('afterNavigation', editObject); - - this.openmct.router.navigate(url); - } - - /** - * @private - */ - _showCreateForm(type) { - const typeDefinition = this.openmct.types.get(type); - const definition = typeDefinition.definition; - const domainObject = { - name: `Unnamed ${definition.name}`, - type, - identifier: { - key: uuid(), - namespace: this.parentDomainObject.identifier.namespace - } - }; - - this.domainObject = this.openmct.objects.toMutable(domainObject); - - if (definition.initialize) { - definition.initialize(this.domainObject); - } - - const createWizard = new CreateWizard(this.openmct, this.domainObject, this.parentDomainObject); - const formStructure = createWizard.getFormStructure(true); - formStructure.title = 'Create a New ' + definition.name; - - this.startTransaction(); - - this.openmct.forms.showForm(formStructure) - .then(this._onSave.bind(this)) - .catch(this._onCancel.bind(this)) - .finally(() => { - this.openmct.objects.destroyMutable(this.domainObject); - }); - } - - startTransaction() { - if (!this.openmct.objects.isTransactionActive()) { - this.#transaction = this.openmct.objects.startTransaction(); - } - } - - async saveTransaction() { - if (!this.#transaction) { - return; - } - - await this.#transaction.commit(); - this.openmct.objects.endTransaction(); - this.#transaction = null; - } + await this.#transaction.commit(); + this.openmct.objects.endTransaction(); + this.#transaction = null; + } } diff --git a/src/plugins/formActions/CreateActionSpec.js b/src/plugins/formActions/CreateActionSpec.js index 1774609590..74098a68ef 100644 --- a/src/plugins/formActions/CreateActionSpec.js +++ b/src/plugins/formActions/CreateActionSpec.js @@ -21,10 +21,7 @@ *****************************************************************************/ import CreateAction from './CreateAction'; -import { - createOpenMct, - resetApplicationState -} from 'utils/testing'; +import { createOpenMct, resetApplicationState } from 'utils/testing'; import { debounce } from 'lodash'; @@ -32,97 +29,96 @@ let parentObject; let parentObjectPath; let unObserve; -describe("The create action plugin", () => { - let openmct; +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' - ]; + 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(); + beforeEach((done) => { + openmct = createOpenMct(); - openmct.on('start', done); - openmct.startHeadless(); + 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(() => { - return resetApplicationState(openmct); + parentObject = null; + unObserve(); }); - describe('creates new objects for a', () => { - beforeEach(() => { - parentObject = { - name: 'mock folder', - type: 'folder', - identifier: { - key: 'mock-folder', - namespace: '' - }, - composition: [] - }; - parentObjectPath = [parentObject]; + TYPES.forEach((type) => { + it(`type ${type}`, (done) => { + function callback(newObject) { + const composition = newObject.composition; - 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 - }); - }); - }); + openmct.objects.get(composition[0]).then((object) => { + expect(object.type).toEqual(type); + expect(object.location).toEqual(openmct.objects.makeKeyString(parentObject.identifier)); - afterEach(() => { - parentObject = null; - unObserve(); - }); + done(); + }); + } - TYPES.forEach(type => { - it(`type ${type}`, (done) => { - function callback(newObject) { - const composition = newObject.composition; + const deBouncedCallback = debounce(callback, 300); + unObserve = openmct.objects.observe(parentObject, '*', deBouncedCallback); - 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(); - }); - }); + const createAction = new CreateAction(openmct, type, parentObject); + createAction.invoke(); + }); }); + }); }); diff --git a/src/plugins/formActions/CreateWizard.js b/src/plugins/formActions/CreateWizard.js index 3c88f92983..e9c3d1284d 100644 --- a/src/plugins/formActions/CreateWizard.js +++ b/src/plugins/formActions/CreateWizard.js @@ -21,121 +21,125 @@ *****************************************************************************/ export default class CreateWizard { - constructor(openmct, domainObject, parent) { - this.openmct = openmct; + constructor(openmct, domainObject, parent) { + this.openmct = openmct; - this.domainObject = domainObject; - this.type = openmct.types.get(domainObject.type); + this.domainObject = domainObject; + this.type = openmct.types.get(domainObject.type); - this.model = domainObject; - this.parent = parent; - this.properties = this.type.definition.form || []; - } + this.model = domainObject; + this.parent = parent; + this.properties = this.type.definition.form || []; + } - addNotes(sections) { - const row = { - control: 'textarea', - cssClass: 'l-input-lg', - key: 'notes', - name: 'Notes', - required: false, - value: this.domainObject.notes - }; + addNotes(sections) { + const row = { + control: 'textarea', + cssClass: 'l-input-lg', + key: 'notes', + name: 'Notes', + required: false, + value: this.domainObject.notes + }; - sections.forEach(section => { - if (section.name !== 'Properties') { - return; - } + sections.forEach((section) => { + if (section.name !== 'Properties') { + return; + } - section.rows.unshift(row); - }); - } + section.rows.unshift(row); + }); + } - addTitle(sections) { - const row = { - control: 'textfield', - cssClass: 'l-input-lg', - key: 'name', - name: 'Title', - pattern: `\\S+`, + addTitle(sections) { + const row = { + control: 'textfield', + cssClass: 'l-input-lg', + key: 'name', + name: 'Title', + pattern: `\\S+`, + required: true, + value: this.domainObject.name + }; + + sections.forEach((section) => { + if (section.name !== 'Properties') { + return; + } + + section.rows.unshift(row); + }); + } + + /** + * Get the form model for this wizard; this is a description + * that will be rendered to an HTML form. See the + * platform/forms bundle + * @param {boolean} includeLocation if true, a 'location' section + * will be included that will allow the user to select the location + * of the newly created object, otherwise the .location property of + * the model will be used. + */ + getFormStructure(includeLocation) { + let sections = []; + let domainObject = this.domainObject; + let self = this; + + sections.push({ + name: 'Properties', + rows: this.properties + .map((property) => { + const row = JSON.parse(JSON.stringify(property)); + row.value = this.getValue(row); + if (property.validate) { + row.validate = property.validate; + } + + return row; + }) + .filter((row) => row && row.control) + }); + + this.addNotes(sections); + this.addTitle(sections); + + // Ensure there is always a 'save in' section + if (includeLocation) { + function validateLocation(data) { + const policyCheck = self.openmct.composition.checkPolicy(data.value[0], domainObject); + const parentIsPersistable = self.openmct.objects.isPersistable(data.value[0].identifier); + + return policyCheck && parentIsPersistable; + } + + sections.push({ + name: 'Location', + cssClass: 'grows', + rows: [ + { + name: 'Save In', + cssClass: 'grows', + control: 'locator', + domainObject, required: true, - value: this.domainObject.name - }; - - sections.forEach(section => { - if (section.name !== 'Properties') { - return; - } - - section.rows.unshift(row); - }); + parent: this.parent, + validate: validateLocation.bind(this), + key: 'location' + } + ] + }); } - /** - * Get the form model for this wizard; this is a description - * that will be rendered to an HTML form. See the - * platform/forms bundle - * @param {boolean} includeLocation if true, a 'location' section - * will be included that will allow the user to select the location - * of the newly created object, otherwise the .location property of - * the model will be used. - */ - getFormStructure(includeLocation) { - let sections = []; - let domainObject = this.domainObject; - let self = this; + return { + sections + }; + } - sections.push({ - name: 'Properties', - rows: this.properties.map(property => { - const row = JSON.parse(JSON.stringify(property)); - row.value = this.getValue(row); - if (property.validate) { - row.validate = property.validate; - } - - return row; - }).filter(row => row && row.control) - }); - - this.addNotes(sections); - this.addTitle(sections); - - // Ensure there is always a 'save in' section - if (includeLocation) { - function validateLocation(data) { - const policyCheck = self.openmct.composition.checkPolicy(data.value[0], domainObject); - const parentIsPersistable = self.openmct.objects.isPersistable(data.value[0].identifier); - - return policyCheck && parentIsPersistable; - } - - sections.push({ - name: 'Location', - cssClass: 'grows', - rows: [{ - name: 'Save In', - cssClass: 'grows', - control: 'locator', - domainObject, - required: true, - parent: this.parent, - validate: validateLocation.bind(this), - key: 'location' - }] - }); - } - - return { - sections - }; - } - - getValue(row) { - if (row.property) { - return row.property.reduce((acc, property) => acc && acc[property], this.domainObject); - } else { - return this.domainObject[row.key]; - } + getValue(row) { + if (row.property) { + return row.property.reduce((acc, property) => acc && acc[property], this.domainObject); + } else { + return this.domainObject[row.key]; } + } } diff --git a/src/plugins/formActions/EditPropertiesAction.js b/src/plugins/formActions/EditPropertiesAction.js index 0e5aae65b9..24fac1dd15 100644 --- a/src/plugins/formActions/EditPropertiesAction.js +++ b/src/plugins/formActions/EditPropertiesAction.js @@ -25,76 +25,77 @@ import CreateWizard from './CreateWizard'; import _ from 'lodash'; export default class EditPropertiesAction extends PropertiesAction { - constructor(openmct) { - super(openmct); + constructor(openmct) { + super(openmct); - this.name = 'Edit Properties...'; - this.key = 'properties'; - this.description = 'Edit properties of this object.'; - this.cssClass = 'major icon-pencil'; - this.hideInDefaultMenu = true; - this.group = 'action'; - this.priority = 10; - this.formProperties = {}; + this.name = 'Edit Properties...'; + this.key = 'properties'; + this.description = 'Edit properties of this object.'; + this.cssClass = 'major icon-pencil'; + this.hideInDefaultMenu = true; + this.group = 'action'; + this.priority = 10; + this.formProperties = {}; + } + + appliesTo(objectPath) { + const object = objectPath[0]; + const definition = this._getTypeDefinition(object.type); + const persistable = this.openmct.objects.isPersistable(object.identifier); + + return persistable && definition && definition.creatable; + } + + invoke(objectPath) { + return this._showEditForm(objectPath); + } + + /** + * @private + */ + async _onSave(changes) { + if (!this.openmct.objects.isTransactionActive()) { + this.openmct.objects.startTransaction(); } - appliesTo(objectPath) { - const object = objectPath[0]; - const definition = this._getTypeDefinition(object.type); - const persistable = this.openmct.objects.isPersistable(object.identifier); - - return persistable && definition && definition.creatable; - } - - invoke(objectPath) { - return this._showEditForm(objectPath); - } - - /** - * @private - */ - async _onSave(changes) { - if (!this.openmct.objects.isTransactionActive()) { - this.openmct.objects.startTransaction(); + try { + Object.entries(changes).forEach(([key, value]) => { + const existingValue = this.domainObject[key]; + if (!Array.isArray(existingValue) && typeof existingValue === 'object') { + value = _.merge(existingValue, value); } - try { - Object.entries(changes).forEach(([key, value]) => { - const existingValue = this.domainObject[key]; - if (!(Array.isArray(existingValue)) && (typeof existingValue === 'object')) { - value = _.merge(existingValue, value); - } - - this.openmct.objects.mutate(this.domainObject, key, value); - }); - const transaction = this.openmct.objects.getActiveTransaction(); - await transaction.commit(); - this.openmct.objects.endTransaction(); - } catch (error) { - this.openmct.notifications.error('Error saving objects'); - console.error(error); - } + this.openmct.objects.mutate(this.domainObject, key, value); + }); + const transaction = this.openmct.objects.getActiveTransaction(); + await transaction.commit(); + this.openmct.objects.endTransaction(); + } catch (error) { + this.openmct.notifications.error('Error saving objects'); + console.error(error); } + } - /** - * @private - */ - _onCancel() { - //noop - } + /** + * @private + */ + _onCancel() { + //noop + } - /** - * @private - */ - _showEditForm(objectPath) { - this.domainObject = objectPath[0]; + /** + * @private + */ + _showEditForm(objectPath) { + this.domainObject = objectPath[0]; - const createWizard = new CreateWizard(this.openmct, this.domainObject, objectPath[1]); - const formStructure = createWizard.getFormStructure(false); - formStructure.title = 'Edit ' + this.domainObject.name; + const createWizard = new CreateWizard(this.openmct, this.domainObject, objectPath[1]); + const formStructure = createWizard.getFormStructure(false); + formStructure.title = 'Edit ' + this.domainObject.name; - return this.openmct.forms.showForm(formStructure) - .then(this._onSave.bind(this)) - .catch(this._onCancel.bind(this)); - } + return this.openmct.forms + .showForm(formStructure) + .then(this._onSave.bind(this)) + .catch(this._onCancel.bind(this)); + } } diff --git a/src/plugins/formActions/PropertiesAction.js b/src/plugins/formActions/PropertiesAction.js index 5bbd211e13..7092a045e4 100644 --- a/src/plugins/formActions/PropertiesAction.js +++ b/src/plugins/formActions/PropertiesAction.js @@ -20,16 +20,16 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ export default class PropertiesAction { - constructor(openmct) { - this.openmct = openmct; - } + constructor(openmct) { + this.openmct = openmct; + } - /** - * @private - */ - _getTypeDefinition(type) { - const TypeDefinition = this.openmct.types.get(type); + /** + * @private + */ + _getTypeDefinition(type) { + const TypeDefinition = this.openmct.types.get(type); - return TypeDefinition.definition; - } + return TypeDefinition.definition; + } } diff --git a/src/plugins/formActions/plugin.js b/src/plugins/formActions/plugin.js index 997f1693ab..cde6ef5744 100644 --- a/src/plugins/formActions/plugin.js +++ b/src/plugins/formActions/plugin.js @@ -23,7 +23,7 @@ import EditPropertiesAction from './EditPropertiesAction'; export default function () { - return function (openmct) { - openmct.actions.register(new EditPropertiesAction(openmct)); - }; + return function (openmct) { + openmct.actions.register(new EditPropertiesAction(openmct)); + }; } diff --git a/src/plugins/formActions/pluginSpec.js b/src/plugins/formActions/pluginSpec.js index 58190bf180..1952b9969c 100644 --- a/src/plugins/formActions/pluginSpec.js +++ b/src/plugins/formActions/pluginSpec.js @@ -19,195 +19,193 @@ * 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 { createMouseEvent, createOpenMct, resetApplicationState } from 'utils/testing'; import Vue from 'vue'; import { debounce } from 'lodash'; describe('EditPropertiesAction plugin', () => { - let editPropertiesAction; - let openmct; - let element; + let editPropertiesAction; + let openmct; + let element; - beforeEach((done) => { - element = document.createElement('div'); - element.style.display = 'block'; - element.style.width = '1920px'; - element.style.height = '1080px'; + 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); + openmct = createOpenMct(); + openmct.on('start', done); + openmct.startHeadless(element); - editPropertiesAction = openmct.actions.getAction('properties'); + 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: [] + }; + + editPropertiesAction + .invoke([domainObject]) + .then(() => { + done(); + }) + .catch(() => { + done(); + }); + + Vue.nextTick(() => { + 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); }); + }); - afterEach(() => { - editPropertiesAction = null; + 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; - return resetApplicationState(openmct); + 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); + + editPropertiesAction.invoke([domainObject]); + + Vue.nextTick(() => { + 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'); + + 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(); + + const clickEvent = createMouseEvent('click'); + buttons[0].dispatchEvent(clickEvent); }); + }); - it('editPropertiesAction exists', () => { - expect(editPropertiesAction.key).toEqual('properties'); - }); + 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: [] + }; - it('edit properties action applies to only persistable objects', () => { - spyOn(openmct.objects, 'isPersistable').and.returnValue(true); + editPropertiesAction + .invoke([domainObject]) + .then(() => { + expect(domainObject.name).toEqual(name); + done(); + }) + .catch(() => { + expect(domainObject.name).toEqual(name); + done(); + }); - 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: [] - }; - - editPropertiesAction.invoke([domainObject]) - .then(() => { - done(); - }) - .catch(() => { - done(); - }); - - Vue.nextTick(() => { - 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); - }); - }); - - 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); - - editPropertiesAction.invoke([domainObject]); - - Vue.nextTick(() => { - 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'); - - 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(); - - const clickEvent = createMouseEvent('click'); - buttons[0].dispatchEvent(clickEvent); - }); - }); - - 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); - }); + 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 43b54fbfeb..20bbcc3759 100644 --- a/src/plugins/gauge/GaugePlugin.js +++ b/src/plugins/gauge/GaugePlugin.js @@ -25,187 +25,169 @@ import GaugeFormController from './components/GaugeFormController.vue'; import Vue from 'vue'; export const GAUGE_TYPES = [ - ['Filled Dial', 'dial-filled'], - ['Needle Dial', 'dial-needle'], - ['Vertical Meter', 'meter-vertical'], - ['Vertical Meter Inverted', 'meter-vertical-inverted'], - ['Horizontal Meter', 'meter-horizontal'] + ['Filled Dial', 'dial-filled'], + ['Needle Dial', 'dial-needle'], + ['Vertical Meter', 'meter-vertical'], + ['Vertical Meter Inverted', 'meter-vertical-inverted'], + ['Horizontal Meter', 'meter-horizontal'] ]; export default function () { - return function install(openmct) { - openmct.objectViews.addProvider(new GaugeViewProvider(openmct)); + return function install(openmct) { + openmct.objectViews.addProvider(new GaugeViewProvider(openmct)); - openmct.forms.addNewFormControl('gauge-controller', getGaugeFormController(openmct)); - openmct.types.addType('gauge', { - name: "Gauge", - creatable: true, - description: "Graphically visualize a telemetry element's current value between a minimum and maximum.", - cssClass: 'icon-gauge', - initialize(domainObject) { - domainObject.composition = []; - domainObject.configuration = { - gaugeController: { - gaugeType: GAUGE_TYPES[0][1], - isDisplayMinMax: true, - isDisplayCurVal: true, - isDisplayUnits: true, - isUseTelemetryLimits: true, - limitLow: 10, - limitHigh: 90, - max: 100, - min: 0, - precision: 2 - } - }; - }, - 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", - key: "isDisplayCurVal", - property: [ - "configuration", - "gaugeController", - "isDisplayCurVal" - ] - }, - { - name: "Display units", - control: "toggleSwitch", - cssClass: "l-input", - key: "isDisplayUnits", - property: [ - "configuration", - "gaugeController", - "isDisplayUnits" - ] - }, - { - name: "Display range values", - control: "toggleSwitch", - cssClass: "l-input", - key: "isDisplayMinMax", - property: [ - "configuration", - "gaugeController", - "isDisplayMinMax" - ] - }, - { - name: "Float precision", - control: "numberfield", - cssClass: "l-input-sm", - key: "precision", - property: [ - "configuration", - "gaugeController", - "precision" - ] - }, - { - name: "Value ranges and limits", - control: "gauge-controller", - cssClass: "l-input", - key: "gaugeController", - required: false, - hideFromInspector: true, - property: [ - "configuration", - "gaugeController" - ], - validate: ({ value }, callback) => { - if (value.isUseTelemetryLimits) { - return true; - } - - const { min, max, limitLow, limitHigh } = value; - const valid = { - min: true, - max: true, - limitLow: true, - limitHigh: true - }; - - if (min === '') { - valid.min = false; - } - - if (max === '') { - valid.max = false; - } - - if (max < min) { - valid.min = false; - valid.max = false; - } - - if (limitLow !== '') { - valid.limitLow = min <= limitLow && limitLow < max; - } - - if (limitHigh !== '') { - valid.limitHigh = min < limitHigh && limitHigh <= max; - } - - if (valid.limitLow && valid.limitHigh - && limitLow !== '' && limitHigh !== '' - && limitLow > limitHigh) { - valid.limitLow = false; - valid.limitHigh = false; - } - - if (callback) { - callback(valid); - } - - return valid.min && valid.max && valid.limitLow && valid.limitHigh; - } - } - ] - }); - }; - - function getGaugeFormController(openmct) { - return { - show(element, model, onChange) { - const rowComponent = new Vue({ - el: element, - components: { - GaugeFormController - }, - provide: { - openmct - }, - data() { - return { - model, - onChange - }; - }, - template: `` - }); - - return rowComponent; - } + openmct.forms.addNewFormControl('gauge-controller', getGaugeFormController(openmct)); + openmct.types.addType('gauge', { + name: 'Gauge', + creatable: true, + description: + "Graphically visualize a telemetry element's current value between a minimum and maximum.", + cssClass: 'icon-gauge', + initialize(domainObject) { + domainObject.composition = []; + domainObject.configuration = { + gaugeController: { + gaugeType: GAUGE_TYPES[0][1], + isDisplayMinMax: true, + isDisplayCurVal: true, + isDisplayUnits: true, + isUseTelemetryLimits: true, + limitLow: 10, + limitHigh: 90, + max: 100, + min: 0, + precision: 2 + } }; - } + }, + 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', + key: 'isDisplayCurVal', + property: ['configuration', 'gaugeController', 'isDisplayCurVal'] + }, + { + name: 'Display units', + control: 'toggleSwitch', + cssClass: 'l-input', + key: 'isDisplayUnits', + property: ['configuration', 'gaugeController', 'isDisplayUnits'] + }, + { + name: 'Display range values', + control: 'toggleSwitch', + cssClass: 'l-input', + key: 'isDisplayMinMax', + property: ['configuration', 'gaugeController', 'isDisplayMinMax'] + }, + { + name: 'Float precision', + control: 'numberfield', + cssClass: 'l-input-sm', + key: 'precision', + property: ['configuration', 'gaugeController', 'precision'] + }, + { + name: 'Value ranges and limits', + control: 'gauge-controller', + cssClass: 'l-input', + key: 'gaugeController', + required: false, + hideFromInspector: true, + property: ['configuration', 'gaugeController'], + validate: ({ value }, callback) => { + if (value.isUseTelemetryLimits) { + return true; + } + + const { min, max, limitLow, limitHigh } = value; + const valid = { + min: true, + max: true, + limitLow: true, + limitHigh: true + }; + + if (min === '') { + valid.min = false; + } + + if (max === '') { + valid.max = false; + } + + if (max < min) { + valid.min = false; + valid.max = false; + } + + if (limitLow !== '') { + valid.limitLow = min <= limitLow && limitLow < max; + } + + if (limitHigh !== '') { + valid.limitHigh = min < limitHigh && limitHigh <= max; + } + + if ( + valid.limitLow && + valid.limitHigh && + limitLow !== '' && + limitHigh !== '' && + limitLow > limitHigh + ) { + valid.limitLow = false; + valid.limitHigh = false; + } + + if (callback) { + callback(valid); + } + + return valid.min && valid.max && valid.limitLow && valid.limitHigh; + } + } + ] + }); + }; + + function getGaugeFormController(openmct) { + return { + show(element, model, onChange) { + const rowComponent = new Vue({ + el: element, + components: { + GaugeFormController + }, + provide: { + openmct + }, + data() { + return { + model, + onChange + }; + }, + template: `` + }); + + return rowComponent; + } + }; + } } diff --git a/src/plugins/gauge/GaugePluginSpec.js b/src/plugins/gauge/GaugePluginSpec.js index cd5f448ad6..36c4a72e10 100644 --- a/src/plugins/gauge/GaugePluginSpec.js +++ b/src/plugins/gauge/GaugePluginSpec.js @@ -19,790 +19,810 @@ * 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 { createOpenMct, resetApplicationState } from 'utils/testing'; import { debounce } from 'lodash'; import Vue from 'vue'; let gaugeDomainObject = { - identifier: { - key: 'gauge', - namespace: 'test-namespace' - }, - type: 'gauge', - composition: [] + identifier: { + key: 'gauge', + namespace: 'test-namespace' + }, + type: 'gauge', + composition: [] }; describe('Gauge plugin', () => { - let openmct; - let child; - let gaugeHolder; + let openmct; + let child; + let gaugeHolder; - beforeEach((done) => { - gaugeHolder = document.createElement('div'); - gaugeHolder.style.display = 'block'; - gaugeHolder.style.width = '1920px'; - gaugeHolder.style.height = '1080px'; + beforeEach((done) => { + gaugeHolder = document.createElement('div'); + gaugeHolder.style.display = 'block'; + gaugeHolder.style.width = '1920px'; + gaugeHolder.style.height = '1080px'; - child = document.createElement('div'); - gaugeHolder.appendChild(child); + child = document.createElement('div'); + gaugeHolder.appendChild(child); - openmct = createOpenMct(); - openmct.on('start', done); + openmct = createOpenMct(); + openmct.on('start', done); - openmct.install(openmct.plugins.Gauge()); + openmct.install(openmct.plugins.Gauge()); - openmct.startHeadless(); + openmct.startHeadless(); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('Plugin installed by default', () => { + const GaugeType = openmct.types.get('gauge'); + + expect(GaugeType).not.toBeNull(); + expect(GaugeType.definition.name).toEqual('Gauge'); + }); + + it('Gauge plugin is creatable', () => { + const GaugeType = openmct.types.get('gauge'); + + expect(GaugeType.definition.creatable).toBeTrue(); + }); + + it('Gauge plugin is creatable', () => { + const GaugeType = openmct.types.get('gauge'); + + expect(GaugeType.definition.creatable).toBeTrue(); + }); + + it('Gauge form controller', () => { + const gaugeController = openmct.forms.getFormControl('gauge-controller'); + expect(gaugeController).toBeDefined(); + }); + + describe('Gauge with Filled Dial', () => { + let gaugeViewProvider; + let gaugeView; + let gaugeViewObject; + let mutablegaugeObject; + let randomValue; + + const minValue = -1; + const maxValue = 1; + + beforeEach(() => { + randomValue = Math.random(); + gaugeViewObject = { + ...gaugeDomainObject, + configuration: { + gaugeController: { + gaugeType: 'dial-filled', + isDisplayMinMax: true, + isDisplayCurVal: true, + isDisplayUnits: true, + isUseTelemetryLimits: false, + limitLow: -0.9, + limitHigh: 0.9, + max: maxValue, + min: minValue, + precision: 2 + } + }, + composition: [ + { + namespace: 'test-namespace', + key: 'test-object' + } + ], + id: 'test-object', + name: 'gauge' + }; + + const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [ + 'get', + 'create', + 'update', + 'observe' + ]); + + openmct.editor = {}; + openmct.editor.isEditing = () => false; + + const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]); + gaugeViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'gauge'); + + testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject)); + testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject)); + openmct.objects.addProvider('test-namespace', testObjectProvider); + testObjectProvider.observe.and.returnValue(() => {}); + testObjectProvider.create.and.returnValue(Promise.resolve(true)); + testObjectProvider.update.and.returnValue(Promise.resolve(true)); + + spyOn(openmct.telemetry, 'getMetadata').and.returnValue({ + valuesForHints: () => { + return [ + { + source: 'sin' + } + ]; + }, + value: () => 1 + }); + spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({ + parse: () => { + return 2000; + } + }); + spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({ + sin: { + format: (datum) => { + return randomValue; + } + } + }); + spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() }); + spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue])); + spyOn(openmct.time, 'bounds').and.returnValue({ + start: 1000, + end: 5000 + }); + + return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { + mutablegaugeObject = mutableObject; + gaugeView = gaugeViewProvider.view(mutablegaugeObject); + gaugeView.show(child); + + return Vue.nextTick(); + }); }); afterEach(() => { - return resetApplicationState(openmct); + gaugeView.destroy(); + + return resetApplicationState(openmct); }); - it('Plugin installed by default', () => { - const GaugeType = openmct.types.get('gauge'); - - expect(GaugeType).not.toBeNull(); - expect(GaugeType.definition.name).toEqual('Gauge'); + it('provides gauge view', () => { + expect(gaugeViewProvider).toBeDefined(); }); - it('Gauge plugin is creatable', () => { - const GaugeType = openmct.types.get('gauge'); - - expect(GaugeType.definition.creatable).toBeTrue(); + it('renders gauge element', () => { + const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper'); + expect(gaugeElement.length).toBe(1); }); - it('Gauge plugin is creatable', () => { - const GaugeType = openmct.types.get('gauge'); + it('renders major elements', () => { + const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper'); + const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range'); + const valueElement = gaugeHolder.querySelector('.js-dial-current-value'); - expect(GaugeType.definition.creatable).toBeTrue(); + const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement); + + expect(hasMajorElements).toBe(true); }); - it('Gauge form controller', () => { - const gaugeController = openmct.forms.getFormControl('gauge-controller'); - expect(gaugeController).toBeDefined(); + it('renders correct min max values', () => { + expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toMatch( + new RegExp(`\\s*${minValue}\\s*${maxValue}\\s*`) + ); }); - describe('Gauge with Filled Dial', () => { - let gaugeViewProvider; - let gaugeView; - let gaugeViewObject; - let mutablegaugeObject; - let randomValue; + it('renders correct current value', (done) => { + function WatchUpdateValue() { + const textElement = gaugeHolder.querySelector('.js-dial-current-value'); + expect( + Number(textElement.textContent).toFixed( + gaugeViewObject.configuration.gaugeController.precision + ) + ).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision)); + done(); + } - const minValue = -1; - const maxValue = 1; + const debouncedWatchUpdate = debounce(WatchUpdateValue, 200); + Vue.nextTick(debouncedWatchUpdate); + }); + }); - beforeEach(() => { - randomValue = Math.random(); - gaugeViewObject = { - ...gaugeDomainObject, - configuration: { - gaugeController: { - gaugeType: 'dial-filled', - isDisplayMinMax: true, - isDisplayCurVal: true, - isDisplayUnits: true, - isUseTelemetryLimits: false, - limitLow: -0.9, - limitHigh: 0.9, - max: maxValue, - min: minValue, - precision: 2 - } - }, - composition: [ - { - namespace: 'test-namespace', - key: 'test-object' - } - ], - id: 'test-object', - name: 'gauge' - }; + describe('Gauge with Needle Dial', () => { + let gaugeViewProvider; + let gaugeView; + let gaugeViewObject; + let mutablegaugeObject; + let randomValue; - const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [ - 'get', - 'create', - 'update', - 'observe' - ]); + const minValue = -1; + const maxValue = 1; + beforeEach(() => { + randomValue = Math.random(); + gaugeViewObject = { + ...gaugeDomainObject, + configuration: { + gaugeController: { + gaugeType: 'dial-needle', + isDisplayMinMax: true, + isDisplayCurVal: true, + isDisplayUnits: true, + isUseTelemetryLimits: false, + limitLow: -0.9, + limitHigh: 0.9, + max: maxValue, + min: minValue, + precision: 2 + } + }, + composition: [ + { + namespace: 'test-namespace', + key: 'test-object' + } + ], + id: 'test-object', + name: 'gauge' + }; - openmct.editor = {}; - openmct.editor.isEditing = () => false; + const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [ + 'get', + 'create', + 'update', + 'observe' + ]); - const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]); - gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge'); + openmct.editor = {}; + openmct.editor.isEditing = () => false; - testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject)); - testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject)); - openmct.objects.addProvider('test-namespace', testObjectProvider); - testObjectProvider.observe.and.returnValue(() => {}); - testObjectProvider.create.and.returnValue(Promise.resolve(true)); - testObjectProvider.update.and.returnValue(Promise.resolve(true)); + const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]); + gaugeViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'gauge'); - spyOn(openmct.telemetry, 'getMetadata').and.returnValue({ - valuesForHints: () => { - return [ - { - source: 'sin' - } - ]; - }, - value: () => 1 - }); - spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({ - parse: () => { - return 2000; - } - }); - spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({ - sin: { - format: (datum) => { - return randomValue; - } - } - }); - spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() }); - spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue])); - spyOn(openmct.time, 'bounds').and.returnValue({ - start: 1000, - end: 5000 - }); + testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject)); + testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject)); + openmct.objects.addProvider('test-namespace', testObjectProvider); + testObjectProvider.observe.and.returnValue(() => {}); + testObjectProvider.create.and.returnValue(Promise.resolve(true)); + testObjectProvider.update.and.returnValue(Promise.resolve(true)); - return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { - mutablegaugeObject = mutableObject; - gaugeView = gaugeViewProvider.view(mutablegaugeObject); - gaugeView.show(child); - - return Vue.nextTick(); - }); - }); - - afterEach(() => { - gaugeView.destroy(); - - return resetApplicationState(openmct); - }); - - it('provides gauge view', () => { - expect(gaugeViewProvider).toBeDefined(); - }); - - it('renders gauge element', () => { - const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper'); - expect(gaugeElement.length).toBe(1); - }); - - it('renders major elements', () => { - const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper'); - const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range'); - const valueElement = gaugeHolder.querySelector('.js-dial-current-value'); - - const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement); - - expect(hasMajorElements).toBe(true); - }); - - it('renders correct min max values', () => { - expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toMatch(new RegExp(`\\s*${minValue}\\s*${maxValue}\\s*`)); - }); - - it('renders correct current value', (done) => { - function WatchUpdateValue() { - const textElement = gaugeHolder.querySelector('.js-dial-current-value'); - expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision)); - done(); + spyOn(openmct.telemetry, 'getMetadata').and.returnValue({ + valuesForHints: () => { + return [ + { + source: 'sin' } + ]; + }, + value: () => 1 + }); + spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({ + parse: () => { + return 2000; + } + }); + spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({ + sin: { + format: (datum) => { + return randomValue; + } + } + }); + spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() }); + spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue])); + spyOn(openmct.time, 'bounds').and.returnValue({ + start: 1000, + end: 5000 + }); - const debouncedWatchUpdate = debounce(WatchUpdateValue, 200); - Vue.nextTick(debouncedWatchUpdate); - }); + return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { + mutablegaugeObject = mutableObject; + gaugeView = gaugeViewProvider.view(mutablegaugeObject); + gaugeView.show(child); + + return Vue.nextTick(); + }); }); - describe('Gauge with Needle Dial', () => { - let gaugeViewProvider; - let gaugeView; - let gaugeViewObject; - let mutablegaugeObject; - let randomValue; + afterEach(() => { + gaugeView.destroy(); - const minValue = -1; - const maxValue = 1; - beforeEach(() => { - randomValue = Math.random(); - gaugeViewObject = { - ...gaugeDomainObject, - configuration: { - gaugeController: { - gaugeType: 'dial-needle', - isDisplayMinMax: true, - isDisplayCurVal: true, - isDisplayUnits: true, - isUseTelemetryLimits: false, - limitLow: -0.9, - limitHigh: 0.9, - max: maxValue, - min: minValue, - precision: 2 - } - }, - composition: [ - { - namespace: 'test-namespace', - key: 'test-object' - } - ], - id: 'test-object', - name: 'gauge' - }; + return resetApplicationState(openmct); + }); - const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [ - 'get', - 'create', - 'update', - 'observe' - ]); + it('provides gauge view', () => { + expect(gaugeViewProvider).toBeDefined(); + }); - openmct.editor = {}; - openmct.editor.isEditing = () => false; + it('renders gauge element', () => { + const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper'); + expect(gaugeElement.length).toBe(1); + }); - const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]); - gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge'); + it('renders major elements', () => { + const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper'); + const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range'); + const valueElement = gaugeHolder.querySelector('.js-dial-current-value'); - testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject)); - testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject)); - openmct.objects.addProvider('test-namespace', testObjectProvider); - testObjectProvider.observe.and.returnValue(() => {}); - testObjectProvider.create.and.returnValue(Promise.resolve(true)); - testObjectProvider.update.and.returnValue(Promise.resolve(true)); + const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement); - spyOn(openmct.telemetry, 'getMetadata').and.returnValue({ - valuesForHints: () => { - return [ - { - source: 'sin' - } - ]; - }, - value: () => 1 - }); - spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({ - parse: () => { - return 2000; - } - }); - spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({ - sin: { - format: (datum) => { - return randomValue; - } - } - }); - spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() }); - spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue])); - spyOn(openmct.time, 'bounds').and.returnValue({ - start: 1000, - end: 5000 - }); + expect(hasMajorElements).toBe(true); + }); - return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { - mutablegaugeObject = mutableObject; - gaugeView = gaugeViewProvider.view(mutablegaugeObject); - gaugeView.show(child); + it('renders correct min max values', () => { + expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toMatch( + new RegExp(`\\s*${minValue}\\s*${maxValue}\\s*`) + ); + }); - return Vue.nextTick(); - }); - }); + it('renders correct current value', (done) => { + function WatchUpdateValue() { + const textElement = gaugeHolder.querySelector('.js-dial-current-value'); + expect( + Number(textElement.textContent).toFixed( + gaugeViewObject.configuration.gaugeController.precision + ) + ).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision)); + done(); + } - afterEach(() => { - gaugeView.destroy(); + const debouncedWatchUpdate = debounce(WatchUpdateValue, 200); + Vue.nextTick(debouncedWatchUpdate); + }); + }); - return resetApplicationState(openmct); - }); + describe('Gauge with Vertical Meter', () => { + let gaugeViewProvider; + let gaugeView; + let gaugeViewObject; + let mutablegaugeObject; + let randomValue; - it('provides gauge view', () => { - expect(gaugeViewProvider).toBeDefined(); - }); + const minValue = -1; + const maxValue = 1; + beforeEach(() => { + randomValue = Math.random(); + gaugeViewObject = { + ...gaugeDomainObject, + configuration: { + gaugeController: { + gaugeType: 'meter-vertical', + isDisplayMinMax: true, + isDisplayCurVal: true, + isDisplayUnits: true, + isUseTelemetryLimits: false, + limitLow: -0.9, + limitHigh: 0.9, + max: maxValue, + min: minValue, + precision: 2 + } + }, + composition: [ + { + namespace: 'test-namespace', + key: 'test-object' + } + ], + id: 'test-object', + name: 'gauge' + }; - it('renders gauge element', () => { - const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper'); - expect(gaugeElement.length).toBe(1); - }); + const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [ + 'get', + 'create', + 'update', + 'observe' + ]); - it('renders major elements', () => { - const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper'); - const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range'); - const valueElement = gaugeHolder.querySelector('.js-dial-current-value'); + openmct.editor = {}; + openmct.editor.isEditing = () => false; - const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement); + const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]); + gaugeViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'gauge'); - expect(hasMajorElements).toBe(true); - }); + testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject)); + testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject)); + openmct.objects.addProvider('test-namespace', testObjectProvider); + testObjectProvider.observe.and.returnValue(() => {}); + testObjectProvider.create.and.returnValue(Promise.resolve(true)); + testObjectProvider.update.and.returnValue(Promise.resolve(true)); - it('renders correct min max values', () => { - expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toMatch(new RegExp(`\\s*${minValue}\\s*${maxValue}\\s*`)); - }); - - it('renders correct current value', (done) => { - function WatchUpdateValue() { - const textElement = gaugeHolder.querySelector('.js-dial-current-value'); - expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision)); - done(); + spyOn(openmct.telemetry, 'getMetadata').and.returnValue({ + valuesForHints: () => { + return [ + { + source: 'sin' } + ]; + }, + value: () => 1 + }); + spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({ + parse: () => { + return 2000; + } + }); + spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({ + sin: { + format: (datum) => { + return randomValue; + } + } + }); + spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() }); + spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue])); + spyOn(openmct.time, 'bounds').and.returnValue({ + start: 1000, + end: 5000 + }); - const debouncedWatchUpdate = debounce(WatchUpdateValue, 200); - Vue.nextTick(debouncedWatchUpdate); - }); + return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { + mutablegaugeObject = mutableObject; + gaugeView = gaugeViewProvider.view(mutablegaugeObject); + gaugeView.show(child); + + return Vue.nextTick(); + }); }); - describe('Gauge with Vertical Meter', () => { - let gaugeViewProvider; - let gaugeView; - let gaugeViewObject; - let mutablegaugeObject; - let randomValue; + afterEach(() => { + gaugeView.destroy(); - const minValue = -1; - const maxValue = 1; - beforeEach(() => { - randomValue = Math.random(); - gaugeViewObject = { - ...gaugeDomainObject, - configuration: { - gaugeController: { - gaugeType: 'meter-vertical', - isDisplayMinMax: true, - isDisplayCurVal: true, - isDisplayUnits: true, - isUseTelemetryLimits: false, - limitLow: -0.9, - limitHigh: 0.9, - max: maxValue, - min: minValue, - precision: 2 - } - }, - composition: [ - { - namespace: 'test-namespace', - key: 'test-object' - } - ], - id: 'test-object', - name: 'gauge' - }; + return resetApplicationState(openmct); + }); - const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [ - 'get', - 'create', - 'update', - 'observe' - ]); + it('provides gauge view', () => { + expect(gaugeViewProvider).toBeDefined(); + }); - openmct.editor = {}; - openmct.editor.isEditing = () => false; + it('renders gauge element', () => { + const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper'); + expect(gaugeElement.length).toBe(1); + }); - const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]); - gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge'); + it('renders major elements', () => { + const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper'); + const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range'); + const valueElement = gaugeHolder.querySelector('.js-gauge-current-value'); - testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject)); - testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject)); - openmct.objects.addProvider('test-namespace', testObjectProvider); - testObjectProvider.observe.and.returnValue(() => {}); - testObjectProvider.create.and.returnValue(Promise.resolve(true)); - testObjectProvider.update.and.returnValue(Promise.resolve(true)); + const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement); - spyOn(openmct.telemetry, 'getMetadata').and.returnValue({ - valuesForHints: () => { - return [ - { - source: 'sin' - } - ]; - }, - value: () => 1 - }); - spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({ - parse: () => { - return 2000; - } - }); - spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({ - sin: { - format: (datum) => { - return randomValue; - } - } - }); - spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() }); - spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue])); - spyOn(openmct.time, 'bounds').and.returnValue({ - start: 1000, - end: 5000 - }); + expect(hasMajorElements).toBe(true); + }); - return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { - mutablegaugeObject = mutableObject; - gaugeView = gaugeViewProvider.view(mutablegaugeObject); - gaugeView.show(child); + it('renders correct min max values', () => { + expect(gaugeHolder.querySelector('.js-gauge-meter-range').textContent).toMatch( + new RegExp(`\\s*${maxValue}\\s*${minValue}\\s*`) + ); + }); - return Vue.nextTick(); - }); - }); + it('renders correct current value', (done) => { + function WatchUpdateValue() { + 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(); + } - afterEach(() => { - gaugeView.destroy(); + const debouncedWatchUpdate = debounce(WatchUpdateValue, 200); + Vue.nextTick(debouncedWatchUpdate); + }); + }); - return resetApplicationState(openmct); - }); + describe('Gauge with Vertical Meter Inverted', () => { + let gaugeViewProvider; + let gaugeView; + let gaugeViewObject; + let mutablegaugeObject; - it('provides gauge view', () => { - expect(gaugeViewProvider).toBeDefined(); - }); + beforeEach(() => { + gaugeViewObject = { + ...gaugeDomainObject, + configuration: { + gaugeController: { + gaugeType: 'meter-vertical', + isDisplayMinMax: true, + isDisplayCurVal: true, + isDisplayUnits: true, + isUseTelemetryLimits: false, + limitLow: -0.9, + limitHigh: 0.9, + max: 1, + min: -1, + precision: 2 + } + }, + id: 'test-object', + name: 'gauge' + }; - it('renders gauge element', () => { - const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper'); - expect(gaugeElement.length).toBe(1); - }); + const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [ + 'get', + 'create', + 'update', + 'observe' + ]); - it('renders major elements', () => { - const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper'); - const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range'); - const valueElement = gaugeHolder.querySelector('.js-gauge-current-value'); + openmct.editor = {}; + openmct.editor.isEditing = () => false; - const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement); + const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]); + gaugeViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'gauge'); - expect(hasMajorElements).toBe(true); - }); + testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject)); + testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject)); + openmct.objects.addProvider('test-namespace', testObjectProvider); + testObjectProvider.observe.and.returnValue(() => {}); + testObjectProvider.create.and.returnValue(Promise.resolve(true)); + testObjectProvider.update.and.returnValue(Promise.resolve(true)); - it('renders correct min max values', () => { - expect(gaugeHolder.querySelector('.js-gauge-meter-range').textContent).toMatch(new RegExp(`\\s*${maxValue}\\s*${minValue}\\s*`)); - }); + return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { + mutablegaugeObject = mutableObject; - it('renders correct current value', (done) => { - function WatchUpdateValue() { - 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(); + gaugeView = gaugeViewProvider.view(mutablegaugeObject); + gaugeView.show(child); + + return Vue.nextTick(); + }); + }); + + afterEach(() => { + gaugeView.destroy(); + + return resetApplicationState(openmct); + }); + + it('provides gauge view', () => { + expect(gaugeViewProvider).toBeDefined(); + }); + + it('renders gauge element', () => { + const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper'); + expect(gaugeElement.length).toBe(1); + }); + + it('renders major elements', () => { + const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper'); + const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range'); + const valueElement = gaugeHolder.querySelector('.js-gauge-current-value'); + + const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement); + + expect(hasMajorElements).toBe(true); + }); + }); + + describe('Gauge with Horizontal Meter', () => { + let gaugeViewProvider; + let gaugeView; + let gaugeViewObject; + let mutablegaugeObject; + + beforeEach(() => { + gaugeViewObject = { + ...gaugeDomainObject, + configuration: { + gaugeController: { + gaugeType: 'meter-vertical', + isDisplayMinMax: true, + isDisplayCurVal: true, + isDisplayUnits: true, + isUseTelemetryLimits: false, + limitLow: -0.9, + limitHigh: 0.9, + max: 1, + min: -1, + precision: 2 + } + }, + id: 'test-object', + name: 'gauge' + }; + + const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [ + 'get', + 'create', + 'update', + 'observe' + ]); + + openmct.editor = {}; + openmct.editor.isEditing = () => false; + + const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]); + gaugeViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'gauge'); + + testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject)); + testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject)); + openmct.objects.addProvider('test-namespace', testObjectProvider); + testObjectProvider.observe.and.returnValue(() => {}); + testObjectProvider.create.and.returnValue(Promise.resolve(true)); + testObjectProvider.update.and.returnValue(Promise.resolve(true)); + + return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { + mutablegaugeObject = mutableObject; + + gaugeView = gaugeViewProvider.view(mutablegaugeObject); + gaugeView.show(child); + + return Vue.nextTick(); + }); + }); + + afterEach(() => { + gaugeView.destroy(); + + return resetApplicationState(openmct); + }); + + it('provides gauge view', () => { + expect(gaugeViewProvider).toBeDefined(); + }); + + it('renders gauge element', () => { + const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper'); + expect(gaugeElement.length).toBe(1); + }); + + it('renders major elements', () => { + const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper'); + const rangeElement = gaugeHolder.querySelector('.c-gauge__range'); + const curveElement = gaugeHolder.querySelector('.c-meter'); + + const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement); + + expect(hasMajorElements).toBe(true); + }); + }); + + describe('Gauge with Filled Dial with Use Telemetry Limits', () => { + let gaugeViewProvider; + let gaugeView; + let gaugeViewObject; + let mutablegaugeObject; + let randomValue; + + beforeEach(() => { + randomValue = Math.random(); + + gaugeViewObject = { + ...gaugeDomainObject, + configuration: { + gaugeController: { + gaugeType: 'dial-filled', + isDisplayMinMax: true, + isDisplayCurVal: true, + isDisplayUnits: true, + isUseTelemetryLimits: true, + limitLow: 10, + limitHigh: 90, + max: 100, + min: 0, + precision: 2 + } + }, + composition: [ + { + namespace: 'test-namespace', + key: 'test-object' + } + ], + id: 'test-object', + name: 'gauge' + }; + + const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [ + 'get', + 'create', + 'update', + 'observe' + ]); + + openmct.editor = {}; + openmct.editor.isEditing = () => false; + + const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]); + gaugeViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'gauge'); + + testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject)); + testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject)); + openmct.objects.addProvider('test-namespace', testObjectProvider); + testObjectProvider.observe.and.returnValue(() => {}); + testObjectProvider.create.and.returnValue(Promise.resolve(true)); + testObjectProvider.update.and.returnValue(Promise.resolve(true)); + + spyOn(openmct.telemetry, 'getMetadata').and.returnValue({ + valuesForHints: () => { + return [ + { + source: 'sin' } - - const debouncedWatchUpdate = debounce(WatchUpdateValue, 200); - Vue.nextTick(debouncedWatchUpdate); - }); - }); - - describe('Gauge with Vertical Meter Inverted', () => { - let gaugeViewProvider; - let gaugeView; - let gaugeViewObject; - let mutablegaugeObject; - - beforeEach(() => { - gaugeViewObject = { - ...gaugeDomainObject, - configuration: { - gaugeController: { - gaugeType: 'meter-vertical', - isDisplayMinMax: true, - isDisplayCurVal: true, - isDisplayUnits: true, - isUseTelemetryLimits: false, - limitLow: -0.9, - limitHigh: 0.9, - max: 1, - min: -1, - precision: 2 - } - }, - id: 'test-object', - name: 'gauge' - }; - - const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [ - 'get', - 'create', - 'update', - 'observe' - ]); - - openmct.editor = {}; - openmct.editor.isEditing = () => false; - - const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]); - gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge'); - - testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject)); - testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject)); - openmct.objects.addProvider('test-namespace', testObjectProvider); - testObjectProvider.observe.and.returnValue(() => {}); - testObjectProvider.create.and.returnValue(Promise.resolve(true)); - testObjectProvider.update.and.returnValue(Promise.resolve(true)); - - return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { - mutablegaugeObject = mutableObject; - - gaugeView = gaugeViewProvider.view(mutablegaugeObject); - gaugeView.show(child); - - return Vue.nextTick(); - }); - }); - - afterEach(() => { - gaugeView.destroy(); - - return resetApplicationState(openmct); - }); - - it('provides gauge view', () => { - expect(gaugeViewProvider).toBeDefined(); - }); - - it('renders gauge element', () => { - const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper'); - expect(gaugeElement.length).toBe(1); - }); - - it('renders major elements', () => { - const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper'); - const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range'); - const valueElement = gaugeHolder.querySelector('.js-gauge-current-value'); - - const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement); - - expect(hasMajorElements).toBe(true); - }); - }); - - describe('Gauge with Horizontal Meter', () => { - let gaugeViewProvider; - let gaugeView; - let gaugeViewObject; - let mutablegaugeObject; - - beforeEach(() => { - gaugeViewObject = { - ...gaugeDomainObject, - configuration: { - gaugeController: { - gaugeType: 'meter-vertical', - isDisplayMinMax: true, - isDisplayCurVal: true, - isDisplayUnits: true, - isUseTelemetryLimits: false, - limitLow: -0.9, - limitHigh: 0.9, - max: 1, - min: -1, - precision: 2 - } - }, - id: 'test-object', - name: 'gauge' - }; - - const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [ - 'get', - 'create', - 'update', - 'observe' - ]); - - openmct.editor = {}; - openmct.editor.isEditing = () => false; - - const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]); - gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge'); - - testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject)); - testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject)); - openmct.objects.addProvider('test-namespace', testObjectProvider); - testObjectProvider.observe.and.returnValue(() => {}); - testObjectProvider.create.and.returnValue(Promise.resolve(true)); - testObjectProvider.update.and.returnValue(Promise.resolve(true)); - - return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { - mutablegaugeObject = mutableObject; - - gaugeView = gaugeViewProvider.view(mutablegaugeObject); - gaugeView.show(child); - - return Vue.nextTick(); - }); - }); - - afterEach(() => { - gaugeView.destroy(); - - return resetApplicationState(openmct); - }); - - it('provides gauge view', () => { - expect(gaugeViewProvider).toBeDefined(); - }); - - it('renders gauge element', () => { - const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper'); - expect(gaugeElement.length).toBe(1); - }); - - it('renders major elements', () => { - const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper'); - const rangeElement = gaugeHolder.querySelector('.c-gauge__range'); - const curveElement = gaugeHolder.querySelector('.c-meter'); - - const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement); - - expect(hasMajorElements).toBe(true); - }); - }); - - describe('Gauge with Filled Dial with Use Telemetry Limits', () => { - let gaugeViewProvider; - let gaugeView; - let gaugeViewObject; - let mutablegaugeObject; - let randomValue; - - beforeEach(() => { - randomValue = Math.random(); - - gaugeViewObject = { - ...gaugeDomainObject, - configuration: { - gaugeController: { - gaugeType: 'dial-filled', - isDisplayMinMax: true, - isDisplayCurVal: true, - isDisplayUnits: true, - isUseTelemetryLimits: true, - limitLow: 10, - limitHigh: 90, - max: 100, - min: 0, - precision: 2 - } - }, - composition: [ - { - namespace: 'test-namespace', - key: 'test-object' - } - ], - id: 'test-object', - name: 'gauge' - }; - - const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [ - 'get', - 'create', - 'update', - 'observe' - ]); - - openmct.editor = {}; - openmct.editor.isEditing = () => false; - - const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]); - gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge'); - - testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject)); - testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject)); - openmct.objects.addProvider('test-namespace', testObjectProvider); - testObjectProvider.observe.and.returnValue(() => {}); - testObjectProvider.create.and.returnValue(Promise.resolve(true)); - testObjectProvider.update.and.returnValue(Promise.resolve(true)); - - spyOn(openmct.telemetry, 'getMetadata').and.returnValue({ - valuesForHints: () => { - return [ - { - source: 'sin' - } - ]; - }, - value: () => 1 - }); - spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({ - parse: () => { - return 2000; - } - }); - spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({ - sin: { - format: (datum) => { - return randomValue; - } - } - }); - spyOn(openmct.telemetry, 'getLimits').and.returnValue( - { - limits: () => Promise.resolve({ - CRITICAL: { - high: 0.99, - low: -0.99 - } - }) - } - ); - spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue])); - spyOn(openmct.time, 'bounds').and.returnValue({ - start: 1000, - end: 5000 - }); - - return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { - mutablegaugeObject = mutableObject; - gaugeView = gaugeViewProvider.view(mutablegaugeObject); - gaugeView.show(child); - - return Vue.nextTick(); - }); - }); - - afterEach(() => { - gaugeView.destroy(); - - return resetApplicationState(openmct); - }); - - it('provides gauge view', () => { - expect(gaugeViewProvider).toBeDefined(); - }); - - it('renders gauge element', () => { - const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper'); - expect(gaugeElement.length).toBe(1); - }); - - it('renders major elements', () => { - const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper'); - const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range'); - const valueElement = gaugeHolder.querySelector('.js-dial-current-value'); - - const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement); - - expect(hasMajorElements).toBe(true); - }); - - it('renders correct min max values', () => { - const { min, max } = gaugeViewObject.configuration.gaugeController; - expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toMatch(new RegExp(`\\s*${min}\\s*${max}\\s*`)); - }); - - it('renders correct current value', (done) => { - function WatchUpdateValue() { - const textElement = gaugeHolder.querySelector('.js-dial-current-value'); - expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision)); - done(); + ]; + }, + value: () => 1 + }); + spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({ + parse: () => { + return 2000; + } + }); + spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({ + sin: { + format: (datum) => { + return randomValue; + } + } + }); + spyOn(openmct.telemetry, 'getLimits').and.returnValue({ + limits: () => + Promise.resolve({ + CRITICAL: { + high: 0.99, + low: -0.99 } + }) + }); + spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue])); + spyOn(openmct.time, 'bounds').and.returnValue({ + start: 1000, + end: 5000 + }); - const debouncedWatchUpdate = debounce(WatchUpdateValue, 200); - Vue.nextTick(debouncedWatchUpdate); - }); + return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { + mutablegaugeObject = mutableObject; + gaugeView = gaugeViewProvider.view(mutablegaugeObject); + gaugeView.show(child); + + return Vue.nextTick(); + }); }); + + afterEach(() => { + gaugeView.destroy(); + + return resetApplicationState(openmct); + }); + + it('provides gauge view', () => { + expect(gaugeViewProvider).toBeDefined(); + }); + + it('renders gauge element', () => { + const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper'); + expect(gaugeElement.length).toBe(1); + }); + + it('renders major elements', () => { + const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper'); + const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range'); + const valueElement = gaugeHolder.querySelector('.js-dial-current-value'); + + const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement); + + expect(hasMajorElements).toBe(true); + }); + + it('renders correct min max values', () => { + const { min, max } = gaugeViewObject.configuration.gaugeController; + expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toMatch( + new RegExp(`\\s*${min}\\s*${max}\\s*`) + ); + }); + + it('renders correct current value', (done) => { + function WatchUpdateValue() { + const textElement = gaugeHolder.querySelector('.js-dial-current-value'); + expect( + Number(textElement.textContent).toFixed( + gaugeViewObject.configuration.gaugeController.precision + ) + ).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision)); + done(); + } + + const debouncedWatchUpdate = debounce(WatchUpdateValue, 200); + Vue.nextTick(debouncedWatchUpdate); + }); + }); }); diff --git a/src/plugins/gauge/GaugeViewProvider.js b/src/plugins/gauge/GaugeViewProvider.js index e8218999d9..ef651790a7 100644 --- a/src/plugins/gauge/GaugeViewProvider.js +++ b/src/plugins/gauge/GaugeViewProvider.js @@ -24,44 +24,44 @@ import GaugeComponent from './components/Gauge.vue'; import Vue from 'vue'; export default function GaugeViewProvider(openmct) { - return { - key: 'gauge', - name: 'Gauge', - cssClass: 'icon-gauge', - canView: function (domainObject) { - return domainObject.type === 'gauge'; - }, - canEdit: function (domainObject) { - if (domainObject.type === 'gauge') { - return true; - } - }, - view: function (domainObject) { - let component; + return { + key: 'gauge', + name: 'Gauge', + cssClass: 'icon-gauge', + canView: function (domainObject) { + return domainObject.type === 'gauge'; + }, + canEdit: function (domainObject) { + if (domainObject.type === 'gauge') { + return true; + } + }, + view: function (domainObject) { + let component; - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - GaugeComponent - }, - provide: { - openmct, - domainObject, - composition: openmct.composition.get(domainObject) - }, - template: '' - }); - }, - destroy: function (element) { - component.$destroy(); - component = undefined; - } - }; + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + GaugeComponent + }, + provide: { + openmct, + domainObject, + composition: openmct.composition.get(domainObject) + }, + template: '' + }); }, - priority: function () { - return 1; + destroy: function (element) { + component.$destroy(); + component = undefined; } - }; + }; + }, + priority: function () { + return 1; + } + }; } diff --git a/src/plugins/gauge/components/Gauge.vue b/src/plugins/gauge/components/Gauge.vue index 49dc670a3a..cd2c63c62d 100644 --- a/src/plugins/gauge/components/Gauge.vue +++ b/src/plugins/gauge/components/Gauge.vue @@ -20,329 +20,306 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/gauge/components/GaugeFormController.vue b/src/plugins/gauge/components/GaugeFormController.vue index 44fb2a2899..ac2e6babf2 100644 --- a/src/plugins/gauge/components/GaugeFormController.vue +++ b/src/plugins/gauge/components/GaugeFormController.vue @@ -21,144 +21,137 @@ --> diff --git a/src/plugins/gauge/gauge-limit-util.js b/src/plugins/gauge/gauge-limit-util.js index 39ea55db78..28467c45ef 100644 --- a/src/plugins/gauge/gauge-limit-util.js +++ b/src/plugins/gauge/gauge-limit-util.js @@ -1,8 +1,8 @@ const GAUGE_LIMITS = { - q1: 0, - q2: 90, - q3: 180, - q4: 270 + q1: 0, + q2: 90, + q3: 180, + q4: 270 }; export const DIAL_VALUE_DEG_OFFSET = 45; @@ -10,30 +10,27 @@ export const DIAL_VALUE_DEG_OFFSET = 45; // type: low, high // quadrant: low, mid, high, max export function getLimitDegree(type, quadrant) { - if (quadrant === 'max') { - return GAUGE_LIMITS.q4 + DIAL_VALUE_DEG_OFFSET; - } + if (quadrant === 'max') { + return GAUGE_LIMITS.q4 + DIAL_VALUE_DEG_OFFSET; + } - return type === 'low' - ? getLowLimitDegree(quadrant) - : getHighLimitDegree(quadrant) - ; + return type === 'low' ? getLowLimitDegree(quadrant) : getHighLimitDegree(quadrant); } function getLowLimitDegree(quadrant) { - return GAUGE_LIMITS[quadrant] + DIAL_VALUE_DEG_OFFSET; + return GAUGE_LIMITS[quadrant] + DIAL_VALUE_DEG_OFFSET; } function getHighLimitDegree(quadrant) { - if (quadrant === 'q1') { - return GAUGE_LIMITS.q4 + DIAL_VALUE_DEG_OFFSET; - } + if (quadrant === 'q1') { + return GAUGE_LIMITS.q4 + DIAL_VALUE_DEG_OFFSET; + } - if (quadrant === 'q2') { - return GAUGE_LIMITS.q3 + DIAL_VALUE_DEG_OFFSET; - } + if (quadrant === 'q2') { + return GAUGE_LIMITS.q3 + DIAL_VALUE_DEG_OFFSET; + } - if (quadrant === 'q3') { - return GAUGE_LIMITS.q2 + DIAL_VALUE_DEG_OFFSET; - } + if (quadrant === 'q3') { + return GAUGE_LIMITS.q2 + DIAL_VALUE_DEG_OFFSET; + } } diff --git a/src/plugins/gauge/gauge.scss b/src/plugins/gauge/gauge.scss index eac263e46e..ad08cfd4c5 100644 --- a/src/plugins/gauge/gauge.scss +++ b/src/plugins/gauge/gauge.scss @@ -11,10 +11,16 @@ $meterNeedleBorderRadius: 5px; width: 20px; &.invalid, - &.invalid.req { @include validationState($glyph-icon-x, $colorFormInvalid); } + &.invalid.req { + @include validationState($glyph-icon-x, $colorFormInvalid); + } &.valid, - &.valid.req { @include validationState($glyph-icon-check, $colorFormValid); } - &.req { @include validationState($glyph-icon-asterisk, $colorFormRequired); } + &.valid.req { + @include validationState($glyph-icon-check, $colorFormValid); + } + &.req { + @include validationState($glyph-icon-asterisk, $colorFormRequired); + } } .c-gauge { @@ -36,7 +42,7 @@ $meterNeedleBorderRadius: 5px; &.is-stale { @include isStaleHolder(); - [class*=__current-value-text] { + [class*='__current-value-text'] { fill: $colorTelemStale; font-style: italic; } diff --git a/src/plugins/goToOriginalAction/goToOriginalAction.js b/src/plugins/goToOriginalAction/goToOriginalAction.js index d759491ccc..00500e09a2 100644 --- a/src/plugins/goToOriginalAction/goToOriginalAction.js +++ b/src/plugins/goToOriginalAction/goToOriginalAction.js @@ -21,40 +21,44 @@ *****************************************************************************/ export default class GoToOriginalAction { - constructor(openmct) { - this.name = 'Go To Original'; - this.key = 'goToOriginal'; - this.description = 'Go to the original unlinked instance of this object'; - this.group = 'action'; - this.priority = 4; + constructor(openmct) { + this.name = 'Go To Original'; + this.key = 'goToOriginal'; + this.description = 'Go to the original unlinked instance of this object'; + this.group = 'action'; + this.priority = 4; - this._openmct = openmct; + this._openmct = openmct; + } + invoke(objectPath) { + this._openmct.objects.getOriginalPath(objectPath[0].identifier).then((originalPath) => { + let url = + '#/browse/' + + originalPath + .map( + function (o) { + return o && this._openmct.objects.makeKeyString(o.identifier); + }.bind(this) + ) + .reverse() + .slice(1) + .join('/'); + + this._openmct.router.navigate(url); + }); + } + appliesTo(objectPath) { + if (this._openmct.editor.isEditing()) { + return false; } - invoke(objectPath) { - this._openmct.objects.getOriginalPath(objectPath[0].identifier) - .then((originalPath) => { - let url = '#/browse/' + originalPath - .map(function (o) { - return o && this._openmct.objects.makeKeyString(o.identifier); - }.bind(this)) - .reverse() - .slice(1) - .join('/'); - this._openmct.router.navigate(url); - }); + let parentKeystring = + objectPath[1] && this._openmct.objects.makeKeyString(objectPath[1].identifier); + + if (!parentKeystring) { + return false; } - appliesTo(objectPath) { - if (this._openmct.editor.isEditing()) { - return false; - } - let parentKeystring = objectPath[1] && this._openmct.objects.makeKeyString(objectPath[1].identifier); - - if (!parentKeystring) { - return false; - } - - return (parentKeystring !== objectPath[0].location); - } + return parentKeystring !== objectPath[0].location; + } } diff --git a/src/plugins/goToOriginalAction/plugin.js b/src/plugins/goToOriginalAction/plugin.js index e4e3cf7808..9c4465b021 100644 --- a/src/plugins/goToOriginalAction/plugin.js +++ b/src/plugins/goToOriginalAction/plugin.js @@ -22,7 +22,7 @@ import GoToOriginalAction from './goToOriginalAction'; export default function () { - return function (openmct) { - openmct.actions.register(new GoToOriginalAction(openmct)); - }; + return function (openmct) { + openmct.actions.register(new GoToOriginalAction(openmct)); + }; } diff --git a/src/plugins/goToOriginalAction/pluginSpec.js b/src/plugins/goToOriginalAction/pluginSpec.js index 1155280f2d..62472d36a6 100644 --- a/src/plugins/goToOriginalAction/pluginSpec.js +++ b/src/plugins/goToOriginalAction/pluginSpec.js @@ -19,182 +19,174 @@ * 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 { createOpenMct, resetApplicationState } from 'utils/testing'; -describe("the goToOriginalAction plugin", () => { - let openmct; - let goToOriginalAction; - let mockRootFolder; - let mockSubFolder; - let mockSubSubFolder; - let mockObject; - let mockObjectPath; - let hash; +describe('the goToOriginalAction plugin', () => { + let openmct; + let goToOriginalAction; + let mockRootFolder; + let mockSubFolder; + let mockSubSubFolder; + let mockObject; + let mockObjectPath; + let hash; - beforeEach((done) => { - openmct = createOpenMct(); + beforeEach((done) => { + openmct = createOpenMct(); - openmct.on('start', done); - openmct.startHeadless(); + openmct.on('start', done); + openmct.startHeadless(); - goToOriginalAction = openmct.actions._allActions.goToOriginal; + goToOriginalAction = openmct.actions._allActions.goToOriginal; + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('installs the go to folder action', () => { + expect(goToOriginalAction).toBeDefined(); + }); + + describe('when invoked', () => { + beforeEach(() => { + mockRootFolder = getMockObject('mock-root'); + mockSubFolder = getMockObject('mock-sub'); + mockSubSubFolder = getMockObject('mock-sub-sub'); + mockObject = getMockObject('mock-table'); + + mockObjectPath = [mockObject, mockSubSubFolder, mockSubFolder, mockRootFolder]; + + spyOn(openmct.objects, 'get').and.callFake((identifier) => { + const mockedObject = getMockObject(identifier); + + return Promise.resolve(mockedObject); + }); + + spyOn(openmct.router, 'navigate').and.callFake((navigateTo) => { + hash = navigateTo; + }); + + return goToOriginalAction.invoke(mockObjectPath); }); - afterEach(() => { - return resetApplicationState(openmct); + it('goes to the original location', () => { + const originalLocationHash = '#/browse/mock-root/mock-table'; + + return waitForNavigation(() => { + return hash === originalLocationHash; + }).then(() => { + expect(hash).toEqual(originalLocationHash); + }); }); + }); - it('installs the go to folder action', () => { - expect(goToOriginalAction).toBeDefined(); + function waitForNavigation(navigated) { + return new Promise((resolve, reject) => { + const start = Date.now(); + + checkNavigated(); + + function checkNavigated() { + const elapsed = Date.now() - start; + + if (navigated()) { + resolve(); + } else if (elapsed >= jasmine.DEFAULT_TIMEOUT_INTERVAL - 1000) { + reject("didn't navigate in time"); + } else { + setTimeout(checkNavigated); + } + } }); + } - describe('when invoked', () => { - beforeEach(() => { - mockRootFolder = getMockObject('mock-root'); - mockSubFolder = getMockObject('mock-sub'); - mockSubSubFolder = getMockObject('mock-sub-sub'); - mockObject = getMockObject('mock-table'); + function getMockObject(key) { + const id = typeof key === 'string' ? key : key.key; - mockObjectPath = [ - mockObject, - mockSubSubFolder, - mockSubFolder, - mockRootFolder - ]; + const mockMCTObjects = { + ROOT: { + composition: [ + { + namespace: '', + key: 'mock-root' + } + ], + identifier: { + namespace: '', + key: 'mock-root' + } + }, + 'mock-root': { + composition: [ + { + namespace: '', + key: 'mock-sub' + }, + { + namespace: '', + key: 'mock-table' + } + ], + name: 'root', + type: 'folder', + id: 'mock-root', + location: 'ROOT', + identifier: { + namespace: '', + key: 'mock-root' + } + }, + 'mock-sub': { + composition: [ + { + namespace: '', + key: 'mock-sub-sub' + }, + { + namespace: '', + key: 'mock-table' + } + ], + name: 'sub', + type: 'folder', + location: 'mock-root', + identifier: { + namespace: '', + key: 'mock-sub' + } + }, + 'mock-table': { + composition: [], + configuration: { + columnWidths: {}, + hiddenColumns: {} + }, + name: 'table', + type: 'table', + location: 'mock-root', + identifier: { + namespace: '', + key: 'mock-table' + } + }, + 'mock-sub-sub': { + composition: [ + { + namespace: '', + key: 'mock-table' + } + ], + name: 'sub sub', + type: 'folder', + location: 'mock-sub', + identifier: { + namespace: '', + key: 'mock-sub-sub' + } + } + }; - spyOn(openmct.objects, 'get').and.callFake(identifier => { - const mockedObject = getMockObject(identifier); - - return Promise.resolve(mockedObject); - }); - - spyOn(openmct.router, 'navigate').and.callFake(navigateTo => { - hash = navigateTo; - }); - - return goToOriginalAction.invoke(mockObjectPath); - }); - - it('goes to the original location', () => { - const originalLocationHash = '#/browse/mock-root/mock-table'; - - return waitForNavigation(() => { - return hash === originalLocationHash; - }).then(() => { - expect(hash).toEqual(originalLocationHash); - }); - }); - }); - - function waitForNavigation(navigated) { - return new Promise((resolve, reject) => { - const start = Date.now(); - - checkNavigated(); - - function checkNavigated() { - const elapsed = Date.now() - start; - - if (navigated()) { - resolve(); - } else if (elapsed >= jasmine.DEFAULT_TIMEOUT_INTERVAL - 1000) { - reject("didn't navigate in time"); - } else { - setTimeout(checkNavigated); - } - } - }); - } - - function getMockObject(key) { - const id = typeof key === 'string' ? key : key.key; - - const mockMCTObjects = { - "ROOT": { - "composition": [ - { - "namespace": "", - "key": "mock-root" - } - ], - "identifier": { - "namespace": "", - "key": "mock-root" - } - }, - "mock-root": { - "composition": [ - { - "namespace": "", - "key": "mock-sub" - }, - { - "namespace": "", - "key": "mock-table" - } - ], - "name": "root", - "type": "folder", - "id": "mock-root", - "location": "ROOT", - "identifier": { - "namespace": "", - "key": "mock-root" - } - }, - "mock-sub": { - "composition": [ - { - "namespace": "", - "key": "mock-sub-sub" - }, - { - "namespace": "", - "key": "mock-table" - } - ], - "name": "sub", - "type": "folder", - "location": "mock-root", - "identifier": { - "namespace": "", - "key": "mock-sub" - } - }, - "mock-table": { - "composition": [], - "configuration": { - "columnWidths": {}, - "hiddenColumns": {} - }, - "name": "table", - "type": "table", - "location": "mock-root", - "identifier": { - "namespace": "", - "key": "mock-table" - } - }, - "mock-sub-sub": { - "composition": [ - { - "namespace": "", - "key": "mock-table" - } - ], - "name": "sub sub", - "type": "folder", - "location": "mock-sub", - "identifier": { - "namespace": "", - "key": "mock-sub-sub" - } - } - }; - - return mockMCTObjects[id]; - } + return mockMCTObjects[id]; + } }); diff --git a/src/plugins/hyperlink/HyperlinkLayout.vue b/src/plugins/hyperlink/HyperlinkLayout.vue index fa5f58c454..e9dbff4d23 100644 --- a/src/plugins/hyperlink/HyperlinkLayout.vue +++ b/src/plugins/hyperlink/HyperlinkLayout.vue @@ -21,36 +21,34 @@ --> diff --git a/src/plugins/hyperlink/HyperlinkProvider.js b/src/plugins/hyperlink/HyperlinkProvider.js index 53970bf4a3..ffbdd1dd11 100644 --- a/src/plugins/hyperlink/HyperlinkProvider.js +++ b/src/plugins/hyperlink/HyperlinkProvider.js @@ -24,36 +24,35 @@ import HyperlinkLayout from './HyperlinkLayout.vue'; import Vue from 'vue'; export default function HyperlinkProvider(openmct) { + return { + key: 'hyperlink.view', + name: 'Hyperlink', + cssClass: 'icon-chain-links', + canView(domainObject) { + return domainObject.type === 'hyperlink'; + }, - return { - key: 'hyperlink.view', - name: 'Hyperlink', - cssClass: 'icon-chain-links', - canView(domainObject) { - return domainObject.type === 'hyperlink'; + view: function (domainObject) { + let component; + + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + HyperlinkLayout + }, + provide: { + domainObject + }, + template: '' + }); }, - - view: function (domainObject) { - let component; - - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - HyperlinkLayout - }, - provide: { - domainObject - }, - template: '' - }); - }, - destroy: function () { - component.$destroy(); - component = undefined; - } - }; + destroy: function () { + component.$destroy(); + component = undefined; } - }; + }; + } + }; } diff --git a/src/plugins/hyperlink/plugin.js b/src/plugins/hyperlink/plugin.js index 3fcfed035a..42a903e795 100644 --- a/src/plugins/hyperlink/plugin.js +++ b/src/plugins/hyperlink/plugin.js @@ -23,67 +23,66 @@ import HyperlinkProvider from './HyperlinkProvider'; export default function () { - return function install(openmct) { - openmct.types.addType('hyperlink', { - name: 'Hyperlink', - key: 'hyperlink', - description: 'A text element or button that links to any URL including Open MCT views.', - creatable: true, - cssClass: 'icon-chain-links', - initialize: function (domainObject) { - domainObject.displayFormat = "link"; - domainObject.linkTarget = "_self"; + return function install(openmct) { + openmct.types.addType('hyperlink', { + name: 'Hyperlink', + key: 'hyperlink', + description: 'A text element or button that links to any URL including Open MCT views.', + creatable: true, + cssClass: 'icon-chain-links', + initialize: function (domainObject) { + domainObject.displayFormat = 'link'; + domainObject.linkTarget = '_self'; + }, + form: [ + { + key: 'url', + name: 'URL', + control: 'textfield', + required: true, + cssClass: 'l-input-lg' + }, + { + key: 'displayText', + name: 'Text to Display', + control: 'textfield', + required: true, + cssClass: 'l-input-lg' + }, + { + key: 'displayFormat', + name: 'Display Format', + control: 'select', + options: [ + { + name: 'Link', + value: 'link' }, - form: [ - { - "key": "url", - "name": "URL", - "control": "textfield", - "required": true, - "cssClass": "l-input-lg" - }, - { - "key": "displayText", - "name": "Text to Display", - "control": "textfield", - "required": true, - "cssClass": "l-input-lg" - }, - { - "key": "displayFormat", - "name": "Display Format", - "control": "select", - "options": [ - { - "name": "Link", - "value": "link" - }, - { - "name": "Button", - "value": "button" - } - ], - "cssClass": "l-inline" - }, - { - "key": "linkTarget", - "name": "Tab to Open Hyperlink", - "control": "select", - "options": [ - { - "name": "Open in this tab", - "value": "_self" - }, - { - "name": "Open in a new tab", - "value": "_blank" - } - ], - "cssClass": "l-inline" - - } - ] - }); - openmct.objectViews.addProvider(new HyperlinkProvider(openmct)); - }; + { + name: 'Button', + value: 'button' + } + ], + cssClass: 'l-inline' + }, + { + key: 'linkTarget', + name: 'Tab to Open Hyperlink', + control: 'select', + options: [ + { + name: 'Open in this tab', + value: '_self' + }, + { + name: 'Open in a new tab', + value: '_blank' + } + ], + cssClass: 'l-inline' + } + ] + }); + openmct.objectViews.addProvider(new HyperlinkProvider(openmct)); + }; } diff --git a/src/plugins/hyperlink/pluginSpec.js b/src/plugins/hyperlink/pluginSpec.js index adc3c1f473..4e1b26f076 100644 --- a/src/plugins/hyperlink/pluginSpec.js +++ b/src/plugins/hyperlink/pluginSpec.js @@ -20,111 +20,112 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { createOpenMct, resetApplicationState } from "utils/testing"; -import HyperlinkPlugin from "./plugin"; +import { createOpenMct, resetApplicationState } from 'utils/testing'; +import HyperlinkPlugin from './plugin'; function getView(openmct, domainObj, objectPath) { - const applicableViews = openmct.objectViews.get(domainObj, objectPath); - const hyperLinkView = applicableViews.find((viewProvider) => viewProvider.key === 'hyperlink.view'); + const applicableViews = openmct.objectViews.get(domainObj, objectPath); + const hyperLinkView = applicableViews.find( + (viewProvider) => viewProvider.key === 'hyperlink.view' + ); - return hyperLinkView.view(domainObj); + return hyperLinkView.view(domainObj); } function destroyView(view) { - return view.destroy(); + return view.destroy(); } -describe("The controller for hyperlinks", function () { - let mockDomainObject; - let mockObjectPath; - let openmct; - let element; - let child; - let view; +describe('The controller for hyperlinks', function () { + let mockDomainObject; + let mockObjectPath; + let openmct; + let element; + let child; + let view; - beforeEach((done) => { - mockObjectPath = [ - { - name: 'mock hyperlink', - type: 'hyperlink', - identifier: { - key: 'mock-hyperlink', - namespace: '' - } - } - ]; + beforeEach((done) => { + mockObjectPath = [ + { + name: 'mock hyperlink', + type: 'hyperlink', + identifier: { + key: 'mock-hyperlink', + namespace: '' + } + } + ]; - mockDomainObject = { - displayFormat: "", - linkTarget: "", - name: "Unnamed HyperLink", - type: "hyperlink", - 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" - } - }; + mockDomainObject = { + displayFormat: '', + linkTarget: '', + name: 'Unnamed HyperLink', + type: 'hyperlink', + 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 HyperlinkPlugin()); + openmct = createOpenMct(); + openmct.install(new HyperlinkPlugin()); - 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); + 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(); + openmct.on('start', done); + openmct.startHeadless(); + }); - }); + afterEach(() => { + destroyView(view); - afterEach(() => { - destroyView(view); + return resetApplicationState(openmct); + }); + it('knows when it should open a new tab', () => { + mockDomainObject.displayFormat = 'link'; + mockDomainObject.linkTarget = '_blank'; - return resetApplicationState(openmct); - }); - it("knows when it should open a new tab", () => { - mockDomainObject.displayFormat = "link"; - mockDomainObject.linkTarget = "_blank"; + view = getView(openmct, mockDomainObject, mockObjectPath); + view.show(child, true); - view = getView(openmct, mockDomainObject, mockObjectPath); - view.show(child, true); + expect(element.querySelector('.c-hyperlink').target).toBe('_blank'); + }); + it('knows when it should open in the same tab', function () { + mockDomainObject.displayFormat = 'button'; + mockDomainObject.linkTarget = '_self'; - expect(element.querySelector('.c-hyperlink').target).toBe('_blank'); - }); - it("knows when it should open in the same tab", function () { - mockDomainObject.displayFormat = "button"; - mockDomainObject.linkTarget = "_self"; + view = getView(openmct, mockDomainObject, mockObjectPath); + view.show(child, true); - view = getView(openmct, mockDomainObject, mockObjectPath); - view.show(child, true); + expect(element.querySelector('.c-hyperlink').target).toBe('_self'); + }); - expect(element.querySelector('.c-hyperlink').target).toBe('_self'); - }); + it('knows when it is a button', function () { + mockDomainObject.displayFormat = 'button'; - it("knows when it is a button", function () { - mockDomainObject.displayFormat = "button"; + view = getView(openmct, mockDomainObject, mockObjectPath); + view.show(child, true); - view = getView(openmct, mockDomainObject, mockObjectPath); - view.show(child, true); + expect(element.querySelector('.c-hyperlink--button')).toBeDefined(); + }); + it('knows when it is a link', function () { + mockDomainObject.displayFormat = 'link'; - expect(element.querySelector('.c-hyperlink--button')).toBeDefined(); - }); - it("knows when it is a link", function () { - mockDomainObject.displayFormat = "link"; + view = getView(openmct, mockDomainObject, mockObjectPath); + view.show(child, true); - view = getView(openmct, mockDomainObject, mockObjectPath); - view.show(child, true); - - expect(element.querySelector('.c-hyperlink')).not.toHaveClass('c-hyperlink--button'); - }); + expect(element.querySelector('.c-hyperlink')).not.toHaveClass('c-hyperlink--button'); + }); }); diff --git a/src/plugins/imagery/ImageryTimestripViewProvider.js b/src/plugins/imagery/ImageryTimestripViewProvider.js index 384d237c80..432c86fb81 100644 --- a/src/plugins/imagery/ImageryTimestripViewProvider.js +++ b/src/plugins/imagery/ImageryTimestripViewProvider.js @@ -20,58 +20,61 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ import ImageryTimeView from './components/ImageryTimeView.vue'; -import Vue from "vue"; +import Vue from 'vue'; export default function ImageryTimestripViewProvider(openmct) { - const type = 'example.imagery.time-strip.view'; + const type = 'example.imagery.time-strip.view'; - function hasImageTelemetry(domainObject) { - const metadata = openmct.telemetry.getMetadata(domainObject); - if (!metadata) { - return false; - } - - return metadata.valuesForHints(['image']).length > 0; + function hasImageTelemetry(domainObject) { + const metadata = openmct.telemetry.getMetadata(domainObject); + if (!metadata) { + return false; } - return { - key: type, - name: 'Imagery Timestrip View', - cssClass: 'icon-image', - canView: function (domainObject, objectPath) { - let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip'); + return metadata.valuesForHints(['image']).length > 0; + } - return hasImageTelemetry(domainObject) && isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); + return { + key: type, + name: 'Imagery Timestrip View', + cssClass: 'icon-image', + canView: function (domainObject, objectPath) { + let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip'); + + return ( + hasImageTelemetry(domainObject) && + isChildOfTimeStrip && + !openmct.router.isNavigatedObject(objectPath) + ); + }, + view: function (domainObject, objectPath) { + let component; + + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + ImageryTimeView + }, + provide: { + openmct: openmct, + domainObject: domainObject, + objectPath: objectPath + }, + template: '' + }); }, - view: function (domainObject, objectPath) { - let component; - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - ImageryTimeView - }, - provide: { - openmct: openmct, - domainObject: domainObject, - objectPath: objectPath - }, - template: '' + destroy: function () { + component.$destroy(); + component = undefined; + }, - }); - }, - - destroy: function () { - component.$destroy(); - component = undefined; - }, - - getComponent() { - return component; - } - }; + getComponent() { + return component; } - }; + }; + } + }; } diff --git a/src/plugins/imagery/ImageryView.js b/src/plugins/imagery/ImageryView.js index 801ccb5950..66d0ead160 100644 --- a/src/plugins/imagery/ImageryView.js +++ b/src/plugins/imagery/ImageryView.js @@ -3,83 +3,83 @@ import ImageryViewComponent from './components/ImageryView.vue'; import Vue from 'vue'; const DEFAULT_IMAGE_FRESHNESS_OPTIONS = { - fadeOutDelayTime: '0s', - fadeOutDurationTime: '30s' + fadeOutDelayTime: '0s', + fadeOutDurationTime: '30s' }; export default class ImageryView { - constructor(openmct, domainObject, objectPath, options) { - this.openmct = openmct; - this.domainObject = domainObject; - this.objectPath = objectPath; - this.options = options; - this.component = undefined; + constructor(openmct, domainObject, objectPath, options) { + this.openmct = openmct; + this.domainObject = domainObject; + this.objectPath = objectPath; + this.options = options; + this.component = undefined; + } + + show(element, isEditing, viewOptions) { + let alternateObjectPath; + let focusedImageTimestamp; + if (viewOptions) { + focusedImageTimestamp = viewOptions.timestamp; + alternateObjectPath = viewOptions.objectPath; } - show(element, isEditing, viewOptions) { - let alternateObjectPath; - let focusedImageTimestamp; - if (viewOptions) { - focusedImageTimestamp = viewOptions.timestamp; - alternateObjectPath = viewOptions.objectPath; - } + this.component = new Vue({ + el: element, + components: { + 'imagery-view': ImageryViewComponent + }, + provide: { + openmct: this.openmct, + domainObject: this.domainObject, + objectPath: alternateObjectPath || this.objectPath, + imageFreshnessOptions: this.options?.imageFreshness || DEFAULT_IMAGE_FRESHNESS_OPTIONS, + currentView: this + }, + data() { + return { + focusedImageTimestamp + }; + }, + template: + '' + }); + } - this.component = new Vue({ - el: element, - components: { - 'imagery-view': ImageryViewComponent - }, - provide: { - openmct: this.openmct, - domainObject: this.domainObject, - objectPath: alternateObjectPath || this.objectPath, - imageFreshnessOptions: this.options?.imageFreshness || DEFAULT_IMAGE_FRESHNESS_OPTIONS, - currentView: this - }, - data() { - return { - focusedImageTimestamp - }; - }, - template: '' - - }); + getViewContext() { + if (!this.component) { + return {}; } - getViewContext() { - if (!this.component) { - return {}; - } + return this.component.$refs.ImageryContainer; + } - return this.component.$refs.ImageryContainer; - } + pause() { + const imageContext = this.getViewContext(); + // persist previous pause value to return to after unpausing + this.previouslyPaused = imageContext.isPaused; + imageContext.thumbnailClicked(imageContext.focusedImageIndex); + } + unpause() { + const pausedStateBefore = this.previouslyPaused; + this.previouslyPaused = undefined; // clear value + const imageContext = this.getViewContext(); + imageContext.paused(pausedStateBefore); + } - pause() { - const imageContext = this.getViewContext(); - // persist previous pause value to return to after unpausing - this.previouslyPaused = imageContext.isPaused; - imageContext.thumbnailClicked(imageContext.focusedImageIndex); - } - unpause() { - const pausedStateBefore = this.previouslyPaused; - this.previouslyPaused = undefined; // clear value - const imageContext = this.getViewContext(); - imageContext.paused(pausedStateBefore); + onPreviewModeChange({ isPreviewing } = {}) { + if (isPreviewing) { + this.pause(); + } else { + this.unpause(); } + } - onPreviewModeChange({ isPreviewing } = {}) { - if (isPreviewing) { - this.pause(); - } else { - this.unpause(); - } - } + destroy() { + this.component.$destroy(); + this.component = undefined; + } - destroy() { - this.component.$destroy(); - this.component = undefined; - } - - _getInstance() { - return this.component; - } + _getInstance() { + return this.component; + } } diff --git a/src/plugins/imagery/ImageryViewProvider.js b/src/plugins/imagery/ImageryViewProvider.js index 306971113d..9fc11ae977 100644 --- a/src/plugins/imagery/ImageryViewProvider.js +++ b/src/plugins/imagery/ImageryViewProvider.js @@ -22,28 +22,31 @@ import ImageryView from './ImageryView'; export default function ImageryViewProvider(openmct, options) { - const type = 'example.imagery'; + const type = 'example.imagery'; - function hasImageTelemetry(domainObject) { - const metadata = openmct.telemetry.getMetadata(domainObject); - if (!metadata) { - return false; - } - - return metadata.valuesForHints(['image']).length > 0; + function hasImageTelemetry(domainObject) { + const metadata = openmct.telemetry.getMetadata(domainObject); + if (!metadata) { + return false; } - return { - key: type, - name: 'Imagery Layout', - cssClass: 'icon-image', - canView: function (domainObject, objectPath) { - let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip'); + return metadata.valuesForHints(['image']).length > 0; + } - return hasImageTelemetry(domainObject) && (!isChildOfTimeStrip || openmct.router.isNavigatedObject(objectPath)); - }, - view: function (domainObject, objectPath) { - return new ImageryView(openmct, domainObject, objectPath, options); - } - }; + return { + key: type, + name: 'Imagery Layout', + cssClass: 'icon-image', + canView: function (domainObject, objectPath) { + let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip'); + + return ( + hasImageTelemetry(domainObject) && + (!isChildOfTimeStrip || openmct.router.isNavigatedObject(objectPath)) + ); + }, + view: function (domainObject, objectPath) { + return new ImageryView(openmct, domainObject, objectPath, options); + } + }; } diff --git a/src/plugins/imagery/components/Compass/Compass.vue b/src/plugins/imagery/components/Compass/Compass.vue index 647d4ebeac..9be17a629b 100644 --- a/src/plugins/imagery/components/Compass/Compass.vue +++ b/src/plugins/imagery/components/Compass/Compass.vue @@ -21,30 +21,27 @@ --> diff --git a/src/plugins/imagery/components/Compass/CompassHUD.vue b/src/plugins/imagery/components/Compass/CompassHUD.vue index 173683e01c..66321f171e 100644 --- a/src/plugins/imagery/components/Compass/CompassHUD.vue +++ b/src/plugins/imagery/components/Compass/CompassHUD.vue @@ -21,140 +21,127 @@ --> diff --git a/src/plugins/imagery/components/Compass/CompassRose.vue b/src/plugins/imagery/components/Compass/CompassRose.vue index e2b3dc4e9a..d045bd4050 100644 --- a/src/plugins/imagery/components/Compass/CompassRose.vue +++ b/src/plugins/imagery/components/Compass/CompassRose.vue @@ -21,236 +21,165 @@ --> diff --git a/src/plugins/imagery/components/Compass/compass.scss b/src/plugins/imagery/components/Compass/compass.scss index a143706b2b..357e88754c 100644 --- a/src/plugins/imagery/components/Compass/compass.scss +++ b/src/plugins/imagery/components/Compass/compass.scss @@ -3,183 +3,192 @@ $interfaceKeyColor: #fff; $elemBg: rgba(black, 0.7); @mixin sun($position: 'circle closest-side') { - $color: #ff9900; - $gradEdgePerc: 60%; - background: radial-gradient(#{$position}, $color, $color $gradEdgePerc, rgba($color, 0.4) $gradEdgePerc + 5%, transparent); - + $color: #ff9900; + $gradEdgePerc: 60%; + background: radial-gradient( + #{$position}, + $color, + $color $gradEdgePerc, + rgba($color, 0.4) $gradEdgePerc + 5%, + transparent + ); } .c-compass { - pointer-events: none; // This allows the image element to receive a browser-level context click - position: absolute; - left: 0; - top: 0; - z-index: 2; - @include userSelectNone; + pointer-events: none; // This allows the image element to receive a browser-level context click + position: absolute; + left: 0; + top: 0; + z-index: 2; + @include userSelectNone; } /***************************** COMPASS HUD */ .c-hud { - // To be placed within a imagery view, in the bounding box of the image - $m: 1px; - $padTB: 2px; - $padLR: $padTB; - color: $interfaceKeyColor; - font-size: 0.8em; + // To be placed within a imagery view, in the bounding box of the image + $m: 1px; + $padTB: 2px; + $padLR: $padTB; + color: $interfaceKeyColor; + font-size: 0.8em; + position: absolute; + top: $m; + right: $m; + left: $m; + height: 18px; + + svg, + div { position: absolute; - top: $m; - right: $m; - left: $m; - height: 18px; + } - svg, div { - position: absolute; - } + &__display { + height: 30px; + pointer-events: all; + position: absolute; + top: 0; + right: 0; + left: 0; + } - &__display { - height: 30px; - pointer-events: all; - position: absolute; - top: 0; - right: 0; - left: 0; - } + &__range { + border: 1px solid $interfaceKeyColor; + border-top-color: transparent; + position: absolute; + top: 50%; + right: $padLR; + bottom: $padTB; + left: $padLR; + } - &__range { - border: 1px solid $interfaceKeyColor; - border-top-color: transparent; - position: absolute; - top: 50%; - right: $padLR; - bottom: $padTB; - left: $padLR; - } + [class*='__dir'] { + // NSEW + display: inline-block; + font-weight: bold; + text-shadow: 0 1px 2px black; + top: 50%; + transform: translate(-50%, -50%); + z-index: 2; + } - [class*="__dir"] { - // NSEW - display: inline-block; - font-weight: bold; - text-shadow: 0 1px 2px black; - top: 50%; - transform: translate(-50%, -50%); - z-index: 2; - } + [class*='__dir--sub'] { + font-weight: normal; + opacity: 0.5; + } - [class*="__dir--sub"] { - font-weight: normal; - opacity: 0.5; - } - - &__sun { - $s: 10px; - @include sun('circle farthest-side at bottom'); - bottom: $padTB + 2px; - height: $s; - width: $s*2; - opacity: 0.8; - transform: translateX(-50%); - z-index: 1; - } + &__sun { + $s: 10px; + @include sun('circle farthest-side at bottom'); + bottom: $padTB + 2px; + height: $s; + width: $s * 2; + opacity: 0.8; + transform: translateX(-50%); + z-index: 1; + } } /***************************** COMPASS SVG */ .c-compass-rose-svg { - $color: $interfaceKeyColor; - position: absolute; - top: 0; left: 0; + $color: $interfaceKeyColor; + position: absolute; + top: 0; + left: 0; - g, path, rect { - // In an SVG, rotation occurs about the center of the SVG, not the element - transform-origin: center; + g, + path, + rect { + // In an SVG, rotation occurs about the center of the SVG, not the element + transform-origin: center; + } + + .c-cr { + &__bg { + fill: #000; + opacity: 0.8; } - .c-cr { - &__bg { - fill: #000; - opacity: 0.8; - } - - &__edge { - opacity: 0.2; - } - - &__sun { - opacity: 0.7; - } - - &__cam { - fill: $interfaceKeyColor; - transform-origin: center; - transform: scale(0.15); - } - - &__cam-fov-l, - &__cam-fov-r { - // Cam FOV indication - opacity: 0.2; - fill: #fff; - } - - &__nsew-text, - &__ticks-major, - &__ticks-minor { - fill: $color; - } - - &__ticks-minor { - opacity: 0.5; - transform: rotate(45deg); - } - - &__spacecraft-body { - opacity: 0.3; - } + &__edge { + opacity: 0.2; } + + &__sun { + opacity: 0.7; + } + + &__cam { + fill: $interfaceKeyColor; + transform-origin: center; + transform: scale(0.15); + } + + &__cam-fov-l, + &__cam-fov-r { + // Cam FOV indication + opacity: 0.2; + fill: #fff; + } + + &__nsew-text, + &__ticks-major, + &__ticks-minor { + fill: $color; + } + + &__ticks-minor { + opacity: 0.5; + transform: rotate(45deg); + } + + &__spacecraft-body { + opacity: 0.3; + } + } } /***************************** DIRECTION ROSE */ .w-direction-rose { - $s: 10%; - $m: 2%; - cursor: pointer; - pointer-events: all; - position: absolute; - bottom: $m; - left: $m; + $s: 10%; + $m: 2%; + cursor: pointer; + pointer-events: all; + position: absolute; + bottom: $m; + left: $m; + width: $s; + padding-top: $s; + z-index: 2; + + &.--rose-min { + $s: 30px; width: $s; padding-top: $s; - z-index: 2; - - &.--rose-min { - $s: 30px; - width: $s; - padding-top: $s; - .--hide-min { - display: none; - } + .--hide-min { + display: none; } + } - &.--rose-small { - .--hide-small { - display: none; - } + &.--rose-small { + .--hide-small { + display: none; } + } - &.--rose-max { - $s: 100px; - width: $s; - padding-top: $s; - } + &.--rose-max { + $s: 100px; + width: $s; + padding-top: $s; + } } /************************** ROVER */ .cr-vrover { - $scale: 0.4; - transform-origin: center; + $scale: 0.4; + transform-origin: center; - &__body { - fill: $interfaceKeyColor; - opacity: 0.3; - transform-origin: center 7% !important; // Places rotation center at mast position - } + &__body { + fill: $interfaceKeyColor; + opacity: 0.3; + transform-origin: center 7% !important; // Places rotation center at mast position + } } diff --git a/src/plugins/imagery/components/Compass/pluginSpec.js b/src/plugins/imagery/components/Compass/pluginSpec.js index 67e57b768e..78d159da74 100644 --- a/src/plugins/imagery/components/Compass/pluginSpec.js +++ b/src/plugins/imagery/components/Compass/pluginSpec.js @@ -25,68 +25,64 @@ import Vue from 'vue'; const COMPASS_ROSE_CLASS = '.c-direction-rose'; const COMPASS_HUD_CLASS = '.c-compass__hud'; -describe("The Compass component", () => { - let app; - let instance; +describe('The Compass component', () => { + let app; + let instance; - beforeEach(() => { - let imageDatum = { - heading: 100, - roll: 90, - pitch: 90, - cameraTilt: 100, - cameraAzimuth: 90, - sunAngle: 30, - transformations: { - translateX: 0, - translateY: 18, - rotation: 0, - scale: 0.3, - cameraAngleOfView: 70 - } - }; - let propsData = { - naturalAspectRatio: 0.9, - image: imageDatum, - sizedImageDimensions: { - width: 100, - height: 100 - } - }; + beforeEach(() => { + let imageDatum = { + heading: 100, + roll: 90, + pitch: 90, + cameraTilt: 100, + cameraAzimuth: 90, + sunAngle: 30, + transformations: { + translateX: 0, + translateY: 18, + rotation: 0, + scale: 0.3, + cameraAngleOfView: 70 + } + }; + let propsData = { + naturalAspectRatio: 0.9, + image: imageDatum, + sizedImageDimensions: { + width: 100, + height: 100 + } + }; - app = new Vue({ - components: { Compass }, - data() { - return propsData; - }, - template: `` - }); - instance = app.$mount(); + }); + instance = app.$mount(); + }); + + afterAll(() => { + app.$destroy(); + }); + + describe('when a heading value and cameraAngleOfView exists on the image', () => { + it('should display a compass rose', () => { + let compassRoseElement = instance.$el.querySelector(COMPASS_ROSE_CLASS); + + expect(compassRoseElement).toBeDefined(); }); - afterAll(() => { - app.$destroy(); + it('should display a compass HUD', () => { + let compassHUDElement = instance.$el.querySelector(COMPASS_HUD_CLASS); + + expect(compassHUDElement).toBeDefined(); }); - - describe("when a heading value and cameraAngleOfView exists on the image", () => { - - it("should display a compass rose", () => { - let compassRoseElement = instance.$el.querySelector(COMPASS_ROSE_CLASS - ); - - expect(compassRoseElement).toBeDefined(); - }); - - it("should display a compass HUD", () => { - let compassHUDElement = instance.$el.querySelector(COMPASS_HUD_CLASS); - - expect(compassHUDElement).toBeDefined(); - }); - - }); - + }); }); diff --git a/src/plugins/imagery/components/Compass/utils.js b/src/plugins/imagery/components/Compass/utils.js index 2d6a0a8a65..562108620a 100644 --- a/src/plugins/imagery/components/Compass/utils.js +++ b/src/plugins/imagery/components/Compass/utils.js @@ -8,37 +8,37 @@ * @returns {number} normalized sum of all rotations - [0, 360) degrees */ export function rotate(...rotations) { - const rotation = rotations.reduce((a, b) => a + b, 0); + const rotation = rotations.reduce((a, b) => a + b, 0); - return normalizeCompassDirection(rotation); + return normalizeCompassDirection(rotation); } export function inRange(degrees, [min, max]) { - const point = rotate(degrees); + const point = rotate(degrees); - return min > max - ? (point >= min && point < 360) || (point <= max && point >= 0) - : point >= min && point <= max; + return min > max + ? (point >= min && point < 360) || (point <= max && point >= 0) + : point >= min && point <= max; } export function percentOfRange(degrees, [min, max]) { - let distance = rotate(degrees); - let minRange = min; - let maxRange = max; + let distance = rotate(degrees); + let minRange = min; + let maxRange = max; - if (min > max) { - if (distance < max) { - distance += 360; - } - - maxRange += 360; + if (min > max) { + if (distance < max) { + distance += 360; } - return (distance - minRange) / (maxRange - minRange); + maxRange += 360; + } + + return (distance - minRange) / (maxRange - minRange); } function normalizeCompassDirection(degrees) { - const base = degrees % 360; + const base = degrees % 360; - return base >= 0 ? base : 360 + base; + return base >= 0 ? base : 360 + base; } diff --git a/src/plugins/imagery/components/FilterSettings.vue b/src/plugins/imagery/components/FilterSettings.vue index e15f76468c..a96e1484b3 100644 --- a/src/plugins/imagery/components/FilterSettings.vue +++ b/src/plugins/imagery/components/FilterSettings.vue @@ -20,80 +20,74 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/imagery/components/ImageControls.vue b/src/plugins/imagery/components/ImageControls.vue index 09b31b84c6..63f389d50a 100644 --- a/src/plugins/imagery/components/ImageControls.vue +++ b/src/plugins/imagery/components/ImageControls.vue @@ -21,69 +21,59 @@ --> diff --git a/src/plugins/imagery/components/ImageThumbnail.vue b/src/plugins/imagery/components/ImageThumbnail.vue index 936b6430cb..598abd6183 100644 --- a/src/plugins/imagery/components/ImageThumbnail.vue +++ b/src/plugins/imagery/components/ImageThumbnail.vue @@ -21,36 +21,27 @@ --> diff --git a/src/plugins/imagery/components/ImageryTimeView.vue b/src/plugins/imagery/components/ImageryTimeView.vue index 249d5e43cb..5fe8cf1325 100644 --- a/src/plugins/imagery/components/ImageryTimeView.vue +++ b/src/plugins/imagery/components/ImageryTimeView.vue @@ -21,25 +21,18 @@ --> diff --git a/src/plugins/imagery/components/ImageryView.vue b/src/plugins/imagery/components/ImageryView.vue index c679baaf38..0ac1840fec 100644 --- a/src/plugins/imagery/components/ImageryView.vue +++ b/src/plugins/imagery/components/ImageryView.vue @@ -21,172 +21,172 @@ --> diff --git a/src/plugins/imagery/components/ImageryViewMenuSwitcher.vue b/src/plugins/imagery/components/ImageryViewMenuSwitcher.vue index f347386fc3..53d466a959 100644 --- a/src/plugins/imagery/components/ImageryViewMenuSwitcher.vue +++ b/src/plugins/imagery/components/ImageryViewMenuSwitcher.vue @@ -20,67 +20,64 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/imagery/components/LayerSettings.vue b/src/plugins/imagery/components/LayerSettings.vue index edc029a5a1..d98fe62778 100644 --- a/src/plugins/imagery/components/LayerSettings.vue +++ b/src/plugins/imagery/components/LayerSettings.vue @@ -20,61 +20,56 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/imagery/components/RelatedTelemetry/RelatedTelemetry.js b/src/plugins/imagery/components/RelatedTelemetry/RelatedTelemetry.js index d1b67cd3d8..9eeb33ecb7 100644 --- a/src/plugins/imagery/components/RelatedTelemetry/RelatedTelemetry.js +++ b/src/plugins/imagery/components/RelatedTelemetry/RelatedTelemetry.js @@ -21,167 +21,174 @@ *****************************************************************************/ function copyRelatedMetadata(metadata) { - let compare = metadata.comparisonFunction; - let copiedMetadata = JSON.parse(JSON.stringify(metadata)); - copiedMetadata.comparisonFunction = compare; + let compare = metadata.comparisonFunction; + let copiedMetadata = JSON.parse(JSON.stringify(metadata)); + copiedMetadata.comparisonFunction = compare; - return copiedMetadata; + return copiedMetadata; } -import IndependentTimeContext from "@/api/time/IndependentTimeContext"; +import IndependentTimeContext from '@/api/time/IndependentTimeContext'; export default class RelatedTelemetry { + constructor(openmct, domainObject, telemetryKeys) { + this._openmct = openmct; + this._domainObject = domainObject; - constructor(openmct, domainObject, telemetryKeys) { - this._openmct = openmct; - this._domainObject = domainObject; + let metadata = this._openmct.telemetry.getMetadata(this._domainObject); + let imageHints = metadata.valuesForHints(['image'])[0]; - let metadata = this._openmct.telemetry.getMetadata(this._domainObject); - let imageHints = metadata.valuesForHints(['image'])[0]; + this.hasRelatedTelemetry = imageHints.relatedTelemetry !== undefined; - this.hasRelatedTelemetry = imageHints.relatedTelemetry !== undefined; + if (this.hasRelatedTelemetry) { + this.keys = telemetryKeys; - if (this.hasRelatedTelemetry) { - this.keys = telemetryKeys; + this._timeFormatter = undefined; + this._timeSystemChange(this._openmct.time.timeSystem()); - this._timeFormatter = undefined; - this._timeSystemChange(this._openmct.time.timeSystem()); - - // grab related telemetry metadata - for (let key of this.keys) { - if (imageHints.relatedTelemetry[key]) { - this[key] = copyRelatedMetadata(imageHints.relatedTelemetry[key]); - } - } - - this.load = this.load.bind(this); - this._parseTime = this._parseTime.bind(this); - this._timeSystemChange = this._timeSystemChange.bind(this); - this.destroy = this.destroy.bind(this); - - this._openmct.time.on('timeSystem', this._timeSystemChange); + // grab related telemetry metadata + for (let key of this.keys) { + if (imageHints.relatedTelemetry[key]) { + this[key] = copyRelatedMetadata(imageHints.relatedTelemetry[key]); } + } + + this.load = this.load.bind(this); + this._parseTime = this._parseTime.bind(this); + this._timeSystemChange = this._timeSystemChange.bind(this); + this.destroy = this.destroy.bind(this); + + this._openmct.time.on('timeSystem', this._timeSystemChange); + } + } + + async load() { + if (!this.hasRelatedTelemetry) { + throw new Error( + 'This domain object does not have related telemetry, use "hasRelatedTelemetry" to check before loading.' + ); } - async load() { - if (!this.hasRelatedTelemetry) { - throw new Error('This domain object does not have related telemetry, use "hasRelatedTelemetry" to check before loading.'); + await Promise.all( + this.keys.map(async (key) => { + if (this[key]) { + if (this[key].historical) { + await this._initializeHistorical(key); + } + + if ( + this[key].realtime && + this[key].realtime.telemetryObjectId && + this[key].realtime.telemetryObjectId !== '' + ) { + await this._intializeRealtime(key); + } } + }) + ); + } - await Promise.all( - this.keys.map(async (key) => { - if (this[key]) { - if (this[key].historical) { - await this._initializeHistorical(key); - } + async _initializeHistorical(key) { + if (!this[key].historical.telemetryObjectId) { + this[key].historical.hasTelemetryOnDatum = true; + } else if (this[key].historical.telemetryObjectId !== '') { + this[key].historicalDomainObject = await this._openmct.objects.get( + this[key].historical.telemetryObjectId + ); - if (this[key].realtime && this[key].realtime.telemetryObjectId && this[key].realtime.telemetryObjectId !== '') { - await this._intializeRealtime(key); - } - } - }) - ); - } + this[key].requestLatestFor = async (datum) => { + // We need to create a throwaway time context and pass it along + // as a request option. We do this to "trick" the Time API + // into thinking we are in fixed time mode in order to bypass this logic: + // https://github.com/akhenry/openmct-yamcs/blob/1060d42ebe43bf346dac0f6a8068cb288ade4ba4/src/providers/historical-telemetry-provider.js#L59 + // Context: https://github.com/akhenry/openmct-yamcs/pull/217 + const ephemeralContext = new IndependentTimeContext(this._openmct, this._openmct.time, [ + this[key].historicalDomainObject + ]); - async _initializeHistorical(key) { - if (!this[key].historical.telemetryObjectId) { - this[key].historical.hasTelemetryOnDatum = true; - } else if (this[key].historical.telemetryObjectId !== '') { - this[key].historicalDomainObject = await this._openmct.objects.get(this[key].historical.telemetryObjectId); - - this[key].requestLatestFor = async (datum) => { - // We need to create a throwaway time context and pass it along - // as a request option. We do this to "trick" the Time API - // into thinking we are in fixed time mode in order to bypass this logic: - // https://github.com/akhenry/openmct-yamcs/blob/1060d42ebe43bf346dac0f6a8068cb288ade4ba4/src/providers/historical-telemetry-provider.js#L59 - // Context: https://github.com/akhenry/openmct-yamcs/pull/217 - const ephemeralContext = new IndependentTimeContext( - this._openmct, - this._openmct.time, - [this[key].historicalDomainObject] - ); - - // Stop following the global context, stop the clock, - // and set bounds. - ephemeralContext.resetContext(); - const newBounds = { - start: this._openmct.time.bounds().start, - end: this._parseTime(datum) - }; - ephemeralContext.stopClock(); - ephemeralContext.bounds(newBounds); - - const options = { - start: newBounds.start, - end: newBounds.end, - timeContext: ephemeralContext, - strategy: 'latest' - }; - let results = await this._openmct.telemetry - .request(this[key].historicalDomainObject, options); - - return results[results.length - 1]; - }; - } - } - - async _intializeRealtime(key) { - this[key].realtimeDomainObject = await this._openmct.objects.get(this[key].realtime.telemetryObjectId); - this[key].listeners = []; - this[key].subscribe = (callback) => { - - if (!this[key].isSubscribed) { - this._subscribeToDataForKey(key); - } - - if (!this[key].listeners.includes(callback)) { - this[key].listeners.push(callback); - - return () => { - this[key].listeners.remove(callback); - }; - } else { - return () => {}; - } + // Stop following the global context, stop the clock, + // and set bounds. + ephemeralContext.resetContext(); + const newBounds = { + start: this._openmct.time.bounds().start, + end: this._parseTime(datum) }; + ephemeralContext.stopClock(); + ephemeralContext.bounds(newBounds); + + const options = { + start: newBounds.start, + end: newBounds.end, + timeContext: ephemeralContext, + strategy: 'latest' + }; + let results = await this._openmct.telemetry.request( + this[key].historicalDomainObject, + options + ); + + return results[results.length - 1]; + }; + } + } + + async _intializeRealtime(key) { + this[key].realtimeDomainObject = await this._openmct.objects.get( + this[key].realtime.telemetryObjectId + ); + this[key].listeners = []; + this[key].subscribe = (callback) => { + if (!this[key].isSubscribed) { + this._subscribeToDataForKey(key); + } + + if (!this[key].listeners.includes(callback)) { + this[key].listeners.push(callback); + + return () => { + this[key].listeners.remove(callback); + }; + } else { + return () => {}; + } + }; + } + + _subscribeToDataForKey(key) { + if (this[key].isSubscribed) { + return; } - _subscribeToDataForKey(key) { - if (this[key].isSubscribed) { - return; + if (this[key].realtimeDomainObject) { + this[key].unsubscribe = this._openmct.telemetry.subscribe( + this[key].realtimeDomainObject, + (datum) => { + this[key].listeners.forEach((callback) => { + callback(datum); + }); } + ); - if (this[key].realtimeDomainObject) { - this[key].unsubscribe = this._openmct.telemetry.subscribe( - this[key].realtimeDomainObject, datum => { - this[key].listeners.forEach(callback => { - callback(datum); - }); - - } - ); - - this[key].isSubscribed = true; - } + this[key].isSubscribed = true; } + } - _parseTime(datum) { - return this._timeFormatter.parse(datum); + _parseTime(datum) { + return this._timeFormatter.parse(datum); + } + + _timeSystemChange(system) { + let key = system.key; + let metadata = this._openmct.telemetry.getMetadata(this._domainObject); + let metadataValue = metadata.value(key) || { format: key }; + this._timeFormatter = this._openmct.telemetry.getValueFormatter(metadataValue); + } + + destroy() { + this._openmct.time.off('timeSystem', this._timeSystemChange); + for (let key of this.keys) { + if (this[key] && this[key].unsubscribe) { + this[key].unsubscribe(); + } } - - _timeSystemChange(system) { - let key = system.key; - let metadata = this._openmct.telemetry.getMetadata(this._domainObject); - let metadataValue = metadata.value(key) || { format: key }; - this._timeFormatter = this._openmct.telemetry.getValueFormatter(metadataValue); - } - - destroy() { - this._openmct.time.off('timeSystem', this._timeSystemChange); - for (let key of this.keys) { - if (this[key] && this[key].unsubscribe) { - this[key].unsubscribe(); - } - } - } - + } } diff --git a/src/plugins/imagery/components/ZoomSettings.vue b/src/plugins/imagery/components/ZoomSettings.vue index 67cd0c47e4..858a4c148e 100644 --- a/src/plugins/imagery/components/ZoomSettings.vue +++ b/src/plugins/imagery/components/ZoomSettings.vue @@ -20,91 +20,83 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/imagery/components/imagery-view.scss b/src/plugins/imagery/components/imagery-view.scss index 708c6848e3..f14a6cebb0 100644 --- a/src/plugins/imagery/components/imagery-view.scss +++ b/src/plugins/imagery/components/imagery-view.scss @@ -1,521 +1,542 @@ @use 'sass:math'; @keyframes fade-out { - from { - background-color: rgba($colorOk, 0.5); - } - to { - background-color: rgba($colorOk, 0); - color: inherit; - } + from { + background-color: rgba($colorOk, 0.5); + } + to { + background-color: rgba($colorOk, 0); + color: inherit; + } } .c-imagery { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + + &:focus { + outline: none; + } + + > * + * { + margin-top: $interiorMargin; + } + + &__main-image-wrapper { display: flex; flex-direction: column; - height: 100%; - overflow: hidden; + flex: 1 1 auto; - &:focus { - outline: none; + &.unsynced { + @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 { + 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 + ); + } + + &__main-image { + &__bg { + background-color: $colorPlotBg; + border: 1px solid transparent; + display: flex; + align-items: center; + justify-content: center; + flex: 1 1 auto; + height: 0; + overflow: hidden; + } + &__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%; + opacity: 0; + } + + &__image-save-proxy { + height: 100%; + width: 100%; + z-index: 10; + } + } + + &__hints { + $m: $interiorMargin; + background: rgba(black, 0.2); + border-radius: $smallCr; + padding: 2px $interiorMargin; + pointer-events: none; + position: absolute; + right: $m; + top: $m; + opacity: 0.9; + z-index: 2; + } + + &__control-bar, + &__time { + display: flex; + align-items: baseline; + > * + * { - margin-top: $interiorMargin; + margin-left: $interiorMarginSm; + } + } + + &__control-bar { + margin-top: 2px; + padding: $interiorMarginSm 0; + justify-content: space-between; + } + + &__time { + flex: 0 1 auto; + overflow: hidden; + } + + &__timestamp, + &__age { + @include ellipsize(); + flex: 0 1 auto; + } + + &__timestamp { + flex-shrink: 10; + } + + &__age { + border-radius: $smallCr; + display: flex; + flex-shrink: 0; + align-items: center; + padding: 2px $interiorMarginSm; + + &:before { + font-size: 0.9em; + opacity: 0.5; + margin-right: $interiorMarginSm; + } + } + + &--new { + // New imagery + $bgColor: $colorOk; + color: $colorOkFg; + background-color: rgba($bgColor, 0.5); + animation-name: fade-out; + animation-timing-function: ease-in; + animation-iteration-count: 1; + animation-fill-mode: forwards; + &.no-animation { + animation: none; + } + } + + &__layer-image { + pointer-events: none; + z-index: 1; + } + + &__thumbs-wrapper { + display: flex; // Uses row layout + justify-content: flex-end; + + &.is-autoscroll-off { + background: $colorInteriorBorder; + [class*='__auto-scroll-resume-button'] { + display: block; + } } - &__main-image-wrapper { - display: flex; - flex-direction: column; - flex: 1 1 auto; - - &.unsynced{ - @include sUnsynced(); - } - - &.cursor-zoom-in { - cursor: zoom-in; - } - - &.cursor-zoom-out { - cursor: zoom-out; - } - - &.pannable { - @include cursorGrab(); - } + &.is-paused { + background: rgba($colorPausedBg, 0.4); } + } - .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); + &__thumbs-scroll-area { + flex: 0 1 auto; + display: flex; + flex-direction: row; + height: 145px; + overflow-x: auto; + overflow-y: hidden; + margin-bottom: 1px; + padding-bottom: $interiorMarginSm; + &.animate-scroll { + scroll-behavior: smooth; } + } - .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); + &__auto-scroll-resume-button { + display: none; // Set to block when __thumbs-wrapper has .is-autoscroll-off + flex: 0 0 auto; + 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; } + } - &__main-image { - &__bg { - background-color: $colorPlotBg; - border: 1px solid transparent; - display: flex; - align-items: center; - justify-content: center; - flex: 1 1 auto; - height: 0; - overflow: hidden; - } - &__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%; - opacity: 0; - } + .c-switcher-menu { + display: contents; - &__image-save-proxy { - height: 100%; - width: 100%; - z-index: 10; - } - } - - &__hints { - $m: $interiorMargin; - background: rgba(black, 0.2); - border-radius: $smallCr; - padding: 2px $interiorMargin; - pointer-events: none; - position: absolute; - right: $m; - top: $m; - opacity: 0.9; - z-index: 2; - } - - &__control-bar, - &__time { - display: flex; - align-items: baseline; - - > * + * { - margin-left: $interiorMarginSm; - } - - } - - &__control-bar { - margin-top: 2px; - padding: $interiorMarginSm 0; - justify-content: space-between; - - } - - &__time { - flex: 0 1 auto; - overflow: hidden; - } - - &__timestamp, - &__age { - @include ellipsize(); - flex: 0 1 auto; - } - - &__timestamp { - flex-shrink: 10; - } - - &__age { - border-radius: $smallCr; - display: flex; - flex-shrink: 0; - align-items: center; - padding: 2px $interiorMarginSm; - - &:before { - font-size: 0.9em; - opacity: 0.5; - margin-right: $interiorMarginSm; - } - } - - &--new { - // New imagery - $bgColor: $colorOk; - color: $colorOkFg; - background-color: rgba($bgColor, 0.5); - animation-name: fade-out; - animation-timing-function: ease-in; - animation-iteration-count: 1; - animation-fill-mode: forwards; - &.no-animation { - animation: none; - } - } - - - &__layer-image { - pointer-events: none; - z-index: 1; - } - - &__thumbs-wrapper { - display: flex; // Uses row layout - justify-content: flex-end; - - &.is-autoscroll-off { - background: $colorInteriorBorder; - [class*='__auto-scroll-resume-button'] { - display: block; - } - } - - &.is-paused { - background: rgba($colorPausedBg, 0.4); - } - } - - &__thumbs-scroll-area { - flex: 0 1 auto; - display: flex; - flex-direction: row; - height: 145px; - overflow-x: auto; - overflow-y: hidden; - margin-bottom: 1px; - padding-bottom: $interiorMarginSm; - &.animate-scroll { - scroll-behavior: smooth; - } - } - - &__auto-scroll-resume-button { - display: none; // Set to block when __thumbs-wrapper has .is-autoscroll-off - flex: 0 0 auto; - 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; - } - } + &__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; + display: contents !important; } .s-image-layer { - position: absolute; - height: 100%; - width: 100%; - opacity: 1; - background-size: contain; - background-repeat: no-repeat; - background-position: center; + position: absolute; + height: 100%; + width: 100%; + opacity: 1; + background-size: contain; + background-repeat: no-repeat; + background-position: center; } /*************************************** THUMBS */ .c-thumb { - $w: $imageThumbsD; - display: flex; - flex-direction: column; - padding: 4px; - min-width: $w; - width: $w; + $w: $imageThumbsD; + display: flex; + flex-direction: column; + padding: 4px; + min-width: $w; + width: $w; - &.active { - background: $colorSelectedBg; - color: $colorSelectedFg; - } - &:hover { - background: $colorThumbHoverBg; - } - &.selected { - // 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; - } + &.active { + background: $colorSelectedBg; + color: $colorSelectedFg; + } + &:hover { + background: $colorThumbHoverBg; + } + &.selected { + // 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 { - background-color: rgba($colorBodyFg, 0.2); - width: 100%; - } + &__image { + background-color: rgba($colorBodyFg, 0.2); + width: 100%; + } - &__timestamp { - flex: 0 0 auto; - padding: 2px 3px; - } + &__timestamp { + flex: 0 0 auto; + padding: 2px 3px; + } - &__viewable-area { - position: absolute; - border: 2px yellow solid; - left: 0; - top: 0; - } + &__viewable-area { + position: absolute; + border: 2px yellow solid; + left: 0; + top: 0; + } } .is-small-thumbs { - .c-imagery__thumbs-scroll-area { - height: 60px; // Allow room for scrollbar - } + .c-imagery__thumbs-scroll-area { + height: 60px; // Allow room for scrollbar + } - .c-thumb { - $w: math.div($imageThumbsD, 2); - min-width: $w; - width: $w; + .c-thumb { + $w: math.div($imageThumbsD, 2); + min-width: $w; + width: $w; - &__timestamp { - display: none; - } + &__timestamp { + display: none; } + } } /*************************************** IMAGERY LOCAL CONTROLS*/ .c-imagery { - .h-local-controls--overlay-content { - display: flex; - flex-direction: row; - position: absolute; - left: $interiorMargin; top: $interiorMargin; - z-index: 10; - background: $colorLocalControlOvrBg; - border-radius: $basicCr; - align-items: center; - padding: $interiorMargin $interiorMargin; + .h-local-controls--overlay-content { + display: flex; + flex-direction: row; + position: absolute; + left: $interiorMargin; + top: $interiorMargin; + z-index: 10; + background: $colorLocalControlOvrBg; + border-radius: $basicCr; + align-items: center; + padding: $interiorMargin $interiorMargin; - .s-status-taking-snapshot & { - display: none; - } + .s-status-taking-snapshot & { + display: none; } - [class*='--menus-aligned'] { - > * + * { - button { margin-left: $interiorMarginSm; } - } + } + [class*='--menus-aligned'] { + > * + * { + button { + margin-left: $interiorMarginSm; + } } + } } .c-image-controls { - &__controls-wrapper { - // Wraps __controls and __close-btn - display: flex; + &__controls-wrapper { + // Wraps __controls and __close-btn + display: flex; + } + + &__controls { + display: flex; + align-items: stretch; + + > * + * { + margin-top: $interiorMargin; } - &__controls { + [class*='c-button'] { + flex: 0 0 auto; + } + } + + &__control, + &__input { + display: flex; + align-items: center; + + &:before { + color: rgba($colorMenuFg, 0.5); + margin-right: $interiorMarginSm; + } + } + + &__zoom { + > * + * { + margin-left: $interiorMargin; + } // Is this used? + } + + &--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 + } + + &__sliders { display: flex; - align-items: stretch; + flex: 1 1 auto; + flex-direction: column; + width: 100%; > * + * { - margin-top: $interiorMargin; + margin-top: 11px; } - [class*='c-button'] { flex: 0 0 auto; } - } + input[type='range'] { + display: block; + width: 100%; + } + } - &__control, - &__input { + &__slider-wrapper { display: flex; align-items: center; &:before { - color: rgba($colorMenuFg, 0.5); - margin-right: $interiorMarginSm; + 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; } - } - - &__zoom { - > * + * { margin-left: $interiorMargin; } // Is this used? - } - - &--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 - } - - &__sliders { - display: flex; - flex: 1 1 auto; - flex-direction: column; - width: 100%; - - > * + * { - 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; - } - } + &:before { + border-top: 1px solid $bc; + margin-bottom: 2px; } + + &:after { + border-bottom: 1px solid $bc; + margin-top: 2px; + } + + .c-icon-link { + color: $colorBtnFg; + } + } } + } } /*************************************** BUTTONS */ .c-button.pause-play { - // Pause icon set by default in markup - justify-self: end; + // Pause icon set by default in markup + justify-self: end; - &.is-paused { - background: $colorPausedBg !important; - color: $colorPausedFg; + &.is-paused { + background: $colorPausedBg !important; + color: $colorPausedFg; - &:before { - content: $glyph-icon-play; - } + &:before { + content: $glyph-icon-play; } + } - .s-status-taking-snapshot & { - display: none; - } + .s-status-taking-snapshot & { + display: none; + } } .c-imagery__prev-next-button { - pointer-events: all; + pointer-events: all; + position: absolute; + top: 50%; + transform: translateY(-75%); // 75% due to transform: rotation approach to the button + + &.c-nav { position: absolute; - top: 50%; - transform: translateY(-75%); // 75% due to transform: rotation approach to the button - &.c-nav { - position: absolute; - - &--prev { left: 0; } - &--next { right: 0; } + &--prev { + left: 0; } - - .s-status-taking-snapshot & { - display: none; + &--next { + right: 0; } + } + + .s-status-taking-snapshot & { + display: none; + } } .c-nav { - @include cArrowButtonBase($colorBg: rgba($colorLocalControlOvrBg, 0.1), $colorFg: $colorBtnBg); - @include cArrowButtonSizing($dimOuter: 48px); - border-radius: $controlCr; + @include cArrowButtonBase($colorBg: rgba($colorLocalControlOvrBg, 0.1), $colorFg: $colorBtnBg); + @include cArrowButtonSizing($dimOuter: 48px); + border-radius: $controlCr; - .--width-less-than-600 & { - @include cArrowButtonSizing($dimOuter: 32px); - } + .--width-less-than-600 & { + @include cArrowButtonSizing($dimOuter: 32px); + } } /*************************************** IMAGERY IN TIMESTRIP VIEWS */ .c-imagery-tsv { - div.c-imagery-tsv__image-wrapper { - cursor: pointer; - position: absolute; - top: 0; - display: flex; - z-index: 1; - margin-top: 5px; + div.c-imagery-tsv__image-wrapper { + cursor: pointer; + position: absolute; + top: 0; + display: flex; + z-index: 1; + margin-top: 5px; - img { - align-self: flex-end; - } - &:hover { - z-index: 2; - - [class*='__image-handle'] { - background-color: $colorBodyFg; - } - - img { - display: block !important; - } - } + img { + align-self: flex-end; } + &:hover { + z-index: 2; - &__no-items { - fill: $colorBodyFg !important; - } + [class*='__image-handle'] { + background-color: $colorBodyFg; + } - &__image-handle { - background-color: rgba($colorBodyFg, 0.5); + img { + display: block !important; + } } + } - &__image-placeholder { - background-color: pushBack($colorBodyBg, 0.3); - display: block; - align-self: flex-end; - } + &__no-items { + fill: $colorBodyFg !important; + } + + &__image-handle { + background-color: rgba($colorBodyFg, 0.5); + } + + &__image-placeholder { + background-color: pushBack($colorBodyBg, 0.3); + display: block; + align-self: flex-end; + } } diff --git a/src/plugins/imagery/lib/eventHelpers.js b/src/plugins/imagery/lib/eventHelpers.js index 337db1bc0c..367072b9f6 100644 --- a/src/plugins/imagery/lib/eventHelpers.js +++ b/src/plugins/imagery/lib/eventHelpers.js @@ -21,78 +21,79 @@ *****************************************************************************/ define([], function () { - const helperFunctions = { - listenTo: function (object, event, callback, context) { - if (!this._listeningTo) { - this._listeningTo = []; - } + const helperFunctions = { + listenTo: function (object, event, callback, context) { + if (!this._listeningTo) { + this._listeningTo = []; + } - const listener = { - object: object, - event: event, - callback: callback, - context: context, - _cb: context ? callback.bind(context) : callback - }; - if (object.$watch && event.indexOf('change:') === 0) { - const scopePath = event.replace('change:', ''); - listener.unlisten = object.$watch(scopePath, listener._cb, true); - } else if (object.$on) { - listener.unlisten = object.$on(event, listener._cb); - } else if (object.addEventListener) { - object.addEventListener(event, listener._cb); - } else { - object.on(event, listener._cb); - } + const listener = { + object: object, + event: event, + callback: callback, + context: context, + _cb: context ? callback.bind(context) : callback + }; + if (object.$watch && event.indexOf('change:') === 0) { + const scopePath = event.replace('change:', ''); + listener.unlisten = object.$watch(scopePath, listener._cb, true); + } else if (object.$on) { + listener.unlisten = object.$on(event, listener._cb); + } else if (object.addEventListener) { + object.addEventListener(event, listener._cb); + } else { + object.on(event, listener._cb); + } - this._listeningTo.push(listener); - }, + this._listeningTo.push(listener); + }, - stopListening: function (object, event, callback, context) { - if (!this._listeningTo) { - this._listeningTo = []; - } + stopListening: function (object, event, callback, context) { + if (!this._listeningTo) { + this._listeningTo = []; + } - this._listeningTo.filter(function (listener) { - if (object && object !== listener.object) { - return false; - } + this._listeningTo + .filter(function (listener) { + if (object && object !== listener.object) { + return false; + } - if (event && event !== listener.event) { - return false; - } + if (event && event !== listener.event) { + return false; + } - if (callback && callback !== listener.callback) { - return false; - } + if (callback && callback !== listener.callback) { + return false; + } - if (context && context !== listener.context) { - return false; - } + if (context && context !== listener.context) { + return false; + } - return true; - }) - .map(function (listener) { - if (listener.unlisten) { - listener.unlisten(); - } else if (listener.object.removeEventListener) { - listener.object.removeEventListener(listener.event, listener._cb); - } else { - listener.object.off(listener.event, listener._cb); - } + return true; + }) + .map(function (listener) { + if (listener.unlisten) { + listener.unlisten(); + } else if (listener.object.removeEventListener) { + listener.object.removeEventListener(listener.event, listener._cb); + } else { + listener.object.off(listener.event, listener._cb); + } - return listener; - }) - .forEach(function (listener) { - this._listeningTo.splice(this._listeningTo.indexOf(listener), 1); - }, this); - }, + return listener; + }) + .forEach(function (listener) { + this._listeningTo.splice(this._listeningTo.indexOf(listener), 1); + }, this); + }, - extend: function (object) { - object.listenTo = helperFunctions.listenTo; - object.stopListening = helperFunctions.stopListening; - } - }; + extend: function (object) { + object.listenTo = helperFunctions.listenTo; + object.stopListening = helperFunctions.stopListening; + } + }; - return helperFunctions; + return helperFunctions; }); diff --git a/src/plugins/imagery/mixins/imageryData.js b/src/plugins/imagery/mixins/imageryData.js index 06dc81c514..aa594c4759 100644 --- a/src/plugins/imagery/mixins/imageryData.js +++ b/src/plugins/imagery/mixins/imageryData.js @@ -26,167 +26,175 @@ const IMAGE_THUMBNAIL_HINT_KEY = 'thumbnail'; const IMAGE_DOWNLOAD_NAME_HINT_KEY = 'imageDownloadName'; export default { - inject: ['openmct', 'domainObject', 'objectPath'], - mounted() { - // listen - this.boundsChange = this.boundsChange.bind(this); - this.timeSystemChange = this.timeSystemChange.bind(this); - this.setDataTimeContext = this.setDataTimeContext.bind(this); - this.setDataTimeContext(); - this.openmct.objectViews.on('clearData', this.dataCleared); + inject: ['openmct', 'domainObject', 'objectPath'], + mounted() { + // listen + this.boundsChange = this.boundsChange.bind(this); + this.timeSystemChange = this.timeSystemChange.bind(this); + this.setDataTimeContext = this.setDataTimeContext.bind(this); + this.setDataTimeContext(); + this.openmct.objectViews.on('clearData', this.dataCleared); - // Get metadata and formatters - this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); - this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); + // Get metadata and formatters + this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); + this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); - this.imageMetadataValue = { ...this.metadata.valuesForHints([IMAGE_HINT_KEY])[0] }; - this.imageFormatter = this.getFormatter(this.imageMetadataValue.key); + this.imageMetadataValue = { ...this.metadata.valuesForHints([IMAGE_HINT_KEY])[0] }; + this.imageFormatter = this.getFormatter(this.imageMetadataValue.key); - this.imageThumbnailMetadataValue = { ...this.metadata.valuesForHints([IMAGE_THUMBNAIL_HINT_KEY])[0] }; - this.imageThumbnailFormatter = this.imageThumbnailMetadataValue.key - ? this.getFormatter(this.imageThumbnailMetadataValue.key) - : null; + this.imageThumbnailMetadataValue = { + ...this.metadata.valuesForHints([IMAGE_THUMBNAIL_HINT_KEY])[0] + }; + this.imageThumbnailFormatter = this.imageThumbnailMetadataValue.key + ? this.getFormatter(this.imageThumbnailMetadataValue.key) + : null; - this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); - this.imageDownloadNameMetadataValue = { ...this.metadata.valuesForHints([IMAGE_DOWNLOAD_NAME_HINT_KEY])[0]}; + this.durationFormatter = this.getFormatter( + this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER + ); + this.imageDownloadNameMetadataValue = { + ...this.metadata.valuesForHints([IMAGE_DOWNLOAD_NAME_HINT_KEY])[0] + }; - // initialize - this.timeKey = this.timeSystem.key; - this.timeFormatter = this.getFormatter(this.timeKey); + // initialize + this.timeKey = this.timeSystem.key; + this.timeFormatter = this.getFormatter(this.timeKey); - 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) { - this.unsubscribe(); - delete this.unsubscribe; - } - - this.stopFollowingDataTimeContext(); - 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(addedItems, addedItemIndices) { - const normalizedDataToAdd = addedItems.map(datum => this.normalizeDatum(datum)); - let newImageHistory = this.imageHistory.slice(); - normalizedDataToAdd.forEach(((datum, index) => { - newImageHistory.splice(addedItemIndices[index] ?? -1, 0, datum); - })); - //Assign just once so imageHistory watchers don't get called too often - this.imageHistory = newImageHistory; - }, - 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); - this.timeContext.on('bounds', this.boundsChange); - this.boundsChange(this.timeContext.bounds()); - this.timeContext.on('timeSystem', this.timeSystemChange); - }, - stopFollowingDataTimeContext() { - if (this.timeContext) { - this.timeContext.off('bounds', this.boundsChange); - this.timeContext.off('timeSystem', this.timeSystemChange); - } - }, - formatImageUrl(datum) { - if (!datum) { - return; - } - - return this.imageFormatter.format(datum); - }, - formatImageThumbnailUrl(datum) { - if (!datum || !this.imageThumbnailFormatter) { - return; - } - - return this.imageThumbnailFormatter.format(datum); - }, - formatTime(datum) { - if (!datum) { - return; - } - - const dateTimeStr = this.timeFormatter.format(datum); - - // Replace ISO "T" with a space to allow wrapping - return dateTimeStr.replace("T", " "); - }, - getImageDownloadName(datum) { - let imageDownloadName = ''; - if (datum) { - const key = this.imageDownloadNameMetadataValue.key; - imageDownloadName = datum[key]; - } - - return imageDownloadName; - }, - parseTime(datum) { - if (!datum) { - return; - } - - return this.timeFormatter.parse(datum); - }, - boundsChange(bounds, isTick) { - if (isTick) { - return; - } - - this.bounds = bounds; // setting bounds for ImageryView watcher - }, - timeSystemChange() { - this.timeSystem = this.timeContext.timeSystem(); - this.timeKey = this.timeSystem.key; - this.timeFormatter = this.getFormatter(this.timeKey); - this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); - }, - normalizeDatum(datum) { - const formattedTime = this.formatTime(datum); - const url = this.formatImageUrl(datum); - const thumbnailUrl = this.formatImageThumbnailUrl(datum); - const time = this.parseTime(formattedTime); - const imageDownloadName = this.getImageDownloadName(datum); - - return { - ...datum, - formattedTime, - url, - thumbnailUrl, - time, - imageDownloadName - }; - }, - getFormatter(key) { - const metadataValue = this.metadata.value(key) || { format: key }; - const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); - - return valueFormatter; - } + 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) { + this.unsubscribe(); + delete this.unsubscribe; } + + this.stopFollowingDataTimeContext(); + 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(addedItems, addedItemIndices) { + const normalizedDataToAdd = addedItems.map((datum) => this.normalizeDatum(datum)); + let newImageHistory = this.imageHistory.slice(); + normalizedDataToAdd.forEach((datum, index) => { + newImageHistory.splice(addedItemIndices[index] ?? -1, 0, datum); + }); + //Assign just once so imageHistory watchers don't get called too often + this.imageHistory = newImageHistory; + }, + 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); + this.timeContext.on('bounds', this.boundsChange); + this.boundsChange(this.timeContext.bounds()); + this.timeContext.on('timeSystem', this.timeSystemChange); + }, + stopFollowingDataTimeContext() { + if (this.timeContext) { + this.timeContext.off('bounds', this.boundsChange); + this.timeContext.off('timeSystem', this.timeSystemChange); + } + }, + formatImageUrl(datum) { + if (!datum) { + return; + } + + return this.imageFormatter.format(datum); + }, + formatImageThumbnailUrl(datum) { + if (!datum || !this.imageThumbnailFormatter) { + return; + } + + return this.imageThumbnailFormatter.format(datum); + }, + formatTime(datum) { + if (!datum) { + return; + } + + const dateTimeStr = this.timeFormatter.format(datum); + + // Replace ISO "T" with a space to allow wrapping + return dateTimeStr.replace('T', ' '); + }, + getImageDownloadName(datum) { + let imageDownloadName = ''; + if (datum) { + const key = this.imageDownloadNameMetadataValue.key; + imageDownloadName = datum[key]; + } + + return imageDownloadName; + }, + parseTime(datum) { + if (!datum) { + return; + } + + return this.timeFormatter.parse(datum); + }, + boundsChange(bounds, isTick) { + if (isTick) { + return; + } + + this.bounds = bounds; // setting bounds for ImageryView watcher + }, + timeSystemChange() { + this.timeSystem = this.timeContext.timeSystem(); + this.timeKey = this.timeSystem.key; + this.timeFormatter = this.getFormatter(this.timeKey); + this.durationFormatter = this.getFormatter( + this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER + ); + }, + normalizeDatum(datum) { + const formattedTime = this.formatTime(datum); + const url = this.formatImageUrl(datum); + const thumbnailUrl = this.formatImageThumbnailUrl(datum); + const time = this.parseTime(formattedTime); + const imageDownloadName = this.getImageDownloadName(datum); + + return { + ...datum, + formattedTime, + url, + thumbnailUrl, + time, + imageDownloadName + }; + }, + getFormatter(key) { + const metadataValue = this.metadata.value(key) || { format: key }; + const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); + + return valueFormatter; + } + } }; diff --git a/src/plugins/imagery/plugin.js b/src/plugins/imagery/plugin.js index c331775fc7..801e156cd1 100644 --- a/src/plugins/imagery/plugin.js +++ b/src/plugins/imagery/plugin.js @@ -24,9 +24,8 @@ import ImageryViewProvider from './ImageryViewProvider'; import ImageryTimestripViewProvider from './ImageryTimestripViewProvider'; export default function (options) { - return function install(openmct) { - openmct.objectViews.addProvider(new ImageryViewProvider(openmct, options)); - openmct.objectViews.addProvider(new ImageryTimestripViewProvider(openmct)); - }; + return function install(openmct) { + openmct.objectViews.addProvider(new ImageryViewProvider(openmct, options)); + openmct.objectViews.addProvider(new ImageryTimestripViewProvider(openmct)); + }; } - diff --git a/src/plugins/imagery/pluginSpec.js b/src/plugins/imagery/pluginSpec.js index 1529c68829..f54cc65c73 100644 --- a/src/plugins/imagery/pluginSpec.js +++ b/src/plugins/imagery/pluginSpec.js @@ -22,10 +22,10 @@ import Vue from 'vue'; import { - createMouseEvent, - createOpenMct, - resetApplicationState, - simulateKeyEvent + createMouseEvent, + createOpenMct, + resetApplicationState, + simulateKeyEvent } from 'utils/testing'; import ClearDataPlugin from '../clearData/plugin'; @@ -36,693 +36,720 @@ const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new'; const REFRESH_CSS_MS = 500; function formatThumbnail(url) { - return url.replace('logo-openmct.svg', 'logo-nasa.svg'); + return url.replace('logo-openmct.svg', 'logo-nasa.svg'); } function getImageInfo(doc) { - let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0]; - let timestamp = imageElement.dataset.openmctImageTimestamp; - let identifier = imageElement.dataset.openmctObjectKeystring; - let url = imageElement.src; + let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0]; + let timestamp = imageElement.dataset.openmctImageTimestamp; + let identifier = imageElement.dataset.openmctObjectKeystring; + let url = imageElement.src; - return { - timestamp, - identifier, - url - }; + return { + timestamp, + identifier, + url + }; } function isNew(doc) { - let newIcon = doc.querySelectorAll(NEW_IMAGE_CLASS); + let newIcon = doc.querySelectorAll(NEW_IMAGE_CLASS); - return newIcon.length !== 0; + return newIcon.length !== 0; } function generateTelemetry(start, count) { - let telemetry = []; + let telemetry = []; - for (let i = 1, l = count + 1; i < l; i++) { - let stringRep = i + 'minute'; - let logo = 'images/logo-openmct.svg'; + for (let i = 1, l = count + 1; i < l; i++) { + let stringRep = i + 'minute'; + let logo = 'images/logo-openmct.svg'; - telemetry.push({ - "name": stringRep + " Imagery", - "utc": start + (i * ONE_MINUTE), - "url": location.host + '/' + logo + '?time=' + stringRep, - "timeId": stringRep, - "value": 100 - }); - } + telemetry.push({ + name: stringRep + ' Imagery', + utc: start + i * ONE_MINUTE, + url: location.host + '/' + logo + '?time=' + stringRep, + timeId: stringRep, + value: 100 + }); + } - return telemetry; + return telemetry; } -describe("The Imagery View Layouts", () => { - const imageryKey = 'example.imagery'; - const imageryForTimeStripKey = 'example.imagery.time-strip.view'; - const START = Date.now(); - const COUNT = 10; +describe('The Imagery View Layouts', () => { + const imageryKey = 'example.imagery'; + const imageryForTimeStripKey = 'example.imagery.time-strip.view'; + const START = Date.now(); + const COUNT = 10; - // let resolveFunction; - let originalRouterPath; - let telemetryPromise; - let telemetryPromiseResolve; - let cleanupFirst; + // let resolveFunction; + let originalRouterPath; + let telemetryPromise; + let telemetryPromiseResolve; + let cleanupFirst; - let openmct; - let parent; - let child; - let historicalProvider; - let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT); - let imageryObject = { - identifier: { - namespace: "", - key: "imageryId" - }, - name: "Example Imagery", - type: "example.imagery", - 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" - }, - { - "name": "Image Thumbnail", - "key": "thumbnail-url", - "format": "thumbnail", - "hints": { - "thumbnail": 1, - "priority": 3 - }, - "source": "url" - }, - { - "name": "Name", - "key": "name", - "source": "name", - "hints": { - "priority": 0 - } - }, - { - "name": "Time", - "key": "utc", - "format": "utc", - "hints": { - "domain": 2, - "priority": 1 - }, - "source": "utc" - }, - { - "name": "Local Time", - "key": "local", - "format": "local-format", - "hints": { - "domain": 1, - "priority": 2 - }, - "source": "local" - } - ] + let openmct; + let parent; + let child; + let historicalProvider; + let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT); + let imageryObject = { + identifier: { + namespace: '', + key: 'imageryId' + }, + name: 'Example Imagery', + type: 'example.imagery', + location: 'parentId', + modified: 0, + persisted: 0, + configuration: { + layers: [ + { + name: '16:9', + visible: true } - }; - - // this setups up the app - beforeEach((done) => { - cleanupFirst = []; - - openmct = createOpenMct(); - - telemetryPromise = new Promise((resolve) => { - telemetryPromiseResolve = resolve; - }); - - historicalProvider = { - request: () => { - return Promise.resolve(imageTelemetry); + ] + }, + telemetry: { + values: [ + { + name: 'Image', + key: 'url', + format: 'image', + layers: [ + { + source: location.host + '/images/bg-splash.jpg', + name: '16:9' } - }; - spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider); + ], + hints: { + image: 1, + priority: 3 + }, + source: 'url' + }, + { + name: 'Image Thumbnail', + key: 'thumbnail-url', + format: 'thumbnail', + hints: { + thumbnail: 1, + priority: 3 + }, + source: 'url' + }, + { + name: 'Name', + key: 'name', + source: 'name', + hints: { + priority: 0 + } + }, + { + name: 'Time', + key: 'utc', + format: 'utc', + hints: { + domain: 2, + priority: 1 + }, + source: 'utc' + }, + { + name: 'Local Time', + key: 'local', + format: 'local-format', + hints: { + domain: 1, + priority: 2 + }, + source: 'local' + } + ] + } + }; - spyOn(openmct.telemetry, 'request').and.callFake(() => { - telemetryPromiseResolve(imageTelemetry); + // this setups up the app + beforeEach((done) => { + cleanupFirst = []; - return telemetryPromise; - }); + openmct = createOpenMct(); - parent = document.createElement('div'); - parent.style.width = '640px'; - parent.style.height = '480px'; - - child = document.createElement('div'); - child.style.width = '640px'; - child.style.height = '480px'; - - parent.appendChild(child); - document.body.appendChild(parent); - - spyOn(window, 'ResizeObserver').and.returnValue({ - observe() {}, - disconnect() {} - }); - - //spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([])); - spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(imageryObject)); - - originalRouterPath = openmct.router.path; - - openmct.telemetry.addFormat({ - key: 'thumbnail', - format: formatThumbnail - }); - - openmct.on('start', done); - openmct.startHeadless(); + telemetryPromise = new Promise((resolve) => { + telemetryPromiseResolve = resolve; }); - afterEach((done) => { - openmct.router.path = originalRouterPath; + historicalProvider = { + request: () => { + return Promise.resolve(imageTelemetry); + } + }; + spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider); - // Needs to be in a timeout because plots use a bunch of setTimeouts, some of which can resolve during or after - // teardown, which causes problems - // This is hacky, we should find a better approach here. + spyOn(openmct.telemetry, 'request').and.callFake(() => { + telemetryPromiseResolve(imageTelemetry); + + return telemetryPromise; + }); + + parent = document.createElement('div'); + parent.style.width = '640px'; + parent.style.height = '480px'; + + child = document.createElement('div'); + child.style.width = '640px'; + child.style.height = '480px'; + + parent.appendChild(child); + document.body.appendChild(parent); + + spyOn(window, 'ResizeObserver').and.returnValue({ + observe() {}, + disconnect() {} + }); + + //spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([])); + spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(imageryObject)); + + originalRouterPath = openmct.router.path; + + openmct.telemetry.addFormat({ + key: 'thumbnail', + format: formatThumbnail + }); + + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach((done) => { + openmct.router.path = originalRouterPath; + + // Needs to be in a timeout because plots use a bunch of setTimeouts, some of which can resolve during or after + // teardown, which causes problems + // This is hacky, we should find a better approach here. + setTimeout(() => { + //Cleanup code that needs to happen before dom elements start being destroyed + cleanupFirst.forEach((cleanup) => cleanup()); + cleanupFirst = []; + document.body.removeChild(parent); + + resetApplicationState(openmct).then(done).catch(done); + }); + }); + + it('should provide an imagery time strip view when in a time strip', () => { + openmct.router.path = [ + { + identifier: { + key: 'test-timestrip', + namespace: '' + }, + type: 'time-strip' + } + ]; + + let applicableViews = openmct.objectViews.get(imageryObject, [ + imageryObject, + { + identifier: { + key: 'test-timestrip', + namespace: '' + }, + type: 'time-strip' + } + ]); + let imageryView = applicableViews.find( + (viewProvider) => viewProvider.key === imageryForTimeStripKey + ); + + expect(imageryView).toBeDefined(); + }); + + it('should provide an imagery view only for imagery producing objects', () => { + let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]); + let imageryView = applicableViews.find((viewProvider) => viewProvider.key === imageryKey); + + expect(imageryView).toBeDefined(); + }); + + it('should not provide an imagery view when in a time strip', () => { + openmct.router.path = [ + { + identifier: { + key: 'test-timestrip', + namespace: '' + }, + type: 'time-strip' + } + ]; + + let applicableViews = openmct.objectViews.get(imageryObject, [ + imageryObject, + { + identifier: { + key: 'test-timestrip', + namespace: '' + }, + type: 'time-strip' + } + ]); + let imageryView = applicableViews.find((viewProvider) => viewProvider.key === imageryKey); + + expect(imageryView).toBeUndefined(); + }); + + it('should provide an imagery view when navigated to in the composition of a time strip', () => { + openmct.router.path = [imageryObject]; + + let applicableViews = openmct.objectViews.get(imageryObject, [ + imageryObject, + { + identifier: { + key: 'test-timestrip', + namespace: '' + }, + type: 'time-strip' + } + ]); + let imageryView = applicableViews.find((viewProvider) => viewProvider.key === imageryKey); + + expect(imageryView).toBeDefined(); + }); + + 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'); + + return Vue.nextTick(); + }); + + it('clear data action is installed', () => { + expect(clearDataAction).toBeDefined(); + }); + + it('on clearData action should clear data for object is selected', (done) => { + // force show the thumbnails + componentView.forceShowThumbnails = true; + Vue.nextTick(() => { + let clearDataResolve; + let telemetryRequestPromise = new Promise((resolve) => { + clearDataResolve = resolve; + }); + expect(parent.querySelectorAll('.c-imagery__thumb').length).not.toBe(0); + + openmct.objectViews.on('clearData', (_domainObject) => { + return Vue.nextTick(() => { + expect(parent.querySelectorAll('.c-imagery__thumb').length).toBe(0); + + clearDataResolve(); + }); + }); + clearDataAction.invoke(imageryObject); + + 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'); + expect(layerEls.length).toEqual(1); + }); + + it('should use the image thumbnailUrl for thumbnails', async () => { + await Vue.nextTick(); + const fullSizeImageUrl = imageTelemetry[5].url; + const thumbnailUrl = formatThumbnail(imageTelemetry[5].url); + + // Ensure thumbnails are shown w/ thumbnail Urls + const thumbnails = parent.querySelectorAll(`img[src='${thumbnailUrl}']`); + expect(thumbnails.length).toBeGreaterThan(0); + + // Click a thumbnail + parent.querySelectorAll(`img[src='${thumbnailUrl}']`)[0].click(); + await Vue.nextTick(); + + // Ensure full size image is shown w/ full size url + const fullSizeImages = parent.querySelectorAll(`img[src='${fullSizeImageUrl}']`); + expect(fullSizeImages.length).toBeGreaterThan(0); + }); + + 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 thumbnailUrl = formatThumbnail(imageTelemetry[5].url); + parent.querySelectorAll(`img[src='${thumbnailUrl}']`)[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', { + start: -1000, + end: 1000 + }); + + Vue.nextTick(() => { + // used in code, need to wait to the 500ms here too setTimeout(() => { - //Cleanup code that needs to happen before dom elements start being destroyed - cleanupFirst.forEach(cleanup => cleanup()); - cleanupFirst = []; - document.body.removeChild(parent); - - resetApplicationState(openmct).then(done).catch(done); - }); + const imageIsNew = isNew(parent); + expect(imageIsNew).toBeTrue(); + done(); + }, REFRESH_CSS_MS); + }); }); - it("should provide an imagery time strip view when in a time strip", () => { - openmct.router.path = [{ - identifier: { - key: 'test-timestrip', - namespace: '' - }, - type: 'time-strip' - }]; + it('should show that an image is not new', async () => { + await Vue.nextTick(); + const target = formatThumbnail(imageTelemetry[4].url); + parent.querySelectorAll(`img[src='${target}']`)[0].click(); - let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, { - identifier: { - key: 'test-timestrip', - namespace: '' - }, - type: 'time-strip' - }]); - let imageryView = applicableViews.find( - viewProvider => viewProvider.key === imageryForTimeStripKey - ); + await Vue.nextTick(); + const imageIsNew = isNew(parent); - expect(imageryView).toBeDefined(); + expect(imageIsNew).toBeFalse(); }); - it("should provide an imagery view only for imagery producing objects", () => { - let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]); - let imageryView = applicableViews.find( - viewProvider => viewProvider.key === imageryKey - ); + it('should navigate via arrow keys', async () => { + await Vue.nextTick(); + const keyOpts = { + element: parent.querySelector('.c-imagery'), + key: 'ArrowLeft', + keyCode: 37, + type: 'keyup' + }; - expect(imageryView).toBeDefined(); + simulateKeyEvent(keyOpts); + + await Vue.nextTick(); + const imageInfo = getImageInfo(parent); + expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1); }); - it("should not provide an imagery view when in a time strip", () => { - openmct.router.path = [{ - identifier: { - key: 'test-timestrip', - namespace: '' - }, - type: 'time-strip' - }]; + 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 + }; - let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, { - identifier: { - key: 'test-timestrip', - namespace: '' - }, - type: 'time-strip' - }]); - let imageryView = applicableViews.find( - viewProvider => viewProvider.key === imageryKey - ); + // left thrice + simulateKeyEvent(leftKeyOpts); + simulateKeyEvent(leftKeyOpts); + simulateKeyEvent(leftKeyOpts); + // right once + simulateKeyEvent(rightKeyOpts); - expect(imageryView).toBeUndefined(); + 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(() => { + // to mock what a scroll would do + imageryView._getInstance().$refs.ImageryContainer.autoScroll = false; + Vue.nextTick(() => { + let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button'); + expect(autoScrollButton).toBeTruthy(); + done(); + }); + }); + }); + it('scrollToRight is called when clicking on auto scroll button', async () => { + await Vue.nextTick(); + // use spyon to spy the scroll function + spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollHandler'); + imageryView._getInstance().$refs.ImageryContainer.autoScroll = false; + await Vue.nextTick(); + parent.querySelector('.c-imagery__auto-scroll-resume-button').click(); + expect(imageryView._getInstance().$refs.ImageryContainer.scrollHandler); + }); + xit('should change the image zoom factor when using the zoom buttons', async () => { + await Vue.nextTick(); + let imageSizeBefore; + let imageSizeAfter; + + // test clicking the zoom in button + imageSizeBefore = parent + .querySelector('.c-imagery_main-image_background-image') + .getBoundingClientRect(); + parent.querySelector('.t-btn-zoom-in').click(); + await Vue.nextTick(); + imageSizeAfter = parent + .querySelector('.c-imagery_main-image_background-image') + .getBoundingClientRect(); + expect(imageSizeAfter.height).toBeGreaterThan(imageSizeBefore.height); + expect(imageSizeAfter.width).toBeGreaterThan(imageSizeBefore.width); + // test clicking the zoom out button + imageSizeBefore = parent + .querySelector('.c-imagery_main-image_background-image') + .getBoundingClientRect(); + parent.querySelector('.t-btn-zoom-out').click(); + await Vue.nextTick(); + imageSizeAfter = parent + .querySelector('.c-imagery_main-image_background-image') + .getBoundingClientRect(); + expect(imageSizeAfter.height).toBeLessThan(imageSizeBefore.height); + expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width); + }); + xit('should reset the zoom factor on the image when clicking the zoom button', async (done) => { + await Vue.nextTick(); + // test clicking the zoom reset button + // zoom in to scale up the image dimensions + parent.querySelector('.t-btn-zoom-in').click(); + await Vue.nextTick(); + let imageSizeBefore = parent + .querySelector('.c-imagery_main-image_background-image') + .getBoundingClientRect(); + await Vue.nextTick(); + parent.querySelector('.t-btn-zoom-reset').click(); + let imageSizeAfter = parent + .querySelector('.c-imagery_main-image_background-image') + .getBoundingClientRect(); + expect(imageSizeAfter.height).toBeLessThan(imageSizeBefore.height); + expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width); + done(); }); - it("should provide an imagery view when navigated to in the composition of a time strip", () => { - openmct.router.path = [imageryObject]; + it('should display the viewable area when zoom factor is greater than 1', async () => { + await Vue.nextTick(); + expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(0); - let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, { - identifier: { - key: 'test-timestrip', - namespace: '' - }, - type: 'time-strip' - }]); - let imageryView = applicableViews.find( - viewProvider => viewProvider.key === imageryKey - ); + parent.querySelector('.t-btn-zoom-in').click(); + await Vue.nextTick(); + expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(1); - expect(imageryView).toBeDefined(); + parent.querySelector('.t-btn-zoom-reset').click(); + await Vue.nextTick(); + expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(0); }); - describe("Clear data action for imagery", () => { - let applicableViews; - let imageryViewProvider; - let imageryView; - let componentView; - let clearDataPlugin; - let clearDataAction; + it('should reset the brightness and contrast when clicking the reset button', async () => { + const viewInstance = imageryView._getInstance(); + await Vue.nextTick(); - beforeEach(() => { - openmct.time.timeSystem('utc', { - start: START - (5 * ONE_MINUTE), - end: START + (5 * ONE_MINUTE) - }); + // Save the original brightness and contrast values + const origBrightness = viewInstance.$refs.ImageryContainer.filters.brightness; + const origContrast = viewInstance.$refs.ImageryContainer.filters.contrast; - 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]; + // Change them to something else (default: 100) + viewInstance.$refs.ImageryContainer.setFilters({ + brightness: 200, + contrast: 200 + }); + await Vue.nextTick(); - clearDataPlugin = new ClearDataPlugin( - ['example.imagery'], - {indicator: true} - ); - openmct.install(clearDataPlugin); - clearDataAction = openmct.actions.getAction('clear-data-action'); + // Verify that the values actually changed + expect(viewInstance.$refs.ImageryContainer.filters.brightness).toBe(200); + expect(viewInstance.$refs.ImageryContainer.filters.contrast).toBe(200); - return Vue.nextTick(); - }); + // Click the reset button + parent.querySelector('.t-btn-reset').click(); + await Vue.nextTick(); - it('clear data action is installed', () => { - expect(clearDataAction).toBeDefined(); - }); + // Verify that the values were reset + expect(viewInstance.$refs.ImageryContainer.filters.brightness).toBe(origBrightness); + expect(viewInstance.$refs.ImageryContainer.filters.contrast).toBe(origContrast); + }); + }); - it('on clearData action should clear data for object is selected', (done) => { - // force show the thumbnails - componentView.forceShowThumbnails = true; - Vue.nextTick(() => { - let clearDataResolve; - let telemetryRequestPromise = new Promise((resolve) => { - clearDataResolve = resolve; - }); - expect(parent.querySelectorAll('.c-imagery__thumb').length).not.toBe(0); + describe('imagery time strip view', () => { + let applicableViews; + let imageryViewProvider; + let imageryView; + let componentView; - openmct.objectViews.on('clearData', (_domainObject) => { - return Vue.nextTick(() => { - expect(parent.querySelectorAll('.c-imagery__thumb').length).toBe(0); + beforeEach(() => { + openmct.time.timeSystem('utc', { + start: START - 5 * ONE_MINUTE, + end: START + 5 * ONE_MINUTE + }); - clearDataResolve(); - }); - }); - clearDataAction.invoke(imageryObject); + const mockClock = jasmine.createSpyObj('clock', ['on', 'off', 'currentValue']); + mockClock.key = 'mockClock'; + mockClock.currentValue.and.returnValue(1); - telemetryRequestPromise.then(() => { - done(); - }); - }); - }); + 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', + namespace: '' + }, + type: 'time-strip' + } + ]; + + applicableViews = openmct.objectViews.get(imageryObject, [ + imageryObject, + { + identifier: { + key: 'test-timestrip', + namespace: '' + }, + type: 'time-strip' + } + ]); + imageryViewProvider = applicableViews.find( + (viewProvider) => viewProvider.key === imageryForTimeStripKey + ); + imageryView = imageryViewProvider.view(imageryObject, [ + imageryObject, + { + identifier: { + key: 'test-timestrip', + namespace: '' + }, + type: 'time-strip' + } + ]); + imageryView.show(child); + + componentView = imageryView.getComponent().$children[0]; + spyOn(componentView.previewAction, 'invoke').and.callThrough(); + + return Vue.nextTick(); }); - 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'); - expect(layerEls.length).toEqual(1); - }); - - it("should use the image thumbnailUrl for thumbnails", async () => { - await Vue.nextTick(); - const fullSizeImageUrl = imageTelemetry[5].url; - const thumbnailUrl = formatThumbnail(imageTelemetry[5].url); - - // Ensure thumbnails are shown w/ thumbnail Urls - const thumbnails = parent.querySelectorAll(`img[src='${thumbnailUrl}']`); - expect(thumbnails.length).toBeGreaterThan(0); - - // Click a thumbnail - parent.querySelectorAll(`img[src='${thumbnailUrl}']`)[0].click(); - await Vue.nextTick(); - - // Ensure full size image is shown w/ full size url - const fullSizeImages = parent.querySelectorAll(`img[src='${fullSizeImageUrl}']`); - expect(fullSizeImages.length).toBeGreaterThan(0); - }); - - 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 thumbnailUrl = formatThumbnail(imageTelemetry[5].url); - parent.querySelectorAll(`img[src='${thumbnailUrl}']`)[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', { - start: -1000, - end: 1000 - }); - - Vue.nextTick(() => { - // used in code, need to wait to the 500ms here too - setTimeout(() => { - const imageIsNew = isNew(parent); - expect(imageIsNew).toBeTrue(); - done(); - }, REFRESH_CSS_MS); - }); - }); - - it("should show that an image is not new", async () => { - await Vue.nextTick(); - const target = formatThumbnail(imageTelemetry[4].url); - parent.querySelectorAll(`img[src='${target}']`)[0].click(); - - await Vue.nextTick(); - const imageIsNew = isNew(parent); - - expect(imageIsNew).toBeFalse(); - }); - - 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); - - 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", 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); - - 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(() => { - // to mock what a scroll would do - imageryView._getInstance().$refs.ImageryContainer.autoScroll = false; - Vue.nextTick(() => { - let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button'); - expect(autoScrollButton).toBeTruthy(); - done(); - }); - }); - }); - it ('scrollToRight is called when clicking on auto scroll button', async () => { - await Vue.nextTick(); - // use spyon to spy the scroll function - spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollHandler'); - imageryView._getInstance().$refs.ImageryContainer.autoScroll = false; - await Vue.nextTick(); - parent.querySelector('.c-imagery__auto-scroll-resume-button').click(); - expect(imageryView._getInstance().$refs.ImageryContainer.scrollHandler); - }); - xit('should change the image zoom factor when using the zoom buttons', async () => { - await Vue.nextTick(); - let imageSizeBefore; - let imageSizeAfter; - - // test clicking the zoom in button - imageSizeBefore = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect(); - parent.querySelector('.t-btn-zoom-in').click(); - await Vue.nextTick(); - imageSizeAfter = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect(); - expect(imageSizeAfter.height).toBeGreaterThan(imageSizeBefore.height); - expect(imageSizeAfter.width).toBeGreaterThan(imageSizeBefore.width); - // test clicking the zoom out button - imageSizeBefore = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect(); - parent.querySelector('.t-btn-zoom-out').click(); - await Vue.nextTick(); - imageSizeAfter = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect(); - expect(imageSizeAfter.height).toBeLessThan(imageSizeBefore.height); - expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width); - }); - xit('should reset the zoom factor on the image when clicking the zoom button', async (done) => { - await Vue.nextTick(); - // test clicking the zoom reset button - // zoom in to scale up the image dimensions - parent.querySelector('.t-btn-zoom-in').click(); - await Vue.nextTick(); - let imageSizeBefore = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect(); - await Vue.nextTick(); - parent.querySelector('.t-btn-zoom-reset').click(); - let imageSizeAfter = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect(); - expect(imageSizeAfter.height).toBeLessThan(imageSizeBefore.height); - expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width); - done(); - }); - - it('should display the viewable area when zoom factor is greater than 1', async () => { - await Vue.nextTick(); - expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(0); - - parent.querySelector('.t-btn-zoom-in').click(); - await Vue.nextTick(); - expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(1); - - parent.querySelector('.t-btn-zoom-reset').click(); - await Vue.nextTick(); - expect(parent.querySelectorAll('.c-thumb__viewable-area').length).toBe(0); - }); - - it('should reset the brightness and contrast when clicking the reset button', async () => { - const viewInstance = imageryView._getInstance(); - await Vue.nextTick(); - - // 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 - }); - 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); - }); + 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(5); + done(); + }); }); - describe("imagery time strip view", () => { - let applicableViews; - let imageryViewProvider; - let imageryView; - let componentView; - - beforeEach(() => { - openmct.time.timeSystem('utc', { - start: START - (5 * ONE_MINUTE), - 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', - namespace: '' - }, - type: 'time-strip' - }]; - - applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, { - identifier: { - key: 'test-timestrip', - namespace: '' - }, - type: 'time-strip' - }]); - imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryForTimeStripKey); - imageryView = imageryViewProvider.view(imageryObject, [imageryObject, { - identifier: { - key: 'test-timestrip', - namespace: '' - }, - type: 'time-strip' - }]); - imageryView.show(child); - - componentView = imageryView.getComponent().$children[0]; - spyOn(componentView.previewAction, 'invoke').and.callThrough(); - - return Vue.nextTick(); - }); - - 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(5); - done(); - }); - }); - - it("should show the clicked thumbnail as the preview image", (done) => { - Vue.nextTick(() => { - const mouseDownEvent = createMouseEvent("mousedown"); - let imageWrapper = parent.querySelectorAll(`.c-imagery-tsv__image-wrapper`); - imageWrapper[2].dispatchEvent(mouseDownEvent); - Vue.nextTick(() => { - const timestamp = imageWrapper[2].id.replace('wrapper-', ''); - expect(componentView.previewAction.invoke).toHaveBeenCalledWith([componentView.objectPath[0]], { - timestamp: Number(timestamp), - objectPath: componentView.objectPath - }); - done(); - }); - }); - }); - - 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); + it('should show the clicked thumbnail as the preview image', (done) => { + Vue.nextTick(() => { + const mouseDownEvent = createMouseEvent('mousedown'); + let imageWrapper = parent.querySelectorAll(`.c-imagery-tsv__image-wrapper`); + imageWrapper[2].dispatchEvent(mouseDownEvent); + Vue.nextTick(() => { + const timestamp = imageWrapper[2].id.replace('wrapper-', ''); + expect(componentView.previewAction.invoke).toHaveBeenCalledWith( + [componentView.objectPath[0]], + { + timestamp: Number(timestamp), + objectPath: componentView.objectPath + } + ); + done(); }); + }); }); + + 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 97ffbefac5..57452e0f9f 100644 --- a/src/plugins/importFromJSONAction/ImportFromJSONAction.js +++ b/src/plugins/importFromJSONAction/ImportFromJSONAction.js @@ -24,276 +24,278 @@ import objectUtils from 'objectUtils'; import { v4 as uuid } from 'uuid'; export default class ImportAsJSONAction { - constructor(openmct) { - this.name = 'Import from JSON'; - this.key = 'import.JSON'; - this.description = ''; - this.cssClass = "icon-import"; - this.group = "import"; - this.priority = 2; - this.newObjects = []; + constructor(openmct) { + this.name = 'Import from JSON'; + this.key = 'import.JSON'; + this.description = ''; + this.cssClass = 'icon-import'; + this.group = 'import'; + this.priority = 2; + this.newObjects = []; - this.openmct = openmct; + this.openmct = openmct; + } + + // Public + /** + * + * @param {object} objectPath + * @returns {boolean} + */ + appliesTo(objectPath) { + const domainObject = objectPath[0]; + const locked = domainObject && domainObject.locked; + const persistable = this.openmct.objects.isPersistable(domainObject.identifier); + const TypeDefinition = this.openmct.types.get(domainObject.type); + const definition = TypeDefinition.definition; + const creatable = definition && definition.creatable; + + if (locked || !persistable || !creatable) { + return false; } - // Public - /** - * - * @param {object} objectPath - * @returns {boolean} - */ - appliesTo(objectPath) { - const domainObject = objectPath[0]; - const locked = domainObject && domainObject.locked; - const persistable = this.openmct.objects.isPersistable(domainObject.identifier); - const TypeDefinition = this.openmct.types.get(domainObject.type); - const definition = TypeDefinition.definition; - const creatable = definition && definition.creatable; + return domainObject !== undefined && this.openmct.composition.get(domainObject); + } + /** + * + * @param {object} objectPath + */ + invoke(objectPath) { + this._showForm(objectPath[0]); + } + /** + * + * @param {object} object + * @param {object} changes + */ - if (locked || !persistable || !creatable) { - return false; + onSave(object, changes) { + const selectFile = changes.selectFile; + const objectTree = selectFile.body; + this._importObjectTree(object, JSON.parse(objectTree)); + } + + /** + * @private + * @param {object} parent + * @param {object} tree + * @param {object} seen + */ + _deepInstantiate(parent, tree, seen) { + let objectIdentifiers = this._getObjectReferenceIds(parent); + + if (objectIdentifiers.length) { + const parentId = this.openmct.objects.makeKeyString(parent.identifier); + seen.push(parentId); + + for (const childId of objectIdentifiers) { + const keystring = this.openmct.objects.makeKeyString(childId); + if (!tree[keystring] || seen.includes(keystring)) { + continue; } - return domainObject !== undefined - && this.openmct.composition.get(domainObject); - } - /** - * - * @param {object} objectPath - */ - invoke(objectPath) { - this._showForm(objectPath[0]); - } - /** - * - * @param {object} object - * @param {object} changes - */ + const newModel = tree[keystring]; + delete newModel.persisted; - onSave(object, changes) { - const selectFile = changes.selectFile; - const objectTree = selectFile.body; - this._importObjectTree(object, JSON.parse(objectTree)); + this.newObjects.push(newModel); + + // make sure there weren't any errors saving + if (newModel) { + this._deepInstantiate(newModel, tree, seen); + } + } + } + } + /** + * @private + * @param {object} parent + * @returns [identifiers] + */ + _getObjectReferenceIds(parent) { + let objectIdentifiers = []; + let itemObjectReferences = []; + const objectStyles = parent?.configuration?.objectStyles; + const parentComposition = this.openmct.composition.get(parent); + + if (parentComposition) { + objectIdentifiers = Array.from(parent.composition); } - /** - * @private - * @param {object} parent - * @param {object} tree - * @param {object} seen - */ - _deepInstantiate(parent, tree, seen) { - let objectIdentifiers = this._getObjectReferenceIds(parent); + //conditional object styles are not saved on the composition, so we need to check for them + if (objectStyles) { + const parentObjectReference = objectStyles.conditionSetIdentifier; - if (objectIdentifiers.length) { - const parentId = this.openmct.objects.makeKeyString(parent.identifier); - seen.push(parentId); + if (parentObjectReference) { + objectIdentifiers.push(parentObjectReference); + } - for (const childId of objectIdentifiers) { - const keystring = this.openmct.objects.makeKeyString(childId); - if (!tree[keystring] || seen.includes(keystring)) { - continue; - } + function hasConditionSetIdentifier(item) { + return Boolean(item.conditionSetIdentifier); + } - const newModel = tree[keystring]; - delete newModel.persisted; + itemObjectReferences = Object.values(objectStyles) + .filter(hasConditionSetIdentifier) + .map((item) => item.conditionSetIdentifier); + } - this.newObjects.push(newModel); + return Array.from(new Set([...objectIdentifiers, ...itemObjectReferences])); + } + /** + * @private + * @param {object} tree + * @param {string} namespace + * @returns {object} + */ + _generateNewIdentifiers(tree, namespace) { + // For each domain object in the file, generate new ID, replace in tree + Object.keys(tree.openmct).forEach((domainObjectId) => { + const newId = { + namespace, + key: uuid() + }; - // make sure there weren't any errors saving - if (newModel) { - this._deepInstantiate(newModel, tree, seen); - } + const oldId = objectUtils.parseKeyString(domainObjectId); + + tree = this._rewriteId(oldId, newId, tree); + }, this); + + return tree; + } + /** + * @private + * @param {object} domainObject + * @param {object} objTree + */ + async _importObjectTree(domainObject, objTree) { + const namespace = domainObject.identifier.namespace; + const tree = this._generateNewIdentifiers(objTree, namespace); + const rootId = tree.rootId; + + const rootObj = tree.openmct[rootId]; + delete rootObj.persisted; + this.newObjects.push(rootObj); + + if (this.openmct.composition.checkPolicy(domainObject, rootObj)) { + this._deepInstantiate(rootObj, tree.openmct, []); + + try { + await Promise.all(this.newObjects.map(this._instantiate, this)); + } catch (error) { + this.openmct.notifications.error('Error saving objects'); + + throw error; + } + + const compositionCollection = this.openmct.composition.get(domainObject); + let domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier); + this.openmct.objects.mutate(rootObj, 'location', domainObjectKeyString); + compositionCollection.add(rootObj); + } else { + const dialog = this.openmct.overlays.dialog({ + iconClass: 'alert', + message: "We're sorry, but you cannot import that object type into this object.", + buttons: [ + { + label: 'Ok', + emphasis: true, + callback: function () { + dialog.dismiss(); } - } + } + ] + }); } - /** - * @private - * @param {object} parent - * @returns [identifiers] - */ - _getObjectReferenceIds(parent) { - let objectIdentifiers = []; - let itemObjectReferences = []; - const objectStyles = parent?.configuration?.objectStyles; - const parentComposition = this.openmct.composition.get(parent); + } + /** + * @private + * @param {object} model + * @returns {object} + */ + _instantiate(model) { + return this.openmct.objects.save(model); + } + /** + * @private + * @param {object} oldId + * @param {object} newId + * @param {object} tree + * @returns {object} + */ + _rewriteId(oldId, newId, tree) { + let newIdKeyString = this.openmct.objects.makeKeyString(newId); + let oldIdKeyString = this.openmct.objects.makeKeyString(oldId); + tree = JSON.stringify(tree).replace(new RegExp(oldIdKeyString, 'g'), newIdKeyString); - if (parentComposition) { - objectIdentifiers = Array.from(parent.composition); - } - - //conditional object styles are not saved on the composition, so we need to check for them - if (objectStyles) { - const parentObjectReference = objectStyles.conditionSetIdentifier; - - if (parentObjectReference) { - objectIdentifiers.push(parentObjectReference); + return JSON.parse(tree, (key, value) => { + if ( + value !== undefined && + value !== null && + Object.prototype.hasOwnProperty.call(value, 'key') && + Object.prototype.hasOwnProperty.call(value, 'namespace') && + value.key === oldId.key && + value.namespace === oldId.namespace + ) { + return newId; + } else { + return value; + } + }); + } + /** + * @private + * @param {object} domainObject + */ + _showForm(domainObject) { + const formStructure = { + title: this.name, + sections: [ + { + rows: [ + { + name: 'Select File', + key: 'selectFile', + control: 'file-input', + required: true, + text: 'Select File...', + validate: this._validateJSON, + type: 'application/json' } - - function hasConditionSetIdentifier(item) { - return Boolean(item.conditionSetIdentifier); - } - - itemObjectReferences = Object.values(objectStyles) - .filter(hasConditionSetIdentifier) - .map(item => item.conditionSetIdentifier); + ] } + ] + }; - return Array.from(new Set([...objectIdentifiers, ...itemObjectReferences])); + this.openmct.forms.showForm(formStructure).then((changes) => { + let onSave = this.onSave.bind(this); + onSave(domainObject, changes); + }); + } + /** + * @private + * @param {object} data + * @returns {boolean} + */ + _validateJSON(data) { + const value = data.value; + const objectTree = value && value.body; + let json; + let success = true; + try { + json = JSON.parse(objectTree); + } catch (e) { + success = false; } - /** - * @private - * @param {object} tree - * @param {string} namespace - * @returns {object} - */ - _generateNewIdentifiers(tree, namespace) { - // For each domain object in the file, generate new ID, replace in tree - Object.keys(tree.openmct).forEach(domainObjectId => { - const newId = { - namespace, - key: uuid() - }; - const oldId = objectUtils.parseKeyString(domainObjectId); - - tree = this._rewriteId(oldId, newId, tree); - }, this); - - return tree; + if (success && (!json.openmct || !json.rootId)) { + success = false; } - /** - * @private - * @param {object} domainObject - * @param {object} objTree - */ - async _importObjectTree(domainObject, objTree) { - const namespace = domainObject.identifier.namespace; - const tree = this._generateNewIdentifiers(objTree, namespace); - const rootId = tree.rootId; - const rootObj = tree.openmct[rootId]; - delete rootObj.persisted; - this.newObjects.push(rootObj); - - if (this.openmct.composition.checkPolicy(domainObject, rootObj)) { - this._deepInstantiate(rootObj, tree.openmct, []); - - try { - await Promise.all(this.newObjects.map(this._instantiate, this)); - } catch (error) { - this.openmct.notifications.error('Error saving objects'); - - throw error; - } - - const compositionCollection = this.openmct.composition.get(domainObject); - let domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier); - this.openmct.objects.mutate(rootObj, 'location', domainObjectKeyString); - compositionCollection.add(rootObj); - } else { - const dialog = this.openmct.overlays.dialog({ - iconClass: 'alert', - message: "We're sorry, but you cannot import that object type into this object.", - buttons: [ - { - label: "Ok", - emphasis: true, - callback: function () { - dialog.dismiss(); - } - } - ] - }); - } + if (!success) { + this.openmct.notifications.error( + 'Invalid File: The selected file was either invalid JSON or was not formatted properly for import into Open MCT.' + ); } - /** - * @private - * @param {object} model - * @returns {object} - */ - _instantiate(model) { - return this.openmct.objects.save(model); - } - /** - * @private - * @param {object} oldId - * @param {object} newId - * @param {object} tree - * @returns {object} - */ - _rewriteId(oldId, newId, tree) { - let newIdKeyString = this.openmct.objects.makeKeyString(newId); - let oldIdKeyString = this.openmct.objects.makeKeyString(oldId); - tree = JSON.stringify(tree).replace(new RegExp(oldIdKeyString, 'g'), newIdKeyString); - return JSON.parse(tree, (key, value) => { - if (value !== undefined - && value !== null - && Object.prototype.hasOwnProperty.call(value, 'key') - && Object.prototype.hasOwnProperty.call(value, 'namespace') - && value.key === oldId.key - && value.namespace === oldId.namespace) { - return newId; - } else { - return value; - } - }); - } - /** - * @private - * @param {object} domainObject - */ - _showForm(domainObject) { - const formStructure = { - title: this.name, - sections: [ - { - rows: [ - { - name: 'Select File', - key: 'selectFile', - control: 'file-input', - required: true, - text: 'Select File...', - validate: this._validateJSON, - type: 'application/json' - } - ] - } - ] - }; - - this.openmct.forms.showForm(formStructure) - .then(changes => { - let onSave = this.onSave.bind(this); - onSave(domainObject, changes); - }); - } - /** - * @private - * @param {object} data - * @returns {boolean} - */ - _validateJSON(data) { - const value = data.value; - const objectTree = value && value.body; - let json; - let success = true; - try { - json = JSON.parse(objectTree); - } catch (e) { - success = false; - } - - if (success && (!json.openmct || !json.rootId)) { - success = false; - } - - if (!success) { - this.openmct.notifications.error('Invalid File: The selected file was either invalid JSON or was not formatted properly for import into Open MCT.'); - } - - return success; - } + return success; + } } diff --git a/src/plugins/importFromJSONAction/ImportFromJSONActionSpec.js b/src/plugins/importFromJSONAction/ImportFromJSONActionSpec.js index 84f34ba467..e743d471bf 100644 --- a/src/plugins/importFromJSONAction/ImportFromJSONActionSpec.js +++ b/src/plugins/importFromJSONAction/ImportFromJSONActionSpec.js @@ -22,110 +22,101 @@ import ImportFromJSONAction from './ImportFromJSONAction'; -import { - createOpenMct, - resetApplicationState -} from 'utils/testing'; +import { createOpenMct, resetApplicationState } from 'utils/testing'; let openmct; let importFromJSONAction; -describe("The import JSON action", function () { - beforeEach((done) => { - openmct = createOpenMct(); +describe('The import JSON action', function () { + beforeEach((done) => { + openmct = createOpenMct(); - openmct.on('start', done); - openmct.startHeadless(); + openmct.on('start', done); + openmct.startHeadless(); - importFromJSONAction = new ImportFromJSONAction(openmct); - }); + importFromJSONAction = new ImportFromJSONAction(openmct); + }); - afterEach(() => { - return resetApplicationState(openmct); - }); + afterEach(() => { + return resetApplicationState(openmct); + }); - it('has import as JSON action', () => { - expect(importFromJSONAction.key).toBe('import.JSON'); - }); + it('has import as JSON action', () => { + expect(importFromJSONAction.key).toBe('import.JSON'); + }); - it('applies to return true for objects with composition', function () { - const domainObject = { - composition: [], - name: 'Unnamed Folder', - type: 'folder', - location: '9f6c9dae-51c3-401d-92f1-c812de942922', - modified: 1637021471624, - persisted: 1637021471624, - id: '84438cda-a071-48d1-b9bf-d77bd53e59ba', - identifier: { - namespace: '', - key: '84438cda-a071-48d1-b9bf-d77bd53e59ba' - } - }; + it('applies to return true for objects with composition', function () { + const domainObject = { + composition: [], + name: 'Unnamed Folder', + type: 'folder', + location: '9f6c9dae-51c3-401d-92f1-c812de942922', + modified: 1637021471624, + persisted: 1637021471624, + id: '84438cda-a071-48d1-b9bf-d77bd53e59ba', + identifier: { + namespace: '', + key: '84438cda-a071-48d1-b9bf-d77bd53e59ba' + } + }; - const objectPath = [ - domainObject - ]; + const objectPath = [domainObject]; - spyOn(openmct.composition, 'get').and.returnValue(true); + spyOn(openmct.composition, 'get').and.returnValue(true); - expect(importFromJSONAction.appliesTo(objectPath)).toBe(true); - }); + expect(importFromJSONAction.appliesTo(objectPath)).toBe(true); + }); - it('applies to return false for objects without composition', function () { - const domainObject = { - telemetry: { - period: 10, - amplitude: 1, - offset: 0, - dataRateInHz: 1, - phase: 0, - randomness: 0 - }, - name: 'Unnamed Sine Wave Generator', - type: 'generator', - location: '84438cda-a071-48d1-b9bf-d77bd53e59ba', - modified: 1637021471172, - identifier: { - namespace: '', - key: 'c102b6e1-3c81-4618-926a-56cc310925f6' - }, - persisted: 1637021471172 - }; + it('applies to return false for objects without composition', function () { + const domainObject = { + telemetry: { + period: 10, + amplitude: 1, + offset: 0, + dataRateInHz: 1, + phase: 0, + randomness: 0 + }, + name: 'Unnamed Sine Wave Generator', + type: 'generator', + location: '84438cda-a071-48d1-b9bf-d77bd53e59ba', + modified: 1637021471172, + identifier: { + namespace: '', + key: 'c102b6e1-3c81-4618-926a-56cc310925f6' + }, + persisted: 1637021471172 + }; - const objectPath = [ - domainObject - ]; + const objectPath = [domainObject]; - spyOn(openmct.types, 'get').and.returnValue({}); - spyOn(openmct.composition, 'get').and.returnValue(false); + spyOn(openmct.types, 'get').and.returnValue({}); + spyOn(openmct.composition, 'get').and.returnValue(false); - expect(importFromJSONAction.appliesTo(objectPath)).toBe(false); - }); + expect(importFromJSONAction.appliesTo(objectPath)).toBe(false); + }); - it('calls showForm on invoke ', function () { - const domainObject = { - composition: [], - name: 'Unnamed Folder', - type: 'folder', - location: '9f6c9dae-51c3-401d-92f1-c812de942922', - modified: 1637021471624, - persisted: 1637021471624, - id: '84438cda-a071-48d1-b9bf-d77bd53e59ba', - identifier: { - namespace: '', - key: '84438cda-a071-48d1-b9bf-d77bd53e59ba' - } - }; + it('calls showForm on invoke ', function () { + const domainObject = { + composition: [], + name: 'Unnamed Folder', + type: 'folder', + location: '9f6c9dae-51c3-401d-92f1-c812de942922', + modified: 1637021471624, + persisted: 1637021471624, + id: '84438cda-a071-48d1-b9bf-d77bd53e59ba', + identifier: { + namespace: '', + key: '84438cda-a071-48d1-b9bf-d77bd53e59ba' + } + }; - const objectPath = [ - domainObject - ]; + const objectPath = [domainObject]; - spyOn(openmct.forms, 'showForm').and.returnValue(Promise.resolve({})); - spyOn(importFromJSONAction, 'onSave').and.returnValue(Promise.resolve({})); - importFromJSONAction.invoke(objectPath); + spyOn(openmct.forms, 'showForm').and.returnValue(Promise.resolve({})); + spyOn(importFromJSONAction, 'onSave').and.returnValue(Promise.resolve({})); + importFromJSONAction.invoke(objectPath); - expect(openmct.forms.showForm).toHaveBeenCalled(); - }); + expect(openmct.forms.showForm).toHaveBeenCalled(); + }); }); diff --git a/src/plugins/importFromJSONAction/plugin.js b/src/plugins/importFromJSONAction/plugin.js index a14ae89a98..c8d6b8b3a6 100644 --- a/src/plugins/importFromJSONAction/plugin.js +++ b/src/plugins/importFromJSONAction/plugin.js @@ -22,7 +22,7 @@ import ImportFromJSONAction from './ImportFromJSONAction'; export default function () { - return function (openmct) { - openmct.actions.register(new ImportFromJSONAction(openmct)); - }; + return function (openmct) { + openmct.actions.register(new ImportFromJSONAction(openmct)); + }; } diff --git a/src/plugins/inspectorViews/annotations/AnnotationsInspectorView.vue b/src/plugins/inspectorViews/annotations/AnnotationsInspectorView.vue index dbc04f86be..0726fa6b7b 100644 --- a/src/plugins/inspectorViews/annotations/AnnotationsInspectorView.vue +++ b/src/plugins/inspectorViews/annotations/AnnotationsInspectorView.vue @@ -21,35 +21,22 @@ --> diff --git a/src/plugins/inspectorViews/annotations/AnnotationsViewProvider.js b/src/plugins/inspectorViews/annotations/AnnotationsViewProvider.js index 8595034aa8..5d618c49df 100644 --- a/src/plugins/inspectorViews/annotations/AnnotationsViewProvider.js +++ b/src/plugins/inspectorViews/annotations/AnnotationsViewProvider.js @@ -24,45 +24,45 @@ import Annotations from './AnnotationsInspectorView.vue'; import Vue from 'vue'; export default function AnnotationsViewProvider(openmct) { - return { - key: 'annotationsView', - name: 'Annotations', - canView: function (selection) { - const availableTags = openmct.annotation.getAvailableTags(); + return { + key: 'annotationsView', + name: 'Annotations', + canView: function (selection) { + const availableTags = openmct.annotation.getAvailableTags(); - if (availableTags.length < 1) { - return false; - } + if (availableTags.length < 1) { + return false; + } - return selection.length; + return selection.length; + }, + view: function (selection) { + let component; + + const domainObject = selection?.[0]?.[0]?.context?.item; + + return { + show: function (el) { + component = new Vue({ + el, + components: { + Annotations + }, + provide: { + openmct, + domainObject + }, + template: `` + }); }, - view: function (selection) { - let component; - - const domainObject = selection?.[0]?.[0]?.context?.item; - - return { - show: function (el) { - component = new Vue({ - el, - components: { - Annotations - }, - provide: { - openmct, - domainObject - }, - template: `` - }); - }, - priority: function () { - return openmct.priority.DEFAULT; - }, - destroy: function () { - component.$destroy(); - component = undefined; - } - }; + priority: function () { + return openmct.priority.DEFAULT; + }, + destroy: function () { + component.$destroy(); + component = undefined; } - }; + }; + } + }; } diff --git a/src/plugins/inspectorViews/annotations/tags/TagEditor.vue b/src/plugins/inspectorViews/annotations/tags/TagEditor.vue index ae9dfa309f..3175441362 100644 --- a/src/plugins/inspectorViews/annotations/tags/TagEditor.vue +++ b/src/plugins/inspectorViews/annotations/tags/TagEditor.vue @@ -21,199 +21,205 @@ --> diff --git a/src/plugins/inspectorViews/annotations/tags/TagSelection.vue b/src/plugins/inspectorViews/annotations/tags/TagSelection.vue index c1e256fe55..692e2357ba 100644 --- a/src/plugins/inspectorViews/annotations/tags/TagSelection.vue +++ b/src/plugins/inspectorViews/annotations/tags/TagSelection.vue @@ -21,146 +21,143 @@ --> diff --git a/src/plugins/inspectorViews/annotations/tags/tags.scss b/src/plugins/inspectorViews/annotations/tags/tags.scss index 01dc73fa82..1310bd6302 100644 --- a/src/plugins/inspectorViews/annotations/tags/tags.scss +++ b/src/plugins/inspectorViews/annotations/tags/tags.scss @@ -10,10 +10,9 @@ } } - /******************************* TAGS */ .c-tag { -/* merge conflict in 5247 + /* merge conflict in 5247 border-radius: 10px; //TODO: convert to theme constant display: inline-flex; padding: 1px 10px; //TODO: convert to theme constant @@ -46,15 +45,15 @@ transition: $transIn; width: 0; - &:hover { - opacity: 1; - } + &:hover { + opacity: 1; } + } - /* SEARCH RESULTS */ - &.--is-not-search-match { - opacity: 0.5; - } + /* SEARCH RESULTS */ + &.--is-not-search-match { + opacity: 0.5; + } } .c-tag-holder { @@ -62,14 +61,14 @@ } .w-tag-wrapper { - $m: $interiorMarginSm; + $m: $interiorMarginSm; - margin: 0 $m $m 0; + margin: 0 $m $m 0; } /******************************* TAGS IN INSPECTOR / TAG SELECTION & APPLICATION */ .c-tag-applier { -/* merge conflict in fix-repaint-5247 + /* merge conflict in fix-repaint-5247 display: flex; flex-direction: row; flex-wrap: wrap; @@ -108,7 +107,9 @@ border-radius: $tagBorderRadius; padding: 3px 10px 3px 4px; - &:before { font-size: 0.9em; } + &:before { + font-size: 0.9em; + } } .c-tag { @@ -116,7 +117,9 @@ align-items: center; padding: $tagApplierPadding; - > * + * { margin-left: $interiorMarginSm; } + > * + * { + margin-left: $interiorMarginSm; + } } .c-tag-selection { @@ -124,15 +127,15 @@ min-height: auto !important; padding: $tagApplierPadding; } -} + } -.c-tag-btn__label { - overflow: visible!important; -} + .c-tag-btn__label { + overflow: visible !important; + } -/******************************* HOVERS */ -.has-tag-applier { -/* merge conflict in fix-repaint-5247 + /******************************* HOVERS */ + .has-tag-applier { + /* merge conflict in fix-repaint-5247 $p: opacity, width; // Apply this class to all components that should trigger tag removal btn on hover .c-tag__remove-btn { @@ -147,34 +150,34 @@ } } */ - // Apply this class to all components that should trigger tag removal btn on hover - &:hover { - .c-tag { - @include userSelectNone(); - transition: $transOut; - } - .c-tag__label { + // Apply this class to all components that should trigger tag removal btn on hover + &:hover { + .c-tag { + @include userSelectNone(); + transition: $transOut; + } + .c-tag__label { opacity: 0.7; + } + .c-tag__remove-btn { + width: 1.3em; + opacity: 0.8; + padding: 2px !important; + transition: $transOut; + right: 5%; + text-align: center; + z-index: 2; + + &:hover { + opacity: 1; + + & ~ * { + // This sibling selector further dims the label + // to make the remove button stand out + opacity: 0.4; + } + } + } } - .c-tag__remove-btn { - width: 1.3em; - opacity: 0.8; - padding: 2px !important; - transition: $transOut; - right: 5%; - text-align: center; - z-index: 2; - - &:hover { - opacity: 1; - - & ~ * { - // This sibling selector further dims the label - // to make the remove button stand out - opacity: 0.4 - } - } - } - } - } -} \ No newline at end of file + } +} diff --git a/src/plugins/inspectorViews/elements/ElementItem.vue b/src/plugins/inspectorViews/elements/ElementItem.vue index e92064569d..3902573cd6 100644 --- a/src/plugins/inspectorViews/elements/ElementItem.vue +++ b/src/plugins/inspectorViews/elements/ElementItem.vue @@ -21,96 +21,93 @@ --> diff --git a/src/plugins/inspectorViews/elements/ElementItemGroup.vue b/src/plugins/inspectorViews/elements/ElementItemGroup.vue index 556b1cf11e..a79ed5b812 100644 --- a/src/plugins/inspectorViews/elements/ElementItemGroup.vue +++ b/src/plugins/inspectorViews/elements/ElementItemGroup.vue @@ -21,81 +21,77 @@ --> diff --git a/src/plugins/inspectorViews/elements/ElementsPool.vue b/src/plugins/inspectorViews/elements/ElementsPool.vue index 7d118889d7..1c4a84addc 100644 --- a/src/plugins/inspectorViews/elements/ElementsPool.vue +++ b/src/plugins/inspectorViews/elements/ElementsPool.vue @@ -21,40 +21,33 @@ --> diff --git a/src/plugins/inspectorViews/elements/ElementsViewProvider.js b/src/plugins/inspectorViews/elements/ElementsViewProvider.js index 6e39c575d1..c93fa13314 100644 --- a/src/plugins/inspectorViews/elements/ElementsViewProvider.js +++ b/src/plugins/inspectorViews/elements/ElementsViewProvider.js @@ -24,47 +24,47 @@ import ElementsPool from './ElementsPool.vue'; import Vue from 'vue'; export default function ElementsViewProvider(openmct) { - return { - key: 'elementsView', - name: 'Elements', - canView: function (selection) { - const hasValidSelection = selection?.length; - const isOverlayPlot = selection?.[0]?.[0]?.context?.item?.type === 'telemetry.plot.overlay'; + return { + key: 'elementsView', + name: 'Elements', + canView: function (selection) { + const hasValidSelection = selection?.length; + const isOverlayPlot = selection?.[0]?.[0]?.context?.item?.type === 'telemetry.plot.overlay'; - return hasValidSelection && !isOverlayPlot; + return hasValidSelection && !isOverlayPlot; + }, + view: function (selection) { + let component; + + const domainObject = selection?.[0]?.[0]?.context?.item; + + return { + show: function (el) { + component = new Vue({ + el, + components: { + ElementsPool + }, + provide: { + openmct, + domainObject + }, + template: `` + }); }, - view: function (selection) { - let component; + showTab: function (isEditing) { + const hasComposition = Boolean(domainObject && openmct.composition.get(domainObject)); - const domainObject = selection?.[0]?.[0]?.context?.item; - - return { - show: function (el) { - component = new Vue({ - el, - components: { - ElementsPool - }, - provide: { - openmct, - domainObject - }, - template: `` - }); - }, - showTab: function (isEditing) { - const hasComposition = Boolean(domainObject && openmct.composition.get(domainObject)); - - return hasComposition && isEditing; - }, - priority: function () { - return openmct.priority.DEFAULT; - }, - destroy: function () { - component.$destroy(); - component = undefined; - } - }; + return hasComposition && isEditing; + }, + priority: function () { + return openmct.priority.DEFAULT; + }, + destroy: function () { + component.$destroy(); + component = undefined; } - }; + }; + } + }; } diff --git a/src/plugins/inspectorViews/elements/PlotElementsPool.vue b/src/plugins/inspectorViews/elements/PlotElementsPool.vue index 5d6afa79de..a7f1f8dd25 100644 --- a/src/plugins/inspectorViews/elements/PlotElementsPool.vue +++ b/src/plugins/inspectorViews/elements/PlotElementsPool.vue @@ -21,58 +21,51 @@ --> diff --git a/src/plugins/inspectorViews/elements/PlotElementsViewProvider.js b/src/plugins/inspectorViews/elements/PlotElementsViewProvider.js index 4174029e42..9827d21f53 100644 --- a/src/plugins/inspectorViews/elements/PlotElementsViewProvider.js +++ b/src/plugins/inspectorViews/elements/PlotElementsViewProvider.js @@ -24,44 +24,44 @@ import PlotElementsPool from './PlotElementsPool.vue'; import Vue from 'vue'; export default function PlotElementsViewProvider(openmct) { - return { - key: 'plotElementsView', - name: 'Elements', - canView: function (selection) { - return selection?.[0]?.[0]?.context?.item?.type === 'telemetry.plot.overlay'; + return { + key: 'plotElementsView', + name: 'Elements', + canView: function (selection) { + return selection?.[0]?.[0]?.context?.item?.type === 'telemetry.plot.overlay'; + }, + view: function (selection) { + let component; + + const domainObject = selection?.[0]?.[0]?.context?.item; + + return { + show: function (el) { + component = new Vue({ + el, + components: { + PlotElementsPool + }, + provide: { + openmct, + domainObject + }, + template: `` + }); }, - view: function (selection) { - let component; + showTab: function (isEditing) { + const hasComposition = Boolean(domainObject && openmct.composition.get(domainObject)); - const domainObject = selection?.[0]?.[0]?.context?.item; - - return { - show: function (el) { - component = new Vue({ - el, - components: { - PlotElementsPool - }, - provide: { - openmct, - domainObject - }, - template: `` - }); - }, - showTab: function (isEditing) { - const hasComposition = Boolean(domainObject && openmct.composition.get(domainObject)); - - return hasComposition && isEditing; - }, - priority: function () { - return openmct.priority.DEFAULT; - }, - destroy: function () { - component.$destroy(); - component = undefined; - } - }; + return hasComposition && isEditing; + }, + priority: function () { + return openmct.priority.DEFAULT; + }, + destroy: function () { + component.$destroy(); + component = undefined; } - }; + }; + } + }; } diff --git a/src/plugins/inspectorViews/elements/elements.scss b/src/plugins/inspectorViews/elements/elements.scss index 12995718bf..72f3c5b933 100644 --- a/src/plugins/inspectorViews/elements/elements.scss +++ b/src/plugins/inspectorViews/elements/elements.scss @@ -1,74 +1,76 @@ .c-elements-pool { - display: flex; - flex-direction: column; - overflow: hidden; - flex: 1 1 auto !important; - > * + * { - margin-top: $interiorMargin; - } - - &.is-object-type-telemetry-plot-overlay { - .c-grippy { - display: none; - } - .c-object-label{ - &:before { - // Grippy - content: ''; - @include grippy($colorItemTreeVC, $dir: 'Y'); - $d: 9px; - width: $d; height: $d; - display: block; - margin-right: $interiorMargin; - } - } - } - - &__item { - &.is-alias { - // Object is an alias to an original. - [class*='__type-icon'] { - @include isAlias(); - } - } - } - - &__search { - flex: 0 0 auto; - } - - &__group { - flex: 1 1 auto; - margin-top: $interiorMarginLg; - } - - &__elements { - flex: 1 1 auto; - overflow: auto; - } - - &__instructions { - display: flex; - font-style: italic; - } + display: flex; + flex-direction: column; + overflow: hidden; + flex: 1 1 auto !important; + > * + * { + margin-top: $interiorMargin; + } + &.is-object-type-telemetry-plot-overlay { .c-grippy { + display: none; + } + .c-object-label { + &:before { + // Grippy + content: ''; + @include grippy($colorItemTreeVC, $dir: 'Y'); $d: 9px; - flex: 0 0 auto; - margin-right: $interiorMarginSm; - transform: translateY(-2px); - width: $d; height: $d; + width: $d; + height: $d; + display: block; + margin-right: $interiorMargin; + } } + } - &.is-context-clicked { - box-shadow: inset $colorItemTreeSelectedBg 0 0 0 1px; + &__item { + &.is-alias { + // Object is an alias to an original. + [class*='__type-icon'] { + @include isAlias(); + } } + } - .hover { - background-color: $colorItemTreeSelectedBg; - } + &__search { + flex: 0 0 auto; + } + + &__group { + flex: 1 1 auto; + margin-top: $interiorMarginLg; + } + + &__elements { + flex: 1 1 auto; + overflow: auto; + } + + &__instructions { + display: flex; + font-style: italic; + } + + .c-grippy { + $d: 9px; + flex: 0 0 auto; + margin-right: $interiorMarginSm; + transform: translateY(-2px); + width: $d; + height: $d; + } + + &.is-context-clicked { + box-shadow: inset $colorItemTreeSelectedBg 0 0 0 1px; + } + + .hover { + background-color: $colorItemTreeSelectedBg; + } } .js-last-place { - height: 10px; + height: 10px; } diff --git a/src/plugins/inspectorViews/plugin.js b/src/plugins/inspectorViews/plugin.js index 603bd70afd..cf4b53647f 100644 --- a/src/plugins/inspectorViews/plugin.js +++ b/src/plugins/inspectorViews/plugin.js @@ -27,11 +27,11 @@ import StylesInspectorViewProvider from './styles/StylesInspectorViewProvider'; import AnnotationsViewProvider from './annotations/AnnotationsViewProvider'; export default function InspectorViewsPlugin() { - return function install(openmct) { - openmct.inspectorViews.addProvider(new PropertiesViewProvider(openmct)); - openmct.inspectorViews.addProvider(new ElementsViewProvider(openmct)); - openmct.inspectorViews.addProvider(new PlotElementsViewProvider(openmct)); - openmct.inspectorViews.addProvider(new StylesInspectorViewProvider(openmct)); - openmct.inspectorViews.addProvider(new AnnotationsViewProvider(openmct)); - }; + return function install(openmct) { + openmct.inspectorViews.addProvider(new PropertiesViewProvider(openmct)); + openmct.inspectorViews.addProvider(new ElementsViewProvider(openmct)); + openmct.inspectorViews.addProvider(new PlotElementsViewProvider(openmct)); + openmct.inspectorViews.addProvider(new StylesInspectorViewProvider(openmct)); + openmct.inspectorViews.addProvider(new AnnotationsViewProvider(openmct)); + }; } diff --git a/src/plugins/inspectorViews/properties/DetailText.vue b/src/plugins/inspectorViews/properties/DetailText.vue index c8f3c10938..f11f2fe4d1 100644 --- a/src/plugins/inspectorViews/properties/DetailText.vue +++ b/src/plugins/inspectorViews/properties/DetailText.vue @@ -21,23 +21,23 @@ --> diff --git a/src/plugins/inspectorViews/properties/Location.vue b/src/plugins/inspectorViews/properties/Location.vue index 20c031cddc..fd32d0927f 100644 --- a/src/plugins/inspectorViews/properties/Location.vue +++ b/src/plugins/inspectorViews/properties/Location.vue @@ -21,97 +21,85 @@ --> diff --git a/src/plugins/inspectorViews/properties/Properties.vue b/src/plugins/inspectorViews/properties/Properties.vue index 4c7fd1d2ea..34ba1e06da 100644 --- a/src/plugins/inspectorViews/properties/Properties.vue +++ b/src/plugins/inspectorViews/properties/Properties.vue @@ -21,37 +21,28 @@ --> diff --git a/src/plugins/inspectorViews/properties/PropertiesViewProvider.js b/src/plugins/inspectorViews/properties/PropertiesViewProvider.js index d90f837a70..d3e28e28d3 100644 --- a/src/plugins/inspectorViews/properties/PropertiesViewProvider.js +++ b/src/plugins/inspectorViews/properties/PropertiesViewProvider.js @@ -24,37 +24,37 @@ import Properties from './Properties.vue'; import Vue from 'vue'; export default function PropertiesViewProvider(openmct) { - return { - key: 'propertiesView', - name: 'Properties', - glyph: 'icon-info', - canView: function (selection) { - return selection.length > 0; - }, - view: function (selection) { - let component; + return { + key: 'propertiesView', + name: 'Properties', + glyph: 'icon-info', + canView: function (selection) { + return selection.length > 0; + }, + view: function (selection) { + let component; - return { - show: function (el) { - component = new Vue({ - el, - components: { - Properties - }, - provide: { - openmct - }, - template: `` - }); - }, - priority: function () { - return openmct.priority.DEFAULT; - }, - destroy: function () { - component.$destroy(); - component = undefined; - } - }; + return { + show: function (el) { + component = new Vue({ + el, + components: { + Properties + }, + provide: { + openmct + }, + template: `` + }); + }, + priority: function () { + return openmct.priority.DEFAULT; + }, + destroy: function () { + component.$destroy(); + component = undefined; } - }; + }; + } + }; } diff --git a/src/plugins/inspectorViews/properties/location.scss b/src/plugins/inspectorViews/properties/location.scss index 4a74d8c2af..6abf77cdce 100644 --- a/src/plugins/inspectorViews/properties/location.scss +++ b/src/plugins/inspectorViews/properties/location.scss @@ -1,48 +1,48 @@ .c-path, .c-location { - // Path is two or more items, not clickable - // Location used in Inspector, is clickable + // Path is two or more items, not clickable + // Location used in Inspector, is clickable + display: flex; + + &__item { display: flex; + align-items: center; + min-width: 0; - &__item { - display: flex; - align-items: center; - min-width: 0; - - &:not(:last-child) { - &:after { - // color: $colorInspectorPropName; - content: $glyph-icon-arrow-right; - font-family: symbolsfont; - font-size: 0.7em; - margin-left: $interiorMarginSm; - opacity: 0.8; - } - } + &:not(:last-child) { + &:after { + // color: $colorInspectorPropName; + content: $glyph-icon-arrow-right; + font-family: symbolsfont; + font-size: 0.7em; + margin-left: $interiorMarginSm; + opacity: 0.8; + } } + } } .c-location { - flex-wrap: wrap; + flex-wrap: wrap; - &__item { - $m: 1px; - cursor: pointer; - margin: 0 $m $m 0; + &__item { + $m: 1px; + cursor: pointer; + margin: 0 $m $m 0; - .c-object-label { - border-radius: $smallCr; - padding: 2px 3px; + .c-object-label { + border-radius: $smallCr; + padding: 2px 3px; - &__type-icon { - width: auto; - font-size: 1em; - min-width: auto; - } + &__type-icon { + width: auto; + font-size: 1em; + min-width: auto; + } - @include hover() { - background: $colorItemTreeHoverBg; - } - } + @include hover() { + background: $colorItemTreeHoverBg; + } } + } } diff --git a/src/plugins/inspectorViews/styles/FontStyleEditor.vue b/src/plugins/inspectorViews/styles/FontStyleEditor.vue index b189f78a6b..2b3eec68ea 100644 --- a/src/plugins/inspectorViews/styles/FontStyleEditor.vue +++ b/src/plugins/inspectorViews/styles/FontStyleEditor.vue @@ -20,108 +20,96 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/inspectorViews/styles/SavedStyleSelector.vue b/src/plugins/inspectorViews/styles/SavedStyleSelector.vue index 96f829bd7e..e06cb3cf50 100644 --- a/src/plugins/inspectorViews/styles/SavedStyleSelector.vue +++ b/src/plugins/inspectorViews/styles/SavedStyleSelector.vue @@ -21,176 +21,156 @@ --> diff --git a/src/plugins/inspectorViews/styles/SavedStylesInspectorView.vue b/src/plugins/inspectorViews/styles/SavedStylesInspectorView.vue index 8e0a6a313d..3a6a77a4f7 100644 --- a/src/plugins/inspectorViews/styles/SavedStylesInspectorView.vue +++ b/src/plugins/inspectorViews/styles/SavedStylesInspectorView.vue @@ -21,7 +21,7 @@ --> diff --git a/src/plugins/inspectorViews/styles/SavedStylesView.vue b/src/plugins/inspectorViews/styles/SavedStylesView.vue index 9eb9af9398..798e547913 100644 --- a/src/plugins/inspectorViews/styles/SavedStylesView.vue +++ b/src/plugins/inspectorViews/styles/SavedStylesView.vue @@ -21,109 +21,105 @@ --> diff --git a/src/plugins/inspectorViews/styles/StylesInspectorView.vue b/src/plugins/inspectorViews/styles/StylesInspectorView.vue index b0ac8f4f3c..4c12998384 100644 --- a/src/plugins/inspectorViews/styles/StylesInspectorView.vue +++ b/src/plugins/inspectorViews/styles/StylesInspectorView.vue @@ -21,23 +21,16 @@ --> diff --git a/src/plugins/inspectorViews/styles/StylesInspectorViewProvider.js b/src/plugins/inspectorViews/styles/StylesInspectorViewProvider.js index 669ae6c524..36497c4124 100644 --- a/src/plugins/inspectorViews/styles/StylesInspectorViewProvider.js +++ b/src/plugins/inspectorViews/styles/StylesInspectorViewProvider.js @@ -27,63 +27,67 @@ import Vue from 'vue'; const NON_STYLABLE_TYPES = ['folder', 'webPage', 'conditionSet', 'summary-widget', 'hyperlink']; function isLayoutObject(selection, objectType) { - //we allow conditionSets to be styled if they're part of a layout - return selection.length > 1 - && ((objectType === 'conditionSet') || (NON_STYLABLE_TYPES.indexOf(objectType) < 0)); + //we allow conditionSets to be styled if they're part of a layout + return ( + selection.length > 1 && + (objectType === 'conditionSet' || NON_STYLABLE_TYPES.indexOf(objectType) < 0) + ); } function isCreatableObject(object, type) { - return (NON_STYLABLE_TYPES.indexOf(object.type) < 0) && type.definition.creatable; + return NON_STYLABLE_TYPES.indexOf(object.type) < 0 && type.definition.creatable; } export default function StylesInspectorViewProvider(openmct) { - return { - key: 'stylesInspectorView', - name: 'Styles', - glyph: 'icon-paint-bucket', - canView: function (selection) { - const objectSelection = selection?.[0]; - const layoutItem = objectSelection?.[0]?.context?.layoutItem; - const domainObject = objectSelection?.[0]?.context?.item; + return { + key: 'stylesInspectorView', + name: 'Styles', + glyph: 'icon-paint-bucket', + canView: function (selection) { + const objectSelection = selection?.[0]; + const layoutItem = objectSelection?.[0]?.context?.layoutItem; + const domainObject = objectSelection?.[0]?.context?.item; - if (layoutItem) { - return true; - } + if (layoutItem) { + return true; + } - if (!domainObject) { - return false; - } + if (!domainObject) { + return false; + } - const type = openmct.types.get(domainObject.type); + const type = openmct.types.get(domainObject.type); - return isLayoutObject(objectSelection, domainObject.type) || isCreatableObject(domainObject, type); + return ( + isLayoutObject(objectSelection, domainObject.type) || isCreatableObject(domainObject, type) + ); + }, + view: function (selection) { + let component; + + return { + show: function (el) { + component = new Vue({ + el, + components: { + StylesInspectorView + }, + provide: { + openmct, + stylesManager, + selection + }, + template: `` + }); }, - view: function (selection) { - let component; - - return { - show: function (el) { - component = new Vue({ - el, - components: { - StylesInspectorView - }, - provide: { - openmct, - stylesManager, - selection - }, - template: `` - }); - }, - priority: function () { - return openmct.priority.DEFAULT; - }, - destroy: function () { - component.$destroy(); - component = undefined; - } - }; + priority: function () { + return openmct.priority.DEFAULT; + }, + destroy: function () { + component.$destroy(); + component = undefined; } - }; + }; + } + }; } diff --git a/src/plugins/inspectorViews/styles/StylesManager.js b/src/plugins/inspectorViews/styles/StylesManager.js index 425a5b9888..b23ea95cf4 100644 --- a/src/plugins/inspectorViews/styles/StylesManager.js +++ b/src/plugins/inspectorViews/styles/StylesManager.js @@ -8,115 +8,117 @@ const LIMIT = 20; * @property {*} property */ class StylesManager extends EventEmitter { - load() { - let styles = window.localStorage.getItem(LOCAL_STORAGE_KEY); - styles = styles ? JSON.parse(styles) : []; + load() { + let styles = window.localStorage.getItem(LOCAL_STORAGE_KEY); + styles = styles ? JSON.parse(styles) : []; - return styles; + return styles; + } + + save(style) { + const normalizedStyle = this.normalizeStyle(style); + const styles = this.load(); + + if (!this.isSaveLimitReached(styles)) { + styles.unshift(normalizedStyle); + + if (this.persist(styles)) { + this.emit('stylesUpdated', styles); + } + } + } + + delete(index) { + const styles = this.load(); + styles.splice(index, 1); + + if (this.persist(styles)) { + this.emit('stylesUpdated', styles); + } + } + + select(style) { + this.emit('styleSelected', style); + } + + /** + * @private + */ + normalizeStyle(style) { + const normalizedStyle = this.getBaseStyleObject(); + + Object.keys(normalizedStyle).forEach((property) => { + const value = style[property]; + if (value !== undefined) { + normalizedStyle[property] = value; + } + }); + + return normalizedStyle; + } + + /** + * @private + */ + getBaseStyleObject() { + return { + backgroundColor: '', + border: '', + color: '', + fontSize: 'default', + font: 'default' + }; + } + + /** + * @private + */ + isSaveLimitReached(styles) { + if (styles.length >= LIMIT) { + this.emit('limitReached'); + + return true; } - save(style) { - const normalizedStyle = this.normalizeStyle(style); - const styles = this.load(); + return false; + } - if (!this.isSaveLimitReached(styles)) { - styles.unshift(normalizedStyle); + /** + * @private + */ + isExistingStyle(style, styles) { + return styles.some((existingStyle) => this.isEqual(style, existingStyle)); + } - if (this.persist(styles)) { - this.emit('stylesUpdated', styles); - } - } + /** + * @private + */ + persist(styles) { + try { + window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(styles)); + + return true; + } catch (e) { + this.emit('persistError'); } - delete(index) { - const styles = this.load(); - styles.splice(index, 1); + return false; + } - if (this.persist(styles)) { - this.emit('stylesUpdated', styles); - } - } + /** + * @private + */ + isEqual(style1, style2) { + const keys = Object.keys(Object.assign({}, style1, style2)); + const different = keys.some( + (key) => + (!style1[key] && style2[key]) || + (style1[key] && !style2[key]) || + style1[key] !== style2[key] + ); - select(style) { - this.emit('styleSelected', style); - } - - /** - * @private - */ - normalizeStyle(style) { - const normalizedStyle = this.getBaseStyleObject(); - - Object.keys(normalizedStyle).forEach(property => { - const value = style[property]; - if (value !== undefined) { - normalizedStyle[property] = value; - } - }); - - return normalizedStyle; - } - - /** - * @private - */ - getBaseStyleObject() { - return { - backgroundColor: '', - border: '', - color: '', - fontSize: 'default', - font: 'default' - }; - } - - /** - * @private - */ - isSaveLimitReached(styles) { - if (styles.length >= LIMIT) { - this.emit('limitReached'); - - return true; - } - - return false; - } - - /** - * @private - */ - isExistingStyle(style, styles) { - return styles.some(existingStyle => this.isEqual(style, existingStyle)); - } - - /** - * @private - */ - persist(styles) { - try { - window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(styles)); - - return true; - } catch (e) { - this.emit('persistError'); - } - - return false; - } - - /** - * @private - */ - isEqual(style1, style2) { - const keys = Object.keys(Object.assign({}, style1, style2)); - const different = keys.some(key => (!style1[key] && style2[key]) - || (style1[key] && !style2[key]) - || (style1[key] !== style2[key]) - ); - - return !different; - } + return !different; + } } const stylesManager = new StylesManager(); diff --git a/src/plugins/inspectorViews/styles/constants.js b/src/plugins/inspectorViews/styles/constants.js index 320ca19662..150241a742 100644 --- a/src/plugins/inspectorViews/styles/constants.js +++ b/src/plugins/inspectorViews/styles/constants.js @@ -1,109 +1,109 @@ export const FONT_SIZES = [ - { - name: 'Default', - value: 'default' - }, - { - name: '8px', - value: '8' - }, - { - name: '9px', - value: '9' - }, - { - name: '10px', - value: '10' - }, - { - name: '11px', - value: '11' - }, - { - name: '12px', - value: '12' - }, - { - name: '13px', - value: '13' - }, - { - name: '14px', - value: '14' - }, - { - name: '16px', - value: '16' - }, - { - name: '18px', - value: '18' - }, - { - name: '20px', - value: '20' - }, - { - name: '24px', - value: '24' - }, - { - name: '28px', - value: '28' - }, - { - name: '32px', - value: '32' - }, - { - name: '36px', - value: '36' - }, - { - name: '42px', - value: '42' - }, - { - name: '48px', - value: '48' - }, - { - name: '72px', - value: '72' - }, - { - name: '96px', - value: '96' - }, - { - name: '128px', - value: '128' - } + { + name: 'Default', + value: 'default' + }, + { + name: '8px', + value: '8' + }, + { + name: '9px', + value: '9' + }, + { + name: '10px', + value: '10' + }, + { + name: '11px', + value: '11' + }, + { + name: '12px', + value: '12' + }, + { + name: '13px', + value: '13' + }, + { + name: '14px', + value: '14' + }, + { + name: '16px', + value: '16' + }, + { + name: '18px', + value: '18' + }, + { + name: '20px', + value: '20' + }, + { + name: '24px', + value: '24' + }, + { + name: '28px', + value: '28' + }, + { + name: '32px', + value: '32' + }, + { + name: '36px', + value: '36' + }, + { + name: '42px', + value: '42' + }, + { + name: '48px', + value: '48' + }, + { + name: '72px', + value: '72' + }, + { + name: '96px', + value: '96' + }, + { + name: '128px', + value: '128' + } ]; export const FONTS = [ - { - name: 'Default', - value: 'default' - }, - { - name: 'Bold', - value: 'default-bold' - }, - { - name: 'Narrow', - value: 'narrow' - }, - { - name: 'Narrow Bold', - value: 'narrow-bold' - }, - { - name: 'Monospace', - value: 'monospace' - }, - { - name: 'Monospace Bold', - value: 'monospace-bold' - } + { + name: 'Default', + value: 'default' + }, + { + name: 'Bold', + value: 'default-bold' + }, + { + name: 'Narrow', + value: 'narrow' + }, + { + name: 'Narrow Bold', + value: 'narrow-bold' + }, + { + name: 'Monospace', + value: 'monospace' + }, + { + name: 'Monospace Bold', + value: 'monospace-bold' + } ]; diff --git a/src/plugins/interceptors/missingObjectInterceptor.js b/src/plugins/interceptors/missingObjectInterceptor.js index 12bd193138..1cb566f324 100644 --- a/src/plugins/interceptors/missingObjectInterceptor.js +++ b/src/plugins/interceptors/missingObjectInterceptor.js @@ -21,23 +21,23 @@ *****************************************************************************/ export default function MissingObjectInterceptor(openmct) { - openmct.objects.addGetInterceptor({ - appliesTo: (identifier, domainObject) => { - return true; - }, - invoke: (identifier, object) => { - if (object === undefined) { - const keyString = openmct.objects.makeKeyString(identifier); - openmct.notifications.error(`Failed to retrieve object ${keyString}`, { minimized: true }); + openmct.objects.addGetInterceptor({ + appliesTo: (identifier, domainObject) => { + return true; + }, + invoke: (identifier, object) => { + if (object === undefined) { + const keyString = openmct.objects.makeKeyString(identifier); + openmct.notifications.error(`Failed to retrieve object ${keyString}`, { minimized: true }); - return { - identifier, - type: 'unknown', - name: 'Missing: ' + keyString - }; - } + return { + identifier, + type: 'unknown', + name: 'Missing: ' + keyString + }; + } - return object; - } - }); + return object; + } + }); } diff --git a/src/plugins/interceptors/plugin.js b/src/plugins/interceptors/plugin.js index df776d5644..87c3a93d29 100644 --- a/src/plugins/interceptors/plugin.js +++ b/src/plugins/interceptors/plugin.js @@ -1,7 +1,7 @@ -import missingObjectInterceptor from "./missingObjectInterceptor"; +import missingObjectInterceptor from './missingObjectInterceptor'; export default function plugin() { - return function install(openmct) { - missingObjectInterceptor(openmct); - }; + return function install(openmct) { + missingObjectInterceptor(openmct); + }; } diff --git a/src/plugins/interceptors/pluginSpec.js b/src/plugins/interceptors/pluginSpec.js index 77546d4092..f41c1e0437 100644 --- a/src/plugins/interceptors/pluginSpec.js +++ b/src/plugins/interceptors/pluginSpec.js @@ -20,60 +20,57 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { createOpenMct, resetApplicationState } from "utils/testing"; -import InterceptorPlugin from "./plugin"; +import { createOpenMct, resetApplicationState } from 'utils/testing'; +import InterceptorPlugin from './plugin'; describe('the plugin', function () { - let element; - let child; - let openmct; - const TEST_NAMESPACE = 'test'; + let element; + let child; + let openmct; + const TEST_NAMESPACE = 'test'; - beforeEach((done) => { - openmct = createOpenMct(); - openmct.install(new InterceptorPlugin(openmct)); + beforeEach((done) => { + openmct = createOpenMct(); + openmct.install(new InterceptorPlugin(openmct)); - 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); + 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(); + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + describe('the missingObjectInterceptor', () => { + let mockProvider; + + beforeEach(() => { + mockProvider = jasmine.createSpyObj('mock provider', ['get']); + mockProvider.get.and.returnValue(Promise.resolve(undefined)); + openmct.objects.addProvider(TEST_NAMESPACE, mockProvider); }); - afterEach(() => { - return resetApplicationState(openmct); - }); + it('returns missing objects', () => { + const identifier = { + namespace: TEST_NAMESPACE, + key: 'hello' + }; - describe('the missingObjectInterceptor', () => { - let mockProvider; - - beforeEach(() => { - mockProvider = jasmine.createSpyObj("mock provider", [ - "get" - ]); - mockProvider.get.and.returnValue(Promise.resolve(undefined)); - openmct.objects.addProvider(TEST_NAMESPACE, mockProvider); + return openmct.objects.get(identifier).then((testObject) => { + expect(testObject).toEqual({ + identifier, + type: 'unknown', + name: 'Missing: test:hello' }); - - it('returns missing objects', () => { - const identifier = { - namespace: TEST_NAMESPACE, - key: 'hello' - }; - - return openmct.objects.get(identifier).then((testObject) => { - expect(testObject).toEqual({ - identifier, - type: 'unknown', - name: 'Missing: test:hello' - }); - }); - }); - + }); }); + }); }); diff --git a/src/plugins/latestDataClock/LADClock.js b/src/plugins/latestDataClock/LADClock.js index dd082966ea..131f7930e9 100644 --- a/src/plugins/latestDataClock/LADClock.js +++ b/src/plugins/latestDataClock/LADClock.js @@ -21,23 +21,23 @@ *****************************************************************************/ define(['../../../src/plugins/utcTimeSystem/LocalClock'], function (LocalClock) { - /** - * A {@link Clock} that mocks a "latest available data" type tick source. - * This is for testing purposes only, and behaves identically to a local clock. - * It DOES NOT tick on receipt of data. - * @constructor - */ - function LADClock(period) { - LocalClock.call(this, period); + /** + * A {@link Clock} that mocks a "latest available data" type tick source. + * This is for testing purposes only, and behaves identically to a local clock. + * It DOES NOT tick on receipt of data. + * @constructor + */ + function LADClock(period) { + LocalClock.call(this, period); - this.key = 'test-lad'; - this.mode = 'lad'; - this.cssClass = 'icon-suitcase'; - this.name = 'Latest available data'; - this.description = "Updates when when new data is available"; - } + this.key = 'test-lad'; + this.mode = 'lad'; + this.cssClass = 'icon-suitcase'; + this.name = 'Latest available data'; + this.description = 'Updates when when new data is available'; + } - LADClock.prototype = Object.create(LocalClock.prototype); + LADClock.prototype = Object.create(LocalClock.prototype); - return LADClock; + return LADClock; }); diff --git a/src/plugins/latestDataClock/plugin.js b/src/plugins/latestDataClock/plugin.js index c86a51ac12..a3ba32ccec 100644 --- a/src/plugins/latestDataClock/plugin.js +++ b/src/plugins/latestDataClock/plugin.js @@ -20,14 +20,10 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - "./LADClock" -], function ( - LADClock -) { - return function () { - return function (openmct) { - openmct.time.addClock(new LADClock()); - }; +define(['./LADClock'], function (LADClock) { + return function () { + return function (openmct) { + openmct.time.addClock(new LADClock()); }; + }; }); diff --git a/src/plugins/licenses/Licenses.vue b/src/plugins/licenses/Licenses.vue index ab4d5a3072..340ab8ae9d 100644 --- a/src/plugins/licenses/Licenses.vue +++ b/src/plugins/licenses/Licenses.vue @@ -20,40 +20,36 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/licenses/plugin.js b/src/plugins/licenses/plugin.js index e1fac9bc10..0678641cd5 100644 --- a/src/plugins/licenses/plugin.js +++ b/src/plugins/licenses/plugin.js @@ -23,16 +23,16 @@ import Licenses from './Licenses.vue'; import Vue from 'vue'; export default function () { - return function install(openmct) { - openmct.router.route(/^\/licenses$/, () => { - let licensesVm = new Vue(Licenses).$mount(); + return function install(openmct) { + openmct.router.route(/^\/licenses$/, () => { + let licensesVm = new Vue(Licenses).$mount(); - openmct.overlays.overlay({ - element: licensesVm.$el, - size: 'fullscreen', - dismissable: false, - onDestroy: () => licensesVm.$destroy() - }); - }); - }; + openmct.overlays.overlay({ + element: licensesVm.$el, + size: 'fullscreen', + dismissable: false, + onDestroy: () => licensesVm.$destroy() + }); + }); + }; } diff --git a/src/plugins/licenses/third-party-licenses.json b/src/plugins/licenses/third-party-licenses.json index c139298ce1..54fbcf9585 100644 --- a/src/plugins/licenses/third-party-licenses.json +++ b/src/plugins/licenses/third-party-licenses.json @@ -258,4 +258,3 @@ "copyright": "Copyright (c) 2013-present, Yuxi (Evan) You" } } - diff --git a/src/plugins/linkAction/LinkAction.js b/src/plugins/linkAction/LinkAction.js index d789c10a0c..1d1ee3b0fc 100644 --- a/src/plugins/linkAction/LinkAction.js +++ b/src/plugins/linkAction/LinkAction.js @@ -21,132 +21,136 @@ *****************************************************************************/ export default class LinkAction { - constructor(openmct) { - this.name = 'Create Link'; - this.key = 'link'; - this.description = 'Create Link to object in another location.'; - this.cssClass = "icon-link"; - this.group = "action"; - this.priority = 7; + constructor(openmct) { + this.name = 'Create Link'; + this.key = 'link'; + this.description = 'Create Link to object in another location.'; + this.cssClass = 'icon-link'; + this.group = 'action'; + this.priority = 7; - this.openmct = openmct; - this.transaction = null; + this.openmct = openmct; + this.transaction = null; + } + + appliesTo(objectPath) { + return true; // link away! + } + + invoke(objectPath) { + this.object = objectPath[0]; + this.parent = objectPath[1]; + this.showForm(this.object, this.parent); + } + + inNavigationPath() { + return this.openmct.router.path.some((objectInPath) => + this.openmct.objects.areIdsEqual(objectInPath.identifier, this.object.identifier) + ); + } + + onSave(changes) { + this.startTransaction(); + + const inNavigationPath = this.inNavigationPath(); + if (inNavigationPath && this.openmct.editor.isEditing()) { + this.openmct.editor.save(); } - appliesTo(objectPath) { - return true; // link away! - } + const parentDomainObjectpath = changes.location || [this.parent]; + const parent = parentDomainObjectpath[0]; - invoke(objectPath) { - this.object = objectPath[0]; - this.parent = objectPath[1]; - this.showForm(this.object, this.parent); - } + this.linkInNewParent(this.object, parent); - inNavigationPath() { - return this.openmct.router.path - .some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, this.object.identifier)); - } + return this.saveTransaction(); + } - onSave(changes) { - this.startTransaction(); + linkInNewParent(child, newParent) { + let compositionCollection = this.openmct.composition.get(newParent); - const inNavigationPath = this.inNavigationPath(); - if (inNavigationPath && this.openmct.editor.isEditing()) { - this.openmct.editor.save(); + compositionCollection.add(child); + } + + showForm(domainObject, parentDomainObject) { + const formStructure = { + title: `Link "${domainObject.name}" to a New Location`, + sections: [ + { + rows: [ + { + name: 'Location', + cssClass: 'grows', + control: 'locator', + parent: parentDomainObject, + required: true, + validate: this.validate(parentDomainObject), + key: 'location' + } + ] } + ] + }; + this.openmct.forms.showForm(formStructure).then(this.onSave.bind(this)); + } - const parentDomainObjectpath = changes.location || [this.parent]; - const parent = parentDomainObjectpath[0]; - - this.linkInNewParent(this.object, parent); - - return this.saveTransaction(); - } - - linkInNewParent(child, newParent) { - let compositionCollection = this.openmct.composition.get(newParent); - - compositionCollection.add(child); - } - - showForm(domainObject, parentDomainObject) { - const formStructure = { - title: `Link "${domainObject.name}" to a New Location`, - sections: [ - { - rows: [ - { - name: "Location", - cssClass: "grows", - control: "locator", - parent: parentDomainObject, - required: true, - validate: this.validate(parentDomainObject), - key: 'location' - } - ] - } - ] + validate(currentParent) { + return (data) => { + // default current parent to ROOT, if it's null, then it's a root level item + if (!currentParent) { + currentParent = { + identifier: { + key: 'ROOT', + namespace: '' + } }; - this.openmct.forms.showForm(formStructure) - .then(this.onSave.bind(this)); + } + + 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; + } + + // check if moving to same place + if (this.openmct.objects.areIdsEqual(parentCandidate.identifier, currentParent.identifier)) { + return false; + } + + // check if moving to a child + if ( + parentCandidatePath.some((candidatePath) => { + return this.openmct.objects.areIdsEqual(candidatePath.identifier, this.object.identifier); + }) + ) { + return false; + } + + const parentCandidateComposition = parentCandidate.composition; + if ( + parentCandidateComposition && + parentCandidateComposition.indexOf(objectKeystring) !== -1 + ) { + return false; + } + + return parentCandidate && this.openmct.composition.checkPolicy(parentCandidate, this.object); + }; + } + startTransaction() { + if (!this.openmct.objects.isTransactionActive()) { + this.transaction = this.openmct.objects.startTransaction(); + } + } + + async saveTransaction() { + if (!this.transaction) { + return; } - validate(currentParent) { - return (data) => { - - // default current parent to ROOT, if it's null, then it's a root level item - if (!currentParent) { - currentParent = { - identifier: { - key: 'ROOT', - namespace: '' - } - }; - } - - 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; - } - - // check if moving to same place - if (this.openmct.objects.areIdsEqual(parentCandidate.identifier, currentParent.identifier)) { - return false; - } - - // check if moving to a child - if (parentCandidatePath.some(candidatePath => { - return this.openmct.objects.areIdsEqual(candidatePath.identifier, this.object.identifier); - })) { - return false; - } - - const parentCandidateComposition = parentCandidate.composition; - if (parentCandidateComposition && parentCandidateComposition.indexOf(objectKeystring) !== -1) { - return false; - } - - return parentCandidate && this.openmct.composition.checkPolicy(parentCandidate, this.object); - }; - } - startTransaction() { - if (!this.openmct.objects.isTransactionActive()) { - this.transaction = this.openmct.objects.startTransaction(); - } - } - - async saveTransaction() { - if (!this.transaction) { - return; - } - - await this.transaction.commit(); - this.openmct.objects.endTransaction(); - this.transaction = null; - } + await this.transaction.commit(); + this.openmct.objects.endTransaction(); + this.transaction = null; + } } diff --git a/src/plugins/linkAction/plugin.js b/src/plugins/linkAction/plugin.js index 494001fa6f..9c2e73c9fe 100644 --- a/src/plugins/linkAction/plugin.js +++ b/src/plugins/linkAction/plugin.js @@ -19,10 +19,10 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import LinkAction from "./LinkAction"; +import LinkAction from './LinkAction'; export default function () { - return function (openmct) { - openmct.actions.register(new LinkAction(openmct)); - }; + return function (openmct) { + openmct.actions.register(new LinkAction(openmct)); + }; } diff --git a/src/plugins/linkAction/pluginSpec.js b/src/plugins/linkAction/pluginSpec.js index 81d25ffb20..71fea49354 100644 --- a/src/plugins/linkAction/pluginSpec.js +++ b/src/plugins/linkAction/pluginSpec.js @@ -21,105 +21,101 @@ *****************************************************************************/ import LinkActionPlugin from './plugin.js'; import LinkAction from './LinkAction.js'; -import { - createOpenMct, - resetApplicationState, - getMockObjects -} from 'utils/testing'; +import { createOpenMct, resetApplicationState, getMockObjects } from 'utils/testing'; -describe("The Link Action plugin", () => { - let openmct; - let linkAction; - let childObject; - let parentObject; - let anotherParentObject; - const ORIGINAL_PARENT_ID = 'original-parent-object'; - const LINK_ACITON_KEY = 'link'; - const LINK_ACITON_NAME = 'Create Link'; +describe('The Link Action plugin', () => { + let openmct; + let linkAction; + let childObject; + let parentObject; + let anotherParentObject; + const ORIGINAL_PARENT_ID = 'original-parent-object'; + const LINK_ACITON_KEY = 'link'; + const LINK_ACITON_NAME = 'Create Link'; - beforeEach((done) => { - const appHolder = document.createElement('div'); - appHolder.style.width = '640px'; - appHolder.style.height = '480px'; + beforeEach((done) => { + const appHolder = document.createElement('div'); + appHolder.style.width = '640px'; + appHolder.style.height = '480px'; - openmct = createOpenMct(); + openmct = createOpenMct(); - childObject = getMockObjects({ - objectKeyStrings: ['folder'], - overwrite: { - folder: { - name: "Child Folder", - location: ORIGINAL_PARENT_ID, - identifier: { - namespace: "", - key: "child-folder-object" - } - } - } - }).folder; + childObject = getMockObjects({ + objectKeyStrings: ['folder'], + overwrite: { + folder: { + name: 'Child Folder', + location: ORIGINAL_PARENT_ID, + identifier: { + namespace: '', + key: 'child-folder-object' + } + } + } + }).folder; - parentObject = getMockObjects({ - objectKeyStrings: ['folder'], - overwrite: { - folder: { - name: "Parent Folder", - identifier: { - namespace: "", - key: "original-parent-object" - }, - composition: [childObject.identifier] - } - } - }).folder; + parentObject = getMockObjects({ + objectKeyStrings: ['folder'], + overwrite: { + folder: { + name: 'Parent Folder', + identifier: { + namespace: '', + key: 'original-parent-object' + }, + composition: [childObject.identifier] + } + } + }).folder; - anotherParentObject = getMockObjects({ - objectKeyStrings: ['folder'], - overwrite: { - folder: { - name: "Another Parent Folder" - } - } - }).folder; + anotherParentObject = getMockObjects({ + objectKeyStrings: ['folder'], + overwrite: { + folder: { + name: 'Another Parent Folder' + } + } + }).folder; - openmct.router.path = [childObject]; // preview action uses this in it's applyTo method + openmct.router.path = [childObject]; // preview action uses this in it's applyTo method - openmct.install(LinkActionPlugin()); + openmct.install(LinkActionPlugin()); - openmct.on('start', done); - openmct.startHeadless(appHolder); + openmct.on('start', done); + openmct.startHeadless(appHolder); + }); + + afterEach(() => { + resetApplicationState(openmct); + }); + + it('should be defined', () => { + expect(LinkActionPlugin).toBeDefined(); + }); + + it('should make the link action available for an appropriate domainObject', () => { + const actionCollection = openmct.actions.getActionsCollection([childObject]); + const visibleActions = actionCollection.getVisibleActions(); + linkAction = visibleActions.find((a) => a.key === LINK_ACITON_KEY); + + expect(linkAction.name).toEqual(LINK_ACITON_NAME); + }); + + describe('when linking an object in a new parent', () => { + beforeEach(() => { + linkAction = new LinkAction(openmct); + linkAction.linkInNewParent(childObject, anotherParentObject); }); - afterEach(() => { - resetApplicationState(openmct); + it("the child object's identifier should be in the new parent's composition and location set to original parent", () => { + let newParentChild = anotherParentObject.composition[0]; + expect(newParentChild).toEqual(childObject.identifier); + expect(childObject.location).toEqual(ORIGINAL_PARENT_ID); }); - it("should be defined", () => { - expect(LinkActionPlugin).toBeDefined(); - }); - - it("should make the link action available for an appropriate domainObject", () => { - const actionCollection = openmct.actions.getActionsCollection([childObject]); - const visibleActions = actionCollection.getVisibleActions(); - linkAction = visibleActions.find(a => a.key === LINK_ACITON_KEY); - - expect(linkAction.name).toEqual(LINK_ACITON_NAME); - }); - - describe("when linking an object in a new parent", () => { - beforeEach(() => { - linkAction = new LinkAction(openmct); - linkAction.linkInNewParent(childObject, anotherParentObject); - }); - - it("the child object's identifier should be in the new parent's composition and location set to original parent", () => { - let newParentChild = anotherParentObject.composition[0]; - expect(newParentChild).toEqual(childObject.identifier); - expect(childObject.location).toEqual(ORIGINAL_PARENT_ID); - }); - - it("the child object's identifier should remain in the original parent's composition", () => { - let oldParentCompositionChild = parentObject.composition[0]; - expect(oldParentCompositionChild).toEqual(childObject.identifier); - }); + it("the child object's identifier should remain in the original parent's composition", () => { + let oldParentCompositionChild = parentObject.composition[0]; + expect(oldParentCompositionChild).toEqual(childObject.identifier); }); + }); }); diff --git a/src/plugins/localStorage/LocalStorageObjectProvider.js b/src/plugins/localStorage/LocalStorageObjectProvider.js index b088c9e593..4d37c937f9 100644 --- a/src/plugins/localStorage/LocalStorageObjectProvider.js +++ b/src/plugins/localStorage/LocalStorageObjectProvider.js @@ -21,84 +21,84 @@ *****************************************************************************/ export default class LocalStorageObjectProvider { - constructor(spaceKey = 'mct') { - this.localStorage = window.localStorage; - this.spaceKey = spaceKey; - this.initializeSpace(spaceKey); + constructor(spaceKey = 'mct') { + this.localStorage = window.localStorage; + this.spaceKey = spaceKey; + this.initializeSpace(spaceKey); + } + + get(identifier) { + if (this.getSpaceAsObject()[identifier.key] !== undefined) { + const persistedModel = this.getSpaceAsObject()[identifier.key]; + const domainObject = { + identifier, + ...persistedModel + }; + + return Promise.resolve(domainObject); + } else { + return Promise.resolve(undefined); } + } - get(identifier) { - if (this.getSpaceAsObject()[identifier.key] !== undefined) { - const persistedModel = this.getSpaceAsObject()[identifier.key]; - const domainObject = { - identifier, - ...persistedModel - }; + getAllObjects() { + return this.getSpaceAsObject(); + } - return Promise.resolve(domainObject); - } else { - return Promise.resolve(undefined); - } + create(object) { + return this.persistObject(object); + } + + update(object) { + return this.persistObject(object); + } + + /** + * @private + */ + persistObject(domainObject) { + let space = this.getSpaceAsObject(); + space[domainObject.identifier.key] = domainObject; + + this.persistSpace(space); + + return Promise.resolve(true); + } + + /** + * @private + */ + persistSpace(space) { + this.localStorage[this.spaceKey] = JSON.stringify(space); + } + + /** + * @private + */ + getSpace() { + return this.localStorage[this.spaceKey]; + } + + /** + * @private + */ + getSpaceAsObject() { + return JSON.parse(this.getSpace()); + } + + /** + * @private + */ + initializeSpace() { + if (this.isEmpty()) { + this.localStorage[this.spaceKey] = JSON.stringify({}); } + } - getAllObjects() { - return this.getSpaceAsObject(); - } - - create(object) { - return this.persistObject(object); - } - - update(object) { - return this.persistObject(object); - } - - /** - * @private - */ - persistObject(domainObject) { - let space = this.getSpaceAsObject(); - space[domainObject.identifier.key] = domainObject; - - this.persistSpace(space); - - return Promise.resolve(true); - } - - /** - * @private - */ - persistSpace(space) { - this.localStorage[this.spaceKey] = JSON.stringify(space); - } - - /** - * @private - */ - getSpace() { - return this.localStorage[this.spaceKey]; - } - - /** - * @private - */ - getSpaceAsObject() { - return JSON.parse(this.getSpace()); - } - - /** - * @private - */ - initializeSpace() { - if (this.isEmpty()) { - this.localStorage[this.spaceKey] = JSON.stringify({}); - } - } - - /** - * @private - */ - isEmpty() { - return this.getSpace() === undefined; - } + /** + * @private + */ + isEmpty() { + return this.getSpace() === undefined; + } } diff --git a/src/plugins/localStorage/plugin.js b/src/plugins/localStorage/plugin.js index 94cfef9ad0..92b9f014f2 100644 --- a/src/plugins/localStorage/plugin.js +++ b/src/plugins/localStorage/plugin.js @@ -23,7 +23,7 @@ import LocalStorageObjectProvider from './LocalStorageObjectProvider'; export default function (namespace = '', storageSpace = 'mct') { - return function (openmct) { - openmct.objects.addProvider(namespace, new LocalStorageObjectProvider(storageSpace)); - }; + return function (openmct) { + openmct.objects.addProvider(namespace, new LocalStorageObjectProvider(storageSpace)); + }; } diff --git a/src/plugins/localStorage/pluginSpec.js b/src/plugins/localStorage/pluginSpec.js index 9968b45907..b1d0c32292 100644 --- a/src/plugins/localStorage/pluginSpec.js +++ b/src/plugins/localStorage/pluginSpec.js @@ -21,76 +21,72 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { - createOpenMct, - resetApplicationState -} from 'utils/testing'; +import { createOpenMct, resetApplicationState } from 'utils/testing'; -describe("The local storage plugin", () => { - let space; - let openmct; +describe('The local storage plugin', () => { + let space; + let openmct; - beforeEach(() => { - space = `test-${Date.now()}`; - openmct = createOpenMct(); + beforeEach(() => { + space = `test-${Date.now()}`; + openmct = createOpenMct(); - openmct.install(openmct.plugins.LocalStorage('', space)); + openmct.install(openmct.plugins.LocalStorage('', space)); + }); - }); + it('initializes localstorage if not already initialized', () => { + const ls = getLocalStorage(); + expect(ls[space]).toBeDefined(); + }); - it('initializes localstorage if not already initialized', () => { - const ls = getLocalStorage(); - expect(ls[space]).toBeDefined(); - }); + it('successfully persists an object to localstorage', async () => { + const domainObject = { + identifier: { + namespace: '', + key: 'test-key' + }, + name: 'A test object' + }; + let spaceAsObject = getSpaceAsObject(); + expect(spaceAsObject['test-key']).not.toBeDefined(); - it('successfully persists an object to localstorage', async () => { - const domainObject = { - identifier: { - namespace: '', - key: 'test-key' - }, - name: 'A test object' - }; - let spaceAsObject = getSpaceAsObject(); - expect(spaceAsObject['test-key']).not.toBeDefined(); + await openmct.objects.save(domainObject); - await openmct.objects.save(domainObject); + spaceAsObject = getSpaceAsObject(); + expect(spaceAsObject['test-key']).toBeDefined(); + }); - spaceAsObject = getSpaceAsObject(); - expect(spaceAsObject['test-key']).toBeDefined(); - }); + it('successfully retrieves an object from localstorage', async () => { + const domainObject = { + identifier: { + namespace: '', + key: 'test-key' + }, + name: 'A test object', + anotherProperty: Date.now() + }; + await openmct.objects.save(domainObject); - it('successfully retrieves an object from localstorage', async () => { - const domainObject = { - identifier: { - namespace: '', - key: 'test-key' - }, - name: 'A test object', - anotherProperty: Date.now() - }; - await openmct.objects.save(domainObject); + let testObject = await openmct.objects.get(domainObject.identifier); - let testObject = await openmct.objects.get(domainObject.identifier); + expect(testObject.name).toEqual(domainObject.name); + expect(testObject.anotherProperty).toEqual(domainObject.anotherProperty); + }); - expect(testObject.name).toEqual(domainObject.name); - expect(testObject.anotherProperty).toEqual(domainObject.anotherProperty); - }); + afterEach(() => { + resetApplicationState(openmct); + resetLocalStorage(); + }); - afterEach(() => { - resetApplicationState(openmct); - resetLocalStorage(); - }); + function resetLocalStorage() { + delete window.localStorage[space]; + } - function resetLocalStorage() { - delete window.localStorage[space]; - } + function getLocalStorage() { + return window.localStorage; + } - function getLocalStorage() { - return window.localStorage; - } - - function getSpaceAsObject() { - return JSON.parse(getLocalStorage()[space]); - } + function getSpaceAsObject() { + return JSON.parse(getLocalStorage()[space]); + } }); diff --git a/src/plugins/localTimeSystem/LocalTimeFormat.js b/src/plugins/localTimeSystem/LocalTimeFormat.js index f0605b856e..2311ebb913 100644 --- a/src/plugins/localTimeSystem/LocalTimeFormat.js +++ b/src/plugins/localTimeSystem/LocalTimeFormat.js @@ -20,58 +20,49 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - 'moment' -], function ( - moment -) { - const DATE_FORMAT = "YYYY-MM-DD h:mm:ss.SSS a"; +define(['moment'], function (moment) { + const DATE_FORMAT = 'YYYY-MM-DD h:mm:ss.SSS a'; - const DATE_FORMATS = [ - DATE_FORMAT, - "YYYY-MM-DD h:mm:ss a", - "YYYY-MM-DD h:mm a", - "YYYY-MM-DD" - ]; + const DATE_FORMATS = [DATE_FORMAT, 'YYYY-MM-DD h:mm:ss a', 'YYYY-MM-DD h:mm a', 'YYYY-MM-DD']; - /** - * @typedef Scale - * @property {number} min the minimum scale value, in ms - * @property {number} max the maximum scale value, in ms - */ + /** + * @typedef Scale + * @property {number} min the minimum scale value, in ms + * @property {number} max the maximum scale value, in ms + */ - /** - * Formatter for UTC timestamps. Interprets numeric values as - * milliseconds since the start of 1970. - * - * @implements {Format} - * @constructor - * @memberof platform/commonUI/formats - */ - function LocalTimeFormat() { - this.key = 'local-format'; + /** + * Formatter for UTC timestamps. Interprets numeric values as + * milliseconds since the start of 1970. + * + * @implements {Format} + * @constructor + * @memberof platform/commonUI/formats + */ + function LocalTimeFormat() { + this.key = 'local-format'; + } + + /** + * + * @param value + * @returns {string} the formatted date + */ + LocalTimeFormat.prototype.format = function (value, scale) { + return moment(value).format(DATE_FORMAT); + }; + + LocalTimeFormat.prototype.parse = function (text) { + if (typeof text === 'number') { + return text; } - /** - * - * @param value - * @returns {string} the formatted date - */ - LocalTimeFormat.prototype.format = function (value, scale) { - return moment(value).format(DATE_FORMAT); - }; + return moment(text, DATE_FORMATS).valueOf(); + }; - LocalTimeFormat.prototype.parse = function (text) { - if (typeof text === 'number') { - return text; - } + LocalTimeFormat.prototype.validate = function (text) { + return moment(text, DATE_FORMATS).isValid(); + }; - return moment(text, DATE_FORMATS).valueOf(); - }; - - LocalTimeFormat.prototype.validate = function (text) { - return moment(text, DATE_FORMATS).isValid(); - }; - - return LocalTimeFormat; + return LocalTimeFormat; }); diff --git a/src/plugins/localTimeSystem/LocalTimeSystem.js b/src/plugins/localTimeSystem/LocalTimeSystem.js index c7b09b7e12..abd36bbcaf 100644 --- a/src/plugins/localTimeSystem/LocalTimeSystem.js +++ b/src/plugins/localTimeSystem/LocalTimeSystem.js @@ -21,28 +21,26 @@ *****************************************************************************/ define([], function () { - + /** + * This time system supports UTC dates and provides a ticking clock source. + * @implements TimeSystem + * @constructor + */ + function LocalTimeSystem() { /** - * This time system supports UTC dates and provides a ticking clock source. - * @implements TimeSystem - * @constructor + * Some metadata, which will be used to identify the time system in + * the UI + * @type {{key: string, name: string, glyph: string}} */ - function LocalTimeSystem() { + this.key = 'local'; + this.name = 'Local'; + this.cssClass = 'icon-clock'; - /** - * Some metadata, which will be used to identify the time system in - * the UI - * @type {{key: string, name: string, glyph: string}} - */ - this.key = 'local'; - this.name = 'Local'; - this.cssClass = 'icon-clock'; + this.timeFormat = 'local-format'; + this.durationFormat = 'duration'; - this.timeFormat = 'local-format'; - this.durationFormat = 'duration'; + this.isUTCBased = true; + } - this.isUTCBased = true; - } - - return LocalTimeSystem; + return LocalTimeSystem; }); diff --git a/src/plugins/localTimeSystem/plugin.js b/src/plugins/localTimeSystem/plugin.js index 325a15142e..161e7b41f6 100644 --- a/src/plugins/localTimeSystem/plugin.js +++ b/src/plugins/localTimeSystem/plugin.js @@ -20,17 +20,11 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - "./LocalTimeSystem", - "./LocalTimeFormat" -], function ( - LocalTimeSystem, - LocalTimeFormat -) { - return function () { - return function (openmct) { - openmct.time.addTimeSystem(new LocalTimeSystem()); - openmct.telemetry.addFormat(new LocalTimeFormat()); - }; +define(['./LocalTimeSystem', './LocalTimeFormat'], function (LocalTimeSystem, LocalTimeFormat) { + return function () { + return function (openmct) { + openmct.time.addTimeSystem(new LocalTimeSystem()); + openmct.telemetry.addFormat(new LocalTimeFormat()); }; + }; }); diff --git a/src/plugins/localTimeSystem/pluginSpec.js b/src/plugins/localTimeSystem/pluginSpec.js index 7a7ba83f50..dce435797e 100644 --- a/src/plugins/localTimeSystem/pluginSpec.js +++ b/src/plugins/localTimeSystem/pluginSpec.js @@ -20,95 +20,88 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { - createOpenMct, - resetApplicationState -} from 'utils/testing'; +import { createOpenMct, resetApplicationState } from 'utils/testing'; -describe("The local time", () => { - const LOCAL_FORMAT_KEY = 'local-format'; - const LOCAL_SYSTEM_KEY = 'local'; - const JUNK = "junk"; - const TIMESTAMP = -14256000000; - const DATESTRING = '1969-07-20 12:00:00.000 am'; - let openmct; +describe('The local time', () => { + const LOCAL_FORMAT_KEY = 'local-format'; + const LOCAL_SYSTEM_KEY = 'local'; + const JUNK = 'junk'; + const TIMESTAMP = -14256000000; + const DATESTRING = '1969-07-20 12:00:00.000 am'; + let openmct; - beforeEach((done) => { + beforeEach((done) => { + openmct = createOpenMct(); - openmct = createOpenMct(); + openmct.install(openmct.plugins.LocalTimeSystem()); - openmct.install(openmct.plugins.LocalTimeSystem()); + openmct.on('start', done); + openmct.startHeadless(); + }); - openmct.on('start', done); - openmct.startHeadless(); + afterEach(() => { + return resetApplicationState(openmct); + }); + describe('system', function () { + let localTimeSystem; + + beforeEach(() => { + localTimeSystem = openmct.time.timeSystem(LOCAL_SYSTEM_KEY, { + start: 0, + end: 1 + }); }); - afterEach(() => { - return resetApplicationState(openmct); + it('is installed', () => { + let timeSystems = openmct.time.getAllTimeSystems(); + let local = timeSystems.find((ts) => ts.key === LOCAL_SYSTEM_KEY); + + expect(local).not.toEqual(-1); }); - describe("system", function () { - - let localTimeSystem; - - beforeEach(() => { - localTimeSystem = openmct.time.timeSystem(LOCAL_SYSTEM_KEY, { - start: 0, - end: 1 - }); - }); - - it("is installed", () => { - let timeSystems = openmct.time.getAllTimeSystems(); - let local = timeSystems.find(ts => ts.key === LOCAL_SYSTEM_KEY); - - expect(local).not.toEqual(-1); - }); - - it("can be set to be the main time system", () => { - expect(openmct.time.timeSystem().key).toBe(LOCAL_SYSTEM_KEY); - }); - - it("uses the local-format time format", () => { - expect(localTimeSystem.timeFormat).toBe(LOCAL_FORMAT_KEY); - }); - - it("is UTC based", () => { - expect(localTimeSystem.isUTCBased).toBe(true); - }); - - it("defines expected metadata", () => { - expect(localTimeSystem.key).toBe(LOCAL_SYSTEM_KEY); - expect(localTimeSystem.name).toBeDefined(); - expect(localTimeSystem.cssClass).toBeDefined(); - expect(localTimeSystem.durationFormat).toBeDefined(); - }); + it('can be set to be the main time system', () => { + expect(openmct.time.timeSystem().key).toBe(LOCAL_SYSTEM_KEY); }); - describe("formatter can be obtained from the telemetry API and", () => { - - let localTimeFormatter; - let dateString; - let timeStamp; - - beforeEach(() => { - localTimeFormatter = openmct.telemetry.getFormatter(LOCAL_FORMAT_KEY); - dateString = localTimeFormatter.format(TIMESTAMP); - timeStamp = localTimeFormatter.parse(DATESTRING); - }); - - it("will format a timestamp in local time format", () => { - expect(localTimeFormatter.format(TIMESTAMP)).toBe(dateString); - }); - - it("will parse an local time Date String into milliseconds", () => { - expect(localTimeFormatter.parse(DATESTRING)).toBe(timeStamp); - }); - - it("will validate correctly", () => { - expect(localTimeFormatter.validate(DATESTRING)).toBe(true); - expect(localTimeFormatter.validate(JUNK)).toBe(false); - }); + it('uses the local-format time format', () => { + expect(localTimeSystem.timeFormat).toBe(LOCAL_FORMAT_KEY); }); + + it('is UTC based', () => { + expect(localTimeSystem.isUTCBased).toBe(true); + }); + + it('defines expected metadata', () => { + expect(localTimeSystem.key).toBe(LOCAL_SYSTEM_KEY); + expect(localTimeSystem.name).toBeDefined(); + expect(localTimeSystem.cssClass).toBeDefined(); + expect(localTimeSystem.durationFormat).toBeDefined(); + }); + }); + + describe('formatter can be obtained from the telemetry API and', () => { + let localTimeFormatter; + let dateString; + let timeStamp; + + beforeEach(() => { + localTimeFormatter = openmct.telemetry.getFormatter(LOCAL_FORMAT_KEY); + dateString = localTimeFormatter.format(TIMESTAMP); + timeStamp = localTimeFormatter.parse(DATESTRING); + }); + + it('will format a timestamp in local time format', () => { + expect(localTimeFormatter.format(TIMESTAMP)).toBe(dateString); + }); + + it('will parse an local time Date String into milliseconds', () => { + expect(localTimeFormatter.parse(DATESTRING)).toBe(timeStamp); + }); + + it('will validate correctly', () => { + expect(localTimeFormatter.validate(DATESTRING)).toBe(true); + expect(localTimeFormatter.validate(JUNK)).toBe(false); + }); + }); }); diff --git a/src/plugins/move/MoveAction.js b/src/plugins/move/MoveAction.js index 8ab0ce307f..60da056857 100644 --- a/src/plugins/move/MoveAction.js +++ b/src/plugins/move/MoveAction.js @@ -20,191 +20,199 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ export default class MoveAction { - constructor(openmct) { - this.name = 'Move'; - this.key = 'move'; - this.description = 'Move this object from its containing object to another object.'; - this.cssClass = "icon-move"; - this.group = "action"; - this.priority = 7; + constructor(openmct) { + this.name = 'Move'; + this.key = 'move'; + this.description = 'Move this object from its containing object to another object.'; + this.cssClass = 'icon-move'; + this.group = 'action'; + this.priority = 7; - this.openmct = openmct; - this.transaction = null; + this.openmct = openmct; + this.transaction = null; + } + + invoke(objectPath) { + this.object = objectPath[0]; + this.oldParent = objectPath[1]; + + this.showForm(this.object, this.oldParent); + } + + inNavigationPath() { + return this.openmct.router.path.some((objectInPath) => + this.openmct.objects.areIdsEqual(objectInPath.identifier, this.object.identifier) + ); + } + + navigateTo(objectPath) { + const urlPath = objectPath + .reverse() + .map((object) => this.openmct.objects.makeKeyString(object.identifier)) + .join('/'); + + this.openmct.router.navigate('#/browse/' + urlPath); + } + + addToNewParent(child, newParent) { + const newParentKeyString = this.openmct.objects.makeKeyString(newParent.identifier); + const compositionCollection = this.openmct.composition.get(newParent); + + this.openmct.objects.mutate(child, 'location', newParentKeyString); + compositionCollection.add(child); + } + + async onSave(changes) { + this.startTransaction(); + + const inNavigationPath = this.inNavigationPath(this.object); + const parentDomainObjectpath = changes.location || [this.parent]; + const parent = parentDomainObjectpath[0]; + + if (this.openmct.objects.areIdsEqual(parent.identifier, this.oldParent.identifier)) { + this.openmct.notifications.error(`Error: new location cant not be same as old`); + + return; } - invoke(objectPath) { - this.object = objectPath[0]; - this.oldParent = objectPath[1]; - - this.showForm(this.object, this.oldParent); + if (changes.name && changes.name !== this.object.name) { + this.object.name = changes.name; } - inNavigationPath() { - return this.openmct.router.path - .some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, this.object.identifier)); + this.addToNewParent(this.object, parent); + this.removeFromOldParent(this.object); + + await this.saveTransaction(); + + if (!inNavigationPath) { + return; } - navigateTo(objectPath) { - const urlPath = objectPath.reverse() - .map(object => this.openmct.objects.makeKeyString(object.identifier)) - .join("/"); + let newObjectPath; - this.openmct.router.navigate('#/browse/' + urlPath); + if (parentDomainObjectpath) { + newObjectPath = parentDomainObjectpath && [this.object].concat(parentDomainObjectpath); + } else { + const root = await this.openmct.objects.getRoot(); + const rootCompositionCollection = this.openmct.composition.get(root); + const rootComposition = await rootCompositionCollection.load(); + const rootChildCount = rootComposition.length; + newObjectPath = await this.openmct.objects.getOriginalPath(this.object.identifier); + + // if not multiple root children, remove root from path + if (rootChildCount < 2) { + newObjectPath.pop(); // remove ROOT + } } - addToNewParent(child, newParent) { - const newParentKeyString = this.openmct.objects.makeKeyString(newParent.identifier); - const compositionCollection = this.openmct.composition.get(newParent); + this.navigateTo(newObjectPath); + } - this.openmct.objects.mutate(child, 'location', newParentKeyString); - compositionCollection.add(child); - } + removeFromOldParent(child) { + const compositionCollection = this.openmct.composition.get(this.oldParent); + compositionCollection.remove(child); + } - async onSave(changes) { - this.startTransaction(); - - const inNavigationPath = this.inNavigationPath(this.object); - const parentDomainObjectpath = changes.location || [this.parent]; - const parent = parentDomainObjectpath[0]; - - if (this.openmct.objects.areIdsEqual(parent.identifier, this.oldParent.identifier)) { - this.openmct.notifications.error(`Error: new location cant not be same as old`); - - return; - } - - if (changes.name && (changes.name !== this.object.name)) { - this.object.name = changes.name; - } - - this.addToNewParent(this.object, parent); - this.removeFromOldParent(this.object); - - await this.saveTransaction(); - - if (!inNavigationPath) { - return; - } - - let newObjectPath; - - if (parentDomainObjectpath) { - newObjectPath = parentDomainObjectpath && [this.object].concat(parentDomainObjectpath); - } else { - const root = await this.openmct.objects.getRoot(); - const rootCompositionCollection = this.openmct.composition.get(root); - const rootComposition = await rootCompositionCollection.load(); - const rootChildCount = rootComposition.length; - newObjectPath = await this.openmct.objects.getOriginalPath(this.object.identifier); - - // if not multiple root children, remove root from path - if (rootChildCount < 2) { - newObjectPath.pop(); // remove ROOT + showForm(domainObject, parentDomainObject) { + const formStructure = { + title: 'Move Item', + sections: [ + { + rows: [ + { + key: 'name', + control: 'textfield', + name: 'Title', + pattern: '\\S+', + required: true, + cssClass: 'l-input-lg', + value: domainObject.name + }, + { + name: 'Location', + cssClass: 'grows', + control: 'locator', + parent: parentDomainObject, + required: true, + validate: this.validate(parentDomainObject), + key: 'location' } + ] } + ] + }; - this.navigateTo(newObjectPath); + this.openmct.forms.showForm(formStructure).then(this.onSave.bind(this)); + } + + validate(currentParent) { + return (data) => { + const parentCandidatePath = data.value; + const parentCandidate = parentCandidatePath[0]; + + // check if moving to same place + if (this.openmct.objects.areIdsEqual(parentCandidate.identifier, currentParent.identifier)) { + return false; + } + + // check if moving to a child + if ( + parentCandidatePath.some((candidatePath) => { + return this.openmct.objects.areIdsEqual(candidatePath.identifier, this.object.identifier); + }) + ) { + return false; + } + + if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) { + return false; + } + + const objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier); + const parentCandidateComposition = parentCandidate.composition; + + if ( + parentCandidateComposition && + parentCandidateComposition.indexOf(objectKeystring) !== -1 + ) { + return false; + } + + return parentCandidate && this.openmct.composition.checkPolicy(parentCandidate, this.object); + }; + } + + appliesTo(objectPath) { + const parent = objectPath[1]; + const parentType = parent && this.openmct.types.get(parent.type); + const child = objectPath[0]; + const childType = child && this.openmct.types.get(child.type); + const isPersistable = this.openmct.objects.isPersistable(child.identifier); + + if (parent?.locked || !isPersistable) { + return false; } - removeFromOldParent(child) { - const compositionCollection = this.openmct.composition.get(this.oldParent); - compositionCollection.remove(child); + return ( + parentType?.definition.creatable && + childType?.definition.creatable && + Array.isArray(parent.composition) + ); + } + + startTransaction() { + if (!this.openmct.objects.isTransactionActive()) { + this.transaction = this.openmct.objects.startTransaction(); + } + } + + async saveTransaction() { + if (!this.transaction) { + return; } - showForm(domainObject, parentDomainObject) { - const formStructure = { - title: "Move Item", - sections: [ - { - rows: [ - { - key: "name", - control: "textfield", - name: "Title", - pattern: "\\S+", - required: true, - cssClass: "l-input-lg", - value: domainObject.name - }, - { - name: "Location", - cssClass: "grows", - control: "locator", - parent: parentDomainObject, - required: true, - validate: this.validate(parentDomainObject), - key: 'location' - } - ] - } - ] - }; - - this.openmct.forms.showForm(formStructure) - .then(this.onSave.bind(this)); - } - - validate(currentParent) { - return (data) => { - const parentCandidatePath = data.value; - const parentCandidate = parentCandidatePath[0]; - - // check if moving to same place - if (this.openmct.objects.areIdsEqual(parentCandidate.identifier, currentParent.identifier)) { - return false; - } - - // check if moving to a child - if (parentCandidatePath.some(candidatePath => { - return this.openmct.objects.areIdsEqual(candidatePath.identifier, this.object.identifier); - })) { - return false; - } - - if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) { - return false; - } - - const objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier); - const parentCandidateComposition = parentCandidate.composition; - - if (parentCandidateComposition && parentCandidateComposition.indexOf(objectKeystring) !== -1) { - return false; - } - - return parentCandidate && this.openmct.composition.checkPolicy(parentCandidate, this.object); - }; - } - - appliesTo(objectPath) { - const parent = objectPath[1]; - const parentType = parent && this.openmct.types.get(parent.type); - const child = objectPath[0]; - const childType = child && this.openmct.types.get(child.type); - const isPersistable = this.openmct.objects.isPersistable(child.identifier); - - if (parent?.locked || !isPersistable) { - return false; - } - - return parentType?.definition.creatable - && childType?.definition.creatable - && Array.isArray(parent.composition); - } - - startTransaction() { - if (!this.openmct.objects.isTransactionActive()) { - this.transaction = this.openmct.objects.startTransaction(); - } - } - - async saveTransaction() { - if (!this.transaction) { - return; - } - - await this.transaction.commit(); - this.openmct.objects.endTransaction(); - this.transaction = null; - } + await this.transaction.commit(); + this.openmct.objects.endTransaction(); + this.transaction = null; + } } diff --git a/src/plugins/move/plugin.js b/src/plugins/move/plugin.js index a84cbf1fa5..ed86d281dd 100644 --- a/src/plugins/move/plugin.js +++ b/src/plugins/move/plugin.js @@ -19,10 +19,10 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import MoveAction from "./MoveAction"; +import MoveAction from './MoveAction'; export default function () { - return function (openmct) { - openmct.actions.register(new MoveAction(openmct)); - }; + return function (openmct) { + openmct.actions.register(new MoveAction(openmct)); + }; } diff --git a/src/plugins/move/pluginSpec.js b/src/plugins/move/pluginSpec.js index 070957144d..2f99ee37ff 100644 --- a/src/plugins/move/pluginSpec.js +++ b/src/plugins/move/pluginSpec.js @@ -19,139 +19,134 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import { - createOpenMct, - resetApplicationState, - getMockObjects -} from 'utils/testing'; +import { createOpenMct, resetApplicationState, getMockObjects } from 'utils/testing'; -describe("The Move Action plugin", () => { - let openmct; - let moveAction; - let childObject; - let parentObject; - let anotherParentObject; +describe('The Move Action plugin', () => { + let openmct; + let moveAction; + let childObject; + let parentObject; + let anotherParentObject; - // this setups up the app + // this setups up the app + beforeEach((done) => { + openmct = createOpenMct(); + + childObject = getMockObjects({ + objectKeyStrings: ['folder'], + overwrite: { + folder: { + name: 'Child Folder', + identifier: { + namespace: '', + key: 'child-folder-object' + }, + location: 'parent-folder-object' + } + } + }).folder; + + parentObject = getMockObjects({ + objectKeyStrings: ['folder'], + overwrite: { + folder: { + name: 'Parent Folder', + composition: [childObject.identifier], + identifier: { + namespace: '', + key: 'parent-folder-object' + } + } + } + }).folder; + + anotherParentObject = getMockObjects({ + objectKeyStrings: ['folder'], + overwrite: { + folder: { + name: 'Another Parent Folder', + identifier: { + namespace: '', + key: 'another-parent-folder-object' + } + } + } + }).folder; + + openmct.on('start', done); + openmct.startHeadless(); + + moveAction = openmct.actions._allActions.move; + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('should be defined', () => { + expect(moveAction).toBeDefined(); + }); + + describe('when determining the object is applicable', () => { + beforeEach(() => { + spyOn(moveAction, 'appliesTo').and.callThrough(); + }); + + it('should be true when the parent is creatable and has composition', () => { + let applies = moveAction.appliesTo([childObject, parentObject]); + expect(applies).toBe(true); + }); + + it('should be true when the child is locked and not an alias', () => { + childObject.locked = true; + let applies = moveAction.appliesTo([childObject, parentObject]); + expect(applies).toBe(true); + }); + + it('should still be true when the child is locked and is an alias', () => { + childObject.locked = true; + childObject.location = 'another-parent-folder-object'; + let applies = moveAction.appliesTo([childObject, parentObject]); + expect(applies).toBe(true); + }); + }); + + describe('when moving an object to a new parent and removing from the old parent', () => { + let unObserve; beforeEach((done) => { - openmct = createOpenMct(); + openmct.router.path = []; - childObject = getMockObjects({ - objectKeyStrings: ['folder'], - overwrite: { - folder: { - name: "Child Folder", - identifier: { - namespace: "", - key: "child-folder-object" - }, - location: "parent-folder-object" - } - } - }).folder; + spyOn(openmct.objects, 'save'); + openmct.objects.save.and.callThrough(); + spyOn(openmct.forms, 'showForm'); + openmct.forms.showForm.and.callFake((formStructure) => { + return Promise.resolve({ + name: 'test', + location: [anotherParentObject] + }); + }); - parentObject = getMockObjects({ - objectKeyStrings: ['folder'], - overwrite: { - folder: { - name: "Parent Folder", - composition: [childObject.identifier], - identifier: { - namespace: "", - key: "parent-folder-object" - } - } - } - }).folder; + unObserve = openmct.objects.observe(parentObject, '*', (newObject) => { + done(); + }); - anotherParentObject = getMockObjects({ - objectKeyStrings: ['folder'], - overwrite: { - folder: { - name: "Another Parent Folder", - identifier: { - namespace: "", - key: "another-parent-folder-object" - } - } - } - }).folder; + moveAction.inNavigationPath = () => false; - openmct.on('start', done); - openmct.startHeadless(); - - moveAction = openmct.actions._allActions.move; + moveAction.invoke([childObject, parentObject]); }); afterEach(() => { - return resetApplicationState(openmct); + unObserve(); }); - it("should be defined", () => { - expect(moveAction).toBeDefined(); + it("the child object's identifier should be in the new parent's composition", () => { + let newParentChild = anotherParentObject.composition[0]; + expect(newParentChild).toEqual(childObject.identifier); }); - describe("when determining the object is applicable", () => { - - beforeEach(() => { - spyOn(moveAction, 'appliesTo').and.callThrough(); - }); - - it("should be true when the parent is creatable and has composition", () => { - let applies = moveAction.appliesTo([childObject, parentObject]); - expect(applies).toBe(true); - }); - - it("should be true when the child is locked and not an alias", () => { - childObject.locked = true; - let applies = moveAction.appliesTo([childObject, parentObject]); - expect(applies).toBe(true); - }); - - it("should still be true when the child is locked and is an alias", () => { - childObject.locked = true; - childObject.location = 'another-parent-folder-object'; - let applies = moveAction.appliesTo([childObject, parentObject]); - expect(applies).toBe(true); - }); - }); - - describe("when moving an object to a new parent and removing from the old parent", () => { - let unObserve; - beforeEach((done) => { - openmct.router.path = []; - - spyOn(openmct.objects, "save"); - openmct.objects.save.and.callThrough(); - spyOn(openmct.forms, "showForm"); - openmct.forms.showForm.and.callFake(formStructure => { - return Promise.resolve({ - name: 'test', - location: [anotherParentObject] - }); - }); - - unObserve = openmct.objects.observe(parentObject, '*', (newObject) => { - done(); - }); - - moveAction.inNavigationPath = () => false; - - 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); - }); - - it("the child object's identifier should be removed from the old parent's composition", () => { - let oldParentComposition = parentObject.composition; - expect(oldParentComposition.length).toEqual(0); - }); + it("the child object's identifier should be removed from the old parent's composition", () => { + let oldParentComposition = parentObject.composition; + expect(oldParentComposition.length).toEqual(0); }); + }); }); diff --git a/src/plugins/myItems/createMyItemsIdentifier.js b/src/plugins/myItems/createMyItemsIdentifier.js index 12139589db..16c1093b37 100644 --- a/src/plugins/myItems/createMyItemsIdentifier.js +++ b/src/plugins/myItems/createMyItemsIdentifier.js @@ -1,8 +1,8 @@ export const MY_ITEMS_KEY = 'mine'; export function createMyItemsIdentifier(namespace = '') { - return { - key: MY_ITEMS_KEY, - namespace - }; + return { + key: MY_ITEMS_KEY, + namespace + }; } diff --git a/src/plugins/myItems/myItemsInterceptor.js b/src/plugins/myItems/myItemsInterceptor.js index 9ab7c70f52..da07d9916c 100644 --- a/src/plugins/myItems/myItemsInterceptor.js +++ b/src/plugins/myItems/myItemsInterceptor.js @@ -20,33 +20,32 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { MY_ITEMS_KEY } from "./createMyItemsIdentifier"; +import { MY_ITEMS_KEY } from './createMyItemsIdentifier'; function myItemsInterceptor(openmct, identifierObject, name) { + const myItemsModel = { + identifier: identifierObject, + name, + type: 'folder', + composition: [], + location: 'ROOT' + }; - const myItemsModel = { - identifier: identifierObject, - name, - type: "folder", - composition: [], - location: "ROOT" - }; + return { + appliesTo: (identifier) => { + return identifier.key === MY_ITEMS_KEY; + }, + invoke: (identifier, object) => { + if (!object || openmct.objects.isMissing(object)) { + openmct.objects.save(myItemsModel); - return { - appliesTo: (identifier) => { - return identifier.key === MY_ITEMS_KEY; - }, - invoke: (identifier, object) => { - if (!object || openmct.objects.isMissing(object)) { - openmct.objects.save(myItemsModel); + return myItemsModel; + } - return myItemsModel; - } - - return object; - }, - priority: openmct.priority.HIGH - }; + return object; + }, + priority: openmct.priority.HIGH + }; } export default myItemsInterceptor; diff --git a/src/plugins/myItems/plugin.js b/src/plugins/myItems/plugin.js index 41e2812480..7eccdf4b49 100644 --- a/src/plugins/myItems/plugin.js +++ b/src/plugins/myItems/plugin.js @@ -20,20 +20,24 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { createMyItemsIdentifier } from "./createMyItemsIdentifier"; -import myItemsInterceptor from "./myItemsInterceptor"; +import { createMyItemsIdentifier } from './createMyItemsIdentifier'; +import myItemsInterceptor from './myItemsInterceptor'; const MY_ITEMS_DEFAULT_NAME = 'My Items'; -export default function MyItemsPlugin(name = MY_ITEMS_DEFAULT_NAME, namespace = '', priority = undefined) { - return function install(openmct) { - const identifier = createMyItemsIdentifier(namespace); +export default function MyItemsPlugin( + name = MY_ITEMS_DEFAULT_NAME, + namespace = '', + priority = undefined +) { + return function install(openmct) { + const identifier = createMyItemsIdentifier(namespace); - if (priority === undefined) { - priority = openmct.priority.LOW; - } + if (priority === undefined) { + priority = openmct.priority.LOW; + } - openmct.objects.addGetInterceptor(myItemsInterceptor(openmct, identifier, name)); - openmct.objects.addRoot(identifier, priority); - }; + openmct.objects.addGetInterceptor(myItemsInterceptor(openmct, identifier, name)); + openmct.objects.addRoot(identifier, priority); + }; } diff --git a/src/plugins/myItems/pluginSpec.js b/src/plugins/myItems/pluginSpec.js index 64a91d8640..b8d3f75f50 100644 --- a/src/plugins/myItems/pluginSpec.js +++ b/src/plugins/myItems/pluginSpec.js @@ -20,104 +20,93 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { - createOpenMct, - resetApplicationState -} from 'utils/testing'; -import { - createMyItemsIdentifier, - MY_ITEMS_KEY -} from './createMyItemsIdentifier'; +import { createOpenMct, resetApplicationState } from 'utils/testing'; +import { createMyItemsIdentifier, MY_ITEMS_KEY } from './createMyItemsIdentifier'; const MISSING_NAME = `Missing: ${MY_ITEMS_KEY}`; const DEFAULT_NAME = 'My Items'; const FANCY_NAME = 'Fancy Items'; const myItemsIdentifier = createMyItemsIdentifier(); -describe("the plugin", () => { - let openmct; - let missingObj = { - identifier: myItemsIdentifier, - type: 'unknown', - name: MISSING_NAME - }; +describe('the plugin', () => { + let openmct; + let missingObj = { + identifier: myItemsIdentifier, + type: 'unknown', + name: MISSING_NAME + }; - describe('with no arguments passed in', () => { - - beforeEach((done) => { - openmct = createOpenMct(); - openmct.install(openmct.plugins.MyItems()); - - openmct.on('start', done); - openmct.startHeadless(); - }); - - afterEach(() => { - return resetApplicationState(openmct); - }); - - it('when installed, adds "My Items" to the root', async () => { - const root = await openmct.objects.get('ROOT'); - const rootCompostionCollection = openmct.composition.get(root); - const rootCompostion = await rootCompostionCollection.load(); - let myItems = rootCompostion.filter((domainObject) => { - return openmct.objects.areIdsEqual(domainObject.identifier, myItemsIdentifier); - })[0]; - - expect(myItems.name).toBe(DEFAULT_NAME); - expect(myItems).toBeDefined(); - }); - - describe('adds an interceptor that returns a "My Items" model for', () => { - let myItemsObject; - let mockNotFoundProvider; - let activeProvider; - - beforeEach(async () => { - mockNotFoundProvider = { - get: () => Promise.reject(new Error('Not found')), - create: () => Promise.resolve(missingObj), - update: () => Promise.resolve(missingObj) - }; - - activeProvider = mockNotFoundProvider; - spyOn(openmct.objects, 'getProvider').and.returnValue(activeProvider); - myItemsObject = await openmct.objects.get(myItemsIdentifier); - }); - - it('missing objects', () => { - let idsMatch = openmct.objects.areIdsEqual(myItemsObject.identifier, myItemsIdentifier); - - expect(myItemsObject).toBeDefined(); - expect(idsMatch).toBeTrue(); - }); - }); + describe('with no arguments passed in', () => { + beforeEach((done) => { + openmct = createOpenMct(); + openmct.install(openmct.plugins.MyItems()); + openmct.on('start', done); + openmct.startHeadless(); }); - describe('with a name argument passed in', () => { - - beforeEach((done) => { - openmct = createOpenMct(); - openmct.install(openmct.plugins.MyItems(FANCY_NAME)); - - spyOn(openmct.objects, 'isMissing').and.returnValue(true); - - openmct.on('start', done); - openmct.startHeadless(); - }); - - afterEach(() => { - return resetApplicationState(openmct); - }); - - it('when installed, uses the passed in name', async () => { - let myItems = await openmct.objects.get(myItemsIdentifier); - - expect(myItems.name).toBe(FANCY_NAME); - expect(myItems).toBeDefined(); - }); - + afterEach(() => { + return resetApplicationState(openmct); }); + it('when installed, adds "My Items" to the root', async () => { + const root = await openmct.objects.get('ROOT'); + const rootCompostionCollection = openmct.composition.get(root); + const rootCompostion = await rootCompostionCollection.load(); + let myItems = rootCompostion.filter((domainObject) => { + return openmct.objects.areIdsEqual(domainObject.identifier, myItemsIdentifier); + })[0]; + + expect(myItems.name).toBe(DEFAULT_NAME); + expect(myItems).toBeDefined(); + }); + + describe('adds an interceptor that returns a "My Items" model for', () => { + let myItemsObject; + let mockNotFoundProvider; + let activeProvider; + + beforeEach(async () => { + mockNotFoundProvider = { + get: () => Promise.reject(new Error('Not found')), + create: () => Promise.resolve(missingObj), + update: () => Promise.resolve(missingObj) + }; + + activeProvider = mockNotFoundProvider; + spyOn(openmct.objects, 'getProvider').and.returnValue(activeProvider); + myItemsObject = await openmct.objects.get(myItemsIdentifier); + }); + + it('missing objects', () => { + let idsMatch = openmct.objects.areIdsEqual(myItemsObject.identifier, myItemsIdentifier); + + expect(myItemsObject).toBeDefined(); + expect(idsMatch).toBeTrue(); + }); + }); + }); + + describe('with a name argument passed in', () => { + beforeEach((done) => { + openmct = createOpenMct(); + openmct.install(openmct.plugins.MyItems(FANCY_NAME)); + + spyOn(openmct.objects, 'isMissing').and.returnValue(true); + + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('when installed, uses the passed in name', async () => { + let myItems = await openmct.objects.get(myItemsIdentifier); + + expect(myItems.name).toBe(FANCY_NAME); + expect(myItems).toBeDefined(); + }); + }); }); diff --git a/src/plugins/newFolderAction/newFolderAction.js b/src/plugins/newFolderAction/newFolderAction.js index fa4f74603e..16fc86d40b 100644 --- a/src/plugins/newFolderAction/newFolderAction.js +++ b/src/plugins/newFolderAction/newFolderAction.js @@ -22,28 +22,28 @@ import CreateAction from '@/plugins/formActions/CreateAction'; export default class NewFolderAction { - constructor(openmct) { - this.type = 'folder'; - this.name = 'Add New Folder'; - this.key = 'newFolder'; - this.description = 'Create a new folder'; - this.cssClass = 'icon-folder-new'; - this.group = "action"; - this.priority = 9; + constructor(openmct) { + this.type = 'folder'; + this.name = 'Add New Folder'; + this.key = 'newFolder'; + this.description = 'Create a new folder'; + this.cssClass = 'icon-folder-new'; + this.group = 'action'; + this.priority = 9; - this._openmct = openmct; - } + this._openmct = openmct; + } - invoke(objectPath) { - const parentDomainObject = objectPath[0]; - const createAction = new CreateAction(this._openmct, this.type, parentDomainObject); - createAction.invoke(); - } + invoke(objectPath) { + const parentDomainObject = objectPath[0]; + const createAction = new CreateAction(this._openmct, this.type, parentDomainObject); + createAction.invoke(); + } - appliesTo(objectPath) { - let domainObject = objectPath[0]; - let isPersistable = this._openmct.objects.isPersistable(domainObject.identifier); + appliesTo(objectPath) { + let domainObject = objectPath[0]; + let isPersistable = this._openmct.objects.isPersistable(domainObject.identifier); - return domainObject.type === this.type && isPersistable; - } + return domainObject.type === this.type && isPersistable; + } } diff --git a/src/plugins/newFolderAction/plugin.js b/src/plugins/newFolderAction/plugin.js index 6a888c9399..3393902ebc 100644 --- a/src/plugins/newFolderAction/plugin.js +++ b/src/plugins/newFolderAction/plugin.js @@ -22,7 +22,7 @@ import NewFolderAction from './newFolderAction'; export default function () { - return function (openmct) { - openmct.actions.register(new NewFolderAction(openmct)); - }; + return function (openmct) { + openmct.actions.register(new NewFolderAction(openmct)); + }; } diff --git a/src/plugins/newFolderAction/pluginSpec.js b/src/plugins/newFolderAction/pluginSpec.js index 74f4076134..40aaf14acf 100644 --- a/src/plugins/newFolderAction/pluginSpec.js +++ b/src/plugins/newFolderAction/pluginSpec.js @@ -19,98 +19,97 @@ * 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 { createOpenMct, resetApplicationState } from 'utils/testing'; -describe("the plugin", () => { - let openmct; - let newFolderAction; +describe('the plugin', () => { + let openmct; + let newFolderAction; + beforeEach((done) => { + openmct = createOpenMct(); + + openmct.on('start', done); + openmct.startHeadless(); + + newFolderAction = openmct.actions._allActions.newFolder; + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('installs the new folder action', () => { + expect(newFolderAction).toBeDefined(); + }); + + describe('when invoked', () => { + let parentObject; + let parentObjectPath; + let changedParentObject; + let unobserve; beforeEach((done) => { - openmct = createOpenMct(); + parentObject = { + name: 'mock folder', + type: 'folder', + identifier: { + key: 'mock-folder', + namespace: '' + }, + composition: [] + }; + parentObjectPath = [parentObject]; - openmct.on('start', done); - openmct.startHeadless(); + spyOn(openmct.objects, 'save'); + openmct.objects.save.and.callThrough(); - newFolderAction = openmct.actions._allActions.newFolder; + spyOn(openmct.forms, 'showForm'); + openmct.forms.showForm.and.callFake((formStructure) => { + return Promise.resolve({ + name: 'test', + notes: 'test notes', + location: parentObjectPath + }); + }); + + unobserve = openmct.objects.observe(parentObject, '*', (newObject) => { + changedParentObject = newObject; + + done(); + }); + + newFolderAction.invoke(parentObjectPath); }); - afterEach(() => { - return resetApplicationState(openmct); + unobserve(); }); - it('installs the new folder action', () => { - expect(newFolderAction).toBeDefined(); + it('creates a new folder object', () => { + expect(openmct.objects.save).toHaveBeenCalled(); }); - describe('when invoked', () => { - let parentObject; - let parentObjectPath; - let changedParentObject; - let unobserve; - beforeEach((done) => { - parentObject = { - name: 'mock folder', - type: 'folder', - identifier: { - key: 'mock-folder', - namespace: '' - }, - composition: [] - }; - parentObjectPath = [parentObject]; + it('adds new folder object to parent composition', () => { + const composition = changedParentObject.composition; - 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 - }); - }); - - unobserve = openmct.objects.observe(parentObject, '*', (newObject) => { - changedParentObject = newObject; - - done(); - }); - - newFolderAction.invoke(parentObjectPath); - }); - afterEach(() => { - unobserve(); - }); - - it('creates a new folder object', () => { - expect(openmct.objects.save).toHaveBeenCalled(); - }); - - it('adds new folder object to parent composition', () => { - const composition = changedParentObject.composition; - - expect(composition.length).toBe(1); - }); - - it('checks if the domainObject is persistable', () => { - const mockObjectPath = [{ - name: 'mock folder', - type: 'folder', - identifier: { - key: 'mock-folder', - namespace: '' - } - }]; - - spyOn(openmct.objects, 'isPersistable').and.returnValue(true); - - newFolderAction.appliesTo(mockObjectPath); - - expect(openmct.objects.isPersistable).toHaveBeenCalled(); - }); + expect(composition.length).toBe(1); }); + + it('checks if the domainObject is persistable', () => { + const mockObjectPath = [ + { + name: 'mock folder', + type: 'folder', + identifier: { + key: 'mock-folder', + namespace: '' + } + } + ]; + + spyOn(openmct.objects, 'isPersistable').and.returnValue(true); + + newFolderAction.appliesTo(mockObjectPath); + + expect(openmct.objects.isPersistable).toHaveBeenCalled(); + }); + }); }); diff --git a/src/plugins/notebook/NotebookType.js b/src/plugins/notebook/NotebookType.js index bda269d3dc..c3af8b4c9f 100644 --- a/src/plugins/notebook/NotebookType.js +++ b/src/plugins/notebook/NotebookType.js @@ -23,66 +23,57 @@ 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" - ] - } - ]; - } + 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' - }; - } + 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 index b9c193cc8f..831e5b3fa9 100644 --- a/src/plugins/notebook/NotebookViewProvider.js +++ b/src/plugins/notebook/NotebookViewProvider.js @@ -25,51 +25,51 @@ import Notebook from './components/Notebook.vue'; import Agent from '@/utils/agent/Agent'; export default class NotebookViewProvider { - constructor(openmct, name, key, type, cssClass, snapshotContainer, entryUrlWhitelist) { - this.openmct = openmct; - this.key = key; - this.name = `${name} View`; - this.type = type; - this.cssClass = cssClass; - this.snapshotContainer = snapshotContainer; - this.entryUrlWhitelist = entryUrlWhitelist; - } + constructor(openmct, name, key, type, cssClass, snapshotContainer, entryUrlWhitelist) { + this.openmct = openmct; + this.key = key; + this.name = `${name} View`; + this.type = type; + this.cssClass = cssClass; + this.snapshotContainer = snapshotContainer; + this.entryUrlWhitelist = entryUrlWhitelist; + } - canView(domainObject) { - return domainObject.type === this.type; - } + canView(domainObject) { + return domainObject.type === this.type; + } - view(domainObject) { - let component; - let openmct = this.openmct; - let snapshotContainer = this.snapshotContainer; - let agent = new Agent(window); - let entryUrlWhitelist = this.entryUrlWhitelist; + view(domainObject) { + let component; + let openmct = this.openmct; + let snapshotContainer = this.snapshotContainer; + let agent = new Agent(window); + let entryUrlWhitelist = this.entryUrlWhitelist; - return { - show(container) { - component = new Vue({ - el: container, - components: { - Notebook - }, - provide: { - openmct, - snapshotContainer, - agent, - entryUrlWhitelist - }, - data() { - return { - domainObject - }; - }, - template: '' - }); - }, - destroy() { - component.$destroy(); - } - }; - } + return { + show(container) { + component = new Vue({ + el: container, + components: { + Notebook + }, + provide: { + openmct, + snapshotContainer, + agent, + entryUrlWhitelist + }, + data() { + return { + domainObject + }; + }, + template: '' + }); + }, + destroy() { + component.$destroy(); + } + }; + } } diff --git a/src/plugins/notebook/actions/CopyToNotebookAction.js b/src/plugins/notebook/actions/CopyToNotebookAction.js index 51ed80a655..709daabac3 100644 --- a/src/plugins/notebook/actions/CopyToNotebookAction.js +++ b/src/plugins/notebook/actions/CopyToNotebookAction.js @@ -2,48 +2,50 @@ import { getDefaultNotebook, getNotebookSectionAndPage } from '../utils/notebook import { addNotebookEntry } from '../utils/notebook-entries'; export default class CopyToNotebookAction { - constructor(openmct) { - this.openmct = openmct; + constructor(openmct) { + this.openmct = openmct; - this.cssClass = 'icon-duplicate'; - this.description = 'Copy value to notebook as an entry'; - this.group = "action"; - this.key = 'copyToNotebook'; - this.name = 'Copy to Notebook'; - this.priority = 1; + this.cssClass = 'icon-duplicate'; + this.description = 'Copy value to notebook as an entry'; + this.group = 'action'; + this.key = 'copyToNotebook'; + this.name = 'Copy to Notebook'; + this.priority = 1; + } + + copyToNotebook(entryText) { + const notebookStorage = getDefaultNotebook(); + this.openmct.objects.get(notebookStorage.identifier).then((domainObject) => { + addNotebookEntry(this.openmct, domainObject, notebookStorage, null, entryText); + + const { section, page } = getNotebookSectionAndPage( + domainObject, + notebookStorage.defaultSectionId, + notebookStorage.defaultPageId + ); + if (!section || !page) { + return; + } + + const defaultPath = `${domainObject.name} - ${section.name} - ${page.name}`; + const msg = `Saved to Notebook ${defaultPath}`; + this.openmct.notifications.info(msg); + }); + } + + invoke(objectPath, view) { + const formattedValueForCopy = view.getViewContext().row.formattedValueForCopy; + + this.copyToNotebook(formattedValueForCopy()); + } + + appliesTo(objectPath, view = {}) { + const viewContext = view.getViewContext && view.getViewContext(); + const row = viewContext && viewContext.row; + if (!row) { + return; } - copyToNotebook(entryText) { - const notebookStorage = getDefaultNotebook(); - this.openmct.objects.get(notebookStorage.identifier) - .then(domainObject => { - addNotebookEntry(this.openmct, domainObject, notebookStorage, null, entryText); - - const { section, page } = getNotebookSectionAndPage(domainObject, notebookStorage.defaultSectionId, notebookStorage.defaultPageId); - if (!section || !page) { - return; - } - - const defaultPath = `${domainObject.name} - ${section.name} - ${page.name}`; - const msg = `Saved to Notebook ${defaultPath}`; - this.openmct.notifications.info(msg); - }); - } - - invoke(objectPath, view) { - const formattedValueForCopy = view.getViewContext().row.formattedValueForCopy; - - this.copyToNotebook(formattedValueForCopy()); - } - - appliesTo(objectPath, view = {}) { - const viewContext = view.getViewContext && view.getViewContext(); - const row = viewContext && viewContext.row; - if (!row) { - return; - } - - return row.formattedValueForCopy - && typeof row.formattedValueForCopy === 'function'; - } + return row.formattedValueForCopy && typeof row.formattedValueForCopy === 'function'; + } } diff --git a/src/plugins/notebook/actions/ExportNotebookAsTextAction.js b/src/plugins/notebook/actions/ExportNotebookAsTextAction.js index 7f0f692e3d..42088cce43 100644 --- a/src/plugins/notebook/actions/ExportNotebookAsTextAction.js +++ b/src/plugins/notebook/actions/ExportNotebookAsTextAction.js @@ -1,167 +1,176 @@ -import {saveAs} from 'saveAs'; +import { saveAs } from 'saveAs'; import Moment from 'moment'; -import {NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE} from '../notebook-constants'; +import { NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE } from '../notebook-constants'; const UNKNOWN_USER = 'Unknown'; const UNKNOWN_TIME = 'Unknown'; const ALLOWED_TYPES = [NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE]; export default class ExportNotebookAsTextAction { + constructor(openmct) { + this.openmct = openmct; - constructor(openmct) { - this.openmct = openmct; + this.cssClass = 'icon-export'; + this.description = 'Exports notebook contents as a text file'; + this.group = 'export'; + this.key = 'exportNotebookAsText'; + this.name = 'Export Notebook as Text'; + } - this.cssClass = 'icon-export'; - this.description = 'Exports notebook contents as a text file'; - this.group = "export"; - this.key = 'exportNotebookAsText'; - this.name = 'Export Notebook as Text'; + invoke(objectPath) { + this.showForm(objectPath); + } + + getTagName(tagId, availableTags) { + const foundTag = availableTags.find((tag) => tag.id === tagId); + if (foundTag) { + return foundTag.label; + } else { + return tagId; + } + } + + getTagsForEntry(entry, domainObjectKeyString, annotations) { + const foundTags = []; + annotations.forEach((annotation) => { + const target = annotation.targets?.[domainObjectKeyString]; + if (target?.entryId === entry.id) { + annotation.tags.forEach((tag) => { + if (!foundTags.includes(tag)) { + foundTags.push(tag); + } + }); + } + }); + + return foundTags; + } + + formatTimeStamp(timestamp) { + if (timestamp) { + return `${Moment.utc(timestamp).format('YYYY-MM-DD HH:mm:ss')} UTC`; + } else { + return UNKNOWN_TIME; + } + } + + appliesTo(objectPath) { + const domainObject = objectPath[0]; + + return ALLOWED_TYPES.includes(domainObject.type); + } + + async onSave(changes, objectPath) { + const availableTags = this.openmct.annotation.getAvailableTags(); + const identifier = objectPath[0].identifier; + const domainObject = await this.openmct.objects.get(identifier); + let foundAnnotations = []; + // only load annotations if there are tags + if (availableTags.length) { + foundAnnotations = await this.openmct.annotation.getAnnotations(domainObject.identifier); } - invoke(objectPath) { - this.showForm(objectPath); + let notebookAsText = `# ${domainObject.name}\n\n`; + + if (changes.exportMetaData) { + const createdTimestamp = domainObject.created; + const createdBy = this.getUserName(domainObject.createdBy); + const modifiedBy = this.getUserName(domainObject.modifiedBy); + const modifiedTimestamp = domainObject.modified ?? domainObject.created; + notebookAsText += `Created on ${this.formatTimeStamp( + createdTimestamp + )} by user ${createdBy}\n\n`; + notebookAsText += `Updated on ${this.formatTimeStamp( + modifiedTimestamp + )} by user ${modifiedBy}\n\n`; } - getTagName(tagId, availableTags) { - const foundTag = availableTags.find(tag => tag.id === tagId); - if (foundTag) { - return foundTag.label; - } else { - return tagId; + const notebookSections = domainObject.configuration.sections; + const notebookEntries = domainObject.configuration.entries; + + notebookSections.forEach((section) => { + notebookAsText += `## ${section.name}\n\n`; + + const notebookPages = section.pages; + + notebookPages.forEach((page) => { + notebookAsText += `### ${page.name}\n\n`; + + const notebookPageEntries = notebookEntries[section.id]?.[page.id]; + if (!notebookPageEntries) { + // blank page + return; } - } - getTagsForEntry(entry, domainObjectKeyString, annotations) { - const foundTags = []; - annotations.forEach(annotation => { - const target = annotation.targets?.[domainObjectKeyString]; - if (target?.entryId === entry.id) { - annotation.tags.forEach(tag => { - if (!foundTags.includes(tag)) { - foundTags.push(tag); - } - }); + notebookPageEntries.forEach((entry) => { + if (changes.exportMetaData) { + const createdTimestamp = entry.createdOn; + const createdBy = this.getUserName(entry.createdBy); + const modifiedBy = this.getUserName(entry.modifiedBy); + const modifiedTimestamp = entry.modified ?? entry.created; + notebookAsText += `Created on ${this.formatTimeStamp( + createdTimestamp + )} by user ${createdBy}\n\n`; + notebookAsText += `Updated on ${this.formatTimeStamp( + modifiedTimestamp + )} by user ${modifiedBy}\n\n`; + } + + if (changes.exportTags) { + const domainObjectKeyString = this.openmct.objects.makeKeyString( + domainObject.identifier + ); + const tags = this.getTagsForEntry(entry, domainObjectKeyString, foundAnnotations); + const tagNames = tags.map((tag) => this.getTagName(tag, availableTags)); + if (tagNames) { + notebookAsText += `Tags: ${tagNames.join(', ')}\n\n`; } + } + + notebookAsText += `${entry.text}\n\n`; }); + }); + }); - return foundTags; + const blob = new Blob([notebookAsText], { type: 'text/markdown' }); + const fileName = domainObject.name + '.md'; + saveAs(blob, fileName); + } + + getUserName(userId) { + if (userId && userId.length) { + return userId; } - formatTimeStamp(timestamp) { - if (timestamp) { - return `${Moment.utc(timestamp).format('YYYY-MM-DD HH:mm:ss')} UTC`; - } else { - return UNKNOWN_TIME; + return UNKNOWN_USER; + } + + async showForm(objectPath) { + const formStructure = { + title: 'Export Notebook Text', + sections: [ + { + rows: [ + { + key: 'exportMetaData', + control: 'toggleSwitch', + name: 'Include Metadata (created/modified, etc.)', + required: true, + value: false + }, + { + name: 'Include Tags', + control: 'toggleSwitch', + required: true, + key: 'exportTags', + value: false + } + ] } - } + ] + }; - appliesTo(objectPath) { - const domainObject = objectPath[0]; + const changes = await this.openmct.forms.showForm(formStructure); - return ALLOWED_TYPES.includes(domainObject.type); - } - - async onSave(changes, objectPath) { - const availableTags = this.openmct.annotation.getAvailableTags(); - const identifier = objectPath[0].identifier; - const domainObject = await this.openmct.objects.get(identifier); - let foundAnnotations = []; - // only load annotations if there are tags - if (availableTags.length) { - foundAnnotations = await this.openmct.annotation.getAnnotations(domainObject.identifier); - } - - let notebookAsText = `# ${domainObject.name}\n\n`; - - if (changes.exportMetaData) { - const createdTimestamp = domainObject.created; - const createdBy = this.getUserName(domainObject.createdBy); - const modifiedBy = this.getUserName(domainObject.modifiedBy); - const modifiedTimestamp = domainObject.modified ?? domainObject.created; - notebookAsText += `Created on ${this.formatTimeStamp(createdTimestamp)} by user ${createdBy}\n\n`; - notebookAsText += `Updated on ${this.formatTimeStamp(modifiedTimestamp)} by user ${modifiedBy}\n\n`; - } - - const notebookSections = domainObject.configuration.sections; - const notebookEntries = domainObject.configuration.entries; - - notebookSections.forEach(section => { - notebookAsText += `## ${section.name}\n\n`; - - const notebookPages = section.pages; - - notebookPages.forEach(page => { - notebookAsText += `### ${page.name}\n\n`; - - const notebookPageEntries = notebookEntries[section.id]?.[page.id]; - if (!notebookPageEntries) { - // blank page - return; - } - - notebookPageEntries.forEach(entry => { - if (changes.exportMetaData) { - const createdTimestamp = entry.createdOn; - const createdBy = this.getUserName(entry.createdBy); - const modifiedBy = this.getUserName(entry.modifiedBy); - const modifiedTimestamp = entry.modified ?? entry.created; - notebookAsText += `Created on ${this.formatTimeStamp(createdTimestamp)} by user ${createdBy}\n\n`; - notebookAsText += `Updated on ${this.formatTimeStamp(modifiedTimestamp)} by user ${modifiedBy}\n\n`; - } - - if (changes.exportTags) { - const domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier); - const tags = this.getTagsForEntry(entry, domainObjectKeyString, foundAnnotations); - const tagNames = tags.map(tag => this.getTagName(tag, availableTags)); - if (tagNames) { - notebookAsText += `Tags: ${tagNames.join(', ')}\n\n`; - } - } - - notebookAsText += `${entry.text}\n\n`; - }); - }); - }); - - const blob = new Blob([notebookAsText], {type: "text/markdown"}); - const fileName = domainObject.name + '.md'; - saveAs(blob, fileName); - } - - getUserName(userId) { - if (userId && userId.length) { - return userId; - } - - return UNKNOWN_USER; - } - - async showForm(objectPath) { - const formStructure = { - title: "Export Notebook Text", - sections: [ - { - rows: [ - { - key: "exportMetaData", - control: "toggleSwitch", - name: "Include Metadata (created/modified, etc.)", - required: true, - value: false - }, - { - name: "Include Tags", - control: "toggleSwitch", - required: true, - key: 'exportTags', - value: false - } - ] - } - ] - }; - - const changes = await this.openmct.forms.showForm(formStructure); - - return this.onSave(changes, objectPath); - } + return this.onSave(changes, objectPath); + } } diff --git a/src/plugins/notebook/components/MenuItems.vue b/src/plugins/notebook/components/MenuItems.vue index e755bc1523..9ff7ecceb5 100644 --- a/src/plugins/notebook/components/MenuItems.vue +++ b/src/plugins/notebook/components/MenuItems.vue @@ -20,23 +20,23 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/notebook/components/Notebook.vue b/src/plugins/notebook/components/Notebook.vue index f3ff457f5c..52cb66a69e 100644 --- a/src/plugins/notebook/components/Notebook.vue +++ b/src/plugins/notebook/components/Notebook.vue @@ -21,169 +21,134 @@ --> diff --git a/src/plugins/notebook/components/NotebookEmbed.vue b/src/plugins/notebook/components/NotebookEmbed.vue index 8f2429eeb1..fea5cba485 100644 --- a/src/plugins/notebook/components/NotebookEmbed.vue +++ b/src/plugins/notebook/components/NotebookEmbed.vue @@ -20,32 +20,26 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/notebook/components/NotebookEntry.vue b/src/plugins/notebook/components/NotebookEntry.vue index ecc7808326..a302bb85f7 100644 --- a/src/plugins/notebook/components/NotebookEntry.vue +++ b/src/plugins/notebook/components/NotebookEntry.vue @@ -22,150 +22,126 @@ --> diff --git a/src/plugins/notebook/components/NotebookMenuSwitcher.vue b/src/plugins/notebook/components/NotebookMenuSwitcher.vue index a649a62d36..ddf267644e 100644 --- a/src/plugins/notebook/components/NotebookMenuSwitcher.vue +++ b/src/plugins/notebook/components/NotebookMenuSwitcher.vue @@ -20,20 +20,15 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/notebook/components/NotebookSnapshotContainer.vue b/src/plugins/notebook/components/NotebookSnapshotContainer.vue index 8f23818e9d..1a738b8c74 100644 --- a/src/plugins/notebook/components/NotebookSnapshotContainer.vue +++ b/src/plugins/notebook/components/NotebookSnapshotContainer.vue @@ -20,59 +20,46 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/notebook/components/NotebookSnapshotIndicator.vue b/src/plugins/notebook/components/NotebookSnapshotIndicator.vue index 3dbdc488e0..5b4f75e038 100644 --- a/src/plugins/notebook/components/NotebookSnapshotIndicator.vue +++ b/src/plugins/notebook/components/NotebookSnapshotIndicator.vue @@ -20,23 +20,23 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/notebook/components/PageCollection.vue b/src/plugins/notebook/components/PageCollection.vue index 5aceb8015c..ad849b2da0 100644 --- a/src/plugins/notebook/components/PageCollection.vue +++ b/src/plugins/notebook/components/PageCollection.vue @@ -20,24 +20,20 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/notebook/components/PageComponent.vue b/src/plugins/notebook/components/PageComponent.vue index 0e99a4a589..69a607e302 100644 --- a/src/plugins/notebook/components/PageComponent.vue +++ b/src/plugins/notebook/components/PageComponent.vue @@ -20,38 +20,42 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/notebook/components/PopupMenu.vue b/src/plugins/notebook/components/PopupMenu.vue index 3f35fe692a..dbf725e638 100644 --- a/src/plugins/notebook/components/PopupMenu.vue +++ b/src/plugins/notebook/components/PopupMenu.vue @@ -20,12 +20,11 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/notebook/components/SearchResults.vue b/src/plugins/notebook/components/SearchResults.vue index 0e67b44e11..8b09489cb3 100644 --- a/src/plugins/notebook/components/SearchResults.vue +++ b/src/plugins/notebook/components/SearchResults.vue @@ -21,63 +21,63 @@ --> diff --git a/src/plugins/notebook/components/SectionCollection.vue b/src/plugins/notebook/components/SectionCollection.vue index 9c8edeedf1..362c00e4d6 100644 --- a/src/plugins/notebook/components/SectionCollection.vue +++ b/src/plugins/notebook/components/SectionCollection.vue @@ -20,24 +20,20 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/notebook/components/SectionComponent.vue b/src/plugins/notebook/components/SectionComponent.vue index 8ab17487e6..d5c72bea45 100644 --- a/src/plugins/notebook/components/SectionComponent.vue +++ b/src/plugins/notebook/components/SectionComponent.vue @@ -20,26 +20,24 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/notebook/components/Sidebar.vue b/src/plugins/notebook/components/Sidebar.vue index 8653bc215c..0975b888b6 100644 --- a/src/plugins/notebook/components/Sidebar.vue +++ b/src/plugins/notebook/components/Sidebar.vue @@ -20,74 +20,71 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/notebook/components/sidebar.scss b/src/plugins/notebook/components/sidebar.scss index 9ab0ea270f..b3dacc62fb 100644 --- a/src/plugins/notebook/components/sidebar.scss +++ b/src/plugins/notebook/components/sidebar.scss @@ -1,94 +1,94 @@ .c-sidebar { - @include userSelectNone(); + @include userSelectNone(); + background: $sideBarBg; + display: flex; + justify-content: stretch; + max-width: 600px; + + &.c-drawer--push.is-expanded { + margin-right: $interiorMargin; + width: 30%; + } + + &.c-drawer--overlays.is-expanded { + width: 95%; + } + + &__pane { background: $sideBarBg; display: flex; - justify-content: stretch; - max-width: 600px; + flex: 1 1 50%; + flex-direction: column; - &.c-drawer--push.is-expanded { - margin-right: $interiorMargin; - width: 30%; + + * { + margin-left: $interiorMarginSm; } - &.c-drawer--overlays.is-expanded { - width: 95%; + > * + * { + // Add margin-top to first and second level children + margin-top: $interiorMargin; + } + } + + &__right-edge { + flex: 0 0 auto; + padding: $interiorMarginSm; + } + + &__header-w { + // Wraps header, used for page pane with collapse buttons + display: flex; + flex: 0 0 auto; + background: $sideBarHeaderBg; + align-items: center; + } + + &__header { + color: $sideBarHeaderFg; + display: flex; + align-items: center; + flex: 1 1 auto; + padding: $interiorMarginSm $interiorMargin; + text-transform: uppercase; + + &-label { + @include ellipsize(); + flex: 1 1 auto; + } + } + + &__contents-and-controls { + // Encloses pane buttons and contents elements + display: flex; + flex-direction: column; + flex: 1 1 auto; + + > * + * { + margin-top: $interiorMargin; + } + } + + &__contents { + flex: 1 1 auto; + overflow-x: hidden; + overflow-y: auto; + padding: auto $interiorMargin; + } + + .c-list__item { + > * + * { + margin-left: $interiorMargin; } - &__pane { - background: $sideBarBg; - display: flex; - flex: 1 1 50%; - flex-direction: column; - - + * { - margin-left: $interiorMarginSm; - } - - > * + * { - // Add margin-top to first and second level children - margin-top: $interiorMargin; - } + &__name { + flex: 1 1 auto; } - &__right-edge { - flex: 0 0 auto; - padding: $interiorMarginSm; - } - - &__header-w { - // Wraps header, used for page pane with collapse buttons - display: flex; - flex: 0 0 auto; - background: $sideBarHeaderBg; - align-items: center; - } - - &__header { - color: $sideBarHeaderFg; - display: flex; - align-items: center; - flex: 1 1 auto; - padding: $interiorMarginSm $interiorMargin; - text-transform: uppercase; - - &-label { - @include ellipsize(); - flex: 1 1 auto; - } - } - - &__contents-and-controls { - // Encloses pane buttons and contents elements - display: flex; - flex-direction: column; - flex: 1 1 auto; - - > * + * { - margin-top: $interiorMargin; - } - } - - &__contents { - flex: 1 1 auto; - overflow-x: hidden; - overflow-y: auto; - padding: auto $interiorMargin; - } - - .c-list__item { - > * + * { - margin-left: $interiorMargin; - } - - &__name { - flex: 1 1 auto; - } - - &__menu-indicator { - // Not sure this is being used - flex: 0 0 auto; - font-size: 0.8em; - opacity: 0; - } + &__menu-indicator { + // Not sure this is being used + flex: 0 0 auto; + font-size: 0.8em; + opacity: 0; } + } } diff --git a/src/plugins/notebook/components/snapshot-template.html b/src/plugins/notebook/components/snapshot-template.html index 4b2d44d111..8f9fd96fca 100644 --- a/src/plugins/notebook/components/snapshot-template.html +++ b/src/plugins/notebook/components/snapshot-template.html @@ -1,48 +1,42 @@
    - -
    -
    -
    - - - {{ name }} - -
    -
    - -
    + +
    +
    +
    + + + {{ name }} + +
    -
    +
    +
    SNAPSHOT {{ createdOn }}
    + + + + + + Annotate +
    +
    + +
    diff --git a/src/plugins/notebook/monkeyPatchObjectAPIForNotebooks.js b/src/plugins/notebook/monkeyPatchObjectAPIForNotebooks.js index e3c9ca863e..f840b324f9 100644 --- a/src/plugins/notebook/monkeyPatchObjectAPIForNotebooks.js +++ b/src/plugins/notebook/monkeyPatchObjectAPIForNotebooks.js @@ -2,124 +2,137 @@ import { isAnnotationType, isNotebookType, isNotebookOrAnnotationType } from './ import _ from 'lodash'; export default function (openmct) { - const apiSave = openmct.objects.save.bind(openmct.objects); + const apiSave = openmct.objects.save.bind(openmct.objects); - openmct.objects.save = async (domainObject) => { - if (!isNotebookOrAnnotationType(domainObject)) { - return apiSave(domainObject); - } + openmct.objects.save = async (domainObject) => { + if (!isNotebookOrAnnotationType(domainObject)) { + return apiSave(domainObject); + } - const isNewMutable = !domainObject.isMutable; - const localMutable = openmct.objects.toMutable(domainObject); - let result; + const isNewMutable = !domainObject.isMutable; + const localMutable = openmct.objects.toMutable(domainObject); + let result; - try { - result = await apiSave(localMutable); - } catch (error) { - if (error instanceof openmct.objects.errors.Conflict) { - result = await resolveConflicts(domainObject, localMutable, openmct); - } else { - result = Promise.reject(error); - } - } finally { - if (isNewMutable) { - openmct.objects.destroyMutable(localMutable); - } - } + try { + result = await apiSave(localMutable); + } catch (error) { + if (error instanceof openmct.objects.errors.Conflict) { + result = await resolveConflicts(domainObject, localMutable, openmct); + } else { + result = Promise.reject(error); + } + } finally { + if (isNewMutable) { + openmct.objects.destroyMutable(localMutable); + } + } - return result; - }; + return result; + }; } function resolveConflicts(domainObject, localMutable, openmct) { - if (isNotebookType(domainObject)) { - return resolveNotebookEntryConflicts(localMutable, openmct); - } else if (isAnnotationType(domainObject)) { - return resolveNotebookTagConflicts(localMutable, openmct); - } + if (isNotebookType(domainObject)) { + return resolveNotebookEntryConflicts(localMutable, openmct); + } else if (isAnnotationType(domainObject)) { + return resolveNotebookTagConflicts(localMutable, openmct); + } } async function resolveNotebookTagConflicts(localAnnotation, openmct) { - const localClonedAnnotation = structuredClone(localAnnotation); - const remoteMutable = await openmct.objects.getMutable(localClonedAnnotation.identifier); + const localClonedAnnotation = structuredClone(localAnnotation); + const remoteMutable = await openmct.objects.getMutable(localClonedAnnotation.identifier); - // should only be one annotation per targetID, entryID, and tag; so for sanity, ensure we have the - // same targetID, entryID, and tags for this conflict - if (!(_.isEqual(remoteMutable.tags, localClonedAnnotation.tags))) { - throw new Error('Conflict on annotation\'s tag has different tags than remote'); + // should only be one annotation per targetID, entryID, and tag; so for sanity, ensure we have the + // same targetID, entryID, and tags for this conflict + if (!_.isEqual(remoteMutable.tags, localClonedAnnotation.tags)) { + throw new Error("Conflict on annotation's tag has different tags than remote"); + } + + Object.keys(localClonedAnnotation.targets).forEach((targetKey) => { + if (!remoteMutable.targets[targetKey]) { + throw new Error(`Conflict on annotation's target is missing ${targetKey}`); } - Object.keys(localClonedAnnotation.targets).forEach(targetKey => { - if (!remoteMutable.targets[targetKey]) { - throw new Error(`Conflict on annotation's target is missing ${targetKey}`); - } + const remoteMutableTarget = remoteMutable.targets[targetKey]; + const localMutableTarget = localClonedAnnotation.targets[targetKey]; - const remoteMutableTarget = remoteMutable.targets[targetKey]; - const localMutableTarget = localClonedAnnotation.targets[targetKey]; - - if (remoteMutableTarget.entryId !== localMutableTarget.entryId) { - throw new Error(`Conflict on annotation's entryID ${remoteMutableTarget.entryId} has a different entry Id ${localMutableTarget.entryId}`); - } - }); - - if (remoteMutable._deleted && (remoteMutable._deleted !== localClonedAnnotation._deleted)) { - // not deleting wins 😘 - openmct.objects.mutate(remoteMutable, '_deleted', false); + if (remoteMutableTarget.entryId !== localMutableTarget.entryId) { + throw new Error( + `Conflict on annotation's entryID ${remoteMutableTarget.entryId} has a different entry Id ${localMutableTarget.entryId}` + ); } + }); - openmct.objects.destroyMutable(remoteMutable); + if (remoteMutable._deleted && remoteMutable._deleted !== localClonedAnnotation._deleted) { + // not deleting wins 😘 + openmct.objects.mutate(remoteMutable, '_deleted', false); + } - return true; + openmct.objects.destroyMutable(remoteMutable); + + return true; } async function resolveNotebookEntryConflicts(localMutable, openmct) { - if (localMutable.configuration.entries) { - const FORCE_REMOTE = true; - const localEntries = structuredClone(localMutable.configuration.entries); - const remoteObject = await openmct.objects.get(localMutable.identifier, undefined, FORCE_REMOTE); + if (localMutable.configuration.entries) { + const FORCE_REMOTE = true; + const localEntries = structuredClone(localMutable.configuration.entries); + const remoteObject = await openmct.objects.get( + localMutable.identifier, + undefined, + FORCE_REMOTE + ); - return applyLocalEntries(remoteObject, localEntries, openmct); - } + return applyLocalEntries(remoteObject, localEntries, openmct); + } - return true; + return true; } function applyLocalEntries(remoteObject, entries, openmct) { - let shouldSave = false; + let shouldSave = false; - Object.entries(entries).forEach(([sectionKey, pagesInSection]) => { - Object.entries(pagesInSection).forEach(([pageKey, localEntries]) => { - const remoteEntries = remoteObject.configuration.entries[sectionKey][pageKey]; - const mergedEntries = [].concat(remoteEntries); - let shouldMutate = false; + Object.entries(entries).forEach(([sectionKey, pagesInSection]) => { + Object.entries(pagesInSection).forEach(([pageKey, localEntries]) => { + const remoteEntries = remoteObject.configuration.entries[sectionKey][pageKey]; + const mergedEntries = [].concat(remoteEntries); + let shouldMutate = false; - const locallyAddedEntries = _.differenceBy(localEntries, remoteEntries, 'id'); - const locallyModifiedEntries = _.differenceWith(localEntries, remoteEntries, (localEntry, remoteEntry) => { - return localEntry.id === remoteEntry.id && localEntry.text === remoteEntry.text; - }); + const locallyAddedEntries = _.differenceBy(localEntries, remoteEntries, 'id'); + const locallyModifiedEntries = _.differenceWith( + localEntries, + remoteEntries, + (localEntry, remoteEntry) => { + return localEntry.id === remoteEntry.id && localEntry.text === remoteEntry.text; + } + ); - locallyAddedEntries.forEach((localEntry) => { - mergedEntries.push(localEntry); - shouldMutate = true; - }); + locallyAddedEntries.forEach((localEntry) => { + mergedEntries.push(localEntry); + shouldMutate = true; + }); - locallyModifiedEntries.forEach((locallyModifiedEntry) => { - let mergedEntry = mergedEntries.find(entry => entry.id === locallyModifiedEntry.id); - if (mergedEntry !== undefined - && locallyModifiedEntry.text.match(/\S/)) { - mergedEntry.text = locallyModifiedEntry.text; - shouldMutate = true; - } - }); + locallyModifiedEntries.forEach((locallyModifiedEntry) => { + let mergedEntry = mergedEntries.find((entry) => entry.id === locallyModifiedEntry.id); + if (mergedEntry !== undefined && locallyModifiedEntry.text.match(/\S/)) { + mergedEntry.text = locallyModifiedEntry.text; + shouldMutate = true; + } + }); - if (shouldMutate) { - shouldSave = true; - openmct.objects.mutate(remoteObject, `configuration.entries.${sectionKey}.${pageKey}`, mergedEntries); - } - }); + if (shouldMutate) { + shouldSave = true; + openmct.objects.mutate( + remoteObject, + `configuration.entries.${sectionKey}.${pageKey}`, + mergedEntries + ); + } }); + }); - if (shouldSave) { - return openmct.objects.save(remoteObject); - } + if (shouldSave) { + return openmct.objects.save(remoteObject); + } } diff --git a/src/plugins/notebook/notebook-constants.js b/src/plugins/notebook/notebook-constants.js index 5af4a3239b..bfd01cabdc 100644 --- a/src/plugins/notebook/notebook-constants.js +++ b/src/plugins/notebook/notebook-constants.js @@ -10,17 +10,17 @@ export const NOTEBOOK_BASE_INSTALLED = '_NOTEBOOK_BASE_FUNCTIONALITY_INSTALLED'; // these only deals with constants, figured this could skip going into a utils file export function isNotebookOrAnnotationType(domainObject) { - return (isNotebookType(domainObject) || isAnnotationType(domainObject)); + return isNotebookType(domainObject) || isAnnotationType(domainObject); } export function isNotebookType(domainObject) { - return [NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE].includes(domainObject.type); + return [NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE].includes(domainObject.type); } export function isAnnotationType(domainObject) { - return [ANNOTATION_TYPE].includes(domainObject.type); + return [ANNOTATION_TYPE].includes(domainObject.type); } export function isNotebookViewType(view) { - return [NOTEBOOK_VIEW_TYPE, RESTRICTED_NOTEBOOK_VIEW_TYPE].includes(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 72281bab8d..949873be78 100644 --- a/src/plugins/notebook/plugin.js +++ b/src/plugins/notebook/plugin.js @@ -30,117 +30,131 @@ import monkeyPatchObjectAPIForNotebooks from './monkeyPatchObjectAPIForNotebooks import { notebookImageMigration } from '../notebook/utils/notebook-migration'; import { - NOTEBOOK_TYPE, - RESTRICTED_NOTEBOOK_TYPE, - NOTEBOOK_VIEW_TYPE, - RESTRICTED_NOTEBOOK_VIEW_TYPE, - NOTEBOOK_BASE_INSTALLED + NOTEBOOK_TYPE, + RESTRICTED_NOTEBOOK_TYPE, + NOTEBOOK_VIEW_TYPE, + RESTRICTED_NOTEBOOK_VIEW_TYPE, + NOTEBOOK_BASE_INSTALLED } from './notebook-constants'; import Vue from 'vue'; let notebookSnapshotContainer; function getSnapshotContainer(openmct) { - if (!notebookSnapshotContainer) { - notebookSnapshotContainer = new SnapshotContainer(openmct); - } + if (!notebookSnapshotContainer) { + notebookSnapshotContainer = new SnapshotContainer(openmct); + } - return notebookSnapshotContainer; + return notebookSnapshotContainer; } function addLegacyNotebookGetInterceptor(openmct) { - openmct.objects.addGetInterceptor({ - appliesTo: (identifier, domainObject) => { - return domainObject && domainObject.type === NOTEBOOK_TYPE; - }, - invoke: (identifier, domainObject) => { - notebookImageMigration(openmct, domainObject); + openmct.objects.addGetInterceptor({ + appliesTo: (identifier, domainObject) => { + return domainObject && domainObject.type === NOTEBOOK_TYPE; + }, + invoke: (identifier, domainObject) => { + notebookImageMigration(openmct, domainObject); - return domainObject; - } - }); + return domainObject; + } + }); } function installBaseNotebookFunctionality(openmct) { - // only need to do this once - if (openmct[NOTEBOOK_BASE_INSTALLED]) { - return; + // only need to do this once + if (openmct[NOTEBOOK_BASE_INSTALLED]) { + 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)); + openmct.actions.register(new ExportNotebookAsTextAction(openmct)); - 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)); - openmct.actions.register(new ExportNotebookAsTextAction(openmct)); + const notebookSnapshotIndicator = new Vue({ + components: { + NotebookSnapshotIndicator + }, + provide: { + openmct, + snapshotContainer + }, + template: '' + }); + const indicator = { + element: notebookSnapshotIndicator.$mount().$el, + key: 'notebook-snapshot-indicator', + priority: openmct.priority.DEFAULT + }; - const notebookSnapshotIndicator = new Vue ({ - components: { - NotebookSnapshotIndicator - }, - provide: { - openmct, - snapshotContainer - }, - template: '' - }); - const indicator = { - element: notebookSnapshotIndicator.$mount().$el, - key: 'notebook-snapshot-indicator', - priority: openmct.priority.DEFAULT - }; + openmct.indicators.add(indicator); - openmct.indicators.add(indicator); + monkeyPatchObjectAPIForNotebooks(openmct); - monkeyPatchObjectAPIForNotebooks(openmct); - - openmct[NOTEBOOK_BASE_INSTALLED] = true; + openmct[NOTEBOOK_BASE_INSTALLED] = true; } function NotebookPlugin(name = 'Notebook', entryUrlWhitelist = []) { - return function install(openmct) { - const icon = 'icon-notebook'; - const description = 'Create and save timestamped notes with embedded object snapshots.'; - const snapshotContainer = getSnapshotContainer(openmct); + return function install(openmct) { + const icon = 'icon-notebook'; + const description = 'Create and save timestamped notes with embedded object snapshots.'; + const snapshotContainer = getSnapshotContainer(openmct); - addLegacyNotebookGetInterceptor(openmct); + addLegacyNotebookGetInterceptor(openmct); - const notebookType = new NotebookType(name, description, icon); - openmct.types.addType(NOTEBOOK_TYPE, notebookType); + const notebookType = new NotebookType(name, description, icon); + openmct.types.addType(NOTEBOOK_TYPE, notebookType); - const notebookView = new NotebookViewProvider(openmct, name, NOTEBOOK_VIEW_TYPE, NOTEBOOK_TYPE, icon, snapshotContainer, entryUrlWhitelist); - openmct.objectViews.addProvider(notebookView, entryUrlWhitelist); + const notebookView = new NotebookViewProvider( + openmct, + name, + NOTEBOOK_VIEW_TYPE, + NOTEBOOK_TYPE, + icon, + snapshotContainer, + entryUrlWhitelist + ); + openmct.objectViews.addProvider(notebookView, entryUrlWhitelist); - installBaseNotebookFunctionality(openmct); - }; + installBaseNotebookFunctionality(openmct); + }; } function RestrictedNotebookPlugin(name = 'Notebook Shift Log', entryUrlWhitelist = []) { - return function install(openmct) { - 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); + return function install(openmct) { + 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 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, entryUrlWhitelist); - openmct.objectViews.addProvider(notebookView, entryUrlWhitelist); + const notebookView = new NotebookViewProvider( + openmct, + name, + RESTRICTED_NOTEBOOK_VIEW_TYPE, + RESTRICTED_NOTEBOOK_TYPE, + icon, + snapshotContainer, + entryUrlWhitelist + ); + openmct.objectViews.addProvider(notebookView, entryUrlWhitelist); - installBaseNotebookFunctionality(openmct); - }; + installBaseNotebookFunctionality(openmct); + }; } -export { - NotebookPlugin, - RestrictedNotebookPlugin -}; +export { NotebookPlugin, RestrictedNotebookPlugin }; diff --git a/src/plugins/notebook/pluginSpec.js b/src/plugins/notebook/pluginSpec.js index da370cfcd7..5bcc5cf1f1 100644 --- a/src/plugins/notebook/pluginSpec.js +++ b/src/plugins/notebook/pluginSpec.js @@ -24,394 +24,410 @@ import { createOpenMct, createMouseEvent, resetApplicationState } from 'utils/te import { NotebookPlugin } from './plugin'; import Vue from 'vue'; -describe("Notebook plugin:", () => { - let openmct; - let notebookDefinition; - let element; - let child; - let appHolder; - let objectProviderObserver; +describe('Notebook plugin:', () => { + let openmct; + let notebookDefinition; + let element; + let child; + let appHolder; + let objectProviderObserver; - let notebookDomainObject; - let originalAnnotations; + let notebookDomainObject; + let originalAnnotations; - beforeEach((done) => { - notebookDomainObject = { - identifier: { - key: 'notebook', - namespace: 'test-namespace' + beforeEach((done) => { + notebookDomainObject = { + identifier: { + key: 'notebook', + namespace: 'test-namespace' + }, + type: 'notebook' + }; + + appHolder = document.createElement('div'); + appHolder.style.width = '640px'; + appHolder.style.height = '480px'; + document.body.appendChild(appHolder); + + openmct = createOpenMct(); + + element = document.createElement('div'); + child = document.createElement('div'); + element.appendChild(child); + + 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); + + openmct.on('start', done); + openmct.start(appHolder); + }); + + afterEach(() => { + appHolder.remove(); + openmct.annotation.getNotebookAnnotation = originalAnnotations; + + return resetApplicationState(openmct); + }); + + it('has type as Notebook', () => { + expect(notebookDefinition.name).toEqual('Notebook'); + }); + + it('is creatable', () => { + expect(notebookDefinition.creatable).toEqual(true); + }); + + describe('Notebook view:', () => { + let notebookViewProvider; + let notebookView; + let notebookViewObject; + let mutableNotebookObject; + + beforeEach(async () => { + notebookViewObject = { + ...notebookDomainObject, + id: 'test-object', + name: 'Notebook', + configuration: { + defaultSort: 'oldest', + entries: { + 'test-section-1': { + 'test-page-1': [ + { + id: 'entry-0', + createdOn: 0, + text: 'First Test Entry', + embeds: [] + }, + { + id: 'entry-1', + createdOn: 0, + text: 'Second Test Entry', + embeds: [] + } + ] + } + }, + pageTitle: 'Page', + sections: [ + { + id: 'test-section-1', + isDefault: false, + isSelected: false, + name: 'Test Section', + pages: [ + { + id: 'test-page-1', + isDefault: false, + isSelected: false, + name: 'Test Page 1', + pageTitle: 'Page' + }, + { + id: 'test-page-2', + isDefault: false, + isSelected: false, + name: 'Test Page 2', + pageTitle: 'Page' + } + ] }, - type: 'notebook' - }; + { + id: 'test-section-2', + isDefault: false, + isSelected: false, + name: 'Test Section 2', + pages: [ + { + id: 'test-page-3', + isDefault: false, + isSelected: false, + name: 'Test Page 3', + pageTitle: 'Page' + } + ] + } + ], + sectionTitle: 'Section', + type: 'General' + } + }; + const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [ + 'get', + 'create', + 'update', + 'observe' + ]); - appHolder = document.createElement('div'); - appHolder.style.width = '640px'; - appHolder.style.height = '480px'; - document.body.appendChild(appHolder); + openmct.editor = {}; + openmct.editor.isEditing = () => false; - openmct = createOpenMct(); + const applicableViews = openmct.objectViews.get(notebookViewObject, [notebookViewObject]); + notebookViewProvider = applicableViews.find( + (viewProvider) => viewProvider.key === 'notebook-vue' + ); - element = document.createElement('div'); - child = document.createElement('div'); - element.appendChild(child); + testObjectProvider.get.and.returnValue(Promise.resolve(notebookViewObject)); + testObjectProvider.create.and.returnValue(Promise.resolve(notebookViewObject)); + openmct.objects.addProvider('test-namespace', testObjectProvider); + testObjectProvider.observe.and.returnValue(() => {}); + testObjectProvider.create.and.returnValue(Promise.resolve(true)); + testObjectProvider.update.and.returnValue(Promise.resolve(true)); - openmct.install(NotebookPlugin()); - originalAnnotations = openmct.annotation.getNotebookAnnotation; - // eslint-disable-next-line require-await - openmct.annotation.getNotebookAnnotation = async function () { - return null; - }; + const mutableObject = await openmct.objects.getMutable(notebookViewObject.identifier); + mutableNotebookObject = mutableObject; + objectProviderObserver = testObjectProvider.observe.calls.mostRecent().args[1]; - notebookDefinition = openmct.types.get('notebook').definition; - notebookDefinition.initialize(notebookDomainObject); + notebookView = notebookViewProvider.view(mutableNotebookObject); + notebookView.show(child); - openmct.on('start', done); - openmct.start(appHolder); + await Vue.nextTick(); }); afterEach(() => { - appHolder.remove(); - openmct.annotation.getNotebookAnnotation = originalAnnotations; - - return resetApplicationState(openmct); + notebookView.destroy(); + openmct.objects.destroyMutable(mutableNotebookObject); }); - it("has type as Notebook", () => { - expect(notebookDefinition.name).toEqual('Notebook'); + it('provides notebook view', () => { + expect(notebookViewProvider).toBeDefined(); }); - it("is creatable", () => { - expect(notebookDefinition.creatable).toEqual(true); + it('renders notebook element', () => { + const notebookElement = element.querySelectorAll('.c-notebook'); + expect(notebookElement.length).toBe(1); }); - describe("Notebook view:", () => { - let notebookViewProvider; - let notebookView; - let notebookViewObject; - let mutableNotebookObject; + it('renders major elements', () => { + const notebookElement = element.querySelector('.c-notebook'); + const searchElement = notebookElement.querySelector('.c-search'); + const sidebarElement = notebookElement.querySelector('.c-sidebar'); + const pageViewElement = notebookElement.querySelector('.c-notebook__page-view'); + const hasMajorElements = Boolean(searchElement && sidebarElement && pageViewElement); - beforeEach(async () => { - notebookViewObject = { - ...notebookDomainObject, - id: "test-object", - name: 'Notebook', - configuration: { - defaultSort: 'oldest', - entries: { - "test-section-1": { - "test-page-1": [{ - "id": "entry-0", - "createdOn": 0, - "text": "First Test Entry", - "embeds": [] - }, { - "id": "entry-1", - "createdOn": 0, - "text": "Second Test Entry", - "embeds": [] - }] - } - }, - pageTitle: 'Page', - sections: [{ - "id": "test-section-1", - "isDefault": false, - "isSelected": false, - "name": "Test Section", - "pages": [{ - "id": "test-page-1", - "isDefault": false, - "isSelected": false, - "name": "Test Page 1", - "pageTitle": "Page" - }, { - "id": "test-page-2", - "isDefault": false, - "isSelected": false, - "name": "Test Page 2", - "pageTitle": "Page" - }] - }, { - "id": "test-section-2", - "isDefault": false, - "isSelected": false, - "name": "Test Section 2", - "pages": [{ - "id": "test-page-3", - "isDefault": false, - "isSelected": false, - "name": "Test Page 3", - "pageTitle": "Page" - }] - }], - sectionTitle: 'Section', - type: 'General' - } - }; - const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [ - 'get', - 'create', - 'update', - 'observe' - ]); - - openmct.editor = {}; - openmct.editor.isEditing = () => false; - - const applicableViews = openmct.objectViews.get(notebookViewObject, [notebookViewObject]); - notebookViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'notebook-vue'); - - testObjectProvider.get.and.returnValue(Promise.resolve(notebookViewObject)); - testObjectProvider.create.and.returnValue(Promise.resolve(notebookViewObject)); - openmct.objects.addProvider('test-namespace', testObjectProvider); - testObjectProvider.observe.and.returnValue(() => {}); - testObjectProvider.create.and.returnValue(Promise.resolve(true)); - testObjectProvider.update.and.returnValue(Promise.resolve(true)); - - const mutableObject = await openmct.objects.getMutable(notebookViewObject.identifier); - mutableNotebookObject = mutableObject; - objectProviderObserver = testObjectProvider.observe.calls.mostRecent().args[1]; - - notebookView = notebookViewProvider.view(mutableNotebookObject); - notebookView.show(child); - - await Vue.nextTick(); - }); - - afterEach(() => { - notebookView.destroy(); - openmct.objects.destroyMutable(mutableNotebookObject); - }); - - it("provides notebook view", () => { - expect(notebookViewProvider).toBeDefined(); - }); - - it("renders notebook element", () => { - const notebookElement = element.querySelectorAll('.c-notebook'); - expect(notebookElement.length).toBe(1); - }); - - it("renders major elements", () => { - const notebookElement = element.querySelector('.c-notebook'); - const searchElement = notebookElement.querySelector('.c-search'); - const sidebarElement = notebookElement.querySelector('.c-sidebar'); - const pageViewElement = notebookElement.querySelector('.c-notebook__page-view'); - const hasMajorElements = Boolean(searchElement && sidebarElement && pageViewElement); - - expect(hasMajorElements).toBe(true); - }); - - it("renders a row for each entry", () => { - const notebookEntryElements = element.querySelectorAll('.c-notebook__entry'); - const firstEntryText = getEntryText(0); - expect(notebookEntryElements.length).toBe(2); - expect(firstEntryText.innerText).toBe('First Test Entry'); - }); - - 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"); - 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"); - }); - }); - - it("shows new entry when another user adds one", () => { - expect(allNotebookEntryElements().length).toBe(2); - objectCloneToSyncFrom.configuration.entries["test-section-1"]["test-page-1"].push({ - "id": "entry-3", - "createdOn": 0, - "text": "Third Test Entry", - "embeds": [] - }); - objectProviderObserver(objectCloneToSyncFrom); - - return Vue.nextTick().then(() => { - expect(allNotebookEntryElements().length).toBe(3); - }); - }); - it("removes an entry when another user removes one", () => { - expect(allNotebookEntryElements().length).toBe(2); - 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); - }); - }); - - it("updates the notebook when a user adds a page", () => { - const newPage = { - "id": "test-page-4", - "isDefault": false, - "isSelected": false, - "name": "Test Page 4", - "pageTitle": "Page" - }; - - expect(allNotebookPageElements().length).toBe(2); - objectCloneToSyncFrom.configuration.sections[0].pages.push(newPage); - objectProviderObserver(objectCloneToSyncFrom); - - return Vue.nextTick().then(() => { - expect(allNotebookPageElements().length).toBe(3); - }); - - }); - - it("updates the notebook when a user removes a page", () => { - expect(allNotebookPageElements().length).toBe(2); - objectCloneToSyncFrom.configuration.sections[0].pages.splice(0, 1); - objectProviderObserver(objectCloneToSyncFrom); - - return Vue.nextTick().then(() => { - expect(allNotebookPageElements().length).toBe(1); - }); - }); - - it("updates the notebook when a user adds a section", () => { - const newSection = { - "id": "test-section-3", - "isDefault": false, - "isSelected": false, - "name": "Test Section 3", - "pages": [{ - "id": "test-page-4", - "isDefault": false, - "isSelected": false, - "name": "Test Page 4", - "pageTitle": "Page" - }] - }; - - expect(allNotebookSectionElements().length).toBe(2); - objectCloneToSyncFrom.configuration.sections.push(newSection); - objectProviderObserver(objectCloneToSyncFrom); - - return Vue.nextTick().then(() => { - expect(allNotebookSectionElements().length).toBe(3); - }); - }); - - it("updates the notebook when a user removes a section", () => { - expect(allNotebookSectionElements().length).toBe(2); - objectCloneToSyncFrom.configuration.sections.splice(0, 1); - objectProviderObserver(objectCloneToSyncFrom); - - return Vue.nextTick().then(() => { - expect(allNotebookSectionElements().length).toBe(1); - }); - }); - }); + expect(hasMajorElements).toBe(true); }); - describe("Notebook Snapshots view:", () => { - let snapshotIndicator; - let drawerElement; + it('renders a row for each entry', () => { + const notebookEntryElements = element.querySelectorAll('.c-notebook__entry'); + const firstEntryText = getEntryText(0); + expect(notebookEntryElements.length).toBe(2); + expect(firstEntryText.innerText).toBe('First Test Entry'); + }); - function clickSnapshotIndicator() { - const indicator = element.querySelector('.icon-camera'); - const button = indicator.querySelector('button'); - const clickEvent = createMouseEvent('click'); + describe('synchronization', () => { + let objectCloneToSyncFrom; - button.dispatchEvent(clickEvent); - } + beforeEach(() => { + objectCloneToSyncFrom = structuredClone(notebookViewObject); + objectCloneToSyncFrom.persisted = notebookViewObject.modified + 1; + }); - beforeEach(() => { - snapshotIndicator = openmct.indicators.indicatorObjects - .find(indicator => indicator.key === 'notebook-snapshot-indicator').element; + it('updates an entry when another user modifies it', () => { + expect(getEntryText(0).innerText).toBe('First Test Entry'); + objectCloneToSyncFrom.configuration.entries['test-section-1']['test-page-1'][0].text = + 'Modified entry text'; + objectProviderObserver(objectCloneToSyncFrom); - element.append(snapshotIndicator); - - return Vue.nextTick().then(() => { - drawerElement = document.querySelector('.l-shell__drawer'); - }); + return Vue.nextTick().then(() => { + expect(getEntryText(0).innerText).toBe('Modified entry text'); }); + }); - afterEach(() => { - if (drawerElement) { - drawerElement.classList.remove('is-expanded'); + it('shows new entry when another user adds one', () => { + expect(allNotebookEntryElements().length).toBe(2); + objectCloneToSyncFrom.configuration.entries['test-section-1']['test-page-1'].push({ + id: 'entry-3', + createdOn: 0, + text: 'Third Test Entry', + embeds: [] + }); + objectProviderObserver(objectCloneToSyncFrom); + + return Vue.nextTick().then(() => { + expect(allNotebookEntryElements().length).toBe(3); + }); + }); + it('removes an entry when another user removes one', () => { + expect(allNotebookEntryElements().length).toBe(2); + 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); + }); + }); + + it('updates the notebook when a user adds a page', () => { + const newPage = { + id: 'test-page-4', + isDefault: false, + isSelected: false, + name: 'Test Page 4', + pageTitle: 'Page' + }; + + expect(allNotebookPageElements().length).toBe(2); + objectCloneToSyncFrom.configuration.sections[0].pages.push(newPage); + objectProviderObserver(objectCloneToSyncFrom); + + return Vue.nextTick().then(() => { + expect(allNotebookPageElements().length).toBe(3); + }); + }); + + it('updates the notebook when a user removes a page', () => { + expect(allNotebookPageElements().length).toBe(2); + objectCloneToSyncFrom.configuration.sections[0].pages.splice(0, 1); + objectProviderObserver(objectCloneToSyncFrom); + + return Vue.nextTick().then(() => { + expect(allNotebookPageElements().length).toBe(1); + }); + }); + + it('updates the notebook when a user adds a section', () => { + const newSection = { + id: 'test-section-3', + isDefault: false, + isSelected: false, + name: 'Test Section 3', + pages: [ + { + id: 'test-page-4', + isDefault: false, + isSelected: false, + name: 'Test Page 4', + pageTitle: 'Page' } + ] + }; - snapshotIndicator.remove(); - snapshotIndicator = undefined; + expect(allNotebookSectionElements().length).toBe(2); + objectCloneToSyncFrom.configuration.sections.push(newSection); + objectProviderObserver(objectCloneToSyncFrom); - if (drawerElement) { - drawerElement.remove(); - drawerElement = undefined; - } + return Vue.nextTick().then(() => { + expect(allNotebookSectionElements().length).toBe(3); }); + }); - it("has Snapshots indicator", () => { - const hasSnapshotIndicator = snapshotIndicator !== null && snapshotIndicator !== undefined; - expect(hasSnapshotIndicator).toBe(true); + it('updates the notebook when a user removes a section', () => { + expect(allNotebookSectionElements().length).toBe(2); + objectCloneToSyncFrom.configuration.sections.splice(0, 1); + objectProviderObserver(objectCloneToSyncFrom); + + return Vue.nextTick().then(() => { + expect(allNotebookSectionElements().length).toBe(1); }); + }); + }); + }); - it("snapshots container has class isExpanded", () => { - let classes = drawerElement.classList; - const isExpandedBefore = classes.contains('is-expanded'); + describe('Notebook Snapshots view:', () => { + let snapshotIndicator; + let drawerElement; - clickSnapshotIndicator(); - classes = drawerElement.classList; - const isExpandedAfterFirstClick = classes.contains('is-expanded'); + function clickSnapshotIndicator() { + const indicator = element.querySelector('.icon-camera'); + const button = indicator.querySelector('button'); + const clickEvent = createMouseEvent('click'); - expect(isExpandedBefore).toBeFalse(); - expect(isExpandedAfterFirstClick).toBeTrue(); - }); + button.dispatchEvent(clickEvent); + } - it("snapshots container does not have class isExpanded", () => { - let classes = drawerElement.classList; - const isExpandedBefore = classes.contains('is-expanded'); + beforeEach(() => { + snapshotIndicator = openmct.indicators.indicatorObjects.find( + (indicator) => indicator.key === 'notebook-snapshot-indicator' + ).element; - clickSnapshotIndicator(); - classes = drawerElement.classList; - const isExpandedAfterFirstClick = classes.contains('is-expanded'); + element.append(snapshotIndicator); - clickSnapshotIndicator(); - classes = drawerElement.classList; - const isExpandedAfterSecondClick = classes.contains('is-expanded'); - - expect(isExpandedBefore).toBeFalse(); - expect(isExpandedAfterFirstClick).toBeTrue(); - expect(isExpandedAfterSecondClick).toBeFalse(); - }); - - it("show notebook snapshots container text", () => { - clickSnapshotIndicator(); - - const notebookSnapshots = drawerElement.querySelector('.l-browse-bar__object-name'); - const snapshotsText = notebookSnapshots.textContent.trim(); - - expect(snapshotsText).toBe('Notebook Snapshots'); - }); + return Vue.nextTick().then(() => { + drawerElement = document.querySelector('.l-shell__drawer'); + }); }); - function getEntryText(entryNumber) { - return element.querySelectorAll('.c-notebook__entry .c-ne__text')[entryNumber]; - } + afterEach(() => { + if (drawerElement) { + drawerElement.classList.remove('is-expanded'); + } - function allNotebookEntryElements() { - return element.querySelectorAll('.c-notebook__entry'); - } + snapshotIndicator.remove(); + snapshotIndicator = undefined; - function allNotebookSectionElements() { - return element.querySelectorAll('.js-sidebar-sections .js-list__item'); - } + if (drawerElement) { + drawerElement.remove(); + drawerElement = undefined; + } + }); - function allNotebookPageElements() { - return element.querySelectorAll('.js-sidebar-pages .js-list__item'); - } + it('has Snapshots indicator', () => { + const hasSnapshotIndicator = snapshotIndicator !== null && snapshotIndicator !== undefined; + expect(hasSnapshotIndicator).toBe(true); + }); + + it('snapshots container has class isExpanded', () => { + let classes = drawerElement.classList; + const isExpandedBefore = classes.contains('is-expanded'); + + clickSnapshotIndicator(); + classes = drawerElement.classList; + const isExpandedAfterFirstClick = classes.contains('is-expanded'); + + expect(isExpandedBefore).toBeFalse(); + expect(isExpandedAfterFirstClick).toBeTrue(); + }); + + it('snapshots container does not have class isExpanded', () => { + let classes = drawerElement.classList; + const isExpandedBefore = classes.contains('is-expanded'); + + clickSnapshotIndicator(); + classes = drawerElement.classList; + const isExpandedAfterFirstClick = classes.contains('is-expanded'); + + clickSnapshotIndicator(); + classes = drawerElement.classList; + const isExpandedAfterSecondClick = classes.contains('is-expanded'); + + expect(isExpandedBefore).toBeFalse(); + expect(isExpandedAfterFirstClick).toBeTrue(); + expect(isExpandedAfterSecondClick).toBeFalse(); + }); + + it('show notebook snapshots container text', () => { + clickSnapshotIndicator(); + + const notebookSnapshots = drawerElement.querySelector('.l-browse-bar__object-name'); + const snapshotsText = notebookSnapshots.textContent.trim(); + + expect(snapshotsText).toBe('Notebook Snapshots'); + }); + }); + + function getEntryText(entryNumber) { + return element.querySelectorAll('.c-notebook__entry .c-ne__text')[entryNumber]; + } + + function allNotebookEntryElements() { + return element.querySelectorAll('.c-notebook__entry'); + } + + function allNotebookSectionElements() { + return element.querySelectorAll('.js-sidebar-sections .js-list__item'); + } + + function allNotebookPageElements() { + return element.querySelectorAll('.js-sidebar-pages .js-list__item'); + } }); diff --git a/src/plugins/notebook/snapshot-container.js b/src/plugins/notebook/snapshot-container.js index 92f70cd49b..6437cff2ef 100644 --- a/src/plugins/notebook/snapshot-container.js +++ b/src/plugins/notebook/snapshot-container.js @@ -5,84 +5,83 @@ const NOTEBOOK_SNAPSHOT_STORAGE = 'notebook-snapshot-storage'; export const NOTEBOOK_SNAPSHOT_MAX_COUNT = 5; export default class SnapshotContainer extends EventEmitter { - constructor(openmct) { - super(); + constructor(openmct) { + super(); - if (!SnapshotContainer.instance) { - SnapshotContainer.instance = this; - } - - this.openmct = openmct; - - // eslint-disable-next-line - return SnapshotContainer.instance; + if (!SnapshotContainer.instance) { + SnapshotContainer.instance = this; } - addSnapshot(notebookImageDomainObject, embedObject) { - const snapshots = this.getSnapshots(); - if (snapshots.length >= NOTEBOOK_SNAPSHOT_MAX_COUNT) { - snapshots.pop(); - } + this.openmct = openmct; - const snapshotObject = { - notebookImageDomainObject, - embedObject - }; + // eslint-disable-next-line + return SnapshotContainer.instance; + } - snapshots.unshift(snapshotObject); - - return this.saveSnapshots(snapshots); + addSnapshot(notebookImageDomainObject, embedObject) { + const snapshots = this.getSnapshots(); + if (snapshots.length >= NOTEBOOK_SNAPSHOT_MAX_COUNT) { + snapshots.pop(); } - getSnapshot(id) { - const snapshots = this.getSnapshots(); + const snapshotObject = { + notebookImageDomainObject, + embedObject + }; - return snapshots.find(s => s.embedObject.id === id); + snapshots.unshift(snapshotObject); + + return this.saveSnapshots(snapshots); + } + + getSnapshot(id) { + const snapshots = this.getSnapshots(); + + return snapshots.find((s) => s.embedObject.id === id); + } + + getSnapshots() { + const snapshots = window.localStorage.getItem(NOTEBOOK_SNAPSHOT_STORAGE) || '[]'; + + return JSON.parse(snapshots); + } + + removeSnapshot(id) { + if (!id) { + return; } - getSnapshots() { - const snapshots = window.localStorage.getItem(NOTEBOOK_SNAPSHOT_STORAGE) || '[]'; + const snapshots = this.getSnapshots(); + const filteredsnapshots = snapshots.filter((snapshot) => snapshot.embedObject.id !== id); - return JSON.parse(snapshots); + return this.saveSnapshots(filteredsnapshots); + } + + removeAllSnapshots() { + return this.saveSnapshots([]); + } + + saveSnapshots(snapshots) { + try { + window.localStorage.setItem(NOTEBOOK_SNAPSHOT_STORAGE, JSON.stringify(snapshots)); + this.emit(EVENT_SNAPSHOTS_UPDATED, true); + + return true; + } catch (e) { + const message = + 'Insufficient memory in localstorage to store snapshot, please delete some snapshots and try again!'; + this.openmct.notifications.error(message); + + return false; } + } - removeSnapshot(id) { - if (!id) { - return; - } + updateSnapshot(snapshot) { + const snapshots = this.getSnapshots(); + const updatedSnapshots = snapshots.map((s) => { + return s.embedObject.id === snapshot.embedObject.id ? snapshot : s; + }); - const snapshots = this.getSnapshots(); - const filteredsnapshots = snapshots.filter(snapshot => snapshot.embedObject.id !== id); - - return this.saveSnapshots(filteredsnapshots); - } - - removeAllSnapshots() { - return this.saveSnapshots([]); - } - - saveSnapshots(snapshots) { - try { - window.localStorage.setItem(NOTEBOOK_SNAPSHOT_STORAGE, JSON.stringify(snapshots)); - this.emit(EVENT_SNAPSHOTS_UPDATED, true); - - return true; - } catch (e) { - const message = 'Insufficient memory in localstorage to store snapshot, please delete some snapshots and try again!'; - this.openmct.notifications.error(message); - - return false; - } - } - - updateSnapshot(snapshot) { - const snapshots = this.getSnapshots(); - const updatedSnapshots = snapshots.map(s => { - return s.embedObject.id === snapshot.embedObject.id - ? snapshot - : s; - }); - - return this.saveSnapshots(updatedSnapshots); - } + return this.saveSnapshots(updatedSnapshots); + } } diff --git a/src/plugins/notebook/snapshot.js b/src/plugins/notebook/snapshot.js index c7a726acc7..6dfa84d87a 100644 --- a/src/plugins/notebook/snapshot.js +++ b/src/plugins/notebook/snapshot.js @@ -1,117 +1,134 @@ import { addNotebookEntry, createNewEmbed } from './utils/notebook-entries'; -import { getDefaultNotebook, getNotebookSectionAndPage, getDefaultNotebookLink, setDefaultNotebook } from './utils/notebook-storage'; +import { + getDefaultNotebook, + getNotebookSectionAndPage, + getDefaultNotebookLink, + setDefaultNotebook +} from './utils/notebook-storage'; import { NOTEBOOK_DEFAULT } from '@/plugins/notebook/notebook-constants'; -import { createNotebookImageDomainObject, saveNotebookImageDomainObject, updateNamespaceOfDomainObject, DEFAULT_SIZE } from './utils/notebook-image'; +import { + createNotebookImageDomainObject, + saveNotebookImageDomainObject, + updateNamespaceOfDomainObject, + DEFAULT_SIZE +} from './utils/notebook-image'; import SnapshotContainer from './snapshot-container'; import ImageExporter from '../../exporters/ImageExporter'; export default class Snapshot { - constructor(openmct) { - this.openmct = openmct; - this.snapshotContainer = new SnapshotContainer(openmct); - this.imageExporter = new ImageExporter(openmct); + constructor(openmct) { + this.openmct = openmct; + this.snapshotContainer = new SnapshotContainer(openmct); + this.imageExporter = new ImageExporter(openmct); - this.capture = this.capture.bind(this); - this._saveSnapShot = this._saveSnapShot.bind(this); - } + this.capture = this.capture.bind(this); + this._saveSnapShot = this._saveSnapShot.bind(this); + } - capture(snapshotMeta, notebookType, domElement) { - const options = { - className: 's-status-taking-snapshot', - thumbnailSize: DEFAULT_SIZE - }; - this.imageExporter.exportPNGtoSRC(domElement, options) - .then(function ({blob, thumbnail}) { - const reader = new window.FileReader(); - reader.readAsDataURL(blob); - reader.onloadend = function () { - this._saveSnapShot(notebookType, reader.result, thumbnail, snapshotMeta); - }.bind(this); - }.bind(this)); - } + capture(snapshotMeta, notebookType, domElement) { + const options = { + className: 's-status-taking-snapshot', + thumbnailSize: DEFAULT_SIZE + }; + this.imageExporter.exportPNGtoSRC(domElement, options).then( + function ({ blob, thumbnail }) { + const reader = new window.FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = function () { + this._saveSnapShot(notebookType, reader.result, thumbnail, snapshotMeta); + }.bind(this); + }.bind(this) + ); + } - /** - * @private - */ - _saveSnapShot(notebookType, fullSizeImageURL, thumbnailImageURL, snapshotMeta) { - const object = createNotebookImageDomainObject(fullSizeImageURL); - const thumbnailImage = { src: thumbnailImageURL || '' }; - const snapshot = { - fullSizeImageObjectIdentifier: object.identifier, - thumbnailImage - }; - createNewEmbed(snapshotMeta, snapshot).then(embed => { - if (notebookType === NOTEBOOK_DEFAULT) { - const notebookStorage = getDefaultNotebook(); + /** + * @private + */ + _saveSnapShot(notebookType, fullSizeImageURL, thumbnailImageURL, snapshotMeta) { + const object = createNotebookImageDomainObject(fullSizeImageURL); + const thumbnailImage = { src: thumbnailImageURL || '' }; + const snapshot = { + fullSizeImageObjectIdentifier: object.identifier, + thumbnailImage + }; + createNewEmbed(snapshotMeta, snapshot).then((embed) => { + if (notebookType === NOTEBOOK_DEFAULT) { + const notebookStorage = getDefaultNotebook(); - this._saveToDefaultNoteBook(notebookStorage, embed); - const notebookImageDomainObject = updateNamespaceOfDomainObject(object, notebookStorage.identifier.namespace); - saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject); - } else { - this._saveToNotebookSnapshots(object, embed); - } - }); - } + this._saveToDefaultNoteBook(notebookStorage, embed); + const notebookImageDomainObject = updateNamespaceOfDomainObject( + object, + notebookStorage.identifier.namespace + ); + saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject); + } else { + this._saveToNotebookSnapshots(object, embed); + } + }); + } - /** - * @private - */ - _saveToDefaultNoteBook(notebookStorage, embed) { - this.openmct.objects.get(notebookStorage.identifier) - .then((domainObject) => { - return addNotebookEntry(this.openmct, domainObject, notebookStorage, embed).then(async () => { - let link = notebookStorage.link; + /** + * @private + */ + _saveToDefaultNoteBook(notebookStorage, embed) { + this.openmct.objects.get(notebookStorage.identifier).then((domainObject) => { + return addNotebookEntry(this.openmct, domainObject, notebookStorage, embed).then(async () => { + let link = notebookStorage.link; - // Backwards compatibility fix (old notebook model without link) - if (!link) { - link = await getDefaultNotebookLink(this.openmct, domainObject); - notebookStorage.link = link; - setDefaultNotebook(this.openmct, notebookStorage); - } - - const { section, page } = getNotebookSectionAndPage(domainObject, notebookStorage.defaultSectionId, notebookStorage.defaultPageId); - if (!section || !page) { - return; - } - - const defaultPath = `${domainObject.name} - ${section.name} - ${page.name}`; - const msg = `Saved to Notebook ${defaultPath}`; - this._showNotification(msg, link); - }); - }); - } - - /** - * @private - */ - _saveToNotebookSnapshots(notebookImageDomainObject, embed) { - this.snapshotContainer.addSnapshot(notebookImageDomainObject, embed); - } - - _showNotification(msg, url) { - const options = { - autoDismissTimeout: 30000 - }; - - if (!this.openmct.editor.isEditing()) { - options.link = { - cssClass: '', - text: 'click to view', - onClick: this._navigateToNotebook(url) - }; + // Backwards compatibility fix (old notebook model without link) + if (!link) { + link = await getDefaultNotebookLink(this.openmct, domainObject); + notebookStorage.link = link; + setDefaultNotebook(this.openmct, notebookStorage); } - this.openmct.notifications.info(msg, options); - } - - _navigateToNotebook(url = null) { - if (!url) { - return () => {}; + const { section, page } = getNotebookSectionAndPage( + domainObject, + notebookStorage.defaultSectionId, + notebookStorage.defaultPageId + ); + if (!section || !page) { + return; } - return () => { - location.hash = url; - }; + const defaultPath = `${domainObject.name} - ${section.name} - ${page.name}`; + const msg = `Saved to Notebook ${defaultPath}`; + this._showNotification(msg, link); + }); + }); + } + + /** + * @private + */ + _saveToNotebookSnapshots(notebookImageDomainObject, embed) { + this.snapshotContainer.addSnapshot(notebookImageDomainObject, embed); + } + + _showNotification(msg, url) { + const options = { + autoDismissTimeout: 30000 + }; + + if (!this.openmct.editor.isEditing()) { + options.link = { + cssClass: '', + text: 'click to view', + onClick: this._navigateToNotebook(url) + }; } + + this.openmct.notifications.info(msg, options); + } + + _navigateToNotebook(url = null) { + if (!url) { + return () => {}; + } + + return () => { + location.hash = url; + }; + } } diff --git a/src/plugins/notebook/utils/notebook-entries.js b/src/plugins/notebook/utils/notebook-entries.js index d591a9e7b2..8c7b388c2d 100644 --- a/src/plugins/notebook/utils/notebook-entries.js +++ b/src/plugins/notebook/utils/notebook-entries.js @@ -2,245 +2,247 @@ import objectLink from '../../../ui/mixins/object-link'; import { v4 as uuid } from 'uuid'; async function getUsername(openmct) { - let username = null; + let username = null; - if (openmct.user.hasProvider()) { - const user = await openmct.user.getCurrentUser(); - username = user.getName(); - } - - return username; + if (openmct.user.hasProvider()) { + const user = await openmct.user.getCurrentUser(); + username = user.getName(); + } + return username; } export const DEFAULT_CLASS = 'notebook-default'; const TIME_BOUNDS = { - START_BOUND: 'tc.startBound', - END_BOUND: 'tc.endBound', - START_DELTA: 'tc.startDelta', - END_DELTA: 'tc.endDelta' + START_BOUND: 'tc.startBound', + END_BOUND: 'tc.endBound', + START_DELTA: 'tc.startDelta', + END_DELTA: 'tc.endDelta' }; export function addEntryIntoPage(notebookStorage, entries, entry) { - const defaultSectionId = notebookStorage.defaultSectionId; - const defaultPageId = notebookStorage.defaultPageId; - if (!defaultSectionId || !defaultPageId) { - return; - } + const defaultSectionId = notebookStorage.defaultSectionId; + const defaultPageId = notebookStorage.defaultPageId; + if (!defaultSectionId || !defaultPageId) { + return; + } - const newEntries = JSON.parse(JSON.stringify(entries)); - let section = newEntries[defaultSectionId]; - if (!section) { - newEntries[defaultSectionId] = {}; - } + const newEntries = JSON.parse(JSON.stringify(entries)); + let section = newEntries[defaultSectionId]; + if (!section) { + newEntries[defaultSectionId] = {}; + } - let page = newEntries[defaultSectionId][defaultPageId]; - if (!page) { - newEntries[defaultSectionId][defaultPageId] = []; - } + let page = newEntries[defaultSectionId][defaultPageId]; + if (!page) { + newEntries[defaultSectionId][defaultPageId] = []; + } - newEntries[defaultSectionId][defaultPageId].push(entry); + newEntries[defaultSectionId][defaultPageId].push(entry); - return newEntries; + return newEntries; } export function selectEntry({ - element, entryId, domainObject, openmct, - onAnnotationChange, notebookAnnotations + element, + entryId, + domainObject, + openmct, + onAnnotationChange, + notebookAnnotations }) { - const targetDetails = {}; - const keyString = openmct.objects.makeKeyString(domainObject.identifier); - targetDetails[keyString] = { - entryId - }; - const targetDomainObjects = {}; - targetDomainObjects[keyString] = domainObject; - openmct.selection.select( - [ - { - element, - context: { - type: 'notebook-entry-selection', - item: domainObject, - targetDetails, - targetDomainObjects, - annotations: notebookAnnotations, - annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, - onAnnotationChange - } - } - ], - false); + const targetDetails = {}; + const keyString = openmct.objects.makeKeyString(domainObject.identifier); + targetDetails[keyString] = { + entryId + }; + const targetDomainObjects = {}; + targetDomainObjects[keyString] = domainObject; + openmct.selection.select( + [ + { + element, + context: { + type: 'notebook-entry-selection', + item: domainObject, + targetDetails, + targetDomainObjects, + annotations: notebookAnnotations, + annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, + onAnnotationChange + } + } + ], + false + ); } export function getHistoricLinkInFixedMode(openmct, bounds, historicLink) { - if (historicLink.includes('tc.mode=fixed')) { - return historicLink; + if (historicLink.includes('tc.mode=fixed')) { + return historicLink; + } + + openmct.time.getAllClocks().forEach((clock) => { + if (historicLink.includes(`tc.mode=${clock.key}`)) { + historicLink.replace(`tc.mode=${clock.key}`, 'tc.mode=fixed'); + + return; + } + }); + + const params = historicLink.split('&').map((param) => { + if (param.includes(TIME_BOUNDS.START_BOUND) || param.includes(TIME_BOUNDS.START_DELTA)) { + param = `${TIME_BOUNDS.START_BOUND}=${bounds.start}`; } - openmct.time.getAllClocks().forEach(clock => { - if (historicLink.includes(`tc.mode=${clock.key}`)) { - historicLink.replace(`tc.mode=${clock.key}`, 'tc.mode=fixed'); + if (param.includes(TIME_BOUNDS.END_BOUND) || param.includes(TIME_BOUNDS.END_DELTA)) { + param = `${TIME_BOUNDS.END_BOUND}=${bounds.end}`; + } - return; - } - }); + return param; + }); - const params = historicLink.split('&').map(param => { - if (param.includes(TIME_BOUNDS.START_BOUND) - || param.includes(TIME_BOUNDS.START_DELTA)) { - param = `${TIME_BOUNDS.START_BOUND}=${bounds.start}`; - } - - if (param.includes(TIME_BOUNDS.END_BOUND) - || param.includes(TIME_BOUNDS.END_DELTA)) { - param = `${TIME_BOUNDS.END_BOUND}=${bounds.end}`; - } - - return param; - }); - - return params.join('&'); + return params.join('&'); } export async function createNewEmbed(snapshotMeta, snapshot = '') { - const { - bounds, - link, + const { bounds, link, objectPath, openmct } = snapshotMeta; + const domainObject = objectPath[0]; + const domainObjectType = openmct.types.get(domainObject.type); + + const cssClass = + domainObjectType && domainObjectType.definition + ? domainObjectType.definition.cssClass + : 'icon-object-unknown'; + const date = Date.now(); + const historicLink = link + ? getHistoricLinkInFixedMode(openmct, bounds, link) + : objectLink.computed.objectLink.call({ objectPath, openmct - } = snapshotMeta; - const domainObject = objectPath[0]; - const domainObjectType = openmct.types.get(domainObject.type); + }); + const name = domainObject.name; + const type = domainObject.identifier.key; + const createdBy = await getUsername(openmct); - const cssClass = domainObjectType && domainObjectType.definition - ? domainObjectType.definition.cssClass - : 'icon-object-unknown'; - const date = Date.now(); - const historicLink = link - ? getHistoricLinkInFixedMode(openmct, bounds, link) - : objectLink.computed.objectLink.call({ - objectPath, - openmct - }); - const name = domainObject.name; - const type = domainObject.identifier.key; - const createdBy = await getUsername(openmct); - - return { - bounds, - createdOn: date, - createdBy, - cssClass, - domainObject, - historicLink, - id: 'embed-' + date, - name, - snapshot, - type - }; + return { + bounds, + createdOn: date, + createdBy, + cssClass, + domainObject, + historicLink, + id: 'embed-' + date, + name, + snapshot, + type + }; } -export async function addNotebookEntry(openmct, domainObject, notebookStorage, embed = null, entryText = '') { - if (!openmct || !domainObject || !notebookStorage) { - return; - } +export async function addNotebookEntry( + openmct, + domainObject, + notebookStorage, + embed = null, + entryText = '' +) { + if (!openmct || !domainObject || !notebookStorage) { + return; + } - const date = Date.now(); - const configuration = domainObject.configuration; - const entries = configuration.entries || {}; - const embeds = embed - ? [embed] - : []; + const date = Date.now(); + const configuration = domainObject.configuration; + const entries = configuration.entries || {}; + const embeds = embed ? [embed] : []; - const id = `entry-${uuid()}`; - const createdBy = await getUsername(openmct); - const entry = { - id, - createdOn: date, - createdBy, - text: entryText, - embeds - }; + const id = `entry-${uuid()}`; + const createdBy = await getUsername(openmct); + const entry = { + id, + createdOn: date, + createdBy, + text: entryText, + embeds + }; - const newEntries = addEntryIntoPage(notebookStorage, entries, entry); + const newEntries = addEntryIntoPage(notebookStorage, entries, entry); - addDefaultClass(domainObject, openmct); - mutateObject(openmct, domainObject, 'configuration.entries', newEntries); + addDefaultClass(domainObject, openmct); + mutateObject(openmct, domainObject, 'configuration.entries', newEntries); - return id; + return id; } export function getNotebookEntries(domainObject, selectedSection, selectedPage) { - if (!domainObject || !selectedSection || !selectedPage || !domainObject.configuration) { - return; - } + if (!domainObject || !selectedSection || !selectedPage || !domainObject.configuration) { + return; + } - const configuration = domainObject.configuration; - const entries = configuration.entries || {}; + const configuration = domainObject.configuration; + const entries = configuration.entries || {}; - let section = entries[selectedSection.id]; - if (!section) { - return; - } + let section = entries[selectedSection.id]; + if (!section) { + return; + } - let page = entries[selectedSection.id][selectedPage.id]; - if (!page) { - return; - } + let page = entries[selectedSection.id][selectedPage.id]; + if (!page) { + return; + } - const specificEntries = entries[selectedSection.id][selectedPage.id]; + const specificEntries = entries[selectedSection.id][selectedPage.id]; - return specificEntries; + return specificEntries; } export function getEntryPosById(entryId, domainObject, selectedSection, selectedPage) { - if (!domainObject || !selectedSection || !selectedPage) { - return; + if (!domainObject || !selectedSection || !selectedPage) { + return; + } + + const entries = getNotebookEntries(domainObject, selectedSection, selectedPage); + let foundId = -1; + entries.forEach((element, index) => { + if (element.id === entryId) { + foundId = index; + + return; } + }); - const entries = getNotebookEntries(domainObject, selectedSection, selectedPage); - let foundId = -1; - entries.forEach((element, index) => { - if (element.id === entryId) { - foundId = index; - - return; - } - }); - - return foundId; + return foundId; } export function deleteNotebookEntries(openmct, domainObject, selectedSection, selectedPage) { - if (!domainObject || !selectedSection) { - return; - } + if (!domainObject || !selectedSection) { + return; + } - const configuration = domainObject.configuration; - const entries = configuration.entries || {}; + const configuration = domainObject.configuration; + const entries = configuration.entries || {}; - // Delete entire section - if (!selectedPage) { - delete entries[selectedSection.id]; + // Delete entire section + if (!selectedPage) { + delete entries[selectedSection.id]; - return; - } + return; + } - let section = entries[selectedSection.id]; - if (!section) { - return; - } + let section = entries[selectedSection.id]; + if (!section) { + return; + } - delete entries[selectedSection.id][selectedPage.id]; + delete entries[selectedSection.id][selectedPage.id]; - mutateObject(openmct, domainObject, 'configuration.entries', entries); + mutateObject(openmct, domainObject, 'configuration.entries', entries); } export function mutateObject(openmct, object, key, value) { - openmct.objects.mutate(object, key, value); + openmct.objects.mutate(object, key, value); } function addDefaultClass(domainObject, openmct) { - openmct.status.set(domainObject.identifier, DEFAULT_CLASS); + openmct.status.set(domainObject.identifier, DEFAULT_CLASS); } diff --git a/src/plugins/notebook/utils/notebook-entriesSpec.js b/src/plugins/notebook/utils/notebook-entriesSpec.js index 30c1ae9f83..b0a23f085b 100644 --- a/src/plugins/notebook/utils/notebook-entriesSpec.js +++ b/src/plugins/notebook/utils/notebook-entriesSpec.js @@ -23,166 +23,212 @@ import * as NotebookEntries from './notebook-entries'; import { createOpenMct, resetApplicationState } from 'utils/testing'; const notebookStorage = { - name: 'notebook', - identifier: { - namespace: '', - key: 'test-notebook' - }, - defaultSectionId: '03a79b6a-971c-4e56-9892-ec536332c3f0', - defaultPageId: '8b548fd9-2b8a-4b02-93a9-4138e22eba00' + name: 'notebook', + identifier: { + namespace: '', + key: 'test-notebook' + }, + defaultSectionId: '03a79b6a-971c-4e56-9892-ec536332c3f0', + defaultPageId: '8b548fd9-2b8a-4b02-93a9-4138e22eba00' }; const notebookEntries = { - '03a79b6a-971c-4e56-9892-ec536332c3f0': { - '8b548fd9-2b8a-4b02-93a9-4138e22eba00': [] - } + '03a79b6a-971c-4e56-9892-ec536332c3f0': { + '8b548fd9-2b8a-4b02-93a9-4138e22eba00': [] + } }; const notebookDomainObject = { - identifier: { - key: 'notebook', - namespace: '' - }, - type: 'notebook', - name: 'Test Notebook', - configuration: { - defaultSort: 'oldest', - entries: notebookEntries, - pageTitle: 'Page', - sections: [], - sectionTitle: 'Section', - type: 'General' - } + identifier: { + key: 'notebook', + namespace: '' + }, + type: 'notebook', + name: 'Test Notebook', + configuration: { + defaultSort: 'oldest', + entries: notebookEntries, + pageTitle: 'Page', + sections: [], + sectionTitle: 'Section', + type: 'General' + } }; const selectedSection = { - id: '03a79b6a-971c-4e56-9892-ec536332c3f0', - isDefault: false, - isSelected: true, - name: 'Day 1', - pages: [ - { - id: '54deb3d5-8267-4be4-95e9-3579ed8c082d', - isDefault: false, - isSelected: false, - name: 'Shift 1', - pageTitle: 'Page' - }, - { - id: '2ea41c78-8e60-4657-a350-53f1a1fa3021', - isDefault: false, - isSelected: false, - name: 'Shift 2', - pageTitle: 'Page' - }, - { - id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00', - isDefault: false, - isSelected: true, - name: 'Unnamed Page', - pageTitle: 'Page' - } - ], - sectionTitle: 'Section' + id: '03a79b6a-971c-4e56-9892-ec536332c3f0', + isDefault: false, + isSelected: true, + name: 'Day 1', + pages: [ + { + id: '54deb3d5-8267-4be4-95e9-3579ed8c082d', + isDefault: false, + isSelected: false, + name: 'Shift 1', + pageTitle: 'Page' + }, + { + id: '2ea41c78-8e60-4657-a350-53f1a1fa3021', + isDefault: false, + isSelected: false, + name: 'Shift 2', + pageTitle: 'Page' + }, + { + id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00', + isDefault: false, + isSelected: true, + name: 'Unnamed Page', + pageTitle: 'Page' + } + ], + sectionTitle: 'Section' }; const selectedPage = { - id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00', - isDefault: false, - isSelected: true, - name: 'Unnamed Page', - pageTitle: 'Page' + id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00', + isDefault: false, + isSelected: true, + name: 'Unnamed Page', + pageTitle: 'Page' }; let openmct; describe('Notebook Entries:', () => { - beforeEach(() => { - openmct = createOpenMct(); - openmct.types.addType('notebook', { - creatable: true - }); - openmct.objects.addProvider('', jasmine.createSpyObj('mockNotebookProvider', [ - 'create', - 'update' - ])); - openmct.editor = { - isEditing: () => false - }; - openmct.objects.isPersistable = () => true; - openmct.objects.save = () => Promise.resolve(true); + beforeEach(() => { + openmct = createOpenMct(); + openmct.types.addType('notebook', { + creatable: true + }); + openmct.objects.addProvider( + '', + jasmine.createSpyObj('mockNotebookProvider', ['create', 'update']) + ); + openmct.editor = { + isEditing: () => false + }; + openmct.objects.isPersistable = () => true; + openmct.objects.save = () => Promise.resolve(true); - window.localStorage.setItem('notebook-storage', null); + window.localStorage.setItem('notebook-storage', null); + }); + + afterEach(() => { + notebookDomainObject.configuration.entries[selectedSection.id][selectedPage.id] = []; + + return resetApplicationState(openmct); + }); + + it('getNotebookEntries has no entries', () => { + const entries = NotebookEntries.getNotebookEntries( + notebookDomainObject, + selectedSection, + selectedPage + ); + + expect(entries.length).toEqual(0); + }); + + it('addNotebookEntry adds entry', async () => { + const unlisten = openmct.objects.observe(notebookDomainObject, '*', (object) => { + const entries = NotebookEntries.getNotebookEntries( + notebookDomainObject, + selectedSection, + selectedPage + ); + + expect(entries.length).toEqual(1); + unlisten(); }); - afterEach(() => { - notebookDomainObject.configuration.entries[selectedSection.id][selectedPage.id] = []; + await NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage); + }); - return resetApplicationState(openmct); + it('addNotebookEntry adds active user to entry', async () => { + const USER = 'Timmy'; + openmct.user.hasProvider = () => true; + openmct.user.getCurrentUser = () => { + return Promise.resolve({ + getName: () => { + return USER; + } + }); + }; + + const unlisten = openmct.objects.observe(notebookDomainObject, '*', (object) => { + const entries = NotebookEntries.getNotebookEntries( + notebookDomainObject, + selectedSection, + selectedPage + ); + + expect(entries[0].createdBy).toEqual(USER); + unlisten(); }); - it('getNotebookEntries has no entries', () => { - const entries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage); + await NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage); + }); - expect(entries.length).toEqual(0); - }); + it('getEntryPosById returns valid position', async () => { + const entryId1 = await NotebookEntries.addNotebookEntry( + openmct, + notebookDomainObject, + notebookStorage + ); + const position1 = NotebookEntries.getEntryPosById( + entryId1, + notebookDomainObject, + selectedSection, + selectedPage + ); - it('addNotebookEntry adds entry', async () => { - const unlisten = openmct.objects.observe(notebookDomainObject, '*', (object) => { - const entries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage); + const entryId2 = await NotebookEntries.addNotebookEntry( + openmct, + notebookDomainObject, + notebookStorage + ); + const position2 = NotebookEntries.getEntryPosById( + entryId2, + notebookDomainObject, + selectedSection, + selectedPage + ); - expect(entries.length).toEqual(1); - unlisten(); - }); + const entryId3 = await NotebookEntries.addNotebookEntry( + openmct, + notebookDomainObject, + notebookStorage + ); + const position3 = NotebookEntries.getEntryPosById( + entryId3, + notebookDomainObject, + selectedSection, + selectedPage + ); - await NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage); - }); + const success = position1 === 0 && position2 === 1 && position3 === 2; - it('addNotebookEntry adds active user to entry', async () => { - const USER = 'Timmy'; - openmct.user.hasProvider = () => true; - openmct.user.getCurrentUser = () => { - return Promise.resolve({ - getName: () => { - return USER; - } - }); - }; + expect(success).toBe(true); + }); - const unlisten = openmct.objects.observe(notebookDomainObject, '*', (object) => { - const entries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage); + it('deleteNotebookEntries deletes correct page entries', async () => { + await NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage); + await NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage); - expect(entries[0].createdBy).toEqual(USER); - unlisten(); - }); + NotebookEntries.deleteNotebookEntries( + openmct, + notebookDomainObject, + selectedSection, + selectedPage + ); + const afterEntries = NotebookEntries.getNotebookEntries( + notebookDomainObject, + selectedSection, + selectedPage + ); - await NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage); - }); - - it('getEntryPosById returns valid position', async () => { - const entryId1 = await NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage); - const position1 = NotebookEntries.getEntryPosById(entryId1, notebookDomainObject, selectedSection, selectedPage); - - const entryId2 = await NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage); - const position2 = NotebookEntries.getEntryPosById(entryId2, notebookDomainObject, selectedSection, selectedPage); - - const entryId3 = await NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage); - const position3 = NotebookEntries.getEntryPosById(entryId3, notebookDomainObject, selectedSection, selectedPage); - - const success = position1 === 0 - && position2 === 1 - && position3 === 2; - - expect(success).toBe(true); - }); - - it('deleteNotebookEntries deletes correct page entries', async () => { - await NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage); - await NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage); - - NotebookEntries.deleteNotebookEntries(openmct, notebookDomainObject, selectedSection, selectedPage); - const afterEntries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage); - - expect(afterEntries).toEqual(undefined); - }); + expect(afterEntries).toEqual(undefined); + }); }); diff --git a/src/plugins/notebook/utils/notebook-image.js b/src/plugins/notebook/utils/notebook-image.js index 0f7e4ab307..894dbcef20 100644 --- a/src/plugins/notebook/utils/notebook-image.js +++ b/src/plugins/notebook/utils/notebook-image.js @@ -1,86 +1,85 @@ import { v4 as uuid } from 'uuid'; export const DEFAULT_SIZE = { - width: 30, - height: 30 + width: 30, + height: 30 }; export function createNotebookImageDomainObject(fullSizeImageURL) { - const identifier = { - key: uuid(), - namespace: '' - }; - const viewType = 'notebookSnapshotImage'; + const identifier = { + key: uuid(), + namespace: '' + }; + const viewType = 'notebookSnapshotImage'; - return { - name: 'Notebook Snapshot Image', - type: viewType, - identifier, - configuration: { - fullSizeImageURL - } - }; + return { + name: 'Notebook Snapshot Image', + type: viewType, + identifier, + configuration: { + fullSizeImageURL + } + }; } export function getThumbnailURLFromCanvas(canvas, size = DEFAULT_SIZE) { - const thumbnailCanvas = document.createElement('canvas'); - thumbnailCanvas.setAttribute('width', size.width); - thumbnailCanvas.setAttribute('height', size.height); - const ctx = thumbnailCanvas.getContext('2d'); - ctx.globalCompositeOperation = "copy"; - ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height); + const thumbnailCanvas = document.createElement('canvas'); + thumbnailCanvas.setAttribute('width', size.width); + thumbnailCanvas.setAttribute('height', size.height); + const ctx = thumbnailCanvas.getContext('2d'); + ctx.globalCompositeOperation = 'copy'; + ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height); - return thumbnailCanvas.toDataURL('image/png'); + return thumbnailCanvas.toDataURL('image/png'); } export function getThumbnailURLFromimageUrl(imageUrl, size = DEFAULT_SIZE) { - return new Promise(resolve => { - const image = new Image(); + return new Promise((resolve) => { + const image = new Image(); - const canvas = document.createElement('canvas'); - canvas.width = size.width; - canvas.height = size.height; + const canvas = document.createElement('canvas'); + canvas.width = size.width; + canvas.height = size.height; - image.onload = function () { - canvas.getContext('2d') - .drawImage(image, 0, 0, size.width, size.height); + image.onload = function () { + canvas.getContext('2d').drawImage(image, 0, 0, size.width, size.height); - resolve(canvas.toDataURL('image/png')); - }; + resolve(canvas.toDataURL('image/png')); + }; - image.src = imageUrl; - }); + image.src = imageUrl; + }); } export function saveNotebookImageDomainObject(openmct, object) { - return new Promise((resolve, reject) => { - openmct.objects.save(object) - .then(result => { - if (result) { - resolve(object); - } else { - reject(); - } - }) - .catch(e => { - console.error(e); - reject(); - }); - }); + return new Promise((resolve, reject) => { + openmct.objects + .save(object) + .then((result) => { + if (result) { + resolve(object); + } else { + reject(); + } + }) + .catch((e) => { + console.error(e); + reject(); + }); + }); } export function updateNotebookImageDomainObject(openmct, identifier, fullSizeImage) { - openmct.objects.get(identifier) - .then(domainObject => { - const configuration = domainObject.configuration; - configuration.fullSizeImageURL = fullSizeImage.src; + openmct.objects.get(identifier).then((domainObject) => { + const configuration = domainObject.configuration; + configuration.fullSizeImageURL = fullSizeImage.src; - openmct.objects.mutate(domainObject, 'configuration', configuration); - }); + openmct.objects.mutate(domainObject, 'configuration', configuration); + }); } export function updateNamespaceOfDomainObject(object, namespace) { - object.identifier.namespace = namespace; + object.identifier.namespace = namespace; - return object; + return object; } diff --git a/src/plugins/notebook/utils/notebook-migration.js b/src/plugins/notebook/utils/notebook-migration.js index 6592ef0e34..875e1cda5a 100644 --- a/src/plugins/notebook/utils/notebook-migration.js +++ b/src/plugins/notebook/utils/notebook-migration.js @@ -1,47 +1,55 @@ -import { createNotebookImageDomainObject, getThumbnailURLFromimageUrl, saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from './notebook-image'; +import { + createNotebookImageDomainObject, + getThumbnailURLFromimageUrl, + saveNotebookImageDomainObject, + updateNamespaceOfDomainObject +} from './notebook-image'; import { mutateObject } from './notebook-entries'; -export const IMAGE_MIGRATION_VER = "v1"; +export const IMAGE_MIGRATION_VER = 'v1'; export function notebookImageMigration(openmct, domainObject) { - const configuration = domainObject.configuration; - const notebookEntries = configuration.entries; + const configuration = domainObject.configuration; + const notebookEntries = configuration.entries; - const imageMigrationVer = configuration.imageMigrationVer; - if (imageMigrationVer && imageMigrationVer === IMAGE_MIGRATION_VER) { - return; - } + const imageMigrationVer = configuration.imageMigrationVer; + if (imageMigrationVer && imageMigrationVer === IMAGE_MIGRATION_VER) { + return; + } - configuration.imageMigrationVer = IMAGE_MIGRATION_VER; + configuration.imageMigrationVer = IMAGE_MIGRATION_VER; - // to avoid muliple notebookImageMigration calls updating images. - mutateObject(openmct, domainObject, 'configuration', configuration); + // to avoid muliple notebookImageMigration calls updating images. + mutateObject(openmct, domainObject, 'configuration', configuration); - configuration.sections.forEach(section => { - const sectionId = section.id; - section.pages.forEach(page => { - const pageId = page.id; - const notebookSection = notebookEntries && notebookEntries[sectionId] || {}; - const pageEntries = notebookSection && notebookSection[pageId] || []; - pageEntries.forEach(entry => { - entry.embeds.forEach(async (embed) => { - const snapshot = embed.snapshot; - const fullSizeImageURL = snapshot.src; - if (fullSizeImageURL) { - const thumbnailImageURL = await getThumbnailURLFromimageUrl(fullSizeImageURL); - const object = createNotebookImageDomainObject(fullSizeImageURL); - const notebookImageDomainObject = updateNamespaceOfDomainObject(object, domainObject.identifier.namespace); - embed.snapshot = { - fullSizeImageObjectIdentifier: notebookImageDomainObject.identifier, - thumbnailImage: { src: thumbnailImageURL || '' } - }; + configuration.sections.forEach((section) => { + const sectionId = section.id; + section.pages.forEach((page) => { + const pageId = page.id; + const notebookSection = (notebookEntries && notebookEntries[sectionId]) || {}; + const pageEntries = (notebookSection && notebookSection[pageId]) || []; + pageEntries.forEach((entry) => { + entry.embeds.forEach(async (embed) => { + const snapshot = embed.snapshot; + const fullSizeImageURL = snapshot.src; + if (fullSizeImageURL) { + const thumbnailImageURL = await getThumbnailURLFromimageUrl(fullSizeImageURL); + const object = createNotebookImageDomainObject(fullSizeImageURL); + const notebookImageDomainObject = updateNamespaceOfDomainObject( + object, + domainObject.identifier.namespace + ); + embed.snapshot = { + fullSizeImageObjectIdentifier: notebookImageDomainObject.identifier, + thumbnailImage: { src: thumbnailImageURL || '' } + }; - mutateObject(openmct, domainObject, 'configuration.entries', notebookEntries); + mutateObject(openmct, domainObject, 'configuration.entries', notebookEntries); - saveNotebookImageDomainObject(openmct, notebookImageDomainObject); - } - }); - }); + saveNotebookImageDomainObject(openmct, notebookImageDomainObject); + } }); + }); }); + }); } diff --git a/src/plugins/notebook/utils/notebook-snapshot-menu.js b/src/plugins/notebook/utils/notebook-snapshot-menu.js index 2816afd232..d4e4158aca 100644 --- a/src/plugins/notebook/utils/notebook-snapshot-menu.js +++ b/src/plugins/notebook/utils/notebook-snapshot-menu.js @@ -1,31 +1,36 @@ import { getDefaultNotebook, getNotebookSectionAndPage } from './notebook-storage'; export async function getMenuItems(openmct, menuItemOptions) { - const notebookTypes = []; + const notebookTypes = []; - const defaultNotebook = getDefaultNotebook(); - const defaultNotebookObject = defaultNotebook && await openmct.objects.get(defaultNotebook.identifier); - if (defaultNotebookObject) { - const { section, page } = getNotebookSectionAndPage(defaultNotebookObject, defaultNotebook.defaultSectionId, defaultNotebook.defaultPageId); - if (section && page) { - const name = defaultNotebookObject.name; - const sectionName = section.name; - const pageName = page.name; - const defaultPath = `${name} - ${sectionName} - ${pageName}`; + const defaultNotebook = getDefaultNotebook(); + const defaultNotebookObject = + defaultNotebook && (await openmct.objects.get(defaultNotebook.identifier)); + if (defaultNotebookObject) { + const { section, page } = getNotebookSectionAndPage( + defaultNotebookObject, + defaultNotebook.defaultSectionId, + defaultNotebook.defaultPageId + ); + if (section && page) { + const name = defaultNotebookObject.name; + const sectionName = section.name; + const pageName = page.name; + const defaultPath = `${name} - ${sectionName} - ${pageName}`; - notebookTypes.push({ - cssClass: menuItemOptions.default.cssClass, - name: `${menuItemOptions.default.name} ${defaultPath}`, - onItemClicked: menuItemOptions.default.onItemClicked - }); - } + notebookTypes.push({ + cssClass: menuItemOptions.default.cssClass, + name: `${menuItemOptions.default.name} ${defaultPath}`, + onItemClicked: menuItemOptions.default.onItemClicked + }); } + } - notebookTypes.push({ - cssClass: menuItemOptions.snapshot.cssClass, - name: menuItemOptions.snapshot.name, - onItemClicked: menuItemOptions.snapshot.onItemClicked - }); + notebookTypes.push({ + cssClass: menuItemOptions.snapshot.cssClass, + name: menuItemOptions.snapshot.name, + onItemClicked: menuItemOptions.snapshot.onItemClicked + }); - return notebookTypes; + return notebookTypes; } diff --git a/src/plugins/notebook/utils/notebook-storage.js b/src/plugins/notebook/utils/notebook-storage.js index 7de2fe1112..aaca877611 100644 --- a/src/plugins/notebook/utils/notebook-storage.js +++ b/src/plugins/notebook/utils/notebook-storage.js @@ -5,118 +5,122 @@ let currentNotebookObjectIdentifier = null; let unlisten = null; function defaultNotebookObjectChanged(newDomainObject) { - if (newDomainObject.location !== null) { - currentNotebookObjectIdentifier = newDomainObject.identifier; + if (newDomainObject.location !== null) { + currentNotebookObjectIdentifier = newDomainObject.identifier; - return; - } + return; + } - if (unlisten) { - unlisten(); - unlisten = null; - } + if (unlisten) { + unlisten(); + unlisten = null; + } - clearDefaultNotebook(); + clearDefaultNotebook(); } function observeDefaultNotebookObject(openmct, notebookStorage, domainObject) { - if (currentNotebookObjectIdentifier - && objectUtils.makeKeyString(currentNotebookObjectIdentifier) === objectUtils.makeKeyString(notebookStorage.identifier)) { - return; - } + if ( + currentNotebookObjectIdentifier && + objectUtils.makeKeyString(currentNotebookObjectIdentifier) === + objectUtils.makeKeyString(notebookStorage.identifier) + ) { + return; + } - removeListener(); + removeListener(); - unlisten = openmct.objects.observe(domainObject, '*', defaultNotebookObjectChanged); + unlisten = openmct.objects.observe(domainObject, '*', defaultNotebookObjectChanged); } function removeListener() { - if (unlisten) { - unlisten(); - unlisten = null; - } + if (unlisten) { + unlisten(); + unlisten = null; + } } function saveDefaultNotebook(notebookStorage) { - window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, JSON.stringify(notebookStorage)); + window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, JSON.stringify(notebookStorage)); } export function clearDefaultNotebook() { - currentNotebookObjectIdentifier = null; - removeListener(); + currentNotebookObjectIdentifier = null; + removeListener(); - window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, null); + window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, null); } export function getDefaultNotebook() { - const notebookStorage = window.localStorage.getItem(NOTEBOOK_LOCAL_STORAGE); + const notebookStorage = window.localStorage.getItem(NOTEBOOK_LOCAL_STORAGE); - return JSON.parse(notebookStorage); + return JSON.parse(notebookStorage); } export function getNotebookSectionAndPage(domainObject, sectionId, pageId) { - const configuration = domainObject.configuration; - const section = configuration && configuration.sections.find(s => s.id === sectionId); - const page = section && section.pages.find(p => p.id === pageId); + const configuration = domainObject.configuration; + const section = configuration && configuration.sections.find((s) => s.id === sectionId); + const page = section && section.pages.find((p) => p.id === pageId); - return { - section, - page - }; + return { + section, + page + }; } export async function getDefaultNotebookLink(openmct, domainObject = null) { - if (!domainObject) { - return null; - } + if (!domainObject) { + return null; + } - const path = await openmct.objects.getOriginalPath(domainObject.identifier) - .then(openmct.objects.getRelativePath); - const { defaultPageId, defaultSectionId } = getDefaultNotebook(); + const path = await openmct.objects + .getOriginalPath(domainObject.identifier) + .then(openmct.objects.getRelativePath); + const { defaultPageId, defaultSectionId } = getDefaultNotebook(); - return `#/browse/${path}?sectionId=${defaultSectionId}&pageId=${defaultPageId}`; + return `#/browse/${path}?sectionId=${defaultSectionId}&pageId=${defaultPageId}`; } export function setDefaultNotebook(openmct, notebookStorage, domainObject) { - observeDefaultNotebookObject(openmct, notebookStorage, domainObject); - saveDefaultNotebook(notebookStorage); + observeDefaultNotebookObject(openmct, notebookStorage, domainObject); + saveDefaultNotebook(notebookStorage); } export function setDefaultNotebookSectionId(sectionId) { - const notebookStorage = getDefaultNotebook(); - notebookStorage.defaultSectionId = sectionId; - saveDefaultNotebook(notebookStorage); + const notebookStorage = getDefaultNotebook(); + notebookStorage.defaultSectionId = sectionId; + saveDefaultNotebook(notebookStorage); } export function setDefaultNotebookPageId(pageId) { - const notebookStorage = getDefaultNotebook(); - notebookStorage.defaultPageId = pageId; - saveDefaultNotebook(notebookStorage); + const notebookStorage = getDefaultNotebook(); + notebookStorage.defaultPageId = pageId; + saveDefaultNotebook(notebookStorage); } export function validateNotebookStorageObject() { - const notebookStorage = getDefaultNotebook(); - if (!notebookStorage) { - return true; - } + const notebookStorage = getDefaultNotebook(); + if (!notebookStorage) { + return true; + } - let valid = false; - if (notebookStorage) { - const oldInvalidKeys = ['notebookMeta', 'page', 'section']; - valid = Object.entries(notebookStorage).every(([key, value]) => { - const validKey = key !== undefined && key !== null; - const validValue = value !== undefined && value !== null; - const hasOldInvalidKeys = oldInvalidKeys.includes(key); + let valid = false; + if (notebookStorage) { + const oldInvalidKeys = ['notebookMeta', 'page', 'section']; + valid = Object.entries(notebookStorage).every(([key, value]) => { + const validKey = key !== undefined && key !== null; + const validValue = value !== undefined && value !== null; + const hasOldInvalidKeys = oldInvalidKeys.includes(key); - return validKey && validValue && !hasOldInvalidKeys; - }); - } + return validKey && validValue && !hasOldInvalidKeys; + }); + } - if (valid) { - return notebookStorage; - } + if (valid) { + return notebookStorage; + } - console.warn('Invalid Notebook object, clearing default notebook storage'); + console.warn('Invalid Notebook object, clearing default notebook storage'); - clearDefaultNotebook(); + clearDefaultNotebook(); } diff --git a/src/plugins/notebook/utils/notebook-storageSpec.js b/src/plugins/notebook/utils/notebook-storageSpec.js index b593625762..48e9006d1d 100644 --- a/src/plugins/notebook/utils/notebook-storageSpec.js +++ b/src/plugins/notebook/utils/notebook-storageSpec.js @@ -24,155 +24,157 @@ import * as NotebookStorage from './notebook-storage'; import { createOpenMct, resetApplicationState } from 'utils/testing'; const notebookSection = { - id: 'temp-section', - isDefault: false, - isSelected: true, - name: 'section', - pages: [ - { - id: 'temp-page', - isDefault: false, - isSelected: true, - name: 'page', - pageTitle: 'Page' - } - ], - sectionTitle: 'Section' + id: 'temp-section', + isDefault: false, + isSelected: true, + name: 'section', + pages: [ + { + id: 'temp-page', + isDefault: false, + isSelected: true, + name: 'page', + pageTitle: 'Page' + } + ], + sectionTitle: 'Section' }; const domainObject = { - name: 'notebook', - identifier: { - namespace: '', - key: 'test-notebook' - }, - configuration: { - sections: [ - notebookSection - ] - } + name: 'notebook', + identifier: { + namespace: '', + key: 'test-notebook' + }, + configuration: { + sections: [notebookSection] + } }; const notebookStorage = { - name: 'notebook', - identifier: { - namespace: '', - key: 'test-notebook' - }, - defaultSectionId: 'temp-section', - defaultPageId: 'temp-page' + name: 'notebook', + identifier: { + namespace: '', + key: 'test-notebook' + }, + defaultSectionId: 'temp-section', + defaultPageId: 'temp-page' }; let openmct; describe('Notebook Storage:', () => { + beforeEach(() => { + openmct = createOpenMct(); + + window.localStorage.setItem('notebook-storage', null); + openmct.objects.addProvider( + '', + jasmine.createSpyObj('mockNotebookProvider', ['create', 'update']) + ); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('has empty local Storage', () => { + expect(window.localStorage).not.toBeNull(); + }); + + it('has null notebookstorage on clearDefaultNotebook', () => { + window.localStorage.setItem('notebook-storage', notebookStorage); + NotebookStorage.clearDefaultNotebook(); + const defaultNotebook = NotebookStorage.getDefaultNotebook(); + + expect(defaultNotebook).toBeNull(); + }); + + it('has correct notebookstorage on setDefaultNotebook', () => { + NotebookStorage.setDefaultNotebook(openmct, notebookStorage, domainObject); + const defaultNotebook = NotebookStorage.getDefaultNotebook(); + + expect(JSON.stringify(defaultNotebook)).toBe(JSON.stringify(notebookStorage)); + }); + + it('has correct section on setDefaultNotebookSectionId', () => { + const section = { + id: 'new-temp-section', + isDefault: true, + isSelected: true, + name: 'new section', + pages: [], + sectionTitle: 'Section' + }; + + NotebookStorage.setDefaultNotebook(openmct, notebookStorage, domainObject); + NotebookStorage.setDefaultNotebookSectionId(section.id); + + const defaultNotebook = NotebookStorage.getDefaultNotebook(); + const defaultSectionId = defaultNotebook.defaultSectionId; + expect(section.id).toBe(defaultSectionId); + }); + + it('has correct page on setDefaultNotebookPageId', () => { + const page = { + id: 'new-temp-page', + isDefault: true, + isSelected: true, + name: 'new page', + pageTitle: 'Page' + }; + + NotebookStorage.setDefaultNotebook(openmct, notebookStorage, domainObject); + NotebookStorage.setDefaultNotebookPageId(page.id); + + const defaultNotebook = NotebookStorage.getDefaultNotebook(); + const newPageId = defaultNotebook.defaultPageId; + expect(page.id).toBe(newPageId); + }); + + describe('is getNotebookSectionAndPage function searches and returns correct,', () => { + let section; + let page; + beforeEach(() => { - openmct = createOpenMct(); + const sectionId = 'temp-section'; + const pageId = 'temp-page'; - window.localStorage.setItem('notebook-storage', null); - openmct.objects.addProvider('', jasmine.createSpyObj('mockNotebookProvider', [ - 'create', - 'update' - ])); + const sectionAndpage = NotebookStorage.getNotebookSectionAndPage( + domainObject, + sectionId, + pageId + ); + section = sectionAndpage.section; + page = sectionAndpage.page; }); - afterEach(() => { - return resetApplicationState(openmct); + it('id for section from notebook domain object', () => { + expect(section.id).toEqual('temp-section'); }); - it('has empty local Storage', () => { - expect(window.localStorage).not.toBeNull(); + it('name for section from notebook domain object', () => { + expect(section.name).toEqual('section'); }); - it('has null notebookstorage on clearDefaultNotebook', () => { - window.localStorage.setItem('notebook-storage', notebookStorage); - NotebookStorage.clearDefaultNotebook(); - const defaultNotebook = NotebookStorage.getDefaultNotebook(); - - expect(defaultNotebook).toBeNull(); + it('sectionTitle for section from notebook domain object', () => { + expect(section.sectionTitle).toEqual('Section'); }); - it('has correct notebookstorage on setDefaultNotebook', () => { - NotebookStorage.setDefaultNotebook(openmct, notebookStorage, domainObject); - const defaultNotebook = NotebookStorage.getDefaultNotebook(); - - expect(JSON.stringify(defaultNotebook)).toBe(JSON.stringify(notebookStorage)); + it('number of pages for section from notebook domain object', () => { + expect(section.pages.length).toEqual(1); }); - it('has correct section on setDefaultNotebookSectionId', () => { - const section = { - id: 'new-temp-section', - isDefault: true, - isSelected: true, - name: 'new section', - pages: [], - sectionTitle: 'Section' - }; - - NotebookStorage.setDefaultNotebook(openmct, notebookStorage, domainObject); - NotebookStorage.setDefaultNotebookSectionId(section.id); - - const defaultNotebook = NotebookStorage.getDefaultNotebook(); - const defaultSectionId = defaultNotebook.defaultSectionId; - expect(section.id).toBe(defaultSectionId); + it('id for page from notebook domain object', () => { + expect(page.id).toEqual('temp-page'); }); - it('has correct page on setDefaultNotebookPageId', () => { - const page = { - id: 'new-temp-page', - isDefault: true, - isSelected: true, - name: 'new page', - pageTitle: 'Page' - }; - - NotebookStorage.setDefaultNotebook(openmct, notebookStorage, domainObject); - NotebookStorage.setDefaultNotebookPageId(page.id); - - const defaultNotebook = NotebookStorage.getDefaultNotebook(); - const newPageId = defaultNotebook.defaultPageId; - expect(page.id).toBe(newPageId); + it('name for page from notebook domain object', () => { + expect(page.name).toEqual('page'); }); - describe('is getNotebookSectionAndPage function searches and returns correct,', () => { - let section; - let page; - - beforeEach(() => { - const sectionId = 'temp-section'; - const pageId = 'temp-page'; - - const sectionAndpage = NotebookStorage.getNotebookSectionAndPage(domainObject, sectionId, pageId); - section = sectionAndpage.section; - page = sectionAndpage.page; - }); - - it('id for section from notebook domain object', () => { - expect(section.id).toEqual('temp-section'); - }); - - it('name for section from notebook domain object', () => { - expect(section.name).toEqual('section'); - }); - - it('sectionTitle for section from notebook domain object', () => { - expect(section.sectionTitle).toEqual('Section'); - }); - - it('number of pages for section from notebook domain object', () => { - expect(section.pages.length).toEqual(1); - }); - - it('id for page from notebook domain object', () => { - expect(page.id).toEqual('temp-page'); - }); - - it('name for page from notebook domain object', () => { - expect(page.name).toEqual('page'); - }); - - it('pageTitle for page from notebook domain object', () => { - expect(page.pageTitle).toEqual('Page'); - }); + it('pageTitle for page from notebook domain object', () => { + expect(page.pageTitle).toEqual('Page'); }); + }); }); diff --git a/src/plugins/notebook/utils/painterroInstance.js b/src/plugins/notebook/utils/painterroInstance.js index 399d26dd17..22b8d464cf 100644 --- a/src/plugins/notebook/utils/painterroInstance.js +++ b/src/plugins/notebook/utils/painterroInstance.js @@ -2,89 +2,89 @@ import Painterro from 'painterro'; import { getThumbnailURLFromimageUrl } from './notebook-image'; const DEFAULT_CONFIG = { - activeColor: '#ff0000', - activeColorAlpha: 1.0, - activeFillColor: '#fff', - activeFillColorAlpha: 0.0, - backgroundFillColor: '#000', - backgroundFillColorAlpha: 0.0, - defaultFontSize: 16, - defaultLineWidth: 2, - defaultTool: 'ellipse', - hiddenTools: ['save', 'open', 'close', 'eraser', 'pixelize', 'rotate', 'settings', 'resize'], - translation: { - name: 'en', - strings: { - lineColor: 'Line', - fillColor: 'Fill', - lineWidth: 'Size', - textColor: 'Color', - fontSize: 'Size', - fontStyle: 'Style' - } + activeColor: '#ff0000', + activeColorAlpha: 1.0, + activeFillColor: '#fff', + activeFillColorAlpha: 0.0, + backgroundFillColor: '#000', + backgroundFillColorAlpha: 0.0, + defaultFontSize: 16, + defaultLineWidth: 2, + defaultTool: 'ellipse', + hiddenTools: ['save', 'open', 'close', 'eraser', 'pixelize', 'rotate', 'settings', 'resize'], + translation: { + name: 'en', + strings: { + lineColor: 'Line', + fillColor: 'Fill', + lineWidth: 'Size', + textColor: 'Color', + fontSize: 'Size', + fontStyle: 'Style' } + } }; export default class PainterroInstance { - constructor(element) { - this.elementId = element.id; - this.isSave = false; - this.painterroInstance = undefined; - this.saveCallback = undefined; + constructor(element) { + this.elementId = element.id; + this.isSave = false; + this.painterroInstance = undefined; + this.saveCallback = undefined; + } + + dismiss() { + this.isSave = false; + this.painterroInstance.save(); + } + + intialize() { + this.config = Object.assign({}, DEFAULT_CONFIG); + + this.config.id = this.elementId; + this.config.saveHandler = this.saveHandler.bind(this); + + this.painterro = Painterro(this.config); + } + + save(callback) { + this.saveCallback = callback; + this.isSave = true; + this.painterroInstance.save(); + } + + saveHandler(image, done) { + if (this.isSave) { + const url = image.asBlob(); + + const reader = new window.FileReader(); + reader.readAsDataURL(url); + reader.onloadend = async () => { + const fullSizeImageURL = reader.result; + const thumbnailURL = await getThumbnailURLFromimageUrl(fullSizeImageURL); + const snapshotObject = { + fullSizeImage: { + src: fullSizeImageURL, + type: url.type, + size: url.size, + modified: Date.now() + }, + thumbnailImage: { + src: thumbnailURL, + modified: Date.now() + } + }; + + this.saveCallback(snapshotObject); + + done(true); + }; + } else { + done(true); } + } - dismiss() { - this.isSave = false; - this.painterroInstance.save(); - } - - intialize() { - this.config = Object.assign({}, DEFAULT_CONFIG); - - this.config.id = this.elementId; - this.config.saveHandler = this.saveHandler.bind(this); - - this.painterro = Painterro(this.config); - } - - save(callback) { - this.saveCallback = callback; - this.isSave = true; - this.painterroInstance.save(); - } - - saveHandler(image, done) { - if (this.isSave) { - const url = image.asBlob(); - - const reader = new window.FileReader(); - reader.readAsDataURL(url); - reader.onloadend = async () => { - const fullSizeImageURL = reader.result; - const thumbnailURL = await getThumbnailURLFromimageUrl(fullSizeImageURL); - const snapshotObject = { - fullSizeImage: { - src: fullSizeImageURL, - type: url.type, - size: url.size, - modified: Date.now() - }, - thumbnailImage: { - src: thumbnailURL, - modified: Date.now() - } - }; - - this.saveCallback(snapshotObject); - - done(true); - }; - } else { - done(true); - } - } - - show(src) { - this.painterroInstance = this.painterro.show(src); - } + show(src) { + this.painterroInstance = this.painterro.show(src); + } } diff --git a/src/plugins/notebook/utils/removeDialog.js b/src/plugins/notebook/utils/removeDialog.js index bee7a7bc3a..2be4edd7eb 100644 --- a/src/plugins/notebook/utils/removeDialog.js +++ b/src/plugins/notebook/utils/removeDialog.js @@ -1,36 +1,38 @@ export default class RemoveDialog { - constructor(openmct, options) { - this.name = options.name; - this.openmct = openmct; + constructor(openmct, options) { + this.name = options.name; + this.openmct = openmct; - this.callback = options.callback; - this.cssClass = options.cssClass || 'icon-trash'; - this.description = options.description || 'Remove action dialog'; - this.iconClass = "error"; - this.key = 'remove'; - this.message = options.message || `This action will permanently ${this.name.toLowerCase()}. Do you wish to continue?`; - } + this.callback = options.callback; + this.cssClass = options.cssClass || 'icon-trash'; + this.description = options.description || 'Remove action dialog'; + this.iconClass = 'error'; + this.key = 'remove'; + this.message = + options.message || + `This action will permanently ${this.name.toLowerCase()}. Do you wish to continue?`; + } - show() { - const dialog = this.openmct.overlays.dialog({ - iconClass: this.iconClass, - message: this.message, - buttons: [ - { - label: "Ok", - callback: () => { - this.callback(true); - dialog.dismiss(); - } - }, - { - label: "Cancel", - callback: () => { - this.callback(false); - dialog.dismiss(); - } - } - ] - }); - } + show() { + const dialog = this.openmct.overlays.dialog({ + iconClass: this.iconClass, + message: this.message, + buttons: [ + { + label: 'Ok', + callback: () => { + this.callback(true); + dialog.dismiss(); + } + }, + { + label: 'Cancel', + callback: () => { + this.callback(false); + dialog.dismiss(); + } + } + ] + }); + } } diff --git a/src/plugins/notificationIndicator/components/NotificationIndicator.vue b/src/plugins/notificationIndicator/components/NotificationIndicator.vue index 63895380a1..464f893b50 100644 --- a/src/plugins/notificationIndicator/components/NotificationIndicator.vue +++ b/src/plugins/notificationIndicator/components/NotificationIndicator.vue @@ -20,78 +20,75 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/notificationIndicator/components/NotificationMessage.vue b/src/plugins/notificationIndicator/components/NotificationMessage.vue index 018c1c6c37..0118b173f2 100644 --- a/src/plugins/notificationIndicator/components/NotificationMessage.vue +++ b/src/plugins/notificationIndicator/components/NotificationMessage.vue @@ -20,107 +20,100 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/notificationIndicator/components/NotificationsList.vue b/src/plugins/notificationIndicator/components/NotificationsList.vue index e7206a54f5..0ae6c270ef 100644 --- a/src/plugins/notificationIndicator/components/NotificationsList.vue +++ b/src/plugins/notificationIndicator/components/NotificationsList.vue @@ -20,79 +20,76 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/notificationIndicator/plugin.js b/src/plugins/notificationIndicator/plugin.js index e12f2c80f1..08b5553113 100644 --- a/src/plugins/notificationIndicator/plugin.js +++ b/src/plugins/notificationIndicator/plugin.js @@ -23,23 +23,23 @@ import Vue from 'vue'; import NotificationIndicator from './components/NotificationIndicator.vue'; export default function plugin() { - return function install(openmct) { - let component = new Vue ({ - components: { - NotificationIndicator: NotificationIndicator - }, - provide: { - openmct - }, - template: '' - }); + return function install(openmct) { + let component = new Vue({ + components: { + NotificationIndicator: NotificationIndicator + }, + provide: { + openmct + }, + template: '' + }); - let indicator = { - key: 'notifications-indicator', - element: component.$mount().$el, - priority: openmct.priority.DEFAULT - }; - - openmct.indicators.add(indicator); + let indicator = { + key: 'notifications-indicator', + element: component.$mount().$el, + priority: openmct.priority.DEFAULT }; + + openmct.indicators.add(indicator); + }; } diff --git a/src/plugins/notificationIndicator/pluginSpec.js b/src/plugins/notificationIndicator/pluginSpec.js index cb20959080..b04b361959 100644 --- a/src/plugins/notificationIndicator/pluginSpec.js +++ b/src/plugins/notificationIndicator/pluginSpec.js @@ -22,51 +22,48 @@ import NotificationIndicatorPlugin from './plugin.js'; import Vue from 'vue'; -import { - createOpenMct, - resetApplicationState -} from 'utils/testing'; +import { createOpenMct, resetApplicationState } from 'utils/testing'; describe('the plugin', () => { - let notificationIndicatorPlugin; - let openmct; - let indicatorElement; - let parentElement; - let mockMessages = ['error', 'test', 'notifications']; + let notificationIndicatorPlugin; + let openmct; + let indicatorElement; + let parentElement; + let mockMessages = ['error', 'test', 'notifications']; - beforeEach((done) => { - openmct = createOpenMct(); + beforeEach((done) => { + openmct = createOpenMct(); - notificationIndicatorPlugin = new NotificationIndicatorPlugin(); - openmct.install(notificationIndicatorPlugin); + notificationIndicatorPlugin = new NotificationIndicatorPlugin(); + openmct.install(notificationIndicatorPlugin); - parentElement = document.createElement('div'); + parentElement = document.createElement('div'); - openmct.on('start', () => { - mockMessages.forEach(message => { - openmct.notifications.error(message); - }); - done(); - }); - - openmct.start(); + openmct.on('start', () => { + mockMessages.forEach((message) => { + openmct.notifications.error(message); + }); + done(); }); - afterEach(() => { - return resetApplicationState(openmct); + openmct.start(); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + describe('the indicator plugin element', () => { + beforeEach(() => { + parentElement.append(indicatorElement); + + return Vue.nextTick(); }); - describe('the indicator plugin element', () => { - beforeEach(() => { - parentElement.append(indicatorElement); + it('notifies the user of the number of notifications', () => { + let notificationCountElement = document.querySelector('.c-indicator__count'); - return Vue.nextTick(); - }); - - it('notifies the user of the number of notifications', () => { - let notificationCountElement = document.querySelector('.c-indicator__count'); - - expect(notificationCountElement.innerText).toEqual(mockMessages.length.toString()); - }); + expect(notificationCountElement.innerText).toEqual(mockMessages.length.toString()); }); + }); }); diff --git a/src/plugins/objectMigration/Migrations.js b/src/plugins/objectMigration/Migrations.js index fdc770e280..63e43d6ebb 100644 --- a/src/plugins/objectMigration/Migrations.js +++ b/src/plugins/objectMigration/Migrations.js @@ -20,253 +20,260 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - 'uuid' -], function ( - { v4: uuid } -) { - return function Migrations(openmct) { - function getColumnNameKeyMap(domainObject) { - let composition = openmct.composition.get(domainObject); - if (composition) { - return composition.load().then(composees => { - return composees.reduce((nameKeyMap, composee) => { - let metadata = openmct.telemetry.getMetadata(composee); - if (metadata !== undefined) { - metadata.values().forEach(value => { - nameKeyMap[value.name] = value.key; - }); - } - - return nameKeyMap; - }, {}); - }); - } else { - return Promise.resolve([]); +define(['uuid'], function ({ v4: uuid }) { + return function Migrations(openmct) { + function getColumnNameKeyMap(domainObject) { + let composition = openmct.composition.get(domainObject); + if (composition) { + return composition.load().then((composees) => { + return composees.reduce((nameKeyMap, composee) => { + let metadata = openmct.telemetry.getMetadata(composee); + if (metadata !== undefined) { + metadata.values().forEach((value) => { + nameKeyMap[value.name] = value.key; + }); } + + return nameKeyMap; + }, {}); + }); + } else { + return Promise.resolve([]); + } + } + + function isTelemetry(domainObject) { + if ( + openmct.telemetry.isTelemetryObject(domainObject) && + domainObject.type !== 'summary-widget' && + domainObject.type !== 'example.imagery' + ) { + return true; + } else { + return false; + } + } + + function migrateDisplayLayout(domainObject, childObjects) { + const DEFAULT_GRID_SIZE = [32, 32]; + let migratedObject = Object.assign({}, domainObject); + let panels = migratedObject.configuration.layout.panels; + let items = []; + + Object.keys(panels).forEach((key) => { + let panel = panels[key]; + let childDomainObject = childObjects[key]; + let identifier = undefined; + + if (isTelemetry(childDomainObject)) { + // If object is a telemetry point, convert it to a plot and + // replace the object in migratedObject composition with the plot. + identifier = { + key: uuid(), + namespace: migratedObject.identifier.namespace + }; + let plotObject = { + identifier: identifier, + location: childDomainObject.location, + name: childDomainObject.name, + type: 'telemetry.plot.overlay' + }; + let plotType = openmct.types.get('telemetry.plot.overlay'); + plotType.definition.initialize(plotObject); + plotObject.composition.push(childDomainObject.identifier); + openmct.objects.mutate(plotObject, 'persisted', Date.now()); + + let keyString = openmct.objects.makeKeyString(childDomainObject.identifier); + let clonedComposition = Object.assign([], migratedObject.composition); + clonedComposition.forEach((objIdentifier, index) => { + if (openmct.objects.makeKeyString(objIdentifier) === keyString) { + migratedObject.composition[index] = plotObject.identifier; + } + }); } - function isTelemetry(domainObject) { - if (openmct.telemetry.isTelemetryObject(domainObject) - && domainObject.type !== 'summary-widget' - && domainObject.type !== 'example.imagery') { - return true; - } else { - return false; - } + items.push({ + width: panel.dimensions[0], + height: panel.dimensions[1], + x: panel.position[0], + y: panel.position[1], + identifier: identifier || childDomainObject.identifier, + id: uuid(), + type: 'subobject-view', + hasFrame: panel.hasFrame + }); + }); + + migratedObject.configuration.items = items; + migratedObject.configuration.layoutGrid = migratedObject.layoutGrid || DEFAULT_GRID_SIZE; + delete migratedObject.layoutGrid; + delete migratedObject.configuration.layout; + + return migratedObject; + } + + function migrateFixedPositionConfiguration(elements, telemetryObjects, gridSize) { + const DEFAULT_STROKE = 'transparent'; + const DEFAULT_SIZE = '13px'; + const DEFAULT_COLOR = ''; + const DEFAULT_FILL = ''; + let items = []; + + elements.forEach((element) => { + let item = { + x: element.x, + y: element.y, + width: element.width, + height: element.height, + id: uuid() + }; + + if (!element.useGrid) { + item.x = Math.round(item.x / gridSize[0]); + item.y = Math.round(item.y / gridSize[1]); + item.width = Math.round(item.width / gridSize[0]); + item.height = Math.round(item.height / gridSize[1]); } - function migrateDisplayLayout(domainObject, childObjects) { - const DEFAULT_GRID_SIZE = [32, 32]; - let migratedObject = Object.assign({}, domainObject); - let panels = migratedObject.configuration.layout.panels; - let items = []; + if (element.type === 'fixed.telemetry') { + item.type = 'telemetry-view'; + item.stroke = element.stroke || DEFAULT_STROKE; + item.fill = element.fill || DEFAULT_FILL; + item.color = element.color || DEFAULT_COLOR; + 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()?.key; + } else if (element.type === 'fixed.box') { + item.type = 'box-view'; + item.stroke = element.stroke || DEFAULT_STROKE; + item.fill = element.fill || DEFAULT_FILL; + } else if (element.type === 'fixed.line') { + item.type = 'line-view'; + item.x2 = element.x2; + item.y2 = element.y2; + item.stroke = element.stroke || DEFAULT_STROKE; + delete item.height; + delete item.width; + } else if (element.type === 'fixed.text') { + item.type = 'text-view'; + item.text = element.text; + item.stroke = element.stroke || DEFAULT_STROKE; + item.fill = element.fill || DEFAULT_FILL; + item.color = element.color || DEFAULT_COLOR; + item.size = element.size || DEFAULT_SIZE; + } else if (element.type === 'fixed.image') { + item.type = 'image-view'; + item.url = element.url; + item.stroke = element.stroke || DEFAULT_STROKE; + } - Object.keys(panels).forEach(key => { - let panel = panels[key]; - let childDomainObject = childObjects[key]; - let identifier = undefined; + items.push(item); + }); - if (isTelemetry(childDomainObject)) { - // If object is a telemetry point, convert it to a plot and - // replace the object in migratedObject composition with the plot. - identifier = { - key: uuid(), - namespace: migratedObject.identifier.namespace - }; - let plotObject = { - identifier: identifier, - location: childDomainObject.location, - name: childDomainObject.name, - type: "telemetry.plot.overlay" - }; - let plotType = openmct.types.get('telemetry.plot.overlay'); - plotType.definition.initialize(plotObject); - plotObject.composition.push(childDomainObject.identifier); - openmct.objects.mutate(plotObject, 'persisted', Date.now()); + return items; + } - let keyString = openmct.objects.makeKeyString(childDomainObject.identifier); - let clonedComposition = Object.assign([], migratedObject.composition); - clonedComposition.forEach((objIdentifier, index) => { - if (openmct.objects.makeKeyString(objIdentifier) === keyString) { - migratedObject.composition[index] = plotObject.identifier; - } - }); - } - - items.push({ - width: panel.dimensions[0], - height: panel.dimensions[1], - x: panel.position[0], - y: panel.position[1], - identifier: identifier || childDomainObject.identifier, - id: uuid(), - type: 'subobject-view', - hasFrame: panel.hasFrame - }); + return [ + { + check(domainObject) { + return ( + domainObject.type === 'layout' && + domainObject.configuration && + domainObject.configuration.layout + ); + }, + migrate(domainObject) { + let childObjects = {}; + let promises = Object.keys(domainObject.configuration.layout.panels).map((key) => { + return openmct.objects.get(key).then((object) => { + childObjects[key] = object; }); + }); - migratedObject.configuration.items = items; - migratedObject.configuration.layoutGrid = migratedObject.layoutGrid || DEFAULT_GRID_SIZE; - delete migratedObject.layoutGrid; - delete migratedObject.configuration.layout; - - return migratedObject; + return Promise.all(promises).then(function () { + return migrateDisplayLayout(domainObject, childObjects); + }); } + }, + { + check(domainObject) { + return ( + domainObject.type === 'telemetry.fixed' && + domainObject.configuration && + domainObject.configuration['fixed-display'] + ); + }, + migrate(domainObject) { + const DEFAULT_GRID_SIZE = [64, 16]; + let newLayoutObject = { + identifier: domainObject.identifier, + location: domainObject.location, + name: domainObject.name, + type: 'layout' + }; + let gridSize = domainObject.layoutGrid || DEFAULT_GRID_SIZE; + let layoutType = openmct.types.get('layout'); + layoutType.definition.initialize(newLayoutObject); + newLayoutObject.composition = domainObject.composition; + newLayoutObject.configuration.layoutGrid = gridSize; - function migrateFixedPositionConfiguration(elements, telemetryObjects, gridSize) { - const DEFAULT_STROKE = "transparent"; - const DEFAULT_SIZE = "13px"; - const DEFAULT_COLOR = ""; - const DEFAULT_FILL = ""; - let items = []; - - elements.forEach(element => { - let item = { - x: element.x, - y: element.y, - width: element.width, - height: element.height, - id: uuid() - }; - - if (!element.useGrid) { - item.x = Math.round(item.x / gridSize[0]); - item.y = Math.round(item.y / gridSize[1]); - item.width = Math.round(item.width / gridSize[0]); - item.height = Math.round(item.height / gridSize[1]); - } - - if (element.type === "fixed.telemetry") { - item.type = "telemetry-view"; - item.stroke = element.stroke || DEFAULT_STROKE; - item.fill = element.fill || DEFAULT_FILL; - item.color = element.color || DEFAULT_COLOR; - 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()?.key; - } else if (element.type === 'fixed.box') { - item.type = "box-view"; - item.stroke = element.stroke || DEFAULT_STROKE; - item.fill = element.fill || DEFAULT_FILL; - } else if (element.type === 'fixed.line') { - item.type = "line-view"; - item.x2 = element.x2; - item.y2 = element.y2; - item.stroke = element.stroke || DEFAULT_STROKE; - delete item.height; - delete item.width; - } else if (element.type === 'fixed.text') { - item.type = "text-view"; - item.text = element.text; - item.stroke = element.stroke || DEFAULT_STROKE; - item.fill = element.fill || DEFAULT_FILL; - item.color = element.color || DEFAULT_COLOR; - item.size = element.size || DEFAULT_SIZE; - } else if (element.type === 'fixed.image') { - item.type = "image-view"; - item.url = element.url; - item.stroke = element.stroke || DEFAULT_STROKE; - } - - items.push(item); - }); - - return items; - } - - return [ - { - check(domainObject) { - return domainObject.type === 'layout' - && domainObject.configuration - && domainObject.configuration.layout; - }, - migrate(domainObject) { - let childObjects = {}; - let promises = Object.keys(domainObject.configuration.layout.panels).map(key => { - return openmct.objects.get(key) - .then(object => { - childObjects[key] = object; - }); - }); - - return Promise.all(promises) - .then(function () { - return migrateDisplayLayout(domainObject, childObjects); - }); - } - }, - { - check(domainObject) { - return domainObject.type === 'telemetry.fixed' - && domainObject.configuration - && domainObject.configuration['fixed-display']; - }, - migrate(domainObject) { - const DEFAULT_GRID_SIZE = [64, 16]; - let newLayoutObject = { - identifier: domainObject.identifier, - location: domainObject.location, - name: domainObject.name, - type: "layout" - }; - let gridSize = domainObject.layoutGrid || DEFAULT_GRID_SIZE; - let layoutType = openmct.types.get('layout'); - layoutType.definition.initialize(newLayoutObject); - newLayoutObject.composition = domainObject.composition; - newLayoutObject.configuration.layoutGrid = gridSize; - - let elements = domainObject.configuration['fixed-display'].elements; - let telemetryObjects = {}; - let promises = elements.map(element => { - if (element.id) { - return openmct.objects.get(element.id) - .then(object => { - telemetryObjects[element.id] = object; - }); - } else { - return Promise.resolve(false); - } - }); - - return Promise.all(promises) - .then(function () { - newLayoutObject.configuration.items = - migrateFixedPositionConfiguration(elements, telemetryObjects, gridSize); - - return newLayoutObject; - }); - } - }, - { - check(domainObject) { - return domainObject.type === 'table' - && domainObject.configuration - && domainObject.configuration.table; - }, - migrate(domainObject) { - let currentTableConfiguration = domainObject.configuration.table || {}; - let currentColumnConfiguration = currentTableConfiguration.columns || {}; - - return getColumnNameKeyMap(domainObject).then(nameKeyMap => { - let hiddenColumns = Object.keys(currentColumnConfiguration).filter(columnName => { - return currentColumnConfiguration[columnName] === false; - }).reduce((hiddenColumnsMap, hiddenColumnName) => { - let key = nameKeyMap[hiddenColumnName]; - hiddenColumnsMap[key] = true; - - return hiddenColumnsMap; - }, {}); - - domainObject.configuration.hiddenColumns = hiddenColumns; - delete domainObject.configuration.table; - - return domainObject; - }); - } + let elements = domainObject.configuration['fixed-display'].elements; + let telemetryObjects = {}; + let promises = elements.map((element) => { + if (element.id) { + return openmct.objects.get(element.id).then((object) => { + telemetryObjects[element.id] = object; + }); + } else { + return Promise.resolve(false); } - ]; - }; + }); + + return Promise.all(promises).then(function () { + newLayoutObject.configuration.items = migrateFixedPositionConfiguration( + elements, + telemetryObjects, + gridSize + ); + + return newLayoutObject; + }); + } + }, + { + check(domainObject) { + return ( + domainObject.type === 'table' && + domainObject.configuration && + domainObject.configuration.table + ); + }, + migrate(domainObject) { + let currentTableConfiguration = domainObject.configuration.table || {}; + let currentColumnConfiguration = currentTableConfiguration.columns || {}; + + return getColumnNameKeyMap(domainObject).then((nameKeyMap) => { + let hiddenColumns = Object.keys(currentColumnConfiguration) + .filter((columnName) => { + return currentColumnConfiguration[columnName] === false; + }) + .reduce((hiddenColumnsMap, hiddenColumnName) => { + let key = nameKeyMap[hiddenColumnName]; + hiddenColumnsMap[key] = true; + + return hiddenColumnsMap; + }, {}); + + domainObject.configuration.hiddenColumns = hiddenColumns; + delete domainObject.configuration.table; + + return domainObject; + }); + } + } + ]; + }; }); diff --git a/src/plugins/objectMigration/plugin.js b/src/plugins/objectMigration/plugin.js index 5e0cc60ba5..f47065d792 100644 --- a/src/plugins/objectMigration/plugin.js +++ b/src/plugins/objectMigration/plugin.js @@ -23,33 +23,30 @@ import Migrations from './Migrations.js'; export default function () { - return function (openmct) { - let migrations = Migrations(openmct); + return function (openmct) { + let migrations = Migrations(openmct); - function needsMigration(domainObject) { - return migrations.some(m => m.check(domainObject)); + function needsMigration(domainObject) { + return migrations.some((m) => m.check(domainObject)); + } + + function migrateObject(domainObject) { + return migrations.filter((m) => m.check(domainObject))[0].migrate(domainObject); + } + + let wrappedFunction = openmct.objects.get; + openmct.objects.get = function migrate() { + return wrappedFunction.apply(openmct.objects, [...arguments]).then(function (object) { + if (needsMigration(object)) { + migrateObject(object).then((newObject) => { + openmct.objects.mutate(newObject, 'persisted', Date.now()); + + return newObject; + }); } - function migrateObject(domainObject) { - return migrations.filter(m => m.check(domainObject))[0] - .migrate(domainObject); - } - - let wrappedFunction = openmct.objects.get; - openmct.objects.get = function migrate() { - return wrappedFunction.apply(openmct.objects, [...arguments]) - .then(function (object) { - if (needsMigration(object)) { - migrateObject(object) - .then(newObject => { - openmct.objects.mutate(newObject, 'persisted', Date.now()); - - return newObject; - }); - } - - return object; - }); - }; + return object; + }); }; + }; } diff --git a/src/plugins/openInNewTabAction/openInNewTabAction.js b/src/plugins/openInNewTabAction/openInNewTabAction.js index 830472a759..be8b59996d 100644 --- a/src/plugins/openInNewTabAction/openInNewTabAction.js +++ b/src/plugins/openInNewTabAction/openInNewTabAction.js @@ -21,18 +21,18 @@ *****************************************************************************/ import objectPathToUrl from '/src/tools/url'; export default class OpenInNewTab { - constructor(openmct) { - this.name = 'Open In New Tab'; - this.key = 'newTab'; - this.description = 'Open in a new browser tab'; - this.group = "windowing"; - this.priority = 10; - this.cssClass = "icon-new-window"; + constructor(openmct) { + this.name = 'Open In New Tab'; + this.key = 'newTab'; + this.description = 'Open in a new browser tab'; + this.group = 'windowing'; + this.priority = 10; + this.cssClass = 'icon-new-window'; - this._openmct = openmct; - } - invoke(objectPath, urlParams = undefined) { - let url = objectPathToUrl(this._openmct, objectPath, urlParams); - window.open(url); - } + this._openmct = openmct; + } + invoke(objectPath, urlParams = undefined) { + let url = objectPathToUrl(this._openmct, objectPath, urlParams); + window.open(url); + } } diff --git a/src/plugins/openInNewTabAction/plugin.js b/src/plugins/openInNewTabAction/plugin.js index 843b3b2317..90eda55112 100644 --- a/src/plugins/openInNewTabAction/plugin.js +++ b/src/plugins/openInNewTabAction/plugin.js @@ -22,7 +22,7 @@ import OpenInNewTabAction from './openInNewTabAction'; export default function () { - return function (openmct) { - openmct.actions.register(new OpenInNewTabAction(openmct)); - }; + return function (openmct) { + openmct.actions.register(new OpenInNewTabAction(openmct)); + }; } diff --git a/src/plugins/openInNewTabAction/pluginSpec.js b/src/plugins/openInNewTabAction/pluginSpec.js index 57f60afba5..147a868f4d 100644 --- a/src/plugins/openInNewTabAction/pluginSpec.js +++ b/src/plugins/openInNewTabAction/pluginSpec.js @@ -19,57 +19,56 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import { - createOpenMct, - resetApplicationState, - spyOnBuiltins -} from 'utils/testing'; +import { createOpenMct, resetApplicationState, spyOnBuiltins } from 'utils/testing'; -describe("the plugin", () => { - let openmct; - let openInNewTabAction; - let mockObjectPath; +describe('the plugin', () => { + let openmct; + let openInNewTabAction; + let mockObjectPath; - beforeEach((done) => { - openmct = createOpenMct(); + beforeEach((done) => { + openmct = createOpenMct(); - openmct.on('start', done); - openmct.startHeadless(); + openmct.on('start', done); + openmct.startHeadless(); - openInNewTabAction = openmct.actions._allActions.newTab; + openInNewTabAction = openmct.actions._allActions.newTab; + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('installs the open in new tab action', () => { + expect(openInNewTabAction).toBeDefined(); + }); + + describe('when invoked', () => { + beforeEach(async () => { + mockObjectPath = [ + { + name: 'mock folder', + type: 'folder', + identifier: { + key: 'mock-folder', + namespace: '' + } + } + ]; + spyOn(openmct.objects, 'get').and.returnValue( + Promise.resolve({ + identifier: { + namespace: '', + key: 'test' + } + }) + ); + spyOnBuiltins(['open']); + await openInNewTabAction.invoke(mockObjectPath); }); - afterEach(() => { - return resetApplicationState(openmct); - }); - - it('installs the open in new tab action', () => { - expect(openInNewTabAction).toBeDefined(); - }); - - describe('when invoked', () => { - - beforeEach(async () => { - mockObjectPath = [{ - name: 'mock folder', - type: 'folder', - identifier: { - key: 'mock-folder', - namespace: '' - } - }]; - spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({ - identifier: { - namespace: '', - key: 'test' - } - })); - spyOnBuiltins(['open']); - await openInNewTabAction.invoke(mockObjectPath); - }); - - it('it opens in a new tab', () => { - expect(window.open).toHaveBeenCalled(); - }); + it('it opens in a new tab', () => { + expect(window.open).toHaveBeenCalled(); }); + }); }); diff --git a/src/plugins/operatorStatus/AbstractStatusIndicator.js b/src/plugins/operatorStatus/AbstractStatusIndicator.js index 0181a2ea4b..7de4565af3 100644 --- a/src/plugins/operatorStatus/AbstractStatusIndicator.js +++ b/src/plugins/operatorStatus/AbstractStatusIndicator.js @@ -22,85 +22,85 @@ import raf from '@/utils/raf'; export default class AbstractStatusIndicator { - #popupComponent; - #indicator; - #configuration; + #popupComponent; + #indicator; + #configuration; - /** - * @param {*} openmct the Open MCT API (proper typescript doc 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; + /** + * @param {*} openmct the Open MCT API (proper typescript doc 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.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(); + 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); } + } - install() { - this.openmct.indicators.add(this.#indicator); - } + createPopupComponent() { + throw new Error('Must override createPopupElement method'); + } - showPopup() { - const popupElement = this.getPopupElement(); + getPopupElement() { + return this.#popupComponent; + } - 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 - }); + createIndicator() { + throw new Error('Must override createIndicator method'); + } - this.positionBox(); + getIndicator() { + return this.#indicator; + } - 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; - } + getConfiguration() { + return this.#configuration; + } } diff --git a/src/plugins/operatorStatus/operator-status.scss b/src/plugins/operatorStatus/operator-status.scss index 11b22d8452..11591d1c18 100644 --- a/src/plugins/operatorStatus/operator-status.scss +++ b/src/plugins/operatorStatus/operator-status.scss @@ -20,137 +20,143 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ - $statusCountWidth: 30px; +$statusCountWidth: 30px; .c-status-poll-panel { - @include menuOuter(); + @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; - flex-direction: column; - padding: $interiorMarginLg; - min-width: 350px; - max-width: 35%; + align-items: center; + flex-direction: row; > * + * { - margin-top: $interiorMarginLg; + 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; } - *:before { - font-size: 0.8em; - margin-right: $interiorMarginSm; + [class*='__label'] { + padding: 3px 0; } - &__section { - display: flex; - align-items: center; - flex-direction: row; - - > * + * { - margin-left: $interiorMarginLg; - } + [class*='__poll-table'] { + grid-column: span 2; } - &__top { - text-transform: uppercase; - } + [class*='new-question'] { + align-items: center; + display: flex; + flex-direction: row; + > * + * { + margin-left: $interiorMargin; + } - &__user-role, - &__updated { - opacity: 50%; - } - - &__updated { + input { 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*='__label'] { - padding: 3px 0; - } - - [class*='__poll-table'] { - grid-column: span 2; - } - - [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; } - } + 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; - > * + * { margin-left: $interiorMargin; } + font-size: 1.25em; + align-items: center; + padding: $interiorMarginSm $interiorMarginLg; - &__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; - } - } - &__actions { - display:flex; - flex: auto; - flex-direction: row; - justify-content: flex-end; + &-type { + line-height: 1em; + opacity: 0.6; } + } + &__actions { + display: flex; + flex: auto; + flex-direction: row; + justify-content: flex-end; + } } .c-indicator { - &:before { - // Indicator icon - color: $colorKey; - } + &:before { + // Indicator icon + color: $colorKey; + } - &--operator-status { - cursor: pointer; - max-width: 150px; + &--operator-status { + cursor: pointer; + max-width: 150px; - @include hover() { - background: $colorIndicatorBgHov; - } + @include hover() { + background: $colorIndicatorBgHov; } + } } diff --git a/src/plugins/operatorStatus/operatorStatus/OperatorStatus.vue b/src/plugins/operatorStatus/operatorStatus/OperatorStatus.vue index 394ebbcd76..26650005a1 100644 --- a/src/plugins/operatorStatus/operatorStatus/OperatorStatus.vue +++ b/src/plugins/operatorStatus/operatorStatus/OperatorStatus.vue @@ -20,169 +20,159 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/operatorStatus/operatorStatus/OperatorStatusIndicator.js b/src/plugins/operatorStatus/operatorStatus/OperatorStatusIndicator.js index fa21d23d72..04d81d6bc0 100644 --- a/src/plugins/operatorStatus/operatorStatus/OperatorStatusIndicator.js +++ b/src/plugins/operatorStatus/operatorStatus/OperatorStatusIndicator.js @@ -25,39 +25,39 @@ 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: '' - }).$mount(); + 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: '' + }).$mount(); - return popupElement; - } + return popupElement; + } - createIndicator() { - const operatorIndicator = this.openmct.indicators.simpleIndicator(); + 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); + 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; - } + return operatorIndicator; + } } diff --git a/src/plugins/operatorStatus/plugin.js b/src/plugins/operatorStatus/plugin.js index 7d3afb8270..f54b56a291 100644 --- a/src/plugins/operatorStatus/plugin.js +++ b/src/plugins/operatorStatus/plugin.js @@ -27,24 +27,23 @@ import PollQuestionIndicator from './pollQuestion/PollQuestionIndicator'; * @returns {function} The plugin install function */ export default function operatorStatusPlugin(configuration) { - return function install(openmct) { + return function install(openmct) { + if (openmct.user.hasProvider()) { + openmct.user.status.canProvideStatusForCurrentUser().then((canProvideStatus) => { + if (canProvideStatus) { + const operatorStatusIndicator = new OperatorStatusIndicator(openmct, configuration); - 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(); - } - }); + 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 index 7ed1945899..8677a59e2b 100644 --- a/src/plugins/operatorStatus/pollQuestion/PollQuestion.vue +++ b/src/plugins/operatorStatus/pollQuestion/PollQuestion.vue @@ -20,274 +20,263 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/operatorStatus/pollQuestion/PollQuestionIndicator.js b/src/plugins/operatorStatus/pollQuestion/PollQuestionIndicator.js index 83eba4b243..49b1364a45 100644 --- a/src/plugins/operatorStatus/pollQuestion/PollQuestionIndicator.js +++ b/src/plugins/operatorStatus/pollQuestion/PollQuestionIndicator.js @@ -25,39 +25,39 @@ 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: '' - }).$mount(); + 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: '' + }).$mount(); - return pollQuestionElement; - } + return pollQuestionElement; + } - createIndicator() { - const pollQuestionIndicator = this.openmct.indicators.simpleIndicator(); + createIndicator() { + const pollQuestionIndicator = this.openmct.indicators.simpleIndicator(); - pollQuestionIndicator.text("No 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); + pollQuestionIndicator.text('No 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; - } + return pollQuestionIndicator; + } } diff --git a/src/plugins/performanceIndicator/plugin.js b/src/plugins/performanceIndicator/plugin.js index 822a179c85..9275fc8947 100644 --- a/src/plugins/performanceIndicator/plugin.js +++ b/src/plugins/performanceIndicator/plugin.js @@ -20,43 +20,43 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ export default function PerformanceIndicator() { - return function install(openmct) { - let frames = 0; - let lastCalculated = performance.now(); - const indicator = openmct.indicators.simpleIndicator(); + return function install(openmct) { + let frames = 0; + let lastCalculated = performance.now(); + const indicator = openmct.indicators.simpleIndicator(); - indicator.text('~ fps'); - indicator.statusClass('s-status-info'); - openmct.indicators.add(indicator); + indicator.text('~ fps'); + indicator.statusClass('s-status-info'); + openmct.indicators.add(indicator); - let rafHandle = requestAnimationFrame(incremementFrames); + let rafHandle = requestAnimationFrame(incremementFrames); - openmct.on('destroy', () => { - cancelAnimationFrame(rafHandle); - }); + openmct.on('destroy', () => { + cancelAnimationFrame(rafHandle); + }); - function incremementFrames() { - let now = performance.now(); - if ((now - lastCalculated) < 1000) { - frames++; - } else { - updateFPS(frames); - lastCalculated = now; - frames = 1; - } + function incremementFrames() { + let now = performance.now(); + if (now - lastCalculated < 1000) { + frames++; + } else { + updateFPS(frames); + lastCalculated = now; + frames = 1; + } - rafHandle = requestAnimationFrame(incremementFrames); - } + rafHandle = requestAnimationFrame(incremementFrames); + } - function updateFPS(fps) { - indicator.text(`${fps} fps`); - if (fps >= 40) { - indicator.statusClass('s-status-on'); - } else if (fps < 40 && fps >= 20) { - indicator.statusClass('s-status-warning'); - } else { - indicator.statusClass('s-status-error'); - } - } - }; + function updateFPS(fps) { + indicator.text(`${fps} fps`); + if (fps >= 40) { + indicator.statusClass('s-status-on'); + } else if (fps < 40 && fps >= 20) { + indicator.statusClass('s-status-warning'); + } else { + indicator.statusClass('s-status-error'); + } + } + }; } diff --git a/src/plugins/performanceIndicator/pluginSpec.js b/src/plugins/performanceIndicator/pluginSpec.js index d546232f4a..6b32e2bff2 100644 --- a/src/plugins/performanceIndicator/pluginSpec.js +++ b/src/plugins/performanceIndicator/pluginSpec.js @@ -23,56 +23,56 @@ import PerformancePlugin from './plugin.js'; import { createOpenMct, resetApplicationState } from 'utils/testing'; describe('the plugin', () => { - let openmct; - let element; - let child; + let openmct; + let element; + let child; - let performanceIndicator; + let performanceIndicator; - beforeEach(done => { - openmct = createOpenMct(); + beforeEach((done) => { + openmct = createOpenMct(); - element = document.createElement('div'); - child = document.createElement('div'); - element.appendChild(child); + element = document.createElement('div'); + child = document.createElement('div'); + element.appendChild(child); - openmct.install(new PerformancePlugin()); + openmct.install(new PerformancePlugin()); - openmct.on('start', done); + openmct.on('start', done); - performanceIndicator = openmct.indicators.indicatorObjects.find(indicator => { - return indicator.text && indicator.text() === '~ fps'; - }); - - openmct.startHeadless(); + performanceIndicator = openmct.indicators.indicatorObjects.find((indicator) => { + return indicator.text && indicator.text() === '~ fps'; }); - afterEach(() => { - return resetApplicationState(openmct); + openmct.startHeadless(); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('installs the performance indicator', () => { + expect(performanceIndicator).toBeDefined(); + }); + + it('calculates an fps value', async () => { + await loopForABit(); + // eslint-disable-next-line radix + const fps = parseInt(performanceIndicator.text().split(' fps')[0]); + expect(fps).toBeGreaterThan(0); + }); + + function loopForABit() { + let frames = 0; + + return new Promise((resolve) => { + requestAnimationFrame(function loop() { + if (++frames > 90) { + resolve(); + } else { + requestAnimationFrame(loop); + } + }); }); - - it('installs the performance indicator', () => { - expect(performanceIndicator).toBeDefined(); - }); - - it('calculates an fps value', async () => { - await loopForABit(); - // eslint-disable-next-line radix - const fps = parseInt(performanceIndicator.text().split(' fps')[0]); - expect(fps).toBeGreaterThan(0); - }); - - function loopForABit() { - let frames = 0; - - return new Promise(resolve => { - requestAnimationFrame(function loop() { - if (++frames > 90) { - resolve(); - } else { - requestAnimationFrame(loop); - } - }); - }); - } + } }); diff --git a/src/plugins/persistence/couch/CouchChangesFeed.js b/src/plugins/persistence/couch/CouchChangesFeed.js index 86059841ca..d47429ba28 100644 --- a/src/plugins/persistence/couch/CouchChangesFeed.js +++ b/src/plugins/persistence/couch/CouchChangesFeed.js @@ -1,121 +1,124 @@ (function () { - const connections = []; - let connected = false; - let couchEventSource; - let changesFeedUrl; - const keepAliveTime = 20 * 1000; - let keepAliveTimer; - const controller = new AbortController(); + const connections = []; + let connected = false; + let couchEventSource; + let changesFeedUrl; + const keepAliveTime = 20 * 1000; + let keepAliveTimer; + const controller = new AbortController(); - self.onconnect = function (e) { - let port = e.ports[0]; - connections.push(port); + self.onconnect = function (e) { + let port = e.ports[0]; + connections.push(port); - port.postMessage({ - type: 'connection', - connectionId: connections.length - }); + port.postMessage({ + type: 'connection', + connectionId: connections.length + }); - port.onmessage = function (event) { - if (event.data.request === 'close') { - console.debug('🚪 Closing couch connection 🚪'); - connections.splice(event.data.connectionId - 1, 1); - if (connections.length <= 0) { - // abort any outstanding requests if there's nobody listening to it. - controller.abort(); - } - - connected = false; - // stop listening for events - couchEventSource.removeEventListener('message', self.onCouchMessage); - couchEventSource.close(); - console.debug('🚪 Closed couch connection 🚪'); - - return; - } - - if (event.data.request === 'changes') { - if (connected === true) { - return; - } - - changesFeedUrl = event.data.url; - self.listenForChanges(); - } - }; - - port.start(); - }; - - 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({ - objectChanges - }); - }); - }; - - self.listenForChanges = function () { - if (keepAliveTimer) { - clearTimeout(keepAliveTimer); + port.onmessage = function (event) { + if (event.data.request === 'close') { + console.debug('🚪 Closing couch connection 🚪'); + connections.splice(event.data.connectionId - 1, 1); + if (connections.length <= 0) { + // abort any outstanding requests if there's nobody listening to it. + controller.abort(); } - /** - * Once the connection has been opened, poll every 20 seconds to see if the EventSource has closed unexpectedly. - * If it has, attempt to reconnect. - */ - keepAliveTimer = setTimeout(self.listenForChanges, keepAliveTime); + connected = false; + // stop listening for events + couchEventSource.removeEventListener('message', self.onCouchMessage); + couchEventSource.close(); + console.debug('🚪 Closed couch connection 🚪'); - if (!couchEventSource || couchEventSource.readyState === EventSource.CLOSED) { - console.debug('⇿ Opening CouchDB change feed connection ⇿'); - couchEventSource = new EventSource(changesFeedUrl); - couchEventSource.onerror = self.onerror; - couchEventSource.onopen = self.onopen; + return; + } - // 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; + if (event.data.request === 'changes') { + if (connected === true) { + return; } - connections.forEach(function (connection) { - connection.postMessage(message); - }); + changesFeedUrl = event.data.url; + self.listenForChanges(); + } }; -}()); + + port.start(); + }; + + 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({ + objectChanges + }); + }); + }; + + self.listenForChanges = function () { + if (keepAliveTimer) { + clearTimeout(keepAliveTimer); + } + + /** + * Once the connection has been opened, poll every 20 seconds to see if the EventSource has closed unexpectedly. + * If it has, attempt to reconnect. + */ + keepAliveTimer = setTimeout(self.listenForChanges, keepAliveTime); + + if (!couchEventSource || couchEventSource.readyState === EventSource.CLOSED) { + console.debug('⇿ Opening CouchDB change feed connection ⇿'); + couchEventSource = new EventSource(changesFeedUrl); + 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 c6a528e739..782111dfd9 100644 --- a/src/plugins/persistence/couch/CouchDocument.js +++ b/src/plugins/persistence/couch/CouchDocument.js @@ -37,16 +37,16 @@ * deleted (see CouchDB docs for _deleted) */ export default function CouchDocument(id, model, rev, markDeleted) { - return { - "_id": id, - "_rev": rev, - "_deleted": markDeleted, - "metadata": { - "category": "domain object", - "type": model.type, - "owner": "admin", - "name": model.name - }, - "model": model - }; + return { + _id: id, + _rev: rev, + _deleted: markDeleted, + metadata: { + category: 'domain object', + type: model.type, + owner: 'admin', + name: model.name + }, + model: model + }; } diff --git a/src/plugins/persistence/couch/CouchObjectProvider.js b/src/plugins/persistence/couch/CouchObjectProvider.js index e6c04b4538..8982d97bd8 100644 --- a/src/plugins/persistence/couch/CouchObjectProvider.js +++ b/src/plugins/persistence/couch/CouchObjectProvider.js @@ -20,708 +20,716 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import CouchDocument from "./CouchDocument"; -import CouchObjectQueue from "./CouchObjectQueue"; -import { PENDING, CONNECTED, DISCONNECTED, UNKNOWN } from "./CouchStatusIndicator"; +import CouchDocument from './CouchDocument'; +import CouchObjectQueue from './CouchObjectQueue'; +import { PENDING, CONNECTED, DISCONNECTED, UNKNOWN } from './CouchStatusIndicator'; import { isNotebookOrAnnotationType } from '../../notebook/notebook-constants.js'; -const REV = "_rev"; -const ID = "_id"; +const REV = '_rev'; +const ID = '_id'; const HEARTBEAT = 50000; -const ALL_DOCS = "_all_docs?include_docs=true"; +const ALL_DOCS = '_all_docs?include_docs=true'; class CouchObjectProvider { - constructor(openmct, options, namespace, indicator) { - options = this.#normalize(options); - this.openmct = openmct; - this.indicator = indicator; - this.url = options.url; - this.namespace = namespace; - this.objectQueue = {}; - this.observers = {}; - this.batchIds = []; - this.onEventMessage = this.onEventMessage.bind(this); - this.onEventError = this.onEventError.bind(this); - } + constructor(openmct, options, namespace, indicator) { + options = this.#normalize(options); + this.openmct = openmct; + this.indicator = indicator; + this.url = options.url; + this.namespace = namespace; + this.objectQueue = {}; + this.observers = {}; + this.batchIds = []; + this.onEventMessage = this.onEventMessage.bind(this); + this.onEventError = this.onEventError.bind(this); + } - /** - * @private - */ - #startSharedWorker() { - let provider = this; - let sharedWorker; + /** + * @private + */ + #startSharedWorker() { + let provider = this; + let sharedWorker; - // eslint-disable-next-line no-undef - const sharedWorkerURL = `${this.openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}couchDBChangesFeed.js`; + // eslint-disable-next-line no-undef + const sharedWorkerURL = `${this.openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}couchDBChangesFeed.js`; - sharedWorker = new SharedWorker(sharedWorkerURL, 'CouchDB SSE Shared Worker'); - sharedWorker.port.onmessage = provider.onSharedWorkerMessage.bind(this); - sharedWorker.port.onmessageerror = provider.onSharedWorkerMessageError.bind(this); - sharedWorker.port.start(); + sharedWorker = new SharedWorker(sharedWorkerURL, 'CouchDB SSE Shared Worker'); + sharedWorker.port.onmessage = provider.onSharedWorkerMessage.bind(this); + sharedWorker.port.onmessageerror = provider.onSharedWorkerMessageError.bind(this); + sharedWorker.port.start(); - this.openmct.on('destroy', () => { - this.changesFeedSharedWorker.port.postMessage({ - request: 'close', - connectionId: this.changesFeedSharedWorkerConnectionId - }); - this.changesFeedSharedWorker.port.close(); + this.openmct.on('destroy', () => { + this.changesFeedSharedWorker.port.postMessage({ + request: 'close', + connectionId: this.changesFeedSharedWorkerConnectionId + }); + this.changesFeedSharedWorker.port.close(); + }); + + return sharedWorker; + } + + onSharedWorkerMessageError(event) { + console.error('Error', event); + } + + isSynchronizedObject(object) { + return ( + object && + object.type && + this.openmct.objects.SYNCHRONIZED_OBJECT_TYPES && + this.openmct.objects.SYNCHRONIZED_OBJECT_TYPES.includes(object.type) + ); + } + + 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 = { + namespace: this.namespace, + key: objectChanges.id + }; + let keyString = this.openmct.objects.makeKeyString(objectIdentifier); + //TODO: Optimize this so that we don't 'get' the object if it's current revision (from this.objectQueue) is the same as the one we already have. + let observersForObject = this.observers[keyString]; + let isInTransaction = false; + + if (this.openmct.objects.isTransactionActive()) { + isInTransaction = this.openmct.objects.transaction.getDirtyObject(objectIdentifier); + } + + if (observersForObject && !isInTransaction) { + observersForObject.forEach(async (observer) => { + const updatedObject = await this.get(objectIdentifier); + if (this.isSynchronizedObject(updatedObject)) { + observer(updatedObject); + } }); + } + } + } - return sharedWorker; + /** + * 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; } - onSharedWorkerMessageError(event) { - console.error('Error', event); + 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; } - isSynchronizedObject(object) { - return (object && object.type - && this.openmct.objects.SYNCHRONIZED_OBJECT_TYPES - && this.openmct.objects.SYNCHRONIZED_OBJECT_TYPES.includes(object.type)); + return state; + } + //backwards compatibility, options used to be a url. Now it's an object + #normalize(options) { + if (typeof options === 'string') { + return { + url: options + }; } - 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); + return options; + } + + async request(subPath, method, body, signal) { + let fetchOptions = { + method, + body, + priority: 'high', + signal + }; + + // stringify body if needed + if (fetchOptions.body) { + fetchOptions.body = JSON.stringify(fetchOptions.body); + fetchOptions.headers = { + 'Content-Type': 'application/json' + }; + } + + let response = null; + + 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 { + if (body?.model && isNotebookOrAnnotationType(body.model)) { + // warn since we handle conflicts for notebooks + console.warn(error.message); } else { - let objectChanges = event.data.objectChanges; - const objectIdentifier = { - namespace: this.namespace, - key: objectChanges.id - }; - let keyString = this.openmct.objects.makeKeyString(objectIdentifier); - //TODO: Optimize this so that we don't 'get' the object if it's current revision (from this.objectQueue) is the same as the one we already have. - let observersForObject = this.observers[keyString]; - let isInTransaction = false; - - if (this.openmct.objects.isTransactionActive()) { - isInTransaction = this.openmct.objects.transaction.getDirtyObject(objectIdentifier); - } - - if (observersForObject && !isInTransaction) { - observersForObject.forEach(async (observer) => { - const updatedObject = await this.get(objectIdentifier); - if (this.isSynchronizedObject(updatedObject)) { - observer(updatedObject); - } - }); - } + 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) { + const objectName = JSON.parse(fetchOptions.body)?.model?.name; + throw new this.openmct.objects.errors.Conflict(`Conflict persisting "${objectName}"`); + } 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}"`); + } + } + + /** + * Check the response to a create/update/delete request; + * track the rev if it's valid, otherwise return false to + * indicate that the request failed. + * persist any queued objects + * @private + */ + #checkResponse(response, intermediateResponse, key) { + let requestSuccess = false; + const id = response ? response.id : undefined; + let rev; + + if (response && response.ok) { + rev = response.rev; + requestSuccess = true; } - /** - * 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; - } + intermediateResponse.resolve(requestSuccess); - return state; + if (id) { + if (!this.objectQueue[id]) { + this.objectQueue[id] = new CouchObjectQueue(undefined, rev); + } + + this.objectQueue[id].updateRevision(rev); + this.objectQueue[id].pending = false; + if (this.objectQueue[id].hasNext()) { + this.#updateQueued(id); + } + } else { + this.objectQueue[key].pending = false; + } + } + + /** + * @private + */ + #getModel(response) { + if (response && response.model) { + let key = response[ID]; + let object = this.fromPersistedModel(response.model, key); + + if (!this.objectQueue[key]) { + this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]); + } + + if (isNotebookOrAnnotationType(object)) { + //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]); + } else if (!this.objectQueue[key].pending) { + //Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress + this.objectQueue[key].updateRevision(response[REV]); + } + + return object; + } else { + return undefined; + } + } + + get(identifier, abortSignal) { + this.batchIds.push(identifier.key); + + if (this.bulkPromise === undefined) { + this.bulkPromise = this.#deferBatchedGet(abortSignal); } - /** - * 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 this.bulkPromise.then((domainObjectMap) => { + return domainObjectMap[identifier.key]; + }); + } - return state; + /** + * @private + */ + #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(() => { + let batchIds = this.batchIds; + + 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) + ); + } else { + return this.#bulkGet(batchIds, abortSignal); + } + }); + } + + /** + * @private + */ + #returnAsMap(objectKey) { + return (result) => { + let objectMap = {}; + objectMap[objectKey] = this.#getModel(result); + + return objectMap; + }; + } + + /** + * @private + */ + #clearBatch() { + this.batchIds = []; + delete this.bulkPromise; + } + + /** + * @private + */ + #waitOneEventCycle() { + return new Promise((resolve) => { + setTimeout(resolve); + }); + } + + /** + * @private + */ + #bulkGet(ids, signal) { + ids = this.removeDuplicates(ids); + + const query = { + keys: ids + }; + + 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); + } + + return map; + }, {}); + } else { + return {}; + } + }); + } + + /** + * @private + */ + removeDuplicates(array) { + return Array.from(new Set(array)); + } + + search() { + // Dummy search function. It has to appear to support search, + // otherwise the in-memory indexer will index all of its objects, + // but actually search results will be provided by a separate search provider + // see CoucheSearchProvider.js + return Promise.resolve([]); + } + + async getObjectsByFilter(filter, abortSignal) { + let objects = []; + + let url = `${this.url}/_find`; + let body = {}; + + if (filter) { + body = JSON.stringify(filter); } - //backwards compatibility, options used to be a url. Now it's an object - #normalize(options) { - if (typeof options === 'string') { - return { - url: options - }; - } + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + signal: abortSignal, + body + }); - return options; + const reader = response.body.getReader(); + let completed = false; + let decoder = new TextDecoder('utf-8'); + let decodedChunk = ''; + while (!completed) { + const { done, value } = await reader.read(); + //done is true when we lose connection with the provider + if (done) { + completed = true; + } + + if (value) { + let chunk = new Uint8Array(value.length); + chunk.set(value, 0); + const partial = decoder.decode(chunk, { stream: !completed }); + decodedChunk = decodedChunk + partial; + } } - async request(subPath, method, body, signal) { - let fetchOptions = { - method, - body, - priority: 'high', - signal - }; - - // stringify body if needed - if (fetchOptions.body) { - fetchOptions.body = JSON.stringify(fetchOptions.body); - fetchOptions.headers = { - "Content-Type": "application/json" - }; - } - - let response = null; - - 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 { - if (body?.model && isNotebookOrAnnotationType(body.model)) { - // warn since we handle conflicts for notebooks - console.warn(error.message); - } else { - console.error(error.message); - } - - throw error; - } - } + try { + const json = JSON.parse(decodedChunk); + if (json) { + let docs = json.docs; + docs.forEach((doc) => { + let object = this.#getModel(doc); + if (object) { + objects.push(object); + } + }); + } + } catch (e) { + //do nothing } - /** - * 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) { - const objectName = JSON.parse(fetchOptions.body)?.model?.name; - throw new this.openmct.objects.errors.Conflict(`Conflict persisting "${objectName}"`); - } else if (status >= CouchObjectProvider.HTTP_BAD_REQUEST) { - if (!json.error || !json.reason) { - throw new Error(`CouchDB Error ${status}`); - } + return objects; + } - throw new Error(`CouchDB Error ${status}: "${json.error} - ${json.reason}"`); - } + observe(identifier, callback) { + const keyString = this.openmct.objects.makeKeyString(identifier); + this.observers[keyString] = this.observers[keyString] || []; + this.observers[keyString].push(callback); + + if (!this.isObservingObjectChanges()) { + this.#observeObjectChanges(); } - /** - * Check the response to a create/update/delete request; - * track the rev if it's valid, otherwise return false to - * indicate that the request failed. - * persist any queued objects - * @private - */ - #checkResponse(response, intermediateResponse, key) { - let requestSuccess = false; - const id = response ? response.id : undefined; - let rev; - - if (response && response.ok) { - rev = response.rev; - requestSuccess = true; + return () => { + if (this.observers[keyString]) { + this.observers[keyString] = this.observers[keyString].filter( + (observer) => observer !== callback + ); + if (this.observers[keyString].length === 0) { + delete this.observers[keyString]; } + } + }; + } - intermediateResponse.resolve(requestSuccess); + isObservingObjectChanges() { + return this.stopObservingObjectChanges !== undefined; + } - if (id) { - if (!this.objectQueue[id]) { - this.objectQueue[id] = new CouchObjectQueue(undefined, rev); - } + /** + * @private + */ + #observeObjectChanges() { + const sseChangesPath = `${this.url}/_changes`; + const sseURL = new URL(sseChangesPath); + sseURL.searchParams.append('feed', 'eventsource'); + sseURL.searchParams.append('style', 'main_only'); + sseURL.searchParams.append('heartbeat', HEARTBEAT); - this.objectQueue[id].updateRevision(rev); - this.objectQueue[id].pending = false; - if (this.objectQueue[id].hasNext()) { - this.#updateQueued(id); - } - } else { - this.objectQueue[key].pending = false; + if (typeof SharedWorker === 'undefined') { + this.fetchChanges(sseURL.toString()); + } else { + this.#initiateSharedWorkerFetchChanges(sseURL.toString()); + } + } + + /** + * @private + */ + #initiateSharedWorkerFetchChanges(url) { + if (!this.changesFeedSharedWorker) { + this.changesFeedSharedWorker = this.#startSharedWorker(); + + if (this.isObservingObjectChanges()) { + this.stopObservingObjectChanges(); + } + + this.stopObservingObjectChanges = () => { + delete this.stopObservingObjectChanges; + }; + + this.changesFeedSharedWorker.port.postMessage({ + request: 'changes', + url + }); + } + } + + onEventError(error) { + console.error('Error on feed', error); + 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) { + observersForObject.forEach(async (observer) => { + const updatedObject = await this.get(identifier); + if (this.isSynchronizedObject(updatedObject)) { + observer(updatedObject); } + }); + } + } + + fetchChanges(url) { + const controller = new AbortController(); + let couchEventSource; + + if (this.isObservingObjectChanges()) { + this.stopObservingObjectChanges(); } - /** - * @private - */ - #getModel(response) { - if (response && response.model) { - let key = response[ID]; - let object = this.fromPersistedModel(response.model, key); + this.stopObservingObjectChanges = () => { + controller.abort(); + couchEventSource.removeEventListener('message', this.onEventMessage.bind(this)); + delete this.stopObservingObjectChanges; + }; - if (!this.objectQueue[key]) { - this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]); - } + console.debug('⇿ Opening CouchDB change feed connection ⇿'); - if (isNotebookOrAnnotationType(object)) { - //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]); - } else if (!this.objectQueue[key].pending) { - //Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress - this.objectQueue[key].updateRevision(response[REV]); - } + couchEventSource = new EventSource(url); + couchEventSource.onerror = this.onEventError.bind(this); + couchEventSource.onopen = this.onEventOpen.bind(this); - return object; - } else { - return undefined; - } + // start listening for events + couchEventSource.addEventListener('message', this.onEventMessage.bind(this)); + + console.debug('⇿ Opened connection ⇿'); + } + + /** + * @private + */ + #getIntermediateResponse() { + let intermediateResponse = {}; + intermediateResponse.promise = new Promise(function (resolve, reject) { + intermediateResponse.resolve = resolve; + intermediateResponse.reject = reject; + }); + + return intermediateResponse; + } + + /** + * 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; } - get(identifier, abortSignal) { - this.batchIds.push(identifier.key); + const indicatorState = this.#messageToIndicatorState(message); + this.indicator.setIndicatorToState(indicatorState); + } - if (this.bulkPromise === undefined) { - this.bulkPromise = this.#deferBatchedGet(abortSignal); - } - - return this.bulkPromise - .then((domainObjectMap) => { - return domainObjectMap[identifier.key]; - }); + /** + * @private + */ + enqueueObject(key, model, intermediateResponse) { + if (this.objectQueue[key]) { + this.objectQueue[key].enqueue({ + model, + intermediateResponse + }); + } else { + this.objectQueue[key] = new CouchObjectQueue({ + model, + intermediateResponse + }); } + } - /** - * @private - */ - #deferBatchedGet(abortSignal) { - // We until the next event loop cycle to "collect" all of the get - // requests triggered in this iteration of the event loop + create(model) { + let intermediateResponse = this.#getIntermediateResponse(); + const key = model.identifier.key; + model = this.toPersistableModel(model); + this.enqueueObject(key, model, intermediateResponse); - return this.#waitOneEventCycle().then(() => { - let batchIds = this.batchIds; - - 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)); - } else { - return this.#bulkGet(batchIds, abortSignal); - } + if (!this.objectQueue[key].pending) { + 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) => { + this.#checkResponse(response, queued.intermediateResponse, key); + }) + .catch((error) => { + queued.intermediateResponse.reject(error); + this.objectQueue[key].pending = false; }); } - /** - * @private - */ - #returnAsMap(objectKey) { - return (result) => { - let objectMap = {}; - objectMap[objectKey] = this.#getModel(result); + return intermediateResponse.promise; + } - return objectMap; - }; - } - - /** - * @private - */ - #clearBatch() { - this.batchIds = []; - delete this.bulkPromise; - } - - /** - * @private - */ - #waitOneEventCycle() { - return new Promise((resolve) => { - setTimeout(resolve); + /** + * @private + */ + #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); + }) + .catch((error) => { + queued.intermediateResponse.reject(error); + this.objectQueue[key].pending = false; }); } + } - /** - * @private - */ - #bulkGet(ids, signal) { - ids = this.removeDuplicates(ids); + update(model) { + let intermediateResponse = this.#getIntermediateResponse(); + const key = model.identifier.key; + model = this.toPersistableModel(model); - const query = { - 'keys': ids - }; + this.enqueueObject(key, model, intermediateResponse); + this.#updateQueued(key); - 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); - } + return intermediateResponse.promise; + } - return map; - }, {}); - } else { - return {}; - } - }); - } + toPersistableModel(model) { + //First make a copy so we are not mutating the provided model. + const persistableModel = JSON.parse(JSON.stringify(model)); + //Delete the identifier. Couch manages namespaces dynamically. + delete persistableModel.identifier; - /** - * @private - */ - removeDuplicates(array) { - return Array.from(new Set(array)); - } + return persistableModel; + } - search() { - // Dummy search function. It has to appear to support search, - // otherwise the in-memory indexer will index all of its objects, - // but actually search results will be provided by a separate search provider - // see CoucheSearchProvider.js - return Promise.resolve([]); - } + fromPersistedModel(model, key) { + model.identifier = { + namespace: this.namespace, + key + }; - async getObjectsByFilter(filter, abortSignal) { - let objects = []; - - let url = `${this.url}/_find`; - let body = {}; - - if (filter) { - body = JSON.stringify(filter); - } - - const response = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - signal: abortSignal, - body - }); - - const reader = response.body.getReader(); - let completed = false; - let decoder = new TextDecoder("utf-8"); - let decodedChunk = ''; - while (!completed) { - const {done, value} = await reader.read(); - //done is true when we lose connection with the provider - if (done) { - completed = true; - } - - if (value) { - let chunk = new Uint8Array(value.length); - chunk.set(value, 0); - const partial = decoder.decode(chunk, {stream: !completed}); - decodedChunk = decodedChunk + partial; - } - } - - try { - const json = JSON.parse(decodedChunk); - if (json) { - let docs = json.docs; - docs.forEach(doc => { - let object = this.#getModel(doc); - if (object) { - objects.push(object); - } - }); - } - } catch (e) { - //do nothing - } - - return objects; - } - - observe(identifier, callback) { - const keyString = this.openmct.objects.makeKeyString(identifier); - this.observers[keyString] = this.observers[keyString] || []; - this.observers[keyString].push(callback); - - if (!this.isObservingObjectChanges()) { - this.#observeObjectChanges(); - } - - return () => { - if (this.observers[keyString]) { - this.observers[keyString] = this.observers[keyString].filter(observer => observer !== callback); - if (this.observers[keyString].length === 0) { - delete this.observers[keyString]; - } - } - }; - } - - isObservingObjectChanges() { - return this.stopObservingObjectChanges !== undefined; - } - - /** - * @private - */ - #observeObjectChanges() { - const sseChangesPath = `${this.url}/_changes`; - const sseURL = new URL(sseChangesPath); - sseURL.searchParams.append('feed', 'eventsource'); - sseURL.searchParams.append('style', 'main_only'); - sseURL.searchParams.append('heartbeat', HEARTBEAT); - - if (typeof SharedWorker === 'undefined') { - this.fetchChanges(sseURL.toString()); - } else { - this.#initiateSharedWorkerFetchChanges(sseURL.toString()); - } - } - - /** - * @private - */ - #initiateSharedWorkerFetchChanges(url) { - if (!this.changesFeedSharedWorker) { - this.changesFeedSharedWorker = this.#startSharedWorker(); - - if (this.isObservingObjectChanges()) { - this.stopObservingObjectChanges(); - } - - this.stopObservingObjectChanges = () => { - delete this.stopObservingObjectChanges; - }; - - this.changesFeedSharedWorker.port.postMessage({ - request: 'changes', - url - }); - } - } - - onEventError(error) { - console.error('Error on feed', error); - 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) { - observersForObject.forEach(async (observer) => { - const updatedObject = await this.get(identifier); - if (this.isSynchronizedObject(updatedObject)) { - observer(updatedObject); - } - }); - } - } - - fetchChanges(url) { - const controller = new AbortController(); - let couchEventSource; - - if (this.isObservingObjectChanges()) { - this.stopObservingObjectChanges(); - } - - this.stopObservingObjectChanges = () => { - controller.abort(); - 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.bind(this); - couchEventSource.onopen = this.onEventOpen.bind(this); - - // start listening for events - couchEventSource.addEventListener('message', this.onEventMessage.bind(this)); - - console.debug('⇿ Opened connection ⇿'); - } - - /** - * @private - */ - #getIntermediateResponse() { - let intermediateResponse = {}; - intermediateResponse.promise = new Promise(function (resolve, reject) { - intermediateResponse.resolve = resolve; - intermediateResponse.reject = reject; - }); - - return intermediateResponse; - } - - /** - * 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) { - if (this.objectQueue[key]) { - this.objectQueue[key].enqueue({ - model, - intermediateResponse - }); - } else { - this.objectQueue[key] = new CouchObjectQueue({ - model, - intermediateResponse - }); - } - } - - create(model) { - let intermediateResponse = this.#getIntermediateResponse(); - const key = model.identifier.key; - model = this.toPersistableModel(model); - this.enqueueObject(key, model, intermediateResponse); - - if (!this.objectQueue[key].pending) { - 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) => { - this.#checkResponse(response, queued.intermediateResponse, key); - }).catch(error => { - queued.intermediateResponse.reject(error); - this.objectQueue[key].pending = false; - }); - } - - return intermediateResponse.promise; - } - - /** - * @private - */ - #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); - }).catch((error) => { - queued.intermediateResponse.reject(error); - this.objectQueue[key].pending = false; - }); - } - } - - update(model) { - let intermediateResponse = this.#getIntermediateResponse(); - const key = model.identifier.key; - model = this.toPersistableModel(model); - - this.enqueueObject(key, model, intermediateResponse); - this.#updateQueued(key); - - return intermediateResponse.promise; - } - - toPersistableModel(model) { - //First make a copy so we are not mutating the provided model. - const persistableModel = JSON.parse(JSON.stringify(model)); - //Delete the identifier. Couch manages namespaces dynamically. - delete persistableModel.identifier; - - return persistableModel; - } - - fromPersistedModel(model, key) { - model.identifier = { - namespace: this.namespace, - key - }; - - return model; - } + return model; + } } // https://docs.couchdb.org/en/3.2.0/api/basics.html diff --git a/src/plugins/persistence/couch/CouchObjectQueue.js b/src/plugins/persistence/couch/CouchObjectQueue.js index 63016a3a15..1785fe48eb 100644 --- a/src/plugins/persistence/couch/CouchObjectQueue.js +++ b/src/plugins/persistence/couch/CouchObjectQueue.js @@ -21,31 +21,30 @@ *****************************************************************************/ export default class CouchObjectQueue { - constructor(object, rev) { - this.rev = rev; - this.objects = object ? [object] : []; - this.pending = false; - } + constructor(object, rev) { + this.rev = rev; + this.objects = object ? [object] : []; + this.pending = false; + } - updateRevision(rev) { - this.rev = rev; - } + updateRevision(rev) { + this.rev = rev; + } - hasNext() { - return this.objects.length; - } + hasNext() { + return this.objects.length; + } - enqueue(item) { - this.objects.push(item); - } + enqueue(item) { + this.objects.push(item); + } - dequeue() { - return this.objects.shift(); - } - - clear() { - this.rev = undefined; - this.objects = []; - } + dequeue() { + return this.objects.shift(); + } + clear() { + this.rev = undefined; + this.objects = []; + } } diff --git a/src/plugins/persistence/couch/CouchSearchProvider.js b/src/plugins/persistence/couch/CouchSearchProvider.js index 3a943a8742..aa7efa74d8 100644 --- a/src/plugins/persistence/couch/CouchSearchProvider.js +++ b/src/plugins/persistence/couch/CouchSearchProvider.js @@ -28,99 +28,100 @@ // back into the object provider. class CouchSearchProvider { - constructor(couchObjectProvider) { - this.couchObjectProvider = couchObjectProvider; - this.searchTypes = couchObjectProvider.openmct.objects.SEARCH_TYPES; - this.supportedSearchTypes = [this.searchTypes.OBJECTS, this.searchTypes.ANNOTATIONS, this.searchTypes.TAGS]; - } + constructor(couchObjectProvider) { + this.couchObjectProvider = couchObjectProvider; + this.searchTypes = couchObjectProvider.openmct.objects.SEARCH_TYPES; + this.supportedSearchTypes = [ + this.searchTypes.OBJECTS, + this.searchTypes.ANNOTATIONS, + this.searchTypes.TAGS + ]; + } - supportsSearchType(searchType) { - return this.supportedSearchTypes.includes(searchType); - } + 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.TAGS) { - return this.searchForTags(query, abortSignal); - } else { - throw new Error(`🤷‍♂️ Unknown search type passed: ${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.TAGS) { + return this.searchForTags(query, abortSignal); + } else { + throw new Error(`🤷‍♂️ Unknown search type passed: ${searchType}`); + } + } + + searchForObjects(query, abortSignal) { + const filter = { + selector: { + model: { + name: { + $regex: `(?i)${query}` + } } - } + } + }; - searchForObjects(query, abortSignal) { - const filter = { - "selector": { - "model": { - "name": { - "$regex": `(?i)${query}` - } - } + return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal); + } + + searchForAnnotations(keyString, abortSignal) { + const filter = { + selector: { + $and: [ + { + model: { + targets: {} } - }; - - return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal); - } - - searchForAnnotations(keyString, abortSignal) { - const filter = { - "selector": { - "$and": [ - { - "model": { - "targets": { - } - } - }, - { - "model.type": { - "$eq": "annotation" - } - } - ] + }, + { + 'model.type': { + $eq: 'annotation' } - }; - filter.selector.$and[0].model.targets[keyString] = { - "$exists": true - }; + } + ] + } + }; + filter.selector.$and[0].model.targets[keyString] = { + $exists: true + }; - return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal); + return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal); + } + + searchForTags(tagsArray, abortSignal) { + if (!tagsArray || !tagsArray.length) { + return []; } - searchForTags(tagsArray, abortSignal) { - if (!tagsArray || !tagsArray.length) { - return []; - } - - const filter = { - "selector": { - "$and": [ - { - "model.tags": { - "$elemMatch": { - "$or": [ - ] - } - } - }, - { - "model.type": { - "$eq": "annotation" - } - } - ] + const filter = { + selector: { + $and: [ + { + 'model.tags': { + $elemMatch: { + $or: [] + } } - }; - tagsArray.forEach(tag => { - filter.selector.$and[0]["model.tags"].$elemMatch.$or.push({ - "$eq": `${tag}` - }); - }); - - return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal); - } + }, + { + 'model.type': { + $eq: 'annotation' + } + } + ] + } + }; + tagsArray.forEach((tag) => { + filter.selector.$and[0]['model.tags'].$elemMatch.$or.push({ + $eq: `${tag}` + }); + }); + 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 index b2a664178b..af337c4c61 100644 --- a/src/plugins/persistence/couch/CouchStatusIndicator.js +++ b/src/plugins/persistence/couch/CouchStatusIndicator.js @@ -39,50 +39,50 @@ /** @type {IndicatorState} */ export const CONNECTED = { - statusClass: "s-status-on", - text: "CouchDB is connected", - description: "CouchDB is online and accepting requests." + 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..." + 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." + 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." + 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(); - } + constructor(simpleIndicator) { + this.indicator = simpleIndicator; + this.#setDefaults(); + } - /** - * Set the default values for the indicator. - * @private - */ - #setDefaults() { - this.setIndicatorToState(PENDING); - } + /** + * 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); - } + /** + * 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/couchdb-compose.yaml b/src/plugins/persistence/couch/couchdb-compose.yaml index fca4dbcb8f..5551d33384 100644 --- a/src/plugins/persistence/couch/couchdb-compose.yaml +++ b/src/plugins/persistence/couch/couchdb-compose.yaml @@ -1,11 +1,11 @@ -version: "3" +version: '3' services: couchdb: image: couchdb:${COUCHDB_IMAGE_TAG:-3.3.2} ports: - - "5984:5984" + - '5984:5984' volumes: - - couchdb:/opt/couchdb/data + - couchdb:/opt/couchdb/data environment: COUCHDB_USER: admin COUCHDB_PASSWORD: password diff --git a/src/plugins/persistence/couch/plugin.js b/src/plugins/persistence/couch/plugin.js index 289a24e28e..c2fdb2755d 100644 --- a/src/plugins/persistence/couch/plugin.js +++ b/src/plugins/persistence/couch/plugin.js @@ -29,16 +29,24 @@ const LEGACY_SPACE = 'mct'; const COUCH_SEARCH_ONLY_NAMESPACE = `COUCH_SEARCH_${Date.now()}`; export default function CouchPlugin(options) { - return function install(openmct) { - const simpleIndicator = openmct.indicators.simpleIndicator(); - openmct.indicators.add(simpleIndicator); - const couchStatusIndicator = new CouchStatusIndicator(simpleIndicator); - install.couchProvider = new CouchObjectProvider(openmct, options, NAMESPACE, couchStatusIndicator); + return function install(openmct) { + 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. - openmct.objects.addProvider(LEGACY_SPACE, install.couchProvider); - openmct.objects.addProvider(NAMESPACE, install.couchProvider); - openmct.objects.addProvider(COUCH_SEARCH_ONLY_NAMESPACE, new CouchSearchProvider(install.couchProvider)); - }; + // 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. + openmct.objects.addProvider(LEGACY_SPACE, install.couchProvider); + openmct.objects.addProvider(NAMESPACE, install.couchProvider); + openmct.objects.addProvider( + COUCH_SEARCH_ONLY_NAMESPACE, + new CouchSearchProvider(install.couchProvider) + ); + }; } diff --git a/src/plugins/persistence/couch/pluginSpec.js b/src/plugins/persistence/couch/pluginSpec.js index ff2f1eb2a6..cd522f71fd 100644 --- a/src/plugins/persistence/couch/pluginSpec.js +++ b/src/plugins/persistence/couch/pluginSpec.js @@ -21,423 +21,414 @@ *****************************************************************************/ import Vue from 'vue'; import CouchPlugin from './plugin.js'; -import { - createOpenMct, - resetApplicationState, spyOnBuiltins -} from 'utils/testing'; +import { createOpenMct, resetApplicationState, spyOnBuiltins } from 'utils/testing'; import { CONNECTED, DISCONNECTED, PENDING, UNKNOWN } from './CouchStatusIndicator'; describe('the plugin', () => { - let openmct; - let provider; - let testPath = 'http://localhost:9990/openmct'; - let options; - let mockIdentifierService; - let mockDomainObject; + let openmct; + let provider; + let testPath = 'http://localhost:9990/openmct'; + let options; + let mockIdentifierService; + let mockDomainObject; - beforeEach((done) => { - spyOnBuiltins(['fetch'], window); + beforeEach((done) => { + spyOnBuiltins(['fetch'], window); - mockDomainObject = { - identifier: { - namespace: '', - key: 'some-value' - }, - type: 'notebook', - modified: 0 - }; - options = { - url: testPath, - filter: {} - }; - openmct = createOpenMct(); + mockDomainObject = { + identifier: { + namespace: '', + key: 'some-value' + }, + type: 'notebook', + modified: 0 + }; + options = { + url: testPath, + filter: {} + }; + openmct = createOpenMct(); - openmct.$injector = jasmine.createSpyObj('$injector', ['get']); - mockIdentifierService = jasmine.createSpyObj( - 'identifierService', - ['parse'] - ); - mockIdentifierService.parse.and.returnValue({ - getSpace: () => { - return 'mct'; - } - }); - - openmct.$injector.get.and.returnValue(mockIdentifierService); - - openmct.install(new CouchPlugin(options)); - - openmct.types.addType('notebook', {creatable: true}); - - openmct.on('start', done); - openmct.startHeadless(); - - provider = openmct.objects.getProvider(mockDomainObject.identifier); - spyOn(provider, 'get').and.callThrough(); - spyOn(provider, 'create').and.callThrough(); - spyOn(provider, 'update').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(); + openmct.$injector = jasmine.createSpyObj('$injector', ['get']); + mockIdentifierService = jasmine.createSpyObj('identifierService', ['parse']); + mockIdentifierService.parse.and.returnValue({ + getSpace: () => { + return 'mct'; + } }); - afterEach(() => { - return resetApplicationState(openmct); + openmct.$injector.get.and.returnValue(mockIdentifierService); + + openmct.install(new CouchPlugin(options)); + + openmct.types.addType('notebook', { creatable: true }); + + openmct.on('start', done); + openmct.startHeadless(); + + provider = openmct.objects.getProvider(mockDomainObject.identifier); + spyOn(provider, 'get').and.callThrough(); + spyOn(provider, 'create').and.callThrough(); + spyOn(provider, 'update').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(() => { + return resetApplicationState(openmct); + }); + + describe('the provider', () => { + let mockPromise; + beforeEach(() => { + mockPromise = Promise.resolve({ + json: () => { + return { + ok: true, + _id: 'some-value', + id: 'some-value', + _rev: 1, + model: {} + }; + } + }); + fetch.and.returnValue(mockPromise); }); - describe('the provider', () => { - let mockPromise; - beforeEach(() => { - mockPromise = Promise.resolve({ - json: () => { - return { - ok: true, - _id: 'some-value', - id: 'some-value', - _rev: 1, - model: {} - }; - } - }); - fetch.and.returnValue(mockPromise); - }); + it('gets an object', async () => { + const result = await openmct.objects.get(mockDomainObject.identifier); + expect(result.identifier.key).toEqual(mockDomainObject.identifier.key); + }); - it('gets an object', async () => { - const result = await openmct.objects.get(mockDomainObject.identifier); - 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('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.observe).toHaveBeenCalled(); + expect(result).toBeTrue(); + }); - it('creates an object and starts shared worker', async () => { - const result = await openmct.objects.save(mockDomainObject); - expect(provider.create).toHaveBeenCalled(); - expect(provider.observe).toHaveBeenCalled(); - expect(result).toBeTrue(); + it('updates an object', (done) => { + openmct.objects.save(mockDomainObject).then((result) => { + expect(result).toBeTrue(); + expect(provider.create).toHaveBeenCalled(); + //Set modified timestamp it detects a change and persists the updated model. + mockDomainObject.modified = mockDomainObject.persisted + 1; + openmct.objects.save(mockDomainObject).then((updatedResult) => { + expect(updatedResult).toBeTrue(); + expect(provider.update).toHaveBeenCalled(); + done(); }); + }); + }); - it('updates an object', (done) => { - openmct.objects.save(mockDomainObject).then((result) => { - expect(result).toBeTrue(); - expect(provider.create).toHaveBeenCalled(); - //Set modified timestamp it detects a change and persists the updated model. - mockDomainObject.modified = mockDomainObject.persisted + 1; - openmct.objects.save(mockDomainObject).then((updatedResult) => { - expect(updatedResult).toBeTrue(); - expect(provider.update).toHaveBeenCalled(); - done(); + it('works without Shared Workers', async () => { + let sharedWorkerCallback; + const cachedSharedWorker = window.SharedWorker; + window.SharedWorker = undefined; + + const mockEventSource = { + addEventListener: (topic, addedListener) => { + sharedWorkerCallback = addedListener; + }, + removeEventListener: () => { + sharedWorkerCallback = null; + } + }; + const cachedEventSource = window.EventSource; + + window.EventSource = function (url) { + return mockEventSource; + }; + + mockDomainObject.id = mockDomainObject.identifier.key; + + const fakeUpdateEvent = { + data: JSON.stringify(mockDomainObject), + target: { + readyState: EventSource.CONNECTED + } + }; + + // eslint-disable-next-line require-await + provider.request = async function (subPath, method, body, signal) { + return { + body: fakeUpdateEvent, + ok: true, + id: mockDomainObject.id, + rev: 5 + }; + }; + + const result = await openmct.objects.save(mockDomainObject); + expect(result).toBeTrue(); + expect(provider.create).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; + const updatedResult = await openmct.objects.save(mockDomainObject); + openmct.objects.observe(mockDomainObject, '*', (updatedObject) => {}); + + 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(); + + window.SharedWorker = cachedSharedWorker; + window.EventSource = cachedEventSource; + }); + }); + describe('batches requests', () => { + let mockPromise; + beforeEach(() => { + mockPromise = Promise.resolve({ + json: () => { + return { + total_rows: 0, + rows: [] + }; + } + }); + fetch.and.returnValue(mockPromise); + }); + it('for multiple simultaneous gets', async () => { + const objectIds = [ + { + namespace: '', + key: 'object-1' + }, + { + namespace: '', + key: 'object-2' + }, + { + namespace: '', + key: 'object-3' + } + ]; + + await Promise.all(objectIds.map((identifier) => openmct.objects.get(identifier))); + + const requestUrl = fetch.calls.mostRecent().args[0]; + const requestMethod = fetch.calls.mostRecent().args[1].method; + expect(fetch).toHaveBeenCalledTimes(1); + expect(requestUrl.includes('_all_docs')).toBeTrue(); + expect(requestMethod).toEqual('POST'); + }); + + it('but not for single gets', async () => { + const objectId = { + namespace: '', + key: 'object-1' + }; + + await openmct.objects.get(objectId); + const requestUrl = fetch.calls.mostRecent().args[0]; + const requestMethod = fetch.calls.mostRecent().args[1].method; + + expect(fetch).toHaveBeenCalledTimes(1); + expect(requestUrl.endsWith(`${objectId.key}`)).toBeTrue(); + expect(requestMethod).toEqual('GET'); + }); + }); + describe('implements server-side search', () => { + let mockPromise; + beforeEach(() => { + mockPromise = Promise.resolve({ + body: { + getReader() { + return { + read() { + return Promise.resolve({ + done: true, + value: undefined }); - }); - }); - - it('works without Shared Workers', async () => { - let sharedWorkerCallback; - const cachedSharedWorker = window.SharedWorker; - window.SharedWorker = undefined; - - const mockEventSource = { - addEventListener: (topic, addedListener) => { - sharedWorkerCallback = addedListener; - }, - removeEventListener: () => { - sharedWorkerCallback = null; - } + } }; - const cachedEventSource = window.EventSource; - - window.EventSource = function (url) { - return mockEventSource; - }; - - mockDomainObject.id = mockDomainObject.identifier.key; - - const fakeUpdateEvent = { - data: JSON.stringify(mockDomainObject), - target: { - readyState: EventSource.CONNECTED - } - }; - - // eslint-disable-next-line require-await - provider.request = async function (subPath, method, body, signal) { - return { - body: fakeUpdateEvent, - ok: true, - id: mockDomainObject.id, - rev: 5 - }; - }; - - const result = await openmct.objects.save(mockDomainObject); - expect(result).toBeTrue(); - expect(provider.create).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; - const updatedResult = await openmct.objects.save(mockDomainObject); - openmct.objects.observe(mockDomainObject, '*', (updatedObject) => { - }); - - 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(); - - window.SharedWorker = cachedSharedWorker; - window.EventSource = cachedEventSource; - }); + } + } + }); + fetch.and.returnValue(mockPromise); }); - describe('batches requests', () => { - let mockPromise; - beforeEach(() => { - mockPromise = Promise.resolve({ - json: () => { - return { - total_rows: 0, - rows: [] - }; - } - }); - fetch.and.returnValue(mockPromise); - }); - it('for multiple simultaneous gets', async () => { - const objectIds = [ - { - namespace: '', - key: 'object-1' - }, { - namespace: '', - key: 'object-2' - }, { - namespace: '', - key: 'object-3' - } - ]; - await Promise.all( - objectIds.map((identifier) => - openmct.objects.get(identifier) - ) - ); + it("using Couch's 'find' endpoint", async () => { + await Promise.all(openmct.objects.search('test')); + const requestUrl = fetch.calls.mostRecent().args[0]; - const requestUrl = fetch.calls.mostRecent().args[0]; - const requestMethod = fetch.calls.mostRecent().args[1].method; - expect(fetch).toHaveBeenCalledTimes(1); - expect(requestUrl.includes('_all_docs')).toBeTrue(); - expect(requestMethod).toEqual('POST'); - }); - - it('but not for single gets', async () => { - const objectId = { - namespace: '', - key: 'object-1' - }; - - await openmct.objects.get(objectId); - const requestUrl = fetch.calls.mostRecent().args[0]; - const requestMethod = fetch.calls.mostRecent().args[1].method; - - expect(fetch).toHaveBeenCalledTimes(1); - expect(requestUrl.endsWith(`${objectId.key}`)).toBeTrue(); - expect(requestMethod).toEqual('GET'); - }); + // we only want one call to fetch, not 2! + // see https://github.com/nasa/openmct/issues/4667 + expect(fetch).toHaveBeenCalledTimes(1); + expect(requestUrl.endsWith('_find')).toBeTrue(); }); - describe('implements server-side search', () => { - let mockPromise; - beforeEach(() => { - mockPromise = Promise.resolve({ - body: { - getReader() { - return { - read() { - return Promise.resolve({ - done: true, - value: undefined - }); - } - }; - } - } - }); - fetch.and.returnValue(mockPromise); - }); - it("using Couch's 'find' endpoint", async () => { - await Promise.all(openmct.objects.search('test')); - const requestUrl = fetch.calls.mostRecent().args[0]; + it('and supports search by object name', async () => { + await Promise.all(openmct.objects.search('test')); + const requestPayload = JSON.parse(fetch.calls.mostRecent().args[1].body); - // we only want one call to fetch, not 2! - // see https://github.com/nasa/openmct/issues/4667 - expect(fetch).toHaveBeenCalledTimes(1); - expect(requestUrl.endsWith('_find')).toBeTrue(); - }); - - it("and supports search by object name", async () => { - await Promise.all(openmct.objects.search('test')); - const requestPayload = JSON.parse(fetch.calls.mostRecent().args[1].body); - - expect(requestPayload).toBeDefined(); - expect(requestPayload.selector.model.name.$regex).toEqual('(?i)test'); - }); + expect(requestPayload).toBeDefined(); + expect(requestPayload.selector.model.name.$regex).toEqual('(?i)test'); }); + }); }); 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(); - }); + 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); - }); + afterEach(() => { + return resetApplicationState(openmct); + }); - describe('updates CouchDB status indicator', () => { - let mockPromise; + 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')); + 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); - 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(); - 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); - }); + 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/plan/GanttChartCompositionPolicy.js b/src/plugins/plan/GanttChartCompositionPolicy.js index 1c149bd25d..7e8b21990b 100644 --- a/src/plugins/plan/GanttChartCompositionPolicy.js +++ b/src/plugins/plan/GanttChartCompositionPolicy.js @@ -19,17 +19,14 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -const ALLOWED_TYPES = [ - 'plan' -]; +const ALLOWED_TYPES = ['plan']; export default function ganttChartCompositionPolicy(openmct) { - return function (parent, child) { - if (parent.type === 'gantt-chart') { - return ALLOWED_TYPES.includes(child.type); - } + return function (parent, child) { + if (parent.type === 'gantt-chart') { + return ALLOWED_TYPES.includes(child.type); + } - return true; - }; + return true; + }; } - diff --git a/src/plugins/plan/PlanViewConfiguration.js b/src/plugins/plan/PlanViewConfiguration.js index 249d97be27..3897dcfb9f 100644 --- a/src/plugins/plan/PlanViewConfiguration.js +++ b/src/plugins/plan/PlanViewConfiguration.js @@ -23,88 +23,92 @@ import EventEmitter from 'EventEmitter'; export const DEFAULT_CONFIGURATION = { - clipActivityNames: false, - swimlaneVisibility: {} + clipActivityNames: false, + swimlaneVisibility: {} }; export default class PlanViewConfiguration extends EventEmitter { - constructor(domainObject, openmct) { - super(); + constructor(domainObject, openmct) { + super(); - this.domainObject = domainObject; - this.openmct = openmct; + this.domainObject = domainObject; + this.openmct = openmct; - this.configurationChanged = this.configurationChanged.bind(this); - this.unlistenFromMutation = openmct.objects.observe(domainObject, 'configuration', this.configurationChanged); + this.configurationChanged = this.configurationChanged.bind(this); + this.unlistenFromMutation = openmct.objects.observe( + domainObject, + 'configuration', + this.configurationChanged + ); + } + + /** + * @returns {Object.} + */ + getConfiguration() { + const configuration = this.domainObject.configuration ?? {}; + for (const configKey of Object.keys(DEFAULT_CONFIGURATION)) { + configuration[configKey] = configuration[configKey] ?? DEFAULT_CONFIGURATION[configKey]; } - /** - * @returns {Object.} - */ - getConfiguration() { - const configuration = this.domainObject.configuration ?? {}; - for (const configKey of Object.keys(DEFAULT_CONFIGURATION)) { - configuration[configKey] = configuration[configKey] ?? DEFAULT_CONFIGURATION[configKey]; - } + return configuration; + } - return configuration; + #updateConfiguration(configuration) { + this.openmct.objects.mutate(this.domainObject, 'configuration', configuration); + } + + /** + * @param {string} swimlaneName + * @param {boolean} isVisible + */ + setSwimlaneVisibility(swimlaneName, isVisible) { + const configuration = this.getConfiguration(); + const { swimlaneVisibility } = configuration; + swimlaneVisibility[swimlaneName] = isVisible; + this.#updateConfiguration(configuration); + } + + resetSwimlaneVisibility() { + const configuration = this.getConfiguration(); + const swimlaneVisibility = {}; + configuration.swimlaneVisibility = swimlaneVisibility; + this.#updateConfiguration(configuration); + } + + initializeSwimlaneVisibility(swimlaneNames) { + const configuration = this.getConfiguration(); + const { swimlaneVisibility } = configuration; + let shouldMutate = false; + for (const swimlaneName of swimlaneNames) { + if (swimlaneVisibility[swimlaneName] === undefined) { + swimlaneVisibility[swimlaneName] = true; + shouldMutate = true; + } } - #updateConfiguration(configuration) { - this.openmct.objects.mutate(this.domainObject, 'configuration', configuration); + if (shouldMutate) { + configuration.swimlaneVisibility = swimlaneVisibility; + this.#updateConfiguration(configuration); } + } - /** - * @param {string} swimlaneName - * @param {boolean} isVisible - */ - setSwimlaneVisibility(swimlaneName, isVisible) { - const configuration = this.getConfiguration(); - const { swimlaneVisibility } = configuration; - swimlaneVisibility[swimlaneName] = isVisible; - this.#updateConfiguration(configuration); + /** + * @param {boolean} isEnabled + */ + setClipActivityNames(isEnabled) { + const configuration = this.getConfiguration(); + configuration.clipActivityNames = isEnabled; + this.#updateConfiguration(configuration); + } + + configurationChanged(configuration) { + if (configuration !== undefined) { + this.emit('change', configuration); } + } - resetSwimlaneVisibility() { - const configuration = this.getConfiguration(); - const swimlaneVisibility = {}; - configuration.swimlaneVisibility = swimlaneVisibility; - this.#updateConfiguration(configuration); - } - - initializeSwimlaneVisibility(swimlaneNames) { - const configuration = this.getConfiguration(); - const { swimlaneVisibility } = configuration; - let shouldMutate = false; - for (const swimlaneName of swimlaneNames) { - if (swimlaneVisibility[swimlaneName] === undefined) { - swimlaneVisibility[swimlaneName] = true; - shouldMutate = true; - } - } - - if (shouldMutate) { - configuration.swimlaneVisibility = swimlaneVisibility; - this.#updateConfiguration(configuration); - } - } - - /** - * @param {boolean} isEnabled - */ - setClipActivityNames(isEnabled) { - const configuration = this.getConfiguration(); - configuration.clipActivityNames = isEnabled; - this.#updateConfiguration(configuration); - } - - configurationChanged(configuration) { - if (configuration !== undefined) { - this.emit('change', configuration); - } - } - - destroy() { - this.unlistenFromMutation(); - } + destroy() { + this.unlistenFromMutation(); + } } diff --git a/src/plugins/plan/PlanViewProvider.js b/src/plugins/plan/PlanViewProvider.js index dcf3ac1056..93b0503f02 100644 --- a/src/plugins/plan/PlanViewProvider.js +++ b/src/plugins/plan/PlanViewProvider.js @@ -24,57 +24,57 @@ import Plan from './components/Plan.vue'; import Vue from 'vue'; export default function PlanViewProvider(openmct) { - function isCompactView(objectPath) { - let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip'); + function isCompactView(objectPath) { + let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip'); - return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); - } + return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); + } - return { - key: 'plan.view', - name: 'Plan', - cssClass: 'icon-plan', - canView(domainObject) { - return domainObject.type === 'plan' || domainObject.type === 'gantt-chart'; - }, + return { + key: 'plan.view', + name: 'Plan', + cssClass: 'icon-plan', + canView(domainObject) { + return domainObject.type === 'plan' || domainObject.type === 'gantt-chart'; + }, - canEdit(domainObject) { - return domainObject.type === 'gantt-chart'; - }, + canEdit(domainObject) { + return domainObject.type === 'gantt-chart'; + }, - view: function (domainObject, objectPath) { - let component; + view: function (domainObject, objectPath) { + let component; - return { - show: function (element) { - let isCompact = isCompactView(objectPath); + return { + show: function (element) { + let isCompact = isCompactView(objectPath); - component = new Vue({ - el: element, - components: { - Plan - }, - provide: { - openmct, - domainObject, - path: objectPath - }, - data() { - return { - options: { - compact: isCompact, - isChildObject: isCompact - } - }; - }, - template: '' - }); - }, - destroy: function () { - component.$destroy(); - component = undefined; + component = new Vue({ + el: element, + components: { + Plan + }, + provide: { + openmct, + domainObject, + path: objectPath + }, + data() { + return { + options: { + compact: isCompact, + isChildObject: isCompact } - }; + }; + }, + template: '' + }); + }, + destroy: function () { + component.$destroy(); + component = undefined; } - }; + }; + } + }; } diff --git a/src/plugins/plan/components/ActivityTimeline.vue b/src/plugins/plan/components/ActivityTimeline.vue index ec0120d392..770a5ba264 100644 --- a/src/plugins/plan/components/ActivityTimeline.vue +++ b/src/plugins/plan/components/ActivityTimeline.vue @@ -20,168 +20,154 @@ at runtime from the About dialog for additional information. --> + + No activities within current timeframe + + - + diff --git a/src/plugins/plan/components/Plan.vue b/src/plugins/plan/components/Plan.vue index 9890d6040b..040211f053 100644 --- a/src/plugins/plan/components/Plan.vue +++ b/src/plugins/plan/components/Plan.vue @@ -21,48 +21,42 @@ --> +
    +
    -
    +
    diff --git a/src/plugins/plan/inspector/ActivityInspectorViewProvider.js b/src/plugins/plan/inspector/ActivityInspectorViewProvider.js index 2dfb756911..911fdfbaef 100644 --- a/src/plugins/plan/inspector/ActivityInspectorViewProvider.js +++ b/src/plugins/plan/inspector/ActivityInspectorViewProvider.js @@ -20,51 +20,50 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import PlanActivitiesView from "./components/PlanActivitiesView.vue"; +import PlanActivitiesView from './components/PlanActivitiesView.vue'; import Vue from 'vue'; export default function ActivityInspectorViewProvider(openmct) { - return { - key: 'activity-inspector', - name: 'Activity', - canView: function (selection) { - if (selection.length === 0 || selection[0].length === 0) { - return false; - } + return { + key: 'activity-inspector', + name: 'Activity', + canView: function (selection) { + if (selection.length === 0 || selection[0].length === 0) { + return false; + } - let context = selection[0][0].context; + let context = selection[0][0].context; - return context - && context.type === 'activity'; + return context && context.type === 'activity'; + }, + view: function (selection) { + let component; + + return { + show: function (element) { + component = new Vue({ + el: element, + name: 'PlanActivitiesView', + components: { + PlanActivitiesView: PlanActivitiesView + }, + provide: { + openmct, + selection: selection + }, + template: '' + }); }, - view: function (selection) { - let component; - - return { - show: function (element) { - component = new Vue({ - el: element, - name: "PlanActivitiesView", - components: { - PlanActivitiesView: PlanActivitiesView - }, - provide: { - openmct, - selection: selection - }, - template: '' - }); - }, - priority: function () { - return openmct.priority.HIGH + 1; - }, - destroy: function () { - if (component) { - component.$destroy(); - component = undefined; - } - } - }; + priority: function () { + return openmct.priority.HIGH + 1; + }, + destroy: function () { + if (component) { + component.$destroy(); + component = undefined; + } } - }; + }; + } + }; } diff --git a/src/plugins/plan/inspector/GanttChartInspectorViewProvider.js b/src/plugins/plan/inspector/GanttChartInspectorViewProvider.js index 56f66b4a5b..dc19cc813c 100644 --- a/src/plugins/plan/inspector/GanttChartInspectorViewProvider.js +++ b/src/plugins/plan/inspector/GanttChartInspectorViewProvider.js @@ -24,45 +24,45 @@ import PlanViewConfiguration from './components/PlanViewConfiguration.vue'; import Vue from 'vue'; export default function GanttChartInspectorViewProvider(openmct) { - return { - key: 'plan-inspector', - name: 'Config', - canView: function (selection) { - if (selection.length === 0 || selection[0].length === 0) { - return false; - } + return { + key: 'plan-inspector', + name: 'Config', + canView: function (selection) { + if (selection.length === 0 || selection[0].length === 0) { + return false; + } - const domainObject = selection[0][0].context.item; + const domainObject = selection[0][0].context.item; - return domainObject?.type === 'gantt-chart'; + return domainObject?.type === 'gantt-chart'; + }, + view: function (selection) { + let component; + + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + PlanViewConfiguration + }, + provide: { + openmct, + selection: selection + }, + template: '' + }); }, - view: function (selection) { - let component; - - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - PlanViewConfiguration - }, - provide: { - openmct, - selection: selection - }, - template: '' - }); - }, - priority: function () { - return openmct.priority.HIGH + 1; - }, - destroy: function () { - if (component) { - component.$destroy(); - component = undefined; - } - } - }; + priority: function () { + return openmct.priority.HIGH + 1; + }, + destroy: function () { + if (component) { + component.$destroy(); + component = undefined; + } } - }; + }; + } + }; } diff --git a/src/plugins/plan/inspector/components/ActivityProperty.vue b/src/plugins/plan/inspector/components/ActivityProperty.vue index 88c1f4d424..1b6560dfb5 100644 --- a/src/plugins/plan/inspector/components/ActivityProperty.vue +++ b/src/plugins/plan/inspector/components/ActivityProperty.vue @@ -21,32 +21,31 @@ --> diff --git a/src/plugins/plan/inspector/components/PlanActivitiesView.vue b/src/plugins/plan/inspector/components/PlanActivitiesView.vue index 8e7b5c1641..6e31acaa62 100644 --- a/src/plugins/plan/inspector/components/PlanActivitiesView.vue +++ b/src/plugins/plan/inspector/components/PlanActivitiesView.vue @@ -20,187 +20,187 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plan/inspector/components/PlanActivityView.vue b/src/plugins/plan/inspector/components/PlanActivityView.vue index 605e09cc22..d514eb472a 100644 --- a/src/plugins/plan/inspector/components/PlanActivityView.vue +++ b/src/plugins/plan/inspector/components/PlanActivityView.vue @@ -21,24 +21,18 @@ --> diff --git a/src/plugins/plan/inspector/components/PlanViewConfiguration.vue b/src/plugins/plan/inspector/components/PlanViewConfiguration.vue index 786e97ed4b..c8747ffad3 100644 --- a/src/plugins/plan/inspector/components/PlanViewConfiguration.vue +++ b/src/plugins/plan/inspector/components/PlanViewConfiguration.vue @@ -20,125 +20,109 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plan/plan.scss b/src/plugins/plan/plan.scss index a582260fa9..4353079658 100644 --- a/src/plugins/plan/plan.scss +++ b/src/plugins/plan/plan.scss @@ -21,34 +21,35 @@ *****************************************************************************/ .c-plan { - svg { - text-rendering: geometricPrecision; + svg { + text-rendering: geometricPrecision; - text { - stroke: none; - } + text { + stroke: none; } + } - &__activity { - cursor: pointer; + &__activity { + cursor: pointer; - &[s-selected] { - rect, use { - outline-style: dotted; - outline-width: 2px; - stroke: $colorGanttSelectedBorder; - stroke-width: 2px; - } - } + &[s-selected] { + rect, + use { + outline-style: dotted; + outline-width: 2px; + stroke: $colorGanttSelectedBorder; + stroke-width: 2px; + } } + } - &__activity-label { - &--outside-rect { - fill: $colorBodyFg !important; - } + &__activity-label { + &--outside-rect { + fill: $colorBodyFg !important; } + } - canvas { - display: none; - } + canvas { + display: none; + } } diff --git a/src/plugins/plan/plugin.js b/src/plugins/plan/plugin.js index 85022fd61b..ae999d3c3f 100644 --- a/src/plugins/plan/plugin.js +++ b/src/plugins/plan/plugin.js @@ -21,57 +21,54 @@ *****************************************************************************/ import PlanViewProvider from './PlanViewProvider'; -import ActivityInspectorViewProvider from "./inspector/ActivityInspectorViewProvider"; -import GanttChartInspectorViewProvider from "./inspector/GanttChartInspectorViewProvider"; +import ActivityInspectorViewProvider from './inspector/ActivityInspectorViewProvider'; +import GanttChartInspectorViewProvider from './inspector/GanttChartInspectorViewProvider'; import ganttChartCompositionPolicy from './GanttChartCompositionPolicy'; import { DEFAULT_CONFIGURATION } from './PlanViewConfiguration'; export default function (options = {}) { - return function install(openmct) { - openmct.types.addType('plan', { - name: 'Plan', - key: 'plan', - description: 'A non-configurable timeline-like view for a compatible plan file.', - creatable: options.creatable ?? false, - cssClass: 'icon-plan', - 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 = { - clipActivityNames: DEFAULT_CONFIGURATION.clipActivityNames - }; - } - }); - // Name TBD and subject to change - openmct.types.addType('gantt-chart', { - name: 'Gantt Chart', - key: 'gantt-chart', - description: 'A configurable timeline-like view for a compatible plan file.', - creatable: true, - cssClass: 'icon-plan', - form: [], - initialize(domainObject) { - domainObject.configuration = { - clipActivityNames: true - }; - domainObject.composition = []; - } - }); - openmct.objectViews.addProvider(new PlanViewProvider(openmct)); - openmct.inspectorViews.addProvider(new ActivityInspectorViewProvider(openmct)); - openmct.inspectorViews.addProvider(new GanttChartInspectorViewProvider(openmct)); - openmct.composition.addPolicy(ganttChartCompositionPolicy(openmct)); - }; + return function install(openmct) { + openmct.types.addType('plan', { + name: 'Plan', + key: 'plan', + description: 'A non-configurable timeline-like view for a compatible plan file.', + creatable: options.creatable ?? false, + cssClass: 'icon-plan', + 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 = { + clipActivityNames: DEFAULT_CONFIGURATION.clipActivityNames + }; + } + }); + // Name TBD and subject to change + openmct.types.addType('gantt-chart', { + name: 'Gantt Chart', + key: 'gantt-chart', + description: 'A configurable timeline-like view for a compatible plan file.', + creatable: true, + cssClass: 'icon-plan', + form: [], + initialize(domainObject) { + domainObject.configuration = { + clipActivityNames: true + }; + domainObject.composition = []; + } + }); + openmct.objectViews.addProvider(new PlanViewProvider(openmct)); + openmct.inspectorViews.addProvider(new ActivityInspectorViewProvider(openmct)); + openmct.inspectorViews.addProvider(new GanttChartInspectorViewProvider(openmct)); + openmct.composition.addPolicy(ganttChartCompositionPolicy(openmct)); + }; } - diff --git a/src/plugins/plan/pluginSpec.js b/src/plugins/plan/pluginSpec.js index 25db86289f..ca4d35651b 100644 --- a/src/plugins/plan/pluginSpec.js +++ b/src/plugins/plan/pluginSpec.js @@ -20,268 +20,281 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import {createOpenMct, resetApplicationState} from "utils/testing"; -import PlanPlugin from "../plan/plugin"; +import { createOpenMct, resetApplicationState } from 'utils/testing'; +import PlanPlugin from '../plan/plugin'; import Vue from 'vue'; -import Properties from "../inspectorViews/properties/Properties.vue"; +import Properties from '../inspectorViews/properties/Properties.vue'; describe('the plugin', function () { - let planDefinition; - let ganttDefinition; - let element; - let child; - let openmct; - let appHolder; - let originalRouterPath; + let planDefinition; + let ganttDefinition; + let element; + let child; + let openmct; + let appHolder; + let originalRouterPath; - beforeEach((done) => { - appHolder = document.createElement('div'); - appHolder.style.width = '640px'; - appHolder.style.height = '480px'; + beforeEach((done) => { + appHolder = document.createElement('div'); + appHolder.style.width = '640px'; + appHolder.style.height = '480px'; - const timeSystemOptions = { - timeSystemKey: 'utc', - bounds: { - start: 1597160002854, - end: 1597181232854 + const timeSystemOptions = { + timeSystemKey: 'utc', + bounds: { + start: 1597160002854, + end: 1597181232854 + } + }; + + openmct = createOpenMct(timeSystemOptions); + openmct.install(new PlanPlugin()); + + planDefinition = openmct.types.get('plan').definition; + ganttDefinition = openmct.types.get('gantt-chart').definition; + + 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); + + originalRouterPath = openmct.router.path; + + openmct.on('start', done); + openmct.start(appHolder); + }); + + afterEach(() => { + openmct.router.path = originalRouterPath; + + return resetApplicationState(openmct); + }); + + let mockPlanObject = { + name: 'Plan', + key: 'plan', + creatable: false + }; + + let mockGanttObject = { + name: 'Gantt', + key: 'gantt-chart', + creatable: true + }; + + describe('the plan type', () => { + it('defines a plan object type with the correct key', () => { + expect(planDefinition.key).toEqual(mockPlanObject.key); + }); + it('is not creatable', () => { + expect(planDefinition.creatable).toEqual(mockPlanObject.creatable); + }); + }); + describe('the gantt-chart type', () => { + it('defines a gantt-chart object type with the correct key', () => { + expect(ganttDefinition.key).toEqual(mockGanttObject.key); + }); + it('is creatable', () => { + expect(ganttDefinition.creatable).toEqual(mockGanttObject.creatable); + }); + }); + + describe('the plan view', () => { + it('provides a plan 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).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(testViewObject)).toBeFalse(); + }); + }); + + describe('the plan view displays activities', () => { + let planDomainObject; + let mockObjectPath = [ + { + identifier: { + key: 'test', + namespace: '' + }, + type: 'time-strip', + name: 'Test Parent Object' + } + ]; + let planView; + + beforeEach(() => { + openmct.time.timeSystem('utc', { + start: 1597160002854, + end: 1597181232854 + }); + + planDomainObject = { + identifier: { + key: 'test-object', + namespace: '' + }, + type: 'plan', + id: 'test-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' + } + ] + }) + } + }; + + openmct.router.path = [planDomainObject]; + + const applicableViews = openmct.objectViews.get(planDomainObject, [planDomainObject]); + planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view'); + let view = planView.view(planDomainObject, mockObjectPath); + view.show(child, true); + + return Vue.nextTick(); + }); + + it('loads activities into the view', () => { + const svgEls = element.querySelectorAll('.c-plan__contents svg'); + expect(svgEls.length).toEqual(1); + }); + + it('displays the group label', () => { + const labelEl = element.querySelector( + '.c-plan__contents .c-object-label .c-object-label__name' + ); + expect(labelEl.innerHTML).toMatch(/TEST-GROUP/); + }); + + it('displays the activities and their labels', async () => { + const bounds = { + start: 1597160002854, + end: 1597181232854 + }; + + openmct.time.bounds(bounds); + + await Vue.nextTick(); + const rectEls = element.querySelectorAll('.c-plan__contents use'); + expect(rectEls.length).toEqual(2); + const textEls = element.querySelectorAll('.c-plan__contents text'); + expect(textEls.length).toEqual(3); + }); + + it('shows the status indicator when available', async () => { + openmct.status.set( + { + key: 'test-object', + namespace: '' + }, + 'draft' + ); + + await Vue.nextTick(); + const statusEl = element.querySelector('.c-plan__contents .is-status--draft'); + expect(statusEl).toBeDefined(); + }); + }); + + describe('the plan version', () => { + let component; + let componentObject; + let testPlanObject = { + name: 'Plan', + type: 'plan', + identifier: { + key: 'test-plan', + namespace: '' + }, + created: 123456789, + modified: 123456790, + version: 'v1' + }; + + beforeEach(async () => { + openmct.selection.select( + [ + { + element: element, + context: { + item: testPlanObject } - }; + }, + { + element: openmct.layout.$refs.browseObject.$el, + context: { + item: testPlanObject, + supportsMultiSelect: false + } + } + ], + false + ); - openmct = createOpenMct(timeSystemOptions); - openmct.install(new PlanPlugin()); - - planDefinition = openmct.types.get('plan').definition; - ganttDefinition = openmct.types.get('gantt-chart').definition; - - 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); - - originalRouterPath = openmct.router.path; - - openmct.on('start', done); - openmct.start(appHolder); + await Vue.nextTick(); + let viewContainer = document.createElement('div'); + child.append(viewContainer); + component = new Vue({ + el: viewContainer, + components: { + Properties + }, + provide: { + openmct: openmct + }, + template: '' + }); }); afterEach(() => { - openmct.router.path = originalRouterPath; - - return resetApplicationState(openmct); + component.$destroy(); }); - let mockPlanObject = { - name: 'Plan', - key: 'plan', - creatable: false - }; - - let mockGanttObject = { - name: 'Gantt', - key: 'gantt-chart', - creatable: true - }; - - describe('the plan type', () => { - it('defines a plan object type with the correct key', () => { - expect(planDefinition.key).toEqual(mockPlanObject.key); - }); - it('is not creatable', () => { - expect(planDefinition.creatable).toEqual(mockPlanObject.creatable); - }); - }); - describe('the gantt-chart type', () => { - it('defines a gantt-chart object type with the correct key', () => { - expect(ganttDefinition.key).toEqual(mockGanttObject.key); - }); - it('is creatable', () => { - expect(ganttDefinition.creatable).toEqual(mockGanttObject.creatable); - }); - }); - - describe('the plan view', () => { - it('provides a plan 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).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(testViewObject)).toBeFalse(); - }); - }); - - describe('the plan view displays activities', () => { - let planDomainObject; - let mockObjectPath = [ - { - identifier: { - key: 'test', - namespace: '' - }, - type: 'time-strip', - name: 'Test Parent Object' - } - ]; - let planView; - - beforeEach(() => { - openmct.time.timeSystem('utc', { - start: 1597160002854, - end: 1597181232854 - }); - - planDomainObject = { - identifier: { - key: 'test-object', - namespace: '' - }, - type: 'plan', - id: "test-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" - } - ] - }) - } - }; - - openmct.router.path = [planDomainObject]; - - const applicableViews = openmct.objectViews.get(planDomainObject, [planDomainObject]); - planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view'); - let view = planView.view(planDomainObject, mockObjectPath); - view.show(child, true); - - return Vue.nextTick(); - }); - - it('loads activities into the view', () => { - const svgEls = element.querySelectorAll('.c-plan__contents svg'); - expect(svgEls.length).toEqual(1); - }); - - it('displays the group label', () => { - const labelEl = element.querySelector('.c-plan__contents .c-object-label .c-object-label__name'); - expect(labelEl.innerHTML).toMatch(/TEST-GROUP/); - }); - - it('displays the activities and their labels', async () => { - const bounds = { - start: 1597160002854, - end: 1597181232854 - }; - - openmct.time.bounds(bounds); - - await Vue.nextTick(); - const rectEls = element.querySelectorAll('.c-plan__contents use'); - expect(rectEls.length).toEqual(2); - const textEls = element.querySelectorAll('.c-plan__contents text'); - expect(textEls.length).toEqual(3); - }); - - it ('shows the status indicator when available', async () => { - openmct.status.set({ - key: "test-object", - namespace: '' - }, 'draft'); - - await Vue.nextTick(); - const statusEl = element.querySelector('.c-plan__contents .is-status--draft'); - expect(statusEl).toBeDefined(); - }); - }); - - describe('the plan version', () => { - let component; - let componentObject; - let testPlanObject = { - name: 'Plan', - type: 'plan', - identifier: { - key: 'test-plan', - namespace: '' - }, - created: 123456789, - modified: 123456790, - version: 'v1' - }; - - beforeEach(async () => { - openmct.selection.select([{ - element: element, - context: { - item: testPlanObject - } - }, { - element: openmct.layout.$refs.browseObject.$el, - context: { - item: testPlanObject, - supportsMultiSelect: false - } - }], false); - - await Vue.nextTick(); - let viewContainer = document.createElement('div'); - child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - Properties - }, - provide: { - openmct: openmct - }, - template: '' - }); - }); - - 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'); - const found = Array.from(propertiesEls).some((propertyEl) => { - return (propertyEl.children[0].innerHTML.trim() === 'Version' - && propertyEl.children[1].innerHTML.trim() === 'v1'); - }); - expect(found).toBeTrue(); - }); + 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'); + 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 27cbcb5491..3914f40b8b 100644 --- a/src/plugins/plan/util.js +++ b/src/plugins/plan/util.js @@ -21,70 +21,74 @@ *****************************************************************************/ export function getValidatedData(domainObject) { - const sourceMap = domainObject.sourceMap; - const body = domainObject.selectFile?.body; - let json = {}; - if (typeof body === 'string') { - try { - json = JSON.parse(body); - } catch (e) { - return json; + const sourceMap = domainObject.sourceMap; + const body = domainObject.selectFile?.body; + let json = {}; + if (typeof body === 'string') { + try { + json = JSON.parse(body); + } catch (e) { + return json; + } + } else if (body !== undefined) { + json = body; + } + + if ( + sourceMap !== undefined && + sourceMap.activities !== undefined && + sourceMap.groupId !== undefined + ) { + let mappedJson = {}; + json[sourceMap.activities].forEach((activity) => { + if (activity[sourceMap.groupId]) { + const groupIdKey = activity[sourceMap.groupId]; + let groupActivity = { + ...activity + }; + + if (sourceMap.start) { + groupActivity.start = activity[sourceMap.start]; } - } else if (body !== undefined) { - json = body; - } - if (sourceMap !== undefined && sourceMap.activities !== undefined && sourceMap.groupId !== undefined) { - let mappedJson = {}; - json[sourceMap.activities].forEach((activity) => { - if (activity[sourceMap.groupId]) { - const groupIdKey = activity[sourceMap.groupId]; - let groupActivity = { - ...activity - }; + if (sourceMap.end) { + groupActivity.end = activity[sourceMap.end]; + } - if (sourceMap.start) { - groupActivity.start = activity[sourceMap.start]; - } + if (!mappedJson[groupIdKey]) { + mappedJson[groupIdKey] = []; + } - if (sourceMap.end) { - groupActivity.end = activity[sourceMap.end]; - } + mappedJson[groupIdKey].push(groupActivity); + } + }); - if (!mappedJson[groupIdKey]) { - mappedJson[groupIdKey] = []; - } - - mappedJson[groupIdKey].push(groupActivity); - } - }); - - return mappedJson; - } else { - return json; - } + return mappedJson; + } else { + return json; + } } export function getContrastingColor(hexColor) { - function cutHex(h, start, end) { - const hStr = (h.charAt(0) === '#') ? h.substring(1, 7) : h; + function cutHex(h, start, end) { + const hStr = h.charAt(0) === '#' ? h.substring(1, 7) : h; - return parseInt(hStr.substring(start, end), 16); - } + return parseInt(hStr.substring(start, end), 16); + } - // https://codepen.io/davidhalford/pen/ywEva/ - const cThreshold = 130; + // https://codepen.io/davidhalford/pen/ywEva/ + const cThreshold = 130; - if (hexColor.indexOf('#') === -1) { - // We weren't given a hex color - return "#ff0000"; - } + if (hexColor.indexOf('#') === -1) { + // We weren't given a hex color + return '#ff0000'; + } - const hR = cutHex(hexColor, 0, 2); - const hG = cutHex(hexColor, 2, 4); - const hB = cutHex(hexColor, 4, 6); + const hR = cutHex(hexColor, 0, 2); + const hG = cutHex(hexColor, 2, 4); + const hB = cutHex(hexColor, 4, 6); - const cBrightness = ((hR * 299) + (hG * 587) + (hB * 114)) / 1000; + const cBrightness = (hR * 299 + hG * 587 + hB * 114) / 1000; - return cBrightness > cThreshold ? "#000000" : "#ffffff"; + return cBrightness > cThreshold ? '#000000' : '#ffffff'; } diff --git a/src/plugins/plot/LinearScale.js b/src/plugins/plot/LinearScale.js index 6d24f3db83..90c1b90722 100644 --- a/src/plugins/plot/LinearScale.js +++ b/src/plugins/plot/LinearScale.js @@ -27,53 +27,53 @@ */ class LinearScale { - constructor(domain) { - this.domain(domain); + constructor(domain) { + this.domain(domain); + } + + domain(newDomain) { + if (newDomain) { + this._domain = newDomain; + this._domainDenominator = newDomain.max - newDomain.min; } - domain(newDomain) { - if (newDomain) { - this._domain = newDomain; - this._domainDenominator = newDomain.max - newDomain.min; - } + return this._domain; + } - return this._domain; + range(newRange) { + if (newRange) { + this._range = newRange; + this._rangeDenominator = newRange.max - newRange.min; } - range(newRange) { - if (newRange) { - this._range = newRange; - this._rangeDenominator = newRange.max - newRange.min; - } + return this._range; + } - return this._range; + scale(domainValue) { + if (!this._domain || !this._range) { + return; } - scale(domainValue) { - if (!this._domain || !this._range) { - return; - } + const domainOffset = domainValue - this._domain.min; + const rangeFraction = domainOffset - this._domainDenominator; + const rangeOffset = rangeFraction * this._rangeDenominator; + const rangeValue = rangeOffset + this._range.min; - const domainOffset = domainValue - this._domain.min; - const rangeFraction = domainOffset - this._domainDenominator; - const rangeOffset = rangeFraction * this._rangeDenominator; - const rangeValue = rangeOffset + this._range.min; + return rangeValue; + } - return rangeValue; + invert(rangeValue) { + if (!this._domain || !this._range) { + return; } - invert(rangeValue) { - if (!this._domain || !this._range) { - return; - } + const rangeOffset = rangeValue - this._range.min; + const domainFraction = rangeOffset / this._rangeDenominator; + const domainOffset = domainFraction * this._domainDenominator; + const domainValue = domainOffset + this._domain.min; - const rangeOffset = rangeValue - this._range.min; - const domainFraction = rangeOffset / this._rangeDenominator; - const domainOffset = domainFraction * this._domainDenominator; - const domainValue = domainOffset + this._domain.min; - - return domainValue; - } + return domainValue; + } } export default LinearScale; diff --git a/src/plugins/plot/MctPlot.vue b/src/plugins/plot/MctPlot.vue index 8227ba044e..c0d0e18e24 100644 --- a/src/plugins/plot/MctPlot.vue +++ b/src/plugins/plot/MctPlot.vue @@ -20,1845 +20,1900 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/MctTicks.vue b/src/plugins/plot/MctTicks.vue index 88d7185011..bfcaf866b2 100644 --- a/src/plugins/plot/MctTicks.vue +++ b/src/plugins/plot/MctTicks.vue @@ -21,270 +21,266 @@ --> diff --git a/src/plugins/plot/Plot.vue b/src/plugins/plot/Plot.vue index f31b5c5496..49605adec8 100644 --- a/src/plugins/plot/Plot.vue +++ b/src/plugins/plot/Plot.vue @@ -20,222 +20,225 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/PlotViewProvider.js b/src/plugins/plot/PlotViewProvider.js index 461696150c..78c3bea730 100644 --- a/src/plugins/plot/PlotViewProvider.js +++ b/src/plugins/plot/PlotViewProvider.js @@ -24,80 +24,82 @@ import Plot from './Plot.vue'; import Vue from 'vue'; export default function PlotViewProvider(openmct) { - function hasNumericTelemetry(domainObject) { - if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) { - return false; - } - - let metadata = openmct.telemetry.getMetadata(domainObject); - - return metadata.values().length > 0 && hasDomainAndNumericRange(metadata); + function hasNumericTelemetry(domainObject) { + if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) { + return false; } - function hasDomainAndNumericRange(metadata) { - const rangeValues = metadata.valuesForHints(['range']); - const domains = metadata.valuesForHints(['domain']); + let metadata = openmct.telemetry.getMetadata(domainObject); - return domains.length > 0 - && rangeValues.length > 0 - && !rangeValues.every(value => value.format === 'string'); - } + return metadata.values().length > 0 && hasDomainAndNumericRange(metadata); + } - function isCompactView(objectPath) { - let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip'); + function hasDomainAndNumericRange(metadata) { + const rangeValues = metadata.valuesForHints(['range']); + const domains = metadata.valuesForHints(['domain']); - return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); - } + return ( + domains.length > 0 && + rangeValues.length > 0 && + !rangeValues.every((value) => value.format === 'string') + ); + } - return { - key: 'plot-single', - name: 'Plot', - cssClass: 'icon-telemetry', - canView(domainObject, objectPath) { - return hasNumericTelemetry(domainObject); - }, + function isCompactView(objectPath) { + let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip'); - view: function (domainObject, objectPath) { - let component; + return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); + } - return { - show: function (element) { - let isCompact = isCompactView(objectPath); - component = new Vue({ - el: element, - components: { - Plot - }, - provide: { - openmct, - domainObject, - path: objectPath - }, - data() { - return { - options: { - compact: isCompact - } - }; - }, - template: '' - }); - }, - getViewContext() { - if (!component) { - return {}; - } + return { + key: 'plot-single', + name: 'Plot', + cssClass: 'icon-telemetry', + canView(domainObject, objectPath) { + return hasNumericTelemetry(domainObject); + }, - return component.$refs.plotComponent.getViewContext(); - }, - destroy: function () { - component.$destroy(); - component = undefined; - }, - getComponent() { - return component; + view: function (domainObject, objectPath) { + let component; + + return { + show: function (element) { + let isCompact = isCompactView(objectPath); + component = new Vue({ + el: element, + components: { + Plot + }, + provide: { + openmct, + domainObject, + path: objectPath + }, + data() { + return { + options: { + compact: isCompact } - }; + }; + }, + template: '' + }); + }, + getViewContext() { + if (!component) { + return {}; + } + + return component.$refs.plotComponent.getViewContext(); + }, + destroy: function () { + component.$destroy(); + component = undefined; + }, + getComponent() { + return component; } - }; + }; + } + }; } diff --git a/src/plugins/plot/actions/ViewActions.js b/src/plugins/plot/actions/ViewActions.js index b038041af0..c036753015 100644 --- a/src/plugins/plot/actions/ViewActions.js +++ b/src/plugins/plot/actions/ViewActions.js @@ -19,39 +19,36 @@ * 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"; +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(); - } + 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(); - } + 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 -]; +const viewActions = [exportPNG, exportJPG]; -viewActions.forEach(action => { - action.appliesTo = (objectPath, view = {}) => { - return isPlotView(view); - }; +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 index 2bebbecf4d..8df92f9979 100644 --- a/src/plugins/plot/actions/utils.js +++ b/src/plugins/plot/actions/utils.js @@ -1,3 +1,3 @@ export function isPlotView(view) { - return view.key === 'plot-single' || view.key === 'plot-overlay' || view.key === 'plot-stacked'; + 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 267b2e3b05..2328494230 100644 --- a/src/plugins/plot/axis/XAxis.vue +++ b/src/plugins/plot/axis/XAxis.vue @@ -21,139 +21,125 @@ --> diff --git a/src/plugins/plot/axis/YAxis.vue b/src/plugins/plot/axis/YAxis.vue index bb1304e57a..b756db3c9f 100644 --- a/src/plugins/plot/axis/YAxis.vue +++ b/src/plugins/plot/axis/YAxis.vue @@ -20,262 +20,262 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/chart/LimitLabel.vue b/src/plugins/plot/chart/LimitLabel.vue index 2d00906324..5e67d891f7 100644 --- a/src/plugins/plot/chart/LimitLabel.vue +++ b/src/plugins/plot/chart/LimitLabel.vue @@ -20,55 +20,51 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/chart/LimitLine.vue b/src/plugins/plot/chart/LimitLine.vue index 777efaf082..4026a38fab 100644 --- a/src/plugins/plot/chart/LimitLine.vue +++ b/src/plugins/plot/chart/LimitLine.vue @@ -20,43 +20,39 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/chart/MCTChartAlarmLineSet.js b/src/plugins/plot/chart/MCTChartAlarmLineSet.js index 45e9f105e8..df87a6c0d8 100644 --- a/src/plugins/plot/chart/MCTChartAlarmLineSet.js +++ b/src/plugins/plot/chart/MCTChartAlarmLineSet.js @@ -23,97 +23,102 @@ import eventHelpers from '../lib/eventHelpers'; export default class MCTChartAlarmLineSet { - /** - * @param {Bounds} bounds - */ - constructor(series, chart, offset, bounds) { - this.series = series; - this.chart = chart; - this.offset = offset; - this.bounds = bounds; - this.limits = []; + /** + * @param {Bounds} bounds + */ + constructor(series, chart, offset, bounds) { + this.series = series; + this.chart = chart; + this.offset = offset; + this.bounds = bounds; + this.limits = []; - eventHelpers.extend(this); - this.listenTo(series, 'limitBounds', this.updateBounds, this); - this.listenTo(series, 'limits', this.getLimitPoints, this); - this.listenTo(series, 'change:xKey', this.getLimitPoints, this); + eventHelpers.extend(this); + this.listenTo(series, 'limitBounds', this.updateBounds, this); + this.listenTo(series, 'limits', this.getLimitPoints, this); + this.listenTo(series, 'change:xKey', this.getLimitPoints, this); - if (series.limits) { - this.getLimitPoints(series); - } + if (series.limits) { + this.getLimitPoints(series); + } + } + + /** + * @param {Bounds} bounds + */ + updateBounds(bounds) { + this.bounds = bounds; + this.getLimitPoints(this.series); + } + + color() { + return this.series.get('color'); + } + + name() { + return this.series.get('name'); + } + + makePoint(point, series) { + if (!this.offset.xVal) { + this.chart.setOffset(point, undefined, series); } - /** - * @param {Bounds} bounds - */ - updateBounds(bounds) { - this.bounds = bounds; - this.getLimitPoints(this.series); - } - - color() { - return this.series.get('color'); - } - - name() { - return this.series.get('name'); - } - - makePoint(point, series) { - if (!this.offset.xVal) { - this.chart.setOffset(point, undefined, series); - } - - return { - x: this.offset.xVal(point, series), - y: this.offset.yVal(point, series) - }; - } - - getLimitPoints(series) { - this.limits = []; - let xKey = series.get('xKey'); - Object.keys(series.limits).forEach((key) => { - const limitForLevel = series.limits[key]; - if (limitForLevel.high) { - this.limits.push({ - seriesKey: series.keyString, - level: key.toLowerCase(), - name: this.name(), - seriesColor: series.get('color').asHexString(), - point: this.makePoint(Object.assign({ [xKey]: this.bounds.start }, limitForLevel.high), series), - value: series.getYVal(limitForLevel.high), - color: limitForLevel.high.color, - isUpper: true - }); - } - - if (limitForLevel.low) { - this.limits.push({ - seriesKey: series.keyString, - level: key.toLowerCase(), - name: this.name(), - seriesColor: series.get('color').asHexString(), - point: this.makePoint(Object.assign({ [xKey]: this.bounds.start }, limitForLevel.low), series), - value: series.getYVal(limitForLevel.low), - color: limitForLevel.low.color, - isUpper: false - }); - } - }, this); - } - - reset() { - this.limits = []; - if (this.series.limits) { - this.getLimitPoints(this.series); - } - } - - destroy() { - this.stopListening(); + return { + x: this.offset.xVal(point, series), + y: this.offset.yVal(point, series) + }; + } + + getLimitPoints(series) { + this.limits = []; + let xKey = series.get('xKey'); + Object.keys(series.limits).forEach((key) => { + const limitForLevel = series.limits[key]; + if (limitForLevel.high) { + this.limits.push({ + seriesKey: series.keyString, + level: key.toLowerCase(), + name: this.name(), + seriesColor: series.get('color').asHexString(), + point: this.makePoint( + Object.assign({ [xKey]: this.bounds.start }, limitForLevel.high), + series + ), + value: series.getYVal(limitForLevel.high), + color: limitForLevel.high.color, + isUpper: true + }); + } + + if (limitForLevel.low) { + this.limits.push({ + seriesKey: series.keyString, + level: key.toLowerCase(), + name: this.name(), + seriesColor: series.get('color').asHexString(), + point: this.makePoint( + Object.assign({ [xKey]: this.bounds.start }, limitForLevel.low), + series + ), + value: series.getYVal(limitForLevel.low), + color: limitForLevel.low.color, + isUpper: false + }); + } + }, this); + } + + reset() { + this.limits = []; + if (this.series.limits) { + this.getLimitPoints(this.series); } + } + destroy() { + this.stopListening(); + } } /** diff --git a/src/plugins/plot/chart/MCTChartAlarmPointSet.js b/src/plugins/plot/chart/MCTChartAlarmPointSet.js index dd2f9cef09..8d26b29e51 100644 --- a/src/plugins/plot/chart/MCTChartAlarmPointSet.js +++ b/src/plugins/plot/chart/MCTChartAlarmPointSet.js @@ -23,45 +23,45 @@ import eventHelpers from '../lib/eventHelpers'; export default class MCTChartAlarmPointSet { - constructor(series, chart, offset) { - this.series = series; - this.chart = chart; - this.offset = offset; - this.points = []; + constructor(series, chart, offset) { + this.series = series; + this.chart = chart; + this.offset = offset; + this.points = []; - eventHelpers.extend(this); + eventHelpers.extend(this); - this.listenTo(series, 'add', this.append, this); - this.listenTo(series, 'remove', this.remove, this); - this.listenTo(series, 'reset', this.reset, this); - this.listenTo(series, 'destroy', this.destroy, this); + this.listenTo(series, 'add', this.append, this); + this.listenTo(series, 'remove', this.remove, this); + this.listenTo(series, 'reset', this.reset, this); + this.listenTo(series, 'destroy', this.destroy, this); - this.series.getSeriesData().forEach(function (point, index) { - this.append(point, index, series); - }, this); + this.series.getSeriesData().forEach(function (point, index) { + this.append(point, index, series); + }, this); + } + + append(datum) { + if (datum.mctLimitState) { + this.points.push({ + x: this.offset.xVal(datum, this.series), + y: this.offset.yVal(datum, this.series), + datum: datum + }); } + } - append(datum) { - if (datum.mctLimitState) { - this.points.push({ - x: this.offset.xVal(datum, this.series), - y: this.offset.yVal(datum, this.series), - datum: datum - }); - } - } + remove(datum) { + this.points = this.points.filter(function (p) { + return p.datum !== datum; + }); + } - remove(datum) { - this.points = this.points.filter(function (p) { - return p.datum !== datum; - }); - } + reset() { + this.points = []; + } - reset() { - this.points = []; - } - - destroy() { - this.stopListening(); - } + destroy() { + this.stopListening(); + } } diff --git a/src/plugins/plot/chart/MCTChartLineLinear.js b/src/plugins/plot/chart/MCTChartLineLinear.js index 1df5af7c92..32c8546077 100644 --- a/src/plugins/plot/chart/MCTChartLineLinear.js +++ b/src/plugins/plot/chart/MCTChartLineLinear.js @@ -23,9 +23,8 @@ import MCTChartSeriesElement from './MCTChartSeriesElement'; export default class MCTChartLineLinear extends MCTChartSeriesElement { - addPoint(point, start) { - this.buffer[start] = point.x; - this.buffer[start + 1] = point.y; - } + addPoint(point, start) { + this.buffer[start] = point.x; + this.buffer[start + 1] = point.y; + } } - diff --git a/src/plugins/plot/chart/MCTChartLineStepAfter.js b/src/plugins/plot/chart/MCTChartLineStepAfter.js index 0073819170..05a7d2c2fe 100644 --- a/src/plugins/plot/chart/MCTChartLineStepAfter.js +++ b/src/plugins/plot/chart/MCTChartLineStepAfter.js @@ -23,52 +23,51 @@ import MCTChartSeriesElement from './MCTChartSeriesElement'; export default class MCTChartLineStepAfter extends MCTChartSeriesElement { - removePoint(index) { - if (index > 0 && index / 2 < this.count) { - this.buffer[index + 1] = this.buffer[index - 1]; - } + removePoint(index) { + if (index > 0 && index / 2 < this.count) { + this.buffer[index + 1] = this.buffer[index - 1]; + } + } + + vertexCountForPointAtIndex(index) { + if (index === 0 && this.count === 0) { + return 2; } - vertexCountForPointAtIndex(index) { - if (index === 0 && this.count === 0) { - return 2; - } + return 4; + } - return 4; + startIndexForPointAtIndex(index) { + if (index === 0) { + return 0; } - startIndexForPointAtIndex(index) { - if (index === 0) { - return 0; - } + return 2 + (index - 1) * 4; + } - return 2 + ((index - 1) * 4); - } - - addPoint(point, start) { - if (start === 0 && this.count === 0) { - // First point is easy. - this.buffer[start] = point.x; - this.buffer[start + 1] = point.y; // one point - } else if (start === 0 && this.count > 0) { - // Unshifting requires adding an extra point. - this.buffer[start] = point.x; - this.buffer[start + 1] = point.y; - this.buffer[start + 2] = this.buffer[start + 4]; - this.buffer[start + 3] = point.y; - } else { - // Appending anywhere in line, insert standard two points. - this.buffer[start] = point.x; - this.buffer[start + 1] = this.buffer[start - 1]; - this.buffer[start + 2] = point.x; - this.buffer[start + 3] = point.y; - - if (start < this.count * 2) { - // Insert into the middle, need to update the following - // point. - this.buffer[start + 5] = point.y; - } - } + addPoint(point, start) { + if (start === 0 && this.count === 0) { + // First point is easy. + this.buffer[start] = point.x; + this.buffer[start + 1] = point.y; // one point + } else if (start === 0 && this.count > 0) { + // Unshifting requires adding an extra point. + this.buffer[start] = point.x; + this.buffer[start + 1] = point.y; + this.buffer[start + 2] = this.buffer[start + 4]; + this.buffer[start + 3] = point.y; + } else { + // Appending anywhere in line, insert standard two points. + this.buffer[start] = point.x; + this.buffer[start + 1] = this.buffer[start - 1]; + this.buffer[start + 2] = point.x; + this.buffer[start + 3] = point.y; + + if (start < this.count * 2) { + // Insert into the middle, need to update the following + // point. + this.buffer[start + 5] = point.y; + } } + } } - diff --git a/src/plugins/plot/chart/MCTChartPointSet.js b/src/plugins/plot/chart/MCTChartPointSet.js index 4c572b772d..fb04eb58f9 100644 --- a/src/plugins/plot/chart/MCTChartPointSet.js +++ b/src/plugins/plot/chart/MCTChartPointSet.js @@ -24,9 +24,8 @@ import MCTChartSeriesElement from './MCTChartSeriesElement'; // TODO: Is this needed? This is identical to MCTChartLineLinear. Why is it a different class? export default class MCTChartPointSet extends MCTChartSeriesElement { - addPoint(point, start) { - this.buffer[start] = point.x; - this.buffer[start + 1] = point.y; - } + addPoint(point, start) { + this.buffer[start] = point.x; + this.buffer[start + 1] = point.y; + } } - diff --git a/src/plugins/plot/chart/MCTChartSeriesElement.js b/src/plugins/plot/chart/MCTChartSeriesElement.js index e8557655ce..709f817e68 100644 --- a/src/plugins/plot/chart/MCTChartSeriesElement.js +++ b/src/plugins/plot/chart/MCTChartSeriesElement.js @@ -24,134 +24,133 @@ import eventHelpers from '../lib/eventHelpers'; /** @abstract */ export default class MCTChartSeriesElement { - constructor(series, chart, offset) { - this.series = series; - this.chart = chart; - this.offset = offset; - this.buffer = new Float32Array(20000); - this.count = 0; + constructor(series, chart, offset) { + this.series = series; + this.chart = chart; + this.offset = offset; + this.buffer = new Float32Array(20000); + this.count = 0; - eventHelpers.extend(this); + eventHelpers.extend(this); - this.listenTo(series, 'add', this.append, this); - this.listenTo(series, 'remove', this.remove, this); - this.listenTo(series, 'reset', this.reset, this); - this.listenTo(series, 'destroy', this.destroy, this); - this.series.getSeriesData().forEach(function (point, index) { - this.append(point, index, series); - }, this); + this.listenTo(series, 'add', this.append, this); + this.listenTo(series, 'remove', this.remove, this); + this.listenTo(series, 'reset', this.reset, this); + this.listenTo(series, 'destroy', this.destroy, this); + this.series.getSeriesData().forEach(function (point, index) { + this.append(point, index, series); + }, this); + } + + getBuffer() { + if (this.isTempBuffer) { + this.buffer = new Float32Array(this.buffer); + this.isTempBuffer = false; } - getBuffer() { - if (this.isTempBuffer) { - this.buffer = new Float32Array(this.buffer); - this.isTempBuffer = false; - } + return this.buffer; + } - return this.buffer; + color() { + return this.series.get('color'); + } + + vertexCountForPointAtIndex(index) { + return 2; + } + + startIndexForPointAtIndex(index) { + return 2 * index; + } + + removeSegments(index, count) { + const target = index; + const start = index + count; + const end = this.count * 2; + this.buffer.copyWithin(target, start, end); + for (let zero = end - count; zero < end; zero++) { + this.buffer[zero] = 0; + } + } + + /** @abstract */ + removePoint(index) {} + + /** @abstract */ + addPoint(point, index) {} + + remove(point, index, series) { + const vertexCount = this.vertexCountForPointAtIndex(index); + const removalPoint = this.startIndexForPointAtIndex(index); + + this.removeSegments(removalPoint, vertexCount); + + // TODO useless makePoint call? + this.makePoint(point, series); + this.removePoint(removalPoint); + + this.count -= vertexCount / 2; + } + + makePoint(point, series) { + if (!this.offset.xVal) { + this.chart.setOffset(point, undefined, series); } - color() { - return this.series.get('color'); + return { + x: this.offset.xVal(point, series), + y: this.offset.yVal(point, series) + }; + } + + append(point, index, series) { + const pointsRequired = this.vertexCountForPointAtIndex(index); + const insertionPoint = this.startIndexForPointAtIndex(index); + this.growIfNeeded(pointsRequired); + this.makeInsertionPoint(insertionPoint, pointsRequired); + this.addPoint(this.makePoint(point, series), insertionPoint); + this.count += pointsRequired / 2; + } + + makeInsertionPoint(insertionPoint, pointsRequired) { + if (this.count * 2 > insertionPoint) { + if (!this.isTempBuffer) { + this.buffer = Array.prototype.slice.apply(this.buffer); + this.isTempBuffer = true; + } + + const target = insertionPoint + pointsRequired; + let start = insertionPoint; + for (; start < target; start++) { + this.buffer.splice(start, 0, 0); + } } + } - vertexCountForPointAtIndex(index) { - return 2; + reset() { + this.buffer = new Float32Array(20000); + this.count = 0; + if (this.offset.x) { + this.series.getSeriesData().forEach(function (point, index) { + this.append(point, index, this.series); + }, this); } + } - startIndexForPointAtIndex(index) { - return 2 * index; - } - - removeSegments(index, count) { - const target = index; - const start = index + count; - const end = this.count * 2; - this.buffer.copyWithin(target, start, end); - for (let zero = end - count; zero < end; zero++) { - this.buffer[zero] = 0; - } - } - - /** @abstract */ - removePoint(index) {} - - /** @abstract */ - addPoint(point, index) {} - - remove(point, index, series) { - const vertexCount = this.vertexCountForPointAtIndex(index); - const removalPoint = this.startIndexForPointAtIndex(index); - - this.removeSegments(removalPoint, vertexCount); - - // TODO useless makePoint call? - this.makePoint(point, series); - this.removePoint(removalPoint); - - this.count -= (vertexCount / 2); - } - - makePoint(point, series) { - if (!this.offset.xVal) { - this.chart.setOffset(point, undefined, series); - } - - return { - x: this.offset.xVal(point, series), - y: this.offset.yVal(point, series) - }; - } - - append(point, index, series) { - const pointsRequired = this.vertexCountForPointAtIndex(index); - const insertionPoint = this.startIndexForPointAtIndex(index); - this.growIfNeeded(pointsRequired); - this.makeInsertionPoint(insertionPoint, pointsRequired); - this.addPoint(this.makePoint(point, series), insertionPoint); - this.count += (pointsRequired / 2); - } - - makeInsertionPoint(insertionPoint, pointsRequired) { - if (this.count * 2 > insertionPoint) { - if (!this.isTempBuffer) { - this.buffer = Array.prototype.slice.apply(this.buffer); - this.isTempBuffer = true; - } - - const target = insertionPoint + pointsRequired; - let start = insertionPoint; - for (; start < target; start++) { - this.buffer.splice(start, 0, 0); - } - } - } - - reset() { - this.buffer = new Float32Array(20000); - this.count = 0; - if (this.offset.x) { - this.series.getSeriesData().forEach(function (point, index) { - this.append(point, index, this.series); - }, this); - } - } - - growIfNeeded(pointsRequired) { - const remainingPoints = this.buffer.length - this.count * 2; - let temp; - - if (remainingPoints <= pointsRequired) { - temp = new Float32Array(this.buffer.length + 20000); - temp.set(this.buffer); - this.buffer = temp; - } - } - - destroy() { - this.stopListening(); + growIfNeeded(pointsRequired) { + const remainingPoints = this.buffer.length - this.count * 2; + let temp; + + if (remainingPoints <= pointsRequired) { + temp = new Float32Array(this.buffer.length + 20000); + temp.set(this.buffer); + this.buffer = temp; } + } + destroy() { + this.stopListening(); + } } /** @typedef {any} TODO */ diff --git a/src/plugins/plot/chart/MctChart.vue b/src/plugins/plot/chart/MctChart.vue index 24e961f096..a4bc9332fb 100644 --- a/src/plugins/plot/chart/MctChart.vue +++ b/src/plugins/plot/chart/MctChart.vue @@ -23,29 +23,25 @@ diff --git a/src/plugins/plot/chart/limitUtil.js b/src/plugins/plot/chart/limitUtil.js index 6ca4df4482..ffe82ce38a 100644 --- a/src/plugins/plot/chart/limitUtil.js +++ b/src/plugins/plot/chart/limitUtil.js @@ -1,32 +1,32 @@ export function getLimitClass(limit, prefix) { - let cssClass = ''; - //If color exists then use it, fall back to the cssClass - if (limit.color) { - cssClass = `${cssClass} ${prefix}${limit.color}`; - } else if (limit.cssClass) { - cssClass = `${cssClass}${limit.cssClass}`; + let cssClass = ''; + //If color exists then use it, fall back to the cssClass + if (limit.color) { + cssClass = `${cssClass} ${prefix}${limit.color}`; + } else if (limit.cssClass) { + cssClass = `${cssClass}${limit.cssClass}`; + } + + // If we applied the cssClass then skip these classes + if (limit.cssClass === undefined) { + if (limit.isUpper) { + cssClass = `${cssClass} ${prefix}upr`; + } else { + cssClass = `${cssClass} ${prefix}lwr`; } - // If we applied the cssClass then skip these classes - if (limit.cssClass === undefined) { - if (limit.isUpper) { - cssClass = `${cssClass} ${prefix}upr`; - } else { - cssClass = `${cssClass} ${prefix}lwr`; - } - - if (limit.level) { - cssClass = `${cssClass} ${prefix}${limit.level}`; - } - - if (limit.needsHorizontalAdjustment) { - cssClass = `${cssClass} --align-label-right`; - } - - if (limit.needsVerticalAdjustment) { - cssClass = `${cssClass} --align-label-below`; - } + if (limit.level) { + cssClass = `${cssClass} ${prefix}${limit.level}`; } - return cssClass; + if (limit.needsHorizontalAdjustment) { + cssClass = `${cssClass} --align-label-right`; + } + + if (limit.needsVerticalAdjustment) { + cssClass = `${cssClass} --align-label-below`; + } + } + + return cssClass; } diff --git a/src/plugins/plot/configuration/Collection.js b/src/plugins/plot/configuration/Collection.js index 03f93ec9e1..d46ae0adf2 100644 --- a/src/plugins/plot/configuration/Collection.js +++ b/src/plugins/plot/configuration/Collection.js @@ -27,91 +27,90 @@ import Model from './Model'; * @extends {Model} */ export default class Collection extends Model { - /** @type {Constructor} */ - modelClass = Model; + /** @type {Constructor} */ + modelClass = Model; - initialize(options) { - super.initialize(options); - if (options.models) { - this.models = options.models.map(this.modelFn, this); - } else { - this.models = []; - } + initialize(options) { + super.initialize(options); + if (options.models) { + this.models = options.models.map(this.modelFn, this); + } else { + this.models = []; + } + } + + modelFn(model) { + //TODO: Come back to this - why are we doing this? + if (model instanceof this.modelClass) { + model.collection = this; + + return model; } - modelFn(model) { - //TODO: Come back to this - why are we doing this? - if (model instanceof this.modelClass) { - model.collection = this; + return new this.modelClass({ + collection: this, + model: model + }); + } - return model; + first() { + return this.at(0); + } - } + forEach(iteree, context) { + this.models.forEach(iteree, context); + } - return new this.modelClass({ - collection: this, - model: model - }); + map(iteree, context) { + return this.models.map(iteree, context); + } + + filter(iteree, context) { + return this.models.filter(iteree, context); + } + + size() { + return this.models.length; + } + + at(index) { + return this.models[index]; + } + + add(model) { + model = this.modelFn(model); + const index = this.models.length; + this.models.push(model); + this.emit('add', model, index); + } + + insert(model, index) { + model = this.modelFn(model); + this.models.splice(index, 0, model); + this.emit('add', model, index + 1); + } + + indexOf(model) { + return this.models.findIndex((m) => m === model); + } + + remove(model) { + const index = this.indexOf(model); + + if (index === -1) { + throw new Error('model not found in collection.'); } - first() { - return this.at(0); - } + this.models.splice(index, 1); + this.emit('remove', model, index); + } - forEach(iteree, context) { - this.models.forEach(iteree, context); - } - - map(iteree, context) { - return this.models.map(iteree, context); - } - - filter(iteree, context) { - return this.models.filter(iteree, context); - } - - size() { - return this.models.length; - } - - at(index) { - return this.models[index]; - } - - add(model) { - model = this.modelFn(model); - const index = this.models.length; - this.models.push(model); - this.emit('add', model, index); - } - - insert(model, index) { - model = this.modelFn(model); - this.models.splice(index, 0, model); - this.emit('add', model, index + 1); - } - - indexOf(model) { - return this.models.findIndex(m => m === model); - } - - remove(model) { - const index = this.indexOf(model); - - if (index === -1) { - throw new Error('model not found in collection.'); - } - - this.models.splice(index, 1); - this.emit('remove', model, index); - } - - destroy(model) { - this.forEach(function (m) { - m.destroy(); - }); - this.stopListening(); - } + destroy(model) { + this.forEach(function (m) { + m.destroy(); + }); + this.stopListening(); + } } /** @typedef {any} TODO */ diff --git a/src/plugins/plot/configuration/ConfigStore.js b/src/plugins/plot/configuration/ConfigStore.js index c47a5edbdb..02dc354be5 100644 --- a/src/plugins/plot/configuration/ConfigStore.js +++ b/src/plugins/plot/configuration/ConfigStore.js @@ -20,42 +20,42 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ class ConfigStore { - /** @type {Record} */ - store = {}; + /** @type {Record} */ + store = {}; - /** + /** @param {string} id */ - deleteStore(id) { - const obj = this.store[id]; + deleteStore(id) { + const obj = this.store[id]; - if (obj) { - if (obj.destroy) { - obj.destroy(); - } + if (obj) { + if (obj.destroy) { + obj.destroy(); + } - delete this.store[id]; - } + delete this.store[id]; } + } - deleteAll() { - Object.keys(this.store).forEach(id => this.deleteStore(id)); - } + deleteAll() { + Object.keys(this.store).forEach((id) => this.deleteStore(id)); + } - /** + /** @param {string} id @param {any} config */ - add(id, config) { - this.store[id] = config; - } + add(id, config) { + this.store[id] = config; + } - /** + /** @param {string} id */ - get(id) { - return this.store[id]; - } + get(id) { + return this.store[id]; + } } const STORE = new ConfigStore(); diff --git a/src/plugins/plot/configuration/LegendModel.js b/src/plugins/plot/configuration/LegendModel.js index e145bffee8..d8da5a500c 100644 --- a/src/plugins/plot/configuration/LegendModel.js +++ b/src/plugins/plot/configuration/LegendModel.js @@ -20,42 +20,42 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import Model from "./Model"; +import Model from './Model'; /** * TODO: doc strings. */ export default class LegendModel extends Model { - listenToSeriesCollection(seriesCollection) { - this.seriesCollection = seriesCollection; - this.listenTo(this.seriesCollection, 'add', this.setHeight, this); - this.listenTo(this.seriesCollection, 'remove', this.setHeight, this); - this.listenTo(this, 'change:expanded', this.setHeight, this); - this.set('expanded', this.get('expandByDefault')); - } + listenToSeriesCollection(seriesCollection) { + this.seriesCollection = seriesCollection; + this.listenTo(this.seriesCollection, 'add', this.setHeight, this); + this.listenTo(this.seriesCollection, 'remove', this.setHeight, this); + this.listenTo(this, 'change:expanded', this.setHeight, this); + this.set('expanded', this.get('expandByDefault')); + } - setHeight() { - const expanded = this.get('expanded'); - if (this.get('position') !== 'top') { - this.set('height', '0px'); - } else { - this.set('height', expanded ? (20 * (this.seriesCollection.size() + 1) + 40) + 'px' : '21px'); - } + setHeight() { + const expanded = this.get('expanded'); + if (this.get('position') !== 'top') { + this.set('height', '0px'); + } else { + this.set('height', expanded ? 20 * (this.seriesCollection.size() + 1) + 40 + 'px' : '21px'); } + } - /** - * @override - */ - defaultModel(options) { - return { - position: 'top', - expandByDefault: false, - hideLegendWhenSmall: false, - valueToShowWhenCollapsed: 'nearestValue', - showTimestampWhenExpanded: true, - showValueWhenExpanded: true, - showMaximumWhenExpanded: true, - showMinimumWhenExpanded: true, - showUnitsWhenExpanded: true - }; - } + /** + * @override + */ + defaultModel(options) { + return { + position: 'top', + expandByDefault: false, + hideLegendWhenSmall: false, + valueToShowWhenCollapsed: 'nearestValue', + showTimestampWhenExpanded: true, + showValueWhenExpanded: true, + showMaximumWhenExpanded: true, + showMinimumWhenExpanded: true, + showUnitsWhenExpanded: true + }; + } } diff --git a/src/plugins/plot/configuration/Model.js b/src/plugins/plot/configuration/Model.js index bd733764c4..428a2ecde5 100644 --- a/src/plugins/plot/configuration/Model.js +++ b/src/plugins/plot/configuration/Model.js @@ -21,7 +21,7 @@ *****************************************************************************/ import EventEmitter from 'eventemitter3'; -import eventHelpers from "../lib/eventHelpers"; +import eventHelpers from '../lib/eventHelpers'; import _ from 'lodash'; /** @@ -29,113 +29,111 @@ import _ from 'lodash'; * @template {object} O */ export default class Model extends EventEmitter { - /** - * @param {ModelOptions} options - */ - constructor(options) { - super(); - Object.defineProperty(this, '_events', { - value: this._events, - enumerable: false, - configurable: false, - writable: true - }); + /** + * @param {ModelOptions} options + */ + 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); + //need to do this as we're already extending EventEmitter + eventHelpers.extend(this); - if (!options) { - options = {}; - } - - // FIXME: this.id is defined as a method further below, but here it is - // assigned a possibly-undefined value. Is this code unused? - this.id = options.id; - - /** @type {ModelType} */ - this.model = options.model; - this.collection = options.collection; - const defaults = this.defaultModel(options); - if (!this.model) { - this.model = options.model = defaults; - } else { - _.defaultsDeep(this.model, defaults); - } - - this.initialize(options); - - /** @type {keyof ModelType } */ - this.idAttr = 'id'; + if (!options) { + options = {}; } - /** - * @param {ModelOptions} options - * @returns {ModelType} - */ - defaultModel(options) { - return {}; + // FIXME: this.id is defined as a method further below, but here it is + // assigned a possibly-undefined value. Is this code unused? + this.id = options.id; + + /** @type {ModelType} */ + this.model = options.model; + this.collection = options.collection; + const defaults = this.defaultModel(options); + if (!this.model) { + this.model = options.model = defaults; + } else { + _.defaultsDeep(this.model, defaults); } - /** - * @abstract - * @param {ModelOptions} options - */ - initialize(options) { + this.initialize(options); - } + /** @type {keyof ModelType } */ + this.idAttr = 'id'; + } - /** - * Destroy the model, removing all listeners and subscriptions. - */ - destroy() { - this.emit('destroy'); - this.removeAllListeners(); - } + /** + * @param {ModelOptions} options + * @returns {ModelType} + */ + defaultModel(options) { + return {}; + } - id() { - return this.get(this.idAttr); - } + /** + * @abstract + * @param {ModelOptions} options + */ + initialize(options) {} - /** - * @template {keyof ModelType} K - * @param {K} attribute - * @returns {ModelType[K]} - */ - get(attribute) { - return this.model[attribute]; - } + /** + * Destroy the model, removing all listeners and subscriptions. + */ + destroy() { + this.emit('destroy'); + this.removeAllListeners(); + } - /** - * @template {keyof ModelType} K - * @param {K} attribute - * @returns boolean - */ - has(attribute) { - return _.has(this.model, attribute); - } + id() { + return this.get(this.idAttr); + } - /** - * @template {keyof ModelType} K - * @param {K} attribute - * @param {ModelType[K]} value - */ - set(attribute, value) { - const oldValue = this.model[attribute]; - this.model[attribute] = value; - this.emit('change', attribute, value, oldValue, this); - this.emit('change:' + attribute, value, oldValue, this); - } + /** + * @template {keyof ModelType} K + * @param {K} attribute + * @returns {ModelType[K]} + */ + get(attribute) { + return this.model[attribute]; + } - /** - * @template {keyof ModelType} K - * @param {K} attribute - */ - unset(attribute) { - const oldValue = this.model[attribute]; - delete this.model[attribute]; - this.emit('change', attribute, undefined, oldValue, this); - this.emit('change:' + attribute, undefined, oldValue, this); - } + /** + * @template {keyof ModelType} K + * @param {K} attribute + * @returns boolean + */ + has(attribute) { + return _.has(this.model, attribute); + } + + /** + * @template {keyof ModelType} K + * @param {K} attribute + * @param {ModelType[K]} value + */ + set(attribute, value) { + const oldValue = this.model[attribute]; + this.model[attribute] = value; + this.emit('change', attribute, value, oldValue, this); + this.emit('change:' + attribute, value, oldValue, this); + } + + /** + * @template {keyof ModelType} K + * @param {K} attribute + */ + unset(attribute) { + const oldValue = this.model[attribute]; + delete this.model[attribute]; + this.emit('change', attribute, undefined, oldValue, this); + this.emit('change:' + attribute, undefined, oldValue, this); + } } /** @typedef {any} TODO */ diff --git a/src/plugins/plot/configuration/PlotConfigurationModel.js b/src/plugins/plot/configuration/PlotConfigurationModel.js index bb67f6272a..87a0922a50 100644 --- a/src/plugins/plot/configuration/PlotConfigurationModel.js +++ b/src/plugins/plot/configuration/PlotConfigurationModel.js @@ -21,11 +21,11 @@ *****************************************************************************/ import _ from 'lodash'; -import Model from "./Model"; -import SeriesCollection from "./SeriesCollection"; -import XAxisModel from "./XAxisModel"; -import YAxisModel from "./YAxisModel"; -import LegendModel from "./LegendModel"; +import Model from './Model'; +import SeriesCollection from './SeriesCollection'; +import XAxisModel from './XAxisModel'; +import YAxisModel from './YAxisModel'; +import LegendModel from './LegendModel'; const MAX_Y_AXES = 3; const MAIN_Y_AXES_ID = 1; @@ -39,149 +39,157 @@ const MAX_ADDITIONAL_AXES = MAX_Y_AXES - 1; * @extends {Model} */ export default class PlotConfigurationModel extends Model { - /** - * Initializes all sub models and then passes references to submodels - * to those that need it. - * - * @override - * @param {import('./Model').ModelOptions} options - */ - initialize(options) { - this.openmct = options.openmct; + /** + * Initializes all sub models and then passes references to submodels + * to those that need it. + * + * @override + * @param {import('./Model').ModelOptions} options + */ + initialize(options) { + this.openmct = options.openmct; - // This is a type assertion for TypeScript, this error is never thrown in practice. - if (!options.model) { - throw new Error('Not a collection model.'); - } + // This is a type assertion for TypeScript, this error is never thrown in practice. + if (!options.model) { + throw new Error('Not a collection model.'); + } - this.xAxis = new XAxisModel({ - model: options.model.xAxis, - plot: this, - openmct: options.openmct - }); - this.yAxis = new YAxisModel({ - model: options.model.yAxis, + this.xAxis = new XAxisModel({ + model: options.model.xAxis, + plot: this, + openmct: options.openmct + }); + this.yAxis = new YAxisModel({ + model: options.model.yAxis, + plot: this, + openmct: options.openmct, + id: options.model.yAxis.id || MAIN_Y_AXES_ID + }); + //Add any axes in addition to the main yAxis above - we must always have at least 1 y-axis + //Addition axes ids will be the MAIN_Y_AXES_ID + x where x is between 1 and MAX_ADDITIONAL_AXES + this.additionalYAxes = []; + const hasAdditionalAxesConfiguration = Array.isArray(options.model.additionalYAxes); + + for (let yAxisCount = 0; yAxisCount < MAX_ADDITIONAL_AXES; yAxisCount++) { + const yAxisId = MAIN_Y_AXES_ID + yAxisCount + 1; + const yAxis = + hasAdditionalAxesConfiguration && + options.model.additionalYAxes.find((additionalYAxis) => additionalYAxis?.id === yAxisId); + if (yAxis) { + this.additionalYAxes.push( + new YAxisModel({ + model: yAxis, plot: this, openmct: options.openmct, - id: options.model.yAxis.id || MAIN_Y_AXES_ID - }); - //Add any axes in addition to the main yAxis above - we must always have at least 1 y-axis - //Addition axes ids will be the MAIN_Y_AXES_ID + x where x is between 1 and MAX_ADDITIONAL_AXES - this.additionalYAxes = []; - const hasAdditionalAxesConfiguration = Array.isArray(options.model.additionalYAxes); - - for (let yAxisCount = 0; yAxisCount < MAX_ADDITIONAL_AXES; yAxisCount++) { - const yAxisId = MAIN_Y_AXES_ID + yAxisCount + 1; - const yAxis = hasAdditionalAxesConfiguration && options.model.additionalYAxes.find(additionalYAxis => additionalYAxis?.id === yAxisId); - if (yAxis) { - this.additionalYAxes.push(new YAxisModel({ - model: yAxis, - plot: this, - openmct: options.openmct, - id: yAxis.id - })); - } else { - this.additionalYAxes.push(new YAxisModel({ - plot: this, - openmct: options.openmct, - id: yAxisId - })); - } - } - // end add additional axes - - this.legend = new LegendModel({ - model: options.model.legend, - plot: this, - openmct: options.openmct - }); - this.series = new SeriesCollection({ - models: options.model.series, + id: yAxis.id + }) + ); + } else { + this.additionalYAxes.push( + new YAxisModel({ plot: this, openmct: options.openmct, - palette: options.palette - }); - - if (this.get('domainObject').type === 'telemetry.plot.overlay') { - this.removeMutationListener = this.openmct.objects.observe( - this.get('domainObject'), - '*', - this.updateDomainObject.bind(this) - ); - } - - this.yAxis.listenToSeriesCollection(this.series); - this.additionalYAxes.forEach(yAxis => { - yAxis.listenToSeriesCollection(this.series); - }); - this.legend.listenToSeriesCollection(this.series); - - this.listenTo(this, 'destroy', this.onDestroy, this); + id: yAxisId + }) + ); + } } - /** - * Retrieve the persisted series config for a given identifier. - * @param {import('./PlotSeries').Identifier} identifier - * @returns {import('./PlotSeries').PlotSeriesModelType=} - */ - getPersistedSeriesConfig(identifier) { - const domainObject = this.get('domainObject'); - if (!domainObject.configuration || !domainObject.configuration.series) { - return; - } + // end add additional axes - return domainObject.configuration.series.filter(function (seriesConfig) { - return seriesConfig.identifier.key === identifier.key - && seriesConfig.identifier.namespace === identifier.namespace; - })[0]; - } - /** - * Retrieve the persisted filters for a given identifier. - */ - getPersistedFilters(identifier) { - const domainObject = this.get('domainObject'); - const keystring = this.openmct.objects.makeKeyString(identifier); + this.legend = new LegendModel({ + model: options.model.legend, + plot: this, + openmct: options.openmct + }); + this.series = new SeriesCollection({ + models: options.model.series, + plot: this, + openmct: options.openmct, + palette: options.palette + }); - if (!domainObject.configuration || !domainObject.configuration.filters) { - return; - } - - return domainObject.configuration.filters[keystring]; - } - /** - * Update the domain object with the given value. - */ - updateDomainObject(domainObject) { - this.set('domainObject', domainObject); + if (this.get('domainObject').type === 'telemetry.plot.overlay') { + this.removeMutationListener = this.openmct.objects.observe( + this.get('domainObject'), + '*', + this.updateDomainObject.bind(this) + ); } - /** - * Clean up all objects and remove all listeners. - */ - onDestroy() { - this.xAxis.destroy(); - this.yAxis.destroy(); - this.series.destroy(); - this.legend.destroy(); - if (this.removeMutationListener) { - this.removeMutationListener(); - } + this.yAxis.listenToSeriesCollection(this.series); + this.additionalYAxes.forEach((yAxis) => { + yAxis.listenToSeriesCollection(this.series); + }); + this.legend.listenToSeriesCollection(this.series); + + this.listenTo(this, 'destroy', this.onDestroy, this); + } + /** + * Retrieve the persisted series config for a given identifier. + * @param {import('./PlotSeries').Identifier} identifier + * @returns {import('./PlotSeries').PlotSeriesModelType=} + */ + getPersistedSeriesConfig(identifier) { + const domainObject = this.get('domainObject'); + if (!domainObject.configuration || !domainObject.configuration.series) { + return; } - /** - * Return defaults, which are extracted from the passed in domain - * object. - * @override - * @param {import('./Model').ModelOptions} options - */ - defaultModel(options) { - return { - series: [], - domainObject: options.domainObject, - xAxis: {}, - yAxis: _.cloneDeep(options.domainObject.configuration?.yAxis ?? {}), - additionalYAxes: _.cloneDeep(options.domainObject.configuration?.additionalYAxes ?? []), - legend: _.cloneDeep(options.domainObject.configuration?.legend ?? {}) - }; + + return domainObject.configuration.series.filter(function (seriesConfig) { + return ( + seriesConfig.identifier.key === identifier.key && + seriesConfig.identifier.namespace === identifier.namespace + ); + })[0]; + } + /** + * Retrieve the persisted filters for a given identifier. + */ + getPersistedFilters(identifier) { + const domainObject = this.get('domainObject'); + const keystring = this.openmct.objects.makeKeyString(identifier); + + if (!domainObject.configuration || !domainObject.configuration.filters) { + return; } + + return domainObject.configuration.filters[keystring]; + } + /** + * Update the domain object with the given value. + */ + updateDomainObject(domainObject) { + this.set('domainObject', domainObject); + } + + /** + * Clean up all objects and remove all listeners. + */ + onDestroy() { + this.xAxis.destroy(); + this.yAxis.destroy(); + this.series.destroy(); + this.legend.destroy(); + if (this.removeMutationListener) { + this.removeMutationListener(); + } + } + /** + * Return defaults, which are extracted from the passed in domain + * object. + * @override + * @param {import('./Model').ModelOptions} options + */ + defaultModel(options) { + return { + series: [], + domainObject: options.domainObject, + xAxis: {}, + yAxis: _.cloneDeep(options.domainObject.configuration?.yAxis ?? {}), + additionalYAxes: _.cloneDeep(options.domainObject.configuration?.additionalYAxes ?? []), + legend: _.cloneDeep(options.domainObject.configuration?.legend ?? {}) + }; + } } /** @typedef {any} TODO */ diff --git a/src/plugins/plot/configuration/PlotSeries.js b/src/plugins/plot/configuration/PlotSeries.js index c7e2e14ee7..4f3cd30a9e 100644 --- a/src/plugins/plot/configuration/PlotSeries.js +++ b/src/plugins/plot/configuration/PlotSeries.js @@ -20,9 +20,9 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ import _ from 'lodash'; -import Model from "./Model"; +import Model from './Model'; import { MARKER_SHAPES } from '../draw/MarkerShapes'; -import configStore from "../configuration/ConfigStore"; +import configStore from '../configuration/ConfigStore'; import { symlog } from '../mathUtils'; /** @@ -64,487 +64,475 @@ import { symlog } from '../mathUtils'; * @extends {Model} */ export default class PlotSeries extends Model { - logMode = false; + logMode = false; - /** + /** @param {import('./Model').ModelOptions} options */ - constructor(options) { + constructor(options) { + super(options); - super(options); + this.logMode = this.getLogMode(options); - this.logMode = this.getLogMode(options); + this.listenTo(this, 'change:xKey', this.onXKeyChange, this); + this.listenTo(this, 'change:yKey', this.onYKeyChange, this); + this.persistedConfig = options.persistedConfig; + this.filters = options.filters; - this.listenTo(this, 'change:xKey', this.onXKeyChange, this); - this.listenTo(this, 'change:yKey', this.onYKeyChange, this); - this.persistedConfig = options.persistedConfig; - this.filters = options.filters; + // Model.apply(this, arguments); + this.onXKeyChange(this.get('xKey')); + this.onYKeyChange(this.get('yKey')); - // Model.apply(this, arguments); - this.onXKeyChange(this.get('xKey')); - this.onYKeyChange(this.get('yKey')); + this.unPlottableValues = [undefined, Infinity, -Infinity]; + } - this.unPlottableValues = [undefined, Infinity, -Infinity]; + getLogMode(options) { + const yAxisId = this.get('yAxisId'); + if (yAxisId === 1) { + return options.collection.plot.model.yAxis.logMode; + } else { + const foundYAxis = options.collection.plot.model.additionalYAxes.find( + (yAxis) => yAxis.id === yAxisId + ); + + return foundYAxis ? foundYAxis.logMode : false; + } + } + + /** + * Set defaults for telemetry series. + * @param {import('./Model').ModelOptions} options + * @override + */ + defaultModel(options) { + this.metadata = options.openmct.telemetry.getMetadata(options.domainObject); + + this.formats = options.openmct.telemetry.getFormatMap(this.metadata); + + //if the object is missing or doesn't have metadata for some reason + let range = {}; + if (this.metadata) { + range = this.metadata.valuesForHints(['range'])[0]; } - getLogMode(options) { - const yAxisId = this.get('yAxisId'); - if (yAxisId === 1) { - return options.collection.plot.model.yAxis.logMode; - } else { - const foundYAxis = options.collection.plot.model.additionalYAxes.find(yAxis => yAxis.id === yAxisId); + return { + name: options.domainObject.name, + unit: range.unit, + xKey: options.collection.plot.xAxis.get('key'), + yKey: range.key, + markers: true, + markerShape: 'point', + markerSize: 2.0, + alarmMarkers: true, + limitLines: false, + yAxisId: options.model.yAxisId || 1 + }; + } - return foundYAxis ? foundYAxis.logMode : false; - } + /** + * Remove real-time subscription when destroyed. + * @override + */ + destroy() { + super.destroy(); + this.openmct.time.off('bounds', this.updateLimits); + + if (this.unsubscribe) { + this.unsubscribe(); } - /** - * Set defaults for telemetry series. - * @param {import('./Model').ModelOptions} options - * @override - */ - defaultModel(options) { - this.metadata = options - .openmct - .telemetry - .getMetadata(options.domainObject); + if (this.removeMutationListener) { + this.removeMutationListener(); + } + } - this.formats = options - .openmct - .telemetry - .getFormatMap(this.metadata); + /** + * Set defaults for telemetry series. + * @override + * @param {import('./Model').ModelOptions} options + */ + initialize(options) { + this.openmct = options.openmct; + this.domainObject = options.domainObject; + this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); + this.dataStoreId = `data-${options.collection.plot.id}-${this.keyString}`; + this.updateSeriesData([]); + this.limitEvaluator = this.openmct.telemetry.limitEvaluator(options.domainObject); + this.limitDefinition = this.openmct.telemetry.limitDefinition(options.domainObject); + this.limits = []; + this.openmct.time.on('bounds', this.updateLimits); + this.removeMutationListener = this.openmct.objects.observe( + this.domainObject, + 'name', + this.updateName.bind(this) + ); + } - //if the object is missing or doesn't have metadata for some reason - let range = {}; - if (this.metadata) { - range = this.metadata.valuesForHints(['range'])[0]; - } + /** + * @param {Bounds} bounds + */ + updateLimits(bounds) { + this.emit('limitBounds', bounds); + } - return { - name: options.domainObject.name, - unit: range.unit, - xKey: options.collection.plot.xAxis.get('key'), - yKey: range.key, - markers: true, - markerShape: 'point', - markerSize: 2.0, - alarmMarkers: true, - limitLines: false, - yAxisId: options.model.yAxisId || 1 - }; + /** + * Fetch historical data and establish a realtime subscription. Returns + * a promise that is resolved when all connections have been successfully + * established. + * + * @returns {Promise} + */ + async fetch(options) { + let strategy; + + if (this.model.interpolate !== 'none') { + strategy = 'minmax'; } - /** - * Remove real-time subscription when destroyed. - * @override - */ - destroy() { - super.destroy(); - this.openmct.time.off('bounds', this.updateLimits); + options = Object.assign( + {}, + { + size: 1000, + strategy, + filters: this.filters + }, + options || {} + ); - if (this.unsubscribe) { - this.unsubscribe(); - } - - if (this.removeMutationListener) { - this.removeMutationListener(); - } + if (!this.unsubscribe) { + this.unsubscribe = this.openmct.telemetry.subscribe(this.domainObject, this.add.bind(this), { + filters: this.filters + }); } - /** - * Set defaults for telemetry series. - * @override - * @param {import('./Model').ModelOptions} options - */ - initialize(options) { - this.openmct = options.openmct; - this.domainObject = options.domainObject; - this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); - this.dataStoreId = `data-${options.collection.plot.id}-${this.keyString}`; - this.updateSeriesData([]); - this.limitEvaluator = this.openmct.telemetry.limitEvaluator(options.domainObject); - this.limitDefinition = this.openmct.telemetry.limitDefinition(options.domainObject); - this.limits = []; - this.openmct.time.on('bounds', this.updateLimits); - this.removeMutationListener = this.openmct.objects.observe( - this.domainObject, - 'name', - this.updateName.bind(this) - ); + try { + const points = await this.openmct.telemetry.request(this.domainObject, options); + const data = this.getSeriesData(); + // eslint-disable-next-line you-dont-need-lodash-underscore/concat + const newPoints = _(data) + .concat(points) + .sortBy(this.getXVal) + .uniq(true, (point) => [this.getXVal(point), this.getYVal(point)].join()) + .value(); + this.reset(newPoints); + } catch (error) { + console.warn('Error fetching data', error); + } + } + + updateName(name) { + if (name !== this.get('name')) { + this.set('name', name); + } + } + /** + * Update x formatter on x change. + */ + onXKeyChange(xKey) { + const format = this.formats[xKey]; + if (format) { + this.getXVal = format.parse.bind(format); + } + } + + /** + * Update y formatter on change, default to stepAfter interpolation if + * y range is an enumeration. + */ + onYKeyChange(newKey, oldKey) { + if (newKey === oldKey) { + return; } - /** - * @param {Bounds} bounds - */ - updateLimits(bounds) { - this.emit('limitBounds', bounds); + const valueMetadata = this.metadata.value(newKey); + //TODO: Should we do this even if there is a persisted config? + if (!this.persistedConfig || !this.persistedConfig.interpolate) { + if (valueMetadata.format === 'enum') { + this.set('interpolate', 'stepAfter'); + } else { + this.set('interpolate', 'linear'); + } } - /** - * Fetch historical data and establish a realtime subscription. Returns - * a promise that is resolved when all connections have been successfully - * established. - * - * @returns {Promise} - */ - async fetch(options) { - let strategy; + this.evaluate = function (datum) { + return this.limitEvaluator.evaluate(datum, valueMetadata); + }.bind(this); + this.set('unit', valueMetadata.unit); + const format = this.formats[newKey]; + this.getYVal = (value) => { + const y = format.parse(value); - if (this.model.interpolate !== 'none') { - strategy = 'minmax'; - } + return this.logMode ? symlog(y, 10) : y; + }; + } - options = Object.assign({}, { - size: 1000, - strategy, - filters: this.filters - }, options || {}); + formatX(point) { + return this.formats[this.get('xKey')].format(point); + } - if (!this.unsubscribe) { - this.unsubscribe = this.openmct - .telemetry - .subscribe( - this.domainObject, - this.add.bind(this), - { - filters: this.filters - } - ); - } + formatY(point) { + return this.formats[this.get('yKey')].format(point); + } - try { - const points = await this.openmct.telemetry.request(this.domainObject, options); - const data = this.getSeriesData(); - // eslint-disable-next-line you-dont-need-lodash-underscore/concat - const newPoints = _(data) - .concat(points) - .sortBy(this.getXVal) - .uniq(true, point => [this.getXVal(point), this.getYVal(point)].join()) - .value(); - this.reset(newPoints); - } catch (error) { - console.warn('Error fetching data', error); - } + /** + * Clear stats and recalculate from existing data. + */ + resetStats() { + this.unset('stats'); + this.getSeriesData().forEach(this.updateStats, this); + } + + /** + * Reset plot series. If new data is provided, will add that + * data to series after reset. + */ + reset(newData) { + this.updateSeriesData([]); + this.resetStats(); + this.emit('reset'); + if (newData) { + newData.forEach(function (point) { + this.add(point, true); + }, this); + } + } + /** + * Return the point closest to a given x value. + */ + nearestPoint(xValue) { + const insertIndex = this.sortedIndex(xValue); + const data = this.getSeriesData(); + const lowPoint = data[insertIndex - 1]; + const highPoint = data[insertIndex]; + const indexVal = this.getXVal(xValue); + const lowDistance = lowPoint ? indexVal - this.getXVal(lowPoint) : Number.POSITIVE_INFINITY; + const highDistance = highPoint ? this.getXVal(highPoint) - indexVal : Number.POSITIVE_INFINITY; + const nearestPoint = highDistance < lowDistance ? highPoint : lowPoint; + + return nearestPoint; + } + /** + * Override this to implement plot series loading functionality. Must return + * a promise that is resolved when loading is completed. + * + * @returns {Promise} + */ + async load(options) { + await this.fetch(options); + this.emit('load'); + const limitsResponse = await this.limitDefinition.limits(); + this.limits = []; + if (limitsResponse) { + this.limits = limitsResponse; } - updateName(name) { - if (name !== this.get('name')) { - this.set('name', name); - } - } - /** - * Update x formatter on x change. - */ - onXKeyChange(xKey) { - const format = this.formats[xKey]; - if (format) { - this.getXVal = format.parse.bind(format); - } + this.emit('limits', this); + this.emit('change:limitLines', this); + } + + /** + * Find the insert index for a given point to maintain sort order. + * @private + */ + sortedIndex(point) { + return _.sortedIndexBy(this.getSeriesData(), point, this.getXVal); + } + /** + * Update min/max stats for the series. + * @private + */ + updateStats(point) { + const value = this.getYVal(point); + let stats = this.get('stats'); + let changed = false; + if (!stats) { + if ([Infinity, -Infinity].includes(value)) { + return; + } + + stats = { + minValue: value, + minPoint: point, + maxValue: value, + maxPoint: point + }; + changed = true; + } else { + if (stats.maxValue < value && value !== Infinity) { + stats.maxValue = value; + stats.maxPoint = point; + changed = true; + } + + if (stats.minValue > value && value !== -Infinity) { + stats.minValue = value; + stats.minPoint = point; + changed = true; + } } - /** - * Update y formatter on change, default to stepAfter interpolation if - * y range is an enumeration. - */ - onYKeyChange(newKey, oldKey) { - if (newKey === oldKey) { - return; - } + if (changed) { + this.set('stats', { + minValue: stats.minValue, + minPoint: stats.minPoint, + maxValue: stats.maxValue, + maxPoint: stats.maxPoint + }); + } + } - const valueMetadata = this.metadata.value(newKey); - //TODO: Should we do this even if there is a persisted config? - if (!this.persistedConfig || !this.persistedConfig.interpolate) { - if (valueMetadata.format === 'enum') { - this.set('interpolate', 'stepAfter'); - } else { - this.set('interpolate', 'linear'); - } - } + /** + * Add a point to the data array while maintaining the sort order of + * the array and preventing insertion of points with a duplicate x + * value. Can provide an optional argument to append a point without + * maintaining sort order and dupe checks, which improves performance + * when adding an array of points that are already properly sorted. + * + * @private + * @param {Object} point a telemetry datum. + * @param {Boolean} [appendOnly] default false, if true will append + * a point to the end without dupe checking. + */ + add(point, appendOnly) { + let data = this.getSeriesData(); + let insertIndex = data.length; + const currentYVal = this.getYVal(point); + const lastYVal = this.getYVal(data[insertIndex - 1]); - this.evaluate = function (datum) { - return this.limitEvaluator.evaluate(datum, valueMetadata); - }.bind(this); - this.set('unit', valueMetadata.unit); - const format = this.formats[newKey]; - this.getYVal = (value) => { - const y = format.parse(value); + if (this.isValueInvalid(currentYVal) && this.isValueInvalid(lastYVal)) { + console.warn('[Plot] Invalid Y Values detected'); - return this.logMode ? symlog(y, 10) : y; - }; + return; } - formatX(point) { - return this.formats[this.get('xKey')].format(point); + if (!appendOnly) { + insertIndex = this.sortedIndex(point); + if (this.getXVal(data[insertIndex]) === this.getXVal(point)) { + return; + } + + if (this.getXVal(data[insertIndex - 1]) === this.getXVal(point)) { + return; + } } - formatY(point) { - return this.formats[this.get('yKey')].format(point); - } + this.updateStats(point); + point.mctLimitState = this.evaluate(point); + data.splice(insertIndex, 0, point); + this.updateSeriesData(data); + this.emit('add', point, insertIndex, this); + } - /** - * Clear stats and recalculate from existing data. - */ - resetStats() { - this.unset('stats'); - this.getSeriesData().forEach(this.updateStats, this); - } + /** + * + * @private + */ + isValueInvalid(val) { + return Number.isNaN(val) || this.unPlottableValues.includes(val); + } - /** - * Reset plot series. If new data is provided, will add that - * data to series after reset. - */ - reset(newData) { - this.updateSeriesData([]); + /** + * Remove a point from the data array and notify listeners. + * @private + */ + remove(point) { + let data = this.getSeriesData(); + const index = data.indexOf(point); + data.splice(index, 1); + this.updateSeriesData(data); + this.emit('remove', point, index, this); + } + /** + * Purges records outside a given x range. Changes removal method based + * on number of records to remove: for large purge, reset data and + * rebuild array. for small purge, removes points and emits updates. + * + * @public + * @param {Object} range + * @param {number} range.min minimum x value to keep + * @param {number} range.max maximum x value to keep. + */ + purgeRecordsOutsideRange(range) { + const startIndex = this.sortedIndex(range.min); + const endIndex = this.sortedIndex(range.max) + 1; + let data = this.getSeriesData(); + const pointsToRemove = startIndex + (data.length - endIndex + 1); + if (pointsToRemove > 0) { + if (pointsToRemove < 1000) { + data.slice(0, startIndex).forEach(this.remove, this); + data.slice(endIndex, data.length).forEach(this.remove, this); + this.updateSeriesData(data); this.resetStats(); - this.emit('reset'); - if (newData) { - newData.forEach(function (point) { - this.add(point, true); - }, this); - } + } else { + const newData = this.getSeriesData().slice(startIndex, endIndex); + this.reset(newData); + } } - /** - * Return the point closest to a given x value. - */ - nearestPoint(xValue) { - const insertIndex = this.sortedIndex(xValue); - const data = this.getSeriesData(); - const lowPoint = data[insertIndex - 1]; - const highPoint = data[insertIndex]; - const indexVal = this.getXVal(xValue); - const lowDistance = lowPoint - ? indexVal - this.getXVal(lowPoint) - : Number.POSITIVE_INFINITY; - const highDistance = highPoint - ? this.getXVal(highPoint) - indexVal - : Number.POSITIVE_INFINITY; - const nearestPoint = highDistance < lowDistance ? highPoint : lowPoint; - - return nearestPoint; - } - /** - * Override this to implement plot series loading functionality. Must return - * a promise that is resolved when loading is completed. - * - * @returns {Promise} - */ - async load(options) { - await this.fetch(options); - this.emit('load'); - const limitsResponse = await this.limitDefinition.limits(); - this.limits = []; - if (limitsResponse) { - this.limits = limitsResponse; - } - - this.emit('limits', this); - this.emit('change:limitLines', this); + } + /** + * Updates filters, clears the plot series, unsubscribes and resubscribes + * @public + */ + updateFiltersAndRefresh(updatedFilters) { + if (updatedFilters === undefined) { + return; } - /** - * Find the insert index for a given point to maintain sort order. - * @private - */ - sortedIndex(point) { - return _.sortedIndexBy(this.getSeriesData(), point, this.getXVal); + let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters)); + + if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) { + this.filters = deepCopiedFilters; + this.reset(); + if (this.unsubscribe) { + this.unsubscribe(); + delete this.unsubscribe; + } + + this.fetch(); + } else { + this.filters = deepCopiedFilters; } - /** - * Update min/max stats for the series. - * @private - */ - updateStats(point) { - const value = this.getYVal(point); - let stats = this.get('stats'); - let changed = false; - if (!stats) { - if ([Infinity, -Infinity].includes(value)) { - return; - } + } + getDisplayRange(xKey) { + const unsortedData = this.getSeriesData(); + this.updateSeriesData([]); + unsortedData.forEach((point) => this.add(point, false)); - stats = { - minValue: value, - minPoint: point, - maxValue: value, - maxPoint: point - }; - changed = true; - } else { - if (stats.maxValue < value && value !== Infinity) { - stats.maxValue = value; - stats.maxPoint = point; - changed = true; - } + let data = this.getSeriesData(); + const minValue = this.getXVal(data[0]); + const maxValue = this.getXVal(data[data.length - 1]); - if (stats.minValue > value && value !== -Infinity) { - stats.minValue = value; - stats.minPoint = point; - changed = true; - } - } - - if (changed) { - this.set('stats', { - minValue: stats.minValue, - minPoint: stats.minPoint, - maxValue: stats.maxValue, - maxPoint: stats.maxPoint - }); - } + return { + min: minValue, + max: maxValue + }; + } + markerOptionsDisplayText() { + const showMarkers = this.get('markers'); + if (!showMarkers) { + return 'Disabled'; } - /** - * Add a point to the data array while maintaining the sort order of - * the array and preventing insertion of points with a duplicate x - * value. Can provide an optional argument to append a point without - * maintaining sort order and dupe checks, which improves performance - * when adding an array of points that are already properly sorted. - * - * @private - * @param {Object} point a telemetry datum. - * @param {Boolean} [appendOnly] default false, if true will append - * a point to the end without dupe checking. - */ - add(point, appendOnly) { - let data = this.getSeriesData(); - let insertIndex = data.length; - const currentYVal = this.getYVal(point); - const lastYVal = this.getYVal(data[insertIndex - 1]); + const markerShapeKey = this.get('markerShape'); + const markerShape = MARKER_SHAPES[markerShapeKey].label; + const markerSize = this.get('markerSize'); - if (this.isValueInvalid(currentYVal) && this.isValueInvalid(lastYVal)) { - console.warn('[Plot] Invalid Y Values detected'); + return `${markerShape}: ${markerSize}px`; + } + nameWithUnit() { + let unit = this.get('unit'); - return; - } + return this.get('name') + (unit ? ' ' + unit : ''); + } - if (!appendOnly) { - insertIndex = this.sortedIndex(point); - if (this.getXVal(data[insertIndex]) === this.getXVal(point)) { - return; - } + /** + * Update the series data with the given value. + */ + updateSeriesData(data) { + configStore.add(this.dataStoreId, data); + } - if (this.getXVal(data[insertIndex - 1]) === this.getXVal(point)) { - return; - } - } - - this.updateStats(point); - point.mctLimitState = this.evaluate(point); - data.splice(insertIndex, 0, point); - this.updateSeriesData(data); - this.emit('add', point, insertIndex, this); - } - - /** - * - * @private - */ - isValueInvalid(val) { - return Number.isNaN(val) || this.unPlottableValues.includes(val); - } - - /** - * Remove a point from the data array and notify listeners. - * @private - */ - remove(point) { - let data = this.getSeriesData(); - const index = data.indexOf(point); - data.splice(index, 1); - this.updateSeriesData(data); - this.emit('remove', point, index, this); - } - /** - * Purges records outside a given x range. Changes removal method based - * on number of records to remove: for large purge, reset data and - * rebuild array. for small purge, removes points and emits updates. - * - * @public - * @param {Object} range - * @param {number} range.min minimum x value to keep - * @param {number} range.max maximum x value to keep. - */ - purgeRecordsOutsideRange(range) { - const startIndex = this.sortedIndex(range.min); - const endIndex = this.sortedIndex(range.max) + 1; - let data = this.getSeriesData(); - const pointsToRemove = startIndex + (data.length - endIndex + 1); - if (pointsToRemove > 0) { - if (pointsToRemove < 1000) { - data.slice(0, startIndex).forEach(this.remove, this); - data.slice(endIndex, data.length).forEach(this.remove, this); - this.updateSeriesData(data); - this.resetStats(); - } else { - const newData = this.getSeriesData().slice(startIndex, endIndex); - this.reset(newData); - } - } - - } - /** - * Updates filters, clears the plot series, unsubscribes and resubscribes - * @public - */ - updateFiltersAndRefresh(updatedFilters) { - if (updatedFilters === undefined) { - return; - } - - let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters)); - - if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) { - this.filters = deepCopiedFilters; - this.reset(); - if (this.unsubscribe) { - this.unsubscribe(); - delete this.unsubscribe; - } - - this.fetch(); - } else { - this.filters = deepCopiedFilters; - } - } - getDisplayRange(xKey) { - const unsortedData = this.getSeriesData(); - this.updateSeriesData([]); - unsortedData.forEach(point => this.add(point, false)); - - let data = this.getSeriesData(); - const minValue = this.getXVal(data[0]); - const maxValue = this.getXVal(data[data.length - 1]); - - return { - min: minValue, - max: maxValue - }; - } - markerOptionsDisplayText() { - const showMarkers = this.get('markers'); - if (!showMarkers) { - return "Disabled"; - } - - const markerShapeKey = this.get('markerShape'); - const markerShape = MARKER_SHAPES[markerShapeKey].label; - const markerSize = this.get('markerSize'); - - return `${markerShape}: ${markerSize}px`; - } - nameWithUnit() { - let unit = this.get('unit'); - - return this.get('name') + (unit ? ' ' + unit : ''); - } - - /** - * Update the series data with the given value. - */ - updateSeriesData(data) { - configStore.add(this.dataStoreId, data); - } - - /** + /** * Update the series data with the given value. * This return type definition is totally wrong, only covers sinwave generator. It needs to be generic. * @return-example {Array<{ @@ -561,9 +549,9 @@ export default class PlotSeries extends Model { yesterday: number }>} */ - getSeriesData() { - return configStore.get(this.dataStoreId) || []; - } + getSeriesData() { + return configStore.get(this.dataStoreId) || []; + } } /** @typedef {any} TODO */ diff --git a/src/plugins/plot/configuration/SeriesCollection.js b/src/plugins/plot/configuration/SeriesCollection.js index 9e3c10b668..b57ff51931 100644 --- a/src/plugins/plot/configuration/SeriesCollection.js +++ b/src/plugins/plot/configuration/SeriesCollection.js @@ -21,168 +21,171 @@ *****************************************************************************/ import _ from 'lodash'; -import PlotSeries from "./PlotSeries"; -import Collection from "./Collection"; -import Color from "@/ui/color/Color"; -import ColorPalette from "@/ui/color/ColorPalette"; +import PlotSeries from './PlotSeries'; +import Collection from './Collection'; +import Color from '@/ui/color/Color'; +import ColorPalette from '@/ui/color/ColorPalette'; /** * @extends {Collection} */ export default class SeriesCollection extends Collection { - /** + /** @override @param {import('./Model').ModelOptions} options */ - initialize(options) { - super.initialize(options); - this.modelClass = PlotSeries; - this.plot = options.plot; - this.openmct = options.openmct; - 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); + initialize(options) { + super.initialize(options); + this.modelClass = PlotSeries; + this.plot = options.plot; + this.openmct = options.openmct; + 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); - const domainObject = this.plot.get('domainObject'); - if (domainObject.telemetry) { - this.addTelemetryObject(domainObject); - } else { - this.watchTelemetryContainer(domainObject); - } + const domainObject = this.plot.get('domainObject'); + if (domainObject.telemetry) { + this.addTelemetryObject(domainObject); + } else { + this.watchTelemetryContainer(domainObject); } - trackPersistedConfig(domainObject) { - domainObject.configuration.series.forEach(function (seriesConfig) { - const series = this.byIdentifier(seriesConfig.identifier); - if (series) { - series.persistedConfig = seriesConfig; - if (!series.persistedConfig.yAxisId) { - return; - } - - if (series.get('yAxisId') !== series.persistedConfig.yAxisId) { - series.set('yAxisId', series.persistedConfig.yAxisId); - } - } - }, this); - } - watchTelemetryContainer(domainObject) { - if (domainObject.type === 'telemetry.plot.stacked') { - return; + } + trackPersistedConfig(domainObject) { + domainObject.configuration.series.forEach(function (seriesConfig) { + const series = this.byIdentifier(seriesConfig.identifier); + if (series) { + series.persistedConfig = seriesConfig; + if (!series.persistedConfig.yAxisId) { + return; } - const composition = this.openmct.composition.get(domainObject); - this.listenTo(composition, 'add', this.addTelemetryObject, this); - this.listenTo(composition, 'remove', this.removeTelemetryObject, this); - composition.load(); - } - addTelemetryObject(domainObject, index) { - let seriesConfig = this.plot.getPersistedSeriesConfig(domainObject.identifier); - const filters = this.plot.getPersistedFilters(domainObject.identifier); - const plotObject = this.plot.get('domainObject'); - - if (!seriesConfig) { - seriesConfig = { - identifier: domainObject.identifier - }; - - if (plotObject.type === 'telemetry.plot.overlay') { - this.openmct.objects.mutate( - plotObject, - 'configuration.series[' + this.size() + ']', - seriesConfig - ); - seriesConfig = this.plot - .getPersistedSeriesConfig(domainObject.identifier); - } + if (series.get('yAxisId') !== series.persistedConfig.yAxisId) { + series.set('yAxisId', series.persistedConfig.yAxisId); } - - // Clone to prevent accidental mutation by ref. - seriesConfig = JSON.parse(JSON.stringify(seriesConfig)); - - if (!seriesConfig) { - throw "not possible"; - } - - this.add(new PlotSeries({ - model: seriesConfig, - domainObject: domainObject, - openmct: this.openmct, - collection: this, - persistedConfig: this.plot - .getPersistedSeriesConfig(domainObject.identifier), - filters: filters - })); + } + }, this); + } + watchTelemetryContainer(domainObject) { + if (domainObject.type === 'telemetry.plot.stacked') { + return; } - removeTelemetryObject(identifier) { - const plotObject = this.plot.get('domainObject'); - if (plotObject.type === 'telemetry.plot.overlay') { - const persistedIndex = plotObject.configuration.series.findIndex(s => { - return _.isEqual(identifier, s.identifier); - }); + const composition = this.openmct.composition.get(domainObject); + this.listenTo(composition, 'add', this.addTelemetryObject, this); + this.listenTo(composition, 'remove', this.removeTelemetryObject, this); + composition.load(); + } + addTelemetryObject(domainObject, index) { + let seriesConfig = this.plot.getPersistedSeriesConfig(domainObject.identifier); + const filters = this.plot.getPersistedFilters(domainObject.identifier); + const plotObject = this.plot.get('domainObject'); - const configIndex = this.models.findIndex(m => { - return _.isEqual(m.domainObject.identifier, identifier); - }); + if (!seriesConfig) { + seriesConfig = { + identifier: domainObject.identifier + }; - /* + if (plotObject.type === 'telemetry.plot.overlay') { + this.openmct.objects.mutate( + plotObject, + 'configuration.series[' + this.size() + ']', + seriesConfig + ); + seriesConfig = this.plot.getPersistedSeriesConfig(domainObject.identifier); + } + } + + // Clone to prevent accidental mutation by ref. + seriesConfig = JSON.parse(JSON.stringify(seriesConfig)); + + if (!seriesConfig) { + throw 'not possible'; + } + + this.add( + new PlotSeries({ + model: seriesConfig, + domainObject: domainObject, + openmct: this.openmct, + collection: this, + persistedConfig: this.plot.getPersistedSeriesConfig(domainObject.identifier), + filters: filters + }) + ); + } + removeTelemetryObject(identifier) { + const plotObject = this.plot.get('domainObject'); + if (plotObject.type === 'telemetry.plot.overlay') { + const persistedIndex = plotObject.configuration.series.findIndex((s) => { + return _.isEqual(identifier, s.identifier); + }); + + const configIndex = this.models.findIndex((m) => { + return _.isEqual(m.domainObject.identifier, identifier); + }); + + /* when cancelling out of edit mode, the config store and domain object are out of sync thus it is necesarry to check both and remove the models that are no longer in composition */ - if (persistedIndex === -1) { - this.remove(this.at(configIndex)); - } else { - this.remove(this.at(persistedIndex)); - // Because this is triggered by a composition change, we have - // to defer mutation of our plot object, otherwise we might - // mutate an outdated version of the plotObject. - setTimeout(function () { - const newPlotObject = this.plot.get('domainObject'); - const cSeries = newPlotObject.configuration.series.slice(); - cSeries.splice(persistedIndex, 1); - this.openmct.objects.mutate(newPlotObject, 'configuration.series', cSeries); - }.bind(this)); - } - } + if (persistedIndex === -1) { + this.remove(this.at(configIndex)); + } else { + this.remove(this.at(persistedIndex)); + // Because this is triggered by a composition change, we have + // to defer mutation of our plot object, otherwise we might + // mutate an outdated version of the plotObject. + setTimeout( + function () { + const newPlotObject = this.plot.get('domainObject'); + const cSeries = newPlotObject.configuration.series.slice(); + cSeries.splice(persistedIndex, 1); + this.openmct.objects.mutate(newPlotObject, 'configuration.series', cSeries); + }.bind(this) + ); + } } - onSeriesAdd(series) { - let seriesColor = series.get('color'); - if (seriesColor) { - if (!(seriesColor instanceof Color)) { - seriesColor = Color.fromHexString(seriesColor); - series.set('color', seriesColor); - } + } + onSeriesAdd(series) { + let seriesColor = series.get('color'); + if (seriesColor) { + if (!(seriesColor instanceof Color)) { + seriesColor = Color.fromHexString(seriesColor); + series.set('color', seriesColor); + } - this.palette.remove(seriesColor); - } else { - series.set('color', this.palette.getNextColor()); - } + this.palette.remove(seriesColor); + } else { + series.set('color', this.palette.getNextColor()); + } - this.listenTo(series, 'change:color', this.updateColorPalette, this); + this.listenTo(series, 'change:color', this.updateColorPalette, this); + } + onSeriesRemove(series) { + this.palette.return(series.get('color')); + this.stopListening(series); + series.destroy(); + } + updateColorPalette(newColor, oldColor) { + this.palette.remove(newColor); + const seriesWithColor = this.filter(function (series) { + return series.get('color') === newColor; + })[0]; + if (!seriesWithColor) { + this.palette.return(oldColor); } - onSeriesRemove(series) { - this.palette.return(series.get('color')); - this.stopListening(series); - series.destroy(); - } - updateColorPalette(newColor, oldColor) { - this.palette.remove(newColor); - const seriesWithColor = this.filter(function (series) { - return series.get('color') === newColor; - })[0]; - if (!seriesWithColor) { - this.palette.return(oldColor); - } - } - byIdentifier(identifier) { - return this.filter(function (series) { - const seriesIdentifier = series.get('identifier'); + } + byIdentifier(identifier) { + return this.filter(function (series) { + const seriesIdentifier = series.get('identifier'); - return seriesIdentifier.namespace === identifier.namespace - && seriesIdentifier.key === identifier.key; - })[0]; - } + return ( + seriesIdentifier.namespace === identifier.namespace && + seriesIdentifier.key === identifier.key + ); + })[0]; + } } /** diff --git a/src/plugins/plot/configuration/XAxisModel.js b/src/plugins/plot/configuration/XAxisModel.js index 57a23c4923..5c8497cba8 100644 --- a/src/plugins/plot/configuration/XAxisModel.js +++ b/src/plugins/plot/configuration/XAxisModel.js @@ -25,92 +25,92 @@ import Model from './Model'; * @extends {Model} */ export default class XAxisModel extends Model { - // Despite providing template types to the Model class, we still need to - // re-define the type of the following initialize() method's options arg. Tracking - // issue for this: https://github.com/microsoft/TypeScript/issues/32082 - // When they fix it, we can remove the `@param` we have here. - /** - * @override - * @param {import('./Model').ModelOptions} options - */ - initialize(options) { - this.plot = options.plot; + // Despite providing template types to the Model class, we still need to + // re-define the type of the following initialize() method's options arg. Tracking + // issue for this: https://github.com/microsoft/TypeScript/issues/32082 + // When they fix it, we can remove the `@param` we have here. + /** + * @override + * @param {import('./Model').ModelOptions} options + */ + initialize(options) { + this.plot = options.plot; - // This is a type assertion for TypeScript, this error is not thrown in practice. - if (!options.model) { - throw new Error('Not a collection model.'); - } - - this.set('label', options.model.name || ''); - - this.on('change:range', (newValue) => { - if (!this.get('frozen')) { - this.set('displayRange', newValue); - } - }); - - this.on('change:frozen', (frozen) => { - if (!frozen) { - this.set('range', this.get('range')); - } - }); - - if (this.get('range')) { - this.set('range', this.get('range')); - } - - this.listenTo(this, 'change:key', this.changeKey, this); + // This is a type assertion for TypeScript, this error is not thrown in practice. + if (!options.model) { + throw new Error('Not a collection model.'); } - /** - * @param {string} newKey - */ - changeKey(newKey) { - const series = this.plot.series.first(); - if (series) { - const xMetadata = series.metadata.value(newKey); - const xFormat = series.formats[newKey]; - this.set('label', xMetadata.name); - this.set('format', xFormat.format.bind(xFormat)); - } else { - this.set('format', function (x) { - return x; - }); - this.set('label', newKey); - } + this.set('label', options.model.name || ''); - this.plot.series.forEach(function (plotSeries) { - plotSeries.set('xKey', newKey); - }); - } - resetSeries() { - this.plot.series.forEach(function (plotSeries) { - plotSeries.reset(); - }); - } - /** - * @param {import('./Model').ModelOptions} options - * @override - */ - defaultModel(options) { - const bounds = options.openmct.time.bounds(); - const timeSystem = options.openmct.time.timeSystem(); - const format = options.openmct.telemetry.getFormatter(timeSystem.timeFormat); + this.on('change:range', (newValue) => { + if (!this.get('frozen')) { + this.set('displayRange', newValue); + } + }); - /** @type {XAxisModelType} */ - const defaultModel = { - name: timeSystem.name, - key: timeSystem.key, - format: format.format.bind(format), - range: { - min: bounds.start, - max: bounds.end - }, - frozen: false - }; + this.on('change:frozen', (frozen) => { + if (!frozen) { + this.set('range', this.get('range')); + } + }); - return defaultModel; + if (this.get('range')) { + this.set('range', this.get('range')); } + + this.listenTo(this, 'change:key', this.changeKey, this); + } + + /** + * @param {string} newKey + */ + changeKey(newKey) { + const series = this.plot.series.first(); + if (series) { + const xMetadata = series.metadata.value(newKey); + const xFormat = series.formats[newKey]; + this.set('label', xMetadata.name); + this.set('format', xFormat.format.bind(xFormat)); + } else { + this.set('format', function (x) { + return x; + }); + this.set('label', newKey); + } + + this.plot.series.forEach(function (plotSeries) { + plotSeries.set('xKey', newKey); + }); + } + resetSeries() { + this.plot.series.forEach(function (plotSeries) { + plotSeries.reset(); + }); + } + /** + * @param {import('./Model').ModelOptions} options + * @override + */ + defaultModel(options) { + const bounds = options.openmct.time.bounds(); + const timeSystem = options.openmct.time.timeSystem(); + const format = options.openmct.telemetry.getFormatter(timeSystem.timeFormat); + + /** @type {XAxisModelType} */ + const defaultModel = { + name: timeSystem.name, + key: timeSystem.key, + format: format.format.bind(format), + range: { + min: bounds.start, + max: bounds.end + }, + frozen: false + }; + + return defaultModel; + } } /** @typedef {any} TODO */ diff --git a/src/plugins/plot/configuration/YAxisModel.js b/src/plugins/plot/configuration/YAxisModel.js index 79feb384ba..eed246f898 100644 --- a/src/plugins/plot/configuration/YAxisModel.js +++ b/src/plugins/plot/configuration/YAxisModel.js @@ -45,332 +45,345 @@ import Model from './Model'; * @extends {Model} */ export default class YAxisModel extends Model { - /** - * @override - * @param {import('./Model').ModelOptions} options - */ - initialize(options) { - this.plot = options.plot; - this.listenTo(this, 'change:stats', this.calculateAutoscaleExtents, this); - this.listenTo(this, 'change:autoscale', this.toggleAutoscale, this); - this.listenTo(this, 'change:autoscalePadding', this.updatePadding, this); - this.listenTo(this, 'change:logMode', this.onLogModeChange, this); - this.listenTo(this, 'change:frozen', this.toggleFreeze, this); - this.listenTo(this, 'change:range', this.updateDisplayRange, this); - const range = this.get('range'); - this.updateDisplayRange(range); - //This is an edge case and should not happen - const invalidRange = !range || (range?.min === undefined || range?.max === undefined); - const invalidAutoScaleOff = (options.model.autoscale === false) && invalidRange; - if (invalidAutoScaleOff) { - this.set('autoscale', true); - } + /** + * @override + * @param {import('./Model').ModelOptions} options + */ + initialize(options) { + this.plot = options.plot; + this.listenTo(this, 'change:stats', this.calculateAutoscaleExtents, this); + this.listenTo(this, 'change:autoscale', this.toggleAutoscale, this); + this.listenTo(this, 'change:autoscalePadding', this.updatePadding, this); + this.listenTo(this, 'change:logMode', this.onLogModeChange, this); + this.listenTo(this, 'change:frozen', this.toggleFreeze, this); + this.listenTo(this, 'change:range', this.updateDisplayRange, this); + const range = this.get('range'); + this.updateDisplayRange(range); + //This is an edge case and should not happen + const invalidRange = !range || range?.min === undefined || range?.max === undefined; + const invalidAutoScaleOff = options.model.autoscale === false && invalidRange; + if (invalidAutoScaleOff) { + this.set('autoscale', true); } - /** - * @param {import('./SeriesCollection').default} seriesCollection - */ - listenToSeriesCollection(seriesCollection) { - this.seriesCollection = seriesCollection; - this.listenTo(this.seriesCollection, 'add', series => { - this.trackSeries(series); - this.updateFromSeries(this.seriesCollection); - }, this); - this.listenTo(this.seriesCollection, 'remove', series => { - this.untrackSeries(series); - this.updateFromSeries(this.seriesCollection); - }, this); - this.seriesCollection.forEach(this.trackSeries, this); + } + /** + * @param {import('./SeriesCollection').default} seriesCollection + */ + listenToSeriesCollection(seriesCollection) { + this.seriesCollection = seriesCollection; + this.listenTo( + this.seriesCollection, + 'add', + (series) => { + this.trackSeries(series); this.updateFromSeries(this.seriesCollection); + }, + this + ); + this.listenTo( + this.seriesCollection, + 'remove', + (series) => { + this.untrackSeries(series); + this.updateFromSeries(this.seriesCollection); + }, + this + ); + this.seriesCollection.forEach(this.trackSeries, this); + this.updateFromSeries(this.seriesCollection); + } + toggleFreeze(frozen) { + if (!frozen) { + this.toggleAutoscale(this.get('autoscale')); } - toggleFreeze(frozen) { - if (!frozen) { - this.toggleAutoscale(this.get('autoscale')); - } - } - applyPadding(range) { - let padding = Math.abs(range.max - range.min) * this.get('autoscalePadding'); - if (padding === 0) { - padding = 1; - } - - return { - min: range.min - padding, - max: range.max + padding - }; - } - updatePadding(newPadding) { - if (this.get('autoscale') && !this.get('frozen') && this.has('stats')) { - this.set('displayRange', this.applyPadding(this.get('stats'))); - } - } - calculateAutoscaleExtents(newStats) { - if (this.get('autoscale') && !this.get('frozen')) { - if (!newStats) { - this.unset('displayRange'); - } else { - this.set('displayRange', this.applyPadding(newStats)); - } - } - } - updateStats(seriesStats) { - if (!this.has('stats')) { - this.set('stats', { - min: seriesStats.minValue, - max: seriesStats.maxValue - }); - - return; - } - - const stats = this.get('stats'); - let changed = false; - if (stats.min > seriesStats.minValue) { - changed = true; - stats.min = seriesStats.minValue; - } - - if (stats.max < seriesStats.maxValue) { - changed = true; - stats.max = seriesStats.maxValue; - } - - if (changed) { - this.set('stats', { - min: stats.min, - max: stats.max - }); - } - } - resetStats() { - //TODO: do we need the series id here? - this.unset('stats'); - this.getSeriesForYAxis(this.seriesCollection).forEach(series => { - if (series.has('stats')) { - this.updateStats(series.get('stats')); - } - }); - } - getSeriesForYAxis(seriesCollection) { - return seriesCollection.filter(series => { - const seriesYAxisId = series.get('yAxisId') || 1; - - return seriesYAxisId === this.id; - }); + } + applyPadding(range) { + let padding = Math.abs(range.max - range.min) * this.get('autoscalePadding'); + if (padding === 0) { + padding = 1; } - getYAxisForId(id) { - const plotModel = this.plot.get('domainObject'); - let yAxis; - if (this.id === 1) { - yAxis = plotModel.configuration?.yAxis; - } else { - if (plotModel.configuration?.additionalYAxes) { - yAxis = plotModel.configuration.additionalYAxes.find(additionalYAxis => additionalYAxis.id === id); - } - } - - return yAxis; + return { + min: range.min - padding, + max: range.max + padding + }; + } + updatePadding(newPadding) { + if (this.get('autoscale') && !this.get('frozen') && this.has('stats')) { + this.set('displayRange', this.applyPadding(this.get('stats'))); } - /** - * @param {import('./PlotSeries').default} series - */ - trackSeries(series) { - this.listenTo(series, 'change:stats', seriesStats => { - if (series.get('yAxisId') !== this.id) { - return; - } - - if (!seriesStats) { - this.resetStats(); - } else { - this.updateStats(seriesStats); - } - }); - this.listenTo(series, 'change:yKey', () => { - if (series.get('yAxisId') !== this.id) { - return; - } - - this.updateFromSeries(this.seriesCollection); - }); - - this.listenTo(series, 'change:yAxisId', (newYAxisId, oldYAxisId) => { - if (oldYAxisId && this.id === oldYAxisId) { - this.resetStats(); - this.updateFromSeries(this.seriesCollection); - } - - if (series.get('yAxisId') === this.id) { - this.resetStats(); - this.updateFromSeries(this.seriesCollection); - } - }); + } + calculateAutoscaleExtents(newStats) { + if (this.get('autoscale') && !this.get('frozen')) { + if (!newStats) { + this.unset('displayRange'); + } else { + this.set('displayRange', this.applyPadding(newStats)); + } } - untrackSeries(series) { - this.stopListening(series); + } + updateStats(seriesStats) { + if (!this.has('stats')) { + this.set('stats', { + min: seriesStats.minValue, + max: seriesStats.maxValue + }); + + return; + } + + const stats = this.get('stats'); + let changed = false; + if (stats.min > seriesStats.minValue) { + changed = true; + stats.min = seriesStats.minValue; + } + + if (stats.max < seriesStats.maxValue) { + changed = true; + stats.max = seriesStats.maxValue; + } + + if (changed) { + this.set('stats', { + min: stats.min, + max: stats.max + }); + } + } + resetStats() { + //TODO: do we need the series id here? + this.unset('stats'); + this.getSeriesForYAxis(this.seriesCollection).forEach((series) => { + if (series.has('stats')) { + this.updateStats(series.get('stats')); + } + }); + } + getSeriesForYAxis(seriesCollection) { + return seriesCollection.filter((series) => { + const seriesYAxisId = series.get('yAxisId') || 1; + + return seriesYAxisId === this.id; + }); + } + + getYAxisForId(id) { + const plotModel = this.plot.get('domainObject'); + let yAxis; + if (this.id === 1) { + yAxis = plotModel.configuration?.yAxis; + } else { + if (plotModel.configuration?.additionalYAxes) { + yAxis = plotModel.configuration.additionalYAxes.find( + (additionalYAxis) => additionalYAxis.id === id + ); + } + } + + return yAxis; + } + /** + * @param {import('./PlotSeries').default} series + */ + trackSeries(series) { + this.listenTo(series, 'change:stats', (seriesStats) => { + if (series.get('yAxisId') !== this.id) { + return; + } + + if (!seriesStats) { + this.resetStats(); + } else { + this.updateStats(seriesStats); + } + }); + this.listenTo(series, 'change:yKey', () => { + if (series.get('yAxisId') !== this.id) { + return; + } + + this.updateFromSeries(this.seriesCollection); + }); + + this.listenTo(series, 'change:yAxisId', (newYAxisId, oldYAxisId) => { + if (oldYAxisId && this.id === oldYAxisId) { this.resetStats(); this.updateFromSeries(this.seriesCollection); - } + } - /** - * This is called in order to map the user-provided `range` to the - * `displayRange` that we actually use for plot display. - * - * @param {import('./XAxisModel').NumberRange} range - */ - updateDisplayRange(range) { - if (this.get('autoscale')) { - return; - } - - const _range = { ...range }; - - if (this.get('logMode')) { - _range.min = symlog(range.min, 10); - _range.max = symlog(range.max, 10); - } - - this.set('displayRange', _range); - } - - /** - * @param {boolean} autoscale - */ - toggleAutoscale(autoscale) { - if (autoscale && this.has('stats')) { - this.set('displayRange', this.applyPadding(this.get('stats'))); - - return; - } - - const range = this.get('range'); - - if (range) { - // If we already have a user-defined range, make sure it maps to the - // range we'll actually use for the ticks. - - const _range = { ...range }; - - if (this.get('logMode')) { - _range.min = symlog(range.min, 10); - _range.max = symlog(range.max, 10); - } - - this.set('displayRange', _range); - } - } - - /** @param {boolean} logMode */ - onLogModeChange(logMode) { - const range = this.get('displayRange'); - - if (logMode) { - range.min = symlog(range.min, 10); - range.max = symlog(range.max, 10); - } else { - range.min = antisymlog(range.min, 10); - range.max = antisymlog(range.max, 10); - } - - this.set('displayRange', range); - - this.resetSeries(); - } - resetSeries() { - const series = this.getSeriesForYAxis(this.seriesCollection); - series.forEach((plotSeries) => { - plotSeries.logMode = this.get('logMode'); - plotSeries.reset(plotSeries.getSeriesData()); - }); - // Update the series collection labels and formatting + if (series.get('yAxisId') === this.id) { + this.resetStats(); this.updateFromSeries(this.seriesCollection); + } + }); + } + untrackSeries(series) { + this.stopListening(series); + this.resetStats(); + this.updateFromSeries(this.seriesCollection); + } + + /** + * This is called in order to map the user-provided `range` to the + * `displayRange` that we actually use for plot display. + * + * @param {import('./XAxisModel').NumberRange} range + */ + updateDisplayRange(range) { + if (this.get('autoscale')) { + return; } - /** - * For a given series collection, get the metadata of the current yKey for each series. - * Then return first available value of the given property from the metadata. - * @param {import('./SeriesCollection').default} series - * @param {String} property - */ - getMetadataValueByProperty(series, property) { - return series.map(s => (s.metadata ? s.metadata.value(s.get('yKey'))[property] : '')) - .reduce((a, b) => { - if (a === undefined) { - return b; - } + const _range = { ...range }; - if (a === b) { - return a; - } - - return ''; - }, undefined); + if (this.get('logMode')) { + _range.min = symlog(range.min, 10); + _range.max = symlog(range.max, 10); } - /** - * Update yAxis format, values, and label from known series. - * @param {import('./SeriesCollection').default} seriesCollection - */ - updateFromSeries(seriesCollection) { - const seriesForThisYAxis = this.getSeriesForYAxis(seriesCollection); - if (!seriesForThisYAxis.length) { - return; - } - const yAxis = this.getYAxisForId(this.id); - const label = yAxis?.label; - const sampleSeries = seriesForThisYAxis[0]; - if (!sampleSeries || !sampleSeries.metadata) { - if (!label) { - this.unset('label'); - } + this.set('displayRange', _range); + } - return; - } + /** + * @param {boolean} autoscale + */ + toggleAutoscale(autoscale) { + if (autoscale && this.has('stats')) { + this.set('displayRange', this.applyPadding(this.get('stats'))); - const yKey = sampleSeries.get('yKey'); - const yMetadata = sampleSeries.metadata.value(yKey); - const yFormat = sampleSeries.formats[yKey]; - - if (this.get('logMode')) { - this.set('format', (n) => yFormat.format(antisymlog(n, 10))); - } else { - this.set('format', (n) => yFormat.format(n)); - } - - this.set('values', yMetadata.values); - - if (!label) { - const labelName = this.getMetadataValueByProperty(seriesForThisYAxis, 'name'); - if (labelName) { - this.set('label', labelName); - - return; - } - - //if the name is not available, set the units as the label - const labelUnits = this.getMetadataValueByProperty(seriesForThisYAxis, 'units'); - if (labelUnits) { - this.set('label', labelUnits); - - return; - } - } + return; } - /** - * @override - * @param {import('./Model').ModelOptions} options - * @returns {Partial} - */ - defaultModel(options) { - return { - frozen: false, - autoscale: true, - logMode: options.model?.logMode ?? false, - autoscalePadding: 0.1, - id: options.id, - range: options.model?.range - }; + + const range = this.get('range'); + + if (range) { + // If we already have a user-defined range, make sure it maps to the + // range we'll actually use for the ticks. + + const _range = { ...range }; + + if (this.get('logMode')) { + _range.min = symlog(range.min, 10); + _range.max = symlog(range.max, 10); + } + + this.set('displayRange', _range); } + } + + /** @param {boolean} logMode */ + onLogModeChange(logMode) { + const range = this.get('displayRange'); + + if (logMode) { + range.min = symlog(range.min, 10); + range.max = symlog(range.max, 10); + } else { + range.min = antisymlog(range.min, 10); + range.max = antisymlog(range.max, 10); + } + + this.set('displayRange', range); + + this.resetSeries(); + } + resetSeries() { + const series = this.getSeriesForYAxis(this.seriesCollection); + series.forEach((plotSeries) => { + plotSeries.logMode = this.get('logMode'); + plotSeries.reset(plotSeries.getSeriesData()); + }); + // Update the series collection labels and formatting + this.updateFromSeries(this.seriesCollection); + } + + /** + * For a given series collection, get the metadata of the current yKey for each series. + * Then return first available value of the given property from the metadata. + * @param {import('./SeriesCollection').default} series + * @param {String} property + */ + getMetadataValueByProperty(series, property) { + return series + .map((s) => (s.metadata ? s.metadata.value(s.get('yKey'))[property] : '')) + .reduce((a, b) => { + if (a === undefined) { + return b; + } + + if (a === b) { + return a; + } + + return ''; + }, undefined); + } + /** + * Update yAxis format, values, and label from known series. + * @param {import('./SeriesCollection').default} seriesCollection + */ + updateFromSeries(seriesCollection) { + const seriesForThisYAxis = this.getSeriesForYAxis(seriesCollection); + if (!seriesForThisYAxis.length) { + return; + } + + const yAxis = this.getYAxisForId(this.id); + const label = yAxis?.label; + const sampleSeries = seriesForThisYAxis[0]; + if (!sampleSeries || !sampleSeries.metadata) { + if (!label) { + this.unset('label'); + } + + return; + } + + const yKey = sampleSeries.get('yKey'); + const yMetadata = sampleSeries.metadata.value(yKey); + const yFormat = sampleSeries.formats[yKey]; + + if (this.get('logMode')) { + this.set('format', (n) => yFormat.format(antisymlog(n, 10))); + } else { + this.set('format', (n) => yFormat.format(n)); + } + + this.set('values', yMetadata.values); + + if (!label) { + const labelName = this.getMetadataValueByProperty(seriesForThisYAxis, 'name'); + if (labelName) { + this.set('label', labelName); + + return; + } + + //if the name is not available, set the units as the label + const labelUnits = this.getMetadataValueByProperty(seriesForThisYAxis, 'units'); + if (labelUnits) { + this.set('label', labelUnits); + + return; + } + } + } + /** + * @override + * @param {import('./Model').ModelOptions} options + * @returns {Partial} + */ + defaultModel(options) { + return { + frozen: false, + autoscale: true, + logMode: options.model?.logMode ?? false, + autoscalePadding: 0.1, + id: options.id, + range: options.model?.range + }; + } } /** @typedef {any} TODO */ diff --git a/src/plugins/plot/draw/Draw2D.js b/src/plugins/plot/draw/Draw2D.js index cb93cb2367..e7bf7ed501 100644 --- a/src/plugins/plot/draw/Draw2D.js +++ b/src/plugins/plot/draw/Draw2D.js @@ -24,12 +24,12 @@ import EventEmitter from 'EventEmitter'; import eventHelpers from '../lib/eventHelpers'; import { MARKER_SHAPES } from './MarkerShapes'; /** - * Create a new draw API utilizing the Canvas's 2D API for rendering. - * - * @constructor - * @param {CanvasElement} canvas the canvas object to render upon - * @throws {Error} an error is thrown if Canvas's 2D API is unavailab - */ + * Create a new draw API utilizing the Canvas's 2D API for rendering. + * + * @constructor + * @param {CanvasElement} canvas the canvas object to render upon + * @throws {Error} an error is thrown if Canvas's 2D API is unavailab + */ /** * Create a new draw API utilizing the Canvas's 2D API for rendering. @@ -39,16 +39,16 @@ import { MARKER_SHAPES } from './MarkerShapes'; * @throws {Error} an error is thrown if Canvas's 2D API is unavailab */ function Draw2D(canvas) { - this.canvas = canvas; - this.c2d = canvas.getContext('2d'); - this.width = canvas.width; - this.height = canvas.height; - this.dimensions = [this.width, this.height]; - this.origin = [0, 0]; + this.canvas = canvas; + this.c2d = canvas.getContext('2d'); + this.width = canvas.width; + this.height = canvas.height; + this.dimensions = [this.width, this.height]; + this.origin = [0, 0]; - if (!this.c2d) { - throw new Error("Canvas 2d API unavailable."); - } + if (!this.c2d) { + throw new Error('Canvas 2d API unavailable.'); + } } Object.assign(Draw2D.prototype, EventEmitter.prototype); @@ -56,108 +56,95 @@ eventHelpers.extend(Draw2D.prototype); // Convert from logical to physical x coordinates Draw2D.prototype.x = function (v) { - return ((v - this.origin[0]) / this.dimensions[0]) * this.width; + return ((v - this.origin[0]) / this.dimensions[0]) * this.width; }; // Convert from logical to physical y coordinates Draw2D.prototype.y = function (v) { - return this.height - - ((v - this.origin[1]) / this.dimensions[1]) * this.height; + return this.height - ((v - this.origin[1]) / this.dimensions[1]) * this.height; }; // Set the color to be used for drawing operations Draw2D.prototype.setColor = function (color) { - const mappedColor = color.map(function (c, i) { - return i < 3 ? Math.floor(c * 255) : (c); - }).join(','); - this.c2d.strokeStyle = "rgba(" + mappedColor + ")"; - this.c2d.fillStyle = "rgba(" + mappedColor + ")"; + const mappedColor = color + .map(function (c, i) { + return i < 3 ? Math.floor(c * 255) : c; + }) + .join(','); + this.c2d.strokeStyle = 'rgba(' + mappedColor + ')'; + this.c2d.fillStyle = 'rgba(' + mappedColor + ')'; }; Draw2D.prototype.clear = function () { - this.width = this.canvas.width = this.canvas.offsetWidth; - this.height = this.canvas.height = this.canvas.offsetHeight; - this.c2d.clearRect(0, 0, this.width, this.height); + this.width = this.canvas.width = this.canvas.offsetWidth; + this.height = this.canvas.height = this.canvas.offsetHeight; + this.c2d.clearRect(0, 0, this.width, this.height); }; Draw2D.prototype.setDimensions = function (newDimensions, newOrigin) { - this.dimensions = newDimensions; - this.origin = newOrigin; + this.dimensions = newDimensions; + this.origin = newOrigin; }; Draw2D.prototype.drawLine = function (buf, color, points) { - let i; + let i; - this.setColor(color); + this.setColor(color); - // Configure context to draw two-pixel-thick lines - this.c2d.lineWidth = 1; + // Configure context to draw two-pixel-thick lines + this.c2d.lineWidth = 1; - // Start a new path... - if (buf.length > 1) { - this.c2d.beginPath(); - this.c2d.moveTo(this.x(buf[0]), this.y(buf[1])); - } + // Start a new path... + if (buf.length > 1) { + this.c2d.beginPath(); + this.c2d.moveTo(this.x(buf[0]), this.y(buf[1])); + } - // ...and add points to it... - for (i = 2; i < points * 2; i = i + 2) { - this.c2d.lineTo(this.x(buf[i]), this.y(buf[i + 1])); - } + // ...and add points to it... + for (i = 2; i < points * 2; i = i + 2) { + this.c2d.lineTo(this.x(buf[i]), this.y(buf[i + 1])); + } - // ...before finally drawing it. - this.c2d.stroke(); + // ...before finally drawing it. + this.c2d.stroke(); }; Draw2D.prototype.drawSquare = function (min, max, color) { - const x1 = this.x(min[0]); - const y1 = this.y(min[1]); - const w = this.x(max[0]) - x1; - const h = this.y(max[1]) - y1; + const x1 = this.x(min[0]); + const y1 = this.y(min[1]); + const w = this.x(max[0]) - x1; + const h = this.y(max[1]) - y1; - this.setColor(color); - this.c2d.fillRect(x1, y1, w, h); + this.setColor(color); + this.c2d.fillRect(x1, y1, w, h); }; -Draw2D.prototype.drawPoints = function ( - buf, - color, - points, - pointSize, - shape -) { - const drawC2DShape = MARKER_SHAPES[shape].drawC2D.bind(this); +Draw2D.prototype.drawPoints = function (buf, color, points, pointSize, shape) { + const drawC2DShape = MARKER_SHAPES[shape].drawC2D.bind(this); - this.setColor(color); + this.setColor(color); - for (let i = 0; i < points; i++) { - drawC2DShape( - this.x(buf[i * 2]), - this.y(buf[i * 2 + 1]), - pointSize - ); - } + for (let i = 0; i < points; i++) { + drawC2DShape(this.x(buf[i * 2]), this.y(buf[i * 2 + 1]), pointSize); + } }; Draw2D.prototype.drawLimitPoint = function (x, y, size) { - this.c2d.fillRect(x + size, y, size, size); - this.c2d.fillRect(x, y + size, size, size); - this.c2d.fillRect(x - size, y, size, size); - this.c2d.fillRect(x, y - size, size, size); + this.c2d.fillRect(x + size, y, size, size); + this.c2d.fillRect(x, y + size, size, size); + this.c2d.fillRect(x - size, y, size, size); + this.c2d.fillRect(x, y - size, size, size); }; Draw2D.prototype.drawLimitPoints = function (points, color, pointSize) { - const limitSize = pointSize * 2; - const offset = limitSize / 2; + const limitSize = pointSize * 2; + const offset = limitSize / 2; - this.setColor(color); + this.setColor(color); - for (let i = 0; i < points.length; i++) { - this.drawLimitPoint( - this.x(points[i].x) - offset, - this.y(points[i].y) - offset, - limitSize - ); - } + for (let i = 0; i < points.length; i++) { + this.drawLimitPoint(this.x(points[i].x) - offset, this.y(points[i].y) - offset, limitSize); + } }; export default Draw2D; diff --git a/src/plugins/plot/draw/DrawLoader.js b/src/plugins/plot/draw/DrawLoader.js index 7b2ad388bc..d40a5e7c63 100644 --- a/src/plugins/plot/draw/DrawLoader.js +++ b/src/plugins/plot/draw/DrawLoader.js @@ -24,79 +24,75 @@ import DrawWebGL from './DrawWebGL'; import Draw2D from './Draw2D'; const CHARTS = [ - { - MAX_INSTANCES: 16, - API: DrawWebGL, - ALLOCATIONS: [] - }, - { - MAX_INSTANCES: Number.POSITIVE_INFINITY, - API: Draw2D, - ALLOCATIONS: [] - } + { + MAX_INSTANCES: 16, + API: DrawWebGL, + ALLOCATIONS: [] + }, + { + MAX_INSTANCES: Number.POSITIVE_INFINITY, + API: Draw2D, + ALLOCATIONS: [] + } ]; /** - * Draw loader attaches a draw API to a canvas element and returns the - * draw API. - */ + * Draw loader attaches a draw API to a canvas element and returns the + * draw API. + */ export const DrawLoader = { - /** + /** * Return the first draw API available. Returns * `undefined` if a draw API could not be constructed. *. * @param {CanvasElement} canvas - The canvas eelement to attach the draw API to. */ - getDrawAPI: function (canvas, overlay) { - let api; + getDrawAPI: function (canvas, overlay) { + let api; - CHARTS.forEach(function (CHART_TYPE) { - if (api) { - return; - } + CHARTS.forEach(function (CHART_TYPE) { + if (api) { + return; + } - if (CHART_TYPE.ALLOCATIONS.length - >= CHART_TYPE.MAX_INSTANCES) { - return; - } + if (CHART_TYPE.ALLOCATIONS.length >= CHART_TYPE.MAX_INSTANCES) { + return; + } - try { - api = new CHART_TYPE.API(canvas, overlay); - CHART_TYPE.ALLOCATIONS.push(api); - } catch (e) { - console.warn([ - "Could not instantiate chart", - CHART_TYPE.API.name, - ";", - e.message - ].join(" ")); - } - }); + try { + api = new CHART_TYPE.API(canvas, overlay); + CHART_TYPE.ALLOCATIONS.push(api); + } catch (e) { + console.warn( + ['Could not instantiate chart', CHART_TYPE.API.name, ';', e.message].join(' ') + ); + } + }); - if (!api) { - console.warn("Cannot initialize mct-chart."); - } - - return api; - }, - /** - * Returns a fallback draw api. - */ - getFallbackDrawAPI: function (canvas, overlay) { - const api = new CHARTS[1].API(canvas, overlay); - CHARTS[1].ALLOCATIONS.push(api); - - return api; - }, - releaseDrawAPI: function (api) { - CHARTS.forEach(function (CHART_TYPE) { - if (api instanceof CHART_TYPE.API) { - CHART_TYPE.ALLOCATIONS.splice(CHART_TYPE.ALLOCATIONS.indexOf(api), 1); - } - }); - if (api.destroy) { - api.destroy(); - } + if (!api) { + console.warn('Cannot initialize mct-chart.'); } + + return api; + }, + /** + * Returns a fallback draw api. + */ + getFallbackDrawAPI: function (canvas, overlay) { + const api = new CHARTS[1].API(canvas, overlay); + CHARTS[1].ALLOCATIONS.push(api); + + return api; + }, + releaseDrawAPI: function (api) { + CHARTS.forEach(function (CHART_TYPE) { + if (api instanceof CHART_TYPE.API) { + CHART_TYPE.ALLOCATIONS.splice(CHART_TYPE.ALLOCATIONS.indexOf(api), 1); + } + }); + if (api.destroy) { + api.destroy(); + } + } }; diff --git a/src/plugins/plot/draw/DrawWebGL.js b/src/plugins/plot/draw/DrawWebGL.js index db9fcd70b5..be3a93b862 100644 --- a/src/plugins/plot/draw/DrawWebGL.js +++ b/src/plugins/plot/draw/DrawWebGL.js @@ -83,124 +83,118 @@ const VERTEX_SHADER = ` * @throws {Error} an error is thrown if WebGL is unavailable. */ function DrawWebGL(canvas, overlay) { - this.canvas = canvas; - this.gl = this.canvas.getContext("webgl", { preserveDrawingBuffer: true }) - || this.canvas.getContext("experimental-webgl", { preserveDrawingBuffer: true }); + this.canvas = canvas; + this.gl = + this.canvas.getContext('webgl', { preserveDrawingBuffer: true }) || + this.canvas.getContext('experimental-webgl', { preserveDrawingBuffer: true }); - this.overlay = overlay; - this.c2d = overlay.getContext('2d'); - if (!this.c2d) { - throw new Error("No canvas 2d!"); - } + this.overlay = overlay; + this.c2d = overlay.getContext('2d'); + if (!this.c2d) { + throw new Error('No canvas 2d!'); + } - // Ensure a context was actually available before proceeding - if (!this.gl) { - throw new Error("WebGL unavailable."); - } + // Ensure a context was actually available before proceeding + if (!this.gl) { + throw new Error('WebGL unavailable.'); + } - this.initContext(); + this.initContext(); - this.listenTo(this.canvas, "webglcontextlost", this.onContextLost, this); + this.listenTo(this.canvas, 'webglcontextlost', this.onContextLost, this); } Object.assign(DrawWebGL.prototype, EventEmitter.prototype); eventHelpers.extend(DrawWebGL.prototype); DrawWebGL.prototype.onContextLost = function (event) { - this.emit('error'); - this.isContextLost = true; - this.destroy(); - // TODO re-initialize and re-draw on context restored + this.emit('error'); + this.isContextLost = true; + this.destroy(); + // TODO re-initialize and re-draw on context restored }; DrawWebGL.prototype.initContext = function () { - // Initialize shaders - this.vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER); - this.gl.shaderSource(this.vertexShader, VERTEX_SHADER); - this.gl.compileShader(this.vertexShader); + // Initialize shaders + this.vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER); + this.gl.shaderSource(this.vertexShader, VERTEX_SHADER); + this.gl.compileShader(this.vertexShader); - this.fragmentShader = this.gl.createShader(this.gl.FRAGMENT_SHADER); - this.gl.shaderSource(this.fragmentShader, FRAGMENT_SHADER); - this.gl.compileShader(this.fragmentShader); + this.fragmentShader = this.gl.createShader(this.gl.FRAGMENT_SHADER); + this.gl.shaderSource(this.fragmentShader, FRAGMENT_SHADER); + this.gl.compileShader(this.fragmentShader); - // Assemble vertex/fragment shaders into programs - this.program = this.gl.createProgram(); - this.gl.attachShader(this.program, this.vertexShader); - this.gl.attachShader(this.program, this.fragmentShader); - this.gl.linkProgram(this.program); - this.gl.useProgram(this.program); + // Assemble vertex/fragment shaders into programs + this.program = this.gl.createProgram(); + this.gl.attachShader(this.program, this.vertexShader); + this.gl.attachShader(this.program, this.fragmentShader); + this.gl.linkProgram(this.program); + this.gl.useProgram(this.program); - // Get locations for attribs/uniforms from the - // shader programs (to pass values into shaders at draw-time) - this.aVertexPosition = this.gl.getAttribLocation(this.program, "aVertexPosition"); - this.uColor = this.gl.getUniformLocation(this.program, "uColor"); - this.uMarkerShape = this.gl.getUniformLocation(this.program, "uMarkerShape"); - this.uDimensions = this.gl.getUniformLocation(this.program, "uDimensions"); - this.uOrigin = this.gl.getUniformLocation(this.program, "uOrigin"); - this.uPointSize = this.gl.getUniformLocation(this.program, "uPointSize"); + // Get locations for attribs/uniforms from the + // shader programs (to pass values into shaders at draw-time) + this.aVertexPosition = this.gl.getAttribLocation(this.program, 'aVertexPosition'); + this.uColor = this.gl.getUniformLocation(this.program, 'uColor'); + this.uMarkerShape = this.gl.getUniformLocation(this.program, 'uMarkerShape'); + this.uDimensions = this.gl.getUniformLocation(this.program, 'uDimensions'); + this.uOrigin = this.gl.getUniformLocation(this.program, 'uOrigin'); + this.uPointSize = this.gl.getUniformLocation(this.program, 'uPointSize'); - this.gl.enableVertexAttribArray(this.aVertexPosition); + this.gl.enableVertexAttribArray(this.aVertexPosition); - // Create a buffer to holds points which will be drawn - this.buffer = this.gl.createBuffer(); - - // Enable blending, for smoothness - this.gl.enable(this.gl.BLEND); - this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA); + // Create a buffer to holds points which will be drawn + this.buffer = this.gl.createBuffer(); + // Enable blending, for smoothness + this.gl.enable(this.gl.BLEND); + this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA); }; DrawWebGL.prototype.destroy = function () { - this.stopListening(); + this.stopListening(); }; // Convert from logical to physical x coordinates DrawWebGL.prototype.x = function (v) { - return ((v - this.origin[0]) / this.dimensions[0]) * this.width; + return ((v - this.origin[0]) / this.dimensions[0]) * this.width; }; // Convert from logical to physical y coordinates DrawWebGL.prototype.y = function (v) { - return this.height - - ((v - this.origin[1]) / this.dimensions[1]) * this.height; + return this.height - ((v - this.origin[1]) / this.dimensions[1]) * this.height; }; DrawWebGL.prototype.doDraw = function (drawType, buf, color, points, shape) { - if (this.isContextLost) { - return; - } + if (this.isContextLost) { + return; + } - const shapeCode = MARKER_SHAPES[shape] ? MARKER_SHAPES[shape].drawWebGL : 0; + const shapeCode = MARKER_SHAPES[shape] ? MARKER_SHAPES[shape].drawWebGL : 0; - this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer); - this.gl.bufferData(this.gl.ARRAY_BUFFER, buf, this.gl.DYNAMIC_DRAW); - this.gl.vertexAttribPointer(this.aVertexPosition, 2, this.gl.FLOAT, false, 0, 0); - this.gl.uniform4fv(this.uColor, color); - this.gl.uniform1i(this.uMarkerShape, shapeCode); - if (points !== 0) { - this.gl.drawArrays(drawType, 0, points); - } + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer); + this.gl.bufferData(this.gl.ARRAY_BUFFER, buf, this.gl.DYNAMIC_DRAW); + this.gl.vertexAttribPointer(this.aVertexPosition, 2, this.gl.FLOAT, false, 0, 0); + this.gl.uniform4fv(this.uColor, color); + this.gl.uniform1i(this.uMarkerShape, shapeCode); + if (points !== 0) { + this.gl.drawArrays(drawType, 0, points); + } }; DrawWebGL.prototype.clear = function () { - if (this.isContextLost) { - return; - } + if (this.isContextLost) { + return; + } - this.height = this.canvas.height = this.canvas.offsetHeight; - this.width = this.canvas.width = this.canvas.offsetWidth; - this.overlay.height = this.overlay.offsetHeight; - this.overlay.width = this.overlay.offsetWidth; - // Set the viewport size; note that we use the width/height - // that our WebGL context reports, which may be lower - // resolution than the canvas we requested. - this.gl.viewport( - 0, - 0, - this.gl.drawingBufferWidth, - this.gl.drawingBufferHeight - ); - this.gl.clear(this.gl.COLOR_BUFFER_BIT + this.gl.DEPTH_BUFFER_BIT); + this.height = this.canvas.height = this.canvas.offsetHeight; + this.width = this.canvas.width = this.canvas.offsetWidth; + this.overlay.height = this.overlay.offsetHeight; + this.overlay.width = this.overlay.offsetWidth; + // Set the viewport size; note that we use the width/height + // that our WebGL context reports, which may be lower + // resolution than the canvas we requested. + this.gl.viewport(0, 0, this.gl.drawingBufferWidth, this.gl.drawingBufferHeight); + this.gl.clear(this.gl.COLOR_BUFFER_BIT + this.gl.DEPTH_BUFFER_BIT); }; /** @@ -211,17 +205,16 @@ DrawWebGL.prototype.clear = function () { * origin of the chart */ DrawWebGL.prototype.setDimensions = function (dimensions, origin) { - this.dimensions = dimensions; - this.origin = origin; - if (this.isContextLost) { - return; - } + this.dimensions = dimensions; + this.origin = origin; + if (this.isContextLost) { + return; + } - if (dimensions && dimensions.length > 0 - && origin && origin.length > 0) { - this.gl.uniform2fv(this.uDimensions, dimensions); - this.gl.uniform2fv(this.uOrigin, origin); - } + if (dimensions && dimensions.length > 0 && origin && origin.length > 0) { + this.gl.uniform2fv(this.uDimensions, dimensions); + this.gl.uniform2fv(this.uOrigin, origin); + } }; /** @@ -235,11 +228,11 @@ DrawWebGL.prototype.setDimensions = function (dimensions, origin) { * @param {number} points the number of points to draw */ DrawWebGL.prototype.drawLine = function (buf, color, points) { - if (this.isContextLost) { - return; - } + if (this.isContextLost) { + return; + } - this.doDraw(this.gl.LINE_STRIP, buf, color, points); + this.doDraw(this.gl.LINE_STRIP, buf, color, points); }; /** @@ -247,12 +240,12 @@ DrawWebGL.prototype.drawLine = function (buf, color, points) { * */ DrawWebGL.prototype.drawPoints = function (buf, color, points, pointSize, shape) { - if (this.isContextLost) { - return; - } + if (this.isContextLost) { + return; + } - this.gl.uniform1f(this.uPointSize, pointSize); - this.doDraw(this.gl.POINTS, buf, color, points, shape); + this.gl.uniform1f(this.uPointSize, pointSize); + this.doDraw(this.gl.POINTS, buf, color, points, shape); }; /** @@ -265,39 +258,40 @@ DrawWebGL.prototype.drawPoints = function (buf, color, points, pointSize, shape) * is in the range of 0.0-1.0 */ DrawWebGL.prototype.drawSquare = function (min, max, color) { - if (this.isContextLost) { - return; - } + if (this.isContextLost) { + return; + } - this.doDraw(this.gl.TRIANGLE_FAN, new Float32Array( - min.concat([min[0], max[1]]).concat(max).concat([max[0], min[1]]) - ), color, 4); + this.doDraw( + this.gl.TRIANGLE_FAN, + new Float32Array(min.concat([min[0], max[1]]).concat(max).concat([max[0], min[1]])), + color, + 4 + ); }; DrawWebGL.prototype.drawLimitPoint = function (x, y, size) { - this.c2d.fillRect(x + size, y, size, size); - this.c2d.fillRect(x, y + size, size, size); - this.c2d.fillRect(x - size, y, size, size); - this.c2d.fillRect(x, y - size, size, size); + this.c2d.fillRect(x + size, y, size, size); + this.c2d.fillRect(x, y + size, size, size); + this.c2d.fillRect(x - size, y, size, size); + this.c2d.fillRect(x, y - size, size, size); }; DrawWebGL.prototype.drawLimitPoints = function (points, color, pointSize) { - const limitSize = pointSize * 2; - const offset = limitSize / 2; + const limitSize = pointSize * 2; + const offset = limitSize / 2; - const mappedColor = color.map(function (c, i) { - return i < 3 ? Math.floor(c * 255) : (c); - }).join(','); - this.c2d.strokeStyle = "rgba(" + mappedColor + ")"; - this.c2d.fillStyle = "rgba(" + mappedColor + ")"; + const mappedColor = color + .map(function (c, i) { + return i < 3 ? Math.floor(c * 255) : c; + }) + .join(','); + this.c2d.strokeStyle = 'rgba(' + mappedColor + ')'; + this.c2d.fillStyle = 'rgba(' + mappedColor + ')'; - for (let i = 0; i < points.length; i++) { - this.drawLimitPoint( - this.x(points[i].x) - offset, - this.y(points[i].y) - offset, - limitSize - ); - } + for (let i = 0; i < points.length; i++) { + this.drawLimitPoint(this.x(points[i].x) - offset, this.y(points[i].y) - offset, limitSize); + } }; export default DrawWebGL; diff --git a/src/plugins/plot/draw/MarkerShapes.js b/src/plugins/plot/draw/MarkerShapes.js index b9edd31f24..532da9efaa 100644 --- a/src/plugins/plot/draw/MarkerShapes.js +++ b/src/plugins/plot/draw/MarkerShapes.js @@ -21,66 +21,66 @@ *****************************************************************************/ /** - * @label string (required) display name of shape - * @drawWebGL integer (unique, required) index provided to WebGL Fragment Shader - * @drawC2D function (required) canvas2d draw function - */ + * @label string (required) display name of shape + * @drawWebGL integer (unique, required) index provided to WebGL Fragment Shader + * @drawC2D function (required) canvas2d draw function + */ export const MARKER_SHAPES = { - point: { - label: 'Point', - drawWebGL: 1, - drawC2D: function (x, y, size) { - const offset = size / 2; + point: { + label: 'Point', + drawWebGL: 1, + drawC2D: function (x, y, size) { + const offset = size / 2; - this.c2d.fillRect(x - offset, y - offset, size, size); - } - }, - circle: { - label: 'Circle', - drawWebGL: 2, - drawC2D: function (x, y, size) { - const radius = size / 2; - - this.c2d.beginPath(); - this.c2d.arc(x, y, radius, 0, 2 * Math.PI, false); - this.c2d.closePath(); - this.c2d.fill(); - } - }, - diamond: { - label: 'Diamond', - drawWebGL: 3, - drawC2D: function (x, y, size) { - const offset = size / 2; - const top = [x, y + offset]; - const right = [x + offset, y]; - const bottom = [x, y - offset]; - const left = [x - offset, y]; - - this.c2d.beginPath(); - this.c2d.moveTo(...top); - this.c2d.lineTo(...right); - this.c2d.lineTo(...bottom); - this.c2d.lineTo(...left); - this.c2d.closePath(); - this.c2d.fill(); - } - }, - triangle: { - label: 'Triangle', - drawWebGL: 4, - drawC2D: function (x, y, size) { - const offset = size / 2; - const v1 = [x, y - offset]; - const v2 = [x - offset, y + offset]; - const v3 = [x + offset, y + offset]; - - this.c2d.beginPath(); - this.c2d.moveTo(...v1); - this.c2d.lineTo(...v2); - this.c2d.lineTo(...v3); - this.c2d.closePath(); - this.c2d.fill(); - } + this.c2d.fillRect(x - offset, y - offset, size, size); } + }, + circle: { + label: 'Circle', + drawWebGL: 2, + drawC2D: function (x, y, size) { + const radius = size / 2; + + this.c2d.beginPath(); + this.c2d.arc(x, y, radius, 0, 2 * Math.PI, false); + this.c2d.closePath(); + this.c2d.fill(); + } + }, + diamond: { + label: 'Diamond', + drawWebGL: 3, + drawC2D: function (x, y, size) { + const offset = size / 2; + const top = [x, y + offset]; + const right = [x + offset, y]; + const bottom = [x, y - offset]; + const left = [x - offset, y]; + + this.c2d.beginPath(); + this.c2d.moveTo(...top); + this.c2d.lineTo(...right); + this.c2d.lineTo(...bottom); + this.c2d.lineTo(...left); + this.c2d.closePath(); + this.c2d.fill(); + } + }, + triangle: { + label: 'Triangle', + drawWebGL: 4, + drawC2D: function (x, y, size) { + const offset = size / 2; + const v1 = [x, y - offset]; + const v2 = [x - offset, y + offset]; + const v3 = [x + offset, y + offset]; + + this.c2d.beginPath(); + this.c2d.moveTo(...v1); + this.c2d.lineTo(...v2); + this.c2d.lineTo(...v3); + this.c2d.closePath(); + this.c2d.fill(); + } + } }; diff --git a/src/plugins/plot/inspector/PlotOptions.vue b/src/plugins/plot/inspector/PlotOptions.vue index a4bbcccebf..5cf131dee9 100644 --- a/src/plugins/plot/inspector/PlotOptions.vue +++ b/src/plugins/plot/inspector/PlotOptions.vue @@ -20,45 +20,45 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/inspector/PlotOptionsBrowse.vue b/src/plugins/plot/inspector/PlotOptionsBrowse.vue index 75c07e5311..2ef301e256 100644 --- a/src/plugins/plot/inspector/PlotOptionsBrowse.vue +++ b/src/plugins/plot/inspector/PlotOptionsBrowse.vue @@ -20,290 +20,255 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/inspector/PlotOptionsEdit.vue b/src/plugins/plot/inspector/PlotOptionsEdit.vue index 76524a41e3..83d16391ba 100644 --- a/src/plugins/plot/inspector/PlotOptionsEdit.vue +++ b/src/plugins/plot/inspector/PlotOptionsEdit.vue @@ -20,218 +20,201 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/inspector/PlotOptionsItem.vue b/src/plugins/plot/inspector/PlotOptionsItem.vue index c290cf5b40..37da490c96 100644 --- a/src/plugins/plot/inspector/PlotOptionsItem.vue +++ b/src/plugins/plot/inspector/PlotOptionsItem.vue @@ -20,182 +20,168 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/inspector/PlotsInspectorViewProvider.js b/src/plugins/plot/inspector/PlotsInspectorViewProvider.js index 0f39c36d9d..82f0bca377 100644 --- a/src/plugins/plot/inspector/PlotsInspectorViewProvider.js +++ b/src/plugins/plot/inspector/PlotsInspectorViewProvider.js @@ -1,59 +1,58 @@ - -import PlotOptions from "./PlotOptions.vue"; +import PlotOptions from './PlotOptions.vue'; import Vue from 'vue'; export default function PlotsInspectorViewProvider(openmct) { - return { - key: 'plots-inspector', - name: 'Config', - canView: function (selection) { - if (selection.length === 0 || selection[0].length === 0) { - return false; - } + return { + key: 'plots-inspector', + name: 'Config', + canView: function (selection) { + if (selection.length === 0 || selection[0].length === 0) { + return false; + } - let object = selection[0][0].context.item; - let parent = selection[0].length > 1 && selection[0][1].context.item; + let object = selection[0][0].context.item; + let 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'; + const isOverlayPlotObject = object && object.type === 'telemetry.plot.overlay'; + const isParentStackedPlotObject = parent && parent.type === 'telemetry.plot.stacked'; - return isOverlayPlotObject || isParentStackedPlotObject; + 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: '' + }); }, - 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: '' - }); - }, - priority: function () { - return openmct.priority.HIGH + 1; - }, - destroy: function () { - if (component) { - component.$destroy(); - component = undefined; - } - } - }; + priority: function () { + return openmct.priority.HIGH + 1; + }, + destroy: function () { + if (component) { + component.$destroy(); + component = undefined; + } } - }; + }; + } + }; } diff --git a/src/plugins/plot/inspector/StackedPlotsInspectorViewProvider.js b/src/plugins/plot/inspector/StackedPlotsInspectorViewProvider.js index 249bb56c6a..26a4306c28 100644 --- a/src/plugins/plot/inspector/StackedPlotsInspectorViewProvider.js +++ b/src/plugins/plot/inspector/StackedPlotsInspectorViewProvider.js @@ -1,57 +1,56 @@ - -import PlotOptions from "./PlotOptions.vue"; +import PlotOptions from './PlotOptions.vue'; import Vue from 'vue'; export default function StackedPlotsInspectorViewProvider(openmct) { - return { - key: 'stacked-plots-inspector', - name: 'Config', - canView: function (selection) { - if (selection.length === 0 || selection[0].length === 0) { - return false; - } + return { + key: 'stacked-plots-inspector', + name: 'Config', + canView: function (selection) { + if (selection.length === 0 || selection[0].length === 0) { + return false; + } - const object = selection[0][0].context.item; + const object = selection[0][0].context.item; - const isStackedPlotObject = object && object.type === 'telemetry.plot.stacked'; + const isStackedPlotObject = object && object.type === 'telemetry.plot.stacked'; - return isStackedPlotObject; + return isStackedPlotObject; + }, + 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: '' + }); }, - 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: '' - }); - }, - priority: function () { - return openmct.priority.HIGH + 1; - }, - destroy: function () { - if (component) { - component.$destroy(); - component = undefined; - } - } - }; + priority: function () { + return openmct.priority.HIGH + 1; + }, + destroy: function () { + if (component) { + component.$destroy(); + component = undefined; + } } - }; + }; + } + }; } diff --git a/src/plugins/plot/inspector/forms/LegendForm.vue b/src/plugins/plot/inspector/forms/LegendForm.vue index f29d8ebf81..5c72b1a382 100644 --- a/src/plugins/plot/inspector/forms/LegendForm.vue +++ b/src/plugins/plot/inspector/forms/LegendForm.vue @@ -20,223 +20,229 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/inspector/forms/SeriesForm.vue b/src/plugins/plot/inspector/forms/SeriesForm.vue index 5dc637c8c0..39f7e8e10a 100644 --- a/src/plugins/plot/inspector/forms/SeriesForm.vue +++ b/src/plugins/plot/inspector/forms/SeriesForm.vue @@ -20,390 +20,336 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/inspector/forms/YAxisForm.vue b/src/plugins/plot/inspector/forms/YAxisForm.vue index 834235bb09..d9f57b402f 100644 --- a/src/plugins/plot/inspector/forms/YAxisForm.vue +++ b/src/plugins/plot/inspector/forms/YAxisForm.vue @@ -20,340 +20,324 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/inspector/forms/formUtil.js b/src/plugins/plot/inspector/forms/formUtil.js index 661d9c4b9b..144ae45648 100644 --- a/src/plugins/plot/inspector/forms/formUtil.js +++ b/src/plugins/plot/inspector/forms/formUtil.js @@ -1,19 +1,19 @@ export function coerce(value, coerceFunc) { - if (coerceFunc) { - return coerceFunc(value); - } + if (coerceFunc) { + return coerceFunc(value); + } - return value; + return value; } export function validate(value, model, validateFunc) { - if (validateFunc) { - return validateFunc(value, model); - } + if (validateFunc) { + return validateFunc(value, model); + } - return true; + return true; } export function objectPath(path) { - return path && typeof path !== 'function' ? () => path : path; + return path && typeof path !== 'function' ? () => path : path; } diff --git a/src/plugins/plot/legend/PlotLegend.vue b/src/plugins/plot/legend/PlotLegend.vue index 75d6787944..9c7e581c28 100644 --- a/src/plugins/plot/legend/PlotLegend.vue +++ b/src/plugins/plot/legend/PlotLegend.vue @@ -20,228 +20,204 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/legend/PlotLegendItemCollapsed.vue b/src/plugins/plot/legend/PlotLegendItemCollapsed.vue index 26ab30764b..f87c24976b 100644 --- a/src/plugins/plot/legend/PlotLegendItemCollapsed.vue +++ b/src/plugins/plot/legend/PlotLegendItemCollapsed.vue @@ -20,155 +20,171 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/legend/PlotLegendItemExpanded.vue b/src/plugins/plot/legend/PlotLegendItemExpanded.vue index b1cd427ecd..ad2e8270b9 100644 --- a/src/plugins/plot/legend/PlotLegendItemExpanded.vue +++ b/src/plugins/plot/legend/PlotLegendItemExpanded.vue @@ -20,190 +20,192 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/lib/eventHelpers.js b/src/plugins/plot/lib/eventHelpers.js index 90b5dedecf..83b4e20944 100644 --- a/src/plugins/plot/lib/eventHelpers.js +++ b/src/plugins/plot/lib/eventHelpers.js @@ -22,71 +22,72 @@ /*jscs:disable disallowDanglingUnderscores */ const helperFunctions = { - listenTo: function (object, event, callback, context) { - if (!this._listeningTo) { - this._listeningTo = []; - } - - const listener = { - object: object, - event: event, - callback: callback, - context: context, - _cb: context ? callback.bind(context) : callback - }; - if (object.addEventListener) { - object.addEventListener(event, listener._cb); - } else { - object.on(event, listener._cb); - } - - this._listeningTo.push(listener); - }, - - stopListening: function (object, event, callback, context) { - if (!this._listeningTo) { - this._listeningTo = []; - } - - this._listeningTo.filter(function (listener) { - if (object && object !== listener.object) { - return false; - } - - if (event && event !== listener.event) { - return false; - } - - if (callback && callback !== listener.callback) { - return false; - } - - if (context && context !== listener.context) { - return false; - } - - return true; - }) - .map(function (listener) { - if (listener.unlisten) { - listener.unlisten(); - } else if (listener.object.removeEventListener) { - listener.object.removeEventListener(listener.event, listener._cb); - } else { - listener.object.off(listener.event, listener._cb); - } - - return listener; - }) - .forEach(function (listener) { - this._listeningTo.splice(this._listeningTo.indexOf(listener), 1); - }, this); - }, - - extend: function (object) { - object.listenTo = helperFunctions.listenTo; - object.stopListening = helperFunctions.stopListening; + listenTo: function (object, event, callback, context) { + if (!this._listeningTo) { + this._listeningTo = []; } + + const listener = { + object: object, + event: event, + callback: callback, + context: context, + _cb: context ? callback.bind(context) : callback + }; + if (object.addEventListener) { + object.addEventListener(event, listener._cb); + } else { + object.on(event, listener._cb); + } + + this._listeningTo.push(listener); + }, + + stopListening: function (object, event, callback, context) { + if (!this._listeningTo) { + this._listeningTo = []; + } + + this._listeningTo + .filter(function (listener) { + if (object && object !== listener.object) { + return false; + } + + if (event && event !== listener.event) { + return false; + } + + if (callback && callback !== listener.callback) { + return false; + } + + if (context && context !== listener.context) { + return false; + } + + return true; + }) + .map(function (listener) { + if (listener.unlisten) { + listener.unlisten(); + } else if (listener.object.removeEventListener) { + listener.object.removeEventListener(listener.event, listener._cb); + } else { + listener.object.off(listener.event, listener._cb); + } + + return listener; + }) + .forEach(function (listener) { + this._listeningTo.splice(this._listeningTo.indexOf(listener), 1); + }, this); + }, + + extend: function (object) { + object.listenTo = helperFunctions.listenTo; + object.stopListening = helperFunctions.stopListening; + } }; export default helperFunctions; diff --git a/src/plugins/plot/mathUtils.js b/src/plugins/plot/mathUtils.js index 38dc356187..c9f4f30de8 100644 --- a/src/plugins/plot/mathUtils.js +++ b/src/plugins/plot/mathUtils.js @@ -8,11 +8,11 @@ Returns the logarithm of a number, using the given base or the natural number @param {number=} base log base, defaults to e */ export function log(n, base = e) { - if (base === e) { - return Math.log(n); - } + if (base === e) { + return Math.log(n); + } - return Math.log(n) / Math.log(base); + return Math.log(n) / Math.log(base); } /** @@ -22,7 +22,7 @@ natural number `e` as base if not specified. @param {number=} base log base, defaults to e */ export function antilog(n, base = e) { - return Math.pow(base, n); + return Math.pow(base, n); } /** @@ -31,7 +31,7 @@ A symmetric logarithm function. See https://github.com/nasa/openmct/issues/2297# @param {number=} base log base, defaults to e */ export function symlog(n, base = e) { - return Math.sign(n) * log(Math.abs(n) + 1, base); + return Math.sign(n) * log(Math.abs(n) + 1, base); } /** @@ -40,5 +40,5 @@ An inverse symmetric logarithm function. See https://github.com/nasa/openmct/iss @param {number=} base log base, defaults to e */ export function antisymlog(n, base = e) { - return Math.sign(n) * (antilog(Math.abs(n), base) - 1); + return Math.sign(n) * (antilog(Math.abs(n), base) - 1); } diff --git a/src/plugins/plot/overlayPlot/OverlayPlotCompositionPolicy.js b/src/plugins/plot/overlayPlot/OverlayPlotCompositionPolicy.js index e769d0181b..ca5185e8b0 100644 --- a/src/plugins/plot/overlayPlot/OverlayPlotCompositionPolicy.js +++ b/src/plugins/plot/overlayPlot/OverlayPlotCompositionPolicy.js @@ -1,29 +1,29 @@ export default function OverlayPlotCompositionPolicy(openmct) { - function hasNumericTelemetry(domainObject) { - const hasTelemetry = openmct.telemetry.isTelemetryObject(domainObject); - if (!hasTelemetry) { - return false; - } - - let metadata = openmct.telemetry.getMetadata(domainObject); - - return metadata.values().length > 0 && hasDomainAndRange(metadata); + function hasNumericTelemetry(domainObject) { + const hasTelemetry = openmct.telemetry.isTelemetryObject(domainObject); + if (!hasTelemetry) { + return false; } - function hasDomainAndRange(metadata) { - return (metadata.valuesForHints(['range']).length > 0 - && metadata.valuesForHints(['domain']).length > 0); + let metadata = openmct.telemetry.getMetadata(domainObject); + + return metadata.values().length > 0 && hasDomainAndRange(metadata); + } + + function hasDomainAndRange(metadata) { + return ( + metadata.valuesForHints(['range']).length > 0 && + metadata.valuesForHints(['domain']).length > 0 + ); + } + + return { + allow: function (parent, child) { + if (parent.type === 'telemetry.plot.overlay' && hasNumericTelemetry(child) === false) { + return false; + } + + return true; } - - return { - allow: function (parent, child) { - - if (parent.type === 'telemetry.plot.overlay' - && (hasNumericTelemetry(child) === false)) { - return false; - } - - return true; - } - }; + }; } diff --git a/src/plugins/plot/overlayPlot/OverlayPlotViewProvider.js b/src/plugins/plot/overlayPlot/OverlayPlotViewProvider.js index 09ba5cf080..87d4a0f3d6 100644 --- a/src/plugins/plot/overlayPlot/OverlayPlotViewProvider.js +++ b/src/plugins/plot/overlayPlot/OverlayPlotViewProvider.js @@ -24,62 +24,62 @@ import Plot from '../Plot.vue'; import Vue from 'vue'; export default function OverlayPlotViewProvider(openmct) { - function isCompactView(objectPath) { - let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip'); + function isCompactView(objectPath) { + let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip'); - return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); - } + return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); + } - return { - key: 'plot-overlay', - name: 'Overlay Plot', - cssClass: 'icon-telemetry', - canView(domainObject, objectPath) { - return domainObject.type === 'telemetry.plot.overlay'; - }, + return { + key: 'plot-overlay', + name: 'Overlay Plot', + cssClass: 'icon-telemetry', + canView(domainObject, objectPath) { + return domainObject.type === 'telemetry.plot.overlay'; + }, - canEdit(domainObject, objectPath) { - return domainObject.type === 'telemetry.plot.overlay'; - }, + canEdit(domainObject, objectPath) { + return domainObject.type === 'telemetry.plot.overlay'; + }, - view: function (domainObject, objectPath) { - let component; + view: function (domainObject, objectPath) { + let component; - return { - show: function (element) { - let isCompact = isCompactView(objectPath); - component = new Vue({ - el: element, - components: { - Plot - }, - provide: { - openmct, - domainObject, - path: objectPath - }, - data() { - return { - options: { - compact: isCompact - } - }; - }, - template: '' - }); - }, - getViewContext() { - if (!component) { - return {}; - } - - return component.$refs.plotComponent.getViewContext(); - }, - destroy: function () { - component.$destroy(); - component = undefined; + return { + show: function (element) { + let isCompact = isCompactView(objectPath); + component = new Vue({ + el: element, + components: { + Plot + }, + provide: { + openmct, + domainObject, + path: objectPath + }, + data() { + return { + options: { + compact: isCompact } - }; + }; + }, + template: '' + }); + }, + getViewContext() { + if (!component) { + return {}; + } + + return component.$refs.plotComponent.getViewContext(); + }, + destroy: function () { + component.$destroy(); + component = undefined; } - }; + }; + } + }; } diff --git a/src/plugins/plot/overlayPlot/pluginSpec.js b/src/plugins/plot/overlayPlot/pluginSpec.js index dcb0eaab9b..27b215f121 100644 --- a/src/plugins/plot/overlayPlot/pluginSpec.js +++ b/src/plugins/plot/overlayPlot/pluginSpec.js @@ -20,485 +20,502 @@ * 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 Plot from "../Plot.vue"; -import configStore from "../configuration/ConfigStore"; -import EventEmitter from "EventEmitter"; -import PlotOptions from "../inspector/PlotOptions.vue"; +import { + createMouseEvent, + createOpenMct, + resetApplicationState, + spyOnBuiltins +} from 'utils/testing'; +import PlotVuePlugin from '../plugin'; +import Vue from 'vue'; +import Plot from '../Plot.vue'; +import configStore from '../configuration/ConfigStore'; +import EventEmitter from 'EventEmitter'; +import PlotOptions from '../inspector/PlotOptions.vue'; -describe("the plugin", function () { - let element; - let child; - let openmct; - let telemetryPromise; - let telemetryPromiseResolve; - let mockObjectPath; - let overlayPlotObject = { +describe('the plugin', function () { + let element; + let child; + let openmct; + let telemetryPromise; + let telemetryPromiseResolve; + let mockObjectPath; + let overlayPlotObject = { + identifier: { + namespace: '', + key: 'test-plot' + }, + type: 'telemetry.plot.overlay', + name: 'Test Overlay Plot', + composition: [], + configuration: { + series: [] + } + }; + + beforeEach((done) => { + mockObjectPath = [ + { + name: 'mock folder', + type: 'fake-folder', identifier: { - namespace: "", - key: "test-plot" - }, - type: "telemetry.plot.overlay", - name: "Test Overlay Plot", - composition: [], - configuration: { - series: [] + 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', + 'some-key2': 'some-value2 1', + 'some-other-key2': 'some-other-value2 1' + }, + { + utc: 2, + 'some-key': 'some-value 2', + 'some-other-key': 'some-other-value 2', + 'some-key2': 'some-value2 2', + 'some-other-key2': 'some-other-value2 2' + }, + { + utc: 3, + 'some-key': 'some-value 3', + 'some-other-key': 'some-other-value 3', + 'some-key2': 'some-value2 2', + 'some-other-key2': 'some-other-value2 2' + } + ]; + + const timeSystem = { + timeSystemKey: 'utc', + bounds: { + start: 0, + end: 4 + } }; - 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', - 'some-key2': 'some-value2 1', - 'some-other-key2': 'some-other-value2 1' - }, - { - 'utc': 2, - 'some-key': 'some-value 2', - 'some-other-key': 'some-other-value 2', - 'some-key2': 'some-value2 2', - 'some-other-key2': 'some-other-value2 2' - }, - { - 'utc': 3, - 'some-key': 'some-value 3', - 'some-other-key': 'some-other-value 3', - 'some-key2': 'some-value2 2', - 'some-other-key2': 'some-other-value2 2' - } - ]; + openmct = createOpenMct(timeSystem); - 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 = [overlayPlotObject]; - openmct.on("start", done); - openmct.startHeadless(); + telemetryPromise = new Promise((resolve) => { + telemetryPromiseResolve = resolve; }); - afterEach((done) => { - openmct.time.timeSystem('utc', { - start: 0, - end: 1 - }); - configStore.deleteAll(); - resetApplicationState(openmct).then(done).catch(done); + 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 = [overlayPlotObject]; + 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 an overlay plot view for objects with telemetry', () => { + const testTelemetryObject = { + id: 'test-object', + type: 'telemetry.plot.overlay', + telemetry: { + values: [ + { + key: 'some-key' + } + ] + } + }; + + const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); + let plotView = applicableViews.find((viewProvider) => viewProvider.key === 'plot-overlay'); + expect(plotView).toBeDefined(); + }); + }); + + describe('The overlay plot view with multiple axes', () => { + let testTelemetryObject; + let testTelemetryObject2; + let config; + let component; + let mockComposition; + afterAll(() => { + component.$destroy(); + 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 + } + } + ] + } + }; + + 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 + } + } + ] + } + }; + overlayPlotObject.composition = [ + { + identifier: testTelemetryObject.identifier + }, + { + identifier: testTelemetryObject2.identifier + } + ]; + overlayPlotObject.configuration.series = [ + { + identifier: testTelemetryObject.identifier, + yAxisId: 1 + }, + { + identifier: testTelemetryObject2.identifier, + yAxisId: 3 + } + ]; + overlayPlotObject.configuration.additionalYAxes = [ + { + label: 'Test Object Label', + id: 2 + }, + { + label: 'Test Object 2 Label', + id: 3 + } + ]; + mockComposition = new EventEmitter(); + mockComposition.load = () => { + mockComposition.emit('add', testTelemetryObject); + mockComposition.emit('add', testTelemetryObject2); + + return [testTelemetryObject, testTelemetryObject2]; + }; + + spyOn(openmct.composition, 'get').and.returnValue(mockComposition); + + let viewContainer = document.createElement('div'); + child.append(viewContainer); + component = new Vue({ + el: viewContainer, + components: { + Plot + }, + provide: { + openmct: openmct, + domainObject: overlayPlotObject, + composition: openmct.composition.get(overlayPlotObject), + path: [overlayPlotObject] + }, + template: '' + }); + + return telemetryPromise.then(Vue.nextTick()).then(() => { + const configId = openmct.objects.makeKeyString(overlayPlotObject.identifier); + config = configStore.get(configId); + }); + }); + + it('Renders multiple Y-axis for the telemetry objects', (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(2); + done(); + }); + }); + + describe('the inspector view', () => { + let inspectorComponent; + let viewComponentObject; + let selection; + beforeEach((done) => { + selection = [ + [ + { + context: { + item: { + id: overlayPlotObject.identifier.key, + identifier: overlayPlotObject.identifier, + type: overlayPlotObject.type, + configuration: overlayPlotObject.configuration, + composition: overlayPlotObject.composition + } + } + } + ] + ]; + + let viewContainer = document.createElement('div'); + child.append(viewContainer); + inspectorComponent = new Vue({ + el: viewContainer, + components: { + PlotOptions + }, + provide: { + openmct: openmct, + domainObject: selection[0][0].context.item, + path: [selection[0][0].context.item] + }, + template: '' + }); + + Vue.nextTick(() => { + viewComponentObject = inspectorComponent.$root.$children[0]; + done(); + }); + }); + + afterEach(() => { openmct.router.path = null; + }); + + describe('in edit mode', () => { + let editOptionsEl; + + beforeEach((done) => { + viewComponentObject.setEditState(true); + Vue.nextTick(() => { + editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit'); + done(); + }); + }); + + it('shows multiple yAxis options', () => { + const yAxisProperties = editOptionsEl.querySelectorAll( + '.js-yaxis-grid-properties .l-inspector-part h2' + ); + expect(yAxisProperties.length).toEqual(2); + }); + + it('saves yAxis options', () => { + //toggle log mode and save + config.additionalYAxes[1].set('displayRange', { + min: 10, + max: 20 + }); + const yAxisProperties = editOptionsEl.querySelectorAll('.js-log-mode-input'); + const clickEvent = createMouseEvent('click'); + yAxisProperties[1].dispatchEvent(clickEvent); + + expect(config.additionalYAxes[1].get('logMode')).toEqual(true); + }); + }); + }); + }); + + describe('The overlay plot view with single axes', () => { + let testTelemetryObject; + let config; + let component; + let mockComposition; + + afterAll(() => { + component.$destroy(); + openmct.router.path = null; }); - describe("the plot views", () => { - it("provides an overlay plot view for objects with telemetry", () => { - const testTelemetryObject = { - id: "test-object", - type: "telemetry.plot.overlay", - telemetry: { - values: [{ - key: "some-key" - }] - } - }; + 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 + } + } + ] + } + }; - const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); - let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-overlay"); - expect(plotView).toBeDefined(); - }); + overlayPlotObject.composition = [ + { + identifier: testTelemetryObject.identifier + } + ]; + overlayPlotObject.configuration.series = [ + { + identifier: testTelemetryObject.identifier + } + ]; + 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: { + Plot + }, + provide: { + openmct: openmct, + domainObject: overlayPlotObject, + composition: openmct.composition.get(overlayPlotObject), + path: [overlayPlotObject] + }, + template: '' + }); + + return telemetryPromise.then(Vue.nextTick()).then(() => { + const configId = openmct.objects.makeKeyString(overlayPlotObject.identifier); + config = configStore.get(configId); + }); }); - describe("The overlay plot view with multiple axes", () => { - let testTelemetryObject; - let testTelemetryObject2; - let config; - let component; - let mockComposition; - - afterAll(() => { - component.$destroy(); - 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 - } - }] - } - }; - - 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 - } - }] - } - }; - overlayPlotObject.composition = [ - { - identifier: testTelemetryObject.identifier - }, - { - identifier: testTelemetryObject2.identifier - } - ]; - overlayPlotObject.configuration.series = [ - { - identifier: testTelemetryObject.identifier, - yAxisId: 1 - }, - { - identifier: testTelemetryObject2.identifier, - yAxisId: 3 - } - ]; - overlayPlotObject.configuration.additionalYAxes = [ - { - label: 'Test Object Label', - id: 2 - }, - { - label: 'Test Object 2 Label', - id: 3 - } - ]; - mockComposition = new EventEmitter(); - mockComposition.load = () => { - mockComposition.emit('add', testTelemetryObject); - mockComposition.emit('add', testTelemetryObject2); - - return [testTelemetryObject, testTelemetryObject2]; - }; - - spyOn(openmct.composition, 'get').and.returnValue(mockComposition); - - let viewContainer = document.createElement("div"); - child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - Plot - }, - provide: { - openmct: openmct, - domainObject: overlayPlotObject, - composition: openmct.composition.get(overlayPlotObject), - path: [overlayPlotObject] - }, - template: '' - }); - - return telemetryPromise - .then(Vue.nextTick()) - .then(() => { - const configId = openmct.objects.makeKeyString(overlayPlotObject.identifier); - config = configStore.get(configId); - }); - }); - - it("Renders multiple Y-axis for the telemetry objects", (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(2); - done(); - }); - }); - - describe('the inspector view', () => { - let inspectorComponent; - let viewComponentObject; - let selection; - beforeEach((done) => { - selection = [ - [ - { - context: { - item: { - id: overlayPlotObject.identifier.key, - identifier: overlayPlotObject.identifier, - type: overlayPlotObject.type, - configuration: overlayPlotObject.configuration, - composition: overlayPlotObject.composition - } - } - } - ] - ]; - - let viewContainer = document.createElement('div'); - child.append(viewContainer); - inspectorComponent = new Vue({ - el: viewContainer, - components: { - PlotOptions - }, - provide: { - openmct: openmct, - domainObject: selection[0][0].context.item, - path: [selection[0][0].context.item] - }, - template: '' - }); - - Vue.nextTick(() => { - viewComponentObject = inspectorComponent.$root.$children[0]; - done(); - }); - }); - - afterEach(() => { - openmct.router.path = null; - }); - - describe('in edit mode', () => { - let editOptionsEl; - - beforeEach((done) => { - viewComponentObject.setEditState(true); - Vue.nextTick(() => { - editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit'); - done(); - }); - }); - - it('shows multiple yAxis options', () => { - const yAxisProperties = editOptionsEl.querySelectorAll(".js-yaxis-grid-properties .l-inspector-part h2"); - expect(yAxisProperties.length).toEqual(2); - }); - - it('saves yAxis options', () => { - //toggle log mode and save - config.additionalYAxes[1].set('displayRange', { - min: 10, - max: 20 - }); - const yAxisProperties = editOptionsEl.querySelectorAll(".js-log-mode-input"); - const clickEvent = createMouseEvent("click"); - yAxisProperties[1].dispatchEvent(clickEvent); - - expect(config.additionalYAxes[1].get('logMode')).toEqual(true); - - }); - }); - - }); - - }); - - describe("The overlay plot view with single axes", () => { - let testTelemetryObject; - let config; - let component; - let mockComposition; - - afterAll(() => { - component.$destroy(); - 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 - } - }] - } - }; - - overlayPlotObject.composition = [ - { - identifier: testTelemetryObject.identifier - } - ]; - overlayPlotObject.configuration.series = [ - { - identifier: testTelemetryObject.identifier - } - ]; - 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: { - Plot - }, - provide: { - openmct: openmct, - domainObject: overlayPlotObject, - composition: openmct.composition.get(overlayPlotObject), - path: [overlayPlotObject] - }, - template: '' - }); - - return telemetryPromise - .then(Vue.nextTick()) - .then(() => { - const configId = openmct.objects.makeKeyString(overlayPlotObject.identifier); - config = configStore.get(configId); - }); - }); - - it("Renders single Y-axis 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); - done(); - }); - }); + it('Renders single Y-axis 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); + done(); + }); }); + }); }); diff --git a/src/plugins/plot/plugin.js b/src/plugins/plot/plugin.js index 315a9a6048..94d76d673d 100644 --- a/src/plugins/plot/plugin.js +++ b/src/plugins/plot/plugin.js @@ -25,60 +25,61 @@ 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"; +import PlotViewActions from './actions/ViewActions'; +import StackedPlotsInspectorViewProvider from './inspector/StackedPlotsInspectorViewProvider'; +import stackedPlotConfigurationInterceptor from './stackedPlot/stackedPlotConfigurationInterceptor'; export default function () { - return function install(openmct) { + return function install(openmct) { + openmct.types.addType('telemetry.plot.overlay', { + key: 'telemetry.plot.overlay', + name: 'Overlay Plot', + cssClass: 'icon-plot-overlay', + description: + 'Combine multiple telemetry elements and view them together as a plot with common X and Y axes. Can be added to Display Layouts.', + creatable: true, + initialize: function (domainObject) { + domainObject.composition = []; + domainObject.configuration = { + //series is an array of objects of type: {identifier, series: {color...}, yAxis:{}} + series: [] + }; + }, + priority: 891 + }); - openmct.types.addType('telemetry.plot.overlay', { - key: "telemetry.plot.overlay", - name: "Overlay Plot", - cssClass: "icon-plot-overlay", - description: "Combine multiple telemetry elements and view them together as a plot with common X and Y axes. Can be added to Display Layouts.", - creatable: true, - initialize: function (domainObject) { - domainObject.composition = []; - domainObject.configuration = { - //series is an array of objects of type: {identifier, series: {color...}, yAxis:{}} - series: [] - }; - }, - priority: 891 - }); + openmct.types.addType('telemetry.plot.stacked', { + key: 'telemetry.plot.stacked', + name: 'Stacked Plot', + cssClass: 'icon-plot-stacked', + description: + 'Combine multiple telemetry elements and view them together as a plot with a common X axis and individual Y axes. Can be added to Display Layouts.', + creatable: true, + initialize: function (domainObject) { + domainObject.composition = []; + domainObject.configuration = { + series: [], + yAxis: {}, + xAxis: {} + }; + }, + priority: 890 + }); - openmct.types.addType('telemetry.plot.stacked', { - key: "telemetry.plot.stacked", - name: "Stacked Plot", - cssClass: "icon-plot-stacked", - description: "Combine multiple telemetry elements and view them together as a plot with a common X axis and individual Y axes. Can be added to Display Layouts.", - creatable: true, - initialize: function (domainObject) { - domainObject.composition = []; - domainObject.configuration = { - series: [], - yAxis: {}, - xAxis: {} - }; - }, - priority: 890 - }); + stackedPlotConfigurationInterceptor(openmct); - stackedPlotConfigurationInterceptor(openmct); + openmct.objectViews.addProvider(new StackedPlotViewProvider(openmct)); + openmct.objectViews.addProvider(new OverlayPlotViewProvider(openmct)); + openmct.objectViews.addProvider(new PlotViewProvider(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.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); - openmct.composition.addPolicy(new OverlayPlotCompositionPolicy(openmct).allow); - openmct.composition.addPolicy(new StackedPlotCompositionPolicy(openmct).allow); - - PlotViewActions.forEach(action => { - openmct.actions.register(action); - }); - }; + PlotViewActions.forEach((action) => { + openmct.actions.register(action); + }); + }; } diff --git a/src/plugins/plot/pluginSpec.js b/src/plugins/plot/pluginSpec.js index 4fec092ff4..f6524b4f5f 100644 --- a/src/plugins/plot/pluginSpec.js +++ b/src/plugins/plot/pluginSpec.js @@ -20,878 +20,921 @@ * 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 configStore from "./configuration/ConfigStore"; -import EventEmitter from "EventEmitter"; -import PlotOptions from "./inspector/PlotOptions.vue"; -import PlotConfigurationModel from "./configuration/PlotConfigurationModel"; +import { + createMouseEvent, + createOpenMct, + resetApplicationState, + spyOnBuiltins +} from 'utils/testing'; +import PlotVuePlugin from './plugin'; +import Vue from 'vue'; +import configStore from './configuration/ConfigStore'; +import EventEmitter from 'EventEmitter'; +import PlotOptions from './inspector/PlotOptions.vue'; +import PlotConfigurationModel from './configuration/PlotConfigurationModel'; const TEST_KEY_ID = 'some-other-key'; -describe("the plugin", function () { - let element; - let child; - let openmct; - let telemetryPromise; - let telemetryPromiseResolve; - let mockObjectPath; - let telemetrylimitProvider; +describe('the plugin', function () { + let element; + let child; + let openmct; + let telemetryPromise; + let telemetryPromiseResolve; + let mockObjectPath; + let telemetrylimitProvider; - beforeEach((done) => { - mockObjectPath = [ + 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; + }); + + telemetrylimitProvider = jasmine.createSpyObj('telemetrylimitProvider', [ + 'supportsLimits', + 'getLimits', + 'getLimitEvaluator' + ]); + telemetrylimitProvider.supportsLimits.and.returnValue(true); + telemetrylimitProvider.getLimits.and.returnValue({ + limits: function () { + return Promise.resolve({ + WARNING: { + low: { + cssClass: 'is-limit--lwr is-limit--yellow', + 'some-key': -0.5 + }, + high: { + cssClass: 'is-limit--upr is-limit--yellow', + 'some-key': 0.5 + } + }, + DISTRESS: { + low: { + cssClass: 'is-limit--lwr is-limit--red', + 'some-key': -0.9 + }, + high: { + cssClass: 'is-limit--upr is-limit--red', + 'some-key': 0.9 + } + } + }); + } + }); + telemetrylimitProvider.getLimitEvaluator.and.returnValue({ + evaluate: function () { + return {}; + } + }); + openmct.telemetry.addProvider(telemetrylimitProvider); + + 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); + + openmct.types.addType('test-object', { + creatable: true + }); + + spyOnBuiltins(['requestAnimationFrame']); + window.requestAnimationFrame.and.callFake((callBack) => { + callBack(); + }); + + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach((done) => { + openmct.time.timeSystem('utc', { + start: 0, + end: 2 + }); + + configStore.deleteAll(); + resetApplicationState(openmct).then(done).catch(done); + }); + + describe('the plot views', () => { + it('provides a plot view for objects with telemetry', () => { + const testTelemetryObject = { + id: 'test-object', + type: 'test-object', + telemetry: { + values: [ { - name: 'mock folder', - type: 'fake-folder', - identifier: { - key: 'mock-folder', - namespace: '' - } + key: 'some-key', + hints: { + domain: 1 + } }, { - name: 'mock parent folder', + key: 'other-key', + hints: { + range: 1 + } + }, + { + key: 'yet-another-key', + format: 'string', + hints: { + range: 2 + } + } + ] + } + }; + + const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); + const plotView = applicableViews.find((viewProvider) => viewProvider.key === 'plot-single'); + + expect(plotView).toBeDefined(); + }); + + it('does not provide a plot view if the telemetry is entirely non numeric', () => { + const testTelemetryObject = { + id: 'test-object', + type: 'test-object', + telemetry: { + values: [ + { + key: 'some-key', + hints: { + domain: 1 + } + }, + { + key: 'other-key', + format: 'string', + hints: { + range: 1 + } + }, + { + key: 'yet-another-key', + format: 'string', + hints: { + range: 1 + } + } + ] + } + }; + + const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); + const plotView = applicableViews.find((viewProvider) => viewProvider.key === 'plot-single'); + + expect(plotView).toBeUndefined(); + }); + + it('provides an overlay plot view for objects with telemetry', () => { + const testTelemetryObject = { + id: 'test-object', + type: 'telemetry.plot.overlay', + telemetry: { + values: [ + { + key: 'some-key' + } + ] + } + }; + + const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); + let plotView = applicableViews.find((viewProvider) => viewProvider.key === 'plot-overlay'); + expect(plotView).toBeDefined(); + }); + + it('provides an inspector view for overlay plots', () => { + let selection = [ + [ + { + context: { + item: { + id: 'test-object', + type: 'telemetry.plot.overlay', + telemetry: { + values: [ + { + key: 'some-key' + } + ] + } + } + } + }, + { + context: { + item: { + type: 'time-strip' + } + } + } + ] + ]; + const applicableInspectorViews = openmct.inspectorViews.get(selection); + const plotInspectorView = applicableInspectorViews.find( + (view) => (view.name = 'Plots Configuration') + ); + + expect(plotInspectorView).toBeDefined(); + }); + + 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 single plot view', () => { + let testTelemetryObject; + let applicableViews; + let plotViewProvider; + let plotView; + + beforeEach(() => { + openmct.time.timeSystem('utc', { + start: 0, + end: 4 + }); + 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 + } + } + ] + } + }; + + openmct.router.path = [testTelemetryObject]; + + applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); + plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'plot-single'); + 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); + }); + + 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) => { + const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier); + const config = configStore.get(configId); + config.xAxis.set('displayRange', { + min: 0, + max: 4 + }); + + Vue.nextTick(() => { + let xAxisElement = element.querySelectorAll( + '.gl-plot-axis-area.gl-plot-x .gl-plot-tick-wrapper' + ); + expect(xAxisElement.length).toBe(1); + + let ticks = xAxisElement[0].querySelectorAll('.gl-plot-tick'); + expect(ticks.length).toBe(9); + + 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); + //Object{name: "Some attribute", key: "some-key"}, Object{name: "Another attribute", key: "some-other-key"} + 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('Updates the Y-axis label when changed', () => { + const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier); + const config = configStore.get(configId); + const yAxisElement = element.querySelectorAll('.gl-plot-axis-area.gl-plot-y')[0].__vue__; + config.yAxis.seriesCollection.models.forEach((plotSeries) => { + expect(plotSeries.model.yKey).toBe('some-key'); + }); + + yAxisElement.$emit('yKeyChanged', TEST_KEY_ID, 1); + config.yAxis.seriesCollection.models.forEach((plotSeries) => { + expect(plotSeries.model.yKey).toBe(TEST_KEY_ID); + }); + }); + + it('hides the pause and play controls', () => { + let pauseEl = element.querySelectorAll('.c-button-set .icon-pause'); + let playEl = element.querySelectorAll('.c-button-set .icon-arrow-right'); + expect(pauseEl.length).toBe(0); + expect(playEl.length).toBe(0); + }); + + describe('pause and play controls', () => { + beforeEach(() => { + openmct.time.clock('local', { + start: -1000, + end: 100 + }); + + return Vue.nextTick(); + }); + + it('shows the pause controls', (done) => { + Vue.nextTick(() => { + let pauseEl = element.querySelectorAll('.c-button-set .icon-pause'); + expect(pauseEl.length).toBe(1); + done(); + }); + }); + + it('shows the play control if plot is paused', (done) => { + let pauseEl = element.querySelector('.c-button-set .icon-pause'); + const clickEvent = createMouseEvent('click'); + + pauseEl.dispatchEvent(clickEvent); + Vue.nextTick(() => { + let playEl = element.querySelectorAll('.c-button-set .is-paused'); + expect(playEl.length).toBe(1); + done(); + }); + }); + }); + + describe('resume actions on errant click', () => { + beforeEach(() => { + openmct.time.clock('local', { + start: -1000, + end: 100 + }); + + return Vue.nextTick(); + }); + + it('clicking the plot view without movement resumes the plot while active', async () => { + const pauseEl = element.querySelectorAll('.c-button-set .icon-pause'); + // if the pause button is present, the chart is running + expect(pauseEl.length).toBe(1); + + // simulate an errant mouse click + // the second item is the canvas we need to use + const canvas = element.querySelectorAll('canvas')[1]; + const mouseDownEvent = new MouseEvent('mousedown'); + const mouseUpEvent = new MouseEvent('mouseup'); + canvas.dispatchEvent(mouseDownEvent); + // mouseup event is bound to the window + window.dispatchEvent(mouseUpEvent); + await Vue.nextTick(); + + const pauseElAfterClick = element.querySelectorAll('.c-button-set .icon-pause'); + console.log('pauseElAfterClick', pauseElAfterClick); + expect(pauseElAfterClick.length).toBe(1); + }); + + it('clicking the plot view without movement leaves the plot paused', async () => { + const pauseEl = element.querySelector('.c-button-set .icon-pause'); + // pause the plot + pauseEl.dispatchEvent(createMouseEvent('click')); + await Vue.nextTick(); + + const playEl = element.querySelectorAll('.c-button-set .is-paused'); + expect(playEl.length).toBe(1); + + // simulate an errant mouse click + // the second item is the canvas we need to use + const canvas = element.querySelectorAll('canvas')[1]; + const mouseDownEvent = new MouseEvent('mousedown'); + const mouseUpEvent = new MouseEvent('mouseup'); + canvas.dispatchEvent(mouseDownEvent); + // mouseup event is bound to the window + window.dispatchEvent(mouseUpEvent); + await Vue.nextTick(); + + const playElAfterChartClick = element.querySelectorAll('.c-button-set .is-paused'); + expect(playElAfterChartClick.length).toBe(1); + }); + + it('clicking the plot does not request historical data', async () => { + expect(openmct.telemetry.request).toHaveBeenCalledTimes(2); + + // simulate an errant mouse click + // the second item is the canvas we need to use + const canvas = element.querySelectorAll('canvas')[1]; + const mouseDownEvent = new MouseEvent('mousedown'); + const mouseUpEvent = new MouseEvent('mouseup'); + canvas.dispatchEvent(mouseDownEvent); + // mouseup event is bound to the window + window.dispatchEvent(mouseUpEvent); + await Vue.nextTick(); + + expect(openmct.telemetry.request).toHaveBeenCalledTimes(2); + }); + + describe('limits', () => { + it('lines are not displayed by default', () => { + let limitEl = element.querySelectorAll('.js-limit-area .js-limit-line'); + expect(limitEl.length).toBe(0); + }); + + it('lines are displayed when configuration is set to true', (done) => { + const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier); + const config = configStore.get(configId); + config.yAxis.set('displayRange', { + min: 0, + max: 4 + }); + 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('controls in time strip view', () => { + it('zoom controls are hidden', () => { + let pauseEl = element.querySelectorAll('.c-button-set .js-zoom'); + expect(pauseEl.length).toBe(0); + }); + + it('pan controls are hidden', () => { + let pauseEl = element.querySelectorAll('.c-button-set .js-pan'); + expect(pauseEl.length).toBe(0); + }); + + it('pause/play controls are hidden', () => { + let pauseEl = element.querySelectorAll('.c-button-set .js-pause'); + expect(pauseEl.length).toBe(0); + }); + }); + }); + + describe('resizing the plot', () => { + let plotContainerResizeObserver; + let resizePromiseResolve; + let testTelemetryObject; + let applicableViews; + let plotViewProvider; + let plotView; + let resizePromise; + + 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 + } + } + ] + } + }; + + openmct.router.path = [testTelemetryObject]; + + applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); + plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'plot-single'); + plotView = plotViewProvider.view(testTelemetryObject, []); + + plotView.show(child, true); + + resizePromise = new Promise((resolve) => { + resizePromiseResolve = resolve; + }); + + const handlePlotResize = _.debounce(() => { + resizePromiseResolve(true); + }, 600); + + plotContainerResizeObserver = new ResizeObserver(handlePlotResize); + plotContainerResizeObserver.observe( + plotView.getComponent().$children[0].$children[1].$parent.$refs.plotWrapper + ); + + return Vue.nextTick(() => { + plotView.getComponent().$children[0].$children[1].stopFollowingTimeContext(); + spyOn( + plotView.getComponent().$children[0].$children[1], + 'loadSeriesData' + ).and.callThrough(); + }); + }); + + afterEach(() => { + plotContainerResizeObserver.disconnect(); + openmct.router.path = null; + }); + + it('requests historical data when over the threshold', (done) => { + element.style.width = '680px'; + resizePromise.then(() => { + expect( + plotView.getComponent().$children[0].$children[1].loadSeriesData + ).toHaveBeenCalledTimes(1); + done(); + }); + }); + + it('does not request historical data when under the threshold', (done) => { + element.style.width = '644px'; + resizePromise.then(() => { + expect( + plotView.getComponent().$children[0].$children[1].loadSeriesData + ).not.toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('the 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: { + id: 'test-object', + identifier: { + key: 'test-object', + namespace: '' + }, + type: 'telemetry.plot.overlay', + configuration: { + series: [ + { + identifier: { + key: 'test-object', + namespace: '' + } + } + ] + }, + composition: [] + } + } + }, + { + context: { + item: { type: 'time-strip', identifier: { - key: 'mock-parent-folder', - namespace: '' + key: 'some-other-key', + 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.router.path = [testTelemetryObject]; + mockComposition = new EventEmitter(); + mockComposition.load = () => { + mockComposition.emit('add', testTelemetryObject); - openmct = createOpenMct(timeSystem); + return [testTelemetryObject]; + }; - telemetryPromise = new Promise((resolve) => { - telemetryPromiseResolve = resolve; + 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: '' + }); + + Vue.nextTick(() => { + viewComponentObject = component.$root.$children[0]; + done(); + }); + }); + + afterEach(() => { + openmct.router.path = null; + }); + + describe('in view only mode', () => { + let browseOptionsEl; + let editOptionsEl; + beforeEach(() => { + browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse'); + editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit'); + }); + + it('does not show the edit options', () => { + expect(editOptionsEl).toBeNull(); + }); + + it('shows the name', () => { + const seriesEl = browseOptionsEl.querySelector('.c-object-label__name'); + expect(seriesEl.innerHTML).toEqual(testTelemetryObject.name); + }); + + it('shows in collapsed mode', () => { + const seriesEl = browseOptionsEl.querySelectorAll('.c-disclosure-triangle--expanded'); + expect(seriesEl.length).toEqual(0); + }); + + it('shows in expanded mode', () => { + let expandControl = browseOptionsEl.querySelector('.c-disclosure-triangle'); + const clickEvent = createMouseEvent('click'); + expandControl.dispatchEvent(clickEvent); + + const plotOptionsProperties = browseOptionsEl.querySelectorAll( + '.js-plot-options-browse-properties .grid-row' + ); + expect(plotOptionsProperties.length).toEqual(6); + }); + }); + + describe('in edit mode', () => { + let editOptionsEl; + let browseOptionsEl; + + beforeEach((done) => { + viewComponentObject.setEditState(true); + Vue.nextTick(() => { + editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit'); + browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse'); + done(); }); + }); - spyOn(openmct.telemetry, 'request').and.callFake(() => { - telemetryPromiseResolve(testTelemetry); + it('does not show the browse options', () => { + expect(browseOptionsEl).toBeNull(); + }); - return telemetryPromise; + it('shows the name', () => { + const seriesEl = editOptionsEl.querySelector('.c-object-label__name'); + expect(seriesEl.innerHTML).toEqual(testTelemetryObject.name); + }); + + it('shows in collapsed mode', () => { + const seriesEl = editOptionsEl.querySelectorAll('.c-disclosure-triangle--expanded'); + expect(seriesEl.length).toEqual(0); + }); + + it('shows in collapsed mode', () => { + const seriesEl = editOptionsEl.querySelectorAll('.c-disclosure-triangle--expanded'); + expect(seriesEl.length).toEqual(0); + }); + + it('renders expanded', () => { + const expandControl = editOptionsEl.querySelector('.c-disclosure-triangle'); + const clickEvent = createMouseEvent('click'); + expandControl.dispatchEvent(clickEvent); + + const plotOptionsProperties = editOptionsEl.querySelectorAll( + '.js-plot-options-edit-properties .grid-row' + ); + expect(plotOptionsProperties.length).toEqual(8); + }); + + it('shows yKeyOptions', () => { + const expandControl = editOptionsEl.querySelector('.c-disclosure-triangle'); + const clickEvent = createMouseEvent('click'); + expandControl.dispatchEvent(clickEvent); + + const plotOptionsProperties = editOptionsEl.querySelectorAll( + '.js-plot-options-edit-properties .grid-row' + ); + + const yKeySelection = plotOptionsProperties[0].querySelector('select'); + const options = Array.from(yKeySelection.options).map((option) => { + return option.value; }); - - telemetrylimitProvider = jasmine.createSpyObj('telemetrylimitProvider', [ - 'supportsLimits', - 'getLimits', - 'getLimitEvaluator' + expect(options).toEqual([ + testTelemetryObject.telemetry.values[1].key, + testTelemetryObject.telemetry.values[2].key ]); - telemetrylimitProvider.supportsLimits.and.returnValue(true); - telemetrylimitProvider.getLimits.and.returnValue({ - limits: function () { - return Promise.resolve({ - WARNING: { - low: { - cssClass: "is-limit--lwr is-limit--yellow", - 'some-key': -0.5 - }, - high: { - cssClass: "is-limit--upr is-limit--yellow", - 'some-key': 0.5 - } - }, - DISTRESS: { - low: { - cssClass: "is-limit--lwr is-limit--red", - 'some-key': -0.9 - }, - high: { - cssClass: "is-limit--upr is-limit--red", - 'some-key': 0.9 - } - } - }); - } - }); - telemetrylimitProvider.getLimitEvaluator.and.returnValue({ - evaluate: function () { - return {}; - } - }); - openmct.telemetry.addProvider(telemetrylimitProvider); + }); - openmct.install(new PlotVuePlugin()); + it('shows yAxis options', () => { + const expandControl = editOptionsEl.querySelector('.c-disclosure-triangle'); + const clickEvent = createMouseEvent('click'); + expandControl.dispatchEvent(clickEvent); - 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); + const yAxisProperties = editOptionsEl.querySelectorAll( + 'div.grid-properties:first-of-type .l-inspector-part' + ); - openmct.types.addType("test-object", { - creatable: true - }); + // TODO better test + expect(yAxisProperties.length).toEqual(2); + }); - spyOnBuiltins(["requestAnimationFrame"]); - window.requestAnimationFrame.and.callFake((callBack) => { - callBack(); - }); - - openmct.on("start", done); - openmct.startHeadless(); - }); - - afterEach((done) => { - openmct.time.timeSystem('utc', { - start: 0, - end: 2 - }); - - configStore.deleteAll(); - resetApplicationState(openmct).then(done).catch(done); - }); - - describe("the plot views", () => { - - it("provides a plot view for objects with telemetry", () => { - const testTelemetryObject = { - id: "test-object", - type: "test-object", - telemetry: { - values: [{ - key: "some-key", - hints: { - domain: 1 - } - }, - { - key: "other-key", - hints: { - range: 1 - } - }, - { - key: "yet-another-key", - format: "string", - hints: { - range: 2 - } - }] - } - }; - - const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); - const plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-single"); - - expect(plotView).toBeDefined(); - }); - - it("does not provide a plot view if the telemetry is entirely non numeric", () => { - const testTelemetryObject = { - id: "test-object", - type: "test-object", - telemetry: { - values: [{ - key: "some-key", - hints: { - domain: 1 - } - }, - { - key: "other-key", - format: "string", - hints: { - range: 1 - } - }, - { - key: "yet-another-key", - format: "string", - hints: { - range: 1 - } - }] - } - }; - - const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); - const plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-single"); - - expect(plotView).toBeUndefined(); - }); - - it("provides an overlay plot view for objects with telemetry", () => { - const testTelemetryObject = { - id: "test-object", - type: "telemetry.plot.overlay", - telemetry: { - values: [{ - key: "some-key" - }] - } - }; - - const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); - let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-overlay"); - expect(plotView).toBeDefined(); - }); - - it('provides an inspector view for overlay plots', () => { - let selection = [ - [ - { - context: { - item: { - id: "test-object", - type: "telemetry.plot.overlay", - telemetry: { - values: [{ - key: "some-key" - }] - } - } - } - }, - { - context: { - item: { - type: 'time-strip' - } - } - } - ] - ]; - const applicableInspectorViews = openmct.inspectorViews.get(selection); - const plotInspectorView = applicableInspectorViews.find(view => view.name = 'Plots Configuration'); - - expect(plotInspectorView).toBeDefined(); - }); - - 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 single plot view", () => { - let testTelemetryObject; - let applicableViews; - let plotViewProvider; - let plotView; - - beforeEach(() => { - openmct.time.timeSystem("utc", { - start: 0, - end: 4 - }); - 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 - } - }] - } - }; - - openmct.router.path = [testTelemetryObject]; - - applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); - plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-single"); - 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); - }); - - 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) => { - const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier); - const config = configStore.get(configId); - config.xAxis.set('displayRange', { - min: 0, - max: 4 - }); - - Vue.nextTick(() => { - let xAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-x .gl-plot-tick-wrapper"); - expect(xAxisElement.length).toBe(1); - - let ticks = xAxisElement[0].querySelectorAll(".gl-plot-tick"); - expect(ticks.length).toBe(9); - - 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); - //Object{name: "Some attribute", key: "some-key"}, Object{name: "Another attribute", key: "some-other-key"} - 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("Updates the Y-axis label when changed", () => { - const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier); - const config = configStore.get(configId); - const yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y")[0].__vue__; - config.yAxis.seriesCollection.models.forEach((plotSeries) => { - expect(plotSeries.model.yKey).toBe('some-key'); - }); - - yAxisElement.$emit('yKeyChanged', TEST_KEY_ID, 1); - config.yAxis.seriesCollection.models.forEach((plotSeries) => { - expect(plotSeries.model.yKey).toBe(TEST_KEY_ID); - }); - }); - - it('hides the pause and play controls', () => { - let pauseEl = element.querySelectorAll(".c-button-set .icon-pause"); - let playEl = element.querySelectorAll(".c-button-set .icon-arrow-right"); - expect(pauseEl.length).toBe(0); - expect(playEl.length).toBe(0); - }); - - describe('pause and play controls', () => { - beforeEach(() => { - openmct.time.clock('local', { - start: -1000, - end: 100 - }); - - return Vue.nextTick(); - }); - - it('shows the pause controls', (done) => { - Vue.nextTick(() => { - let pauseEl = element.querySelectorAll(".c-button-set .icon-pause"); - expect(pauseEl.length).toBe(1); - done(); - }); - - }); - - it('shows the play control if plot is paused', (done) => { - let pauseEl = element.querySelector(".c-button-set .icon-pause"); - const clickEvent = createMouseEvent("click"); - - pauseEl.dispatchEvent(clickEvent); - Vue.nextTick(() => { - let playEl = element.querySelectorAll(".c-button-set .is-paused"); - expect(playEl.length).toBe(1); - done(); - }); - - }); - }); - - describe('resume actions on errant click', () => { - beforeEach(() => { - openmct.time.clock('local', { - start: -1000, - end: 100 - }); - - return Vue.nextTick(); - }); - - it("clicking the plot view without movement resumes the plot while active", async () => { - - const pauseEl = element.querySelectorAll(".c-button-set .icon-pause"); - // if the pause button is present, the chart is running - expect(pauseEl.length).toBe(1); - - // simulate an errant mouse click - // the second item is the canvas we need to use - const canvas = element.querySelectorAll("canvas")[1]; - const mouseDownEvent = new MouseEvent('mousedown'); - const mouseUpEvent = new MouseEvent('mouseup'); - canvas.dispatchEvent(mouseDownEvent); - // mouseup event is bound to the window - window.dispatchEvent(mouseUpEvent); - await Vue.nextTick(); - - const pauseElAfterClick = element.querySelectorAll(".c-button-set .icon-pause"); - console.log('pauseElAfterClick', pauseElAfterClick); - expect(pauseElAfterClick.length).toBe(1); - - }); - - it("clicking the plot view without movement leaves the plot paused", async () => { - - const pauseEl = element.querySelector(".c-button-set .icon-pause"); - // pause the plot - pauseEl.dispatchEvent(createMouseEvent('click')); - await Vue.nextTick(); - - const playEl = element.querySelectorAll('.c-button-set .is-paused'); - expect(playEl.length).toBe(1); - - // simulate an errant mouse click - // the second item is the canvas we need to use - const canvas = element.querySelectorAll("canvas")[1]; - const mouseDownEvent = new MouseEvent('mousedown'); - const mouseUpEvent = new MouseEvent('mouseup'); - canvas.dispatchEvent(mouseDownEvent); - // mouseup event is bound to the window - window.dispatchEvent(mouseUpEvent); - await Vue.nextTick(); - - const playElAfterChartClick = element.querySelectorAll(".c-button-set .is-paused"); - expect(playElAfterChartClick.length).toBe(1); - - }); - - it("clicking the plot does not request historical data", async () => { - expect(openmct.telemetry.request).toHaveBeenCalledTimes(2); - - // simulate an errant mouse click - // the second item is the canvas we need to use - const canvas = element.querySelectorAll("canvas")[1]; - const mouseDownEvent = new MouseEvent('mousedown'); - const mouseUpEvent = new MouseEvent('mouseup'); - canvas.dispatchEvent(mouseDownEvent); - // mouseup event is bound to the window - window.dispatchEvent(mouseUpEvent); - await Vue.nextTick(); - - expect(openmct.telemetry.request).toHaveBeenCalledTimes(2); - - }); - - describe('limits', () => { - - it('lines are not displayed by default', () => { - let limitEl = element.querySelectorAll(".js-limit-area .js-limit-line"); - expect(limitEl.length).toBe(0); - }); - - it('lines are displayed when configuration is set to true', (done) => { - const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier); - const config = configStore.get(configId); - config.yAxis.set('displayRange', { - min: 0, - max: 4 - }); - 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('controls in time strip view', () => { - - it('zoom controls are hidden', () => { - let pauseEl = element.querySelectorAll(".c-button-set .js-zoom"); - expect(pauseEl.length).toBe(0); - }); - - it('pan controls are hidden', () => { - let pauseEl = element.querySelectorAll(".c-button-set .js-pan"); - expect(pauseEl.length).toBe(0); - }); - - it('pause/play controls are hidden', () => { - let pauseEl = element.querySelectorAll(".c-button-set .js-pause"); - expect(pauseEl.length).toBe(0); - }); - - }); - }); - - describe('resizing the plot', () => { - let plotContainerResizeObserver; - let resizePromiseResolve; - let testTelemetryObject; - let applicableViews; - let plotViewProvider; - let plotView; - let resizePromise; - - 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 - } - }] - } - }; - - openmct.router.path = [testTelemetryObject]; - - applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); - plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-single"); - plotView = plotViewProvider.view(testTelemetryObject, []); - - plotView.show(child, true); - - resizePromise = new Promise((resolve) => { - resizePromiseResolve = resolve; - }); - - const handlePlotResize = _.debounce(() => { - resizePromiseResolve(true); - }, 600); - - plotContainerResizeObserver = new ResizeObserver(handlePlotResize); - plotContainerResizeObserver.observe(plotView.getComponent().$children[0].$children[1].$parent.$refs.plotWrapper); - - return Vue.nextTick(() => { - plotView.getComponent().$children[0].$children[1].stopFollowingTimeContext(); - spyOn(plotView.getComponent().$children[0].$children[1], 'loadSeriesData').and.callThrough(); - }); - }); - - afterEach(() => { - plotContainerResizeObserver.disconnect(); - openmct.router.path = null; - }); - - it("requests historical data when over the threshold", (done) => { - element.style.width = '680px'; - resizePromise.then(() => { - expect(plotView.getComponent().$children[0].$children[1].loadSeriesData).toHaveBeenCalledTimes(1); - done(); - }); - }); - - it("does not request historical data when under the threshold", (done) => { - element.style.width = '644px'; - resizePromise.then(() => { - expect(plotView.getComponent().$children[0].$children[1].loadSeriesData).not.toHaveBeenCalled(); - done(); - }); - }); - }); - - describe('the 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: { - id: "test-object", - identifier: { - key: "test-object", - namespace: '' - }, - type: "telemetry.plot.overlay", - configuration: { - series: [ - { - identifier: { - key: "test-object", - namespace: '' - } - } - ] - }, - composition: [] - } - } - }, - { - context: { - item: { - type: 'time-strip', - identifier: { - key: 'some-other-key', - namespace: '' - } - } - } - } - ] - ]; - - 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: '' - }); - - Vue.nextTick(() => { - viewComponentObject = component.$root.$children[0]; - done(); - }); - }); - - afterEach(() => { - openmct.router.path = null; - }); - - describe('in view only mode', () => { - let browseOptionsEl; - let editOptionsEl; - beforeEach(() => { - browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse'); - editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit'); - }); - - it('does not show the edit options', () => { - expect(editOptionsEl).toBeNull(); - }); - - it('shows the name', () => { - const seriesEl = browseOptionsEl.querySelector('.c-object-label__name'); - expect(seriesEl.innerHTML).toEqual(testTelemetryObject.name); - }); - - it('shows in collapsed mode', () => { - const seriesEl = browseOptionsEl.querySelectorAll('.c-disclosure-triangle--expanded'); - expect(seriesEl.length).toEqual(0); - }); - - it('shows in expanded mode', () => { - let expandControl = browseOptionsEl.querySelector(".c-disclosure-triangle"); - const clickEvent = createMouseEvent("click"); - expandControl.dispatchEvent(clickEvent); - - const plotOptionsProperties = browseOptionsEl.querySelectorAll('.js-plot-options-browse-properties .grid-row'); - expect(plotOptionsProperties.length).toEqual(6); - }); - }); - - describe('in edit mode', () => { - let editOptionsEl; - let browseOptionsEl; - - beforeEach((done) => { - viewComponentObject.setEditState(true); - Vue.nextTick(() => { - editOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-edit'); - browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse'); - done(); - }); - }); - - it('does not show the browse options', () => { - expect(browseOptionsEl).toBeNull(); - }); - - it('shows the name', () => { - const seriesEl = editOptionsEl.querySelector('.c-object-label__name'); - expect(seriesEl.innerHTML).toEqual(testTelemetryObject.name); - }); - - it('shows in collapsed mode', () => { - const seriesEl = editOptionsEl.querySelectorAll('.c-disclosure-triangle--expanded'); - expect(seriesEl.length).toEqual(0); - }); - - it('shows in collapsed mode', () => { - const seriesEl = editOptionsEl.querySelectorAll('.c-disclosure-triangle--expanded'); - expect(seriesEl.length).toEqual(0); - }); - - it('renders expanded', () => { - const expandControl = editOptionsEl.querySelector(".c-disclosure-triangle"); - const clickEvent = createMouseEvent("click"); - expandControl.dispatchEvent(clickEvent); - - const plotOptionsProperties = editOptionsEl.querySelectorAll(".js-plot-options-edit-properties .grid-row"); - expect(plotOptionsProperties.length).toEqual(8); - }); - - it('shows yKeyOptions', () => { - const expandControl = editOptionsEl.querySelector(".c-disclosure-triangle"); - const clickEvent = createMouseEvent("click"); - expandControl.dispatchEvent(clickEvent); - - const plotOptionsProperties = editOptionsEl.querySelectorAll(".js-plot-options-edit-properties .grid-row"); - - const yKeySelection = plotOptionsProperties[0].querySelector('select'); - const options = Array.from(yKeySelection.options).map((option) => { - return option.value; - }); - expect(options).toEqual([testTelemetryObject.telemetry.values[1].key, testTelemetryObject.telemetry.values[2].key]); - }); - - it('shows yAxis options', () => { - const expandControl = editOptionsEl.querySelector(".c-disclosure-triangle"); - const clickEvent = createMouseEvent("click"); - expandControl.dispatchEvent(clickEvent); - - const yAxisProperties = editOptionsEl.querySelectorAll("div.grid-properties:first-of-type .l-inspector-part"); - - // TODO better test - expect(yAxisProperties.length).toEqual(2); - }); - - it('renders color palette options', () => { - const colorSwatch = editOptionsEl.querySelector(".c-click-swatch"); - expect(colorSwatch).toBeDefined(); - }); - }); + it('renders color palette options', () => { + const colorSwatch = editOptionsEl.querySelector('.c-click-swatch'); + expect(colorSwatch).toBeDefined(); + }); }); + }); }); diff --git a/src/plugins/plot/stackedPlot/StackedPlot.vue b/src/plugins/plot/stackedPlot/StackedPlot.vue index d6e3ba6677..c57cfa91eb 100644 --- a/src/plugins/plot/stackedPlot/StackedPlot.vue +++ b/src/plugins/plot/stackedPlot/StackedPlot.vue @@ -21,301 +21,308 @@ --> diff --git a/src/plugins/plot/stackedPlot/StackedPlotCompositionPolicy.js b/src/plugins/plot/stackedPlot/StackedPlotCompositionPolicy.js index 882b6c2a98..2d16188da8 100644 --- a/src/plugins/plot/stackedPlot/StackedPlotCompositionPolicy.js +++ b/src/plugins/plot/stackedPlot/StackedPlotCompositionPolicy.js @@ -1,30 +1,33 @@ export default function StackedPlotCompositionPolicy(openmct) { - function hasNumericTelemetry(domainObject) { - const hasTelemetry = openmct.telemetry.isTelemetryObject(domainObject); - if (!hasTelemetry) { - return false; - } - - let metadata = openmct.telemetry.getMetadata(domainObject); - - return metadata.values().length > 0 && hasDomainAndRange(metadata); + function hasNumericTelemetry(domainObject) { + const hasTelemetry = openmct.telemetry.isTelemetryObject(domainObject); + if (!hasTelemetry) { + return false; } - function hasDomainAndRange(metadata) { - return (metadata.valuesForHints(['range']).length > 0 - && metadata.valuesForHints(['domain']).length > 0); + let metadata = openmct.telemetry.getMetadata(domainObject); + + return metadata.values().length > 0 && hasDomainAndRange(metadata); + } + + function hasDomainAndRange(metadata) { + return ( + metadata.valuesForHints(['range']).length > 0 && + metadata.valuesForHints(['domain']).length > 0 + ); + } + + return { + allow: function (parent, child) { + if ( + parent.type === 'telemetry.plot.stacked' && + child.type !== 'telemetry.plot.overlay' && + hasNumericTelemetry(child) === false + ) { + return false; + } + + return true; } - - return { - allow: function (parent, child) { - - if ((parent.type === 'telemetry.plot.stacked') - && ((child.type !== 'telemetry.plot.overlay') && (hasNumericTelemetry(child) === false)) - ) { - return false; - } - - return true; - } - }; + }; } diff --git a/src/plugins/plot/stackedPlot/StackedPlotItem.vue b/src/plugins/plot/stackedPlot/StackedPlotItem.vue index 491b30f41b..19c6bca6ff 100644 --- a/src/plugins/plot/stackedPlot/StackedPlotItem.vue +++ b/src/plugins/plot/stackedPlot/StackedPlotItem.vue @@ -20,209 +20,206 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/plot/stackedPlot/StackedPlotViewProvider.js b/src/plugins/plot/stackedPlot/StackedPlotViewProvider.js index 85f7aceaea..cd67d655e6 100644 --- a/src/plugins/plot/stackedPlot/StackedPlotViewProvider.js +++ b/src/plugins/plot/stackedPlot/StackedPlotViewProvider.js @@ -24,63 +24,63 @@ import StackedPlot from './StackedPlot.vue'; import Vue from 'vue'; export default function StackedPlotViewProvider(openmct) { - function isCompactView(objectPath) { - let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip'); + function isCompactView(objectPath) { + let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip'); - return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); - } + return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); + } - return { - key: 'plot-stacked', - name: 'Stacked Plot', - cssClass: 'icon-telemetry', - canView(domainObject, objectPath) { - return domainObject.type === 'telemetry.plot.stacked'; - }, + return { + key: 'plot-stacked', + name: 'Stacked Plot', + cssClass: 'icon-telemetry', + canView(domainObject, objectPath) { + return domainObject.type === 'telemetry.plot.stacked'; + }, - canEdit(domainObject, objectPath) { - return domainObject.type === 'telemetry.plot.stacked'; - }, + canEdit(domainObject, objectPath) { + return domainObject.type === 'telemetry.plot.stacked'; + }, - view: function (domainObject, objectPath) { - let component; + view: function (domainObject, objectPath) { + let component; - return { - show: function (element) { - let isCompact = isCompactView(objectPath); + return { + show: function (element) { + let isCompact = isCompactView(objectPath); - component = new Vue({ - el: element, - components: { - StackedPlot - }, - provide: { - openmct, - domainObject, - path: objectPath - }, - data() { - return { - options: { - compact: isCompact - } - }; - }, - template: '' - }); - }, - getViewContext() { - if (!component) { - return {}; - } - - return component.$refs.plotComponent.getViewContext(); - }, - destroy: function () { - component.$destroy(); - component = undefined; + component = new Vue({ + el: element, + components: { + StackedPlot + }, + provide: { + openmct, + domainObject, + path: objectPath + }, + data() { + return { + options: { + compact: isCompact } - }; + }; + }, + template: '' + }); + }, + 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 c917a6b500..698312688a 100644 --- a/src/plugins/plot/stackedPlot/mixins/objectStyles-mixin.js +++ b/src/plugins/plot/stackedPlot/mixins/objectStyles-mixin.js @@ -20,118 +20,141 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import StyleRuleManager from "@/plugins/condition/StyleRuleManager"; -import {STYLE_CONSTANTS} from "@/plugins/condition/utils/constants"; +import StyleRuleManager from '@/plugins/condition/StyleRuleManager'; +import { STYLE_CONSTANTS } from '@/plugins/condition/utils/constants'; export default { - inject: ['openmct', 'domainObject', 'path'], - data() { - return { - objectStyle: undefined - }; - }, - mounted() { - this.objectStyles = this.getObjectStyleForItem(this.childObject.configuration); - this.initObjectStyles(); - }, - beforeDestroy() { - if (this.stopListeningStyles) { - this.stopListeningStyles(); - } - - if (this.styleRuleManager) { - this.styleRuleManager.destroy(); - } - }, - methods: { - getObjectStyleForItem(config) { - if (config && config.objectStyles) { - return config.objectStyles ? Object.assign({}, config.objectStyles) : undefined; - } else { - return undefined; - } - }, - initObjectStyles() { - if (!this.styleRuleManager) { - this.styleRuleManager = new StyleRuleManager(this.objectStyles, this.openmct, this.updateStyle.bind(this), true); - } else { - this.styleRuleManager.updateObjectStyleConfig(this.objectStyles); - } - - if (this.stopListeningStyles) { - this.stopListeningStyles(); - } - - 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.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.childObject, 'configuration.fontStyle', (newFontStyle) => { - this.setFontSize(newFontStyle.fontSize); - this.setFont(newFontStyle.font); - }); - }, - getStyleReceiver() { - let styleReceiver; - - if (this.$el !== undefined) { - styleReceiver = this.$el.querySelector('.js-style-receiver') - || this.$el.querySelector(':first-child'); - - if (styleReceiver === null) { - styleReceiver = undefined; - } - } - - return styleReceiver; - }, - setFontSize(newSize) { - let elemToStyle = this.getStyleReceiver(); - - if (elemToStyle !== undefined) { - elemToStyle.dataset.fontSize = newSize; - } - }, - setFont(newFont) { - let elemToStyle = this.getStyleReceiver(); - - if (elemToStyle !== undefined) { - elemToStyle.dataset.font = newFont; - } - }, - updateStyle(styleObj) { - let elemToStyle = this.getStyleReceiver(); - - if (!styleObj || elemToStyle === undefined) { - return; - } - - let keys = Object.keys(styleObj); - - keys.forEach(key => { - if (elemToStyle) { - if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('__no_value') > -1)) { - if (elemToStyle.style[key]) { - elemToStyle.style[key] = ''; - } - } else { - if (!styleObj.isStyleInvisible && elemToStyle.classList.contains(STYLE_CONSTANTS.isStyleInvisible)) { - elemToStyle.classList.remove(STYLE_CONSTANTS.isStyleInvisible); - } else if (styleObj.isStyleInvisible && !elemToStyle.classList.contains(styleObj.isStyleInvisible)) { - elemToStyle.classList.add(styleObj.isStyleInvisible); - } - - elemToStyle.style[key] = styleObj[key]; - } - } - }); - } + inject: ['openmct', 'domainObject', 'path'], + data() { + return { + objectStyle: undefined + }; + }, + mounted() { + this.objectStyles = this.getObjectStyleForItem(this.childObject.configuration); + this.initObjectStyles(); + }, + beforeDestroy() { + if (this.stopListeningStyles) { + this.stopListeningStyles(); } + + if (this.styleRuleManager) { + this.styleRuleManager.destroy(); + } + }, + methods: { + getObjectStyleForItem(config) { + if (config && config.objectStyles) { + return config.objectStyles ? Object.assign({}, config.objectStyles) : undefined; + } else { + return undefined; + } + }, + initObjectStyles() { + if (!this.styleRuleManager) { + this.styleRuleManager = new StyleRuleManager( + this.objectStyles, + this.openmct, + this.updateStyle.bind(this), + true + ); + } else { + this.styleRuleManager.updateObjectStyleConfig(this.objectStyles); + } + + if (this.stopListeningStyles) { + this.stopListeningStyles(); + } + + 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.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.childObject, + 'configuration.fontStyle', + (newFontStyle) => { + this.setFontSize(newFontStyle.fontSize); + this.setFont(newFontStyle.font); + } + ); + }, + getStyleReceiver() { + let styleReceiver; + + if (this.$el !== undefined) { + styleReceiver = + this.$el.querySelector('.js-style-receiver') || this.$el.querySelector(':first-child'); + + if (styleReceiver === null) { + styleReceiver = undefined; + } + } + + return styleReceiver; + }, + setFontSize(newSize) { + let elemToStyle = this.getStyleReceiver(); + + if (elemToStyle !== undefined) { + elemToStyle.dataset.fontSize = newSize; + } + }, + setFont(newFont) { + let elemToStyle = this.getStyleReceiver(); + + if (elemToStyle !== undefined) { + elemToStyle.dataset.font = newFont; + } + }, + updateStyle(styleObj) { + let elemToStyle = this.getStyleReceiver(); + + if (!styleObj || elemToStyle === undefined) { + return; + } + + let keys = Object.keys(styleObj); + + keys.forEach((key) => { + if (elemToStyle) { + if (typeof styleObj[key] === 'string' && styleObj[key].indexOf('__no_value') > -1) { + if (elemToStyle.style[key]) { + elemToStyle.style[key] = ''; + } + } else { + if ( + !styleObj.isStyleInvisible && + elemToStyle.classList.contains(STYLE_CONSTANTS.isStyleInvisible) + ) { + elemToStyle.classList.remove(STYLE_CONSTANTS.isStyleInvisible); + } else if ( + styleObj.isStyleInvisible && + !elemToStyle.classList.contains(styleObj.isStyleInvisible) + ) { + elemToStyle.classList.add(styleObj.isStyleInvisible); + } + + elemToStyle.style[key] = styleObj[key]; + } + } + }); + } + } }; diff --git a/src/plugins/plot/stackedPlot/pluginSpec.js b/src/plugins/plot/stackedPlot/pluginSpec.js index 4bb9c7f58d..d6e5da4a69 100644 --- a/src/plugins/plot/stackedPlot/pluginSpec.js +++ b/src/plugins/plot/stackedPlot/pluginSpec.js @@ -20,768 +20,799 @@ * 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"; +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 = { +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: { - namespace: "", - key: "test-plot" - }, - type: "telemetry.plot.stacked", - name: "Test Stacked Plot", - configuration: { - series: [] + 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 + } }; - 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' - } - ]; + openmct = createOpenMct(timeSystem); - 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(); + telemetryPromise = new Promise((resolve) => { + telemetryPromiseResolve = resolve; }); - afterEach((done) => { - openmct.time.timeSystem('utc', { - start: 0, - end: 1 - }); - configStore.deleteAll(); - resetApplicationState(openmct).then(done).catch(done); + 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 mockCompositionList = []; + let plotViewComponentObject; + afterAll(() => { - openmct.router.path = null; + 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" - }] + 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: '' } - }; + } + ] + } + } + }; - const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); - let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-stacked"); - expect(plotView).toBeDefined(); - }); + 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 + } + } + ] + } + }; + stackedPlotObject.composition = [ + { + identifier: testTelemetryObject.identifier + } + ]; + + mockCompositionList = []; + spyOn(openmct.composition, 'get').and.callFake((domainObject) => { + //We need unique compositions here - one for the StackedPlot view and one for the PlotLegend view + const numObjects = domainObject.composition.length; + const mockComposition = new EventEmitter(); + mockComposition.load = () => { + if (numObjects === 1) { + mockComposition.emit('add', testTelemetryObject); + + return [testTelemetryObject]; + } else if (numObjects === 2) { + mockComposition.emit('add', testTelemetryObject); + mockComposition.emit('add', testTelemetryObject2); + + return [testTelemetryObject, testTelemetryObject2]; + } else { + return []; + } + }; + + mockCompositionList.push(mockComposition); + + return mockComposition; + }); + + let viewContainer = document.createElement('div'); + child.append(viewContainer); + component = new Vue({ + el: viewContainer, + components: { + StackedPlot + }, + provide: { + openmct: openmct, + domainObject: stackedPlotObject, + path: [stackedPlotObject] + }, + template: '' + }); + + return telemetryPromise.then(Vue.nextTick()).then(() => { + plotViewComponentObject = component.$root.$children[0]; + const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier); + config = configStore.get(configId); + }); }); - describe("The stacked plot view", () => { - let testTelemetryObject; - let testTelemetryObject2; - let config; - let component; - let mockCompositionList = []; - let plotViewComponentObject; + 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'); + }); - afterAll(() => { - openmct.router.path = null; + 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); + }); + + // disable due to flakiness + xit('Renders X-axis ticks for the telemetry object', () => { + 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 + }); + let ticks = xAxisElement[0].querySelectorAll('.gl-plot-tick'); + expect(ticks.length).toBe(9); + }); + + 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(); + }); + }); - beforeEach(() => { - testTelemetryObject = { + it('plots a new series when a new telemetry object is added', (done) => { + //setting composition here so that any new triggers to composition.load with correctly load the mockComposition in the beforeEach + stackedPlotObject.composition = [testTelemetryObject, testTelemetryObject2]; + mockCompositionList[0].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) => { + stackedPlotObject.composition = []; + mockCompositionList[0].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('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[1].xScale.domain()).toEqual( + { + min: 0, + max: 10 + } + ); + done(); + }); + }); + + it('updates the yscale', (done) => { + const yAxisList = [config.yAxis, ...config.additionalYAxes]; + yAxisList.forEach((yAxis) => { + yAxis.set('displayRange', { + min: 10, + max: 20 + }); + }); + Vue.nextTick(() => { + const yAxesScales = plotViewComponentObject.$children[0].component.$children[1].yScale; + yAxesScales.forEach((yAxisScale) => { + expect(yAxisScale.scale.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: { - 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 - } - }] + key: 'some-stacked-plot', + namespace: '' }, 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: '' - } - } - ] - } + series: [] } - }; + } + } + } + ] + ]; - 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 - } - }] - } - }; + openmct.router.path = [testTelemetryObject]; + mockComposition = new EventEmitter(); + mockComposition.load = () => { + mockComposition.emit('add', testTelemetryObject); - stackedPlotObject.composition = [{ - identifier: testTelemetryObject.identifier - }]; + return [testTelemetryObject]; + }; - mockCompositionList = []; - spyOn(openmct.composition, 'get').and.callFake((domainObject) => { - //We need unique compositions here - one for the StackedPlot view and one for the PlotLegend view - const numObjects = domainObject.composition.length; - const mockComposition = new EventEmitter(); - mockComposition.load = () => { - if (numObjects === 1) { - mockComposition.emit('add', testTelemetryObject); + spyOn(openmct.composition, 'get').and.returnValue(mockComposition); - return [testTelemetryObject]; - } else if (numObjects === 2) { - mockComposition.emit('add', testTelemetryObject); - mockComposition.emit('add', testTelemetryObject2); + 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); - return [testTelemetryObject, testTelemetryObject2]; - } else { - return []; - } - }; + 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: '' + }); - mockCompositionList.push(mockComposition); - - return mockComposition; - }); - - let viewContainer = document.createElement("div"); - child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - StackedPlot - }, - provide: { - openmct: openmct, - domainObject: stackedPlotObject, - path: [stackedPlotObject] - }, - template: "" - }); - - 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); - }); - - // disable due to flakiness - xit("Renders X-axis ticks for the telemetry object", () => { - 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 - }); - let ticks = xAxisElement[0].querySelectorAll(".gl-plot-tick"); - expect(ticks.length).toBe(9); - }); - - 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) => { - //setting composition here so that any new triggers to composition.load with correctly load the mockComposition in the beforeEach - stackedPlotObject.composition = [testTelemetryObject, testTelemetryObject2]; - mockCompositionList[0].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) => { - stackedPlotObject.composition = []; - mockCompositionList[0].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("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[1].xScale.domain()).toEqual({ - min: 0, - max: 10 - }); - done(); - }); - }); - - it("updates the yscale", (done) => { - const yAxisList = [config.yAxis, ...config.additionalYAxes]; - yAxisList.forEach((yAxis) => { - yAxis.set('displayRange', { - min: 10, - max: 20 - }); - }); - Vue.nextTick(() => { - const yAxesScales = plotViewComponentObject.$children[0].component.$children[1].yScale; - yAxesScales.forEach((yAxisScale) => { - expect(yAxisScale.scale.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(); - }); - }); + Vue.nextTick(() => { + viewComponentObject = component.$root.$children[0]; + 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: '' - }); - - 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(); - }); - }); - + afterEach(() => { + openmct.router.path = null; }); - 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 - } - }] - } - }; + describe('in view only mode', () => { + let browseOptionsEl; + beforeEach(() => { + browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse'); + }); - 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: [] - } - } - } - } - ] - ]; + it('shows legend properties', () => { + const legendPropertiesEl = browseOptionsEl.querySelector('.js-legend-properties'); + expect(legendPropertiesEl).not.toBeNull(); + }); - 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: '' - }); - - 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(); - }); - }); + 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: '' + }); + + 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 index b8296197e0..e9a7bd2fd3 100644 --- a/src/plugins/plot/stackedPlot/stackedPlotConfigurationInterceptor.js +++ b/src/plugins/plot/stackedPlot/stackedPlotConfigurationInterceptor.js @@ -21,18 +21,16 @@ *****************************************************************************/ 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 = []; + } - 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; - } - }); + return object; + } + }); } diff --git a/src/plugins/plot/tickUtils.js b/src/plugins/plot/tickUtils.js index a83a779891..98b3ed8515 100644 --- a/src/plugins/plot/tickUtils.js +++ b/src/plugins/plot/tickUtils.js @@ -1,4 +1,4 @@ -import { antisymlog, symlog } from "./mathUtils"; +import { antisymlog, symlog } from './mathUtils'; const e10 = Math.sqrt(50); const e5 = Math.sqrt(10); @@ -8,18 +8,18 @@ const e2 = Math.sqrt(2); * Nicely formatted tick steps from d3-array. */ function tickStep(start, stop, count) { - const step0 = Math.abs(stop - start) / Math.max(0, count); - let step1 = Math.pow(10, Math.floor(Math.log(step0) / Math.LN10)); - const error = step0 / step1; - if (error >= e10) { - step1 *= 10; - } else if (error >= e5) { - step1 *= 5; - } else if (error >= e2) { - step1 *= 2; - } + const step0 = Math.abs(stop - start) / Math.max(0, count); + let step1 = Math.pow(10, Math.floor(Math.log(step0) / Math.LN10)); + const error = step0 / step1; + if (error >= e10) { + step1 *= 10; + } else if (error >= e5) { + step1 *= 5; + } else if (error >= e2) { + step1 *= 2; + } - return stop < start ? -step1 : step1; + return stop < start ? -step1 : step1; } /** @@ -27,132 +27,130 @@ function tickStep(start, stop, count) { * ticks to precise values. */ function getPrecision(step) { - const exponential = step.toExponential(); - const i = exponential.indexOf('e'); - if (i === -1) { - return 0; - } + const exponential = step.toExponential(); + const i = exponential.indexOf('e'); + if (i === -1) { + return 0; + } - let precision = Math.max(0, -(Number(exponential.slice(i + 1)))); + let precision = Math.max(0, -Number(exponential.slice(i + 1))); - if (precision > 20) { - precision = 20; - } + if (precision > 20) { + precision = 20; + } - return precision; + return precision; } export function getLogTicks(start, stop, mainTickCount = 8, secondaryTickCount = 6) { - // log()'ed values - const mainLogTicks = ticks(start, stop, mainTickCount); + // log()'ed values + const mainLogTicks = ticks(start, stop, mainTickCount); - // original values - const mainTicks = mainLogTicks.map(n => antisymlog(n, 10)); + // original values + const mainTicks = mainLogTicks.map((n) => antisymlog(n, 10)); - const result = []; + const result = []; - let i = 0; - for (const logTick of mainLogTicks) { - result.push(logTick); + let i = 0; + for (const logTick of mainLogTicks) { + result.push(logTick); - if (i === mainLogTicks.length - 1) { - break; - } - - const tick = mainTicks[i]; - const nextTick = mainTicks[i + 1]; - const rangeBetweenMainTicks = nextTick - tick; - - const secondaryLogTicks = ticks( - tick + rangeBetweenMainTicks / (secondaryTickCount + 1), - nextTick - rangeBetweenMainTicks / (secondaryTickCount + 1), - secondaryTickCount - 2 - ) - .map(n => symlog(n, 10)); - - result.push(...secondaryLogTicks); - - i++; + if (i === mainLogTicks.length - 1) { + break; } - return result; + const tick = mainTicks[i]; + const nextTick = mainTicks[i + 1]; + const rangeBetweenMainTicks = nextTick - tick; + + const secondaryLogTicks = ticks( + tick + rangeBetweenMainTicks / (secondaryTickCount + 1), + nextTick - rangeBetweenMainTicks / (secondaryTickCount + 1), + secondaryTickCount - 2 + ).map((n) => symlog(n, 10)); + + result.push(...secondaryLogTicks); + + i++; + } + + return result; } /** * Linear tick generation from d3-array. */ export function ticks(start, stop, count) { - const step = tickStep(start, stop, count); - const precision = getPrecision(step); + const step = tickStep(start, stop, count); + const precision = getPrecision(step); - return _.range( - Math.ceil(start / step) * step, - Math.floor(stop / step) * step + step / 2, // inclusive - step - ).map(function round(tick) { - return Number(tick.toFixed(precision)); - }); + return _.range( + Math.ceil(start / step) * step, + Math.floor(stop / step) * step + step / 2, // inclusive + step + ).map(function round(tick) { + return Number(tick.toFixed(precision)); + }); } export function commonPrefix(a, b) { - const maxLen = Math.min(a.length, b.length); - let breakpoint = 0; - for (let i = 0; i < maxLen; i++) { - if (a[i] !== b[i]) { - break; - } - - if (a[i] === ' ') { - breakpoint = i + 1; - } + const maxLen = Math.min(a.length, b.length); + let breakpoint = 0; + for (let i = 0; i < maxLen; i++) { + if (a[i] !== b[i]) { + break; } - return a.slice(0, breakpoint); + if (a[i] === ' ') { + breakpoint = i + 1; + } + } + + return a.slice(0, breakpoint); } export function commonSuffix(a, b) { - const maxLen = Math.min(a.length, b.length); - let breakpoint = 0; - for (let i = 0; i <= maxLen; i++) { - if (a[a.length - i] !== b[b.length - i]) { - break; - } - - if ('. '.indexOf(a[a.length - i]) !== -1) { - breakpoint = i; - } + const maxLen = Math.min(a.length, b.length); + let breakpoint = 0; + for (let i = 0; i <= maxLen; i++) { + if (a[a.length - i] !== b[b.length - i]) { + break; } - return a.slice(a.length - breakpoint); + if ('. '.indexOf(a[a.length - i]) !== -1) { + breakpoint = i; + } + } + + return a.slice(a.length - breakpoint); } export function getFormattedTicks(newTicks, format) { - newTicks = newTicks - .map(function (tickValue) { - return { - value: tickValue, - text: format(tickValue) - }; - }); + newTicks = newTicks.map(function (tickValue) { + return { + value: tickValue, + text: format(tickValue) + }; + }); - if (newTicks.length && typeof newTicks[0].text === 'string') { - const tickText = newTicks.map(function (t) { - return t.text; - }); - const prefix = tickText.reduce(commonPrefix); - const suffix = tickText.reduce(commonSuffix); - newTicks.forEach(function (t) { - t.fullText = t.text; + if (newTicks.length && typeof newTicks[0].text === 'string') { + const tickText = newTicks.map(function (t) { + return t.text; + }); + const prefix = tickText.reduce(commonPrefix); + const suffix = tickText.reduce(commonSuffix); + newTicks.forEach(function (t) { + t.fullText = t.text; - if (typeof t.text === 'string') { - if (suffix.length) { - t.text = t.text.slice(prefix.length, -suffix.length); - } else { - t.text = t.text.slice(prefix.length); - } - } - }); - } + if (typeof t.text === 'string') { + if (suffix.length) { + t.text = t.text.slice(prefix.length, -suffix.length); + } else { + t.text = t.text.slice(prefix.length); + } + } + }); + } - return newTicks; + return newTicks; } diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js index a095e91a37..bcf2c04586 100644 --- a/src/plugins/plugins.js +++ b/src/plugins/plugins.js @@ -21,217 +21,217 @@ *****************************************************************************/ define([ - 'lodash', - './utcTimeSystem/plugin', - './remoteClock/plugin', - './localTimeSystem/plugin', - './ISOTimeFormat/plugin', - './myItems/plugin', - '../../example/generator/plugin', - '../../example/eventGenerator/plugin', - './autoflow/AutoflowTabularPlugin', - './timeConductor/plugin', - '../../example/imagery/plugin', - '../../example/faultManagement/exampleFaultSource', - './imagery/plugin', - './summaryWidget/plugin', - './URLIndicatorPlugin/URLIndicatorPlugin', - './telemetryMean/plugin', - './plot/plugin', - './charts/bar/plugin', - './charts/scatter/plugin', - './telemetryTable/plugin', - './staticRootPlugin/plugin', - './notebook/plugin', - './displayLayout/plugin', - './formActions/plugin', - './folderView/plugin', - './flexibleLayout/plugin', - './tabs/plugin', - './LADTable/plugin', - './filters/plugin', - './objectMigration/plugin', - './goToOriginalAction/plugin', - './openInNewTabAction/plugin', - './clearData/plugin', - './webPage/plugin', - './condition/plugin', - './conditionWidget/plugin', - './themes/espresso', - './themes/snow', - './URLTimeSettingsSynchronizer/plugin', - './notificationIndicator/plugin', - './newFolderAction/plugin', - './persistence/couch/plugin', - './defaultRootName/plugin', - './plan/plugin', - './viewDatumAction/plugin', - './viewLargeAction/plugin', - './interceptors/plugin', - './performanceIndicator/plugin', - './CouchDBSearchFolder/plugin', - './timeline/plugin', - './hyperlink/plugin', - './clock/plugin', - './DeviceClassifier/plugin', - './timer/plugin', - './userIndicator/plugin', - '../../example/exampleUser/plugin', - './localStorage/plugin', - './operatorStatus/plugin', - './gauge/GaugePlugin', - './timelist/plugin', - './faultManagement/FaultManagementPlugin', - '../../example/exampleTags/plugin', - './inspectorViews/plugin' + 'lodash', + './utcTimeSystem/plugin', + './remoteClock/plugin', + './localTimeSystem/plugin', + './ISOTimeFormat/plugin', + './myItems/plugin', + '../../example/generator/plugin', + '../../example/eventGenerator/plugin', + './autoflow/AutoflowTabularPlugin', + './timeConductor/plugin', + '../../example/imagery/plugin', + '../../example/faultManagement/exampleFaultSource', + './imagery/plugin', + './summaryWidget/plugin', + './URLIndicatorPlugin/URLIndicatorPlugin', + './telemetryMean/plugin', + './plot/plugin', + './charts/bar/plugin', + './charts/scatter/plugin', + './telemetryTable/plugin', + './staticRootPlugin/plugin', + './notebook/plugin', + './displayLayout/plugin', + './formActions/plugin', + './folderView/plugin', + './flexibleLayout/plugin', + './tabs/plugin', + './LADTable/plugin', + './filters/plugin', + './objectMigration/plugin', + './goToOriginalAction/plugin', + './openInNewTabAction/plugin', + './clearData/plugin', + './webPage/plugin', + './condition/plugin', + './conditionWidget/plugin', + './themes/espresso', + './themes/snow', + './URLTimeSettingsSynchronizer/plugin', + './notificationIndicator/plugin', + './newFolderAction/plugin', + './persistence/couch/plugin', + './defaultRootName/plugin', + './plan/plugin', + './viewDatumAction/plugin', + './viewLargeAction/plugin', + './interceptors/plugin', + './performanceIndicator/plugin', + './CouchDBSearchFolder/plugin', + './timeline/plugin', + './hyperlink/plugin', + './clock/plugin', + './DeviceClassifier/plugin', + './timer/plugin', + './userIndicator/plugin', + '../../example/exampleUser/plugin', + './localStorage/plugin', + './operatorStatus/plugin', + './gauge/GaugePlugin', + './timelist/plugin', + './faultManagement/FaultManagementPlugin', + '../../example/exampleTags/plugin', + './inspectorViews/plugin' ], function ( - _, - UTCTimeSystem, - RemoteClock, - LocalTimeSystem, - ISOTimeFormat, - MyItems, - GeneratorPlugin, - EventGeneratorPlugin, - AutoflowPlugin, - TimeConductorPlugin, - ExampleImagery, - ExampleFaultSource, - ImageryPlugin, - SummaryWidget, - URLIndicatorPlugin, - TelemetryMean, - PlotPlugin, - BarChartPlugin, - ScatterPlotPlugin, - TelemetryTablePlugin, - StaticRootPlugin, - Notebook, - DisplayLayoutPlugin, - FormActions, - FolderView, - FlexibleLayout, - Tabs, - LADTable, - Filters, - ObjectMigration, - GoToOriginalAction, - OpenInNewTabAction, - ClearData, - WebPagePlugin, - ConditionPlugin, - ConditionWidgetPlugin, - Espresso, - Snow, - URLTimeSettingsSynchronizer, - NotificationIndicator, - NewFolderAction, - CouchDBPlugin, - DefaultRootName, - PlanLayout, - ViewDatumAction, - ViewLargeAction, - ObjectInterceptors, - PerformanceIndicator, - CouchDBSearchFolder, - Timeline, - Hyperlink, - Clock, - DeviceClassifier, - Timer, - UserIndicator, - ExampleUser, - LocalStorage, - OperatorStatus, - GaugePlugin, - TimeList, - FaultManagementPlugin, - ExampleTags, - InspectorViews + _, + UTCTimeSystem, + RemoteClock, + LocalTimeSystem, + ISOTimeFormat, + MyItems, + GeneratorPlugin, + EventGeneratorPlugin, + AutoflowPlugin, + TimeConductorPlugin, + ExampleImagery, + ExampleFaultSource, + ImageryPlugin, + SummaryWidget, + URLIndicatorPlugin, + TelemetryMean, + PlotPlugin, + BarChartPlugin, + ScatterPlotPlugin, + TelemetryTablePlugin, + StaticRootPlugin, + Notebook, + DisplayLayoutPlugin, + FormActions, + FolderView, + FlexibleLayout, + Tabs, + LADTable, + Filters, + ObjectMigration, + GoToOriginalAction, + OpenInNewTabAction, + ClearData, + WebPagePlugin, + ConditionPlugin, + ConditionWidgetPlugin, + Espresso, + Snow, + URLTimeSettingsSynchronizer, + NotificationIndicator, + NewFolderAction, + CouchDBPlugin, + DefaultRootName, + PlanLayout, + ViewDatumAction, + ViewLargeAction, + ObjectInterceptors, + PerformanceIndicator, + CouchDBSearchFolder, + Timeline, + Hyperlink, + Clock, + DeviceClassifier, + Timer, + UserIndicator, + ExampleUser, + LocalStorage, + OperatorStatus, + GaugePlugin, + TimeList, + FaultManagementPlugin, + ExampleTags, + InspectorViews ) { - const plugins = {}; + 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.default; + 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.default; - plugins.UTCTimeSystem = UTCTimeSystem.default; - plugins.LocalTimeSystem = LocalTimeSystem; - plugins.RemoteClock = RemoteClock.default; + plugins.UTCTimeSystem = UTCTimeSystem.default; + plugins.LocalTimeSystem = LocalTimeSystem; + plugins.RemoteClock = RemoteClock.default; - plugins.MyItems = MyItems.default; + plugins.MyItems = MyItems.default; - plugins.StaticRootPlugin = StaticRootPlugin.default; + plugins.StaticRootPlugin = StaticRootPlugin.default; - /** - * A tabular view showing the latest values of multiple telemetry points at - * once. Formatted so that labels and values are aligned. - * - * @param {Object} [options] Optional settings to apply to the autoflow - * tabular view. Currently supports one option, 'type'. - * @param {string} [options.type] The key of an object type to apply this view - * to exclusively. - */ - plugins.AutoflowView = AutoflowPlugin; + /** + * A tabular view showing the latest values of multiple telemetry points at + * once. Formatted so that labels and values are aligned. + * + * @param {Object} [options] Optional settings to apply to the autoflow + * tabular view. Currently supports one option, 'type'. + * @param {string} [options.type] The key of an object type to apply this view + * to exclusively. + */ + plugins.AutoflowView = AutoflowPlugin; - plugins.Conductor = TimeConductorPlugin.default; + plugins.Conductor = TimeConductorPlugin.default; - plugins.CouchDB = CouchDBPlugin.default; + plugins.CouchDB = CouchDBPlugin.default; - plugins.ImageryPlugin = ImageryPlugin; - plugins.Plot = PlotPlugin.default; - plugins.BarChart = BarChartPlugin.default; - plugins.ScatterPlot = ScatterPlotPlugin.default; - plugins.TelemetryTable = TelemetryTablePlugin; + plugins.ImageryPlugin = ImageryPlugin; + plugins.Plot = PlotPlugin.default; + plugins.BarChart = BarChartPlugin.default; + plugins.ScatterPlot = ScatterPlotPlugin.default; + plugins.TelemetryTable = TelemetryTablePlugin; - plugins.SummaryWidget = SummaryWidget; - plugins.TelemetryMean = TelemetryMean; - plugins.URLIndicator = URLIndicatorPlugin; - 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; - plugins.FlexibleLayout = FlexibleLayout; - plugins.LADTable = LADTable.default; - plugins.Filters = Filters; - plugins.ObjectMigration = ObjectMigration.default; - plugins.GoToOriginalAction = GoToOriginalAction.default; - plugins.OpenInNewTabAction = OpenInNewTabAction.default; - plugins.ClearData = ClearData; - plugins.WebPage = WebPagePlugin.default; - plugins.Espresso = Espresso.default; - plugins.Snow = Snow.default; - plugins.Condition = ConditionPlugin.default; - plugins.ConditionWidget = ConditionWidgetPlugin.default; - plugins.URLTimeSettingsSynchronizer = URLTimeSettingsSynchronizer.default; - plugins.NotificationIndicator = NotificationIndicator.default; - plugins.NewFolderAction = NewFolderAction.default; - plugins.ISOTimeFormat = ISOTimeFormat.default; - plugins.DefaultRootName = DefaultRootName.default; - plugins.PlanLayout = PlanLayout.default; - plugins.ViewDatumAction = ViewDatumAction.default; - plugins.ViewLargeAction = ViewLargeAction.default; - plugins.ObjectInterceptors = ObjectInterceptors.default; - plugins.PerformanceIndicator = PerformanceIndicator.default; - plugins.CouchDBSearchFolder = CouchDBSearchFolder.default; - plugins.Timeline = Timeline.default; - plugins.Hyperlink = Hyperlink.default; - plugins.Clock = Clock.default; - plugins.Timer = Timer.default; - plugins.DeviceClassifier = DeviceClassifier.default; - plugins.UserIndicator = UserIndicator.default; - plugins.LocalStorage = LocalStorage.default; - plugins.OperatorStatus = OperatorStatus.default; - plugins.Gauge = GaugePlugin.default; - plugins.Timelist = TimeList.default; - plugins.InspectorViews = InspectorViews.default; + plugins.SummaryWidget = SummaryWidget; + plugins.TelemetryMean = TelemetryMean; + plugins.URLIndicator = URLIndicatorPlugin; + 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; + plugins.FlexibleLayout = FlexibleLayout; + plugins.LADTable = LADTable.default; + plugins.Filters = Filters; + plugins.ObjectMigration = ObjectMigration.default; + plugins.GoToOriginalAction = GoToOriginalAction.default; + plugins.OpenInNewTabAction = OpenInNewTabAction.default; + plugins.ClearData = ClearData; + plugins.WebPage = WebPagePlugin.default; + plugins.Espresso = Espresso.default; + plugins.Snow = Snow.default; + plugins.Condition = ConditionPlugin.default; + plugins.ConditionWidget = ConditionWidgetPlugin.default; + plugins.URLTimeSettingsSynchronizer = URLTimeSettingsSynchronizer.default; + plugins.NotificationIndicator = NotificationIndicator.default; + plugins.NewFolderAction = NewFolderAction.default; + plugins.ISOTimeFormat = ISOTimeFormat.default; + plugins.DefaultRootName = DefaultRootName.default; + plugins.PlanLayout = PlanLayout.default; + plugins.ViewDatumAction = ViewDatumAction.default; + plugins.ViewLargeAction = ViewLargeAction.default; + plugins.ObjectInterceptors = ObjectInterceptors.default; + plugins.PerformanceIndicator = PerformanceIndicator.default; + plugins.CouchDBSearchFolder = CouchDBSearchFolder.default; + plugins.Timeline = Timeline.default; + plugins.Hyperlink = Hyperlink.default; + plugins.Clock = Clock.default; + plugins.Timer = Timer.default; + plugins.DeviceClassifier = DeviceClassifier.default; + plugins.UserIndicator = UserIndicator.default; + plugins.LocalStorage = LocalStorage.default; + plugins.OperatorStatus = OperatorStatus.default; + plugins.Gauge = GaugePlugin.default; + plugins.Timelist = TimeList.default; + plugins.InspectorViews = InspectorViews.default; - return plugins; + return plugins; }); diff --git a/src/plugins/remoteClock/RemoteClock.js b/src/plugins/remoteClock/RemoteClock.js index bd77f15aad..7faa3b9290 100644 --- a/src/plugins/remoteClock/RemoteClock.js +++ b/src/plugins/remoteClock/RemoteClock.js @@ -33,135 +33,136 @@ import remoteClockRequestInterceptor from './requestInterceptor'; */ export default class RemoteClock extends DefaultClock { - constructor(openmct, identifier) { - super(); + constructor(openmct, identifier) { + super(); - this.key = 'remote-clock'; + this.key = 'remote-clock'; - this.openmct = openmct; - this.identifier = identifier; + this.openmct = openmct; + this.identifier = identifier; - this.name = 'Remote Clock'; - this.description = "Provides telemetry based timestamps from a configurable source."; + this.name = 'Remote Clock'; + this.description = 'Provides telemetry based timestamps from a configurable source.'; - this.timeTelemetryObject = undefined; - this.parseTime = undefined; - this.formatTime = undefined; - this.metadata = undefined; + this.timeTelemetryObject = undefined; + this.parseTime = undefined; + this.formatTime = undefined; + this.metadata = undefined; - this.lastTick = 0; + this.lastTick = 0; - this.openmct.telemetry.addRequestInterceptor( - remoteClockRequestInterceptor( - this.openmct, - this.identifier, - this.#waitForReady.bind(this) - ) - ); + this.openmct.telemetry.addRequestInterceptor( + remoteClockRequestInterceptor(this.openmct, this.identifier, this.#waitForReady.bind(this)) + ); - this._processDatum = this._processDatum.bind(this); + this._processDatum = this._processDatum.bind(this); + } + + start() { + this.openmct.objects + .get(this.identifier) + .then((domainObject) => { + this.openmct.time.on('timeSystem', this._timeSystemChange); + this.timeTelemetryObject = domainObject; + this.metadata = this.openmct.telemetry.getMetadata(domainObject); + this._timeSystemChange(); + this._requestLatest(); + this._subscribe(); + }) + .catch((error) => { + throw new Error(error); + }); + } + + stop() { + this.openmct.time.off('timeSystem', this._timeSystemChange); + if (this._unsubscribe) { + this._unsubscribe(); } - start() { - this.openmct.objects.get(this.identifier).then((domainObject) => { - this.openmct.time.on('timeSystem', this._timeSystemChange); - this.timeTelemetryObject = domainObject; - this.metadata = this.openmct.telemetry.getMetadata(domainObject); - this._timeSystemChange(); - this._requestLatest(); - this._subscribe(); - }).catch((error) => { - throw new Error(error); + this.removeAllListeners(); + } + + /** + * Will start a subscription to the timeTelemetryObject as well + * handle the unsubscribe callback + * + * @private + */ + _subscribe() { + this._unsubscribe = this.openmct.telemetry.subscribe( + this.timeTelemetryObject, + this._processDatum + ); + } + + /** + * Will request the latest data for the timeTelemetryObject + * + * @private + */ + _requestLatest() { + this.openmct.telemetry + .request(this.timeTelemetryObject, { + size: 1, + strategy: 'latest' + }) + .then((data) => { + this._processDatum(data[data.length - 1]); + }); + } + + /** + * Function to parse the datum from the timeTelemetryObject as well + * as check if it's valid, calls "tick" + * + * @private + */ + _processDatum(datum) { + let time = this.parseTime(datum); + + if (time > this.lastTick) { + this.tick(time); + } + } + + /** + * Callback function for timeSystem change events + * + * @private + */ + _timeSystemChange() { + let timeSystem = this.openmct.time.timeSystem(); + let timeKey = timeSystem.key; + let metadataValue = this.metadata.value(timeKey); + let timeFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); + this.parseTime = (datum) => { + return timeFormatter.parse(datum); + }; + + this.formatTime = (datum) => { + return timeFormatter.format(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); + } + }; - stop() { - this.openmct.time.off('timeSystem', this._timeSystemChange); - if (this._unsubscribe) { - this._unsubscribe(); - } - - this.removeAllListeners(); - } - - /** - * Will start a subscription to the timeTelemetryObject as well - * handle the unsubscribe callback - * - * @private - */ - _subscribe() { - this._unsubscribe = this.openmct.telemetry.subscribe( - this.timeTelemetryObject, - this._processDatum - ); - } - - /** - * Will request the latest data for the timeTelemetryObject - * - * @private - */ - _requestLatest() { - this.openmct.telemetry.request(this.timeTelemetryObject, { - size: 1, - strategy: 'latest' - }).then(data => { - this._processDatum(data[data.length - 1]); - }); - } - - /** - * Function to parse the datum from the timeTelemetryObject as well - * as check if it's valid, calls "tick" - * - * @private - */ - _processDatum(datum) { - let time = this.parseTime(datum); - - if (time > this.lastTick) { - this.tick(time); - } - } - - /** - * Callback function for timeSystem change events - * - * @private - */ - _timeSystemChange() { - let timeSystem = this.openmct.time.timeSystem(); - let timeKey = timeSystem.key; - let metadataValue = this.metadata.value(timeKey); - let timeFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); - this.parseTime = (datum) => { - return timeFormatter.parse(datum); - }; - - this.formatTime = (datum) => { - return timeFormatter.format(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); - } + return new Promise(waitForInitialTick); + } } diff --git a/src/plugins/remoteClock/RemoteClockSpec.js b/src/plugins/remoteClock/RemoteClockSpec.js index bfbb843856..e41ea872ac 100644 --- a/src/plugins/remoteClock/RemoteClockSpec.js +++ b/src/plugins/remoteClock/RemoteClockSpec.js @@ -20,144 +20,148 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { - createOpenMct, - resetApplicationState -} from 'utils/testing'; +import { createOpenMct, resetApplicationState } from 'utils/testing'; const REMOTE_CLOCK_KEY = 'remote-clock'; const TIME_TELEMETRY_ID = { - namespace: 'remote', - key: 'telemetry' + namespace: 'remote', + key: 'telemetry' }; const TIME_VALUE = 12345; const REQ_OPTIONS = { - size: 1, - strategy: 'latest' + size: 1, + strategy: 'latest' }; const OFFSET_START = -10; const OFFSET_END = 1; -describe("the RemoteClock plugin", () => { - let openmct; - let object = { - name: 'remote-telemetry', - identifier: TIME_TELEMETRY_ID +describe('the RemoteClock plugin', () => { + let openmct; + let object = { + name: 'remote-telemetry', + identifier: TIME_TELEMETRY_ID + }; + + beforeEach((done) => { + openmct = createOpenMct(); + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + describe('once installed', () => { + let remoteClock; + let boundsCallback; + let metadataValue = { some: 'value' }; + let timeSystem = { key: 'utc' }; + let metadata = { + value: () => metadataValue + }; + let reqDatum = { + key: TIME_VALUE }; - beforeEach((done) => { - openmct = createOpenMct(); - openmct.on('start', done); - openmct.startHeadless(); + let formatter = { + parse: (datum) => datum.key + }; + + let objectPromise; + let requestPromise; + + beforeEach(() => { + openmct.install(openmct.plugins.RemoteClock(TIME_TELEMETRY_ID)); + + let clocks = openmct.time.getAllClocks(); + remoteClock = clocks.filter((clock) => clock.key === REMOTE_CLOCK_KEY)[0]; + + boundsCallback = jasmine.createSpy('boundsCallback'); + openmct.time.on('bounds', boundsCallback); + + spyOn(remoteClock, '_timeSystemChange').and.callThrough(); + spyOn(openmct.telemetry, 'getMetadata').and.returnValue(metadata); + spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue(formatter); + spyOn(openmct.telemetry, 'subscribe').and.callThrough(); + spyOn(openmct.time, 'on').and.callThrough(); + spyOn(openmct.time, 'timeSystem').and.returnValue(timeSystem); + spyOn(metadata, 'value').and.callThrough(); + + let requestPromiseResolve; + let objectPromiseResolve; + + requestPromise = new Promise((resolve) => { + requestPromiseResolve = resolve; + }); + spyOn(openmct.telemetry, 'request').and.callFake(() => { + requestPromiseResolve([reqDatum]); + + return requestPromise; + }); + + objectPromise = new Promise((resolve) => { + objectPromiseResolve = resolve; + }); + spyOn(openmct.objects, 'get').and.callFake(() => { + objectPromiseResolve(object); + + return objectPromise; + }); + + openmct.time.clock(REMOTE_CLOCK_KEY, { + start: OFFSET_START, + end: OFFSET_END + }); }); - afterEach(() => { - return resetApplicationState(openmct); + it('Does not throw error if time system is changed before remote clock initialized', () => { + expect(() => openmct.time.timeSystem('utc')).not.toThrow(); }); - describe('once installed', () => { - let remoteClock; - let boundsCallback; - let metadataValue = { some: 'value' }; - let timeSystem = { key: 'utc' }; - let metadata = { - value: () => metadataValue - }; - let reqDatum = { - key: TIME_VALUE - }; + describe('once resolved', () => { + beforeEach(async () => { + await Promise.all([objectPromise, requestPromise]); + }); - let formatter = { - parse: (datum) => datum.key - }; + it('is available and sets up initial values and listeners', () => { + expect(remoteClock.key).toEqual(REMOTE_CLOCK_KEY); + expect(remoteClock.identifier).toEqual(TIME_TELEMETRY_ID); + expect(openmct.time.on).toHaveBeenCalledWith('timeSystem', remoteClock._timeSystemChange); + expect(remoteClock._timeSystemChange).toHaveBeenCalled(); + }); - let objectPromise; - let requestPromise; + it('will request/store the object based on the identifier passed in', () => { + expect(remoteClock.timeTelemetryObject).toEqual(object); + }); - beforeEach(() => { - openmct.install(openmct.plugins.RemoteClock(TIME_TELEMETRY_ID)); + it('will request metadata and set up formatters', () => { + expect(remoteClock.metadata).toEqual(metadata); + expect(metadata.value).toHaveBeenCalled(); + expect(openmct.telemetry.getValueFormatter).toHaveBeenCalledWith(metadataValue); + }); - let clocks = openmct.time.getAllClocks(); - remoteClock = clocks.filter(clock => clock.key === REMOTE_CLOCK_KEY)[0]; - - boundsCallback = jasmine.createSpy("boundsCallback"); - openmct.time.on('bounds', boundsCallback); - - spyOn(remoteClock, '_timeSystemChange').and.callThrough(); - spyOn(openmct.telemetry, 'getMetadata').and.returnValue(metadata); - spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue(formatter); - spyOn(openmct.telemetry, 'subscribe').and.callThrough(); - spyOn(openmct.time, 'on').and.callThrough(); - spyOn(openmct.time, 'timeSystem').and.returnValue(timeSystem); - spyOn(metadata, 'value').and.callThrough(); - - let requestPromiseResolve; - let objectPromiseResolve; - - requestPromise = new Promise((resolve) => { - requestPromiseResolve = resolve; - }); - spyOn(openmct.telemetry, 'request').and.callFake(() => { - requestPromiseResolve([reqDatum]); - - return requestPromise; - }); - - objectPromise = new Promise((resolve) => { - objectPromiseResolve = resolve; - }); - spyOn(openmct.objects, 'get').and.callFake(() => { - objectPromiseResolve(object); - - return objectPromise; - }); - - openmct.time.clock(REMOTE_CLOCK_KEY, { - start: OFFSET_START, - end: OFFSET_END - }); - }); - - it("Does not throw error if time system is changed before remote clock initialized", () => { - expect(() => openmct.time.timeSystem('utc')).not.toThrow(); - }); - - describe('once resolved', () => { - beforeEach(async () => { - await Promise.all([objectPromise, requestPromise]); - }); - - it('is available and sets up initial values and listeners', () => { - expect(remoteClock.key).toEqual(REMOTE_CLOCK_KEY); - expect(remoteClock.identifier).toEqual(TIME_TELEMETRY_ID); - expect(openmct.time.on).toHaveBeenCalledWith('timeSystem', remoteClock._timeSystemChange); - expect(remoteClock._timeSystemChange).toHaveBeenCalled(); - }); - - it('will request/store the object based on the identifier passed in', () => { - expect(remoteClock.timeTelemetryObject).toEqual(object); - }); - - it('will request metadata and set up formatters', () => { - expect(remoteClock.metadata).toEqual(metadata); - expect(metadata.value).toHaveBeenCalled(); - expect(openmct.telemetry.getValueFormatter).toHaveBeenCalledWith(metadataValue); - }); - - it('will request the latest datum for the object it received and process the datum returned', () => { - expect(openmct.telemetry.request).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, REQ_OPTIONS); - expect(boundsCallback).toHaveBeenCalledWith({ - start: TIME_VALUE + OFFSET_START, - end: TIME_VALUE + OFFSET_END - }, true); - }); - - it('will set up subscriptions correctly', () => { - expect(remoteClock._unsubscribe).toBeDefined(); - expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(remoteClock.timeTelemetryObject, remoteClock._processDatum); - }); - }); + it('will request the latest datum for the object it received and process the datum returned', () => { + expect(openmct.telemetry.request).toHaveBeenCalledWith( + remoteClock.timeTelemetryObject, + REQ_OPTIONS + ); + expect(boundsCallback).toHaveBeenCalledWith( + { + start: TIME_VALUE + OFFSET_START, + end: TIME_VALUE + OFFSET_END + }, + true + ); + }); + it('will set up subscriptions correctly', () => { + expect(remoteClock._unsubscribe).toBeDefined(); + expect(openmct.telemetry.subscribe).toHaveBeenCalledWith( + remoteClock.timeTelemetryObject, + remoteClock._processDatum + ); + }); }); - + }); }); diff --git a/src/plugins/remoteClock/plugin.js b/src/plugins/remoteClock/plugin.js index 93b2640d37..749fee9abe 100644 --- a/src/plugins/remoteClock/plugin.js +++ b/src/plugins/remoteClock/plugin.js @@ -20,13 +20,13 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import RemoteClock from "./RemoteClock"; +import RemoteClock from './RemoteClock'; /** * Install a clock that uses a configurable telemetry endpoint. */ export default function (identifier) { - return function (openmct) { - openmct.time.addClock(new RemoteClock(openmct, identifier)); - }; + return function (openmct) { + openmct.time.addClock(new RemoteClock(openmct, identifier)); + }; } diff --git a/src/plugins/remoteClock/requestInterceptor.js b/src/plugins/remoteClock/requestInterceptor.js index 4102f87bdd..a963249e8b 100644 --- a/src/plugins/remoteClock/requestInterceptor.js +++ b/src/plugins/remoteClock/requestInterceptor.js @@ -21,24 +21,24 @@ *****************************************************************************/ function remoteClockRequestInterceptor(openmct, _remoteClockIdentifier, waitForBounds) { - let remoteClockLoaded = false; + let remoteClockLoaded = false; - return { - appliesTo: () => { - // Get the activeClock from the Global Time Context - const { activeClock } = openmct.time; + 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 activeClock?.key === 'remote-clock' && !remoteClockLoaded; + }, + invoke: async (request) => { + const { start, end } = await waitForBounds(); + remoteClockLoaded = true; + request.start = start; + request.end = end; - return request; - } - }; + return request; + } + }; } export default remoteClockRequestInterceptor; diff --git a/src/plugins/remove/RemoveAction.js b/src/plugins/remove/RemoveAction.js index 68cee6ef59..0671dfd75e 100644 --- a/src/plugins/remove/RemoveAction.js +++ b/src/plugins/remove/RemoveAction.js @@ -23,141 +23,142 @@ const SPECIAL_MESSAGE_TYPES = ['layout', 'flexible-layout']; export default class RemoveAction { - #transaction; + #transaction; - constructor(openmct) { + constructor(openmct) { + this.name = 'Remove'; + this.key = 'remove'; + this.description = 'Remove this object from its containing object.'; + this.cssClass = 'icon-trash'; + this.group = 'action'; + this.priority = 1; - this.name = 'Remove'; - this.key = 'remove'; - this.description = 'Remove this object from its containing object.'; - this.cssClass = "icon-trash"; - this.group = "action"; - this.priority = 1; + this.openmct = openmct; - this.openmct = openmct; + this.removeFromComposition = this.removeFromComposition.bind(this); // for access to private transaction variable + this.#transaction = null; + } - this.removeFromComposition = this.removeFromComposition.bind(this); // for access to private transaction variable - this.#transaction = null; + async invoke(objectPath) { + const child = objectPath[0]; + const parent = objectPath[1]; + + try { + await this.showConfirmDialog(child, parent); + } catch (error) { + return; // form canceled, exit invoke } - async invoke(objectPath) { - const child = objectPath[0]; - const parent = objectPath[1]; + await this.removeFromComposition(parent, child, objectPath); - try { - await this.showConfirmDialog(child, parent); - } catch (error) { - return; // form canceled, exit invoke - } + if (this.inNavigationPath(child)) { + this.navigateTo(objectPath.slice(1)); + } + } - await this.removeFromComposition(parent, child, objectPath); + showConfirmDialog(child, parent) { + let message = + 'Warning! This action will remove this object. Are you sure you want to continue?'; - if (this.inNavigationPath(child)) { - this.navigateTo(objectPath.slice(1)); - } + if (SPECIAL_MESSAGE_TYPES.includes(parent.type)) { + const type = this.openmct.types.get(parent.type); + const typeName = type.definition.name; + + message = `Warning! This action will remove this item from the ${typeName}. Are you sure you want to continue?`; } - showConfirmDialog(child, parent) { - let message = 'Warning! This action will remove this object. Are you sure you want to continue?'; - - if (SPECIAL_MESSAGE_TYPES.includes(parent.type)) { - const type = this.openmct.types.get(parent.type); - const typeName = type.definition.name; - - message = `Warning! This action will remove this item from the ${typeName}. Are you sure you want to continue?`; - } - - return new Promise((resolve, reject) => { - const dialog = this.openmct.overlays.dialog({ - title: `Remove ${child.name}`, - iconClass: 'alert', - message, - buttons: [ - { - label: 'OK', - callback: () => { - dialog.dismiss(); - resolve(); - } - }, - { - label: 'Cancel', - callback: () => { - dialog.dismiss(); - reject(); - } - } - ] - }); - }); - } - - inNavigationPath(object) { - return this.openmct.router.path - .some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, object.identifier)); - } - - navigateTo(objectPath) { - let urlPath = objectPath.reverse() - .map(object => this.openmct.objects.makeKeyString(object.identifier)) - .join("/"); - - this.openmct.router.navigate('#/browse/' + urlPath); - } - - async removeFromComposition(parent, child, objectPath) { - this.startTransaction(); - - const composition = this.openmct.composition.get(parent); - composition.remove(child); - - if (!this.openmct.objects.isObjectPathToALink(child, objectPath)) { - this.openmct.objects.mutate(child, 'location', null); - } - - if (this.inNavigationPath(child) && this.openmct.editor.isEditing()) { - this.openmct.editor.save(); - } - - await this.saveTransaction(); - } - - appliesTo(objectPath) { - const parent = objectPath[1]; - const parentType = parent && this.openmct.types.get(parent.type); - const child = objectPath[0]; - const locked = child.locked ? child.locked : parent && parent.locked; - const isEditing = this.openmct.editor.isEditing(); - const isPersistable = this.openmct.objects.isPersistable(child.identifier); - const isLink = this.openmct.objects.isObjectPathToALink(child, objectPath); - - if (!isLink && (locked || !isPersistable)) { - return false; - } - - if (isEditing) { - if (this.openmct.router.isNavigatedObject(objectPath)) { - return false; + return new Promise((resolve, reject) => { + const dialog = this.openmct.overlays.dialog({ + title: `Remove ${child.name}`, + iconClass: 'alert', + message, + buttons: [ + { + label: 'OK', + callback: () => { + dialog.dismiss(); + resolve(); } - } + }, + { + label: 'Cancel', + callback: () => { + dialog.dismiss(); + reject(); + } + } + ] + }); + }); + } - return parentType?.definition.creatable - && Array.isArray(parent?.composition); + inNavigationPath(object) { + return this.openmct.router.path.some((objectInPath) => + this.openmct.objects.areIdsEqual(objectInPath.identifier, object.identifier) + ); + } + + navigateTo(objectPath) { + let urlPath = objectPath + .reverse() + .map((object) => this.openmct.objects.makeKeyString(object.identifier)) + .join('/'); + + this.openmct.router.navigate('#/browse/' + urlPath); + } + + async removeFromComposition(parent, child, objectPath) { + this.startTransaction(); + + const composition = this.openmct.composition.get(parent); + composition.remove(child); + + if (!this.openmct.objects.isObjectPathToALink(child, objectPath)) { + this.openmct.objects.mutate(child, 'location', null); } - startTransaction() { - if (!this.openmct.objects.isTransactionActive()) { - this.#transaction = this.openmct.objects.startTransaction(); - } + if (this.inNavigationPath(child) && this.openmct.editor.isEditing()) { + this.openmct.editor.save(); } - async saveTransaction() { - if (!this.#transaction) { - return; - } + await this.saveTransaction(); + } - await this.#transaction.commit(); - this.openmct.objects.endTransaction(); - this.#transaction = null; + appliesTo(objectPath) { + const parent = objectPath[1]; + const parentType = parent && this.openmct.types.get(parent.type); + const child = objectPath[0]; + const locked = child.locked ? child.locked : parent && parent.locked; + const isEditing = this.openmct.editor.isEditing(); + const isPersistable = this.openmct.objects.isPersistable(child.identifier); + const isLink = this.openmct.objects.isObjectPathToALink(child, objectPath); + + if (!isLink && (locked || !isPersistable)) { + return false; } + + if (isEditing) { + if (this.openmct.router.isNavigatedObject(objectPath)) { + return false; + } + } + + return parentType?.definition.creatable && Array.isArray(parent?.composition); + } + + startTransaction() { + if (!this.openmct.objects.isTransactionActive()) { + this.#transaction = this.openmct.objects.startTransaction(); + } + } + + async saveTransaction() { + if (!this.#transaction) { + return; + } + + await this.#transaction.commit(); + this.openmct.objects.endTransaction(); + this.#transaction = null; + } } diff --git a/src/plugins/remove/plugin.js b/src/plugins/remove/plugin.js index 60ff4eb055..03cd1e878a 100644 --- a/src/plugins/remove/plugin.js +++ b/src/plugins/remove/plugin.js @@ -19,10 +19,10 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import RemoveAction from "./RemoveAction"; +import RemoveAction from './RemoveAction'; export default function () { - return function (openmct) { - openmct.actions.register(new RemoveAction(openmct)); - }; + return function (openmct) { + openmct.actions.register(new RemoveAction(openmct)); + }; } diff --git a/src/plugins/remove/pluginSpec.js b/src/plugins/remove/pluginSpec.js index 0c592be8c7..404546610f 100644 --- a/src/plugins/remove/pluginSpec.js +++ b/src/plugins/remove/pluginSpec.js @@ -19,119 +19,112 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import { - createOpenMct, - resetApplicationState, - getMockObjects -} from 'utils/testing'; +import { createOpenMct, resetApplicationState, getMockObjects } from 'utils/testing'; -describe("The Remove Action plugin", () => { +describe('The Remove Action plugin', () => { + let openmct; + let removeAction; + let childObject; + let parentObject; - let openmct; - let removeAction; - let childObject; - let parentObject; + // this setups up the app + beforeEach((done) => { + openmct = createOpenMct(); - // this setups up the app - beforeEach((done) => { - openmct = createOpenMct(); + childObject = getMockObjects({ + objectKeyStrings: ['folder'], + overwrite: { + folder: { + name: 'Child Folder', + identifier: { + namespace: '', + key: 'child-folder-object' + } + } + } + }).folder; + parentObject = getMockObjects({ + objectKeyStrings: ['folder'], + overwrite: { + folder: { + identifier: { + namespace: '', + key: 'parent-folder-object' + }, + name: 'Parent Folder', + composition: [childObject.identifier] + } + } + }).folder; - childObject = getMockObjects({ - objectKeyStrings: ['folder'], - overwrite: { - folder: { - name: "Child Folder", - identifier: { - namespace: "", - key: "child-folder-object" - } - } - } - }).folder; - parentObject = getMockObjects({ - objectKeyStrings: ['folder'], - overwrite: { - folder: { - identifier: { - namespace: "", - key: "parent-folder-object" - }, - name: "Parent Folder", - composition: [childObject.identifier] - } - } - }).folder; + openmct.on('start', done); + openmct.startHeadless(); - openmct.on('start', done); - openmct.startHeadless(); + removeAction = openmct.actions._allActions.remove; + }); - removeAction = openmct.actions._allActions.remove; + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('should be defined', () => { + expect(removeAction).toBeDefined(); + }); + + describe('when removing an object from a parent composition', () => { + beforeEach(() => { + spyOn(removeAction, 'removeFromComposition').and.callThrough(); + spyOn(removeAction, 'inNavigationPath').and.returnValue(false); + spyOn(openmct.objects, 'mutate').and.callThrough(); + spyOn(openmct.objects, 'startTransaction').and.callThrough(); + spyOn(openmct.objects, 'endTransaction').and.callThrough(); + removeAction.removeFromComposition(parentObject, childObject); }); - afterEach(() => { - return resetApplicationState(openmct); + it('removeFromComposition should be called with the parent and child', () => { + expect(removeAction.removeFromComposition).toHaveBeenCalled(); + expect(removeAction.removeFromComposition).toHaveBeenCalledWith(parentObject, childObject); }); - it("should be defined", () => { - expect(removeAction).toBeDefined(); + it('it should mutate the parent object', () => { + expect(openmct.objects.mutate).toHaveBeenCalled(); + expect(openmct.objects.mutate.calls.argsFor(0)[0]).toEqual(parentObject); }); - describe("when removing an object from a parent composition", () => { - - beforeEach(() => { - spyOn(removeAction, 'removeFromComposition').and.callThrough(); - spyOn(removeAction, 'inNavigationPath').and.returnValue(false); - spyOn(openmct.objects, 'mutate').and.callThrough(); - spyOn(openmct.objects, 'startTransaction').and.callThrough(); - spyOn(openmct.objects, 'endTransaction').and.callThrough(); - removeAction.removeFromComposition(parentObject, childObject); - }); - - it("removeFromComposition should be called with the parent and child", () => { - expect(removeAction.removeFromComposition).toHaveBeenCalled(); - expect(removeAction.removeFromComposition).toHaveBeenCalledWith(parentObject, childObject); - }); - - it("it should mutate the parent object", () => { - expect(openmct.objects.mutate).toHaveBeenCalled(); - expect(openmct.objects.mutate.calls.argsFor(0)[0]).toEqual(parentObject); - }); - - it("it should start a transaction", () => { - expect(openmct.objects.startTransaction).toHaveBeenCalled(); - }); - - it("it should end the transaction", (done) => { - setTimeout(() => { - expect(openmct.objects.endTransaction).toHaveBeenCalled(); - done(); - }, 100); - }); + it('it should start a transaction', () => { + expect(openmct.objects.startTransaction).toHaveBeenCalled(); }); - describe("when determining the object is applicable", () => { - - beforeEach(() => { - spyOn(removeAction, 'appliesTo').and.callThrough(); - }); - - it("should be true when the parent is creatable and has composition", () => { - let applies = removeAction.appliesTo([childObject, parentObject]); - expect(applies).toBe(true); - }); - - it("should be false when the child is locked and not an alias", () => { - childObject.locked = true; - childObject.location = 'parent-folder-object'; - let applies = removeAction.appliesTo([childObject, parentObject]); - expect(applies).toBe(false); - }); - - it("should be true when the child is locked and IS an alias", () => { - childObject.locked = true; - childObject.location = 'other-folder-object'; - let applies = removeAction.appliesTo([childObject, parentObject]); - expect(applies).toBe(true); - }); + it('it should end the transaction', (done) => { + setTimeout(() => { + expect(openmct.objects.endTransaction).toHaveBeenCalled(); + done(); + }, 100); }); + }); + + describe('when determining the object is applicable', () => { + beforeEach(() => { + spyOn(removeAction, 'appliesTo').and.callThrough(); + }); + + it('should be true when the parent is creatable and has composition', () => { + let applies = removeAction.appliesTo([childObject, parentObject]); + expect(applies).toBe(true); + }); + + it('should be false when the child is locked and not an alias', () => { + childObject.locked = true; + childObject.location = 'parent-folder-object'; + let applies = removeAction.appliesTo([childObject, parentObject]); + expect(applies).toBe(false); + }); + + it('should be true when the child is locked and IS an alias', () => { + childObject.locked = true; + childObject.location = 'other-folder-object'; + let applies = removeAction.appliesTo([childObject, parentObject]); + expect(applies).toBe(true); + }); + }); }); diff --git a/src/plugins/staticRootPlugin/StaticModelProvider.js b/src/plugins/staticRootPlugin/StaticModelProvider.js index 4bf9ffbca3..9d2248d7a5 100644 --- a/src/plugins/staticRootPlugin/StaticModelProvider.js +++ b/src/plugins/staticRootPlugin/StaticModelProvider.js @@ -29,159 +29,171 @@ import objectUtils from 'objectUtils'; class StaticModelProvider { - constructor(importData, rootIdentifier) { - this.objectMap = {}; - this.rewriteModel(importData, rootIdentifier); + constructor(importData, rootIdentifier) { + this.objectMap = {}; + this.rewriteModel(importData, rootIdentifier); + } + + /** + * Standard "Get". + */ + get(identifier) { + const keyString = objectUtils.makeKeyString(identifier); + if (this.objectMap[keyString]) { + return this.objectMap[keyString]; } - /** - * Standard "Get". - */ - get(identifier) { - const keyString = objectUtils.makeKeyString(identifier); - if (this.objectMap[keyString]) { - return this.objectMap[keyString]; - } + throw new Error(keyString + ' not found in import models.'); + } - throw new Error(keyString + ' not found in import models.'); + parseObjectLeaf(objectLeaf, idMap, newRootNamespace, oldRootNamespace) { + Object.keys(objectLeaf).forEach((nodeKey) => { + if (idMap.get(nodeKey)) { + const newIdentifier = objectUtils.makeKeyString({ + namespace: newRootNamespace, + key: idMap.get(nodeKey) + }); + objectLeaf[newIdentifier] = { ...objectLeaf[nodeKey] }; + delete objectLeaf[nodeKey]; + objectLeaf[newIdentifier] = this.parseTreeLeaf( + newIdentifier, + objectLeaf[newIdentifier], + idMap, + newRootNamespace, + oldRootNamespace + ); + } else { + objectLeaf[nodeKey] = this.parseTreeLeaf( + nodeKey, + objectLeaf[nodeKey], + idMap, + newRootNamespace, + oldRootNamespace + ); + } + }); + + return objectLeaf; + } + + parseArrayLeaf(arrayLeaf, idMap, newRootNamespace, oldRootNamespace) { + return arrayLeaf.map((leafValue, index) => + this.parseTreeLeaf(null, leafValue, idMap, newRootNamespace, oldRootNamespace) + ); + } + + parseBranchedLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace) { + if (Array.isArray(branchedLeafValue)) { + return this.parseArrayLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace); + } else { + return this.parseObjectLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace); + } + } + + parseTreeLeaf(leafKey, leafValue, idMap, newRootNamespace, oldRootNamespace) { + if (leafValue === null || leafValue === undefined) { + return leafValue; } - parseObjectLeaf(objectLeaf, idMap, newRootNamespace, oldRootNamespace) { - Object.keys(objectLeaf).forEach((nodeKey) => { - if (idMap.get(nodeKey)) { - const newIdentifier = objectUtils.makeKeyString({ - namespace: newRootNamespace, - key: idMap.get(nodeKey) - }); - objectLeaf[newIdentifier] = { ...objectLeaf[nodeKey] }; - delete objectLeaf[nodeKey]; - objectLeaf[newIdentifier] = this.parseTreeLeaf(newIdentifier, objectLeaf[newIdentifier], idMap, newRootNamespace, oldRootNamespace); - } else { - objectLeaf[nodeKey] = this.parseTreeLeaf(nodeKey, objectLeaf[nodeKey], idMap, newRootNamespace, oldRootNamespace); - } + const hasChild = typeof leafValue === 'object'; + if (hasChild) { + return this.parseBranchedLeaf(leafValue, idMap, newRootNamespace, oldRootNamespace); + } + + if (leafKey === 'key') { + let mappedLeafValue; + if (oldRootNamespace) { + mappedLeafValue = idMap.get( + objectUtils.makeKeyString({ + namespace: oldRootNamespace, + key: leafValue + }) + ); + } else { + mappedLeafValue = idMap.get(leafValue); + } + + return mappedLeafValue ?? leafValue; + } else if (leafKey === 'namespace') { + // Only rewrite the namespace if it matches the old root namespace. + // This is to prevent rewriting namespaces of objects that are not + // children of the root object (e.g.: objects from a telemetry dictionary) + return leafValue === oldRootNamespace ? newRootNamespace : leafValue; + } else if (leafKey === 'location') { + const mappedLeafValue = idMap.get(leafValue); + if (!mappedLeafValue) { + return null; + } + + const newLocationIdentifier = objectUtils.makeKeyString({ + namespace: newRootNamespace, + key: mappedLeafValue + }); + + return newLocationIdentifier; + } else { + const mappedLeafValue = idMap.get(leafValue); + if (mappedLeafValue) { + const newIdentifier = objectUtils.makeKeyString({ + namespace: newRootNamespace, + key: mappedLeafValue }); - return objectLeaf; + return newIdentifier; + } else { + return leafValue; + } } + } - parseArrayLeaf(arrayLeaf, idMap, newRootNamespace, oldRootNamespace) { - return arrayLeaf.map((leafValue, index) => this.parseTreeLeaf( - null, leafValue, idMap, newRootNamespace, oldRootNamespace)); - } + rewriteObjectIdentifiers(importData, rootIdentifier) { + const { namespace: oldRootNamespace } = objectUtils.parseKeyString(importData.rootId); + const { namespace: newRootNamespace } = rootIdentifier; + const idMap = new Map(); + const objectTree = importData.openmct; - parseBranchedLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace) { - if (Array.isArray(branchedLeafValue)) { - return this.parseArrayLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace); - } else { - return this.parseObjectLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace); - } - } + Object.keys(objectTree).forEach((originalId, index) => { + let newId = index.toString(); + if (originalId === importData.rootId) { + newId = rootIdentifier.key; + } - parseTreeLeaf(leafKey, leafValue, idMap, newRootNamespace, oldRootNamespace) { - if (leafValue === null || leafValue === undefined) { - return leafValue; - } + idMap.set(originalId, newId); + }); - const hasChild = typeof leafValue === 'object'; - if (hasChild) { - return this.parseBranchedLeaf(leafValue, idMap, newRootNamespace, oldRootNamespace); - } + const newTree = this.parseTreeLeaf(null, objectTree, idMap, newRootNamespace, oldRootNamespace); - if (leafKey === 'key') { - let mappedLeafValue; - if (oldRootNamespace) { - mappedLeafValue = idMap.get(objectUtils.makeKeyString({ - namespace: oldRootNamespace, - key: leafValue - })); - } else { - mappedLeafValue = idMap.get(leafValue); - } + return newTree; + } - return mappedLeafValue ?? leafValue; - } else if (leafKey === 'namespace') { - // Only rewrite the namespace if it matches the old root namespace. - // This is to prevent rewriting namespaces of objects that are not - // children of the root object (e.g.: objects from a telemetry dictionary) - return leafValue === oldRootNamespace - ? newRootNamespace - : leafValue; - } else if (leafKey === 'location') { - const mappedLeafValue = idMap.get(leafValue); - if (!mappedLeafValue) { - return null; - } + /** + * Converts all objects in an object make from old format objects to new + * format objects. + */ + convertToNewObjects(oldObjectMap) { + return Object.keys(oldObjectMap).reduce(function (newObjectMap, key) { + newObjectMap[key] = objectUtils.toNewFormat(oldObjectMap[key], key); - const newLocationIdentifier = objectUtils.makeKeyString({ - namespace: newRootNamespace, - key: mappedLeafValue - }); + return newObjectMap; + }, {}); + } - return newLocationIdentifier; - } else { - const mappedLeafValue = idMap.get(leafValue); - if (mappedLeafValue) { - const newIdentifier = objectUtils.makeKeyString({ - namespace: newRootNamespace, - key: mappedLeafValue - }); + /* Set the root location correctly for a top-level object */ + setRootLocation(objectMap, rootIdentifier) { + objectMap[objectUtils.makeKeyString(rootIdentifier)].location = 'ROOT'; - return newIdentifier; - } else { - return leafValue; - } - } - } + return objectMap; + } - rewriteObjectIdentifiers(importData, rootIdentifier) { - const { namespace: oldRootNamespace } = objectUtils.parseKeyString(importData.rootId); - const { namespace: newRootNamespace } = rootIdentifier; - const idMap = new Map(); - const objectTree = importData.openmct; - - Object.keys(objectTree).forEach((originalId, index) => { - let newId = index.toString(); - if (originalId === importData.rootId) { - newId = rootIdentifier.key; - } - - idMap.set(originalId, newId); - }); - - const newTree = this.parseTreeLeaf(null, objectTree, idMap, newRootNamespace, oldRootNamespace); - - return newTree; - } - - /** - * Converts all objects in an object make from old format objects to new - * format objects. - */ - convertToNewObjects(oldObjectMap) { - return Object.keys(oldObjectMap) - .reduce(function (newObjectMap, key) { - newObjectMap[key] = objectUtils.toNewFormat(oldObjectMap[key], key); - - return newObjectMap; - }, {}); - } - - /* Set the root location correctly for a top-level object */ - setRootLocation(objectMap, rootIdentifier) { - objectMap[objectUtils.makeKeyString(rootIdentifier)].location = 'ROOT'; - - return objectMap; - } - - /** - * Takes importData (as provided by the ImportExport plugin) and exposes - * an object provider to fetch those objects. - */ - rewriteModel(importData, rootIdentifier) { - const oldFormatObjectMap = this.rewriteObjectIdentifiers(importData, rootIdentifier); - const newFormatObjectMap = this.convertToNewObjects(oldFormatObjectMap); - this.objectMap = this.setRootLocation(newFormatObjectMap, rootIdentifier); - } + /** + * Takes importData (as provided by the ImportExport plugin) and exposes + * an object provider to fetch those objects. + */ + rewriteModel(importData, rootIdentifier) { + const oldFormatObjectMap = this.rewriteObjectIdentifiers(importData, rootIdentifier); + const newFormatObjectMap = this.convertToNewObjects(oldFormatObjectMap); + this.objectMap = this.setRootLocation(newFormatObjectMap, rootIdentifier); + } } export default StaticModelProvider; diff --git a/src/plugins/staticRootPlugin/StaticModelProviderSpec.js b/src/plugins/staticRootPlugin/StaticModelProviderSpec.js index 347a160dd9..e8a70a4ffe 100644 --- a/src/plugins/staticRootPlugin/StaticModelProviderSpec.js +++ b/src/plugins/staticRootPlugin/StaticModelProviderSpec.js @@ -25,260 +25,255 @@ import testStaticDataFooNamespace from './test-data/static-provider-test-foo-nam import StaticModelProvider from './StaticModelProvider'; describe('StaticModelProvider', function () { - describe('with empty namespace', function () { + describe('with empty namespace', function () { + let staticProvider; - let staticProvider; - - beforeEach(function () { - const staticData = JSON.parse(JSON.stringify(testStaticDataEmptyNamespace)); - staticProvider = new StaticModelProvider(staticData, { - namespace: 'my-import', - key: 'root' - }); - }); - - describe('rootObject', function () { - let rootModel; - - beforeEach(function () { - rootModel = staticProvider.get({ - namespace: 'my-import', - key: 'root' - }); - }); - - it('is located at top level', function () { - expect(rootModel.location).toBe('ROOT'); - }); - - it('has remapped identifier', function () { - expect(rootModel.identifier).toEqual({ - namespace: 'my-import', - key: 'root' - }); - }); - - it('has remapped identifiers in composition', function () { - expect(rootModel.composition).toContain({ - namespace: 'my-import', - key: '1' - }); - expect(rootModel.composition).toContain({ - namespace: 'my-import', - key: '2' - }); - }); - }); - - describe('childObjects', function () { - let swg; - let layout; - let fixed; - - beforeEach(function () { - swg = staticProvider.get({ - namespace: 'my-import', - key: '1' - }); - layout = staticProvider.get({ - namespace: 'my-import', - key: '2' - }); - fixed = staticProvider.get({ - namespace: 'my-import', - key: '3' - }); - }); - - it('match expected ordering', function () { - // this is a sanity check to make sure the identifiers map in - // the correct order. - expect(swg.type).toBe('generator'); - expect(layout.type).toBe('layout'); - expect(fixed.type).toBe('telemetry.fixed'); - }); - - it('have remapped identifiers', function () { - expect(swg.identifier).toEqual({ - namespace: 'my-import', - key: '1' - }); - expect(layout.identifier).toEqual({ - namespace: 'my-import', - key: '2' - }); - expect(fixed.identifier).toEqual({ - namespace: 'my-import', - key: '3' - }); - }); - - it('have remapped composition', function () { - expect(layout.composition).toContain({ - namespace: 'my-import', - key: '1' - }); - expect(layout.composition).toContain({ - namespace: 'my-import', - key: '3' - }); - expect(fixed.composition).toContain({ - namespace: 'my-import', - key: '1' - }); - }); - - it('rewrites locations', function () { - expect(swg.location).toBe('my-import:root'); - expect(layout.location).toBe('my-import:root'); - expect(fixed.location).toBe('my-import:2'); - }); - - it('rewrites matched identifiers in objects', function () { - expect(layout.configuration.layout.panels['my-import:1']) - .toBeDefined(); - expect(layout.configuration.layout.panels['my-import:3']) - .toBeDefined(); - expect(layout.configuration.layout.panels['483c00d4-bb1d-4b42-b29a-c58e06b322a0']) - .not.toBeDefined(); - expect(layout.configuration.layout.panels['20273193-f069-49e9-b4f7-b97a87ed755d']) - .not.toBeDefined(); - expect(fixed.configuration['fixed-display'].elements[0].id) - .toBe('my-import:1'); - }); - - }); + beforeEach(function () { + const staticData = JSON.parse(JSON.stringify(testStaticDataEmptyNamespace)); + staticProvider = new StaticModelProvider(staticData, { + namespace: 'my-import', + key: 'root' + }); }); - describe('with namespace "foo"', function () { - let staticProvider; + describe('rootObject', function () { + let rootModel; - beforeEach(function () { - const staticData = JSON.parse(JSON.stringify(testStaticDataFooNamespace)); - staticProvider = new StaticModelProvider(staticData, { - namespace: 'my-import', - key: 'root' - }); + beforeEach(function () { + rootModel = staticProvider.get({ + namespace: 'my-import', + key: 'root' }); + }); - describe('rootObject', function () { - let rootModel; + it('is located at top level', function () { + expect(rootModel.location).toBe('ROOT'); + }); - beforeEach(function () { - rootModel = staticProvider.get({ - namespace: 'my-import', - key: 'root' - }); - }); - - it('is located at top level', function () { - expect(rootModel.location).toBe('ROOT'); - }); - - it('has remapped identifier', function () { - expect(rootModel.identifier).toEqual({ - namespace: 'my-import', - key: 'root' - }); - }); - - it('has remapped composition', function () { - expect(rootModel.composition).toContain({ - namespace: 'my-import', - key: '1' - }); - expect(rootModel.composition).toContain({ - namespace: 'my-import', - key: '2' - }); - }); + it('has remapped identifier', function () { + expect(rootModel.identifier).toEqual({ + namespace: 'my-import', + key: 'root' }); + }); - describe('childObjects', function () { - let clock; - let layout; - let swg; - let folder; - - beforeEach(function () { - folder = staticProvider.get({ - namespace: 'my-import', - key: 'root' - }); - layout = staticProvider.get({ - namespace: 'my-import', - key: '1' - }); - swg = staticProvider.get({ - namespace: 'my-import', - key: '2' - }); - clock = staticProvider.get({ - namespace: 'my-import', - key: '3' - }); - }); - - it('match expected ordering', function () { - // this is a sanity check to make sure the identifiers map in - // the correct order. - expect(folder.type).toBe('folder'); - expect(swg.type).toBe('generator'); - expect(layout.type).toBe('layout'); - expect(clock.type).toBe('clock'); - }); - - it('have remapped identifiers', function () { - expect(folder.identifier).toEqual({ - namespace: 'my-import', - key: 'root' - }); - expect(layout.identifier).toEqual({ - namespace: 'my-import', - key: '1' - }); - expect(swg.identifier).toEqual({ - namespace: 'my-import', - key: '2' - }); - expect(clock.identifier).toEqual({ - namespace: 'my-import', - key: '3' - }); - }); - - it('have remapped identifiers in composition', function () { - expect(layout.composition).toContain({ - namespace: 'my-import', - key: '2' - }); - expect(layout.composition).toContain({ - namespace: 'my-import', - key: '3' - }); - }); - - it('layout has remapped identifiers in configuration', function () { - const identifiers = layout.configuration.items - .map(item => item.identifier) - .filter(identifier => identifier !== undefined); - expect(identifiers).toContain({ - namespace: 'my-import', - key: '2' - }); - expect(identifiers).toContain({ - namespace: 'my-import', - key: '3' - }); - }); - - it('rewrites locations', function () { - expect(folder.location).toBe('ROOT'); - expect(swg.location).toBe('my-import:root'); - expect(layout.location).toBe('my-import:root'); - expect(clock.location).toBe('my-import:root'); - }); + it('has remapped identifiers in composition', function () { + expect(rootModel.composition).toContain({ + namespace: 'my-import', + key: '1' }); + expect(rootModel.composition).toContain({ + namespace: 'my-import', + key: '2' + }); + }); }); + + describe('childObjects', function () { + let swg; + let layout; + let fixed; + + beforeEach(function () { + swg = staticProvider.get({ + namespace: 'my-import', + key: '1' + }); + layout = staticProvider.get({ + namespace: 'my-import', + key: '2' + }); + fixed = staticProvider.get({ + namespace: 'my-import', + key: '3' + }); + }); + + it('match expected ordering', function () { + // this is a sanity check to make sure the identifiers map in + // the correct order. + expect(swg.type).toBe('generator'); + expect(layout.type).toBe('layout'); + expect(fixed.type).toBe('telemetry.fixed'); + }); + + it('have remapped identifiers', function () { + expect(swg.identifier).toEqual({ + namespace: 'my-import', + key: '1' + }); + expect(layout.identifier).toEqual({ + namespace: 'my-import', + key: '2' + }); + expect(fixed.identifier).toEqual({ + namespace: 'my-import', + key: '3' + }); + }); + + it('have remapped composition', function () { + expect(layout.composition).toContain({ + namespace: 'my-import', + key: '1' + }); + expect(layout.composition).toContain({ + namespace: 'my-import', + key: '3' + }); + expect(fixed.composition).toContain({ + namespace: 'my-import', + key: '1' + }); + }); + + it('rewrites locations', function () { + expect(swg.location).toBe('my-import:root'); + expect(layout.location).toBe('my-import:root'); + expect(fixed.location).toBe('my-import:2'); + }); + + it('rewrites matched identifiers in objects', function () { + expect(layout.configuration.layout.panels['my-import:1']).toBeDefined(); + expect(layout.configuration.layout.panels['my-import:3']).toBeDefined(); + expect( + layout.configuration.layout.panels['483c00d4-bb1d-4b42-b29a-c58e06b322a0'] + ).not.toBeDefined(); + expect( + layout.configuration.layout.panels['20273193-f069-49e9-b4f7-b97a87ed755d'] + ).not.toBeDefined(); + expect(fixed.configuration['fixed-display'].elements[0].id).toBe('my-import:1'); + }); + }); + }); + describe('with namespace "foo"', function () { + let staticProvider; + + beforeEach(function () { + const staticData = JSON.parse(JSON.stringify(testStaticDataFooNamespace)); + staticProvider = new StaticModelProvider(staticData, { + namespace: 'my-import', + key: 'root' + }); + }); + + describe('rootObject', function () { + let rootModel; + + beforeEach(function () { + rootModel = staticProvider.get({ + namespace: 'my-import', + key: 'root' + }); + }); + + it('is located at top level', function () { + expect(rootModel.location).toBe('ROOT'); + }); + + it('has remapped identifier', function () { + expect(rootModel.identifier).toEqual({ + namespace: 'my-import', + key: 'root' + }); + }); + + it('has remapped composition', function () { + expect(rootModel.composition).toContain({ + namespace: 'my-import', + key: '1' + }); + expect(rootModel.composition).toContain({ + namespace: 'my-import', + key: '2' + }); + }); + }); + + describe('childObjects', function () { + let clock; + let layout; + let swg; + let folder; + + beforeEach(function () { + folder = staticProvider.get({ + namespace: 'my-import', + key: 'root' + }); + layout = staticProvider.get({ + namespace: 'my-import', + key: '1' + }); + swg = staticProvider.get({ + namespace: 'my-import', + key: '2' + }); + clock = staticProvider.get({ + namespace: 'my-import', + key: '3' + }); + }); + + it('match expected ordering', function () { + // this is a sanity check to make sure the identifiers map in + // the correct order. + expect(folder.type).toBe('folder'); + expect(swg.type).toBe('generator'); + expect(layout.type).toBe('layout'); + expect(clock.type).toBe('clock'); + }); + + it('have remapped identifiers', function () { + expect(folder.identifier).toEqual({ + namespace: 'my-import', + key: 'root' + }); + expect(layout.identifier).toEqual({ + namespace: 'my-import', + key: '1' + }); + expect(swg.identifier).toEqual({ + namespace: 'my-import', + key: '2' + }); + expect(clock.identifier).toEqual({ + namespace: 'my-import', + key: '3' + }); + }); + + it('have remapped identifiers in composition', function () { + expect(layout.composition).toContain({ + namespace: 'my-import', + key: '2' + }); + expect(layout.composition).toContain({ + namespace: 'my-import', + key: '3' + }); + }); + + it('layout has remapped identifiers in configuration', function () { + const identifiers = layout.configuration.items + .map((item) => item.identifier) + .filter((identifier) => identifier !== undefined); + expect(identifiers).toContain({ + namespace: 'my-import', + key: '2' + }); + expect(identifiers).toContain({ + namespace: 'my-import', + key: '3' + }); + }); + + it('rewrites locations', function () { + expect(folder.location).toBe('ROOT'); + expect(swg.location).toBe('my-import:root'); + expect(layout.location).toBe('my-import:root'); + expect(clock.location).toBe('my-import:root'); + }); + }); + }); }); - diff --git a/src/plugins/staticRootPlugin/plugin.js b/src/plugins/staticRootPlugin/plugin.js index d87251bd0e..fb6822ac8b 100644 --- a/src/plugins/staticRootPlugin/plugin.js +++ b/src/plugins/staticRootPlugin/plugin.js @@ -23,41 +23,41 @@ import StaticModelProvider from './StaticModelProvider'; export default function StaticRootPlugin(options) { - const rootIdentifier = { - namespace: options.namespace, - key: 'root' - }; + const rootIdentifier = { + namespace: options.namespace, + key: 'root' + }; - let cachedProvider; + let cachedProvider; - function loadProvider() { - return fetch(options.exportUrl) - .then(function (response) { - return response.json(); - }) - .then(function (importData) { - cachedProvider = new StaticModelProvider(importData, rootIdentifier); + function loadProvider() { + return fetch(options.exportUrl) + .then(function (response) { + return response.json(); + }) + .then(function (importData) { + cachedProvider = new StaticModelProvider(importData, rootIdentifier); - return cachedProvider; - }); + return cachedProvider; + }); + } + + function getProvider() { + if (!cachedProvider) { + cachedProvider = loadProvider(); } - function getProvider() { - if (!cachedProvider) { - cachedProvider = loadProvider(); - } + return Promise.resolve(cachedProvider); + } - return Promise.resolve(cachedProvider); - } - - return function install(openmct) { - openmct.objects.addRoot(rootIdentifier); - openmct.objects.addProvider(options.namespace, { - get: function (identifier) { - return getProvider().then(function (provider) { - return provider.get(identifier); - }); - } + return function install(openmct) { + openmct.objects.addRoot(rootIdentifier); + openmct.objects.addProvider(options.namespace, { + get: function (identifier) { + return getProvider().then(function (provider) { + return provider.get(identifier); }); - }; + } + }); + }; } diff --git a/src/plugins/staticRootPlugin/test-data/static-provider-test-empty-namespace.json b/src/plugins/staticRootPlugin/test-data/static-provider-test-empty-namespace.json index 8c523de4a7..0011a32ed3 100644 --- a/src/plugins/staticRootPlugin/test-data/static-provider-test-empty-namespace.json +++ b/src/plugins/staticRootPlugin/test-data/static-provider-test-empty-namespace.json @@ -1 +1,104 @@ -{"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 +{ + "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" +} diff --git a/src/plugins/staticRootPlugin/test-data/static-provider-test-foo-namespace.json b/src/plugins/staticRootPlugin/test-data/static-provider-test-foo-namespace.json index 49dd9d5926..6ec6cd52f7 100644 --- a/src/plugins/staticRootPlugin/test-data/static-provider-test-foo-namespace.json +++ b/src/plugins/staticRootPlugin/test-data/static-provider-test-foo-namespace.json @@ -1 +1,120 @@ -{"openmct":{"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1":{"identifier":{"key":"a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","namespace":"foo"},"name":"Folder Foo","type":"folder","composition":[{"key":"95729018-86ed-4484-867d-10c63c41c5a1","namespace":"foo"},{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"}],"modified":1681164966705,"location":"foo:mine","created":1681164829371,"persisted":1681164966706},"foo:95729018-86ed-4484-867d-10c63c41c5a1":{"identifier":{"key":"95729018-86ed-4484-867d-10c63c41c5a1","namespace":"foo"},"name":"Display Layout Bar","type":"layout","composition":[{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"}],"configuration":{"items":[{"fill":"#666666","stroke":"","x":42,"y":42,"width":20,"height":4,"type":"box-view","id":"14505a5d-b846-4504-961f-8c9bcdf19f39"},{"identifier":{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},"x":0,"y":0,"width":40,"height":15,"displayMode":"all","value":"sin","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"05baa95f-2064-4cb0-ad9f-575758491220"},{"width":40,"height":15,"x":0,"y":15,"identifier":{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"70e1b8b7-cd59-4a52-b796-d68fb0c48fc5"}],"layoutGrid":[10,10],"objectStyles":{"05baa95f-2064-4cb0-ad9f-575758491220":{"staticStyle":{"style":{"border":"1px solid #00ff00","backgroundColor":"#0000ff","color":"#ff00ff"}}}}},"modified":1681165037189,"location":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","created":1681164838178,"persisted":1681165037190},"foo:22c438f0-953b-42c5-8fb2-9d5dbeb88a0c":{"identifier":{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},"name":"SWG Baz","type":"generator","telemetry":{"period":"20","amplitude":"2","offset":"5","dataRateInHz":1,"phase":0,"randomness":0,"loadDelay":0,"infinityValues":false,"staleness":false},"modified":1681164910719,"location":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","created":1681164903684,"persisted":1681164910719},"foo:3545554b-53c8-467d-a70d-e90d1a120e4a":{"identifier":{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"},"name":"Clock Qux","type":"clock","configuration":{"baseFormat":"YYYY/MM/DD hh:mm:ss","use24":"clock12","timezone":"UTC"},"modified":1681164989837,"location":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","created":1681164966702,"persisted":1681164989837}},"rootId":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1"} \ No newline at end of file +{ + "openmct": { + "foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1": { + "identifier": { "key": "a6c9e745-89e5-4c3a-8f54-7a34711aaeb1", "namespace": "foo" }, + "name": "Folder Foo", + "type": "folder", + "composition": [ + { "key": "95729018-86ed-4484-867d-10c63c41c5a1", "namespace": "foo" }, + { "key": "22c438f0-953b-42c5-8fb2-9d5dbeb88a0c", "namespace": "foo" }, + { "key": "3545554b-53c8-467d-a70d-e90d1a120e4a", "namespace": "foo" } + ], + "modified": 1681164966705, + "location": "foo:mine", + "created": 1681164829371, + "persisted": 1681164966706 + }, + "foo:95729018-86ed-4484-867d-10c63c41c5a1": { + "identifier": { "key": "95729018-86ed-4484-867d-10c63c41c5a1", "namespace": "foo" }, + "name": "Display Layout Bar", + "type": "layout", + "composition": [ + { "key": "22c438f0-953b-42c5-8fb2-9d5dbeb88a0c", "namespace": "foo" }, + { "key": "3545554b-53c8-467d-a70d-e90d1a120e4a", "namespace": "foo" } + ], + "configuration": { + "items": [ + { + "fill": "#666666", + "stroke": "", + "x": 42, + "y": 42, + "width": 20, + "height": 4, + "type": "box-view", + "id": "14505a5d-b846-4504-961f-8c9bcdf19f39" + }, + { + "identifier": { "key": "22c438f0-953b-42c5-8fb2-9d5dbeb88a0c", "namespace": "foo" }, + "x": 0, + "y": 0, + "width": 40, + "height": 15, + "displayMode": "all", + "value": "sin", + "stroke": "", + "fill": "", + "color": "", + "fontSize": "default", + "font": "default", + "type": "telemetry-view", + "id": "05baa95f-2064-4cb0-ad9f-575758491220" + }, + { + "width": 40, + "height": 15, + "x": 0, + "y": 15, + "identifier": { "key": "3545554b-53c8-467d-a70d-e90d1a120e4a", "namespace": "foo" }, + "hasFrame": true, + "fontSize": "default", + "font": "default", + "type": "subobject-view", + "id": "70e1b8b7-cd59-4a52-b796-d68fb0c48fc5" + } + ], + "layoutGrid": [10, 10], + "objectStyles": { + "05baa95f-2064-4cb0-ad9f-575758491220": { + "staticStyle": { + "style": { + "border": "1px solid #00ff00", + "backgroundColor": "#0000ff", + "color": "#ff00ff" + } + } + } + } + }, + "modified": 1681165037189, + "location": "foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1", + "created": 1681164838178, + "persisted": 1681165037190 + }, + "foo:22c438f0-953b-42c5-8fb2-9d5dbeb88a0c": { + "identifier": { "key": "22c438f0-953b-42c5-8fb2-9d5dbeb88a0c", "namespace": "foo" }, + "name": "SWG Baz", + "type": "generator", + "telemetry": { + "period": "20", + "amplitude": "2", + "offset": "5", + "dataRateInHz": 1, + "phase": 0, + "randomness": 0, + "loadDelay": 0, + "infinityValues": false, + "staleness": false + }, + "modified": 1681164910719, + "location": "foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1", + "created": 1681164903684, + "persisted": 1681164910719 + }, + "foo:3545554b-53c8-467d-a70d-e90d1a120e4a": { + "identifier": { "key": "3545554b-53c8-467d-a70d-e90d1a120e4a", "namespace": "foo" }, + "name": "Clock Qux", + "type": "clock", + "configuration": { + "baseFormat": "YYYY/MM/DD hh:mm:ss", + "use24": "clock12", + "timezone": "UTC" + }, + "modified": 1681164989837, + "location": "foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1", + "created": 1681164966702, + "persisted": 1681164989837 + } + }, + "rootId": "foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1" +} diff --git a/src/plugins/summaryWidget/SummaryWidgetViewPolicy.js b/src/plugins/summaryWidget/SummaryWidgetViewPolicy.js index 0521d0a728..fcca9d21e0 100644 --- a/src/plugins/summaryWidget/SummaryWidgetViewPolicy.js +++ b/src/plugins/summaryWidget/SummaryWidgetViewPolicy.js @@ -20,27 +20,20 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ +define([], function () { + /** + * Policy determining which views can apply to summary widget. Disables + * any view other than normal summary widget view. + */ + function SummaryWidgetViewPolicy() {} -], function ( - -) { - - /** - * Policy determining which views can apply to summary widget. Disables - * any view other than normal summary widget view. - */ - function SummaryWidgetViewPolicy() { + SummaryWidgetViewPolicy.prototype.allow = function (view, domainObject) { + if (domainObject.getModel().type === 'summary-widget') { + return view.key === 'summary-widget-viewer'; } - SummaryWidgetViewPolicy.prototype.allow = function (view, domainObject) { - if (domainObject.getModel().type === 'summary-widget') { - return view.key === 'summary-widget-viewer'; - } + return true; + }; - return true; - - }; - - return SummaryWidgetViewPolicy; + return SummaryWidgetViewPolicy; }); diff --git a/src/plugins/summaryWidget/SummaryWidgetsCompositionPolicy.js b/src/plugins/summaryWidget/SummaryWidgetsCompositionPolicy.js index 9fc9a4528c..30342cb9fc 100644 --- a/src/plugins/summaryWidget/SummaryWidgetsCompositionPolicy.js +++ b/src/plugins/summaryWidget/SummaryWidgetsCompositionPolicy.js @@ -20,24 +20,20 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define( - [], - function () { +define([], function () { + function SummaryWidgetsCompositionPolicy(openmct) { + this.openmct = openmct; + } - function SummaryWidgetsCompositionPolicy(openmct) { - this.openmct = openmct; - } + SummaryWidgetsCompositionPolicy.prototype.allow = function (parent, child) { + const parentType = parent.type; - SummaryWidgetsCompositionPolicy.prototype.allow = function (parent, child) { - const parentType = parent.type; - - if (parentType === 'summary-widget' && !this.openmct.telemetry.isTelemetryObject(child)) { - return false; - } - - return true; - }; - - return SummaryWidgetsCompositionPolicy; + if (parentType === 'summary-widget' && !this.openmct.telemetry.isTelemetryObject(child)) { + return false; } -); + + return true; + }; + + return SummaryWidgetsCompositionPolicy; +}); diff --git a/src/plugins/summaryWidget/plugin.js b/src/plugins/summaryWidget/plugin.js index 3513cbe04f..e14993ad57 100755 --- a/src/plugins/summaryWidget/plugin.js +++ b/src/plugins/summaryWidget/plugin.js @@ -1,96 +1,98 @@ -define([ - './SummaryWidgetsCompositionPolicy', - './src/telemetry/SummaryWidgetMetadataProvider', - './src/telemetry/SummaryWidgetTelemetryProvider', - './src/views/SummaryWidgetViewProvider', - './SummaryWidgetViewPolicy' -], function ( - SummaryWidgetsCompositionPolicy, - SummaryWidgetMetadataProvider, - SummaryWidgetTelemetryProvider, - SummaryWidgetViewProvider, - SummaryWidgetViewPolicy -) { - - function plugin() { - - const widgetType = { - name: 'Summary Widget', - description: 'A compact status update for collections of telemetry-producing items', - cssClass: 'icon-summary-widget', - initialize: function (domainObject) { - domainObject.composition = []; - domainObject.configuration = { - ruleOrder: ['default'], - ruleConfigById: { - default: { - name: 'Default', - label: 'Unnamed Rule', - message: '', - id: 'default', - icon: ' ', - style: { - 'color': '#ffffff', - 'background-color': '#38761d', - 'border-color': 'rgba(0,0,0,0)' - }, - description: 'Default appearance for the widget', - conditions: [{ - object: '', - key: '', - operation: '', - values: [] - }], - jsCondition: '', - trigger: 'any', - expanded: 'true' - } - }, - testDataConfig: [{ - object: '', - key: '', - value: '' - }] - }; - domainObject.openNewTab = 'thisTab'; - domainObject.telemetry = {}; - }, - form: [ - { - "key": "url", - "name": "URL", - "control": "textfield", - "required": false, - "cssClass": "l-input-lg" - }, - { - "key": "openNewTab", - "name": "Tab to Open Hyperlink", - "control": "select", - "options": [ - { - "value": "thisTab", - "name": "Open in this tab" - }, - { - "value": "newTab", - "name": "Open in a new tab" - } - ], - "cssClass": "l-inline" - } - ] - }; - - return function install(openmct) { - openmct.types.addType('summary-widget', widgetType); - let compositionPolicy = new SummaryWidgetsCompositionPolicy(openmct); - openmct.composition.addPolicy(compositionPolicy.allow.bind(compositionPolicy)); - openmct.telemetry.addProvider(new SummaryWidgetMetadataProvider(openmct)); - openmct.telemetry.addProvider(new SummaryWidgetTelemetryProvider(openmct)); - openmct.objectViews.addProvider(new SummaryWidgetViewProvider(openmct)); - }; - } - - return plugin; -}); +define([ + './SummaryWidgetsCompositionPolicy', + './src/telemetry/SummaryWidgetMetadataProvider', + './src/telemetry/SummaryWidgetTelemetryProvider', + './src/views/SummaryWidgetViewProvider', + './SummaryWidgetViewPolicy' +], function ( + SummaryWidgetsCompositionPolicy, + SummaryWidgetMetadataProvider, + SummaryWidgetTelemetryProvider, + SummaryWidgetViewProvider, + SummaryWidgetViewPolicy +) { + function plugin() { + const widgetType = { + name: 'Summary Widget', + description: 'A compact status update for collections of telemetry-producing items', + cssClass: 'icon-summary-widget', + initialize: function (domainObject) { + domainObject.composition = []; + domainObject.configuration = { + ruleOrder: ['default'], + ruleConfigById: { + default: { + name: 'Default', + label: 'Unnamed Rule', + message: '', + id: 'default', + icon: ' ', + style: { + color: '#ffffff', + 'background-color': '#38761d', + 'border-color': 'rgba(0,0,0,0)' + }, + description: 'Default appearance for the widget', + conditions: [ + { + object: '', + key: '', + operation: '', + values: [] + } + ], + jsCondition: '', + trigger: 'any', + expanded: 'true' + } + }, + testDataConfig: [ + { + object: '', + key: '', + value: '' + } + ] + }; + domainObject.openNewTab = 'thisTab'; + domainObject.telemetry = {}; + }, + form: [ + { + key: 'url', + name: 'URL', + control: 'textfield', + required: false, + cssClass: 'l-input-lg' + }, + { + key: 'openNewTab', + name: 'Tab to Open Hyperlink', + control: 'select', + options: [ + { + value: 'thisTab', + name: 'Open in this tab' + }, + { + value: 'newTab', + name: 'Open in a new tab' + } + ], + cssClass: 'l-inline' + } + ] + }; + + return function install(openmct) { + openmct.types.addType('summary-widget', widgetType); + let compositionPolicy = new SummaryWidgetsCompositionPolicy(openmct); + openmct.composition.addPolicy(compositionPolicy.allow.bind(compositionPolicy)); + openmct.telemetry.addProvider(new SummaryWidgetMetadataProvider(openmct)); + openmct.telemetry.addProvider(new SummaryWidgetTelemetryProvider(openmct)); + openmct.objectViews.addProvider(new SummaryWidgetViewProvider(openmct)); + }; + } + + return plugin; +}); diff --git a/src/plugins/summaryWidget/res/conditionTemplate.html b/src/plugins/summaryWidget/res/conditionTemplate.html index ac5af862e6..aa0ec6cf73 100644 --- a/src/plugins/summaryWidget/res/conditionTemplate.html +++ b/src/plugins/summaryWidget/res/conditionTemplate.html @@ -1,11 +1,11 @@
  • - - - - - - - - - + + + + + + + + +
  • diff --git a/src/plugins/summaryWidget/res/input/paletteTemplate.html b/src/plugins/summaryWidget/res/input/paletteTemplate.html index 4a086bf703..5547e3724d 100644 --- a/src/plugins/summaryWidget/res/input/paletteTemplate.html +++ b/src/plugins/summaryWidget/res/input/paletteTemplate.html @@ -9,13 +9,13 @@
    - -
    -
    -
    -
    -
    + +
    +
    +
    +
    +
    diff --git a/src/plugins/summaryWidget/res/input/selectTemplate.html b/src/plugins/summaryWidget/res/input/selectTemplate.html index e49c69830e..830d7f728d 100644 --- a/src/plugins/summaryWidget/res/input/selectTemplate.html +++ b/src/plugins/summaryWidget/res/input/selectTemplate.html @@ -1,4 +1,3 @@ - - + + diff --git a/src/plugins/summaryWidget/res/ruleImageTemplate.html b/src/plugins/summaryWidget/res/ruleImageTemplate.html index fec0f79b00..9c06621476 100644 --- a/src/plugins/summaryWidget/res/ruleImageTemplate.html +++ b/src/plugins/summaryWidget/res/ruleImageTemplate.html @@ -1,3 +1,3 @@
    -
    +
    diff --git a/src/plugins/summaryWidget/res/ruleTemplate.html b/src/plugins/summaryWidget/res/ruleTemplate.html index c05276a63d..8a528dc602 100644 --- a/src/plugins/summaryWidget/res/ruleTemplate.html +++ b/src/plugins/summaryWidget/res/ruleTemplate.html @@ -1,67 +1,72 @@
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    Default Title
    -
    Rule description goes here
    -
    - - -
    -
    -
    -
      -
    • - - - - -
    • -
    • - - - - -
    • -
    • - - - - -
    • -
    • - - -
    • -
    -
      -
    • - - - - -
    • -
    • - - - - -
    • -
    -
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    Default Title
    +
    Rule description goes here
    +
    + + +
    - +
    +
      +
    • + + + + +
    • +
    • + + + + +
    • +
    • + + + + +
    • +
    • + + +
    • +
    +
      +
    • + + + + +
    • +
    • + + + + +
    • +
    +
    +
    +
    diff --git a/src/plugins/summaryWidget/res/testDataItemTemplate.html b/src/plugins/summaryWidget/res/testDataItemTemplate.html index b9b0ef2a87..c5206afc6f 100644 --- a/src/plugins/summaryWidget/res/testDataItemTemplate.html +++ b/src/plugins/summaryWidget/res/testDataItemTemplate.html @@ -1,16 +1,20 @@ -
    -
      -
    • - - - - - - - - - - -
    • -
    +
    +
      +
    • + + + + + + + + + + +
    • +
    diff --git a/src/plugins/summaryWidget/res/testDataTemplate.html b/src/plugins/summaryWidget/res/testDataTemplate.html index 6470589c93..53cc6ec909 100644 --- a/src/plugins/summaryWidget/res/testDataTemplate.html +++ b/src/plugins/summaryWidget/res/testDataTemplate.html @@ -1,17 +1,18 @@
    -
    -
    - -
    -
    -
    - -
    -
    +
    +
    +
    +
    +
    + +
    +
    +
    diff --git a/src/plugins/summaryWidget/res/widgetTemplate.html b/src/plugins/summaryWidget/res/widgetTemplate.html index 3d26dfe586..ad72aad208 100755 --- a/src/plugins/summaryWidget/res/widgetTemplate.html +++ b/src/plugins/summaryWidget/res/widgetTemplate.html @@ -1,30 +1,40 @@ -
    - -
    -
    Default Static Name
    -
    -
    -
    - You must add at least one telemetry object to edit this widget. -
    -
    -
    -
    - - Test Data Values -
    -
    -
    - - Rules -
    -
    -
    -
    - -
    -
    -
    -
    \ No newline at end of file +
    + +
    +
    + Default Static Name +
    +
    +
    +
    + You must add at least one telemetry object to edit this widget. +
    +
    +
    +
    + + Test Data Values +
    +
    +
    + + Rules +
    +
    +
    +
    + +
    +
    +
    +
    diff --git a/src/plugins/summaryWidget/src/Condition.js b/src/plugins/summaryWidget/src/Condition.js index 66f9ecbeb2..972530e4fb 100644 --- a/src/plugins/summaryWidget/src/Condition.js +++ b/src/plugins/summaryWidget/src/Condition.js @@ -1,236 +1,237 @@ define([ - '../res/conditionTemplate.html', - './input/ObjectSelect', - './input/KeySelect', - './input/OperationSelect', - './eventHelpers', - '../../../utils/template/templateHelpers', - 'EventEmitter' + '../res/conditionTemplate.html', + './input/ObjectSelect', + './input/KeySelect', + './input/OperationSelect', + './eventHelpers', + '../../../utils/template/templateHelpers', + 'EventEmitter' ], function ( - conditionTemplate, - ObjectSelect, - KeySelect, - OperationSelect, - eventHelpers, - templateHelpers, - EventEmitter + conditionTemplate, + ObjectSelect, + KeySelect, + OperationSelect, + eventHelpers, + templateHelpers, + EventEmitter ) { + /** + * Represents an individual condition for a summary widget rule. Manages the + * associated inputs and view. + * @param {Object} conditionConfig The configurration for this condition, consisting + * of object, key, operation, and values fields + * @param {number} index the index of this Condition object in it's parent Rule's data model, + * to be injected into callbacks for removes + * @param {ConditionManager} conditionManager A ConditionManager instance for populating + * selects with configuration data + */ + function Condition(conditionConfig, index, conditionManager) { + eventHelpers.extend(this); + this.config = conditionConfig; + this.index = index; + this.conditionManager = conditionManager; + + this.domElement = templateHelpers.convertTemplateToHTML(conditionTemplate)[0]; + + this.eventEmitter = new EventEmitter(); + this.supportedCallbacks = ['remove', 'duplicate', 'change']; + + this.deleteButton = this.domElement.querySelector('.t-delete'); + this.duplicateButton = this.domElement.querySelector('.t-duplicate'); + + this.selects = {}; + this.valueInputs = []; + + this.remove = this.remove.bind(this); + this.duplicate = this.duplicate.bind(this); + + const self = this; + /** - * Represents an individual condition for a summary widget rule. Manages the - * associated inputs and view. - * @param {Object} conditionConfig The configurration for this condition, consisting - * of object, key, operation, and values fields - * @param {number} index the index of this Condition object in it's parent Rule's data model, - * to be injected into callbacks for removes - * @param {ConditionManager} conditionManager A ConditionManager instance for populating - * selects with configuration data + * Event handler for a change in one of this conditions' custom selects + * @param {string} value The new value of this selects + * @param {string} property The property of this condition to modify + * @private */ - function Condition(conditionConfig, index, conditionManager) { - eventHelpers.extend(this); - this.config = conditionConfig; - this.index = index; - this.conditionManager = conditionManager; + function onSelectChange(value, property) { + if (property === 'operation') { + self.generateValueInputs(value); + } - this.domElement = templateHelpers.convertTemplateToHTML(conditionTemplate)[0]; - - this.eventEmitter = new EventEmitter(); - this.supportedCallbacks = ['remove', 'duplicate', 'change']; - - this.deleteButton = this.domElement.querySelector('.t-delete'); - this.duplicateButton = this.domElement.querySelector('.t-duplicate'); - - this.selects = {}; - this.valueInputs = []; - - this.remove = this.remove.bind(this); - this.duplicate = this.duplicate.bind(this); - - const self = this; - - /** - * Event handler for a change in one of this conditions' custom selects - * @param {string} value The new value of this selects - * @param {string} property The property of this condition to modify - * @private - */ - function onSelectChange(value, property) { - if (property === 'operation') { - self.generateValueInputs(value); - } - - self.eventEmitter.emit('change', { - value: value, - property: property, - index: self.index - }); - } - - /** - * Event handler for this conditions value inputs - * @param {Event} event The oninput event that triggered this callback - * @private - */ - function onValueInput(event) { - const elem = event.target; - const value = isNaN(Number(elem.value)) ? elem.value : Number(elem.value); - const inputIndex = self.valueInputs.indexOf(elem); - - self.eventEmitter.emit('change', { - value: value, - property: 'values[' + inputIndex + ']', - index: self.index - }); - } - - this.listenTo(this.deleteButton, 'click', this.remove, this); - this.listenTo(this.duplicateButton, 'click', this.duplicate, this); - - this.selects.object = new ObjectSelect(this.config, this.conditionManager, [ - ['any', 'any telemetry'], - ['all', 'all telemetry'] - ]); - this.selects.key = new KeySelect(this.config, this.selects.object, this.conditionManager); - this.selects.operation = new OperationSelect( - this.config, - this.selects.key, - this.conditionManager, - function (value) { - onSelectChange(value, 'operation'); - }); - - this.selects.object.on('change', function (value) { - onSelectChange(value, 'object'); - }); - this.selects.key.on('change', function (value) { - onSelectChange(value, 'key'); - }); - - Object.values(this.selects).forEach(function (select) { - self.domElement.querySelector('.t-configuration').append(select.getDOM()); - }); - - this.listenTo(this.domElement.querySelector('.t-value-inputs'), 'input', onValueInput); + self.eventEmitter.emit('change', { + value: value, + property: property, + index: self.index + }); } - Condition.prototype.getDOM = function (container) { - return this.domElement; - }; - /** - * Register a callback with this condition: supported callbacks are remove, change, - * duplicate - * @param {string} event The key for the event to listen to - * @param {function} callback The function that this rule will envoke on this event - * @param {Object} context A reference to a scope to use as the context for - * context for the callback function + * Event handler for this conditions value inputs + * @param {Event} event The oninput event that triggered this callback + * @private */ - Condition.prototype.on = function (event, callback, context) { - if (this.supportedCallbacks.includes(event)) { - this.eventEmitter.on(event, callback, context || this); + function onValueInput(event) { + const elem = event.target; + const value = isNaN(Number(elem.value)) ? elem.value : Number(elem.value); + const inputIndex = self.valueInputs.indexOf(elem); + + self.eventEmitter.emit('change', { + value: value, + property: 'values[' + inputIndex + ']', + index: self.index + }); + } + + this.listenTo(this.deleteButton, 'click', this.remove, this); + this.listenTo(this.duplicateButton, 'click', this.duplicate, this); + + this.selects.object = new ObjectSelect(this.config, this.conditionManager, [ + ['any', 'any telemetry'], + ['all', 'all telemetry'] + ]); + this.selects.key = new KeySelect(this.config, this.selects.object, this.conditionManager); + this.selects.operation = new OperationSelect( + this.config, + this.selects.key, + this.conditionManager, + function (value) { + onSelectChange(value, 'operation'); + } + ); + + this.selects.object.on('change', function (value) { + onSelectChange(value, 'object'); + }); + this.selects.key.on('change', function (value) { + onSelectChange(value, 'key'); + }); + + Object.values(this.selects).forEach(function (select) { + self.domElement.querySelector('.t-configuration').append(select.getDOM()); + }); + + this.listenTo(this.domElement.querySelector('.t-value-inputs'), 'input', onValueInput); + } + + Condition.prototype.getDOM = function (container) { + return this.domElement; + }; + + /** + * Register a callback with this condition: supported callbacks are remove, change, + * duplicate + * @param {string} event The key for the event to listen to + * @param {function} callback The function that this rule will envoke on this event + * @param {Object} context A reference to a scope to use as the context for + * context for the callback function + */ + Condition.prototype.on = function (event, callback, context) { + if (this.supportedCallbacks.includes(event)) { + this.eventEmitter.on(event, callback, context || this); + } + }; + + /** + * Hide the appropriate inputs when this is the only condition + */ + Condition.prototype.hideButtons = function () { + this.deleteButton.style.display = 'none'; + }; + + /** + * Remove this condition from the configuration. Invokes any registered + * remove callbacks + */ + Condition.prototype.remove = function () { + this.eventEmitter.emit('remove', this.index); + this.destroy(); + }; + + Condition.prototype.destroy = function () { + this.stopListening(); + Object.values(this.selects).forEach(function (select) { + select.destroy(); + }); + }; + + /** + * Make a deep clone of this condition's configuration and invoke any duplicate + * callbacks with the cloned configuration and this rule's index + */ + Condition.prototype.duplicate = function () { + const sourceCondition = JSON.parse(JSON.stringify(this.config)); + this.eventEmitter.emit('duplicate', { + sourceCondition: sourceCondition, + index: this.index + }); + }; + + /** + * When an operation is selected, create the appropriate value inputs + * and add them to the view. If an operation is of type enum, create + * a drop-down menu instead. + * + * @param {string} operation The key of currently selected operation + */ + Condition.prototype.generateValueInputs = function (operation) { + const evaluator = this.conditionManager.getEvaluator(); + const inputArea = this.domElement.querySelector('.t-value-inputs'); + let inputCount; + let inputType; + let newInput; + let index = 0; + let emitChange = false; + + inputArea.innerHTML = ''; + this.valueInputs = []; + this.config.values = this.config.values || []; + + if (evaluator.getInputCount(operation)) { + inputCount = evaluator.getInputCount(operation); + inputType = evaluator.getInputType(operation); + + while (index < inputCount) { + if (inputType === '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 = document.createElement('input'); + newInput.type = `${inputType}`; + newInput.value = `${value}`; } - }; - /** - * Hide the appropriate inputs when this is the only condition - */ - Condition.prototype.hideButtons = function () { - this.deleteButton.style.display = 'none'; - }; + this.valueInputs.push(newInput); + inputArea.appendChild(newInput); + index += 1; + } - /** - * Remove this condition from the configuration. Invokes any registered - * remove callbacks - */ - Condition.prototype.remove = function () { - this.eventEmitter.emit('remove', this.index); - this.destroy(); - }; - - Condition.prototype.destroy = function () { - this.stopListening(); - Object.values(this.selects).forEach(function (select) { - select.destroy(); + if (emitChange) { + this.eventEmitter.emit('change', { + value: Number(newInput[0].options[0].value), + property: 'values[0]', + index: this.index }); - }; + } + } + }; - /** - * Make a deep clone of this condition's configuration and invoke any duplicate - * callbacks with the cloned configuration and this rule's index - */ - Condition.prototype.duplicate = function () { - const sourceCondition = JSON.parse(JSON.stringify(this.config)); - this.eventEmitter.emit('duplicate', { - sourceCondition: sourceCondition, - index: this.index - }); - }; + Condition.prototype.generateSelectOptions = function () { + let telemetryMetadata = this.conditionManager.getTelemetryMetadata(this.config.object); + let options = ''; + telemetryMetadata[this.config.key].enumerations.forEach((enumeration) => { + options += ''; + }); - /** - * When an operation is selected, create the appropriate value inputs - * and add them to the view. If an operation is of type enum, create - * a drop-down menu instead. - * - * @param {string} operation The key of currently selected operation - */ - Condition.prototype.generateValueInputs = function (operation) { - const evaluator = this.conditionManager.getEvaluator(); - const inputArea = this.domElement.querySelector('.t-value-inputs'); - let inputCount; - let inputType; - let newInput; - let index = 0; - let emitChange = false; + return options; + }; - inputArea.innerHTML = ''; - this.valueInputs = []; - this.config.values = this.config.values || []; - - if (evaluator.getInputCount(operation)) { - inputCount = evaluator.getInputCount(operation); - inputType = evaluator.getInputType(operation); - - while (index < inputCount) { - if (inputType === '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 = document.createElement("input"); - newInput.type = `${inputType}`; - newInput.value = `${value}`; - } - - this.valueInputs.push(newInput); - inputArea.appendChild(newInput); - index += 1; - } - - if (emitChange) { - this.eventEmitter.emit('change', { - value: Number(newInput[0].options[0].value), - property: 'values[0]', - index: this.index - }); - } - } - }; - - Condition.prototype.generateSelectOptions = function () { - let telemetryMetadata = this.conditionManager.getTelemetryMetadata(this.config.object); - let options = ''; - telemetryMetadata[this.config.key].enumerations.forEach(enumeration => { - options += ''; - }); - - return options; - }; - - return Condition; + return Condition; }); diff --git a/src/plugins/summaryWidget/src/ConditionEvaluator.js b/src/plugins/summaryWidget/src/ConditionEvaluator.js index 6431690a93..1bef29b5c0 100644 --- a/src/plugins/summaryWidget/src/ConditionEvaluator.js +++ b/src/plugins/summaryWidget/src/ConditionEvaluator.js @@ -1,483 +1,486 @@ define([], function () { + /** + * Responsible for maintaining the possible operations for conditions + * in this widget, and evaluating the boolean value of conditions passed as + * input. + * @constructor + * @param {Object} subscriptionCache A cache consisting of the latest available + * data for any telemetry sources in the widget's + * composition. + * @param {Object} compositionObjs The current set of composition objects to + * evaluate for 'any' and 'all' conditions + */ + function ConditionEvaluator(subscriptionCache, compositionObjs) { + this.subscriptionCache = subscriptionCache; + this.compositionObjs = compositionObjs; + + this.testCache = {}; + this.useTestCache = false; /** - * Responsible for maintaining the possible operations for conditions - * in this widget, and evaluating the boolean value of conditions passed as - * input. - * @constructor - * @param {Object} subscriptionCache A cache consisting of the latest available - * data for any telemetry sources in the widget's - * composition. - * @param {Object} compositionObjs The current set of composition objects to - * evaluate for 'any' and 'all' conditions + * Maps value types to HTML input field types. These + * type of inputs will be generated by conditions expecting this data type */ - function ConditionEvaluator(subscriptionCache, compositionObjs) { - this.subscriptionCache = subscriptionCache; - this.compositionObjs = compositionObjs; + this.inputTypes = { + number: 'number', + string: 'text', + enum: 'select' + }; - this.testCache = {}; - this.useTestCache = false; + /** + * Functions to validate that the input to an operation is of the type + * that it expects, in order to prevent unexpected behavior. Will be + * invoked before the corresponding operation is executed + */ + this.inputValidators = { + number: this.validateNumberInput, + string: this.validateStringInput, + enum: this.validateNumberInput + }; - /** - * Maps value types to HTML input field types. These - * type of inputs will be generated by conditions expecting this data type - */ - this.inputTypes = { - number: 'number', - string: 'text', - enum: 'select' - }; + /** + * A library of operations supported by this rule evaluator. Each operation + * consists of the following fields: + * operation: a function with boolean return type to be invoked when this + * operation is used. Will be called with an array of inputs + * where input [0] is the telemetry value and input [1..n] are + * any comparison values + * text: a human-readable description of this operation to populate selects + * appliesTo: an array of identifiers for types that operation may be used on + * inputCount: the number of inputs required to get any necessary comparison + * values for the operation + * getDescription: A function returning a human-readable shorthand description of + * this operation to populate the 'description' field in the rule header. + * Will be invoked with an array of a condition's comparison values. + */ + this.operations = { + equalTo: { + operation: function (input) { + return input[0] === input[1]; + }, + text: 'is equal to', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' == ' + values[0]; + } + }, + notEqualTo: { + operation: function (input) { + return input[0] !== input[1]; + }, + text: 'is not equal to', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' != ' + values[0]; + } + }, + greaterThan: { + operation: function (input) { + return input[0] > input[1]; + }, + text: 'is greater than', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' > ' + values[0]; + } + }, + lessThan: { + operation: function (input) { + return input[0] < input[1]; + }, + text: 'is less than', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' < ' + values[0]; + } + }, + greaterThanOrEq: { + operation: function (input) { + return input[0] >= input[1]; + }, + text: 'is greater than or equal to', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' >= ' + values[0]; + } + }, + lessThanOrEq: { + operation: function (input) { + return input[0] <= input[1]; + }, + text: 'is less than or equal to', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' <= ' + values[0]; + } + }, + between: { + operation: function (input) { + return input[0] > input[1] && input[0] < input[2]; + }, + text: 'is between', + appliesTo: ['number'], + inputCount: 2, + getDescription: function (values) { + return ' between ' + values[0] + ' and ' + values[1]; + } + }, + notBetween: { + operation: function (input) { + return input[0] < input[1] || input[0] > input[2]; + }, + text: 'is not between', + appliesTo: ['number'], + inputCount: 2, + getDescription: function (values) { + return ' not between ' + values[0] + ' and ' + values[1]; + } + }, + textContains: { + operation: function (input) { + return input[0] && input[1] && input[0].includes(input[1]); + }, + text: 'text contains', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' contains ' + values[0]; + } + }, + textDoesNotContain: { + operation: function (input) { + return input[0] && input[1] && !input[0].includes(input[1]); + }, + text: 'text does not contain', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' does not contain ' + values[0]; + } + }, + textStartsWith: { + operation: function (input) { + return input[0].startsWith(input[1]); + }, + text: 'text starts with', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' starts with ' + values[0]; + } + }, + textEndsWith: { + operation: function (input) { + return input[0].endsWith(input[1]); + }, + text: 'text ends with', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' ends with ' + values[0]; + } + }, + textIsExactly: { + operation: function (input) { + return input[0] === input[1]; + }, + text: 'text is exactly', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' is exactly ' + values[0]; + } + }, + isUndefined: { + operation: function (input) { + return typeof input[0] === 'undefined'; + }, + text: 'is undefined', + appliesTo: ['string', 'number', 'enum'], + inputCount: 0, + getDescription: function () { + return ' is undefined'; + } + }, + isDefined: { + operation: function (input) { + return typeof input[0] !== 'undefined'; + }, + text: 'is defined', + appliesTo: ['string', 'number', 'enum'], + inputCount: 0, + getDescription: function () { + return ' is defined'; + } + }, + enumValueIs: { + operation: function (input) { + return input[0] === input[1]; + }, + text: 'is', + appliesTo: ['enum'], + inputCount: 1, + getDescription: function (values) { + return ' == ' + values[0]; + } + }, + enumValueIsNot: { + operation: function (input) { + return input[0] !== input[1]; + }, + text: 'is not', + appliesTo: ['enum'], + inputCount: 1, + getDescription: function (values) { + return ' != ' + values[0]; + } + } + }; + } - /** - * Functions to validate that the input to an operation is of the type - * that it expects, in order to prevent unexpected behavior. Will be - * invoked before the corresponding operation is executed - */ - this.inputValidators = { - number: this.validateNumberInput, - string: this.validateStringInput, - enum: this.validateNumberInput - }; + /** + * Evaluate the conditions passed in as an argument, and return the boolean + * value of these conditions. Available evaluation modes are 'any', which will + * return true if any of the conditions evaluates to true (i.e. logical OR); 'all', + * which returns true only if all conditions evalute to true (i.e. logical AND); + * or 'js', which returns the boolean value of a custom JavaScript conditional. + * @param {} conditions Either an array of objects with object, key, operation, + * and value fields, or a string representing a JavaScript + * condition. + * @param {string} mode The key of the mode to use when evaluating the conditions. + * @return {boolean} The boolean value of the conditions + */ + ConditionEvaluator.prototype.execute = function (conditions, mode) { + let active = false; + let conditionValue; + let conditionDefined = false; + const self = this; + let firstRuleEvaluated = false; + const compositionObjs = this.compositionObjs; - /** - * A library of operations supported by this rule evaluator. Each operation - * consists of the following fields: - * operation: a function with boolean return type to be invoked when this - * operation is used. Will be called with an array of inputs - * where input [0] is the telemetry value and input [1..n] are - * any comparison values - * text: a human-readable description of this operation to populate selects - * appliesTo: an array of identifiers for types that operation may be used on - * inputCount: the number of inputs required to get any necessary comparison - * values for the operation - * getDescription: A function returning a human-readable shorthand description of - * this operation to populate the 'description' field in the rule header. - * Will be invoked with an array of a condition's comparison values. - */ - this.operations = { - equalTo: { - operation: function (input) { - return input[0] === input[1]; - }, - text: 'is equal to', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' == ' + values[0]; - } - }, - notEqualTo: { - operation: function (input) { - return input[0] !== input[1]; - }, - text: 'is not equal to', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' != ' + values[0]; - } - }, - greaterThan: { - operation: function (input) { - return input[0] > input[1]; - }, - text: 'is greater than', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' > ' + values[0]; - } - }, - lessThan: { - operation: function (input) { - return input[0] < input[1]; - }, - text: 'is less than', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' < ' + values[0]; - } - }, - greaterThanOrEq: { - operation: function (input) { - return input[0] >= input[1]; - }, - text: 'is greater than or equal to', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' >= ' + values[0]; - } - }, - lessThanOrEq: { - operation: function (input) { - return input[0] <= input[1]; - }, - text: 'is less than or equal to', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' <= ' + values[0]; - } - }, - between: { - operation: function (input) { - return input[0] > input[1] && input[0] < input[2]; - }, - text: 'is between', - appliesTo: ['number'], - inputCount: 2, - getDescription: function (values) { - return ' between ' + values[0] + ' and ' + values[1]; - } - }, - notBetween: { - operation: function (input) { - return input[0] < input[1] || input[0] > input[2]; - }, - text: 'is not between', - appliesTo: ['number'], - inputCount: 2, - getDescription: function (values) { - return ' not between ' + values[0] + ' and ' + values[1]; - } - }, - textContains: { - operation: function (input) { - return input[0] && input[1] && input[0].includes(input[1]); - }, - text: 'text contains', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' contains ' + values[0]; - } - }, - textDoesNotContain: { - operation: function (input) { - return input[0] && input[1] && !input[0].includes(input[1]); - }, - text: 'text does not contain', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' does not contain ' + values[0]; - } - }, - textStartsWith: { - operation: function (input) { - return input[0].startsWith(input[1]); - }, - text: 'text starts with', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' starts with ' + values[0]; - } - }, - textEndsWith: { - operation: function (input) { - return input[0].endsWith(input[1]); - }, - text: 'text ends with', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' ends with ' + values[0]; - } - }, - textIsExactly: { - operation: function (input) { - return input[0] === input[1]; - }, - text: 'text is exactly', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' is exactly ' + values[0]; - } - }, - isUndefined: { - operation: function (input) { - return typeof input[0] === 'undefined'; - }, - text: 'is undefined', - appliesTo: ['string', 'number', 'enum'], - inputCount: 0, - getDescription: function () { - return ' is undefined'; - } - }, - isDefined: { - operation: function (input) { - return typeof input[0] !== 'undefined'; - }, - text: 'is defined', - appliesTo: ['string', 'number', 'enum'], - inputCount: 0, - getDescription: function () { - return ' is defined'; - } - }, - enumValueIs: { - operation: function (input) { - return input[0] === input[1]; - }, - text: 'is', - appliesTo: ['enum'], - inputCount: 1, - getDescription: function (values) { - return ' == ' + values[0]; - } - }, - enumValueIsNot: { - operation: function (input) { - return input[0] !== input[1]; - }, - text: 'is not', - appliesTo: ['enum'], - inputCount: 1, - getDescription: function (values) { - return ' != ' + values[0]; - } + if (mode === 'js') { + active = this.executeJavaScriptCondition(conditions); + } else { + (conditions || []).forEach(function (condition) { + conditionDefined = false; + if (condition.object === 'any') { + conditionValue = false; + Object.keys(compositionObjs).forEach(function (objId) { + try { + conditionValue = + conditionValue || + self.executeCondition(objId, condition.key, condition.operation, condition.values); + conditionDefined = true; + } catch (e) { + //ignore a malformed condition } - }; + }); + } else if (condition.object === 'all') { + conditionValue = true; + Object.keys(compositionObjs).forEach(function (objId) { + try { + conditionValue = + conditionValue && + self.executeCondition(objId, condition.key, condition.operation, condition.values); + conditionDefined = true; + } catch (e) { + //ignore a malformed condition + } + }); + } else { + try { + conditionValue = self.executeCondition( + condition.object, + condition.key, + condition.operation, + condition.values + ); + conditionDefined = true; + } catch (e) { + //ignore malformed condition + } + } + + if (conditionDefined) { + active = mode === 'all' && !firstRuleEvaluated ? true : active; + firstRuleEvaluated = true; + if (mode === 'any') { + active = active || conditionValue; + } else if (mode === 'all') { + active = active && conditionValue; + } + } + }); } - /** - * Evaluate the conditions passed in as an argument, and return the boolean - * value of these conditions. Available evaluation modes are 'any', which will - * return true if any of the conditions evaluates to true (i.e. logical OR); 'all', - * which returns true only if all conditions evalute to true (i.e. logical AND); - * or 'js', which returns the boolean value of a custom JavaScript conditional. - * @param {} conditions Either an array of objects with object, key, operation, - * and value fields, or a string representing a JavaScript - * condition. - * @param {string} mode The key of the mode to use when evaluating the conditions. - * @return {boolean} The boolean value of the conditions - */ - ConditionEvaluator.prototype.execute = function (conditions, mode) { - let active = false; - let conditionValue; - let conditionDefined = false; - const self = this; - let firstRuleEvaluated = false; - const compositionObjs = this.compositionObjs; + return active; + }; - if (mode === 'js') { - active = this.executeJavaScriptCondition(conditions); - } else { - (conditions || []).forEach(function (condition) { - conditionDefined = false; - if (condition.object === 'any') { - conditionValue = false; - Object.keys(compositionObjs).forEach(function (objId) { - try { - conditionValue = conditionValue - || self.executeCondition(objId, condition.key, - condition.operation, condition.values); - conditionDefined = true; - } catch (e) { - //ignore a malformed condition - } - }); - } else if (condition.object === 'all') { - conditionValue = true; - Object.keys(compositionObjs).forEach(function (objId) { - try { - conditionValue = conditionValue - && self.executeCondition(objId, condition.key, - condition.operation, condition.values); - conditionDefined = true; - } catch (e) { - //ignore a malformed condition - } - }); - } else { - try { - conditionValue = self.executeCondition(condition.object, condition.key, - condition.operation, condition.values); - conditionDefined = true; - } catch (e) { - //ignore malformed condition - } - } + /** + * Execute a condition defined as an object. + * @param {string} object The identifier of the telemetry object to retrieve data from + * @param {string} key The property of the telemetry object + * @param {string} operation The key of the operation in this ConditionEvaluator to executeCondition + * @param {string} values An array of comparison values to invoke the operation with + * @return {boolean} The value of this condition + */ + ConditionEvaluator.prototype.executeCondition = function (object, key, operation, values) { + const cache = this.useTestCache ? this.testCache : this.subscriptionCache; + let telemetryValue; + let op; + let input; + let validator; - if (conditionDefined) { - active = (mode === 'all' && !firstRuleEvaluated ? true : active); - firstRuleEvaluated = true; - if (mode === 'any') { - active = active || conditionValue; - } else if (mode === 'all') { - active = active && conditionValue; - } - } - }); - } + if (cache[object] && typeof cache[object][key] !== 'undefined') { + let value = cache[object][key]; + telemetryValue = [isNaN(Number(value)) ? value : Number(value)]; + } - return active; - }; + op = this.operations[operation] && this.operations[operation].operation; + input = telemetryValue && telemetryValue.concat(values); + validator = op && this.inputValidators[this.operations[operation].appliesTo[0]]; - /** - * Execute a condition defined as an object. - * @param {string} object The identifier of the telemetry object to retrieve data from - * @param {string} key The property of the telemetry object - * @param {string} operation The key of the operation in this ConditionEvaluator to executeCondition - * @param {string} values An array of comparison values to invoke the operation with - * @return {boolean} The value of this condition - */ - ConditionEvaluator.prototype.executeCondition = function (object, key, operation, values) { - const cache = (this.useTestCache ? this.testCache : this.subscriptionCache); - let telemetryValue; - let op; - let input; - let validator; + if (op && input && validator) { + if (this.operations[operation].appliesTo.length > 1) { + return (this.validateNumberInput(input) || this.validateStringInput(input)) && op(input); + } else { + return validator(input) && op(input); + } + } else { + throw new Error('Malformed condition'); + } + }; - if (cache[object] && typeof cache[object][key] !== 'undefined') { - let value = cache[object][key]; - telemetryValue = [isNaN(Number(value)) ? value : Number(value)]; - } + /** + * A function that returns true only if each value in its input argument is + * of a numerical type + * @param {[]} input An array of values + * @returns {boolean} + */ + ConditionEvaluator.prototype.validateNumberInput = function (input) { + let valid = true; + input.forEach(function (value) { + valid = valid && typeof value === 'number'; + }); - op = this.operations[operation] && this.operations[operation].operation; - input = telemetryValue && telemetryValue.concat(values); - validator = op && this.inputValidators[this.operations[operation].appliesTo[0]]; + return valid; + }; - if (op && input && validator) { - if (this.operations[operation].appliesTo.length > 1) { - return (this.validateNumberInput(input) || this.validateStringInput(input)) && op(input); - } else { - return validator(input) && op(input); - } - } else { - throw new Error('Malformed condition'); - } - }; + /** + * A function that returns true only if each value in its input argument is + * a string + * @param {[]} input An array of values + * @returns {boolean} + */ + ConditionEvaluator.prototype.validateStringInput = function (input) { + let valid = true; + input.forEach(function (value) { + valid = valid && typeof value === 'string'; + }); - /** - * A function that returns true only if each value in its input argument is - * of a numerical type - * @param {[]} input An array of values - * @returns {boolean} - */ - ConditionEvaluator.prototype.validateNumberInput = function (input) { - let valid = true; - input.forEach(function (value) { - valid = valid && (typeof value === 'number'); - }); + return valid; + }; - return valid; - }; + /** + * Get the keys of operations supported by this evaluator + * @return {string[]} An array of the keys of supported operations + */ + ConditionEvaluator.prototype.getOperationKeys = function () { + return Object.keys(this.operations); + }; - /** - * A function that returns true only if each value in its input argument is - * a string - * @param {[]} input An array of values - * @returns {boolean} - */ - ConditionEvaluator.prototype.validateStringInput = function (input) { - let valid = true; - input.forEach(function (value) { - valid = valid && (typeof value === 'string'); - }); + /** + * Get the human-readable text corresponding to a given operation + * @param {string} key The key of the operation + * @return {string} The text description of the operation + */ + ConditionEvaluator.prototype.getOperationText = function (key) { + return this.operations[key].text; + }; - return valid; - }; + /** + * Returns true only if the given operation applies to a given type + * @param {string} key The key of the operation + * @param {string} type The value type to query + * @returns {boolean} True if the condition applies, false otherwise + */ + ConditionEvaluator.prototype.operationAppliesTo = function (key, type) { + return this.operations[key].appliesTo.includes(type); + }; - /** - * Get the keys of operations supported by this evaluator - * @return {string[]} An array of the keys of supported operations - */ - ConditionEvaluator.prototype.getOperationKeys = function () { - return Object.keys(this.operations); - }; + /** + * Return the number of value inputs required by an operation + * @param {string} key The key of the operation to query + * @return {number} + */ + ConditionEvaluator.prototype.getInputCount = function (key) { + if (this.operations[key]) { + return this.operations[key].inputCount; + } + }; - /** - * Get the human-readable text corresponding to a given operation - * @param {string} key The key of the operation - * @return {string} The text description of the operation - */ - ConditionEvaluator.prototype.getOperationText = function (key) { - return this.operations[key].text; - }; + /** + * Return the human-readable shorthand description of the operation for a rule header + * @param {string} key The key of the operation to query + * @param {} values An array of values with which to invoke the getDescription function + * of the operation + * @return {string} A text description of this operation + */ + ConditionEvaluator.prototype.getOperationDescription = function (key, values) { + if (this.operations[key]) { + return this.operations[key].getDescription(values); + } + }; - /** - * Returns true only if the given operation applies to a given type - * @param {string} key The key of the operation - * @param {string} type The value type to query - * @returns {boolean} True if the condition applies, false otherwise - */ - ConditionEvaluator.prototype.operationAppliesTo = function (key, type) { - return (this.operations[key].appliesTo.includes(type)); - }; + /** + * Return the HTML input type associated with a given operation + * @param {string} key The key of the operation to query + * @return {string} The key for an HTML5 input type + */ + ConditionEvaluator.prototype.getInputType = function (key) { + let type; + if (this.operations[key]) { + type = this.operations[key].appliesTo[0]; + } - /** - * Return the number of value inputs required by an operation - * @param {string} key The key of the operation to query - * @return {number} - */ - ConditionEvaluator.prototype.getInputCount = function (key) { - if (this.operations[key]) { - return this.operations[key].inputCount; - } - }; + if (this.inputTypes[type]) { + return this.inputTypes[type]; + } + }; - /** - * Return the human-readable shorthand description of the operation for a rule header - * @param {string} key The key of the operation to query - * @param {} values An array of values with which to invoke the getDescription function - * of the operation - * @return {string} A text description of this operation - */ - ConditionEvaluator.prototype.getOperationDescription = function (key, values) { - if (this.operations[key]) { - return this.operations[key].getDescription(values); - } - }; + /** + * Returns the HTML input type associated with a value type + * @param {string} dataType The JavaScript value type + * @return {string} The key for an HTML5 input type + */ + ConditionEvaluator.prototype.getInputTypeById = function (dataType) { + return this.inputTypes[dataType]; + }; - /** - * Return the HTML input type associated with a given operation - * @param {string} key The key of the operation to query - * @return {string} The key for an HTML5 input type - */ - ConditionEvaluator.prototype.getInputType = function (key) { - let type; - if (this.operations[key]) { - type = this.operations[key].appliesTo[0]; - } + /** + * Set the test data cache used by this rule evaluator + * @param {object} testCache A mock cache following the format of the real + * subscription cache + */ + ConditionEvaluator.prototype.setTestDataCache = function (testCache) { + this.testCache = testCache; + }; - if (this.inputTypes[type]) { - return this.inputTypes[type]; - } - }; + /** + * Have this RuleEvaluator pull data values from the provided test cache + * instead of its actual subscription cache when evaluating. If invoked with true, + * will use the test cache; otherwise, will use the subscription cache + * @param {boolean} useTestData Boolean flag + */ + ConditionEvaluator.prototype.useTestData = function (useTestCache) { + this.useTestCache = useTestCache; + }; - /** - * Returns the HTML input type associated with a value type - * @param {string} dataType The JavaScript value type - * @return {string} The key for an HTML5 input type - */ - ConditionEvaluator.prototype.getInputTypeById = function (dataType) { - return this.inputTypes[dataType]; - }; - - /** - * Set the test data cache used by this rule evaluator - * @param {object} testCache A mock cache following the format of the real - * subscription cache - */ - ConditionEvaluator.prototype.setTestDataCache = function (testCache) { - this.testCache = testCache; - }; - - /** - * Have this RuleEvaluator pull data values from the provided test cache - * instead of its actual subscription cache when evaluating. If invoked with true, - * will use the test cache; otherwise, will use the subscription cache - * @param {boolean} useTestData Boolean flag - */ - ConditionEvaluator.prototype.useTestData = function (useTestCache) { - this.useTestCache = useTestCache; - }; - - return ConditionEvaluator; + return ConditionEvaluator; }); diff --git a/src/plugins/summaryWidget/src/ConditionManager.js b/src/plugins/summaryWidget/src/ConditionManager.js index e502649030..825d65b69b 100644 --- a/src/plugins/summaryWidget/src/ConditionManager.js +++ b/src/plugins/summaryWidget/src/ConditionManager.js @@ -1,386 +1,386 @@ -define ([ - './ConditionEvaluator', - 'objectUtils', - 'EventEmitter', - 'lodash' -], function ( - ConditionEvaluator, - objectUtils, - EventEmitter, - _ +define(['./ConditionEvaluator', 'objectUtils', 'EventEmitter', 'lodash'], function ( + ConditionEvaluator, + objectUtils, + EventEmitter, + _ ) { + /** + * Provides a centralized content manager for conditions in the summary widget. + * Loads and caches composition and telemetry subscriptions, and maintains a + * {ConditionEvaluator} instance to handle evaluation + * @constructor + * @param {Object} domainObject the Summary Widget domain object + * @param {MCT} openmct an MCT instance + */ + function ConditionManager(domainObject, openmct) { + this.domainObject = domainObject; + this.openmct = openmct; - /** - * Provides a centralized content manager for conditions in the summary widget. - * Loads and caches composition and telemetry subscriptions, and maintains a - * {ConditionEvaluator} instance to handle evaluation - * @constructor - * @param {Object} domainObject the Summary Widget domain object - * @param {MCT} openmct an MCT instance - */ - function ConditionManager(domainObject, openmct) { - this.domainObject = domainObject; - this.openmct = openmct; + this.composition = this.openmct.composition.get(this.domainObject); + this.compositionObjs = {}; + this.eventEmitter = new EventEmitter(); + this.supportedCallbacks = ['add', 'remove', 'load', 'metadata', 'receiveTelemetry']; - this.composition = this.openmct.composition.get(this.domainObject); - this.compositionObjs = {}; - this.eventEmitter = new EventEmitter(); - this.supportedCallbacks = ['add', 'remove', 'load', 'metadata', 'receiveTelemetry']; + this.keywordLabels = { + any: 'any Telemetry', + all: 'all Telemetry' + }; - this.keywordLabels = { - any: 'any Telemetry', - all: 'all Telemetry' - }; + this.telemetryMetadataById = { + any: {}, + all: {} + }; - this.telemetryMetadataById = { - any: {}, - all: {} - }; + this.telemetryTypesById = { + any: {}, + all: {} + }; - this.telemetryTypesById = { - any: {}, - all: {} - }; + this.subscriptions = {}; + this.subscriptionCache = {}; + this.loadComplete = false; + this.metadataLoadComplete = false; + this.evaluator = new ConditionEvaluator(this.subscriptionCache, this.compositionObjs); - this.subscriptions = {}; - this.subscriptionCache = {}; - this.loadComplete = false; - this.metadataLoadComplete = false; - this.evaluator = new ConditionEvaluator(this.subscriptionCache, this.compositionObjs); + this.composition.on('add', this.onCompositionAdd, this); + this.composition.on('remove', this.onCompositionRemove, this); + this.composition.on('load', this.onCompositionLoad, this); - this.composition.on('add', this.onCompositionAdd, this); - this.composition.on('remove', this.onCompositionRemove, this); - this.composition.on('load', this.onCompositionLoad, this); + this.composition.load(); + } - this.composition.load(); + /** + * Register a callback with this ConditionManager: supported callbacks are add + * remove, load, metadata, and receiveTelemetry + * @param {string} event The key for the event to listen to + * @param {function} callback The function that this rule will envoke on this event + * @param {Object} context A reference to a scope to use as the context for + * context for the callback function + */ + ConditionManager.prototype.on = function (event, callback, context) { + if (this.supportedCallbacks.includes(event)) { + this.eventEmitter.on(event, callback, context || this); + } else { + throw ( + event + ' is not a supported callback. Supported callbacks are ' + this.supportedCallbacks + ); + } + }; + + /** + * Given a set of rules, execute the conditions associated with each rule + * and return the id of the last rule whose conditions evaluate to true + * @param {string[]} ruleOrder An array of rule IDs indicating what order They + * should be evaluated in + * @param {Object} rules An object mapping rule IDs to rule configurations + * @return {string} The ID of the rule to display on the widget + */ + ConditionManager.prototype.executeRules = function (ruleOrder, rules) { + const self = this; + let activeId = ruleOrder[0]; + let rule; + let conditions; + + ruleOrder.forEach(function (ruleId) { + rule = rules[ruleId]; + conditions = rule.getProperty('conditions'); + if (self.evaluator.execute(conditions, rule.getProperty('trigger'))) { + activeId = ruleId; + } + }); + + return activeId; + }; + + /** + * Adds a field to the list of all available metadata fields in the widget + * @param {Object} metadatum An object representing a set of telemetry metadata + */ + ConditionManager.prototype.addGlobalMetadata = function (metadatum) { + this.telemetryMetadataById.any[metadatum.key] = metadatum; + this.telemetryMetadataById.all[metadatum.key] = metadatum; + }; + + /** + * Adds a field to the list of properties for globally available metadata + * @param {string} key The key for the property this type applies to + * @param {string} type The type that should be associated with this property + */ + ConditionManager.prototype.addGlobalPropertyType = function (key, type) { + this.telemetryTypesById.any[key] = type; + this.telemetryTypesById.all[key] = type; + }; + + /** + * Given a telemetry-producing domain object, associate each of it's telemetry + * fields with a type, parsing from historical data. + * @param {Object} object a domain object that can produce telemetry + * @return {Promise} A promise that resolves when a telemetry request + * has completed and types have been parsed + */ + ConditionManager.prototype.parsePropertyTypes = function (object) { + const objectId = objectUtils.makeKeyString(object.identifier); + + this.telemetryTypesById[objectId] = {}; + Object.values(this.telemetryMetadataById[objectId]).forEach(function (valueMetadata) { + let type; + if (valueMetadata.enumerations !== undefined) { + type = 'enum'; + } else if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'range')) { + type = 'number'; + } else if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'domain')) { + type = 'number'; + } else if (valueMetadata.key === 'name') { + type = 'string'; + } else { + type = 'string'; + } + + this.telemetryTypesById[objectId][valueMetadata.key] = type; + this.addGlobalPropertyType(valueMetadata.key, type); + }, this); + }; + + /** + * Parse types of telemetry fields from all composition objects; used internally + * to perform a block types load once initial composition load has completed + * @return {Promise} A promise that resolves when all metadata has been loaded + * and property types parsed + */ + ConditionManager.prototype.parseAllPropertyTypes = function () { + Object.values(this.compositionObjs).forEach(this.parsePropertyTypes, this); + this.metadataLoadComplete = true; + this.eventEmitter.emit('metadata'); + }; + + /** + * Invoked when a telemtry subscription yields new data. Updates the LAD + * cache and invokes any registered receiveTelemetry callbacks + * @param {string} objId The key associated with the telemetry source + * @param {datum} datum The new data from the telemetry source + * @private + */ + ConditionManager.prototype.handleSubscriptionCallback = function (objId, telemetryDatum) { + this.subscriptionCache[objId] = this.createNormalizedDatum(objId, telemetryDatum); + this.eventEmitter.emit('receiveTelemetry'); + }; + + ConditionManager.prototype.createNormalizedDatum = function (objId, telemetryDatum) { + return Object.values(this.telemetryMetadataById[objId]).reduce((normalizedDatum, metadatum) => { + normalizedDatum[metadatum.key] = telemetryDatum[metadatum.source]; + + return normalizedDatum; + }, {}); + }; + + /** + * Event handler for an add event in this Summary Widget's composition. + * Sets up subscription handlers and parses its property types. + * @param {Object} obj The newly added domain object + * @private + */ + ConditionManager.prototype.onCompositionAdd = function (obj) { + let compositionKeys; + const telemetryAPI = this.openmct.telemetry; + const objId = objectUtils.makeKeyString(obj.identifier); + let telemetryMetadata; + const self = this; + + if (telemetryAPI.isTelemetryObject(obj)) { + self.compositionObjs[objId] = obj; + self.telemetryMetadataById[objId] = {}; + + // FIXME: this should just update based on listener. + compositionKeys = self.domainObject.composition.map(objectUtils.makeKeyString); + if (!compositionKeys.includes(objId)) { + self.domainObject.composition.push(obj.identifier); + } + + telemetryMetadata = telemetryAPI.getMetadata(obj).values(); + telemetryMetadata.forEach(function (metaDatum) { + self.telemetryMetadataById[objId][metaDatum.key] = metaDatum; + self.addGlobalMetadata(metaDatum); + }); + + self.subscriptionCache[objId] = {}; + self.subscriptions[objId] = telemetryAPI.subscribe( + obj, + function (datum) { + self.handleSubscriptionCallback(objId, datum); + }, + {} + ); + telemetryAPI + .request(obj, { + strategy: 'latest', + size: 1 + }) + .then(function (results) { + if (results && results.length) { + self.handleSubscriptionCallback(objId, results[results.length - 1]); + } + }); + + /** + * if this is the initial load, parsing property types will be postponed + * until all composition objects have been loaded + */ + if (self.loadComplete) { + self.parsePropertyTypes(obj); + } + + self.eventEmitter.emit('add', obj); + + const summaryWidget = document.querySelector('.w-summary-widget'); + if (summaryWidget) { + summaryWidget.classList.remove('s-status-no-data'); + } + } + }; + + /** + * Invoked on a remove event in this Summary Widget's compostion. Removes + * the object from the local composition, and untracks it + * @param {object} identifier The identifier of the object to be removed + * @private + */ + ConditionManager.prototype.onCompositionRemove = function (identifier) { + const objectId = objectUtils.makeKeyString(identifier); + // FIXME: this should just update by listener. + _.remove(this.domainObject.composition, function (id) { + return id.key === identifier.key && id.namespace === identifier.namespace; + }); + delete this.compositionObjs[objectId]; + delete this.subscriptionCache[objectId]; + this.subscriptions[objectId](); //unsubscribe from telemetry source + delete this.subscriptions[objectId]; + this.eventEmitter.emit('remove', identifier); + + if (_.isEmpty(this.compositionObjs)) { + const summaryWidget = document.querySelector('.w-summary-widget'); + if (summaryWidget) { + summaryWidget.classList.add('s-status-no-data'); + } + } + }; + + /** + * Invoked when the Summary Widget's composition finishes its initial load. + * Invokes any registered load callbacks, does a block load of all metadata, + * and then invokes any registered metadata load callbacks. + * @private + */ + ConditionManager.prototype.onCompositionLoad = function () { + this.loadComplete = true; + this.eventEmitter.emit('load'); + this.parseAllPropertyTypes(); + }; + + /** + * Returns the currently tracked telemetry sources + * @return {Object} An object mapping object keys to domain objects + */ + ConditionManager.prototype.getComposition = function () { + return this.compositionObjs; + }; + + /** + * Get the human-readable name of a domain object from its key + * @param {string} id The key of the domain object + * @return {string} The human-readable name of the domain object + */ + ConditionManager.prototype.getObjectName = function (id) { + let name; + + if (this.keywordLabels[id]) { + name = this.keywordLabels[id]; + } else if (this.compositionObjs[id]) { + name = this.compositionObjs[id].name; } - /** - * Register a callback with this ConditionManager: supported callbacks are add - * remove, load, metadata, and receiveTelemetry - * @param {string} event The key for the event to listen to - * @param {function} callback The function that this rule will envoke on this event - * @param {Object} context A reference to a scope to use as the context for - * context for the callback function - */ - ConditionManager.prototype.on = function (event, callback, context) { - if (this.supportedCallbacks.includes(event)) { - this.eventEmitter.on(event, callback, context || this); - } else { - throw event + " is not a supported callback. Supported callbacks are " + this.supportedCallbacks; - } - }; + return name; + }; - /** - * Given a set of rules, execute the conditions associated with each rule - * and return the id of the last rule whose conditions evaluate to true - * @param {string[]} ruleOrder An array of rule IDs indicating what order They - * should be evaluated in - * @param {Object} rules An object mapping rule IDs to rule configurations - * @return {string} The ID of the rule to display on the widget - */ - ConditionManager.prototype.executeRules = function (ruleOrder, rules) { - const self = this; - let activeId = ruleOrder[0]; - let rule; - let conditions; + /** + * Returns the property metadata associated with a given telemetry source + * @param {string} id The key associated with the domain object + * @return {Object} Returns an object with fields representing each telemetry field + */ + ConditionManager.prototype.getTelemetryMetadata = function (id) { + return this.telemetryMetadataById[id]; + }; - ruleOrder.forEach(function (ruleId) { - rule = rules[ruleId]; - conditions = rule.getProperty('conditions'); - if (self.evaluator.execute(conditions, rule.getProperty('trigger'))) { - activeId = ruleId; - } - }); + /** + * Returns the type associated with a telemtry data field of a particular domain + * object + * @param {string} id The key associated with the domain object + * @param {string} property The telemetry field key to retrieve the type of + * @return {string} The type name + */ + ConditionManager.prototype.getTelemetryPropertyType = function (id, property) { + if (this.telemetryTypesById[id]) { + return this.telemetryTypesById[id][property]; + } + }; - return activeId; - }; + /** + * Returns the human-readable name of a telemtry data field of a particular domain + * object + * @param {string} id The key associated with the domain object + * @param {string} property The telemetry field key to retrieve the type of + * @return {string} The telemetry field name + */ + ConditionManager.prototype.getTelemetryPropertyName = function (id, property) { + if (this.telemetryMetadataById[id] && this.telemetryMetadataById[id][property]) { + return this.telemetryMetadataById[id][property].name; + } + }; - /** - * Adds a field to the list of all available metadata fields in the widget - * @param {Object} metadatum An object representing a set of telemetry metadata - */ - ConditionManager.prototype.addGlobalMetadata = function (metadatum) { - this.telemetryMetadataById.any[metadatum.key] = metadatum; - this.telemetryMetadataById.all[metadatum.key] = metadatum; - }; + /** + * Returns the {ConditionEvaluator} instance associated with this condition + * manager + * @return {ConditionEvaluator} + */ + ConditionManager.prototype.getEvaluator = function () { + return this.evaluator; + }; - /** - * Adds a field to the list of properties for globally available metadata - * @param {string} key The key for the property this type applies to - * @param {string} type The type that should be associated with this property - */ - ConditionManager.prototype.addGlobalPropertyType = function (key, type) { - this.telemetryTypesById.any[key] = type; - this.telemetryTypesById.all[key] = type; - }; + /** + * Returns true if the initial compostion load has completed + * @return {boolean} + */ + ConditionManager.prototype.loadCompleted = function () { + return this.loadComplete; + }; - /** - * Given a telemetry-producing domain object, associate each of it's telemetry - * fields with a type, parsing from historical data. - * @param {Object} object a domain object that can produce telemetry - * @return {Promise} A promise that resolves when a telemetry request - * has completed and types have been parsed - */ - ConditionManager.prototype.parsePropertyTypes = function (object) { - const objectId = objectUtils.makeKeyString(object.identifier); + /** + * Returns true if the initial block metadata load has completed + */ + ConditionManager.prototype.metadataLoadCompleted = function () { + return this.metadataLoadComplete; + }; - this.telemetryTypesById[objectId] = {}; - Object.values(this.telemetryMetadataById[objectId]).forEach(function (valueMetadata) { - let type; - if (valueMetadata.enumerations !== undefined) { - type = 'enum'; - } else if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'range')) { - type = 'number'; - } else if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'domain')) { - type = 'number'; - } else if (valueMetadata.key === 'name') { - type = 'string'; - } else { - type = 'string'; - } + /** + * Triggers the telemetryRecieve callbacks registered to this ConditionManager, + * used by the {TestDataManager} to force a rule evaluation when test data is + * enabled + */ + ConditionManager.prototype.triggerTelemetryCallback = function () { + this.eventEmitter.emit('receiveTelemetry'); + }; - this.telemetryTypesById[objectId][valueMetadata.key] = type; - this.addGlobalPropertyType(valueMetadata.key, type); - }, this); - }; + /** + * Unsubscribe from all registered telemetry sources and unregister all event + * listeners registered with the Open MCT APIs + */ + ConditionManager.prototype.destroy = function () { + Object.values(this.subscriptions).forEach(function (unsubscribeFunction) { + unsubscribeFunction(); + }); + this.composition.off('add', this.onCompositionAdd, this); + this.composition.off('remove', this.onCompositionRemove, this); + this.composition.off('load', this.onCompositionLoad, this); + }; - /** - * Parse types of telemetry fields from all composition objects; used internally - * to perform a block types load once initial composition load has completed - * @return {Promise} A promise that resolves when all metadata has been loaded - * and property types parsed - */ - ConditionManager.prototype.parseAllPropertyTypes = function () { - Object.values(this.compositionObjs).forEach(this.parsePropertyTypes, this); - this.metadataLoadComplete = true; - this.eventEmitter.emit('metadata'); - }; - - /** - * Invoked when a telemtry subscription yields new data. Updates the LAD - * cache and invokes any registered receiveTelemetry callbacks - * @param {string} objId The key associated with the telemetry source - * @param {datum} datum The new data from the telemetry source - * @private - */ - ConditionManager.prototype.handleSubscriptionCallback = function (objId, telemetryDatum) { - this.subscriptionCache[objId] = this.createNormalizedDatum(objId, telemetryDatum); - this.eventEmitter.emit('receiveTelemetry'); - }; - - ConditionManager.prototype.createNormalizedDatum = function (objId, telemetryDatum) { - return Object.values(this.telemetryMetadataById[objId]).reduce((normalizedDatum, metadatum) => { - normalizedDatum[metadatum.key] = telemetryDatum[metadatum.source]; - - return normalizedDatum; - }, {}); - }; - - /** - * Event handler for an add event in this Summary Widget's composition. - * Sets up subscription handlers and parses its property types. - * @param {Object} obj The newly added domain object - * @private - */ - ConditionManager.prototype.onCompositionAdd = function (obj) { - let compositionKeys; - const telemetryAPI = this.openmct.telemetry; - const objId = objectUtils.makeKeyString(obj.identifier); - let telemetryMetadata; - const self = this; - - if (telemetryAPI.isTelemetryObject(obj)) { - self.compositionObjs[objId] = obj; - self.telemetryMetadataById[objId] = {}; - - // FIXME: this should just update based on listener. - compositionKeys = self.domainObject.composition.map(objectUtils.makeKeyString); - if (!compositionKeys.includes(objId)) { - self.domainObject.composition.push(obj.identifier); - } - - telemetryMetadata = telemetryAPI.getMetadata(obj).values(); - telemetryMetadata.forEach(function (metaDatum) { - self.telemetryMetadataById[objId][metaDatum.key] = metaDatum; - self.addGlobalMetadata(metaDatum); - }); - - self.subscriptionCache[objId] = {}; - self.subscriptions[objId] = telemetryAPI.subscribe(obj, function (datum) { - self.handleSubscriptionCallback(objId, datum); - }, {}); - telemetryAPI.request(obj, { - strategy: 'latest', - size: 1 - }) - .then(function (results) { - if (results && results.length) { - self.handleSubscriptionCallback(objId, results[results.length - 1]); - } - }); - - /** - * if this is the initial load, parsing property types will be postponed - * until all composition objects have been loaded - */ - if (self.loadComplete) { - self.parsePropertyTypes(obj); - } - - self.eventEmitter.emit('add', obj); - - const summaryWidget = document.querySelector('.w-summary-widget'); - if (summaryWidget) { - summaryWidget.classList.remove('s-status-no-data'); - } - } - }; - - /** - * Invoked on a remove event in this Summary Widget's compostion. Removes - * the object from the local composition, and untracks it - * @param {object} identifier The identifier of the object to be removed - * @private - */ - ConditionManager.prototype.onCompositionRemove = function (identifier) { - const objectId = objectUtils.makeKeyString(identifier); - // FIXME: this should just update by listener. - _.remove(this.domainObject.composition, function (id) { - return id.key === identifier.key - && id.namespace === identifier.namespace; - }); - delete this.compositionObjs[objectId]; - delete this.subscriptionCache[objectId]; - this.subscriptions[objectId](); //unsubscribe from telemetry source - delete this.subscriptions[objectId]; - this.eventEmitter.emit('remove', identifier); - - if (_.isEmpty(this.compositionObjs)) { - const summaryWidget = document.querySelector('.w-summary-widget'); - if (summaryWidget) { - summaryWidget.classList.add('s-status-no-data'); - } - } - }; - - /** - * Invoked when the Summary Widget's composition finishes its initial load. - * Invokes any registered load callbacks, does a block load of all metadata, - * and then invokes any registered metadata load callbacks. - * @private - */ - ConditionManager.prototype.onCompositionLoad = function () { - this.loadComplete = true; - this.eventEmitter.emit('load'); - this.parseAllPropertyTypes(); - }; - - /** - * Returns the currently tracked telemetry sources - * @return {Object} An object mapping object keys to domain objects - */ - ConditionManager.prototype.getComposition = function () { - return this.compositionObjs; - }; - - /** - * Get the human-readable name of a domain object from its key - * @param {string} id The key of the domain object - * @return {string} The human-readable name of the domain object - */ - ConditionManager.prototype.getObjectName = function (id) { - let name; - - if (this.keywordLabels[id]) { - name = this.keywordLabels[id]; - } else if (this.compositionObjs[id]) { - name = this.compositionObjs[id].name; - } - - return name; - }; - - /** - * Returns the property metadata associated with a given telemetry source - * @param {string} id The key associated with the domain object - * @return {Object} Returns an object with fields representing each telemetry field - */ - ConditionManager.prototype.getTelemetryMetadata = function (id) { - return this.telemetryMetadataById[id]; - }; - - /** - * Returns the type associated with a telemtry data field of a particular domain - * object - * @param {string} id The key associated with the domain object - * @param {string} property The telemetry field key to retrieve the type of - * @return {string} The type name - */ - ConditionManager.prototype.getTelemetryPropertyType = function (id, property) { - if (this.telemetryTypesById[id]) { - return this.telemetryTypesById[id][property]; - } - }; - - /** - * Returns the human-readable name of a telemtry data field of a particular domain - * object - * @param {string} id The key associated with the domain object - * @param {string} property The telemetry field key to retrieve the type of - * @return {string} The telemetry field name - */ - ConditionManager.prototype.getTelemetryPropertyName = function (id, property) { - if (this.telemetryMetadataById[id] && this.telemetryMetadataById[id][property]) { - return this.telemetryMetadataById[id][property].name; - } - }; - - /** - * Returns the {ConditionEvaluator} instance associated with this condition - * manager - * @return {ConditionEvaluator} - */ - ConditionManager.prototype.getEvaluator = function () { - return this.evaluator; - }; - - /** - * Returns true if the initial compostion load has completed - * @return {boolean} - */ - ConditionManager.prototype.loadCompleted = function () { - return this.loadComplete; - }; - - /** - * Returns true if the initial block metadata load has completed - */ - ConditionManager.prototype.metadataLoadCompleted = function () { - return this.metadataLoadComplete; - }; - - /** - * Triggers the telemetryRecieve callbacks registered to this ConditionManager, - * used by the {TestDataManager} to force a rule evaluation when test data is - * enabled - */ - ConditionManager.prototype.triggerTelemetryCallback = function () { - this.eventEmitter.emit('receiveTelemetry'); - }; - - /** - * Unsubscribe from all registered telemetry sources and unregister all event - * listeners registered with the Open MCT APIs - */ - ConditionManager.prototype.destroy = function () { - Object.values(this.subscriptions).forEach(function (unsubscribeFunction) { - unsubscribeFunction(); - }); - this.composition.off('add', this.onCompositionAdd, this); - this.composition.off('remove', this.onCompositionRemove, this); - this.composition.off('load', this.onCompositionLoad, this); - }; - - return ConditionManager; + return ConditionManager; }); diff --git a/src/plugins/summaryWidget/src/Rule.js b/src/plugins/summaryWidget/src/Rule.js index 0b8f28804f..831fb0d2a7 100644 --- a/src/plugins/summaryWidget/src/Rule.js +++ b/src/plugins/summaryWidget/src/Rule.js @@ -1,525 +1,536 @@ define([ - '../res/ruleTemplate.html', - './Condition', - './input/ColorPalette', - './input/IconPalette', - './eventHelpers', - '../../../utils/template/templateHelpers', - 'EventEmitter', - 'lodash' + '../res/ruleTemplate.html', + './Condition', + './input/ColorPalette', + './input/IconPalette', + './eventHelpers', + '../../../utils/template/templateHelpers', + 'EventEmitter', + 'lodash' ], function ( - ruleTemplate, - Condition, - ColorPalette, - IconPalette, - eventHelpers, - templateHelpers, - EventEmitter, - _ + ruleTemplate, + Condition, + ColorPalette, + IconPalette, + eventHelpers, + templateHelpers, + EventEmitter, + _ ) { + /** + * An object representing a summary widget rule. Maintains a set of text + * and css properties for output, and a set of conditions for configuring + * when the rule will be applied to the summary widget. + * @constructor + * @param {Object} ruleConfig A JavaScript object representing the configuration of this rule + * @param {Object} domainObject The Summary Widget domain object which contains this rule + * @param {MCT} openmct An MCT instance + * @param {ConditionManager} conditionManager A ConditionManager instance + * @param {WidgetDnD} widgetDnD A WidgetDnD instance to handle dragging and dropping rules + * @param {element} container The DOM element which cotains this summary widget + */ + function Rule(ruleConfig, domainObject, openmct, conditionManager, widgetDnD, container) { + eventHelpers.extend(this); + const self = this; + const THUMB_ICON_CLASS = 'c-sw__icon js-sw__icon'; + + this.config = ruleConfig; + this.domainObject = domainObject; + this.openmct = openmct; + this.conditionManager = conditionManager; + this.widgetDnD = widgetDnD; + this.container = container; + + this.domElement = templateHelpers.convertTemplateToHTML(ruleTemplate)[0]; + this.eventEmitter = new EventEmitter(); + this.supportedCallbacks = ['remove', 'duplicate', 'change', 'conditionChange']; + this.conditions = []; + this.dragging = false; + + this.remove = this.remove.bind(this); + this.duplicate = this.duplicate.bind(this); + + 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'); + /** - * An object representing a summary widget rule. Maintains a set of text - * and css properties for output, and a set of conditions for configuring - * when the rule will be applied to the summary widget. - * @constructor - * @param {Object} ruleConfig A JavaScript object representing the configuration of this rule - * @param {Object} domainObject The Summary Widget domain object which contains this rule - * @param {MCT} openmct An MCT instance - * @param {ConditionManager} conditionManager A ConditionManager instance - * @param {WidgetDnD} widgetDnD A WidgetDnD instance to handle dragging and dropping rules - * @param {element} container The DOM element which cotains this summary widget + * 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 */ - function Rule(ruleConfig, domainObject, openmct, conditionManager, widgetDnD, container) { - eventHelpers.extend(this); - const self = this; - const THUMB_ICON_CLASS = 'c-sw__icon js-sw__icon'; - this.config = ruleConfig; - this.domainObject = domainObject; - this.openmct = openmct; - this.conditionManager = conditionManager; - this.widgetDnD = widgetDnD; - this.container = container; + this.textInputs = { + 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.domElement = templateHelpers.convertTemplateToHTML(ruleTemplate)[0]; - this.eventEmitter = new EventEmitter(); - this.supportedCallbacks = ['remove', 'duplicate', 'change', 'conditionChange']; - this.conditions = []; - this.dragging = false; + this.iconInput = new IconPalette('', container); + this.colorInputs = { + 'background-color': new ColorPalette('icon-paint-bucket', container), + 'border-color': new ColorPalette('icon-line-horz', container), + color: new ColorPalette('icon-font', container) + }; - this.remove = this.remove.bind(this); - this.duplicate = this.duplicate.bind(this); + this.colorInputs.color.toggleNullOption(); - 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: 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); - this.colorInputs = { - 'background-color': new ColorPalette('icon-paint-bucket', container), - 'border-color': new ColorPalette('icon-line-horz', container), - 'color': new ColorPalette('icon-font', container) - }; - - this.colorInputs.color.toggleNullOption(); - - /** - * An onchange event handler method for this rule's icon palettes - * @param {string} icon The css class name corresponding to this icon - * @private - */ - function onIconInput(icon) { - self.config.icon = icon; - self.updateDomainObject('icon', icon); - self.thumbnailIcon.className = `${THUMB_ICON_CLASS + ' ' + icon}`; - self.eventEmitter.emit('change'); - } - - /** - * An onchange event handler method for this rule's color palettes palettes - * @param {string} color The color selected in the palette - * @param {string} property The css property which this color corresponds to - * @private - */ - function onColorInput(color, property) { - self.config.style[property] = color; - self.thumbnail.style[property] = color; - self.eventEmitter.emit('change'); - } - - /** - * Parse input text from textbox to prevent HTML Injection - * @param {string} msg The text to be Parsed - * @private - */ - function encodeMsg(msg) { - const div = document.createElement('div'); - div.innerText = msg; - - return div.innerText; - } - - /** - * An onchange event handler method for this rule's trigger key - * @param {event} event The change event from this rule's select element - * @private - */ - function onTriggerInput(event) { - const elem = event.target; - self.config.trigger = encodeMsg(elem.value); - self.generateDescription(); - self.updateDomainObject(); - self.refreshConditions(); - self.eventEmitter.emit('conditionChange'); - } - - /** - * An onchange event handler method for this rule's text inputs - * @param {element} elem The input element that generated the event - * @param {string} inputKey The field of this rule's configuration to update - * @private - */ - function onTextInput(elem, inputKey) { - const text = encodeMsg(elem.value); - self.config[inputKey] = text; - self.updateDomainObject(); - if (inputKey === 'name') { - self.title.innerText = text; - } else if (inputKey === 'label') { - self.thumbnailLabel.innerText = text; - } - - self.eventEmitter.emit('change'); - } - - /** - * An onchange event handler for a mousedown event that initiates a drag gesture - * @param {event} event A mouseup event that was registered on this rule's grippy - * @private - */ - function onDragStart(event) { - document.querySelectorAll('.t-drag-indicator').forEach(indicator => { - // eslint-disable-next-line no-invalid-this - const ruleHeader = self.domElement.querySelectorAll('.widget-rule-header')[0].cloneNode(true); - indicator.innerHTML = ruleHeader; - }); - self.widgetDnD.setDragImage(self.domElement.querySelectorAll('.widget-rule-header')[0].cloneNode(true)); - self.widgetDnD.dragStart(self.config.id); - self.domElement.style.display = 'none'; - } - - /** - * Show or hide this rule's configuration properties - * @private - */ - function toggleConfig() { - 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; - } - - 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.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]; - - input.set(self.config.style[inputKey]); - onColorInput(self.config.style[inputKey], inputKey); - - input.on('change', function (value) { - onColorInput(value, inputKey); - self.updateDomainObject(); - }); - - self.domElement.querySelector('.t-style-input').append(input.getDOM()); - }); - - Object.keys(this.textInputs).forEach(function (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); - this.listenTo(this.duplicateButton, 'click', this.duplicate); - this.listenTo(this.addConditionButton, 'click', function () { - self.initCondition(); - }); - this.listenTo(this.toggleConfigButton, 'click', toggleConfig); - this.listenTo(this.trigger, 'change', onTriggerInput); - - 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(); - document.querySelector('.t-drag-indicator').style.display = 'none'; - }, this); - - if (!this.conditionManager.loadCompleted()) { - this.config.expanded = false; - } - - if (!this.config.expanded) { - this.configArea.classList.remove('expanded'); - this.toggleConfigButton.classList.remove('c-disclosure-triangle--expanded'); - } - - if (this.domainObject.configuration.ruleOrder.length === 2) { - 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') { - 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'; - } + /** + * An onchange event handler method for this rule's icon palettes + * @param {string} icon The css class name corresponding to this icon + * @private + */ + function onIconInput(icon) { + self.config.icon = icon; + self.updateDomainObject('icon', icon); + self.thumbnailIcon.className = `${THUMB_ICON_CLASS + ' ' + icon}`; + self.eventEmitter.emit('change'); } /** - * Return the DOM element representing this rule - * @return {Element} A DOM element + * An onchange event handler method for this rule's color palettes palettes + * @param {string} color The color selected in the palette + * @param {string} property The css property which this color corresponds to + * @private */ - Rule.prototype.getDOM = function () { - return this.domElement; - }; + function onColorInput(color, property) { + self.config.style[property] = color; + self.thumbnail.style[property] = color; + self.eventEmitter.emit('change'); + } /** - * Unregister any event handlers registered with external sources + * Parse input text from textbox to prevent HTML Injection + * @param {string} msg The text to be Parsed + * @private */ - Rule.prototype.destroy = function () { - Object.values(this.colorInputs).forEach(function (palette) { - palette.destroy(); + function encodeMsg(msg) { + const div = document.createElement('div'); + div.innerText = msg; + + return div.innerText; + } + + /** + * An onchange event handler method for this rule's trigger key + * @param {event} event The change event from this rule's select element + * @private + */ + function onTriggerInput(event) { + const elem = event.target; + self.config.trigger = encodeMsg(elem.value); + self.generateDescription(); + self.updateDomainObject(); + self.refreshConditions(); + self.eventEmitter.emit('conditionChange'); + } + + /** + * An onchange event handler method for this rule's text inputs + * @param {element} elem The input element that generated the event + * @param {string} inputKey The field of this rule's configuration to update + * @private + */ + function onTextInput(elem, inputKey) { + const text = encodeMsg(elem.value); + self.config[inputKey] = text; + self.updateDomainObject(); + if (inputKey === 'name') { + self.title.innerText = text; + } else if (inputKey === 'label') { + self.thumbnailLabel.innerText = text; + } + + self.eventEmitter.emit('change'); + } + + /** + * An onchange event handler for a mousedown event that initiates a drag gesture + * @param {event} event A mouseup event that was registered on this rule's grippy + * @private + */ + function onDragStart(event) { + document.querySelectorAll('.t-drag-indicator').forEach((indicator) => { + // eslint-disable-next-line no-invalid-this + const ruleHeader = self.domElement + .querySelectorAll('.widget-rule-header')[0] + .cloneNode(true); + indicator.innerHTML = ruleHeader; + }); + self.widgetDnD.setDragImage( + self.domElement.querySelectorAll('.widget-rule-header')[0].cloneNode(true) + ); + self.widgetDnD.dragStart(self.config.id); + self.domElement.style.display = 'none'; + } + + /** + * Show or hide this rule's configuration properties + * @private + */ + function toggleConfig() { + 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; + } + + 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.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]; + + input.set(self.config.style[inputKey]); + onColorInput(self.config.style[inputKey], inputKey); + + input.on('change', function (value) { + onColorInput(value, inputKey); + self.updateDomainObject(); + }); + + self.domElement.querySelector('.t-style-input').append(input.getDOM()); + }); + + Object.keys(this.textInputs).forEach(function (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.iconInput.destroy(); - this.stopListening(); - this.conditions.forEach(function (condition) { - condition.destroy(); - }); - }; + } + }); - /** - * Register a callback with this rule: supported callbacks are remove, change, - * conditionChange, and duplicate - * @param {string} event The key for the event to listen to - * @param {function} callback The function that this rule will envoke on this event - * @param {Object} context A reference to a scope to use as the context for - * context for the callback function - */ - Rule.prototype.on = function (event, callback, context) { - if (this.supportedCallbacks.includes(event)) { - this.eventEmitter.on(event, callback, context || this); - } - }; + this.listenTo(this.deleteButton, 'click', this.remove); + this.listenTo(this.duplicateButton, 'click', this.duplicate); + this.listenTo(this.addConditionButton, 'click', function () { + self.initCondition(); + }); + this.listenTo(this.toggleConfigButton, 'click', toggleConfig); + this.listenTo(this.trigger, 'change', onTriggerInput); - /** - * An event handler for when a condition's configuration is modified - * @param {} value - * @param {string} property The path in the configuration to updateDomainObject - * @param {number} index The index of the condition that initiated this change - */ - Rule.prototype.onConditionChange = function (event) { - _.set(this.config.conditions[event.index], event.property, event.value); - this.generateDescription(); - this.updateDomainObject(); - this.eventEmitter.emit('conditionChange'); - }; + this.title.innerHTML = self.config.name; + this.description.innerHTML = self.config.description; + this.trigger.value = self.config.trigger; - /** - * During a rule drag event, show the placeholder element after this rule - */ - Rule.prototype.showDragIndicator = function () { + this.listenTo(this.grippy, 'mousedown', onDragStart); + this.widgetDnD.on( + 'drop', + function () { + // eslint-disable-next-line no-invalid-this + this.domElement.show(); document.querySelector('.t-drag-indicator').style.display = 'none'; - this.domElement.querySelector('.t-drag-indicator').style.display = ''; + }, + this + ); + + if (!this.conditionManager.loadCompleted()) { + this.config.expanded = false; + } + + if (!this.config.expanded) { + this.configArea.classList.remove('expanded'); + this.toggleConfigButton.classList.remove('c-disclosure-triangle--expanded'); + } + + if (this.domainObject.configuration.ruleOrder.length === 2) { + 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') { + 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'; + } + } + + /** + * Return the DOM element representing this rule + * @return {Element} A DOM element + */ + Rule.prototype.getDOM = function () { + return this.domElement; + }; + + /** + * Unregister any event handlers registered with external sources + */ + Rule.prototype.destroy = function () { + Object.values(this.colorInputs).forEach(function (palette) { + palette.destroy(); + }); + this.iconInput.destroy(); + this.stopListening(); + this.conditions.forEach(function (condition) { + condition.destroy(); + }); + }; + + /** + * Register a callback with this rule: supported callbacks are remove, change, + * conditionChange, and duplicate + * @param {string} event The key for the event to listen to + * @param {function} callback The function that this rule will envoke on this event + * @param {Object} context A reference to a scope to use as the context for + * context for the callback function + */ + Rule.prototype.on = function (event, callback, context) { + if (this.supportedCallbacks.includes(event)) { + this.eventEmitter.on(event, callback, context || this); + } + }; + + /** + * An event handler for when a condition's configuration is modified + * @param {} value + * @param {string} property The path in the configuration to updateDomainObject + * @param {number} index The index of the condition that initiated this change + */ + Rule.prototype.onConditionChange = function (event) { + _.set(this.config.conditions[event.index], event.property, event.value); + this.generateDescription(); + this.updateDomainObject(); + this.eventEmitter.emit('conditionChange'); + }; + + /** + * During a rule drag event, show the placeholder element after this rule + */ + Rule.prototype.showDragIndicator = function () { + document.querySelector('.t-drag-indicator').style.display = 'none'; + this.domElement.querySelector('.t-drag-indicator').style.display = ''; + }; + + /** + * Mutate thet domain object with this rule's local configuration + */ + Rule.prototype.updateDomainObject = function () { + this.openmct.objects.mutate( + this.domainObject, + 'configuration.ruleConfigById.' + this.config.id, + this.config + ); + }; + + /** + * Get a property of this rule by key + * @param {string} prop They property key of this rule to get + * @return {} The queried property + */ + Rule.prototype.getProperty = function (prop) { + return this.config[prop]; + }; + + /** + * Remove this rule from the domain object's configuration and invoke any + * registered remove callbacks + */ + Rule.prototype.remove = function () { + const ruleOrder = this.domainObject.configuration.ruleOrder; + const ruleConfigById = this.domainObject.configuration.ruleConfigById; + const self = this; + + ruleConfigById[self.config.id] = undefined; + _.remove(ruleOrder, function (ruleId) { + return ruleId === self.config.id; + }); + + this.openmct.objects.mutate(this.domainObject, 'configuration.ruleConfigById', ruleConfigById); + this.openmct.objects.mutate(this.domainObject, 'configuration.ruleOrder', ruleOrder); + this.destroy(); + this.eventEmitter.emit('remove'); + }; + + /** + * Makes a deep clone of this rule's configuration, and calls the duplicate event + * callback with the cloned configuration as an argument if one has been registered + */ + Rule.prototype.duplicate = function () { + const sourceRule = JSON.parse(JSON.stringify(this.config)); + sourceRule.expanded = true; + this.eventEmitter.emit('duplicate', sourceRule); + }; + + /** + * Initialze a new condition. If called with the sourceConfig and sourceIndex arguments, + * will insert a new condition with the provided configuration after the sourceIndex + * index. Otherwise, initializes a new blank rule and inserts it at the end + * of the list. + * @param {Object} [config] The configuration to initialize this rule from, + * consisting of sourceCondition and index fields + */ + Rule.prototype.initCondition = function (config) { + const ruleConfigById = this.domainObject.configuration.ruleConfigById; + let newConfig; + const sourceIndex = config && config.index; + const defaultConfig = { + object: '', + key: '', + operation: '', + values: [] }; - /** - * Mutate thet domain object with this rule's local configuration - */ - Rule.prototype.updateDomainObject = function () { - this.openmct.objects.mutate(this.domainObject, 'configuration.ruleConfigById.' - + this.config.id, this.config); - }; + newConfig = config !== undefined ? config.sourceCondition : defaultConfig; + if (sourceIndex !== undefined) { + ruleConfigById[this.config.id].conditions.splice(sourceIndex + 1, 0, newConfig); + } else { + ruleConfigById[this.config.id].conditions.push(newConfig); + } - /** - * Get a property of this rule by key - * @param {string} prop They property key of this rule to get - * @return {} The queried property - */ - Rule.prototype.getProperty = function (prop) { - return this.config[prop]; - }; + this.domainObject.configuration.ruleConfigById = ruleConfigById; + this.updateDomainObject(); + this.refreshConditions(); + this.generateDescription(); + }; - /** - * Remove this rule from the domain object's configuration and invoke any - * registered remove callbacks - */ - Rule.prototype.remove = function () { - const ruleOrder = this.domainObject.configuration.ruleOrder; - const ruleConfigById = this.domainObject.configuration.ruleConfigById; - const self = this; + /** + * Build {Condition} objects from configuration and rebuild associated view + */ + Rule.prototype.refreshConditions = function () { + const self = this; + let $condition = null; + let loopCnt = 0; + const triggerContextStr = self.config.trigger === 'any' ? ' or ' : ' and '; - ruleConfigById[self.config.id] = undefined; - _.remove(ruleOrder, function (ruleId) { - return ruleId === self.config.id; - }); + self.conditions = []; - this.openmct.objects.mutate(this.domainObject, 'configuration.ruleConfigById', ruleConfigById); - this.openmct.objects.mutate(this.domainObject, 'configuration.ruleOrder', ruleOrder); - this.destroy(); - this.eventEmitter.emit('remove'); - }; + this.domElement.querySelectorAll('.t-condition').forEach((condition) => { + condition.remove(); + }); - /** - * Makes a deep clone of this rule's configuration, and calls the duplicate event - * callback with the cloned configuration as an argument if one has been registered - */ - Rule.prototype.duplicate = function () { - const sourceRule = JSON.parse(JSON.stringify(this.config)); - sourceRule.expanded = true; - this.eventEmitter.emit('duplicate', sourceRule); - }; + this.config.conditions.forEach(function (condition, index) { + const newCondition = new Condition(condition, index, self.conditionManager); + newCondition.on('remove', self.removeCondition, self); + newCondition.on('duplicate', self.initCondition, self); + newCondition.on('change', self.onConditionChange, self); + self.conditions.push(newCondition); + }); - /** - * Initialze a new condition. If called with the sourceConfig and sourceIndex arguments, - * will insert a new condition with the provided configuration after the sourceIndex - * index. Otherwise, initializes a new blank rule and inserts it at the end - * of the list. - * @param {Object} [config] The configuration to initialize this rule from, - * consisting of sourceCondition and index fields - */ - Rule.prototype.initCondition = function (config) { - const ruleConfigById = this.domainObject.configuration.ruleConfigById; - let newConfig; - const sourceIndex = config && config.index; - const defaultConfig = { - object: '', - key: '', - operation: '', - values: [] - }; + if (this.config.trigger === 'js') { + if (this.jsConditionArea) { + this.jsConditionArea.style.display = ''; + } - newConfig = (config !== undefined ? config.sourceCondition : defaultConfig); - if (sourceIndex !== undefined) { - ruleConfigById[this.config.id].conditions.splice(sourceIndex + 1, 0, newConfig); - } else { - ruleConfigById[this.config.id].conditions.push(newConfig); + this.addConditionButton.style.display = 'none'; + } else { + if (this.jsConditionArea) { + this.jsConditionArea.style.display = 'none'; + } + + this.addConditionButton.style.display = ''; + self.conditions.forEach(function (condition) { + $condition = condition.getDOM(); + const lastOfType = self.conditionArea.querySelector('li:last-of-type'); + lastOfType.parentNode.insertBefore($condition, lastOfType); + if (loopCnt > 0) { + $condition.querySelector('.t-condition-context').innerHTML = triggerContextStr + ' when'; } - this.domainObject.configuration.ruleConfigById = ruleConfigById; - this.updateDomainObject(); - this.refreshConditions(); - this.generateDescription(); - }; + loopCnt++; + }); + } - /** - * Build {Condition} objects from configuration and rebuild associated view - */ - Rule.prototype.refreshConditions = function () { - const self = this; - let $condition = null; - let loopCnt = 0; - const triggerContextStr = self.config.trigger === 'any' ? ' or ' : ' and '; + if (self.conditions.length === 1) { + self.conditions[0].hideButtons(); + } + }; - self.conditions = []; + /** + * Remove a condition from this rule's configuration at the given index + * @param {number} removeIndex The index of the condition to remove + */ + Rule.prototype.removeCondition = function (removeIndex) { + const ruleConfigById = this.domainObject.configuration.ruleConfigById; + const conditions = ruleConfigById[this.config.id].conditions; - this.domElement.querySelectorAll('.t-condition').forEach(condition => { - condition.remove(); - }); + _.remove(conditions, function (condition, index) { + return index === removeIndex; + }); + this.domainObject.configuration.ruleConfigById[this.config.id] = this.config; + this.updateDomainObject(); + this.refreshConditions(); + this.generateDescription(); + this.eventEmitter.emit('conditionChange'); + }; + + /** + * Build a human-readable description from this rule's conditions + */ + Rule.prototype.generateDescription = function () { + let description = ''; + const manager = this.conditionManager; + const evaluator = manager.getEvaluator(); + let name; + let property; + let operation; + const self = this; + + if (this.config.conditions && this.config.id !== 'default') { + if (self.config.trigger === 'js') { + description = 'when a custom JavaScript condition evaluates to true'; + } else { this.config.conditions.forEach(function (condition, index) { - const newCondition = new Condition(condition, index, self.conditionManager); - newCondition.on('remove', self.removeCondition, self); - newCondition.on('duplicate', self.initCondition, self); - newCondition.on('change', self.onConditionChange, self); - self.conditions.push(newCondition); + name = manager.getObjectName(condition.object); + property = manager.getTelemetryPropertyName(condition.object, condition.key); + operation = evaluator.getOperationDescription(condition.operation, condition.values); + if (name || property || operation) { + description += + 'when ' + + (name ? name + "'s " : '') + + (property ? property + ' ' : '') + + (operation ? operation + ' ' : '') + + (self.config.trigger === 'any' ? ' OR ' : ' AND '); + } }); + } + } - if (this.config.trigger === 'js') { - if (this.jsConditionArea) { - this.jsConditionArea.style.display = ''; - } + if (description.endsWith('OR ')) { + description = description.substring(0, description.length - 3); + } - this.addConditionButton.style.display = 'none'; - } else { - if (this.jsConditionArea) { - this.jsConditionArea.style.display = 'none'; - } + if (description.endsWith('AND ')) { + description = description.substring(0, description.length - 4); + } - this.addConditionButton.style.display = ''; - self.conditions.forEach(function (condition) { - $condition = condition.getDOM(); - const lastOfType = self.conditionArea.querySelector('li:last-of-type'); - lastOfType.parentNode.insertBefore($condition, lastOfType); - if (loopCnt > 0) { - $condition.querySelector('.t-condition-context').innerHTML = triggerContextStr + ' when'; - } + description = description === '' ? this.config.description : description; + this.description.innerHTML = self.config.description; + this.config.description = description; + }; - loopCnt++; - }); - } - - if (self.conditions.length === 1) { - self.conditions[0].hideButtons(); - } - - }; - - /** - * Remove a condition from this rule's configuration at the given index - * @param {number} removeIndex The index of the condition to remove - */ - Rule.prototype.removeCondition = function (removeIndex) { - const ruleConfigById = this.domainObject.configuration.ruleConfigById; - const conditions = ruleConfigById[this.config.id].conditions; - - _.remove(conditions, function (condition, index) { - return index === removeIndex; - }); - - this.domainObject.configuration.ruleConfigById[this.config.id] = this.config; - this.updateDomainObject(); - this.refreshConditions(); - this.generateDescription(); - this.eventEmitter.emit('conditionChange'); - }; - - /** - * Build a human-readable description from this rule's conditions - */ - Rule.prototype.generateDescription = function () { - let description = ''; - const manager = this.conditionManager; - const evaluator = manager.getEvaluator(); - let name; - let property; - let operation; - const self = this; - - if (this.config.conditions && this.config.id !== 'default') { - if (self.config.trigger === 'js') { - description = 'when a custom JavaScript condition evaluates to true'; - } else { - this.config.conditions.forEach(function (condition, index) { - name = manager.getObjectName(condition.object); - property = manager.getTelemetryPropertyName(condition.object, condition.key); - operation = evaluator.getOperationDescription(condition.operation, condition.values); - if (name || property || operation) { - description += 'when ' - + (name ? name + '\'s ' : '') - + (property ? property + ' ' : '') - + (operation ? operation + ' ' : '') - + (self.config.trigger === 'any' ? ' OR ' : ' AND '); - } - }); - } - } - - if (description.endsWith('OR ')) { - description = description.substring(0, description.length - 3); - } - - if (description.endsWith('AND ')) { - description = description.substring(0, description.length - 4); - } - - description = (description === '' ? this.config.description : description); - this.description.innerHTML = self.config.description; - this.config.description = description; - }; - - return Rule; + return Rule; }); diff --git a/src/plugins/summaryWidget/src/SummaryWidget.js b/src/plugins/summaryWidget/src/SummaryWidget.js index e9c1442bf2..3a6966bf35 100644 --- a/src/plugins/summaryWidget/src/SummaryWidget.js +++ b/src/plugins/summaryWidget/src/SummaryWidget.js @@ -1,382 +1,412 @@ -define([ - '../res/widgetTemplate.html', - './Rule', - './ConditionManager', - './TestDataManager', - './WidgetDnD', - './eventHelpers', - '../../../utils/template/templateHelpers', - 'objectUtils', - 'lodash', - '@braintree/sanitize-url' -], function ( - widgetTemplate, - Rule, - ConditionManager, - TestDataManager, - WidgetDnD, - eventHelpers, - templateHelpers, - objectUtils, - _, - urlSanitizeLib -) { - - //default css configuration for new rules - const DEFAULT_PROPS = { - 'color': '#cccccc', - 'background-color': '#666666', - 'border-color': 'rgba(0,0,0,0)' - }; - - /** - * A Summary Widget object, which allows a user to configure rules based - * on telemetry producing domain objects, and update a compact display - * accordingly. - * @constructor - * @param {Object} domainObject The domain Object represented by this Widget - * @param {MCT} openmct An MCT instance - */ - function SummaryWidget(domainObject, openmct) { - eventHelpers.extend(this); - - this.domainObject = domainObject; - this.openmct = openmct; - - this.domainObject.configuration = this.domainObject.configuration || {}; - this.domainObject.configuration.ruleConfigById = this.domainObject.configuration.ruleConfigById || {}; - this.domainObject.configuration.ruleOrder = this.domainObject.configuration.ruleOrder || ['default']; - this.domainObject.configuration.testDataConfig = this.domainObject.configuration.testDataConfig || [{ - object: '', - key: '', - value: '' - }]; - - this.activeId = 'default'; - this.rulesById = {}; - 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 = () => {}; - - this.outerWrapper = this.domElement.querySelector('.widget-edit-holder'); - this.ruleArea = this.domElement.querySelector('#ruleArea'); - this.configAreaRules = this.domElement.querySelector('.widget-rules-wrapper'); - - 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); - - this.watchForChanges = this.watchForChanges.bind(this); - this.show = this.show.bind(this); - this.destroy = this.destroy.bind(this); - this.addRule = this.addRule.bind(this); - - this.addHyperlink(domainObject.url, domainObject.openNewTab); - this.watchForChanges(openmct, domainObject); - - const self = this; - - /** - * Toggles the configuration area for test data in the view - * @private - */ - function toggleTestData() { - 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); - - /** - * Toggles the configuration area for rules in the view - * @private - */ - function toggleRules() { - templateHelpers.toggleClass(self.outerWrapper, 'expanded-widget-rules'); - templateHelpers.toggleClass(self.toggleRulesControl, 'c-disclosure-triangle--expanded'); - } - - this.listenTo(this.toggleRulesControl, 'click', toggleRules); - - } - - /** - * adds or removes href to widget button and adds or removes openInNewTab - * @param {string} url String that denotes the url to be opened - * @param {string} openNewTab String that denotes wether to open link in new tab or not - */ - SummaryWidget.prototype.addHyperlink = function (url, openNewTab) { - if (url) { - this.widgetButton.href = urlSanitizeLib.sanitizeUrl(url); - } else { - this.widgetButton.removeAttribute('href'); - } - - if (openNewTab === 'newTab') { - this.widgetButton.target = '_blank'; - } else { - this.widgetButton.removeAttribute('target'); - } - }; - - /** - * adds a listener to the object to watch for any changes made by user - * only executes if changes are observed - * @param {openmct} Object Instance of OpenMCT - * @param {domainObject} Object instance of this object - */ - SummaryWidget.prototype.watchForChanges = function (openmct, domainObject) { - this.watchForChangesUnsubscribe = openmct.objects.observe(domainObject, '*', function (newDomainObject) { - if (newDomainObject.url !== this.domainObject.url - || newDomainObject.openNewTab !== this.domainObject.openNewTab) { - this.addHyperlink(newDomainObject.url, newDomainObject.openNewTab); - } - }.bind(this)); - }; - - /** - * Builds the Summary Widget's DOM, performs other necessary setup, and attaches - * this Summary Widget's view to the supplied container. - * @param {element} container The DOM element that will contain this Summary - * Widget's view. - */ - SummaryWidget.prototype.show = function (container) { - const self = this; - this.container = container; - 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) { - if (ruleId !== 'default') { - self.initRule(ruleId); - } - }); - this.refreshRules(); - this.updateWidget(); - - this.listenTo(this.addRuleButton, 'click', this.addRule); - this.conditionManager.on('receiveTelemetry', this.executeRules, this); - this.widgetDnD.on('drop', this.reorder, this); - }; - - /** - * Unregister event listeners with the Open MCT APIs, unsubscribe from telemetry, - * and clean up event handlers - */ - SummaryWidget.prototype.destroy = function (container) { - this.editListenerUnsubscribe(); - this.conditionManager.destroy(); - this.testDataManager.destroy(); - this.widgetDnD.destroy(); - this.watchForChangesUnsubscribe(); - Object.values(this.rulesById).forEach(function (rule) { - rule.destroy(); - }); - - this.stopListening(); - }; - - /** - * Update the view from the current rule configuration and order - */ - SummaryWidget.prototype.refreshRules = function () { - const self = this; - const ruleOrder = self.domainObject.configuration.ruleOrder; - const rules = self.rulesById; - self.ruleArea.innerHTML = ''; - Object.values(ruleOrder).forEach(function (ruleId) { - self.ruleArea.append(rules[ruleId].getDOM()); - }); - - this.executeRules(); - this.addOrRemoveDragIndicator(); - }; - - SummaryWidget.prototype.addOrRemoveDragIndicator = function () { - const rules = this.domainObject.configuration.ruleOrder; - const rulesById = this.rulesById; - - rules.forEach(function (ruleKey, index, array) { - if (array.length > 2 && index > 0) { - rulesById[ruleKey].domElement.querySelector('.t-grippy').style.display = ''; - } else { - rulesById[ruleKey].domElement.querySelector('.t-grippy').style.display = 'none'; - } - }); - }; - - /** - * Update the widget's appearance from the configuration of the active rule - */ - SummaryWidget.prototype.updateWidget = function () { - const WIDGET_ICON_CLASS = 'c-sw__icon js-sw__icon'; - const activeRule = this.rulesById[this.activeId]; - 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'); - }; - - /** - * Get the active rule and update the Widget's appearance. - */ - SummaryWidget.prototype.executeRules = function () { - this.activeId = this.conditionManager.executeRules( - this.domainObject.configuration.ruleOrder, - this.rulesById - ); - this.updateWidget(); - }; - - /** - * Add a new rule to this widget - */ - SummaryWidget.prototype.addRule = function () { - let ruleCount = 0; - let ruleId; - const ruleOrder = this.domainObject.configuration.ruleOrder; - - while (Object.keys(this.rulesById).includes('rule' + ruleCount)) { - ruleCount++; - } - - ruleId = 'rule' + ruleCount; - ruleOrder.push(ruleId); - this.domainObject.configuration.ruleOrder = ruleOrder; - - this.initRule(ruleId, 'Rule'); - this.updateDomainObject(); - this.refreshRules(); - }; - - /** - * Duplicate an existing widget rule from its configuration and splice it in - * after the rule it duplicates - * @param {Object} sourceConfig The configuration properties of the rule to be - * instantiated - */ - SummaryWidget.prototype.duplicateRule = function (sourceConfig) { - let ruleCount = 0; - let ruleId; - const sourceRuleId = sourceConfig.id; - const ruleOrder = this.domainObject.configuration.ruleOrder; - const ruleIds = Object.keys(this.rulesById); - - while (ruleIds.includes('rule' + ruleCount)) { - ruleCount = ++ruleCount; - } - - ruleId = 'rule' + ruleCount; - sourceConfig.id = ruleId; - sourceConfig.name += ' Copy'; - ruleOrder.splice(ruleOrder.indexOf(sourceRuleId) + 1, 0, ruleId); - this.domainObject.configuration.ruleOrder = ruleOrder; - this.domainObject.configuration.ruleConfigById[ruleId] = sourceConfig; - this.initRule(ruleId, sourceConfig.name); - this.updateDomainObject(); - this.refreshRules(); - }; - - /** - * Initialze a new rule from a default configuration, or build a {Rule} object - * from it if already exists - * @param {string} ruleId An key to be used to identify this ruleId, or the key - of the rule to be instantiated - * @param {string} ruleName The initial human-readable name of this rule - */ - SummaryWidget.prototype.initRule = function (ruleId, ruleName) { - let ruleConfig; - const styleObj = {}; - - Object.assign(styleObj, DEFAULT_PROPS); - if (!this.domainObject.configuration.ruleConfigById[ruleId]) { - this.domainObject.configuration.ruleConfigById[ruleId] = { - name: ruleName || 'Rule', - label: 'Unnamed Rule', - message: '', - id: ruleId, - icon: ' ', - style: styleObj, - description: ruleId === 'default' ? 'Default appearance for the widget' : 'A new rule', - conditions: [{ - object: '', - key: '', - operation: '', - values: [] - }], - jsCondition: '', - trigger: 'any', - expanded: 'true' - }; - - } - - ruleConfig = this.domainObject.configuration.ruleConfigById[ruleId]; - this.rulesById[ruleId] = new Rule(ruleConfig, this.domainObject, this.openmct, - this.conditionManager, this.widgetDnD, this.container); - this.rulesById[ruleId].on('remove', this.refreshRules, this); - this.rulesById[ruleId].on('duplicate', this.duplicateRule, this); - this.rulesById[ruleId].on('change', this.updateWidget, this); - this.rulesById[ruleId].on('conditionChange', this.executeRules, this); - }; - - /** - * Given two ruleIds, move the source rule after the target rule and update - * the view. - * @param {Object} event An event object representing this drop with draggingId - * and dropTarget fields - */ - SummaryWidget.prototype.reorder = function (event) { - const ruleOrder = this.domainObject.configuration.ruleOrder; - const sourceIndex = ruleOrder.indexOf(event.draggingId); - let targetIndex; - - if (event.draggingId !== event.dropTarget) { - ruleOrder.splice(sourceIndex, 1); - targetIndex = ruleOrder.indexOf(event.dropTarget); - ruleOrder.splice(targetIndex + 1, 0, event.draggingId); - this.domainObject.configuration.ruleOrder = ruleOrder; - this.updateDomainObject(); - } - - this.refreshRules(); - }; - - /** - * Apply a list of css properties to an element - * @param {element} elem The DOM element to which the rules will be applied - * @param {object} style an object representing the style - */ - SummaryWidget.prototype.applyStyle = function (elem, style) { - Object.keys(style).forEach(function (propId) { - elem.style[propId] = style[propId]; - }); - }; - - /** - * Mutate this domain object's configuration with the current local configuration - */ - SummaryWidget.prototype.updateDomainObject = function () { - this.openmct.objects.mutate(this.domainObject, 'configuration', this.domainObject.configuration); - }; - - return SummaryWidget; -}); +define([ + '../res/widgetTemplate.html', + './Rule', + './ConditionManager', + './TestDataManager', + './WidgetDnD', + './eventHelpers', + '../../../utils/template/templateHelpers', + 'objectUtils', + 'lodash', + '@braintree/sanitize-url' +], function ( + widgetTemplate, + Rule, + ConditionManager, + TestDataManager, + WidgetDnD, + eventHelpers, + templateHelpers, + objectUtils, + _, + urlSanitizeLib +) { + //default css configuration for new rules + const DEFAULT_PROPS = { + color: '#cccccc', + 'background-color': '#666666', + 'border-color': 'rgba(0,0,0,0)' + }; + + /** + * A Summary Widget object, which allows a user to configure rules based + * on telemetry producing domain objects, and update a compact display + * accordingly. + * @constructor + * @param {Object} domainObject The domain Object represented by this Widget + * @param {MCT} openmct An MCT instance + */ + function SummaryWidget(domainObject, openmct) { + eventHelpers.extend(this); + + this.domainObject = domainObject; + this.openmct = openmct; + + this.domainObject.configuration = this.domainObject.configuration || {}; + this.domainObject.configuration.ruleConfigById = + this.domainObject.configuration.ruleConfigById || {}; + this.domainObject.configuration.ruleOrder = this.domainObject.configuration.ruleOrder || [ + 'default' + ]; + this.domainObject.configuration.testDataConfig = this.domainObject.configuration + .testDataConfig || [ + { + object: '', + key: '', + value: '' + } + ]; + + this.activeId = 'default'; + this.rulesById = {}; + 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 = () => {}; + + this.outerWrapper = this.domElement.querySelector('.widget-edit-holder'); + this.ruleArea = this.domElement.querySelector('#ruleArea'); + this.configAreaRules = this.domElement.querySelector('.widget-rules-wrapper'); + + 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 + ); + + this.watchForChanges = this.watchForChanges.bind(this); + this.show = this.show.bind(this); + this.destroy = this.destroy.bind(this); + this.addRule = this.addRule.bind(this); + + this.addHyperlink(domainObject.url, domainObject.openNewTab); + this.watchForChanges(openmct, domainObject); + + const self = this; + + /** + * Toggles the configuration area for test data in the view + * @private + */ + function toggleTestData() { + 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); + + /** + * Toggles the configuration area for rules in the view + * @private + */ + function toggleRules() { + templateHelpers.toggleClass(self.outerWrapper, 'expanded-widget-rules'); + templateHelpers.toggleClass(self.toggleRulesControl, 'c-disclosure-triangle--expanded'); + } + + this.listenTo(this.toggleRulesControl, 'click', toggleRules); + } + + /** + * adds or removes href to widget button and adds or removes openInNewTab + * @param {string} url String that denotes the url to be opened + * @param {string} openNewTab String that denotes wether to open link in new tab or not + */ + SummaryWidget.prototype.addHyperlink = function (url, openNewTab) { + if (url) { + this.widgetButton.href = urlSanitizeLib.sanitizeUrl(url); + } else { + this.widgetButton.removeAttribute('href'); + } + + if (openNewTab === 'newTab') { + this.widgetButton.target = '_blank'; + } else { + this.widgetButton.removeAttribute('target'); + } + }; + + /** + * adds a listener to the object to watch for any changes made by user + * only executes if changes are observed + * @param {openmct} Object Instance of OpenMCT + * @param {domainObject} Object instance of this object + */ + SummaryWidget.prototype.watchForChanges = function (openmct, domainObject) { + this.watchForChangesUnsubscribe = openmct.objects.observe( + domainObject, + '*', + function (newDomainObject) { + if ( + newDomainObject.url !== this.domainObject.url || + newDomainObject.openNewTab !== this.domainObject.openNewTab + ) { + this.addHyperlink(newDomainObject.url, newDomainObject.openNewTab); + } + }.bind(this) + ); + }; + + /** + * Builds the Summary Widget's DOM, performs other necessary setup, and attaches + * this Summary Widget's view to the supplied container. + * @param {element} container The DOM element that will contain this Summary + * Widget's view. + */ + SummaryWidget.prototype.show = function (container) { + const self = this; + this.container = container; + 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) { + if (ruleId !== 'default') { + self.initRule(ruleId); + } + }); + this.refreshRules(); + this.updateWidget(); + + this.listenTo(this.addRuleButton, 'click', this.addRule); + this.conditionManager.on('receiveTelemetry', this.executeRules, this); + this.widgetDnD.on('drop', this.reorder, this); + }; + + /** + * Unregister event listeners with the Open MCT APIs, unsubscribe from telemetry, + * and clean up event handlers + */ + SummaryWidget.prototype.destroy = function (container) { + this.editListenerUnsubscribe(); + this.conditionManager.destroy(); + this.testDataManager.destroy(); + this.widgetDnD.destroy(); + this.watchForChangesUnsubscribe(); + Object.values(this.rulesById).forEach(function (rule) { + rule.destroy(); + }); + + this.stopListening(); + }; + + /** + * Update the view from the current rule configuration and order + */ + SummaryWidget.prototype.refreshRules = function () { + const self = this; + const ruleOrder = self.domainObject.configuration.ruleOrder; + const rules = self.rulesById; + self.ruleArea.innerHTML = ''; + Object.values(ruleOrder).forEach(function (ruleId) { + self.ruleArea.append(rules[ruleId].getDOM()); + }); + + this.executeRules(); + this.addOrRemoveDragIndicator(); + }; + + SummaryWidget.prototype.addOrRemoveDragIndicator = function () { + const rules = this.domainObject.configuration.ruleOrder; + const rulesById = this.rulesById; + + rules.forEach(function (ruleKey, index, array) { + if (array.length > 2 && index > 0) { + rulesById[ruleKey].domElement.querySelector('.t-grippy').style.display = ''; + } else { + rulesById[ruleKey].domElement.querySelector('.t-grippy').style.display = 'none'; + } + }); + }; + + /** + * Update the widget's appearance from the configuration of the active rule + */ + SummaryWidget.prototype.updateWidget = function () { + const WIDGET_ICON_CLASS = 'c-sw__icon js-sw__icon'; + const activeRule = this.rulesById[this.activeId]; + 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'); + }; + + /** + * Get the active rule and update the Widget's appearance. + */ + SummaryWidget.prototype.executeRules = function () { + this.activeId = this.conditionManager.executeRules( + this.domainObject.configuration.ruleOrder, + this.rulesById + ); + this.updateWidget(); + }; + + /** + * Add a new rule to this widget + */ + SummaryWidget.prototype.addRule = function () { + let ruleCount = 0; + let ruleId; + const ruleOrder = this.domainObject.configuration.ruleOrder; + + while (Object.keys(this.rulesById).includes('rule' + ruleCount)) { + ruleCount++; + } + + ruleId = 'rule' + ruleCount; + ruleOrder.push(ruleId); + this.domainObject.configuration.ruleOrder = ruleOrder; + + this.initRule(ruleId, 'Rule'); + this.updateDomainObject(); + this.refreshRules(); + }; + + /** + * Duplicate an existing widget rule from its configuration and splice it in + * after the rule it duplicates + * @param {Object} sourceConfig The configuration properties of the rule to be + * instantiated + */ + SummaryWidget.prototype.duplicateRule = function (sourceConfig) { + let ruleCount = 0; + let ruleId; + const sourceRuleId = sourceConfig.id; + const ruleOrder = this.domainObject.configuration.ruleOrder; + const ruleIds = Object.keys(this.rulesById); + + while (ruleIds.includes('rule' + ruleCount)) { + ruleCount = ++ruleCount; + } + + ruleId = 'rule' + ruleCount; + sourceConfig.id = ruleId; + sourceConfig.name += ' Copy'; + ruleOrder.splice(ruleOrder.indexOf(sourceRuleId) + 1, 0, ruleId); + this.domainObject.configuration.ruleOrder = ruleOrder; + this.domainObject.configuration.ruleConfigById[ruleId] = sourceConfig; + this.initRule(ruleId, sourceConfig.name); + this.updateDomainObject(); + this.refreshRules(); + }; + + /** + * Initialze a new rule from a default configuration, or build a {Rule} object + * from it if already exists + * @param {string} ruleId An key to be used to identify this ruleId, or the key + of the rule to be instantiated + * @param {string} ruleName The initial human-readable name of this rule + */ + SummaryWidget.prototype.initRule = function (ruleId, ruleName) { + let ruleConfig; + const styleObj = {}; + + Object.assign(styleObj, DEFAULT_PROPS); + if (!this.domainObject.configuration.ruleConfigById[ruleId]) { + this.domainObject.configuration.ruleConfigById[ruleId] = { + name: ruleName || 'Rule', + label: 'Unnamed Rule', + message: '', + id: ruleId, + icon: ' ', + style: styleObj, + description: ruleId === 'default' ? 'Default appearance for the widget' : 'A new rule', + conditions: [ + { + object: '', + key: '', + operation: '', + values: [] + } + ], + jsCondition: '', + trigger: 'any', + expanded: 'true' + }; + } + + ruleConfig = this.domainObject.configuration.ruleConfigById[ruleId]; + this.rulesById[ruleId] = new Rule( + ruleConfig, + this.domainObject, + this.openmct, + this.conditionManager, + this.widgetDnD, + this.container + ); + this.rulesById[ruleId].on('remove', this.refreshRules, this); + this.rulesById[ruleId].on('duplicate', this.duplicateRule, this); + this.rulesById[ruleId].on('change', this.updateWidget, this); + this.rulesById[ruleId].on('conditionChange', this.executeRules, this); + }; + + /** + * Given two ruleIds, move the source rule after the target rule and update + * the view. + * @param {Object} event An event object representing this drop with draggingId + * and dropTarget fields + */ + SummaryWidget.prototype.reorder = function (event) { + const ruleOrder = this.domainObject.configuration.ruleOrder; + const sourceIndex = ruleOrder.indexOf(event.draggingId); + let targetIndex; + + if (event.draggingId !== event.dropTarget) { + ruleOrder.splice(sourceIndex, 1); + targetIndex = ruleOrder.indexOf(event.dropTarget); + ruleOrder.splice(targetIndex + 1, 0, event.draggingId); + this.domainObject.configuration.ruleOrder = ruleOrder; + this.updateDomainObject(); + } + + this.refreshRules(); + }; + + /** + * Apply a list of css properties to an element + * @param {element} elem The DOM element to which the rules will be applied + * @param {object} style an object representing the style + */ + SummaryWidget.prototype.applyStyle = function (elem, style) { + Object.keys(style).forEach(function (propId) { + elem.style[propId] = style[propId]; + }); + }; + + /** + * Mutate this domain object's configuration with the current local configuration + */ + SummaryWidget.prototype.updateDomainObject = function () { + this.openmct.objects.mutate( + this.domainObject, + 'configuration', + this.domainObject.configuration + ); + }; + + return SummaryWidget; +}); diff --git a/src/plugins/summaryWidget/src/TestDataItem.js b/src/plugins/summaryWidget/src/TestDataItem.js index ae005c46d0..4e9323c947 100644 --- a/src/plugins/summaryWidget/src/TestDataItem.js +++ b/src/plugins/summaryWidget/src/TestDataItem.js @@ -1,200 +1,193 @@ define([ - '../res/testDataItemTemplate.html', - './input/ObjectSelect', - './input/KeySelect', - './eventHelpers', - '../../../utils/template/templateHelpers', - 'EventEmitter' -], function ( - itemTemplate, - ObjectSelect, - KeySelect, - eventHelpers, - templateHelpers, - EventEmitter -) { + '../res/testDataItemTemplate.html', + './input/ObjectSelect', + './input/KeySelect', + './eventHelpers', + '../../../utils/template/templateHelpers', + 'EventEmitter' +], function (itemTemplate, ObjectSelect, KeySelect, eventHelpers, templateHelpers, EventEmitter) { + /** + * An object representing a single mock telemetry value + * @param {object} itemConfig the configuration for this item, consisting of + * object, key, and value fields + * @param {number} index the index of this TestDataItem object in the data + * model of its parent {TestDataManager} o be injected into callbacks + * for removes + * @param {ConditionManager} conditionManager a conditionManager instance + * for populating selects with configuration data + * @constructor + */ + function TestDataItem(itemConfig, index, conditionManager) { + eventHelpers.extend(this); + this.config = itemConfig; + this.index = index; + this.conditionManager = conditionManager; + + this.domElement = templateHelpers.convertTemplateToHTML(itemTemplate)[0]; + this.eventEmitter = new EventEmitter(); + this.supportedCallbacks = ['remove', 'duplicate', 'change']; + + this.deleteButton = this.domElement.querySelector('.t-delete'); + this.duplicateButton = this.domElement.querySelector('.t-duplicate'); + + this.selects = {}; + this.valueInputs = []; + + this.remove = this.remove.bind(this); + this.duplicate = this.duplicate.bind(this); + + const self = this; /** - * An object representing a single mock telemetry value - * @param {object} itemConfig the configuration for this item, consisting of - * object, key, and value fields - * @param {number} index the index of this TestDataItem object in the data - * model of its parent {TestDataManager} o be injected into callbacks - * for removes - * @param {ConditionManager} conditionManager a conditionManager instance - * for populating selects with configuration data - * @constructor + * A change event handler for this item's select inputs, which also invokes + * change callbacks registered with this item + * @param {string} value The new value of this select item + * @param {string} property The property of this item to modify + * @private */ - function TestDataItem(itemConfig, index, conditionManager) { - eventHelpers.extend(this); - this.config = itemConfig; - this.index = index; - this.conditionManager = conditionManager; + function onSelectChange(value, property) { + if (property === 'key') { + self.generateValueInput(value); + } - this.domElement = templateHelpers.convertTemplateToHTML(itemTemplate)[0]; - this.eventEmitter = new EventEmitter(); - this.supportedCallbacks = ['remove', 'duplicate', 'change']; - - this.deleteButton = this.domElement.querySelector('.t-delete'); - this.duplicateButton = this.domElement.querySelector('.t-duplicate'); - - this.selects = {}; - this.valueInputs = []; - - this.remove = this.remove.bind(this); - this.duplicate = this.duplicate.bind(this); - - const self = this; - - /** - * A change event handler for this item's select inputs, which also invokes - * change callbacks registered with this item - * @param {string} value The new value of this select item - * @param {string} property The property of this item to modify - * @private - */ - function onSelectChange(value, property) { - if (property === 'key') { - self.generateValueInput(value); - } - - self.eventEmitter.emit('change', { - value: value, - property: property, - index: self.index - }); - } - - /** - * An input event handler for this item's value field. Invokes any change - * callbacks associated with this item - * @param {Event} event The input event that initiated this callback - * @private - */ - function onValueInput(event) { - const elem = event.target; - const value = (isNaN(elem.valueAsNumber) ? elem.value : elem.valueAsNumber); - - if (elem.tagName.toUpperCase() === 'INPUT') { - self.eventEmitter.emit('change', { - value: value, - property: 'value', - index: self.index - }); - } - } - - this.listenTo(this.deleteButton, 'click', this.remove); - this.listenTo(this.duplicateButton, 'click', this.duplicate); - - this.selects.object = new ObjectSelect(this.config, this.conditionManager); - this.selects.key = new KeySelect( - this.config, - this.selects.object, - this.conditionManager, - function (value) { - onSelectChange(value, 'key'); - }); - - this.selects.object.on('change', function (value) { - onSelectChange(value, 'object'); - }); - - Object.values(this.selects).forEach(function (select) { - self.domElement.querySelector('.t-configuration').append(select.getDOM()); - }); - this.listenTo(this.domElement, 'input', onValueInput); + self.eventEmitter.emit('change', { + value: value, + property: property, + index: self.index + }); } /** - * Gets the DOM associated with this element's view - * @return {Element} + * An input event handler for this item's value field. Invokes any change + * callbacks associated with this item + * @param {Event} event The input event that initiated this callback + * @private */ - TestDataItem.prototype.getDOM = function (container) { - return this.domElement; - }; + function onValueInput(event) { + const elem = event.target; + const value = isNaN(elem.valueAsNumber) ? elem.value : elem.valueAsNumber; - /** - * Register a callback with this item: supported callbacks are remove, change, - * and duplicate - * @param {string} event The key for the event to listen to - * @param {function} callback The function that this rule will envoke on this event - * @param {Object} context A reference to a scope to use as the context for - * context for the callback function - */ - TestDataItem.prototype.on = function (event, callback, context) { - if (this.supportedCallbacks.includes(event)) { - this.eventEmitter.on(event, callback, context || this); - } - }; - - /** - * Implement "off" to complete event emitter interface. - */ - TestDataItem.prototype.off = function (event, callback, context) { - this.eventEmitter.off(event, callback, context); - }; - - /** - * Hide the appropriate inputs when this is the only item - */ - TestDataItem.prototype.hideButtons = function () { - this.deleteButton.style.display = 'none'; - }; - - /** - * Remove this item from the configuration. Invokes any registered - * remove callbacks - */ - TestDataItem.prototype.remove = function () { - const self = this; - this.eventEmitter.emit('remove', self.index); - this.stopListening(); - - Object.values(this.selects).forEach(function (select) { - select.destroy(); + if (elem.tagName.toUpperCase() === 'INPUT') { + self.eventEmitter.emit('change', { + value: value, + property: 'value', + index: self.index }); - }; + } + } - /** - * Makes a deep clone of this item's configuration, and invokes any registered - * duplicate callbacks with the cloned configuration as an argument - */ - TestDataItem.prototype.duplicate = function () { - const sourceItem = JSON.parse(JSON.stringify(this.config)); - const self = this; + this.listenTo(this.deleteButton, 'click', this.remove); + this.listenTo(this.duplicateButton, 'click', this.duplicate); - this.eventEmitter.emit('duplicate', { - sourceItem: sourceItem, - index: self.index - }); - }; + this.selects.object = new ObjectSelect(this.config, this.conditionManager); + this.selects.key = new KeySelect( + this.config, + this.selects.object, + this.conditionManager, + function (value) { + onSelectChange(value, 'key'); + } + ); - /** - * When a telemetry property key is selected, create the appropriate value input - * and add it to the view - * @param {string} key The key of currently selected telemetry property - */ - TestDataItem.prototype.generateValueInput = function (key) { - const evaluator = this.conditionManager.getEvaluator(); - const inputArea = this.domElement.querySelector('.t-value-inputs'); - const dataType = this.conditionManager.getTelemetryPropertyType(this.config.object, key); - const inputType = evaluator.getInputTypeById(dataType); + this.selects.object.on('change', function (value) { + onSelectChange(value, 'object'); + }); - inputArea.innerHTML = ''; - if (inputType) { - if (!this.config.value) { - this.config.value = (inputType === 'number' ? 0 : ''); - } + Object.values(this.selects).forEach(function (select) { + self.domElement.querySelector('.t-configuration').append(select.getDOM()); + }); + this.listenTo(this.domElement, 'input', onValueInput); + } - const newInput = document.createElement("input"); - newInput.type = `${inputType}`; - newInput.value = `${this.config.value}`; + /** + * Gets the DOM associated with this element's view + * @return {Element} + */ + TestDataItem.prototype.getDOM = function (container) { + return this.domElement; + }; - this.valueInput = newInput; - inputArea.append(this.valueInput); - } - }; + /** + * Register a callback with this item: supported callbacks are remove, change, + * and duplicate + * @param {string} event The key for the event to listen to + * @param {function} callback The function that this rule will envoke on this event + * @param {Object} context A reference to a scope to use as the context for + * context for the callback function + */ + TestDataItem.prototype.on = function (event, callback, context) { + if (this.supportedCallbacks.includes(event)) { + this.eventEmitter.on(event, callback, context || this); + } + }; - return TestDataItem; + /** + * Implement "off" to complete event emitter interface. + */ + TestDataItem.prototype.off = function (event, callback, context) { + this.eventEmitter.off(event, callback, context); + }; + + /** + * Hide the appropriate inputs when this is the only item + */ + TestDataItem.prototype.hideButtons = function () { + this.deleteButton.style.display = 'none'; + }; + + /** + * Remove this item from the configuration. Invokes any registered + * remove callbacks + */ + TestDataItem.prototype.remove = function () { + const self = this; + this.eventEmitter.emit('remove', self.index); + this.stopListening(); + + Object.values(this.selects).forEach(function (select) { + select.destroy(); + }); + }; + + /** + * Makes a deep clone of this item's configuration, and invokes any registered + * duplicate callbacks with the cloned configuration as an argument + */ + TestDataItem.prototype.duplicate = function () { + const sourceItem = JSON.parse(JSON.stringify(this.config)); + const self = this; + + this.eventEmitter.emit('duplicate', { + sourceItem: sourceItem, + index: self.index + }); + }; + + /** + * When a telemetry property key is selected, create the appropriate value input + * and add it to the view + * @param {string} key The key of currently selected telemetry property + */ + TestDataItem.prototype.generateValueInput = function (key) { + const evaluator = this.conditionManager.getEvaluator(); + const inputArea = this.domElement.querySelector('.t-value-inputs'); + const dataType = this.conditionManager.getTelemetryPropertyType(this.config.object, key); + const inputType = evaluator.getInputTypeById(dataType); + + inputArea.innerHTML = ''; + if (inputType) { + if (!this.config.value) { + this.config.value = inputType === 'number' ? 0 : ''; + } + + const newInput = document.createElement('input'); + newInput.type = `${inputType}`; + newInput.value = `${this.config.value}`; + + this.valueInput = newInput; + inputArea.append(this.valueInput); + } + }; + + return TestDataItem; }); diff --git a/src/plugins/summaryWidget/src/TestDataManager.js b/src/plugins/summaryWidget/src/TestDataManager.js index 70240453d6..8566117feb 100644 --- a/src/plugins/summaryWidget/src/TestDataManager.js +++ b/src/plugins/summaryWidget/src/TestDataManager.js @@ -1,208 +1,201 @@ define([ - './eventHelpers', - '../res/testDataTemplate.html', - './TestDataItem', - '../../../utils/template/templateHelpers', - 'lodash' -], function ( - eventHelpers, - testDataTemplate, - TestDataItem, - templateHelpers, - _ -) { + './eventHelpers', + '../res/testDataTemplate.html', + './TestDataItem', + '../../../utils/template/templateHelpers', + 'lodash' +], function (eventHelpers, testDataTemplate, TestDataItem, templateHelpers, _) { + /** + * Controls the input and usage of test data in the summary widget. + * @constructor + * @param {Object} domainObject The summary widget domain object + * @param {ConditionManager} conditionManager A conditionManager instance + * @param {MCT} openmct and MCT instance + */ + function TestDataManager(domainObject, conditionManager, openmct) { + eventHelpers.extend(this); + const self = this; + + this.domainObject = domainObject; + this.manager = conditionManager; + this.openmct = openmct; + + this.evaluator = this.manager.getEvaluator(); + this.domElement = templateHelpers.convertTemplateToHTML(testDataTemplate)[0]; + this.config = this.domainObject.configuration.testDataConfig; + this.testCache = {}; + + 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'); /** - * Controls the input and usage of test data in the summary widget. - * @constructor - * @param {Object} domainObject The summary widget domain object - * @param {ConditionManager} conditionManager A conditionManager instance - * @param {MCT} openmct and MCT instance + * Toggles whether the associated {ConditionEvaluator} uses the actual + * subscription cache or the test data cache + * @param {Event} event The change event that triggered this callback + * @private */ - function TestDataManager(domainObject, conditionManager, openmct) { - eventHelpers.extend(this); - const self = this; - - this.domainObject = domainObject; - this.manager = conditionManager; - this.openmct = openmct; - - this.evaluator = this.manager.getEvaluator(); - this.domElement = templateHelpers.convertTemplateToHTML(testDataTemplate)[0]; - this.config = this.domainObject.configuration.testDataConfig; - this.testCache = {}; - - 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 - * subscription cache or the test data cache - * @param {Event} event The change event that triggered this callback - * @private - */ - function toggleTestData(event) { - const elem = event.target; - self.evaluator.useTestData(elem.checked); - self.updateTestCache(); - } - - this.listenTo(this.addItemButton, 'click', function () { - self.initItem(); - }); - this.listenTo(this.testDataInput, 'change', toggleTestData); - - this.evaluator.setTestDataCache(this.testCache); - this.evaluator.useTestData(false); - - this.refreshItems(); + function toggleTestData(event) { + const elem = event.target; + self.evaluator.useTestData(elem.checked); + self.updateTestCache(); } - /** - * Get the DOM element representing this test data manager in the view - */ - TestDataManager.prototype.getDOM = function () { - return this.domElement; + this.listenTo(this.addItemButton, 'click', function () { + self.initItem(); + }); + this.listenTo(this.testDataInput, 'change', toggleTestData); + + this.evaluator.setTestDataCache(this.testCache); + this.evaluator.useTestData(false); + + this.refreshItems(); + } + + /** + * Get the DOM element representing this test data manager in the view + */ + TestDataManager.prototype.getDOM = function () { + return this.domElement; + }; + + /** + * Initialze a new test data item, either from a source configuration, or with + * the default empty configuration + * @param {Object} [config] An object with sourceItem and index fields to instantiate + * this rule from, optional + */ + TestDataManager.prototype.initItem = function (config) { + const sourceIndex = config && config.index; + const defaultItem = { + object: '', + key: '', + value: '' }; + let newItem; - /** - * Initialze a new test data item, either from a source configuration, or with - * the default empty configuration - * @param {Object} [config] An object with sourceItem and index fields to instantiate - * this rule from, optional - */ - TestDataManager.prototype.initItem = function (config) { - const sourceIndex = config && config.index; - const defaultItem = { - object: '', - key: '', - value: '' - }; - let newItem; + newItem = config !== undefined ? config.sourceItem : defaultItem; + if (sourceIndex !== undefined) { + this.config.splice(sourceIndex + 1, 0, newItem); + } else { + this.config.push(newItem); + } - newItem = (config !== undefined ? config.sourceItem : defaultItem); - if (sourceIndex !== undefined) { - this.config.splice(sourceIndex + 1, 0, newItem); - } else { - this.config.push(newItem); - } + this.updateDomainObject(); + this.refreshItems(); + }; - this.updateDomainObject(); - this.refreshItems(); - }; + /** + * Remove an item from this TestDataManager at the given index + * @param {number} removeIndex The index of the item to remove + */ + TestDataManager.prototype.removeItem = function (removeIndex) { + _.remove(this.config, function (item, index) { + return index === removeIndex; + }); + this.updateDomainObject(); + this.refreshItems(); + }; - /** - * Remove an item from this TestDataManager at the given index - * @param {number} removeIndex The index of the item to remove - */ - TestDataManager.prototype.removeItem = function (removeIndex) { - _.remove(this.config, function (item, index) { - return index === removeIndex; - }); - this.updateDomainObject(); - this.refreshItems(); - }; + /** + * Change event handler for the test data items which compose this + * test data generateor + * @param {Object} event An object representing this event, with value, property, + * and index fields + */ + TestDataManager.prototype.onItemChange = function (event) { + this.config[event.index][event.property] = event.value; + this.updateDomainObject(); + this.updateTestCache(); + }; - /** - * Change event handler for the test data items which compose this - * test data generateor - * @param {Object} event An object representing this event, with value, property, - * and index fields - */ - TestDataManager.prototype.onItemChange = function (event) { - this.config[event.index][event.property] = event.value; - this.updateDomainObject(); - this.updateTestCache(); - }; + /** + * Builds the test cache from the current item configuration, and passes + * the new test cache to the associated {ConditionEvaluator} instance + */ + TestDataManager.prototype.updateTestCache = function () { + this.generateTestCache(); + this.evaluator.setTestDataCache(this.testCache); + this.manager.triggerTelemetryCallback(); + }; - /** - * Builds the test cache from the current item configuration, and passes - * the new test cache to the associated {ConditionEvaluator} instance - */ - TestDataManager.prototype.updateTestCache = function () { - this.generateTestCache(); - this.evaluator.setTestDataCache(this.testCache); - this.manager.triggerTelemetryCallback(); - }; + /** + * Intantiate {TestDataItem} objects from the current configuration, and + * update the view accordingly + */ + TestDataManager.prototype.refreshItems = function () { + const self = this; + if (this.items) { + this.items.forEach(function (item) { + this.stopListening(item); + }, this); + } - /** - * Intantiate {TestDataItem} objects from the current configuration, and - * update the view accordingly - */ - TestDataManager.prototype.refreshItems = function () { - const self = this; - if (this.items) { - this.items.forEach(function (item) { - this.stopListening(item); - }, this); - } + self.items = []; - self.items = []; + this.domElement.querySelectorAll('.t-test-data-item').forEach((item) => { + item.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); + self.listenTo(newItem, 'remove', self.removeItem, self); + self.listenTo(newItem, 'duplicate', self.initItem, self); + self.listenTo(newItem, 'change', self.onItemChange, self); + self.items.push(newItem); + }); - this.config.forEach(function (item, index) { - const newItem = new TestDataItem(item, index, self.manager); - self.listenTo(newItem, 'remove', self.removeItem, self); - self.listenTo(newItem, 'duplicate', self.initItem, self); - self.listenTo(newItem, 'change', self.onItemChange, self); - self.items.push(newItem); - }); + self.items.forEach(function (item) { + self.itemArea.prepend(item.getDOM()); + }); - self.items.forEach(function (item) { - self.itemArea.prepend(item.getDOM()); - }); + if (self.items.length === 1) { + self.items[0].hideButtons(); + } - if (self.items.length === 1) { - self.items[0].hideButtons(); - } + this.updateTestCache(); + }; - this.updateTestCache(); - }; + /** + * Builds a test data cache in the format of a telemetry subscription cache + * as expected by a {ConditionEvaluator} + */ + TestDataManager.prototype.generateTestCache = function () { + let testCache = this.testCache; + const manager = this.manager; + const compositionObjs = manager.getComposition(); + let metadata; - /** - * Builds a test data cache in the format of a telemetry subscription cache - * as expected by a {ConditionEvaluator} - */ - TestDataManager.prototype.generateTestCache = function () { - let testCache = this.testCache; - const manager = this.manager; - const compositionObjs = manager.getComposition(); - let metadata; + testCache = {}; + Object.keys(compositionObjs).forEach(function (id) { + testCache[id] = {}; + metadata = manager.getTelemetryMetadata(id); + Object.keys(metadata).forEach(function (key) { + testCache[id][key] = ''; + }); + }); + this.config.forEach(function (item) { + if (testCache[item.object]) { + testCache[item.object][item.key] = item.value; + } + }); - testCache = {}; - Object.keys(compositionObjs).forEach(function (id) { - testCache[id] = {}; - metadata = manager.getTelemetryMetadata(id); - Object.keys(metadata).forEach(function (key) { - testCache[id][key] = ''; - }); - }); - this.config.forEach(function (item) { - if (testCache[item.object]) { - testCache[item.object][item.key] = item.value; - } - }); + this.testCache = testCache; + }; - this.testCache = testCache; - }; + /** + * Update the domain object configuration associated with this test data manager + */ + TestDataManager.prototype.updateDomainObject = function () { + this.openmct.objects.mutate(this.domainObject, 'configuration.testDataConfig', this.config); + }; - /** - * Update the domain object configuration associated with this test data manager - */ - TestDataManager.prototype.updateDomainObject = function () { - this.openmct.objects.mutate(this.domainObject, 'configuration.testDataConfig', this.config); - }; + TestDataManager.prototype.destroy = function () { + this.stopListening(); + this.items.forEach(function (item) { + item.remove(); + }); + }; - TestDataManager.prototype.destroy = function () { - this.stopListening(); - this.items.forEach(function (item) { - item.remove(); - }); - }; - - return TestDataManager; + return TestDataManager; }); diff --git a/src/plugins/summaryWidget/src/WidgetDnD.js b/src/plugins/summaryWidget/src/WidgetDnD.js index 90cd3b6971..15c79ed66d 100644 --- a/src/plugins/summaryWidget/src/WidgetDnD.js +++ b/src/plugins/summaryWidget/src/WidgetDnD.js @@ -1,170 +1,165 @@ define([ - '../res/ruleImageTemplate.html', - 'EventEmitter', - '../../../utils/template/templateHelpers' -], function ( - ruleImageTemplate, - EventEmitter, - templateHelpers -) { + '../res/ruleImageTemplate.html', + 'EventEmitter', + '../../../utils/template/templateHelpers' +], function (ruleImageTemplate, EventEmitter, templateHelpers) { + /** + * Manages the Sortable List interface for reordering rules by drag and drop + * @param {Element} container The DOM element that contains this Summary Widget's view + * @param {string[]} ruleOrder An array of rule IDs representing the current rule order + * @param {Object} rulesById An object mapping rule IDs to rule configurations + */ + function WidgetDnD(container, ruleOrder, rulesById) { + this.container = container; + this.ruleOrder = ruleOrder; + this.rulesById = rulesById; - /** - * Manages the Sortable List interface for reordering rules by drag and drop - * @param {Element} container The DOM element that contains this Summary Widget's view - * @param {string[]} ruleOrder An array of rule IDs representing the current rule order - * @param {Object} rulesById An object mapping rule IDs to rule configurations - */ - function WidgetDnD(container, ruleOrder, rulesById) { - this.container = container; - this.ruleOrder = ruleOrder; - this.rulesById = rulesById; + this.imageContainer = templateHelpers.convertTemplateToHTML(ruleImageTemplate)[0]; + this.image = this.imageContainer.querySelector('.t-drag-rule-image'); + this.draggingId = ''; + this.draggingRulePrevious = ''; + this.eventEmitter = new EventEmitter(); + this.supportedCallbacks = ['drop']; - this.imageContainer = templateHelpers.convertTemplateToHTML(ruleImageTemplate)[0]; - this.image = this.imageContainer.querySelector('.t-drag-rule-image'); - this.draggingId = ''; - this.draggingRulePrevious = ''; - this.eventEmitter = new EventEmitter(); - this.supportedCallbacks = ['drop']; + this.drag = this.drag.bind(this); + this.drop = this.drop.bind(this); - this.drag = this.drag.bind(this); - this.drop = this.drop.bind(this); + this.container.addEventListener('mousemove', this.drag); + document.addEventListener('mouseup', this.drop); + this.container.parentNode.insertBefore(this.imageContainer, this.container); + this.imageContainer.style.display = 'none'; + } - 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.removeEventListener('mousemove', this.drag); + document.removeEventListener('mouseup', this.drop); + }; + + /** + * Register a callback with this WidgetDnD: supported callback is drop + * @param {string} event The key for the event to listen to + * @param {function} callback The function that this rule will envoke on this event + * @param {Object} context A reference to a scope to use as the context for + * context for the callback function + */ + WidgetDnD.prototype.on = function (event, callback, context) { + if (this.supportedCallbacks.includes(event)) { + this.eventEmitter.on(event, callback, context || this); } + }; - /** - * Remove event listeners registered to elements external to the widget - */ - WidgetDnD.prototype.destroy = function () { - this.container.removeEventListener('mousemove', this.drag); - document.removeEventListener('mouseup', this.drop); - }; + /** + * Sets the image for the dragged element to the given DOM element + * @param {Element} image The HTML element to set as the drap image + */ + WidgetDnD.prototype.setDragImage = function (image) { + this.image.html(image); + }; - /** - * Register a callback with this WidgetDnD: supported callback is drop - * @param {string} event The key for the event to listen to - * @param {function} callback The function that this rule will envoke on this event - * @param {Object} context A reference to a scope to use as the context for - * context for the callback function - */ - WidgetDnD.prototype.on = function (event, callback, context) { - if (this.supportedCallbacks.includes(event)) { - this.eventEmitter.on(event, callback, context || this); - } - }; - - /** - * Sets the image for the dragged element to the given DOM element - * @param {Element} image The HTML element to set as the drap image - */ - WidgetDnD.prototype.setDragImage = function (image) { - this.image.html(image); - }; - - /** + /** * Calculate where this rule has been dragged relative to the other rules * @param {Event} event The mousemove or mouseup event that triggered this event handler * @return {string} The ID of the rule whose drag indicator should be displayed */ - WidgetDnD.prototype.getDropLocation = function (event) { - const ruleOrder = this.ruleOrder; - const rulesById = this.rulesById; - const draggingId = this.draggingId; - let offset; - let y; - let height; - const dropY = event.pageY; - let target = ''; + WidgetDnD.prototype.getDropLocation = function (event) { + const ruleOrder = this.ruleOrder; + const rulesById = this.rulesById; + const draggingId = this.draggingId; + let offset; + let y; + let height; + const dropY = event.pageY; + let target = ''; - ruleOrder.forEach(function (ruleId, index) { - const ruleDOM = rulesById[ruleId].getDOM(); - offset = window.innerWidth - (ruleDOM.offsetLeft + ruleDOM.offsetWidth); - y = offset.top; - height = offset.height; - if (index === 0) { - if (dropY < y + 7 * height / 3) { - target = ruleId; - } - } else if (index === ruleOrder.length - 1 && ruleId !== draggingId) { - if (y + height / 3 < dropY) { - target = ruleId; - } - } else { - if (y + height / 3 < dropY && dropY < y + 7 * height / 3) { - target = ruleId; - } - } - }); + ruleOrder.forEach(function (ruleId, index) { + const ruleDOM = rulesById[ruleId].getDOM(); + offset = window.innerWidth - (ruleDOM.offsetLeft + ruleDOM.offsetWidth); + y = offset.top; + height = offset.height; + if (index === 0) { + if (dropY < y + (7 * height) / 3) { + target = ruleId; + } + } else if (index === ruleOrder.length - 1 && ruleId !== draggingId) { + if (y + height / 3 < dropY) { + target = ruleId; + } + } else { + if (y + height / 3 < dropY && dropY < y + (7 * height) / 3) { + target = ruleId; + } + } + }); - return target; - }; + return target; + }; - /** - * Called by a {Rule} instance that initiates a drag gesture - * @param {string} ruleId The identifier of the rule which is being dragged - */ - WidgetDnD.prototype.dragStart = function (ruleId) { - const ruleOrder = this.ruleOrder; - this.draggingId = ruleId; - this.draggingRulePrevious = ruleOrder[ruleOrder.indexOf(ruleId) - 1]; + /** + * Called by a {Rule} instance that initiates a drag gesture + * @param {string} ruleId The identifier of the rule which is being dragged + */ + WidgetDnD.prototype.dragStart = function (ruleId) { + const ruleOrder = this.ruleOrder; + this.draggingId = ruleId; + this.draggingRulePrevious = ruleOrder[ruleOrder.indexOf(ruleId) - 1]; + this.rulesById[this.draggingRulePrevious].showDragIndicator(); + this.imageContainer.show(); + this.imageContainer.offset({ + top: event.pageY - this.image.height() / 2, + left: event.pageX - this.image.querySelector('.t-grippy').style.width + }); + }; + + /** + * An event handler for a mousemove event, once a rule has begun a drag gesture + * @param {Event} event The mousemove event that triggered this callback + */ + WidgetDnD.prototype.drag = function (event) { + let dragTarget; + if (this.draggingId && this.draggingId !== '') { + event.preventDefault(); + dragTarget = this.getDropLocation(event); + this.imageContainer.offset({ + top: event.pageY - this.image.height() / 2, + left: event.pageX - this.image.querySelector('.t-grippy').style.width + }); + if (this.rulesById[dragTarget]) { + this.rulesById[dragTarget].showDragIndicator(); + } else { this.rulesById[this.draggingRulePrevious].showDragIndicator(); - this.imageContainer.show(); - this.imageContainer.offset({ - top: event.pageY - this.image.height() / 2, - left: event.pageX - this.image.querySelector('.t-grippy').style.width - }); - }; + } + } + }; - /** - * An event handler for a mousemove event, once a rule has begun a drag gesture - * @param {Event} event The mousemove event that triggered this callback - */ - WidgetDnD.prototype.drag = function (event) { - let dragTarget; - if (this.draggingId && this.draggingId !== '') { - event.preventDefault(); - dragTarget = this.getDropLocation(event); - this.imageContainer.offset({ - top: event.pageY - this.image.height() / 2, - left: event.pageX - this.image.querySelector('.t-grippy').style.width - }); - if (this.rulesById[dragTarget]) { - this.rulesById[dragTarget].showDragIndicator(); - } else { - this.rulesById[this.draggingRulePrevious].showDragIndicator(); - } - } - }; + /** + * Handles the mouseup event that corresponds to the user dropping the rule + * in its final location. Invokes any registered drop callbacks with the dragged + * rule's ID and the ID of the target rule that the dragged rule should be + * inserted after + * @param {Event} event The mouseup event that triggered this callback + */ + WidgetDnD.prototype.drop = function (event) { + let dropTarget = this.getDropLocation(event); + const draggingId = this.draggingId; - /** - * Handles the mouseup event that corresponds to the user dropping the rule - * in its final location. Invokes any registered drop callbacks with the dragged - * rule's ID and the ID of the target rule that the dragged rule should be - * inserted after - * @param {Event} event The mouseup event that triggered this callback - */ - WidgetDnD.prototype.drop = function (event) { - let dropTarget = this.getDropLocation(event); - const draggingId = this.draggingId; + if (this.draggingId && this.draggingId !== '') { + if (!this.rulesById[dropTarget]) { + dropTarget = this.draggingId; + } - if (this.draggingId && this.draggingId !== '') { - if (!this.rulesById[dropTarget]) { - dropTarget = this.draggingId; - } + this.eventEmitter.emit('drop', { + draggingId: draggingId, + dropTarget: dropTarget + }); + this.draggingId = ''; + this.draggingRulePrevious = ''; + this.imageContainer.hide(); + } + }; - this.eventEmitter.emit('drop', { - draggingId: draggingId, - dropTarget: dropTarget - }); - this.draggingId = ''; - this.draggingRulePrevious = ''; - this.imageContainer.hide(); - } - }; - - return WidgetDnD; + return WidgetDnD; }); diff --git a/src/plugins/summaryWidget/src/eventHelpers.js b/src/plugins/summaryWidget/src/eventHelpers.js index 337db1bc0c..367072b9f6 100644 --- a/src/plugins/summaryWidget/src/eventHelpers.js +++ b/src/plugins/summaryWidget/src/eventHelpers.js @@ -21,78 +21,79 @@ *****************************************************************************/ define([], function () { - const helperFunctions = { - listenTo: function (object, event, callback, context) { - if (!this._listeningTo) { - this._listeningTo = []; - } + const helperFunctions = { + listenTo: function (object, event, callback, context) { + if (!this._listeningTo) { + this._listeningTo = []; + } - const listener = { - object: object, - event: event, - callback: callback, - context: context, - _cb: context ? callback.bind(context) : callback - }; - if (object.$watch && event.indexOf('change:') === 0) { - const scopePath = event.replace('change:', ''); - listener.unlisten = object.$watch(scopePath, listener._cb, true); - } else if (object.$on) { - listener.unlisten = object.$on(event, listener._cb); - } else if (object.addEventListener) { - object.addEventListener(event, listener._cb); - } else { - object.on(event, listener._cb); - } + const listener = { + object: object, + event: event, + callback: callback, + context: context, + _cb: context ? callback.bind(context) : callback + }; + if (object.$watch && event.indexOf('change:') === 0) { + const scopePath = event.replace('change:', ''); + listener.unlisten = object.$watch(scopePath, listener._cb, true); + } else if (object.$on) { + listener.unlisten = object.$on(event, listener._cb); + } else if (object.addEventListener) { + object.addEventListener(event, listener._cb); + } else { + object.on(event, listener._cb); + } - this._listeningTo.push(listener); - }, + this._listeningTo.push(listener); + }, - stopListening: function (object, event, callback, context) { - if (!this._listeningTo) { - this._listeningTo = []; - } + stopListening: function (object, event, callback, context) { + if (!this._listeningTo) { + this._listeningTo = []; + } - this._listeningTo.filter(function (listener) { - if (object && object !== listener.object) { - return false; - } + this._listeningTo + .filter(function (listener) { + if (object && object !== listener.object) { + return false; + } - if (event && event !== listener.event) { - return false; - } + if (event && event !== listener.event) { + return false; + } - if (callback && callback !== listener.callback) { - return false; - } + if (callback && callback !== listener.callback) { + return false; + } - if (context && context !== listener.context) { - return false; - } + if (context && context !== listener.context) { + return false; + } - return true; - }) - .map(function (listener) { - if (listener.unlisten) { - listener.unlisten(); - } else if (listener.object.removeEventListener) { - listener.object.removeEventListener(listener.event, listener._cb); - } else { - listener.object.off(listener.event, listener._cb); - } + return true; + }) + .map(function (listener) { + if (listener.unlisten) { + listener.unlisten(); + } else if (listener.object.removeEventListener) { + listener.object.removeEventListener(listener.event, listener._cb); + } else { + listener.object.off(listener.event, listener._cb); + } - return listener; - }) - .forEach(function (listener) { - this._listeningTo.splice(this._listeningTo.indexOf(listener), 1); - }, this); - }, + return listener; + }) + .forEach(function (listener) { + this._listeningTo.splice(this._listeningTo.indexOf(listener), 1); + }, this); + }, - extend: function (object) { - object.listenTo = helperFunctions.listenTo; - object.stopListening = helperFunctions.stopListening; - } - }; + extend: function (object) { + object.listenTo = helperFunctions.listenTo; + object.stopListening = helperFunctions.stopListening; + } + }; - return helperFunctions; + return helperFunctions; }); diff --git a/src/plugins/summaryWidget/src/input/ColorPalette.js b/src/plugins/summaryWidget/src/input/ColorPalette.js index 2319f98304..82ed0b3981 100644 --- a/src/plugins/summaryWidget/src/input/ColorPalette.js +++ b/src/plugins/summaryWidget/src/input/ColorPalette.js @@ -1,62 +1,128 @@ -define([ - './Palette' -], -function ( - Palette -) { +define(['./Palette'], function (Palette) { + //The colors that will be used to instantiate this palette if none are provided + const DEFAULT_COLORS = [ + '#000000', + '#434343', + '#666666', + '#999999', + '#b7b7b7', + '#cccccc', + '#d9d9d9', + '#efefef', + '#f3f3f3', + '#ffffff', + '#980000', + '#ff0000', + '#ff9900', + '#ffff00', + '#00ff00', + '#00ffff', + '#4a86e8', + '#0000ff', + '#9900ff', + '#ff00ff', + '#e6b8af', + '#f4cccc', + '#fce5cd', + '#fff2cc', + '#d9ead3', + '#d0e0e3', + '#c9daf8', + '#cfe2f3', + '#d9d2e9', + '#ead1dc', + '#dd7e6b', + '#dd7e6b', + '#f9cb9c', + '#ffe599', + '#b6d7a8', + '#a2c4c9', + '#a4c2f4', + '#9fc5e8', + '#b4a7d6', + '#d5a6bd', + '#cc4125', + '#e06666', + '#f6b26b', + '#ffd966', + '#93c47d', + '#76a5af', + '#6d9eeb', + '#6fa8dc', + '#8e7cc3', + '#c27ba0', + '#a61c00', + '#cc0000', + '#e69138', + '#f1c232', + '#6aa84f', + '#45818e', + '#3c78d8', + '#3d85c6', + '#674ea7', + '#a64d79', + '#85200c', + '#990000', + '#b45f06', + '#bf9000', + '#38761d', + '#134f5c', + '#1155cc', + '#0b5394', + '#351c75', + '#741b47', + '#5b0f00', + '#660000', + '#783f04', + '#7f6000', + '#274e13', + '#0c343d', + '#1c4587', + '#073763', + '#20124d', + '#4c1130' + ]; - //The colors that will be used to instantiate this palette if none are provided - const DEFAULT_COLORS = [ - '#000000', '#434343', '#666666', '#999999', '#b7b7b7', '#cccccc', '#d9d9d9', '#efefef', '#f3f3f3', '#ffffff', - '#980000', '#ff0000', '#ff9900', '#ffff00', '#00ff00', '#00ffff', '#4a86e8', '#0000ff', '#9900ff', '#ff00ff', - '#e6b8af', '#f4cccc', '#fce5cd', '#fff2cc', '#d9ead3', '#d0e0e3', '#c9daf8', '#cfe2f3', '#d9d2e9', '#ead1dc', - '#dd7e6b', '#dd7e6b', '#f9cb9c', '#ffe599', '#b6d7a8', '#a2c4c9', '#a4c2f4', '#9fc5e8', '#b4a7d6', '#d5a6bd', - '#cc4125', '#e06666', '#f6b26b', '#ffd966', '#93c47d', '#76a5af', '#6d9eeb', '#6fa8dc', '#8e7cc3', '#c27ba0', - '#a61c00', '#cc0000', '#e69138', '#f1c232', '#6aa84f', '#45818e', '#3c78d8', '#3d85c6', '#674ea7', '#a64d79', - '#85200c', '#990000', '#b45f06', '#bf9000', '#38761d', '#134f5c', '#1155cc', '#0b5394', '#351c75', '#741b47', - '#5b0f00', '#660000', '#783f04', '#7f6000', '#274e13', '#0c343d', '#1c4587', '#073763', '#20124d', '#4c1130' - ]; + /** + * Instantiates a new Open MCT Color Palette input + * @constructor + * @param {string} cssClass The class name of the icon which should be applied + * to this palette + * @param {Element} container The view that contains this palette + * @param {string[]} colors (optional) A list of colors that should be used to instantiate this palette + */ + function ColorPalette(cssClass, container, colors) { + this.colors = colors || DEFAULT_COLORS; + this.palette = new Palette(cssClass, container, this.colors); + + this.palette.setNullOption('rgba(0,0,0,0)'); + + const domElement = this.palette.getDOM(); + const self = this; + + 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'); + + domElement.querySelectorAll('.c-palette__item').forEach((item) => { + // eslint-disable-next-line no-invalid-this + item.style.backgroundColor = item.dataset.item; + }); /** - * Instantiates a new Open MCT Color Palette input - * @constructor - * @param {string} cssClass The class name of the icon which should be applied - * to this palette - * @param {Element} container The view that contains this palette - * @param {string[]} colors (optional) A list of colors that should be used to instantiate this palette + * Update this palette's current selection indicator with the style + * of the currently selected item + * @private */ - function ColorPalette(cssClass, container, colors) { - this.colors = colors || DEFAULT_COLORS; - this.palette = new Palette(cssClass, container, this.colors); - - this.palette.setNullOption('rgba(0,0,0,0)'); - - const domElement = this.palette.getDOM(); - const self = this; - - 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'); - - domElement.querySelectorAll('.c-palette__item').forEach(item => { - // eslint-disable-next-line no-invalid-this - item.style.backgroundColor = item.dataset.item; - }); - - /** - * Update this palette's current selection indicator with the style - * of the currently selected item - * @private - */ - function updateSwatch() { - const color = self.palette.getCurrent(); - domElement.querySelector('.color-swatch').style.backgroundColor = color; - } - - this.palette.on('change', updateSwatch); - - return this.palette; + function updateSwatch() { + const color = self.palette.getCurrent(); + domElement.querySelector('.color-swatch').style.backgroundColor = color; } - return ColorPalette; + this.palette.on('change', updateSwatch); + + return this.palette; + } + + return ColorPalette; }); diff --git a/src/plugins/summaryWidget/src/input/IconPalette.js b/src/plugins/summaryWidget/src/input/IconPalette.js index 557cc4d958..4c2cf5e6ff 100644 --- a/src/plugins/summaryWidget/src/input/IconPalette.js +++ b/src/plugins/summaryWidget/src/input/IconPalette.js @@ -1,81 +1,77 @@ -define([ - './Palette' -], function ( - Palette -) { - //The icons that will be used to instantiate this palette if none are provided - const DEFAULT_ICONS = [ - 'icon-alert-rect', - 'icon-alert-triangle', - 'icon-arrow-down', - 'icon-arrow-left', - 'icon-arrow-right', - 'icon-arrow-double-up', - 'icon-arrow-tall-up', - 'icon-arrow-tall-down', - 'icon-arrow-double-down', - 'icon-arrow-up', - 'icon-asterisk', - 'icon-bell', - 'icon-check', - 'icon-eye-open', - 'icon-gear', - 'icon-hourglass', - 'icon-info', - 'icon-link', - 'icon-lock', - 'icon-people', - 'icon-person', - 'icon-plus', - 'icon-trash', - 'icon-x' - ]; +define(['./Palette'], function (Palette) { + //The icons that will be used to instantiate this palette if none are provided + const DEFAULT_ICONS = [ + 'icon-alert-rect', + 'icon-alert-triangle', + 'icon-arrow-down', + 'icon-arrow-left', + 'icon-arrow-right', + 'icon-arrow-double-up', + 'icon-arrow-tall-up', + 'icon-arrow-tall-down', + 'icon-arrow-double-down', + 'icon-arrow-up', + 'icon-asterisk', + 'icon-bell', + 'icon-check', + 'icon-eye-open', + 'icon-gear', + 'icon-hourglass', + 'icon-info', + 'icon-link', + 'icon-lock', + 'icon-people', + 'icon-person', + 'icon-plus', + 'icon-trash', + 'icon-x' + ]; + + /** + * Instantiates a new Open MCT Icon Palette input + * @constructor + * @param {string} cssClass The class name of the icon which should be applied + * to this palette + * @param {Element} container The view that contains this palette + * @param {string[]} icons (optional) A list of icons that should be used to instantiate this palette + */ + function IconPalette(cssClass, container, icons) { + this.icons = icons || DEFAULT_ICONS; + this.palette = new Palette(cssClass, container, this.icons); + + this.palette.setNullOption(''); + this.oldIcon = this.palette.current || ''; + + const domElement = this.palette.getDOM(); + const self = this; + + 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'); + + domElement.querySelectorAll('.c-palette-item').forEach((item) => { + // eslint-disable-next-line no-invalid-this + item.classList.add(item.dataset.item); + }); /** - * Instantiates a new Open MCT Icon Palette input - * @constructor - * @param {string} cssClass The class name of the icon which should be applied - * to this palette - * @param {Element} container The view that contains this palette - * @param {string[]} icons (optional) A list of icons that should be used to instantiate this palette + * Update this palette's current selection indicator with the style + * of the currently selected item + * @private */ - function IconPalette(cssClass, container, icons) { - this.icons = icons || DEFAULT_ICONS; - this.palette = new Palette(cssClass, container, this.icons); + function updateSwatch() { + if (self.oldIcon) { + domElement.querySelector('.icon-swatch').classList.remove(self.oldIcon); + } - this.palette.setNullOption(''); - this.oldIcon = this.palette.current || ''; - - const domElement = this.palette.getDOM(); - const self = this; - - 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'); - - domElement.querySelectorAll('.c-palette-item').forEach(item => { - // eslint-disable-next-line no-invalid-this - item.classList.add(item.dataset.item); - }); - - /** - * Update this palette's current selection indicator with the style - * of the currently selected item - * @private - */ - function updateSwatch() { - 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(); - } - - this.palette.on('change', updateSwatch); - - return this.palette; + domElement.querySelector('.icon-swatch').classList.add(self.palette.getCurrent()); + self.oldIcon = self.palette.getCurrent(); } - return IconPalette; + this.palette.on('change', updateSwatch); + + return this.palette; + } + + return IconPalette; }); diff --git a/src/plugins/summaryWidget/src/input/KeySelect.js b/src/plugins/summaryWidget/src/input/KeySelect.js index 7be2b8dbb3..42671027cc 100644 --- a/src/plugins/summaryWidget/src/input/KeySelect.js +++ b/src/plugins/summaryWidget/src/input/KeySelect.js @@ -1,99 +1,95 @@ -define([ - './Select' -], function ( - Select -) { +define(['./Select'], function (Select) { + /** + * Create a {Select} element whose composition is dynamically updated with + * the telemetry fields of a particular domain object + * @constructor + * @param {Object} config The current state of this select. Must have object + * and key fields + * @param {ObjectSelect} objectSelect The linked ObjectSelect instance to which + * this KeySelect should listen to for change + * events + * @param {ConditionManager} manager A ConditionManager instance from which + * to receive telemetry metadata + * @param {function} changeCallback A change event callback to register with this + * select on initialization + */ + const NULLVALUE = '- Select Field -'; - /** - * Create a {Select} element whose composition is dynamically updated with - * the telemetry fields of a particular domain object - * @constructor - * @param {Object} config The current state of this select. Must have object - * and key fields - * @param {ObjectSelect} objectSelect The linked ObjectSelect instance to which - * this KeySelect should listen to for change - * events - * @param {ConditionManager} manager A ConditionManager instance from which - * to receive telemetry metadata - * @param {function} changeCallback A change event callback to register with this - * select on initialization - */ - const NULLVALUE = '- Select Field -'; + function KeySelect(config, objectSelect, manager, changeCallback) { + const self = this; - function KeySelect(config, objectSelect, manager, changeCallback) { - const self = this; + this.config = config; + this.objectSelect = objectSelect; + this.manager = manager; - this.config = config; - this.objectSelect = objectSelect; - this.manager = manager; - - this.select = new Select(); - this.select.hide(); - this.select.addOption('', NULLVALUE); - if (changeCallback) { - this.select.on('change', changeCallback); - } - - /** - * Change event handler for the {ObjectSelect} to which this KeySelect instance - * is linked. Loads the new object's metadata and updates its select element's - * composition. - * @param {Object} key The key identifying the newly selected domain object - * @private - */ - function onObjectChange(key) { - const selected = self.manager.metadataLoadCompleted() ? self.select.getSelected() : self.config.key; - self.telemetryMetadata = self.manager.getTelemetryMetadata(key) || {}; - self.generateOptions(); - self.select.setSelected(selected); - } - - /** - * Event handler for the intial metadata load event from the associated - * ConditionManager. Retreives metadata from the manager and populates - * the select element. - * @private - */ - function onMetadataLoad() { - if (self.manager.getTelemetryMetadata(self.config.object)) { - self.telemetryMetadata = self.manager.getTelemetryMetadata(self.config.object); - self.generateOptions(); - } - - self.select.setSelected(self.config.key); - } - - if (self.manager.metadataLoadCompleted()) { - onMetadataLoad(); - } - - this.objectSelect.on('change', onObjectChange, this); - this.manager.on('metadata', onMetadataLoad); - - return this.select; + this.select = new Select(); + this.select.hide(); + this.select.addOption('', NULLVALUE); + if (changeCallback) { + this.select.on('change', changeCallback); } /** - * Populate this select with options based on its current composition + * Change event handler for the {ObjectSelect} to which this KeySelect instance + * is linked. Loads the new object's metadata and updates its select element's + * composition. + * @param {Object} key The key identifying the newly selected domain object + * @private */ - KeySelect.prototype.generateOptions = function () { - const items = Object.entries(this.telemetryMetadata).map(function (metaDatum) { - return [metaDatum[0], metaDatum[1].name]; - }); - items.splice(0, 0, ['', NULLVALUE]); - this.select.setOptions(items); + function onObjectChange(key) { + const selected = self.manager.metadataLoadCompleted() + ? self.select.getSelected() + : self.config.key; + self.telemetryMetadata = self.manager.getTelemetryMetadata(key) || {}; + self.generateOptions(); + self.select.setSelected(selected); + } - if (this.select.options.length < 2) { - this.select.hide(); - } else if (this.select.options.length > 1) { - this.select.show(); - } - }; + /** + * Event handler for the intial metadata load event from the associated + * ConditionManager. Retreives metadata from the manager and populates + * the select element. + * @private + */ + function onMetadataLoad() { + if (self.manager.getTelemetryMetadata(self.config.object)) { + self.telemetryMetadata = self.manager.getTelemetryMetadata(self.config.object); + self.generateOptions(); + } - KeySelect.prototype.destroy = function () { - this.objectSelect.destroy(); - }; + self.select.setSelected(self.config.key); + } - return KeySelect; + if (self.manager.metadataLoadCompleted()) { + onMetadataLoad(); + } + this.objectSelect.on('change', onObjectChange, this); + this.manager.on('metadata', onMetadataLoad); + + return this.select; + } + + /** + * Populate this select with options based on its current composition + */ + KeySelect.prototype.generateOptions = function () { + const items = Object.entries(this.telemetryMetadata).map(function (metaDatum) { + return [metaDatum[0], metaDatum[1].name]; + }); + items.splice(0, 0, ['', NULLVALUE]); + this.select.setOptions(items); + + if (this.select.options.length < 2) { + this.select.hide(); + } else if (this.select.options.length > 1) { + this.select.show(); + } + }; + + KeySelect.prototype.destroy = function () { + this.objectSelect.destroy(); + }; + + return KeySelect; }); diff --git a/src/plugins/summaryWidget/src/input/ObjectSelect.js b/src/plugins/summaryWidget/src/input/ObjectSelect.js index 4b2a8a20be..f3bac8b377 100644 --- a/src/plugins/summaryWidget/src/input/ObjectSelect.js +++ b/src/plugins/summaryWidget/src/input/ObjectSelect.js @@ -1,93 +1,86 @@ -define([ - './Select', - 'objectUtils' -], function ( - Select, - objectUtils -) { +define(['./Select', 'objectUtils'], function (Select, objectUtils) { + /** + * Create a {Select} element whose composition is dynamically updated with + * the current composition of the Summary Widget + * @constructor + * @param {Object} config The current state of this select. Must have an + * object field + * @param {ConditionManager} manager A ConditionManager instance from which + * to receive the current composition status + * @param {string[][]} baseOptions A set of [value, label] keyword pairs to + * display regardless of the composition state + */ + function ObjectSelect(config, manager, baseOptions) { + const self = this; + + this.config = config; + this.manager = manager; + + this.select = new Select(); + this.baseOptions = [['', '- Select Telemetry -']]; + if (baseOptions) { + this.baseOptions = this.baseOptions.concat(baseOptions); + } + + this.baseOptions.forEach(function (option) { + self.select.addOption(option[0], option[1]); + }); + + this.compositionObjs = this.manager.getComposition(); + self.generateOptions(); /** - * Create a {Select} element whose composition is dynamically updated with - * the current composition of the Summary Widget - * @constructor - * @param {Object} config The current state of this select. Must have an - * object field - * @param {ConditionManager} manager A ConditionManager instance from which - * to receive the current composition status - * @param {string[][]} baseOptions A set of [value, label] keyword pairs to - * display regardless of the composition state + * Add a new composition object to this select when a composition added + * is detected on the Summary Widget + * @param {Object} obj The newly added domain object + * @private */ - function ObjectSelect(config, manager, baseOptions) { - const self = this; - - this.config = config; - this.manager = manager; - - this.select = new Select(); - this.baseOptions = [['', '- Select Telemetry -']]; - if (baseOptions) { - this.baseOptions = this.baseOptions.concat(baseOptions); - } - - this.baseOptions.forEach(function (option) { - self.select.addOption(option[0], option[1]); - }); - - this.compositionObjs = this.manager.getComposition(); - self.generateOptions(); - - /** - * Add a new composition object to this select when a composition added - * is detected on the Summary Widget - * @param {Object} obj The newly added domain object - * @private - */ - function onCompositionAdd(obj) { - self.select.addOption(objectUtils.makeKeyString(obj.identifier), obj.name); - } - - /** - * Refresh the composition of this select when a domain object is removed - * from the Summary Widget's composition - * @private - */ - function onCompositionRemove() { - const selected = self.select.getSelected(); - self.generateOptions(); - self.select.setSelected(selected); - } - - /** - * Defer setting the selected state on initial load until load is complete - * @private - */ - function onCompositionLoad() { - self.select.setSelected(self.config.object); - } - - this.manager.on('add', onCompositionAdd); - this.manager.on('remove', onCompositionRemove); - this.manager.on('load', onCompositionLoad); - - if (this.manager.loadCompleted()) { - onCompositionLoad(); - } - - return this.select; + function onCompositionAdd(obj) { + self.select.addOption(objectUtils.makeKeyString(obj.identifier), obj.name); } /** - * Populate this select with options based on its current composition + * Refresh the composition of this select when a domain object is removed + * from the Summary Widget's composition + * @private */ - ObjectSelect.prototype.generateOptions = function () { - const items = Object.values(this.compositionObjs).map(function (obj) { - return [objectUtils.makeKeyString(obj.identifier), obj.name]; - }); - this.baseOptions.forEach(function (option, index) { - items.splice(index, 0, option); - }); - this.select.setOptions(items); - }; + function onCompositionRemove() { + const selected = self.select.getSelected(); + self.generateOptions(); + self.select.setSelected(selected); + } - return ObjectSelect; + /** + * Defer setting the selected state on initial load until load is complete + * @private + */ + function onCompositionLoad() { + self.select.setSelected(self.config.object); + } + + this.manager.on('add', onCompositionAdd); + this.manager.on('remove', onCompositionRemove); + this.manager.on('load', onCompositionLoad); + + if (this.manager.loadCompleted()) { + onCompositionLoad(); + } + + return this.select; + } + + /** + * Populate this select with options based on its current composition + */ + ObjectSelect.prototype.generateOptions = function () { + const items = Object.values(this.compositionObjs).map(function (obj) { + return [objectUtils.makeKeyString(obj.identifier), obj.name]; + }); + this.baseOptions.forEach(function (option, index) { + items.splice(index, 0, option); + }); + this.select.setOptions(items); + }; + + return ObjectSelect; }); diff --git a/src/plugins/summaryWidget/src/input/OperationSelect.js b/src/plugins/summaryWidget/src/input/OperationSelect.js index 1e1f7a889b..822852f478 100644 --- a/src/plugins/summaryWidget/src/input/OperationSelect.js +++ b/src/plugins/summaryWidget/src/input/OperationSelect.js @@ -1,128 +1,120 @@ -define([ - './Select', - '../eventHelpers' -], function ( - Select, - eventHelpers -) { +define(['./Select', '../eventHelpers'], function (Select, eventHelpers) { + /** + * Create a {Select} element whose composition is dynamically updated with + * the operations applying to a particular telemetry property + * @constructor + * @param {Object} config The current state of this select. Must have object, + * key, and operation fields + * @param {KeySelect} keySelect The linked Key Select instance to which + * this OperationSelect should listen to for change + * events + * @param {ConditionManager} manager A ConditionManager instance from which + * to receive telemetry metadata + * @param {function} changeCallback A change event callback to register with this + * select on initialization + */ + const NULLVALUE = '- Select Comparison -'; - /** - * Create a {Select} element whose composition is dynamically updated with - * the operations applying to a particular telemetry property - * @constructor - * @param {Object} config The current state of this select. Must have object, - * key, and operation fields - * @param {KeySelect} keySelect The linked Key Select instance to which - * this OperationSelect should listen to for change - * events - * @param {ConditionManager} manager A ConditionManager instance from which - * to receive telemetry metadata - * @param {function} changeCallback A change event callback to register with this - * select on initialization - */ - const NULLVALUE = '- Select Comparison -'; + function OperationSelect(config, keySelect, manager, changeCallback) { + eventHelpers.extend(this); + const self = this; - function OperationSelect(config, keySelect, manager, changeCallback) { - eventHelpers.extend(this); - const self = this; + this.config = config; + this.keySelect = keySelect; + this.manager = manager; - this.config = config; - this.keySelect = keySelect; - this.manager = manager; + this.operationKeys = []; + this.evaluator = this.manager.getEvaluator(); + this.loadComplete = false; - this.operationKeys = []; - this.evaluator = this.manager.getEvaluator(); - this.loadComplete = false; - - this.select = new Select(); - this.select.hide(); - this.select.addOption('', NULLVALUE); - if (changeCallback) { - this.listenTo(this.select, 'change', changeCallback); - } - - /** - * Change event handler for the {KeySelect} to which this OperationSelect instance - * is linked. Loads the operations applicable to the given telemetry property and updates - * its select element's composition - * @param {Object} key The key identifying the newly selected property - * @private - */ - function onKeyChange(key) { - const selected = self.config.operation; - if (self.manager.metadataLoadCompleted()) { - self.loadOptions(key); - self.generateOptions(); - self.select.setSelected(selected); - } - } - - /** - * Event handler for the intial metadata load event from the associated - * ConditionManager. Retreives telemetry property types and updates the - * select - * @private - */ - function onMetadataLoad() { - if (self.manager.getTelemetryPropertyType(self.config.object, self.config.key)) { - self.loadOptions(self.config.key); - self.generateOptions(); - } - - self.select.setSelected(self.config.operation); - } - - this.keySelect.on('change', onKeyChange); - this.manager.on('metadata', onMetadataLoad); - - if (this.manager.metadataLoadCompleted()) { - onMetadataLoad(); - } - - return this.select; + this.select = new Select(); + this.select.hide(); + this.select.addOption('', NULLVALUE); + if (changeCallback) { + this.listenTo(this.select, 'change', changeCallback); } /** - * Populate this select with options based on its current composition + * Change event handler for the {KeySelect} to which this OperationSelect instance + * is linked. Loads the operations applicable to the given telemetry property and updates + * its select element's composition + * @param {Object} key The key identifying the newly selected property + * @private */ - OperationSelect.prototype.generateOptions = function () { - const self = this; - const items = this.operationKeys.map(function (operation) { - return [operation, self.evaluator.getOperationText(operation)]; - }); - items.splice(0, 0, ['', NULLVALUE]); - this.select.setOptions(items); - - if (this.select.options.length < 2) { - this.select.hide(); - } else { - this.select.show(); - } - }; + function onKeyChange(key) { + const selected = self.config.operation; + if (self.manager.metadataLoadCompleted()) { + self.loadOptions(key); + self.generateOptions(); + self.select.setSelected(selected); + } + } /** - * Retrieve the data type associated with a given telemetry property and - * the applicable operations from the {ConditionEvaluator} - * @param {string} key The telemetry property to load operations for + * Event handler for the intial metadata load event from the associated + * ConditionManager. Retreives telemetry property types and updates the + * select + * @private */ - OperationSelect.prototype.loadOptions = function (key) { - const self = this; - const operations = self.evaluator.getOperationKeys(); - let type; + function onMetadataLoad() { + if (self.manager.getTelemetryPropertyType(self.config.object, self.config.key)) { + self.loadOptions(self.config.key); + self.generateOptions(); + } - type = self.manager.getTelemetryPropertyType(self.config.object, key); + self.select.setSelected(self.config.operation); + } - if (type !== undefined) { - self.operationKeys = operations.filter(function (operation) { - return self.evaluator.operationAppliesTo(operation, type); - }); - } - }; + this.keySelect.on('change', onKeyChange); + this.manager.on('metadata', onMetadataLoad); - OperationSelect.prototype.destroy = function () { - this.stopListening(); - }; + if (this.manager.metadataLoadCompleted()) { + onMetadataLoad(); + } - return OperationSelect; + return this.select; + } + /** + * Populate this select with options based on its current composition + */ + OperationSelect.prototype.generateOptions = function () { + const self = this; + const items = this.operationKeys.map(function (operation) { + return [operation, self.evaluator.getOperationText(operation)]; + }); + items.splice(0, 0, ['', NULLVALUE]); + this.select.setOptions(items); + + if (this.select.options.length < 2) { + this.select.hide(); + } else { + this.select.show(); + } + }; + + /** + * Retrieve the data type associated with a given telemetry property and + * the applicable operations from the {ConditionEvaluator} + * @param {string} key The telemetry property to load operations for + */ + OperationSelect.prototype.loadOptions = function (key) { + const self = this; + const operations = self.evaluator.getOperationKeys(); + let type; + + type = self.manager.getTelemetryPropertyType(self.config.object, key); + + if (type !== undefined) { + self.operationKeys = operations.filter(function (operation) { + return self.evaluator.operationAppliesTo(operation, type); + }); + } + }; + + OperationSelect.prototype.destroy = function () { + this.stopListening(); + }; + + return OperationSelect; }); diff --git a/src/plugins/summaryWidget/src/input/Palette.js b/src/plugins/summaryWidget/src/input/Palette.js index 96df813de2..1515aacff4 100644 --- a/src/plugins/summaryWidget/src/input/Palette.js +++ b/src/plugins/summaryWidget/src/input/Palette.js @@ -1,188 +1,183 @@ define([ - '../eventHelpers', - '../../res/input/paletteTemplate.html', - '../../../../utils/template/templateHelpers', - 'EventEmitter' -], function ( - eventHelpers, - paletteTemplate, - templateHelpers, - EventEmitter -) { - /** - * Instantiates a new Open MCT Color Palette input - * @constructor - * @param {string} cssClass The class name of the icon which should be applied - * to this palette - * @param {Element} container The view that contains this palette - * @param {string[]} items A list of data items that will be associated with each - * palette item in the view; how this data is represented is - * up to the descendent class - */ - function Palette(cssClass, container, items) { - eventHelpers.extend(this); + '../eventHelpers', + '../../res/input/paletteTemplate.html', + '../../../../utils/template/templateHelpers', + 'EventEmitter' +], function (eventHelpers, paletteTemplate, templateHelpers, EventEmitter) { + /** + * Instantiates a new Open MCT Color Palette input + * @constructor + * @param {string} cssClass The class name of the icon which should be applied + * to this palette + * @param {Element} container The view that contains this palette + * @param {string[]} items A list of data items that will be associated with each + * palette item in the view; how this data is represented is + * up to the descendent class + */ + function Palette(cssClass, container, items) { + eventHelpers.extend(this); - const self = this; + const self = this; - this.cssClass = cssClass; - this.items = items; - this.container = container; + this.cssClass = cssClass; + this.items = items; + this.container = container; - this.domElement = templateHelpers.convertTemplateToHTML(paletteTemplate)[0]; + this.domElement = templateHelpers.convertTemplateToHTML(paletteTemplate)[0]; - this.itemElements = { - 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 = this.domElement.querySelector('.js-button'); - this.menu = this.domElement.querySelector('.c-menu'); + this.itemElements = { + 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 = this.domElement.querySelector('.js-button'); + this.menu = this.domElement.querySelector('.c-menu'); - this.hideMenu = this.hideMenu.bind(this); + this.hideMenu = this.hideMenu.bind(this); - if (this.cssClass) { - self.button.classList.add(this.cssClass); - } - - self.setNullOption(this.nullOption); - - self.items.forEach(function (item) { - const itemElement = `
    `; - const temp = document.createElement('div'); - temp.innerHTML = itemElement; - self.itemElements[item] = temp.firstChild; - self.domElement.querySelector('.c-palette__items').appendChild(temp.firstChild); - }); - - self.domElement.querySelector('.c-menu').style.display = 'none'; - - this.listenTo(window.document, 'click', this.hideMenu); - this.listenTo(self.domElement.querySelector('.js-button'), 'click', function (event) { - event.stopPropagation(); - self.container.querySelector('.c-menu').style.display = 'none'; - self.domElement.querySelector('.c-menu').style.display = ''; - }); - - /** - * Event handler for selection of an individual palette item. Sets the - * currently selected element to be the one associated with that item's data - * @param {Event} event the click event that initiated this callback - * @private - */ - function handleItemClick(event) { - const elem = event.currentTarget; - const item = elem.dataset.item; - self.set(item); - self.domElement.querySelector('.c-menu').style.display = 'none'; - } - - self.domElement.querySelectorAll('.c-palette__item').forEach(item => { - this.listenTo(item, 'click', handleItemClick); - }); + if (this.cssClass) { + self.button.classList.add(this.cssClass); } - /** - * Get the DOM element representing this palette in the view - */ - Palette.prototype.getDOM = function () { - return this.domElement; - }; + self.setNullOption(this.nullOption); + + self.items.forEach(function (item) { + const itemElement = `
    `; + const temp = document.createElement('div'); + temp.innerHTML = itemElement; + self.itemElements[item] = temp.firstChild; + self.domElement.querySelector('.c-palette__items').appendChild(temp.firstChild); + }); + + self.domElement.querySelector('.c-menu').style.display = 'none'; + + this.listenTo(window.document, 'click', this.hideMenu); + this.listenTo(self.domElement.querySelector('.js-button'), 'click', function (event) { + event.stopPropagation(); + self.container.querySelector('.c-menu').style.display = 'none'; + self.domElement.querySelector('.c-menu').style.display = ''; + }); /** - * Clean up any event listeners registered to DOM elements external to the widget + * Event handler for selection of an individual palette item. Sets the + * currently selected element to be the one associated with that item's data + * @param {Event} event the click event that initiated this callback + * @private */ - Palette.prototype.destroy = function () { - this.stopListening(); - }; + function handleItemClick(event) { + const elem = event.currentTarget; + const item = elem.dataset.item; + self.set(item); + self.domElement.querySelector('.c-menu').style.display = 'none'; + } - Palette.prototype.hideMenu = function () { - this.domElement.querySelector('.c-menu').style.display = 'none'; - }; + self.domElement.querySelectorAll('.c-palette__item').forEach((item) => { + this.listenTo(item, 'click', handleItemClick); + }); + } - /** - * Register a callback with this palette: supported callback is change - * @param {string} event The key for the event to listen to - * @param {function} callback The function that this rule will envoke on this event - * @param {Object} context A reference to a scope to use as the context for - * context for the callback function - */ - Palette.prototype.on = function (event, callback, context) { - if (this.supportedCallbacks.includes(event)) { - this.eventEmitter.on(event, callback, context || this); - } else { - throw new Error('Unsupported event type: ' + event); - } - }; + /** + * Get the DOM element representing this palette in the view + */ + Palette.prototype.getDOM = function () { + return this.domElement; + }; - /** - * Get the currently selected value of this palette - * @return {string} The selected value - */ - Palette.prototype.getCurrent = function () { - return this.value; - }; + /** + * Clean up any event listeners registered to DOM elements external to the widget + */ + Palette.prototype.destroy = function () { + this.stopListening(); + }; - /** - * Set the selected value of this palette; if the item doesn't exist in the - * palette's data model, the selected value will not change. Invokes any - * change callbacks associated with this palette. - * @param {string} item The key of the item to set as selected - */ - Palette.prototype.set = function (item) { - const self = this; - if (this.items.includes(item) || item === this.nullOption) { - this.value = item; - if (item === this.nullOption) { - this.updateSelected('nullOption'); - } else { - this.updateSelected(item); - } - } + Palette.prototype.hideMenu = function () { + this.domElement.querySelector('.c-menu').style.display = 'none'; + }; - this.eventEmitter.emit('change', self.value); - }; + /** + * Register a callback with this palette: supported callback is change + * @param {string} event The key for the event to listen to + * @param {function} callback The function that this rule will envoke on this event + * @param {Object} context A reference to a scope to use as the context for + * context for the callback function + */ + Palette.prototype.on = function (event, callback, context) { + if (this.supportedCallbacks.includes(event)) { + this.eventEmitter.on(event, callback, context || this); + } else { + throw new Error('Unsupported event type: ' + event); + } + }; - /** - * Update the view assoicated with the currently selected item - */ - Palette.prototype.updateSelected = function (item) { - 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') { - this.domElement.querySelector('.t-swatch').classList.add('no-selection'); - } else { - this.domElement.querySelector('.t-swatch').classList.remove('no-selection'); - } - }; + /** + * Get the currently selected value of this palette + * @return {string} The selected value + */ + Palette.prototype.getCurrent = function () { + return this.value; + }; - /** - * set the property to be used for the 'no selection' item. If not set, this - * defaults to a single space - * @param {string} item The key to use as the 'no selection' item - */ - Palette.prototype.setNullOption = function (item) { - this.nullOption = item; - this.itemElements.nullOption.data = { item: item }; - }; + /** + * Set the selected value of this palette; if the item doesn't exist in the + * palette's data model, the selected value will not change. Invokes any + * change callbacks associated with this palette. + * @param {string} item The key of the item to set as selected + */ + Palette.prototype.set = function (item) { + const self = this; + if (this.items.includes(item) || item === this.nullOption) { + this.value = item; + if (item === this.nullOption) { + this.updateSelected('nullOption'); + } else { + this.updateSelected(item); + } + } - /** - * Hides the 'no selection' option to be hidden in the view if it doesn't apply - */ - Palette.prototype.toggleNullOption = function () { - const elem = this.domElement.querySelector('.c-palette__item-none'); + this.eventEmitter.emit('change', self.value); + }; - 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'; - } - }; + /** + * Update the view assoicated with the currently selected item + */ + Palette.prototype.updateSelected = function (item) { + 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') { + this.domElement.querySelector('.t-swatch').classList.add('no-selection'); + } else { + this.domElement.querySelector('.t-swatch').classList.remove('no-selection'); + } + }; - return Palette; + /** + * set the property to be used for the 'no selection' item. If not set, this + * defaults to a single space + * @param {string} item The key to use as the 'no selection' item + */ + Palette.prototype.setNullOption = function (item) { + this.nullOption = 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 () { + 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 676a9791b2..38df9c5fc1 100644 --- a/src/plugins/summaryWidget/src/input/Select.js +++ b/src/plugins/summaryWidget/src/input/Select.js @@ -1,160 +1,154 @@ define([ - '../eventHelpers', - '../../res/input/selectTemplate.html', - '../../../../utils/template/templateHelpers', - 'EventEmitter' -], function ( - eventHelpers, - selectTemplate, - templateHelpers, - EventEmitter -) { + '../eventHelpers', + '../../res/input/selectTemplate.html', + '../../../../utils/template/templateHelpers', + 'EventEmitter' +], function (eventHelpers, selectTemplate, templateHelpers, EventEmitter) { + /** + * Wraps an HTML select element, and provides methods for dynamically altering + * its composition from the data model + * @constructor + */ + function Select() { + eventHelpers.extend(this); + + const self = this; + + this.domElement = templateHelpers.convertTemplateToHTML(selectTemplate)[0]; + + this.options = []; + this.eventEmitter = new EventEmitter(); + this.supportedCallbacks = ['change']; + + this.populate(); /** - * Wraps an HTML select element, and provides methods for dynamically altering - * its composition from the data model - * @constructor + * Event handler for the wrapped select element. Also invokes any change + * callbacks registered with this select with the new value + * @param {Event} event The change event that triggered this callback + * @private */ - function Select() { - eventHelpers.extend(this); + function onChange(event) { + const elem = event.target; + const value = self.options[elem.selectedIndex]; - const self = this; - - this.domElement = templateHelpers.convertTemplateToHTML(selectTemplate)[0]; - - this.options = []; - this.eventEmitter = new EventEmitter(); - this.supportedCallbacks = ['change']; - - this.populate(); - - /** - * Event handler for the wrapped select element. Also invokes any change - * callbacks registered with this select with the new value - * @param {Event} event The change event that triggered this callback - * @private - */ - function onChange(event) { - const elem = event.target; - const value = self.options[elem.selectedIndex]; - - self.eventEmitter.emit('change', value[0]); - } - - this.listenTo(this.domElement.querySelector('select'), 'change', onChange, this); + self.eventEmitter.emit('change', value[0]); } - /** - * Get the DOM element representing this Select in the view - * @return {Element} - */ - Select.prototype.getDOM = function () { - return this.domElement; - }; + this.listenTo(this.domElement.querySelector('select'), 'change', onChange, this); + } - /** - * Register a callback with this select: supported callback is change - * @param {string} event The key for the event to listen to - * @param {function} callback The function that this rule will envoke on this event - * @param {Object} context A reference to a scope to use as the context for - * context for the callback function - */ - Select.prototype.on = function (event, callback, context) { - if (this.supportedCallbacks.includes(event)) { - this.eventEmitter.on(event, callback, context || this); - } else { - throw new Error('Unsupported event type' + event); - } - }; + /** + * Get the DOM element representing this Select in the view + * @return {Element} + */ + Select.prototype.getDOM = function () { + return this.domElement; + }; - /** - * Update the select element in the view from the current state of the data - * model - */ - Select.prototype.populate = function () { - const self = this; - let selectedIndex = 0; + /** + * Register a callback with this select: supported callback is change + * @param {string} event The key for the event to listen to + * @param {function} callback The function that this rule will envoke on this event + * @param {Object} context A reference to a scope to use as the context for + * context for the callback function + */ + Select.prototype.on = function (event, callback, context) { + if (this.supportedCallbacks.includes(event)) { + this.eventEmitter.on(event, callback, context || this); + } else { + throw new Error('Unsupported event type' + event); + } + }; - selectedIndex = this.domElement.querySelector('select').selectedIndex; + /** + * Update the select element in the view from the current state of the data + * model + */ + Select.prototype.populate = function () { + const self = this; + let selectedIndex = 0; - this.domElement.querySelector('select').innerHTML = ''; + selectedIndex = this.domElement.querySelector('select').selectedIndex; - self.options.forEach(function (option) { - const optionElement = document.createElement('option'); - optionElement.value = option[0]; - optionElement.innerText = `+ ${option[1]}`; + this.domElement.querySelector('select').innerHTML = ''; - self.domElement.querySelector('select').appendChild(optionElement); - }); + self.options.forEach(function (option) { + const optionElement = document.createElement('option'); + optionElement.value = option[0]; + optionElement.innerText = `+ ${option[1]}`; - this.domElement.querySelector('select').selectedIndex = selectedIndex; - }; + self.domElement.querySelector('select').appendChild(optionElement); + }); - /** - * Add a single option to this select - * @param {string} value The value for the new option - * @param {string} label The human-readable text for the new option - */ - Select.prototype.addOption = function (value, label) { - this.options.push([value, label]); - this.populate(); - }; + this.domElement.querySelector('select').selectedIndex = selectedIndex; + }; - /** - * Set the available options for this select. Replaces any existing options - * @param {string[][]} options An array of [value, label] pairs to display - */ - Select.prototype.setOptions = function (options) { - this.options = options; - this.populate(); - }; + /** + * Add a single option to this select + * @param {string} value The value for the new option + * @param {string} label The human-readable text for the new option + */ + Select.prototype.addOption = function (value, label) { + this.options.push([value, label]); + this.populate(); + }; - /** - * Sets the currently selected element an invokes any registered change - * callbacks with the new value. If the value doesn't exist in this select's - * model, its state will not change. - * @param {string} value The value to set as the selected option - */ - Select.prototype.setSelected = function (value) { - let selectedIndex = 0; - let selectedOption; + /** + * Set the available options for this select. Replaces any existing options + * @param {string[][]} options An array of [value, label] pairs to display + */ + Select.prototype.setOptions = function (options) { + this.options = options; + this.populate(); + }; - this.options.forEach (function (option, index) { - if (option[0] === value) { - selectedIndex = index; - } - }); - this.domElement.querySelector('select').selectedIndex = selectedIndex; + /** + * Sets the currently selected element an invokes any registered change + * callbacks with the new value. If the value doesn't exist in this select's + * model, its state will not change. + * @param {string} value The value to set as the selected option + */ + Select.prototype.setSelected = function (value) { + let selectedIndex = 0; + let selectedOption; - selectedOption = this.options[selectedIndex]; - this.eventEmitter.emit('change', selectedOption[0]); - }; + this.options.forEach(function (option, index) { + if (option[0] === value) { + selectedIndex = index; + } + }); + this.domElement.querySelector('select').selectedIndex = selectedIndex; - /** - * Get the value of the currently selected item - * @return {string} - */ - Select.prototype.getSelected = function () { - return this.domElement.querySelector('select').value; - }; + selectedOption = this.options[selectedIndex]; + this.eventEmitter.emit('change', selectedOption[0]); + }; - Select.prototype.hide = function () { - this.domElement.classList.add('hidden'); - if (this.domElement.querySelector('.equal-to')) { - this.domElement.querySelector('.equal-to').classList.add('hidden'); - } - }; + /** + * Get the value of the currently selected item + * @return {string} + */ + Select.prototype.getSelected = function () { + return this.domElement.querySelector('select').value; + }; - Select.prototype.show = function () { - this.domElement.classList.remove('hidden'); - if (this.domElement.querySelector('.equal-to')) { - this.domElement.querySelector('.equal-to').classList.remove('hidden'); - } - }; + Select.prototype.hide = function () { + this.domElement.classList.add('hidden'); + if (this.domElement.querySelector('.equal-to')) { + this.domElement.querySelector('.equal-to').classList.add('hidden'); + } + }; - Select.prototype.destroy = function () { - this.stopListening(); - }; + Select.prototype.show = function () { + this.domElement.classList.remove('hidden'); + if (this.domElement.querySelector('.equal-to')) { + this.domElement.querySelector('.equal-to').classList.remove('hidden'); + } + }; - return Select; + Select.prototype.destroy = function () { + this.stopListening(); + }; + + return Select; }); diff --git a/src/plugins/summaryWidget/src/telemetry/EvaluatorPool.js b/src/plugins/summaryWidget/src/telemetry/EvaluatorPool.js index 8954ff4e3d..b6cac0595e 100644 --- a/src/plugins/summaryWidget/src/telemetry/EvaluatorPool.js +++ b/src/plugins/summaryWidget/src/telemetry/EvaluatorPool.js @@ -20,47 +20,40 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './SummaryWidgetEvaluator', - 'objectUtils' -], function ( - SummaryWidgetEvaluator, - objectUtils -) { +define(['./SummaryWidgetEvaluator', 'objectUtils'], function (SummaryWidgetEvaluator, objectUtils) { + function EvaluatorPool(openmct) { + this.openmct = openmct; + this.byObjectId = {}; + this.byEvaluator = new WeakMap(); + } - function EvaluatorPool(openmct) { - this.openmct = openmct; - this.byObjectId = {}; - this.byEvaluator = new WeakMap(); + EvaluatorPool.prototype.get = function (domainObject) { + const objectId = objectUtils.makeKeyString(domainObject.identifier); + let poolEntry = this.byObjectId[objectId]; + if (!poolEntry) { + poolEntry = { + leases: 0, + objectId: objectId, + evaluator: new SummaryWidgetEvaluator(domainObject, this.openmct) + }; + this.byEvaluator.set(poolEntry.evaluator, poolEntry); + this.byObjectId[objectId] = poolEntry; } - EvaluatorPool.prototype.get = function (domainObject) { - const objectId = objectUtils.makeKeyString(domainObject.identifier); - let poolEntry = this.byObjectId[objectId]; - if (!poolEntry) { - poolEntry = { - leases: 0, - objectId: objectId, - evaluator: new SummaryWidgetEvaluator(domainObject, this.openmct) - }; - this.byEvaluator.set(poolEntry.evaluator, poolEntry); - this.byObjectId[objectId] = poolEntry; - } + poolEntry.leases += 1; - poolEntry.leases += 1; + return poolEntry.evaluator; + }; - return poolEntry.evaluator; - }; + EvaluatorPool.prototype.release = function (evaluator) { + const poolEntry = this.byEvaluator.get(evaluator); + poolEntry.leases -= 1; + if (poolEntry.leases === 0) { + evaluator.destroy(); + this.byEvaluator.delete(evaluator); + delete this.byObjectId[poolEntry.objectId]; + } + }; - EvaluatorPool.prototype.release = function (evaluator) { - const poolEntry = this.byEvaluator.get(evaluator); - poolEntry.leases -= 1; - if (poolEntry.leases === 0) { - evaluator.destroy(); - this.byEvaluator.delete(evaluator); - delete this.byObjectId[poolEntry.objectId]; - } - }; - - return EvaluatorPool; + return EvaluatorPool; }); diff --git a/src/plugins/summaryWidget/src/telemetry/EvaluatorPoolSpec.js b/src/plugins/summaryWidget/src/telemetry/EvaluatorPoolSpec.js index 28fc2d129f..d5b3398267 100644 --- a/src/plugins/summaryWidget/src/telemetry/EvaluatorPoolSpec.js +++ b/src/plugins/summaryWidget/src/telemetry/EvaluatorPoolSpec.js @@ -20,84 +20,78 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './EvaluatorPool', - './SummaryWidgetEvaluator' -], function ( - EvaluatorPool, - SummaryWidgetEvaluator +define(['./EvaluatorPool', './SummaryWidgetEvaluator'], function ( + EvaluatorPool, + SummaryWidgetEvaluator ) { - describe('EvaluatorPool', function () { - let pool; - let openmct; - let objectA; - let objectB; + describe('EvaluatorPool', function () { + let pool; + let openmct; + let objectA; + let objectB; - beforeEach(function () { - openmct = { - composition: jasmine.createSpyObj('compositionAPI', ['get']), - objects: jasmine.createSpyObj('objectAPI', ['observe']) - }; - openmct.composition.get.and.callFake(function () { - const compositionCollection = jasmine.createSpyObj( - 'compositionCollection', - [ - 'load', - 'on', - 'off' - ] - ); - compositionCollection.load.and.returnValue(Promise.resolve()); + beforeEach(function () { + openmct = { + composition: jasmine.createSpyObj('compositionAPI', ['get']), + objects: jasmine.createSpyObj('objectAPI', ['observe']) + }; + openmct.composition.get.and.callFake(function () { + const compositionCollection = jasmine.createSpyObj('compositionCollection', [ + 'load', + 'on', + 'off' + ]); + compositionCollection.load.and.returnValue(Promise.resolve()); - return compositionCollection; - }); - openmct.objects.observe.and.callFake(function () { - return function () {}; - }); - pool = new EvaluatorPool(openmct); - objectA = { - identifier: { - namespace: 'someNamespace', - key: 'someKey' - }, - configuration: { - ruleOrder: [] - } - }; - objectB = { - identifier: { - namespace: 'otherNamespace', - key: 'otherKey' - }, - configuration: { - ruleOrder: [] - } - }; - }); - - it('returns new evaluators for different objects', function () { - const evaluatorA = pool.get(objectA); - const evaluatorB = pool.get(objectB); - expect(evaluatorA).not.toBe(evaluatorB); - }); - - it('returns the same evaluator for the same object', function () { - const evaluatorA = pool.get(objectA); - const evaluatorB = pool.get(objectA); - expect(evaluatorA).toBe(evaluatorB); - - const evaluatorC = pool.get(JSON.parse(JSON.stringify(objectA))); - expect(evaluatorA).toBe(evaluatorC); - }); - - it('returns new evaluator when old is released', function () { - const evaluatorA = pool.get(objectA); - const evaluatorB = pool.get(objectA); - expect(evaluatorA).toBe(evaluatorB); - pool.release(evaluatorA); - pool.release(evaluatorB); - const evaluatorC = pool.get(objectA); - expect(evaluatorA).not.toBe(evaluatorC); - }); + return compositionCollection; + }); + openmct.objects.observe.and.callFake(function () { + return function () {}; + }); + pool = new EvaluatorPool(openmct); + objectA = { + identifier: { + namespace: 'someNamespace', + key: 'someKey' + }, + configuration: { + ruleOrder: [] + } + }; + objectB = { + identifier: { + namespace: 'otherNamespace', + key: 'otherKey' + }, + configuration: { + ruleOrder: [] + } + }; }); + + it('returns new evaluators for different objects', function () { + const evaluatorA = pool.get(objectA); + const evaluatorB = pool.get(objectB); + expect(evaluatorA).not.toBe(evaluatorB); + }); + + it('returns the same evaluator for the same object', function () { + const evaluatorA = pool.get(objectA); + const evaluatorB = pool.get(objectA); + expect(evaluatorA).toBe(evaluatorB); + + const evaluatorC = pool.get(JSON.parse(JSON.stringify(objectA))); + expect(evaluatorA).toBe(evaluatorC); + }); + + it('returns new evaluator when old is released', function () { + const evaluatorA = pool.get(objectA); + const evaluatorB = pool.get(objectA); + expect(evaluatorA).toBe(evaluatorB); + pool.release(evaluatorA); + pool.release(evaluatorB); + const evaluatorC = pool.get(objectA); + expect(evaluatorA).not.toBe(evaluatorC); + }); + }); }); diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetCondition.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetCondition.js index df6828fa07..621e656d37 100644 --- a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetCondition.js +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetCondition.js @@ -20,64 +20,57 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './operations' -], function ( - OPERATIONS -) { - - function SummaryWidgetCondition(definition) { - this.object = definition.object; - this.key = definition.key; - this.values = definition.values; - if (!definition.operation) { - // TODO: better handling for default rule. - this.evaluate = function () { - return true; - }; - } else { - this.comparator = OPERATIONS[definition.operation].operation; - } +define(['./operations'], function (OPERATIONS) { + function SummaryWidgetCondition(definition) { + this.object = definition.object; + this.key = definition.key; + this.values = definition.values; + if (!definition.operation) { + // TODO: better handling for default rule. + this.evaluate = function () { + return true; + }; + } else { + this.comparator = OPERATIONS[definition.operation].operation; } + } - SummaryWidgetCondition.prototype.evaluate = function (telemetryState) { - const stateKeys = Object.keys(telemetryState); - let state; - let result; - let i; + SummaryWidgetCondition.prototype.evaluate = function (telemetryState) { + const stateKeys = Object.keys(telemetryState); + let state; + let result; + let i; - if (this.object === 'any') { - for (i = 0; i < stateKeys.length; i++) { - state = telemetryState[stateKeys[i]]; - result = this.evaluateState(state); - if (result) { - return true; - } - } - - return false; - } else if (this.object === 'all') { - for (i = 0; i < stateKeys.length; i++) { - state = telemetryState[stateKeys[i]]; - result = this.evaluateState(state); - if (!result) { - return false; - } - } - - return true; - } else { - return this.evaluateState(telemetryState[this.object]); + if (this.object === 'any') { + for (i = 0; i < stateKeys.length; i++) { + state = telemetryState[stateKeys[i]]; + result = this.evaluateState(state); + if (result) { + return true; } - }; + } - SummaryWidgetCondition.prototype.evaluateState = function (state) { - const testValues = [ - state.formats[this.key].parse(state.lastDatum) - ].concat(this.values); + return false; + } else if (this.object === 'all') { + for (i = 0; i < stateKeys.length; i++) { + state = telemetryState[stateKeys[i]]; + result = this.evaluateState(state); + if (!result) { + return false; + } + } - return this.comparator(testValues); - }; + return true; + } else { + return this.evaluateState(telemetryState[this.object]); + } + }; - return SummaryWidgetCondition; + SummaryWidgetCondition.prototype.evaluateState = function (state) { + const testValues = [state.formats[this.key].parse(state.lastDatum)].concat(this.values); + + return this.comparator(testValues); + }; + + return SummaryWidgetCondition; }); diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetConditionSpec.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetConditionSpec.js index 49cf9bf4ef..9bf35a3f27 100644 --- a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetConditionSpec.js +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetConditionSpec.js @@ -20,123 +20,106 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './SummaryWidgetCondition' -], function ( - SummaryWidgetCondition -) { +define(['./SummaryWidgetCondition'], function (SummaryWidgetCondition) { + describe('SummaryWidgetCondition', function () { + let condition; + let telemetryState; - describe('SummaryWidgetCondition', function () { - let condition; - let telemetryState; - - beforeEach(function () { - // Format map intentionally uses different keys than those present - // in datum, which serves to verify conditions use format map to get - // data. - const formatMap = { - adjusted: { - parse: function (datum) { - return datum.value + 10; - } - }, - raw: { - parse: function (datum) { - return datum.value; - } - } - }; - - telemetryState = { - objectId: { - formats: formatMap, - lastDatum: { - } - }, - otherObjectId: { - formats: formatMap, - lastDatum: { - } - } - }; - - }); - - it('can evaluate if a single object matches', function () { - condition = new SummaryWidgetCondition({ - object: 'objectId', - key: 'raw', - operation: 'greaterThan', - values: [ - 10 - ] - }); - telemetryState.objectId.lastDatum.value = 5; - expect(condition.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 15; - expect(condition.evaluate(telemetryState)).toBe(true); - }); - - it('can evaluate if a single object matches (alternate keys)', function () { - condition = new SummaryWidgetCondition({ - object: 'objectId', - key: 'adjusted', - operation: 'greaterThan', - values: [ - 10 - ] - }); - telemetryState.objectId.lastDatum.value = -5; - expect(condition.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 5; - expect(condition.evaluate(telemetryState)).toBe(true); - }); - - it('can evaluate "if all objects match"', function () { - condition = new SummaryWidgetCondition({ - object: 'all', - key: 'raw', - operation: 'greaterThan', - values: [ - 10 - ] - }); - telemetryState.objectId.lastDatum.value = 0; - telemetryState.otherObjectId.lastDatum.value = 0; - expect(condition.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 0; - telemetryState.otherObjectId.lastDatum.value = 15; - expect(condition.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 15; - telemetryState.otherObjectId.lastDatum.value = 0; - expect(condition.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 15; - telemetryState.otherObjectId.lastDatum.value = 15; - expect(condition.evaluate(telemetryState)).toBe(true); - }); - - it('can evaluate "if any object matches"', function () { - condition = new SummaryWidgetCondition({ - object: 'any', - key: 'raw', - operation: 'greaterThan', - values: [ - 10 - ] - }); - telemetryState.objectId.lastDatum.value = 0; - telemetryState.otherObjectId.lastDatum.value = 0; - expect(condition.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 0; - telemetryState.otherObjectId.lastDatum.value = 15; - expect(condition.evaluate(telemetryState)).toBe(true); - telemetryState.objectId.lastDatum.value = 15; - telemetryState.otherObjectId.lastDatum.value = 0; - expect(condition.evaluate(telemetryState)).toBe(true); - telemetryState.objectId.lastDatum.value = 15; - telemetryState.otherObjectId.lastDatum.value = 15; - expect(condition.evaluate(telemetryState)).toBe(true); - }); + beforeEach(function () { + // Format map intentionally uses different keys than those present + // in datum, which serves to verify conditions use format map to get + // data. + const formatMap = { + adjusted: { + parse: function (datum) { + return datum.value + 10; + } + }, + raw: { + parse: function (datum) { + return datum.value; + } + } + }; + telemetryState = { + objectId: { + formats: formatMap, + lastDatum: {} + }, + otherObjectId: { + formats: formatMap, + lastDatum: {} + } + }; }); + + it('can evaluate if a single object matches', function () { + condition = new SummaryWidgetCondition({ + object: 'objectId', + key: 'raw', + operation: 'greaterThan', + values: [10] + }); + telemetryState.objectId.lastDatum.value = 5; + expect(condition.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 15; + expect(condition.evaluate(telemetryState)).toBe(true); + }); + + it('can evaluate if a single object matches (alternate keys)', function () { + condition = new SummaryWidgetCondition({ + object: 'objectId', + key: 'adjusted', + operation: 'greaterThan', + values: [10] + }); + telemetryState.objectId.lastDatum.value = -5; + expect(condition.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 5; + expect(condition.evaluate(telemetryState)).toBe(true); + }); + + it('can evaluate "if all objects match"', function () { + condition = new SummaryWidgetCondition({ + object: 'all', + key: 'raw', + operation: 'greaterThan', + values: [10] + }); + telemetryState.objectId.lastDatum.value = 0; + telemetryState.otherObjectId.lastDatum.value = 0; + expect(condition.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 0; + telemetryState.otherObjectId.lastDatum.value = 15; + expect(condition.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 15; + telemetryState.otherObjectId.lastDatum.value = 0; + expect(condition.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 15; + telemetryState.otherObjectId.lastDatum.value = 15; + expect(condition.evaluate(telemetryState)).toBe(true); + }); + + it('can evaluate "if any object matches"', function () { + condition = new SummaryWidgetCondition({ + object: 'any', + key: 'raw', + operation: 'greaterThan', + values: [10] + }); + telemetryState.objectId.lastDatum.value = 0; + telemetryState.otherObjectId.lastDatum.value = 0; + expect(condition.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 0; + telemetryState.otherObjectId.lastDatum.value = 15; + expect(condition.evaluate(telemetryState)).toBe(true); + telemetryState.objectId.lastDatum.value = 15; + telemetryState.otherObjectId.lastDatum.value = 0; + expect(condition.evaluate(telemetryState)).toBe(true); + telemetryState.objectId.lastDatum.value = 15; + telemetryState.otherObjectId.lastDatum.value = 15; + expect(condition.evaluate(telemetryState)).toBe(true); + }); + }); }); diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetEvaluator.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetEvaluator.js index 0139ff4418..3a98e643c4 100644 --- a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetEvaluator.js +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetEvaluator.js @@ -20,273 +20,251 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './SummaryWidgetRule', - '../eventHelpers', - 'objectUtils', - 'lodash' -], function ( - SummaryWidgetRule, - eventHelpers, - objectUtils, - _ +define(['./SummaryWidgetRule', '../eventHelpers', 'objectUtils', 'lodash'], function ( + SummaryWidgetRule, + eventHelpers, + objectUtils, + _ ) { + /** + * evaluates rules defined in a summary widget against either lad or + * realtime state. + * + */ + function SummaryWidgetEvaluator(domainObject, openmct) { + this.openmct = openmct; + this.baseState = {}; - /** - * evaluates rules defined in a summary widget against either lad or - * realtime state. - * - */ - function SummaryWidgetEvaluator(domainObject, openmct) { - this.openmct = openmct; - this.baseState = {}; + this.updateRules(domainObject); + this.removeObserver = openmct.objects.observe(domainObject, '*', this.updateRules.bind(this)); - this.updateRules(domainObject); - this.removeObserver = openmct.objects.observe( - domainObject, - '*', - this.updateRules.bind(this) - ); + const composition = openmct.composition.get(domainObject); - const composition = openmct.composition.get(domainObject); + this.listenTo(composition, 'add', this.addChild, this); + this.listenTo(composition, 'remove', this.removeChild, this); - this.listenTo(composition, 'add', this.addChild, this); - this.listenTo(composition, 'remove', this.removeChild, this); + this.loadPromise = composition.load(); + } - this.loadPromise = composition.load(); - } + eventHelpers.extend(SummaryWidgetEvaluator.prototype); - eventHelpers.extend(SummaryWidgetEvaluator.prototype); + /** + * Subscribes to realtime telemetry for the given summary widget. + */ + SummaryWidgetEvaluator.prototype.subscribe = function (callback) { + let active = true; + let unsubscribes = []; - /** - * Subscribes to realtime telemetry for the given summary widget. - */ - SummaryWidgetEvaluator.prototype.subscribe = function (callback) { - let active = true; - let unsubscribes = []; - - this.getBaseStateClone() - .then(function (realtimeStates) { - if (!active) { - return; - } - - const updateCallback = function () { - const datum = this.evaluateState( - realtimeStates, - this.openmct.time.timeSystem().key - ); - if (datum) { - callback(datum); - } - }.bind(this); - - /* eslint-disable you-dont-need-lodash-underscore/map */ - unsubscribes = _.map( - realtimeStates, - this.subscribeToObjectState.bind(this, updateCallback) - ); - /* eslint-enable you-dont-need-lodash-underscore/map */ - }.bind(this)); - - return function () { - active = false; - unsubscribes.forEach(function (unsubscribe) { - unsubscribe(); - }); - }; - }; - - /** - * Returns a promise for a telemetry datum obtained by evaluating the - * current lad data. - */ - SummaryWidgetEvaluator.prototype.requestLatest = function (options) { - return this.getBaseStateClone() - .then(function (ladState) { - const promises = Object.values(ladState) - .map(this.updateObjectStateFromLAD.bind(this, options)); - - return Promise.all(promises) - .then(function () { - return ladState; - }); - }.bind(this)) - .then(function (ladStates) { - return this.evaluateState(ladStates, options.domain); - }.bind(this)); - }; - - SummaryWidgetEvaluator.prototype.updateRules = function (domainObject) { - this.rules = domainObject.configuration.ruleOrder.map(function (ruleId) { - return new SummaryWidgetRule(domainObject.configuration.ruleConfigById[ruleId]); - }); - }; - - SummaryWidgetEvaluator.prototype.addChild = function (childObject) { - const childId = objectUtils.makeKeyString(childObject.identifier); - const metadata = this.openmct.telemetry.getMetadata(childObject); - const formats = this.openmct.telemetry.getFormatMap(metadata); - - this.baseState[childId] = { - id: childId, - domainObject: childObject, - metadata: metadata, - formats: formats - }; - }; - - SummaryWidgetEvaluator.prototype.removeChild = function (childObject) { - const childId = objectUtils.makeKeyString(childObject.identifier); - delete this.baseState[childId]; - }; - - SummaryWidgetEvaluator.prototype.load = function () { - return this.loadPromise; - }; - - /** - * Return a promise for a 2-deep clone of the base state object: object - * states are shallow cloned, and then assembled and returned as a new base - * state. Allows object states to be mutated while sharing telemetry - * metadata and formats. - */ - SummaryWidgetEvaluator.prototype.getBaseStateClone = function () { - return this.load() - .then(function () { - /* eslint-disable you-dont-need-lodash-underscore/values */ - return _(this.baseState) - .values() - .map(_.clone) - .keyBy('id') - .value(); - /* eslint-enable you-dont-need-lodash-underscore/values */ - }.bind(this)); - }; - - /** - * Subscribes to realtime updates for a given objectState, and invokes - * the supplied callback when objectState has been updated. Returns - * a function to unsubscribe. - * @private. - */ - SummaryWidgetEvaluator.prototype.subscribeToObjectState = function (callback, objectState) { - return this.openmct.telemetry.subscribe( - objectState.domainObject, - function (datum) { - objectState.lastDatum = datum; - objectState.timestamps = this.getTimestamps(objectState.id, datum); - callback(); - }.bind(this) - ); - }; - - /** - * Given an object state, will return a promise that is resolved when the - * object state has been updated from the LAD. - * @private. - */ - SummaryWidgetEvaluator.prototype.updateObjectStateFromLAD = function (options, objectState) { - options = Object.assign({}, options, { - strategy: 'latest', - size: 1 - }); - - return this.openmct - .telemetry - .request( - objectState.domainObject, - options - ) - .then(function (results) { - objectState.lastDatum = results[results.length - 1]; - objectState.timestamps = this.getTimestamps( - objectState.id, - objectState.lastDatum - ); - }.bind(this)); - }; - - /** - * Returns an object containing all domain values in a datum. - * @private. - */ - SummaryWidgetEvaluator.prototype.getTimestamps = function (childId, datum) { - const timestampedDatum = {}; - this.openmct.time.getAllTimeSystems().forEach(function (timeSystem) { - timestampedDatum[timeSystem.key] = - this.baseState[childId].formats[timeSystem.key].parse(datum); - }, this); - - return timestampedDatum; - }; - - /** - * Given a base datum(containing timestamps) and rule index, adds values - * from the matching rule. - * @private - */ - SummaryWidgetEvaluator.prototype.makeDatumFromRule = function (ruleIndex, baseDatum) { - const rule = this.rules[ruleIndex]; - - baseDatum.ruleLabel = rule.label; - baseDatum.ruleName = rule.name; - baseDatum.message = rule.message; - baseDatum.ruleIndex = ruleIndex; - baseDatum.backgroundColor = rule.style['background-color']; - baseDatum.textColor = rule.style.color; - baseDatum.borderColor = rule.style['border-color']; - baseDatum.icon = rule.icon; - - return baseDatum; - }; - - /** - * Evaluate a `state` object and return a summary widget telemetry datum. - * Datum timestamps will be taken from the "latest" datum in the `state` - * where "latest" is the datum with the largest value for the given - * `timestampKey`. - * @private. - */ - SummaryWidgetEvaluator.prototype.evaluateState = function (state, timestampKey) { - const hasRequiredData = Object.keys(state).reduce(function (itDoes, k) { - return itDoes && state[k].lastDatum; - }, true); - if (!hasRequiredData) { - return; + this.getBaseStateClone().then( + function (realtimeStates) { + if (!active) { + return; } - let i; - for (i = this.rules.length - 1; i > 0; i--) { - if (this.rules[i].evaluate(state, false)) { - break; - } - } + const updateCallback = function () { + const datum = this.evaluateState(realtimeStates, this.openmct.time.timeSystem().key); + if (datum) { + callback(datum); + } + }.bind(this); /* eslint-disable you-dont-need-lodash-underscore/map */ - let latestTimestamp = _(state) - .map('timestamps') - .sortBy(timestampKey) - .last(); + unsubscribes = _.map( + realtimeStates, + this.subscribeToObjectState.bind(this, updateCallback) + ); /* eslint-enable you-dont-need-lodash-underscore/map */ + }.bind(this) + ); - if (!latestTimestamp) { - latestTimestamp = {}; - } - - const baseDatum = _.clone(latestTimestamp); - - return this.makeDatumFromRule(i, baseDatum); + return function () { + active = false; + unsubscribes.forEach(function (unsubscribe) { + unsubscribe(); + }); }; + }; - /** - * remove all listeners and clean up any resources. - */ - SummaryWidgetEvaluator.prototype.destroy = function () { - this.stopListening(); - this.removeObserver(); + /** + * Returns a promise for a telemetry datum obtained by evaluating the + * current lad data. + */ + SummaryWidgetEvaluator.prototype.requestLatest = function (options) { + return this.getBaseStateClone() + .then( + function (ladState) { + const promises = Object.values(ladState).map( + this.updateObjectStateFromLAD.bind(this, options) + ); + + return Promise.all(promises).then(function () { + return ladState; + }); + }.bind(this) + ) + .then( + function (ladStates) { + return this.evaluateState(ladStates, options.domain); + }.bind(this) + ); + }; + + SummaryWidgetEvaluator.prototype.updateRules = function (domainObject) { + this.rules = domainObject.configuration.ruleOrder.map(function (ruleId) { + return new SummaryWidgetRule(domainObject.configuration.ruleConfigById[ruleId]); + }); + }; + + SummaryWidgetEvaluator.prototype.addChild = function (childObject) { + const childId = objectUtils.makeKeyString(childObject.identifier); + const metadata = this.openmct.telemetry.getMetadata(childObject); + const formats = this.openmct.telemetry.getFormatMap(metadata); + + this.baseState[childId] = { + id: childId, + domainObject: childObject, + metadata: metadata, + formats: formats }; + }; - return SummaryWidgetEvaluator; + SummaryWidgetEvaluator.prototype.removeChild = function (childObject) { + const childId = objectUtils.makeKeyString(childObject.identifier); + delete this.baseState[childId]; + }; + SummaryWidgetEvaluator.prototype.load = function () { + return this.loadPromise; + }; + + /** + * Return a promise for a 2-deep clone of the base state object: object + * states are shallow cloned, and then assembled and returned as a new base + * state. Allows object states to be mutated while sharing telemetry + * metadata and formats. + */ + SummaryWidgetEvaluator.prototype.getBaseStateClone = function () { + return this.load().then( + function () { + /* eslint-disable you-dont-need-lodash-underscore/values */ + return _(this.baseState).values().map(_.clone).keyBy('id').value(); + /* eslint-enable you-dont-need-lodash-underscore/values */ + }.bind(this) + ); + }; + + /** + * Subscribes to realtime updates for a given objectState, and invokes + * the supplied callback when objectState has been updated. Returns + * a function to unsubscribe. + * @private. + */ + SummaryWidgetEvaluator.prototype.subscribeToObjectState = function (callback, objectState) { + return this.openmct.telemetry.subscribe( + objectState.domainObject, + function (datum) { + objectState.lastDatum = datum; + objectState.timestamps = this.getTimestamps(objectState.id, datum); + callback(); + }.bind(this) + ); + }; + + /** + * Given an object state, will return a promise that is resolved when the + * object state has been updated from the LAD. + * @private. + */ + SummaryWidgetEvaluator.prototype.updateObjectStateFromLAD = function (options, objectState) { + options = Object.assign({}, options, { + strategy: 'latest', + size: 1 + }); + + return this.openmct.telemetry.request(objectState.domainObject, options).then( + function (results) { + objectState.lastDatum = results[results.length - 1]; + objectState.timestamps = this.getTimestamps(objectState.id, objectState.lastDatum); + }.bind(this) + ); + }; + + /** + * Returns an object containing all domain values in a datum. + * @private. + */ + SummaryWidgetEvaluator.prototype.getTimestamps = function (childId, datum) { + const timestampedDatum = {}; + this.openmct.time.getAllTimeSystems().forEach(function (timeSystem) { + timestampedDatum[timeSystem.key] = + this.baseState[childId].formats[timeSystem.key].parse(datum); + }, this); + + return timestampedDatum; + }; + + /** + * Given a base datum(containing timestamps) and rule index, adds values + * from the matching rule. + * @private + */ + SummaryWidgetEvaluator.prototype.makeDatumFromRule = function (ruleIndex, baseDatum) { + const rule = this.rules[ruleIndex]; + + baseDatum.ruleLabel = rule.label; + baseDatum.ruleName = rule.name; + baseDatum.message = rule.message; + baseDatum.ruleIndex = ruleIndex; + baseDatum.backgroundColor = rule.style['background-color']; + baseDatum.textColor = rule.style.color; + baseDatum.borderColor = rule.style['border-color']; + baseDatum.icon = rule.icon; + + return baseDatum; + }; + + /** + * Evaluate a `state` object and return a summary widget telemetry datum. + * Datum timestamps will be taken from the "latest" datum in the `state` + * where "latest" is the datum with the largest value for the given + * `timestampKey`. + * @private. + */ + SummaryWidgetEvaluator.prototype.evaluateState = function (state, timestampKey) { + const hasRequiredData = Object.keys(state).reduce(function (itDoes, k) { + return itDoes && state[k].lastDatum; + }, true); + if (!hasRequiredData) { + return; + } + + let i; + for (i = this.rules.length - 1; i > 0; i--) { + if (this.rules[i].evaluate(state, false)) { + break; + } + } + + /* eslint-disable you-dont-need-lodash-underscore/map */ + let latestTimestamp = _(state).map('timestamps').sortBy(timestampKey).last(); + /* eslint-enable you-dont-need-lodash-underscore/map */ + + if (!latestTimestamp) { + latestTimestamp = {}; + } + + const baseDatum = _.clone(latestTimestamp); + + return this.makeDatumFromRule(i, baseDatum); + }; + + /** + * remove all listeners and clean up any resources. + */ + SummaryWidgetEvaluator.prototype.destroy = function () { + this.stopListening(); + this.removeObserver(); + }; + + return SummaryWidgetEvaluator; }); diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetMetadataProvider.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetMetadataProvider.js index 813f559f42..796e60cb8b 100644 --- a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetMetadataProvider.js +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetMetadataProvider.js @@ -20,100 +20,94 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ +define([], function () { + function SummaryWidgetMetadataProvider(openmct) { + this.openmct = openmct; + } -], function ( + SummaryWidgetMetadataProvider.prototype.supportsMetadata = function (domainObject) { + return domainObject.type === 'summary-widget'; + }; -) { + SummaryWidgetMetadataProvider.prototype.getDomains = function (domainObject) { + return this.openmct.time.getAllTimeSystems().map(function (ts, i) { + return { + key: ts.key, + name: ts.name, + format: ts.timeFormat, + hints: { + domain: i + } + }; + }); + }; - function SummaryWidgetMetadataProvider(openmct) { - this.openmct = openmct; - } - - SummaryWidgetMetadataProvider.prototype.supportsMetadata = function (domainObject) { - return domainObject.type === 'summary-widget'; - }; - - SummaryWidgetMetadataProvider.prototype.getDomains = function (domainObject) { - return this.openmct.time.getAllTimeSystems().map(function (ts, i) { - return { - key: ts.key, - name: ts.name, - format: ts.timeFormat, - hints: { - domain: i - } - }; - }); - }; - - SummaryWidgetMetadataProvider.prototype.getMetadata = function (domainObject) { - const ruleOrder = domainObject.configuration.ruleOrder || []; - const enumerations = ruleOrder - .filter(function (ruleId) { - return Boolean(domainObject.configuration.ruleConfigById[ruleId]); - }) - .map(function (ruleId, ruleIndex) { - return { - string: domainObject.configuration.ruleConfigById[ruleId].label, - value: ruleIndex - }; - }); - - const metadata = { - // Generally safe assumption is that we have one domain per timeSystem. - values: this.getDomains().concat([ - { - name: 'State', - key: 'state', - source: 'ruleIndex', - format: 'enum', - enumerations: enumerations, - hints: { - range: 1 - } - }, - { - name: 'Rule Label', - key: 'ruleLabel', - format: 'string' - }, - { - name: 'Rule Name', - key: 'ruleName', - format: 'string' - }, - { - name: 'Message', - key: 'message', - format: 'string' - }, - { - name: 'Background Color', - key: 'backgroundColor', - format: 'string' - }, - { - name: 'Text Color', - key: 'textColor', - format: 'string' - }, - { - name: 'Border Color', - key: 'borderColor', - format: 'string' - }, - { - name: 'Display Icon', - key: 'icon', - format: 'string' - } - ]) + SummaryWidgetMetadataProvider.prototype.getMetadata = function (domainObject) { + const ruleOrder = domainObject.configuration.ruleOrder || []; + const enumerations = ruleOrder + .filter(function (ruleId) { + return Boolean(domainObject.configuration.ruleConfigById[ruleId]); + }) + .map(function (ruleId, ruleIndex) { + return { + string: domainObject.configuration.ruleConfigById[ruleId].label, + value: ruleIndex }; + }); - return metadata; + const metadata = { + // Generally safe assumption is that we have one domain per timeSystem. + values: this.getDomains().concat([ + { + name: 'State', + key: 'state', + source: 'ruleIndex', + format: 'enum', + enumerations: enumerations, + hints: { + range: 1 + } + }, + { + name: 'Rule Label', + key: 'ruleLabel', + format: 'string' + }, + { + name: 'Rule Name', + key: 'ruleName', + format: 'string' + }, + { + name: 'Message', + key: 'message', + format: 'string' + }, + { + name: 'Background Color', + key: 'backgroundColor', + format: 'string' + }, + { + name: 'Text Color', + key: 'textColor', + format: 'string' + }, + { + name: 'Border Color', + key: 'borderColor', + format: 'string' + }, + { + name: 'Display Icon', + key: 'icon', + format: 'string' + } + ]) }; - return SummaryWidgetMetadataProvider; + return metadata; + }; + return SummaryWidgetMetadataProvider; }); diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetRule.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetRule.js index dae51ec654..f611506986 100644 --- a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetRule.js +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetRule.js @@ -20,56 +20,51 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './SummaryWidgetCondition' -], function ( - SummaryWidgetCondition -) { - function SummaryWidgetRule(definition) { - this.name = definition.name; - this.label = definition.label; - this.id = definition.id; - this.icon = definition.icon; - this.style = definition.style; - this.message = definition.message; - this.description = definition.description; - this.conditions = definition.conditions.map(function (cDefinition) { - return new SummaryWidgetCondition(cDefinition); - }); - this.trigger = definition.trigger; - } +define(['./SummaryWidgetCondition'], function (SummaryWidgetCondition) { + function SummaryWidgetRule(definition) { + this.name = definition.name; + this.label = definition.label; + this.id = definition.id; + this.icon = definition.icon; + this.style = definition.style; + this.message = definition.message; + this.description = definition.description; + this.conditions = definition.conditions.map(function (cDefinition) { + return new SummaryWidgetCondition(cDefinition); + }); + this.trigger = definition.trigger; + } - /** - * Evaluate the given rule against a telemetryState and return true if it - * matches. - */ - SummaryWidgetRule.prototype.evaluate = function (telemetryState) { - let i; - let result; + /** + * Evaluate the given rule against a telemetryState and return true if it + * matches. + */ + SummaryWidgetRule.prototype.evaluate = function (telemetryState) { + let i; + let result; - if (this.trigger === 'all') { - for (i = 0; i < this.conditions.length; i++) { - result = this.conditions[i].evaluate(telemetryState); - if (!result) { - return false; - } - } - - return true; - } else if (this.trigger === 'any') { - for (i = 0; i < this.conditions.length; i++) { - result = this.conditions[i].evaluate(telemetryState); - if (result) { - return true; - } - } - - return false; - } else { - throw new Error('Invalid rule trigger: ' + this.trigger); + if (this.trigger === 'all') { + for (i = 0; i < this.conditions.length; i++) { + result = this.conditions[i].evaluate(telemetryState); + if (!result) { + return false; } - }; + } - return SummaryWidgetRule; + return true; + } else if (this.trigger === 'any') { + for (i = 0; i < this.conditions.length; i++) { + result = this.conditions[i].evaluate(telemetryState); + if (result) { + return true; + } + } + + return false; + } else { + throw new Error('Invalid rule trigger: ' + this.trigger); + } + }; + + return SummaryWidgetRule; }); - diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetRuleSpec.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetRuleSpec.js index 1570893540..5f740bca9f 100644 --- a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetRuleSpec.js +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetRuleSpec.js @@ -20,144 +20,134 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './SummaryWidgetRule' -], function ( - SummaryWidgetRule -) { - describe('SummaryWidgetRule', function () { +define(['./SummaryWidgetRule'], function (SummaryWidgetRule) { + describe('SummaryWidgetRule', function () { + let rule; + let telemetryState; - let rule; - let telemetryState; + beforeEach(function () { + const formatMap = { + raw: { + parse: function (datum) { + return datum.value; + } + } + }; - beforeEach(function () { - const formatMap = { - raw: { - parse: function (datum) { - return datum.value; - } - } - }; - - telemetryState = { - objectId: { - formats: formatMap, - lastDatum: { - } - }, - otherObjectId: { - formats: formatMap, - lastDatum: { - } - } - }; - }); - - it('allows single condition rules with any', function () { - rule = new SummaryWidgetRule({ - trigger: 'any', - conditions: [{ - object: 'objectId', - key: 'raw', - operation: 'greaterThan', - values: [ - 10 - ] - }] - }); - - telemetryState.objectId.lastDatum.value = 5; - expect(rule.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 15; - expect(rule.evaluate(telemetryState)).toBe(true); - }); - - it('allows single condition rules with all', function () { - rule = new SummaryWidgetRule({ - trigger: 'all', - conditions: [{ - object: 'objectId', - key: 'raw', - operation: 'greaterThan', - values: [ - 10 - ] - }] - }); - - telemetryState.objectId.lastDatum.value = 5; - expect(rule.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 15; - expect(rule.evaluate(telemetryState)).toBe(true); - }); - - it('can combine multiple conditions with all', function () { - rule = new SummaryWidgetRule({ - trigger: 'all', - conditions: [{ - object: 'objectId', - key: 'raw', - operation: 'greaterThan', - values: [ - 10 - ] - }, { - object: 'otherObjectId', - key: 'raw', - operation: 'greaterThan', - values: [ - 20 - ] - }] - }); - - telemetryState.objectId.lastDatum.value = 5; - telemetryState.otherObjectId.lastDatum.value = 5; - expect(rule.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 5; - telemetryState.otherObjectId.lastDatum.value = 25; - expect(rule.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 15; - telemetryState.otherObjectId.lastDatum.value = 5; - expect(rule.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 15; - telemetryState.otherObjectId.lastDatum.value = 25; - expect(rule.evaluate(telemetryState)).toBe(true); - - }); - - it('can combine multiple conditions with any', function () { - rule = new SummaryWidgetRule({ - trigger: 'any', - conditions: [{ - object: 'objectId', - key: 'raw', - operation: 'greaterThan', - values: [ - 10 - ] - }, { - object: 'otherObjectId', - key: 'raw', - operation: 'greaterThan', - values: [ - 20 - ] - }] - }); - - telemetryState.objectId.lastDatum.value = 5; - telemetryState.otherObjectId.lastDatum.value = 5; - expect(rule.evaluate(telemetryState)).toBe(false); - telemetryState.objectId.lastDatum.value = 5; - telemetryState.otherObjectId.lastDatum.value = 25; - expect(rule.evaluate(telemetryState)).toBe(true); - telemetryState.objectId.lastDatum.value = 15; - telemetryState.otherObjectId.lastDatum.value = 5; - expect(rule.evaluate(telemetryState)).toBe(true); - telemetryState.objectId.lastDatum.value = 15; - telemetryState.otherObjectId.lastDatum.value = 25; - expect(rule.evaluate(telemetryState)).toBe(true); - }); + telemetryState = { + objectId: { + formats: formatMap, + lastDatum: {} + }, + otherObjectId: { + formats: formatMap, + lastDatum: {} + } + }; }); + + it('allows single condition rules with any', function () { + rule = new SummaryWidgetRule({ + trigger: 'any', + conditions: [ + { + object: 'objectId', + key: 'raw', + operation: 'greaterThan', + values: [10] + } + ] + }); + + telemetryState.objectId.lastDatum.value = 5; + expect(rule.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 15; + expect(rule.evaluate(telemetryState)).toBe(true); + }); + + it('allows single condition rules with all', function () { + rule = new SummaryWidgetRule({ + trigger: 'all', + conditions: [ + { + object: 'objectId', + key: 'raw', + operation: 'greaterThan', + values: [10] + } + ] + }); + + telemetryState.objectId.lastDatum.value = 5; + expect(rule.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 15; + expect(rule.evaluate(telemetryState)).toBe(true); + }); + + it('can combine multiple conditions with all', function () { + rule = new SummaryWidgetRule({ + trigger: 'all', + conditions: [ + { + object: 'objectId', + key: 'raw', + operation: 'greaterThan', + values: [10] + }, + { + object: 'otherObjectId', + key: 'raw', + operation: 'greaterThan', + values: [20] + } + ] + }); + + telemetryState.objectId.lastDatum.value = 5; + telemetryState.otherObjectId.lastDatum.value = 5; + expect(rule.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 5; + telemetryState.otherObjectId.lastDatum.value = 25; + expect(rule.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 15; + telemetryState.otherObjectId.lastDatum.value = 5; + expect(rule.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 15; + telemetryState.otherObjectId.lastDatum.value = 25; + expect(rule.evaluate(telemetryState)).toBe(true); + }); + + it('can combine multiple conditions with any', function () { + rule = new SummaryWidgetRule({ + trigger: 'any', + conditions: [ + { + object: 'objectId', + key: 'raw', + operation: 'greaterThan', + values: [10] + }, + { + object: 'otherObjectId', + key: 'raw', + operation: 'greaterThan', + values: [20] + } + ] + }); + + telemetryState.objectId.lastDatum.value = 5; + telemetryState.otherObjectId.lastDatum.value = 5; + expect(rule.evaluate(telemetryState)).toBe(false); + telemetryState.objectId.lastDatum.value = 5; + telemetryState.otherObjectId.lastDatum.value = 25; + expect(rule.evaluate(telemetryState)).toBe(true); + telemetryState.objectId.lastDatum.value = 15; + telemetryState.otherObjectId.lastDatum.value = 5; + expect(rule.evaluate(telemetryState)).toBe(true); + telemetryState.objectId.lastDatum.value = 15; + telemetryState.otherObjectId.lastDatum.value = 25; + expect(rule.evaluate(telemetryState)).toBe(true); + }); + }); }); diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProvider.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProvider.js index 376b7ba379..13a3737360 100644 --- a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProvider.js +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProvider.js @@ -20,48 +20,44 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './EvaluatorPool' -], function ( - EvaluatorPool -) { +define(['./EvaluatorPool'], function (EvaluatorPool) { + function SummaryWidgetTelemetryProvider(openmct) { + this.pool = new EvaluatorPool(openmct); + } - function SummaryWidgetTelemetryProvider(openmct) { - this.pool = new EvaluatorPool(openmct); + SummaryWidgetTelemetryProvider.prototype.supportsRequest = function (domainObject, options) { + return domainObject.type === 'summary-widget'; + }; + + SummaryWidgetTelemetryProvider.prototype.request = function (domainObject, options) { + if (options.strategy !== 'latest' && options.size !== 1) { + return Promise.resolve([]); } - SummaryWidgetTelemetryProvider.prototype.supportsRequest = function (domainObject, options) { - return domainObject.type === 'summary-widget'; - }; + const evaluator = this.pool.get(domainObject); - SummaryWidgetTelemetryProvider.prototype.request = function (domainObject, options) { - if (options.strategy !== 'latest' && options.size !== 1) { - return Promise.resolve([]); - } + return evaluator.requestLatest(options).then( + function (latestDatum) { + this.pool.release(evaluator); - const evaluator = this.pool.get(domainObject); + return latestDatum ? [latestDatum] : []; + }.bind(this) + ); + }; - return evaluator.requestLatest(options) - .then(function (latestDatum) { - this.pool.release(evaluator); + SummaryWidgetTelemetryProvider.prototype.supportsSubscribe = function (domainObject) { + return domainObject.type === 'summary-widget'; + }; - return latestDatum ? [latestDatum] : []; - }.bind(this)); - }; + SummaryWidgetTelemetryProvider.prototype.subscribe = function (domainObject, callback) { + const evaluator = this.pool.get(domainObject); + const unsubscribe = evaluator.subscribe(callback); - SummaryWidgetTelemetryProvider.prototype.supportsSubscribe = function (domainObject) { - return domainObject.type === 'summary-widget'; - }; + return function () { + this.pool.release(evaluator); + unsubscribe(); + }.bind(this); + }; - SummaryWidgetTelemetryProvider.prototype.subscribe = function (domainObject, callback) { - const evaluator = this.pool.get(domainObject); - const unsubscribe = evaluator.subscribe(callback); - - return function () { - this.pool.release(evaluator); - unsubscribe(); - }.bind(this); - }; - - return SummaryWidgetTelemetryProvider; + return SummaryWidgetTelemetryProvider; }); diff --git a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProviderSpec.js b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProviderSpec.js index e563065fa8..ec7853f130 100644 --- a/src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProviderSpec.js +++ b/src/plugins/summaryWidget/src/telemetry/SummaryWidgetTelemetryProviderSpec.js @@ -20,462 +20,444 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './SummaryWidgetTelemetryProvider' -], function ( - SummaryWidgetTelemetryProvider -) { +define(['./SummaryWidgetTelemetryProvider'], function (SummaryWidgetTelemetryProvider) { + xdescribe('SummaryWidgetTelemetryProvider', function () { + let telemObjectA; + let telemObjectB; + let summaryWidgetObject; + let openmct; + let telemUnsubscribes; + let unobserver; + let composition; + let telemetryProvider; + let loader; - xdescribe('SummaryWidgetTelemetryProvider', function () { - let telemObjectA; - let telemObjectB; - let summaryWidgetObject; - let openmct; - let telemUnsubscribes; - let unobserver; - let composition; - let telemetryProvider; - let loader; - - beforeEach(function () { - telemObjectA = { - identifier: { - namespace: 'a', - key: 'telem' + beforeEach(function () { + telemObjectA = { + identifier: { + namespace: 'a', + key: 'telem' + } + }; + telemObjectB = { + identifier: { + namespace: 'b', + key: 'telem' + } + }; + summaryWidgetObject = { + name: 'Summary Widget', + type: 'summary-widget', + identifier: { + namespace: 'base', + key: 'widgetId' + }, + composition: ['a:telem', 'b:telem'], + configuration: { + ruleOrder: ['default', 'rule0', 'rule1'], + ruleConfigById: { + default: { + name: 'safe', + label: "Don't Worry", + message: "It's Ok", + id: 'default', + icon: 'a-ok', + style: { + color: '#ffffff', + 'background-color': '#38761d', + 'border-color': 'rgba(0,0,0,0)' + }, + conditions: [ + { + object: '', + key: '', + operation: '', + values: [] } - }; - telemObjectB = { - identifier: { - namespace: 'b', - key: 'telem' + ], + trigger: 'any' + }, + rule0: { + name: 'A High', + label: 'Start Worrying', + message: 'A is a little high...', + id: 'rule0', + icon: 'a-high', + style: { + color: '#000000', + 'background-color': '#ffff00', + 'border-color': 'rgba(1,1,0,0)' + }, + conditions: [ + { + object: 'a:telem', + key: 'measurement', + operation: 'greaterThan', + values: [50] } - }; - summaryWidgetObject = { - name: "Summary Widget", - type: "summary-widget", - identifier: { - namespace: 'base', - key: 'widgetId' - }, - composition: [ - 'a:telem', - 'b:telem' - ], - configuration: { - ruleOrder: [ - "default", - "rule0", - "rule1" - ], - ruleConfigById: { - "default": { - name: "safe", - label: "Don't Worry", - message: "It's Ok", - id: "default", - icon: "a-ok", - style: { - "color": "#ffffff", - "background-color": "#38761d", - "border-color": "rgba(0,0,0,0)" - }, - conditions: [ - { - object: "", - key: "", - operation: "", - values: [] - } - ], - trigger: "any" - }, - "rule0": { - name: "A High", - label: "Start Worrying", - message: "A is a little high...", - id: "rule0", - icon: "a-high", - style: { - "color": "#000000", - "background-color": "#ffff00", - "border-color": "rgba(1,1,0,0)" - }, - conditions: [ - { - object: "a:telem", - key: "measurement", - operation: "greaterThan", - values: [ - 50 - ] - } - ], - trigger: "any" - }, - rule1: { - name: "B Low", - label: "WORRY!", - message: "B is Low", - id: "rule1", - icon: "b-low", - style: { - "color": "#ff00ff", - "background-color": "#ff0000", - "border-color": "rgba(1,0,0,0)" - }, - conditions: [ - { - object: "b:telem", - key: "measurement", - operation: "lessThan", - values: [ - 10 - ] - } - ], - trigger: "any" - } - } + ], + trigger: 'any' + }, + rule1: { + name: 'B Low', + label: 'WORRY!', + message: 'B is Low', + id: 'rule1', + icon: 'b-low', + style: { + color: '#ff00ff', + 'background-color': '#ff0000', + 'border-color': 'rgba(1,0,0,0)' + }, + conditions: [ + { + object: 'b:telem', + key: 'measurement', + operation: 'lessThan', + values: [10] } - }; - openmct = { - objects: jasmine.createSpyObj('objectAPI', [ - 'get', - 'observe' - ]), - telemetry: jasmine.createSpyObj('telemetryAPI', [ - 'getMetadata', - 'getFormatMap', - 'request', - 'subscribe', - 'addProvider' - ]), - composition: jasmine.createSpyObj('compositionAPI', [ - 'get' - ]), - time: jasmine.createSpyObj('timeAPI', [ - 'getAllTimeSystems', - 'timeSystem' - ]) - }; - - openmct.time.getAllTimeSystems.and.returnValue([{key: 'timestamp'}]); - openmct.time.timeSystem.and.returnValue({key: 'timestamp'}); - - unobserver = jasmine.createSpy('unobserver'); - openmct.objects.observe.and.returnValue(unobserver); - - composition = jasmine.createSpyObj('compositionCollection', [ - 'on', - 'off', - 'load' - ]); - - function notify(eventName, a, b) { - composition.on.calls.all().filter(function (c) { - return c.args[0] === eventName; - }).forEach(function (c) { - if (c.args[2]) { // listener w/ context. - c.args[1].call(c.args[2], a, b); - } else { // listener w/o context. - c.args[1](a, b); - } - }); + ], + trigger: 'any' } + } + } + }; + openmct = { + objects: jasmine.createSpyObj('objectAPI', ['get', 'observe']), + telemetry: jasmine.createSpyObj('telemetryAPI', [ + 'getMetadata', + 'getFormatMap', + 'request', + 'subscribe', + 'addProvider' + ]), + composition: jasmine.createSpyObj('compositionAPI', ['get']), + time: jasmine.createSpyObj('timeAPI', ['getAllTimeSystems', 'timeSystem']) + }; - loader = {}; - loader.promise = new Promise(function (resolve, reject) { - loader.resolve = resolve; - loader.reject = reject; + openmct.time.getAllTimeSystems.and.returnValue([{ key: 'timestamp' }]); + openmct.time.timeSystem.and.returnValue({ key: 'timestamp' }); + + unobserver = jasmine.createSpy('unobserver'); + openmct.objects.observe.and.returnValue(unobserver); + + composition = jasmine.createSpyObj('compositionCollection', ['on', 'off', 'load']); + + function notify(eventName, a, b) { + composition.on.calls + .all() + .filter(function (c) { + return c.args[0] === eventName; + }) + .forEach(function (c) { + if (c.args[2]) { + // listener w/ context. + c.args[1].call(c.args[2], a, b); + } else { + // listener w/o context. + c.args[1](a, b); + } + }); + } + + loader = {}; + loader.promise = new Promise(function (resolve, reject) { + loader.resolve = resolve; + loader.reject = reject; + }); + + composition.load.and.callFake(function () { + setTimeout(function () { + notify('add', telemObjectA); + setTimeout(function () { + notify('add', telemObjectB); + setTimeout(function () { + loader.resolve(); }); - - composition.load.and.callFake(function () { - setTimeout(function () { - notify('add', telemObjectA); - setTimeout(function () { - notify('add', telemObjectB); - setTimeout(function () { - loader.resolve(); - }); - }); - }); - - return loader.promise; - }); - openmct.composition.get.and.returnValue(composition); - - telemUnsubscribes = []; - openmct.telemetry.subscribe.and.callFake(function () { - const unsubscriber = jasmine.createSpy('unsubscriber' + telemUnsubscribes.length); - telemUnsubscribes.push(unsubscriber); - - return unsubscriber; - }); - - openmct.telemetry.getMetadata.and.callFake(function (object) { - return { - name: 'fake metadata manager', - object: object, - keys: ['timestamp', 'measurement'] - }; - }); - - openmct.telemetry.getFormatMap.and.callFake(function (metadata) { - expect(metadata.name).toBe('fake metadata manager'); - - return { - metadata: metadata, - timestamp: { - parse: function (datum) { - return datum.t; - } - }, - measurement: { - parse: function (datum) { - return datum.m; - } - } - }; - }); - telemetryProvider = new SummaryWidgetTelemetryProvider(openmct); + }); }); - it("supports subscription for summary widgets", function () { - expect(telemetryProvider.supportsSubscribe(summaryWidgetObject)) - .toBe(true); - }); + return loader.promise; + }); + openmct.composition.get.and.returnValue(composition); - it("supports requests for summary widgets", function () { - expect(telemetryProvider.supportsRequest(summaryWidgetObject)) - .toBe(true); - }); + telemUnsubscribes = []; + openmct.telemetry.subscribe.and.callFake(function () { + const unsubscriber = jasmine.createSpy('unsubscriber' + telemUnsubscribes.length); + telemUnsubscribes.push(unsubscriber); - it("does not support other requests or subscriptions", function () { - expect(telemetryProvider.supportsSubscribe(telemObjectA)) - .toBe(false); - expect(telemetryProvider.supportsRequest(telemObjectA)) - .toBe(false); - }); + return unsubscriber; + }); - it("Returns no results for basic requests", function () { - return telemetryProvider.request(summaryWidgetObject, {}) - .then(function (result) { - expect(result).toEqual([]); - }); - }); + openmct.telemetry.getMetadata.and.callFake(function (object) { + return { + name: 'fake metadata manager', + object: object, + keys: ['timestamp', 'measurement'] + }; + }); - it('provides realtime telemetry', function () { - const callback = jasmine.createSpy('callback'); - telemetryProvider.subscribe(summaryWidgetObject, callback); - - return loader.promise.then(function () { - return new Promise(function (resolve) { - setTimeout(resolve); - }); - }).then(function () { - expect(openmct.telemetry.subscribe.calls.count()).toBe(2); - expect(openmct.telemetry.subscribe) - .toHaveBeenCalledWith(telemObjectA, jasmine.any(Function)); - expect(openmct.telemetry.subscribe) - .toHaveBeenCalledWith(telemObjectB, jasmine.any(Function)); - - const aCallback = openmct.telemetry.subscribe.calls.all()[0].args[1]; - const bCallback = openmct.telemetry.subscribe.calls.all()[1].args[1]; - - aCallback({ - t: 123, - m: 25 - }); - expect(callback).not.toHaveBeenCalled(); - bCallback({ - t: 123, - m: 25 - }); - expect(callback).toHaveBeenCalledWith({ - timestamp: 123, - ruleLabel: "Don't Worry", - ruleName: "safe", - message: "It's Ok", - ruleIndex: 0, - backgroundColor: '#38761d', - textColor: '#ffffff', - borderColor: 'rgba(0,0,0,0)', - icon: 'a-ok' - }); - - aCallback({ - t: 140, - m: 55 - }); - expect(callback).toHaveBeenCalledWith({ - timestamp: 140, - ruleLabel: "Start Worrying", - ruleName: "A High", - message: "A is a little high...", - ruleIndex: 1, - backgroundColor: '#ffff00', - textColor: '#000000', - borderColor: 'rgba(1,1,0,0)', - icon: 'a-high' - }); - - bCallback({ - t: 140, - m: -10 - }); - expect(callback).toHaveBeenCalledWith({ - timestamp: 140, - ruleLabel: "WORRY!", - ruleName: "B Low", - message: "B is Low", - ruleIndex: 2, - backgroundColor: '#ff0000', - textColor: '#ff00ff', - borderColor: 'rgba(1,0,0,0)', - icon: 'b-low' - }); - - aCallback({ - t: 160, - m: 25 - }); - expect(callback).toHaveBeenCalledWith({ - timestamp: 160, - ruleLabel: "WORRY!", - ruleName: "B Low", - message: "B is Low", - ruleIndex: 2, - backgroundColor: '#ff0000', - textColor: '#ff00ff', - borderColor: 'rgba(1,0,0,0)', - icon: 'b-low' - }); - - bCallback({ - t: 160, - m: 25 - }); - expect(callback).toHaveBeenCalledWith({ - timestamp: 160, - ruleLabel: "Don't Worry", - ruleName: "safe", - message: "It's Ok", - ruleIndex: 0, - backgroundColor: '#38761d', - textColor: '#ffffff', - borderColor: 'rgba(0,0,0,0)', - icon: 'a-ok' - }); - }); - }); - - describe('providing lad telemetry', function () { - let responseDatums; - let resultsShouldBe; - - beforeEach(function () { - openmct.telemetry.request.and.callFake(function (rObj, options) { - expect(rObj).toEqual(jasmine.any(Object)); - expect(options).toEqual({ - size: 1, - strategy: 'latest', - domain: 'timestamp' - }); - expect(responseDatums[rObj.identifier.namespace]).toBeDefined(); - - return Promise.resolve([responseDatums[rObj.identifier.namespace]]); - }); - responseDatums = {}; - - resultsShouldBe = function (results) { - return telemetryProvider - .request(summaryWidgetObject, { - size: 1, - strategy: 'latest', - domain: 'timestamp' - }) - .then(function (r) { - expect(r).toEqual(results); - }); - }; - }); - - it("returns default when no rule matches", function () { - responseDatums = { - a: { - t: 122, - m: 25 - }, - b: { - t: 111, - m: 25 - } - }; - - return resultsShouldBe([{ - timestamp: 122, - ruleLabel: "Don't Worry", - ruleName: "safe", - message: "It's Ok", - ruleIndex: 0, - backgroundColor: '#38761d', - textColor: '#ffffff', - borderColor: 'rgba(0,0,0,0)', - icon: 'a-ok' - }]); - }); - - it("returns highest priority when multiple match", function () { - responseDatums = { - a: { - t: 131, - m: 55 - }, - b: { - t: 139, - m: 5 - } - }; - - return resultsShouldBe([{ - timestamp: 139, - ruleLabel: "WORRY!", - ruleName: "B Low", - message: "B is Low", - ruleIndex: 2, - backgroundColor: '#ff0000', - textColor: '#ff00ff', - borderColor: 'rgba(1,0,0,0)', - icon: 'b-low' - }]); - }); - - it("returns matching rule", function () { - responseDatums = { - a: { - t: 144, - m: 55 - }, - b: { - t: 141, - m: 15 - } - }; - - return resultsShouldBe([{ - timestamp: 144, - ruleLabel: "Start Worrying", - ruleName: "A High", - message: "A is a little high...", - ruleIndex: 1, - backgroundColor: '#ffff00', - textColor: '#000000', - borderColor: 'rgba(1,1,0,0)', - icon: 'a-high' - }]); - }); - - }); + openmct.telemetry.getFormatMap.and.callFake(function (metadata) { + expect(metadata.name).toBe('fake metadata manager'); + return { + metadata: metadata, + timestamp: { + parse: function (datum) { + return datum.t; + } + }, + measurement: { + parse: function (datum) { + return datum.m; + } + } + }; + }); + telemetryProvider = new SummaryWidgetTelemetryProvider(openmct); }); + + it('supports subscription for summary widgets', function () { + expect(telemetryProvider.supportsSubscribe(summaryWidgetObject)).toBe(true); + }); + + it('supports requests for summary widgets', function () { + expect(telemetryProvider.supportsRequest(summaryWidgetObject)).toBe(true); + }); + + it('does not support other requests or subscriptions', function () { + expect(telemetryProvider.supportsSubscribe(telemObjectA)).toBe(false); + expect(telemetryProvider.supportsRequest(telemObjectA)).toBe(false); + }); + + it('Returns no results for basic requests', function () { + return telemetryProvider.request(summaryWidgetObject, {}).then(function (result) { + expect(result).toEqual([]); + }); + }); + + it('provides realtime telemetry', function () { + const callback = jasmine.createSpy('callback'); + telemetryProvider.subscribe(summaryWidgetObject, callback); + + return loader.promise + .then(function () { + return new Promise(function (resolve) { + setTimeout(resolve); + }); + }) + .then(function () { + expect(openmct.telemetry.subscribe.calls.count()).toBe(2); + expect(openmct.telemetry.subscribe).toHaveBeenCalledWith( + telemObjectA, + jasmine.any(Function) + ); + expect(openmct.telemetry.subscribe).toHaveBeenCalledWith( + telemObjectB, + jasmine.any(Function) + ); + + const aCallback = openmct.telemetry.subscribe.calls.all()[0].args[1]; + const bCallback = openmct.telemetry.subscribe.calls.all()[1].args[1]; + + aCallback({ + t: 123, + m: 25 + }); + expect(callback).not.toHaveBeenCalled(); + bCallback({ + t: 123, + m: 25 + }); + expect(callback).toHaveBeenCalledWith({ + timestamp: 123, + ruleLabel: "Don't Worry", + ruleName: 'safe', + message: "It's Ok", + ruleIndex: 0, + backgroundColor: '#38761d', + textColor: '#ffffff', + borderColor: 'rgba(0,0,0,0)', + icon: 'a-ok' + }); + + aCallback({ + t: 140, + m: 55 + }); + expect(callback).toHaveBeenCalledWith({ + timestamp: 140, + ruleLabel: 'Start Worrying', + ruleName: 'A High', + message: 'A is a little high...', + ruleIndex: 1, + backgroundColor: '#ffff00', + textColor: '#000000', + borderColor: 'rgba(1,1,0,0)', + icon: 'a-high' + }); + + bCallback({ + t: 140, + m: -10 + }); + expect(callback).toHaveBeenCalledWith({ + timestamp: 140, + ruleLabel: 'WORRY!', + ruleName: 'B Low', + message: 'B is Low', + ruleIndex: 2, + backgroundColor: '#ff0000', + textColor: '#ff00ff', + borderColor: 'rgba(1,0,0,0)', + icon: 'b-low' + }); + + aCallback({ + t: 160, + m: 25 + }); + expect(callback).toHaveBeenCalledWith({ + timestamp: 160, + ruleLabel: 'WORRY!', + ruleName: 'B Low', + message: 'B is Low', + ruleIndex: 2, + backgroundColor: '#ff0000', + textColor: '#ff00ff', + borderColor: 'rgba(1,0,0,0)', + icon: 'b-low' + }); + + bCallback({ + t: 160, + m: 25 + }); + expect(callback).toHaveBeenCalledWith({ + timestamp: 160, + ruleLabel: "Don't Worry", + ruleName: 'safe', + message: "It's Ok", + ruleIndex: 0, + backgroundColor: '#38761d', + textColor: '#ffffff', + borderColor: 'rgba(0,0,0,0)', + icon: 'a-ok' + }); + }); + }); + + describe('providing lad telemetry', function () { + let responseDatums; + let resultsShouldBe; + + beforeEach(function () { + openmct.telemetry.request.and.callFake(function (rObj, options) { + expect(rObj).toEqual(jasmine.any(Object)); + expect(options).toEqual({ + size: 1, + strategy: 'latest', + domain: 'timestamp' + }); + expect(responseDatums[rObj.identifier.namespace]).toBeDefined(); + + return Promise.resolve([responseDatums[rObj.identifier.namespace]]); + }); + responseDatums = {}; + + resultsShouldBe = function (results) { + return telemetryProvider + .request(summaryWidgetObject, { + size: 1, + strategy: 'latest', + domain: 'timestamp' + }) + .then(function (r) { + expect(r).toEqual(results); + }); + }; + }); + + it('returns default when no rule matches', function () { + responseDatums = { + a: { + t: 122, + m: 25 + }, + b: { + t: 111, + m: 25 + } + }; + + return resultsShouldBe([ + { + timestamp: 122, + ruleLabel: "Don't Worry", + ruleName: 'safe', + message: "It's Ok", + ruleIndex: 0, + backgroundColor: '#38761d', + textColor: '#ffffff', + borderColor: 'rgba(0,0,0,0)', + icon: 'a-ok' + } + ]); + }); + + it('returns highest priority when multiple match', function () { + responseDatums = { + a: { + t: 131, + m: 55 + }, + b: { + t: 139, + m: 5 + } + }; + + return resultsShouldBe([ + { + timestamp: 139, + ruleLabel: 'WORRY!', + ruleName: 'B Low', + message: 'B is Low', + ruleIndex: 2, + backgroundColor: '#ff0000', + textColor: '#ff00ff', + borderColor: 'rgba(1,0,0,0)', + icon: 'b-low' + } + ]); + }); + + it('returns matching rule', function () { + responseDatums = { + a: { + t: 144, + m: 55 + }, + b: { + t: 141, + m: 15 + } + }; + + return resultsShouldBe([ + { + timestamp: 144, + ruleLabel: 'Start Worrying', + ruleName: 'A High', + message: 'A is a little high...', + ruleIndex: 1, + backgroundColor: '#ffff00', + textColor: '#000000', + borderColor: 'rgba(1,1,0,0)', + icon: 'a-high' + } + ]); + }); + }); + }); }); diff --git a/src/plugins/summaryWidget/src/telemetry/operations.js b/src/plugins/summaryWidget/src/telemetry/operations.js index 534a5b1d87..01f87c2179 100644 --- a/src/plugins/summaryWidget/src/telemetry/operations.js +++ b/src/plugins/summaryWidget/src/telemetry/operations.js @@ -20,200 +20,196 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ +define([], function () { + const OPERATIONS = { + equalTo: { + operation: function (input) { + return input[0] === input[1]; + }, + text: 'is equal to', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' == ' + values[0]; + } + }, + notEqualTo: { + operation: function (input) { + return input[0] !== input[1]; + }, + text: 'is not equal to', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' != ' + values[0]; + } + }, + greaterThan: { + operation: function (input) { + return input[0] > input[1]; + }, + text: 'is greater than', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' > ' + values[0]; + } + }, + lessThan: { + operation: function (input) { + return input[0] < input[1]; + }, + text: 'is less than', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' < ' + values[0]; + } + }, + greaterThanOrEq: { + operation: function (input) { + return input[0] >= input[1]; + }, + text: 'is greater than or equal to', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' >= ' + values[0]; + } + }, + lessThanOrEq: { + operation: function (input) { + return input[0] <= input[1]; + }, + text: 'is less than or equal to', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' <= ' + values[0]; + } + }, + between: { + operation: function (input) { + return input[0] > input[1] && input[0] < input[2]; + }, + text: 'is between', + appliesTo: ['number'], + inputCount: 2, + getDescription: function (values) { + return ' between ' + values[0] + ' and ' + values[1]; + } + }, + notBetween: { + operation: function (input) { + return input[0] < input[1] || input[0] > input[2]; + }, + text: 'is not between', + appliesTo: ['number'], + inputCount: 2, + getDescription: function (values) { + return ' not between ' + values[0] + ' and ' + values[1]; + } + }, + textContains: { + operation: function (input) { + return input[0] && input[1] && input[0].includes(input[1]); + }, + text: 'text contains', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' contains ' + values[0]; + } + }, + textDoesNotContain: { + operation: function (input) { + return input[0] && input[1] && !input[0].includes(input[1]); + }, + text: 'text does not contain', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' does not contain ' + values[0]; + } + }, + textStartsWith: { + operation: function (input) { + return input[0].startsWith(input[1]); + }, + text: 'text starts with', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' starts with ' + values[0]; + } + }, + textEndsWith: { + operation: function (input) { + return input[0].endsWith(input[1]); + }, + text: 'text ends with', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' ends with ' + values[0]; + } + }, + textIsExactly: { + operation: function (input) { + return input[0] === input[1]; + }, + text: 'text is exactly', + appliesTo: ['string'], + inputCount: 1, + getDescription: function (values) { + return ' is exactly ' + values[0]; + } + }, + isUndefined: { + operation: function (input) { + return typeof input[0] === 'undefined'; + }, + text: 'is undefined', + appliesTo: ['string', 'number', 'enum'], + inputCount: 0, + getDescription: function () { + return ' is undefined'; + } + }, + isDefined: { + operation: function (input) { + return typeof input[0] !== 'undefined'; + }, + text: 'is defined', + appliesTo: ['string', 'number', 'enum'], + inputCount: 0, + getDescription: function () { + return ' is defined'; + } + }, + enumValueIs: { + operation: function (input) { + return input[0] === input[1]; + }, + text: 'is', + appliesTo: ['enum'], + inputCount: 1, + getDescription: function (values) { + return ' == ' + values[0]; + } + }, + enumValueIsNot: { + operation: function (input) { + return input[0] !== input[1]; + }, + text: 'is not', + appliesTo: ['enum'], + inputCount: 1, + getDescription: function (values) { + return ' != ' + values[0]; + } + } + }; -], function ( - -) { - const OPERATIONS = { - equalTo: { - operation: function (input) { - return input[0] === input[1]; - }, - text: 'is equal to', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' == ' + values[0]; - } - }, - notEqualTo: { - operation: function (input) { - return input[0] !== input[1]; - }, - text: 'is not equal to', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' != ' + values[0]; - } - }, - greaterThan: { - operation: function (input) { - return input[0] > input[1]; - }, - text: 'is greater than', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' > ' + values[0]; - } - }, - lessThan: { - operation: function (input) { - return input[0] < input[1]; - }, - text: 'is less than', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' < ' + values[0]; - } - }, - greaterThanOrEq: { - operation: function (input) { - return input[0] >= input[1]; - }, - text: 'is greater than or equal to', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' >= ' + values[0]; - } - }, - lessThanOrEq: { - operation: function (input) { - return input[0] <= input[1]; - }, - text: 'is less than or equal to', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' <= ' + values[0]; - } - }, - between: { - operation: function (input) { - return input[0] > input[1] && input[0] < input[2]; - }, - text: 'is between', - appliesTo: ['number'], - inputCount: 2, - getDescription: function (values) { - return ' between ' + values[0] + ' and ' + values[1]; - } - }, - notBetween: { - operation: function (input) { - return input[0] < input[1] || input[0] > input[2]; - }, - text: 'is not between', - appliesTo: ['number'], - inputCount: 2, - getDescription: function (values) { - return ' not between ' + values[0] + ' and ' + values[1]; - } - }, - textContains: { - operation: function (input) { - return input[0] && input[1] && input[0].includes(input[1]); - }, - text: 'text contains', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' contains ' + values[0]; - } - }, - textDoesNotContain: { - operation: function (input) { - return input[0] && input[1] && !input[0].includes(input[1]); - }, - text: 'text does not contain', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' does not contain ' + values[0]; - } - }, - textStartsWith: { - operation: function (input) { - return input[0].startsWith(input[1]); - }, - text: 'text starts with', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' starts with ' + values[0]; - } - }, - textEndsWith: { - operation: function (input) { - return input[0].endsWith(input[1]); - }, - text: 'text ends with', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' ends with ' + values[0]; - } - }, - textIsExactly: { - operation: function (input) { - return input[0] === input[1]; - }, - text: 'text is exactly', - appliesTo: ['string'], - inputCount: 1, - getDescription: function (values) { - return ' is exactly ' + values[0]; - } - }, - isUndefined: { - operation: function (input) { - return typeof input[0] === 'undefined'; - }, - text: 'is undefined', - appliesTo: ['string', 'number', 'enum'], - inputCount: 0, - getDescription: function () { - return ' is undefined'; - } - }, - isDefined: { - operation: function (input) { - return typeof input[0] !== 'undefined'; - }, - text: 'is defined', - appliesTo: ['string', 'number', 'enum'], - inputCount: 0, - getDescription: function () { - return ' is defined'; - } - }, - enumValueIs: { - operation: function (input) { - return input[0] === input[1]; - }, - text: 'is', - appliesTo: ['enum'], - inputCount: 1, - getDescription: function (values) { - return ' == ' + values[0]; - } - }, - enumValueIsNot: { - operation: function (input) { - return input[0] !== input[1]; - }, - text: 'is not', - appliesTo: ['enum'], - inputCount: 1, - getDescription: function (values) { - return ' != ' + values[0]; - } - } - }; - - return OPERATIONS; + return OPERATIONS; }); diff --git a/src/plugins/summaryWidget/src/views/SummaryWidgetView.js b/src/plugins/summaryWidget/src/views/SummaryWidgetView.js index b59e521fd3..a3dff12703 100644 --- a/src/plugins/summaryWidget/src/views/SummaryWidgetView.js +++ b/src/plugins/summaryWidget/src/views/SummaryWidgetView.js @@ -1,103 +1,106 @@ -define([ - './summary-widget.html', - '@braintree/sanitize-url' -], function ( - summaryWidgetTemplate, - urlSanitizeLib +define(['./summary-widget.html', '@braintree/sanitize-url'], function ( + summaryWidgetTemplate, + urlSanitizeLib ) { - const WIDGET_ICON_CLASS = 'c-sw__icon js-sw__icon'; + const WIDGET_ICON_CLASS = 'c-sw__icon js-sw__icon'; - function SummaryWidgetView(domainObject, openmct) { - this.openmct = openmct; - this.domainObject = domainObject; - this.hasUpdated = false; - this.render = this.render.bind(this); + function SummaryWidgetView(domainObject, openmct) { + this.openmct = openmct; + this.domainObject = domainObject; + this.hasUpdated = false; + this.render = this.render.bind(this); + } + + SummaryWidgetView.prototype.updateState = function (datum) { + this.hasUpdated = true; + this.widget.style.color = datum.textColor; + this.widget.style.backgroundColor = datum.backgroundColor; + this.widget.style.borderColor = datum.borderColor; + this.widget.title = datum.message; + this.label.title = datum.message; + this.label.innerHTML = datum.ruleLabel; + this.icon.className = WIDGET_ICON_CLASS + ' ' + datum.icon; + }; + + SummaryWidgetView.prototype.render = function () { + if (this.unsubscribe) { + this.unsubscribe(); } - SummaryWidgetView.prototype.updateState = function (datum) { - this.hasUpdated = true; - this.widget.style.color = datum.textColor; - this.widget.style.backgroundColor = datum.backgroundColor; - this.widget.style.borderColor = datum.borderColor; - this.widget.title = datum.message; - this.label.title = datum.message; - this.label.innerHTML = datum.ruleLabel; - this.icon.className = WIDGET_ICON_CLASS + ' ' + datum.icon; - }; + this.hasUpdated = false; - SummaryWidgetView.prototype.render = function () { - if (this.unsubscribe) { - this.unsubscribe(); - } + this.container.innerHTML = summaryWidgetTemplate; + this.widget = this.container.querySelector('a'); + this.icon = this.container.querySelector('#widgetIcon'); + this.label = this.container.querySelector('.js-sw__label'); - this.hasUpdated = false; + let url = this.domainObject.url; + if (url) { + this.widget.setAttribute('href', urlSanitizeLib.sanitizeUrl(url)); + } else { + this.widget.removeAttribute('href'); + } - this.container.innerHTML = summaryWidgetTemplate; - this.widget = this.container.querySelector('a'); - this.icon = this.container.querySelector('#widgetIcon'); - this.label = this.container.querySelector('.js-sw__label'); + if (this.domainObject.openNewTab === 'newTab') { + this.widget.setAttribute('target', '_blank'); + } else { + this.widget.removeAttribute('target'); + } - let url = this.domainObject.url; - if (url) { - this.widget.setAttribute('href', urlSanitizeLib.sanitizeUrl(url)); - } else { - this.widget.removeAttribute('href'); - } + const renderTracker = {}; + this.renderTracker = renderTracker; + this.openmct.telemetry + .request(this.domainObject, { + strategy: 'latest', + size: 1 + }) + .then( + function (results) { + if ( + this.destroyed || + this.hasUpdated || + this.renderTracker !== renderTracker || + results.length === 0 + ) { + return; + } - if (this.domainObject.openNewTab === 'newTab') { - this.widget.setAttribute('target', '_blank'); - } else { - this.widget.removeAttribute('target'); - } + this.updateState(results[results.length - 1]); + }.bind(this) + ); - const renderTracker = {}; - this.renderTracker = renderTracker; - this.openmct.telemetry.request(this.domainObject, { - strategy: 'latest', - size: 1 - }).then(function (results) { - if (this.destroyed - || this.hasUpdated - || this.renderTracker !== renderTracker - || results.length === 0) { - return; - } + this.unsubscribe = this.openmct.telemetry.subscribe( + this.domainObject, + this.updateState.bind(this) + ); + }; - this.updateState(results[results.length - 1]); - }.bind(this)); + SummaryWidgetView.prototype.show = function (container) { + this.container = container; + this.render(); + this.removeMutationListener = this.openmct.objects.observe( + this.domainObject, + '*', + this.onMutation.bind(this) + ); + this.openmct.time.on('timeSystem', this.render); + }; - this.unsubscribe = this.openmct - .telemetry - .subscribe(this.domainObject, this.updateState.bind(this)); - }; + SummaryWidgetView.prototype.onMutation = function (domainObject) { + this.domainObject = domainObject; + this.render(); + }; - SummaryWidgetView.prototype.show = function (container) { - this.container = container; - this.render(); - this.removeMutationListener = this.openmct.objects.observe( - this.domainObject, - '*', - this.onMutation.bind(this) - ); - this.openmct.time.on('timeSystem', this.render); - }; - - SummaryWidgetView.prototype.onMutation = function (domainObject) { - this.domainObject = domainObject; - this.render(); - }; - - SummaryWidgetView.prototype.destroy = function (container) { - this.unsubscribe(); - this.removeMutationListener(); - this.openmct.time.off('timeSystem', this.render); - this.destroyed = true; - delete this.widget; - delete this.label; - delete this.openmct; - delete this.domainObject; - }; - - return SummaryWidgetView; + SummaryWidgetView.prototype.destroy = function (container) { + this.unsubscribe(); + this.removeMutationListener(); + this.openmct.time.off('timeSystem', this.render); + this.destroyed = true; + delete this.widget; + delete this.label; + delete this.openmct; + delete this.domainObject; + }; + return SummaryWidgetView; }); diff --git a/src/plugins/summaryWidget/src/views/SummaryWidgetViewProvider.js b/src/plugins/summaryWidget/src/views/SummaryWidgetViewProvider.js index db569a0e58..aa90739e64 100644 --- a/src/plugins/summaryWidget/src/views/SummaryWidgetViewProvider.js +++ b/src/plugins/summaryWidget/src/views/SummaryWidgetViewProvider.js @@ -1,43 +1,38 @@ -define([ - '../SummaryWidget', - './SummaryWidgetView', - 'objectUtils' -], function ( - SummaryWidgetEditView, - SummaryWidgetView, - objectUtils +define(['../SummaryWidget', './SummaryWidgetView', 'objectUtils'], function ( + SummaryWidgetEditView, + SummaryWidgetView, + objectUtils ) { + const DEFAULT_VIEW_PRIORITY = 100; + /** + * + */ + function SummaryWidgetViewProvider(openmct) { + return { + key: 'summary-widget-viewer', + name: 'Summary View', + cssClass: 'icon-summary-widget', + canView: function (domainObject) { + return domainObject.type === 'summary-widget'; + }, + canEdit: function (domainObject) { + return domainObject.type === 'summary-widget'; + }, + view: function (domainObject) { + return new SummaryWidgetView(domainObject, openmct); + }, + edit: function (domainObject) { + return new SummaryWidgetEditView(domainObject, openmct); + }, + priority: function (domainObject) { + if (domainObject.type === 'summary-widget') { + return Number.MAX_VALUE; + } else { + return DEFAULT_VIEW_PRIORITY; + } + } + }; + } - const DEFAULT_VIEW_PRIORITY = 100; - /** - * - */ - function SummaryWidgetViewProvider(openmct) { - return { - key: 'summary-widget-viewer', - name: 'Summary View', - cssClass: 'icon-summary-widget', - canView: function (domainObject) { - return domainObject.type === 'summary-widget'; - }, - canEdit: function (domainObject) { - return domainObject.type === 'summary-widget'; - }, - view: function (domainObject) { - return new SummaryWidgetView(domainObject, openmct); - }, - edit: function (domainObject) { - return new SummaryWidgetEditView(domainObject, openmct); - }, - priority: function (domainObject) { - if (domainObject.type === 'summary-widget') { - return Number.MAX_VALUE; - } else { - return DEFAULT_VIEW_PRIORITY; - } - } - }; - } - - return SummaryWidgetViewProvider; + return SummaryWidgetViewProvider; }); diff --git a/src/plugins/summaryWidget/src/views/summary-widget.html b/src/plugins/summaryWidget/src/views/summary-widget.html index 738fbaf08b..3278bf2781 100644 --- a/src/plugins/summaryWidget/src/views/summary-widget.html +++ b/src/plugins/summaryWidget/src/views/summary-widget.html @@ -1,4 +1,4 @@ -
    -
    Loading...
    -
    \ No newline at end of file +
    +
    Loading...
    + diff --git a/src/plugins/summaryWidget/test/ConditionEvaluatorSpec.js b/src/plugins/summaryWidget/test/ConditionEvaluatorSpec.js index 49ff638a49..b9fddbb463 100644 --- a/src/plugins/summaryWidget/test/ConditionEvaluatorSpec.js +++ b/src/plugins/summaryWidget/test/ConditionEvaluatorSpec.js @@ -1,340 +1,363 @@ define(['../src/ConditionEvaluator'], function (ConditionEvaluator) { - describe('A Summary Widget Rule Evaluator', function () { - let evaluator; - let testEvaluator; - let testOperation; - let mockCache; - let mockTestCache; - let mockComposition; - let mockConditions; - let mockConditionsEmpty; - let mockConditionsUndefined; - let mockConditionsAnyTrue; - let mockConditionsAllTrue; - let mockConditionsAnyFalse; - let mockConditionsAllFalse; - let mockOperations; + describe('A Summary Widget Rule Evaluator', function () { + let evaluator; + let testEvaluator; + let testOperation; + let mockCache; + let mockTestCache; + let mockComposition; + let mockConditions; + let mockConditionsEmpty; + let mockConditionsUndefined; + let mockConditionsAnyTrue; + let mockConditionsAllTrue; + let mockConditionsAnyFalse; + let mockConditionsAllFalse; + let mockOperations; - beforeEach(function () { - mockCache = { - a: { - alpha: 3, - beta: 9, - gamma: 'Testing 1 2 3' - }, - b: { - alpha: 44, - beta: 23, - gamma: 'Hello World' - }, - c: { - foo: 'bar', - iAm: 'The Walrus', - creature: { - type: 'Centaur' - } - } - }; - mockTestCache = { - a: { - alpha: 1, - beta: 1, - gamma: 'Testing 4 5 6' - }, - b: { - alpha: 2, - beta: 2, - gamma: 'Goodbye world' - } - }; - mockComposition = { - a: {}, - b: {}, - c: {} - }; - mockConditions = [{ - object: 'a', - key: 'alpha', - operation: 'greaterThan', - values: [2] - }, { - object: 'b', - key: 'gamma', - operation: 'lessThan', - values: [5] - }]; - mockConditionsEmpty = [{ - object: '', - key: '', - operation: '', - values: [] - }]; - mockConditionsUndefined = [{ - object: 'No Such Object', - key: '', - operation: '', - values: [] - }, { - object: 'a', - key: 'No Such Key', - operation: '', - values: [] - }, { - object: 'a', - key: 'alpha', - operation: 'No Such Operation', - values: [] - }, { - object: 'all', - key: 'Nonexistent Field', - operation: 'Random Operation', - values: [] - }, { - object: 'any', - key: 'Nonexistent Field', - operation: 'Whatever Operation', - values: [] - }]; - mockConditionsAnyTrue = [{ - object: 'any', - key: 'alpha', - operation: 'greaterThan', - values: [5] - }]; - mockConditionsAnyFalse = [{ - object: 'any', - key: 'alpha', - operation: 'greaterThan', - values: [1000] - }]; - mockConditionsAllFalse = [{ - object: 'all', - key: 'alpha', - operation: 'greaterThan', - values: [5] - }]; - mockConditionsAllTrue = [{ - object: 'all', - key: 'alpha', - operation: 'greaterThan', - values: [0] - }]; - mockOperations = { - greaterThan: { - operation: function (input) { - return input[0] > input[1]; - }, - text: 'is greater than', - appliesTo: ['number'], - inputCount: 1, - getDescription: function (values) { - return ' > ' + values[0]; - } - }, - lessThan: { - operation: function (input) { - return input[0] < input[1]; - }, - text: 'is less than', - appliesTo: ['number'], - inputCount: 1 - }, - textContains: { - operation: function (input) { - return input[0] && input[1] && input[0].includes(input[1]); - }, - text: 'text contains', - appliesTo: ['string'], - inputCount: 1 - }, - textIsExactly: { - operation: function (input) { - return input[0] === input[1]; - }, - text: 'text is exactly', - appliesTo: ['string'], - inputCount: 1 - }, - isHalfHorse: { - operation: function (input) { - return input[0].type === 'Centaur'; - }, - text: 'is Half Horse', - appliesTo: ['mythicalCreature'], - inputCount: 0, - getDescription: function () { - return 'is half horse'; - } - } - }; - evaluator = new ConditionEvaluator(mockCache, mockComposition); - testEvaluator = new ConditionEvaluator(mockCache, mockComposition); - evaluator.operations = mockOperations; - }); - - it('evaluates a condition when it has no configuration', function () { - expect(evaluator.execute(mockConditionsEmpty, 'any')).toEqual(false); - expect(evaluator.execute(mockConditionsEmpty, 'all')).toEqual(false); - }); - - it('correctly evaluates a set of conditions', function () { - expect(evaluator.execute(mockConditions, 'any')).toEqual(true); - expect(evaluator.execute(mockConditions, 'all')).toEqual(false); - }); - - it('correctly evaluates conditions involving "any telemetry"', function () { - expect(evaluator.execute(mockConditionsAnyTrue, 'any')).toEqual(true); - expect(evaluator.execute(mockConditionsAnyFalse, 'any')).toEqual(false); - }); - - it('correctly evaluates conditions involving "all telemetry"', function () { - expect(evaluator.execute(mockConditionsAllTrue, 'any')).toEqual(true); - expect(evaluator.execute(mockConditionsAllFalse, 'any')).toEqual(false); - }); - - it('handles malformed conditions gracefully', function () { - //if no conditions are fully defined, should return false for any mode - expect(evaluator.execute(mockConditionsUndefined, 'any')).toEqual(false); - expect(evaluator.execute(mockConditionsUndefined, 'all')).toEqual(false); - //these conditions are true: evaluator should ignore undefined conditions, - //and evaluate the rule as true - mockConditionsUndefined.push({ - object: 'a', - key: 'gamma', - operation: 'textContains', - values: ['Testing'] - }); - expect(evaluator.execute(mockConditionsUndefined, 'any')).toEqual(true); - mockConditionsUndefined.push({ - object: 'c', - key: 'iAm', - operation: 'textContains', - values: ['Walrus'] - }); - expect(evaluator.execute(mockConditionsUndefined, 'all')).toEqual(true); - }); - - it('gets the keys for possible operations', function () { - expect(evaluator.getOperationKeys()).toEqual( - ['greaterThan', 'lessThan', 'textContains', 'textIsExactly', 'isHalfHorse'] - ); - }); - - it('gets output text for a given operation', function () { - expect(evaluator.getOperationText('isHalfHorse')).toEqual('is Half Horse'); - }); - - it('correctly returns whether an operation applies to a given type', function () { - expect(evaluator.operationAppliesTo('isHalfHorse', 'mythicalCreature')).toEqual(true); - expect(evaluator.operationAppliesTo('isHalfHorse', 'spaceJunk')).toEqual(false); - }); - - it('returns the HTML input type associated with a given data type', function () { - expect(evaluator.getInputTypeById('string')).toEqual('text'); - }); - - it('gets the number of inputs required for a given operation', function () { - expect(evaluator.getInputCount('isHalfHorse')).toEqual(0); - expect(evaluator.getInputCount('greaterThan')).toEqual(1); - }); - - it('gets a human-readable description of a condition', function () { - expect(evaluator.getOperationDescription('isHalfHorse')).toEqual('is half horse'); - expect(evaluator.getOperationDescription('greaterThan', [1])).toEqual(' > 1'); - }); - - it('allows setting a substitute cache for testing purposes, and toggling its use', function () { - evaluator.setTestDataCache(mockTestCache); - evaluator.useTestData(true); - expect(evaluator.execute(mockConditions, 'any')).toEqual(false); - expect(evaluator.execute(mockConditions, 'all')).toEqual(false); - mockConditions.push({ - object: 'a', - key: 'gamma', - operation: 'textContains', - values: ['4 5 6'] - }); - expect(evaluator.execute(mockConditions, 'any')).toEqual(true); - expect(evaluator.execute(mockConditions, 'all')).toEqual(false); - mockConditions.pop(); - evaluator.useTestData(false); - expect(evaluator.execute(mockConditions, 'any')).toEqual(true); - expect(evaluator.execute(mockConditions, 'all')).toEqual(false); - }); - - it('supports all required operations', function () { - //equal to - testOperation = testEvaluator.operations.equalTo.operation; - expect(testOperation([33, 33])).toEqual(true); - expect(testOperation([55, 147])).toEqual(false); - //not equal to - testOperation = testEvaluator.operations.notEqualTo.operation; - expect(testOperation([33, 33])).toEqual(false); - expect(testOperation([55, 147])).toEqual(true); - //greater than - testOperation = testEvaluator.operations.greaterThan.operation; - expect(testOperation([100, 33])).toEqual(true); - expect(testOperation([33, 33])).toEqual(false); - expect(testOperation([55, 147])).toEqual(false); - //less than - testOperation = testEvaluator.operations.lessThan.operation; - expect(testOperation([100, 33])).toEqual(false); - expect(testOperation([33, 33])).toEqual(false); - expect(testOperation([55, 147])).toEqual(true); - //greater than or equal to - testOperation = testEvaluator.operations.greaterThanOrEq.operation; - expect(testOperation([100, 33])).toEqual(true); - expect(testOperation([33, 33])).toEqual(true); - expect(testOperation([55, 147])).toEqual(false); - //less than or equal to - testOperation = testEvaluator.operations.lessThanOrEq.operation; - expect(testOperation([100, 33])).toEqual(false); - expect(testOperation([33, 33])).toEqual(true); - expect(testOperation([55, 147])).toEqual(true); - //between - testOperation = testEvaluator.operations.between.operation; - expect(testOperation([100, 33, 66])).toEqual(false); - expect(testOperation([1, 33, 66])).toEqual(false); - expect(testOperation([45, 33, 66])).toEqual(true); - //not between - testOperation = testEvaluator.operations.notBetween.operation; - expect(testOperation([100, 33, 66])).toEqual(true); - expect(testOperation([1, 33, 66])).toEqual(true); - expect(testOperation([45, 33, 66])).toEqual(false); - //text contains - testOperation = testEvaluator.operations.textContains.operation; - expect(testOperation(['Testing', 'tin'])).toEqual(true); - expect(testOperation(['Testing', 'bind'])).toEqual(false); - //text does not contain - testOperation = testEvaluator.operations.textDoesNotContain.operation; - expect(testOperation(['Testing', 'tin'])).toEqual(false); - expect(testOperation(['Testing', 'bind'])).toEqual(true); - //text starts with - testOperation = testEvaluator.operations.textStartsWith.operation; - expect(testOperation(['Testing', 'Tes'])).toEqual(true); - expect(testOperation(['Testing', 'ting'])).toEqual(false); - //text ends with - testOperation = testEvaluator.operations.textEndsWith.operation; - expect(testOperation(['Testing', 'Tes'])).toEqual(false); - expect(testOperation(['Testing', 'ting'])).toEqual(true); - //text is exactly - testOperation = testEvaluator.operations.textIsExactly.operation; - expect(testOperation(['Testing', 'Testing'])).toEqual(true); - expect(testOperation(['Testing', 'Test'])).toEqual(false); - //undefined - testOperation = testEvaluator.operations.isUndefined.operation; - expect(testOperation([1])).toEqual(false); - expect(testOperation([])).toEqual(true); - //isDefined - testOperation = testEvaluator.operations.isDefined.operation; - expect(testOperation([1])).toEqual(true); - expect(testOperation([])).toEqual(false); - }); - - it('can produce a description for all supported operations', function () { - testEvaluator.getOperationKeys().forEach(function (key) { - expect(testEvaluator.getOperationDescription(key, [])).toBeDefined(); - }); - }); + beforeEach(function () { + mockCache = { + a: { + alpha: 3, + beta: 9, + gamma: 'Testing 1 2 3' + }, + b: { + alpha: 44, + beta: 23, + gamma: 'Hello World' + }, + c: { + foo: 'bar', + iAm: 'The Walrus', + creature: { + type: 'Centaur' + } + } + }; + mockTestCache = { + a: { + alpha: 1, + beta: 1, + gamma: 'Testing 4 5 6' + }, + b: { + alpha: 2, + beta: 2, + gamma: 'Goodbye world' + } + }; + mockComposition = { + a: {}, + b: {}, + c: {} + }; + mockConditions = [ + { + object: 'a', + key: 'alpha', + operation: 'greaterThan', + values: [2] + }, + { + object: 'b', + key: 'gamma', + operation: 'lessThan', + values: [5] + } + ]; + mockConditionsEmpty = [ + { + object: '', + key: '', + operation: '', + values: [] + } + ]; + mockConditionsUndefined = [ + { + object: 'No Such Object', + key: '', + operation: '', + values: [] + }, + { + object: 'a', + key: 'No Such Key', + operation: '', + values: [] + }, + { + object: 'a', + key: 'alpha', + operation: 'No Such Operation', + values: [] + }, + { + object: 'all', + key: 'Nonexistent Field', + operation: 'Random Operation', + values: [] + }, + { + object: 'any', + key: 'Nonexistent Field', + operation: 'Whatever Operation', + values: [] + } + ]; + mockConditionsAnyTrue = [ + { + object: 'any', + key: 'alpha', + operation: 'greaterThan', + values: [5] + } + ]; + mockConditionsAnyFalse = [ + { + object: 'any', + key: 'alpha', + operation: 'greaterThan', + values: [1000] + } + ]; + mockConditionsAllFalse = [ + { + object: 'all', + key: 'alpha', + operation: 'greaterThan', + values: [5] + } + ]; + mockConditionsAllTrue = [ + { + object: 'all', + key: 'alpha', + operation: 'greaterThan', + values: [0] + } + ]; + mockOperations = { + greaterThan: { + operation: function (input) { + return input[0] > input[1]; + }, + text: 'is greater than', + appliesTo: ['number'], + inputCount: 1, + getDescription: function (values) { + return ' > ' + values[0]; + } + }, + lessThan: { + operation: function (input) { + return input[0] < input[1]; + }, + text: 'is less than', + appliesTo: ['number'], + inputCount: 1 + }, + textContains: { + operation: function (input) { + return input[0] && input[1] && input[0].includes(input[1]); + }, + text: 'text contains', + appliesTo: ['string'], + inputCount: 1 + }, + textIsExactly: { + operation: function (input) { + return input[0] === input[1]; + }, + text: 'text is exactly', + appliesTo: ['string'], + inputCount: 1 + }, + isHalfHorse: { + operation: function (input) { + return input[0].type === 'Centaur'; + }, + text: 'is Half Horse', + appliesTo: ['mythicalCreature'], + inputCount: 0, + getDescription: function () { + return 'is half horse'; + } + } + }; + evaluator = new ConditionEvaluator(mockCache, mockComposition); + testEvaluator = new ConditionEvaluator(mockCache, mockComposition); + evaluator.operations = mockOperations; }); + + it('evaluates a condition when it has no configuration', function () { + expect(evaluator.execute(mockConditionsEmpty, 'any')).toEqual(false); + expect(evaluator.execute(mockConditionsEmpty, 'all')).toEqual(false); + }); + + it('correctly evaluates a set of conditions', function () { + expect(evaluator.execute(mockConditions, 'any')).toEqual(true); + expect(evaluator.execute(mockConditions, 'all')).toEqual(false); + }); + + it('correctly evaluates conditions involving "any telemetry"', function () { + expect(evaluator.execute(mockConditionsAnyTrue, 'any')).toEqual(true); + expect(evaluator.execute(mockConditionsAnyFalse, 'any')).toEqual(false); + }); + + it('correctly evaluates conditions involving "all telemetry"', function () { + expect(evaluator.execute(mockConditionsAllTrue, 'any')).toEqual(true); + expect(evaluator.execute(mockConditionsAllFalse, 'any')).toEqual(false); + }); + + it('handles malformed conditions gracefully', function () { + //if no conditions are fully defined, should return false for any mode + expect(evaluator.execute(mockConditionsUndefined, 'any')).toEqual(false); + expect(evaluator.execute(mockConditionsUndefined, 'all')).toEqual(false); + //these conditions are true: evaluator should ignore undefined conditions, + //and evaluate the rule as true + mockConditionsUndefined.push({ + object: 'a', + key: 'gamma', + operation: 'textContains', + values: ['Testing'] + }); + expect(evaluator.execute(mockConditionsUndefined, 'any')).toEqual(true); + mockConditionsUndefined.push({ + object: 'c', + key: 'iAm', + operation: 'textContains', + values: ['Walrus'] + }); + expect(evaluator.execute(mockConditionsUndefined, 'all')).toEqual(true); + }); + + it('gets the keys for possible operations', function () { + expect(evaluator.getOperationKeys()).toEqual([ + 'greaterThan', + 'lessThan', + 'textContains', + 'textIsExactly', + 'isHalfHorse' + ]); + }); + + it('gets output text for a given operation', function () { + expect(evaluator.getOperationText('isHalfHorse')).toEqual('is Half Horse'); + }); + + it('correctly returns whether an operation applies to a given type', function () { + expect(evaluator.operationAppliesTo('isHalfHorse', 'mythicalCreature')).toEqual(true); + expect(evaluator.operationAppliesTo('isHalfHorse', 'spaceJunk')).toEqual(false); + }); + + it('returns the HTML input type associated with a given data type', function () { + expect(evaluator.getInputTypeById('string')).toEqual('text'); + }); + + it('gets the number of inputs required for a given operation', function () { + expect(evaluator.getInputCount('isHalfHorse')).toEqual(0); + expect(evaluator.getInputCount('greaterThan')).toEqual(1); + }); + + it('gets a human-readable description of a condition', function () { + expect(evaluator.getOperationDescription('isHalfHorse')).toEqual('is half horse'); + expect(evaluator.getOperationDescription('greaterThan', [1])).toEqual(' > 1'); + }); + + it('allows setting a substitute cache for testing purposes, and toggling its use', function () { + evaluator.setTestDataCache(mockTestCache); + evaluator.useTestData(true); + expect(evaluator.execute(mockConditions, 'any')).toEqual(false); + expect(evaluator.execute(mockConditions, 'all')).toEqual(false); + mockConditions.push({ + object: 'a', + key: 'gamma', + operation: 'textContains', + values: ['4 5 6'] + }); + expect(evaluator.execute(mockConditions, 'any')).toEqual(true); + expect(evaluator.execute(mockConditions, 'all')).toEqual(false); + mockConditions.pop(); + evaluator.useTestData(false); + expect(evaluator.execute(mockConditions, 'any')).toEqual(true); + expect(evaluator.execute(mockConditions, 'all')).toEqual(false); + }); + + it('supports all required operations', function () { + //equal to + testOperation = testEvaluator.operations.equalTo.operation; + expect(testOperation([33, 33])).toEqual(true); + expect(testOperation([55, 147])).toEqual(false); + //not equal to + testOperation = testEvaluator.operations.notEqualTo.operation; + expect(testOperation([33, 33])).toEqual(false); + expect(testOperation([55, 147])).toEqual(true); + //greater than + testOperation = testEvaluator.operations.greaterThan.operation; + expect(testOperation([100, 33])).toEqual(true); + expect(testOperation([33, 33])).toEqual(false); + expect(testOperation([55, 147])).toEqual(false); + //less than + testOperation = testEvaluator.operations.lessThan.operation; + expect(testOperation([100, 33])).toEqual(false); + expect(testOperation([33, 33])).toEqual(false); + expect(testOperation([55, 147])).toEqual(true); + //greater than or equal to + testOperation = testEvaluator.operations.greaterThanOrEq.operation; + expect(testOperation([100, 33])).toEqual(true); + expect(testOperation([33, 33])).toEqual(true); + expect(testOperation([55, 147])).toEqual(false); + //less than or equal to + testOperation = testEvaluator.operations.lessThanOrEq.operation; + expect(testOperation([100, 33])).toEqual(false); + expect(testOperation([33, 33])).toEqual(true); + expect(testOperation([55, 147])).toEqual(true); + //between + testOperation = testEvaluator.operations.between.operation; + expect(testOperation([100, 33, 66])).toEqual(false); + expect(testOperation([1, 33, 66])).toEqual(false); + expect(testOperation([45, 33, 66])).toEqual(true); + //not between + testOperation = testEvaluator.operations.notBetween.operation; + expect(testOperation([100, 33, 66])).toEqual(true); + expect(testOperation([1, 33, 66])).toEqual(true); + expect(testOperation([45, 33, 66])).toEqual(false); + //text contains + testOperation = testEvaluator.operations.textContains.operation; + expect(testOperation(['Testing', 'tin'])).toEqual(true); + expect(testOperation(['Testing', 'bind'])).toEqual(false); + //text does not contain + testOperation = testEvaluator.operations.textDoesNotContain.operation; + expect(testOperation(['Testing', 'tin'])).toEqual(false); + expect(testOperation(['Testing', 'bind'])).toEqual(true); + //text starts with + testOperation = testEvaluator.operations.textStartsWith.operation; + expect(testOperation(['Testing', 'Tes'])).toEqual(true); + expect(testOperation(['Testing', 'ting'])).toEqual(false); + //text ends with + testOperation = testEvaluator.operations.textEndsWith.operation; + expect(testOperation(['Testing', 'Tes'])).toEqual(false); + expect(testOperation(['Testing', 'ting'])).toEqual(true); + //text is exactly + testOperation = testEvaluator.operations.textIsExactly.operation; + expect(testOperation(['Testing', 'Testing'])).toEqual(true); + expect(testOperation(['Testing', 'Test'])).toEqual(false); + //undefined + testOperation = testEvaluator.operations.isUndefined.operation; + expect(testOperation([1])).toEqual(false); + expect(testOperation([])).toEqual(true); + //isDefined + testOperation = testEvaluator.operations.isDefined.operation; + expect(testOperation([1])).toEqual(true); + expect(testOperation([])).toEqual(false); + }); + + it('can produce a description for all supported operations', function () { + testEvaluator.getOperationKeys().forEach(function (key) { + expect(testEvaluator.getOperationDescription(key, [])).toBeDefined(); + }); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/ConditionManagerSpec.js b/src/plugins/summaryWidget/test/ConditionManagerSpec.js index fad16ae2b7..c1b4413d36 100644 --- a/src/plugins/summaryWidget/test/ConditionManagerSpec.js +++ b/src/plugins/summaryWidget/test/ConditionManagerSpec.js @@ -21,412 +21,423 @@ *****************************************************************************/ define(['../src/ConditionManager'], function (ConditionManager) { - xdescribe('A Summary Widget Condition Manager', function () { - let conditionManager; - let mockDomainObject; - let mockCompObject1; - let mockCompObject2; - let mockCompObject3; - let mockMetadata; - let mockTelemetryCallbacks; - let mockEventCallbacks; - let unsubscribeSpies; - let unregisterSpies; - let mockMetadataManagers; - let mockComposition; - let mockOpenMCT; - let mockTelemetryAPI; - let addCallbackSpy; - let loadCallbackSpy; - let removeCallbackSpy; - let telemetryCallbackSpy; - let metadataCallbackSpy; - let telemetryRequests; - let mockTelemetryValues; - let mockTelemetryValues2; - let mockConditionEvaluator; + xdescribe('A Summary Widget Condition Manager', function () { + let conditionManager; + let mockDomainObject; + let mockCompObject1; + let mockCompObject2; + let mockCompObject3; + let mockMetadata; + let mockTelemetryCallbacks; + let mockEventCallbacks; + let unsubscribeSpies; + let unregisterSpies; + let mockMetadataManagers; + let mockComposition; + let mockOpenMCT; + let mockTelemetryAPI; + let addCallbackSpy; + let loadCallbackSpy; + let removeCallbackSpy; + let telemetryCallbackSpy; + let metadataCallbackSpy; + let telemetryRequests; + let mockTelemetryValues; + let mockTelemetryValues2; + let mockConditionEvaluator; - beforeEach(function () { - mockDomainObject = { - identifier: { - key: 'testKey' - }, - name: 'Test Object', - composition: [{ - mockCompObject1: { - key: 'mockCompObject1' - }, - mockCompObject2: { - key: 'mockCompObject2' - } - }], - configuration: {} - }; - mockCompObject1 = { - identifier: { - key: 'mockCompObject1' - }, - name: 'Object 1' - }; - mockCompObject2 = { - identifier: { - key: 'mockCompObject2' - }, - name: 'Object 2' - }; - mockCompObject3 = { - identifier: { - key: 'mockCompObject3' - }, - name: 'Object 3' - }; - mockMetadata = { - mockCompObject1: { - property1: { - key: 'property1', - name: 'Property 1', - format: 'string', - hints: {} - }, - property2: { - key: 'property2', - name: 'Property 2', - hints: { - domain: 1 - } - } - }, - mockCompObject2: { - property3: { - key: 'property3', - name: 'Property 3', - format: 'string', - hints: {} - }, - property4: { - key: 'property4', - name: 'Property 4', - hints: { - range: 1 - } - } - }, - mockCompObject3: { - property1: { - key: 'property1', - name: 'Property 1', - hints: {} - }, - property2: { - key: 'property2', - name: 'Property 2', - hints: {} - } - } - }; - mockTelemetryCallbacks = {}; - mockEventCallbacks = {}; - unsubscribeSpies = jasmine.createSpyObj('mockUnsubscribeFunction', [ - 'mockCompObject1', - 'mockCompObject2', - 'mockCompObject3' - ]); - unregisterSpies = jasmine.createSpyObj('mockUnregisterFunctions', [ - 'load', - 'remove', - 'add' - ]); - mockTelemetryValues = { - mockCompObject1: { - property1: 'Its a string', - property2: 42 - }, - mockCompObject2: { - property3: 'Execute order:', - property4: 66 - }, - mockCompObject3: { - property1: 'Testing 1 2 3', - property2: 9000 - } - }; - mockTelemetryValues2 = { - mockCompObject1: { - property1: 'Its a different string', - property2: 44 - }, - mockCompObject2: { - property3: 'Execute catch:', - property4: 22 - }, - mockCompObject3: { - property1: 'Walrus', - property2: 22 - } - }; - mockMetadataManagers = { - mockCompObject1: { - values: jasmine.createSpy('metadataManager').and.returnValue( - Object.values(mockMetadata.mockCompObject1) - ) - }, - mockCompObject2: { - values: jasmine.createSpy('metadataManager').and.returnValue( - Object.values(mockMetadata.mockCompObject2) - ) - }, - mockCompObject3: { - values: jasmine.createSpy('metadataManager').and.returnValue( - Object.values(mockMetadata.mockCompObject2) - ) - } - }; + beforeEach(function () { + mockDomainObject = { + identifier: { + key: 'testKey' + }, + name: 'Test Object', + composition: [ + { + mockCompObject1: { + key: 'mockCompObject1' + }, + mockCompObject2: { + key: 'mockCompObject2' + } + } + ], + configuration: {} + }; + mockCompObject1 = { + identifier: { + key: 'mockCompObject1' + }, + name: 'Object 1' + }; + mockCompObject2 = { + identifier: { + key: 'mockCompObject2' + }, + name: 'Object 2' + }; + mockCompObject3 = { + identifier: { + key: 'mockCompObject3' + }, + name: 'Object 3' + }; + mockMetadata = { + mockCompObject1: { + property1: { + key: 'property1', + name: 'Property 1', + format: 'string', + hints: {} + }, + property2: { + key: 'property2', + name: 'Property 2', + hints: { + domain: 1 + } + } + }, + mockCompObject2: { + property3: { + key: 'property3', + name: 'Property 3', + format: 'string', + hints: {} + }, + property4: { + key: 'property4', + name: 'Property 4', + hints: { + range: 1 + } + } + }, + mockCompObject3: { + property1: { + key: 'property1', + name: 'Property 1', + hints: {} + }, + property2: { + key: 'property2', + name: 'Property 2', + hints: {} + } + } + }; + mockTelemetryCallbacks = {}; + mockEventCallbacks = {}; + unsubscribeSpies = jasmine.createSpyObj('mockUnsubscribeFunction', [ + 'mockCompObject1', + 'mockCompObject2', + 'mockCompObject3' + ]); + unregisterSpies = jasmine.createSpyObj('mockUnregisterFunctions', ['load', 'remove', 'add']); + mockTelemetryValues = { + mockCompObject1: { + property1: 'Its a string', + property2: 42 + }, + mockCompObject2: { + property3: 'Execute order:', + property4: 66 + }, + mockCompObject3: { + property1: 'Testing 1 2 3', + property2: 9000 + } + }; + mockTelemetryValues2 = { + mockCompObject1: { + property1: 'Its a different string', + property2: 44 + }, + mockCompObject2: { + property3: 'Execute catch:', + property4: 22 + }, + mockCompObject3: { + property1: 'Walrus', + property2: 22 + } + }; + mockMetadataManagers = { + mockCompObject1: { + values: jasmine + .createSpy('metadataManager') + .and.returnValue(Object.values(mockMetadata.mockCompObject1)) + }, + mockCompObject2: { + values: jasmine + .createSpy('metadataManager') + .and.returnValue(Object.values(mockMetadata.mockCompObject2)) + }, + mockCompObject3: { + values: jasmine + .createSpy('metadataManager') + .and.returnValue(Object.values(mockMetadata.mockCompObject2)) + } + }; - mockComposition = jasmine.createSpyObj('composition', [ - 'on', - 'off', - 'load', - 'triggerCallback' - ]); - mockComposition.on.and.callFake(function (event, callback, context) { - mockEventCallbacks[event] = callback.bind(context); - }); - mockComposition.off.and.callFake(function (event) { - unregisterSpies[event](); - }); - mockComposition.load.and.callFake(function () { - mockComposition.triggerCallback('add', mockCompObject1); - mockComposition.triggerCallback('add', mockCompObject2); - mockComposition.triggerCallback('load'); - }); - mockComposition.triggerCallback.and.callFake(function (event, obj) { - if (event === 'add') { - mockEventCallbacks.add(obj); - } else if (event === 'remove') { - mockEventCallbacks.remove(obj.identifier); - } else { - mockEventCallbacks[event](); - } - }); - telemetryRequests = []; - mockTelemetryAPI = jasmine.createSpyObj('telemetryAPI', [ - 'request', - 'isTelemetryObject', - 'getMetadata', - 'subscribe', - 'triggerTelemetryCallback' - ]); - mockTelemetryAPI.request.and.callFake(function (obj) { - const req = { - object: obj - }; - req.promise = new Promise(function (resolve, reject) { - req.resolve = resolve; - req.reject = reject; - }); - telemetryRequests.push(req); - - return req.promise; - }); - mockTelemetryAPI.isTelemetryObject.and.returnValue(true); - mockTelemetryAPI.getMetadata.and.callFake(function (obj) { - return mockMetadataManagers[obj.identifier.key]; - }); - mockTelemetryAPI.subscribe.and.callFake(function (obj, callback) { - mockTelemetryCallbacks[obj.identifier.key] = callback; - - return unsubscribeSpies[obj.identifier.key]; - }); - mockTelemetryAPI.triggerTelemetryCallback.and.callFake(function (key) { - mockTelemetryCallbacks[key](mockTelemetryValues2[key]); - }); - - mockOpenMCT = { - telemetry: mockTelemetryAPI, - composition: {} - }; - mockOpenMCT.composition.get = jasmine.createSpy('get').and.returnValue(mockComposition); - - loadCallbackSpy = jasmine.createSpy('loadCallbackSpy'); - addCallbackSpy = jasmine.createSpy('addCallbackSpy'); - removeCallbackSpy = jasmine.createSpy('removeCallbackSpy'); - metadataCallbackSpy = jasmine.createSpy('metadataCallbackSpy'); - telemetryCallbackSpy = jasmine.createSpy('telemetryCallbackSpy'); - - conditionManager = new ConditionManager(mockDomainObject, mockOpenMCT); - conditionManager.on('load', loadCallbackSpy); - conditionManager.on('add', addCallbackSpy); - conditionManager.on('remove', removeCallbackSpy); - conditionManager.on('metadata', metadataCallbackSpy); - conditionManager.on('receiveTelemetry', telemetryCallbackSpy); - - mockConditionEvaluator = jasmine.createSpy('mockConditionEvaluator'); - mockConditionEvaluator.execute = jasmine.createSpy('execute'); - conditionManager.evaluator = mockConditionEvaluator; + mockComposition = jasmine.createSpyObj('composition', [ + 'on', + 'off', + 'load', + 'triggerCallback' + ]); + mockComposition.on.and.callFake(function (event, callback, context) { + mockEventCallbacks[event] = callback.bind(context); + }); + mockComposition.off.and.callFake(function (event) { + unregisterSpies[event](); + }); + mockComposition.load.and.callFake(function () { + mockComposition.triggerCallback('add', mockCompObject1); + mockComposition.triggerCallback('add', mockCompObject2); + mockComposition.triggerCallback('load'); + }); + mockComposition.triggerCallback.and.callFake(function (event, obj) { + if (event === 'add') { + mockEventCallbacks.add(obj); + } else if (event === 'remove') { + mockEventCallbacks.remove(obj.identifier); + } else { + mockEventCallbacks[event](); + } + }); + telemetryRequests = []; + mockTelemetryAPI = jasmine.createSpyObj('telemetryAPI', [ + 'request', + 'isTelemetryObject', + 'getMetadata', + 'subscribe', + 'triggerTelemetryCallback' + ]); + mockTelemetryAPI.request.and.callFake(function (obj) { + const req = { + object: obj + }; + req.promise = new Promise(function (resolve, reject) { + req.resolve = resolve; + req.reject = reject; }); + telemetryRequests.push(req); - it('loads the initial composition and invokes the appropriate handlers', function () { - mockComposition.triggerCallback('load'); - expect(conditionManager.getComposition()).toEqual({ - mockCompObject1: mockCompObject1, - mockCompObject2: mockCompObject2 - }); - expect(loadCallbackSpy).toHaveBeenCalled(); - expect(conditionManager.loadCompleted()).toEqual(true); - }); + return req.promise; + }); + mockTelemetryAPI.isTelemetryObject.and.returnValue(true); + mockTelemetryAPI.getMetadata.and.callFake(function (obj) { + return mockMetadataManagers[obj.identifier.key]; + }); + mockTelemetryAPI.subscribe.and.callFake(function (obj, callback) { + mockTelemetryCallbacks[obj.identifier.key] = callback; - it('loads metadata from composition and gets it upon request', function () { - expect(conditionManager.getTelemetryMetadata('mockCompObject1')) - .toEqual(mockMetadata.mockCompObject1); - expect(conditionManager.getTelemetryMetadata('mockCompObject2')) - .toEqual(mockMetadata.mockCompObject2); - }); + return unsubscribeSpies[obj.identifier.key]; + }); + mockTelemetryAPI.triggerTelemetryCallback.and.callFake(function (key) { + mockTelemetryCallbacks[key](mockTelemetryValues2[key]); + }); - it('maintains lists of global metadata, and does not duplicate repeated fields', function () { - const allKeys = { - property1: { - key: 'property1', - name: 'Property 1', - format: 'string', - hints: {} - }, - property2: { - key: 'property2', - name: 'Property 2', - hints: { - domain: 1 - } - }, - property3: { - key: 'property3', - name: 'Property 3', - format: 'string', - hints: {} - }, - property4: { - key: 'property4', - name: 'Property 4', - hints: { - range: 1 - } - } - }; - expect(conditionManager.getTelemetryMetadata('all')).toEqual(allKeys); - expect(conditionManager.getTelemetryMetadata('any')).toEqual(allKeys); - mockComposition.triggerCallback('add', mockCompObject3); - expect(conditionManager.getTelemetryMetadata('all')).toEqual(allKeys); - expect(conditionManager.getTelemetryMetadata('any')).toEqual(allKeys); - }); + mockOpenMCT = { + telemetry: mockTelemetryAPI, + composition: {} + }; + mockOpenMCT.composition.get = jasmine.createSpy('get').and.returnValue(mockComposition); - it('loads and gets telemetry property types', function () { - conditionManager.parseAllPropertyTypes(); - expect(conditionManager.getTelemetryPropertyType('mockCompObject1', 'property1')) - .toEqual('string'); - expect(conditionManager.getTelemetryPropertyType('mockCompObject2', 'property4')) - .toEqual('number'); - expect(conditionManager.metadataLoadCompleted()).toEqual(true); - expect(metadataCallbackSpy).toHaveBeenCalled(); - }); + loadCallbackSpy = jasmine.createSpy('loadCallbackSpy'); + addCallbackSpy = jasmine.createSpy('addCallbackSpy'); + removeCallbackSpy = jasmine.createSpy('removeCallbackSpy'); + metadataCallbackSpy = jasmine.createSpy('metadataCallbackSpy'); + telemetryCallbackSpy = jasmine.createSpy('telemetryCallbackSpy'); - it('responds to a composition add event and invokes the appropriate handlers', function () { - mockComposition.triggerCallback('add', mockCompObject3); - expect(addCallbackSpy).toHaveBeenCalledWith(mockCompObject3); - expect(conditionManager.getComposition()).toEqual({ - mockCompObject1: mockCompObject1, - mockCompObject2: mockCompObject2, - mockCompObject3: mockCompObject3 - }); - }); + conditionManager = new ConditionManager(mockDomainObject, mockOpenMCT); + conditionManager.on('load', loadCallbackSpy); + conditionManager.on('add', addCallbackSpy); + conditionManager.on('remove', removeCallbackSpy); + conditionManager.on('metadata', metadataCallbackSpy); + conditionManager.on('receiveTelemetry', telemetryCallbackSpy); - it('responds to a composition remove event and invokes the appropriate handlers', function () { - mockComposition.triggerCallback('remove', mockCompObject2); - expect(removeCallbackSpy).toHaveBeenCalledWith({ - key: 'mockCompObject2' - }); - expect(unsubscribeSpies.mockCompObject2).toHaveBeenCalled(); - expect(conditionManager.getComposition()).toEqual({ - mockCompObject1: mockCompObject1 - }); - }); - - it('unregisters telemetry subscriptions and composition listeners on destroy', function () { - mockComposition.triggerCallback('add', mockCompObject3); - conditionManager.destroy(); - Object.values(unsubscribeSpies).forEach(function (spy) { - expect(spy).toHaveBeenCalled(); - }); - Object.values(unregisterSpies).forEach(function (spy) { - expect(spy).toHaveBeenCalled(); - }); - }); - - it('populates its LAD cache with historial data on load, if available', function (done) { - expect(telemetryRequests.length).toBe(2); - expect(telemetryRequests[0].object).toBe(mockCompObject1); - expect(telemetryRequests[1].object).toBe(mockCompObject2); - - expect(telemetryCallbackSpy).not.toHaveBeenCalled(); - - telemetryCallbackSpy.and.callFake(function () { - if (telemetryCallbackSpy.calls.count() === 2) { - expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual('Its a string'); - expect(conditionManager.subscriptionCache.mockCompObject2.property4).toEqual(66); - done(); - } - }); - - telemetryRequests[0].resolve([mockTelemetryValues.mockCompObject1]); - telemetryRequests[1].resolve([mockTelemetryValues.mockCompObject2]); - }); - - it('updates its LAD cache upon receiving telemetry and invokes the appropriate handlers', function () { - mockTelemetryAPI.triggerTelemetryCallback('mockCompObject1'); - expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual('Its a different string'); - mockTelemetryAPI.triggerTelemetryCallback('mockCompObject2'); - expect(conditionManager.subscriptionCache.mockCompObject2.property4).toEqual(22); - expect(telemetryCallbackSpy).toHaveBeenCalled(); - }); - - it('evalutes a set of rules and returns the id of the' - + 'last active rule, or the first if no rules are active', function () { - const mockRuleOrder = ['default', 'rule0', 'rule1']; - const mockRules = { - default: { - getProperty: function () {} - }, - rule0: { - getProperty: function () {} - }, - rule1: { - getProperty: function () {} - } - }; - - mockConditionEvaluator.execute.and.returnValue(false); - expect(conditionManager.executeRules(mockRuleOrder, mockRules)).toEqual('default'); - mockConditionEvaluator.execute.and.returnValue(true); - expect(conditionManager.executeRules(mockRuleOrder, mockRules)).toEqual('rule1'); - }); - - it('gets the human-readable name of a composition object', function () { - expect(conditionManager.getObjectName('mockCompObject1')).toEqual('Object 1'); - expect(conditionManager.getObjectName('all')).toEqual('all Telemetry'); - }); - - it('gets the human-readable name of a telemetry field', function () { - expect(conditionManager.getTelemetryPropertyName('mockCompObject1', 'property1')) - .toEqual('Property 1'); - expect(conditionManager.getTelemetryPropertyName('mockCompObject2', 'property4')) - .toEqual('Property 4'); - }); - - it('gets its associated ConditionEvaluator', function () { - expect(conditionManager.getEvaluator()).toEqual(mockConditionEvaluator); - }); - - it('allows forcing a receive telemetry event', function () { - conditionManager.triggerTelemetryCallback(); - expect(telemetryCallbackSpy).toHaveBeenCalled(); - }); + mockConditionEvaluator = jasmine.createSpy('mockConditionEvaluator'); + mockConditionEvaluator.execute = jasmine.createSpy('execute'); + conditionManager.evaluator = mockConditionEvaluator; }); + + it('loads the initial composition and invokes the appropriate handlers', function () { + mockComposition.triggerCallback('load'); + expect(conditionManager.getComposition()).toEqual({ + mockCompObject1: mockCompObject1, + mockCompObject2: mockCompObject2 + }); + expect(loadCallbackSpy).toHaveBeenCalled(); + expect(conditionManager.loadCompleted()).toEqual(true); + }); + + it('loads metadata from composition and gets it upon request', function () { + expect(conditionManager.getTelemetryMetadata('mockCompObject1')).toEqual( + mockMetadata.mockCompObject1 + ); + expect(conditionManager.getTelemetryMetadata('mockCompObject2')).toEqual( + mockMetadata.mockCompObject2 + ); + }); + + it('maintains lists of global metadata, and does not duplicate repeated fields', function () { + const allKeys = { + property1: { + key: 'property1', + name: 'Property 1', + format: 'string', + hints: {} + }, + property2: { + key: 'property2', + name: 'Property 2', + hints: { + domain: 1 + } + }, + property3: { + key: 'property3', + name: 'Property 3', + format: 'string', + hints: {} + }, + property4: { + key: 'property4', + name: 'Property 4', + hints: { + range: 1 + } + } + }; + expect(conditionManager.getTelemetryMetadata('all')).toEqual(allKeys); + expect(conditionManager.getTelemetryMetadata('any')).toEqual(allKeys); + mockComposition.triggerCallback('add', mockCompObject3); + expect(conditionManager.getTelemetryMetadata('all')).toEqual(allKeys); + expect(conditionManager.getTelemetryMetadata('any')).toEqual(allKeys); + }); + + it('loads and gets telemetry property types', function () { + conditionManager.parseAllPropertyTypes(); + expect(conditionManager.getTelemetryPropertyType('mockCompObject1', 'property1')).toEqual( + 'string' + ); + expect(conditionManager.getTelemetryPropertyType('mockCompObject2', 'property4')).toEqual( + 'number' + ); + expect(conditionManager.metadataLoadCompleted()).toEqual(true); + expect(metadataCallbackSpy).toHaveBeenCalled(); + }); + + it('responds to a composition add event and invokes the appropriate handlers', function () { + mockComposition.triggerCallback('add', mockCompObject3); + expect(addCallbackSpy).toHaveBeenCalledWith(mockCompObject3); + expect(conditionManager.getComposition()).toEqual({ + mockCompObject1: mockCompObject1, + mockCompObject2: mockCompObject2, + mockCompObject3: mockCompObject3 + }); + }); + + it('responds to a composition remove event and invokes the appropriate handlers', function () { + mockComposition.triggerCallback('remove', mockCompObject2); + expect(removeCallbackSpy).toHaveBeenCalledWith({ + key: 'mockCompObject2' + }); + expect(unsubscribeSpies.mockCompObject2).toHaveBeenCalled(); + expect(conditionManager.getComposition()).toEqual({ + mockCompObject1: mockCompObject1 + }); + }); + + it('unregisters telemetry subscriptions and composition listeners on destroy', function () { + mockComposition.triggerCallback('add', mockCompObject3); + conditionManager.destroy(); + Object.values(unsubscribeSpies).forEach(function (spy) { + expect(spy).toHaveBeenCalled(); + }); + Object.values(unregisterSpies).forEach(function (spy) { + expect(spy).toHaveBeenCalled(); + }); + }); + + it('populates its LAD cache with historial data on load, if available', function (done) { + expect(telemetryRequests.length).toBe(2); + expect(telemetryRequests[0].object).toBe(mockCompObject1); + expect(telemetryRequests[1].object).toBe(mockCompObject2); + + expect(telemetryCallbackSpy).not.toHaveBeenCalled(); + + telemetryCallbackSpy.and.callFake(function () { + if (telemetryCallbackSpy.calls.count() === 2) { + expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual( + 'Its a string' + ); + expect(conditionManager.subscriptionCache.mockCompObject2.property4).toEqual(66); + done(); + } + }); + + telemetryRequests[0].resolve([mockTelemetryValues.mockCompObject1]); + telemetryRequests[1].resolve([mockTelemetryValues.mockCompObject2]); + }); + + it('updates its LAD cache upon receiving telemetry and invokes the appropriate handlers', function () { + mockTelemetryAPI.triggerTelemetryCallback('mockCompObject1'); + expect(conditionManager.subscriptionCache.mockCompObject1.property1).toEqual( + 'Its a different string' + ); + mockTelemetryAPI.triggerTelemetryCallback('mockCompObject2'); + expect(conditionManager.subscriptionCache.mockCompObject2.property4).toEqual(22); + expect(telemetryCallbackSpy).toHaveBeenCalled(); + }); + + it( + 'evalutes a set of rules and returns the id of the' + + 'last active rule, or the first if no rules are active', + function () { + const mockRuleOrder = ['default', 'rule0', 'rule1']; + const mockRules = { + default: { + getProperty: function () {} + }, + rule0: { + getProperty: function () {} + }, + rule1: { + getProperty: function () {} + } + }; + + mockConditionEvaluator.execute.and.returnValue(false); + expect(conditionManager.executeRules(mockRuleOrder, mockRules)).toEqual('default'); + mockConditionEvaluator.execute.and.returnValue(true); + expect(conditionManager.executeRules(mockRuleOrder, mockRules)).toEqual('rule1'); + } + ); + + it('gets the human-readable name of a composition object', function () { + expect(conditionManager.getObjectName('mockCompObject1')).toEqual('Object 1'); + expect(conditionManager.getObjectName('all')).toEqual('all Telemetry'); + }); + + it('gets the human-readable name of a telemetry field', function () { + expect(conditionManager.getTelemetryPropertyName('mockCompObject1', 'property1')).toEqual( + 'Property 1' + ); + expect(conditionManager.getTelemetryPropertyName('mockCompObject2', 'property4')).toEqual( + 'Property 4' + ); + }); + + it('gets its associated ConditionEvaluator', function () { + expect(conditionManager.getEvaluator()).toEqual(mockConditionEvaluator); + }); + + it('allows forcing a receive telemetry event', function () { + conditionManager.triggerTelemetryCallback(); + expect(telemetryCallbackSpy).toHaveBeenCalled(); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/ConditionSpec.js b/src/plugins/summaryWidget/test/ConditionSpec.js index 29a21a40b3..657fd3b843 100644 --- a/src/plugins/summaryWidget/test/ConditionSpec.js +++ b/src/plugins/summaryWidget/test/ConditionSpec.js @@ -21,185 +21,185 @@ *****************************************************************************/ define(['../src/Condition'], function (Condition) { - xdescribe('A summary widget condition', function () { - let testCondition; - let mockConfig; - let mockConditionManager; - let mockContainer; - let mockEvaluator; - let changeSpy; - let duplicateSpy; - let removeSpy; - let generateValuesSpy; + xdescribe('A summary widget condition', function () { + let testCondition; + let mockConfig; + let mockConditionManager; + let mockContainer; + let mockEvaluator; + let changeSpy; + let duplicateSpy; + let removeSpy; + let generateValuesSpy; - beforeEach(function () { - mockContainer = document.createElement('div'); + beforeEach(function () { + mockContainer = document.createElement('div'); - mockConfig = { - object: 'object1', - key: 'property1', - operation: 'operation1', - values: [1, 2, 3] - }; + mockConfig = { + object: 'object1', + key: 'property1', + operation: 'operation1', + values: [1, 2, 3] + }; - mockEvaluator = {}; - mockEvaluator.getInputCount = jasmine.createSpy('inputCount'); - mockEvaluator.getInputType = jasmine.createSpy('inputType'); + mockEvaluator = {}; + mockEvaluator.getInputCount = jasmine.createSpy('inputCount'); + mockEvaluator.getInputType = jasmine.createSpy('inputType'); - mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ - 'on', - 'getComposition', - 'loadCompleted', - 'getEvaluator', - 'getTelemetryMetadata', - 'metadataLoadCompleted', - 'getObjectName', - 'getTelemetryPropertyName' - ]); - mockConditionManager.loadCompleted.and.returnValue(false); - mockConditionManager.metadataLoadCompleted.and.returnValue(false); - mockConditionManager.getEvaluator.and.returnValue(mockEvaluator); - mockConditionManager.getComposition.and.returnValue({}); - mockConditionManager.getTelemetryMetadata.and.returnValue({}); - mockConditionManager.getObjectName.and.returnValue('Object Name'); - mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name'); + mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ + 'on', + 'getComposition', + 'loadCompleted', + 'getEvaluator', + 'getTelemetryMetadata', + 'metadataLoadCompleted', + 'getObjectName', + 'getTelemetryPropertyName' + ]); + mockConditionManager.loadCompleted.and.returnValue(false); + mockConditionManager.metadataLoadCompleted.and.returnValue(false); + mockConditionManager.getEvaluator.and.returnValue(mockEvaluator); + mockConditionManager.getComposition.and.returnValue({}); + mockConditionManager.getTelemetryMetadata.and.returnValue({}); + mockConditionManager.getObjectName.and.returnValue('Object Name'); + mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name'); - duplicateSpy = jasmine.createSpy('duplicate'); - removeSpy = jasmine.createSpy('remove'); - changeSpy = jasmine.createSpy('change'); - generateValuesSpy = jasmine.createSpy('generateValueInputs'); + duplicateSpy = jasmine.createSpy('duplicate'); + removeSpy = jasmine.createSpy('remove'); + changeSpy = jasmine.createSpy('change'); + generateValuesSpy = jasmine.createSpy('generateValueInputs'); - testCondition = new Condition(mockConfig, 54, mockConditionManager); + testCondition = new Condition(mockConfig, 54, mockConditionManager); - testCondition.on('duplicate', duplicateSpy); - testCondition.on('remove', removeSpy); - testCondition.on('change', changeSpy); - }); - - it('exposes a DOM element to represent itself in the view', function () { - mockContainer.append(testCondition.getDOM()); - expect(mockContainer.querySelectorAll('.t-condition').length).toEqual(1); - }); - - it('responds to a change in its object select', function () { - testCondition.selects.object.setSelected(''); - expect(changeSpy).toHaveBeenCalledWith({ - value: '', - property: 'object', - index: 54 - }); - }); - - it('responds to a change in its key select', function () { - testCondition.selects.key.setSelected(''); - expect(changeSpy).toHaveBeenCalledWith({ - value: '', - property: 'key', - index: 54 - }); - }); - - it('responds to a change in its operation select', function () { - testCondition.generateValueInputs = generateValuesSpy; - testCondition.selects.operation.setSelected(''); - expect(changeSpy).toHaveBeenCalledWith({ - value: '', - property: 'operation', - index: 54 - }); - expect(generateValuesSpy).toHaveBeenCalledWith(''); - }); - - 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(''); - - 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(''); - - 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(''); - - 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(''); - - inputs = Array.from(mockContainer.querySelectorAll('input')); - - expect(inputs[0].value).toEqual(''); - expect(inputs[1].value).toEqual(''); - expect(testCondition.config.values).toEqual(['', '']); - }); - - it('responds to a change in its value inputs', function () { - mockContainer.append(testCondition.getDOM()); - mockEvaluator.getInputType.and.returnValue('number'); - mockEvaluator.getInputCount.and.returnValue(3); - testCondition.generateValueInputs(''); - - 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]', - index: 54 - }); - }); - - it('can remove itself from the configuration', function () { - testCondition.remove(); - expect(removeSpy).toHaveBeenCalledWith(54); - }); - - it('can duplicate itself', function () { - testCondition.duplicate(); - expect(duplicateSpy).toHaveBeenCalledWith({ - sourceCondition: mockConfig, - index: 54 - }); - }); + testCondition.on('duplicate', duplicateSpy); + testCondition.on('remove', removeSpy); + testCondition.on('change', changeSpy); }); + + it('exposes a DOM element to represent itself in the view', function () { + mockContainer.append(testCondition.getDOM()); + expect(mockContainer.querySelectorAll('.t-condition').length).toEqual(1); + }); + + it('responds to a change in its object select', function () { + testCondition.selects.object.setSelected(''); + expect(changeSpy).toHaveBeenCalledWith({ + value: '', + property: 'object', + index: 54 + }); + }); + + it('responds to a change in its key select', function () { + testCondition.selects.key.setSelected(''); + expect(changeSpy).toHaveBeenCalledWith({ + value: '', + property: 'key', + index: 54 + }); + }); + + it('responds to a change in its operation select', function () { + testCondition.generateValueInputs = generateValuesSpy; + testCondition.selects.operation.setSelected(''); + expect(changeSpy).toHaveBeenCalledWith({ + value: '', + property: 'operation', + index: 54 + }); + expect(generateValuesSpy).toHaveBeenCalledWith(''); + }); + + 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(''); + + 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(''); + + 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(''); + + 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(''); + + inputs = Array.from(mockContainer.querySelectorAll('input')); + + expect(inputs[0].value).toEqual(''); + expect(inputs[1].value).toEqual(''); + expect(testCondition.config.values).toEqual(['', '']); + }); + + it('responds to a change in its value inputs', function () { + mockContainer.append(testCondition.getDOM()); + mockEvaluator.getInputType.and.returnValue('number'); + mockEvaluator.getInputCount.and.returnValue(3); + testCondition.generateValueInputs(''); + + 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]', + index: 54 + }); + }); + + it('can remove itself from the configuration', function () { + testCondition.remove(); + expect(removeSpy).toHaveBeenCalledWith(54); + }); + + it('can duplicate itself', function () { + testCondition.duplicate(); + expect(duplicateSpy).toHaveBeenCalledWith({ + sourceCondition: mockConfig, + index: 54 + }); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/RuleSpec.js b/src/plugins/summaryWidget/test/RuleSpec.js index df5108f7ae..880708529a 100644 --- a/src/plugins/summaryWidget/test/RuleSpec.js +++ b/src/plugins/summaryWidget/test/RuleSpec.js @@ -1,278 +1,293 @@ define(['../src/Rule'], function (Rule) { - describe('A Summary Widget Rule', function () { - let mockRuleConfig; - let mockDomainObject; - let mockOpenMCT; - let mockConditionManager; - let mockWidgetDnD; - let mockEvaluator; - let mockContainer; - let testRule; - let removeSpy; - let duplicateSpy; - let changeSpy; - let conditionChangeSpy; + describe('A Summary Widget Rule', function () { + let mockRuleConfig; + let mockDomainObject; + let mockOpenMCT; + let mockConditionManager; + let mockWidgetDnD; + let mockEvaluator; + let mockContainer; + let testRule; + let removeSpy; + let duplicateSpy; + let changeSpy; + let conditionChangeSpy; - beforeEach(function () { - mockRuleConfig = { - name: 'Name', - id: 'mockRule', - icon: 'test-icon-name', - style: { - 'background-color': '', - 'border-color': '', - 'color': '' - }, - expanded: true, - conditions: [{ - object: '', - key: '', - operation: '', - values: [] - }, { - object: 'blah', - key: 'blah', - operation: 'blah', - values: ['blah.', 'blah!', 'blah?'] - }] - }; - mockDomainObject = { - configuration: { - ruleConfigById: { - mockRule: mockRuleConfig, - otherRule: {} - }, - ruleOrder: ['default', 'mockRule', 'otherRule'] - } - }; + beforeEach(function () { + mockRuleConfig = { + name: 'Name', + id: 'mockRule', + icon: 'test-icon-name', + style: { + 'background-color': '', + 'border-color': '', + color: '' + }, + expanded: true, + conditions: [ + { + object: '', + key: '', + operation: '', + values: [] + }, + { + object: 'blah', + key: 'blah', + operation: 'blah', + values: ['blah.', 'blah!', 'blah?'] + } + ] + }; + mockDomainObject = { + configuration: { + ruleConfigById: { + mockRule: mockRuleConfig, + otherRule: {} + }, + ruleOrder: ['default', 'mockRule', 'otherRule'] + } + }; - mockOpenMCT = {}; - mockOpenMCT.objects = {}; - mockOpenMCT.objects.mutate = jasmine.createSpy('mutate'); + mockOpenMCT = {}; + mockOpenMCT.objects = {}; + mockOpenMCT.objects.mutate = jasmine.createSpy('mutate'); - mockEvaluator = {}; - mockEvaluator.getOperationDescription = jasmine.createSpy('evaluator') - .and.returnValue('Operation Description'); + mockEvaluator = {}; + mockEvaluator.getOperationDescription = jasmine + .createSpy('evaluator') + .and.returnValue('Operation Description'); - mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ - 'on', - 'getComposition', - 'loadCompleted', - 'getEvaluator', - 'getTelemetryMetadata', - 'metadataLoadCompleted', - 'getObjectName', - 'getTelemetryPropertyName' - ]); - mockConditionManager.loadCompleted.and.returnValue(false); - mockConditionManager.metadataLoadCompleted.and.returnValue(false); - mockConditionManager.getEvaluator.and.returnValue(mockEvaluator); - mockConditionManager.getComposition.and.returnValue({}); - mockConditionManager.getTelemetryMetadata.and.returnValue({}); - mockConditionManager.getObjectName.and.returnValue('Object Name'); - mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name'); + mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ + 'on', + 'getComposition', + 'loadCompleted', + 'getEvaluator', + 'getTelemetryMetadata', + 'metadataLoadCompleted', + 'getObjectName', + 'getTelemetryPropertyName' + ]); + mockConditionManager.loadCompleted.and.returnValue(false); + mockConditionManager.metadataLoadCompleted.and.returnValue(false); + mockConditionManager.getEvaluator.and.returnValue(mockEvaluator); + mockConditionManager.getComposition.and.returnValue({}); + mockConditionManager.getTelemetryMetadata.and.returnValue({}); + mockConditionManager.getObjectName.and.returnValue('Object Name'); + mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name'); - mockWidgetDnD = jasmine.createSpyObj('dnd', [ - 'on', - 'setDragImage', - 'dragStart' - ]); + mockWidgetDnD = jasmine.createSpyObj('dnd', ['on', 'setDragImage', 'dragStart']); - mockContainer = document.createElement('div'); + mockContainer = document.createElement('div'); - removeSpy = jasmine.createSpy('removeCallback'); - duplicateSpy = jasmine.createSpy('duplicateCallback'); - changeSpy = jasmine.createSpy('changeCallback'); - conditionChangeSpy = jasmine.createSpy('conditionChangeCallback'); + removeSpy = jasmine.createSpy('removeCallback'); + duplicateSpy = jasmine.createSpy('duplicateCallback'); + changeSpy = jasmine.createSpy('changeCallback'); + conditionChangeSpy = jasmine.createSpy('conditionChangeCallback'); - testRule = new Rule(mockRuleConfig, mockDomainObject, mockOpenMCT, mockConditionManager, - mockWidgetDnD); - testRule.on('remove', removeSpy); - testRule.on('duplicate', duplicateSpy); - testRule.on('change', changeSpy); - testRule.on('conditionChange', conditionChangeSpy); - }); - - it('closes its configuration panel on initial load', function () { - expect(testRule.getProperty('expanded')).toEqual(false); - }); - - it('gets its DOM element', function () { - mockContainer.append(testRule.getDOM()); - expect(mockContainer.querySelectorAll('.l-widget-rule').length).toBeGreaterThan(0); - }); - - it('gets its configuration properties', function () { - expect(testRule.getProperty('name')).toEqual('Name'); - expect(testRule.getProperty('icon')).toEqual('test-icon-name'); - }); - - it('can duplicate itself', function () { - testRule.duplicate(); - mockRuleConfig.expanded = true; - expect(duplicateSpy).toHaveBeenCalledWith(mockRuleConfig); - }); - - it('can remove itself from the configuration', function () { - testRule.remove(); - expect(removeSpy).toHaveBeenCalled(); - expect(mockDomainObject.configuration.ruleConfigById.mockRule).not.toBeDefined(); - expect(mockDomainObject.configuration.ruleOrder).toEqual(['default', 'otherRule']); - }); - - it('updates its configuration on a condition change and invokes callbacks', function () { - testRule.onConditionChange({ - value: 'newValue', - property: 'object', - index: 0 - }); - expect(testRule.getProperty('conditions')[0].object).toEqual('newValue'); - expect(conditionChangeSpy).toHaveBeenCalled(); - }); - - it('allows initializing a new condition with a default configuration', function () { - testRule.initCondition(); - expect(mockRuleConfig.conditions).toEqual([{ - object: '', - key: '', - operation: '', - values: [] - }, { - object: 'blah', - key: 'blah', - operation: 'blah', - values: ['blah.', 'blah!', 'blah?'] - }, { - object: '', - key: '', - operation: '', - values: [] - }]); - }); - - it('allows initializing a new condition from a given configuration', function () { - testRule.initCondition({ - sourceCondition: { - object: 'object1', - key: 'key1', - operation: 'operation1', - values: [1, 2, 3] - }, - index: 0 - }); - expect(mockRuleConfig.conditions).toEqual([{ - object: '', - key: '', - operation: '', - values: [] - }, { - object: 'object1', - key: 'key1', - operation: 'operation1', - values: [1, 2, 3] - }, { - object: 'blah', - key: 'blah', - operation: 'blah', - values: ['blah.', 'blah!', 'blah?'] - }]); - }); - - it('invokes mutate when updating the domain object', function () { - testRule.updateDomainObject(); - expect(mockOpenMCT.objects.mutate).toHaveBeenCalled(); - }); - - it('builds condition view from condition configuration', function () { - mockContainer.append(testRule.getDOM()); - expect(mockContainer.querySelectorAll('.t-condition').length).toEqual(2); - }); - - it('responds to input of style properties, and updates the preview', function () { - testRule.colorInputs['background-color'].set('#434343'); - expect(mockRuleConfig.style['background-color']).toEqual('#434343'); - testRule.colorInputs['border-color'].set('#666666'); - expect(mockRuleConfig.style['border-color']).toEqual('#666666'); - testRule.colorInputs.color.set('#999999'); - expect(mockRuleConfig.style.color).toEqual('#999999'); - - 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(); - }); - - it('responds to input for the icon property', function () { - testRule.iconInput.set('icon-alert-rect'); - expect(mockRuleConfig.icon).toEqual('icon-alert-rect'); - expect(changeSpy).toHaveBeenCalled(); - }); - - /* - test for js condition commented out for v1 - */ - - // it('responds to input of text properties', function () { - // var testInputs = ['name', 'label', 'message', 'jsCondition'], - // input; - - // testInputs.forEach(function (key) { - // input = testRule.textInputs[key]; - // input.prop('value', 'A new ' + key); - // input.trigger('input'); - // expect(mockRuleConfig[key]).toEqual('A new ' + key); - // }); - - // expect(changeSpy).toHaveBeenCalled(); - // }); - - it('allows input for when the rule triggers', function () { - 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(); - }); - - it('generates a human-readable description from its conditions', function () { - testRule.generateDescription(); - expect(testRule.config.description).toContain( - 'Object Name\'s Property Name Operation Description' - ); - testRule.config.trigger = 'js'; - testRule.generateDescription(); - expect(testRule.config.description).toContain( - 'when a custom JavaScript condition evaluates to true' - ); - }); - - it('initiates a drag event when its grippy is clicked', function () { - const event = new Event('mousedown', { - bubbles: true, - cancelable: true - }); - testRule.grippy.dispatchEvent(event); - - expect(mockWidgetDnD.setDragImage).toHaveBeenCalled(); - expect(mockWidgetDnD.dragStart).toHaveBeenCalledWith('mockRule'); - }); - - /* - test for js condition commented out for v1 - */ - - it('can remove a condition from its configuration', function () { - testRule.removeCondition(0); - expect(testRule.config.conditions).toEqual([{ - object: 'blah', - key: 'blah', - operation: 'blah', - values: ['blah.', 'blah!', 'blah?'] - }]); - }); + testRule = new Rule( + mockRuleConfig, + mockDomainObject, + mockOpenMCT, + mockConditionManager, + mockWidgetDnD + ); + testRule.on('remove', removeSpy); + testRule.on('duplicate', duplicateSpy); + testRule.on('change', changeSpy); + testRule.on('conditionChange', conditionChangeSpy); }); + + it('closes its configuration panel on initial load', function () { + expect(testRule.getProperty('expanded')).toEqual(false); + }); + + it('gets its DOM element', function () { + mockContainer.append(testRule.getDOM()); + expect(mockContainer.querySelectorAll('.l-widget-rule').length).toBeGreaterThan(0); + }); + + it('gets its configuration properties', function () { + expect(testRule.getProperty('name')).toEqual('Name'); + expect(testRule.getProperty('icon')).toEqual('test-icon-name'); + }); + + it('can duplicate itself', function () { + testRule.duplicate(); + mockRuleConfig.expanded = true; + expect(duplicateSpy).toHaveBeenCalledWith(mockRuleConfig); + }); + + it('can remove itself from the configuration', function () { + testRule.remove(); + expect(removeSpy).toHaveBeenCalled(); + expect(mockDomainObject.configuration.ruleConfigById.mockRule).not.toBeDefined(); + expect(mockDomainObject.configuration.ruleOrder).toEqual(['default', 'otherRule']); + }); + + it('updates its configuration on a condition change and invokes callbacks', function () { + testRule.onConditionChange({ + value: 'newValue', + property: 'object', + index: 0 + }); + expect(testRule.getProperty('conditions')[0].object).toEqual('newValue'); + expect(conditionChangeSpy).toHaveBeenCalled(); + }); + + it('allows initializing a new condition with a default configuration', function () { + testRule.initCondition(); + expect(mockRuleConfig.conditions).toEqual([ + { + object: '', + key: '', + operation: '', + values: [] + }, + { + object: 'blah', + key: 'blah', + operation: 'blah', + values: ['blah.', 'blah!', 'blah?'] + }, + { + object: '', + key: '', + operation: '', + values: [] + } + ]); + }); + + it('allows initializing a new condition from a given configuration', function () { + testRule.initCondition({ + sourceCondition: { + object: 'object1', + key: 'key1', + operation: 'operation1', + values: [1, 2, 3] + }, + index: 0 + }); + expect(mockRuleConfig.conditions).toEqual([ + { + object: '', + key: '', + operation: '', + values: [] + }, + { + object: 'object1', + key: 'key1', + operation: 'operation1', + values: [1, 2, 3] + }, + { + object: 'blah', + key: 'blah', + operation: 'blah', + values: ['blah.', 'blah!', 'blah?'] + } + ]); + }); + + it('invokes mutate when updating the domain object', function () { + testRule.updateDomainObject(); + expect(mockOpenMCT.objects.mutate).toHaveBeenCalled(); + }); + + it('builds condition view from condition configuration', function () { + mockContainer.append(testRule.getDOM()); + expect(mockContainer.querySelectorAll('.t-condition').length).toEqual(2); + }); + + it('responds to input of style properties, and updates the preview', function () { + testRule.colorInputs['background-color'].set('#434343'); + expect(mockRuleConfig.style['background-color']).toEqual('#434343'); + testRule.colorInputs['border-color'].set('#666666'); + expect(mockRuleConfig.style['border-color']).toEqual('#666666'); + testRule.colorInputs.color.set('#999999'); + expect(mockRuleConfig.style.color).toEqual('#999999'); + + 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(); + }); + + it('responds to input for the icon property', function () { + testRule.iconInput.set('icon-alert-rect'); + expect(mockRuleConfig.icon).toEqual('icon-alert-rect'); + expect(changeSpy).toHaveBeenCalled(); + }); + + /* + test for js condition commented out for v1 + */ + + // it('responds to input of text properties', function () { + // var testInputs = ['name', 'label', 'message', 'jsCondition'], + // input; + + // testInputs.forEach(function (key) { + // input = testRule.textInputs[key]; + // input.prop('value', 'A new ' + key); + // input.trigger('input'); + // expect(mockRuleConfig[key]).toEqual('A new ' + key); + // }); + + // expect(changeSpy).toHaveBeenCalled(); + // }); + + it('allows input for when the rule triggers', function () { + 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(); + }); + + it('generates a human-readable description from its conditions', function () { + testRule.generateDescription(); + expect(testRule.config.description).toContain( + "Object Name's Property Name Operation Description" + ); + testRule.config.trigger = 'js'; + testRule.generateDescription(); + expect(testRule.config.description).toContain( + 'when a custom JavaScript condition evaluates to true' + ); + }); + + it('initiates a drag event when its grippy is clicked', function () { + const event = new Event('mousedown', { + bubbles: true, + cancelable: true + }); + testRule.grippy.dispatchEvent(event); + + expect(mockWidgetDnD.setDragImage).toHaveBeenCalled(); + expect(mockWidgetDnD.dragStart).toHaveBeenCalledWith('mockRule'); + }); + + /* + test for js condition commented out for v1 + */ + + it('can remove a condition from its configuration', function () { + testRule.removeCondition(0); + expect(testRule.config.conditions).toEqual([ + { + object: 'blah', + key: 'blah', + operation: 'blah', + values: ['blah.', 'blah!', 'blah?'] + } + ]); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/SummaryWidgetSpec.js b/src/plugins/summaryWidget/test/SummaryWidgetSpec.js index 819eb0b069..94b7646514 100644 --- a/src/plugins/summaryWidget/test/SummaryWidgetSpec.js +++ b/src/plugins/summaryWidget/test/SummaryWidgetSpec.js @@ -21,175 +21,172 @@ *****************************************************************************/ define(['../src/SummaryWidget'], function (SummaryWidget) { - xdescribe('The Summary Widget', function () { - let summaryWidget; - let mockDomainObject; - let mockOldDomainObject; - let mockOpenMCT; - let mockObjectService; - let mockStatusCapability; - let mockComposition; - let mockContainer; - let listenCallback; - let listenCallbackSpy; + xdescribe('The Summary Widget', function () { + let summaryWidget; + let mockDomainObject; + let mockOldDomainObject; + let mockOpenMCT; + let mockObjectService; + let mockStatusCapability; + let mockComposition; + let mockContainer; + let listenCallback; + let listenCallbackSpy; - beforeEach(function () { - mockDomainObject = { - identifier: { - key: 'testKey', - namespace: 'testNamespace' - }, - name: 'testName', - composition: [], - configuration: {} - }; - mockComposition = jasmine.createSpyObj('composition', [ - 'on', - 'off', - 'load' - ]); - mockStatusCapability = jasmine.createSpyObj('statusCapability', [ - 'get', - 'listen', - 'triggerCallback' - ]); + beforeEach(function () { + mockDomainObject = { + identifier: { + key: 'testKey', + namespace: 'testNamespace' + }, + name: 'testName', + composition: [], + configuration: {} + }; + mockComposition = jasmine.createSpyObj('composition', ['on', 'off', 'load']); + mockStatusCapability = jasmine.createSpyObj('statusCapability', [ + 'get', + 'listen', + 'triggerCallback' + ]); - listenCallbackSpy = jasmine.createSpy('listenCallbackSpy', function () {}); - mockStatusCapability.get.and.returnValue([]); - mockStatusCapability.listen.and.callFake(function (callback) { - listenCallback = callback; + listenCallbackSpy = jasmine.createSpy('listenCallbackSpy', function () {}); + mockStatusCapability.get.and.returnValue([]); + mockStatusCapability.listen.and.callFake(function (callback) { + listenCallback = callback; - return listenCallbackSpy; - }); - mockStatusCapability.triggerCallback.and.callFake(function () { - listenCallback(['editing']); - }); + return listenCallbackSpy; + }); + mockStatusCapability.triggerCallback.and.callFake(function () { + listenCallback(['editing']); + }); - mockOldDomainObject = {}; - mockOldDomainObject.getCapability = jasmine.createSpy('capability'); - mockOldDomainObject.getCapability.and.returnValue(mockStatusCapability); + mockOldDomainObject = {}; + mockOldDomainObject.getCapability = jasmine.createSpy('capability'); + mockOldDomainObject.getCapability.and.returnValue(mockStatusCapability); - mockObjectService = {}; - mockObjectService.getObjects = jasmine.createSpy('objectService'); - mockObjectService.getObjects.and.returnValue(new Promise(function (resolve, reject) { - resolve({ - 'testNamespace:testKey': mockOldDomainObject - }); - })); - mockOpenMCT = jasmine.createSpyObj('openmct', [ - '$injector', - 'composition', - 'objects' - ]); - mockOpenMCT.$injector.get = jasmine.createSpy('get'); - mockOpenMCT.$injector.get.and.returnValue(mockObjectService); - mockOpenMCT.composition = jasmine.createSpyObj('composition', [ - 'get', - 'on' - ]); - mockOpenMCT.composition.get.and.returnValue(mockComposition); - mockOpenMCT.objects.mutate = jasmine.createSpy('mutate'); - mockOpenMCT.objects.observe = jasmine.createSpy('observe'); - mockOpenMCT.objects.observe.and.returnValue(function () {}); + mockObjectService = {}; + mockObjectService.getObjects = jasmine.createSpy('objectService'); + mockObjectService.getObjects.and.returnValue( + new Promise(function (resolve, reject) { + resolve({ + 'testNamespace:testKey': mockOldDomainObject + }); + }) + ); + mockOpenMCT = jasmine.createSpyObj('openmct', ['$injector', 'composition', 'objects']); + mockOpenMCT.$injector.get = jasmine.createSpy('get'); + mockOpenMCT.$injector.get.and.returnValue(mockObjectService); + mockOpenMCT.composition = jasmine.createSpyObj('composition', ['get', 'on']); + mockOpenMCT.composition.get.and.returnValue(mockComposition); + mockOpenMCT.objects.mutate = jasmine.createSpy('mutate'); + mockOpenMCT.objects.observe = jasmine.createSpy('observe'); + mockOpenMCT.objects.observe.and.returnValue(function () {}); - summaryWidget = new SummaryWidget(mockDomainObject, mockOpenMCT); - mockContainer = document.createElement('div'); - summaryWidget.show(mockContainer); - }); - - it('queries with legacyId', function () { - expect(mockObjectService.getObjects).toHaveBeenCalledWith(['testNamespace:testKey']); - }); - - it('adds its DOM element to the view', function () { - expect(mockContainer.getElementsByClassName('w-summary-widget').length).toBeGreaterThan(0); - }); - - it('initialzes a default rule', function () { - expect(mockDomainObject.configuration.ruleConfigById.default).toBeDefined(); - expect(mockDomainObject.configuration.ruleOrder).toEqual(['default']); - }); - - it('builds rules and rule placeholders in view from configuration', function () { - expect(summaryWidget.ruleArea.querySelectorAll('.l-widget-rule').length).toEqual(2); - }); - - it('allows initializing a new rule with a particular identifier', function () { - summaryWidget.initRule('rule0', 'Rule'); - expect(mockDomainObject.configuration.ruleConfigById.rule0).toBeDefined(); - }); - - it('allows adding a new rule with a unique identifier to the configuration and view', function () { - summaryWidget.addRule(); - expect(mockDomainObject.configuration.ruleOrder.length).toEqual(2); - mockDomainObject.configuration.ruleOrder.forEach(function (ruleId) { - expect(mockDomainObject.configuration.ruleConfigById[ruleId]).toBeDefined(); - }); - summaryWidget.addRule(); - expect(mockDomainObject.configuration.ruleOrder.length).toEqual(3); - mockDomainObject.configuration.ruleOrder.forEach(function (ruleId) { - expect(mockDomainObject.configuration.ruleConfigById[ruleId]).toBeDefined(); - }); - expect(summaryWidget.ruleArea.querySelectorAll('.l-widget-rule').length).toEqual(6); - }); - - it('allows duplicating a rule from source configuration', function () { - const sourceConfig = JSON.parse(JSON.stringify(mockDomainObject.configuration.ruleConfigById.default)); - summaryWidget.duplicateRule(sourceConfig); - expect(Object.keys(mockDomainObject.configuration.ruleConfigById).length).toEqual(2); - }); - - it('does not duplicate an existing rule in the configuration', function () { - summaryWidget.initRule('default', 'Default'); - expect(Object.keys(mockDomainObject.configuration.ruleConfigById).length).toEqual(1); - }); - - it('uses mutate when updating the domain object only when in edit mode', function () { - summaryWidget.editing = true; - summaryWidget.updateDomainObject(); - expect(mockOpenMCT.objects.mutate).toHaveBeenCalled(); - }); - - it('shows configuration interfaces when in edit mode, and hides them otherwise', function () { - setTimeout(function () { - summaryWidget.onEdit([]); - expect(summaryWidget.editing).toEqual(false); - expect(summaryWidget.ruleArea.css('display')).toEqual('none'); - expect(summaryWidget.testDataArea.css('display')).toEqual('none'); - expect(summaryWidget.addRuleButton.css('display')).toEqual('none'); - summaryWidget.onEdit(['editing']); - expect(summaryWidget.editing).toEqual(true); - expect(summaryWidget.ruleArea.css('display')).not.toEqual('none'); - expect(summaryWidget.testDataArea.css('display')).not.toEqual('none'); - expect(summaryWidget.addRuleButton.css('display')).not.toEqual('none'); - }, 100); - }); - - it('unregisters any registered listeners on a destroy', function () { - setTimeout(function () { - summaryWidget.destroy(); - expect(listenCallbackSpy).toHaveBeenCalled(); - }, 100); - }); - - it('allows reorders of rules', function () { - summaryWidget.initRule('rule0'); - summaryWidget.initRule('rule1'); - summaryWidget.domainObject.configuration.ruleOrder = ['default', 'rule0', 'rule1']; - summaryWidget.reorder({ - draggingId: 'rule1', - dropTarget: 'default' - }); - expect(summaryWidget.domainObject.configuration.ruleOrder).toEqual(['default', 'rule1', 'rule0']); - }); - - it('adds hyperlink to the widget button and sets newTab preference', function () { - summaryWidget.addHyperlink('https://www.nasa.gov', 'newTab'); - - const widgetButton = mockContainer.querySelector('#widget'); - - expect(widgetButton.href).toEqual('https://www.nasa.gov/'); - expect(widgetButton.target).toEqual('_blank'); - }); + summaryWidget = new SummaryWidget(mockDomainObject, mockOpenMCT); + mockContainer = document.createElement('div'); + summaryWidget.show(mockContainer); }); + + it('queries with legacyId', function () { + expect(mockObjectService.getObjects).toHaveBeenCalledWith(['testNamespace:testKey']); + }); + + it('adds its DOM element to the view', function () { + expect(mockContainer.getElementsByClassName('w-summary-widget').length).toBeGreaterThan(0); + }); + + it('initialzes a default rule', function () { + expect(mockDomainObject.configuration.ruleConfigById.default).toBeDefined(); + expect(mockDomainObject.configuration.ruleOrder).toEqual(['default']); + }); + + it('builds rules and rule placeholders in view from configuration', function () { + expect(summaryWidget.ruleArea.querySelectorAll('.l-widget-rule').length).toEqual(2); + }); + + it('allows initializing a new rule with a particular identifier', function () { + summaryWidget.initRule('rule0', 'Rule'); + expect(mockDomainObject.configuration.ruleConfigById.rule0).toBeDefined(); + }); + + it('allows adding a new rule with a unique identifier to the configuration and view', function () { + summaryWidget.addRule(); + expect(mockDomainObject.configuration.ruleOrder.length).toEqual(2); + mockDomainObject.configuration.ruleOrder.forEach(function (ruleId) { + expect(mockDomainObject.configuration.ruleConfigById[ruleId]).toBeDefined(); + }); + summaryWidget.addRule(); + expect(mockDomainObject.configuration.ruleOrder.length).toEqual(3); + mockDomainObject.configuration.ruleOrder.forEach(function (ruleId) { + expect(mockDomainObject.configuration.ruleConfigById[ruleId]).toBeDefined(); + }); + expect(summaryWidget.ruleArea.querySelectorAll('.l-widget-rule').length).toEqual(6); + }); + + it('allows duplicating a rule from source configuration', function () { + const sourceConfig = JSON.parse( + JSON.stringify(mockDomainObject.configuration.ruleConfigById.default) + ); + summaryWidget.duplicateRule(sourceConfig); + expect(Object.keys(mockDomainObject.configuration.ruleConfigById).length).toEqual(2); + }); + + it('does not duplicate an existing rule in the configuration', function () { + summaryWidget.initRule('default', 'Default'); + expect(Object.keys(mockDomainObject.configuration.ruleConfigById).length).toEqual(1); + }); + + it('uses mutate when updating the domain object only when in edit mode', function () { + summaryWidget.editing = true; + summaryWidget.updateDomainObject(); + expect(mockOpenMCT.objects.mutate).toHaveBeenCalled(); + }); + + it('shows configuration interfaces when in edit mode, and hides them otherwise', function () { + setTimeout(function () { + summaryWidget.onEdit([]); + expect(summaryWidget.editing).toEqual(false); + expect(summaryWidget.ruleArea.css('display')).toEqual('none'); + expect(summaryWidget.testDataArea.css('display')).toEqual('none'); + expect(summaryWidget.addRuleButton.css('display')).toEqual('none'); + summaryWidget.onEdit(['editing']); + expect(summaryWidget.editing).toEqual(true); + expect(summaryWidget.ruleArea.css('display')).not.toEqual('none'); + expect(summaryWidget.testDataArea.css('display')).not.toEqual('none'); + expect(summaryWidget.addRuleButton.css('display')).not.toEqual('none'); + }, 100); + }); + + it('unregisters any registered listeners on a destroy', function () { + setTimeout(function () { + summaryWidget.destroy(); + expect(listenCallbackSpy).toHaveBeenCalled(); + }, 100); + }); + + it('allows reorders of rules', function () { + summaryWidget.initRule('rule0'); + summaryWidget.initRule('rule1'); + summaryWidget.domainObject.configuration.ruleOrder = ['default', 'rule0', 'rule1']; + summaryWidget.reorder({ + draggingId: 'rule1', + dropTarget: 'default' + }); + expect(summaryWidget.domainObject.configuration.ruleOrder).toEqual([ + 'default', + 'rule1', + 'rule0' + ]); + }); + + it('adds hyperlink to the widget button and sets newTab preference', function () { + summaryWidget.addHyperlink('https://www.nasa.gov', 'newTab'); + + const widgetButton = mockContainer.querySelector('#widget'); + + expect(widgetButton.href).toEqual('https://www.nasa.gov/'); + expect(widgetButton.target).toEqual('_blank'); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/SummaryWidgetViewPolicySpec.js b/src/plugins/summaryWidget/test/SummaryWidgetViewPolicySpec.js index 431de187dd..e10a5caaa4 100644 --- a/src/plugins/summaryWidget/test/SummaryWidgetViewPolicySpec.js +++ b/src/plugins/summaryWidget/test/SummaryWidgetViewPolicySpec.js @@ -20,47 +20,39 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - '../SummaryWidgetViewPolicy' -], function ( - SummaryWidgetViewPolicy -) { - - describe('SummaryWidgetViewPolicy', function () { - let policy; - let domainObject; - let view; - beforeEach(function () { - policy = new SummaryWidgetViewPolicy(); - domainObject = jasmine.createSpyObj('domainObject', [ - 'getModel' - ]); - domainObject.getModel.and.returnValue({}); - view = {}; - }); - - it('returns true for other object types', function () { - domainObject.getModel.and.returnValue({ - type: 'random' - }); - expect(policy.allow(view, domainObject)).toBe(true); - }); - - it('allows summary widget view for summary widgets', function () { - domainObject.getModel.and.returnValue({ - type: 'summary-widget' - }); - view.key = 'summary-widget-viewer'; - expect(policy.allow(view, domainObject)).toBe(true); - }); - - it('disallows other views for summary widgets', function () { - domainObject.getModel.and.returnValue({ - type: 'summary-widget' - }); - view.key = 'other view'; - expect(policy.allow(view, domainObject)).toBe(false); - }); - +define(['../SummaryWidgetViewPolicy'], function (SummaryWidgetViewPolicy) { + describe('SummaryWidgetViewPolicy', function () { + let policy; + let domainObject; + let view; + beforeEach(function () { + policy = new SummaryWidgetViewPolicy(); + domainObject = jasmine.createSpyObj('domainObject', ['getModel']); + domainObject.getModel.and.returnValue({}); + view = {}; }); + + it('returns true for other object types', function () { + domainObject.getModel.and.returnValue({ + type: 'random' + }); + expect(policy.allow(view, domainObject)).toBe(true); + }); + + it('allows summary widget view for summary widgets', function () { + domainObject.getModel.and.returnValue({ + type: 'summary-widget' + }); + view.key = 'summary-widget-viewer'; + expect(policy.allow(view, domainObject)).toBe(true); + }); + + it('disallows other views for summary widgets', function () { + domainObject.getModel.and.returnValue({ + type: 'summary-widget' + }); + view.key = 'other view'; + expect(policy.allow(view, domainObject)).toBe(false); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/TestDataItemSpec.js b/src/plugins/summaryWidget/test/TestDataItemSpec.js index 171753efe4..3e9ac3d31f 100644 --- a/src/plugins/summaryWidget/test/TestDataItemSpec.js +++ b/src/plugins/summaryWidget/test/TestDataItemSpec.js @@ -1,167 +1,167 @@ define(['../src/TestDataItem'], function (TestDataItem) { - describe('A summary widget test data item', function () { - let testDataItem; - let mockConfig; - let mockConditionManager; - let mockContainer; - let mockEvaluator; - let changeSpy; - let duplicateSpy; - let removeSpy; - let generateValueSpy; + describe('A summary widget test data item', function () { + let testDataItem; + let mockConfig; + let mockConditionManager; + let mockContainer; + let mockEvaluator; + let changeSpy; + let duplicateSpy; + let removeSpy; + let generateValueSpy; - beforeEach(function () { - mockContainer = document.createElement('div'); + beforeEach(function () { + mockContainer = document.createElement('div'); - mockConfig = { - object: 'object1', - key: 'property1', - value: 1 - }; + mockConfig = { + object: 'object1', + key: 'property1', + value: 1 + }; - mockEvaluator = {}; - mockEvaluator.getInputTypeById = jasmine.createSpy('inputType'); + mockEvaluator = {}; + mockEvaluator.getInputTypeById = jasmine.createSpy('inputType'); - mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ - 'on', - 'getComposition', - 'loadCompleted', - 'getEvaluator', - 'getTelemetryMetadata', - 'metadataLoadCompleted', - 'getObjectName', - 'getTelemetryPropertyName', - 'getTelemetryPropertyType' - ]); - mockConditionManager.loadCompleted.and.returnValue(false); - mockConditionManager.metadataLoadCompleted.and.returnValue(false); - mockConditionManager.getEvaluator.and.returnValue(mockEvaluator); - mockConditionManager.getComposition.and.returnValue({}); - mockConditionManager.getTelemetryMetadata.and.returnValue({}); - mockConditionManager.getObjectName.and.returnValue('Object Name'); - mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name'); - mockConditionManager.getTelemetryPropertyType.and.returnValue(''); + mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ + 'on', + 'getComposition', + 'loadCompleted', + 'getEvaluator', + 'getTelemetryMetadata', + 'metadataLoadCompleted', + 'getObjectName', + 'getTelemetryPropertyName', + 'getTelemetryPropertyType' + ]); + mockConditionManager.loadCompleted.and.returnValue(false); + mockConditionManager.metadataLoadCompleted.and.returnValue(false); + mockConditionManager.getEvaluator.and.returnValue(mockEvaluator); + mockConditionManager.getComposition.and.returnValue({}); + mockConditionManager.getTelemetryMetadata.and.returnValue({}); + mockConditionManager.getObjectName.and.returnValue('Object Name'); + mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name'); + mockConditionManager.getTelemetryPropertyType.and.returnValue(''); - duplicateSpy = jasmine.createSpy('duplicate'); - removeSpy = jasmine.createSpy('remove'); - changeSpy = jasmine.createSpy('change'); - generateValueSpy = jasmine.createSpy('generateValueInput'); + duplicateSpy = jasmine.createSpy('duplicate'); + removeSpy = jasmine.createSpy('remove'); + changeSpy = jasmine.createSpy('change'); + generateValueSpy = jasmine.createSpy('generateValueInput'); - testDataItem = new TestDataItem(mockConfig, 54, mockConditionManager); + testDataItem = new TestDataItem(mockConfig, 54, mockConditionManager); - testDataItem.on('duplicate', duplicateSpy); - testDataItem.on('remove', removeSpy); - testDataItem.on('change', changeSpy); - }); - - it('exposes a DOM element to represent itself in the view', function () { - mockContainer.append(testDataItem.getDOM()); - expect(mockContainer.querySelectorAll('.t-test-data-item').length).toEqual(1); - }); - - it('responds to a change in its object select', function () { - testDataItem.selects.object.setSelected(''); - expect(changeSpy).toHaveBeenCalledWith({ - value: '', - property: 'object', - index: 54 - }); - }); - - it('responds to a change in its key select', function () { - testDataItem.generateValueInput = generateValueSpy; - testDataItem.selects.key.setSelected(''); - expect(changeSpy).toHaveBeenCalledWith({ - value: '', - property: 'key', - index: 54 - }); - expect(generateValueSpy).toHaveBeenCalledWith(''); - }); - - it('generates a value input of the appropriate type', function () { - let inputs; - - mockContainer.append(testDataItem.getDOM()); - mockEvaluator.getInputTypeById.and.returnValue('number'); - testDataItem.generateValueInput(''); - - 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(''); - - 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(''); - - 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(''); - - 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(''); - }); - - it('responds to a change in its value inputs', function () { - mockContainer.append(testDataItem.getDOM()); - mockEvaluator.getInputTypeById.and.returnValue('number'); - testDataItem.generateValueInput(''); - - 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', - index: 54 - }); - }); - - it('can remove itself from the configuration', function () { - testDataItem.remove(); - expect(removeSpy).toHaveBeenCalledWith(54); - }); - - it('can duplicate itself', function () { - testDataItem.duplicate(); - expect(duplicateSpy).toHaveBeenCalledWith({ - sourceItem: mockConfig, - index: 54 - }); - }); + testDataItem.on('duplicate', duplicateSpy); + testDataItem.on('remove', removeSpy); + testDataItem.on('change', changeSpy); }); + + it('exposes a DOM element to represent itself in the view', function () { + mockContainer.append(testDataItem.getDOM()); + expect(mockContainer.querySelectorAll('.t-test-data-item').length).toEqual(1); + }); + + it('responds to a change in its object select', function () { + testDataItem.selects.object.setSelected(''); + expect(changeSpy).toHaveBeenCalledWith({ + value: '', + property: 'object', + index: 54 + }); + }); + + it('responds to a change in its key select', function () { + testDataItem.generateValueInput = generateValueSpy; + testDataItem.selects.key.setSelected(''); + expect(changeSpy).toHaveBeenCalledWith({ + value: '', + property: 'key', + index: 54 + }); + expect(generateValueSpy).toHaveBeenCalledWith(''); + }); + + it('generates a value input of the appropriate type', function () { + let inputs; + + mockContainer.append(testDataItem.getDOM()); + mockEvaluator.getInputTypeById.and.returnValue('number'); + testDataItem.generateValueInput(''); + + 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(''); + + 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(''); + + 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(''); + + 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(''); + }); + + it('responds to a change in its value inputs', function () { + mockContainer.append(testDataItem.getDOM()); + mockEvaluator.getInputTypeById.and.returnValue('number'); + testDataItem.generateValueInput(''); + + 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', + index: 54 + }); + }); + + it('can remove itself from the configuration', function () { + testDataItem.remove(); + expect(removeSpy).toHaveBeenCalledWith(54); + }); + + it('can duplicate itself', function () { + testDataItem.duplicate(); + expect(duplicateSpy).toHaveBeenCalledWith({ + sourceItem: mockConfig, + index: 54 + }); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/TestDataManagerSpec.js b/src/plugins/summaryWidget/test/TestDataManagerSpec.js index 59ce37d92c..3cf488ca09 100644 --- a/src/plugins/summaryWidget/test/TestDataManagerSpec.js +++ b/src/plugins/summaryWidget/test/TestDataManagerSpec.js @@ -1,230 +1,250 @@ define(['../src/TestDataManager'], function (TestDataManager) { - describe('A Summary Widget Rule', function () { - let mockDomainObject; - let mockOpenMCT; - let mockConditionManager; - let mockEvaluator; - let mockContainer; - let mockTelemetryMetadata; - let testDataManager; - let mockCompObject1; - let mockCompObject2; + describe('A Summary Widget Rule', function () { + let mockDomainObject; + let mockOpenMCT; + let mockConditionManager; + let mockEvaluator; + let mockContainer; + let mockTelemetryMetadata; + let testDataManager; + let mockCompObject1; + let mockCompObject2; - beforeEach(function () { - mockDomainObject = { - configuration: { - testDataConfig: [{ - object: '', - key: '', - value: '' - }, { - object: 'object1', - key: 'property1', - value: 66 - }, { - object: 'object2', - key: 'property4', - value: 'Text It Is' - }] - }, - composition: [{ - object1: { - key: 'object1', - name: 'Object 1' - }, - object2: { - key: 'object2', - name: 'Object 2' - } - }] - }; + beforeEach(function () { + mockDomainObject = { + configuration: { + testDataConfig: [ + { + object: '', + key: '', + value: '' + }, + { + object: 'object1', + key: 'property1', + value: 66 + }, + { + object: 'object2', + key: 'property4', + value: 'Text It Is' + } + ] + }, + composition: [ + { + object1: { + key: 'object1', + name: 'Object 1' + }, + object2: { + key: 'object2', + name: 'Object 2' + } + } + ] + }; - mockTelemetryMetadata = { - object1: { - property1: { - key: 'property1' - }, - property2: { - key: 'property2' - } - }, - object2: { - property3: { - key: 'property3' - }, - property4: { - key: 'property4' - } - } - }; + mockTelemetryMetadata = { + object1: { + property1: { + key: 'property1' + }, + property2: { + key: 'property2' + } + }, + object2: { + property3: { + key: 'property3' + }, + property4: { + key: 'property4' + } + } + }; - mockCompObject1 = { - identifier: { - key: 'object1' - }, - name: 'Object 1' - }; - mockCompObject2 = { - identifier: { - key: 'object2' - }, - name: 'Object 2' - }; + mockCompObject1 = { + identifier: { + key: 'object1' + }, + name: 'Object 1' + }; + mockCompObject2 = { + identifier: { + key: 'object2' + }, + name: 'Object 2' + }; - mockOpenMCT = {}; - mockOpenMCT.objects = {}; - mockOpenMCT.objects.mutate = jasmine.createSpy('mutate'); + mockOpenMCT = {}; + mockOpenMCT.objects = {}; + mockOpenMCT.objects.mutate = jasmine.createSpy('mutate'); - mockEvaluator = {}; - mockEvaluator.setTestDataCache = jasmine.createSpy('testDataCache'); - mockEvaluator.useTestData = jasmine.createSpy('useTestData'); + mockEvaluator = {}; + mockEvaluator.setTestDataCache = jasmine.createSpy('testDataCache'); + mockEvaluator.useTestData = jasmine.createSpy('useTestData'); - mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ - 'on', - 'getComposition', - 'loadCompleted', - 'getEvaluator', - 'getTelemetryMetadata', - 'metadataLoadCompleted', - 'getObjectName', - 'getTelemetryPropertyName', - 'triggerTelemetryCallback' - ]); - mockConditionManager.loadCompleted.and.returnValue(false); - mockConditionManager.metadataLoadCompleted.and.returnValue(false); - mockConditionManager.getEvaluator.and.returnValue(mockEvaluator); - mockConditionManager.getComposition.and.returnValue({ - object1: mockCompObject1, - object2: mockCompObject2 - }); - mockConditionManager.getTelemetryMetadata.and.callFake(function (id) { - return mockTelemetryMetadata[id]; - }); - mockConditionManager.getObjectName.and.returnValue('Object Name'); - mockConditionManager.getTelemetryPropertyName.and.returnValue('Property Name'); + mockConditionManager = jasmine.createSpyObj('mockConditionManager', [ + 'on', + 'getComposition', + 'loadCompleted', + 'getEvaluator', + 'getTelemetryMetadata', + 'metadataLoadCompleted', + 'getObjectName', + 'getTelemetryPropertyName', + 'triggerTelemetryCallback' + ]); + mockConditionManager.loadCompleted.and.returnValue(false); + mockConditionManager.metadataLoadCompleted.and.returnValue(false); + mockConditionManager.getEvaluator.and.returnValue(mockEvaluator); + mockConditionManager.getComposition.and.returnValue({ + object1: mockCompObject1, + object2: mockCompObject2 + }); + mockConditionManager.getTelemetryMetadata.and.callFake(function (id) { + return mockTelemetryMetadata[id]; + }); + 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); - }); - - it('closes its configuration panel on initial load', function () { - - }); - - it('exposes a DOM element to represent itself in the view', function () { - mockContainer.append(testDataManager.getDOM()); - 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 () { - testDataManager.updateTestCache(); - expect(mockEvaluator.setTestDataCache).toHaveBeenCalledWith({ - object1: { - property1: 66, - property2: '' - }, - object2: { - property3: '', - property4: 'Text It Is' - } - }); - }); - - it('updates its configuration on a item change and provides an updated' - + 'cache to the evaluator', function () { - testDataManager.onItemChange({ - value: 26, - property: 'value', - index: 1 - }); - expect(testDataManager.config[1].value).toEqual(26); - expect(mockEvaluator.setTestDataCache).toHaveBeenCalledWith({ - object1: { - property1: 26, - property2: '' - }, - object2: { - property3: '', - property4: 'Text It Is' - } - }); - }); - - it('allows initializing a new item with a default configuration', function () { - testDataManager.initItem(); - expect(mockDomainObject.configuration.testDataConfig).toEqual([{ - object: '', - key: '', - value: '' - }, { - object: 'object1', - key: 'property1', - value: 66 - }, { - object: 'object2', - key: 'property4', - value: 'Text It Is' - }, { - object: '', - key: '', - value: '' - }]); - }); - - it('allows initializing a new item from a given configuration', function () { - testDataManager.initItem({ - sourceItem: { - object: 'object2', - key: 'property3', - value: 1 - }, - index: 0 - }); - expect(mockDomainObject.configuration.testDataConfig).toEqual([{ - object: '', - key: '', - value: '' - }, { - object: 'object2', - key: 'property3', - value: 1 - }, { - object: 'object1', - key: 'property1', - value: 66 - }, { - object: 'object2', - key: 'property4', - value: 'Text It Is' - }]); - }); - - it('invokes mutate when updating the domain object', function () { - testDataManager.updateDomainObject(); - expect(mockOpenMCT.objects.mutate).toHaveBeenCalled(); - }); - - it('builds item view from item configuration', function () { - mockContainer.append(testDataManager.getDOM()); - expect(mockContainer.querySelectorAll('.t-test-data-item').length).toEqual(3); - }); - - it('can remove a item from its configuration', function () { - testDataManager.removeItem(0); - expect(mockDomainObject.configuration.testDataConfig).toEqual([{ - object: 'object1', - key: 'property1', - value: 66 - }, { - object: 'object2', - key: 'property4', - value: 'Text It Is' - }]); - }); - - it('exposes a UI element to toggle test data on and off', function () { - - }); + testDataManager = new TestDataManager(mockDomainObject, mockConditionManager, mockOpenMCT); }); + + it('closes its configuration panel on initial load', function () {}); + + it('exposes a DOM element to represent itself in the view', function () { + mockContainer.append(testDataManager.getDOM()); + 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 () { + testDataManager.updateTestCache(); + expect(mockEvaluator.setTestDataCache).toHaveBeenCalledWith({ + object1: { + property1: 66, + property2: '' + }, + object2: { + property3: '', + property4: 'Text It Is' + } + }); + }); + + it( + 'updates its configuration on a item change and provides an updated' + + 'cache to the evaluator', + function () { + testDataManager.onItemChange({ + value: 26, + property: 'value', + index: 1 + }); + expect(testDataManager.config[1].value).toEqual(26); + expect(mockEvaluator.setTestDataCache).toHaveBeenCalledWith({ + object1: { + property1: 26, + property2: '' + }, + object2: { + property3: '', + property4: 'Text It Is' + } + }); + } + ); + + it('allows initializing a new item with a default configuration', function () { + testDataManager.initItem(); + expect(mockDomainObject.configuration.testDataConfig).toEqual([ + { + object: '', + key: '', + value: '' + }, + { + object: 'object1', + key: 'property1', + value: 66 + }, + { + object: 'object2', + key: 'property4', + value: 'Text It Is' + }, + { + object: '', + key: '', + value: '' + } + ]); + }); + + it('allows initializing a new item from a given configuration', function () { + testDataManager.initItem({ + sourceItem: { + object: 'object2', + key: 'property3', + value: 1 + }, + index: 0 + }); + expect(mockDomainObject.configuration.testDataConfig).toEqual([ + { + object: '', + key: '', + value: '' + }, + { + object: 'object2', + key: 'property3', + value: 1 + }, + { + object: 'object1', + key: 'property1', + value: 66 + }, + { + object: 'object2', + key: 'property4', + value: 'Text It Is' + } + ]); + }); + + it('invokes mutate when updating the domain object', function () { + testDataManager.updateDomainObject(); + expect(mockOpenMCT.objects.mutate).toHaveBeenCalled(); + }); + + it('builds item view from item configuration', function () { + mockContainer.append(testDataManager.getDOM()); + expect(mockContainer.querySelectorAll('.t-test-data-item').length).toEqual(3); + }); + + it('can remove a item from its configuration', function () { + testDataManager.removeItem(0); + expect(mockDomainObject.configuration.testDataConfig).toEqual([ + { + object: 'object1', + key: 'property1', + value: 66 + }, + { + object: 'object2', + key: 'property4', + value: 'Text It Is' + } + ]); + }); + + it('exposes a UI element to toggle test data on and off', function () {}); + }); }); diff --git a/src/plugins/summaryWidget/test/input/ColorPaletteSpec.js b/src/plugins/summaryWidget/test/input/ColorPaletteSpec.js index d169ef748a..0470c0f0f3 100644 --- a/src/plugins/summaryWidget/test/input/ColorPaletteSpec.js +++ b/src/plugins/summaryWidget/test/input/ColorPaletteSpec.js @@ -1,24 +1,24 @@ define(['../../src/input/ColorPalette'], function (ColorPalette) { - describe('An Open MCT color palette', function () { - let colorPalette; - let changeCallback; + describe('An Open MCT color palette', function () { + let colorPalette; + let changeCallback; - beforeEach(function () { - changeCallback = jasmine.createSpy('changeCallback'); - }); - - it('allows defining a custom color set', function () { - colorPalette = new ColorPalette('someClass', 'someContainer', ['color1', 'color2', 'color3']); - expect(colorPalette.getCurrent()).toEqual('color1'); - colorPalette.on('change', changeCallback); - colorPalette.set('color2'); - expect(colorPalette.getCurrent()).toEqual('color2'); - expect(changeCallback).toHaveBeenCalledWith('color2'); - }); - - it('loads with a default color set if one is not provided', function () { - colorPalette = new ColorPalette('someClass', 'someContainer'); - expect(colorPalette.getCurrent()).toBeDefined(); - }); + beforeEach(function () { + changeCallback = jasmine.createSpy('changeCallback'); }); + + it('allows defining a custom color set', function () { + colorPalette = new ColorPalette('someClass', 'someContainer', ['color1', 'color2', 'color3']); + expect(colorPalette.getCurrent()).toEqual('color1'); + colorPalette.on('change', changeCallback); + colorPalette.set('color2'); + expect(colorPalette.getCurrent()).toEqual('color2'); + expect(changeCallback).toHaveBeenCalledWith('color2'); + }); + + it('loads with a default color set if one is not provided', function () { + colorPalette = new ColorPalette('someClass', 'someContainer'); + expect(colorPalette.getCurrent()).toBeDefined(); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/input/IconPaletteSpec.js b/src/plugins/summaryWidget/test/input/IconPaletteSpec.js index 6bb80a6be5..3a9128c17d 100644 --- a/src/plugins/summaryWidget/test/input/IconPaletteSpec.js +++ b/src/plugins/summaryWidget/test/input/IconPaletteSpec.js @@ -1,24 +1,24 @@ define(['../../src/input/IconPalette'], function (IconPalette) { - describe('An Open MCT icon palette', function () { - let iconPalette; - let changeCallback; + describe('An Open MCT icon palette', function () { + let iconPalette; + let changeCallback; - beforeEach(function () { - changeCallback = jasmine.createSpy('changeCallback'); - }); - - it('allows defining a custom icon set', function () { - iconPalette = new IconPalette('', 'someContainer', ['icon1', 'icon2', 'icon3']); - expect(iconPalette.getCurrent()).toEqual('icon1'); - iconPalette.on('change', changeCallback); - iconPalette.set('icon2'); - expect(iconPalette.getCurrent()).toEqual('icon2'); - expect(changeCallback).toHaveBeenCalledWith('icon2'); - }); - - it('loads with a default icon set if one is not provided', function () { - iconPalette = new IconPalette('someClass', 'someContainer'); - expect(iconPalette.getCurrent()).toBeDefined(); - }); + beforeEach(function () { + changeCallback = jasmine.createSpy('changeCallback'); }); + + it('allows defining a custom icon set', function () { + iconPalette = new IconPalette('', 'someContainer', ['icon1', 'icon2', 'icon3']); + expect(iconPalette.getCurrent()).toEqual('icon1'); + iconPalette.on('change', changeCallback); + iconPalette.set('icon2'); + expect(iconPalette.getCurrent()).toEqual('icon2'); + expect(changeCallback).toHaveBeenCalledWith('icon2'); + }); + + it('loads with a default icon set if one is not provided', function () { + iconPalette = new IconPalette('someClass', 'someContainer'); + expect(iconPalette.getCurrent()).toBeDefined(); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/input/KeySelectSpec.js b/src/plugins/summaryWidget/test/input/KeySelectSpec.js index 374b310d38..d5c22ce3b7 100644 --- a/src/plugins/summaryWidget/test/input/KeySelectSpec.js +++ b/src/plugins/summaryWidget/test/input/KeySelectSpec.js @@ -1,127 +1,123 @@ define(['../../src/input/KeySelect'], function (KeySelect) { - describe('A select for choosing composition object properties', function () { - let mockConfig; - let mockBadConfig; - let mockManager; - let keySelect; - let mockMetadata; - let mockObjectSelect; - beforeEach(function () { - mockConfig = { - object: 'object1', - key: 'a' - }; + describe('A select for choosing composition object properties', function () { + let mockConfig; + let mockBadConfig; + let mockManager; + let keySelect; + let mockMetadata; + let mockObjectSelect; + beforeEach(function () { + mockConfig = { + object: 'object1', + key: 'a' + }; - mockBadConfig = { - object: 'object1', - key: 'someNonexistentKey' - }; + mockBadConfig = { + object: 'object1', + key: 'someNonexistentKey' + }; - mockMetadata = { - object1: { - a: { - name: 'A' - }, - b: { - name: 'B' - } - }, - object2: { - alpha: { - name: 'Alpha' - }, - beta: { - name: 'Beta' - } - }, - object3: { - a: { - name: 'A' - } - } - }; + mockMetadata = { + object1: { + a: { + name: 'A' + }, + b: { + name: 'B' + } + }, + object2: { + alpha: { + name: 'Alpha' + }, + beta: { + name: 'Beta' + } + }, + object3: { + a: { + name: 'A' + } + } + }; - mockManager = jasmine.createSpyObj('mockManager', [ - 'on', - 'metadataLoadCompleted', - 'triggerCallback', - 'getTelemetryMetadata' - ]); + mockManager = jasmine.createSpyObj('mockManager', [ + 'on', + 'metadataLoadCompleted', + 'triggerCallback', + 'getTelemetryMetadata' + ]); - mockObjectSelect = jasmine.createSpyObj('mockObjectSelect', [ - 'on', - 'triggerCallback' - ]); + mockObjectSelect = jasmine.createSpyObj('mockObjectSelect', ['on', 'triggerCallback']); - mockObjectSelect.on.and.callFake((event, callback) => { - mockObjectSelect.callbacks = mockObjectSelect.callbacks || {}; - mockObjectSelect.callbacks[event] = callback; - }); + mockObjectSelect.on.and.callFake((event, callback) => { + mockObjectSelect.callbacks = mockObjectSelect.callbacks || {}; + mockObjectSelect.callbacks[event] = callback; + }); - mockObjectSelect.triggerCallback.and.callFake((event, key) => { - mockObjectSelect.callbacks[event](key); - }); + mockObjectSelect.triggerCallback.and.callFake((event, key) => { + mockObjectSelect.callbacks[event](key); + }); - mockManager.on.and.callFake((event, callback) => { - mockManager.callbacks = mockManager.callbacks || {}; - mockManager.callbacks[event] = callback; - }); + mockManager.on.and.callFake((event, callback) => { + mockManager.callbacks = mockManager.callbacks || {}; + mockManager.callbacks[event] = callback; + }); - mockManager.triggerCallback.and.callFake(event => { - mockManager.callbacks[event](); - }); + mockManager.triggerCallback.and.callFake((event) => { + mockManager.callbacks[event](); + }); - mockManager.getTelemetryMetadata.and.callFake(function (key) { - return mockMetadata[key]; - }); - - }); - - it('waits until the metadata fully loads to populate itself', function () { - mockManager.metadataLoadCompleted.and.returnValue(false); - keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); - expect(keySelect.getSelected()).toEqual(''); - }); - - it('populates itself with metadata on a metadata load', function () { - mockManager.metadataLoadCompleted.and.returnValue(false); - keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); - mockManager.triggerCallback('metadata'); - expect(keySelect.getSelected()).toEqual('a'); - }); - - it('populates itself with metadata if metadata load is already complete', function () { - mockManager.metadataLoadCompleted.and.returnValue(true); - keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); - expect(keySelect.getSelected()).toEqual('a'); - }); - - it('clears its selection state if the property in its config is not in its object', function () { - mockManager.metadataLoadCompleted.and.returnValue(true); - keySelect = new KeySelect(mockBadConfig, mockObjectSelect, mockManager); - expect(keySelect.getSelected()).toEqual(''); - }); - - it('populates with the appropriate options when its linked object changes', function () { - mockManager.metadataLoadCompleted.and.returnValue(true); - keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); - mockObjectSelect.triggerCallback('change', 'object2'); - keySelect.setSelected('alpha'); - expect(keySelect.getSelected()).toEqual('alpha'); - }); - - it('clears its selected state on change if the field is not present in the new object', function () { - mockManager.metadataLoadCompleted.and.returnValue(true); - keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); - mockObjectSelect.triggerCallback('change', 'object2'); - expect(keySelect.getSelected()).toEqual(''); - }); - - it('maintains its selected state on change if field is present in new object', function () { - mockManager.metadataLoadCompleted.and.returnValue(true); - keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); - mockObjectSelect.triggerCallback('change', 'object3'); - expect(keySelect.getSelected()).toEqual('a'); - }); + mockManager.getTelemetryMetadata.and.callFake(function (key) { + return mockMetadata[key]; + }); }); + + it('waits until the metadata fully loads to populate itself', function () { + mockManager.metadataLoadCompleted.and.returnValue(false); + keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); + expect(keySelect.getSelected()).toEqual(''); + }); + + it('populates itself with metadata on a metadata load', function () { + mockManager.metadataLoadCompleted.and.returnValue(false); + keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); + mockManager.triggerCallback('metadata'); + expect(keySelect.getSelected()).toEqual('a'); + }); + + it('populates itself with metadata if metadata load is already complete', function () { + mockManager.metadataLoadCompleted.and.returnValue(true); + keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); + expect(keySelect.getSelected()).toEqual('a'); + }); + + it('clears its selection state if the property in its config is not in its object', function () { + mockManager.metadataLoadCompleted.and.returnValue(true); + keySelect = new KeySelect(mockBadConfig, mockObjectSelect, mockManager); + expect(keySelect.getSelected()).toEqual(''); + }); + + it('populates with the appropriate options when its linked object changes', function () { + mockManager.metadataLoadCompleted.and.returnValue(true); + keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); + mockObjectSelect.triggerCallback('change', 'object2'); + keySelect.setSelected('alpha'); + expect(keySelect.getSelected()).toEqual('alpha'); + }); + + it('clears its selected state on change if the field is not present in the new object', function () { + mockManager.metadataLoadCompleted.and.returnValue(true); + keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); + mockObjectSelect.triggerCallback('change', 'object2'); + expect(keySelect.getSelected()).toEqual(''); + }); + + it('maintains its selected state on change if field is present in new object', function () { + mockManager.metadataLoadCompleted.and.returnValue(true); + keySelect = new KeySelect(mockConfig, mockObjectSelect, mockManager); + mockObjectSelect.triggerCallback('change', 'object3'); + expect(keySelect.getSelected()).toEqual('a'); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/input/ObjectSelectSpec.js b/src/plugins/summaryWidget/test/input/ObjectSelectSpec.js index 90d3c8c41f..5717331376 100644 --- a/src/plugins/summaryWidget/test/input/ObjectSelectSpec.js +++ b/src/plugins/summaryWidget/test/input/ObjectSelectSpec.js @@ -1,113 +1,112 @@ define(['../../src/input/ObjectSelect'], function (ObjectSelect) { - describe('A select for choosing composition objects', function () { - let mockConfig; - let mockBadConfig; - let mockManager; - let objectSelect; - let mockComposition; - beforeEach(function () { - mockConfig = { - object: 'key1' - }; + describe('A select for choosing composition objects', function () { + let mockConfig; + let mockBadConfig; + let mockManager; + let objectSelect; + let mockComposition; + beforeEach(function () { + mockConfig = { + object: 'key1' + }; - mockBadConfig = { - object: 'someNonexistentObject' - }; + mockBadConfig = { + object: 'someNonexistentObject' + }; - mockComposition = { - key1: { - identifier: { - key: 'key1' - }, - name: 'Object 1' - }, - key2: { - identifier: { - key: 'key2' - }, - name: 'Object 2' - } - }; - mockManager = jasmine.createSpyObj('mockManager', [ - 'on', - 'loadCompleted', - 'triggerCallback', - 'getComposition' - ]); + mockComposition = { + key1: { + identifier: { + key: 'key1' + }, + name: 'Object 1' + }, + key2: { + identifier: { + key: 'key2' + }, + name: 'Object 2' + } + }; + mockManager = jasmine.createSpyObj('mockManager', [ + 'on', + 'loadCompleted', + 'triggerCallback', + 'getComposition' + ]); - mockManager.on.and.callFake((event, callback) => { - mockManager.callbacks = mockManager.callbacks || {}; - mockManager.callbacks[event] = callback; - }); + mockManager.on.and.callFake((event, callback) => { + mockManager.callbacks = mockManager.callbacks || {}; + mockManager.callbacks[event] = callback; + }); - mockManager.triggerCallback.and.callFake((event, newObj) => { - if (event === 'add') { - mockManager.callbacks.add(newObj); - } else { - mockManager.callbacks[event](); - } - }); + mockManager.triggerCallback.and.callFake((event, newObj) => { + if (event === 'add') { + mockManager.callbacks.add(newObj); + } else { + mockManager.callbacks[event](); + } + }); - mockManager.getComposition.and.callFake(function () { - return mockComposition; - }); - - }); - - it('allows setting special keyword options', function () { - mockManager.loadCompleted.and.returnValue(true); - objectSelect = new ObjectSelect(mockConfig, mockManager, [ - ['keyword1', 'A special option'], - ['keyword2', 'A special option'] - ]); - objectSelect.setSelected('keyword1'); - expect(objectSelect.getSelected()).toEqual('keyword1'); - }); - - it('waits until the composition fully loads to populate itself', function () { - mockManager.loadCompleted.and.returnValue(false); - objectSelect = new ObjectSelect(mockConfig, mockManager); - expect(objectSelect.getSelected()).toEqual(''); - }); - - it('populates itself with composition objects on a composition load', function () { - mockManager.loadCompleted.and.returnValue(false); - objectSelect = new ObjectSelect(mockConfig, mockManager); - mockManager.triggerCallback('load'); - expect(objectSelect.getSelected()).toEqual('key1'); - }); - - it('populates itself with composition objects if load is already complete', function () { - mockManager.loadCompleted.and.returnValue(true); - objectSelect = new ObjectSelect(mockConfig, mockManager); - expect(objectSelect.getSelected()).toEqual('key1'); - }); - - it('clears its selection state if the object in its config is not in the composition', function () { - mockManager.loadCompleted.and.returnValue(true); - objectSelect = new ObjectSelect(mockBadConfig, mockManager); - expect(objectSelect.getSelected()).toEqual(''); - }); - - it('adds a new option on a composition add', function () { - mockManager.loadCompleted.and.returnValue(true); - objectSelect = new ObjectSelect(mockConfig, mockManager); - mockManager.triggerCallback('add', { - identifier: { - key: 'key3' - }, - name: 'Object 3' - }); - objectSelect.setSelected('key3'); - expect(objectSelect.getSelected()).toEqual('key3'); - }); - - it('removes an option on a composition remove', function () { - mockManager.loadCompleted.and.returnValue(true); - objectSelect = new ObjectSelect(mockConfig, mockManager); - delete mockComposition.key1; - mockManager.triggerCallback('remove'); - expect(objectSelect.getSelected()).not.toEqual('key1'); - }); + mockManager.getComposition.and.callFake(function () { + return mockComposition; + }); }); + + it('allows setting special keyword options', function () { + mockManager.loadCompleted.and.returnValue(true); + objectSelect = new ObjectSelect(mockConfig, mockManager, [ + ['keyword1', 'A special option'], + ['keyword2', 'A special option'] + ]); + objectSelect.setSelected('keyword1'); + expect(objectSelect.getSelected()).toEqual('keyword1'); + }); + + it('waits until the composition fully loads to populate itself', function () { + mockManager.loadCompleted.and.returnValue(false); + objectSelect = new ObjectSelect(mockConfig, mockManager); + expect(objectSelect.getSelected()).toEqual(''); + }); + + it('populates itself with composition objects on a composition load', function () { + mockManager.loadCompleted.and.returnValue(false); + objectSelect = new ObjectSelect(mockConfig, mockManager); + mockManager.triggerCallback('load'); + expect(objectSelect.getSelected()).toEqual('key1'); + }); + + it('populates itself with composition objects if load is already complete', function () { + mockManager.loadCompleted.and.returnValue(true); + objectSelect = new ObjectSelect(mockConfig, mockManager); + expect(objectSelect.getSelected()).toEqual('key1'); + }); + + it('clears its selection state if the object in its config is not in the composition', function () { + mockManager.loadCompleted.and.returnValue(true); + objectSelect = new ObjectSelect(mockBadConfig, mockManager); + expect(objectSelect.getSelected()).toEqual(''); + }); + + it('adds a new option on a composition add', function () { + mockManager.loadCompleted.and.returnValue(true); + objectSelect = new ObjectSelect(mockConfig, mockManager); + mockManager.triggerCallback('add', { + identifier: { + key: 'key3' + }, + name: 'Object 3' + }); + objectSelect.setSelected('key3'); + expect(objectSelect.getSelected()).toEqual('key3'); + }); + + it('removes an option on a composition remove', function () { + mockManager.loadCompleted.and.returnValue(true); + objectSelect = new ObjectSelect(mockConfig, mockManager); + delete mockComposition.key1; + mockManager.triggerCallback('remove'); + expect(objectSelect.getSelected()).not.toEqual('key1'); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/input/OperationSelectSpec.js b/src/plugins/summaryWidget/test/input/OperationSelectSpec.js index 2f1a3c38fa..c57bc36578 100644 --- a/src/plugins/summaryWidget/test/input/OperationSelectSpec.js +++ b/src/plugins/summaryWidget/test/input/OperationSelectSpec.js @@ -1,148 +1,143 @@ define(['../../src/input/OperationSelect'], function (OperationSelect) { - describe('A select for choosing composition object properties', function () { - let mockConfig; - let mockBadConfig; - let mockManager; - let operationSelect; - let mockOperations; - let mockPropertyTypes; - let mockKeySelect; - let mockEvaluator; - beforeEach(function () { + describe('A select for choosing composition object properties', function () { + let mockConfig; + let mockBadConfig; + let mockManager; + let operationSelect; + let mockOperations; + let mockPropertyTypes; + let mockKeySelect; + let mockEvaluator; + beforeEach(function () { + mockConfig = { + object: 'object1', + key: 'a', + operation: 'operation1' + }; - mockConfig = { - object: 'object1', - key: 'a', - operation: 'operation1' - }; + mockBadConfig = { + object: 'object1', + key: 'a', + operation: 'someNonexistentOperation' + }; - mockBadConfig = { - object: 'object1', - key: 'a', - operation: 'someNonexistentOperation' - }; + mockOperations = { + operation1: { + text: 'An operation', + appliesTo: ['number'] + }, + operation2: { + text: 'Another operation', + appliesTo: ['string'] + } + }; - mockOperations = { - operation1: { - text: 'An operation', - appliesTo: ['number'] - }, - operation2: { - text: 'Another operation', - appliesTo: ['string'] - } - }; + mockPropertyTypes = { + object1: { + a: 'number', + b: 'string', + c: 'number' + } + }; - mockPropertyTypes = { - object1: { - a: 'number', - b: 'string', - c: 'number' - } - }; + mockManager = jasmine.createSpyObj('mockManager', [ + 'on', + 'metadataLoadCompleted', + 'triggerCallback', + 'getTelemetryPropertyType', + 'getEvaluator' + ]); - mockManager = jasmine.createSpyObj('mockManager', [ - 'on', - 'metadataLoadCompleted', - 'triggerCallback', - 'getTelemetryPropertyType', - 'getEvaluator' + mockKeySelect = jasmine.createSpyObj('mockKeySelect', ['on', 'triggerCallback']); - ]); + mockEvaluator = jasmine.createSpyObj('mockEvaluator', [ + 'getOperationKeys', + 'operationAppliesTo', + 'getOperationText' + ]); - mockKeySelect = jasmine.createSpyObj('mockKeySelect', [ - 'on', - 'triggerCallback' - ]); + mockEvaluator.getOperationKeys.and.returnValue(Object.keys(mockOperations)); - mockEvaluator = jasmine.createSpyObj('mockEvaluator', [ - 'getOperationKeys', - 'operationAppliesTo', - 'getOperationText' - ]); + mockEvaluator.getOperationText.and.callFake(function (key) { + return mockOperations[key].text; + }); - mockEvaluator.getOperationKeys.and.returnValue(Object.keys(mockOperations)); + mockEvaluator.operationAppliesTo.and.callFake(function (operation, type) { + return mockOperations[operation].appliesTo.includes(type); + }); - mockEvaluator.getOperationText.and.callFake(function (key) { - return mockOperations[key].text; - }); + mockKeySelect.on.and.callFake((event, callback) => { + mockKeySelect.callbacks = mockKeySelect.callbacks || {}; + mockKeySelect.callbacks[event] = callback; + }); - mockEvaluator.operationAppliesTo.and.callFake(function (operation, type) { - return (mockOperations[operation].appliesTo.includes(type)); - }); + mockKeySelect.triggerCallback.and.callFake((event, key) => { + mockKeySelect.callbacks[event](key); + }); - mockKeySelect.on.and.callFake((event, callback) => { - mockKeySelect.callbacks = mockKeySelect.callbacks || {}; - mockKeySelect.callbacks[event] = callback; - }); + mockManager.on.and.callFake((event, callback) => { + mockManager.callbacks = mockManager.callbacks || {}; + mockManager.callbacks[event] = callback; + }); - mockKeySelect.triggerCallback.and.callFake((event, key) => { - mockKeySelect.callbacks[event](key); - }); + mockManager.triggerCallback.and.callFake((event) => { + mockManager.callbacks[event](); + }); - mockManager.on.and.callFake((event, callback) => { - mockManager.callbacks = mockManager.callbacks || {}; - mockManager.callbacks[event] = callback; - }); + mockManager.getTelemetryPropertyType.and.callFake(function (object, key) { + return mockPropertyTypes[object][key]; + }); - mockManager.triggerCallback.and.callFake(event => { - mockManager.callbacks[event](); - }); - - mockManager.getTelemetryPropertyType.and.callFake(function (object, key) { - return mockPropertyTypes[object][key]; - }); - - mockManager.getEvaluator.and.returnValue(mockEvaluator); - }); - - it('waits until the metadata fully loads to populate itself', function () { - mockManager.metadataLoadCompleted.and.returnValue(false); - operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); - expect(operationSelect.getSelected()).toEqual(''); - }); - - it('populates itself with operations on a metadata load', function () { - mockManager.metadataLoadCompleted.and.returnValue(false); - operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); - mockManager.triggerCallback('metadata'); - expect(operationSelect.getSelected()).toEqual('operation1'); - }); - - it('populates itself with operations if metadata load is already complete', function () { - mockManager.metadataLoadCompleted.and.returnValue(true); - operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); - expect(operationSelect.getSelected()).toEqual('operation1'); - }); - - it('clears its selection state if the operation in its config does not apply', function () { - mockManager.metadataLoadCompleted.and.returnValue(true); - operationSelect = new OperationSelect(mockBadConfig, mockKeySelect, mockManager); - expect(operationSelect.getSelected()).toEqual(''); - }); - - it('populates with the appropriate options when its linked key changes', function () { - mockManager.metadataLoadCompleted.and.returnValue(true); - operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); - mockKeySelect.triggerCallback('change', 'b'); - operationSelect.setSelected('operation2'); - expect(operationSelect.getSelected()).toEqual('operation2'); - operationSelect.setSelected('operation1'); - expect(operationSelect.getSelected()).not.toEqual('operation1'); - }); - - it('clears its selection on a change if the operation does not apply', function () { - mockManager.metadataLoadCompleted.and.returnValue(true); - operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); - mockKeySelect.triggerCallback('change', 'b'); - expect(operationSelect.getSelected()).toEqual(''); - }); - - it('maintains its selected state on change if the operation does apply', function () { - mockManager.metadataLoadCompleted.and.returnValue(true); - operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); - mockKeySelect.triggerCallback('change', 'c'); - expect(operationSelect.getSelected()).toEqual('operation1'); - }); + mockManager.getEvaluator.and.returnValue(mockEvaluator); }); + + it('waits until the metadata fully loads to populate itself', function () { + mockManager.metadataLoadCompleted.and.returnValue(false); + operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); + expect(operationSelect.getSelected()).toEqual(''); + }); + + it('populates itself with operations on a metadata load', function () { + mockManager.metadataLoadCompleted.and.returnValue(false); + operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); + mockManager.triggerCallback('metadata'); + expect(operationSelect.getSelected()).toEqual('operation1'); + }); + + it('populates itself with operations if metadata load is already complete', function () { + mockManager.metadataLoadCompleted.and.returnValue(true); + operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); + expect(operationSelect.getSelected()).toEqual('operation1'); + }); + + it('clears its selection state if the operation in its config does not apply', function () { + mockManager.metadataLoadCompleted.and.returnValue(true); + operationSelect = new OperationSelect(mockBadConfig, mockKeySelect, mockManager); + expect(operationSelect.getSelected()).toEqual(''); + }); + + it('populates with the appropriate options when its linked key changes', function () { + mockManager.metadataLoadCompleted.and.returnValue(true); + operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); + mockKeySelect.triggerCallback('change', 'b'); + operationSelect.setSelected('operation2'); + expect(operationSelect.getSelected()).toEqual('operation2'); + operationSelect.setSelected('operation1'); + expect(operationSelect.getSelected()).not.toEqual('operation1'); + }); + + it('clears its selection on a change if the operation does not apply', function () { + mockManager.metadataLoadCompleted.and.returnValue(true); + operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); + mockKeySelect.triggerCallback('change', 'b'); + expect(operationSelect.getSelected()).toEqual(''); + }); + + it('maintains its selected state on change if the operation does apply', function () { + mockManager.metadataLoadCompleted.and.returnValue(true); + operationSelect = new OperationSelect(mockConfig, mockKeySelect, mockManager); + mockKeySelect.triggerCallback('change', 'c'); + expect(operationSelect.getSelected()).toEqual('operation1'); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/input/PaletteSpec.js b/src/plugins/summaryWidget/test/input/PaletteSpec.js index 25f6819f95..67678cb049 100644 --- a/src/plugins/summaryWidget/test/input/PaletteSpec.js +++ b/src/plugins/summaryWidget/test/input/PaletteSpec.js @@ -1,44 +1,44 @@ define(['../../src/input/Palette'], function (Palette) { - describe('A generic Open MCT palette input', function () { - let palette; - let callbackSpy1; - let callbackSpy2; + describe('A generic Open MCT palette input', function () { + let palette; + let callbackSpy1; + let callbackSpy2; - beforeEach(function () { - palette = new Palette('someClass', 'someContainer', ['item1', 'item2', 'item3']); - callbackSpy1 = jasmine.createSpy('changeCallback1'); - callbackSpy2 = jasmine.createSpy('changeCallback2'); - }); - - it('gets the current item', function () { - expect(palette.getCurrent()).toEqual('item1'); - }); - - it('allows setting the current item', function () { - palette.set('item2'); - expect(palette.getCurrent()).toEqual('item2'); - }); - - it('allows registering change callbacks, and errors when an unsupported event is registered', function () { - expect(function () { - palette.on('change', callbackSpy1); - }).not.toThrow(); - expect(function () { - palette.on('someUnsupportedEvent', callbackSpy1); - }).toThrow(); - }); - - it('injects its callbacks with the new selected item on change', function () { - palette.on('change', callbackSpy1); - palette.on('change', callbackSpy2); - palette.set('item2'); - expect(callbackSpy1).toHaveBeenCalledWith('item2'); - expect(callbackSpy2).toHaveBeenCalledWith('item2'); - }); - - it('gracefully handles being set to an item not included in its set', function () { - palette.set('foobar'); - expect(palette.getCurrent()).not.toEqual('foobar'); - }); + beforeEach(function () { + palette = new Palette('someClass', 'someContainer', ['item1', 'item2', 'item3']); + callbackSpy1 = jasmine.createSpy('changeCallback1'); + callbackSpy2 = jasmine.createSpy('changeCallback2'); }); + + it('gets the current item', function () { + expect(palette.getCurrent()).toEqual('item1'); + }); + + it('allows setting the current item', function () { + palette.set('item2'); + expect(palette.getCurrent()).toEqual('item2'); + }); + + it('allows registering change callbacks, and errors when an unsupported event is registered', function () { + expect(function () { + palette.on('change', callbackSpy1); + }).not.toThrow(); + expect(function () { + palette.on('someUnsupportedEvent', callbackSpy1); + }).toThrow(); + }); + + it('injects its callbacks with the new selected item on change', function () { + palette.on('change', callbackSpy1); + palette.on('change', callbackSpy2); + palette.set('item2'); + expect(callbackSpy1).toHaveBeenCalledWith('item2'); + expect(callbackSpy2).toHaveBeenCalledWith('item2'); + }); + + it('gracefully handles being set to an item not included in its set', function () { + palette.set('foobar'); + expect(palette.getCurrent()).not.toEqual('foobar'); + }); + }); }); diff --git a/src/plugins/summaryWidget/test/input/SelectSpec.js b/src/plugins/summaryWidget/test/input/SelectSpec.js index bd895507fa..bdd0dac774 100644 --- a/src/plugins/summaryWidget/test/input/SelectSpec.js +++ b/src/plugins/summaryWidget/test/input/SelectSpec.js @@ -1,54 +1,61 @@ define(['../../src/input/Select'], function (Select) { - describe('A select wrapper', function () { - let select; - let testOptions; - let callbackSpy1; - let callbackSpy2; - beforeEach(function () { - select = new Select(); - testOptions = [['item1', 'Item 1'], ['item2', 'Item 2'], ['item3', 'Item 3']]; - select.setOptions(testOptions); - callbackSpy1 = jasmine.createSpy('callbackSpy1'); - callbackSpy2 = jasmine.createSpy('callbackSpy2'); - }); - - it('gets and sets the current item', function () { - select.setSelected('item1'); - expect(select.getSelected()).toEqual('item1'); - }); - - it('allows adding a single new option', function () { - select.addOption('newOption', 'A New Option'); - select.setSelected('newOption'); - expect(select.getSelected()).toEqual('newOption'); - }); - - it('allows populating with a new set of options', function () { - select.setOptions([['newItem1', 'Item 1'], ['newItem2', 'Item 2']]); - select.setSelected('newItem1'); - expect(select.getSelected()).toEqual('newItem1'); - }); - - it('allows registering change callbacks, and errors when an unsupported event is registered', function () { - expect(function () { - select.on('change', callbackSpy1); - }).not.toThrow(); - expect(function () { - select.on('someUnsupportedEvent', callbackSpy1); - }).toThrow(); - }); - - it('injects its callbacks with its property and value on a change', function () { - select.on('change', callbackSpy1); - select.on('change', callbackSpy2); - select.setSelected('item2'); - expect(callbackSpy1).toHaveBeenCalledWith('item2'); - expect(callbackSpy2).toHaveBeenCalledWith('item2'); - }); - - it('gracefully handles being set to an item not included in its set', function () { - select.setSelected('foobar'); - expect(select.getSelected()).not.toEqual('foobar'); - }); + describe('A select wrapper', function () { + let select; + let testOptions; + let callbackSpy1; + let callbackSpy2; + beforeEach(function () { + select = new Select(); + testOptions = [ + ['item1', 'Item 1'], + ['item2', 'Item 2'], + ['item3', 'Item 3'] + ]; + select.setOptions(testOptions); + callbackSpy1 = jasmine.createSpy('callbackSpy1'); + callbackSpy2 = jasmine.createSpy('callbackSpy2'); }); + + it('gets and sets the current item', function () { + select.setSelected('item1'); + expect(select.getSelected()).toEqual('item1'); + }); + + it('allows adding a single new option', function () { + select.addOption('newOption', 'A New Option'); + select.setSelected('newOption'); + expect(select.getSelected()).toEqual('newOption'); + }); + + it('allows populating with a new set of options', function () { + select.setOptions([ + ['newItem1', 'Item 1'], + ['newItem2', 'Item 2'] + ]); + select.setSelected('newItem1'); + expect(select.getSelected()).toEqual('newItem1'); + }); + + it('allows registering change callbacks, and errors when an unsupported event is registered', function () { + expect(function () { + select.on('change', callbackSpy1); + }).not.toThrow(); + expect(function () { + select.on('someUnsupportedEvent', callbackSpy1); + }).toThrow(); + }); + + it('injects its callbacks with its property and value on a change', function () { + select.on('change', callbackSpy1); + select.on('change', callbackSpy2); + select.setSelected('item2'); + expect(callbackSpy1).toHaveBeenCalledWith('item2'); + expect(callbackSpy2).toHaveBeenCalledWith('item2'); + }); + + it('gracefully handles being set to an item not included in its set', function () { + select.setSelected('foobar'); + expect(select.getSelected()).not.toEqual('foobar'); + }); + }); }); diff --git a/src/plugins/tabs/components/tabs.scss b/src/plugins/tabs/components/tabs.scss index bb5df6144b..1f1cb3d6b0 100644 --- a/src/plugins/tabs/components/tabs.scss +++ b/src/plugins/tabs/components/tabs.scss @@ -1,60 +1,60 @@ .c-tabs-view { - $h: 20px; - @include abs(); - display: flex; - flex-flow: column nowrap; + $h: 20px; + @include abs(); + display: flex; + flex-flow: column nowrap; + + > * + * { + margin-top: $interiorMargin; + } + + &__tabs-holder { + min-height: $h; + } + + &__tab { + justify-content: space-between; // Places remove button to far side of tab + + &__close-btn { + flex: 0 0 auto; + pointer-events: all; + } > * + * { - margin-top: $interiorMargin; + margin-left: $interiorMargin; } + } - &__tabs-holder { - min-height: $h; + &__object-holder { + flex: 1 1 auto; + display: flex; + flex-direction: column; + + &--hidden { + position: absolute; + left: -9999px; + top: -9999px; } + } - &__tab { - justify-content: space-between; // Places remove button to far side of tab + &__object-name { + font-size: 1em; + margin: $interiorMargin 0 $interiorMarginLg 0; + } - &__close-btn { - flex: 0 0 auto; - pointer-events: all; - } + &__object { + display: flex; + flex-flow: column nowrap; + flex: 1 1 auto; + height: 0; // Chrome 73 overflow bug fix + } - > * + * { - margin-left: $interiorMargin; - } - } - - &__object-holder { - flex: 1 1 auto; - display: flex; - flex-direction: column; - - &--hidden { - position: absolute; - left: -9999px; - top: -9999px; - } - } - - &__object-name { - font-size: 1em; - margin: $interiorMargin 0 $interiorMarginLg 0; - } - - &__object { - display: flex; - flex-flow: column nowrap; - flex: 1 1 auto; - height: 0; // Chrome 73 overflow bug fix - } - - &__empty-message { - background: rgba($colorBodyFg, 0.1); - color: rgba($colorBodyFg, 0.7); - font-style: italic; - text-align: center; - line-height: $h; - width: 100%; - } + &__empty-message { + background: rgba($colorBodyFg, 0.1); + color: rgba($colorBodyFg, 0.7); + font-style: italic; + text-align: center; + line-height: $h; + width: 100%; + } } diff --git a/src/plugins/tabs/components/tabs.vue b/src/plugins/tabs/components/tabs.vue index 1aec04a099..5835a62766 100644 --- a/src/plugins/tabs/components/tabs.vue +++ b/src/plugins/tabs/components/tabs.vue @@ -20,77 +20,60 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/tabs/plugin.js b/src/plugins/tabs/plugin.js index e38c47b51e..87be068713 100644 --- a/src/plugins/tabs/plugin.js +++ b/src/plugins/tabs/plugin.js @@ -20,44 +20,40 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './tabs' -], function ( - Tabs -) { - return function plugin() { - return function install(openmct) { - openmct.objectViews.addProvider(new Tabs(openmct)); +define(['./tabs'], function (Tabs) { + return function plugin() { + return function install(openmct) { + openmct.objectViews.addProvider(new Tabs(openmct)); - openmct.types.addType('tabs', { - name: "Tabs View", - description: 'Quickly navigate between multiple objects of any type using tabs.', - creatable: true, - cssClass: 'icon-tabs-view', - initialize(domainObject) { - domainObject.composition = []; - domainObject.keep_alive = true; - }, - form: [ - { - "key": "keep_alive", - "name": "Eager Load Tabs", - "control": "select", - "options": [ - { - 'name': 'True', - 'value': true - }, - { - 'name': 'False', - 'value': false - } - ], - "required": true, - "cssClass": "l-input" - } - ] - }); - }; + openmct.types.addType('tabs', { + name: 'Tabs View', + description: 'Quickly navigate between multiple objects of any type using tabs.', + creatable: true, + cssClass: 'icon-tabs-view', + initialize(domainObject) { + domainObject.composition = []; + domainObject.keep_alive = true; + }, + form: [ + { + key: 'keep_alive', + name: 'Eager Load Tabs', + control: 'select', + options: [ + { + name: 'True', + value: true + }, + { + name: 'False', + value: false + } + ], + required: true, + cssClass: 'l-input' + } + ] + }); }; + }; }); diff --git a/src/plugins/tabs/pluginSpec.js b/src/plugins/tabs/pluginSpec.js index 3261682a4d..63eef73586 100644 --- a/src/plugins/tabs/pluginSpec.js +++ b/src/plugins/tabs/pluginSpec.js @@ -20,200 +20,195 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { - createOpenMct, - resetApplicationState -} from 'utils/testing'; +import { createOpenMct, resetApplicationState } from 'utils/testing'; import TabsLayout from './plugin'; -import Vue from "vue"; -import EventEmitter from "EventEmitter"; +import Vue from 'vue'; +import EventEmitter from 'EventEmitter'; describe('the plugin', function () { - let element; - let child; - let openmct; - let tabsLayoutDefinition; - const testViewObject = { + let element; + let child; + let openmct; + let tabsLayoutDefinition; + const testViewObject = { + identifier: { + key: 'mock-tabs-object', + namespace: '' + }, + type: 'tabs', + name: 'Tabs view', + keep_alive: true, + composition: [ + { identifier: { - key: 'mock-tabs-object', - namespace: '' - }, - type: 'tabs', - name: 'Tabs view', - keep_alive: true, - composition: [ - { - 'identifier': { - 'namespace': '', - 'key': 'swg-1' - } - }, - { - 'identifier': { - 'namespace': '', - 'key': 'swg-2' - } - } - ] - }; - const telemetryItemTemplate = { - 'telemetry': { - 'period': 5, - 'amplitude': 5, - 'offset': 5, - 'dataRateInHz': 5, - 'phase': 5, - 'randomness': 0 - }, - 'type': 'generator', - 'modified': 1592851063871, - 'location': 'mine', - 'persisted': 1592851063871 - }; - let telemetryItem1 = Object.assign({}, telemetryItemTemplate, { - 'name': 'Sine Wave Generator 1', - 'identifier': { - 'namespace': '', - 'key': 'swg-1' + namespace: '', + key: 'swg-1' } - }); - let telemetryItem2 = Object.assign({}, telemetryItemTemplate, { - 'name': 'Sine Wave Generator 2', - 'identifier': { - 'namespace': '', - 'key': 'swg-2' + }, + { + identifier: { + namespace: '', + key: 'swg-2' } + } + ] + }; + const telemetryItemTemplate = { + telemetry: { + period: 5, + amplitude: 5, + offset: 5, + dataRateInHz: 5, + phase: 5, + randomness: 0 + }, + type: 'generator', + modified: 1592851063871, + location: 'mine', + persisted: 1592851063871 + }; + let telemetryItem1 = Object.assign({}, telemetryItemTemplate, { + name: 'Sine Wave Generator 1', + identifier: { + namespace: '', + key: 'swg-1' + } + }); + let telemetryItem2 = Object.assign({}, telemetryItemTemplate, { + name: 'Sine Wave Generator 2', + identifier: { + namespace: '', + key: 'swg-2' + } + }); + + beforeEach((done) => { + openmct = createOpenMct(); + openmct.install(new TabsLayout()); + tabsLayoutDefinition = openmct.types.get('tabs'); + + element = document.createElement('div'); + child = document.createElement('div'); + child.style.display = 'block'; + child.style.width = '1920px'; + child.style.height = '1080px'; + element.appendChild(child); + + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('defines a tabs object type with the correct key', () => { + expect(tabsLayoutDefinition.definition.name).toEqual('Tabs View'); + }); + + it('is creatable', () => { + expect(tabsLayoutDefinition.definition.creatable).toEqual(true); + }); + + describe('the view', function () { + let tabsLayoutViewProvider; + let mockComposition; + + beforeEach(() => { + mockComposition = new EventEmitter(); + mockComposition.load = () => { + return Promise.resolve([telemetryItem1]); + }; + + spyOn(openmct.composition, 'get').and.returnValue(mockComposition); + + const applicableViews = openmct.objectViews.get(testViewObject, []); + tabsLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'tabs'); + let view = tabsLayoutViewProvider.view(testViewObject, []); + view.show(child, true); + + return Vue.nextTick(); }); - beforeEach((done) => { - openmct = createOpenMct(); - openmct.install(new TabsLayout()); - tabsLayoutDefinition = openmct.types.get('tabs'); + it('provides a view', () => { + expect(tabsLayoutViewProvider).toBeDefined(); + }); - element = document.createElement('div'); - child = document.createElement('div'); - child.style.display = 'block'; - child.style.width = '1920px'; - child.style.height = '1080px'; - element.appendChild(child); + it('renders tab element', () => { + const tabsElements = element.querySelectorAll('.c-tabs'); - openmct.on('start', done); - openmct.startHeadless(); + expect(tabsElements.length).toBe(1); + }); + + it('renders empty tab element with msg', () => { + const tabsElement = element.querySelector('.c-tabs'); + + expect(tabsElement.innerText.trim()).toEqual('Drag objects here to add them to this view.'); + }); + }); + + describe('the view', function () { + let tabsLayoutViewProvider; + let mockComposition; + let count = 0; + + beforeEach(() => { + mockComposition = new EventEmitter(); + mockComposition.load = () => { + if (count === 0) { + mockComposition.emit('add', telemetryItem1); + mockComposition.emit('add', telemetryItem2); + count++; + } + + return Promise.resolve([telemetryItem1, telemetryItem2]); + }; + + spyOn(openmct.composition, 'get').and.returnValue(mockComposition); + + const applicableViews = openmct.objectViews.get(testViewObject, []); + tabsLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'tabs'); + let view = tabsLayoutViewProvider.view(testViewObject, []); + view.show(child, true); + + return Vue.nextTick(); }); afterEach(() => { - return resetApplicationState(openmct); + count = 0; + testViewObject.keep_alive = true; }); - it('defines a tabs object type with the correct key', () => { - expect(tabsLayoutDefinition.definition.name).toEqual('Tabs View'); + it('renders a tab for each item', () => { + let tabEls = element.querySelectorAll('.js-tab'); + + expect(tabEls.length).toEqual(2); }); - it('is creatable', () => { - expect(tabsLayoutDefinition.definition.creatable).toEqual(true); - }); - - describe('the view', function () { - let tabsLayoutViewProvider; - let mockComposition; - - beforeEach(() => { - mockComposition = new EventEmitter(); - mockComposition.load = () => { - return Promise.resolve([telemetryItem1]); - }; - - spyOn(openmct.composition, 'get').and.returnValue(mockComposition); - - const applicableViews = openmct.objectViews.get(testViewObject, []); - tabsLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'tabs'); - let view = tabsLayoutViewProvider.view(testViewObject, []); - view.show(child, true); - - return Vue.nextTick(); - }); - - it('provides a view', () => { - expect(tabsLayoutViewProvider).toBeDefined(); - }); - - it('renders tab element', () => { - const tabsElements = element.querySelectorAll('.c-tabs'); - - expect(tabsElements.length).toBe(1); - }); - - it('renders empty tab element with msg', () => { - const tabsElement = element.querySelector('.c-tabs'); - - expect(tabsElement.innerText.trim()).toEqual('Drag objects here to add them to this view.'); - }); - }); - - describe('the view', function () { - let tabsLayoutViewProvider; - let mockComposition; - let count = 0; - - beforeEach(() => { - mockComposition = new EventEmitter(); - mockComposition.load = () => { - if (count === 0) { - mockComposition.emit('add', telemetryItem1); - mockComposition.emit('add', telemetryItem2); - count++; - } - - return Promise.resolve([telemetryItem1, telemetryItem2]); - }; - - spyOn(openmct.composition, 'get').and.returnValue(mockComposition); - - const applicableViews = openmct.objectViews.get(testViewObject, []); - tabsLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'tabs'); - let view = tabsLayoutViewProvider.view(testViewObject, []); - view.show(child, true); - - return Vue.nextTick(); - }); - - afterEach(() => { - count = 0; - testViewObject.keep_alive = true; - }); - - it ('renders a tab for each item', () => { - let tabEls = element.querySelectorAll('.js-tab'); - - expect(tabEls.length).toEqual(2); - }); - - describe('with domainObject.keep_alive set to', () => { - - it ('true, will keep all views loaded, regardless of current tab view', async () => { - let tabEls = element.querySelectorAll('.js-tab'); - - for (let i = 0; i < tabEls.length; i++) { - const tab = tabEls[i]; - - tab.click(); - await Vue.nextTick(); - - const tabViewEls = element.querySelectorAll('.c-tabs-view__object'); - expect(tabViewEls.length).toEqual(2); - } - }); - - it ('false, will only keep the current tab view loaded', async () => { - testViewObject.keep_alive = false; - - await Vue.nextTick(); - - let tabViewEls = element.querySelectorAll('.c-tabs-view__object'); - - expect(tabViewEls.length).toEqual(1); - }); - - }); + describe('with domainObject.keep_alive set to', () => { + it('true, will keep all views loaded, regardless of current tab view', async () => { + let tabEls = element.querySelectorAll('.js-tab'); + + for (let i = 0; i < tabEls.length; i++) { + const tab = tabEls[i]; + + tab.click(); + await Vue.nextTick(); + + const tabViewEls = element.querySelectorAll('.c-tabs-view__object'); + expect(tabViewEls.length).toEqual(2); + } + }); + + it('false, will only keep the current tab view loaded', async () => { + testViewObject.keep_alive = false; + + await Vue.nextTick(); + + let tabViewEls = element.querySelectorAll('.c-tabs-view__object'); + + expect(tabViewEls.length).toEqual(1); + }); }); + }); }); diff --git a/src/plugins/tabs/tabs.js b/src/plugins/tabs/tabs.js index e6cc5a75a1..4cfe9b3afd 100644 --- a/src/plugins/tabs/tabs.js +++ b/src/plugins/tabs/tabs.js @@ -20,62 +20,56 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './components/tabs.vue', - 'vue' -], function ( - TabsComponent, - Vue -) { - function Tabs(openmct) { +define(['./components/tabs.vue', 'vue'], function (TabsComponent, Vue) { + function Tabs(openmct) { + return { + key: 'tabs', + name: 'Tabs', + cssClass: 'icon-list-view', + canView: function (domainObject) { + return domainObject.type === 'tabs'; + }, + canEdit: function (domainObject) { + return domainObject.type === 'tabs'; + }, + view: function (domainObject, objectPath) { + let component; + return { - key: 'tabs', - name: 'Tabs', - cssClass: 'icon-list-view', - canView: function (domainObject) { - return domainObject.type === 'tabs'; - }, - canEdit: function (domainObject) { - return domainObject.type === 'tabs'; - }, - view: function (domainObject, objectPath) { - let component; - + show: function (element, editMode) { + component = new Vue({ + el: element, + components: { + TabsComponent: TabsComponent.default + }, + provide: { + openmct, + domainObject, + objectPath, + composition: openmct.composition.get(domainObject) + }, + data() { return { - show: function (element, editMode) { - component = new Vue({ - el: element, - components: { - TabsComponent: TabsComponent.default - }, - provide: { - openmct, - domainObject, - objectPath, - composition: openmct.composition.get(domainObject) - }, - data() { - return { - isEditing: editMode - }; - }, - template: '' - }); - }, - onEditModeChange(editMode) { - component.isEditing = editMode; - }, - destroy: function (element) { - component.$destroy(); - component = undefined; - } + isEditing: editMode }; - }, - priority: function () { - return 1; - } + }, + template: '' + }); + }, + onEditModeChange(editMode) { + component.isEditing = editMode; + }, + destroy: function (element) { + component.$destroy(); + component = undefined; + } }; - } + }, + priority: function () { + return 1; + } + }; + } - return Tabs; + return Tabs; }); diff --git a/src/plugins/telemetryMean/plugin.js b/src/plugins/telemetryMean/plugin.js index ee49cecd03..3920d811e7 100755 --- a/src/plugins/telemetryMean/plugin.js +++ b/src/plugins/telemetryMean/plugin.js @@ -1,76 +1,78 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2023, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define(['./src/MeanTelemetryProvider'], function (MeanTelemetryProvider) { - const DEFAULT_SAMPLES = 10; - - function plugin() { - return function install(openmct) { - openmct.types.addType('telemetry-mean', { - name: 'Telemetry Filter', - description: 'Provides telemetry values that represent the mean of the last N values of a telemetry stream', - creatable: true, - cssClass: 'icon-telemetry', - initialize: function (domainObject) { - domainObject.samples = DEFAULT_SAMPLES; - domainObject.telemetry = {}; - domainObject.telemetry.values = - openmct.time.getAllTimeSystems().map(function (timeSystem, index) { - return { - key: timeSystem.key, - name: timeSystem.name, - hints: { - domain: index + 1 - } - }; - }); - domainObject.telemetry.values.push({ - key: "value", - name: "Value", - hints: { - range: 1 - } - }); - }, - form: [ - { - "key": "telemetryPoint", - "name": "Telemetry Point", - "control": "textfield", - "required": true, - "cssClass": "l-input-lg" - }, - { - "key": "samples", - "name": "Samples to Average", - "control": "textfield", - "required": true, - "cssClass": "l-input-sm" - } - ] - }); - openmct.telemetry.addProvider(new MeanTelemetryProvider(openmct)); - }; - } - - return plugin; -}); +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2023, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define(['./src/MeanTelemetryProvider'], function (MeanTelemetryProvider) { + const DEFAULT_SAMPLES = 10; + + function plugin() { + return function install(openmct) { + openmct.types.addType('telemetry-mean', { + name: 'Telemetry Filter', + description: + 'Provides telemetry values that represent the mean of the last N values of a telemetry stream', + creatable: true, + cssClass: 'icon-telemetry', + initialize: function (domainObject) { + domainObject.samples = DEFAULT_SAMPLES; + domainObject.telemetry = {}; + domainObject.telemetry.values = openmct.time + .getAllTimeSystems() + .map(function (timeSystem, index) { + return { + key: timeSystem.key, + name: timeSystem.name, + hints: { + domain: index + 1 + } + }; + }); + domainObject.telemetry.values.push({ + key: 'value', + name: 'Value', + hints: { + range: 1 + } + }); + }, + form: [ + { + key: 'telemetryPoint', + name: 'Telemetry Point', + control: 'textfield', + required: true, + cssClass: 'l-input-lg' + }, + { + key: 'samples', + name: 'Samples to Average', + control: 'textfield', + required: true, + cssClass: 'l-input-sm' + } + ] + }); + openmct.telemetry.addProvider(new MeanTelemetryProvider(openmct)); + }; + } + + return plugin; +}); diff --git a/src/plugins/telemetryMean/src/MeanTelemetryProvider.js b/src/plugins/telemetryMean/src/MeanTelemetryProvider.js index 00545f2327..65dd435d0b 100644 --- a/src/plugins/telemetryMean/src/MeanTelemetryProvider.js +++ b/src/plugins/telemetryMean/src/MeanTelemetryProvider.js @@ -20,97 +20,114 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - 'objectUtils', - './TelemetryAverager' -], function (objectUtils, TelemetryAverager) { +define(['objectUtils', './TelemetryAverager'], function (objectUtils, TelemetryAverager) { + function MeanTelemetryProvider(openmct) { + this.openmct = openmct; + this.telemetryAPI = openmct.telemetry; + this.timeAPI = openmct.time; + this.objectAPI = openmct.objects; + this.perObjectProviders = {}; + } - function MeanTelemetryProvider(openmct) { - this.openmct = openmct; - this.telemetryAPI = openmct.telemetry; - this.timeAPI = openmct.time; - this.objectAPI = openmct.objects; - this.perObjectProviders = {}; + MeanTelemetryProvider.prototype.canProvideTelemetry = function (domainObject) { + return domainObject.type === 'telemetry-mean'; + }; + + MeanTelemetryProvider.prototype.supportsRequest = + MeanTelemetryProvider.prototype.supportsSubscribe = + MeanTelemetryProvider.prototype.canProvideTelemetry; + + MeanTelemetryProvider.prototype.subscribe = function (domainObject, callback) { + let wrappedUnsubscribe; + let unsubscribeCalled = false; + const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint); + const samples = domainObject.samples; + + this.objectAPI + .get(objectId) + .then( + function (linkedDomainObject) { + if (!unsubscribeCalled) { + wrappedUnsubscribe = this.subscribeToAverage(linkedDomainObject, samples, callback); + } + }.bind(this) + ) + .catch(logError); + + return function unsubscribe() { + unsubscribeCalled = true; + if (wrappedUnsubscribe !== undefined) { + wrappedUnsubscribe(); + } + }; + }; + + MeanTelemetryProvider.prototype.subscribeToAverage = function (domainObject, samples, callback) { + const telemetryAverager = new TelemetryAverager( + this.telemetryAPI, + this.timeAPI, + domainObject, + samples, + callback + ); + const createAverageDatum = telemetryAverager.createAverageDatum.bind(telemetryAverager); + + return this.telemetryAPI.subscribe(domainObject, createAverageDatum); + }; + + MeanTelemetryProvider.prototype.request = function (domainObject, request) { + const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint); + const samples = domainObject.samples; + + return this.objectAPI.get(objectId).then( + function (linkedDomainObject) { + return this.requestAverageTelemetry(linkedDomainObject, request, samples); + }.bind(this) + ); + }; + + /** + * @private + */ + MeanTelemetryProvider.prototype.requestAverageTelemetry = function ( + domainObject, + request, + samples + ) { + const averageData = []; + const addToAverageData = averageData.push.bind(averageData); + const telemetryAverager = new TelemetryAverager( + this.telemetryAPI, + this.timeAPI, + domainObject, + samples, + addToAverageData + ); + const createAverageDatum = telemetryAverager.createAverageDatum.bind(telemetryAverager); + + return this.telemetryAPI.request(domainObject, request).then(function (telemetryData) { + telemetryData.forEach(createAverageDatum); + + return averageData; + }); + }; + + /** + * @private + */ + MeanTelemetryProvider.prototype.getLinkedObject = function (domainObject) { + const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint); + + return this.objectAPI.get(objectId); + }; + + function logError(error) { + if (error.stack) { + console.error(error.stack); + } else { + console.error(error); } + } - MeanTelemetryProvider.prototype.canProvideTelemetry = function (domainObject) { - return domainObject.type === 'telemetry-mean'; - }; - - MeanTelemetryProvider.prototype.supportsRequest = - MeanTelemetryProvider.prototype.supportsSubscribe = - MeanTelemetryProvider.prototype.canProvideTelemetry; - - MeanTelemetryProvider.prototype.subscribe = function (domainObject, callback) { - let wrappedUnsubscribe; - let unsubscribeCalled = false; - const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint); - const samples = domainObject.samples; - - this.objectAPI.get(objectId) - .then(function (linkedDomainObject) { - if (!unsubscribeCalled) { - wrappedUnsubscribe = this.subscribeToAverage(linkedDomainObject, samples, callback); - } - }.bind(this)) - .catch(logError); - - return function unsubscribe() { - unsubscribeCalled = true; - if (wrappedUnsubscribe !== undefined) { - wrappedUnsubscribe(); - } - }; - }; - - MeanTelemetryProvider.prototype.subscribeToAverage = function (domainObject, samples, callback) { - const telemetryAverager = new TelemetryAverager(this.telemetryAPI, this.timeAPI, domainObject, samples, callback); - const createAverageDatum = telemetryAverager.createAverageDatum.bind(telemetryAverager); - - return this.telemetryAPI.subscribe(domainObject, createAverageDatum); - }; - - MeanTelemetryProvider.prototype.request = function (domainObject, request) { - const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint); - const samples = domainObject.samples; - - return this.objectAPI.get(objectId).then(function (linkedDomainObject) { - return this.requestAverageTelemetry(linkedDomainObject, request, samples); - }.bind(this)); - }; - - /** - * @private - */ - MeanTelemetryProvider.prototype.requestAverageTelemetry = function (domainObject, request, samples) { - const averageData = []; - const addToAverageData = averageData.push.bind(averageData); - const telemetryAverager = new TelemetryAverager(this.telemetryAPI, this.timeAPI, domainObject, samples, addToAverageData); - const createAverageDatum = telemetryAverager.createAverageDatum.bind(telemetryAverager); - - return this.telemetryAPI.request(domainObject, request).then(function (telemetryData) { - telemetryData.forEach(createAverageDatum); - - return averageData; - }); - }; - - /** - * @private - */ - MeanTelemetryProvider.prototype.getLinkedObject = function (domainObject) { - const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint); - - return this.objectAPI.get(objectId); - }; - - function logError(error) { - if (error.stack) { - console.error(error.stack); - } else { - console.error(error); - } - } - - return MeanTelemetryProvider; + return MeanTelemetryProvider; }); diff --git a/src/plugins/telemetryMean/src/MeanTelemetryProviderSpec.js b/src/plugins/telemetryMean/src/MeanTelemetryProviderSpec.js index a0b402323a..53015cce7e 100644 --- a/src/plugins/telemetryMean/src/MeanTelemetryProviderSpec.js +++ b/src/plugins/telemetryMean/src/MeanTelemetryProviderSpec.js @@ -20,599 +20,604 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ /* eslint-disable no-invalid-this */ -define([ - "./MeanTelemetryProvider", - "./MockTelemetryApi" -], function ( - MeanTelemetryProvider, - MockTelemetryApi +define(['./MeanTelemetryProvider', './MockTelemetryApi'], function ( + MeanTelemetryProvider, + MockTelemetryApi ) { - const RANGE_KEY = 'value'; - - describe("The Mean Telemetry Provider", function () { - let mockApi; - let meanTelemetryProvider; - let mockDomainObject; - let associatedObject; - let allPromises; - - beforeEach(function () { - allPromises = []; - createMockApi(); - setTimeSystemTo('utc'); - createMockObjects(); - meanTelemetryProvider = new MeanTelemetryProvider(mockApi); - }); - - it("supports telemetry-mean objects only", function () { - const mockTelemetryMeanObject = mockObjectWithType('telemetry-mean'); - const mockOtherObject = mockObjectWithType('other'); - - expect(meanTelemetryProvider.canProvideTelemetry(mockTelemetryMeanObject)).toBe(true); - expect(meanTelemetryProvider.canProvideTelemetry(mockOtherObject)).toBe(false); - }); - - describe("the subscribe function", function () { - let subscriptionCallback; - - beforeEach(function () { - subscriptionCallback = jasmine.createSpy('subscriptionCallback'); - }); - - it("subscribes to telemetry for the associated object", function () { - meanTelemetryProvider.subscribe(mockDomainObject); - - return expectObjectWasSubscribedTo(associatedObject); - }); - - it("returns a function that unsubscribes from the associated object", function () { - const unsubscribe = meanTelemetryProvider.subscribe(mockDomainObject); - - return waitForPromises() - .then(unsubscribe) - .then(waitForPromises) - .then(function () { - expect(mockApi.telemetry.unsubscribe).toHaveBeenCalled(); - }); - }); - - it("returns an average only when the sample size is reached", function () { - const inputTelemetry = [ - { - 'utc': 1, - 'defaultRange': 123.1231 - }, - { - 'utc': 2, - 'defaultRange': 321.3223 - }, - { - 'utc': 3, - 'defaultRange': 111.4446 - }, - { - 'utc': 4, - 'defaultRange': 555.2313 - } - ]; - - setSampleSize(5); - meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); - - return waitForPromises() - .then(feedInputTelemetry.bind(this, inputTelemetry)) - .then(function () { - expect(subscriptionCallback).not.toHaveBeenCalled(); - }); - }); - - it("correctly averages a sample of five values", function () { - const inputTelemetry = [ - { - 'utc': 1, - 'defaultRange': 123.1231 - }, - { - 'utc': 2, - 'defaultRange': 321.3223 - }, - { - 'utc': 3, - 'defaultRange': 111.4446 - }, - { - 'utc': 4, - 'defaultRange': 555.2313 - }, - { - 'utc': 5, - 'defaultRange': 1.1231 - } - ]; - const expectedAverages = [{ - 'utc': 5, - 'value': 222.44888 - }]; - - setSampleSize(5); - meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); - - return waitForPromises() - .then(feedInputTelemetry.bind(this, inputTelemetry)) - .then(expectAveragesForTelemetry.bind(this, expectedAverages)); - }); - - it("correctly averages a sample of ten values", function () { - const inputTelemetry = [ - { - 'utc': 1, - 'defaultRange': 123.1231 - }, - { - 'utc': 2, - 'defaultRange': 321.3223 - }, - { - 'utc': 3, - 'defaultRange': 111.4446 - }, - { - 'utc': 4, - 'defaultRange': 555.2313 - }, - { - 'utc': 5, - 'defaultRange': 1.1231 - }, - { - 'utc': 6, - 'defaultRange': 2323.12 - }, - { - 'utc': 7, - 'defaultRange': 532.12 - }, - { - 'utc': 8, - 'defaultRange': 453.543 - }, - { - 'utc': 9, - 'defaultRange': 89.2111 - }, - { - 'utc': 10, - 'defaultRange': 0.543 - } - ]; - const expectedAverages = [{ - 'utc': 10, - 'value': 451.07815 - }]; - - setSampleSize(10); - meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); - - return waitForPromises() - .then(feedInputTelemetry.bind(this, inputTelemetry)) - .then(expectAveragesForTelemetry.bind(this, expectedAverages)); - }); - - it("only averages values within its sample window", function () { - const inputTelemetry = [ - { - 'utc': 1, - 'defaultRange': 123.1231 - }, - { - 'utc': 2, - 'defaultRange': 321.3223 - }, - { - 'utc': 3, - 'defaultRange': 111.4446 - }, - { - 'utc': 4, - 'defaultRange': 555.2313 - }, - { - 'utc': 5, - 'defaultRange': 1.1231 - }, - { - 'utc': 6, - 'defaultRange': 2323.12 - }, - { - 'utc': 7, - 'defaultRange': 532.12 - }, - { - 'utc': 8, - 'defaultRange': 453.543 - }, - { - 'utc': 9, - 'defaultRange': 89.2111 - }, - { - 'utc': 10, - 'defaultRange': 0.543 - } - ]; - const expectedAverages = [ - { - 'utc': 5, - 'value': 222.44888 - }, - { - 'utc': 6, - 'value': 662.4482599999999 - }, - { - 'utc': 7, - 'value': 704.6078 - }, - { - 'utc': 8, - 'value': 773.02748 - }, - { - 'utc': 9, - 'value': 679.8234399999999 - }, - { - 'utc': 10, - 'value': 679.70742 - } - ]; - - setSampleSize(5); - meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); - - return waitForPromises() - .then(feedInputTelemetry.bind(this, inputTelemetry)) - .then(expectAveragesForTelemetry.bind(this, expectedAverages)); - }); - describe("given telemetry input with range values", function () { - let inputTelemetry; - - beforeEach(function () { - inputTelemetry = [{ - 'utc': 1, - 'rangeKey': 5678, - 'otherKey': 9999 - }]; - setSampleSize(1); - }); - it("uses the 'rangeKey' input range, when it is the default, to calculate the average", function () { - const averageTelemetryForRangeKey = [{ - 'utc': 1, - 'value': 5678 - }]; - - meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); - mockApi.telemetry.setDefaultRangeTo('rangeKey'); - - return waitForPromises() - .then(feedInputTelemetry.bind(this, inputTelemetry)) - .then(expectAveragesForTelemetry.bind(this, averageTelemetryForRangeKey)); - }); - - it("uses the 'otherKey' input range, when it is the default, to calculate the average", function () { - const averageTelemetryForOtherKey = [{ - 'utc': 1, - 'value': 9999 - }]; - - meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); - mockApi.telemetry.setDefaultRangeTo('otherKey'); - - return waitForPromises() - .then(feedInputTelemetry.bind(this, inputTelemetry)) - .then(expectAveragesForTelemetry.bind(this, averageTelemetryForOtherKey)); - }); - }); - describe("given telemetry input with range values", function () { - let inputTelemetry; - - beforeEach(function () { - inputTelemetry = [{ - 'utc': 1, - 'rangeKey': 5678, - 'otherKey': 9999 - }]; - setSampleSize(1); - }); - it("uses the 'rangeKey' input range, when it is the default, to calculate the average", function () { - const averageTelemetryForRangeKey = [{ - 'utc': 1, - 'value': 5678 - }]; - - meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); - mockApi.telemetry.setDefaultRangeTo('rangeKey'); - - return waitForPromises() - .then(feedInputTelemetry.bind(this, inputTelemetry)) - .then(expectAveragesForTelemetry.bind(this, averageTelemetryForRangeKey)); - }); - - it("uses the 'otherKey' input range, when it is the default, to calculate the average", function () { - const averageTelemetryForOtherKey = [{ - 'utc': 1, - 'value': 9999 - }]; - - meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); - mockApi.telemetry.setDefaultRangeTo('otherKey'); - - return waitForPromises() - .then(feedInputTelemetry.bind(this, inputTelemetry)) - .then(expectAveragesForTelemetry.bind(this, averageTelemetryForOtherKey)); - }); - }); - - function feedInputTelemetry(inputTelemetry) { - inputTelemetry.forEach(mockApi.telemetry.mockReceiveTelemetry); - } - - function expectAveragesForTelemetry(expectedAverages) { - return waitForPromises().then(function () { - expectedAverages.forEach(function (averageDatum) { - expect(subscriptionCallback).toHaveBeenCalledWith(averageDatum); - }); - }); - } - - function expectObjectWasSubscribedTo(object) { - return waitForPromises().then(function () { - expect(mockApi.telemetry.subscribe).toHaveBeenCalledWith(object, jasmine.any(Function)); - }); - } - - }); - - describe("the request function", function () { - - it("requests telemetry for the associated object", function () { - whenTelemetryRequestedReturn([]); - - return meanTelemetryProvider.request(mockDomainObject).then(function () { - expect(mockApi.telemetry.request).toHaveBeenCalledWith(associatedObject, undefined); - }); - }); - - it("returns an average only when the sample size is reached", function () { - const inputTelemetry = [ - { - 'utc': 1, - 'defaultRange': 123.1231 - }, - { - 'utc': 2, - 'defaultRange': 321.3223 - }, - { - 'utc': 3, - 'defaultRange': 111.4446 - }, - { - 'utc': 4, - 'defaultRange': 555.2313 - } - ]; - - setSampleSize(5); - whenTelemetryRequestedReturn(inputTelemetry); - - return meanTelemetryProvider.request(mockDomainObject).then(function (averageData) { - expect(averageData.length).toBe(0); - }); - }); - - it("correctly averages a sample of five values", function () { - const inputTelemetry = [ - { - 'utc': 1, - 'defaultRange': 123.1231 - }, - { - 'utc': 2, - 'defaultRange': 321.3223 - }, - { - 'utc': 3, - 'defaultRange': 111.4446 - }, - { - 'utc': 4, - 'defaultRange': 555.2313 - }, - { - 'utc': 5, - 'defaultRange': 1.1231 - } - ]; - - setSampleSize(5); - whenTelemetryRequestedReturn(inputTelemetry); - - return meanTelemetryProvider.request(mockDomainObject) - .then(function (averageData) { - expectAverageToBe(222.44888, averageData); - }); - }); - - it("correctly averages a sample of ten values", function () { - const inputTelemetry = [ - { - 'utc': 1, - 'defaultRange': 123.1231 - }, - { - 'utc': 2, - 'defaultRange': 321.3223 - }, - { - 'utc': 3, - 'defaultRange': 111.4446 - }, - { - 'utc': 4, - 'defaultRange': 555.2313 - }, - { - 'utc': 5, - 'defaultRange': 1.1231 - }, - { - 'utc': 6, - 'defaultRange': 2323.12 - }, - { - 'utc': 7, - 'defaultRange': 532.12 - }, - { - 'utc': 8, - 'defaultRange': 453.543 - }, - { - 'utc': 9, - 'defaultRange': 89.2111 - }, - { - 'utc': 10, - 'defaultRange': 0.543 - } - ]; - - setSampleSize(10); - whenTelemetryRequestedReturn(inputTelemetry); - - return meanTelemetryProvider.request(mockDomainObject) - .then(function (averageData) { - expectAverageToBe(451.07815, averageData); - }); - }); - - it("only averages values within its sample window", function () { - const inputTelemetry = [ - { - 'utc': 1, - 'defaultRange': 123.1231 - }, - { - 'utc': 2, - 'defaultRange': 321.3223 - }, - { - 'utc': 3, - 'defaultRange': 111.4446 - }, - { - 'utc': 4, - 'defaultRange': 555.2313 - }, - { - 'utc': 5, - 'defaultRange': 1.1231 - }, - { - 'utc': 6, - 'defaultRange': 2323.12 - }, - { - 'utc': 7, - 'defaultRange': 532.12 - }, - { - 'utc': 8, - 'defaultRange': 453.543 - }, - { - 'utc': 9, - 'defaultRange': 89.2111 - }, - { - 'utc': 10, - 'defaultRange': 0.543 - } - ]; - - setSampleSize(5); - whenTelemetryRequestedReturn(inputTelemetry); - - return meanTelemetryProvider.request(mockDomainObject) - .then(function (averageData) { - expectAverageToBe(679.70742, averageData); - }); - }); - - function expectAverageToBe(expectedValue, averageData) { - const averageDatum = averageData[averageData.length - 1]; - expect(averageDatum[RANGE_KEY]).toBe(expectedValue); - } - - function whenTelemetryRequestedReturn(telemetry) { - mockApi.telemetry.request.and.returnValue(resolvePromiseWith(telemetry)); - } - }); - - function createMockObjects() { - mockDomainObject = { - telemetryPoint: 'someTelemetryPoint' - }; - associatedObject = {}; - mockApi.objects.get.and.returnValue(resolvePromiseWith(associatedObject)); - } - - function setSampleSize(sampleSize) { - mockDomainObject.samples = sampleSize; - } - - function createMockApi() { - mockApi = { - telemetry: new MockTelemetryApi(), - objects: createMockObjectApi(), - time: createMockTimeApi() - }; - } - - function createMockObjectApi() { - return jasmine.createSpyObj('ObjectAPI', [ - 'get' - ]); - } - - function mockObjectWithType(type) { - return { - type: type - }; - } - - function resolvePromiseWith(value) { - const promise = Promise.resolve(value); - allPromises.push(promise); - - return promise; - } - - function waitForPromises() { - return Promise.all(allPromises); - } - - function createMockTimeApi() { - return jasmine.createSpyObj("timeApi", ['timeSystem']); - } - - function setTimeSystemTo(timeSystemKey) { - mockApi.time.timeSystem.and.returnValue({ - key: timeSystemKey - }); - } + const RANGE_KEY = 'value'; + + describe('The Mean Telemetry Provider', function () { + let mockApi; + let meanTelemetryProvider; + let mockDomainObject; + let associatedObject; + let allPromises; + + beforeEach(function () { + allPromises = []; + createMockApi(); + setTimeSystemTo('utc'); + createMockObjects(); + meanTelemetryProvider = new MeanTelemetryProvider(mockApi); }); + it('supports telemetry-mean objects only', function () { + const mockTelemetryMeanObject = mockObjectWithType('telemetry-mean'); + const mockOtherObject = mockObjectWithType('other'); + + expect(meanTelemetryProvider.canProvideTelemetry(mockTelemetryMeanObject)).toBe(true); + expect(meanTelemetryProvider.canProvideTelemetry(mockOtherObject)).toBe(false); + }); + + describe('the subscribe function', function () { + let subscriptionCallback; + + beforeEach(function () { + subscriptionCallback = jasmine.createSpy('subscriptionCallback'); + }); + + it('subscribes to telemetry for the associated object', function () { + meanTelemetryProvider.subscribe(mockDomainObject); + + return expectObjectWasSubscribedTo(associatedObject); + }); + + it('returns a function that unsubscribes from the associated object', function () { + const unsubscribe = meanTelemetryProvider.subscribe(mockDomainObject); + + return waitForPromises() + .then(unsubscribe) + .then(waitForPromises) + .then(function () { + expect(mockApi.telemetry.unsubscribe).toHaveBeenCalled(); + }); + }); + + it('returns an average only when the sample size is reached', function () { + const inputTelemetry = [ + { + utc: 1, + defaultRange: 123.1231 + }, + { + utc: 2, + defaultRange: 321.3223 + }, + { + utc: 3, + defaultRange: 111.4446 + }, + { + utc: 4, + defaultRange: 555.2313 + } + ]; + + setSampleSize(5); + meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); + + return waitForPromises() + .then(feedInputTelemetry.bind(this, inputTelemetry)) + .then(function () { + expect(subscriptionCallback).not.toHaveBeenCalled(); + }); + }); + + it('correctly averages a sample of five values', function () { + const inputTelemetry = [ + { + utc: 1, + defaultRange: 123.1231 + }, + { + utc: 2, + defaultRange: 321.3223 + }, + { + utc: 3, + defaultRange: 111.4446 + }, + { + utc: 4, + defaultRange: 555.2313 + }, + { + utc: 5, + defaultRange: 1.1231 + } + ]; + const expectedAverages = [ + { + utc: 5, + value: 222.44888 + } + ]; + + setSampleSize(5); + meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); + + return waitForPromises() + .then(feedInputTelemetry.bind(this, inputTelemetry)) + .then(expectAveragesForTelemetry.bind(this, expectedAverages)); + }); + + it('correctly averages a sample of ten values', function () { + const inputTelemetry = [ + { + utc: 1, + defaultRange: 123.1231 + }, + { + utc: 2, + defaultRange: 321.3223 + }, + { + utc: 3, + defaultRange: 111.4446 + }, + { + utc: 4, + defaultRange: 555.2313 + }, + { + utc: 5, + defaultRange: 1.1231 + }, + { + utc: 6, + defaultRange: 2323.12 + }, + { + utc: 7, + defaultRange: 532.12 + }, + { + utc: 8, + defaultRange: 453.543 + }, + { + utc: 9, + defaultRange: 89.2111 + }, + { + utc: 10, + defaultRange: 0.543 + } + ]; + const expectedAverages = [ + { + utc: 10, + value: 451.07815 + } + ]; + + setSampleSize(10); + meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); + + return waitForPromises() + .then(feedInputTelemetry.bind(this, inputTelemetry)) + .then(expectAveragesForTelemetry.bind(this, expectedAverages)); + }); + + it('only averages values within its sample window', function () { + const inputTelemetry = [ + { + utc: 1, + defaultRange: 123.1231 + }, + { + utc: 2, + defaultRange: 321.3223 + }, + { + utc: 3, + defaultRange: 111.4446 + }, + { + utc: 4, + defaultRange: 555.2313 + }, + { + utc: 5, + defaultRange: 1.1231 + }, + { + utc: 6, + defaultRange: 2323.12 + }, + { + utc: 7, + defaultRange: 532.12 + }, + { + utc: 8, + defaultRange: 453.543 + }, + { + utc: 9, + defaultRange: 89.2111 + }, + { + utc: 10, + defaultRange: 0.543 + } + ]; + const expectedAverages = [ + { + utc: 5, + value: 222.44888 + }, + { + utc: 6, + value: 662.4482599999999 + }, + { + utc: 7, + value: 704.6078 + }, + { + utc: 8, + value: 773.02748 + }, + { + utc: 9, + value: 679.8234399999999 + }, + { + utc: 10, + value: 679.70742 + } + ]; + + setSampleSize(5); + meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); + + return waitForPromises() + .then(feedInputTelemetry.bind(this, inputTelemetry)) + .then(expectAveragesForTelemetry.bind(this, expectedAverages)); + }); + describe('given telemetry input with range values', function () { + let inputTelemetry; + + beforeEach(function () { + inputTelemetry = [ + { + utc: 1, + rangeKey: 5678, + otherKey: 9999 + } + ]; + setSampleSize(1); + }); + it("uses the 'rangeKey' input range, when it is the default, to calculate the average", function () { + const averageTelemetryForRangeKey = [ + { + utc: 1, + value: 5678 + } + ]; + + meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); + mockApi.telemetry.setDefaultRangeTo('rangeKey'); + + return waitForPromises() + .then(feedInputTelemetry.bind(this, inputTelemetry)) + .then(expectAveragesForTelemetry.bind(this, averageTelemetryForRangeKey)); + }); + + it("uses the 'otherKey' input range, when it is the default, to calculate the average", function () { + const averageTelemetryForOtherKey = [ + { + utc: 1, + value: 9999 + } + ]; + + meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); + mockApi.telemetry.setDefaultRangeTo('otherKey'); + + return waitForPromises() + .then(feedInputTelemetry.bind(this, inputTelemetry)) + .then(expectAveragesForTelemetry.bind(this, averageTelemetryForOtherKey)); + }); + }); + describe('given telemetry input with range values', function () { + let inputTelemetry; + + beforeEach(function () { + inputTelemetry = [ + { + utc: 1, + rangeKey: 5678, + otherKey: 9999 + } + ]; + setSampleSize(1); + }); + it("uses the 'rangeKey' input range, when it is the default, to calculate the average", function () { + const averageTelemetryForRangeKey = [ + { + utc: 1, + value: 5678 + } + ]; + + meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); + mockApi.telemetry.setDefaultRangeTo('rangeKey'); + + return waitForPromises() + .then(feedInputTelemetry.bind(this, inputTelemetry)) + .then(expectAveragesForTelemetry.bind(this, averageTelemetryForRangeKey)); + }); + + it("uses the 'otherKey' input range, when it is the default, to calculate the average", function () { + const averageTelemetryForOtherKey = [ + { + utc: 1, + value: 9999 + } + ]; + + meanTelemetryProvider.subscribe(mockDomainObject, subscriptionCallback); + mockApi.telemetry.setDefaultRangeTo('otherKey'); + + return waitForPromises() + .then(feedInputTelemetry.bind(this, inputTelemetry)) + .then(expectAveragesForTelemetry.bind(this, averageTelemetryForOtherKey)); + }); + }); + + function feedInputTelemetry(inputTelemetry) { + inputTelemetry.forEach(mockApi.telemetry.mockReceiveTelemetry); + } + + function expectAveragesForTelemetry(expectedAverages) { + return waitForPromises().then(function () { + expectedAverages.forEach(function (averageDatum) { + expect(subscriptionCallback).toHaveBeenCalledWith(averageDatum); + }); + }); + } + + function expectObjectWasSubscribedTo(object) { + return waitForPromises().then(function () { + expect(mockApi.telemetry.subscribe).toHaveBeenCalledWith(object, jasmine.any(Function)); + }); + } + }); + + describe('the request function', function () { + it('requests telemetry for the associated object', function () { + whenTelemetryRequestedReturn([]); + + return meanTelemetryProvider.request(mockDomainObject).then(function () { + expect(mockApi.telemetry.request).toHaveBeenCalledWith(associatedObject, undefined); + }); + }); + + it('returns an average only when the sample size is reached', function () { + const inputTelemetry = [ + { + utc: 1, + defaultRange: 123.1231 + }, + { + utc: 2, + defaultRange: 321.3223 + }, + { + utc: 3, + defaultRange: 111.4446 + }, + { + utc: 4, + defaultRange: 555.2313 + } + ]; + + setSampleSize(5); + whenTelemetryRequestedReturn(inputTelemetry); + + return meanTelemetryProvider.request(mockDomainObject).then(function (averageData) { + expect(averageData.length).toBe(0); + }); + }); + + it('correctly averages a sample of five values', function () { + const inputTelemetry = [ + { + utc: 1, + defaultRange: 123.1231 + }, + { + utc: 2, + defaultRange: 321.3223 + }, + { + utc: 3, + defaultRange: 111.4446 + }, + { + utc: 4, + defaultRange: 555.2313 + }, + { + utc: 5, + defaultRange: 1.1231 + } + ]; + + setSampleSize(5); + whenTelemetryRequestedReturn(inputTelemetry); + + return meanTelemetryProvider.request(mockDomainObject).then(function (averageData) { + expectAverageToBe(222.44888, averageData); + }); + }); + + it('correctly averages a sample of ten values', function () { + const inputTelemetry = [ + { + utc: 1, + defaultRange: 123.1231 + }, + { + utc: 2, + defaultRange: 321.3223 + }, + { + utc: 3, + defaultRange: 111.4446 + }, + { + utc: 4, + defaultRange: 555.2313 + }, + { + utc: 5, + defaultRange: 1.1231 + }, + { + utc: 6, + defaultRange: 2323.12 + }, + { + utc: 7, + defaultRange: 532.12 + }, + { + utc: 8, + defaultRange: 453.543 + }, + { + utc: 9, + defaultRange: 89.2111 + }, + { + utc: 10, + defaultRange: 0.543 + } + ]; + + setSampleSize(10); + whenTelemetryRequestedReturn(inputTelemetry); + + return meanTelemetryProvider.request(mockDomainObject).then(function (averageData) { + expectAverageToBe(451.07815, averageData); + }); + }); + + it('only averages values within its sample window', function () { + const inputTelemetry = [ + { + utc: 1, + defaultRange: 123.1231 + }, + { + utc: 2, + defaultRange: 321.3223 + }, + { + utc: 3, + defaultRange: 111.4446 + }, + { + utc: 4, + defaultRange: 555.2313 + }, + { + utc: 5, + defaultRange: 1.1231 + }, + { + utc: 6, + defaultRange: 2323.12 + }, + { + utc: 7, + defaultRange: 532.12 + }, + { + utc: 8, + defaultRange: 453.543 + }, + { + utc: 9, + defaultRange: 89.2111 + }, + { + utc: 10, + defaultRange: 0.543 + } + ]; + + setSampleSize(5); + whenTelemetryRequestedReturn(inputTelemetry); + + return meanTelemetryProvider.request(mockDomainObject).then(function (averageData) { + expectAverageToBe(679.70742, averageData); + }); + }); + + function expectAverageToBe(expectedValue, averageData) { + const averageDatum = averageData[averageData.length - 1]; + expect(averageDatum[RANGE_KEY]).toBe(expectedValue); + } + + function whenTelemetryRequestedReturn(telemetry) { + mockApi.telemetry.request.and.returnValue(resolvePromiseWith(telemetry)); + } + }); + + function createMockObjects() { + mockDomainObject = { + telemetryPoint: 'someTelemetryPoint' + }; + associatedObject = {}; + mockApi.objects.get.and.returnValue(resolvePromiseWith(associatedObject)); + } + + function setSampleSize(sampleSize) { + mockDomainObject.samples = sampleSize; + } + + function createMockApi() { + mockApi = { + telemetry: new MockTelemetryApi(), + objects: createMockObjectApi(), + time: createMockTimeApi() + }; + } + + function createMockObjectApi() { + return jasmine.createSpyObj('ObjectAPI', ['get']); + } + + function mockObjectWithType(type) { + return { + type: type + }; + } + + function resolvePromiseWith(value) { + const promise = Promise.resolve(value); + allPromises.push(promise); + + return promise; + } + + function waitForPromises() { + return Promise.all(allPromises); + } + + function createMockTimeApi() { + return jasmine.createSpyObj('timeApi', ['timeSystem']); + } + + function setTimeSystemTo(timeSystemKey) { + mockApi.time.timeSystem.and.returnValue({ + key: timeSystemKey + }); + } + }); }); diff --git a/src/plugins/telemetryMean/src/MockTelemetryApi.js b/src/plugins/telemetryMean/src/MockTelemetryApi.js index 34c4e41723..85cef97571 100644 --- a/src/plugins/telemetryMean/src/MockTelemetryApi.js +++ b/src/plugins/telemetryMean/src/MockTelemetryApi.js @@ -21,87 +21,81 @@ *****************************************************************************/ define([], function () { + function MockTelemetryApi() { + this.createSpy('subscribe'); + this.createSpy('getMetadata'); - function MockTelemetryApi() { - this.createSpy('subscribe'); - this.createSpy('getMetadata'); + this.metadata = this.createMockMetadata(); + this.setDefaultRangeTo('defaultRange'); + this.unsubscribe = jasmine.createSpy('unsubscribe'); + this.mockReceiveTelemetry = this.mockReceiveTelemetry.bind(this); + } - this.metadata = this.createMockMetadata(); - this.setDefaultRangeTo('defaultRange'); - this.unsubscribe = jasmine.createSpy('unsubscribe'); - this.mockReceiveTelemetry = this.mockReceiveTelemetry.bind(this); - } + MockTelemetryApi.prototype.subscribe = function () { + return this.unsubscribe; + }; - MockTelemetryApi.prototype.subscribe = function () { - return this.unsubscribe; + MockTelemetryApi.prototype.getMetadata = function (object) { + return this.metadata; + }; + + MockTelemetryApi.prototype.request = jasmine.createSpy('request'); + + MockTelemetryApi.prototype.getValueFormatter = function (valueMetadata) { + const mockValueFormatter = jasmine.createSpyObj('valueFormatter', ['parse']); + + mockValueFormatter.parse.and.callFake(function (value) { + return value[valueMetadata.key]; + }); + + return mockValueFormatter; + }; + + MockTelemetryApi.prototype.mockReceiveTelemetry = function (newTelemetryDatum) { + const subscriptionCallback = this.subscribe.calls.mostRecent().args[1]; + subscriptionCallback(newTelemetryDatum); + }; + + /** + * @private + */ + MockTelemetryApi.prototype.onRequestReturn = function (telemetryData) { + this.requestTelemetry = telemetryData; + }; + + /** + * @private + */ + MockTelemetryApi.prototype.setDefaultRangeTo = function (rangeKey) { + const mockMetadataValue = { + key: rangeKey }; + this.metadata.valuesForHints.and.returnValue([mockMetadataValue]); + }; - MockTelemetryApi.prototype.getMetadata = function (object) { - return this.metadata; - }; + /** + * @private + */ + MockTelemetryApi.prototype.createMockMetadata = function () { + const mockMetadata = jasmine.createSpyObj('metadata', ['value', 'valuesForHints']); - MockTelemetryApi.prototype.request = jasmine.createSpy('request'); + mockMetadata.value.and.callFake(function (key) { + return { + key: key + }; + }); - MockTelemetryApi.prototype.getValueFormatter = function (valueMetadata) { - const mockValueFormatter = jasmine.createSpyObj("valueFormatter", [ - "parse" - ]); + return mockMetadata; + }; - mockValueFormatter.parse.and.callFake(function (value) { - return value[valueMetadata.key]; - }); + /** + * @private + */ + MockTelemetryApi.prototype.createSpy = function (functionName) { + this[functionName] = this[functionName].bind(this); + spyOn(this, functionName); + this[functionName].and.callThrough(); + }; - return mockValueFormatter; - }; - - MockTelemetryApi.prototype.mockReceiveTelemetry = function (newTelemetryDatum) { - const subscriptionCallback = this.subscribe.calls.mostRecent().args[1]; - subscriptionCallback(newTelemetryDatum); - }; - - /** - * @private - */ - MockTelemetryApi.prototype.onRequestReturn = function (telemetryData) { - this.requestTelemetry = telemetryData; - }; - - /** - * @private - */ - MockTelemetryApi.prototype.setDefaultRangeTo = function (rangeKey) { - const mockMetadataValue = { - key: rangeKey - }; - this.metadata.valuesForHints.and.returnValue([mockMetadataValue]); - }; - - /** - * @private - */ - MockTelemetryApi.prototype.createMockMetadata = function () { - const mockMetadata = jasmine.createSpyObj("metadata", [ - 'value', - 'valuesForHints' - ]); - - mockMetadata.value.and.callFake(function (key) { - return { - key: key - }; - }); - - return mockMetadata; - }; - - /** - * @private - */ - MockTelemetryApi.prototype.createSpy = function (functionName) { - this[functionName] = this[functionName].bind(this); - spyOn(this, functionName); - this[functionName].and.callThrough(); - }; - - return MockTelemetryApi; + return MockTelemetryApi; }); diff --git a/src/plugins/telemetryMean/src/TelemetryAverager.js b/src/plugins/telemetryMean/src/TelemetryAverager.js index a2eb10b62b..3d40deee44 100644 --- a/src/plugins/telemetryMean/src/TelemetryAverager.js +++ b/src/plugins/telemetryMean/src/TelemetryAverager.js @@ -21,100 +21,99 @@ *****************************************************************************/ define([], function () { + function TelemetryAverager(telemetryAPI, timeAPI, domainObject, samples, averageDatumCallback) { + this.telemetryAPI = telemetryAPI; + this.timeAPI = timeAPI; - function TelemetryAverager(telemetryAPI, timeAPI, domainObject, samples, averageDatumCallback) { - this.telemetryAPI = telemetryAPI; - this.timeAPI = timeAPI; + this.domainObject = domainObject; + this.samples = samples; + this.averagingWindow = []; - this.domainObject = domainObject; - this.samples = samples; - this.averagingWindow = []; + this.rangeKey = undefined; + this.rangeFormatter = undefined; + this.setRangeKeyAndFormatter(); - this.rangeKey = undefined; - this.rangeFormatter = undefined; - this.setRangeKeyAndFormatter(); + // Defined dynamically based on current time system + this.domainKey = undefined; + this.domainFormatter = undefined; - // Defined dynamically based on current time system - this.domainKey = undefined; - this.domainFormatter = undefined; + this.averageDatumCallback = averageDatumCallback; + } - this.averageDatumCallback = averageDatumCallback; + TelemetryAverager.prototype.createAverageDatum = function (telemetryDatum) { + this.setDomainKeyAndFormatter(); + + const timeValue = this.domainFormatter.parse(telemetryDatum); + const rangeValue = this.rangeFormatter.parse(telemetryDatum); + + this.averagingWindow.push(rangeValue); + + if (this.averagingWindow.length < this.samples) { + // We do not have enough data to produce an average + return; + } else if (this.averagingWindow.length > this.samples) { + //Do not let averaging window grow beyond defined sample size + this.averagingWindow.shift(); } - TelemetryAverager.prototype.createAverageDatum = function (telemetryDatum) { - this.setDomainKeyAndFormatter(); + const averageValue = this.calculateMean(); - const timeValue = this.domainFormatter.parse(telemetryDatum); - const rangeValue = this.rangeFormatter.parse(telemetryDatum); + const meanDatum = {}; + meanDatum[this.domainKey] = timeValue; + meanDatum.value = averageValue; - this.averagingWindow.push(rangeValue); + this.averageDatumCallback(meanDatum); + }; - if (this.averagingWindow.length < this.samples) { - // We do not have enough data to produce an average - return; - } else if (this.averagingWindow.length > this.samples) { - //Do not let averaging window grow beyond defined sample size - this.averagingWindow.shift(); - } + /** + * @private + */ + TelemetryAverager.prototype.calculateMean = function () { + let sum = 0; + let i = 0; - const averageValue = this.calculateMean(); + for (; i < this.averagingWindow.length; i++) { + sum += this.averagingWindow[i]; + } - const meanDatum = {}; - meanDatum[this.domainKey] = timeValue; - meanDatum.value = averageValue; + return sum / this.averagingWindow.length; + }; - this.averageDatumCallback(meanDatum); - }; + /** + * The mean telemetry filter produces domain values in whatever time + * system is currently selected from the conductor. Because this can + * change dynamically, the averager needs to be updated regularly with + * the current domain. + * @private + */ + TelemetryAverager.prototype.setDomainKeyAndFormatter = function () { + const domainKey = this.timeAPI.timeSystem().key; + if (domainKey !== this.domainKey) { + this.domainKey = domainKey; + this.domainFormatter = this.getFormatter(domainKey); + } + }; - /** - * @private - */ - TelemetryAverager.prototype.calculateMean = function () { - let sum = 0; - let i = 0; + /** + * @private + */ + TelemetryAverager.prototype.setRangeKeyAndFormatter = function () { + const metadatas = this.telemetryAPI.getMetadata(this.domainObject); + const rangeValues = metadatas.valuesForHints(['range']); - for (; i < this.averagingWindow.length; i++) { - sum += this.averagingWindow[i]; - } + this.rangeKey = rangeValues[0].key; + this.rangeFormatter = this.getFormatter(this.rangeKey); + }; - return sum / this.averagingWindow.length; - }; + /** + * @private + */ + TelemetryAverager.prototype.getFormatter = function (key) { + const objectMetadata = this.telemetryAPI.getMetadata(this.domainObject); + const valueMetadata = objectMetadata.value(key); - /** - * The mean telemetry filter produces domain values in whatever time - * system is currently selected from the conductor. Because this can - * change dynamically, the averager needs to be updated regularly with - * the current domain. - * @private - */ - TelemetryAverager.prototype.setDomainKeyAndFormatter = function () { - const domainKey = this.timeAPI.timeSystem().key; - if (domainKey !== this.domainKey) { - this.domainKey = domainKey; - this.domainFormatter = this.getFormatter(domainKey); - } - }; + return this.telemetryAPI.getValueFormatter(valueMetadata); + }; - /** - * @private - */ - TelemetryAverager.prototype.setRangeKeyAndFormatter = function () { - const metadatas = this.telemetryAPI.getMetadata(this.domainObject); - const rangeValues = metadatas.valuesForHints(['range']); - - this.rangeKey = rangeValues[0].key; - this.rangeFormatter = this.getFormatter(this.rangeKey); - }; - - /** - * @private - */ - TelemetryAverager.prototype.getFormatter = function (key) { - const objectMetadata = this.telemetryAPI.getMetadata(this.domainObject); - const valueMetadata = objectMetadata.value(key); - - return this.telemetryAPI.getValueFormatter(valueMetadata); - }; - - return TelemetryAverager; + return TelemetryAverager; }); diff --git a/src/plugins/telemetryTable/TableConfigurationViewProvider.js b/src/plugins/telemetryTable/TableConfigurationViewProvider.js index 6da6a4ce50..400eca37ba 100644 --- a/src/plugins/telemetryTable/TableConfigurationViewProvider.js +++ b/src/plugins/telemetryTable/TableConfigurationViewProvider.js @@ -21,64 +21,58 @@ *****************************************************************************/ define([ - 'objectUtils', - './components/table-configuration.vue', - './TelemetryTableConfiguration', - 'vue' -], function ( - objectUtils, - TableConfigurationComponent, - TelemetryTableConfiguration, - Vue -) { + 'objectUtils', + './components/table-configuration.vue', + './TelemetryTableConfiguration', + 'vue' +], function (objectUtils, TableConfigurationComponent, TelemetryTableConfiguration, Vue) { + function TableConfigurationViewProvider(openmct) { + return { + key: 'table-configuration', + name: 'Configuration', + canView: function (selection) { + if (selection.length !== 1 || selection[0].length === 0) { + return false; + } + + let object = selection[0][0].context.item; + + return object && object.type === 'table'; + }, + view: function (selection) { + let component; + let domainObject = selection[0][0].context.item; + let tableConfiguration = new TelemetryTableConfiguration(domainObject, openmct); - function TableConfigurationViewProvider(openmct) { return { - key: 'table-configuration', - name: 'Configuration', - canView: function (selection) { - if (selection.length !== 1 || selection[0].length === 0) { - return false; - } - - let object = selection[0][0].context.item; - - return object && object.type === 'table'; - }, - view: function (selection) { - let component; - let domainObject = selection[0][0].context.item; - let tableConfiguration = new TelemetryTableConfiguration(domainObject, openmct); - - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - TableConfiguration: TableConfigurationComponent.default - }, - provide: { - openmct, - tableConfiguration - }, - template: '' - }); - }, - priority: function () { - return 1; - }, - destroy: function () { - if (component) { - component.$destroy(); - component = undefined; - } - - tableConfiguration = undefined; - } - }; + show: function (element) { + component = new Vue({ + el: element, + components: { + TableConfiguration: TableConfigurationComponent.default + }, + provide: { + openmct, + tableConfiguration + }, + template: '' + }); + }, + priority: function () { + return 1; + }, + destroy: function () { + if (component) { + component.$destroy(); + component = undefined; } - }; - } - return TableConfigurationViewProvider; + tableConfiguration = undefined; + } + }; + } + }; + } + + return TableConfigurationViewProvider; }); diff --git a/src/plugins/telemetryTable/TelemetryTable.js b/src/plugins/telemetryTable/TelemetryTable.js index 4c8a2ec121..f133bc9630 100644 --- a/src/plugins/telemetryTable/TelemetryTable.js +++ b/src/plugins/telemetryTable/TelemetryTable.js @@ -21,395 +21,415 @@ *****************************************************************************/ define([ - 'EventEmitter', - 'lodash', - './collections/TableRowCollection', - './TelemetryTableRow', - './TelemetryTableNameColumn', - './TelemetryTableColumn', - './TelemetryTableUnitColumn', - './TelemetryTableConfiguration', - '../../utils/staleness' + 'EventEmitter', + 'lodash', + './collections/TableRowCollection', + './TelemetryTableRow', + './TelemetryTableNameColumn', + './TelemetryTableColumn', + './TelemetryTableUnitColumn', + './TelemetryTableConfiguration', + '../../utils/staleness' ], function ( - EventEmitter, - _, - TableRowCollection, - TelemetryTableRow, - TelemetryTableNameColumn, - TelemetryTableColumn, - TelemetryTableUnitColumn, - TelemetryTableConfiguration, - StalenessUtils + EventEmitter, + _, + TableRowCollection, + TelemetryTableRow, + TelemetryTableNameColumn, + TelemetryTableColumn, + TelemetryTableUnitColumn, + TelemetryTableConfiguration, + StalenessUtils ) { - class TelemetryTable extends EventEmitter { - constructor(domainObject, openmct) { - super(); - - this.domainObject = domainObject; - this.openmct = openmct; - this.rowCount = 100; - this.tableComposition = undefined; - this.datumCache = []; - this.configuration = new TelemetryTableConfiguration(domainObject, openmct); - this.paused = false; - this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); - - this.telemetryObjects = {}; - this.telemetryCollections = {}; - this.delayedActions = []; - this.outstandingRequests = 0; - this.stalenessSubscription = {}; - - this.addTelemetryObject = this.addTelemetryObject.bind(this); - this.removeTelemetryObject = this.removeTelemetryObject.bind(this); - this.removeTelemetryCollection = this.removeTelemetryCollection.bind(this); - this.incrementOutstandingRequests = this.incrementOutstandingRequests.bind(this); - this.decrementOutstandingRequests = this.decrementOutstandingRequests.bind(this); - this.resetRowsFromAllData = this.resetRowsFromAllData.bind(this); - this.isTelemetryObject = this.isTelemetryObject.bind(this); - this.updateFilters = this.updateFilters.bind(this); - this.clearData = this.clearData.bind(this); - this.buildOptionsFromConfiguration = this.buildOptionsFromConfiguration.bind(this); - - this.filterObserver = undefined; - - this.createTableRowCollections(); - } - - /** - * @private - */ - addNameColumn(telemetryObject, metadataValues) { - let metadatum = metadataValues.find(m => m.key === 'name'); - if (!metadatum) { - metadatum = { - format: 'string', - key: 'name', - name: 'Name' - }; - } - - const column = new TelemetryTableNameColumn(this.openmct, telemetryObject, metadatum); - - this.configuration.addSingleColumnForObject(telemetryObject, column); - } - - initialize() { - if (this.domainObject.type === 'table') { - this.filterObserver = this.openmct.objects.observe(this.domainObject, 'configuration.filters', this.updateFilters); - this.filters = this.domainObject.configuration.filters; - this.loadComposition(); - } else { - this.addTelemetryObject(this.domainObject); - } - } - - createTableRowCollections() { - this.tableRows = new TableRowCollection(); - - //Fetch any persisted default sort - let sortOptions = this.configuration.getConfiguration().sortOptions; - - //If no persisted sort order, default to sorting by time system, ascending. - sortOptions = sortOptions || { - key: this.openmct.time.timeSystem().key, - direction: 'asc' - }; - - this.tableRows.sortBy(sortOptions); - this.tableRows.on('resetRowsFromAllData', this.resetRowsFromAllData); - } - - loadComposition() { - this.tableComposition = this.openmct.composition.get(this.domainObject); - - if (this.tableComposition !== undefined) { - this.tableComposition.load().then((composition) => { - - composition = composition.filter(this.isTelemetryObject); - composition.forEach(this.addTelemetryObject); - - this.tableComposition.on('add', this.addTelemetryObject); - this.tableComposition.on('remove', this.removeTelemetryObject); - }); - } - } - - addTelemetryObject(telemetryObject) { - this.addColumnsForObject(telemetryObject, true); - - const keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); - let requestOptions = this.buildOptionsFromConfiguration(telemetryObject); - let columnMap = this.getColumnMapForObject(keyString); - let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject); - - const telemetryProcessor = this.getTelemetryProcessor(keyString, columnMap, limitEvaluator); - const telemetryRemover = this.getTelemetryRemover(); - - this.removeTelemetryCollection(keyString); - - this.telemetryCollections[keyString] = this.openmct.telemetry - .requestCollection(telemetryObject, requestOptions); - - this.telemetryCollections[keyString].on('requestStarted', this.incrementOutstandingRequests); - this.telemetryCollections[keyString].on('requestEnded', this.decrementOutstandingRequests); - this.telemetryCollections[keyString].on('remove', telemetryRemover); - this.telemetryCollections[keyString].on('add', telemetryProcessor); - this.telemetryCollections[keyString].on('clear', this.clearData); - this.telemetryCollections[keyString].load(); - - this.stalenessSubscription[keyString] = {}; - this.stalenessSubscription[keyString].stalenessUtils = new StalenessUtils.default(this.openmct, telemetryObject); - this.openmct.telemetry.isStale(telemetryObject).then(stalenessResponse => { - if (stalenessResponse !== undefined) { - this.handleStaleness(keyString, stalenessResponse); - } - }); - const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(telemetryObject, (stalenessResponse) => { - this.handleStaleness(keyString, stalenessResponse); - }); - - this.stalenessSubscription[keyString].unsubscribe = stalenessSubscription; - - this.telemetryObjects[keyString] = { - telemetryObject, - keyString, - requestOptions, - columnMap, - limitEvaluator - }; - - this.emit('object-added', telemetryObject); - } - - handleStaleness(keyString, stalenessResponse, skipCheck = false) { - if (skipCheck || this.stalenessSubscription[keyString].stalenessUtils.shouldUpdateStaleness(stalenessResponse, keyString)) { - this.emit('telemetry-staleness', { - keyString, - isStale: stalenessResponse.isStale - }); - } - } - - getTelemetryProcessor(keyString, columnMap, limitEvaluator) { - return (telemetry) => { - //Check that telemetry object has not been removed since telemetry was requested. - if (!this.telemetryObjects[keyString]) { - return; - } - - let telemetryRows = telemetry.map(datum => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator)); - - if (this.paused) { - this.delayedActions.push(this.tableRows.addRows.bind(this, telemetryRows, 'add')); - } else { - this.tableRows.addRows(telemetryRows); - } - }; - } - - getTelemetryRemover() { - return (telemetry) => { - if (this.paused) { - this.delayedActions.push(this.tableRows.removeRowsByData.bind(this, telemetry)); - } else { - this.tableRows.removeRowsByData(telemetry); - } - }; - } - - /** - * @private - */ - incrementOutstandingRequests() { - if (this.outstandingRequests === 0) { - this.emit('outstanding-requests', true); - } - - this.outstandingRequests++; - } - - /** - * @private - */ - decrementOutstandingRequests() { - this.outstandingRequests--; - - if (this.outstandingRequests === 0) { - this.emit('outstanding-requests', false); - } - } - - // will pull all necessary information for all existing bounded telemetry - // and pass to table row collection to reset without making any new requests - // triggered by filtering - resetRowsFromAllData() { - let allRows = []; - - Object.keys(this.telemetryCollections).forEach(keyString => { - let { columnMap, limitEvaluator } = this.telemetryObjects[keyString]; - - this.telemetryCollections[keyString].getAll().forEach(datum => { - allRows.push(new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator)); - }); - }); - - this.tableRows.clearRowsFromTableAndFilter(allRows); - } - - updateFilters(updatedFilters) { - let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters)); - - if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) { - this.filters = deepCopiedFilters; - this.tableRows.clear(); - this.clearAndResubscribe(); - } else { - this.filters = deepCopiedFilters; - } - } - - clearAndResubscribe() { - let objectKeys = Object.keys(this.telemetryObjects); - - this.tableRows.clear(); - objectKeys.forEach((keyString) => { - this.addTelemetryObject(this.telemetryObjects[keyString].telemetryObject); - }); - } - - removeTelemetryObject(objectIdentifier) { - const keyString = this.openmct.objects.makeKeyString(objectIdentifier); - const SKIP_CHECK = true; - - this.configuration.removeColumnsForObject(objectIdentifier, true); - this.tableRows.removeRowsByObject(keyString); - - this.removeTelemetryCollection(keyString); - delete this.telemetryObjects[keyString]; - - this.emit('object-removed', objectIdentifier); - - this.stalenessSubscription[keyString].unsubscribe(); - this.stalenessSubscription[keyString].stalenessUtils.destroy(); - this.handleStaleness(keyString, { isStale: false }, SKIP_CHECK); - delete this.stalenessSubscription[keyString]; - } - - clearData() { - this.tableRows.clear(); - this.emit('refresh'); - } - - addColumnsForObject(telemetryObject) { - let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values(); - - this.addNameColumn(telemetryObject, metadataValues); - metadataValues.forEach(metadatum => { - if (metadatum.key === 'name') { - return; - } - - let column = this.createColumn(metadatum); - this.configuration.addSingleColumnForObject(telemetryObject, column); - // add units column if available - if (metadatum.unit !== undefined) { - let unitColumn = this.createUnitColumn(metadatum); - this.configuration.addSingleColumnForObject(telemetryObject, unitColumn); - } - }); - } - - getColumnMapForObject(objectKeyString) { - let columns = this.configuration.getColumns(); - - if (columns[objectKeyString]) { - return columns[objectKeyString].reduce((map, column) => { - map[column.getKey()] = column; - - return map; - }, {}); - } - - return {}; - } - - buildOptionsFromConfiguration(telemetryObject) { - let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); - let filters = this.domainObject.configuration - && this.domainObject.configuration.filters - && this.domainObject.configuration.filters[keyString]; - - return {filters} || {}; - } - - createColumn(metadatum) { - return new TelemetryTableColumn(this.openmct, metadatum); - } - - createUnitColumn(metadatum) { - return new TelemetryTableUnitColumn(this.openmct, metadatum); - } - - isTelemetryObject(domainObject) { - return Object.prototype.hasOwnProperty.call(domainObject, 'telemetry'); - } - - sortBy(sortOptions) { - this.tableRows.sortBy(sortOptions); - - if (this.openmct.editor.isEditing()) { - let configuration = this.configuration.getConfiguration(); - configuration.sortOptions = sortOptions; - this.configuration.updateConfiguration(configuration); - } - } - - runDelayedActions() { - this.delayedActions.forEach(action => action()); - this.delayedActions = []; - } - - removeTelemetryCollection(keyString) { - if (this.telemetryCollections[keyString]) { - this.telemetryCollections[keyString].destroy(); - this.telemetryCollections[keyString] = undefined; - delete this.telemetryCollections[keyString]; - } - } - - pause() { - this.paused = true; - } - - unpause() { - this.paused = false; - this.runDelayedActions(); - } - - destroy() { - this.tableRows.destroy(); - - this.tableRows.off('resetRowsFromAllData', this.resetRowsFromAllData); - - let keystrings = Object.keys(this.telemetryCollections); - keystrings.forEach(this.removeTelemetryCollection); - - if (this.filterObserver) { - this.filterObserver(); - } - - Object.values(this.stalenessSubscription).forEach(stalenessSubscription => { - stalenessSubscription.unsubscribe(); - stalenessSubscription.stalenessUtils.destroy(); - }); - - if (this.tableComposition !== undefined) { - this.tableComposition.off('add', this.addTelemetryObject); - this.tableComposition.off('remove', this.removeTelemetryObject); - } - } + class TelemetryTable extends EventEmitter { + constructor(domainObject, openmct) { + super(); + + this.domainObject = domainObject; + this.openmct = openmct; + this.rowCount = 100; + this.tableComposition = undefined; + this.datumCache = []; + this.configuration = new TelemetryTableConfiguration(domainObject, openmct); + this.paused = false; + this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); + + this.telemetryObjects = {}; + this.telemetryCollections = {}; + this.delayedActions = []; + this.outstandingRequests = 0; + this.stalenessSubscription = {}; + + this.addTelemetryObject = this.addTelemetryObject.bind(this); + this.removeTelemetryObject = this.removeTelemetryObject.bind(this); + this.removeTelemetryCollection = this.removeTelemetryCollection.bind(this); + this.incrementOutstandingRequests = this.incrementOutstandingRequests.bind(this); + this.decrementOutstandingRequests = this.decrementOutstandingRequests.bind(this); + this.resetRowsFromAllData = this.resetRowsFromAllData.bind(this); + this.isTelemetryObject = this.isTelemetryObject.bind(this); + this.updateFilters = this.updateFilters.bind(this); + this.clearData = this.clearData.bind(this); + this.buildOptionsFromConfiguration = this.buildOptionsFromConfiguration.bind(this); + + this.filterObserver = undefined; + + this.createTableRowCollections(); } - return TelemetryTable; + /** + * @private + */ + addNameColumn(telemetryObject, metadataValues) { + let metadatum = metadataValues.find((m) => m.key === 'name'); + if (!metadatum) { + metadatum = { + format: 'string', + key: 'name', + name: 'Name' + }; + } + + const column = new TelemetryTableNameColumn(this.openmct, telemetryObject, metadatum); + + this.configuration.addSingleColumnForObject(telemetryObject, column); + } + + initialize() { + if (this.domainObject.type === 'table') { + this.filterObserver = this.openmct.objects.observe( + this.domainObject, + 'configuration.filters', + this.updateFilters + ); + this.filters = this.domainObject.configuration.filters; + this.loadComposition(); + } else { + this.addTelemetryObject(this.domainObject); + } + } + + createTableRowCollections() { + this.tableRows = new TableRowCollection(); + + //Fetch any persisted default sort + let sortOptions = this.configuration.getConfiguration().sortOptions; + + //If no persisted sort order, default to sorting by time system, ascending. + sortOptions = sortOptions || { + key: this.openmct.time.timeSystem().key, + direction: 'asc' + }; + + this.tableRows.sortBy(sortOptions); + this.tableRows.on('resetRowsFromAllData', this.resetRowsFromAllData); + } + + loadComposition() { + this.tableComposition = this.openmct.composition.get(this.domainObject); + + if (this.tableComposition !== undefined) { + this.tableComposition.load().then((composition) => { + composition = composition.filter(this.isTelemetryObject); + composition.forEach(this.addTelemetryObject); + + this.tableComposition.on('add', this.addTelemetryObject); + this.tableComposition.on('remove', this.removeTelemetryObject); + }); + } + } + + addTelemetryObject(telemetryObject) { + this.addColumnsForObject(telemetryObject, true); + + const keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); + let requestOptions = this.buildOptionsFromConfiguration(telemetryObject); + let columnMap = this.getColumnMapForObject(keyString); + let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject); + + const telemetryProcessor = this.getTelemetryProcessor(keyString, columnMap, limitEvaluator); + const telemetryRemover = this.getTelemetryRemover(); + + this.removeTelemetryCollection(keyString); + + this.telemetryCollections[keyString] = this.openmct.telemetry.requestCollection( + telemetryObject, + requestOptions + ); + + this.telemetryCollections[keyString].on('requestStarted', this.incrementOutstandingRequests); + this.telemetryCollections[keyString].on('requestEnded', this.decrementOutstandingRequests); + this.telemetryCollections[keyString].on('remove', telemetryRemover); + this.telemetryCollections[keyString].on('add', telemetryProcessor); + this.telemetryCollections[keyString].on('clear', this.clearData); + this.telemetryCollections[keyString].load(); + + this.stalenessSubscription[keyString] = {}; + this.stalenessSubscription[keyString].stalenessUtils = new StalenessUtils.default( + this.openmct, + telemetryObject + ); + this.openmct.telemetry.isStale(telemetryObject).then((stalenessResponse) => { + if (stalenessResponse !== undefined) { + this.handleStaleness(keyString, stalenessResponse); + } + }); + const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness( + telemetryObject, + (stalenessResponse) => { + this.handleStaleness(keyString, stalenessResponse); + } + ); + + this.stalenessSubscription[keyString].unsubscribe = stalenessSubscription; + + this.telemetryObjects[keyString] = { + telemetryObject, + keyString, + requestOptions, + columnMap, + limitEvaluator + }; + + this.emit('object-added', telemetryObject); + } + + handleStaleness(keyString, stalenessResponse, skipCheck = false) { + if ( + skipCheck || + this.stalenessSubscription[keyString].stalenessUtils.shouldUpdateStaleness( + stalenessResponse, + keyString + ) + ) { + this.emit('telemetry-staleness', { + keyString, + isStale: stalenessResponse.isStale + }); + } + } + + getTelemetryProcessor(keyString, columnMap, limitEvaluator) { + return (telemetry) => { + //Check that telemetry object has not been removed since telemetry was requested. + if (!this.telemetryObjects[keyString]) { + return; + } + + let telemetryRows = telemetry.map( + (datum) => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator) + ); + + if (this.paused) { + this.delayedActions.push(this.tableRows.addRows.bind(this, telemetryRows, 'add')); + } else { + this.tableRows.addRows(telemetryRows); + } + }; + } + + getTelemetryRemover() { + return (telemetry) => { + if (this.paused) { + this.delayedActions.push(this.tableRows.removeRowsByData.bind(this, telemetry)); + } else { + this.tableRows.removeRowsByData(telemetry); + } + }; + } + + /** + * @private + */ + incrementOutstandingRequests() { + if (this.outstandingRequests === 0) { + this.emit('outstanding-requests', true); + } + + this.outstandingRequests++; + } + + /** + * @private + */ + decrementOutstandingRequests() { + this.outstandingRequests--; + + if (this.outstandingRequests === 0) { + this.emit('outstanding-requests', false); + } + } + + // will pull all necessary information for all existing bounded telemetry + // and pass to table row collection to reset without making any new requests + // triggered by filtering + resetRowsFromAllData() { + let allRows = []; + + Object.keys(this.telemetryCollections).forEach((keyString) => { + let { columnMap, limitEvaluator } = this.telemetryObjects[keyString]; + + this.telemetryCollections[keyString].getAll().forEach((datum) => { + allRows.push(new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator)); + }); + }); + + this.tableRows.clearRowsFromTableAndFilter(allRows); + } + + updateFilters(updatedFilters) { + let deepCopiedFilters = JSON.parse(JSON.stringify(updatedFilters)); + + if (this.filters && !_.isEqual(this.filters, deepCopiedFilters)) { + this.filters = deepCopiedFilters; + this.tableRows.clear(); + this.clearAndResubscribe(); + } else { + this.filters = deepCopiedFilters; + } + } + + clearAndResubscribe() { + let objectKeys = Object.keys(this.telemetryObjects); + + this.tableRows.clear(); + objectKeys.forEach((keyString) => { + this.addTelemetryObject(this.telemetryObjects[keyString].telemetryObject); + }); + } + + removeTelemetryObject(objectIdentifier) { + const keyString = this.openmct.objects.makeKeyString(objectIdentifier); + const SKIP_CHECK = true; + + this.configuration.removeColumnsForObject(objectIdentifier, true); + this.tableRows.removeRowsByObject(keyString); + + this.removeTelemetryCollection(keyString); + delete this.telemetryObjects[keyString]; + + this.emit('object-removed', objectIdentifier); + + this.stalenessSubscription[keyString].unsubscribe(); + this.stalenessSubscription[keyString].stalenessUtils.destroy(); + this.handleStaleness(keyString, { isStale: false }, SKIP_CHECK); + delete this.stalenessSubscription[keyString]; + } + + clearData() { + this.tableRows.clear(); + this.emit('refresh'); + } + + addColumnsForObject(telemetryObject) { + let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values(); + + this.addNameColumn(telemetryObject, metadataValues); + metadataValues.forEach((metadatum) => { + if (metadatum.key === 'name') { + return; + } + + let column = this.createColumn(metadatum); + this.configuration.addSingleColumnForObject(telemetryObject, column); + // add units column if available + if (metadatum.unit !== undefined) { + let unitColumn = this.createUnitColumn(metadatum); + this.configuration.addSingleColumnForObject(telemetryObject, unitColumn); + } + }); + } + + getColumnMapForObject(objectKeyString) { + let columns = this.configuration.getColumns(); + + if (columns[objectKeyString]) { + return columns[objectKeyString].reduce((map, column) => { + map[column.getKey()] = column; + + return map; + }, {}); + } + + return {}; + } + + buildOptionsFromConfiguration(telemetryObject) { + let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); + let filters = + this.domainObject.configuration && + this.domainObject.configuration.filters && + this.domainObject.configuration.filters[keyString]; + + return { filters } || {}; + } + + createColumn(metadatum) { + return new TelemetryTableColumn(this.openmct, metadatum); + } + + createUnitColumn(metadatum) { + return new TelemetryTableUnitColumn(this.openmct, metadatum); + } + + isTelemetryObject(domainObject) { + return Object.prototype.hasOwnProperty.call(domainObject, 'telemetry'); + } + + sortBy(sortOptions) { + this.tableRows.sortBy(sortOptions); + + if (this.openmct.editor.isEditing()) { + let configuration = this.configuration.getConfiguration(); + configuration.sortOptions = sortOptions; + this.configuration.updateConfiguration(configuration); + } + } + + runDelayedActions() { + this.delayedActions.forEach((action) => action()); + this.delayedActions = []; + } + + removeTelemetryCollection(keyString) { + if (this.telemetryCollections[keyString]) { + this.telemetryCollections[keyString].destroy(); + this.telemetryCollections[keyString] = undefined; + delete this.telemetryCollections[keyString]; + } + } + + pause() { + this.paused = true; + } + + unpause() { + this.paused = false; + this.runDelayedActions(); + } + + destroy() { + this.tableRows.destroy(); + + this.tableRows.off('resetRowsFromAllData', this.resetRowsFromAllData); + + let keystrings = Object.keys(this.telemetryCollections); + keystrings.forEach(this.removeTelemetryCollection); + + if (this.filterObserver) { + this.filterObserver(); + } + + Object.values(this.stalenessSubscription).forEach((stalenessSubscription) => { + stalenessSubscription.unsubscribe(); + stalenessSubscription.stalenessUtils.destroy(); + }); + + if (this.tableComposition !== undefined) { + this.tableComposition.off('add', this.addTelemetryObject); + this.tableComposition.off('remove', this.removeTelemetryObject); + } + } + } + + return TelemetryTable; }); diff --git a/src/plugins/telemetryTable/TelemetryTableColumn.js b/src/plugins/telemetryTable/TelemetryTableColumn.js index fce935954d..dc72225e9a 100644 --- a/src/plugins/telemetryTable/TelemetryTableColumn.js +++ b/src/plugins/telemetryTable/TelemetryTableColumn.js @@ -20,47 +20,47 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ define(function () { - class TelemetryTableColumn { - constructor(openmct, metadatum, options = {selectable: false}) { - this.metadatum = metadatum; - this.formatter = openmct.telemetry.getValueFormatter(metadatum); - this.titleValue = this.metadatum.name; - this.selectable = options.selectable; - } - - getKey() { - return this.metadatum.key; - } - - getTitle() { - return this.metadatum.name; - } - - getMetadatum() { - return this.metadatum; - } - - hasValueForDatum(telemetryDatum) { - return Object.prototype.hasOwnProperty.call(telemetryDatum, this.metadatum.source); - } - - getRawValue(telemetryDatum) { - return telemetryDatum[this.metadatum.source]; - } - - getFormattedValue(telemetryDatum) { - let formattedValue = this.formatter.format(telemetryDatum); - if (formattedValue !== undefined && typeof formattedValue !== 'string') { - return formattedValue.toString(); - } else { - return formattedValue; - } - } - - getParsedValue(telemetryDatum) { - return this.formatter.parse(telemetryDatum); - } + class TelemetryTableColumn { + constructor(openmct, metadatum, options = { selectable: false }) { + this.metadatum = metadatum; + this.formatter = openmct.telemetry.getValueFormatter(metadatum); + this.titleValue = this.metadatum.name; + this.selectable = options.selectable; } - return TelemetryTableColumn; + getKey() { + return this.metadatum.key; + } + + getTitle() { + return this.metadatum.name; + } + + getMetadatum() { + return this.metadatum; + } + + hasValueForDatum(telemetryDatum) { + return Object.prototype.hasOwnProperty.call(telemetryDatum, this.metadatum.source); + } + + getRawValue(telemetryDatum) { + return telemetryDatum[this.metadatum.source]; + } + + getFormattedValue(telemetryDatum) { + let formattedValue = this.formatter.format(telemetryDatum); + if (formattedValue !== undefined && typeof formattedValue !== 'string') { + return formattedValue.toString(); + } else { + return formattedValue; + } + } + + getParsedValue(telemetryDatum) { + return this.formatter.parse(telemetryDatum); + } + } + + return TelemetryTableColumn; }); diff --git a/src/plugins/telemetryTable/TelemetryTableConfiguration.js b/src/plugins/telemetryTable/TelemetryTableConfiguration.js index 22e16bfb68..d79af9fec5 100644 --- a/src/plugins/telemetryTable/TelemetryTableConfiguration.js +++ b/src/plugins/telemetryTable/TelemetryTableConfiguration.js @@ -20,147 +20,149 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - 'lodash', - 'EventEmitter' -], function (_, EventEmitter) { +define(['lodash', 'EventEmitter'], function (_, EventEmitter) { + class TelemetryTableConfiguration extends EventEmitter { + constructor(domainObject, openmct) { + super(); - class TelemetryTableConfiguration extends EventEmitter { - constructor(domainObject, openmct) { - super(); + this.domainObject = domainObject; + this.openmct = openmct; + this.columns = {}; - this.domainObject = domainObject; - this.openmct = openmct; - this.columns = {}; + this.removeColumnsForObject = this.removeColumnsForObject.bind(this); + this.objectMutated = this.objectMutated.bind(this); - this.removeColumnsForObject = this.removeColumnsForObject.bind(this); - this.objectMutated = this.objectMutated.bind(this); - - this.unlistenFromMutation = openmct.objects.observe(domainObject, 'configuration', this.objectMutated); - } - - getConfiguration() { - let configuration = this.domainObject.configuration || {}; - configuration.hiddenColumns = configuration.hiddenColumns || {}; - configuration.columnWidths = configuration.columnWidths || {}; - configuration.columnOrder = configuration.columnOrder || []; - configuration.cellFormat = configuration.cellFormat || {}; - configuration.autosize = configuration.autosize === undefined ? true : configuration.autosize; - - return configuration; - } - - updateConfiguration(configuration) { - this.openmct.objects.mutate(this.domainObject, 'configuration', configuration); - } - - /** - * @private - * @param {*} object - */ - objectMutated(configuration) { - if (configuration !== undefined) { - this.emit('change', configuration); - } - } - - addSingleColumnForObject(telemetryObject, column, position) { - let objectKeyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); - this.columns[objectKeyString] = this.columns[objectKeyString] || []; - position = position || this.columns[objectKeyString].length; - this.columns[objectKeyString].splice(position, 0, column); - } - - removeColumnsForObject(objectIdentifier) { - let objectKeyString = this.openmct.objects.makeKeyString(objectIdentifier); - let columnsToRemove = this.columns[objectKeyString]; - - delete this.columns[objectKeyString]; - - let configuration = this.domainObject.configuration; - let configurationChanged = false; - columnsToRemove.forEach((column) => { - //There may be more than one column with the same key (eg. time system columns) - if (!this.hasColumnWithKey(column.getKey())) { - delete configuration.hiddenColumns[column.getKey()]; - configurationChanged = true; - } - }); - if (configurationChanged) { - this.updateConfiguration(configuration); - } - } - - hasColumnWithKey(columnKey) { - return _.flatten(Object.values(this.columns)) - .some(column => column.getKey() === columnKey); - } - - getColumns() { - return this.columns; - } - - getAllHeaders() { - let flattenedColumns = _.flatten(Object.values(this.columns)); - /* eslint-disable you-dont-need-lodash-underscore/uniq */ - let headers = _.uniq(flattenedColumns, false, column => column.getKey()) - .reduce(fromColumnsToHeadersMap, {}); - /* eslint-enable you-dont-need-lodash-underscore/uniq */ - function fromColumnsToHeadersMap(headersMap, column) { - headersMap[column.getKey()] = column.getTitle(); - - return headersMap; - } - - return headers; - } - - getVisibleHeaders() { - let allHeaders = this.getAllHeaders(); - let configuration = this.getConfiguration(); - - let orderedColumns = this.getColumnOrder(); - let unorderedColumns = _.difference(Object.keys(allHeaders), orderedColumns); - - return orderedColumns.concat(unorderedColumns) - .filter((headerKey) => { - return configuration.hiddenColumns[headerKey] !== true; - }) - .reduce((headers, headerKey) => { - headers[headerKey] = allHeaders[headerKey]; - - return headers; - }, {}); - } - - getColumnWidths() { - let configuration = this.getConfiguration(); - - return configuration.columnWidths; - } - - setColumnWidths(columnWidths) { - let configuration = this.getConfiguration(); - configuration.columnWidths = columnWidths; - this.updateConfiguration(configuration); - } - - getColumnOrder() { - let configuration = this.getConfiguration(); - - return configuration.columnOrder; - } - - setColumnOrder(columnOrder) { - let configuration = this.getConfiguration(); - configuration.columnOrder = columnOrder; - this.updateConfiguration(configuration); - } - - destroy() { - this.unlistenFromMutation(); - } + this.unlistenFromMutation = openmct.objects.observe( + domainObject, + 'configuration', + this.objectMutated + ); } - return TelemetryTableConfiguration; + getConfiguration() { + let configuration = this.domainObject.configuration || {}; + configuration.hiddenColumns = configuration.hiddenColumns || {}; + configuration.columnWidths = configuration.columnWidths || {}; + configuration.columnOrder = configuration.columnOrder || []; + configuration.cellFormat = configuration.cellFormat || {}; + configuration.autosize = configuration.autosize === undefined ? true : configuration.autosize; + + return configuration; + } + + updateConfiguration(configuration) { + this.openmct.objects.mutate(this.domainObject, 'configuration', configuration); + } + + /** + * @private + * @param {*} object + */ + objectMutated(configuration) { + if (configuration !== undefined) { + this.emit('change', configuration); + } + } + + addSingleColumnForObject(telemetryObject, column, position) { + let objectKeyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); + this.columns[objectKeyString] = this.columns[objectKeyString] || []; + position = position || this.columns[objectKeyString].length; + this.columns[objectKeyString].splice(position, 0, column); + } + + removeColumnsForObject(objectIdentifier) { + let objectKeyString = this.openmct.objects.makeKeyString(objectIdentifier); + let columnsToRemove = this.columns[objectKeyString]; + + delete this.columns[objectKeyString]; + + let configuration = this.domainObject.configuration; + let configurationChanged = false; + columnsToRemove.forEach((column) => { + //There may be more than one column with the same key (eg. time system columns) + if (!this.hasColumnWithKey(column.getKey())) { + delete configuration.hiddenColumns[column.getKey()]; + configurationChanged = true; + } + }); + if (configurationChanged) { + this.updateConfiguration(configuration); + } + } + + hasColumnWithKey(columnKey) { + return _.flatten(Object.values(this.columns)).some((column) => column.getKey() === columnKey); + } + + getColumns() { + return this.columns; + } + + getAllHeaders() { + let flattenedColumns = _.flatten(Object.values(this.columns)); + /* eslint-disable you-dont-need-lodash-underscore/uniq */ + let headers = _.uniq(flattenedColumns, false, (column) => column.getKey()).reduce( + fromColumnsToHeadersMap, + {} + ); + /* eslint-enable you-dont-need-lodash-underscore/uniq */ + function fromColumnsToHeadersMap(headersMap, column) { + headersMap[column.getKey()] = column.getTitle(); + + return headersMap; + } + + return headers; + } + + getVisibleHeaders() { + let allHeaders = this.getAllHeaders(); + let configuration = this.getConfiguration(); + + let orderedColumns = this.getColumnOrder(); + let unorderedColumns = _.difference(Object.keys(allHeaders), orderedColumns); + + return orderedColumns + .concat(unorderedColumns) + .filter((headerKey) => { + return configuration.hiddenColumns[headerKey] !== true; + }) + .reduce((headers, headerKey) => { + headers[headerKey] = allHeaders[headerKey]; + + return headers; + }, {}); + } + + getColumnWidths() { + let configuration = this.getConfiguration(); + + return configuration.columnWidths; + } + + setColumnWidths(columnWidths) { + let configuration = this.getConfiguration(); + configuration.columnWidths = columnWidths; + this.updateConfiguration(configuration); + } + + getColumnOrder() { + let configuration = this.getConfiguration(); + + return configuration.columnOrder; + } + + setColumnOrder(columnOrder) { + let configuration = this.getConfiguration(); + configuration.columnOrder = columnOrder; + this.updateConfiguration(configuration); + } + + destroy() { + this.unlistenFromMutation(); + } + } + + return TelemetryTableConfiguration; }); diff --git a/src/plugins/telemetryTable/TelemetryTableNameColumn.js b/src/plugins/telemetryTable/TelemetryTableNameColumn.js index 7b400c6da7..0ae1ba352d 100644 --- a/src/plugins/telemetryTable/TelemetryTableNameColumn.js +++ b/src/plugins/telemetryTable/TelemetryTableNameColumn.js @@ -19,26 +19,22 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './TelemetryTableColumn.js' -], function ( - TelemetryTableColumn -) { - class TelemetryTableNameColumn extends TelemetryTableColumn { - constructor(openmct, telemetryObject, metadatum) { - super(openmct, metadatum); +define(['./TelemetryTableColumn.js'], function (TelemetryTableColumn) { + class TelemetryTableNameColumn extends TelemetryTableColumn { + constructor(openmct, telemetryObject, metadatum) { + super(openmct, metadatum); - this.telemetryObject = telemetryObject; - } - - getRawValue() { - return this.telemetryObject.name; - } - - getFormattedValue() { - return this.telemetryObject.name; - } + this.telemetryObject = telemetryObject; } - return TelemetryTableNameColumn; + getRawValue() { + return this.telemetryObject.name; + } + + getFormattedValue() { + return this.telemetryObject.name; + } + } + + return TelemetryTableNameColumn; }); diff --git a/src/plugins/telemetryTable/TelemetryTableRow.js b/src/plugins/telemetryTable/TelemetryTableRow.js index a49edcbbd9..762a72642b 100644 --- a/src/plugins/telemetryTable/TelemetryTableRow.js +++ b/src/plugins/telemetryTable/TelemetryTableRow.js @@ -21,93 +21,91 @@ *****************************************************************************/ define([], function () { - class TelemetryTableRow { - constructor(datum, columns, objectKeyString, limitEvaluator) { - this.columns = columns; + class TelemetryTableRow { + constructor(datum, columns, objectKeyString, limitEvaluator) { + this.columns = columns; - this.datum = createNormalizedDatum(datum, columns); - this.fullDatum = datum; - this.limitEvaluator = limitEvaluator; - this.objectKeyString = objectKeyString; - } - - getFormattedDatum(headers) { - return Object.keys(headers).reduce((formattedDatum, columnKey) => { - formattedDatum[columnKey] = this.getFormattedValue(columnKey); - - return formattedDatum; - }, {}); - } - - getFormattedValue(key) { - let column = this.columns[key]; - - return column && column.getFormattedValue(this.datum[key]); - } - - getParsedValue(key) { - let column = this.columns[key]; - - return column && column.getParsedValue(this.datum[key]); - } - - getCellComponentName(key) { - let column = this.columns[key]; - - return column - && column.getCellComponentName - && column.getCellComponentName(); - } - - getRowClass() { - if (!this.rowClass) { - let limitEvaluation = this.limitEvaluator.evaluate(this.datum); - this.rowClass = limitEvaluation && limitEvaluation.cssClass; - } - - return this.rowClass; - } - - getCellLimitClasses() { - if (!this.cellLimitClasses) { - this.cellLimitClasses = Object.values(this.columns).reduce((alarmStateMap, column) => { - if (!column.isUnit) { - let limitEvaluation = this.limitEvaluator.evaluate(this.datum, column.getMetadatum()); - alarmStateMap[column.getKey()] = limitEvaluation && limitEvaluation.cssClass; - } - - return alarmStateMap; - }, {}); - } - - return this.cellLimitClasses; - } - - getContextualDomainObject(openmct, objectKeyString) { - return openmct.objects.get(objectKeyString); - } - - getContextMenuActions() { - return ['viewDatumAction', 'viewHistoricalData']; - } + this.datum = createNormalizedDatum(datum, columns); + this.fullDatum = datum; + this.limitEvaluator = limitEvaluator; + this.objectKeyString = objectKeyString; } - /** - * Normalize the structure of datums to assist sorting and merging of columns. - * Maps all sources to keys. - * @private - * @param {*} telemetryDatum - * @param {*} metadataValues - */ - function createNormalizedDatum(datum, columns) { - const normalizedDatum = JSON.parse(JSON.stringify(datum)); + getFormattedDatum(headers) { + return Object.keys(headers).reduce((formattedDatum, columnKey) => { + formattedDatum[columnKey] = this.getFormattedValue(columnKey); - Object.values(columns).forEach(column => { - normalizedDatum[column.getKey()] = column.getRawValue(datum); - }); - - return normalizedDatum; + return formattedDatum; + }, {}); } - return TelemetryTableRow; + getFormattedValue(key) { + let column = this.columns[key]; + + return column && column.getFormattedValue(this.datum[key]); + } + + getParsedValue(key) { + let column = this.columns[key]; + + return column && column.getParsedValue(this.datum[key]); + } + + getCellComponentName(key) { + let column = this.columns[key]; + + return column && column.getCellComponentName && column.getCellComponentName(); + } + + getRowClass() { + if (!this.rowClass) { + let limitEvaluation = this.limitEvaluator.evaluate(this.datum); + this.rowClass = limitEvaluation && limitEvaluation.cssClass; + } + + return this.rowClass; + } + + getCellLimitClasses() { + if (!this.cellLimitClasses) { + this.cellLimitClasses = Object.values(this.columns).reduce((alarmStateMap, column) => { + if (!column.isUnit) { + let limitEvaluation = this.limitEvaluator.evaluate(this.datum, column.getMetadatum()); + alarmStateMap[column.getKey()] = limitEvaluation && limitEvaluation.cssClass; + } + + return alarmStateMap; + }, {}); + } + + return this.cellLimitClasses; + } + + getContextualDomainObject(openmct, objectKeyString) { + return openmct.objects.get(objectKeyString); + } + + getContextMenuActions() { + return ['viewDatumAction', 'viewHistoricalData']; + } + } + + /** + * Normalize the structure of datums to assist sorting and merging of columns. + * Maps all sources to keys. + * @private + * @param {*} telemetryDatum + * @param {*} metadataValues + */ + function createNormalizedDatum(datum, columns) { + const normalizedDatum = JSON.parse(JSON.stringify(datum)); + + Object.values(columns).forEach((column) => { + normalizedDatum[column.getKey()] = column.getRawValue(datum); + }); + + return normalizedDatum; + } + + return TelemetryTableRow; }); diff --git a/src/plugins/telemetryTable/TelemetryTableType.js b/src/plugins/telemetryTable/TelemetryTableType.js index 978746fbfc..0087db2605 100644 --- a/src/plugins/telemetryTable/TelemetryTableType.js +++ b/src/plugins/telemetryTable/TelemetryTableType.js @@ -21,17 +21,18 @@ *****************************************************************************/ define(function () { - return { - name: 'Telemetry Table', - 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-scrolling', - initialize(domainObject) { - domainObject.composition = []; - domainObject.configuration = { - columnWidths: {}, - hiddenColumns: {} - }; - } - }; + return { + name: 'Telemetry Table', + 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-scrolling', + initialize(domainObject) { + domainObject.composition = []; + domainObject.configuration = { + columnWidths: {}, + hiddenColumns: {} + }; + } + }; }); diff --git a/src/plugins/telemetryTable/TelemetryTableUnitColumn.js b/src/plugins/telemetryTable/TelemetryTableUnitColumn.js index 4ff49a04f9..2f89b6b9d8 100644 --- a/src/plugins/telemetryTable/TelemetryTableUnitColumn.js +++ b/src/plugins/telemetryTable/TelemetryTableUnitColumn.js @@ -19,42 +19,38 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './TelemetryTableColumn.js' -], function ( - TelemetryTableColumn -) { - class TelemetryTableUnitColumn extends TelemetryTableColumn { - constructor(openmct, metadatum) { - super(openmct, metadatum); - this.isUnit = true; - this.titleValue += ' Unit'; - this.formatter = { - format: (datum) => { - return this.metadatum.unit; - }, - parse: (datum) => { - return this.metadatum.unit; - } - }; - } - - getKey() { - return this.metadatum.key + '-unit'; - } - - getTitle() { - return this.metadatum.name + ' Unit'; - } - - getRawValue(telemetryDatum) { - return this.metadatum.unit; - } - - getFormattedValue(telemetryDatum) { - return this.formatter.format(telemetryDatum); +define(['./TelemetryTableColumn.js'], function (TelemetryTableColumn) { + class TelemetryTableUnitColumn extends TelemetryTableColumn { + constructor(openmct, metadatum) { + super(openmct, metadatum); + this.isUnit = true; + this.titleValue += ' Unit'; + this.formatter = { + format: (datum) => { + return this.metadatum.unit; + }, + parse: (datum) => { + return this.metadatum.unit; } + }; } - return TelemetryTableUnitColumn; + getKey() { + return this.metadatum.key + '-unit'; + } + + getTitle() { + return this.metadatum.name + ' Unit'; + } + + getRawValue(telemetryDatum) { + return this.metadatum.unit; + } + + getFormattedValue(telemetryDatum) { + return this.formatter.format(telemetryDatum); + } + } + + return TelemetryTableUnitColumn; }); diff --git a/src/plugins/telemetryTable/TelemetryTableView.js b/src/plugins/telemetryTable/TelemetryTableView.js index 9f7dccbb1e..74ed12681e 100644 --- a/src/plugins/telemetryTable/TelemetryTableView.js +++ b/src/plugins/telemetryTable/TelemetryTableView.js @@ -3,69 +3,70 @@ import TelemetryTable from './TelemetryTable'; import Vue from 'vue'; export default class TelemetryTableView { - constructor(openmct, domainObject, objectPath) { - this.openmct = openmct; - this.domainObject = domainObject; - this.objectPath = objectPath; - this.component = undefined; + constructor(openmct, domainObject, objectPath) { + this.openmct = openmct; + this.domainObject = domainObject; + this.objectPath = objectPath; + this.component = undefined; - Object.defineProperty(this, 'table', { - value: new TelemetryTable(domainObject, openmct), - enumerable: false, - configurable: false - }); + Object.defineProperty(this, 'table', { + value: new TelemetryTable(domainObject, openmct), + enumerable: false, + configurable: false + }); + } + + getViewContext() { + if (!this.component) { + return {}; } - getViewContext() { - if (!this.component) { - return {}; - } + return this.component.$refs.tableComponent.getViewContext(); + } - return this.component.$refs.tableComponent.getViewContext(); - } + onEditModeChange(editMode) { + this.component.isEditing = editMode; + } - onEditModeChange(editMode) { - this.component.isEditing = editMode; - } + onClearData() { + this.table.clearData(); + } - onClearData() { - this.table.clearData(); - } + getTable() { + return this.table; + } - getTable() { - return this.table; - } + destroy(element) { + this.component.$destroy(); + this.component = undefined; + } - destroy(element) { - this.component.$destroy(); - this.component = undefined; - } - - show(element, editMode) { - this.component = new Vue({ - el: element, - components: { - TableComponent - }, - provide: { - openmct: this.openmct, - objectPath: this.objectPath, - table: this.table, - currentView: this - }, - data() { - return { - isEditing: editMode, - marking: { - disableMultiSelect: false, - enable: true, - rowName: '', - rowNamePlural: '', - useAlternateControlBar: false - } - }; - }, - template: '' - }); - } + show(element, editMode) { + this.component = new Vue({ + el: element, + components: { + TableComponent + }, + provide: { + openmct: this.openmct, + objectPath: this.objectPath, + table: this.table, + currentView: this + }, + data() { + return { + isEditing: editMode, + marking: { + disableMultiSelect: false, + enable: true, + rowName: '', + rowNamePlural: '', + useAlternateControlBar: false + } + }; + }, + template: + '' + }); + } } diff --git a/src/plugins/telemetryTable/TelemetryTableViewProvider.js b/src/plugins/telemetryTable/TelemetryTableViewProvider.js index 58deeb81f0..97fe611d0f 100644 --- a/src/plugins/telemetryTable/TelemetryTableViewProvider.js +++ b/src/plugins/telemetryTable/TelemetryTableViewProvider.js @@ -23,32 +23,31 @@ import TelemetryTableView from './TelemetryTableView'; export default function TelemetryTableViewProvider(openmct) { - function hasTelemetry(domainObject) { - if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) { - return false; - } - - let metadata = openmct.telemetry.getMetadata(domainObject); - - return metadata.values().length > 0; + function hasTelemetry(domainObject) { + if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) { + return false; } - return { - key: 'table', - name: 'Telemetry Table', - cssClass: 'icon-tabular-scrolling', - canView(domainObject) { - return domainObject.type === 'table' - || hasTelemetry(domainObject); - }, - canEdit(domainObject) { - return domainObject.type === 'table'; - }, - view(domainObject, objectPath) { - return new TelemetryTableView(openmct, domainObject, objectPath); - }, - priority() { - return 1; - } - }; + let metadata = openmct.telemetry.getMetadata(domainObject); + + return metadata.values().length > 0; + } + + return { + key: 'table', + name: 'Telemetry Table', + cssClass: 'icon-tabular-scrolling', + canView(domainObject) { + return domainObject.type === 'table' || hasTelemetry(domainObject); + }, + canEdit(domainObject) { + return domainObject.type === 'table'; + }, + view(domainObject, objectPath) { + return new TelemetryTableView(openmct, domainObject, objectPath); + }, + priority() { + return 1; + } + }; } diff --git a/src/plugins/telemetryTable/ViewActions.js b/src/plugins/telemetryTable/ViewActions.js index ac380513cc..83b75f3767 100644 --- a/src/plugins/telemetryTable/ViewActions.js +++ b/src/plugins/telemetryTable/ViewActions.js @@ -21,106 +21,106 @@ *****************************************************************************/ const exportCSV = { - name: 'Export Table Data', - key: 'export-csv-all', - description: "Export this view's data", - cssClass: 'icon-download labeled', - invoke: (objectPath, view) => { - view.getViewContext().exportAllDataAsCSV(); - }, - group: 'view' + name: 'Export Table Data', + key: 'export-csv-all', + description: "Export this view's data", + cssClass: 'icon-download labeled', + invoke: (objectPath, view) => { + view.getViewContext().exportAllDataAsCSV(); + }, + group: 'view' }; const exportMarkedDataAsCSV = { - name: 'Export Marked Rows', - key: 'export-csv-marked', - description: "Export marked rows as CSV", - cssClass: 'icon-download labeled', - invoke: (objectPath, view) => { - view.getViewContext().exportMarkedDataAsCSV(); - }, - group: 'view' + name: 'Export Marked Rows', + key: 'export-csv-marked', + description: 'Export marked rows as CSV', + cssClass: 'icon-download labeled', + invoke: (objectPath, view) => { + view.getViewContext().exportMarkedDataAsCSV(); + }, + group: 'view' }; const unmarkAllRows = { - name: 'Unmark All Rows', - key: 'unmark-all-rows', - description: 'Unmark all rows', - cssClass: 'icon-x labeled', - invoke: (objectPath, view) => { - view.getViewContext().unmarkAllRows(); - }, - showInStatusBar: true, - group: 'view' + name: 'Unmark All Rows', + key: 'unmark-all-rows', + description: 'Unmark all rows', + cssClass: 'icon-x labeled', + invoke: (objectPath, view) => { + view.getViewContext().unmarkAllRows(); + }, + showInStatusBar: true, + group: 'view' }; const pause = { - name: 'Pause', - key: 'pause-data', - description: 'Pause real-time data flow', - cssClass: 'icon-pause', - invoke: (objectPath, view) => { - view.getViewContext().togglePauseByButton(); - }, - showInStatusBar: true, - group: 'view' + name: 'Pause', + key: 'pause-data', + description: 'Pause real-time data flow', + cssClass: 'icon-pause', + invoke: (objectPath, view) => { + view.getViewContext().togglePauseByButton(); + }, + showInStatusBar: true, + group: 'view' }; const play = { - name: 'Play', - key: 'play-data', - description: 'Continue real-time data flow', - cssClass: 'c-button pause-play is-paused', - invoke: (objectPath, view) => { - view.getViewContext().togglePauseByButton(); - }, - showInStatusBar: true, - group: 'view' + name: 'Play', + key: 'play-data', + description: 'Continue real-time data flow', + cssClass: 'c-button pause-play is-paused', + invoke: (objectPath, view) => { + view.getViewContext().togglePauseByButton(); + }, + showInStatusBar: true, + group: 'view' }; const expandColumns = { - name: 'Expand Columns', - key: 'expand-columns', - description: "Increase column widths to fit currently available data.", - cssClass: 'icon-arrows-right-left labeled', - invoke: (objectPath, view) => { - view.getViewContext().expandColumns(); - }, - showInStatusBar: true, - group: 'view' + name: 'Expand Columns', + key: 'expand-columns', + description: 'Increase column widths to fit currently available data.', + cssClass: 'icon-arrows-right-left labeled', + invoke: (objectPath, view) => { + view.getViewContext().expandColumns(); + }, + showInStatusBar: true, + group: 'view' }; const autosizeColumns = { - name: 'Autosize Columns', - key: 'autosize-columns', - description: "Automatically size columns to fit the table into the available space.", - cssClass: 'icon-expand labeled', - invoke: (objectPath, view) => { - view.getViewContext().autosizeColumns(); - }, - showInStatusBar: true, - group: 'view' + name: 'Autosize Columns', + key: 'autosize-columns', + description: 'Automatically size columns to fit the table into the available space.', + cssClass: 'icon-expand labeled', + invoke: (objectPath, view) => { + view.getViewContext().autosizeColumns(); + }, + showInStatusBar: true, + group: 'view' }; const viewActions = [ - exportCSV, - exportMarkedDataAsCSV, - unmarkAllRows, - pause, - play, - expandColumns, - autosizeColumns + exportCSV, + exportMarkedDataAsCSV, + unmarkAllRows, + pause, + play, + expandColumns, + autosizeColumns ]; -viewActions.forEach(action => { - action.appliesTo = (objectPath, view = {}) => { - const viewContext = view.getViewContext && view.getViewContext(); - if (!viewContext) { - return false; - } +viewActions.forEach((action) => { + action.appliesTo = (objectPath, view = {}) => { + const viewContext = view.getViewContext && view.getViewContext(); + if (!viewContext) { + return false; + } - return viewContext.type === 'telemetry-table'; - }; + return viewContext.type === 'telemetry-table'; + }; }); export default viewActions; diff --git a/src/plugins/telemetryTable/collections/TableRowCollection.js b/src/plugins/telemetryTable/collections/TableRowCollection.js index d893c18e5c..0d7d039289 100644 --- a/src/plugins/telemetryTable/collections/TableRowCollection.js +++ b/src/plugins/telemetryTable/collections/TableRowCollection.js @@ -20,336 +20,328 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define( - [ - 'lodash', - 'EventEmitter' - ], - function ( - _, - EventEmitter - ) { - /** - * @constructor - */ - class TableRowCollection extends EventEmitter { - constructor() { - super(); +define(['lodash', 'EventEmitter'], function (_, EventEmitter) { + /** + * @constructor + */ + class TableRowCollection extends EventEmitter { + constructor() { + super(); - this.rows = []; - this.columnFilters = {}; - this.addRows = this.addRows.bind(this); - this.removeRowsByObject = this.removeRowsByObject.bind(this); - this.removeRowsByData = this.removeRowsByData.bind(this); + this.rows = []; + this.columnFilters = {}; + this.addRows = this.addRows.bind(this); + this.removeRowsByObject = this.removeRowsByObject.bind(this); + this.removeRowsByData = this.removeRowsByData.bind(this); - this.clear = this.clear.bind(this); - } + this.clear = this.clear.bind(this); + } - removeRowsByObject(keyString) { - let removed = []; + removeRowsByObject(keyString) { + let removed = []; - this.rows = this.rows.filter((row) => { - if (row.objectKeyString === keyString) { - removed.push(row); + this.rows = this.rows.filter((row) => { + if (row.objectKeyString === keyString) { + removed.push(row); - return false; - } else { - return true; - } - }); + return false; + } else { + return true; + } + }); - this.emit('remove', removed); - } + this.emit('remove', removed); + } - addRows(rows) { - let rowsToAdd = this.filterRows(rows); + addRows(rows) { + let rowsToAdd = this.filterRows(rows); - this.sortAndMergeRows(rowsToAdd); + this.sortAndMergeRows(rowsToAdd); - // we emit filter no matter what to trigger - // an update of visible rows - if (rowsToAdd.length > 0) { - this.emit('add', rowsToAdd); - } - } + // we emit filter no matter what to trigger + // an update of visible rows + if (rowsToAdd.length > 0) { + this.emit('add', rowsToAdd); + } + } - clearRowsFromTableAndFilter(rows) { + clearRowsFromTableAndFilter(rows) { + let rowsToAdd = this.filterRows(rows); + // Reset of all rows, need to wipe current rows + this.rows = []; - let rowsToAdd = this.filterRows(rows); - // Reset of all rows, need to wipe current rows - this.rows = []; + this.sortAndMergeRows(rowsToAdd); - this.sortAndMergeRows(rowsToAdd); + // We emit filter and update of visible rows + this.emit('filter', rowsToAdd); + } - // We emit filter and update of visible rows - this.emit('filter', rowsToAdd); - } + filterRows(rows) { + if (Object.keys(this.columnFilters).length > 0) { + return rows.filter(this.matchesFilters, this); + } - filterRows(rows) { + return rows; + } - if (Object.keys(this.columnFilters).length > 0) { - return rows.filter(this.matchesFilters, this); - } + sortAndMergeRows(rows) { + const sortedRowsToAdd = this.sortCollection(rows); - return rows; - } + if (this.rows.length === 0) { + this.rows = sortedRowsToAdd; - sortAndMergeRows(rows) { - const sortedRowsToAdd = this.sortCollection(rows); + return; + } - if (this.rows.length === 0) { - this.rows = sortedRowsToAdd; + const firstIncomingRow = sortedRowsToAdd[0]; + const lastIncomingRow = sortedRowsToAdd[sortedRowsToAdd.length - 1]; + const firstExistingRow = this.rows[0]; + const lastExistingRow = this.rows[this.rows.length - 1]; - return; - } + if (this.firstRowInSortOrder(lastIncomingRow, firstExistingRow) === lastIncomingRow) { + this.rows = [...sortedRowsToAdd, ...this.rows]; + } else if (this.firstRowInSortOrder(lastExistingRow, firstIncomingRow) === lastExistingRow) { + this.rows = [...this.rows, ...sortedRowsToAdd]; + } else { + this.mergeSortedRows(sortedRowsToAdd); + } + } - const firstIncomingRow = sortedRowsToAdd[0]; - const lastIncomingRow = sortedRowsToAdd[sortedRowsToAdd.length - 1]; - const firstExistingRow = this.rows[0]; - const lastExistingRow = this.rows[this.rows.length - 1]; + sortCollection(rows) { + const sortedRows = _.orderBy( + rows, + (row) => row.getParsedValue(this.sortOptions.key), + this.sortOptions.direction + ); - if (this.firstRowInSortOrder(lastIncomingRow, firstExistingRow) - === lastIncomingRow - ) { - this.rows = [...sortedRowsToAdd, ...this.rows]; - } else if (this.firstRowInSortOrder(lastExistingRow, firstIncomingRow) - === lastExistingRow - ) { - this.rows = [...this.rows, ...sortedRowsToAdd]; - } else { - this.mergeSortedRows(sortedRowsToAdd); - } - } + return sortedRows; + } - sortCollection(rows) { - const sortedRows = _.orderBy( - rows, - row => row.getParsedValue(this.sortOptions.key), this.sortOptions.direction - ); + mergeSortedRows(rows) { + const mergedRows = []; + let i = 0; + let j = 0; - return sortedRows; - } + while (i < this.rows.length && j < rows.length) { + const existingRow = this.rows[i]; + const incomingRow = rows[j]; - mergeSortedRows(rows) { - const mergedRows = []; - let i = 0; - let j = 0; + if (this.firstRowInSortOrder(existingRow, incomingRow) === existingRow) { + mergedRows.push(existingRow); + i++; + } else { + mergedRows.push(incomingRow); + j++; + } + } - while (i < this.rows.length && j < rows.length) { - const existingRow = this.rows[i]; - const incomingRow = rows[j]; + // tail of existing rows is all that is left to merge + if (i < this.rows.length) { + for (i; i < this.rows.length; i++) { + mergedRows.push(this.rows[i]); + } + } - if (this.firstRowInSortOrder(existingRow, incomingRow) === existingRow) { - mergedRows.push(existingRow); - i++; - } else { - mergedRows.push(incomingRow); - j++; - } - } + // tail of incoming rows is all that is left to merge + if (j < rows.length) { + for (j; j < rows.length; j++) { + mergedRows.push(rows[j]); + } + } - // tail of existing rows is all that is left to merge - if (i < this.rows.length) { - for (i; i < this.rows.length; i++) { - mergedRows.push(this.rows[i]); - } - } + this.rows = mergedRows; + } - // tail of incoming rows is all that is left to merge - if (j < rows.length) { - for (j; j < rows.length; j++) { - mergedRows.push(rows[j]); - } - } + firstRowInSortOrder(row1, row2) { + const val1 = this.getValueForSortColumn(row1); + const val2 = this.getValueForSortColumn(row2); - this.rows = mergedRows; - } + if (this.sortOptions.direction === 'asc') { + return val1 <= val2 ? row1 : row2; + } else { + return val1 >= val2 ? row1 : row2; + } + } - firstRowInSortOrder(row1, row2) { - const val1 = this.getValueForSortColumn(row1); - const val2 = this.getValueForSortColumn(row2); + removeRowsByData(data) { + let removed = []; - if (this.sortOptions.direction === 'asc') { - return val1 <= val2 ? row1 : row2; - } else { - return val1 >= val2 ? row1 : row2; - } - } + this.rows = this.rows.filter((row) => { + if (data.includes(row.fullDatum)) { + removed.push(row); - removeRowsByData(data) { - let removed = []; + return false; + } else { + return true; + } + }); - this.rows = this.rows.filter((row) => { - if (data.includes(row.fullDatum)) { - removed.push(row); + this.emit('remove', removed); + } - return false; - } else { - return true; - } - }); + /** + * Sorts the telemetry collection based on the provided sort field + * specifier. Subsequent inserts are sorted to maintain specified sport + * order. + * + * @example + * // First build some mock telemetry for the purpose of an example + * let now = Date.now(); + * let telemetry = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(function (value) { + * return { + * // define an object property to demonstrate nested paths + * timestamp: { + * ms: now - value * 1000, + * text: + * }, + * value: value + * } + * }); + * let collection = new TelemetryCollection(); + * + * collection.add(telemetry); + * + * // Sort by telemetry value + * collection.sortBy({ + * key: 'value', direction: 'asc' + * }); + * + * // Sort by ms since epoch + * collection.sort({ + * key: 'timestamp.ms', + * direction: 'asc' + * }); + * + * // Sort by 'text' attribute, descending + * collection.sort("timestamp.text"); + * + * + * @param {object} sortOptions An object specifying a sort key, and direction. + */ + sortBy(sortOptions) { + if (arguments.length > 0) { + this.sortOptions = sortOptions; + this.rows = _.orderBy( + this.rows, + (row) => row.getParsedValue(sortOptions.key), + sortOptions.direction + ); + this.emit('sort'); + } - this.emit('remove', removed); - } + // Return duplicate to avoid direct modification of underlying object + return Object.assign({}, this.sortOptions); + } - /** - * Sorts the telemetry collection based on the provided sort field - * specifier. Subsequent inserts are sorted to maintain specified sport - * order. - * - * @example - * // First build some mock telemetry for the purpose of an example - * let now = Date.now(); - * let telemetry = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(function (value) { - * return { - * // define an object property to demonstrate nested paths - * timestamp: { - * ms: now - value * 1000, - * text: - * }, - * value: value - * } - * }); - * let collection = new TelemetryCollection(); - * - * collection.add(telemetry); - * - * // Sort by telemetry value - * collection.sortBy({ - * key: 'value', direction: 'asc' - * }); - * - * // Sort by ms since epoch - * collection.sort({ - * key: 'timestamp.ms', - * direction: 'asc' - * }); - * - * // Sort by 'text' attribute, descending - * collection.sort("timestamp.text"); - * - * - * @param {object} sortOptions An object specifying a sort key, and direction. - */ - sortBy(sortOptions) { - if (arguments.length > 0) { - this.sortOptions = sortOptions; - this.rows = _.orderBy(this.rows, (row) => row.getParsedValue(sortOptions.key), sortOptions.direction); - this.emit('sort'); - } + setColumnFilter(columnKey, filter) { + filter = filter.trim().toLowerCase(); + let wasBlank = this.columnFilters[columnKey] === undefined; + let isSubset = this.isSubsetOfCurrentFilter(columnKey, filter); - // Return duplicate to avoid direct modification of underlying object - return Object.assign({}, this.sortOptions); - } + if (filter.length === 0) { + delete this.columnFilters[columnKey]; + } else { + this.columnFilters[columnKey] = filter; + } - setColumnFilter(columnKey, filter) { - filter = filter.trim().toLowerCase(); - let wasBlank = this.columnFilters[columnKey] === undefined; - let isSubset = this.isSubsetOfCurrentFilter(columnKey, filter); + if (isSubset || wasBlank) { + this.rows = this.rows.filter(this.matchesFilters, this); + this.emit('filter'); + } else { + this.emit('resetRowsFromAllData'); + } + } - if (filter.length === 0) { - delete this.columnFilters[columnKey]; - } else { - this.columnFilters[columnKey] = filter; - } + setColumnRegexFilter(columnKey, filter) { + filter = filter.trim(); + this.columnFilters[columnKey] = new RegExp(filter); - if (isSubset || wasBlank) { - this.rows = this.rows.filter(this.matchesFilters, this); - this.emit('filter'); - } else { - this.emit('resetRowsFromAllData'); - } + this.emit('resetRowsFromAllData'); + } - } + getColumnMapForObject(objectKeyString) { + let columns = this.configuration.getColumns(); - setColumnRegexFilter(columnKey, filter) { - filter = filter.trim(); - this.columnFilters[columnKey] = new RegExp(filter); + if (columns[objectKeyString]) { + return columns[objectKeyString].reduce((map, column) => { + map[column.getKey()] = column; - this.emit('resetRowsFromAllData'); - } + return map; + }, {}); + } - getColumnMapForObject(objectKeyString) { - let columns = this.configuration.getColumns(); + return {}; + } - if (columns[objectKeyString]) { - return columns[objectKeyString].reduce((map, column) => { - map[column.getKey()] = column; + // /** + // * @private + // */ + isSubsetOfCurrentFilter(columnKey, filter) { + if (this.columnFilters[columnKey] instanceof RegExp) { + return false; + } - return map; - }, {}); - } + return ( + this.columnFilters[columnKey] && + filter.startsWith(this.columnFilters[columnKey]) && + // startsWith check will otherwise fail when filter cleared + // because anyString.startsWith('') === true + filter !== '' + ); + } - return {}; - } - - // /** - // * @private - // */ - isSubsetOfCurrentFilter(columnKey, filter) { - if (this.columnFilters[columnKey] instanceof RegExp) { - return false; - } - - return this.columnFilters[columnKey] - && filter.startsWith(this.columnFilters[columnKey]) - // startsWith check will otherwise fail when filter cleared - // because anyString.startsWith('') === true - && filter !== ''; - } - - /** - * @private - */ - matchesFilters(row) { - let doesMatchFilters = true; - Object.keys(this.columnFilters).forEach((key) => { - if (!doesMatchFilters || !this.rowHasColumn(row, key)) { - return false; - } - - let formattedValue = row.getFormattedValue(key); - if (formattedValue === undefined) { - return false; - } - - if (this.columnFilters[key] instanceof RegExp) { - doesMatchFilters = this.columnFilters[key].test(formattedValue); - } else { - doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1; - } - }); - - return doesMatchFilters; - } - - rowHasColumn(row, key) { - return Object.prototype.hasOwnProperty.call(row.columns, key); - } - - getRows() { - return this.rows; - } - - getRowsLength() { - return this.rows.length; - } - - getValueForSortColumn(row) { - return row.getParsedValue(this.sortOptions.key); - } - - clear() { - let removedRows = this.rows; - this.rows = []; - - this.emit('remove', removedRows); - } - - destroy() { - this.removeAllListeners(); - } + /** + * @private + */ + matchesFilters(row) { + let doesMatchFilters = true; + Object.keys(this.columnFilters).forEach((key) => { + if (!doesMatchFilters || !this.rowHasColumn(row, key)) { + return false; } - return TableRowCollection; - }); + let formattedValue = row.getFormattedValue(key); + if (formattedValue === undefined) { + return false; + } + + if (this.columnFilters[key] instanceof RegExp) { + doesMatchFilters = this.columnFilters[key].test(formattedValue); + } else { + doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1; + } + }); + + return doesMatchFilters; + } + + rowHasColumn(row, key) { + return Object.prototype.hasOwnProperty.call(row.columns, key); + } + + getRows() { + return this.rows; + } + + getRowsLength() { + return this.rows.length; + } + + getValueForSortColumn(row) { + return row.getParsedValue(this.sortOptions.key); + } + + clear() { + let removedRows = this.rows; + this.rows = []; + + this.emit('remove', removedRows); + } + + destroy() { + this.removeAllListeners(); + } + } + + return TableRowCollection; +}); diff --git a/src/plugins/telemetryTable/components/sizing-row.vue b/src/plugins/telemetryTable/components/sizing-row.vue index 9a725730d5..cf62f904ee 100644 --- a/src/plugins/telemetryTable/components/sizing-row.vue +++ b/src/plugins/telemetryTable/components/sizing-row.vue @@ -20,56 +20,58 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/telemetryTable/components/table-cell.vue b/src/plugins/telemetryTable/components/table-cell.vue index 6545aff792..bf2797d9a4 100644 --- a/src/plugins/telemetryTable/components/table-cell.vue +++ b/src/plugins/telemetryTable/components/table-cell.vue @@ -20,60 +20,63 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/telemetryTable/components/table-column-header.vue b/src/plugins/telemetryTable/components/table-column-header.vue index af593631ed..085ad06fb3 100644 --- a/src/plugins/telemetryTable/components/table-column-header.vue +++ b/src/plugins/telemetryTable/components/table-column-header.vue @@ -20,148 +20,152 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/telemetryTable/components/table-configuration.vue b/src/plugins/telemetryTable/components/table-configuration.vue index becb0c5f00..f8ba969336 100644 --- a/src/plugins/telemetryTable/components/table-configuration.vue +++ b/src/plugins/telemetryTable/components/table-configuration.vue @@ -20,72 +20,55 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/telemetryTable/components/table-footer-indicator.scss b/src/plugins/telemetryTable/components/table-footer-indicator.scss index 3419cec3f9..02955af97d 100644 --- a/src/plugins/telemetryTable/components/table-footer-indicator.scss +++ b/src/plugins/telemetryTable/components/table-footer-indicator.scss @@ -1,29 +1,29 @@ .c-table-indicator { + display: flex; + align-items: center; + font-size: 0.9em; + overflow: hidden; + + &__elem { + @include ellipsize(); + flex: 0 1 auto; + padding: 2px; + text-transform: uppercase; + + > * { + //display: contents; + } + } + + &__counts { + //background: rgba(deeppink, 0.1); display: flex; - align-items: center; - font-size: 0.9em; + flex: 1 1 auto; + justify-content: flex-end; overflow: hidden; - &__elem { - @include ellipsize(); - flex: 0 1 auto; - padding: 2px; - text-transform: uppercase; - - > * { - //display: contents; - } - } - - &__counts { - //background: rgba(deeppink, 0.1); - display: flex; - flex: 1 1 auto; - justify-content: flex-end; - overflow: hidden; - - > * { - margin-left: $interiorMargin; - } + > * { + margin-left: $interiorMargin; } + } } diff --git a/src/plugins/telemetryTable/components/table-footer-indicator.vue b/src/plugins/telemetryTable/components/table-footer-indicator.vue index 2517387ea1..59171ab252 100644 --- a/src/plugins/telemetryTable/components/table-footer-indicator.vue +++ b/src/plugins/telemetryTable/components/table-footer-indicator.vue @@ -20,44 +20,36 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/telemetryTable/components/table-row.scss b/src/plugins/telemetryTable/components/table-row.scss index f21b297f23..a21397c81f 100644 --- a/src/plugins/telemetryTable/components/table-row.scss +++ b/src/plugins/telemetryTable/components/table-row.scss @@ -1,9 +1,9 @@ .noselect { --webkit-touch-callout: none; /* iOS Safari */ - -webkit-user-select: none; /* Safari */ - -khtml-user-select: none; /* Konqueror HTML */ - -moz-user-select: none; /* Firefox */ - -ms-user-select: none; /* Internet Explorer/Edge */ - user-select: none; /* Non-prefixed version, currently + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently supported by Chrome and Opera */ } diff --git a/src/plugins/telemetryTable/components/table-row.vue b/src/plugins/telemetryTable/components/table-row.vue index 6c8eca257d..874548f5c9 100644 --- a/src/plugins/telemetryTable/components/table-row.vue +++ b/src/plugins/telemetryTable/components/table-row.vue @@ -20,188 +20,204 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/telemetryTable/components/table.scss b/src/plugins/telemetryTable/components/table.scss index 03d54c0f72..1dda7f06b8 100644 --- a/src/plugins/telemetryTable/components/table.scss +++ b/src/plugins/telemetryTable/components/table.scss @@ -1,244 +1,249 @@ .c-telemetry-table__drop-target { - position: absolute; - width: 2px; - background-color: $editUIColor; - box-shadow: rgba($editUIColor, 0.5) 0 0 10px; - z-index: 1; - pointer-events: none; + position: absolute; + width: 2px; + background-color: $editUIColor; + box-shadow: rgba($editUIColor, 0.5) 0 0 10px; + z-index: 1; + pointer-events: none; } .c-telemetry-table { - // Table that displays telemetry in a scrolling body area + // Table that displays telemetry in a scrolling body area - @include fontAndSize(); + @include fontAndSize(); - display: flex; - flex-flow: column nowrap; - justify-content: flex-start; + display: flex; + flex-flow: column nowrap; + justify-content: flex-start; + overflow: hidden; + + th, + td { + display: block; + flex: 1 0 auto; + width: 100px; + vertical-align: middle; // This is crucial to hiding 4px height injected by browser by default + } + + /******************************* WRAPPERS */ + &__headers-w { + // Wraps __headers table + flex: 0 0 auto; overflow: hidden; + background: $colorTabHeaderBg; + } - th, td { - display: block; - flex: 1 0 auto; - width: 100px; - vertical-align: middle; // This is crucial to hiding 4px height injected by browser by default + /******************************* TABLES */ + &__headers, + &__body { + tr { + display: flex; + align-items: stretch; + } + } + + &__headers { + // A table + thead { + display: block; } - /******************************* WRAPPERS */ - &__headers-w { - // Wraps __headers table - flex: 0 0 auto; - overflow: hidden; - background: $colorTabHeaderBg; + &__labels { + // Top row, has labels + .c-telemetry-table__headers__content { + // Holds __label, sort indicator and resize-hitarea + display: flex; + align-items: center; + justify-content: center; + width: 100%; + } } - /******************************* TABLES */ - &__headers, - &__body { - tr { - display: flex; - align-items: stretch; - } + &__filter { + .c-table__search { + padding-top: 0; + padding-bottom: 0; + } + + .--width-less-than-600 & { + display: none !important; + } } + } - &__headers { - // A table - thead { - display: block; - } + &__headers__label { + overflow: hidden; + flex: 0 1 auto; + } - &__labels { - // Top row, has labels - .c-telemetry-table__headers__content { - // Holds __label, sort indicator and resize-hitarea - display: flex; - align-items: center; - justify-content: center; - width: 100%; - } - } + &__resize-hitarea { + // In table-column-header.vue + @include abs(); + display: none; // Set to display: block in .is-editing section below + left: auto; + right: -1 * $tabularTdPadLR; + width: $tableResizeColHitareaD; + cursor: col-resize; + transform: translateX(50%); // Move so this element sits over border between columns + } - &__filter { - .c-table__search { - padding-top: 0; - padding-bottom: 0; - } + /******************************* ELEMENTS */ + &__scroll-forcer { + // Force horz scroll when needed; width set via JS + font-size: 0; + height: 1px; // Height 0 won't force scroll properly + position: relative; + } - .--width-less-than-600 & { - display: none !important; - } - } - } + &__progress-bar { + margin-bottom: 3px; + } - &__headers__label { - overflow: hidden; - flex: 0 1 auto; - } + /******************************* WRAPPERS */ + &__body-w { + // Wraps __body table provides scrolling + flex: 1 1 100%; + height: 0; // Fixes Chrome 73 overflow bug + overflow-x: auto; + overflow-y: scroll; + } - &__resize-hitarea { - // In table-column-header.vue - @include abs(); - display: none; // Set to display: block in .is-editing section below - left: auto; right: -1 * $tabularTdPadLR; - width: $tableResizeColHitareaD; - cursor: col-resize; - transform: translateX(50%); // Move so this element sits over border between columns - } + /******************************* TABLES */ + &__body { + // A table + flex: 1 1 100%; + overflow-x: auto; - /******************************* ELEMENTS */ - &__scroll-forcer { - // Force horz scroll when needed; width set via JS - font-size: 0; - height: 1px; // Height 0 won't force scroll properly - position: relative; - } + tr { + display: flex; // flex-flow defaults to row nowrap (which is what we want) so no need to define + align-items: stretch; + position: absolute; + min-height: 18px; // Needed when a row has empty values in its cells - &__progress-bar { - margin-bottom: 3px; - } - - /******************************* WRAPPERS */ - &__body-w { - // Wraps __body table provides scrolling - flex: 1 1 100%; - height: 0; // Fixes Chrome 73 overflow bug - overflow-x: auto; - overflow-y: scroll; - } - - /******************************* TABLES */ - &__body { - // A table - flex: 1 1 100%; - overflow-x: auto; - - tr { - display: flex; // flex-flow defaults to row nowrap (which is what we want) so no need to define - align-items: stretch; - position: absolute; - min-height: 18px; // Needed when a row has empty values in its cells - - .is-editing .l-layout__frame & { - pointer-events: none; - } - - &.is-selected { - background-color: $colorSelectedBg !important; - color: $colorSelectedFg !important; - td { - background: none !important; - color: inherit !important; - } - } - } + .is-editing .l-layout__frame & { + pointer-events: none; + } + &.is-selected { + background-color: $colorSelectedBg !important; + color: $colorSelectedFg !important; td { - overflow: hidden; - text-overflow: ellipsis; + background: none !important; + color: inherit !important; } + } } - &__sizing { - // A table - display: table; - z-index: -1; + td { + overflow: hidden; + text-overflow: ellipsis; + } + } + + &__sizing { + // A table + display: table; + z-index: -1; + visibility: hidden; + pointer-events: none; + position: absolute; + + //Add some padding to allow for decorations such as limits indicator + tr { + display: table-row; + } + + th, + td { + display: table-cell; + padding-right: 10px; + padding-left: 10px; + white-space: nowrap; + } + } + + &__sizing-tr { + // A row element used to determine sizing of rows based on font size + visibility: hidden; + pointer-events: none; + } + + &__footer { + $pt: 2px; + border-top: 1px solid $colorInteriorBorder; + margin-top: $interiorMargin; + padding: $pt 0; + overflow: hidden; + transition: all 250ms; + + &:not(.is-filtering) { + .c-frame & { + height: 0; + padding: 0; visibility: hidden; - pointer-events: none; - position: absolute; - - //Add some padding to allow for decorations such as limits indicator - tr { - display: table-row; - } - - th, td { - display: table-cell; - padding-right: 10px; - padding-left: 10px; - white-space: nowrap; - } + } } + } - &__sizing-tr { - // A row element used to determine sizing of rows based on font size - visibility: hidden; - pointer-events: none; - } - - &__footer { - $pt: 2px; - border-top: 1px solid $colorInteriorBorder; - margin-top: $interiorMargin; - padding: $pt 0; - overflow: hidden; - transition: all 250ms; - - &:not(.is-filtering) { - .c-frame & { - height: 0; - padding: 0; - visibility: hidden; - } - } - } - - .c-frame & { - // target .c-frame .c-telemetry-table {} - $pt: 2px; - &:hover { - .c-telemetry-table__footer:not(.is-filtering) { - height: $pt + 16px; - padding: initial; - visibility: visible; - } - } + .c-frame & { + // target .c-frame .c-telemetry-table {} + $pt: 2px; + &:hover { + .c-telemetry-table__footer:not(.is-filtering) { + height: $pt + 16px; + padding: initial; + visibility: visible; + } } + } } // All tables td { - @include isLimit(); + @include isLimit(); } /******************************* SPECIFIC CASE WRAPPERS */ .is-editing { - .c-telemetry-table__headers__labels { - th[draggable], - th[draggable] > * { - cursor: move; - } - - th[draggable]:hover { - $b: $editFrameHovMovebarColorBg; - background: $b; - > * { background: $b; } - } + .c-telemetry-table__headers__labels { + th[draggable], + th[draggable] > * { + cursor: move; } - .c-telemetry-table__resize-hitarea { - display: block; + th[draggable]:hover { + $b: $editFrameHovMovebarColorBg; + background: $b; + > * { + background: $b; + } } + } + + .c-telemetry-table__resize-hitarea { + display: block; + } } .is-paused { - .c-table__body-w { - border: 1px solid rgba($colorPausedBg, 0.8); - } + .c-table__body-w { + border: 1px solid rgba($colorPausedBg, 0.8); + } } /******************************* LEGACY */ .s-status-taking-snapshot, .overlay.snapshot { - .c-table { - &__body-w { - overflow: auto; // Handle overflow-y issues with tables and html2canvas - } - - &-control-bar { - display: none; - + * { - margin-top: 0 !important; - } - } + .c-table { + &__body-w { + overflow: auto; // Handle overflow-y issues with tables and html2canvas } + + &-control-bar { + display: none; + + * { + margin-top: 0 !important; + } + } + } } diff --git a/src/plugins/telemetryTable/components/table.vue b/src/plugins/telemetryTable/components/table.vue index e726e5ad78..f6eebcbbac 100644 --- a/src/plugins/telemetryTable/components/table.vue +++ b/src/plugins/telemetryTable/components/table.vue @@ -20,269 +20,251 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/telemetryTable/plugin.js b/src/plugins/telemetryTable/plugin.js index 60fe41d859..8f9be40503 100644 --- a/src/plugins/telemetryTable/plugin.js +++ b/src/plugins/telemetryTable/plugin.js @@ -25,20 +25,20 @@ import TelemetryTableType from './TelemetryTableType'; import TelemetryTableViewActions from './ViewActions'; export default function plugin() { - return function install(openmct) { - openmct.objectViews.addProvider(new TelemetryTableViewProvider(openmct)); - openmct.inspectorViews.addProvider(new TableConfigurationViewProvider(openmct)); - openmct.types.addType('table', TelemetryTableType); - openmct.composition.addPolicy((parent, child) => { - if (parent.type === 'table') { - return Object.prototype.hasOwnProperty.call(child, 'telemetry'); - } else { - return true; - } - }); + return function install(openmct) { + openmct.objectViews.addProvider(new TelemetryTableViewProvider(openmct)); + openmct.inspectorViews.addProvider(new TableConfigurationViewProvider(openmct)); + openmct.types.addType('table', TelemetryTableType); + openmct.composition.addPolicy((parent, child) => { + if (parent.type === 'table') { + return Object.prototype.hasOwnProperty.call(child, 'telemetry'); + } else { + return true; + } + }); - TelemetryTableViewActions.forEach(action => { - openmct.actions.register(action); - }); - }; + TelemetryTableViewActions.forEach((action) => { + openmct.actions.register(action); + }); + }; } diff --git a/src/plugins/telemetryTable/pluginSpec.js b/src/plugins/telemetryTable/pluginSpec.js index adda5f10a9..9f1eeaf7dd 100644 --- a/src/plugins/telemetryTable/pluginSpec.js +++ b/src/plugins/telemetryTable/pluginSpec.js @@ -22,432 +22,460 @@ import TablePlugin from './plugin.js'; import Vue from 'vue'; import { - createOpenMct, - createMouseEvent, - spyOnBuiltins, - resetApplicationState + createOpenMct, + createMouseEvent, + spyOnBuiltins, + resetApplicationState } from 'utils/testing'; class MockDataTransfer { - constructor() { - this.data = {}; - } - get types() { - return Object.keys(this.data); - } - setData(format, data) { - this.data[format] = data; - } - getData(format) { - return this.data[format]; - } + constructor() { + this.data = {}; + } + get types() { + return Object.keys(this.data); + } + setData(format, data) { + this.data[format] = data; + } + getData(format) { + return this.data[format]; + } } -describe("the plugin", () => { - let openmct; - let tablePlugin; - let element; - let child; - let historicalProvider; - let originalRouterPath; - let unlistenConfigMutation; +describe('the plugin', () => { + let openmct; + let tablePlugin; + let element; + let child; + let historicalProvider; + let originalRouterPath; + let unlistenConfigMutation; - beforeEach((done) => { - openmct = createOpenMct(); + beforeEach((done) => { + openmct = createOpenMct(); - // Table Plugin is actually installed by default, but because installing it - // again is harmless it is left here as an examplar for non-default plugins. - tablePlugin = new TablePlugin(); - openmct.install(tablePlugin); + // Table Plugin is actually installed by default, but because installing it + // again is harmless it is left here as an examplar for non-default plugins. + tablePlugin = new TablePlugin(); + openmct.install(tablePlugin); - historicalProvider = { - request: () => { - return Promise.resolve([]); + historicalProvider = { + request: () => { + return Promise.resolve([]); + } + }; + spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider); + + element = document.createElement('div'); + child = document.createElement('div'); + element.appendChild(child); + + openmct.time.timeSystem('utc', { + start: 0, + end: 4 + }); + + openmct.types.addType('test-object', { + creatable: true + }); + + spyOnBuiltins(['requestAnimationFrame']); + window.requestAnimationFrame.and.callFake((callBack) => { + callBack(); + }); + + originalRouterPath = openmct.router.path; + + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach(() => { + openmct.time.timeSystem('utc', { + start: 0, + end: 1 + }); + + if (unlistenConfigMutation) { + unlistenConfigMutation(); + } + + return resetApplicationState(openmct); + }); + + describe('defines a table object', function () { + it('that is creatable', () => { + let tableType = openmct.types.get('table'); + expect(tableType.definition.creatable).toBe(true); + }); + }); + + it('provides a table view for objects with telemetry', () => { + const testTelemetryObject = { + id: 'test-object', + type: 'test-object', + telemetry: { + values: [ + { + key: 'some-key' + } + ] + } + }; + + const applicableViews = openmct.objectViews.get(testTelemetryObject, []); + let tableView = applicableViews.find((viewProvider) => viewProvider.key === 'table'); + expect(tableView).toBeDefined(); + }); + + describe('The table view', () => { + let testTelemetryObject; + let applicableViews; + let tableViewProvider; + let tableView; + let tableInstance; + let mockClock; + + 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: '', + 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 + } } - }; - spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider); + ] + }, + configuration: { + hiddenColumns: { + name: false, + utc: false, + 'some-key': false, + 'some-other-key': false + } + } + }; + 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' + } + ]; - element = document.createElement('div'); - child = document.createElement('div'); - element.appendChild(child); + historicalProvider.request = () => Promise.resolve(testTelemetry); - openmct.time.timeSystem('utc', { - start: 0, - end: 4 - }); + openmct.router.path = [testTelemetryObject]; - openmct.types.addType('test-object', { - creatable: true - }); + applicableViews = openmct.objectViews.get(testTelemetryObject, []); + tableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'table'); + tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]); + tableView.show(child, true); - spyOnBuiltins(['requestAnimationFrame']); - window.requestAnimationFrame.and.callFake((callBack) => { - callBack(); - }); + tableInstance = tableView.getTable(); - originalRouterPath = openmct.router.path; - - openmct.on('start', done); - openmct.startHeadless(); + await Vue.nextTick(); }); afterEach(() => { - openmct.time.timeSystem('utc', { - start: 0, - end: 1 - }); - - if (unlistenConfigMutation) { - unlistenConfigMutation(); - } - - return resetApplicationState(openmct); + openmct.router.path = originalRouterPath; }); - describe("defines a table object", function () { - it("that is creatable", () => { - let tableType = openmct.types.get('table'); - expect(tableType.definition.creatable).toBe(true); - }); + it('Shows no progress bar initially', () => { + let progressBar = element.querySelector('.c-progress-bar'); + + expect(tableInstance.outstandingRequests).toBe(0); + expect(progressBar).toBeNull(); }); - it("provides a table view for objects with telemetry", () => { - const testTelemetryObject = { - id: "test-object", - type: "test-object", - telemetry: { - values: [{ - key: "some-key" - }] - } - }; + it('Shows a progress bar while making requests', async () => { + tableInstance.incrementOutstandingRequests(); + await Vue.nextTick(); - const applicableViews = openmct.objectViews.get(testTelemetryObject, []); - let tableView = applicableViews.find((viewProvider) => viewProvider.key === 'table'); - expect(tableView).toBeDefined(); + let progressBar = element.querySelector('.c-progress-bar'); + + expect(tableInstance.outstandingRequests).toBe(1); + expect(progressBar).not.toBeNull(); }); - describe("The table view", () => { - let testTelemetryObject; - let applicableViews; - let tableViewProvider; - let tableView; - let tableInstance; - let mockClock; - - 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: "", - 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: { - hiddenColumns: { - name: false, - utc: false, - 'some-key': false, - 'some-other-key': false - } - } - }; - 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' - } - ]; - - historicalProvider.request = () => Promise.resolve(testTelemetry); - - openmct.router.path = [testTelemetryObject]; - - applicableViews = openmct.objectViews.get(testTelemetryObject, []); - tableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'table'); - tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]); - tableView.show(child, true); - - tableInstance = tableView.getTable(); - - await Vue.nextTick(); - }); - - afterEach(() => { - openmct.router.path = originalRouterPath; - }); - - it("Shows no progress bar initially", () => { - let progressBar = element.querySelector('.c-progress-bar'); - - expect(tableInstance.outstandingRequests).toBe(0); - expect(progressBar).toBeNull(); - }); - - it("Shows a progress bar while making requests", async () => { - tableInstance.incrementOutstandingRequests(); - await Vue.nextTick(); - - let progressBar = element.querySelector('.c-progress-bar'); - - expect(tableInstance.outstandingRequests).toBe(1); - expect(progressBar).not.toBeNull(); - - }); - - it("Renders a row for every telemetry datum returned", async () => { - let rows = element.querySelectorAll('table.c-telemetry-table__body tr'); - await Vue.nextTick(); - expect(rows.length).toBe(3); - }); - - it("Renders a column for every item in telemetry metadata", () => { - let headers = element.querySelectorAll('span.c-telemetry-table__headers__label'); - expect(headers.length).toBe(4); - expect(headers[0].innerText).toBe('Name'); - expect(headers[1].innerText).toBe('Time'); - expect(headers[2].innerText).toBe('Some attribute'); - expect(headers[3].innerText).toBe('Another attribute'); - }); - - 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]; - let fromColumnText = fromColumn.querySelector('span.c-telemetry-table__headers__label').innerText; - let toColumnText = toColumn.querySelector('span.c-telemetry-table__headers__label').innerText; - - let dragStartEvent = createMouseEvent('dragstart'); - let dragOverEvent = createMouseEvent('dragover'); - let dropEvent = createMouseEvent('drop'); - - dragStartEvent.dataTransfer = - dragOverEvent.dataTransfer = - dropEvent.dataTransfer = new MockDataTransfer(); - - fromColumn.dispatchEvent(dragStartEvent); - toColumn.dispatchEvent(dragOverEvent); - toColumn.dispatchEvent(dropEvent); - - 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", async () => { - tableInstance.tableRows.setColumnFilter("some-key", "1"); - await Vue.nextTick(); - let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); - - expect(filteredRowElements.length).toEqual(1); - tableInstance.tableRows.setColumnFilter("some-key", ""); - await Vue.nextTick(); - - let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); - expect(allRowElements.length).toEqual(3); - }); - - 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'); - - expect(filteredRowElements.length).toEqual(0); - - 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); - }); - - it("displays the correct number of column headers when the configuration is mutated", async () => { - const tableInstanceConfiguration = tableInstance.domainObject.configuration; - tableInstanceConfiguration.hiddenColumns['some-key'] = true; - unlistenConfigMutation = tableInstance.openmct.objects.mutate(tableInstance.domainObject, 'configuration', tableInstanceConfiguration); - - await Vue.nextTick(); - let tableHeaderElements = element.querySelectorAll('.c-telemetry-table__headers__label'); - expect(tableHeaderElements.length).toEqual(3); - - tableInstanceConfiguration.hiddenColumns['some-key'] = false; - unlistenConfigMutation = tableInstance.openmct.objects.mutate(tableInstance.domainObject, 'configuration', tableInstanceConfiguration); - - await Vue.nextTick(); - tableHeaderElements = element.querySelectorAll('.c-telemetry-table__headers__label'); - expect(tableHeaderElements.length).toEqual(4); - }); - - it("displays the correct number of table cells in a row when the configuration is mutated", async () => { - const tableInstanceConfiguration = tableInstance.domainObject.configuration; - tableInstanceConfiguration.hiddenColumns['some-key'] = true; - unlistenConfigMutation = tableInstance.openmct.objects.mutate(tableInstance.domainObject, 'configuration', tableInstanceConfiguration); - - await Vue.nextTick(); - let tableRowCells = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr:first-child td'); - expect(tableRowCells.length).toEqual(3); - - tableInstanceConfiguration.hiddenColumns['some-key'] = false; - unlistenConfigMutation = tableInstance.openmct.objects.mutate(tableInstance.domainObject, 'configuration', tableInstanceConfiguration); - - await Vue.nextTick(); - 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); - }); + it('Renders a row for every telemetry datum returned', async () => { + let rows = element.querySelectorAll('table.c-telemetry-table__body tr'); + await Vue.nextTick(); + expect(rows.length).toBe(3); }); + + it('Renders a column for every item in telemetry metadata', () => { + let headers = element.querySelectorAll('span.c-telemetry-table__headers__label'); + expect(headers.length).toBe(4); + expect(headers[0].innerText).toBe('Name'); + expect(headers[1].innerText).toBe('Time'); + expect(headers[2].innerText).toBe('Some attribute'); + expect(headers[3].innerText).toBe('Another attribute'); + }); + + 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]; + let fromColumnText = fromColumn.querySelector( + 'span.c-telemetry-table__headers__label' + ).innerText; + let toColumnText = toColumn.querySelector('span.c-telemetry-table__headers__label').innerText; + + let dragStartEvent = createMouseEvent('dragstart'); + let dragOverEvent = createMouseEvent('dragover'); + let dropEvent = createMouseEvent('drop'); + + dragStartEvent.dataTransfer = + dragOverEvent.dataTransfer = + dropEvent.dataTransfer = + new MockDataTransfer(); + + fromColumn.dispatchEvent(dragStartEvent); + toColumn.dispatchEvent(dragOverEvent); + toColumn.dispatchEvent(dropEvent); + + 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', async () => { + tableInstance.tableRows.setColumnFilter('some-key', '1'); + await Vue.nextTick(); + let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); + + expect(filteredRowElements.length).toEqual(1); + tableInstance.tableRows.setColumnFilter('some-key', ''); + await Vue.nextTick(); + + let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr'); + expect(allRowElements.length).toEqual(3); + }); + + 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'); + + expect(filteredRowElements.length).toEqual(0); + + 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); + }); + + it('displays the correct number of column headers when the configuration is mutated', async () => { + const tableInstanceConfiguration = tableInstance.domainObject.configuration; + tableInstanceConfiguration.hiddenColumns['some-key'] = true; + unlistenConfigMutation = tableInstance.openmct.objects.mutate( + tableInstance.domainObject, + 'configuration', + tableInstanceConfiguration + ); + + await Vue.nextTick(); + let tableHeaderElements = element.querySelectorAll('.c-telemetry-table__headers__label'); + expect(tableHeaderElements.length).toEqual(3); + + tableInstanceConfiguration.hiddenColumns['some-key'] = false; + unlistenConfigMutation = tableInstance.openmct.objects.mutate( + tableInstance.domainObject, + 'configuration', + tableInstanceConfiguration + ); + + await Vue.nextTick(); + tableHeaderElements = element.querySelectorAll('.c-telemetry-table__headers__label'); + expect(tableHeaderElements.length).toEqual(4); + }); + + it('displays the correct number of table cells in a row when the configuration is mutated', async () => { + const tableInstanceConfiguration = tableInstance.domainObject.configuration; + tableInstanceConfiguration.hiddenColumns['some-key'] = true; + unlistenConfigMutation = tableInstance.openmct.objects.mutate( + tableInstance.domainObject, + 'configuration', + tableInstanceConfiguration + ); + + await Vue.nextTick(); + let tableRowCells = element.querySelectorAll( + 'table.c-telemetry-table__body > tbody > tr:first-child td' + ); + expect(tableRowCells.length).toEqual(3); + + tableInstanceConfiguration.hiddenColumns['some-key'] = false; + unlistenConfigMutation = tableInstance.openmct.objects.mutate( + tableInstance.domainObject, + 'configuration', + tableInstanceConfiguration + ); + + await Vue.nextTick(); + 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/espresso-theme.scss b/src/plugins/themes/espresso-theme.scss index 58d039a8ec..05b5a0de1a 100644 --- a/src/plugins/themes/espresso-theme.scss +++ b/src/plugins/themes/espresso-theme.scss @@ -1,22 +1,22 @@ -@import "../../styles/vendor/normalize-min"; -@import "../../styles/constants"; -@import "../../styles/constants-mobile.scss"; +@import '../../styles/vendor/normalize-min'; +@import '../../styles/constants'; +@import '../../styles/constants-mobile.scss'; -@import "../../styles/constants-espresso"; +@import '../../styles/constants-espresso'; -@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/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"; +@import '../../styles/vue-styles.scss'; diff --git a/src/plugins/themes/espresso.js b/src/plugins/themes/espresso.js index d403c67696..99127db482 100644 --- a/src/plugins/themes/espresso.js +++ b/src/plugins/themes/espresso.js @@ -1,7 +1,7 @@ import { installTheme } from './installTheme'; export default function plugin() { - return function install(openmct) { - installTheme(openmct, 'espresso'); - }; + return function install(openmct) { + installTheme(openmct, 'espresso'); + }; } diff --git a/src/plugins/themes/installTheme.js b/src/plugins/themes/installTheme.js index ffdf0924ba..9987b09b3d 100644 --- a/src/plugins/themes/installTheme.js +++ b/src/plugins/themes/installTheme.js @@ -1,18 +1,18 @@ const dataAttribute = 'theme'; export function installTheme(openmct, themeName) { - const currentTheme = document.querySelector(`link[data-${dataAttribute}]`); - if (currentTheme) { - currentTheme.remove(); - } + const currentTheme = document.querySelector(`link[data-${dataAttribute}]`); + if (currentTheme) { + currentTheme.remove(); + } - const newTheme = document.createElement('link'); - newTheme.setAttribute('rel', 'stylesheet'); + const newTheme = document.createElement('link'); + newTheme.setAttribute('rel', 'stylesheet'); - // eslint-disable-next-line no-undef - const href = `${openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}${themeName}Theme.css`; - newTheme.setAttribute('href', href); - newTheme.dataset[dataAttribute] = themeName; + // eslint-disable-next-line no-undef + const href = `${openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}${themeName}Theme.css`; + newTheme.setAttribute('href', href); + newTheme.dataset[dataAttribute] = themeName; - document.head.appendChild(newTheme); + document.head.appendChild(newTheme); } diff --git a/src/plugins/themes/snow-theme.scss b/src/plugins/themes/snow-theme.scss index 294be82237..24342f120f 100644 --- a/src/plugins/themes/snow-theme.scss +++ b/src/plugins/themes/snow-theme.scss @@ -1,22 +1,22 @@ -@import "../../styles/vendor/normalize-min"; -@import "../../styles/constants"; -@import "../../styles/constants-mobile.scss"; +@import '../../styles/vendor/normalize-min'; +@import '../../styles/constants'; +@import '../../styles/constants-mobile.scss'; -@import "../../styles/constants-snow"; +@import '../../styles/constants-snow'; -@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/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"; +@import '../../styles/vue-styles.scss'; diff --git a/src/plugins/themes/snow.js b/src/plugins/themes/snow.js index 3befb82252..af6095282f 100644 --- a/src/plugins/themes/snow.js +++ b/src/plugins/themes/snow.js @@ -1,7 +1,7 @@ import { installTheme } from './installTheme'; export default function plugin() { - return function install(openmct) { - installTheme(openmct, 'snow'); - }; + return function install(openmct) { + installTheme(openmct, 'snow'); + }; } diff --git a/src/plugins/timeConductor/Conductor.vue b/src/plugins/timeConductor/Conductor.vue index 171175afb6..9079ed0fb1 100644 --- a/src/plugins/timeConductor/Conductor.vue +++ b/src/plugins/timeConductor/Conductor.vue @@ -20,50 +20,46 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/ConductorAxis.vue b/src/plugins/timeConductor/ConductorAxis.vue index 3794ad9ab1..05838d05c7 100644 --- a/src/plugins/timeConductor/ConductorAxis.vue +++ b/src/plugins/timeConductor/ConductorAxis.vue @@ -20,20 +20,12 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/ConductorHistory.vue b/src/plugins/timeConductor/ConductorHistory.vue index 7418e2ff09..3aed6944a9 100644 --- a/src/plugins/timeConductor/ConductorHistory.vue +++ b/src/plugins/timeConductor/ConductorHistory.vue @@ -20,20 +20,17 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/ConductorInputsFixed.vue b/src/plugins/timeConductor/ConductorInputsFixed.vue index dcbf552b81..ed404b9007 100644 --- a/src/plugins/timeConductor/ConductorInputsFixed.vue +++ b/src/plugins/timeConductor/ConductorInputsFixed.vue @@ -20,293 +20,288 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/ConductorInputsRealtime.vue b/src/plugins/timeConductor/ConductorInputsRealtime.vue index 2b7e22bd8c..76fb824326 100644 --- a/src/plugins/timeConductor/ConductorInputsRealtime.vue +++ b/src/plugins/timeConductor/ConductorInputsRealtime.vue @@ -20,310 +20,302 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/ConductorMode.vue b/src/plugins/timeConductor/ConductorMode.vue index 4d7c99f846..7852820844 100644 --- a/src/plugins/timeConductor/ConductorMode.vue +++ b/src/plugins/timeConductor/ConductorMode.vue @@ -20,163 +20,157 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/ConductorModeIcon.vue b/src/plugins/timeConductor/ConductorModeIcon.vue index 2a3cc59133..1486dcfb4d 100644 --- a/src/plugins/timeConductor/ConductorModeIcon.vue +++ b/src/plugins/timeConductor/ConductorModeIcon.vue @@ -20,8 +20,8 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/ConductorTimeSystem.vue b/src/plugins/timeConductor/ConductorTimeSystem.vue index 7812137738..80e7743e7f 100644 --- a/src/plugins/timeConductor/ConductorTimeSystem.vue +++ b/src/plugins/timeConductor/ConductorTimeSystem.vue @@ -20,114 +20,115 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/DatePicker.vue b/src/plugins/timeConductor/DatePicker.vue index 8be915d67c..25a7843792 100644 --- a/src/plugins/timeConductor/DatePicker.vue +++ b/src/plugins/timeConductor/DatePicker.vue @@ -20,69 +20,54 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/conductor-axis.scss b/src/plugins/timeConductor/conductor-axis.scss index 8fec88e73b..1f58cd8b12 100644 --- a/src/plugins/timeConductor/conductor-axis.scss +++ b/src/plugins/timeConductor/conductor-axis.scss @@ -1,67 +1,67 @@ @use 'sass:math'; .c-conductor-axis { - $h: 18px; - $tickYPos: math.div($h, 2) + 12px; + $h: 18px; + $tickYPos: math.div($h, 2) + 12px; - @include userSelectNone(); - @include bgTicks($c: rgba($colorBodyFg, 0.4)); - background-position: 0 50%; - background-size: 5px 2px; - border-radius: $controlCr; - height: $h; + @include userSelectNone(); + @include bgTicks($c: rgba($colorBodyFg, 0.4)); + background-position: 0 50%; + background-size: 5px 2px; + border-radius: $controlCr; + height: $h; - svg { - text-rendering: geometricPrecision; - width: 100%; - height: 100%; - > g.axis { - // Overall Tick holder - transform: translateY($tickYPos); - path { - // Domain line - display: none; - } + svg { + text-rendering: geometricPrecision; + width: 100%; + height: 100%; + > g.axis { + // Overall Tick holder + transform: translateY($tickYPos); + path { + // Domain line + display: none; + } - g { - // Each tick. These move on drag. - line { - // Line beneath ticks - display: none; - } - } - } - - text { - // Tick labels - fill: $colorBodyFg; - font-size: 1em; - paint-order: stroke; - font-weight: bold; - stroke: $colorBodyBg; - stroke-linecap: butt; - stroke-linejoin: bevel; - stroke-width: 6px; + g { + // Each tick. These move on drag. + line { + // Line beneath ticks + display: none; } + } } - body.desktop .is-fixed-mode & { - background-size: 3px 30%; - background-color: $colorBodyBgSubtle; - box-shadow: inset rgba(black, 0.4) 0 1px 1px; - - svg text { - fill: $colorBodyFg; - stroke: $colorBodyBgSubtle; - } + text { + // Tick labels + fill: $colorBodyFg; + font-size: 1em; + paint-order: stroke; + font-weight: bold; + stroke: $colorBodyBg; + stroke-linecap: butt; + stroke-linejoin: bevel; + stroke-width: 6px; } + } - .is-realtime-mode & { - $c: 1px solid rgba($colorTime, 0.7); - border-left: $c; - border-right: $c; - svg text { - fill: $colorTime; - } + body.desktop .is-fixed-mode & { + background-size: 3px 30%; + background-color: $colorBodyBgSubtle; + box-shadow: inset rgba(black, 0.4) 0 1px 1px; + + svg text { + fill: $colorBodyFg; + stroke: $colorBodyBgSubtle; } + } + + .is-realtime-mode & { + $c: 1px solid rgba($colorTime, 0.7); + border-left: $c; + border-right: $c; + svg text { + fill: $colorTime; + } + } } diff --git a/src/plugins/timeConductor/conductor-mode-icon.scss b/src/plugins/timeConductor/conductor-mode-icon.scss index cc7748ed93..a7091a0a35 100644 --- a/src/plugins/timeConductor/conductor-mode-icon.scss +++ b/src/plugins/timeConductor/conductor-mode-icon.scss @@ -1,107 +1,160 @@ @keyframes clock-hands { - 0% { transform: translate(-50%, -50%) rotate(0deg); } - 100% { transform: translate(-50%, -50%) rotate(360deg); } + 0% { + transform: translate(-50%, -50%) rotate(0deg); + } + 100% { + transform: translate(-50%, -50%) rotate(360deg); + } } @keyframes clock-hands-sticky { - 0% { transform: translate(-50%, -50%) rotate(0deg); } - 7% { transform: translate(-50%, -50%) rotate(0deg); } - 8% { transform: translate(-50%, -50%) rotate(30deg); } - 15% { transform: translate(-50%, -50%) rotate(30deg); } - 16% { transform: translate(-50%, -50%) rotate(60deg); } - 24% { transform: translate(-50%, -50%) rotate(60deg); } - 25% { transform: translate(-50%, -50%) rotate(90deg); } - 32% { transform: translate(-50%, -50%) rotate(90deg); } - 33% { transform: translate(-50%, -50%) rotate(120deg); } - 40% { transform: translate(-50%, -50%) rotate(120deg); } - 41% { transform: translate(-50%, -50%) rotate(150deg); } - 49% { transform: translate(-50%, -50%) rotate(150deg); } - 50% { transform: translate(-50%, -50%) rotate(180deg); } - 57% { transform: translate(-50%, -50%) rotate(180deg); } - 58% { transform: translate(-50%, -50%) rotate(210deg); } - 65% { transform: translate(-50%, -50%) rotate(210deg); } - 66% { transform: translate(-50%, -50%) rotate(240deg); } - 74% { transform: translate(-50%, -50%) rotate(240deg); } - 75% { transform: translate(-50%, -50%) rotate(270deg); } - 82% { transform: translate(-50%, -50%) rotate(270deg); } - 83% { transform: translate(-50%, -50%) rotate(300deg); } - 90% { transform: translate(-50%, -50%) rotate(300deg); } - 91% { transform: translate(-50%, -50%) rotate(330deg); } - 99% { transform: translate(-50%, -50%) rotate(330deg); } - 100% { transform: translate(-50%, -50%) rotate(360deg); } + 0% { + transform: translate(-50%, -50%) rotate(0deg); + } + 7% { + transform: translate(-50%, -50%) rotate(0deg); + } + 8% { + transform: translate(-50%, -50%) rotate(30deg); + } + 15% { + transform: translate(-50%, -50%) rotate(30deg); + } + 16% { + transform: translate(-50%, -50%) rotate(60deg); + } + 24% { + transform: translate(-50%, -50%) rotate(60deg); + } + 25% { + transform: translate(-50%, -50%) rotate(90deg); + } + 32% { + transform: translate(-50%, -50%) rotate(90deg); + } + 33% { + transform: translate(-50%, -50%) rotate(120deg); + } + 40% { + transform: translate(-50%, -50%) rotate(120deg); + } + 41% { + transform: translate(-50%, -50%) rotate(150deg); + } + 49% { + transform: translate(-50%, -50%) rotate(150deg); + } + 50% { + transform: translate(-50%, -50%) rotate(180deg); + } + 57% { + transform: translate(-50%, -50%) rotate(180deg); + } + 58% { + transform: translate(-50%, -50%) rotate(210deg); + } + 65% { + transform: translate(-50%, -50%) rotate(210deg); + } + 66% { + transform: translate(-50%, -50%) rotate(240deg); + } + 74% { + transform: translate(-50%, -50%) rotate(240deg); + } + 75% { + transform: translate(-50%, -50%) rotate(270deg); + } + 82% { + transform: translate(-50%, -50%) rotate(270deg); + } + 83% { + transform: translate(-50%, -50%) rotate(300deg); + } + 90% { + transform: translate(-50%, -50%) rotate(300deg); + } + 91% { + transform: translate(-50%, -50%) rotate(330deg); + } + 99% { + transform: translate(-50%, -50%) rotate(330deg); + } + 100% { + transform: translate(-50%, -50%) rotate(360deg); + } } - .c-clock-symbol { - $c: $colorBtnBg; //$colorObjHdrIc; - $d: 18px; - height: $d; - width: $d; - position: relative; + $c: $colorBtnBg; //$colorObjHdrIc; + $d: 18px; + height: $d; + width: $d; + position: relative; + &:before { + font-family: symbolsfont; + color: $c; + content: $glyph-icon-brackets; + font-size: $d; + line-height: normal; + display: block; + width: 100%; + height: 100%; + z-index: 1; + } + + // Clock hands + div[class*='hand'] { + $handW: 2px; + $handH: $d * 0.4; + animation-iteration-count: infinite; + animation-timing-function: steps(12); + transform-origin: bottom; + position: absolute; + height: $handW; + width: $handW; + left: 50%; + top: 50%; + z-index: 2; &:before { - font-family: symbolsfont; - color: $c; - content: $glyph-icon-brackets; - font-size: $d; - line-height: normal; - display: block; - width: 100%; - height: 100%; - z-index: 1; + background: $c; + content: ''; + display: block; + position: absolute; + width: 100%; + bottom: -1px; } + &.hand-little { + z-index: 2; + animation-duration: 12s; + transform: translate(-50%, -50%) rotate(120deg); + &:before { + height: ceil($handH * 0.6); + } + } + &.hand-big { + z-index: 1; + animation-duration: 1s; + transform: translate(-50%, -50%); + &:before { + height: $handH; + } + } + } - // Clock hands - div[class*="hand"] { - $handW: 2px; - $handH: $d * 0.4; - animation-iteration-count: infinite; - animation-timing-function: steps(12); - transform-origin: bottom; - position: absolute; - height: $handW; - width: $handW; - left: 50%; - top: 50%; - z-index: 2; - &:before { - background: $c; - content: ''; - display: block; - position: absolute; - width: 100%; - bottom: -1px; - } - &.hand-little { - z-index: 2; - animation-duration: 12s; - transform: translate(-50%, -50%) rotate(120deg); - &:before { - height: ceil($handH * 0.6); - } - } - &.hand-big { - z-index: 1; - animation-duration: 1s; - transform: translate(-50%, -50%); - &:before { - height: $handH; - } - } + // Modes + .is-realtime-mode &, + .is-lad-mode & { + &:before { + // Brackets icon + color: $colorTime; } - - // Modes - .is-realtime-mode &, - .is-lad-mode & { - &:before { - // Brackets icon - color: $colorTime; - } - div[class*="hand"] { - animation-name: clock-hands; - &:before { - background: $colorTime; - } - } + div[class*='hand'] { + animation-name: clock-hands; + &:before { + background: $colorTime; + } } + } } diff --git a/src/plugins/timeConductor/conductor-mode.scss b/src/plugins/timeConductor/conductor-mode.scss index 6939cb00cf..6835faeb8e 100644 --- a/src/plugins/timeConductor/conductor-mode.scss +++ b/src/plugins/timeConductor/conductor-mode.scss @@ -1,14 +1,14 @@ .c-conductor__mode-menu { - max-height: 80vh; - max-width: 500px; - min-height: 250px; - z-index: 70; + max-height: 80vh; + max-width: 500px; + min-height: 250px; + z-index: 70; - [class*="__icon"] { - filter: $colorKeyFilter; - } + [class*='__icon'] { + filter: $colorKeyFilter; + } - [class*="__item-description"] { - min-width: 200px; - } + [class*='__item-description'] { + min-width: 200px; + } } diff --git a/src/plugins/timeConductor/conductor.scss b/src/plugins/timeConductor/conductor.scss index 45acbf7a56..28dee1ac6a 100644 --- a/src/plugins/timeConductor/conductor.scss +++ b/src/plugins/timeConductor/conductor.scss @@ -1,310 +1,310 @@ .c-input--submit { - // Can't use display: none because some browsers will pretend the input doesn't exist, and enter won't work - visibility: none; - height: 0; - width: 0; - padding: 0; + // Can't use display: none because some browsers will pretend the input doesn't exist, and enter won't work + visibility: none; + height: 0; + width: 0; + padding: 0; } /*********************************************** CONDUCTOR LAYOUT */ .c-conductor { - &__inputs { - display: contents; - } + &__inputs { + display: contents; + } - &__time-bounds { - display: grid; - grid-column-gap: $interiorMargin; - grid-row-gap: $interiorMargin; - align-items: center; + &__time-bounds { + display: grid; + grid-column-gap: $interiorMargin; + grid-row-gap: $interiorMargin; + align-items: center; - // Default: fixed mode, desktop - grid-template-rows: 1fr; - grid-template-columns: 20px auto 1fr auto; - grid-template-areas: "tc-mode-icon tc-start tc-ticks tc-end"; - } + // Default: fixed mode, desktop + grid-template-rows: 1fr; + grid-template-columns: 20px auto 1fr auto; + grid-template-areas: 'tc-mode-icon tc-start tc-ticks tc-end'; + } - &__mode-icon { - grid-area: tc-mode-icon; - } + &__mode-icon { + grid-area: tc-mode-icon; + } - &__start-fixed, - &__start-delta { - grid-area: tc-start; - display: flex; - } + &__start-fixed, + &__start-delta { + grid-area: tc-start; + display: flex; + } - &__end-fixed, - &__end-delta { - grid-area: tc-end; - display: flex; - justify-content: flex-end; - } + &__end-fixed, + &__end-delta { + grid-area: tc-end; + display: flex; + justify-content: flex-end; + } - &__ticks { - grid-area: tc-ticks; - } + &__ticks { + grid-area: tc-ticks; + } - &__controls { - grid-area: tc-controls; - display: flex; - align-items: center; - > * + * { - margin-left: $interiorMargin; - } - } - - &.is-fixed-mode { - .c-conductor-axis { - &__zoom-indicator { - border: 1px solid transparent; - display: none; // Hidden by default - } - } - - &:not(.is-panning), - &:not(.is-zooming) { - .c-conductor-axis { - &:hover, - &:active { - cursor: col-resize; - } - } - } - - &.is-panning, - &.is-zooming { - .c-conductor-input input { - // Styles for inputs while zooming or panning - background: rgba($timeConductorActiveBg, 0.4); - } - } - - &.alt-pressed { - .c-conductor-axis:hover { - // When alt is being pressed and user is hovering over the axis, set the cursor - @include cursorGrab(); - } - } - - &.is-panning { - .c-conductor-axis { - @include cursorGrab(); - background-color: $timeConductorActivePanBg; - transition: $transIn; - - svg text { - stroke: $timeConductorActivePanBg; - transition: $transIn; - } - } - } - - &.is-zooming { - .c-conductor-axis__zoom-indicator { - display: block; - position: absolute; - background: rgba($timeConductorActiveBg, 0.4); - border-left-color: $timeConductorActiveBg; - border-right-color: $timeConductorActiveBg; - top: 0; bottom: 0; - } - } - } - - &.is-realtime-mode { - .c-conductor__time-bounds { - grid-template-columns: 20px auto 1fr auto auto; - grid-template-areas: "tc-mode-icon tc-start tc-ticks tc-updated tc-end"; - } - - .c-conductor__end-fixed { - grid-area: tc-updated; - } - } - - body.phone.portrait & { - .c-conductor__time-bounds { - grid-row-gap: $interiorMargin; - grid-template-rows: auto auto; - grid-template-columns: 20px auto auto; - } - - .c-conductor__controls { - padding-left: 25px; // Line up visually with other controls - } - - &__mode-icon { - grid-row: 1; - } - - &__ticks, - &__zoom { - display: none; - } - - &.is-fixed-mode { - [class*='__start-fixed'], - [class*='__end-fixed'] { - [class*='__label'] { - // Start and end are in separate columns; make the labels line up - width: 30px; - } - } - - [class*='__end-input'] { - justify-content: flex-start; - } - - .c-conductor__time-bounds { - grid-template-areas: - "tc-mode-icon tc-start tc-start" - "tc-mode-icon tc-end tc-end" - } - } - - &.is-realtime-mode { - .c-conductor__time-bounds { - grid-template-areas: - "tc-mode-icon tc-start tc-updated" - "tc-mode-icon tc-end tc-end"; - } - - .c-conductor__end-fixed { - justify-content: flex-end; - } - } - } -} - -.c-conductor-holder--compact { - min-height: 22px; - - .c-conductor { - &__inputs, - &__time-bounds { - display: flex; - - .c-toggle-switch { - // Used in independent Time Conductor - flex: 0 0 auto; - } - } - - &__inputs { - > * + * { - margin-left: $interiorMarginSm; - } - } - } - - .is-realtime-mode .c-conductor__end-fixed { - display: none !important; - } -} - -.c-conductor-input { - color: $colorInputFg; + &__controls { + grid-area: tc-controls; display: flex; align-items: center; - justify-content: flex-start; - > * + * { - margin-left: $interiorMarginSm; + margin-left: $interiorMargin; + } + } + + &.is-fixed-mode { + .c-conductor-axis { + &__zoom-indicator { + border: 1px solid transparent; + display: none; // Hidden by default + } } - &:before { - // Realtime-mode clock icon symbol - margin-right: $interiorMarginSm; - } - - .c-direction-indicator { - // Holds realtime-mode + and - symbols - font-size: 0.7em; - } - - input:invalid { - background: rgba($colorFormInvalid, 0.5); - } -} - -.is-realtime-mode { - .c-conductor__controls button, - .c-conductor__delta-button { - @include themedButton($colorTimeBg); - color: $colorTimeFg; - } - - .c-conductor-input { - &:before { - color: $colorTime; + &:not(.is-panning), + &:not(.is-zooming) { + .c-conductor-axis { + &:hover, + &:active { + cursor: col-resize; } + } + } + + &.is-panning, + &.is-zooming { + .c-conductor-input input { + // Styles for inputs while zooming or panning + background: rgba($timeConductorActiveBg, 0.4); + } + } + + &.alt-pressed { + .c-conductor-axis:hover { + // When alt is being pressed and user is hovering over the axis, set the cursor + @include cursorGrab(); + } + } + + &.is-panning { + .c-conductor-axis { + @include cursorGrab(); + background-color: $timeConductorActivePanBg; + transition: $transIn; + + svg text { + stroke: $timeConductorActivePanBg; + transition: $transIn; + } + } + } + + &.is-zooming { + .c-conductor-axis__zoom-indicator { + display: block; + position: absolute; + background: rgba($timeConductorActiveBg, 0.4); + border-left-color: $timeConductorActiveBg; + border-right-color: $timeConductorActiveBg; + top: 0; + bottom: 0; + } + } + } + + &.is-realtime-mode { + .c-conductor__time-bounds { + grid-template-columns: 20px auto 1fr auto auto; + grid-template-areas: 'tc-mode-icon tc-start tc-ticks tc-updated tc-end'; } .c-conductor__end-fixed { - // Displays last RT udpate - color: $colorTime; - - input { - // Remove input look - background: none; - box-shadow: none; - color: $colorTime; - pointer-events: none; - - &[disabled] { - opacity: 1 !important; - } - } + grid-area: tc-updated; } + } + + body.phone.portrait & { + .c-conductor__time-bounds { + grid-row-gap: $interiorMargin; + grid-template-rows: auto auto; + grid-template-columns: 20px auto auto; + } + + .c-conductor__controls { + padding-left: 25px; // Line up visually with other controls + } + + &__mode-icon { + grid-row: 1; + } + + &__ticks, + &__zoom { + display: none; + } + + &.is-fixed-mode { + [class*='__start-fixed'], + [class*='__end-fixed'] { + [class*='__label'] { + // Start and end are in separate columns; make the labels line up + width: 30px; + } + } + + [class*='__end-input'] { + justify-content: flex-start; + } + + .c-conductor__time-bounds { + grid-template-areas: + 'tc-mode-icon tc-start tc-start' + 'tc-mode-icon tc-end tc-end'; + } + } + + &.is-realtime-mode { + .c-conductor__time-bounds { + grid-template-areas: + 'tc-mode-icon tc-start tc-updated' + 'tc-mode-icon tc-end tc-end'; + } + + .c-conductor__end-fixed { + justify-content: flex-end; + } + } + } +} + +.c-conductor-holder--compact { + min-height: 22px; + + .c-conductor { + &__inputs, + &__time-bounds { + display: flex; + + .c-toggle-switch { + // Used in independent Time Conductor + flex: 0 0 auto; + } + } + + &__inputs { + > * + * { + margin-left: $interiorMarginSm; + } + } + } + + .is-realtime-mode .c-conductor__end-fixed { + display: none !important; + } +} + +.c-conductor-input { + color: $colorInputFg; + display: flex; + align-items: center; + justify-content: flex-start; + + > * + * { + margin-left: $interiorMarginSm; + } + + &:before { + // Realtime-mode clock icon symbol + margin-right: $interiorMarginSm; + } + + .c-direction-indicator { + // Holds realtime-mode + and - symbols + font-size: 0.7em; + } + + input:invalid { + background: rgba($colorFormInvalid, 0.5); + } +} + +.is-realtime-mode { + .c-conductor__controls button, + .c-conductor__delta-button { + @include themedButton($colorTimeBg); + color: $colorTimeFg; + } + + .c-conductor-input { + &:before { + color: $colorTime; + } + } + + .c-conductor__end-fixed { + // Displays last RT udpate + color: $colorTime; + + input { + // Remove input look + background: none; + box-shadow: none; + color: $colorTime; + pointer-events: none; + + &[disabled] { + opacity: 1 !important; + } + } + } } [class^='pr-tc-input-menu'] { - // Uses ^= here to target both start and end menus - background: $colorBodyBg; - border-radius: $controlCr; - display: grid; - grid-template-columns: 1fr 1fr 2fr; - grid-column-gap: 3px; - grid-row-gap: 4px; - align-items: start; - box-shadow: $shdwMenu; - padding: $interiorMargin; - position: absolute; - left: 8px; - bottom: 24px; - z-index: 99; + // Uses ^= here to target both start and end menus + background: $colorBodyBg; + border-radius: $controlCr; + display: grid; + grid-template-columns: 1fr 1fr 2fr; + grid-column-gap: 3px; + grid-row-gap: 4px; + align-items: start; + box-shadow: $shdwMenu; + padding: $interiorMargin; + position: absolute; + left: 8px; + bottom: 24px; + z-index: 99; - &[class*='--bottom'] { - bottom: auto; - top: 24px; - } + &[class*='--bottom'] { + bottom: auto; + top: 24px; + } } .l-shell__time-conductor .pr-tc-input-menu--end { - left: auto; - right: 0; + left: auto; + right: 0; } - [class^='pr-time'] { - &[class*='label'] { - font-size: 0.8em; - opacity: 0.6; - text-transform: uppercase; - } + &[class*='label'] { + font-size: 0.8em; + opacity: 0.6; + text-transform: uppercase; + } - &[class*='controls'] { - display: flex; - align-items: center; - white-space: nowrap; + &[class*='controls'] { + display: flex; + align-items: center; + white-space: nowrap; - input { - height: 22px; - line-height: 22px; - margin-right: $interiorMarginSm; - font-size: 1.25em; - width: 42px; - } + input { + height: 22px; + line-height: 22px; + margin-right: $interiorMarginSm; + font-size: 1.25em; + width: 42px; } + } } diff --git a/src/plugins/timeConductor/date-picker.scss b/src/plugins/timeConductor/date-picker.scss index 3c447e9745..cd36818e63 100644 --- a/src/plugins/timeConductor/date-picker.scss +++ b/src/plugins/timeConductor/date-picker.scss @@ -1,101 +1,101 @@ /******************************************************** PICKER */ .c-datetime-picker { - @include userSelectNone(); - padding: $interiorMarginLg !important; - display: flex !important; // Override .c-menu display: block; - flex-direction: column; - > * + * { - margin-top: $interiorMargin; - } + @include userSelectNone(); + padding: $interiorMarginLg !important; + display: flex !important; // Override .c-menu display: block; + flex-direction: column; + > * + * { + margin-top: $interiorMargin; + } - &__close-button { - display: none; // Only show when body.phone, see below. - } + &__close-button { + display: none; // Only show when body.phone, see below. + } - &__pager { - flex: 0 0 auto; - } + &__pager { + flex: 0 0 auto; + } - &__calendar { - border-top: 1px solid $colorInteriorBorder; - flex: 1 1 auto; - } + &__calendar { + border-top: 1px solid $colorInteriorBorder; + flex: 1 1 auto; + } } .c-pager { - display: grid; - grid-column-gap: $interiorMargin; - grid-template-rows: 1fr; - grid-template-columns: auto 1fr auto; - align-items: center; + display: grid; + grid-column-gap: $interiorMargin; + grid-template-rows: 1fr; + grid-template-columns: auto 1fr auto; + align-items: center; - .c-icon-button { - font-size: 0.8em; - } + .c-icon-button { + font-size: 0.8em; + } - &__month-year { - text-align: center; - } + &__month-year { + text-align: center; + } } /******************************************************** CALENDAR */ .c-calendar { - display: grid; - grid-template-columns: repeat(7, min-content); - grid-template-rows: auto; - grid-gap: 1px; - height: 100%; + display: grid; + grid-template-columns: repeat(7, min-content); + grid-template-rows: auto; + grid-gap: 1px; + height: 100%; - $mutedOpacity: 0.5; + $mutedOpacity: 0.5; - ul { - display: contents; - &[class*='--header'] { - pointer-events: none; - li { - opacity: $mutedOpacity; - } - } + ul { + display: contents; + &[class*='--header'] { + pointer-events: none; + li { + opacity: $mutedOpacity; + } + } + } + + li { + display: flex; + flex-direction: column; + justify-content: center !important; + padding: $interiorMargin; + + &.is-in-month { + background: $colorMenuElementHilite; } - li { - display: flex; - flex-direction: column; - justify-content: center !important; - padding: $interiorMargin; - - &.is-in-month { - background: $colorMenuElementHilite; - } - - &.selected { - background: $colorKey; - color: $colorKeyFg; - } + &.selected { + background: $colorKey; + color: $colorKeyFg; } + } - &__day { - &--sub { - opacity: $mutedOpacity; - font-size: 0.8em; - } + &__day { + &--sub { + opacity: $mutedOpacity; + font-size: 0.8em; } + } } /******************************************************** MOBILE */ body.phone { - .c-datetime-picker { - &.c-menu { - @include modalFullScreen(); - } - - &__close-button { - display: flex; - justify-content: flex-end; - } + .c-datetime-picker { + &.c-menu { + @include modalFullScreen(); } - .c-calendar { - grid-template-columns: repeat(7, auto); + &__close-button { + display: flex; + justify-content: flex-end; } + } + + .c-calendar { + grid-template-columns: repeat(7, auto); + } } diff --git a/src/plugins/timeConductor/independent/IndependentTimeConductor.vue b/src/plugins/timeConductor/independent/IndependentTimeConductor.vue index 7b5cde4927..19a363ac7a 100644 --- a/src/plugins/timeConductor/independent/IndependentTimeConductor.vue +++ b/src/plugins/timeConductor/independent/IndependentTimeConductor.vue @@ -20,237 +20,242 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/independent/Mode.vue b/src/plugins/timeConductor/independent/Mode.vue index b3fcc98aa2..94d439a623 100644 --- a/src/plugins/timeConductor/independent/Mode.vue +++ b/src/plugins/timeConductor/independent/Mode.vue @@ -20,206 +20,212 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/plugin.js b/src/plugins/timeConductor/plugin.js index 2eec7210e6..849eb18857 100644 --- a/src/plugins/timeConductor/plugin.js +++ b/src/plugins/timeConductor/plugin.js @@ -23,101 +23,110 @@ import Conductor from './Conductor.vue'; function isTruthy(a) { - return Boolean(a); + return Boolean(a); } function validateMenuOption(menuOption, index) { - if (menuOption.clock && !menuOption.clockOffsets) { - return `Conductor menu option is missing required property 'clockOffsets'. This field is required when configuring a menu option with a clock.\r\n${JSON.stringify(menuOption)}`; - } + if (menuOption.clock && !menuOption.clockOffsets) { + return `Conductor menu option is missing required property 'clockOffsets'. This field is required when configuring a menu option with a clock.\r\n${JSON.stringify( + menuOption + )}`; + } - if (!menuOption.timeSystem) { - return `Conductor menu option is missing required property 'timeSystem'\r\n${JSON.stringify(menuOption)}`; - } + if (!menuOption.timeSystem) { + return `Conductor menu option is missing required property 'timeSystem'\r\n${JSON.stringify( + menuOption + )}`; + } - if (!menuOption.bounds && !menuOption.clock) { - return `Conductor menu option is missing required property 'bounds'. This field is required when configuring a menu option with fixed bounds.\r\n${JSON.stringify(menuOption)}`; - } + if (!menuOption.bounds && !menuOption.clock) { + return `Conductor menu option is missing required property 'bounds'. This field is required when configuring a menu option with fixed bounds.\r\n${JSON.stringify( + menuOption + )}`; + } } function hasRequiredOptions(config) { - if (config === undefined - || config.menuOptions === undefined - || config.menuOptions.length === 0) { - return "You must specify one or more 'menuOptions'."; - } + if (config === undefined || config.menuOptions === undefined || config.menuOptions.length === 0) { + return "You must specify one or more 'menuOptions'."; + } - if (config.menuOptions.some(validateMenuOption)) { - return config.menuOptions.map(validateMenuOption) - .filter(isTruthy) - .join('\n'); - } + if (config.menuOptions.some(validateMenuOption)) { + return config.menuOptions.map(validateMenuOption).filter(isTruthy).join('\n'); + } - return undefined; + return undefined; } function validateConfiguration(config, openmct) { - const systems = openmct.time.getAllTimeSystems() - .reduce(function (m, ts) { - m[ts.key] = ts; + const systems = openmct.time.getAllTimeSystems().reduce(function (m, ts) { + m[ts.key] = ts; - return m; - }, {}); - const clocks = openmct.time.getAllClocks() - .reduce(function (m, c) { - m[c.key] = c; + return m; + }, {}); + const clocks = openmct.time.getAllClocks().reduce(function (m, c) { + m[c.key] = c; - return m; - }, {}); + return m; + }, {}); - return config.menuOptions.map(function (menuOption) { - let message = ''; - if (menuOption.timeSystem && !systems[menuOption.timeSystem]) { - message = `Time system '${menuOption.timeSystem}' has not been registered: \r\n ${JSON.stringify(menuOption)}`; - } + return config.menuOptions + .map(function (menuOption) { + let message = ''; + if (menuOption.timeSystem && !systems[menuOption.timeSystem]) { + message = `Time system '${ + menuOption.timeSystem + }' has not been registered: \r\n ${JSON.stringify(menuOption)}`; + } - if (menuOption.clock && !clocks[menuOption.clock]) { - message = `Clock '${menuOption.clock}' has not been registered: \r\n ${JSON.stringify(menuOption)}`; - } + if (menuOption.clock && !clocks[menuOption.clock]) { + message = `Clock '${menuOption.clock}' has not been registered: \r\n ${JSON.stringify( + menuOption + )}`; + } - return message; - }).filter(isTruthy).join('\n'); + return message; + }) + .filter(isTruthy) + .join('\n'); } function throwIfError(configResult) { - if (configResult) { - throw new Error(`Invalid Time Conductor Configuration. ${configResult} \r\n https://github.com/nasa/openmct/blob/master/API.md#the-time-conductor`); - } + if (configResult) { + throw new Error( + `Invalid Time Conductor Configuration. ${configResult} \r\n https://github.com/nasa/openmct/blob/master/API.md#the-time-conductor` + ); + } } function mountComponent(openmct, configuration) { - openmct.layout.conductorComponent = Object.create({ - components: { - Conductor - }, - template: "", - provide: { - openmct: openmct, - configuration: configuration - } - }); + openmct.layout.conductorComponent = Object.create({ + components: { + Conductor + }, + template: '', + provide: { + openmct: openmct, + configuration: configuration + } + }); } export default function (config) { - return function (openmct) { - let configResult = hasRequiredOptions(config) || validateConfiguration(config, openmct); - throwIfError(configResult); + return function (openmct) { + let configResult = hasRequiredOptions(config) || validateConfiguration(config, openmct); + throwIfError(configResult); - const defaults = config.menuOptions[0]; - if (defaults.clock) { - openmct.time.clock(defaults.clock, defaults.clockOffsets); - openmct.time.timeSystem(defaults.timeSystem, openmct.time.bounds()); - } else { - openmct.time.timeSystem(defaults.timeSystem, defaults.bounds); - } + const defaults = config.menuOptions[0]; + if (defaults.clock) { + openmct.time.clock(defaults.clock, defaults.clockOffsets); + openmct.time.timeSystem(defaults.timeSystem, openmct.time.bounds()); + } else { + openmct.time.timeSystem(defaults.timeSystem, defaults.bounds); + } - openmct.on('start', function () { - mountComponent(openmct, config); - }); - }; + openmct.on('start', function () { + mountComponent(openmct, config); + }); + }; } diff --git a/src/plugins/timeConductor/pluginSpec.js b/src/plugins/timeConductor/pluginSpec.js index 32b3fb0b62..b7753ac0c8 100644 --- a/src/plugins/timeConductor/pluginSpec.js +++ b/src/plugins/timeConductor/pluginSpec.js @@ -20,9 +20,9 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import {createMouseEvent, createOpenMct, resetApplicationState} from "utils/testing"; -import {millisecondsToDHMS, getPreciseDuration} from "../../utils/duration"; -import ConductorPlugin from "./plugin"; +import { createMouseEvent, createOpenMct, resetApplicationState } from 'utils/testing'; +import { millisecondsToDHMS, getPreciseDuration } from '../../utils/duration'; +import ConductorPlugin from './plugin'; import Vue from 'vue'; const THIRTY_SECONDS = 30 * 1000; @@ -33,132 +33,149 @@ const THIRTY_MINUTES = FIFTEEN_MINUTES * 2; const date = new Date(Date.UTC(78, 0, 20, 0, 0, 0)).getTime(); describe('time conductor', () => { - let element; - let child; - let appHolder; - let openmct; - let config = { - menuOptions: [ - { - name: "FixedTimeRange", - timeSystem: 'utc', - bounds: { - start: date - THIRTY_MINUTES, - end: date - }, - presets: [], - records: 2 - }, - { - name: "LocalClock", - timeSystem: 'utc', - clock: 'local', - clockOffsets: { - start: -THIRTY_MINUTES, - end: THIRTY_SECONDS - }, - presets: [] - } - ] - }; + let element; + let child; + let appHolder; + let openmct; + let config = { + menuOptions: [ + { + name: 'FixedTimeRange', + timeSystem: 'utc', + bounds: { + start: date - THIRTY_MINUTES, + end: date + }, + presets: [], + records: 2 + }, + { + name: 'LocalClock', + timeSystem: 'utc', + clock: 'local', + clockOffsets: { + start: -THIRTY_MINUTES, + end: THIRTY_SECONDS + }, + presets: [] + } + ] + }; + beforeEach((done) => { + openmct = createOpenMct(); + openmct.install(new ConductorPlugin(config)); + + 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', () => { + openmct.time.bounds({ + start: config.menuOptions[0].bounds.start, + end: config.menuOptions[0].bounds.end + }); + Vue.nextTick(() => { + done(); + }); + }); + appHolder = document.createElement('div'); + openmct.start(appHolder); + }); + + afterEach(() => { + appHolder = undefined; + openmct = undefined; + + return resetApplicationState(openmct); + }); + + describe('in fixed time mode', () => { + it('shows delta inputs', () => { + const fixedModeEl = appHolder.querySelector('.is-fixed-mode'); + const dateTimeInputs = fixedModeEl.querySelectorAll('.c-input--datetime'); + expect(dateTimeInputs[0].value).toEqual('1978-01-19 23:30:00.000Z'); + expect(dateTimeInputs[1].value).toEqual('1978-01-20 00:00:00.000Z'); + expect(fixedModeEl.querySelector('.c-mode-button .c-button__label').innerHTML).toEqual( + 'Fixed Timespan' + ); + }); + }); + + describe('in realtime mode', () => { beforeEach((done) => { - openmct = createOpenMct(); - openmct.install(new ConductorPlugin(config)); + const switcher = appHolder.querySelector('.c-mode-button'); + const clickEvent = createMouseEvent('click'); - 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', () => { - openmct.time.bounds({ - start: config.menuOptions[0].bounds.start, - end: config.menuOptions[0].bounds.end - }); - Vue.nextTick(() => { - done(); - }); + switcher.dispatchEvent(clickEvent); + Vue.nextTick(() => { + const clockItem = document.querySelectorAll('.c-conductor__mode-menu li')[1]; + clockItem.dispatchEvent(clickEvent); + Vue.nextTick(() => { + done(); }); - appHolder = document.createElement("div"); - openmct.start(appHolder); + }); }); - afterEach(() => { - appHolder = undefined; - openmct = undefined; + it('shows delta inputs', () => { + const realtimeModeEl = appHolder.querySelector('.is-realtime-mode'); + const dateTimeInputs = realtimeModeEl.querySelectorAll('.c-conductor__delta-button'); - return resetApplicationState(openmct); + expect(dateTimeInputs[0].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:30:00'); + expect(dateTimeInputs[1].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:00:30'); }); - describe('in fixed time mode', () => { - it('shows delta inputs', () => { - const fixedModeEl = appHolder.querySelector('.is-fixed-mode'); - const dateTimeInputs = fixedModeEl.querySelectorAll('.c-input--datetime'); - expect(dateTimeInputs[0].value).toEqual('1978-01-19 23:30:00.000Z'); - expect(dateTimeInputs[1].value).toEqual('1978-01-20 00:00:00.000Z'); - expect(fixedModeEl.querySelector('.c-mode-button .c-button__label').innerHTML).toEqual('Fixed Timespan'); - }); + it('shows clock options', () => { + const realtimeModeEl = appHolder.querySelector('.is-realtime-mode'); + + expect(realtimeModeEl.querySelector('.c-mode-button .c-button__label').innerHTML).toEqual( + 'Local Clock' + ); }); - describe('in realtime mode', () => { - beforeEach((done) => { - const switcher = appHolder.querySelector('.c-mode-button'); - const clickEvent = createMouseEvent("click"); + it('shows the current time', () => { + const realtimeModeEl = appHolder.querySelector('.is-realtime-mode'); + const currentTimeEl = realtimeModeEl.querySelector('.c-input--datetime'); + const currentTime = openmct.time.clock().currentValue(); + const { start, end } = openmct.time.bounds(); - switcher.dispatchEvent(clickEvent); - Vue.nextTick(() => { - const clockItem = document.querySelectorAll('.c-conductor__mode-menu li')[1]; - clockItem.dispatchEvent(clickEvent); - Vue.nextTick(() => { - done(); - }); - }); - }); - - it('shows delta inputs', () => { - const realtimeModeEl = appHolder.querySelector('.is-realtime-mode'); - const dateTimeInputs = realtimeModeEl.querySelectorAll('.c-conductor__delta-button'); - - expect(dateTimeInputs[0].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:30:00'); - expect(dateTimeInputs[1].innerHTML.replace(/[^(\d|:)]/g, '')).toEqual('00:00:30'); - }); - - it('shows clock options', () => { - const realtimeModeEl = appHolder.querySelector('.is-realtime-mode'); - - expect(realtimeModeEl.querySelector('.c-mode-button .c-button__label').innerHTML).toEqual('Local Clock'); - }); - - it('shows the current time', () => { - const realtimeModeEl = appHolder.querySelector('.is-realtime-mode'); - const currentTimeEl = realtimeModeEl.querySelector('.c-input--datetime'); - const currentTime = openmct.time.clock().currentValue(); - const { start, end } = openmct.time.bounds(); - - expect(currentTime).toBeGreaterThan(start); - expect(currentTime).toBeLessThanOrEqual(end); - expect(currentTimeEl.value.length).toBeGreaterThan(0); - }); + expect(currentTime).toBeGreaterThan(start); + expect(currentTime).toBeLessThanOrEqual(end); + expect(currentTimeEl.value.length).toBeGreaterThan(0); }); - + }); }); describe('duration functions', () => { - it('should transform milliseconds to DHMS', () => { - const functionResults = [millisecondsToDHMS(0), millisecondsToDHMS(86400000), - 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 transform milliseconds to DHMS', () => { + const functionResults = [ + millisecondsToDHMS(0), + millisecondsToDHMS(86400000), + 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), 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); - }); + it('should get precise duration', () => { + const functionResults = [ + getPreciseDuration(0), + getPreciseDuration(643680000), + 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/timeConductor/timePopup.vue b/src/plugins/timeConductor/timePopup.vue index ddc20334e3..9802d11a71 100644 --- a/src/plugins/timeConductor/timePopup.vue +++ b/src/plugins/timeConductor/timePopup.vue @@ -20,183 +20,179 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timeConductor/utcMultiTimeFormat.js b/src/plugins/timeConductor/utcMultiTimeFormat.js index 7765e376a1..8880f72241 100644 --- a/src/plugins/timeConductor/utcMultiTimeFormat.js +++ b/src/plugins/timeConductor/utcMultiTimeFormat.js @@ -23,44 +23,67 @@ import moment from 'moment'; export default function multiFormat(date) { - const momentified = moment.utc(date); - /** - * Uses logic from d3 Time-Scales, v3 of the API. See - * https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Scales.md - * - * Licensed - */ - const format = [ - [".SSS", function (m) { - return m.milliseconds(); - }], - [":ss", function (m) { - return m.seconds(); - }], - ["HH:mm", function (m) { - return m.minutes(); - }], - ["HH:mm", function (m) { - return m.hours(); - }], - ["ddd DD", function (m) { - return m.days() - && m.date() !== 1; - }], - ["MMM DD", function (m) { - return m.date() !== 1; - }], - ["MMMM", function (m) { - return m.month(); - }], - ["YYYY", function () { - return true; - }] - ].filter(function (row) { - return row[1](momentified); - })[0][0]; + const momentified = moment.utc(date); + /** + * Uses logic from d3 Time-Scales, v3 of the API. See + * https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Scales.md + * + * Licensed + */ + const format = [ + [ + '.SSS', + function (m) { + return m.milliseconds(); + } + ], + [ + ':ss', + function (m) { + return m.seconds(); + } + ], + [ + 'HH:mm', + function (m) { + return m.minutes(); + } + ], + [ + 'HH:mm', + function (m) { + return m.hours(); + } + ], + [ + 'ddd DD', + function (m) { + return m.days() && m.date() !== 1; + } + ], + [ + 'MMM DD', + function (m) { + return m.date() !== 1; + } + ], + [ + 'MMMM', + function (m) { + return m.month(); + } + ], + [ + 'YYYY', + function () { + return true; + } + ] + ].filter(function (row) { + return row[1](momentified); + })[0][0]; - if (format !== undefined) { - return moment.utc(date).format(format); - } + if (format !== undefined) { + return moment.utc(date).format(format); + } } diff --git a/src/plugins/timeline/TimelineCompositionPolicy.js b/src/plugins/timeline/TimelineCompositionPolicy.js index 4d2dc675e9..f8e20b9c2d 100644 --- a/src/plugins/timeline/TimelineCompositionPolicy.js +++ b/src/plugins/timeline/TimelineCompositionPolicy.js @@ -20,53 +20,51 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -const ALLOWED_TYPES = [ - 'telemetry.plot.overlay', - 'telemetry.plot.stacked', - 'plan', - 'gantt-chart' -]; -const DISALLOWED_TYPES = [ - 'telemetry.plot.bar-graph', - 'telemetry.plot.scatter-plot' -]; +const ALLOWED_TYPES = ['telemetry.plot.overlay', 'telemetry.plot.stacked', 'plan', 'gantt-chart']; +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 hasNumericTelemetry(domainObject, metadata) { + const hasTelemetry = openmct.telemetry.isTelemetryObject(domainObject); + if (!hasTelemetry || !metadata) { + return false; } - function hasDomainAndRange(metadata) { - return (metadata.valuesForHints(['range']).length > 0 - && metadata.valuesForHints(['domain']).length > 0); + 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; } - 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 metadata.valuesForHints(['image']).length > 0; + return false; + } + + return true; } - - 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 e291d2a73c..665d207a89 100644 --- a/src/plugins/timeline/TimelineObjectView.vue +++ b/src/plugins/timeline/TimelineObjectView.vue @@ -21,118 +21,124 @@ --> diff --git a/src/plugins/timeline/TimelineViewLayout.vue b/src/plugins/timeline/TimelineViewLayout.vue index ff754baf5b..32793c5da9 100644 --- a/src/plugins/timeline/TimelineViewLayout.vue +++ b/src/plugins/timeline/TimelineViewLayout.vue @@ -21,179 +21,176 @@ --> diff --git a/src/plugins/timeline/TimelineViewProvider.js b/src/plugins/timeline/TimelineViewProvider.js index 96d067930c..357a1c5fa3 100644 --- a/src/plugins/timeline/TimelineViewProvider.js +++ b/src/plugins/timeline/TimelineViewProvider.js @@ -24,43 +24,42 @@ import TimelineViewLayout from './TimelineViewLayout.vue'; import Vue from 'vue'; export default function TimelineViewProvider(openmct) { + return { + key: 'time-strip.view', + name: 'TimeStrip', + cssClass: 'icon-clock', + canView(domainObject) { + return domainObject.type === 'time-strip'; + }, - return { - key: 'time-strip.view', - name: 'TimeStrip', - cssClass: 'icon-clock', - canView(domainObject) { - return domainObject.type === 'time-strip'; + canEdit(domainObject) { + return domainObject.type === 'time-strip'; + }, + + view: function (domainObject, objectPath) { + let component; + + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + TimelineViewLayout + }, + provide: { + openmct, + domainObject, + composition: openmct.composition.get(domainObject), + objectPath + }, + template: '' + }); }, - - canEdit(domainObject) { - return domainObject.type === 'time-strip'; - }, - - view: function (domainObject, objectPath) { - let component; - - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - TimelineViewLayout - }, - provide: { - openmct, - domainObject, - composition: openmct.composition.get(domainObject), - objectPath - }, - template: '' - }); - }, - destroy: function () { - component.$destroy(); - component = undefined; - } - }; + destroy: function () { + component.$destroy(); + component = undefined; } - }; + }; + } + }; } diff --git a/src/plugins/timeline/plugin.js b/src/plugins/timeline/plugin.js index 7aa7aec47c..3839925bd9 100644 --- a/src/plugins/timeline/plugin.js +++ b/src/plugins/timeline/plugin.js @@ -21,28 +21,28 @@ *****************************************************************************/ import TimelineViewProvider from './TimelineViewProvider'; -import timelineInterceptor from "./timelineInterceptor"; -import TimelineCompositionPolicy from "./TimelineCompositionPolicy"; +import timelineInterceptor from './timelineInterceptor'; +import TimelineCompositionPolicy from './TimelineCompositionPolicy'; export default function () { - return function install(openmct) { - openmct.types.addType('time-strip', { - name: 'Time Strip', - key: 'time-strip', - description: 'Compose and display time-based telemetry and other object types in a timeline-like view.', - creatable: true, - cssClass: 'icon-timeline', - initialize: function (domainObject) { - domainObject.composition = []; - domainObject.configuration = { - useIndependentTime: false - }; - } - }); - timelineInterceptor(openmct); - openmct.composition.addPolicy(new TimelineCompositionPolicy(openmct).allow); + return function install(openmct) { + openmct.types.addType('time-strip', { + name: 'Time Strip', + key: 'time-strip', + description: + 'Compose and display time-based telemetry and other object types in a timeline-like view.', + creatable: true, + cssClass: 'icon-timeline', + initialize: function (domainObject) { + domainObject.composition = []; + domainObject.configuration = { + useIndependentTime: false + }; + } + }); + timelineInterceptor(openmct); + openmct.composition.addPolicy(new TimelineCompositionPolicy(openmct).allow); - openmct.objectViews.addProvider(new TimelineViewProvider(openmct)); - }; + openmct.objectViews.addProvider(new TimelineViewProvider(openmct)); + }; } - diff --git a/src/plugins/timeline/pluginSpec.js b/src/plugins/timeline/pluginSpec.js index 2aa0f8ac18..dc2181fd74 100644 --- a/src/plugins/timeline/pluginSpec.js +++ b/src/plugins/timeline/pluginSpec.js @@ -20,351 +20,369 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { createOpenMct, resetApplicationState } from "@/utils/testing"; -import TimelinePlugin from "./plugin"; +import { createOpenMct, resetApplicationState } from '@/utils/testing'; +import TimelinePlugin from './plugin'; import Vue from 'vue'; -import EventEmitter from "EventEmitter"; +import EventEmitter from 'EventEmitter'; describe('the plugin', function () { - let objectDef; - let appHolder; - let element; - let child; - let openmct; - let mockObjectPath; - let mockCompositionForTimelist; - let planObject = { + let objectDef; + let appHolder; + let element; + 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) => { + appHolder = document.createElement('div'); + appHolder.style.width = '640px'; + appHolder.style.height = '480px'; + + mockObjectPath = [ + { + name: 'mock folder', + type: 'fake-folder', 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" - } - ] - }) + key: 'mock-folder', + namespace: '' } + }, + { + name: 'mock parent folder', + type: 'time-strip', + identifier: { + key: 'mock-parent-folder', + namespace: '' + } + } + ]; + + const timeSystem = { + timeSystemKey: 'utc', + bounds: { + start: 1597160002854, + end: 1597181232854 + } }; - let timelineObject = { - "composition": [], - configuration: { - useIndependentTime: false, - timeOptions: { - mode: { - key: 'fixed' - }, - fixedOffsets: { - start: 10, - end: 11 - }, - clockOffsets: { - start: -(30 * 60 * 1000), - end: (30 * 60 * 1000) - } + + openmct = createOpenMct(timeSystem); + openmct.install(new TimelinePlugin()); + + objectDef = openmct.types.get('time-strip').definition; + + 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.start(appHolder); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + let mockObject = { + name: 'Time Strip', + key: 'time-strip', + creatable: true + }; + + it('defines a time-strip object type with the correct key', () => { + expect(objectDef.key).toEqual(mockObject.key); + }); + + describe('the time-strip object', () => { + it('is creatable', () => { + expect(objectDef.creatable).toEqual(mockObject.creatable); + }); + }); + + describe('the view', () => { + let timelineView; + let testViewObject; + + beforeEach(() => { + testViewObject = { + ...timelineObject + }; + + const applicableViews = openmct.objectViews.get(testViewObject, mockObjectPath); + timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view'); + let view = timelineView.view(testViewObject, mockObjectPath); + view.show(child, true); + + return Vue.nextTick(); + }); + + it('provides a view', () => { + expect(timelineView).toBeDefined(); + }); + + it('displays a time axis', () => { + const el = element.querySelector('.c-timesystem-axis'); + expect(el).toBeDefined(); + }); + + it('does not show the independent time conductor based on configuration', () => { + const independentTimeConductorEl = element.querySelector( + '.c-timeline-holder > .c-conductor__controls' + ); + expect(independentTimeConductorEl).toBeNull(); + }); + }); + + describe('the timeline composition', () => { + let timelineDomainObject; + let timelineView; + + beforeEach(() => { + timelineDomainObject = { + ...timelineObject, + composition: [ + { + identifier: { + key: 'test-plan-object', + namespace: '' } - }, - "name": "Some timestrip", - "type": "time-strip", - "location": "mine", - "modified": 1631005183584, - "persisted": 1631005183502, - "identifier": { - "namespace": "", - "key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9" - } + } + ] + }; + + 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 = { + ...timelineObject, + configuration: { + ...timelineObject.configuration, + useIndependentTime: true + } }; beforeEach((done) => { - appHolder = document.createElement('div'); - appHolder.style.width = '640px'; - appHolder.style.height = '480px'; + const applicableViews = openmct.objectViews.get(testViewObject, mockObjectPath); + timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view'); + let view = timelineView.view(testViewObject, mockObjectPath); + view.show(child, true); - 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 timeSystem = { - timeSystemKey: 'utc', - bounds: { - start: 1597160002854, - end: 1597181232854 - } - }; - - openmct = createOpenMct(timeSystem); - openmct.install(new TimelinePlugin()); - - objectDef = openmct.types.get('time-strip').definition; - - 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.start(appHolder); + Vue.nextTick(done); }); - afterEach(() => { - return resetApplicationState(openmct); - }); + it('displays an independent time conductor with saved options - local clock', () => { + return Vue.nextTick(() => { + const independentTimeConductorEl = element.querySelector( + '.c-timeline-holder > .c-conductor__controls' + ); + expect(independentTimeConductorEl).toBeDefined(); - let mockObject = { - name: 'Time Strip', - key: 'time-strip', - creatable: true + const independentTimeContext = openmct.time.getIndependentContext( + testViewObject.identifier.key + ); + expect(independentTimeContext.clockOffsets()).toEqual( + testViewObject.configuration.timeOptions.clockOffsets + ); + }); + }); + }); + + describe('the independent time conductor - fixed', () => { + let timelineView; + let testViewObject2 = { + ...timelineObject, + id: 'test-object2', + identifier: { + key: 'test-object2', + namespace: '' + }, + configuration: { + ...timelineObject.configuration, + useIndependentTime: true + } }; - it('defines a time-strip object type with the correct key', () => { - expect(objectDef.key).toEqual(mockObject.key); + beforeEach((done) => { + const applicableViews = openmct.objectViews.get(testViewObject2, mockObjectPath); + timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view'); + let view = timelineView.view(testViewObject2, mockObjectPath); + view.show(child, true); + + Vue.nextTick(done); }); - describe('the time-strip object', () => { - it('is creatable', () => { - expect(objectDef.creatable).toEqual(mockObject.creatable); - }); + it('displays an independent time conductor with saved options - fixed timespan', () => { + return Vue.nextTick(() => { + const independentTimeConductorEl = element.querySelector( + '.c-timeline-holder > .c-conductor__controls' + ); + expect(independentTimeConductorEl).toBeDefined(); + + const independentTimeContext = openmct.time.getIndependentContext( + testViewObject2.identifier.key + ); + expect(independentTimeContext.bounds()).toEqual( + testViewObject2.configuration.timeOptions.fixedOffsets + ); + }); + }); + }); + + describe('The timestrip composition policy', () => { + let testObject; + beforeEach(() => { + testObject = { + ...timelineObject, + composition: [] + }; }); - describe('the view', () => { - let timelineView; - let testViewObject; - - beforeEach(() => { - testViewObject = { - ...timelineObject - }; - - const applicableViews = openmct.objectViews.get(testViewObject, mockObjectPath); - timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view'); - let view = timelineView.view(testViewObject, mockObjectPath); - view.show(child, true); - - return Vue.nextTick(); - }); - - it('provides a view', () => { - expect(timelineView).toBeDefined(); - }); - - it('displays a time axis', () => { - const el = element.querySelector('.c-timesystem-axis'); - expect(el).toBeDefined(); - }); - - it('does not show the independent time conductor based on configuration', () => { - const independentTimeConductorEl = element.querySelector('.c-timeline-holder > .c-conductor__controls'); - expect(independentTimeConductorEl).toBeNull(); - }); - }); - - 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 = { - ...timelineObject, - configuration: { - ...timelineObject.configuration, - useIndependentTime: true - } - }; - - beforeEach(done => { - const applicableViews = openmct.objectViews.get(testViewObject, mockObjectPath); - timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view'); - let view = timelineView.view(testViewObject, mockObjectPath); - view.show(child, true); - - Vue.nextTick(done); - }); - - it('displays an independent time conductor with saved options - local clock', () => { - - return Vue.nextTick(() => { - const independentTimeConductorEl = element.querySelector('.c-timeline-holder > .c-conductor__controls'); - expect(independentTimeConductorEl).toBeDefined(); - - const independentTimeContext = openmct.time.getIndependentContext(testViewObject.identifier.key); - expect(independentTimeContext.clockOffsets()).toEqual(testViewObject.configuration.timeOptions.clockOffsets); - }); - }); - }); - - describe('the independent time conductor - fixed', () => { - let timelineView; - let testViewObject2 = { - ...timelineObject, - id: "test-object2", - identifier: { - key: "test-object2", - namespace: '' + 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 + } }, - configuration: { - ...timelineObject.configuration, - useIndependentTime: true + { + key: 'some-other-key', + name: 'Another attribute', + hints: { + range: 1 + } } - }; - - beforeEach((done) => { - const applicableViews = openmct.objectViews.get(testViewObject2, mockObjectPath); - timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view'); - let view = timelineView.view(testViewObject2, mockObjectPath); - view.show(child, true); - - Vue.nextTick(done); - }); - - it('displays an independent time conductor with saved options - fixed timespan', () => { - return Vue.nextTick(() => { - const independentTimeConductorEl = element.querySelector('.c-timeline-holder > .c-conductor__controls'); - expect(independentTimeConductorEl).toBeDefined(); - - const independentTimeContext = openmct.time.getIndependentContext(testViewObject2.identifier.key); - expect(independentTimeContext.bounds()).toEqual(testViewObject2.configuration.timeOptions.fixedOffsets); - }); - }); + ] + } + }; + const composition = openmct.composition.get(testObject); + expect(() => { + composition.add(testTelemetryObject); + }).not.toThrow(); + expect(testObject.composition.length).toBe(1); }); - 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); - }); + 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/timeline/timeline.scss b/src/plugins/timeline/timeline.scss index 537fdc3384..bd5e3f6d5a 100644 --- a/src/plugins/timeline/timeline.scss +++ b/src/plugins/timeline/timeline.scss @@ -1,12 +1,12 @@ .c-timeline-holder { - overflow: hidden; + overflow: hidden; } .c-plan.c-timeline-holder { - overflow-x: hidden; - overflow-y: auto; + overflow-x: hidden; + overflow-y: auto; } .c-timeline__objects { - display: contents; + display: contents; } diff --git a/src/plugins/timeline/timelineInterceptor.js b/src/plugins/timeline/timelineInterceptor.js index 3bb75f2140..631b1ade94 100644 --- a/src/plugins/timeline/timelineInterceptor.js +++ b/src/plugins/timeline/timelineInterceptor.js @@ -21,20 +21,18 @@ *****************************************************************************/ export default function timelineInterceptor(openmct) { + openmct.objects.addGetInterceptor({ + appliesTo: (identifier, domainObject) => { + return domainObject && domainObject.type === 'time-strip'; + }, + invoke: (identifier, object) => { + if (object && object.configuration === undefined) { + object.configuration = { + useIndependentTime: true + }; + } - openmct.objects.addGetInterceptor({ - appliesTo: (identifier, domainObject) => { - return domainObject && domainObject.type === 'time-strip'; - }, - invoke: (identifier, object) => { - - if (object && object.configuration === undefined) { - object.configuration = { - useIndependentTime: true - }; - } - - return object; - } - }); + return object; + } + }); } diff --git a/src/plugins/timelist/Timelist.vue b/src/plugins/timelist/Timelist.vue index 825c04e57a..54c392571c 100644 --- a/src/plugins/timelist/Timelist.vue +++ b/src/plugins/timelist/Timelist.vue @@ -21,475 +21,499 @@ --> diff --git a/src/plugins/timelist/TimelistCompositionPolicy.js b/src/plugins/timelist/TimelistCompositionPolicy.js index e3695c85ce..89dccfbaa4 100644 --- a/src/plugins/timelist/TimelistCompositionPolicy.js +++ b/src/plugins/timelist/TimelistCompositionPolicy.js @@ -19,16 +19,16 @@ * 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"; +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 { + allow: function (parent, child) { + if (parent.type === TIMELIST_TYPE && child.type !== 'plan') { + return false; + } - return true; - } - }; + return true; + } + }; } diff --git a/src/plugins/timelist/TimelistViewProvider.js b/src/plugins/timelist/TimelistViewProvider.js index 03d38d043e..65c1d54725 100644 --- a/src/plugins/timelist/TimelistViewProvider.js +++ b/src/plugins/timelist/TimelistViewProvider.js @@ -25,44 +25,42 @@ import { TIMELIST_TYPE } from './constants'; import Vue from 'vue'; export default function TimelistViewProvider(openmct) { + return { + key: 'timelist.view', + name: 'Time List', + cssClass: 'icon-timelist', + canView(domainObject) { + return domainObject.type === TIMELIST_TYPE; + }, - return { - key: 'timelist.view', - name: 'Time List', - cssClass: 'icon-timelist', - canView(domainObject) { - return domainObject.type === TIMELIST_TYPE; + canEdit(domainObject) { + return domainObject.type === TIMELIST_TYPE; + }, + + view: function (domainObject, objectPath) { + let component; + + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + Timelist + }, + provide: { + openmct, + domainObject, + path: objectPath, + composition: openmct.composition.get(domainObject) + }, + template: '' + }); }, - - canEdit(domainObject) { - return domainObject.type === TIMELIST_TYPE; - }, - - view: function (domainObject, objectPath) { - let component; - - return { - show: function (element) { - - component = new Vue({ - el: element, - components: { - Timelist - }, - provide: { - openmct, - domainObject, - path: objectPath, - composition: openmct.composition.get(domainObject) - }, - template: '' - }); - }, - destroy: function () { - component.$destroy(); - component = undefined; - } - }; + destroy: function () { + component.$destroy(); + component = undefined; } - }; + }; + } + }; } diff --git a/src/plugins/timelist/constants.js b/src/plugins/timelist/constants.js index 7d9c978c06..0849e34bca 100644 --- a/src/plugins/timelist/constants.js +++ b/src/plugins/timelist/constants.js @@ -1,24 +1,24 @@ export const SORT_ORDER_OPTIONS = [ - { - label: 'Start ascending', - property: 'start', - direction: 'ASC' - }, - { - label: 'Start descending', - property: 'start', - direction: 'DESC' - }, - { - label: 'End ascending', - property: 'end', - direction: 'ASC' - }, - { - label: 'End descending', - property: 'end', - direction: 'DESC' - } + { + label: 'Start ascending', + property: 'start', + direction: 'ASC' + }, + { + label: 'Start descending', + property: 'start', + direction: 'DESC' + }, + { + label: 'End ascending', + property: 'end', + direction: 'ASC' + }, + { + label: 'End descending', + property: 'end', + direction: 'DESC' + } ]; export const TIMELIST_TYPE = 'timelist'; diff --git a/src/plugins/timelist/inspector/EventProperties.vue b/src/plugins/timelist/inspector/EventProperties.vue index 691fc6bed5..1a79b67052 100644 --- a/src/plugins/timelist/inspector/EventProperties.vue +++ b/src/plugins/timelist/inspector/EventProperties.vue @@ -20,126 +20,106 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timelist/inspector/Filtering.vue b/src/plugins/timelist/inspector/Filtering.vue index ad5a3d77c5..f302fe59b0 100644 --- a/src/plugins/timelist/inspector/Filtering.vue +++ b/src/plugins/timelist/inspector/Filtering.vue @@ -20,93 +20,82 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/timelist/inspector/TimeListInspectorViewProvider.js b/src/plugins/timelist/inspector/TimeListInspectorViewProvider.js index e79eb239c0..3b9c0e0517 100644 --- a/src/plugins/timelist/inspector/TimeListInspectorViewProvider.js +++ b/src/plugins/timelist/inspector/TimeListInspectorViewProvider.js @@ -20,51 +20,50 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import TimelistPropertiesView from "./TimelistPropertiesView.vue"; +import TimelistPropertiesView from './TimelistPropertiesView.vue'; import { TIMELIST_TYPE } from '../constants'; import Vue from 'vue'; export default function TimeListInspectorViewProvider(openmct) { - return { - key: 'timelist-inspector', - name: 'Timelist Inspector View', - canView: function (selection) { - if (selection.length === 0 || selection[0].length === 0) { - return false; - } + return { + key: 'timelist-inspector', + name: 'Timelist Inspector View', + canView: function (selection) { + if (selection.length === 0 || selection[0].length === 0) { + return false; + } - let context = selection[0][0].context; + let context = selection[0][0].context; - return context && context.item - && context.item.type === TIMELIST_TYPE; + return context && context.item && context.item.type === TIMELIST_TYPE; + }, + view: function (selection) { + let component; + + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + TimelistPropertiesView: TimelistPropertiesView + }, + provide: { + openmct, + domainObject: selection[0][0].context.item + }, + template: '' + }); }, - view: function (selection) { - let component; - - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - TimelistPropertiesView: TimelistPropertiesView - }, - provide: { - openmct, - domainObject: selection[0][0].context.item - }, - template: '' - }); - }, - priority: function () { - return openmct.priority.HIGH + 1; - }, - destroy: function () { - if (component) { - component.$destroy(); - component = undefined; - } - } - }; + priority: function () { + return openmct.priority.HIGH + 1; + }, + destroy: function () { + if (component) { + component.$destroy(); + component = undefined; + } } - }; + }; + } + }; } diff --git a/src/plugins/timelist/inspector/TimelistPropertiesView.vue b/src/plugins/timelist/inspector/TimelistPropertiesView.vue index 88a2e41ed8..8d7e01860c 100644 --- a/src/plugins/timelist/inspector/TimelistPropertiesView.vue +++ b/src/plugins/timelist/inspector/TimelistPropertiesView.vue @@ -21,126 +21,111 @@ --> diff --git a/src/plugins/timelist/plugin.js b/src/plugins/timelist/plugin.js index 3dc59642c0..9de9becb2b 100644 --- a/src/plugins/timelist/plugin.js +++ b/src/plugins/timelist/plugin.js @@ -22,37 +22,37 @@ import TimelistViewProvider from './TimelistViewProvider'; import { TIMELIST_TYPE } from './constants'; -import TimeListInspectorViewProvider from "./inspector/TimeListInspectorViewProvider"; -import TimelistCompositionPolicy from "@/plugins/timelist/TimelistCompositionPolicy"; +import TimeListInspectorViewProvider from './inspector/TimeListInspectorViewProvider'; +import TimelistCompositionPolicy from '@/plugins/timelist/TimelistCompositionPolicy'; export default function () { - return function install(openmct) { - openmct.types.addType(TIMELIST_TYPE, { - name: 'Time List', - key: TIMELIST_TYPE, - description: 'A configurable, time-ordered list view of activities for a compatible mission plan file.', - creatable: true, - cssClass: 'icon-timelist', - initialize: function (domainObject) { - domainObject.configuration = { - sortOrderIndex: 0, - futureEventsIndex: 1, - futureEventsDurationIndex: 0, - futureEventsDuration: 20, - currentEventsIndex: 1, - currentEventsDurationIndex: 0, - currentEventsDuration: 20, - 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); - - }; + return function install(openmct) { + openmct.types.addType(TIMELIST_TYPE, { + name: 'Time List', + key: TIMELIST_TYPE, + description: + 'A configurable, time-ordered list view of activities for a compatible mission plan file.', + creatable: true, + cssClass: 'icon-timelist', + initialize: function (domainObject) { + domainObject.configuration = { + sortOrderIndex: 0, + futureEventsIndex: 1, + futureEventsDurationIndex: 0, + futureEventsDuration: 20, + currentEventsIndex: 1, + currentEventsDurationIndex: 0, + currentEventsDuration: 20, + 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 481be82f52..c4633e32f7 100644 --- a/src/plugins/timelist/pluginSpec.js +++ b/src/plugins/timelist/pluginSpec.js @@ -20,364 +20,378 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import {createOpenMct, resetApplicationState} from "utils/testing"; -import TimelistPlugin from "./plugin"; -import { TIMELIST_TYPE } from "./constants"; +import { createOpenMct, resetApplicationState } from 'utils/testing'; +import TimelistPlugin from './plugin'; +import { TIMELIST_TYPE } from './constants'; import Vue from 'vue'; -import moment from "moment"; -import EventEmitter from "EventEmitter"; +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'; const LIST_ITEM_BODY_CLASS = '.js-table__body th'; describe('the plugin', function () { - let timelistDefinition; - let element; - let child; - 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 = { + let timelistDefinition; + let element; + let child; + 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'); + appHolder.style.width = '640px'; + appHolder.style.height = '480px'; + + openmct = createOpenMct(); + openmct.install(new TimelistPlugin()); + + timelistDefinition = openmct.types.get(TIMELIST_TYPE).definition; + + 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); + + 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); + }); + + afterEach(() => { + openmct.router.path = originalRouterPath; + + return resetApplicationState(openmct); + }); + + let mockTimelistObject = { + name: 'Timelist', + key: TIMELIST_TYPE, + creatable: true + }; + + it('defines a timelist object type with the correct key', () => { + expect(timelistDefinition.key).toEqual(mockTimelistObject.key); + }); + + it('is creatable', () => { + expect(timelistDefinition.creatable).toEqual(mockTimelistObject.creatable); + }); + + describe('the timelist view', () => { + it('provides a timelist view', () => { + const testViewObject = { + id: 'test-object', + type: TIMELIST_TYPE + }; + openmct.router.path = [testViewObject]; + + const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]); + let timelistView = applicableViews.find( + (viewProvider) => viewProvider.key === 'timelist.view' + ); + expect(timelistView).toBeDefined(); + }); + }); + + describe('the timelist view displays activities', () => { + let timelistDomainObject; + let timelistView; + + beforeEach(() => { + timelistDomainObject = { identifier: { - key: 'test-plan-object', - namespace: '' + 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: '' }, - 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" - } - ] - }) + 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'); - appHolder.style.width = '640px'; - appHolder.style.height = '480px'; + openmct.router.path = [timelistDomainObject]; - openmct = createOpenMct(); - openmct.install(new TimelistPlugin()); + const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]); + timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view'); + let view = timelistView.view(timelistDomainObject, []); + view.show(child, true); - timelistDefinition = openmct.types.get(TIMELIST_TYPE).definition; - - 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); - - 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); + return Vue.nextTick(); }); - afterEach(() => { - openmct.router.path = originalRouterPath; - - return resetApplicationState(openmct); + it('displays the activities', () => { + const items = element.querySelectorAll(LIST_ITEM_CLASS); + expect(items.length).toEqual(2); }); - let mockTimelistObject = { - name: 'Timelist', - key: TIMELIST_TYPE, - creatable: true - }; - - it('defines a timelist object type with the correct key', () => { - expect(timelistDefinition.key).toEqual(mockTimelistObject.key); + it('displays the activity headers', () => { + const headers = element.querySelectorAll(LIST_ITEM_BODY_CLASS); + expect(headers.length).toEqual(4); }); - it('is creatable', () => { - expect(timelistDefinition.creatable).toEqual(mockTimelistObject.creatable); + it('displays activity details', (done) => { + Vue.nextTick(() => { + const itemEls = element.querySelectorAll(LIST_ITEM_CLASS); + 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(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(); }); - describe('the timelist view', () => { - it('provides a timelist view', () => { - const testViewObject = { - id: "test-object", - type: TIMELIST_TYPE - }; - openmct.router.path = [testViewObject]; + it('loads the plan from composition', () => { + mockComposition.emit('add', planObject); - const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]); - let timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view'); - expect(timelistView).toBeDefined(); - }); + 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(); }); - describe('the timelist view displays activities', () => { - let timelistDomainObject; - let timelistView; + it('activities', () => { + mockComposition.emit('add', planObject); - 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: '' - }, - 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" - } - ] - }) - } - }; + return Vue.nextTick(() => { + const items = element.querySelectorAll(LIST_ITEM_CLASS); + expect(items.length).toEqual(1); + }); + }); + }); - openmct.router.path = [timelistDomainObject]; + describe('time filtering - past', () => { + let timelistDomainObject; + let timelistView; - const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]); - timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view'); - let view = timelistView.view(timelistDomainObject, []); - view.show(child, true); + 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: '' + } + } + ] + }; - return Vue.nextTick(); - }); + openmct.router.path = [timelistDomainObject]; - it('displays the activities', () => { - const items = element.querySelectorAll(LIST_ITEM_CLASS); - expect(items.length).toEqual(2); - }); + 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); - it('displays the activity headers', () => { - const headers = element.querySelectorAll(LIST_ITEM_BODY_CLASS); - expect(headers.length).toEqual(4); - }); - - it('displays activity details', (done) => { - Vue.nextTick(() => { - const itemEls = element.querySelectorAll(LIST_ITEM_CLASS); - 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(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(); - }); - }); + return Vue.nextTick(); }); - describe('the timelist composition', () => { - let timelistDomainObject; - let timelistView; + it('hides past events', () => { + mockComposition.emit('add', planObject); - 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); - }); - }); + 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 652e105cfe..af66e0509b 100644 --- a/src/plugins/timelist/timelist.scss +++ b/src/plugins/timelist/timelist.scss @@ -57,5 +57,4 @@ } } } - } diff --git a/src/plugins/timer/TimerViewProvider.js b/src/plugins/timer/TimerViewProvider.js index 608441056a..8d54b7baf1 100644 --- a/src/plugins/timer/TimerViewProvider.js +++ b/src/plugins/timer/TimerViewProvider.js @@ -24,42 +24,42 @@ import Timer from './components/Timer.vue'; import Vue from 'vue'; export default function TimerViewProvider(openmct) { - return { - key: 'timer.view', - name: 'Timer', - cssClass: 'icon-timer', - canView(domainObject) { - return domainObject.type === 'timer'; + return { + key: 'timer.view', + name: 'Timer', + cssClass: 'icon-timer', + canView(domainObject) { + return domainObject.type === 'timer'; + }, + + view: function (domainObject, objectPath) { + let component; + + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + Timer + }, + provide: { + openmct, + objectPath, + currentView: this + }, + data() { + return { + domainObject + }; + }, + template: '' + }); }, - - view: function (domainObject, objectPath) { - let component; - - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - Timer - }, - provide: { - openmct, - objectPath, - currentView: this - }, - data() { - return { - domainObject - }; - }, - template: '' - }); - }, - destroy: function () { - component.$destroy(); - component = undefined; - } - }; + destroy: function () { + component.$destroy(); + component = undefined; } - }; + }; + } + }; } diff --git a/src/plugins/timer/actions/PauseTimerAction.js b/src/plugins/timer/actions/PauseTimerAction.js index 7839b13f66..843fcde21a 100644 --- a/src/plugins/timer/actions/PauseTimerAction.js +++ b/src/plugins/timer/actions/PauseTimerAction.js @@ -21,41 +21,41 @@ *****************************************************************************/ export default class PauseTimerAction { - constructor(openmct) { - this.name = 'Pause'; - this.key = 'timer.pause'; - this.description = 'Pause the currently displayed timer'; - this.group = 'view'; - this.cssClass = 'icon-pause'; - this.priority = 3; + constructor(openmct) { + this.name = 'Pause'; + this.key = 'timer.pause'; + this.description = 'Pause the currently displayed timer'; + this.group = 'view'; + this.cssClass = 'icon-pause'; + this.priority = 3; - this.openmct = openmct; + this.openmct = openmct; + } + invoke(objectPath) { + const domainObject = objectPath[0]; + if (!domainObject || !domainObject.configuration) { + return new Error('Unable to run pause timer action. No domainObject provided.'); } - invoke(objectPath) { - const domainObject = objectPath[0]; - if (!domainObject || !domainObject.configuration) { - return new Error('Unable to run pause timer action. No domainObject provided.'); - } - const newConfiguration = { ...domainObject.configuration }; - newConfiguration.timerState = 'paused'; - newConfiguration.pausedTime = new Date(); + const newConfiguration = { ...domainObject.configuration }; + newConfiguration.timerState = 'paused'; + newConfiguration.pausedTime = new Date(); - this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration); + this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration); + } + appliesTo(objectPath, view = {}) { + const domainObject = objectPath[0]; + if (!domainObject || !domainObject.configuration) { + return; } - 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; + // 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 viewKey - ? domainObject.type === 'timer' - : 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 dcfe95a508..8c05003401 100644 --- a/src/plugins/timer/actions/RestartTimerAction.js +++ b/src/plugins/timer/actions/RestartTimerAction.js @@ -21,42 +21,42 @@ *****************************************************************************/ export default class RestartTimerAction { - constructor(openmct) { - this.name = 'Restart at 0'; - this.key = 'timer.restart'; - this.description = 'Restart the currently displayed timer'; - this.group = 'view'; - this.cssClass = 'icon-refresh'; - this.priority = 2; + constructor(openmct) { + this.name = 'Restart at 0'; + this.key = 'timer.restart'; + this.description = 'Restart the currently displayed timer'; + this.group = 'view'; + this.cssClass = 'icon-refresh'; + this.priority = 2; - this.openmct = openmct; + this.openmct = openmct; + } + invoke(objectPath) { + const domainObject = objectPath[0]; + if (!domainObject || !domainObject.configuration) { + return new Error('Unable to run restart timer action. No domainObject provided.'); } - invoke(objectPath) { - const domainObject = objectPath[0]; - if (!domainObject || !domainObject.configuration) { - return new Error('Unable to run restart timer action. No domainObject provided.'); - } - const newConfiguration = { ...domainObject.configuration }; - newConfiguration.timerState = 'started'; - newConfiguration.timestamp = new Date(); - newConfiguration.pausedTime = undefined; + const newConfiguration = { ...domainObject.configuration }; + newConfiguration.timerState = 'started'; + newConfiguration.timestamp = new Date(); + newConfiguration.pausedTime = undefined; - this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration); + this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration); + } + appliesTo(objectPath, view = {}) { + const domainObject = objectPath[0]; + if (!domainObject || !domainObject.configuration) { + return; } - 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; + // 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 viewKey - ? domainObject.type === 'timer' - : 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 6d520d0cc1..f200773952 100644 --- a/src/plugins/timer/actions/StartTimerAction.js +++ b/src/plugins/timer/actions/StartTimerAction.js @@ -23,59 +23,59 @@ import moment from 'moment'; export default class StartTimerAction { - constructor(openmct) { - this.name = 'Start'; - this.key = 'timer.start'; - this.description = 'Start the currently displayed timer'; - this.group = 'view'; - this.cssClass = 'icon-play'; - this.priority = 3; + constructor(openmct) { + this.name = 'Start'; + this.key = 'timer.start'; + this.description = 'Start the currently displayed timer'; + this.group = 'view'; + this.cssClass = 'icon-play'; + this.priority = 3; - this.openmct = openmct; + this.openmct = openmct; + } + invoke(objectPath) { + const domainObject = objectPath[0]; + if (!domainObject || !domainObject.configuration) { + return new Error('Unable to run start timer action. No domainObject provided.'); } - invoke(objectPath) { - const domainObject = objectPath[0]; - if (!domainObject || !domainObject.configuration) { - return new Error('Unable to run start timer action. No domainObject provided.'); - } - let { pausedTime, timestamp } = domainObject.configuration; - const newConfiguration = { ...domainObject.configuration }; + let { pausedTime, timestamp } = domainObject.configuration; + const newConfiguration = { ...domainObject.configuration }; - if (pausedTime) { - pausedTime = moment(pausedTime); - } - - if (timestamp) { - timestamp = moment(timestamp); - } - - const now = moment(new Date()); - if (pausedTime) { - const timeShift = moment.duration(now.diff(pausedTime)); - const shiftedTime = timestamp.add(timeShift); - newConfiguration.timestamp = shiftedTime.toDate(); - } else if (!timestamp) { - newConfiguration.timestamp = now.toDate(); - } - - newConfiguration.timerState = 'started'; - newConfiguration.pausedTime = undefined; - this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration); + if (pausedTime) { + pausedTime = moment(pausedTime); } - 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 viewKey - ? domainObject.type === 'timer' - : domainObject.type === 'timer' && timerState !== 'started'; + if (timestamp) { + timestamp = moment(timestamp); } + + const now = moment(new Date()); + if (pausedTime) { + const timeShift = moment.duration(now.diff(pausedTime)); + const shiftedTime = timestamp.add(timeShift); + newConfiguration.timestamp = shiftedTime.toDate(); + } else if (!timestamp) { + newConfiguration.timestamp = now.toDate(); + } + + newConfiguration.timerState = 'started'; + newConfiguration.pausedTime = undefined; + this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration); + } + 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 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 ca29cc6125..7514bbb618 100644 --- a/src/plugins/timer/actions/StopTimerAction.js +++ b/src/plugins/timer/actions/StopTimerAction.js @@ -21,42 +21,42 @@ *****************************************************************************/ export default class StopTimerAction { - constructor(openmct) { - this.name = 'Stop'; - this.key = 'timer.stop'; - this.description = 'Stop the currently displayed timer'; - this.group = 'view'; - this.cssClass = 'icon-box-round-corners'; - this.priority = 1; + constructor(openmct) { + this.name = 'Stop'; + this.key = 'timer.stop'; + this.description = 'Stop the currently displayed timer'; + this.group = 'view'; + this.cssClass = 'icon-box-round-corners'; + this.priority = 1; - this.openmct = openmct; + this.openmct = openmct; + } + invoke(objectPath) { + const domainObject = objectPath[0]; + if (!domainObject || !domainObject.configuration) { + return new Error('Unable to run stop timer action. No domainObject provided.'); } - invoke(objectPath) { - const domainObject = objectPath[0]; - if (!domainObject || !domainObject.configuration) { - return new Error('Unable to run stop timer action. No domainObject provided.'); - } - const newConfiguration = { ...domainObject.configuration }; - newConfiguration.timerState = 'stopped'; - newConfiguration.timestamp = undefined; - newConfiguration.pausedTime = undefined; + const newConfiguration = { ...domainObject.configuration }; + newConfiguration.timerState = 'stopped'; + newConfiguration.timestamp = undefined; + newConfiguration.pausedTime = undefined; - this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration); + this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration); + } + appliesTo(objectPath, view = {}) { + const domainObject = objectPath[0]; + if (!domainObject || !domainObject.configuration) { + return; } - 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; + // 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 viewKey - ? domainObject.type === 'timer' - : 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 09a13f7d84..65636feb3a 100644 --- a/src/plugins/timer/components/Timer.vue +++ b/src/plugins/timer/components/Timer.vue @@ -21,242 +21,239 @@ --> diff --git a/src/plugins/timer/plugin.js b/src/plugins/timer/plugin.js index b0079c07b0..9081d6653e 100644 --- a/src/plugins/timer/plugin.js +++ b/src/plugins/timer/plugin.js @@ -1,4 +1,3 @@ - /***************************************************************************** * Open MCT, Copyright (c) 2014-2023, United States Government * as represented by the Administrator of the National Aeronautics and Space @@ -29,91 +28,85 @@ import StartTimerAction from './actions/StartTimerAction'; import StopTimerAction from './actions/StopTimerAction'; export default function TimerPlugin() { - return function install(openmct) { - openmct.types.addType('timer', { - name: 'Timer', - description: 'A timer that counts up or down to a datetime. Timers can be started, stopped and reset whenever needed, and support a variety of display formats. Each Timer displays the same value to all users. Timers can be added to Display Layouts.', - creatable: true, - cssClass: 'icon-timer', - initialize: function (domainObject) { - domainObject.configuration = { - timerFormat: 'long', - timestamp: undefined, - timezone: 'UTC', - timerState: undefined, - pausedTime: undefined - }; + return function install(openmct) { + openmct.types.addType('timer', { + name: 'Timer', + description: + 'A timer that counts up or down to a datetime. Timers can be started, stopped and reset whenever needed, and support a variety of display formats. Each Timer displays the same value to all users. Timers can be added to Display Layouts.', + creatable: true, + cssClass: 'icon-timer', + initialize: function (domainObject) { + domainObject.configuration = { + timerFormat: 'long', + timestamp: undefined, + timezone: 'UTC', + timerState: undefined, + pausedTime: undefined + }; + }, + form: [ + { + key: 'timestamp', + control: 'datetime', + name: 'Target', + property: ['configuration', 'timestamp'] + }, + { + key: 'timerFormat', + name: 'Display Format', + control: 'select', + options: [ + { + value: 'long', + name: 'DDD hh:mm:ss' }, - "form": [ - { - "key": "timestamp", - "control": "datetime", - "name": "Target", - property: [ - 'configuration', - 'timestamp' - ] - }, - { - "key": "timerFormat", - "name": "Display Format", - "control": "select", - "options": [ - { - "value": "long", - "name": "DDD hh:mm:ss" - }, - { - "value": "short", - "name": "hh:mm:ss" - } - ], - property: [ - 'configuration', - 'timerFormat' - ] - } - ] - }); - openmct.objectViews.addProvider(new TimerViewProvider(openmct)); - - openmct.actions.register(new PauseTimerAction(openmct)); - openmct.actions.register(new RestartTimerAction(openmct)); - openmct.actions.register(new StartTimerAction(openmct)); - openmct.actions.register(new StopTimerAction(openmct)); - - openmct.objects.addGetInterceptor({ - appliesTo: (identifier, domainObject) => { - return domainObject && domainObject.type === 'timer'; - }, - invoke: (identifier, domainObject) => { - if (domainObject.configuration) { - return domainObject; - } - - const configuration = {}; - - if (domainObject.timerFormat) { - configuration.timerFormat = domainObject.timerFormat; - } - - if (domainObject.timestamp) { - configuration.timestamp = domainObject.timestamp; - } - - if (domainObject.timerState) { - configuration.timerState = domainObject.timerState; - } - - if (domainObject.pausedTime) { - configuration.pausedTime = domainObject.pausedTime; - } - - openmct.objects.mutate(domainObject, 'configuration', configuration); - - return domainObject; + { + value: 'short', + name: 'hh:mm:ss' } - }); + ], + property: ['configuration', 'timerFormat'] + } + ] + }); + openmct.objectViews.addProvider(new TimerViewProvider(openmct)); - }; + openmct.actions.register(new PauseTimerAction(openmct)); + openmct.actions.register(new RestartTimerAction(openmct)); + openmct.actions.register(new StartTimerAction(openmct)); + openmct.actions.register(new StopTimerAction(openmct)); + + openmct.objects.addGetInterceptor({ + appliesTo: (identifier, domainObject) => { + return domainObject && domainObject.type === 'timer'; + }, + invoke: (identifier, domainObject) => { + if (domainObject.configuration) { + return domainObject; + } + + const configuration = {}; + + if (domainObject.timerFormat) { + configuration.timerFormat = domainObject.timerFormat; + } + + if (domainObject.timestamp) { + configuration.timestamp = domainObject.timestamp; + } + + if (domainObject.timerState) { + configuration.timerState = domainObject.timerState; + } + + if (domainObject.pausedTime) { + configuration.pausedTime = domainObject.pausedTime; + } + + openmct.objects.mutate(domainObject, 'configuration', configuration); + + return domainObject; + } + }); + }; } diff --git a/src/plugins/timer/pluginSpec.js b/src/plugins/timer/pluginSpec.js index 3bd9dbfde6..077ca44317 100644 --- a/src/plugins/timer/pluginSpec.js +++ b/src/plugins/timer/pluginSpec.js @@ -25,341 +25,343 @@ import timerPlugin from './plugin'; import Vue from 'vue'; -describe("Timer plugin:", () => { - let openmct; - let timerDefinition; - let element; - let child; - let appHolder; +describe('Timer plugin:', () => { + let openmct; + let timerDefinition; + let element; + let child; + let appHolder; - let timerDomainObject; + let timerDomainObject; - function setupTimer() { - return new Promise((resolve, reject) => { - timerDomainObject = { - identifier: { - key: 'timer', - namespace: 'test-namespace' - }, - type: 'timer' - }; + function setupTimer() { + return new Promise((resolve, reject) => { + timerDomainObject = { + identifier: { + key: 'timer', + namespace: 'test-namespace' + }, + type: 'timer' + }; - appHolder = document.createElement('div'); - appHolder.style.width = '640px'; - appHolder.style.height = '480px'; - document.body.appendChild(appHolder); + appHolder = document.createElement('div'); + appHolder.style.width = '640px'; + appHolder.style.height = '480px'; + document.body.appendChild(appHolder); - openmct = createOpenMct(); + openmct = createOpenMct(); - element = document.createElement('div'); - child = document.createElement('div'); - element.appendChild(child); + element = document.createElement('div'); + child = document.createElement('div'); + element.appendChild(child); - openmct.install(timerPlugin()); + openmct.install(timerPlugin()); - timerDefinition = openmct.types.get('timer').definition; - timerDefinition.initialize(timerDomainObject); + timerDefinition = openmct.types.get('timer').definition; + timerDefinition.initialize(timerDomainObject); - spyOn(openmct.objects, 'supportsMutation').and.returnValue(true); + spyOn(openmct.objects, 'supportsMutation').and.returnValue(true); - openmct.on('start', resolve); - openmct.start(appHolder); - }); - } + openmct.on('start', resolve); + openmct.start(appHolder); + }); + } + + afterEach(() => { + return resetApplicationState(openmct); + }); + + describe("should still work if it's in the old format", () => { + let timerViewProvider; + let timerView; + let timerViewObject; + let mutableTimerObject; + let timerObjectPath; + const relativeTimestamp = 1634774400000; // Oct 21 2021, 12:00 AM + + beforeEach(async () => { + await setupTimer(); + + timerViewObject = { + identifier: { + key: 'timer', + namespace: 'test-namespace' + }, + type: 'timer', + id: 'test-object', + name: 'Timer', + timerFormat: 'short', + timestamp: relativeTimestamp, + timerState: 'paused', + pausedTime: relativeTimestamp + }; + + const applicableViews = openmct.objectViews.get(timerViewObject, [timerViewObject]); + timerViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'timer.view'); + + spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(timerViewObject)); + spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true)); + + mutableTimerObject = await openmct.objects.getMutable(timerViewObject.identifier); + + timerObjectPath = [mutableTimerObject]; + timerView = timerViewProvider.view(mutableTimerObject, timerObjectPath); + timerView.show(child); + + await Vue.nextTick(); + }); afterEach(() => { - return resetApplicationState(openmct); + timerView.destroy(); }); - describe("should still work if it's in the old format", () => { - let timerViewProvider; - let timerView; - let timerViewObject; - let mutableTimerObject; - let timerObjectPath; - const relativeTimestamp = 1634774400000; // Oct 21 2021, 12:00 AM + it('should migrate old object properties to the configuration section', () => { + openmct.objects.applyGetInterceptors(timerViewObject.identifier, timerViewObject); + expect(timerViewObject.configuration.timerFormat).toBe('short'); + expect(timerViewObject.configuration.timestamp).toBe(relativeTimestamp); + expect(timerViewObject.configuration.timerState).toBe('paused'); + expect(timerViewObject.configuration.pausedTime).toBe(relativeTimestamp); + }); + }); - beforeEach(async () => { - await setupTimer(); + describe('Timer view:', () => { + let timerViewProvider; + let timerView; + let timerViewObject; + let mutableTimerObject; + let timerObjectPath; - timerViewObject = { - identifier: { - key: 'timer', - namespace: 'test-namespace' - }, - type: 'timer', - id: "test-object", - name: 'Timer', - timerFormat: 'short', - timestamp: relativeTimestamp, - timerState: 'paused', - pausedTime: relativeTimestamp - }; + beforeEach(async () => { + await setupTimer(); - const applicableViews = openmct.objectViews.get(timerViewObject, [timerViewObject]); - timerViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'timer.view'); + spyOnBuiltins(['requestAnimationFrame']); + window.requestAnimationFrame.and.callFake((cb) => setTimeout(cb, 500)); + const baseTimestamp = 1634688000000; // Oct 20, 2021, 12:00 AM + const relativeTimestamp = 1634774400000; // Oct 21 2021, 12:00 AM - spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(timerViewObject)); - spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true)); + jasmine.clock().install(); + const baseTime = new Date(baseTimestamp); + jasmine.clock().mockDate(baseTime); - mutableTimerObject = await openmct.objects.getMutable(timerViewObject.identifier); + timerViewObject = { + ...timerDomainObject, + id: 'test-object', + name: 'Timer', + configuration: { + timerFormat: 'long', + timestamp: relativeTimestamp, + timezone: 'UTC', + timerState: undefined, + pausedTime: undefined + } + }; - timerObjectPath = [mutableTimerObject]; - timerView = timerViewProvider.view(mutableTimerObject, timerObjectPath); - timerView.show(child); + spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(timerViewObject)); + spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true)); - await Vue.nextTick(); - }); + const applicableViews = openmct.objectViews.get(timerViewObject, [timerViewObject]); + timerViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'timer.view'); - afterEach(() => { - timerView.destroy(); - }); + mutableTimerObject = await openmct.objects.getMutable(timerViewObject.identifier); - it("should migrate old object properties to the configuration section", () => { - openmct.objects.applyGetInterceptors(timerViewObject.identifier, timerViewObject); - expect(timerViewObject.configuration.timerFormat).toBe('short'); - expect(timerViewObject.configuration.timestamp).toBe(relativeTimestamp); - expect(timerViewObject.configuration.timerState).toBe('paused'); - expect(timerViewObject.configuration.pausedTime).toBe(relativeTimestamp); - }); + timerObjectPath = [mutableTimerObject]; + timerView = timerViewProvider.view(mutableTimerObject, timerObjectPath); + timerView.show(child); + + await Vue.nextTick(); }); - describe("Timer view:", () => { - let timerViewProvider; - let timerView; - let timerViewObject; - let mutableTimerObject; - let timerObjectPath; - - beforeEach(async () => { - await setupTimer(); - - spyOnBuiltins(['requestAnimationFrame']); - window.requestAnimationFrame.and.callFake((cb) => setTimeout(cb, 500)); - const baseTimestamp = 1634688000000; // Oct 20, 2021, 12:00 AM - const relativeTimestamp = 1634774400000; // Oct 21 2021, 12:00 AM - - jasmine.clock().install(); - const baseTime = new Date(baseTimestamp); - jasmine.clock().mockDate(baseTime); - - timerViewObject = { - ...timerDomainObject, - id: "test-object", - name: 'Timer', - configuration: { - timerFormat: 'long', - timestamp: relativeTimestamp, - timezone: 'UTC', - timerState: undefined, - pausedTime: undefined - } - }; - - spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(timerViewObject)); - spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true)); - - const applicableViews = openmct.objectViews.get(timerViewObject, [timerViewObject]); - timerViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'timer.view'); - - mutableTimerObject = await openmct.objects.getMutable(timerViewObject.identifier); - - timerObjectPath = [mutableTimerObject]; - timerView = timerViewProvider.view(mutableTimerObject, timerObjectPath); - timerView.show(child); - - await Vue.nextTick(); - }); - - afterEach(() => { - jasmine.clock().uninstall(); - timerView.destroy(); - openmct.objects.destroyMutable(mutableTimerObject); - if (appHolder) { - appHolder.remove(); - } - }); - - it("has name as Timer", () => { - expect(timerDefinition.name).toEqual('Timer'); - }); - - it("is creatable", () => { - expect(timerDefinition.creatable).toEqual(true); - }); - - it("provides timer view", () => { - expect(timerViewProvider).toBeDefined(); - }); - - it("renders timer element", () => { - const timerElement = element.querySelectorAll('.c-timer'); - expect(timerElement.length).toBe(1); - }); - - it("renders major elements", () => { - const timerElement = element.querySelector('.c-timer'); - const resetButton = timerElement.querySelector('.c-timer__ctrl-reset'); - const pausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); - const timerDirectionIcon = timerElement.querySelector('.c-timer__direction'); - const timerValue = timerElement.querySelector('.c-timer__value'); - const hasMajorElements = Boolean(resetButton && pausePlayButton && timerDirectionIcon && timerValue); - - expect(hasMajorElements).toBe(true); - }); - - it("gets errors from actions if configuration is not passed", async () => { - await Vue.nextTick(); - const objectPath = _.cloneDeep(timerObjectPath); - delete objectPath[0].configuration; - - let action = openmct.actions.getAction('timer.start'); - let actionResults = action.invoke(objectPath); - let actionFilterWithoutConfig = action.appliesTo(objectPath); - await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'started' }); - let actionFilterWithConfig = action.appliesTo(timerObjectPath); - - let actionError = new Error('Unable to run start timer action. No domainObject provided.'); - expect(actionResults).toEqual(actionError); - expect(actionFilterWithoutConfig).toBe(undefined); - expect(actionFilterWithConfig).toBe(false); - - action = openmct.actions.getAction('timer.stop'); - actionResults = action.invoke(objectPath); - actionFilterWithoutConfig = action.appliesTo(objectPath); - await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'stopped' }); - actionFilterWithConfig = action.appliesTo(timerObjectPath); - - actionError = new Error('Unable to run stop timer action. No domainObject provided.'); - expect(actionResults).toEqual(actionError); - expect(actionFilterWithoutConfig).toBe(undefined); - expect(actionFilterWithConfig).toBe(false); - - action = openmct.actions.getAction('timer.pause'); - actionResults = action.invoke(objectPath); - actionFilterWithoutConfig = action.appliesTo(objectPath); - await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'paused' }); - actionFilterWithConfig = action.appliesTo(timerObjectPath); - - actionError = new Error('Unable to run pause timer action. No domainObject provided.'); - expect(actionResults).toEqual(actionError); - expect(actionFilterWithoutConfig).toBe(undefined); - expect(actionFilterWithConfig).toBe(false); - - action = openmct.actions.getAction('timer.restart'); - actionResults = action.invoke(objectPath); - actionFilterWithoutConfig = action.appliesTo(objectPath); - await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'stopped' }); - actionFilterWithConfig = action.appliesTo(timerObjectPath); - - actionError = new Error('Unable to run restart timer action. No domainObject provided.'); - expect(actionResults).toEqual(actionError); - expect(actionFilterWithoutConfig).toBe(undefined); - expect(actionFilterWithConfig).toBe(false); - }); - - it("displays a started timer ticking down to a future date", async () => { - const newBaseTime = 1634774400000; // Oct 21 2021, 12:00 AM - openmct.objects.mutate(timerViewObject, 'configuration.timestamp', newBaseTime); - - jasmine.clock().tick(5000); - await Vue.nextTick(); - - const timerElement = element.querySelector('.c-timer'); - const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); - const timerDirectionIcon = timerElement.querySelector('.c-timer__direction'); - const timerValue = timerElement.querySelector('.c-timer__value').innerText; - - expect(timerPausePlayButton.classList.contains('icon-pause')).toBe(true); - expect(timerDirectionIcon.classList.contains('icon-minus')).toBe(true); - expect(timerValue).toBe('0D 23:59:55'); - }); - - it("displays a started timer ticking up from a past date", async () => { - const newBaseTime = 1634601600000; // Oct 19, 2021, 12:00 AM - openmct.objects.mutate(timerViewObject, 'configuration.timestamp', newBaseTime); - - jasmine.clock().tick(5000); - await Vue.nextTick(); - - const timerElement = element.querySelector('.c-timer'); - const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); - const timerDirectionIcon = timerElement.querySelector('.c-timer__direction'); - const timerValue = timerElement.querySelector('.c-timer__value').innerText; - - expect(timerPausePlayButton.classList.contains('icon-pause')).toBe(true); - expect(timerDirectionIcon.classList.contains('icon-plus')).toBe(true); - expect(timerValue).toBe('1D 00:00:05'); - }); - - it("displays a paused timer correctly in the DOM", async () => { - jasmine.clock().tick(5000); - await Vue.nextTick(); - - let action = openmct.actions.getAction('timer.pause'); - if (action) { - action.invoke(timerObjectPath, timerView); - } - - await Vue.nextTick(); - const timerElement = element.querySelector('.c-timer'); - const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); - let timerValue = timerElement.querySelector('.c-timer__value').innerText; - - expect(timerPausePlayButton.classList.contains('icon-play')).toBe(true); - expect(timerValue).toBe('0D 23:59:55'); - - jasmine.clock().tick(5000); - await Vue.nextTick(); - expect(timerValue).toBe('0D 23:59:55'); - - action = openmct.actions.getAction('timer.start'); - if (action) { - action.invoke(timerObjectPath, timerView); - } - - await Vue.nextTick(); - action = openmct.actions.getAction('timer.pause'); - if (action) { - action.invoke(timerObjectPath, timerView); - } - - await Vue.nextTick(); - timerValue = timerElement.querySelector('.c-timer__value').innerText; - expect(timerValue).toBe('1D 00:00:00'); - }); - - it("displays a stopped timer correctly in the DOM", async () => { - const action = openmct.actions.getAction('timer.stop'); - if (action) { - action.invoke(timerObjectPath, timerView); - } - - await Vue.nextTick(); - const timerElement = element.querySelector('.c-timer'); - const timerValue = timerElement.querySelector('.c-timer__value').innerText; - const timerResetButton = timerElement.querySelector('.c-timer__ctrl-reset'); - const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); - - expect(timerResetButton.classList.contains('hide')).toBe(true); - expect(timerPausePlayButton.classList.contains('icon-play')).toBe(true); - expect(timerValue).toBe('--:--:--'); - }); - - it("displays a restarted timer correctly in the DOM", async () => { - const action = openmct.actions.getAction('timer.restart'); - if (action) { - action.invoke(timerObjectPath, timerView); - } - - jasmine.clock().tick(5000); - await Vue.nextTick(); - const timerElement = element.querySelector('.c-timer'); - const timerValue = timerElement.querySelector('.c-timer__value').innerText; - const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); - - expect(timerPausePlayButton.classList.contains('icon-pause')).toBe(true); - expect(timerValue).toBe('0D 00:00:05'); - }); + afterEach(() => { + jasmine.clock().uninstall(); + timerView.destroy(); + openmct.objects.destroyMutable(mutableTimerObject); + if (appHolder) { + appHolder.remove(); + } }); + + it('has name as Timer', () => { + expect(timerDefinition.name).toEqual('Timer'); + }); + + it('is creatable', () => { + expect(timerDefinition.creatable).toEqual(true); + }); + + it('provides timer view', () => { + expect(timerViewProvider).toBeDefined(); + }); + + it('renders timer element', () => { + const timerElement = element.querySelectorAll('.c-timer'); + expect(timerElement.length).toBe(1); + }); + + it('renders major elements', () => { + const timerElement = element.querySelector('.c-timer'); + const resetButton = timerElement.querySelector('.c-timer__ctrl-reset'); + const pausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); + const timerDirectionIcon = timerElement.querySelector('.c-timer__direction'); + const timerValue = timerElement.querySelector('.c-timer__value'); + const hasMajorElements = Boolean( + resetButton && pausePlayButton && timerDirectionIcon && timerValue + ); + + expect(hasMajorElements).toBe(true); + }); + + it('gets errors from actions if configuration is not passed', async () => { + await Vue.nextTick(); + const objectPath = _.cloneDeep(timerObjectPath); + delete objectPath[0].configuration; + + let action = openmct.actions.getAction('timer.start'); + let actionResults = action.invoke(objectPath); + let actionFilterWithoutConfig = action.appliesTo(objectPath); + await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'started' }); + let actionFilterWithConfig = action.appliesTo(timerObjectPath); + + let actionError = new Error('Unable to run start timer action. No domainObject provided.'); + expect(actionResults).toEqual(actionError); + expect(actionFilterWithoutConfig).toBe(undefined); + expect(actionFilterWithConfig).toBe(false); + + action = openmct.actions.getAction('timer.stop'); + actionResults = action.invoke(objectPath); + actionFilterWithoutConfig = action.appliesTo(objectPath); + await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'stopped' }); + actionFilterWithConfig = action.appliesTo(timerObjectPath); + + actionError = new Error('Unable to run stop timer action. No domainObject provided.'); + expect(actionResults).toEqual(actionError); + expect(actionFilterWithoutConfig).toBe(undefined); + expect(actionFilterWithConfig).toBe(false); + + action = openmct.actions.getAction('timer.pause'); + actionResults = action.invoke(objectPath); + actionFilterWithoutConfig = action.appliesTo(objectPath); + await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'paused' }); + actionFilterWithConfig = action.appliesTo(timerObjectPath); + + actionError = new Error('Unable to run pause timer action. No domainObject provided.'); + expect(actionResults).toEqual(actionError); + expect(actionFilterWithoutConfig).toBe(undefined); + expect(actionFilterWithConfig).toBe(false); + + action = openmct.actions.getAction('timer.restart'); + actionResults = action.invoke(objectPath); + actionFilterWithoutConfig = action.appliesTo(objectPath); + await openmct.objects.mutate(timerObjectPath[0], 'configuration', { timerState: 'stopped' }); + actionFilterWithConfig = action.appliesTo(timerObjectPath); + + actionError = new Error('Unable to run restart timer action. No domainObject provided.'); + expect(actionResults).toEqual(actionError); + expect(actionFilterWithoutConfig).toBe(undefined); + expect(actionFilterWithConfig).toBe(false); + }); + + it('displays a started timer ticking down to a future date', async () => { + const newBaseTime = 1634774400000; // Oct 21 2021, 12:00 AM + openmct.objects.mutate(timerViewObject, 'configuration.timestamp', newBaseTime); + + jasmine.clock().tick(5000); + await Vue.nextTick(); + + const timerElement = element.querySelector('.c-timer'); + const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); + const timerDirectionIcon = timerElement.querySelector('.c-timer__direction'); + const timerValue = timerElement.querySelector('.c-timer__value').innerText; + + expect(timerPausePlayButton.classList.contains('icon-pause')).toBe(true); + expect(timerDirectionIcon.classList.contains('icon-minus')).toBe(true); + expect(timerValue).toBe('0D 23:59:55'); + }); + + it('displays a started timer ticking up from a past date', async () => { + const newBaseTime = 1634601600000; // Oct 19, 2021, 12:00 AM + openmct.objects.mutate(timerViewObject, 'configuration.timestamp', newBaseTime); + + jasmine.clock().tick(5000); + await Vue.nextTick(); + + const timerElement = element.querySelector('.c-timer'); + const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); + const timerDirectionIcon = timerElement.querySelector('.c-timer__direction'); + const timerValue = timerElement.querySelector('.c-timer__value').innerText; + + expect(timerPausePlayButton.classList.contains('icon-pause')).toBe(true); + expect(timerDirectionIcon.classList.contains('icon-plus')).toBe(true); + expect(timerValue).toBe('1D 00:00:05'); + }); + + it('displays a paused timer correctly in the DOM', async () => { + jasmine.clock().tick(5000); + await Vue.nextTick(); + + let action = openmct.actions.getAction('timer.pause'); + if (action) { + action.invoke(timerObjectPath, timerView); + } + + await Vue.nextTick(); + const timerElement = element.querySelector('.c-timer'); + const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); + let timerValue = timerElement.querySelector('.c-timer__value').innerText; + + expect(timerPausePlayButton.classList.contains('icon-play')).toBe(true); + expect(timerValue).toBe('0D 23:59:55'); + + jasmine.clock().tick(5000); + await Vue.nextTick(); + expect(timerValue).toBe('0D 23:59:55'); + + action = openmct.actions.getAction('timer.start'); + if (action) { + action.invoke(timerObjectPath, timerView); + } + + await Vue.nextTick(); + action = openmct.actions.getAction('timer.pause'); + if (action) { + action.invoke(timerObjectPath, timerView); + } + + await Vue.nextTick(); + timerValue = timerElement.querySelector('.c-timer__value').innerText; + expect(timerValue).toBe('1D 00:00:00'); + }); + + it('displays a stopped timer correctly in the DOM', async () => { + const action = openmct.actions.getAction('timer.stop'); + if (action) { + action.invoke(timerObjectPath, timerView); + } + + await Vue.nextTick(); + const timerElement = element.querySelector('.c-timer'); + const timerValue = timerElement.querySelector('.c-timer__value').innerText; + const timerResetButton = timerElement.querySelector('.c-timer__ctrl-reset'); + const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); + + expect(timerResetButton.classList.contains('hide')).toBe(true); + expect(timerPausePlayButton.classList.contains('icon-play')).toBe(true); + expect(timerValue).toBe('--:--:--'); + }); + + it('displays a restarted timer correctly in the DOM', async () => { + const action = openmct.actions.getAction('timer.restart'); + if (action) { + action.invoke(timerObjectPath, timerView); + } + + jasmine.clock().tick(5000); + await Vue.nextTick(); + const timerElement = element.querySelector('.c-timer'); + const timerValue = timerElement.querySelector('.c-timer__value').innerText; + const timerPausePlayButton = timerElement.querySelector('.c-timer__ctrl-pause-play'); + + expect(timerPausePlayButton.classList.contains('icon-pause')).toBe(true); + expect(timerValue).toBe('0D 00:00:05'); + }); + }); }); diff --git a/src/plugins/userIndicator/components/UserIndicator.vue b/src/plugins/userIndicator/components/UserIndicator.vue index 9cef01e463..99d51874ad 100644 --- a/src/plugins/userIndicator/components/UserIndicator.vue +++ b/src/plugins/userIndicator/components/UserIndicator.vue @@ -21,34 +21,33 @@ --> diff --git a/src/plugins/userIndicator/plugin.js b/src/plugins/userIndicator/plugin.js index d653a40fd5..ffa44a2193 100644 --- a/src/plugins/userIndicator/plugin.js +++ b/src/plugins/userIndicator/plugin.js @@ -24,33 +24,32 @@ import UserIndicator from './components/UserIndicator.vue'; import Vue from 'vue'; export default function UserIndicatorPlugin() { + function addIndicator(openmct) { + const userIndicator = new Vue({ + components: { + UserIndicator + }, + provide: { + openmct: openmct + }, + template: '' + }); - function addIndicator(openmct) { - const userIndicator = new Vue ({ - components: { - UserIndicator - }, - provide: { - openmct: openmct - }, - template: '' - }); + openmct.indicators.add({ + key: 'user-indicator', + element: userIndicator.$mount().$el, + priority: openmct.priority.HIGH + }); + } - openmct.indicators.add({ - key: 'user-indicator', - element: userIndicator.$mount().$el, - priority: openmct.priority.HIGH - }); + return function install(openmct) { + if (openmct.user.hasProvider()) { + addIndicator(openmct); + } else { + // back up if user provider added after indicator installed + openmct.user.on('providerAdded', () => { + addIndicator(openmct); + }); } - - return function install(openmct) { - if (openmct.user.hasProvider()) { - addIndicator(openmct); - } else { - // back up if user provider added after indicator installed - openmct.user.on('providerAdded', () => { - addIndicator(openmct); - }); - } - }; + }; } diff --git a/src/plugins/userIndicator/pluginSpec.js b/src/plugins/userIndicator/pluginSpec.js index 511a701f75..29ce5ffb4f 100644 --- a/src/plugins/userIndicator/pluginSpec.js +++ b/src/plugins/userIndicator/pluginSpec.js @@ -20,81 +20,82 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { - createOpenMct, - resetApplicationState -} from 'utils/testing'; +import { createOpenMct, resetApplicationState } from 'utils/testing'; import Vue from 'vue'; import ExampleUserProvider from '../../../example/exampleUser/ExampleUserProvider'; const USERNAME = 'Coach McGuirk'; describe('The User Indicator plugin', () => { - let openmct; - let element; - let child; - let appHolder; - let userIndicator; - let provider; + let openmct; + let element; + let child; + let appHolder; + let userIndicator; + let provider; - beforeEach((done) => { - appHolder = document.createElement('div'); - appHolder.style.width = '640px'; - appHolder.style.height = '480px'; - document.body.appendChild(appHolder); + beforeEach((done) => { + appHolder = document.createElement('div'); + appHolder.style.width = '640px'; + appHolder.style.height = '480px'; + document.body.appendChild(appHolder); - element = document.createElement('div'); - child = document.createElement('div'); - element.appendChild(child); + element = document.createElement('div'); + child = document.createElement('div'); + element.appendChild(child); - openmct = createOpenMct(); - openmct.on('start', done); - openmct.start(appHolder); + openmct = createOpenMct(); + openmct.on('start', done); + openmct.start(appHolder); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('will not show, if there is no user provider', () => { + userIndicator = openmct.indicators.indicatorObjects.find( + (indicator) => indicator.key === 'user-indicator' + ); + + expect(userIndicator).toBe(undefined); + }); + + describe('with a user provider installed', () => { + beforeEach(() => { + provider = new ExampleUserProvider(openmct); + provider.autoLogin(USERNAME); + + openmct.user.setProvider(provider); + + return Vue.nextTick(); }); - afterEach(() => { - return resetApplicationState(openmct); + it('exists', () => { + userIndicator = openmct.indicators.indicatorObjects.find( + (indicator) => indicator.key === 'user-indicator' + ).element; + + const hasClockIndicator = userIndicator !== null && userIndicator !== undefined; + expect(hasClockIndicator).toBe(true); }); - it('will not show, if there is no user provider', () => { - userIndicator = openmct.indicators.indicatorObjects - .find(indicator => indicator.key === 'user-indicator'); - - expect(userIndicator).toBe(undefined); - }); - - describe('with a user provider installed', () => { - - beforeEach(() => { - provider = new ExampleUserProvider(openmct); - provider.autoLogin(USERNAME); - - openmct.user.setProvider(provider); - - return Vue.nextTick(); - }); - - it('exists', () => { - userIndicator = openmct.indicators.indicatorObjects - .find(indicator => indicator.key === 'user-indicator').element; - - const hasClockIndicator = userIndicator !== null && userIndicator !== undefined; - expect(hasClockIndicator).toBe(true); - }); - - it('contains the logged in user name', (done) => { - openmct.user.getCurrentUser().then(async (user) => { - await Vue.nextTick(); - - userIndicator = openmct.indicators.indicatorObjects - .find(indicator => indicator.key === 'user-indicator').element; - - const userName = userIndicator.textContent.trim(); - - expect(user.name).toEqual(USERNAME); - expect(userName).toContain(USERNAME); - }).finally(done); - }); + it('contains the logged in user name', (done) => { + openmct.user + .getCurrentUser() + .then(async (user) => { + await Vue.nextTick(); + userIndicator = openmct.indicators.indicatorObjects.find( + (indicator) => indicator.key === 'user-indicator' + ).element; + + const userName = userIndicator.textContent.trim(); + + expect(user.name).toEqual(USERNAME); + expect(userName).toContain(USERNAME); + }) + .finally(done); }); + }); }); diff --git a/src/plugins/utcTimeSystem/DurationFormat.js b/src/plugins/utcTimeSystem/DurationFormat.js index 455828294b..657e8c2f70 100644 --- a/src/plugins/utcTimeSystem/DurationFormat.js +++ b/src/plugins/utcTimeSystem/DurationFormat.js @@ -1,10 +1,7 @@ - import moment from 'moment'; -const DATE_FORMAT = "HH:mm:ss"; -const DATE_FORMATS = [ - DATE_FORMAT -]; +const DATE_FORMAT = 'HH:mm:ss'; +const DATE_FORMATS = [DATE_FORMAT]; /** * Formatter for duration. Uses moment to produce a date from a given @@ -18,20 +15,20 @@ const DATE_FORMATS = [ * @memberof platform/commonUI/formats */ class DurationFormat { - constructor() { - this.key = "duration"; - } - format(value) { - return moment.utc(value).format(DATE_FORMAT); - } + constructor() { + this.key = 'duration'; + } + format(value) { + return moment.utc(value).format(DATE_FORMAT); + } - parse(text) { - return moment.duration(text).asMilliseconds(); - } + parse(text) { + return moment.duration(text).asMilliseconds(); + } - validate(text) { - return moment.utc(text, DATE_FORMATS, true).isValid(); - } + validate(text) { + return moment.utc(text, DATE_FORMATS, true).isValid(); + } } export default DurationFormat; diff --git a/src/plugins/utcTimeSystem/LocalClock.js b/src/plugins/utcTimeSystem/LocalClock.js index b84e67e1a7..6983aaf72c 100644 --- a/src/plugins/utcTimeSystem/LocalClock.js +++ b/src/plugins/utcTimeSystem/LocalClock.js @@ -20,7 +20,7 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import DefaultClock from "../../utils/clock/DefaultClock"; +import DefaultClock from '../../utils/clock/DefaultClock'; /** * A {@link openmct.TimeAPI.Clock} that updates the temporal bounds of the * application based on UTC time values provided by a ticking local clock, @@ -30,33 +30,32 @@ import DefaultClock from "../../utils/clock/DefaultClock"; */ export default class LocalClock extends DefaultClock { - constructor(period = 100) { - super(); + constructor(period = 100) { + super(); - this.key = 'local'; - this.name = 'Local Clock'; - this.description = "Provides UTC timestamps every second from the local system clock."; + this.key = 'local'; + this.name = 'Local Clock'; + this.description = 'Provides UTC timestamps every second from the local system clock.'; - this.period = period; - this.timeoutHandle = undefined; - this.lastTick = Date.now(); - } - - start() { - this.timeoutHandle = setTimeout(this.tick.bind(this), this.period); - } - - stop() { - if (this.timeoutHandle) { - clearTimeout(this.timeoutHandle); - this.timeoutHandle = undefined; - } - } - - tick() { - const now = Date.now(); - super.tick(now); - this.timeoutHandle = setTimeout(this.tick.bind(this), this.period); + this.period = period; + this.timeoutHandle = undefined; + this.lastTick = Date.now(); + } + + start() { + this.timeoutHandle = setTimeout(this.tick.bind(this), this.period); + } + + stop() { + if (this.timeoutHandle) { + clearTimeout(this.timeoutHandle); + this.timeoutHandle = undefined; } + } + tick() { + const now = Date.now(); + super.tick(now); + this.timeoutHandle = setTimeout(this.tick.bind(this), this.period); + } } diff --git a/src/plugins/utcTimeSystem/UTCTimeFormat.js b/src/plugins/utcTimeSystem/UTCTimeFormat.js index 79fae9df8b..54699794e2 100644 --- a/src/plugins/utcTimeSystem/UTCTimeFormat.js +++ b/src/plugins/utcTimeSystem/UTCTimeFormat.js @@ -31,58 +31,57 @@ import moment from 'moment'; * @memberof platform/commonUI/formats */ export default class UTCTimeFormat { - constructor() { - this.key = 'utc'; - this.DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSS'; - this.DATE_FORMATS = { - PRECISION_DEFAULT: this.DATE_FORMAT, - PRECISION_DEFAULT_WITH_ZULU: this.DATE_FORMAT + 'Z', - PRECISION_SECONDS: 'YYYY-MM-DD HH:mm:ss', - PRECISION_MINUTES: 'YYYY-MM-DD HH:mm', - PRECISION_DAYS: 'YYYY-MM-DD' - }; + constructor() { + this.key = 'utc'; + this.DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSS'; + this.DATE_FORMATS = { + PRECISION_DEFAULT: this.DATE_FORMAT, + PRECISION_DEFAULT_WITH_ZULU: this.DATE_FORMAT + 'Z', + PRECISION_SECONDS: 'YYYY-MM-DD HH:mm:ss', + PRECISION_MINUTES: 'YYYY-MM-DD HH:mm', + PRECISION_DAYS: 'YYYY-MM-DD' + }; + } + + /** + * @param {string} formatString + * @returns the value of formatString if the value is a string type and exists in the DATE_FORMATS array; otherwise the DATE_FORMAT value. + */ + isValidFormatString(formatString) { + return Object.values(this.DATE_FORMATS).includes(formatString); + } + + /** + * @param {number} value The value to format. + * @returns {string} the formatted date(s). If multiple values were requested, then an array of + * formatted values will be returned. Where a value could not be formatted, `undefined` will be returned at its position + * in the array. + */ + format(value, formatString) { + if (value !== undefined) { + const utc = moment.utc(value); + + if (formatString !== undefined && !this.isValidFormatString(formatString)) { + throw 'Invalid format requested from UTC Time Formatter '; + } + + let format = formatString || this.DATE_FORMATS.PRECISION_DEFAULT; + + return utc.format(format) + (formatString ? '' : 'Z'); + } else { + return value; + } + } + + parse(text) { + if (typeof text === 'number') { + return text; } - /** - * @param {string} formatString - * @returns the value of formatString if the value is a string type and exists in the DATE_FORMATS array; otherwise the DATE_FORMAT value. - */ - isValidFormatString(formatString) { - return Object.values(this.DATE_FORMATS).includes(formatString); - } - - /** - * @param {number} value The value to format. - * @returns {string} the formatted date(s). If multiple values were requested, then an array of - * formatted values will be returned. Where a value could not be formatted, `undefined` will be returned at its position - * in the array. - */ - format(value, formatString) { - if (value !== undefined) { - const utc = moment.utc(value); - - if (formatString !== undefined && !this.isValidFormatString(formatString)) { - throw "Invalid format requested from UTC Time Formatter "; - } - - let format = formatString || this.DATE_FORMATS.PRECISION_DEFAULT; - - return utc.format(format) + (formatString ? '' : 'Z'); - } else { - return value; - } - } - - parse(text) { - if (typeof text === 'number') { - return text; - } - - return moment.utc(text, Object.values(this.DATE_FORMATS)).valueOf(); - } - - validate(text) { - return moment.utc(text, Object.values(this.DATE_FORMATS), true).isValid(); - } + return moment.utc(text, Object.values(this.DATE_FORMATS)).valueOf(); + } + validate(text) { + return moment.utc(text, Object.values(this.DATE_FORMATS), true).isValid(); + } } diff --git a/src/plugins/utcTimeSystem/UTCTimeSystem.js b/src/plugins/utcTimeSystem/UTCTimeSystem.js index d8f806ec5e..5002948152 100644 --- a/src/plugins/utcTimeSystem/UTCTimeSystem.js +++ b/src/plugins/utcTimeSystem/UTCTimeSystem.js @@ -21,24 +21,23 @@ *****************************************************************************/ define([], function () { + /** + * This time system supports UTC dates. + * @implements TimeSystem + * @constructor + */ + function UTCTimeSystem() { /** - * This time system supports UTC dates. - * @implements TimeSystem - * @constructor + * Metadata used to identify the time system in + * the UI */ - function UTCTimeSystem() { + this.key = 'utc'; + this.name = 'UTC'; + this.cssClass = 'icon-clock'; + this.timeFormat = 'utc'; + this.durationFormat = 'duration'; + this.isUTCBased = true; + } - /** - * Metadata used to identify the time system in - * the UI - */ - this.key = 'utc'; - this.name = 'UTC'; - this.cssClass = 'icon-clock'; - this.timeFormat = 'utc'; - this.durationFormat = 'duration'; - this.isUTCBased = true; - } - - return UTCTimeSystem; + return UTCTimeSystem; }); diff --git a/src/plugins/utcTimeSystem/plugin.js b/src/plugins/utcTimeSystem/plugin.js index fc5bf347f2..0a5bfc3da0 100644 --- a/src/plugins/utcTimeSystem/plugin.js +++ b/src/plugins/utcTimeSystem/plugin.js @@ -30,11 +30,11 @@ import DurationFormat from './DurationFormat'; * clock source that ticks every 100ms, providing UTC times. */ export default function () { - return function (openmct) { - const timeSystem = new UTCTimeSystem(); - openmct.time.addTimeSystem(timeSystem); - openmct.time.addClock(new LocalClock(100)); - openmct.telemetry.addFormat(new UTCTimeFormat()); - openmct.telemetry.addFormat(new DurationFormat()); - }; + return function (openmct) { + const timeSystem = new UTCTimeSystem(); + openmct.time.addTimeSystem(timeSystem); + openmct.time.addClock(new LocalClock(100)); + openmct.telemetry.addFormat(new UTCTimeFormat()); + openmct.telemetry.addFormat(new DurationFormat()); + }; } diff --git a/src/plugins/utcTimeSystem/pluginSpec.js b/src/plugins/utcTimeSystem/pluginSpec.js index 893875564d..9b78567bb7 100644 --- a/src/plugins/utcTimeSystem/pluginSpec.js +++ b/src/plugins/utcTimeSystem/pluginSpec.js @@ -22,173 +22,170 @@ import LocalClock from './LocalClock.js'; import UTCTimeSystem from './UTCTimeSystem'; -import { - createOpenMct, - resetApplicationState -} from 'utils/testing'; +import { createOpenMct, resetApplicationState } from 'utils/testing'; import UTCTimeFormat from './UTCTimeFormat.js'; -describe("The UTC Time System", () => { - const UTC_SYSTEM_AND_FORMAT_KEY = 'utc'; - const DURATION_FORMAT_KEY = 'duration'; - let openmct; - let utcTimeSystem; - let mockTimeout; +describe('The UTC Time System', () => { + const UTC_SYSTEM_AND_FORMAT_KEY = 'utc'; + const DURATION_FORMAT_KEY = 'duration'; + let openmct; + let utcTimeSystem; + let mockTimeout; + + beforeEach(() => { + openmct = createOpenMct(); + openmct.install(openmct.plugins.UTCTimeSystem()); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + describe('plugin', function () { + beforeEach(function () { + mockTimeout = jasmine.createSpy('timeout'); + utcTimeSystem = new UTCTimeSystem(mockTimeout); + }); + + it('is installed', () => { + let timeSystems = openmct.time.getAllTimeSystems(); + let utc = timeSystems.find((ts) => ts.key === UTC_SYSTEM_AND_FORMAT_KEY); + + expect(utc).not.toEqual(-1); + }); + + it('can be set to be the main time system', () => { + openmct.time.timeSystem(UTC_SYSTEM_AND_FORMAT_KEY, { + start: 0, + end: 1 + }); + + expect(openmct.time.timeSystem().key).toBe(UTC_SYSTEM_AND_FORMAT_KEY); + }); + + it('uses the utc time format', () => { + expect(utcTimeSystem.timeFormat).toBe(UTC_SYSTEM_AND_FORMAT_KEY); + }); + + it('is UTC based', () => { + expect(utcTimeSystem.isUTCBased).toBe(true); + }); + + it('defines expected metadata', () => { + expect(utcTimeSystem.key).toBe(UTC_SYSTEM_AND_FORMAT_KEY); + expect(utcTimeSystem.name).toBeDefined(); + expect(utcTimeSystem.cssClass).toBeDefined(); + expect(utcTimeSystem.durationFormat).toBeDefined(); + }); + }); + + describe('LocalClock class', function () { + let clock; + const timeoutHandle = {}; + + beforeEach(function () { + mockTimeout = jasmine.createSpy('timeout'); + mockTimeout.and.returnValue(timeoutHandle); + + clock = new LocalClock(0); + clock.start(); + }); + + it('calls listeners on tick with current time', function () { + const mockListener = jasmine.createSpy('listener'); + clock.on('tick', mockListener); + clock.tick(); + expect(mockListener).toHaveBeenCalledWith(jasmine.any(Number)); + }); + }); + + describe('UTC Time Format', () => { + let utcTimeFormatter; beforeEach(() => { - openmct = createOpenMct(); - openmct.install(openmct.plugins.UTCTimeSystem()); + utcTimeFormatter = openmct.telemetry.getFormatter(UTC_SYSTEM_AND_FORMAT_KEY); }); - afterEach(() => { - return resetApplicationState(openmct); + it('is installed by the plugin', () => { + expect(utcTimeFormatter).toBeDefined(); }); - describe("plugin", function () { + it('formats from ms since Unix epoch into Open MCT UTC time format', () => { + const TIME_IN_MS = 1638574560945; + const TIME_AS_STRING = '2021-12-03 23:36:00.945Z'; - beforeEach(function () { - mockTimeout = jasmine.createSpy("timeout"); - utcTimeSystem = new UTCTimeSystem(mockTimeout); - }); - - it("is installed", () => { - let timeSystems = openmct.time.getAllTimeSystems(); - let utc = timeSystems.find(ts => ts.key === UTC_SYSTEM_AND_FORMAT_KEY); - - expect(utc).not.toEqual(-1); - }); - - it("can be set to be the main time system", () => { - openmct.time.timeSystem(UTC_SYSTEM_AND_FORMAT_KEY, { - start: 0, - end: 1 - }); - - expect(openmct.time.timeSystem().key).toBe(UTC_SYSTEM_AND_FORMAT_KEY); - }); - - it("uses the utc time format", () => { - expect(utcTimeSystem.timeFormat).toBe(UTC_SYSTEM_AND_FORMAT_KEY); - }); - - it("is UTC based", () => { - expect(utcTimeSystem.isUTCBased).toBe(true); - }); - - it("defines expected metadata", () => { - expect(utcTimeSystem.key).toBe(UTC_SYSTEM_AND_FORMAT_KEY); - expect(utcTimeSystem.name).toBeDefined(); - expect(utcTimeSystem.cssClass).toBeDefined(); - expect(utcTimeSystem.durationFormat).toBeDefined(); - }); + const formattedTime = utcTimeFormatter.format(TIME_IN_MS); + expect(formattedTime).toEqual(TIME_AS_STRING); }); - describe("LocalClock class", function () { - let clock; - const timeoutHandle = {}; + it('formats from ms since Unix epoch into terse UTC formats', () => { + const utcTimeFormatterInstance = new UTCTimeFormat(); - beforeEach(function () { - mockTimeout = jasmine.createSpy("timeout"); - mockTimeout.and.returnValue(timeoutHandle); + const TIME_IN_MS = 1638574560945; + const EXPECTED_FORMATS = { + PRECISION_DEFAULT: '2021-12-03 23:36:00.945', + PRECISION_SECONDS: '2021-12-03 23:36:00', + PRECISION_MINUTES: '2021-12-03 23:36', + PRECISION_DAYS: '2021-12-03' + }; - clock = new LocalClock(0); - clock.start(); - }); - - it("calls listeners on tick with current time", function () { - const mockListener = jasmine.createSpy("listener"); - clock.on('tick', mockListener); - clock.tick(); - expect(mockListener).toHaveBeenCalledWith(jasmine.any(Number)); - }); + Object.keys(EXPECTED_FORMATS).forEach((formatKey) => { + const formattedTime = utcTimeFormatterInstance.format( + TIME_IN_MS, + utcTimeFormatterInstance.DATE_FORMATS[formatKey] + ); + expect(formattedTime).toEqual(EXPECTED_FORMATS[formatKey]); + }); }); - describe("UTC Time Format", () => { - let utcTimeFormatter; + it('parses from Open MCT UTC time format to ms since Unix epoch.', () => { + const TIME_IN_MS = 1638574560945; + const TIME_AS_STRING = '2021-12-03 23:36:00.945Z'; - beforeEach(() => { - utcTimeFormatter = openmct.telemetry.getFormatter(UTC_SYSTEM_AND_FORMAT_KEY); - }); - - it("is installed by the plugin", () => { - expect(utcTimeFormatter).toBeDefined(); - }); - - it("formats from ms since Unix epoch into Open MCT UTC time format", () => { - const TIME_IN_MS = 1638574560945; - const TIME_AS_STRING = "2021-12-03 23:36:00.945Z"; - - const formattedTime = utcTimeFormatter.format(TIME_IN_MS); - expect(formattedTime).toEqual(TIME_AS_STRING); - - }); - - it("formats from ms since Unix epoch into terse UTC formats", () => { - const utcTimeFormatterInstance = new UTCTimeFormat(); - - const TIME_IN_MS = 1638574560945; - const EXPECTED_FORMATS = { - PRECISION_DEFAULT: "2021-12-03 23:36:00.945", - PRECISION_SECONDS: "2021-12-03 23:36:00", - PRECISION_MINUTES: "2021-12-03 23:36", - PRECISION_DAYS: "2021-12-03" - }; - - Object.keys(EXPECTED_FORMATS).forEach((formatKey) => { - const formattedTime = utcTimeFormatterInstance.format(TIME_IN_MS, utcTimeFormatterInstance.DATE_FORMATS[formatKey]); - expect(formattedTime).toEqual(EXPECTED_FORMATS[formatKey]); - }); - }); - - it("parses from Open MCT UTC time format to ms since Unix epoch.", () => { - const TIME_IN_MS = 1638574560945; - const TIME_AS_STRING = "2021-12-03 23:36:00.945Z"; - - const parsedTime = utcTimeFormatter.parse(TIME_AS_STRING); - expect(parsedTime).toEqual(TIME_IN_MS); - }); - - it("validates correctly formatted Open MCT UTC times.", () => { - const TIME_AS_STRING = "2021-12-03 23:36:00.945Z"; - - const isValid = utcTimeFormatter.validate(TIME_AS_STRING); - expect(isValid).toBeTrue(); - }); + const parsedTime = utcTimeFormatter.parse(TIME_AS_STRING); + expect(parsedTime).toEqual(TIME_IN_MS); }); - describe("Duration Format", () => { - let durationTimeFormatter; + it('validates correctly formatted Open MCT UTC times.', () => { + const TIME_AS_STRING = '2021-12-03 23:36:00.945Z'; - beforeEach(() => { - durationTimeFormatter = openmct.telemetry.getFormatter(DURATION_FORMAT_KEY); - }); - - it("is installed by the plugin", () => { - expect(durationTimeFormatter).toBeDefined(); - }); - - it("formats from ms into Open MCT duration format", () => { - const TIME_IN_MS = 2000; - const TIME_AS_STRING = "00:00:02"; - - const formattedTime = durationTimeFormatter.format(TIME_IN_MS); - expect(formattedTime).toEqual(TIME_AS_STRING); - - }); - - it("parses from Open MCT duration format to ms", () => { - const TIME_IN_MS = 2000; - const TIME_AS_STRING = "00:00:02"; - - const parsedTime = durationTimeFormatter.parse(TIME_AS_STRING); - expect(parsedTime).toEqual(TIME_IN_MS); - }); - - it("validates correctly formatted Open MCT duration strings.", () => { - const TIME_AS_STRING = "00:00:02"; - - const isValid = durationTimeFormatter.validate(TIME_AS_STRING); - expect(isValid).toBeTrue(); - }); + const isValid = utcTimeFormatter.validate(TIME_AS_STRING); + expect(isValid).toBeTrue(); }); + }); + + describe('Duration Format', () => { + let durationTimeFormatter; + + beforeEach(() => { + durationTimeFormatter = openmct.telemetry.getFormatter(DURATION_FORMAT_KEY); + }); + + it('is installed by the plugin', () => { + expect(durationTimeFormatter).toBeDefined(); + }); + + it('formats from ms into Open MCT duration format', () => { + const TIME_IN_MS = 2000; + const TIME_AS_STRING = '00:00:02'; + + const formattedTime = durationTimeFormatter.format(TIME_IN_MS); + expect(formattedTime).toEqual(TIME_AS_STRING); + }); + + it('parses from Open MCT duration format to ms', () => { + const TIME_IN_MS = 2000; + const TIME_AS_STRING = '00:00:02'; + + const parsedTime = durationTimeFormatter.parse(TIME_AS_STRING); + expect(parsedTime).toEqual(TIME_IN_MS); + }); + + it('validates correctly formatted Open MCT duration strings.', () => { + const TIME_AS_STRING = '00:00:02'; + + const isValid = durationTimeFormatter.validate(TIME_AS_STRING); + expect(isValid).toBeTrue(); + }); + }); }); diff --git a/src/plugins/viewDatumAction/ViewDatumAction.js b/src/plugins/viewDatumAction/ViewDatumAction.js index 8285a5a47a..6c4f7a8218 100644 --- a/src/plugins/viewDatumAction/ViewDatumAction.js +++ b/src/plugins/viewDatumAction/ViewDatumAction.js @@ -24,51 +24,51 @@ import MetadataListView from './components/MetadataList.vue'; import Vue from 'vue'; export default class ViewDatumAction { - constructor(openmct) { - this.name = 'View Full Datum'; - this.key = 'viewDatumAction'; - this.description = 'View full value of datum received'; - this.cssClass = 'icon-object'; + constructor(openmct) { + this.name = 'View Full Datum'; + this.key = 'viewDatumAction'; + this.description = 'View full value of datum received'; + this.cssClass = 'icon-object'; - this._openmct = openmct; + this._openmct = openmct; + } + invoke(objectPath, view) { + let viewContext = view.getViewContext && view.getViewContext(); + const row = viewContext.row; + let attributes = row.getDatum && row.getDatum(); + let component = new Vue({ + components: { + MetadataListView + }, + provide: { + name: this.name, + attributes + }, + template: '' + }); + + this._openmct.overlays.overlay({ + element: component.$mount().$el, + size: 'large', + dismissable: true, + onDestroy: () => { + component.$destroy(); + } + }); + } + appliesTo(objectPath, view = {}) { + let viewContext = (view.getViewContext && view.getViewContext()) || {}; + const row = viewContext.row; + if (!row) { + return false; } - invoke(objectPath, view) { - let viewContext = view.getViewContext && view.getViewContext(); - const row = viewContext.row; - let attributes = row.getDatum && row.getDatum(); - let component = new Vue ({ - components: { - MetadataListView - }, - provide: { - name: this.name, - attributes - }, - template: '' - }); - this._openmct.overlays.overlay({ - element: component.$mount().$el, - size: 'large', - dismissable: true, - onDestroy: () => { - component.$destroy(); - } - }); + let datum = row.getDatum; + let enabled = row.viewDatumAction; + if (enabled && datum) { + return true; } - appliesTo(objectPath, view = {}) { - let viewContext = (view.getViewContext && view.getViewContext()) || {}; - const row = viewContext.row; - if (!row) { - return false; - } - let datum = row.getDatum; - let enabled = row.viewDatumAction; - if (enabled && datum) { - return true; - } - - return false; - } + return false; + } } diff --git a/src/plugins/viewDatumAction/components/MetadataList.vue b/src/plugins/viewDatumAction/components/MetadataList.vue index f1d03c707a..8676f0e475 100644 --- a/src/plugins/viewDatumAction/components/MetadataList.vue +++ b/src/plugins/viewDatumAction/components/MetadataList.vue @@ -20,26 +20,23 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/viewDatumAction/components/metadata-list.scss b/src/plugins/viewDatumAction/components/metadata-list.scss index 778d4f3ded..4ecf235bf3 100644 --- a/src/plugins/viewDatumAction/components/metadata-list.scss +++ b/src/plugins/viewDatumAction/components/metadata-list.scss @@ -1,30 +1,30 @@ .c-attributes-view { - display: flex; - flex: 1 1 auto; - flex-direction: column; + display: flex; + flex: 1 1 auto; + flex-direction: column; - > * { - flex: 0 0 auto; + > * { + flex: 0 0 auto; + } + + &__content { + $p: 3px; + + display: grid; + grid-template-columns: max-content 1fr; + grid-row-gap: $p; + + li { + display: contents; } - &__content { - $p: 3px; - - display: grid; - grid-template-columns: max-content 1fr; - grid-row-gap: $p; - - li { display: contents; } - - [class*="__grid-item"] { - border-bottom: 1px solid rgba(#999, 0.2); - padding: 0 5px $p 0; - } - - [class*="__label"] { - opacity: 0.8; - } + [class*='__grid-item'] { + border-bottom: 1px solid rgba(#999, 0.2); + padding: 0 5px $p 0; } - + [class*='__label'] { + opacity: 0.8; + } + } } diff --git a/src/plugins/viewDatumAction/plugin.js b/src/plugins/viewDatumAction/plugin.js index ca586fbf6d..0e9842aaeb 100644 --- a/src/plugins/viewDatumAction/plugin.js +++ b/src/plugins/viewDatumAction/plugin.js @@ -23,7 +23,7 @@ import ViewDatumAction from './ViewDatumAction.js'; export default function plugin() { - return function install(openmct) { - openmct.actions.register(new ViewDatumAction(openmct)); - }; + return function install(openmct) { + openmct.actions.register(new ViewDatumAction(openmct)); + }; } diff --git a/src/plugins/viewDatumAction/pluginSpec.js b/src/plugins/viewDatumAction/pluginSpec.js index 4dfeeb1d3d..203048cfd6 100644 --- a/src/plugins/viewDatumAction/pluginSpec.js +++ b/src/plugins/viewDatumAction/pluginSpec.js @@ -19,75 +19,73 @@ * 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 { createOpenMct, resetApplicationState } from 'utils/testing'; -describe("the plugin", () => { - let openmct; - let viewDatumAction; - let mockObjectPath; - let mockView; - let mockDatum; +describe('the plugin', () => { + let openmct; + let viewDatumAction; + let mockObjectPath; + let mockView; + let mockDatum; - beforeEach((done) => { - openmct = createOpenMct(); + beforeEach((done) => { + openmct = createOpenMct(); - openmct.on('start', done); - openmct.startHeadless(); + openmct.on('start', done); + openmct.startHeadless(); - viewDatumAction = openmct.actions._allActions.viewDatumAction; + viewDatumAction = openmct.actions._allActions.viewDatumAction; - mockObjectPath = [{ - name: 'mock object', - type: 'telemetry-table', - identifier: { - key: 'mock-object', - namespace: '' + mockObjectPath = [ + { + name: 'mock object', + type: 'telemetry-table', + identifier: { + key: 'mock-object', + namespace: '' + } + } + ]; + + mockDatum = { + time: 123456789, + sin: 0.4455512, + cos: 0.4455512 + }; + + mockView = { + getViewContext: () => { + return { + row: { + viewDatumAction: true, + getDatum: () => { + return mockDatum; } - }]; - - mockDatum = { - time: 123456789, - sin: 0.4455512, - cos: 0.4455512 + } }; + } + }; + }); - mockView = { - getViewContext: () => { - return { - row: { - viewDatumAction: true, - getDatum: () => { - return mockDatum; - } - } - }; - } - }; + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('installs the view datum action', () => { + expect(viewDatumAction).toBeDefined(); + }); + + describe('when invoked', () => { + beforeEach(() => { + openmct.overlays.overlay = function (options) {}; + + spyOn(openmct.overlays, 'overlay'); + + viewDatumAction.invoke(mockObjectPath, mockView); }); - afterEach(() => { - return resetApplicationState(openmct); - }); - - it('installs the view datum action', () => { - expect(viewDatumAction).toBeDefined(); - }); - - describe('when invoked', () => { - - beforeEach(() => { - openmct.overlays.overlay = function (options) {}; - - spyOn(openmct.overlays, 'overlay'); - - viewDatumAction.invoke(mockObjectPath, mockView); - }); - - it('creates an overlay', () => { - expect(openmct.overlays.overlay).toHaveBeenCalled(); - }); + it('creates an overlay', () => { + expect(openmct.overlays.overlay).toHaveBeenCalled(); }); + }); }); diff --git a/src/plugins/viewLargeAction/plugin.js b/src/plugins/viewLargeAction/plugin.js index 0b3d9fb7b9..4335f678eb 100644 --- a/src/plugins/viewLargeAction/plugin.js +++ b/src/plugins/viewLargeAction/plugin.js @@ -23,7 +23,7 @@ import ViewLargeAction from './viewLargeAction.js'; export default function plugin() { - return function install(openmct) { - openmct.actions.register(new ViewLargeAction(openmct)); - }; + return function install(openmct) { + openmct.actions.register(new ViewLargeAction(openmct)); + }; } diff --git a/src/plugins/viewLargeAction/viewLargeAction.js b/src/plugins/viewLargeAction/viewLargeAction.js index 04b0d8810e..30f6e647db 100644 --- a/src/plugins/viewLargeAction/viewLargeAction.js +++ b/src/plugins/viewLargeAction/viewLargeAction.js @@ -25,71 +25,74 @@ import Preview from '@/ui/preview/Preview.vue'; import Vue from 'vue'; export default class ViewLargeAction { - constructor(openmct) { - this.openmct = openmct; + constructor(openmct) { + this.openmct = openmct; - this.cssClass = 'icon-items-expand'; - this.description = 'View Large'; - this.group = 'windowing'; - this.key = 'large.view'; - this.name = 'Large View'; - this.priority = 1; - this.showInStatusBar = true; + this.cssClass = 'icon-items-expand'; + this.description = 'View Large'; + this.group = 'windowing'; + this.key = 'large.view'; + this.name = 'Large View'; + this.priority = 1; + this.showInStatusBar = true; + } + + invoke(objectPath, view) { + performance.mark('viewlarge.start'); + const childElement = view?.parentElement?.firstChild; + if (!childElement) { + const message = 'ViewLargeAction: missing element'; + this.openmct.notifications.error(message); + throw new Error(message); } - invoke(objectPath, view) { - performance.mark('viewlarge.start'); - const childElement = view?.parentElement?.firstChild; - if (!childElement) { - const message = "ViewLargeAction: missing element"; - this.openmct.notifications.error(message); - throw new Error(message); - } + this._expand(objectPath, view); + } - this._expand(objectPath, view); - } + appliesTo(objectPath, view) { + const childElement = view?.parentElement?.firstChild; - appliesTo(objectPath, view) { - const childElement = view?.parentElement?.firstChild; + return ( + childElement && + !childElement.classList.contains('js-main-container') && + !this.openmct.router.isNavigatedObject(objectPath) + ); + } - return childElement && !childElement.classList.contains('js-main-container') - && !this.openmct.router.isNavigatedObject(objectPath); - } + _expand(objectPath, view) { + const element = this._getPreview(objectPath, view); + view.onPreviewModeChange?.({ isPreviewing: true }); - _expand(objectPath, view) { - const element = this._getPreview(objectPath, view); - view.onPreviewModeChange?.({ isPreviewing: true }); + this.overlay = this.openmct.overlays.overlay({ + element, + size: 'large', + autoHide: false, + onDestroy: () => { + this.preview.$destroy(); + this.preview = undefined; + delete this.preview; + view.onPreviewModeChange?.(); + } + }); + } - this.overlay = this.openmct.overlays.overlay({ - element, - size: 'large', - autoHide: false, - onDestroy: () => { - this.preview.$destroy(); - this.preview = undefined; - delete this.preview; - view.onPreviewModeChange?.(); - } - }); - } + _getPreview(objectPath, view) { + this.preview = new Vue({ + components: { + Preview + }, + provide: { + openmct: this.openmct, + objectPath + }, + data() { + return { + view + }; + }, + template: '' + }); - _getPreview(objectPath, view) { - this.preview = new Vue({ - components: { - Preview - }, - provide: { - openmct: this.openmct, - objectPath - }, - data() { - return { - view - }; - }, - template: '' - }); - - return this.preview.$mount().$el; - } + return this.preview.$mount().$el; + } } diff --git a/src/plugins/webPage/WebPageViewProvider.js b/src/plugins/webPage/WebPageViewProvider.js index 43ba670eff..74630c9ad8 100644 --- a/src/plugins/webPage/WebPageViewProvider.js +++ b/src/plugins/webPage/WebPageViewProvider.js @@ -24,38 +24,38 @@ import WebPageComponent from './components/WebPage.vue'; import Vue from 'vue'; export default function WebPage(openmct) { - return { - key: 'webPage', - name: 'Web Page', - cssClass: 'icon-page', - canView: function (domainObject) { - return domainObject.type === 'webPage'; - }, - view: function (domainObject) { - let component; + return { + key: 'webPage', + name: 'Web Page', + cssClass: 'icon-page', + canView: function (domainObject) { + return domainObject.type === 'webPage'; + }, + view: function (domainObject) { + let component; - return { - show: function (element) { - component = new Vue({ - el: element, - components: { - WebPageComponent: WebPageComponent - }, - provide: { - openmct, - domainObject - }, - template: '' - }); - }, - destroy: function (element) { - component.$destroy(); - component = undefined; - } - }; + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + WebPageComponent: WebPageComponent + }, + provide: { + openmct, + domainObject + }, + template: '' + }); }, - priority: function () { - return 1; + destroy: function (element) { + component.$destroy(); + component = undefined; } - }; + }; + }, + priority: function () { + return 1; + } + }; } diff --git a/src/plugins/webPage/components/WebPage.vue b/src/plugins/webPage/components/WebPage.vue index 4f53cd8d59..13ac6a487d 100644 --- a/src/plugins/webPage/components/WebPage.vue +++ b/src/plugins/webPage/components/WebPage.vue @@ -20,25 +20,25 @@ at runtime from the About dialog for additional information. --> diff --git a/src/plugins/webPage/plugin.js b/src/plugins/webPage/plugin.js index 7daf222e2a..0350bebc5a 100644 --- a/src/plugins/webPage/plugin.js +++ b/src/plugins/webPage/plugin.js @@ -23,23 +23,24 @@ import WebPageViewProvider from './WebPageViewProvider.js'; export default function plugin() { - return function install(openmct) { - openmct.objectViews.addProvider(new WebPageViewProvider(openmct)); + return function install(openmct) { + openmct.objectViews.addProvider(new WebPageViewProvider(openmct)); - openmct.types.addType('webPage', { - name: "Web Page", - description: "Embed a web page or web-based image in a resizeable window component. Note that the URL being embedded must allow iframing.", - creatable: true, - cssClass: 'icon-page', - form: [ - { - "key": "url", - "name": "URL", - "control": "textfield", - "required": true, - "cssClass": "l-input-lg" - } - ] - }); - }; + openmct.types.addType('webPage', { + name: 'Web Page', + description: + 'Embed a web page or web-based image in a resizeable window component. Note that the URL being embedded must allow iframing.', + creatable: true, + cssClass: 'icon-page', + form: [ + { + key: 'url', + name: 'URL', + control: 'textfield', + required: true, + cssClass: 'l-input-lg' + } + ] + }); + }; } diff --git a/src/plugins/webPage/pluginSpec.js b/src/plugins/webPage/pluginSpec.js index 3326cb90b0..e77fabbe1e 100644 --- a/src/plugins/webPage/pluginSpec.js +++ b/src/plugins/webPage/pluginSpec.js @@ -20,87 +20,85 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import { createOpenMct, resetApplicationState } from "utils/testing"; -import WebPagePlugin from "./plugin"; +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'); + const applicableViews = openmct.objectViews.get(domainObj, objectPath); + const webpageView = applicableViews.find((viewProvider) => viewProvider.key === 'webPage'); - return webpageView.view(domainObj); + return webpageView.view(domainObj); } function destroyView(view) { - return view.destroy(); + return view.destroy(); } -describe("The web page plugin", function () { - let mockDomainObject; - let mockDomainObjectPath; - let openmct; - let element; - let child; - let view; +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: '' - } - } - ]; + 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" - } - }; + 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()); + 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); + 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(); + 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); }); - afterEach(() => { - destroyView(view); - - return resetApplicationState(openmct); + it('provides a view', () => { + expect(view).toBeDefined(); }); - - describe('the view', () => { - beforeEach(() => { - view = getView(openmct, mockDomainObject, mockDomainObjectPath); - view.show(child, true); - }); - - it('provides a view', () => { - expect(view).toBeDefined(); - }); - }); - + }); }); diff --git a/src/selection/Selection.js b/src/selection/Selection.js index 1ee2624a6b..dd84f6b62f 100644 --- a/src/selection/Selection.js +++ b/src/selection/Selection.js @@ -28,217 +28,218 @@ import _ from 'lodash'; * @private */ export default class Selection extends EventEmitter { - constructor(openmct) { - super(); + constructor(openmct) { + super(); - this.openmct = openmct; - this.selected = []; + this.openmct = openmct; + this.selected = []; + } + /** + * Gets the selected object. + * @public + */ + get() { + return this.selected; + } + /** + * Selects the selectable object and emits the 'change' event. + * + * @param {object} selectable an object with element and context properties + * @param {Boolean} isMultiSelectEvent flag indication shift key is pressed or not + * @private + */ + select(selectable, isMultiSelectEvent) { + if (!Array.isArray(selectable)) { + selectable = [selectable]; } - /** - * Gets the selected object. - * @public - */ - get() { - return this.selected; + + let multiSelect = + isMultiSelectEvent && + this.parentSupportsMultiSelect(selectable) && + this.isPeer(selectable) && + !this.selectionContainsParent(selectable); + + if (multiSelect) { + this.handleMultiSelect(selectable); + } else { + this.handleSingleSelect(selectable); } - /** - * Selects the selectable object and emits the 'change' event. - * - * @param {object} selectable an object with element and context properties - * @param {Boolean} isMultiSelectEvent flag indication shift key is pressed or not - * @private - */ - select(selectable, isMultiSelectEvent) { - if (!Array.isArray(selectable)) { - selectable = [selectable]; - } - - let multiSelect = isMultiSelectEvent - && this.parentSupportsMultiSelect(selectable) - && this.isPeer(selectable) - && !this.selectionContainsParent(selectable); - - if (multiSelect) { - this.handleMultiSelect(selectable); - } else { - this.handleSingleSelect(selectable); - } + } + /** + * @private + */ + handleMultiSelect(selectable) { + if (this.elementSelected(selectable)) { + this.remove(selectable); + } else { + this.addSelectionAttributes(selectable); + this.selected.push(selectable); } - /** - * @private - */ - handleMultiSelect(selectable) { - if (this.elementSelected(selectable)) { - this.remove(selectable); - } else { - this.addSelectionAttributes(selectable); - this.selected.push(selectable); - } - this.emit('change', this.selected); + this.emit('change', this.selected); + } + /** + * @private + */ + handleSingleSelect(selectable) { + if (!_.isEqual([selectable], this.selected)) { + this.setSelectionStyles(selectable); + this.selected = [selectable]; + + this.emit('change', this.selected); } - /** - * @private - */ - handleSingleSelect(selectable) { - if (!_.isEqual([selectable], this.selected)) { - this.setSelectionStyles(selectable); - this.selected = [selectable]; + } + /** + * @private + */ + elementSelected(selectable) { + return this.selected.some((selectionPath) => _.isEqual(selectionPath, selectable)); + } + /** + * @private + */ + remove(selectable) { + this.selected = this.selected.filter((selectionPath) => !_.isEqual(selectionPath, selectable)); - this.emit('change', this.selected); - } + if (this.selected.length === 0) { + this.removeSelectionAttributes(selectable); + selectable[1].element.click(); // Select the parent if there is no selection. + } else { + this.removeSelectionAttributes(selectable, true); } - /** - * @private - */ - elementSelected(selectable) { - return this.selected.some(selectionPath => _.isEqual(selectionPath, selectable)); + } + /** + * @private + */ + setSelectionStyles(selectable) { + this.selected.forEach((selectionPath) => this.removeSelectionAttributes(selectionPath)); + this.addSelectionAttributes(selectable); + } + removeSelectionAttributes(selectionPath, keepParentStyle) { + if (selectionPath[0] && selectionPath[0].element) { + selectionPath[0].element.removeAttribute('s-selected'); } - /** - * @private - */ - remove(selectable) { - this.selected = this.selected.filter(selectionPath => !_.isEqual(selectionPath, selectable)); - if (this.selected.length === 0) { - this.removeSelectionAttributes(selectable); - selectable[1].element.click(); // Select the parent if there is no selection. - } else { - this.removeSelectionAttributes(selectable, true); - } + if (selectionPath[1] && selectionPath[1].element && !keepParentStyle) { + selectionPath[1].element.removeAttribute('s-selected-parent'); } - /** - * @private - */ - setSelectionStyles(selectable) { - this.selected.forEach(selectionPath => this.removeSelectionAttributes(selectionPath)); - this.addSelectionAttributes(selectable); + } + /** + * Adds selection attributes to the selected element and its parent. + * @private + */ + addSelectionAttributes(selectable) { + if (selectable[0] && selectable[0].element) { + selectable[0].element.setAttribute('s-selected', ''); } - removeSelectionAttributes(selectionPath, keepParentStyle) { - if (selectionPath[0] && selectionPath[0].element) { - selectionPath[0].element.removeAttribute('s-selected'); - } - if (selectionPath[1] && selectionPath[1].element && !keepParentStyle) { - selectionPath[1].element.removeAttribute('s-selected-parent'); - } + if (selectable[1] && selectable[1].element) { + selectable[1].element.setAttribute('s-selected-parent', ''); } - /** - * Adds selection attributes to the selected element and its parent. - * @private - */ - addSelectionAttributes(selectable) { - if (selectable[0] && selectable[0].element) { - selectable[0].element.setAttribute('s-selected', ""); - } - - if (selectable[1] && selectable[1].element) { - selectable[1].element.setAttribute('s-selected-parent', ""); - } + } + /** + * @private + */ + parentSupportsMultiSelect(selectable) { + return selectable[1] && selectable[1].context.supportsMultiSelect; + } + /** + * @private + */ + selectionContainsParent(selectable) { + return this.selected.some((selectionPath) => _.isEqual(selectionPath[0], selectable[1])); + } + /** + * @private + */ + isPeer(selectable) { + return this.selected.some((selectionPath) => _.isEqual(selectionPath[1], selectable[1])); + } + /** + * @private + */ + isSelectable(element) { + if (!element) { + return false; } - /** - * @private - */ - parentSupportsMultiSelect(selectable) { - return selectable[1] && selectable[1].context.supportsMultiSelect; + + return Boolean(element.closest('[data-selectable]')); + } + /** + * @private + */ + capture(selectable) { + let capturingContainsSelectable = this.capturing && this.capturing.includes(selectable); + + if (!this.capturing || capturingContainsSelectable) { + this.capturing = []; } - /** - * @private - */ - selectionContainsParent(selectable) { - return this.selected.some(selectionPath => _.isEqual(selectionPath[0], selectable[1])); + + this.capturing.push(selectable); + } + /** + * @private + */ + selectCapture(selectable, event) { + if (!this.capturing) { + return; } - /** - * @private - */ - isPeer(selectable) { - return this.selected.some(selectionPath => _.isEqual(selectionPath[1], selectable[1])); + + let reversedCapturing = this.capturing.reverse(); + delete this.capturing; + this.select(reversedCapturing, event.shiftKey); + } + /** + * Attaches the click handlers to the element. + * + * @param element an html element + * @param context object which defines item or other arbitrary properties. + * e.g. { + * item: domainObject, + * elementProxy: element, + * controller: fixedController + * } + * @param select a flag to select the element if true + * @returns a function that removes the click handlers from the element + * @public + */ + selectable(element, context, select) { + if (!this.isSelectable(element)) { + return () => {}; } - /** - * @private - */ - isSelectable(element) { - if (!element) { - return false; - } - return Boolean(element.closest('[data-selectable]')); + let selectable = { + context: context, + element: element + }; + + const capture = this.capture.bind(this, selectable); + const selectCapture = this.selectCapture.bind(this, selectable); + let removeMutable = false; + + element.addEventListener('click', capture, true); + element.addEventListener('click', selectCapture); + + if (context.item && context.item.isMutable !== true) { + removeMutable = true; + context.item = this.openmct.objects.toMutable(context.item); } - /** - * @private - */ - capture(selectable) { - let capturingContainsSelectable = this.capturing && this.capturing.includes(selectable); - if (!this.capturing || capturingContainsSelectable) { - this.capturing = []; - } - - this.capturing.push(selectable); + if (select) { + if (typeof select === 'object') { + element.dispatchEvent(select); + } else if (typeof select === 'boolean') { + element.click(); + } } - /** - * @private - */ - selectCapture(selectable, event) { - if (!this.capturing) { - return; - } - let reversedCapturing = this.capturing.reverse(); - delete this.capturing; - this.select(reversedCapturing, event.shiftKey); - } - /** - * Attaches the click handlers to the element. - * - * @param element an html element - * @param context object which defines item or other arbitrary properties. - * e.g. { - * item: domainObject, - * elementProxy: element, - * controller: fixedController - * } - * @param select a flag to select the element if true - * @returns a function that removes the click handlers from the element - * @public - */ - selectable(element, context, select) { - if (!this.isSelectable(element)) { - return () => { }; - } + return function () { + element.removeEventListener('click', capture, true); + element.removeEventListener('click', selectCapture); - let selectable = { - context: context, - element: element - }; - - const capture = this.capture.bind(this, selectable); - const selectCapture = this.selectCapture.bind(this, selectable); - let removeMutable = false; - - element.addEventListener('click', capture, true); - element.addEventListener('click', selectCapture); - - if (context.item && context.item.isMutable !== true) { - removeMutable = true; - context.item = this.openmct.objects.toMutable(context.item); - } - - if (select) { - if (typeof select === 'object') { - element.dispatchEvent(select); - } else if (typeof select === 'boolean') { - element.click(); - } - } - - return (function () { - element.removeEventListener('click', capture, true); - element.removeEventListener('click', selectCapture); - - if (context.item !== undefined && context.item.isMutable && removeMutable === true) { - this.openmct.objects.destroyMutable(context.item); - } - }).bind(this); - } + if (context.item !== undefined && context.item.isMutable && removeMutable === true) { + this.openmct.objects.destroyMutable(context.item); + } + }.bind(this); + } } diff --git a/src/styles/_about.scss b/src/styles/_about.scss index 3f2a0e3a15..16fc97484d 100644 --- a/src/styles/_about.scss +++ b/src/styles/_about.scss @@ -22,105 +22,107 @@ // Used by About screen, licenses, etc. .c-splash-image { + background-position: center; + background-repeat: no-repeat; + background-size: cover; + background-image: url('../ui/layout/assets/images/bg-splash.jpg'); + margin-top: 30px; // Don't overlap with close "X" button + + &:before, + &:after { background-position: center; background-repeat: no-repeat; - background-size: cover; - background-image: url('../ui/layout/assets/images/bg-splash.jpg'); - margin-top: 30px; // Don't overlap with close "X" button + position: absolute; + background-image: url('../ui/layout/assets/images/logo-openmct-shdw.svg'); + background-size: contain; + content: ''; + } - &:before, - &:after { - background-position: center; - background-repeat: no-repeat; - position: absolute; - background-image: url('../ui/layout/assets/images/logo-openmct-shdw.svg'); - background-size: contain; - content: ''; - } + &:before { + // NASA logo, dude + $w: 5%; + $m: 10px; + background-image: url('../ui/layout/assets/images/logo-nasa.svg'); + top: $m; + right: auto; + bottom: auto; + left: $m; + height: auto; + width: $w * 2; + padding-bottom: $w; + padding-top: $w; + } - &:before { - // NASA logo, dude - $w: 5%; - $m: 10px; - background-image: url('../ui/layout/assets/images/logo-nasa.svg'); - top: $m; - right: auto; - bottom: auto; - left: $m; - height: auto; - width: $w * 2; - padding-bottom: $w; - padding-top: $w; - } - - &:after { - // App logo - $d: 25%; - top: $d; - right: $d; - bottom: $d; - left: $d; - } + &:after { + // App logo + $d: 25%; + top: $d; + right: $d; + bottom: $d; + left: $d; + } } .c-about { - &--splash { - // Large initial image after click on app logo with text beneath - @include abs(); - display: flex; - flex-direction: column; - } + &--splash { + // Large initial image after click on app logo with text beneath + @include abs(); + display: flex; + flex-direction: column; + } + > * + * { + margin-top: $interiorMargin; + } + + &__image, + &__text { + flex: 1 1 auto; + } + + &__image { + height: 35%; + } + + &__text { + height: 65%; + overflow: auto; > * + * { - margin-top: $interiorMargin; + border-top: 1px solid $colorInteriorBorder; + margin-top: 1em; } + } - &__image, - &__text { - flex: 1 1 auto; + &--licenses { + padding: 0 10%; + .c-license { + + .c-license { + border-top: 1px solid $colorInteriorBorder; + margin-top: 2em; + } } + } - &__image { - height: 35%; - } + a { + color: $colorAboutLink; + } - &__text { - height: 65%; - overflow: auto; - > * + * { - border-top: 1px solid $colorInteriorBorder; - margin-top: 1em; - } - } + em { + color: pushBack($colorBodyFg, 20%); + } - &--licenses { - padding: 0 10%; - .c-license { - + .c-license { - border-top: 1px solid $colorInteriorBorder; - margin-top: 2em; - } - } - } + h1, + h2, + h3 { + font-weight: normal; + margin-bottom: 0.25em; + } - a { - color: $colorAboutLink; - } + h1 { + font-size: 2.25em; + } - em { - color: pushBack($colorBodyFg, 20%); - } - - h1, h2, h3 { - font-weight: normal; - margin-bottom: .25em; - } - - h1 { - font-size: 2.25em; - } - - h2 { - font-size: 1.5em; - } + h2 { + font-size: 1.5em; + } } diff --git a/src/styles/_animations.scss b/src/styles/_animations.scss index f95e73560c..d62a663402 100644 --- a/src/styles/_animations.scss +++ b/src/styles/_animations.scss @@ -1,91 +1,101 @@ @keyframes rotation { - 100% { transform: rotate(360deg); } + 100% { + transform: rotate(360deg); + } } @keyframes rotation-centered { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } @keyframes clock-hands { - 0% { transform: translate(-50%, -50%) rotate(0deg); } - 100% { transform: translate(-50%, -50%) rotate(360deg); } + 0% { + transform: translate(-50%, -50%) rotate(0deg); + } + 100% { + transform: translate(-50%, -50%) rotate(360deg); + } } @keyframes clock-hands-sticky { - 0% { - transform: translate(-50%, -50%) rotate(0deg); - } - 7% { - transform: translate(-50%, -50%) rotate(0deg); - } - 8% { - transform: translate(-50%, -50%) rotate(30deg); - } - 15% { - transform: translate(-50%, -50%) rotate(30deg); - } - 16% { - transform: translate(-50%, -50%) rotate(60deg); - } - 24% { - transform: translate(-50%, -50%) rotate(60deg); - } - 25% { - transform: translate(-50%, -50%) rotate(90deg); - } - 32% { - transform: translate(-50%, -50%) rotate(90deg); - } - 33% { - transform: translate(-50%, -50%) rotate(120deg); - } - 40% { - transform: translate(-50%, -50%) rotate(120deg); - } - 41% { - transform: translate(-50%, -50%) rotate(150deg); - } - 49% { - transform: translate(-50%, -50%) rotate(150deg); - } - 50% { - transform: translate(-50%, -50%) rotate(180deg); - } - 57% { - transform: translate(-50%, -50%) rotate(180deg); - } - 58% { - transform: translate(-50%, -50%) rotate(210deg); - } - 65% { - transform: translate(-50%, -50%) rotate(210deg); - } - 66% { - transform: translate(-50%, -50%) rotate(240deg); - } - 74% { - transform: translate(-50%, -50%) rotate(240deg); - } - 75% { - transform: translate(-50%, -50%) rotate(270deg); - } - 82% { - transform: translate(-50%, -50%) rotate(270deg); - } - 83% { - transform: translate(-50%, -50%) rotate(300deg); - } - 90% { - transform: translate(-50%, -50%) rotate(300deg); - } - 91% { - transform: translate(-50%, -50%) rotate(330deg); - } - 99% { - transform: translate(-50%, -50%) rotate(330deg); - } - 100% { - transform: translate(-50%, -50%) rotate(360deg); - } + 0% { + transform: translate(-50%, -50%) rotate(0deg); + } + 7% { + transform: translate(-50%, -50%) rotate(0deg); + } + 8% { + transform: translate(-50%, -50%) rotate(30deg); + } + 15% { + transform: translate(-50%, -50%) rotate(30deg); + } + 16% { + transform: translate(-50%, -50%) rotate(60deg); + } + 24% { + transform: translate(-50%, -50%) rotate(60deg); + } + 25% { + transform: translate(-50%, -50%) rotate(90deg); + } + 32% { + transform: translate(-50%, -50%) rotate(90deg); + } + 33% { + transform: translate(-50%, -50%) rotate(120deg); + } + 40% { + transform: translate(-50%, -50%) rotate(120deg); + } + 41% { + transform: translate(-50%, -50%) rotate(150deg); + } + 49% { + transform: translate(-50%, -50%) rotate(150deg); + } + 50% { + transform: translate(-50%, -50%) rotate(180deg); + } + 57% { + transform: translate(-50%, -50%) rotate(180deg); + } + 58% { + transform: translate(-50%, -50%) rotate(210deg); + } + 65% { + transform: translate(-50%, -50%) rotate(210deg); + } + 66% { + transform: translate(-50%, -50%) rotate(240deg); + } + 74% { + transform: translate(-50%, -50%) rotate(240deg); + } + 75% { + transform: translate(-50%, -50%) rotate(270deg); + } + 82% { + transform: translate(-50%, -50%) rotate(270deg); + } + 83% { + transform: translate(-50%, -50%) rotate(300deg); + } + 90% { + transform: translate(-50%, -50%) rotate(300deg); + } + 91% { + transform: translate(-50%, -50%) rotate(330deg); + } + 99% { + transform: translate(-50%, -50%) rotate(330deg); + } + 100% { + transform: translate(-50%, -50%) rotate(360deg); + } } diff --git a/src/styles/_constants-espresso.scss b/src/styles/_constants-espresso.scss index 4f43d85252..8c99132946 100644 --- a/src/styles/_constants-espresso.scss +++ b/src/styles/_constants-espresso.scss @@ -23,36 +23,36 @@ /************************************************** ESPRESSO THEME CONSTANTS */ // Fonts -$heroFont: "Helvetica Neue", Helvetica, Arial, sans-serif; +$heroFont: 'Helvetica Neue', Helvetica, Arial, sans-serif; $headerFont: $heroFont; $bodyFont: $heroFont; @mixin heroFont($size: 1em) { - font-family: $heroFont; - font-size: $size; + font-family: $heroFont; + font-size: $size; } @mixin headerFont($size: 1em) { - font-family: $headerFont; - font-size: $size; + font-family: $headerFont; + font-size: $size; } @mixin bodyFont($size: 1em) { - font-family: $bodyFont; - font-size: $size; + font-family: $bodyFont; + font-size: $size; } // Functions @function buttonBg($c: $colorBtnBg) { - @return linear-gradient(lighten($c, 5%), $c); + @return linear-gradient(lighten($c, 5%), $c); } @function pullForward($val, $amt) { - @return lighten($val, $amt); + @return lighten($val, $amt); } @function pushBack($val, $amt) { - @return darken($val, $amt); + @return darken($val, $amt); } // Constants @@ -73,8 +73,10 @@ $colorHeadFg: $colorBodyFg; $colorKey: #0099cc; $colorKeyFg: #fff; $colorKeyHov: lighten($colorKey, 10%); -$colorKeyFilter: invert(36%) sepia(76%) saturate(2514%) hue-rotate(170deg) brightness(99%) contrast(101%); -$colorKeyFilterHov: invert(63%) sepia(88%) saturate(3029%) hue-rotate(154deg) brightness(101%) contrast(100%); +$colorKeyFilter: invert(36%) sepia(76%) saturate(2514%) hue-rotate(170deg) brightness(99%) + contrast(101%); +$colorKeyFilterHov: invert(63%) sepia(88%) saturate(3029%) hue-rotate(154deg) brightness(101%) + contrast(100%); $colorKeySelectedBg: $colorKey; $uiColor: #0093ff; // Resize bars, splitter bars, etc. $colorInteriorBorder: rgba($colorBodyFg, 0.2); @@ -101,11 +103,14 @@ $sideBarHeaderFg: rgba($colorBodyFg, 0.7); $colorStatusFg: #888; $colorStatusDefault: #ccc; $colorStatusInfo: #60ba7b; -$colorStatusInfoFilter: invert(58%) sepia(44%) saturate(405%) hue-rotate(85deg) brightness(102%) contrast(92%); +$colorStatusInfoFilter: invert(58%) sepia(44%) saturate(405%) hue-rotate(85deg) brightness(102%) + contrast(92%); $colorStatusAlert: #ffb66c; -$colorStatusAlertFilter: invert(78%) sepia(26%) saturate(1160%) hue-rotate(324deg) brightness(107%) contrast(101%); +$colorStatusAlertFilter: invert(78%) sepia(26%) saturate(1160%) hue-rotate(324deg) brightness(107%) + contrast(101%); $colorStatusError: #da0004; -$colorStatusErrorFilter: invert(10%) sepia(96%) saturate(4360%) hue-rotate(351deg) brightness(111%) contrast(115%); +$colorStatusErrorFilter: invert(10%) sepia(96%) saturate(4360%) hue-rotate(351deg) brightness(111%) + contrast(115%); $colorStatusBtnBg: #666; // Where is this used? $colorStatusPartialBg: #3f5e8b; $colorStatusCompleteBg: #457638; @@ -114,11 +119,11 @@ $colorAlertFg: #fff; $colorError: #ff3c00; $colorErrorFg: #fff; $colorWarningHi: #990000; -$colorWarningHiFg: #FF9594; +$colorWarningHiFg: #ff9594; $colorWarningLo: #ff9900; $colorWarningLoFg: #523400; $colorDiagnostic: #a4b442; -$colorDiagnosticFg: #39461A; +$colorDiagnosticFg: #39461a; $colorCommand: #3693bd; $colorCommandFg: #fff; $colorInfo: #2294a2; @@ -164,7 +169,10 @@ $borderMissing: 1px dashed $colorAlert !important; $editUIColor: $uiColor; // Base color $editUIColorBg: $editUIColor; $editUIColorFg: #fff; -$editUIColorHov: pullForward(saturate($uiColor, 10%), 10%); // 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. @@ -183,7 +191,10 @@ $editFrameColorHandleBg: $colorBodyBg; // Resize handle 'offset' color to make h $editFrameColorHandleFg: $editFrameColorSelected; // Resize handle main color $editFrameSelectedShdw: rgba(black, 0.4) 0 1px 5px 1px; $editFrameMovebarColorBg: $editFrameColor; // Movebar bg color -$editFrameMovebarColorFg: pullForward($editFrameMovebarColorBg, 20%); // Grippy lines, container size text +$editFrameMovebarColorFg: pullForward( + $editFrameMovebarColorBg, + 20% +); // Grippy lines, container size text $editFrameHovMovebarColorBg: pullForward($editFrameMovebarColorBg, 10%); // Hover style $editFrameHovMovebarColorFg: pullForward($editFrameMovebarColorFg, 10%); $editFrameSelectedMovebarColorBg: pullForward($editFrameMovebarColorBg, 15%); // Selected style @@ -241,7 +252,7 @@ $controlDisabledOpacity: 0.2; $colorMenuBg: $colorBodyBg; $colorMenuFg: $colorBodyFg; $colorMenuIc: $colorKey; -$filterMenu: brightness(1.4); +$filterMenu: brightness(1.4); $colorMenuHovBg: rgba($colorKey, 0.5); $colorMenuHovFg: $colorBodyFgEm; $colorMenuHovIc: $colorMenuHovFg; @@ -314,25 +325,25 @@ $colorIndicatorMenuFgHov: pullForward($colorHeadFg, 10%); // Staleness $colorTelemStale: cyan; -$colorTelemStaleFg: #002A2A; +$colorTelemStaleFg: #002a2a; $styleTelemStale: italic; // Limits -$colorLimitYellowBg: #B18B05; -$colorLimitYellowFg: #FEEEB5; -$colorLimitYellowIc: #FDC707; -$colorLimitOrangeBg: #B36B00; -$colorLimitOrangeFg: #FFE0B2; +$colorLimitYellowBg: #b18b05; +$colorLimitYellowFg: #feeeb5; +$colorLimitYellowIc: #fdc707; +$colorLimitOrangeBg: #b36b00; +$colorLimitOrangeFg: #ffe0b2; $colorLimitOrangeIc: #ff9900; $colorLimitRedBg: #940000; $colorLimitRedFg: #ffa489; $colorLimitRedIc: #ff4222; -$colorLimitPurpleBg: #891BB3; -$colorLimitPurpleFg: #EDBEFF; +$colorLimitPurpleBg: #891bb3; +$colorLimitPurpleFg: #edbeff; $colorLimitPurpleIc: #c327ff; -$colorLimitCyanBg: #4BA6B3; -$colorLimitCyanFg: #D3FAFF; -$colorLimitCyanIc: #6BEDFF; +$colorLimitCyanBg: #4ba6b3; +$colorLimitCyanFg: #d3faff; +$colorLimitCyanIc: #6bedff; // Events $colorEventPurpleFg: #6433ff; @@ -473,36 +484,34 @@ $transIn: all $transInTime ease-in-out; $transOut: all $transOutTime ease-in-out; $transInTransform: transform $transInTime ease-in-out; $transOutTransform: transform $transOutTime ease-in-out; -$transInBounce: all 200ms cubic-bezier(.47,.01,.25,1.5); -$transInBounceBig: all 300ms cubic-bezier(.2,1.6,.6,3); +$transInBounce: all 200ms cubic-bezier(0.47, 0.01, 0.25, 1.5); +$transInBounceBig: all 300ms cubic-bezier(0.2, 1.6, 0.6, 3); // Discrete items, like Notebook entries, Widget rules $createBtnTextTransform: uppercase; -$colorDiscreteItemBg: rgba($colorBodyFg,0.1); -$colorDiscreteItemCurrentBg: rgba($colorOk,0.3); +$colorDiscreteItemBg: rgba($colorBodyFg, 0.1); +$colorDiscreteItemCurrentBg: rgba($colorOk, 0.3); $scrollContainer: $colorBodyBg; -; - @mixin discreteItem() { - background: $colorDiscreteItemBg; - border: none; - border-radius: $controlCr; + background: $colorDiscreteItemBg; + border: none; + border-radius: $controlCr; - .c-input-inline:hover { - background: $colorBodyBg; - } + .c-input-inline:hover { + background: $colorBodyBg; + } - &--current-match { - background: $colorDiscreteItemCurrentBg; - } + &--current-match { + background: $colorDiscreteItemCurrentBg; + } } @mixin discreteItemInnerElem() { - border: 1px solid rgba(#fff, 0.1); - border-radius: $controlCr; + border: 1px solid rgba(#fff, 0.1); + border-radius: $controlCr; } @mixin themedButton($c: $colorBtnBg) { - background: linear-gradient(pullForward($c, 5%), $c); - box-shadow: rgba(black, 0.5) 0 0.5px 2px; + background: linear-gradient(pullForward($c, 5%), $c); + box-shadow: rgba(black, 0.5) 0 0.5px 2px; } diff --git a/src/styles/_constants-maelstrom.scss b/src/styles/_constants-maelstrom.scss index 385c304ad5..73c4dc4d14 100644 --- a/src/styles/_constants-maelstrom.scss +++ b/src/styles/_constants-maelstrom.scss @@ -30,33 +30,33 @@ $headerFont: 'Michroma', sans-serif; $bodyFont: 'Chakra Petch', sans-serif; @mixin heroFont($size: 1em) { - font-family: $heroFont; - font-size: $size; + font-family: $heroFont; + font-size: $size; } @mixin headerFont($size: 1em) { - font-family: $headerFont; - font-size: $size * 0.8; // This font is comparatively large, so reduce it a bit - text-transform: uppercase; - word-spacing: 0.25em; + font-family: $headerFont; + font-size: $size * 0.8; // This font is comparatively large, so reduce it a bit + text-transform: uppercase; + word-spacing: 0.25em; } @mixin bodyFont($size: 1em) { - font-family: $bodyFont; - font-size: $size; + font-family: $bodyFont; + font-size: $size; } // Functions @function buttonBg($c: $colorBtnBg) { - @return linear-gradient(lighten($c, 5%), $c); + @return linear-gradient(lighten($c, 5%), $c); } @function pullForward($val, $amt) { - @return lighten($val, $amt); + @return lighten($val, $amt); } @function pushBack($val, $amt) { - @return darken($val, $amt); + @return darken($val, $amt); } // Constants @@ -77,8 +77,10 @@ $colorHeadFg: $colorBodyFg; $colorKey: #0099cc; $colorKeyFg: #fff; $colorKeyHov: #26d8ff; -$colorKeyFilter: invert(36%) sepia(76%) saturate(2514%) hue-rotate(170deg) brightness(99%) contrast(101%); -$colorKeyFilterHov: invert(63%) sepia(88%) saturate(3029%) hue-rotate(154deg) brightness(101%) contrast(100%); +$colorKeyFilter: invert(36%) sepia(76%) saturate(2514%) hue-rotate(170deg) brightness(99%) + contrast(101%); +$colorKeyFilterHov: invert(63%) sepia(88%) saturate(3029%) hue-rotate(154deg) brightness(101%) + contrast(100%); $colorKeySelectedBg: $colorKey; $uiColor: #0093ff; // Resize bars, splitter bars, etc. $colorInteriorBorder: rgba($colorBodyFg, 0.2); @@ -105,11 +107,14 @@ $sideBarHeaderFg: rgba($colorBodyFg, 0.7); $colorStatusFg: #999; $colorStatusDefault: #ccc; $colorStatusInfo: #60ba7b; -$colorStatusInfoFilter: invert(58%) sepia(44%) saturate(405%) hue-rotate(85deg) brightness(102%) contrast(92%); +$colorStatusInfoFilter: invert(58%) sepia(44%) saturate(405%) hue-rotate(85deg) brightness(102%) + contrast(92%); $colorStatusAlert: #ffb66c; -$colorStatusAlertFilter: invert(78%) sepia(26%) saturate(1160%) hue-rotate(324deg) brightness(107%) contrast(101%); +$colorStatusAlertFilter: invert(78%) sepia(26%) saturate(1160%) hue-rotate(324deg) brightness(107%) + contrast(101%); $colorStatusError: #da0004; -$colorStatusErrorFilter: invert(10%) sepia(96%) saturate(4360%) hue-rotate(351deg) brightness(111%) contrast(115%); +$colorStatusErrorFilter: invert(10%) sepia(96%) saturate(4360%) hue-rotate(351deg) brightness(111%) + contrast(115%); $colorStatusBtnBg: #666; // Where is this used? $colorStatusPartialBg: #3f5e8b; $colorStatusCompleteBg: #457638; @@ -118,11 +123,11 @@ $colorAlertFg: #fff; $colorError: #ff3c00; $colorErrorFg: #fff; $colorWarningHi: #990000; -$colorWarningHiFg: #FF9594; +$colorWarningHiFg: #ff9594; $colorWarningLo: #ff9900; $colorWarningLoFg: #523400; $colorDiagnostic: #a4b442; -$colorDiagnosticFg: #39461A; +$colorDiagnosticFg: #39461a; $colorCommand: #3693bd; $colorCommandFg: #fff; $colorInfo: #2294a2; @@ -168,7 +173,10 @@ $borderMissing: 1px dashed $colorAlert !important; $editUIColor: $uiColor; // Base color $editUIColorBg: $editUIColor; $editUIColorFg: #fff; -$editUIColorHov: pullForward(saturate($uiColor, 10%), 10%); // 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. @@ -187,7 +195,10 @@ $editFrameColorHandleBg: $colorBodyBg; // Resize handle 'offset' color to make h $editFrameColorHandleFg: $editFrameColorSelected; // Resize handle main color $editFrameSelectedShdw: rgba(black, 0.4) 0 1px 5px 1px; $editFrameMovebarColorBg: $editFrameColor; // Movebar bg color -$editFrameMovebarColorFg: pullForward($editFrameMovebarColorBg, 20%); // Grippy lines, container size text +$editFrameMovebarColorFg: pullForward( + $editFrameMovebarColorBg, + 20% +); // Grippy lines, container size text $editFrameHovMovebarColorBg: pullForward($editFrameMovebarColorBg, 10%); // Hover style $editFrameHovMovebarColorFg: pullForward($editFrameMovebarColorFg, 10%); $editFrameSelectedMovebarColorBg: pullForward($editFrameMovebarColorBg, 15%); // Selected style @@ -244,7 +255,7 @@ $controlDisabledOpacity: 0.2; $colorMenuBg: $colorBodyBg; $colorMenuFg: $colorBodyFg; $colorMenuIc: $colorKey; -$filterMenu: brightness(1.4); +$filterMenu: brightness(1.4); $colorMenuHovBg: rgba($colorKey, 0.5); $colorMenuHovFg: $colorBodyFgEm; $colorMenuHovIc: $colorMenuHovFg; @@ -317,25 +328,25 @@ $colorIndicatorMenuFgHov: pullForward($colorHeadFg, 10%); // Staleness $colorTelemStale: cyan; -$colorTelemStaleFg: #002A2A; +$colorTelemStaleFg: #002a2a; $styleTelemStale: italic; // Limits -$colorLimitYellowBg: #B18B05; -$colorLimitYellowFg: #FEEEB5; -$colorLimitYellowIc: #FDC707; -$colorLimitOrangeBg: #B36B00; -$colorLimitOrangeFg: #FFE0B2; +$colorLimitYellowBg: #b18b05; +$colorLimitYellowFg: #feeeb5; +$colorLimitYellowIc: #fdc707; +$colorLimitOrangeBg: #b36b00; +$colorLimitOrangeFg: #ffe0b2; $colorLimitOrangeIc: #ff9900; $colorLimitRedBg: #940000; $colorLimitRedFg: #ffa489; $colorLimitRedIc: #ff4222; -$colorLimitPurpleBg: #891BB3; -$colorLimitPurpleFg: #EDBEFF; +$colorLimitPurpleBg: #891bb3; +$colorLimitPurpleFg: #edbeff; $colorLimitPurpleIc: #c327ff; -$colorLimitCyanBg: #4BA6B3; -$colorLimitCyanFg: #D3FAFF; -$colorLimitCyanIc: #6BEDFF; +$colorLimitCyanBg: #4ba6b3; +$colorLimitCyanFg: #d3faff; +$colorLimitCyanIc: #6bedff; // Events $colorEventPurpleFg: #6433ff; @@ -476,47 +487,47 @@ $transIn: all $transInTime ease-in-out; $transOut: all $transOutTime ease-in-out; $transInTransform: transform $transInTime ease-in-out; $transOutTransform: transform $transOutTime ease-in-out; -$transInBounce: all 200ms cubic-bezier(.47,.01,.25,1.5); -$transInBounceBig: all 300ms cubic-bezier(.2,1.6,.6,3); +$transInBounce: all 200ms cubic-bezier(0.47, 0.01, 0.25, 1.5); +$transInBounceBig: all 300ms cubic-bezier(0.2, 1.6, 0.6, 3); // Discrete items, like Notebook entries, Widget rules $createBtnTextTransform: uppercase; -$colorDiscreteItemBg: rgba($colorBodyFg,0.1); -$colorDiscreteItemCurrentBg: rgba($colorOk,0.3); +$colorDiscreteItemBg: rgba($colorBodyFg, 0.1); +$colorDiscreteItemCurrentBg: rgba($colorOk, 0.3); $scrollContainer: $colorBodyBg; @mixin discreteItem() { - background: rgba($colorBodyFg,0.1); - border: none; - border-radius: $controlCr; + background: rgba($colorBodyFg, 0.1); + border: none; + border-radius: $controlCr; - &--current-match { - background: $colorDiscreteItemCurrentBg; - } + &--current-match { + background: $colorDiscreteItemCurrentBg; + } } @mixin discreteItemInnerElem() { - border: 1px solid rgba(#fff, 0.1); - border-radius: $controlCr; + border: 1px solid rgba(#fff, 0.1); + border-radius: $controlCr; } @mixin themedButton($c: $colorBtnBg) { - background: linear-gradient(pullForward($c, 5%), $c); - box-shadow: rgba(black, 0.5) 0 0.5px 2px; + background: linear-gradient(pullForward($c, 5%), $c); + box-shadow: rgba(black, 0.5) 0 0.5px 2px; } /**************************************************** OVERRIDES */ .c-frame { - &:not(.no-frame) { - $bc: #666; - $bLR: 3px solid transparent; - $br: 20px; - background: none !important; - border-radius: $br; - border-top: 4px solid $bc !important; - border-bottom: 2px solid $bc !important; - border-left: $bLR !important;; - border-right: $bLR !important;; - padding: 5px 10px 10px 10px !important; - } + &:not(.no-frame) { + $bc: #666; + $bLR: 3px solid transparent; + $br: 20px; + background: none !important; + border-radius: $br; + border-top: 4px solid $bc !important; + border-bottom: 2px solid $bc !important; + border-left: $bLR !important; + border-right: $bLR !important; + padding: 5px 10px 10px 10px !important; + } } diff --git a/src/styles/_constants-mobile.scss b/src/styles/_constants-mobile.scss index 5ff8a95019..89a41874fa 100644 --- a/src/styles/_constants-mobile.scss +++ b/src/styles/_constants-mobile.scss @@ -45,23 +45,23 @@ $tabMaxW: 1024px; $desktopMinW: 1025px; /************************** MEDIA QUERIES: WINDOW CHECKS FOR SPECIFIC ORIENTATIONS FOR EACH DEVICE */ -$screenPortrait: "(orientation: portrait)"; -$screenLandscape: "(orientation: landscape)"; +$screenPortrait: '(orientation: portrait)'; +$screenLandscape: '(orientation: landscape)'; //$mobileDevice: "(max-device-width: #{$tabMaxW})"; -$phoneCheck: "(max-device-width: #{$phoMaxW})"; -$tabletCheck: "(min-device-width: #{$tabMinW}) and (max-device-width: #{$tabMaxW})"; -$desktopCheck: "(min-device-width: #{$desktopMinW}) and (-webkit-min-device-pixel-ratio: 1)"; +$phoneCheck: '(max-device-width: #{$phoMaxW})'; +$tabletCheck: '(min-device-width: #{$tabMinW}) and (max-device-width: #{$tabMaxW})'; +$desktopCheck: '(min-device-width: #{$desktopMinW}) and (-webkit-min-device-pixel-ratio: 1)'; /************************** MEDIA QUERIES: WINDOWS FOR SPECIFIC ORIENTATIONS FOR EACH DEVICE */ -$phonePortrait: "only screen and #{$screenPortrait} and #{$phoneCheck}"; -$phoneLandscape: "only screen and #{$screenLandscape} and #{$phoneCheck}"; +$phonePortrait: 'only screen and #{$screenPortrait} and #{$phoneCheck}'; +$phoneLandscape: 'only screen and #{$screenLandscape} and #{$phoneCheck}'; -$tabletPortrait: "only screen and #{$screenPortrait} and #{$tabletCheck}"; -$tabletLandscape: "only screen and #{$screenLandscape} and #{$tabletCheck}"; +$tabletPortrait: 'only screen and #{$screenPortrait} and #{$tabletCheck}'; +$tabletLandscape: 'only screen and #{$screenLandscape} and #{$tabletCheck}'; -$desktop: "only screen and #{$desktopCheck}"; +$desktop: 'only screen and #{$desktopCheck}'; /************************** DEVICE PARAMETERS FOR MENUS/REPRESENTATIONS */ $proporMenuOnly: 90%; @@ -69,81 +69,81 @@ $proporMenuWithView: 40%; // Phones in any orientation @mixin phone { - @media #{$phonePortrait}, + @media #{$phonePortrait}, #{$phoneLandscape} { - @content - } + @content; + } } //Phones in portrait orientation @mixin phonePortrait { - @media #{$phonePortrait} { - @content - } + @media #{$phonePortrait} { + @content; + } } // Phones in landscape orientation @mixin phoneLandscape { - @media #{$phoneLandscape} { - @content - } + @media #{$phoneLandscape} { + @content; + } } // Tablets in any orientation @mixin tablet { - @media #{$tabletPortrait}, + @media #{$tabletPortrait}, #{$tabletLandscape} { - @content - } + @content; + } } // Tablets in portrait orientation @mixin tabletPortrait { - @media #{$tabletPortrait} { - @content - } + @media #{$tabletPortrait} { + @content; + } } // Tablets in landscape orientation @mixin tabletLandscape { - @media #{$tabletLandscape} { - @content - } + @media #{$tabletLandscape} { + @content; + } } // Phones and tablets in any orientation @mixin phoneandtablet { - @media #{$phonePortrait}, + @media #{$phonePortrait}, #{$phoneLandscape}, #{$tabletPortrait}, #{$tabletLandscape} { - @content - } + @content; + } } // Desktop monitors in any orientation @mixin desktopandtablet { - // Keeping only for legacy - should not be used moving forward - // Use body.desktop, body.tablet instead. - @media #{$tabletPortrait}, + // Keeping only for legacy - should not be used moving forward + // Use body.desktop, body.tablet instead. + @media #{$tabletPortrait}, #{$tabletLandscape}, #{$desktop} { - @content - } + @content; + } } // Desktop monitors in any orientation @mixin desktop { - // Keeping only for legacy - should not be used moving forward - // Use body.desktop instead. - @media #{$desktop} { - @content - } + // Keeping only for legacy - should not be used moving forward + // Use body.desktop instead. + @media #{$desktop} { + @content; + } } // Transition used for the slide menu @mixin slMenuTransitions { - @include transition-duration(.35s); - transition-timing-function: ease; - backface-visibility: hidden; + @include transition-duration(0.35s); + transition-timing-function: ease; + backface-visibility: hidden; } diff --git a/src/styles/_constants-snow.scss b/src/styles/_constants-snow.scss index b5a0bc360c..5bd4ba4d54 100644 --- a/src/styles/_constants-snow.scss +++ b/src/styles/_constants-snow.scss @@ -23,36 +23,36 @@ /****************************************************** SNOW THEME CONSTANTS */ // Fonts -$heroFont: "Helvetica Neue", Helvetica, Arial, sans-serif; +$heroFont: 'Helvetica Neue', Helvetica, Arial, sans-serif; $headerFont: $heroFont; $bodyFont: $heroFont; @mixin heroFont($size: 1em) { - font-family: $heroFont; - font-size: $size; + font-family: $heroFont; + font-size: $size; } @mixin headerFont($size: 1em) { - font-family: $headerFont; - font-size: $size; + font-family: $headerFont; + font-size: $size; } @mixin bodyFont($size: 1em) { - font-family: $bodyFont; - font-size: $size; + font-family: $bodyFont; + font-size: $size; } // Functions @function buttonBg($c: $colorBtnBg) { - @return $c; + @return $c; } @function pullForward($val, $amt) { - @return darken($val, $amt); + @return darken($val, $amt); } @function pushBack($val, $amt) { - @return lighten($val, $amt); + @return lighten($val, $amt); } // General @@ -73,8 +73,10 @@ $colorHeadFg: $colorBodyFg; $colorKey: #0099cc; $colorKeyFg: #fff; $colorKeyHov: #00c0f6; -$colorKeyFilter: invert(37%) sepia(100%) saturate(686%) hue-rotate(157deg) brightness(102%) contrast(102%); -$colorKeyFilterHov: invert(69%) sepia(87%) saturate(3243%) hue-rotate(151deg) brightness(97%) contrast(102%); +$colorKeyFilter: invert(37%) sepia(100%) saturate(686%) hue-rotate(157deg) brightness(102%) + contrast(102%); +$colorKeyFilterHov: invert(69%) sepia(87%) saturate(3243%) hue-rotate(151deg) brightness(97%) + contrast(102%); $colorKeySelectedBg: $colorKey; $uiColor: #289fec; // Resize bars, splitter bars, etc. $colorInteriorBorder: rgba($colorBodyFg, 0.2); @@ -101,11 +103,14 @@ $sideBarHeaderFg: rgba($colorBodyFg, 0.7); $colorStatusFg: #999; $colorStatusDefault: #ccc; $colorStatusInfo: #60ba7b; -$colorStatusInfoFilter: invert(64%) sepia(42%) saturate(416%) hue-rotate(85deg) brightness(93%) contrast(93%); +$colorStatusInfoFilter: invert(64%) sepia(42%) saturate(416%) hue-rotate(85deg) brightness(93%) + contrast(93%); $colorStatusAlert: #ff8a0d; -$colorStatusAlertFilter: invert(89%) sepia(26%) saturate(5035%) hue-rotate(316deg) brightness(114%) contrast(107%); +$colorStatusAlertFilter: invert(89%) sepia(26%) saturate(5035%) hue-rotate(316deg) brightness(114%) + contrast(107%); $colorStatusError: #da0004; -$colorStatusErrorFilter: invert(8%) sepia(96%) saturate(4511%) hue-rotate(352deg) brightness(136%) contrast(114%); +$colorStatusErrorFilter: invert(8%) sepia(96%) saturate(4511%) hue-rotate(352deg) brightness(136%) + contrast(114%); $colorStatusBtnBg: #666; // Where is this used? $colorStatusPartialBg: #c9d6ff; $colorStatusCompleteBg: #a4e4b4; @@ -114,11 +119,11 @@ $colorAlertFg: #fff; $colorError: #ff3c00; $colorErrorFg: #fff; $colorWarningHi: #990000; -$colorWarningHiFg: #FF9594; +$colorWarningHiFg: #ff9594; $colorWarningLo: #ff9900; $colorWarningLoFg: #523400; $colorDiagnostic: #a4b442; -$colorDiagnosticFg: #39461A; +$colorDiagnosticFg: #39461a; $colorCommand: #3693bd; $colorCommandFg: #fff; $colorInfo: #2294a2; @@ -148,7 +153,7 @@ $colorTOI: $colorBodyFg; // was $timeControllerToiLineColor $colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov $timeConductorAxisHoverFilter: brightness(0.8); $timeConductorActiveBg: $colorKey; -$timeConductorActivePanBg: #A0CDE1; +$timeConductorActivePanBg: #a0cde1; /************************************************** BROWSING */ $browseFrameColor: pullForward($colorBodyBg, 10%); @@ -164,7 +169,10 @@ $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%), + 20% +); // Hover color when $editUIColor is applied as a base color $editUIBaseColor: #cae1ff; // Base color, toolbar bg $editUIBaseColorHov: pushBack($editUIBaseColor, 20%); $editUIBaseColorFg: #4c4c4c; // Toolbar button icon colors, etc. @@ -183,7 +191,10 @@ $editFrameColorHandleBg: $colorBodyBg; // Resize handle 'offset' color to make h $editFrameColorHandleFg: $editFrameColorSelected; // Resize handle main color $editFrameSelectedShdw: rgba(black, 0.5) 0 1px 5px 2px; $editFrameMovebarColorBg: $editFrameColor; // Movebar bg color -$editFrameMovebarColorFg: pullForward($editFrameMovebarColorBg, 20%); // Grippy lines, container size text +$editFrameMovebarColorFg: pullForward( + $editFrameMovebarColorBg, + 20% +); // Grippy lines, container size text $editFrameHovMovebarColorBg: pullForward($editFrameMovebarColorBg, 10%); // Hover style $editFrameHovMovebarColorFg: pullForward($editFrameMovebarColorFg, 10%); $editFrameSelectedMovebarColorBg: pullForward($editFrameMovebarColorBg, 15%); // Selected style @@ -297,7 +308,7 @@ $colorTabCurrentBg: $colorBodyFg; //pullForward($colorTabBg, 10%); $colorTabCurrentFg: $colorBodyBg; //pullForward($colorTabFg, 10%); $colorTabsBaseline: $colorTabCurrentBg; - // Overlay +// Overlay $colorOvrBlocker: rgba(black, 0.7); $overlayCr: $interiorMarginLg; @@ -314,24 +325,24 @@ $colorIndicatorMenuFgHov: pullForward($colorHeadFg, 10%); // Staleness $colorTelemStale: #00c9c9; -$colorTelemStaleFg: #002A2A; +$colorTelemStaleFg: #002a2a; $styleTelemStale: italic; // Limits $colorLimitYellowBg: #ffe64d; $colorLimitYellowFg: #7f4f20; $colorLimitYellowIc: #e7a115; -$colorLimitOrangeBg: #B36B00; -$colorLimitOrangeFg: #FFE0B2; +$colorLimitOrangeBg: #b36b00; +$colorLimitOrangeFg: #ffe0b2; $colorLimitOrangeIc: #ff9900; $colorLimitRedBg: #ff0000; $colorLimitRedFg: #fff; $colorLimitRedIc: #ffa99a; -$colorLimitPurpleBg: #891BB3; -$colorLimitPurpleFg: #EDBEFF; +$colorLimitPurpleBg: #891bb3; +$colorLimitPurpleFg: #edbeff; $colorLimitPurpleIc: #c327ff; -$colorLimitCyanBg: #4BA6B3; -$colorLimitCyanFg: #D3FAFF; +$colorLimitCyanBg: #4ba6b3; +$colorLimitCyanFg: #d3faff; $colorLimitCyanIc: #1795c0; // Events @@ -473,34 +484,34 @@ $transIn: all $transInTime ease-in-out; $transOut: all $transOutTime ease-in-out; $transInTransform: transform $transInTime ease-in-out; $transOutTransform: transform $transOutTime ease-in-out; -$transInBounce: all 200ms cubic-bezier(.47,.01,.25,1.5); -$transInBounceBig: all 300ms cubic-bezier(.2,1.6,.6,3); +$transInBounce: all 200ms cubic-bezier(0.47, 0.01, 0.25, 1.5); +$transInBounceBig: all 300ms cubic-bezier(0.2, 1.6, 0.6, 3); // Discrete items, like Notebook entries, Widget rules $createBtnTextTransform: uppercase; -$colorDiscreteItemBg: rgba($colorBodyFg,0.1); -$colorDiscreteItemCurrentBg: rgba($colorOk,0.3); +$colorDiscreteItemBg: rgba($colorBodyFg, 0.1); +$colorDiscreteItemCurrentBg: rgba($colorOk, 0.3); $scrollContainer: rgba(102, 102, 102, 0.1); @mixin discreteItem() { - background: $colorDiscreteItemBg; - border: 1px solid $colorInteriorBorder; - border-radius: $controlCr; + background: $colorDiscreteItemBg; + border: 1px solid $colorInteriorBorder; + border-radius: $controlCr; - &--current-match { - background: $colorDiscreteItemCurrentBg; - } + &--current-match { + background: $colorDiscreteItemCurrentBg; + } - .c-input-inline:hover { - background: $colorBodyBg; - } + .c-input-inline:hover { + background: $colorBodyBg; + } } @mixin discreteItemInnerElem() { - border: 1px solid $colorBodyBg; - border-radius: $controlCr; + border: 1px solid $colorBodyBg; + border-radius: $controlCr; } @mixin themedButton($c: $colorBtnBg) { - background: $c; + background: $c; } diff --git a/src/styles/_constants.scss b/src/styles/_constants.scss index 19d2b24263..8718a918ad 100755 --- a/src/styles/_constants.scss +++ b/src/styles/_constants.scss @@ -109,7 +109,7 @@ $colorProgressBar: #0085ad; $progressAnimW: 500px; $progressBarMinH: 4px; /************************** FONT STYLING */ -$listFontSizes: 8,9,10,11,12,13,14,16,18,20,24,28,32,36,42,48,72,96,128; +$listFontSizes: 8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 24, 28, 32, 36, 42, 48, 72, 96, 128; /************************** GLYPH CHAR UNICODES */ $glyph-icon-alert-rect: '\e900'; diff --git a/src/styles/_controls.scss b/src/styles/_controls.scss index 5e321aa5ee..636ee98c77 100644 --- a/src/styles/_controls.scss +++ b/src/styles/_controls.scss @@ -24,224 +24,224 @@ /******************************************************** 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; + 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; - } + > * { + flex: 0 0 auto; + } } @mixin menuPositioning() { - display: flex; - flex-direction: column; - position: absolute; - z-index: 100; + display: flex; + flex-direction: column; + position: absolute; + z-index: 100; - > * { - flex: 0 0 auto; - } + > * { + 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; + 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 - } - - } + @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 { - @include htmlInputReset(); + @include htmlInputReset(); } .c-button, .c-button--menu { - @include cButton(); + @include cButton(); } .c-button { - &--menu { - &:after { - content: $glyph-icon-arrow-down; - font-family: symbolsfont; - opacity: 0.5; - } + &--menu { + &:after { + content: $glyph-icon-arrow-down; + font-family: symbolsfont; + opacity: 0.5; + } + } + + &--swatched { + // Used with c-button--menu: a visual button with a larger swatch element adjacent to an icon + .c-swatch { + $d: 12px; + margin-left: $interiorMarginSm; + height: $d; + width: $d; + } + } + + &[class*='__collapse-button'] { + box-shadow: none; + background: $splitterBtnColorBg; + color: $splitterBtnColorFg; + border-radius: $smallCr; + line-height: 90%; + padding: 3px 10px; + + @include desktop() { + font-size: 6px; } - &--swatched { - // Used with c-button--menu: a visual button with a larger swatch element adjacent to an icon - .c-swatch { - $d: 12px; - margin-left: $interiorMarginSm; - height: $d; width: $d; - } + &:before { + content: $glyph-icon-arrow-down; + font-size: 1.1em; } + } - &[class*='__collapse-button'] { - box-shadow: none; - background: $splitterBtnColorBg; - color: $splitterBtnColorFg; - border-radius: $smallCr; - line-height: 90%; - padding: 3px 10px; + &.is-active { + background: $colorBtnActiveBg; + color: $colorBtnActiveFg; + } - @include desktop() { - font-size: 6px; - } - - &:before { - content: $glyph-icon-arrow-down; - font-size: 1.1em; - } - } - - &.is-active { - background: $colorBtnActiveBg; - color: $colorBtnActiveFg; - } - - &.is-selected { - background: $colorBtnSelectedBg; - color: $colorBtnSelectedFg; - } + &.is-selected { + background: $colorBtnSelectedBg; + color: $colorBtnSelectedFg; + } } /********* Icon Buttons and Links */ .c-click-icon { - @include cClickIcon(); + @include cClickIcon(); - &--section-collapse { - color: inherit; - display: block; - transition: transform $transOutTime; - &:before { - content: $glyph-icon-arrow-down; - font-family: symbolsfont; - } - - &.is-collapsed { - transform: rotate(180deg); - } + &--section-collapse { + color: inherit; + display: block; + transition: transform $transOutTime; + &:before { + content: $glyph-icon-arrow-down; + font-family: symbolsfont; } + + &.is-collapsed { + transform: rotate(180deg); + } + } } .c-click-link, .c-icon-link { - // A clickable element, typically inline, with an icon and label - @include cControl(); - cursor: pointer; + // A clickable element, typically inline, with an icon and label + @include cControl(); + cursor: pointer; } .c-icon-button, .c-click-swatch { - @include cClickIconButton(); + @include cClickIconButton(); - &--menu { - @include hasMenu(); - } + &--menu { + @include hasMenu(); + } } .c-icon-button--disabled { - @include cClickIconButtonLayout(); + @include cClickIconButtonLayout(); } .c-icon-link { - &:before { - // Icon - //color: $colorBtnMajorBg; - } + &:before { + // Icon + //color: $colorBtnMajorBg; + } } .c-icon-button { - [class*='label'] { - opacity: 0.8; - padding: 1px 0; + [class*='label'] { + opacity: 0.8; + padding: 1px 0; + } + + &--mixed { + @include mixedBg(); + } + + &--swatched { + // Color control, show swatch element + display: flex; + flex-flow: column nowrap; + align-items: center; + justify-content: center; + + > [class*='swatch'] { + box-shadow: inset rgba($editUIBaseColorFg, 0.2) 0 0 0 1px; + flex: 0 0 auto; + height: 5px; + width: 100%; + margin-top: 1px; } - &--mixed { - @include mixedBg(); - } - - &--swatched { - // Color control, show swatch element - display: flex; - flex-flow: column nowrap; - align-items: center; - justify-content: center; - - > [class*='swatch'] { - box-shadow: inset rgba($editUIBaseColorFg, 0.2) 0 0 0 1px; - flex: 0 0 auto; - height: 5px; - width: 100%; - margin-top: 1px; - } - - &:before { - // Reduce size of icon to make a bit of room - flex: 1 1 auto; - font-size: 1.1em; - } + &:before { + // Reduce size of icon to make a bit of room + flex: 1 1 auto; + font-size: 1.1em; } + } } .c-list-button { - @include cControl(); - color: $colorBodyFg; - cursor: pointer; - justify-content: start; - padding: $interiorMargin; + @include cControl(); + color: $colorBodyFg; + cursor: pointer; + justify-content: start; + padding: $interiorMargin; - > * + * { - margin-left: $interiorMargin; - } + > * + * { + margin-left: $interiorMargin; + } - @include hover() { - background: $colorItemTreeHoverBg; - } + @include hover() { + background: $colorItemTreeHoverBg; + } - .c-button { - flex: 0 0 auto; - } + .c-button { + flex: 0 0 auto; + } } /******************************************************** DISCLOSURE CONTROLS */ @@ -249,340 +249,347 @@ button { // Provides a downward arrow icon that when clicked displays additional options and/or info. // Always placed AFTER an element .c-disclosure-button { - @include cClickIcon(); - margin-left: $interiorMarginSm; + @include cClickIcon(); + margin-left: $interiorMarginSm; - &:before { - content: $glyph-icon-arrow-down; - font-family: symbolsfont; - font-size: 0.7em; - } + &:before { + content: $glyph-icon-arrow-down; + font-family: symbolsfont; + font-size: 0.7em; + } } /********* Disclosure Triangle */ // Provides an arrow icon that when clicked expands an element to reveal its contents. // Used in tree items, plot legends. Always placed BEFORE an element. .c-disclosure-triangle { - $d: 12px; - color: $colorDisclosureCtrl; - display: flex; - align-items: center; - justify-content: center; - flex: 0 0 auto; - width: $d; - position: relative; - visibility: hidden; + $d: 12px; + color: $colorDisclosureCtrl; + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + width: $d; + position: relative; + visibility: hidden; - &.is-enabled { - cursor: pointer; - visibility: visible; + &.is-enabled { + cursor: pointer; + visibility: visible; - &:hover { - color: $colorDisclosureCtrlHov; - } - - &:before { - $s: .65; - content: $glyph-icon-arrow-right-equilateral; - display: block; - font-family: symbolsfont; - font-size: 1rem * $s; - } + &:hover { + color: $colorDisclosureCtrlHov; } - &--expanded { - &:before { - transform: rotate(90deg); - } + &:before { + $s: 0.65; + content: $glyph-icon-arrow-right-equilateral; + display: block; + font-family: symbolsfont; + font-size: 1rem * $s; } + } + + &--expanded { + &:before { + transform: rotate(90deg); + } + } } /******************************************************** DRAG AFFORDANCES */ .c-grippy { - $d: 10px; - @include grippy($c: $colorItemTreeVC, $dir: 'y'); - width: $d; height: $d; + $d: 10px; + @include grippy($c: $colorItemTreeVC, $dir: 'y'); + width: $d; + height: $d; - &--vertical-drag { - cursor: ns-resize; - } + &--vertical-drag { + cursor: ns-resize; + } } /******************************************************** SECTION */ section { - flex: 0 1 auto; - overflow: hidden; - + section { - margin-top: $interiorMargin; - } + flex: 0 1 auto; + overflow: hidden; + + section { + margin-top: $interiorMargin; + } - .c-section__header { - @include propertiesHeader(); - display: flex; - flex: 0 0 auto; - align-items: center; - margin-bottom: $interiorMargin; + .c-section__header { + @include propertiesHeader(); + display: flex; + flex: 0 0 auto; + align-items: center; + margin-bottom: $interiorMargin; - > * + * { margin-left: $interiorMarginSm; } + > * + * { + margin-left: $interiorMarginSm; } + } - > [class*='__label'] { - flex: 1 1 auto; - text-transform: uppercase; - } + > [class*='__label'] { + flex: 1 1 auto; + text-transform: uppercase; + } } /******************************************************** FORM ELEMENTS */ -input, textarea { - font-family: inherit; - font-weight: inherit; - letter-spacing: inherit; +input, +textarea { + font-family: inherit; + font-weight: inherit; + letter-spacing: inherit; } -input[type=text], -input[type=search], -input[type=number], -input[type=password], -input[type=date], +input[type='text'], +input[type='search'], +input[type='number'], +input[type='password'], +input[type='date'], textarea { - @include reactive-input(); - &.numeric { - text-align: right; - } + @include reactive-input(); + &.numeric { + text-align: right; + } } -input[type=text], -input[type=search], -input[type=password], -input[type=date], +input[type='text'], +input[type='search'], +input[type='password'], +input[type='date'], textarea { - padding: $inputTextP; + padding: $inputTextP; } .c-input { - &--flex { - width: 100%; - min-width: 20px; + &--flex { + width: 100%; + min-width: 20px; + } + + &--datetime { + // Sized for values such as 2018-09-28 22:32:33.468Z + width: 160px; + } + + &--hrs-min-sec { + // Sized for values such as 00:25:00 + width: 60px; + } + + &-inline, + &--inline { + // A text input or contenteditable element that indicates edit affordance on hover and looks like an input on focus + @include inlineInput; + + &:hover, + &:focus { + background: $colorInputBg; + padding-left: $inputTextPLeftRight; + padding-right: $inputTextPLeftRight; } + } - &--datetime { - // Sized for values such as 2018-09-28 22:32:33.468Z - width: 160px; - } - - &--hrs-min-sec { - // Sized for values such as 00:25:00 - width: 60px; - } - - &-inline, - &--inline { - // A text input or contenteditable element that indicates edit affordance on hover and looks like an input on focus - @include inlineInput; - - &:hover, - &:focus { - background: $colorInputBg; - padding-left: $inputTextPLeftRight; - padding-right: $inputTextPLeftRight; - } - } - - &--labeled { - // TODO: replace .c-labeled-input with this - // An input used in the Toolbar - // Assumes label is before the input - @include cControl(); - - input { - margin-left: $interiorMarginSm; - } - } - - &--sm { - // Small inputs, like small numerics - width: 40px; - } - - &--autocomplete { - &__wrapper { - display: flex; - flex-direction: row; - align-items: center; - overflow: hidden; - width: 100%; - } - - &__input { - min-width: 100px; - width: 100%; - - // Fend off from afford-arrow - 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 { - $p: 2px; - font-size: 0.8em; - padding-bottom: $p; - padding-top: $p; - position: absolute; - right: 2px; - z-index: 2; - } - } -} - -input[type=number].c-input-number--no-spinners { - &::-webkit-inner-spin-button, - &::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; - } - -moz-appearance: textfield; -} - -.c-labeled-input { + &--labeled { + // TODO: replace .c-labeled-input with this // An input used in the Toolbar // Assumes label is before the input @include cControl(); input { - margin-left: $interiorMarginSm; + margin-left: $interiorMarginSm; } + } + + &--sm { + // Small inputs, like small numerics + width: 40px; + } + + &--autocomplete { + &__wrapper { + display: flex; + flex-direction: row; + align-items: center; + overflow: hidden; + width: 100%; + } + + &__input { + min-width: 100px; + width: 100%; + + // Fend off from afford-arrow + 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 { + $p: 2px; + font-size: 0.8em; + padding-bottom: $p; + padding-top: $p; + position: absolute; + right: 2px; + z-index: 2; + } + } } -.c-scrollcontainer{ - @include nice-input(); - margin-top: $interiorMargin; - background: $scrollContainer; - border-radius: $controlCr; - overflow: auto; - padding: $interiorMarginSm; +input[type='number'].c-input-number--no-spinners { + &::-webkit-inner-spin-button, + &::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } + -moz-appearance: textfield; +} + +.c-labeled-input { + // An input used in the Toolbar + // Assumes label is before the input + @include cControl(); + + input { + margin-left: $interiorMarginSm; + } +} + +.c-scrollcontainer { + @include nice-input(); + margin-top: $interiorMargin; + background: $scrollContainer; + border-radius: $controlCr; + overflow: auto; + padding: $interiorMarginSm; } // SELECTS select { - @include appearanceNone(); - background-color: $colorSelectBg; - 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"); - color: $colorSelectFg; - box-shadow: $shdwSelect; - background-repeat: no-repeat, no-repeat; - background-position: right .4em top 80%, 0 0; - border: none; - border-radius: $controlCr; - padding: 2px 20px 2px $interiorMargin; + @include appearanceNone(); + background-color: $colorSelectBg; + 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"); + color: $colorSelectFg; + box-shadow: $shdwSelect; + background-repeat: no-repeat, no-repeat; + background-position: right 0.4em top 80%, 0 0; + border: none; + border-radius: $controlCr; + padding: 2px 20px 2px $interiorMargin; - *, - option { - background: $colorBtnBg; - color: $colorBtnFg; - } + *, + option { + background: $colorBtnBg; + color: $colorBtnFg; + } } // CHECKBOX LISTS // __input followed by __label .c-checkbox-list { - // Rows - &__row + &__row { margin-top: $interiorMarginSm; } + // Rows + &__row + &__row { + margin-top: $interiorMarginSm; + } - // input and label in each __row - &__row { - > * + * { margin-left: $interiorMargin; } + // input and label in each __row + &__row { + > * + * { + margin-left: $interiorMargin; } + } - li { - white-space: nowrap; - } + li { + white-space: nowrap; + } } /******************************************************** TABS */ .c-tabs { - // Single horizontal strip of tabs, with a bottom divider line - @include userSelectNone(); - display: flex; - flex: 0 0 auto; - flex-wrap: wrap; - position: relative; // Required in case this is applied to a
    + diff --git a/src/ui/layout/search/search.scss b/src/ui/layout/search/search.scss index fe9c11d2e5..91f9173b13 100644 --- a/src/ui/layout/search/search.scss +++ b/src/ui/layout/search/search.scss @@ -22,123 +22,123 @@ /******************************* EXPANDED SEARCH 2022 */ .c-gsearch { - .l-shell__head & { - // Search input in the shell head - width: 20%; + .l-shell__head & { + // Search input in the shell head + width: 20%; - .c-search { - background: rgba($colorHeadFg, 0.2); - box-shadow: none; - } + .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-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, - &__results-section { - flex: 1 1 auto; + &__results-section { + > * + * { + margin-top: $interiorMarginSm; } + } - &__results { - // Holds n __results-sections - padding-right: $interiorMargin; // Fend off scrollbar - overflow-y: auto; + &__results-section-title { + @include propertiesHeader(); + } - > * + * { - margin-top: $interiorMarginLg; - } - } - - &__results-section { - > * + * { - margin-top: $interiorMarginSm; - } - } - - &__results-section-title { - @include propertiesHeader(); - } - - &__result-pane-msg { - > * + * { - margin-top: $interiorMargin; - } + &__result-pane-msg { + > * + * { + margin-top: $interiorMargin; } + } } .c-gsearch-result { - display: flex; - padding: $interiorMargin $interiorMarginSm; + 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-left: $interiorMarginLg; + margin-top: $interiorMarginSm; } - + .c-gsearch-result { - border-top: 1px solid $colorInteriorBorder; + .c-location { + font-size: 0.9em; + opacity: 0.8; } + } - &__type-icon, - &__more-options-button { - flex: 0 0 auto; + &__tags { + display: flex; + + > * + * { + margin-left: $interiorMargin; } + } - &__type-icon { - color: $colorItemTreeIcon; - font-size: 2.2em; + &__title { + border-radius: $basicCr; + color: pullForward($colorBodyFg, 30%); + cursor: pointer; + font-size: 1.15em; + padding: 3px $interiorMarginSm; - // TEMP: uses object-label component, hide label part - .c-object-label__name { - display: none; - } + &:hover { + background-color: $colorItemTreeHoverBg; } + } - &__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; - } + .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 ebcd749d60..886019a7d7 100644 --- a/src/ui/layout/status-bar/Indicators.vue +++ b/src/ui/layout/status-bar/Indicators.vue @@ -17,26 +17,25 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/layout/status-bar/NotificationBanner.vue b/src/ui/layout/status-bar/NotificationBanner.vue index 00590185d5..6f6c4bf1fe 100644 --- a/src/ui/layout/status-bar/NotificationBanner.vue +++ b/src/ui/layout/status-bar/NotificationBanner.vue @@ -17,37 +17,39 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/layout/status-bar/indicators.scss b/src/ui/layout/status-bar/indicators.scss index ea51401bc1..957356f9e9 100644 --- a/src/ui/layout/status-bar/indicators.scss +++ b/src/ui/layout/status-bar/indicators.scss @@ -1,132 +1,134 @@ .c-indicator { - @include cControl(); - @include cClickIconButtonLayout(); - border-radius: $controlCr; - overflow: visible; - position: relative; + @include cControl(); + @include cClickIconButtonLayout(); + border-radius: $controlCr; + overflow: visible; + position: relative; + text-transform: uppercase; + + button { text-transform: uppercase; + } - button { text-transform: uppercase; } + &.no-minify { + // For items that cannot be minified + display: flex; + flex-flow: row nowrap; + align-items: center; - &.no-minify { - // For items that cannot be minified - display: flex; - flex-flow: row nowrap; - align-items: center; - - > *, - &:before { - flex: 1 1 auto; - } - - &:before { - margin-right: $interiorMarginSm; - } + > *, + &:before { + flex: 1 1 auto; } - &:not(.no-minify) { - &:before { - margin-right: 0 !important; - } + &:before { + margin-right: $interiorMarginSm; } + } + + &:not(.no-minify) { + &:before { + margin-right: 0 !important; + } + } } .c-indicator__label { - // Label element. Appears as a hover bubble element when Indicators are minified; - // Appears as an inline element when not. - display: inline-block; - transition:none; - white-space: nowrap; + // Label element. Appears as a hover bubble element when Indicators are minified; + // Appears as an inline element when not. + display: inline-block; + transition: none; + white-space: nowrap; - a, - button, - .s-button, - .c-button { - // Make in label look like buttons - @include transition(background-color); - background-color: transparent; - border: 1px solid rgba($colorIndicatorMenuFg, 0.5); - border-radius: $controlCr; - box-sizing: border-box; - color: inherit; - font-size: inherit; - height: auto; - line-height: normal; - padding: 0 2px; - @include hover { - background-color: rgba($colorIndicatorMenuFg, 0.1); - border-color: rgba($colorIndicatorMenuFg, 0.75); - color: $colorIndicatorMenuFgHov; - } + a, + button, + .s-button, + .c-button { + // Make in label look like buttons + @include transition(background-color); + background-color: transparent; + border: 1px solid rgba($colorIndicatorMenuFg, 0.5); + border-radius: $controlCr; + box-sizing: border-box; + color: inherit; + font-size: inherit; + height: auto; + line-height: normal; + padding: 0 2px; + @include hover { + background-color: rgba($colorIndicatorMenuFg, 0.1); + border-color: rgba($colorIndicatorMenuFg, 0.75); + color: $colorIndicatorMenuFgHov; } + } - [class*='icon-'] { - // If any elements within label include the class 'icon-*' then deal with their :before's - &:before { - font-size: 0.8em; - margin-right: $interiorMarginSm; - } + [class*='icon-'] { + // If any elements within label include the class 'icon-*' then deal with their :before's + &:before { + font-size: 0.8em; + margin-right: $interiorMarginSm; } + } } .c-indicator__count { - display: none; // Only displays when Indicator is minified, see below + display: none; // Only displays when Indicator is minified, see below } [class*='minify-indicators'] { - // All styles for minified Indicators should go in here - .c-indicator:not(.no-minify) { - border: 1px solid transparent; // Hack to make minified sizing work in Safari. Have no idea why this works. + // All styles for minified Indicators should go in here + .c-indicator:not(.no-minify) { + border: 1px solid transparent; // Hack to make minified sizing work in Safari. Have no idea why this works. + overflow: visible; + transition: transform; + + @include hover() { + background: $colorIndicatorBgHov; + transition: transform 250ms ease-in 200ms; // Go-away transition + + .c-indicator__label { + box-shadow: $colorIndicatorMenuBgShdw; + transform: scale(1); overflow: visible; - transition: transform; - - @include hover() { - background: $colorIndicatorBgHov; - transition: transform 250ms ease-in 200ms; // Go-away transition - - .c-indicator__label { - box-shadow: $colorIndicatorMenuBgShdw; - transform: scale(1.0); - overflow: visible; - transition: transform 100ms ease-out 100ms; // Appear transition - } - } - .c-indicator__label { - transition: transform 250ms ease-in 200ms; // Go-away transition - background: $colorIndicatorMenuBg; - color: $colorIndicatorMenuFg; - border-radius: $controlCr; - right: 0; - top: 130%; - padding: $interiorMargin $interiorMargin; - position: absolute; - transform-origin: 90% 0; - transform: scale(0.0); - overflow: hidden; - z-index: 50; - - &:before { - // Infobubble-style arrow element - content: ''; - display: block; - position: absolute; - bottom: 100%; - right: 8px; - @include triangle('up', $size: 4px, $ratio: 1, $color: $colorIndicatorMenuBg); - } - } - - .c-indicator__count { - display: inline-block; - margin-left: $interiorMarginSm; - } + transition: transform 100ms ease-out 100ms; // Appear transition + } } + .c-indicator__label { + transition: transform 250ms ease-in 200ms; // Go-away transition + background: $colorIndicatorMenuBg; + color: $colorIndicatorMenuFg; + border-radius: $controlCr; + right: 0; + top: 130%; + padding: $interiorMargin $interiorMargin; + position: absolute; + transform-origin: 90% 0; + transform: scale(0); + overflow: hidden; + z-index: 50; + + &:before { + // Infobubble-style arrow element + content: ''; + display: block; + position: absolute; + bottom: 100%; + right: 8px; + @include triangle('up', $size: 4px, $ratio: 1, $color: $colorIndicatorMenuBg); + } + } + + .c-indicator__count { + display: inline-block; + margin-left: $interiorMarginSm; + } + } } /* Mobile */ // Hide the clock indicator when we're phone portrait body.phone.portrait { - .c-indicator.t-indicator-clock { - display: none; - } + .c-indicator.t-indicator-clock { + display: none; + } } diff --git a/src/ui/layout/status-bar/notification-banner.scss b/src/ui/layout/status-bar/notification-banner.scss index c818cd47a2..bac7845c2d 100644 --- a/src/ui/layout/status-bar/notification-banner.scss +++ b/src/ui/layout/status-bar/notification-banner.scss @@ -1,74 +1,74 @@ @mixin statusBannerColors($bg, $fg: $colorStatusFg) { - $bgPb: 10%; - $bgPbD: 10%; - background-color: darken($bg, $bgPb); - color: $fg; + $bgPb: 10%; + $bgPbD: 10%; + background-color: darken($bg, $bgPb); + color: $fg; + &:hover { + background-color: darken($bg, $bgPb - $bgPbD); + } + .s-action { + background-color: darken($bg, $bgPb + $bgPbD); &:hover { - background-color: darken($bg, $bgPb - $bgPbD); - } - .s-action { - background-color: darken($bg, $bgPb + $bgPbD); - &:hover { - background-color: darken($bg, $bgPb); - } + background-color: darken($bg, $bgPb); } + } } .c-message-banner { - $closeBtnSize: 7px; + $closeBtnSize: 7px; - border-radius: $controlCr; - @include statusBannerColors($colorStatusDefault, $colorStatusFg); - cursor: pointer; - display: flex; - align-items: center; - left: 50%; - top: 50%; - max-width: 50%; - max-height: 25px; - padding: $interiorMarginSm $interiorMargin $interiorMarginSm $interiorMarginLg; - position: absolute; - transform: translate(-50%, -50%); - z-index: 2; + border-radius: $controlCr; + @include statusBannerColors($colorStatusDefault, $colorStatusFg); + cursor: pointer; + display: flex; + align-items: center; + left: 50%; + top: 50%; + max-width: 50%; + max-height: 25px; + padding: $interiorMarginSm $interiorMargin $interiorMarginSm $interiorMarginLg; + position: absolute; + transform: translate(-50%, -50%); + z-index: 2; - > * + * { - margin-left: $interiorMargin; - } + > * + * { + margin-left: $interiorMargin; + } - &.ok { - @include statusBannerColors($colorOk, $colorOkFg); - } + &.ok { + @include statusBannerColors($colorOk, $colorOkFg); + } - &.info { - @include statusBannerColors($colorInfo, $colorInfoFg); - } - &.caution, - &.warning, - &.alert { - @include statusBannerColors($colorWarningLo,$colorWarningLoFg); - } - &.error { - @include statusBannerColors($colorWarningHi, $colorWarningHiFg); - } + &.info { + @include statusBannerColors($colorInfo, $colorInfoFg); + } + &.caution, + &.warning, + &.alert { + @include statusBannerColors($colorWarningLo, $colorWarningLoFg); + } + &.error { + @include statusBannerColors($colorWarningHi, $colorWarningHiFg); + } - &__message { - @include ellipsize(); - flex: 1 1 auto; - } + &__message { + @include ellipsize(); + flex: 1 1 auto; + } - &__progress-bar { - height: 7px; - width: 70px; + &__progress-bar { + height: 7px; + width: 70px; - // Only show the progress bar - .c-progress-bar { - &__text { - display: none; - } - } + // Only show the progress bar + .c-progress-bar { + &__text { + display: none; + } } + } - &__close-button { - font-size: 1.25em; - } + &__close-button { + font-size: 1.25em; + } } diff --git a/src/ui/layout/tree-item.vue b/src/ui/layout/tree-item.vue index c8489e3fa4..3e7e2d5760 100644 --- a/src/ui/layout/tree-item.vue +++ b/src/ui/layout/tree-item.vue @@ -20,45 +20,44 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/mixins/context-menu-gesture.js b/src/ui/mixins/context-menu-gesture.js index ad7fa0de31..66f68b018d 100644 --- a/src/ui/mixins/context-menu-gesture.js +++ b/src/ui/mixins/context-menu-gesture.js @@ -1,61 +1,67 @@ export default { - inject: ['openmct'], - props: { - 'objectPath': { - type: Array, - default() { - return []; - } - } - }, - data() { - return { - contextClickActive: false - }; - }, - mounted() { - //TODO: touch support - this.$el.addEventListener('contextmenu', this.showContextMenu); - - function updateObject(oldObject, newObject) { - Object.assign(oldObject, newObject); - } - - this.objectPath.forEach(object => { - if (object) { - this.$once('hook:destroyed', - this.openmct.objects.observe(object, '*', updateObject.bind(this, object))); - } - }); - }, - destroyed() { - this.$el.removeEventListener('contextMenu', this.showContextMenu); - }, - methods: { - showContextMenu(event) { - if (this.readOnly) { - return; - } - - event.preventDefault(); - event.stopPropagation(); - - let actionsCollection = this.openmct.actions.getActionsCollection(this.objectPath); - let actions = actionsCollection.getVisibleActions(); - let sortedActions = this.openmct.actions._groupAndSortActions(actions); - - const menuOptions = { - onDestroy: this.onContextMenuDestroyed - }; - - const menuItems = this.openmct.menus.actionsToMenuItems(sortedActions, actionsCollection.objectPath, actionsCollection.view); - this.openmct.menus.showMenu(event.clientX, event.clientY, menuItems, menuOptions); - this.contextClickActive = true; - this.$emit('context-click-active', true); - }, - onContextMenuDestroyed() { - this.contextClickActive = false; - this.$emit('context-click-active', false); - } + inject: ['openmct'], + props: { + objectPath: { + type: Array, + default() { + return []; + } } + }, + data() { + return { + contextClickActive: false + }; + }, + mounted() { + //TODO: touch support + this.$el.addEventListener('contextmenu', this.showContextMenu); + + function updateObject(oldObject, newObject) { + Object.assign(oldObject, newObject); + } + + this.objectPath.forEach((object) => { + if (object) { + this.$once( + 'hook:destroyed', + this.openmct.objects.observe(object, '*', updateObject.bind(this, object)) + ); + } + }); + }, + destroyed() { + this.$el.removeEventListener('contextMenu', this.showContextMenu); + }, + methods: { + showContextMenu(event) { + if (this.readOnly) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + let actionsCollection = this.openmct.actions.getActionsCollection(this.objectPath); + let actions = actionsCollection.getVisibleActions(); + let sortedActions = this.openmct.actions._groupAndSortActions(actions); + + const menuOptions = { + onDestroy: this.onContextMenuDestroyed + }; + + const menuItems = this.openmct.menus.actionsToMenuItems( + sortedActions, + actionsCollection.objectPath, + actionsCollection.view + ); + this.openmct.menus.showMenu(event.clientX, event.clientY, menuItems, menuOptions); + this.contextClickActive = true; + this.$emit('context-click-active', true); + }, + onContextMenuDestroyed() { + this.contextClickActive = false; + this.$emit('context-click-active', false); + } + } }; diff --git a/src/ui/mixins/object-link.js b/src/ui/mixins/object-link.js index f65a343fcd..b21b8bd11c 100644 --- a/src/ui/mixins/object-link.js +++ b/src/ui/mixins/object-link.js @@ -1,28 +1,28 @@ import objectPathToUrl from '../../tools/url'; export default { - inject: ['openmct'], - props: { - objectPath: { - type: Array, - default() { - return []; - } - } - }, - computed: { - objectLink() { - if (!this.objectPath.length) { - return; - } - - if (this.navigateToPath) { - return '#' + this.navigateToPath; - } - - const url = objectPathToUrl(this.openmct, this.objectPath); - - return url; - } + inject: ['openmct'], + props: { + objectPath: { + type: Array, + default() { + return []; + } } + }, + computed: { + objectLink() { + if (!this.objectPath.length) { + return; + } + + if (this.navigateToPath) { + return '#' + this.navigateToPath; + } + + const url = objectPathToUrl(this.openmct, this.objectPath); + + return url; + } + } }; diff --git a/src/ui/mixins/staleness-mixin.js b/src/ui/mixins/staleness-mixin.js index e082ff0b3d..80d1910133 100644 --- a/src/ui/mixins/staleness-mixin.js +++ b/src/ui/mixins/staleness-mixin.js @@ -23,46 +23,49 @@ import StalenessUtils from '@/utils/staleness'; export default { - data() { - return { - isStale: false - }; - }, - beforeDestroy() { - this.triggerUnsubscribeFromStaleness(); - }, - methods: { - subscribeToStaleness(domainObject, callback) { - if (!this.stalenessUtils) { - this.stalenessUtils = new StalenessUtils(this.openmct, domainObject); - } + data() { + return { + isStale: false + }; + }, + beforeDestroy() { + this.triggerUnsubscribeFromStaleness(); + }, + methods: { + subscribeToStaleness(domainObject, callback) { + if (!this.stalenessUtils) { + this.stalenessUtils = new StalenessUtils(this.openmct, domainObject); + } - this.requestStaleness(domainObject); - this.unsubscribeFromStaleness = this.openmct.telemetry.subscribeToStaleness(domainObject, (stalenessResponse) => { - this.handleStalenessResponse(stalenessResponse, callback); - }); - }, - async requestStaleness(domainObject) { - const stalenessResponse = await this.openmct.telemetry.isStale(domainObject); - if (stalenessResponse !== undefined) { - this.handleStalenessResponse(stalenessResponse); - } - }, - handleStalenessResponse(stalenessResponse, callback) { - if (this.stalenessUtils.shouldUpdateStaleness(stalenessResponse)) { - if (typeof callback === 'function') { - callback(stalenessResponse.isStale); - } else { - this.isStale = stalenessResponse.isStale; - } - } - }, - triggerUnsubscribeFromStaleness() { - if (this.unsubscribeFromStaleness) { - this.unsubscribeFromStaleness(); - delete this.unsubscribeFromStaleness; - this.stalenessUtils.destroy(); - } + this.requestStaleness(domainObject); + this.unsubscribeFromStaleness = this.openmct.telemetry.subscribeToStaleness( + domainObject, + (stalenessResponse) => { + this.handleStalenessResponse(stalenessResponse, callback); } + ); + }, + async requestStaleness(domainObject) { + const stalenessResponse = await this.openmct.telemetry.isStale(domainObject); + if (stalenessResponse !== undefined) { + this.handleStalenessResponse(stalenessResponse); + } + }, + handleStalenessResponse(stalenessResponse, callback) { + if (this.stalenessUtils.shouldUpdateStaleness(stalenessResponse)) { + if (typeof callback === 'function') { + callback(stalenessResponse.isStale); + } else { + this.isStale = stalenessResponse.isStale; + } + } + }, + triggerUnsubscribeFromStaleness() { + if (this.unsubscribeFromStaleness) { + this.unsubscribeFromStaleness(); + delete this.unsubscribeFromStaleness; + this.stalenessUtils.destroy(); + } } + } }; diff --git a/src/ui/mixins/toggle-mixin.js b/src/ui/mixins/toggle-mixin.js index 72ae339b06..eeda8f0842 100644 --- a/src/ui/mixins/toggle-mixin.js +++ b/src/ui/mixins/toggle-mixin.js @@ -1,31 +1,31 @@ export default { - data() { - return { - open: false - }; - }, - methods: { - toggle(event) { - if (this.open) { - if (this.isOpening) { - // Prevent document event handler from closing immediately - // after opening. Can't use stopPropagation because that - // would break other menus with similar behavior. - this.isOpening = false; + data() { + return { + open: false + }; + }, + methods: { + toggle(event) { + if (this.open) { + if (this.isOpening) { + // Prevent document event handler from closing immediately + // after opening. Can't use stopPropagation because that + // would break other menus with similar behavior. + this.isOpening = false; - return; - } - - document.removeEventListener('click', this.toggle); - this.open = false; - } else { - document.addEventListener('click', this.toggle); - this.open = true; - this.isOpening = true; - } + return; } - }, - destroyed() { + document.removeEventListener('click', this.toggle); + this.open = false; + } else { + document.addEventListener('click', this.toggle); + this.open = true; + this.isOpening = true; + } } + }, + destroyed() { + document.removeEventListener('click', this.toggle); + } }; diff --git a/src/ui/preview/Preview.vue b/src/ui/preview/Preview.vue index 0e14ac6a6e..d441bb4367 100644 --- a/src/ui/preview/Preview.vue +++ b/src/ui/preview/Preview.vue @@ -20,198 +20,211 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/preview/PreviewAction.js b/src/ui/preview/PreviewAction.js index cbf4c3b2d0..f2b8c07800 100644 --- a/src/ui/preview/PreviewAction.js +++ b/src/ui/preview/PreviewAction.js @@ -24,79 +24,81 @@ import Vue from 'vue'; import EventEmitter from 'EventEmitter'; export default class PreviewAction extends EventEmitter { - constructor(openmct) { - super(); - /** - * Metadata - */ - this.name = 'View'; - this.key = 'preview'; - this.description = 'View in large dialog'; - this.cssClass = 'icon-items-expand'; - this.group = 'windowing'; - this.priority = 1; + constructor(openmct) { + super(); + /** + * Metadata + */ + this.name = 'View'; + this.key = 'preview'; + this.description = 'View in large dialog'; + this.cssClass = 'icon-items-expand'; + this.group = 'windowing'; + this.priority = 1; - /** - * Dependencies - */ - this._openmct = openmct; + /** + * Dependencies + */ + this._openmct = openmct; - if (PreviewAction.isVisible === undefined) { - PreviewAction.isVisible = false; + if (PreviewAction.isVisible === undefined) { + PreviewAction.isVisible = false; + } + } + + invoke(objectPath, viewOptions) { + let preview = new Vue({ + components: { + Preview + }, + provide: { + openmct: this._openmct, + objectPath: objectPath + }, + data() { + return { + viewOptions + }; + }, + template: '' + }); + preview.$mount(); + + let overlay = this._openmct.overlays.overlay({ + element: preview.$el, + size: 'large', + autoHide: false, + buttons: [ + { + label: 'Done', + callback: () => overlay.dismiss() } - } + ], + onDestroy: () => { + PreviewAction.isVisible = false; + preview.$destroy(); + this.emit('isVisible', false); + } + }); - invoke(objectPath, viewOptions) { - let preview = new Vue({ - components: { - Preview - }, - provide: { - openmct: this._openmct, - objectPath: objectPath - }, - data() { - return { - viewOptions - }; - }, - template: '' - }); - preview.$mount(); + PreviewAction.isVisible = true; + this.emit('isVisible', true); + } - let overlay = this._openmct.overlays.overlay({ - element: preview.$el, - size: 'large', - autoHide: false, - buttons: [ - { - label: 'Done', - callback: () => overlay.dismiss() - } - ], - onDestroy: () => { - PreviewAction.isVisible = false; - preview.$destroy(); - this.emit('isVisible', false); - } - }); + appliesTo(objectPath, view = {}) { + const parentElement = view.parentElement; + const isObjectView = parentElement && parentElement.classList.contains('js-object-view'); - PreviewAction.isVisible = true; - this.emit('isVisible', true); - } + return ( + !PreviewAction.isVisible && + !this._openmct.router.isNavigatedObject(objectPath) && + !isObjectView + ); + } - appliesTo(objectPath, view = {}) { - const parentElement = view.parentElement; - const isObjectView = parentElement && parentElement.classList.contains('js-object-view'); + _preventPreview(objectPath) { + const noPreviewTypes = ['folder']; - return !PreviewAction.isVisible - && !this._openmct.router.isNavigatedObject(objectPath) - && !isObjectView; - } - - _preventPreview(objectPath) { - const noPreviewTypes = ['folder']; - - return noPreviewTypes.includes(objectPath[0].type); - } + return noPreviewTypes.includes(objectPath[0].type); + } } diff --git a/src/ui/preview/ViewHistoricalDataAction.js b/src/ui/preview/ViewHistoricalDataAction.js index b80450d69a..2a982b8650 100644 --- a/src/ui/preview/ViewHistoricalDataAction.js +++ b/src/ui/preview/ViewHistoricalDataAction.js @@ -23,22 +23,21 @@ import PreviewAction from './PreviewAction'; export default class ViewHistoricalDataAction extends PreviewAction { - constructor(openmct) { - super(openmct); + constructor(openmct) { + super(openmct); - this.name = 'View Historical Data'; - this.key = 'viewHistoricalData'; - this.description = 'View Historical Data in a Table or Plot'; - this.cssClass = 'icon-eye-open'; - this.hideInDefaultMenu = true; - } + this.name = 'View Historical Data'; + this.key = 'viewHistoricalData'; + this.description = 'View Historical Data in a Table or Plot'; + this.cssClass = 'icon-eye-open'; + this.hideInDefaultMenu = true; + } - appliesTo(objectPath, view = {}) { - let viewContext = view.getViewContext && view.getViewContext(); + appliesTo(objectPath, view = {}) { + let viewContext = view.getViewContext && view.getViewContext(); - return objectPath.length - && viewContext - && viewContext.row - && viewContext.row.viewHistoricalData; - } + return ( + objectPath.length && viewContext && viewContext.row && viewContext.row.viewHistoricalData + ); + } } diff --git a/src/ui/preview/plugin.js b/src/ui/preview/plugin.js index 200442d644..c9e8c5713b 100644 --- a/src/ui/preview/plugin.js +++ b/src/ui/preview/plugin.js @@ -23,8 +23,8 @@ import PreviewAction from './PreviewAction.js'; import ViewHistoricalDataAction from './ViewHistoricalDataAction'; export default function () { - return function (openmct) { - openmct.actions.register(new PreviewAction(openmct)); - openmct.actions.register(new ViewHistoricalDataAction(openmct)); - }; + return function (openmct) { + openmct.actions.register(new PreviewAction(openmct)); + openmct.actions.register(new ViewHistoricalDataAction(openmct)); + }; } diff --git a/src/ui/preview/preview-header.vue b/src/ui/preview/preview-header.vue index fb62c89aee..c94a30538a 100644 --- a/src/ui/preview/preview-header.vue +++ b/src/ui/preview/preview-header.vue @@ -20,167 +20,156 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/preview/preview.scss b/src/ui/preview/preview.scss index 418c19175b..7d5082f9cd 100644 --- a/src/ui/preview/preview.scss +++ b/src/ui/preview/preview.scss @@ -1,25 +1,28 @@ .l-preview-window { - display: flex; - flex-direction: column; - position: absolute; - top: 0; right: 0; bottom: 0; left: 0; + display: flex; + flex-direction: column; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; - > * + * { - margin-top: $interiorMargin; - } - - &__object-name { - flex: 0 0 auto; - } - - &__object-view { - flex: 1 1 auto; - height: 100%; // Chrome 73 - overflow: auto; - - > div:not([class]) { - // Target an immediate child div without a class and make it display: contents - display: contents; - } + > * + * { + margin-top: $interiorMargin; + } + + &__object-name { + flex: 0 0 auto; + } + + &__object-view { + flex: 1 1 auto; + height: 100%; // Chrome 73 + overflow: auto; + + > div:not([class]) { + // Target an immediate child div without a class and make it display: contents + display: contents; } + } } diff --git a/src/ui/registries/InspectorViewRegistry.js b/src/ui/registries/InspectorViewRegistry.js index f131cd051b..803afb2ec5 100644 --- a/src/ui/registries/InspectorViewRegistry.js +++ b/src/ui/registries/InspectorViewRegistry.js @@ -30,70 +30,71 @@ const DEFAULT_VIEW_PRIORITY = 0; * @memberof module:openmct */ export default class InspectorViewRegistry { - constructor() { - this.providers = {}; + constructor() { + this.providers = {}; + } + + /** + * + * @param {object} selection the object to be viewed + * @returns {module:openmct.InspectorViewRegistry[]} any providers + * which can provide views of this object + * @private for platform-internal use + */ + get(selection) { + function byPriority(providerA, providerB) { + const priorityA = providerA.priority?.() ?? DEFAULT_VIEW_PRIORITY; + const priorityB = providerB.priority?.() ?? DEFAULT_VIEW_PRIORITY; + + return priorityB - priorityA; } - /** - * - * @param {object} selection the object to be viewed - * @returns {module:openmct.InspectorViewRegistry[]} any providers - * which can provide views of this object - * @private for platform-internal use - */ - get(selection) { - function byPriority(providerA, providerB) { - const priorityA = providerA.priority?.() ?? DEFAULT_VIEW_PRIORITY; - const priorityB = providerB.priority?.() ?? DEFAULT_VIEW_PRIORITY; + return this.#getAllProviders() + .filter((provider) => provider.canView(selection)) + .map((provider) => { + const view = provider.view(selection); + view.key = provider.key; + view.name = provider.name; + view.glyph = provider.glyph; - return priorityB - priorityA; - } + return view; + }) + .sort(byPriority); + } - return this.#getAllProviders() - .filter(provider => provider.canView(selection)) - .map(provider => { - const view = provider.view(selection); - view.key = provider.key; - view.name = provider.name; - view.glyph = provider.glyph; + /** + * Registers a new type of view. + * + * @param {module:openmct.InspectorViewRegistry} provider the provider for this view + * @method addProvider + * @memberof module:openmct.InspectorViewRegistry# + */ + addProvider(provider) { + const key = provider.key; + const name = provider.name; - return view; - }).sort(byPriority); + if (key === undefined) { + throw "View providers must have a unique 'key' property defined"; } - /** - * Registers a new type of view. - * - * @param {module:openmct.InspectorViewRegistry} provider the provider for this view - * @method addProvider - * @memberof module:openmct.InspectorViewRegistry# - */ - addProvider(provider) { - const key = provider.key; - const name = provider.name; - - if (key === undefined) { - throw "View providers must have a unique 'key' property defined"; - } - - if (name === undefined) { - throw "View providers must have a 'name' property defined"; - } - - if (this.providers[key] !== undefined) { - console.warn(`Provider already defined for key '${key}'. Provider keys must be unique.`); - } - - this.providers[key] = provider; + if (name === undefined) { + throw "View providers must have a 'name' property defined"; } - getByProviderKey(key) { - return this.providers[key]; + if (this.providers[key] !== undefined) { + console.warn(`Provider already defined for key '${key}'. Provider keys must be unique.`); } - #getAllProviders() { - return Object.values(this.providers); - } + this.providers[key] = provider; + } + + getByProviderKey(key) { + return this.providers[key]; + } + + #getAllProviders() { + return Object.values(this.providers); + } } /** diff --git a/src/ui/registries/ToolbarRegistry.js b/src/ui/registries/ToolbarRegistry.js index a6803320aa..aab0080f51 100644 --- a/src/ui/registries/ToolbarRegistry.js +++ b/src/ui/registries/ToolbarRegistry.js @@ -21,102 +21,101 @@ *****************************************************************************/ define([], function () { + /** + * A ToolbarRegistry maintains the definitions for toolbars. + * + * @interface ToolbarRegistry + * @memberof module:openmct + */ + function ToolbarRegistry() { + this.providers = {}; + } - /** - * A ToolbarRegistry maintains the definitions for toolbars. - * - * @interface ToolbarRegistry - * @memberof module:openmct - */ - function ToolbarRegistry() { - this.providers = {}; + /** + * Gets toolbar controls from providers which can provide a toolbar for this selection. + * + * @param {object} selection the selection object + * @returns {Object[]} an array of objects defining controls for the toolbar + * @private for platform-internal use + */ + ToolbarRegistry.prototype.get = function (selection) { + const providers = this.getAllProviders().filter(function (provider) { + return provider.forSelection(selection); + }); + + const structure = []; + + providers.forEach((provider) => { + provider.toolbar(selection).forEach((item) => structure.push(item)); + }); + + return structure; + }; + + /** + * @private + */ + ToolbarRegistry.prototype.getAllProviders = function () { + return Object.values(this.providers); + }; + + /** + * @private + */ + ToolbarRegistry.prototype.getByProviderKey = function (key) { + return this.providers[key]; + }; + + /** + * Registers a new type of toolbar. + * + * @param {module:openmct.ToolbarRegistry} provider the provider for this toolbar + * @method addProvider + * @memberof module:openmct.ToolbarRegistry# + */ + ToolbarRegistry.prototype.addProvider = function (provider) { + const key = provider.key; + + if (key === undefined) { + throw "Toolbar providers must have a unique 'key' property defined."; } - /** - * Gets toolbar controls from providers which can provide a toolbar for this selection. - * - * @param {object} selection the selection object - * @returns {Object[]} an array of objects defining controls for the toolbar - * @private for platform-internal use - */ - ToolbarRegistry.prototype.get = function (selection) { - const providers = this.getAllProviders().filter(function (provider) { - return provider.forSelection(selection); - }); + if (this.providers[key] !== undefined) { + console.warn("Provider already defined for key '%s'. Provider keys must be unique.", key); + } - const structure = []; + this.providers[key] = provider; + }; - providers.forEach(provider => { - provider.toolbar(selection).forEach(item => structure.push(item)); - }); + /** + * Exposes types of toolbars in Open MCT. + * + * @interface ToolbarProvider + * @property {string} key a unique identifier for this toolbar + * @property {string} name the human-readable name of this toolbar + * @property {string} [description] a longer-form description (typically + * a single sentence or short paragraph) of this kind of toolbar + * @memberof module:openmct + */ - return structure; - }; + /** + * Checks if this provider can supply toolbar for a selection. + * + * @method forSelection + * @memberof module:openmct.ToolbarProvider# + * @param {module:openmct.selection} selection + * @returns {boolean} 'true' if the toolbar applies to the provided selection, + * otherwise 'false'. + */ - /** - * @private - */ - ToolbarRegistry.prototype.getAllProviders = function () { - return Object.values(this.providers); - }; + /** + * Provides controls that comprise a toolbar. + * + * @method toolbar + * @memberof module:openmct.ToolbarProvider# + * @param {object} selection the selection object + * @returns {Object[]} an array of objects defining controls for the toolbar. + */ - /** - * @private - */ - ToolbarRegistry.prototype.getByProviderKey = function (key) { - return this.providers[key]; - }; - - /** - * Registers a new type of toolbar. - * - * @param {module:openmct.ToolbarRegistry} provider the provider for this toolbar - * @method addProvider - * @memberof module:openmct.ToolbarRegistry# - */ - ToolbarRegistry.prototype.addProvider = function (provider) { - const key = provider.key; - - if (key === undefined) { - throw "Toolbar providers must have a unique 'key' property defined."; - } - - if (this.providers[key] !== undefined) { - console.warn("Provider already defined for key '%s'. Provider keys must be unique.", key); - } - - this.providers[key] = provider; - }; - - /** - * Exposes types of toolbars in Open MCT. - * - * @interface ToolbarProvider - * @property {string} key a unique identifier for this toolbar - * @property {string} name the human-readable name of this toolbar - * @property {string} [description] a longer-form description (typically - * a single sentence or short paragraph) of this kind of toolbar - * @memberof module:openmct - */ - - /** - * Checks if this provider can supply toolbar for a selection. - * - * @method forSelection - * @memberof module:openmct.ToolbarProvider# - * @param {module:openmct.selection} selection - * @returns {boolean} 'true' if the toolbar applies to the provided selection, - * otherwise 'false'. - */ - - /** - * Provides controls that comprise a toolbar. - * - * @method toolbar - * @memberof module:openmct.ToolbarProvider# - * @param {object} selection the selection object - * @returns {Object[]} an array of objects defining controls for the toolbar. - */ - - return ToolbarRegistry; + return ToolbarRegistry; }); diff --git a/src/ui/registries/ViewRegistry.js b/src/ui/registries/ViewRegistry.js index ea774b2662..e649fc3ac4 100644 --- a/src/ui/registries/ViewRegistry.js +++ b/src/ui/registries/ViewRegistry.js @@ -21,254 +21,254 @@ *****************************************************************************/ define(['EventEmitter'], function (EventEmitter) { - const DEFAULT_VIEW_PRIORITY = 100; + const DEFAULT_VIEW_PRIORITY = 100; - /** - * A ViewRegistry maintains the definitions for different kinds of views - * that may occur in different places in the user interface. - * @interface ViewRegistry - * @memberof module:openmct - */ - function ViewRegistry() { - EventEmitter.apply(this); - this.providers = {}; + /** + * A ViewRegistry maintains the definitions for different kinds of views + * that may occur in different places in the user interface. + * @interface ViewRegistry + * @memberof module:openmct + */ + function ViewRegistry() { + EventEmitter.apply(this); + this.providers = {}; + } + + ViewRegistry.prototype = Object.create(EventEmitter.prototype); + + /** + * @private for platform-internal use + * @param {*} item the object to be viewed + * @param {array} objectPath - The current contextual object path of the view object + * eg current domainObject is located under MyItems which is under Root + * @returns {module:openmct.ViewProvider[]} any providers + * which can provide views of this object + */ + ViewRegistry.prototype.get = function (item, objectPath) { + if (objectPath === undefined) { + throw 'objectPath must be provided to get applicable views for an object'; } - ViewRegistry.prototype = Object.create(EventEmitter.prototype); + function byPriority(providerA, providerB) { + let priorityA = providerA.priority ? providerA.priority(item) : DEFAULT_VIEW_PRIORITY; + let priorityB = providerB.priority ? providerB.priority(item) : DEFAULT_VIEW_PRIORITY; - /** - * @private for platform-internal use - * @param {*} item the object to be viewed - * @param {array} objectPath - The current contextual object path of the view object - * eg current domainObject is located under MyItems which is under Root - * @returns {module:openmct.ViewProvider[]} any providers - * which can provide views of this object - */ - ViewRegistry.prototype.get = function (item, objectPath) { - if (objectPath === undefined) { - throw "objectPath must be provided to get applicable views for an object"; - } + return priorityB - priorityA; + } - function byPriority(providerA, providerB) { - let priorityA = providerA.priority ? providerA.priority(item) : DEFAULT_VIEW_PRIORITY; - let priorityB = providerB.priority ? providerB.priority(item) : DEFAULT_VIEW_PRIORITY; + return this.getAllProviders() + .filter(function (provider) { + return provider.canView(item, objectPath); + }) + .sort(byPriority); + }; - return priorityB - priorityA; - } + /** + * @private + */ + ViewRegistry.prototype.getAllProviders = function () { + return Object.values(this.providers); + }; - return this.getAllProviders() - .filter(function (provider) { - return provider.canView(item, objectPath); - }).sort(byPriority); + /** + * Register a new type of view. + * + * @param {module:openmct.ViewProvider} provider the provider for this view + * @method addProvider + * @memberof module:openmct.ViewRegistry# + */ + ViewRegistry.prototype.addProvider = function (provider) { + const key = provider.key; + if (key === undefined) { + throw "View providers must have a unique 'key' property defined"; + } + + if (this.providers[key] !== undefined) { + console.warn("Provider already defined for key '%s'. Provider keys must be unique.", key); + } + + const wrappedView = provider.view.bind(provider); + provider.view = (domainObject, objectPath) => { + const viewObject = wrappedView(domainObject, objectPath); + const wrappedShow = viewObject.show.bind(viewObject); + viewObject.key = key; // provide access to provider key on view object + viewObject.show = (element, isEditing, viewOptions) => { + viewObject.parentElement = element.parentElement; + wrappedShow(element, isEditing, viewOptions); + }; + + return viewObject; }; - /** - * @private - */ - ViewRegistry.prototype.getAllProviders = function () { - return Object.values(this.providers); - }; + this.providers[key] = provider; + }; - /** - * Register a new type of view. - * - * @param {module:openmct.ViewProvider} provider the provider for this view - * @method addProvider - * @memberof module:openmct.ViewRegistry# - */ - ViewRegistry.prototype.addProvider = function (provider) { - const key = provider.key; - if (key === undefined) { - throw "View providers must have a unique 'key' property defined"; - } + /** + * @private + */ + ViewRegistry.prototype.getByProviderKey = function (key) { + return this.providers[key]; + }; - if (this.providers[key] !== undefined) { - console.warn("Provider already defined for key '%s'. Provider keys must be unique.", key); - } + /** + * Used internally to support seamless usage of new views with old + * views. + * @private + */ + ViewRegistry.prototype.getByVPID = function (vpid) { + return this.providers.filter(function (p) { + return p.vpid === vpid; + })[0]; + }; - const wrappedView = provider.view.bind(provider); - provider.view = (domainObject, objectPath) => { - const viewObject = wrappedView(domainObject, objectPath); - const wrappedShow = viewObject.show.bind(viewObject); - viewObject.key = key; // provide access to provider key on view object - viewObject.show = (element, isEditing, viewOptions) => { - viewObject.parentElement = element.parentElement; - wrappedShow(element, isEditing, viewOptions); - }; + /** + * A View is used to provide displayable content, and to react to + * associated life cycle events. + * + * @name View + * @interface + * @memberof module:openmct + */ - return viewObject; - }; + /** + * Populate the supplied DOM element with the contents of this view. + * + * View implementations should use this method to attach any + * listeners or acquire other resources that are necessary to keep + * the contents of this view up-to-date. + * + * @param {HTMLElement} container the DOM element to populate + * @method show + * @memberof module:openmct.View# + */ - this.providers[key] = provider; - }; + /** + * Indicates whether or not the application is in edit mode. This supports + * views that have distinct visual and behavioral elements when the + * navigated object is being edited. + * + * For cases where a completely separate view is desired for editing purposes, + * see {@link openmct.ViewProvider#edit} + * + * @param {boolean} isEditing + * @method show + * @memberof module:openmct.View# + */ - /** - * @private - */ - ViewRegistry.prototype.getByProviderKey = function (key) { - return this.providers[key]; - }; + /** + * Release any resources associated with this view. + * + * View implementations should use this method to detach any + * listeners or release other resources that are no longer necessary + * once a view is no longer used. + * + * @method destroy + * @memberof module:openmct.View# + */ - /** - * Used internally to support seamless usage of new views with old - * views. - * @private - */ - ViewRegistry.prototype.getByVPID = function (vpid) { - return this.providers.filter(function (p) { - return p.vpid === vpid; - })[0]; - }; + /** + * Returns the selection context. + * + * View implementations should use this method to customize + * the selection context. + * + * @method getSelectionContext + * @memberof module:openmct.View# + */ - /** - * A View is used to provide displayable content, and to react to - * associated life cycle events. - * - * @name View - * @interface - * @memberof module:openmct - */ + /** + * Exposes types of views in Open MCT. + * + * @interface ViewProvider + * @property {string} key a unique identifier for this view + * @property {string} name the human-readable name of this view + * @property {string} [description] a longer-form description (typically + * a single sentence or short paragraph) of this kind of view + * @property {string} [cssClass] the CSS class to apply to labels for this + * view (to add icons, for instance) + * @memberof module:openmct + */ - /** - * Populate the supplied DOM element with the contents of this view. - * - * View implementations should use this method to attach any - * listeners or acquire other resources that are necessary to keep - * the contents of this view up-to-date. - * - * @param {HTMLElement} container the DOM element to populate - * @method show - * @memberof module:openmct.View# - */ + /** + * Check if this provider can supply views for a domain object. + * + * When called by Open MCT, this may include additional arguments + * which are on the path to the object to be viewed; for instance, + * when viewing "A Folder" within "My Items", this method will be + * invoked with "A Folder" (as a domain object) as the first argument + * + * @method canView + * @memberof module:openmct.ViewProvider# + * @param {module:openmct.DomainObject} domainObject the domain object + * to be viewed + * @param {array} objectPath - The current contextual object path of the view object + * eg current domainObject is located under MyItems which is under Root + * @returns {boolean} 'true' if the view applies to the provided object, + * otherwise 'false'. + */ - /** - * Indicates whether or not the application is in edit mode. This supports - * views that have distinct visual and behavioral elements when the - * navigated object is being edited. - * - * For cases where a completely separate view is desired for editing purposes, - * see {@link openmct.ViewProvider#edit} - * - * @param {boolean} isEditing - * @method show - * @memberof module:openmct.View# - */ + /** + * An optional function that defines whether or not this view can be used to edit a given object. + * If not provided, will default to `false` and the view will not support editing. To support editing, + * return true from this function and then - + * * Return a {@link openmct.View} from the `view` function, using the `onEditModeChange` callback to + * add and remove editing elements from the view + * OR + * * Return a {@link openmct.View} from the `view` function defining a read-only view. + * AND + * * Define an {@link openmct.ViewProvider#Edit} function on the view provider that returns an + * editing-specific view. + * + * @method canEdit + * @memberof module:openmct.ViewProvider# + * @param {module:openmct.DomainObject} domainObject the domain object + * to be edited + * @param {array} objectPath - The current contextual object path of the view object + * eg current domainObject is located under MyItems which is under Root + * @returns {boolean} 'true' if the view can be used to edit the provided object, + * otherwise 'false'. + */ - /** - * Release any resources associated with this view. - * - * View implementations should use this method to detach any - * listeners or release other resources that are no longer necessary - * once a view is no longer used. - * - * @method destroy - * @memberof module:openmct.View# - */ + /** + * Optional method determining the priority of a given view. If this + * function is not defined on a view provider, then a default priority + * of 100 will be applicable for all objects supported by this view. + * + * @method priority + * @memberof module:openmct.ViewProvider# + * @param {module:openmct.DomainObject} domainObject the domain object + * to be viewed + * @returns {number} The priority of the view. If multiple views could apply + * to an object, the view that returns the lowest number will be + * the default view. + */ - /** - * Returns the selection context. - * - * View implementations should use this method to customize - * the selection context. - * - * @method getSelectionContext - * @memberof module:openmct.View# - */ + /** + * Provide a view of this object. + * + * When called by Open MCT, the following arguments will be passed to it: + * @param {object} domainObject - the domainObject that the view is provided for + * @param {array} objectPath - The current contextual object path of the view object + * eg current domainObject is located under MyItems which is under Root + * + * @method view + * @memberof module:openmct.ViewProvider# + * @param {*} object the object to be viewed + * @returns {module:openmct.View} a view of this domain object + */ - /** - * Exposes types of views in Open MCT. - * - * @interface ViewProvider - * @property {string} key a unique identifier for this view - * @property {string} name the human-readable name of this view - * @property {string} [description] a longer-form description (typically - * a single sentence or short paragraph) of this kind of view - * @property {string} [cssClass] the CSS class to apply to labels for this - * view (to add icons, for instance) - * @memberof module:openmct - */ - - /** - * Check if this provider can supply views for a domain object. - * - * When called by Open MCT, this may include additional arguments - * which are on the path to the object to be viewed; for instance, - * when viewing "A Folder" within "My Items", this method will be - * invoked with "A Folder" (as a domain object) as the first argument - * - * @method canView - * @memberof module:openmct.ViewProvider# - * @param {module:openmct.DomainObject} domainObject the domain object - * to be viewed - * @param {array} objectPath - The current contextual object path of the view object - * eg current domainObject is located under MyItems which is under Root - * @returns {boolean} 'true' if the view applies to the provided object, - * otherwise 'false'. - */ - - /** - * An optional function that defines whether or not this view can be used to edit a given object. - * If not provided, will default to `false` and the view will not support editing. To support editing, - * return true from this function and then - - * * Return a {@link openmct.View} from the `view` function, using the `onEditModeChange` callback to - * add and remove editing elements from the view - * OR - * * Return a {@link openmct.View} from the `view` function defining a read-only view. - * AND - * * Define an {@link openmct.ViewProvider#Edit} function on the view provider that returns an - * editing-specific view. - * - * @method canEdit - * @memberof module:openmct.ViewProvider# - * @param {module:openmct.DomainObject} domainObject the domain object - * to be edited - * @param {array} objectPath - The current contextual object path of the view object - * eg current domainObject is located under MyItems which is under Root - * @returns {boolean} 'true' if the view can be used to edit the provided object, - * otherwise 'false'. - */ - - /** - * Optional method determining the priority of a given view. If this - * function is not defined on a view provider, then a default priority - * of 100 will be applicable for all objects supported by this view. - * - * @method priority - * @memberof module:openmct.ViewProvider# - * @param {module:openmct.DomainObject} domainObject the domain object - * to be viewed - * @returns {number} The priority of the view. If multiple views could apply - * to an object, the view that returns the lowest number will be - * the default view. - */ - - /** - * Provide a view of this object. - * - * When called by Open MCT, the following arguments will be passed to it: - * @param {object} domainObject - the domainObject that the view is provided for - * @param {array} objectPath - The current contextual object path of the view object - * eg current domainObject is located under MyItems which is under Root - * - * @method view - * @memberof module:openmct.ViewProvider# - * @param {*} object the object to be viewed - * @returns {module:openmct.View} a view of this domain object - */ - - /** - * Provide an edit-mode specific view of this object. - * - * If optionally specified, this function will be called when the application - * enters edit mode. This will cause the active non-edit mode view and its - * dom element to be destroyed. - * - * @method edit - * @memberof module:openmct.ViewProvider# - * @param {*} object the object to be edit - * @returns {module:openmct.View} an editable view of this domain object - */ - - return ViewRegistry; + /** + * Provide an edit-mode specific view of this object. + * + * If optionally specified, this function will be called when the application + * enters edit mode. This will cause the active non-edit mode view and its + * dom element to be destroyed. + * + * @method edit + * @memberof module:openmct.ViewProvider# + * @param {*} object the object to be edit + * @returns {module:openmct.View} an editable view of this domain object + */ + return ViewRegistry; }); diff --git a/src/ui/router/ApplicationRouter.js b/src/ui/router/ApplicationRouter.js index fc6a08fe90..9717d05e36 100644 --- a/src/ui/router/ApplicationRouter.js +++ b/src/ui/router/ApplicationRouter.js @@ -26,7 +26,7 @@ const EventEmitter = require('EventEmitter'); const _ = require('lodash'); class ApplicationRouter extends EventEmitter { - /** + /** * events * change:params -> notify listeners w/ new, old, and changed. * change:path -> notify listeners w/ new, old paths. @@ -41,378 +41,369 @@ class ApplicationRouter extends EventEmitter { * route(path, handler); * start(); Start routing. */ - constructor(openmct) { - super(); + constructor(openmct) { + super(); - this.locationBar = new LocationBar(); - this.openmct = openmct; - this.routes = []; - this.started = false; + this.locationBar = new LocationBar(); + this.openmct = openmct; + this.routes = []; + this.started = false; - this.setHash = _.debounce(this.setHash.bind(this), 300); + this.setHash = _.debounce(this.setHash.bind(this), 300); - openmct.once('destroy', () => { - this.destroy(); - }); + openmct.once('destroy', () => { + this.destroy(); + }); + } + + // Public Methods + + destroy() { + this.locationBar.stop(); + } + + /** + * Delete a given query parameter from current url + * + * @param {string} paramName name of searchParam to delete from current url searchParams + */ + deleteSearchParam(paramName) { + let url = this.getHashRelativeURL(); + + url.searchParams.delete(paramName); + this.setLocationFromUrl(); + } + + /** + * object for accessing all current search parameters + * + * @returns {URLSearchParams} A {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/entries|URLSearchParams} + */ + getAllSearchParams() { + return this.getHashRelativeURL().searchParams; + } + + /** + * Uniquely identifies a domain object. + * + * @typedef CurrentLocation + * @property {URL} url current url location + * @property {string} path current url location pathname + * @property {string} getQueryString a function which returns url search query + * @property {object} params object representing url searchParams + */ + + /** + * object for accessing current url location and search params + * + * @returns {CurrentLocation} A {@link CurrentLocation} + */ + getCurrentLocation() { + return this.currentLocation; + } + + /** + * Get current location URL Object + * + * @returns {URL} current url location + */ + getHashRelativeURL() { + return this.getCurrentLocation().url; + } + + /** + * Get current location URL Object searchParams + * + * @returns {object} object representing current url searchParams + */ + getParams() { + return this.currentLocation.params; + } + + /** + * Get a value of given param from current url searchParams + * + * @returns {string} value of paramName from current url searchParams + */ + getSearchParam(paramName) { + return this.getAllSearchParams().get(paramName); + } + + /** + * Navigate to given hash, update current location object, and notify listeners about location change + * + * @param {string} hash The URL hash to navigate to in the form of "#/browse/mine/{keyString}/{keyString}". + * Should not include any params. + */ + navigate(hash) { + this.handleLocationChange(hash.substring(1)); + } + + /** + * Check if a given object and current location object are same + * + * @param {Array} objectPath Object path of a given Domain Object + * + * @returns {Boolean} + */ + isNavigatedObject(objectPath) { + let targetObject = objectPath[0]; + let navigatedObject = this.path[0]; + + if (!targetObject.identifier) { + return false; } - // Public Methods + return this.openmct.objects.areIdsEqual(targetObject.identifier, navigatedObject.identifier); + } - destroy() { - this.locationBar.stop(); + /** + * Add routes listeners + * + * @param {string} matcher Regex to match value in url + * @param {@function} callback function called when found match in url + */ + route(matcher, callback) { + this.routes.push({ + matcher, + callback + }); + } + + /** + * Set url hash using path and queryString + * + * @param {string} path path for url + * @param {string} queryString queryString for url + */ + set(path, queryString) { + this.setHash(`${path}?${queryString}`); + } + + /** + * Will replace all current search parameters with the ones defined in urlSearchParams + */ + setAllSearchParams() { + this.setLocationFromUrl(); + } + + /** + * To force update url based on value in currentLocation object + */ + setLocationFromUrl() { + this.updateTimeSettings(); + } + + /** + * Set url hash using path + * + * @param {string} path path for url + */ + setPath(path) { + this.handleLocationChange(path.substring(1)); + } + + /** + * Update param value from current url searchParams + * + * @param {string} paramName param name from current url searchParams + * @param {string} paramValue param value from current url searchParams + */ + setSearchParam(paramName, paramValue) { + let url = this.getHashRelativeURL(); + + url.searchParams.set(paramName, paramValue); + this.setLocationFromUrl(); + } + + /** + * start application routing, should be done after handlers are registered. + */ + start() { + if (this.started) { + throw new Error('Router already started!'); } - /** - * Delete a given query parameter from current url - * - * @param {string} paramName name of searchParam to delete from current url searchParams - */ - deleteSearchParam(paramName) { - let url = this.getHashRelativeURL(); + this.started = true; - url.searchParams.delete(paramName); - this.setLocationFromUrl(); + this.locationBar.onChange((p) => this.hashChanged(p)); + this.locationBar.start({ + root: location.pathname + }); + } + + /** + * Set url hash using path and searchParams object + * + * @param {string} path path for url + * @param {string} params oject representing searchParams key/value + */ + update(path, params) { + let searchParams = this.currentLocation.url.searchParams; + for (let [key, value] of Object.entries(params)) { + if (typeof value === 'undefined') { + searchParams.delete(key); + } else { + searchParams.set(key, value); + } } - /** - * object for accessing all current search parameters - * - * @returns {URLSearchParams} A {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/entries|URLSearchParams} - */ - getAllSearchParams() { - return this.getHashRelativeURL().searchParams; + this.set(path, searchParams.toString()); + } + + /** + * Update route params. Takes an object of updates. New parameters + */ + updateParams(updateParams) { + let searchParams = this.currentLocation.url.searchParams; + Object.entries(updateParams).forEach(([key, value]) => { + if (typeof value === 'undefined') { + searchParams.delete(key); + } else { + searchParams.set(key, value); + } + }); + + this.setQueryString(searchParams.toString()); + } + + /** + * To force update url based on value in currentLocation object + */ + updateTimeSettings() { + const hash = `${this.currentLocation.path}?${this.currentLocation.getQueryString()}`; + + this.setHash(hash); + } + + // Private Methods + + /** + * @private + * Create currentLocation object + * + * @param {string} pathString USVString representing relative URL. + * + * @returns {CurrentLocation} A {@link CurrentLocation} + */ + createLocation(pathString) { + if (pathString[0] !== '/') { + pathString = '/' + pathString; } - /** - * Uniquely identifies a domain object. - * - * @typedef CurrentLocation - * @property {URL} url current url location - * @property {string} path current url location pathname - * @property {string} getQueryString a function which returns url search query - * @property {object} params object representing url searchParams - */ + let url = new URL(pathString, `${location.protocol}//${location.host}${location.pathname}`); - /** - * object for accessing current url location and search params - * - * @returns {CurrentLocation} A {@link CurrentLocation} - */ - getCurrentLocation() { - return this.currentLocation; + return { + url: url, + path: url.pathname, + getQueryString: () => url.search.replace(/^\?/, ''), + params: paramsToObject(url.searchParams) + }; + } + + /** + * @private + * Compare new and old path and on change emit event 'change:path' + * + * @param {string} newPath new path of url + * @param {string} oldPath old path of url + */ + doPathChange(newPath, oldPath) { + if (newPath === oldPath) { + return; } - /** - * Get current location URL Object - * - * @returns {URL} current url location - */ - getHashRelativeURL() { - return this.getCurrentLocation().url; + let route = this.routes.filter((r) => r.matcher.test(newPath))[0]; + if (route) { + route.callback(newPath, route.matcher.exec(newPath), this.currentLocation.params); } - /** - * Get current location URL Object searchParams - * - * @returns {object} object representing current url searchParams - */ - getParams() { - return this.currentLocation.params; + this.openmct.telemetry.abortAllRequests(); + + this.emit('change:path', newPath, oldPath); + } + + /** + * @private + * Compare new and old params and on change emit event 'change:params' + * + * @param {object} newParams new params of url + * @param {object} oldParams old params of url + */ + doParamsChange(newParams, oldParams) { + if (_.isEqual(newParams, oldParams)) { + return; } - /** - * Get a value of given param from current url searchParams - * - * @returns {string} value of paramName from current url searchParams - */ - getSearchParam(paramName) { - return this.getAllSearchParams().get(paramName); + let changedParams = {}; + Object.entries(newParams).forEach(([key, value]) => { + if (value !== oldParams[key]) { + changedParams[key] = value; + } + }); + Object.keys(oldParams).forEach((key) => { + if (!Object.prototype.hasOwnProperty.call(newParams, key)) { + changedParams[key] = undefined; + } + }); + + this.emit('change:params', newParams, oldParams, changedParams); + } + + /** + * @private + * On location change, update currentLocation object and emit appropriate events + * + * @param {string} pathString USVString representing relative URL. + */ + handleLocationChange(pathString) { + let oldLocation = this.currentLocation; + let newLocation = this.createLocation(pathString); + + this.currentLocation = newLocation; + + if (!oldLocation) { + this.doPathChange(newLocation.path, null); + this.doParamsChange(newLocation.params, {}); + + return; } - /** - * Navigate to given hash, update current location object, and notify listeners about location change - * - * @param {string} hash The URL hash to navigate to in the form of "#/browse/mine/{keyString}/{keyString}". - * Should not include any params. - */ - navigate(hash) { - this.handleLocationChange(hash.substring(1)); - } + this.doPathChange(newLocation.path, oldLocation.path); - /** - * Check if a given object and current location object are same - * - * @param {Array} objectPath Object path of a given Domain Object - * - * @returns {Boolean} - */ - isNavigatedObject(objectPath) { - let targetObject = objectPath[0]; - let navigatedObject = this.path[0]; + this.doParamsChange(newLocation.params, oldLocation.params); + } - if (!targetObject.identifier) { - return false; - } + /** + * @private + * On hash changed, update currentLocation object and emit appropriate events + * + * @param {string} hash new hash for url + */ + hashChanged(hash) { + this.emit('change:hash', hash); + this.handleLocationChange(hash); + } - return this.openmct.objects.areIdsEqual(targetObject.identifier, navigatedObject.identifier); - } + /** + * @private + * Set new hash for url + * + * @param {string} hash new hash for url + */ + setHash(hash) { + location.hash = '#' + hash.replace(/#/g, ''); + } - /** - * Add routes listeners - * - * @param {string} matcher Regex to match value in url - * @param {@function} callback function called when found match in url - */ - route(matcher, callback) { - this.routes.push({ - matcher, - callback - }); - } - - /** - * Set url hash using path and queryString - * - * @param {string} path path for url - * @param {string} queryString queryString for url - */ - set(path, queryString) { - this.setHash(`${path}?${queryString}`); - } - - /** - * Will replace all current search parameters with the ones defined in urlSearchParams - */ - setAllSearchParams() { - this.setLocationFromUrl(); - } - - /** - * To force update url based on value in currentLocation object - */ - setLocationFromUrl() { - this.updateTimeSettings(); - } - - /** - * Set url hash using path - * - * @param {string} path path for url - */ - setPath(path) { - this.handleLocationChange(path.substring(1)); - } - - /** - * Update param value from current url searchParams - * - * @param {string} paramName param name from current url searchParams - * @param {string} paramValue param value from current url searchParams - */ - setSearchParam(paramName, paramValue) { - let url = this.getHashRelativeURL(); - - url.searchParams.set(paramName, paramValue); - this.setLocationFromUrl(); - } - - /** - * start application routing, should be done after handlers are registered. - */ - start() { - if (this.started) { - throw new Error('Router already started!'); - } - - this.started = true; - - this.locationBar.onChange(p => this.hashChanged(p)); - this.locationBar.start({ - root: location.pathname - }); - } - - /** - * Set url hash using path and searchParams object - * - * @param {string} path path for url - * @param {string} params oject representing searchParams key/value - */ - update(path, params) { - let searchParams = this.currentLocation.url.searchParams; - for (let [key, value] of Object.entries(params)) { - if (typeof value === 'undefined') { - searchParams.delete(key); - } else { - searchParams.set(key, value); - } - } - - this.set(path, searchParams.toString()); - } - - /** - * Update route params. Takes an object of updates. New parameters - */ - updateParams(updateParams) { - let searchParams = this.currentLocation.url.searchParams; - Object.entries(updateParams).forEach(([key, value]) => { - if (typeof value === 'undefined') { - searchParams.delete(key); - } else { - searchParams.set(key, value); - } - }); - - this.setQueryString(searchParams.toString()); - } - - /** - * To force update url based on value in currentLocation object - */ - updateTimeSettings() { - const hash = `${this.currentLocation.path}?${this.currentLocation.getQueryString()}`; - - this.setHash(hash); - } - - // Private Methods - - /** - * @private - * Create currentLocation object - * - * @param {string} pathString USVString representing relative URL. - * - * @returns {CurrentLocation} A {@link CurrentLocation} - */ - createLocation(pathString) { - if (pathString[0] !== '/') { - pathString = '/' + pathString; - } - - let url = new URL( - pathString, - `${location.protocol}//${location.host}${location.pathname}` - ); - - return { - url: url, - path: url.pathname, - getQueryString: () => url.search.replace(/^\?/, ''), - params: paramsToObject(url.searchParams) - }; - } - - /** - * @private - * Compare new and old path and on change emit event 'change:path' - * - * @param {string} newPath new path of url - * @param {string} oldPath old path of url - */ - doPathChange(newPath, oldPath) { - if (newPath === oldPath) { - return; - } - - let route = this.routes.filter(r => r.matcher.test(newPath))[0]; - if (route) { - route.callback(newPath, route.matcher.exec(newPath), this.currentLocation.params); - } - - this.openmct.telemetry.abortAllRequests(); - - this.emit('change:path', newPath, oldPath); - } - - /** - * @private - * Compare new and old params and on change emit event 'change:params' - * - * @param {object} newParams new params of url - * @param {object} oldParams old params of url - */ - doParamsChange(newParams, oldParams) { - if (_.isEqual(newParams, oldParams)) { - return; - } - - let changedParams = {}; - Object.entries(newParams).forEach(([key, value]) => { - if (value !== oldParams[key]) { - changedParams[key] = value; - } - }); - Object.keys(oldParams).forEach(key => { - if (!Object.prototype.hasOwnProperty.call(newParams, key)) { - changedParams[key] = undefined; - } - }); - - this.emit('change:params', newParams, oldParams, changedParams); - } - - /** - * @private - * On location change, update currentLocation object and emit appropriate events - * - * @param {string} pathString USVString representing relative URL. - */ - handleLocationChange(pathString) { - let oldLocation = this.currentLocation; - let newLocation = this.createLocation(pathString); - - this.currentLocation = newLocation; - - if (!oldLocation) { - this.doPathChange(newLocation.path, null); - this.doParamsChange(newLocation.params, {}); - - return; - } - - this.doPathChange( - newLocation.path, - oldLocation.path - ); - - this.doParamsChange( - newLocation.params, - oldLocation.params - ); - } - - /** - * @private - * On hash changed, update currentLocation object and emit appropriate events - * - * @param {string} hash new hash for url - */ - hashChanged(hash) { - this.emit('change:hash', hash); - this.handleLocationChange(hash); - } - - /** - * @private - * Set new hash for url - * - * @param {string} hash new hash for url - */ - setHash(hash) { - location.hash = '#' + hash.replace(/#/g, ''); - } - - /** - * @private - * Set queryString part of current url - * - * @param {string} queryString queryString part of url - */ - setQueryString(queryString) { - this.handleLocationChange(`${this.currentLocation.path}?${queryString}`); - } + /** + * @private + * Set queryString part of current url + * + * @param {string} queryString queryString part of url + */ + setQueryString(queryString) { + this.handleLocationChange(`${this.currentLocation.path}?${queryString}`); + } } /** @@ -423,20 +414,20 @@ class ApplicationRouter extends EventEmitter { * @returns {Object} */ function paramsToObject(searchParams) { - let params = {}; - for (let [key, value] of searchParams.entries()) { - if (params[key]) { - if (!Array.isArray(params[key])) { - params[key] = [params[key]]; - } + let params = {}; + for (let [key, value] of searchParams.entries()) { + if (params[key]) { + if (!Array.isArray(params[key])) { + params[key] = [params[key]]; + } - params[key].push(value); - } else { - params[key] = value; - } + params[key].push(value); + } else { + params[key] = value; } + } - return params; + return params; } module.exports = ApplicationRouter; diff --git a/src/ui/router/ApplicationRouterSpec.js b/src/ui/router/ApplicationRouterSpec.js index 772058df2c..932765b3d2 100644 --- a/src/ui/router/ApplicationRouterSpec.js +++ b/src/ui/router/ApplicationRouterSpec.js @@ -7,84 +7,84 @@ let appHolder; let resolveFunction; xdescribe('Application router utility functions', () => { - beforeEach(done => { - appHolder = document.createElement('div'); - appHolder.style.width = '640px'; - appHolder.style.height = '480px'; + beforeEach((done) => { + appHolder = document.createElement('div'); + appHolder.style.width = '640px'; + appHolder.style.height = '480px'; - openmct = createOpenMct(); - openmct.install(openmct.plugins.MyItems()); + openmct = createOpenMct(); + openmct.install(openmct.plugins.MyItems()); - element = document.createElement('div'); - child = document.createElement('div'); - element.appendChild(child); + element = document.createElement('div'); + child = document.createElement('div'); + element.appendChild(child); - openmct.on('start', () => { - resolveFunction = () => { - const success = window.location.hash !== null && window.location.hash !== ''; - if (success) { - done(); - } - }; + openmct.on('start', () => { + resolveFunction = () => { + const success = window.location.hash !== null && window.location.hash !== ''; + if (success) { + done(); + } + }; - 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(); - }); - - openmct.start(appHolder); - - document.body.append(appHolder); + 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(); }); - afterEach(() => { - openmct.router.removeListener('change:hash', resolveFunction); - appHolder.remove(); + openmct.start(appHolder); - return resetApplicationState(openmct); - }); + document.body.append(appHolder); + }); - it('has initial hash when loaded', () => { - const success = window.location.hash !== null; - expect(success).toBe(true); - }); + afterEach(() => { + openmct.router.removeListener('change:hash', resolveFunction); + appHolder.remove(); - it('The setSearchParam function sets an individual search parameter in the window location hash', () => { - openmct.router.setSearchParam('testParam1', 'testValue1'); + return resetApplicationState(openmct); + }); - const searchParams = openmct.router.getAllSearchParams(); - expect(searchParams.get('testParam1')).toBe('testValue1'); - }); + it('has initial hash when loaded', () => { + const success = window.location.hash !== null; + expect(success).toBe(true); + }); - it('The deleteSearchParam function deletes an individual search paramater in the window location hash', () => { - openmct.router.deleteSearchParam('testParam'); - const searchParams = openmct.router.getAllSearchParams(); - expect(searchParams.get('testParam')).toBe(null); - }); + it('The setSearchParam function sets an individual search parameter in the window location hash', () => { + openmct.router.setSearchParam('testParam1', 'testValue1'); - it('The setSearchParam function sets a multiple individual search parameters in the window location hash', () => { - openmct.router.setSearchParam('testParam1', 'testValue1'); - openmct.router.setSearchParam('testParam2', 'testValue2'); + const searchParams = openmct.router.getAllSearchParams(); + expect(searchParams.get('testParam1')).toBe('testValue1'); + }); - const searchParams = openmct.router.getAllSearchParams(); - expect(searchParams.get('testParam1')).toBe('testValue1'); - expect(searchParams.get('testParam2')).toBe('testValue2'); - }); + it('The deleteSearchParam function deletes an individual search paramater in the window location hash', () => { + openmct.router.deleteSearchParam('testParam'); + const searchParams = openmct.router.getAllSearchParams(); + expect(searchParams.get('testParam')).toBe(null); + }); - it('The setAllSearchParams function replaces all search paramaters in the window location hash', () => { - openmct.router.setSearchParam('testParam2', 'updatedtestValue2'); - openmct.router.setSearchParam('newTestParam3', 'newTestValue3'); + it('The setSearchParam function sets a multiple individual search parameters in the window location hash', () => { + openmct.router.setSearchParam('testParam1', 'testValue1'); + openmct.router.setSearchParam('testParam2', 'testValue2'); - const searchParams = openmct.router.getAllSearchParams(); - expect(searchParams.get('testParam2')).toBe('updatedtestValue2'); - expect(searchParams.get('newTestParam3')).toBe('newTestValue3'); - }); + const searchParams = openmct.router.getAllSearchParams(); + expect(searchParams.get('testParam1')).toBe('testValue1'); + expect(searchParams.get('testParam2')).toBe('testValue2'); + }); - it('The doPathChange function triggers aborting all requests when doing a path change', () => { - const abortSpy = spyOn(openmct.telemetry, 'abortAllRequests'); - openmct.router.doPathChange('newPath', 'oldPath'); - expect(abortSpy).toHaveBeenCalledTimes(1); - }); + it('The setAllSearchParams function replaces all search paramaters in the window location hash', () => { + openmct.router.setSearchParam('testParam2', 'updatedtestValue2'); + openmct.router.setSearchParam('newTestParam3', 'newTestValue3'); + + const searchParams = openmct.router.getAllSearchParams(); + expect(searchParams.get('testParam2')).toBe('updatedtestValue2'); + expect(searchParams.get('newTestParam3')).toBe('newTestValue3'); + }); + + it('The doPathChange function triggers aborting all requests when doing a path change', () => { + const abortSpy = spyOn(openmct.telemetry, 'abortAllRequests'); + openmct.router.doPathChange('newPath', 'oldPath'); + expect(abortSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/ui/router/Browse.js b/src/ui/router/Browse.js index 1c8f622457..39dc3ecc8c 100644 --- a/src/ui/router/Browse.js +++ b/src/ui/router/Browse.js @@ -1,156 +1,151 @@ -define([ +define([], function () { + return function install(openmct) { + let navigateCall = 0; + let browseObject; + let unobserve = undefined; + let currentObjectPath; + let isRoutingInProgress = false; -], function ( + openmct.router.route(/^\/browse\/?$/, navigateToFirstChildOfRoot); + openmct.router.route(/^\/browse\/(.*)$/, (path, results, params) => { + isRoutingInProgress = true; + let navigatePath = results[1]; + clearMutationListeners(); -) { + navigateToPath(navigatePath, params.view); + }); - return function install(openmct) { - let navigateCall = 0; - let browseObject; - let unobserve = undefined; - let currentObjectPath; - let isRoutingInProgress = false; + openmct.router.on('change:params', onParamsChanged); - openmct.router.route(/^\/browse\/?$/, navigateToFirstChildOfRoot); - openmct.router.route(/^\/browse\/(.*)$/, (path, results, params) => { - isRoutingInProgress = true; - let navigatePath = results[1]; - clearMutationListeners(); + function onParamsChanged(newParams, oldParams, changed) { + if (isRoutingInProgress) { + return; + } - navigateToPath(navigatePath, params.view); + if (changed.view && browseObject) { + let provider = openmct.objectViews.getByProviderKey(changed.view); + viewObject(browseObject, provider); + } + } + + function viewObject(object, viewProvider) { + currentObjectPath = openmct.router.path; + + openmct.layout.$refs.browseObject.show(object, viewProvider.key, true, currentObjectPath); + openmct.layout.$refs.browseBar.domainObject = object; + + openmct.layout.$refs.browseBar.viewKey = viewProvider.key; + } + + function updateDocumentTitleOnNameMutation(domainObject) { + if (typeof domainObject.name === 'string' && domainObject.name !== document.title) { + document.title = domainObject.name; + } + } + + function navigateToPath(path, currentViewKey) { + navigateCall++; + let currentNavigation = navigateCall; + + if (unobserve) { + unobserve(); + unobserve = undefined; + } + + //Split path into object identifiers + if (!Array.isArray(path)) { + path = path.split('/'); + } + + return pathToObjects(path).then((objects) => { + isRoutingInProgress = false; + + if (currentNavigation !== navigateCall) { + return; // Prevent race. + } + + objects = objects.reverse(); + + openmct.router.path = objects; + openmct.router.emit('afterNavigation'); + browseObject = objects[0]; + + openmct.layout.$refs.browseBar.domainObject = browseObject; + if (!browseObject) { + openmct.layout.$refs.browseObject.clear(); + + return; + } + + let currentProvider = openmct.objectViews.getByProviderKey(currentViewKey); + document.title = browseObject.name; //change document title to current object in main view + // assign listener to global for later clearing + unobserve = openmct.objects.observe(browseObject, '*', updateDocumentTitleOnNameMutation); + + if (currentProvider && currentProvider.canView(browseObject, openmct.router.path)) { + viewObject(browseObject, currentProvider); + + return; + } + + let defaultProvider = openmct.objectViews.get(browseObject, openmct.router.path)[0]; + if (defaultProvider) { + openmct.router.updateParams({ + view: defaultProvider.key + }); + } else { + openmct.router.updateParams({ + view: undefined + }); + openmct.layout.$refs.browseObject.clear(); + } + }); + } + + function pathToObjects(path) { + return Promise.all( + path.map((keyString) => { + let identifier = openmct.objects.parseKeyString(keyString); + if (openmct.objects.supportsMutation(identifier)) { + return openmct.objects.getMutable(identifier); + } else { + return openmct.objects.get(identifier); + } + }) + ); + } + + function navigateToFirstChildOfRoot() { + openmct.objects + .get('ROOT') + .then((rootObject) => { + const composition = openmct.composition.get(rootObject); + if (!composition) { + return; + } + + composition + .load() + .then((children) => { + let lastChild = children[children.length - 1]; + if (lastChild) { + let lastChildId = openmct.objects.makeKeyString(lastChild.identifier); + openmct.router.setPath(`#/browse/${lastChildId}`); + } + }) + .catch((e) => console.error(e)); + }) + .catch((e) => console.error(e)); + } + + function clearMutationListeners() { + if (openmct.router.path !== undefined) { + openmct.router.path.forEach((pathObject) => { + if (pathObject.isMutable) { + openmct.objects.destroyMutable(pathObject); + } }); - - openmct.router.on('change:params', onParamsChanged); - - function onParamsChanged(newParams, oldParams, changed) { - if (isRoutingInProgress) { - return; - } - - if (changed.view && browseObject) { - let provider = openmct - .objectViews - .getByProviderKey(changed.view); - viewObject(browseObject, provider); - } - } - - function viewObject(object, viewProvider) { - currentObjectPath = openmct.router.path; - - openmct.layout.$refs.browseObject.show(object, viewProvider.key, true, currentObjectPath); - openmct.layout.$refs.browseBar.domainObject = object; - - openmct.layout.$refs.browseBar.viewKey = viewProvider.key; - } - - function updateDocumentTitleOnNameMutation(domainObject) { - if (typeof domainObject.name === 'string' && domainObject.name !== document.title) { - document.title = domainObject.name; - } - } - - function navigateToPath(path, currentViewKey) { - navigateCall++; - let currentNavigation = navigateCall; - - if (unobserve) { - unobserve(); - unobserve = undefined; - } - - //Split path into object identifiers - if (!Array.isArray(path)) { - path = path.split('/'); - } - - return pathToObjects(path).then(objects => { - isRoutingInProgress = false; - - if (currentNavigation !== navigateCall) { - return; // Prevent race. - } - - objects = objects.reverse(); - - openmct.router.path = objects; - openmct.router.emit('afterNavigation'); - browseObject = objects[0]; - - openmct.layout.$refs.browseBar.domainObject = browseObject; - if (!browseObject) { - openmct.layout.$refs.browseObject.clear(); - - return; - } - - let currentProvider = openmct - .objectViews - .getByProviderKey(currentViewKey); - document.title = browseObject.name; //change document title to current object in main view - // assign listener to global for later clearing - unobserve = openmct.objects.observe(browseObject, '*', updateDocumentTitleOnNameMutation); - - if (currentProvider && currentProvider.canView(browseObject, openmct.router.path)) { - viewObject(browseObject, currentProvider); - - return; - } - - let defaultProvider = openmct.objectViews.get(browseObject, openmct.router.path)[0]; - if (defaultProvider) { - openmct.router.updateParams({ - view: defaultProvider.key - }); - } else { - openmct.router.updateParams({ - view: undefined - }); - openmct.layout.$refs.browseObject.clear(); - } - }); - } - - function pathToObjects(path) { - return Promise.all(path.map((keyString) => { - let identifier = openmct.objects.parseKeyString(keyString); - if (openmct.objects.supportsMutation(identifier)) { - return openmct.objects.getMutable(identifier); - } else { - return openmct.objects.get(identifier); - } - })); - } - - function navigateToFirstChildOfRoot() { - openmct.objects.get('ROOT') - .then(rootObject => { - const composition = openmct.composition.get(rootObject); - if (!composition) { - return; - } - - composition.load() - .then(children => { - let lastChild = children[children.length - 1]; - if (lastChild) { - let lastChildId = openmct.objects.makeKeyString(lastChild.identifier); - openmct.router.setPath(`#/browse/${lastChildId}`); - } - }) - .catch(e => console.error(e)); - }) - .catch(e => console.error(e)); - } - - function clearMutationListeners() { - if (openmct.router.path !== undefined) { - openmct.router.path.forEach((pathObject) => { - if (pathObject.isMutable) { - openmct.objects.destroyMutable(pathObject); - } - }); - } - } - }; + } + } + }; }); diff --git a/src/ui/toolbar/Toolbar.vue b/src/ui/toolbar/Toolbar.vue index dfe4661020..df220be9d9 100644 --- a/src/ui/toolbar/Toolbar.vue +++ b/src/ui/toolbar/Toolbar.vue @@ -20,28 +20,28 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/toolbar/components/toolbar-button.vue b/src/ui/toolbar/components/toolbar-button.vue index 3136a0820f..e530d97858 100644 --- a/src/ui/toolbar/components/toolbar-button.vue +++ b/src/ui/toolbar/components/toolbar-button.vue @@ -20,74 +20,72 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/toolbar/components/toolbar-checkbox.scss b/src/ui/toolbar/components/toolbar-checkbox.scss index 1614a97076..618c4ffc9c 100644 --- a/src/ui/toolbar/components/toolbar-checkbox.scss +++ b/src/ui/toolbar/components/toolbar-checkbox.scss @@ -1,45 +1,45 @@ .c-custom-checkbox { - $d: 14px; + $d: 14px; + display: flex; + align-items: center; + + label { + @include userSelectNone(); display: flex; align-items: center; + } - label { - @include userSelectNone(); - display: flex; - align-items: center; + &__box { + @include nice-input(); + display: flex; + align-items: center; + justify-content: center; + line-height: $d; + width: $d; + height: $d; + margin-right: $interiorMarginSm; + } + + input { + opacity: 0; + position: absolute; + + &:checked + label > .c-custom-checkbox__box { + background: $colorKey; + &:before { + color: $colorKeyFg; + content: $glyph-icon-check; + font-family: symbolsfont; + font-size: 0.6em; + } } - &__box { - @include nice-input(); - display: flex; - align-items: center; - justify-content: center; - line-height: $d; - width: $d; - height: $d; - margin-right: $interiorMarginSm; + &:not(:disabled) + label { + cursor: pointer; } - input { - opacity: 0; - position: absolute; - - &:checked + label > .c-custom-checkbox__box { - background: $colorKey; - &:before { - color: $colorKeyFg; - content: $glyph-icon-check; - font-family: symbolsfont; - font-size: 0.6em; - } - } - - &:not(:disabled) + label { - cursor: pointer; - } - - &:disabled + label { - opacity: 0.5; - } + &:disabled + label { + opacity: 0.5; } + } } diff --git a/src/ui/toolbar/components/toolbar-checkbox.vue b/src/ui/toolbar/components/toolbar-checkbox.vue index fc66a0d93a..e8723b809f 100644 --- a/src/ui/toolbar/components/toolbar-checkbox.vue +++ b/src/ui/toolbar/components/toolbar-checkbox.vue @@ -20,46 +20,46 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/toolbar/components/toolbar-color-picker.vue b/src/ui/toolbar/components/toolbar-color-picker.vue index 2165dfde51..0435fcf5fb 100644 --- a/src/ui/toolbar/components/toolbar-color-picker.vue +++ b/src/ui/toolbar/components/toolbar-color-picker.vue @@ -20,159 +20,155 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/toolbar/components/toolbar-input.vue b/src/ui/toolbar/components/toolbar-input.vue index e244694c60..36ec100395 100644 --- a/src/ui/toolbar/components/toolbar-input.vue +++ b/src/ui/toolbar/components/toolbar-input.vue @@ -20,64 +20,55 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/toolbar/components/toolbar-menu.vue b/src/ui/toolbar/components/toolbar-menu.vue index b31cbf4ac5..c7d11abf0f 100644 --- a/src/ui/toolbar/components/toolbar-menu.vue +++ b/src/ui/toolbar/components/toolbar-menu.vue @@ -20,57 +20,50 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/toolbar/components/toolbar-select-menu.vue b/src/ui/toolbar/components/toolbar-select-menu.vue index fbbf65efd5..d974e0dc0c 100644 --- a/src/ui/toolbar/components/toolbar-select-menu.vue +++ b/src/ui/toolbar/components/toolbar-select-menu.vue @@ -20,72 +20,64 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/toolbar/components/toolbar-separator.vue b/src/ui/toolbar/components/toolbar-separator.vue index e005a505ed..22f614fb38 100644 --- a/src/ui/toolbar/components/toolbar-separator.vue +++ b/src/ui/toolbar/components/toolbar-separator.vue @@ -20,16 +20,16 @@ at runtime from the About dialog for additional information. --> diff --git a/src/ui/toolbar/components/toolbar-toggle-button.vue b/src/ui/toolbar/components/toolbar-toggle-button.vue index 91f51fddfa..5c716e9791 100644 --- a/src/ui/toolbar/components/toolbar-toggle-button.vue +++ b/src/ui/toolbar/components/toolbar-toggle-button.vue @@ -20,50 +20,48 @@ at runtime from the About dialog for additional information. --> + + diff --git a/src/utils/agent/Agent.js b/src/utils/agent/Agent.js index ba2d47d43b..1e9c9b84ab 100644 --- a/src/utils/agent/Agent.js +++ b/src/utils/agent/Agent.js @@ -21,115 +21,119 @@ *****************************************************************************/ /** * The query service handles calls for browser and userAgent -* info using a comparison between the userAgent and key -* device names -* @constructor -* @param window the broser object model -* @memberof /utils/agent -*/ + * info using a comparison between the userAgent and key + * device names + * @constructor + * @param window the broser object model + * @memberof /utils/agent + */ export default class Agent { - constructor(window) { - const userAgent = window.navigator.userAgent; - const matches = userAgent.match(/iPad|iPhone|Android/i) || []; + constructor(window) { + const userAgent = window.navigator.userAgent; + const matches = userAgent.match(/iPad|iPhone|Android/i) || []; - this.userAgent = userAgent; - this.mobileName = matches[0]; - this.window = window; - this.touchEnabled = (window.ontouchstart !== undefined); + this.userAgent = userAgent; + this.mobileName = matches[0]; + this.window = window; + this.touchEnabled = window.ontouchstart !== undefined; + } + /** + * Check if the user is on a mobile device. + * @returns {boolean} true on mobile + */ + isMobile() { + return Boolean(this.mobileName); + } + /** + * Check if the user is on a phone-sized mobile device. + * @returns {boolean} true on a phone + */ + isPhone() { + if (this.isMobile()) { + if (this.isAndroidTablet()) { + return false; + } else if (this.mobileName === 'iPad') { + return false; + } else { + return true; + } + } else { + return false; } - /** - * Check if the user is on a mobile device. - * @returns {boolean} true on mobile - */ - isMobile() { - return Boolean(this.mobileName); + } + /** + * Check if the user is on a tablet sized android device + * @returns {boolean} true on an android tablet + */ + isAndroidTablet() { + if (this.mobileName === 'Android') { + if (this.isPortrait() && this.window.innerWidth >= 768) { + return true; + } else if (this.isLandscape() && this.window.innerHeight >= 768) { + return true; + } + } else { + return false; } - /** - * Check if the user is on a phone-sized mobile device. - * @returns {boolean} true on a phone - */ - isPhone() { - if (this.isMobile()) { - if (this.isAndroidTablet()) { - return false; - } else if (this.mobileName === 'iPad') { - return false; - } else { - return true; - } - } else { - return false; - } - } - /** - * Check if the user is on a tablet sized android device - * @returns {boolean} true on an android tablet - */ - isAndroidTablet() { - if (this.mobileName === 'Android') { - if (this.isPortrait() && this.window.innerWidth >= 768) { - return true; - } else if (this.isLandscape() && this.window.innerHeight >= 768) { - return true; - } - } else { - return false; - } - } - /** - * Check if the user is on a tablet-sized mobile device. - * @returns {boolean} true on a tablet - */ - isTablet() { - return (this.isMobile() && !this.isPhone() && this.mobileName !== 'Android') || (this.isMobile() && this.isAndroidTablet()); - } - /** - * Check if the user's device is in a portrait-style - * orientation (display width is narrower than display height.) - * @returns {boolean} true in portrait mode - */ - isPortrait() { - const { screen } = this.window; - const hasScreenOrientation = screen && Object.prototype.hasOwnProperty.call(screen, 'orientation'); - const hasWindowOrientation = Object.prototype.hasOwnProperty.call(this.window, 'orientation'); + } + /** + * Check if the user is on a tablet-sized mobile device. + * @returns {boolean} true on a tablet + */ + isTablet() { + return ( + (this.isMobile() && !this.isPhone() && this.mobileName !== 'Android') || + (this.isMobile() && this.isAndroidTablet()) + ); + } + /** + * Check if the user's device is in a portrait-style + * orientation (display width is narrower than display height.) + * @returns {boolean} true in portrait mode + */ + isPortrait() { + 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; + 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; - } + 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 - * orientation (display width is greater than display height.) - * @returns {boolean} true in landscape mode - */ - isLandscape() { - return !this.isPortrait(); - } - /** - * Check if the user's device supports a touch interface. - * @returns {boolean} true if touch is supported - */ - isTouch() { - return this.touchEnabled; - } - /** - * Check if the user agent matches a certain named device, - * as indicated by checking for a case-insensitive substring - * match. - * @param {string} name the name to check for - * @returns {boolean} true if the user agent includes that name - */ - isBrowser(name) { - name = name.toLowerCase(); + } + /** + * Check if the user's device is in a landscape-style + * orientation (display width is greater than display height.) + * @returns {boolean} true in landscape mode + */ + isLandscape() { + return !this.isPortrait(); + } + /** + * Check if the user's device supports a touch interface. + * @returns {boolean} true if touch is supported + */ + isTouch() { + return this.touchEnabled; + } + /** + * Check if the user agent matches a certain named device, + * as indicated by checking for a case-insensitive substring + * match. + * @param {string} name the name to check for + * @returns {boolean} true if the user agent includes that name + */ + isBrowser(name) { + name = name.toLowerCase(); - return this.userAgent.toLowerCase().indexOf(name) !== -1; - } + return this.userAgent.toLowerCase().indexOf(name) !== -1; + } } diff --git a/src/utils/agent/AgentSpec.js b/src/utils/agent/AgentSpec.js index e362c30cb6..570b441dfe 100644 --- a/src/utils/agent/AgentSpec.js +++ b/src/utils/agent/AgentSpec.js @@ -19,106 +19,105 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -import Agent from "./Agent"; +import Agent from './Agent'; const TEST_USER_AGENTS = { - DESKTOP: - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.89 Safari/537.36", - IPAD: - "Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53", - IPHONE: - "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53" + DESKTOP: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.89 Safari/537.36', + IPAD: 'Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53', + IPHONE: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53' }; -describe("The Agent", function () { - let testWindow; - let agent; +describe('The Agent', function () { + let testWindow; + let agent; - beforeEach(function () { - testWindow = { - innerWidth: 640, - innerHeight: 480, - navigator: { - userAgent: TEST_USER_AGENTS.DESKTOP - } - }; - }); + beforeEach(function () { + testWindow = { + innerWidth: 640, + innerHeight: 480, + navigator: { + userAgent: TEST_USER_AGENTS.DESKTOP + } + }; + }); - it("recognizes desktop devices as non-mobile", function () { - testWindow.navigator.userAgent = TEST_USER_AGENTS.DESKTOP; - agent = new Agent(testWindow); - expect(agent.isMobile()).toBeFalsy(); - expect(agent.isPhone()).toBeFalsy(); - expect(agent.isTablet()).toBeFalsy(); - }); + it('recognizes desktop devices as non-mobile', function () { + testWindow.navigator.userAgent = TEST_USER_AGENTS.DESKTOP; + agent = new Agent(testWindow); + expect(agent.isMobile()).toBeFalsy(); + expect(agent.isPhone()).toBeFalsy(); + expect(agent.isTablet()).toBeFalsy(); + }); - it("detects iPhones", function () { - testWindow.navigator.userAgent = TEST_USER_AGENTS.IPHONE; - agent = new Agent(testWindow); - expect(agent.isMobile()).toBeTruthy(); - expect(agent.isPhone()).toBeTruthy(); - expect(agent.isTablet()).toBeFalsy(); - }); + it('detects iPhones', function () { + testWindow.navigator.userAgent = TEST_USER_AGENTS.IPHONE; + agent = new Agent(testWindow); + expect(agent.isMobile()).toBeTruthy(); + expect(agent.isPhone()).toBeTruthy(); + expect(agent.isTablet()).toBeFalsy(); + }); - it("detects iPads", function () { - testWindow.navigator.userAgent = TEST_USER_AGENTS.IPAD; - agent = new Agent(testWindow); - expect(agent.isMobile()).toBeTruthy(); - expect(agent.isPhone()).toBeFalsy(); - expect(agent.isTablet()).toBeTruthy(); - }); + it('detects iPads', function () { + testWindow.navigator.userAgent = TEST_USER_AGENTS.IPAD; + agent = new Agent(testWindow); + expect(agent.isMobile()).toBeTruthy(); + expect(agent.isPhone()).toBeFalsy(); + expect(agent.isTablet()).toBeTruthy(); + }); - it("detects display orientation by innerHeight and innerWidth", function () { - agent = new Agent(testWindow); - testWindow.innerWidth = 1024; - testWindow.innerHeight = 400; - expect(agent.isPortrait()).toBeFalsy(); - expect(agent.isLandscape()).toBeTruthy(); - testWindow.innerWidth = 400; - testWindow.innerHeight = 1024; - expect(agent.isPortrait()).toBeTruthy(); - expect(agent.isLandscape()).toBeFalsy(); - }); + it('detects display orientation by innerHeight and innerWidth', function () { + agent = new Agent(testWindow); + testWindow.innerWidth = 1024; + testWindow.innerHeight = 400; + expect(agent.isPortrait()).toBeFalsy(); + expect(agent.isLandscape()).toBeTruthy(); + testWindow.innerWidth = 400; + testWindow.innerHeight = 1024; + expect(agent.isPortrait()).toBeTruthy(); + 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 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 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); - delete testWindow.ontouchstart; - expect(new Agent(testWindow).isTouch()).toBe(false); - }); + it('detects touch support', function () { + testWindow.ontouchstart = null; + expect(new Agent(testWindow).isTouch()).toBe(true); + delete testWindow.ontouchstart; + expect(new Agent(testWindow).isTouch()).toBe(false); + }); - it("allows for checking browser type", function () { - testWindow.navigator.userAgent = "Chromezilla Safarifox"; - agent = new Agent(testWindow); - expect(agent.isBrowser("Chrome")).toBe(true); - expect(agent.isBrowser("Firefox")).toBe(false); - }); + it('allows for checking browser type', function () { + testWindow.navigator.userAgent = 'Chromezilla Safarifox'; + agent = new Agent(testWindow); + expect(agent.isBrowser('Chrome')).toBe(true); + expect(agent.isBrowser('Firefox')).toBe(false); + }); }); diff --git a/src/utils/clipboard.js b/src/utils/clipboard.js index b4f7645c88..5a35ca58a6 100644 --- a/src/utils/clipboard.js +++ b/src/utils/clipboard.js @@ -1,13 +1,13 @@ class Clipboard { - updateClipboard(newClip) { - // return promise - return navigator.clipboard.writeText(newClip); - } + updateClipboard(newClip) { + // return promise + return navigator.clipboard.writeText(newClip); + } - readClipboard() { - // return promise - return navigator.clipboard.readText(); - } + readClipboard() { + // return promise + return navigator.clipboard.readText(); + } } export default new Clipboard(); diff --git a/src/utils/clock/DefaultClock.js b/src/utils/clock/DefaultClock.js index 534a0ad4fb..1ee4fb11b6 100644 --- a/src/utils/clock/DefaultClock.js +++ b/src/utils/clock/DefaultClock.js @@ -30,60 +30,59 @@ import EventEmitter from 'EventEmitter'; */ export default class DefaultClock extends EventEmitter { - constructor() { - super(); + constructor() { + super(); - this.key = 'clock'; + this.key = 'clock'; - this.cssClass = 'icon-clock'; - this.name = 'Clock'; - this.description = "A default clock for openmct."; + this.cssClass = 'icon-clock'; + this.name = 'Clock'; + this.description = 'A default clock for openmct.'; + } + + tick(tickValue) { + this.emit('tick', tickValue); + this.lastTick = tickValue; + } + + /** + * Register a listener for the clock. When it ticks, the + * clock will provide the time from the configured endpoint + * + * @param listener + * @returns {function} a function for deregistering the provided listener + */ + on(event) { + let result = super.on.apply(this, arguments); + + if (this.listeners(event).length === 1) { + this.start(); } - tick(tickValue) { - this.emit("tick", tickValue); - this.lastTick = tickValue; + return result; + } + + /** + * Register a listener for the clock. When it ticks, the + * clock will provide the current local system time + * + * @param listener + * @returns {function} a function for deregistering the provided listener + */ + off(event) { + let result = super.off.apply(this, arguments); + + if (this.listeners(event).length === 0) { + this.stop(); } - /** - * Register a listener for the clock. When it ticks, the - * clock will provide the time from the configured endpoint - * - * @param listener - * @returns {function} a function for deregistering the provided listener - */ - on(event) { - let result = super.on.apply(this, arguments); - - if (this.listeners(event).length === 1) { - this.start(); - } - - return result; - } - - /** - * Register a listener for the clock. When it ticks, the - * clock will provide the current local system time - * - * @param listener - * @returns {function} a function for deregistering the provided listener - */ - off(event) { - let result = super.off.apply(this, arguments); - - if (this.listeners(event).length === 0) { - this.stop(); - } - - return result; - } - - /** - * @returns {number} The last value provided for a clock tick - */ - currentValue() { - return this.lastTick; - } + return result; + } + /** + * @returns {number} The last value provided for a clock tick + */ + currentValue() { + return this.lastTick; + } } diff --git a/src/utils/clock/Ticker.js b/src/utils/clock/Ticker.js index 210d576f57..af04b79751 100644 --- a/src/utils/clock/Ticker.js +++ b/src/utils/clock/Ticker.js @@ -21,65 +21,69 @@ *****************************************************************************/ class Ticker { - constructor() { - this.callbacks = []; - this.last = new Date() - 1000; + constructor() { + this.callbacks = []; + this.last = new Date() - 1000; + } + + /** + * Calls functions every second, as close to the actual second + * tick as is feasible. + * @constructor + * @memberof utils/clock + */ + tick() { + const timestamp = new Date(); + const millis = timestamp % 1000; + + // Only update callbacks if a second has actually passed. + if (timestamp >= this.last + 1000) { + this.callbacks.forEach(function (callback) { + callback(timestamp); + }); + this.last = timestamp - millis; } - /** - * Calls functions every second, as close to the actual second - * tick as is feasible. - * @constructor - * @memberof utils/clock - */ - tick() { - const timestamp = new Date(); - const millis = timestamp % 1000; + // Try to update at exactly the next second + this.timeoutHandle = setTimeout( + () => { + this.tick(); + }, + 1000 - millis, + true + ); + } - // Only update callbacks if a second has actually passed. - if (timestamp >= this.last + 1000) { - this.callbacks.forEach(function (callback) { - callback(timestamp); - }); - this.last = timestamp - millis; - } - - // Try to update at exactly the next second - this.timeoutHandle = setTimeout(() => { - this.tick(); - }, 1000 - millis, true); + /** + * Listen for clock ticks. The provided callback will + * be invoked with the current timestamp (in milliseconds + * since Jan 1 1970) at regular intervals, as near to the + * second boundary as possible. + * + * @param {Function} callback callback to invoke + * @returns {Function} a function to unregister this listener + */ + listen(callback) { + if (this.callbacks.length === 0) { + this.tick(); } - /** - * Listen for clock ticks. The provided callback will - * be invoked with the current timestamp (in milliseconds - * since Jan 1 1970) at regular intervals, as near to the - * second boundary as possible. - * - * @param {Function} callback callback to invoke - * @returns {Function} a function to unregister this listener - */ - listen(callback) { - if (this.callbacks.length === 0) { - this.tick(); - } + this.callbacks.push(callback); - this.callbacks.push(callback); + // Provide immediate feedback + callback(this.last); - // Provide immediate feedback - callback(this.last); + // Provide a deregistration function + return () => { + this.callbacks = this.callbacks.filter(function (cb) { + return cb !== callback; + }); - // Provide a deregistration function - return () => { - this.callbacks = this.callbacks.filter(function (cb) { - return cb !== callback; - }); - - if (this.callbacks.length === 0) { - clearTimeout(this.timeoutHandle); - } - }; - } + if (this.callbacks.length === 0) { + clearTimeout(this.timeoutHandle); + } + }; + } } let ticker = new Ticker(); diff --git a/src/utils/duration.js b/src/utils/duration.js index 087161aa23..d7e8fa62f1 100644 --- a/src/utils/duration.js +++ b/src/utils/duration.js @@ -26,50 +26,51 @@ const ONE_HOUR = ONE_MINUTE * 60; const ONE_DAY = ONE_HOUR * 24; function normalizeAge(num) { - const hundredtized = num * 100; - const isWhole = hundredtized % 100 === 0; + const hundredtized = num * 100; + const isWhole = hundredtized % 100 === 0; - return isWhole ? hundredtized / 100 : num; + return isWhole ? hundredtized / 100 : num; } function padLeadingZeros(num, numOfLeadingZeros) { - return num.toString().padStart(numOfLeadingZeros, '0'); + return num.toString().padStart(numOfLeadingZeros, '0'); } function toDoubleDigits(num) { - return padLeadingZeros(num, 2); + return padLeadingZeros(num, 2); } function toTripleDigits(num) { - return padLeadingZeros(num, 3); + return padLeadingZeros(num, 3); } function addTimeSuffix(value, suffix) { - return typeof value === 'number' && value > 0 ? `${value + suffix}` : ''; + return typeof value === 'number' && value > 0 ? `${value + suffix}` : ''; } export function millisecondsToDHMS(numericDuration) { - const ms = numericDuration || 0; - const dhms = [ - 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_SECOND)), "ms") - ].filter(Boolean).join(' '); + const ms = numericDuration || 0; + const dhms = [ + 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_SECOND)), 'ms') + ] + .filter(Boolean) + .join(' '); - return `${ dhms ? '+' : ''} ${dhms}`; + return `${dhms ? '+' : ''} ${dhms}`; } export function getPreciseDuration(value) { - const ms = value || 0; - - return [ - 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))), - toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND))) - ].join(":"); + const ms = value || 0; + return [ + 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))), + toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND))) + ].join(':'); } diff --git a/src/utils/raf.js b/src/utils/raf.js index d5c0c48fe5..1f4ee555e4 100644 --- a/src/utils/raf.js +++ b/src/utils/raf.js @@ -1,14 +1,14 @@ export default function raf(callback) { - let rendering = false; + let rendering = false; - return () => { - if (!rendering) { - rendering = true; + return () => { + if (!rendering) { + rendering = true; - requestAnimationFrame(() => { - callback(); - rendering = false; - }); - } - }; + requestAnimationFrame(() => { + callback(); + rendering = false; + }); + } + }; } diff --git a/src/utils/rafSpec.js b/src/utils/rafSpec.js index 0bf5ae9d9c..f03912b355 100644 --- a/src/utils/rafSpec.js +++ b/src/utils/rafSpec.js @@ -1,61 +1,65 @@ -import raf from "./raf"; +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); + 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++) { - unthrottledFunction(); - throttledFunction(); + throttledFunction(); } return new Promise((resolve) => { - requestAnimationFrame(resolve); - }).then(() => { - expect(unthrottledFunction).toHaveBeenCalledTimes(10); - expect(throttledCallback).toHaveBeenCalledTimes(1); + requestAnimationFrame(resolve); }); - }); - 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); - }); - }); + }) + .then(() => { + expect(throttledCallback).toHaveBeenCalledTimes(2); + }); + }); }); diff --git a/src/utils/staleness.js b/src/utils/staleness.js index 6c5e57632d..9603aba7f9 100644 --- a/src/utils/staleness.js +++ b/src/utils/staleness.js @@ -21,56 +21,56 @@ *****************************************************************************/ export default class StalenessUtils { - constructor(openmct, domainObject) { - this.openmct = openmct; - this.domainObject = domainObject; - this.metadata = this.openmct.telemetry.getMetadata(domainObject); - this.lastStalenessResponseTime = 0; + constructor(openmct, domainObject) { + this.openmct = openmct; + this.domainObject = domainObject; + this.metadata = this.openmct.telemetry.getMetadata(domainObject); + this.lastStalenessResponseTime = 0; - this.setTimeSystem(this.openmct.time.timeSystem()); - this.watchTimeSystem(); + this.setTimeSystem(this.openmct.time.timeSystem()); + this.watchTimeSystem(); + } + + shouldUpdateStaleness(stalenessResponse, id) { + const stalenessResponseTime = this.parseTime(stalenessResponse); + + if (stalenessResponseTime > this.lastStalenessResponseTime) { + this.lastStalenessResponseTime = stalenessResponseTime; + + return true; + } else { + return false; + } + } + + watchTimeSystem() { + this.openmct.time.on('timeSystem', this.setTimeSystem, this); + } + + unwatchTimeSystem() { + this.openmct.time.off('timeSystem', this.setTimeSystem, this); + } + + setTimeSystem(timeSystem) { + let metadataValue = { format: timeSystem.key }; + + if (this.metadata) { + metadataValue = this.metadata.value(timeSystem.key) ?? metadataValue; } - shouldUpdateStaleness(stalenessResponse, id) { - const stalenessResponseTime = this.parseTime(stalenessResponse); + const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); - if (stalenessResponseTime > this.lastStalenessResponseTime) { - this.lastStalenessResponseTime = stalenessResponseTime; + this.parseTime = (stalenessResponse) => { + const stalenessDatum = { + ...stalenessResponse, + source: stalenessResponse[timeSystem.key] + }; - return true; - } else { - return false; - } - } + return valueFormatter.parse(stalenessDatum); + }; + } - watchTimeSystem() { - this.openmct.time.on('timeSystem', this.setTimeSystem, this); - } - - unwatchTimeSystem() { - this.openmct.time.off('timeSystem', this.setTimeSystem, this); - } - - setTimeSystem(timeSystem) { - let metadataValue = { format: timeSystem.key }; - - if (this.metadata) { - metadataValue = this.metadata.value(timeSystem.key) ?? metadataValue; - } - - const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); - - this.parseTime = (stalenessResponse) => { - const stalenessDatum = { - ...stalenessResponse, - source: stalenessResponse[timeSystem.key] - }; - - return valueFormatter.parse(stalenessDatum); - }; - } - - destroy() { - this.unwatchTimeSystem(); - } + destroy() { + this.unwatchTimeSystem(); + } } diff --git a/src/utils/template/templateHelpers.js b/src/utils/template/templateHelpers.js index 70d381ce7d..884b0c6db8 100644 --- a/src/utils/template/templateHelpers.js +++ b/src/utils/template/templateHelpers.js @@ -1,14 +1,14 @@ export function convertTemplateToHTML(templateString) { - const template = document.createElement('template'); - template.innerHTML = templateString; + const template = document.createElement('template'); + template.innerHTML = templateString; - return template.content.cloneNode(true).children; + 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); - } + 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 index 974041f461..fec50c5afb 100644 --- a/src/utils/template/templateHelpersSpec.js +++ b/src/utils/template/templateHelpersSpec.js @@ -19,7 +19,7 @@ * 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"; +import { toggleClass } from '@/utils/template/templateHelpers'; const CLASS_AS_NON_EMPTY_STRING = 'class-to-toggle'; const CLASS_AS_EMPTY_STRING = ''; @@ -30,77 +30,86 @@ 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('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('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('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('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); - }); + }); + 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/testing.js b/src/utils/testing.js index 27bf7bd7c4..7c80c87233 100644 --- a/src/utils/testing.js +++ b/src/utils/testing.js @@ -26,122 +26,121 @@ let nativeFunctions = []; let mockObjects = setMockObjects(); const DEFAULT_TIME_OPTIONS = { - timeSystemKey: 'utc', - bounds: { - start: 0, - end: 1 - } + timeSystemKey: 'utc', + bounds: { + start: 0, + end: 1 + } }; export function createOpenMct(timeSystemOptions = DEFAULT_TIME_OPTIONS) { - const openmct = new MCT(); - openmct.install(openmct.plugins.LocalStorage()); - openmct.install(openmct.plugins.UTCTimeSystem()); - openmct.setAssetPath('/base'); + const openmct = new MCT(); + openmct.install(openmct.plugins.LocalStorage()); + openmct.install(openmct.plugins.UTCTimeSystem()); + openmct.setAssetPath('/base'); - const timeSystemKey = timeSystemOptions.timeSystemKey; - const start = timeSystemOptions.bounds.start; - const end = timeSystemOptions.bounds.end; + const timeSystemKey = timeSystemOptions.timeSystemKey; + const start = timeSystemOptions.bounds.start; + const end = timeSystemOptions.bounds.end; - openmct.time.timeSystem(timeSystemKey, { - start, - end - }); + openmct.time.timeSystem(timeSystemKey, { + start, + end + }); - return openmct; + return openmct; } export function createMouseEvent(eventName) { - return new MouseEvent(eventName, { - bubbles: true, - cancelable: true, - view: window - }); + return new MouseEvent(eventName, { + bubbles: true, + cancelable: true, + view: window + }); } export function spyOnBuiltins(functionNames, object = window) { - functionNames.forEach(functionName => { - if (nativeFunctions[functionName]) { - throw `Builtin spy function already defined for ${functionName}`; - } + functionNames.forEach((functionName) => { + if (nativeFunctions[functionName]) { + throw `Builtin spy function already defined for ${functionName}`; + } - nativeFunctions.push({ - functionName, - object, - nativeFunction: object[functionName] - }); - spyOn(object, functionName); + nativeFunctions.push({ + functionName, + object, + nativeFunction: object[functionName] }); + spyOn(object, functionName); + }); } export function clearBuiltinSpies() { - nativeFunctions.forEach(clearBuiltinSpy); - nativeFunctions = []; + nativeFunctions.forEach(clearBuiltinSpy); + nativeFunctions = []; } export function resetApplicationState(openmct) { - let promise; + let promise; - clearBuiltinSpies(); + clearBuiltinSpies(); - if (openmct !== undefined) { - openmct.destroy(); - } + if (openmct !== undefined) { + openmct.destroy(); + } - if (window.location.hash !== '#' && window.location.hash !== '') { - promise = new Promise((resolve, reject) => { - window.addEventListener('hashchange', cleanup); - window.location.hash = '#'; + if (window.location.hash !== '#' && window.location.hash !== '') { + promise = new Promise((resolve, reject) => { + window.addEventListener('hashchange', cleanup); + window.location.hash = '#'; - function cleanup() { - window.removeEventListener('hashchange', cleanup); - resolve(); - } - }); - } else { - promise = Promise.resolve(); - } + function cleanup() { + window.removeEventListener('hashchange', cleanup); + resolve(); + } + }); + } else { + promise = Promise.resolve(); + } - return promise; + return promise; } // required: key // optional: element, keyCode, type export function simulateKeyEvent(opts) { + if (!opts.key) { + console.warn('simulateKeyEvent needs a key'); - if (!opts.key) { - console.warn('simulateKeyEvent needs a key'); + return; + } - return; - } + const el = opts.element || document; + const key = opts.key; + const keyCode = opts.keyCode || key; + const type = opts.type || 'keydown'; + const event = new Event(type); - const el = opts.element || document; - const key = opts.key; - const keyCode = opts.keyCode || key; - const type = opts.type || 'keydown'; - const event = new Event(type); + event.keyCode = keyCode; + event.key = key; - event.keyCode = keyCode; - event.key = key; - - el.dispatchEvent(event); + el.dispatchEvent(event); } function clearBuiltinSpy(funcDefinition) { - funcDefinition.object[funcDefinition.functionName] = funcDefinition.nativeFunction; + funcDefinition.object[funcDefinition.functionName] = funcDefinition.nativeFunction; } export function getLatestTelemetry(telemetry = [], opts = {}) { - let latest = []; - let timeFormat = opts.timeFormat || 'utc'; + let latest = []; + let timeFormat = opts.timeFormat || 'utc'; - if (telemetry.length) { - latest = telemetry.reduce((prev, cur) => { - return prev[timeFormat] > cur[timeFormat] ? prev : cur; - }); - } + if (telemetry.length) { + latest = telemetry.reduce((prev, cur) => { + return prev[timeFormat] > cur[timeFormat] ? prev : cur; + }); + } - return latest; + return latest; } // EXAMPLE: @@ -161,81 +160,80 @@ export function getLatestTelemetry(telemetry = [], opts = {}) { // } // }) export function getMockObjects(opts = {}) { - opts.type = opts.type || 'default'; - if (opts.objectKeyStrings && !Array.isArray(opts.objectKeyStrings)) { - throw `"getMockObjects" optional parameter "objectKeyStrings" must be an array of string object keys`; - } + opts.type = opts.type || 'default'; + if (opts.objectKeyStrings && !Array.isArray(opts.objectKeyStrings)) { + throw `"getMockObjects" optional parameter "objectKeyStrings" must be an array of string object keys`; + } - let requestedMocks = {}; + let requestedMocks = {}; - if (!opts.objectKeyStrings) { - requestedMocks = copyObj(mockObjects[opts.type]); + if (!opts.objectKeyStrings) { + requestedMocks = copyObj(mockObjects[opts.type]); + } else { + opts.objectKeyStrings.forEach((objKey) => { + if (mockObjects[opts.type] && mockObjects[opts.type][objKey]) { + requestedMocks[objKey] = copyObj(mockObjects[opts.type][objKey]); + } else { + throw `No mock object for object key "${objKey}" of type "${opts.type}"`; + } + }); + } + + // build out custom telemetry mappings if necessary + if (requestedMocks.telemetry && opts.telemetryConfig) { + let keys = opts.telemetryConfig.keys; + let format = opts.telemetryConfig.format || 'utc'; + let hints = opts.telemetryConfig.hints; + let values; + + // if utc, keep default + if (format === 'utc') { + // save for later if new keys + if (keys) { + format = requestedMocks.telemetry.telemetry.values.find((vals) => vals.key === 'utc'); + } } else { - opts.objectKeyStrings.forEach(objKey => { - if (mockObjects[opts.type] && mockObjects[opts.type][objKey]) { - requestedMocks[objKey] = copyObj(mockObjects[opts.type][objKey]); - } else { - throw `No mock object for object key "${objKey}" of type "${opts.type}"`; - } - }); + format = { + key: format, + name: 'Time', + format: format === 'local' ? 'local-format' : format, + hints: { + domain: 1 + } + }; } - // build out custom telemetry mappings if necessary - if (requestedMocks.telemetry && opts.telemetryConfig) { - let keys = opts.telemetryConfig.keys; - let format = opts.telemetryConfig.format || 'utc'; - let hints = opts.telemetryConfig.hints; - let values; - - // if utc, keep default - if (format === 'utc') { - // save for later if new keys - if (keys) { - format = requestedMocks.telemetry - .telemetry.values.find((vals) => vals.key === 'utc'); - } - } else { - format = { - key: format, - name: "Time", - format: format === 'local' ? 'local-format' : format, - hints: { - domain: 1 - } - }; - } - - if (keys) { - values = keys.map((key) => ({ - key, - name: key + ' attribute' - })); - values.push(format); // add time format back in - } else { - values = requestedMocks.telemetry.telemetry.values; - } - - if (hints) { - for (let val of values) { - if (hints[val.key]) { - val.hints = hints[val.key]; - } - } - } - - requestedMocks.telemetry.telemetry.values = values; + if (keys) { + values = keys.map((key) => ({ + key, + name: key + ' attribute' + })); + values.push(format); // add time format back in + } else { + values = requestedMocks.telemetry.telemetry.values; } - // overwrite any field keys - if (opts.overwrite) { - for (let mock in requestedMocks) { - if (opts.overwrite[mock]) { - requestedMocks[mock] = Object.assign(requestedMocks[mock], opts.overwrite[mock]); - } + if (hints) { + for (let val of values) { + if (hints[val.key]) { + val.hints = hints[val.key]; } + } } - return requestedMocks; + requestedMocks.telemetry.telemetry.values = values; + } + + // overwrite any field keys + if (opts.overwrite) { + for (let mock in requestedMocks) { + if (opts.overwrite[mock]) { + requestedMocks[mock] = Object.assign(requestedMocks[mock], opts.overwrite[mock]); + } + } + } + + return requestedMocks; } // EXAMPLE: @@ -246,107 +244,112 @@ export function getMockObjects(opts = {}) { // format: 'local' // }) export function getMockTelemetry(opts = {}) { - let count = opts.count || 2; - let format = opts.format || 'utc'; - let name = opts.name || 'Mock Telemetry Datum'; - let keyCount = 2; - let keys = false; - let telemetry = []; + let count = opts.count || 2; + let format = opts.format || 'utc'; + let name = opts.name || 'Mock Telemetry Datum'; + let keyCount = 2; + let keys = false; + let telemetry = []; - if (opts.keys && Array.isArray(opts.keys)) { - keyCount = opts.keys.length; - keys = opts.keys; - } else if (opts.keyCount) { - keyCount = opts.keyCount; + if (opts.keys && Array.isArray(opts.keys)) { + keyCount = opts.keys.length; + keys = opts.keys; + } else if (opts.keyCount) { + keyCount = opts.keyCount; + } + + for (let i = 1; i < count + 1; i++) { + let datum = { + [format]: i, + name + }; + + for (let k = 1; k < keyCount + 1; k++) { + let key = keys ? keys[k - 1] : 'some-key-' + k; + let value = keys ? keys[k - 1] + ' value ' + i : 'some value ' + i + '-' + k; + datum[key] = value; } - for (let i = 1; i < count + 1; i++) { - let datum = { - [format]: i, - name - }; + telemetry.push(datum); + } - for (let k = 1; k < keyCount + 1; k++) { - let key = keys ? keys[k - 1] : 'some-key-' + k; - let value = keys ? keys[k - 1] + ' value ' + i : 'some value ' + i + '-' + k; - datum[key] = value; - } - - telemetry.push(datum); - } - - return telemetry; + return telemetry; } // copy objects a bit more easily function copyObj(obj) { - return JSON.parse(JSON.stringify(obj)); + return JSON.parse(JSON.stringify(obj)); } // add any other necessary types to this mockObjects object function setMockObjects() { - return { - default: { - folder: { - identifier: { - namespace: "", - key: "folder-object" - }, - name: "Test Folder Object", - type: "folder", - composition: [], - location: "mine" - }, - ladTable: { - identifier: { - namespace: "", - key: "lad-object" - }, - type: 'LadTable', - composition: [] - }, - ladTableSet: { - identifier: { - namespace: "", - key: "lad-set-object" - }, - type: 'LadTableSet', - composition: [] - }, - telemetry: { - identifier: { - namespace: "", - key: "telemetry-object" - }, - type: "test-telemetry-object", - name: "Test Telemetry Object", - telemetry: { - values: [{ - key: "name", - name: "Name", - format: "string" - }, { - key: "utc", - name: "Time", - format: "utc", - hints: { - domain: 1 - } - }, { - name: "Some attribute 1", - key: "some-key-1", - hints: { - range: 1 - } - }, { - name: "Some attribute 2", - key: "some-key-2" - }] - } - } + return { + default: { + folder: { + identifier: { + namespace: '', + key: 'folder-object' }, - otherType: { - example: {} + name: 'Test Folder Object', + type: 'folder', + composition: [], + location: 'mine' + }, + ladTable: { + identifier: { + namespace: '', + key: 'lad-object' + }, + type: 'LadTable', + composition: [] + }, + ladTableSet: { + identifier: { + namespace: '', + key: 'lad-set-object' + }, + type: 'LadTableSet', + composition: [] + }, + telemetry: { + identifier: { + namespace: '', + key: 'telemetry-object' + }, + type: 'test-telemetry-object', + name: 'Test Telemetry Object', + telemetry: { + values: [ + { + key: 'name', + name: 'Name', + format: 'string' + }, + { + key: 'utc', + name: 'Time', + format: 'utc', + hints: { + domain: 1 + } + }, + { + name: 'Some attribute 1', + key: 'some-key-1', + hints: { + range: 1 + } + }, + { + name: 'Some attribute 2', + key: 'some-key-2' + } + ] } - }; + } + }, + otherType: { + example: {} + } + }; } diff --git a/src/utils/testing/mockLocalStorage.js b/src/utils/testing/mockLocalStorage.js index baf993e70d..9200eb6e24 100644 --- a/src/utils/testing/mockLocalStorage.js +++ b/src/utils/testing/mockLocalStorage.js @@ -1,33 +1,33 @@ export function mockLocalStorage() { - let store; + let store; - beforeEach(() => { - spyOn(Storage.prototype, 'getItem').and.callFake(getItem); - spyOn(Storage.prototype, 'setItem').and.callFake(setItem); - spyOn(Storage.prototype, 'removeItem').and.callFake(removeItem); - spyOn(Storage.prototype, 'clear').and.callFake(clear); + beforeEach(() => { + spyOn(Storage.prototype, 'getItem').and.callFake(getItem); + spyOn(Storage.prototype, 'setItem').and.callFake(setItem); + spyOn(Storage.prototype, 'removeItem').and.callFake(removeItem); + spyOn(Storage.prototype, 'clear').and.callFake(clear); - store = {}; + store = {}; - function getItem(key) { - return store[key]; - } + function getItem(key) { + return store[key]; + } - function setItem(key, value) { - store[key] = typeof value === 'string' ? value : JSON.stringify(value); - } + function setItem(key, value) { + store[key] = typeof value === 'string' ? value : JSON.stringify(value); + } - function removeItem(key) { - store[key] = undefined; - delete store[key]; - } + function removeItem(key) { + store[key] = undefined; + delete store[key]; + } - function clear() { - store = {}; - } - }); + function clear() { + store = {}; + } + }); - afterEach(() => { - store = undefined; - }); + afterEach(() => { + store = undefined; + }); } diff --git a/src/utils/textHighlight/TextHighlight.vue b/src/utils/textHighlight/TextHighlight.vue index 06e1b30a50..d22ad706f1 100644 --- a/src/utils/textHighlight/TextHighlight.vue +++ b/src/utils/textHighlight/TextHighlight.vue @@ -20,48 +20,44 @@ at runtime from the About dialog for additional information. --> diff --git a/tsconfig.json b/tsconfig.json index b618a6e789..545e4e4f10 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,33 +1,27 @@ /* Note: Open MCT does not intend to support the entire Typescript ecosystem at this time. * This file is intended to add Intellisense for IDEs like VSCode. For more information * about Typescript, please discuss in https://github.com/nasa/openmct/discussions/4693 -*/ + */ { - "compilerOptions": { - "baseUrl": "./", - "allowJs": true, - "checkJs": false, - "declaration": true, - "emitDeclarationOnly": true, - "declarationMap": true, - "strict": true, - "esModuleInterop": true, - "noImplicitOverride": true, - "module": "esnext", - "moduleResolution": "node", - "outDir": "dist", - "skipLibCheck": true, - "paths": { - // matches the alias in webpack config, so that types for those imports are visible. - "@/*": ["src/*"] - } - }, - "include": [ - "src/api/**/*.js" - ], - "exclude": [ - "node_modules", - "dist", - "**/*Spec.js" - ] + "compilerOptions": { + "baseUrl": "./", + "allowJs": true, + "checkJs": false, + "declaration": true, + "emitDeclarationOnly": true, + "declarationMap": true, + "strict": true, + "esModuleInterop": true, + "noImplicitOverride": true, + "module": "esnext", + "moduleResolution": "node", + "outDir": "dist", + "skipLibCheck": true, + "paths": { + // matches the alias in webpack config, so that types for those imports are visible. + "@/*": ["src/*"] + } + }, + "include": ["src/api/**/*.js"], + "exclude": ["node_modules", "dist", "**/*Spec.js"] } From 804dbf0caba9efd631d6b7b90e9645ddfe4e6b66 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Thu, 18 May 2023 15:08:13 -0700 Subject: [PATCH 02/45] chore: add `prettier` (3/3): update `.git-blame-ignore-revs` file (#6684) Update `.git-blame-ignore-revs` file --- .git-blame-ignore-revs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index ab9bcde005..6aa1fd0665 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -4,3 +4,9 @@ # Requires Git > 2.23 # See https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revs-fileltfilegt +# Copyright year update 2022 +4a9744e916d24122a81092f6b7950054048ba860 +# Copyright year update 2023 +8040b275fcf2ba71b42cd72d4daa64bb25c19c2d +# Apply `prettier` formatting +caa7bc6faebc204f67aedae3e35fb0d0d3ce27a7 From 7e12a4596049d2281e8d820b20261be874ba6a91 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 May 2023 14:34:42 -0700 Subject: [PATCH 03/45] chore(deps-dev): bump vue-eslint-parser from 9.2.1 to 9.3.0 (#6671) Bumps [vue-eslint-parser](https://github.com/vuejs/vue-eslint-parser) from 9.2.1 to 9.3.0. - [Release notes](https://github.com/vuejs/vue-eslint-parser/releases) - [Commits](https://github.com/vuejs/vue-eslint-parser/compare/v9.2.1...v9.3.0) --- updated-dependencies: - dependency-name: vue-eslint-parser dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c41900879b..faa7fbd8d8 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "typescript": "5.0.4", "uuid": "9.0.0", "vue": "2.6.14", - "vue-eslint-parser": "9.2.1", + "vue-eslint-parser": "9.3.0", "vue-loader": "15.9.8", "vue-template-compiler": "2.6.14", "webpack": "5.81.0", From 356c90ca45b9ed19a2593b03796156cfc2875f26 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 May 2023 18:21:33 +0000 Subject: [PATCH 04/45] chore(deps-dev): bump @babel/eslint-parser from 7.19.1 to 7.21.8 (#6648) Bumps [@babel/eslint-parser](https://github.com/babel/babel/tree/HEAD/eslint/babel-eslint-parser) from 7.19.1 to 7.21.8. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.21.8/eslint/babel-eslint-parser) --- updated-dependencies: - dependency-name: "@babel/eslint-parser" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index faa7fbd8d8..7b6fe1e365 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "2.2.4-SNAPSHOT", "description": "The Open MCT core platform", "devDependencies": { - "@babel/eslint-parser": "7.19.1", + "@babel/eslint-parser": "7.21.8", "@braintree/sanitize-url": "6.0.2", "@deploysentinel/playwright": "0.3.4", "@percy/cli": "1.24.0", From fea68381a75adae5b4b8954cf6f9d9e807b2505d Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Wed, 24 May 2023 02:29:19 -0700 Subject: [PATCH 05/45] fix: unlisten to annotation event beforeDestroy (#6690) * fix: unlisten to annotation event beforeDestroy * refactor: `npm run lint:fix` --------- Co-authored-by: Scott Bell --- .../inspectorViews/annotations/AnnotationsInspectorView.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/inspectorViews/annotations/AnnotationsInspectorView.vue b/src/plugins/inspectorViews/annotations/AnnotationsInspectorView.vue index 0726fa6b7b..fcf2f29653 100644 --- a/src/plugins/inspectorViews/annotations/AnnotationsInspectorView.vue +++ b/src/plugins/inspectorViews/annotations/AnnotationsInspectorView.vue @@ -128,6 +128,7 @@ export default { await this.updateSelection(this.openmct.selection.get()); }, beforeDestroy() { + this.openmct.annotation.off('targetDomainObjectAnnotated', this.loadAnnotationForTargetObject); this.openmct.selection.off('change', this.updateSelection); const unobserveEntryFunctions = Object.values(this.unobserveEntries); unobserveEntryFunctions.forEach((unobserveEntry) => { From 47b44cebbaaaa5292bccea60c5ce3294371a6bab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 16:22:50 +0000 Subject: [PATCH 06/45] chore(deps-dev): bump jasmine-core from 4.5.0 to 5.0.0 (#6666) Bumps [jasmine-core](https://github.com/jasmine/jasmine) from 4.5.0 to 5.0.0. - [Release notes](https://github.com/jasmine/jasmine/releases) - [Changelog](https://github.com/jasmine/jasmine/blob/main/RELEASE.md) - [Commits](https://github.com/jasmine/jasmine/compare/v4.5.0...v5.0.0) --- updated-dependencies: - dependency-name: jasmine-core dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7b6fe1e365..c07c3523eb 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "git-rev-sync": "3.0.2", "html2canvas": "1.4.1", "imports-loader": "4.0.1", - "jasmine-core": "4.5.0", + "jasmine-core": "5.0.0", "karma": "6.4.2", "karma-chrome-launcher": "3.2.0", "karma-cli": "2.0.0", From 4d375ec765a6ae346112f306836938e74d3e2811 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 10:35:28 -0700 Subject: [PATCH 07/45] chore(deps-dev): bump webpack-cli from 5.0.2 to 5.1.1 (#6653) Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 5.0.2 to 5.1.1. - [Release notes](https://github.com/webpack/webpack-cli/releases) - [Changelog](https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md) - [Commits](https://github.com/webpack/webpack-cli/compare/webpack-cli@5.0.2...webpack-cli@5.1.1) --- updated-dependencies: - dependency-name: webpack-cli dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c07c3523eb..c83038625b 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "vue-loader": "15.9.8", "vue-template-compiler": "2.6.14", "webpack": "5.81.0", - "webpack-cli": "5.0.2", + "webpack-cli": "5.1.1", "webpack-dev-server": "4.13.3", "webpack-merge": "5.8.0" }, From 0bafdad605af2d34f24ece026fa8472d25323d40 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 17:45:50 +0000 Subject: [PATCH 08/45] chore(deps-dev): bump webpack from 5.81.0 to 5.84.0 (#6692) Bumps [webpack](https://github.com/webpack/webpack) from 5.81.0 to 5.84.0. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.81.0...v5.84.0) --- updated-dependencies: - dependency-name: webpack dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c83038625b..9aa54f4e9b 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "vue-eslint-parser": "9.3.0", "vue-loader": "15.9.8", "vue-template-compiler": "2.6.14", - "webpack": "5.81.0", + "webpack": "5.84.0", "webpack-cli": "5.1.1", "webpack-dev-server": "4.13.3", "webpack-merge": "5.8.0" From 4cab97cb4b90588966e5c322d4349cfe535667ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 17:57:44 +0000 Subject: [PATCH 09/45] chore(deps-dev): bump sinon from 15.0.1 to 15.1.0 (#6683) Bumps [sinon](https://github.com/sinonjs/sinon) from 15.0.1 to 15.1.0. - [Release notes](https://github.com/sinonjs/sinon/releases) - [Changelog](https://github.com/sinonjs/sinon/blob/main/docs/changelog.md) - [Commits](https://github.com/sinonjs/sinon/compare/v15.0.1...v15.1.0) --- updated-dependencies: - dependency-name: sinon dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9aa54f4e9b..90a58fb58b 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "sanitize-html": "2.10.0", "sass": "1.62.1", "sass-loader": "13.2.2", - "sinon": "15.0.1", + "sinon": "15.1.0", "style-loader": "3.3.2", "typescript": "5.0.4", "uuid": "9.0.0", From 1c6214fe79f399af1dbc109579f8901f0ea8b349 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 15:54:37 -0700 Subject: [PATCH 10/45] chore(deps-dev): bump eslint from 8.40.0 to 8.41.0 (#6700) Bumps [eslint](https://github.com/eslint/eslint) from 8.40.0 to 8.41.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v8.40.0...v8.41.0) --- updated-dependencies: - dependency-name: eslint dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 90a58fb58b..cce92e0e48 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "d3-axis": "3.0.0", "d3-scale": "3.3.0", "d3-selection": "3.0.0", - "eslint": "8.40.0", + "eslint": "8.41.0", "eslint-plugin-compat": "4.1.4", "eslint-config-prettier": "8.8.0", "eslint-plugin-playwright": "0.12.0", From 295bfe92940e6a121ff0bba381d58f06d7301af8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 23:16:31 +0000 Subject: [PATCH 11/45] chore(deps-dev): bump eslint-plugin-vue from 9.13.0 to 9.14.1 (#6696) Bumps [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue) from 9.13.0 to 9.14.1. - [Release notes](https://github.com/vuejs/eslint-plugin-vue/releases) - [Commits](https://github.com/vuejs/eslint-plugin-vue/compare/v9.13.0...v9.14.1) --- updated-dependencies: - dependency-name: eslint-plugin-vue dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cce92e0e48..6956ccd2e0 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "eslint-config-prettier": "8.8.0", "eslint-plugin-playwright": "0.12.0", "eslint-plugin-prettier": "4.2.1", - "eslint-plugin-vue": "9.13.0", + "eslint-plugin-vue": "9.14.1", "eslint-plugin-you-dont-need-lodash-underscore": "6.12.0", "eventemitter3": "1.2.0", "file-saver": "2.0.5", From 47c5863edffbf59ef6d05dd6af9097bfe22a4c51 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 23:32:56 +0000 Subject: [PATCH 12/45] chore(deps-dev): bump @percy/cli from 1.24.0 to 1.24.2 (#6699) Bumps [@percy/cli](https://github.com/percy/cli/tree/HEAD/packages/cli) from 1.24.0 to 1.24.2. - [Release notes](https://github.com/percy/cli/releases) - [Commits](https://github.com/percy/cli/commits/v1.24.2/packages/cli) --- updated-dependencies: - dependency-name: "@percy/cli" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6956ccd2e0..a24f4b9132 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@babel/eslint-parser": "7.21.8", "@braintree/sanitize-url": "6.0.2", "@deploysentinel/playwright": "0.3.4", - "@percy/cli": "1.24.0", + "@percy/cli": "1.24.2", "@percy/playwright": "1.0.4", "@playwright/test": "1.32.3", "@types/eventemitter3": "1.2.0", From 9247951456799efed66c1cf77377b7476dbc9e49 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Wed, 31 May 2023 16:50:41 -0700 Subject: [PATCH 13/45] chore: bump version to `2.2.5-SNAPSHOT` (#6705) chore: bump snapshot version to 2.2.5-SNAPSHOT --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a24f4b9132..ec042facaa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openmct", - "version": "2.2.4-SNAPSHOT", + "version": "2.2.5-SNAPSHOT", "description": "The Open MCT core platform", "devDependencies": { "@babel/eslint-parser": "7.21.8", From 07373817b0758eccbed557808b2e4ad99d73540d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jun 2023 12:59:47 -0700 Subject: [PATCH 14/45] chore(deps-dev): bump webpack from 5.84.0 to 5.85.0 (#6704) Bumps [webpack](https://github.com/webpack/webpack) from 5.84.0 to 5.85.0. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.84.0...v5.85.0) --- updated-dependencies: - dependency-name: webpack dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ec042facaa..71cdf9a041 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "vue-eslint-parser": "9.3.0", "vue-loader": "15.9.8", "vue-template-compiler": "2.6.14", - "webpack": "5.84.0", + "webpack": "5.85.0", "webpack-cli": "5.1.1", "webpack-dev-server": "4.13.3", "webpack-merge": "5.8.0" From a9158a90d5b61eeda90393e9da01516e80658b02 Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Thu, 1 Jun 2023 23:26:14 +0200 Subject: [PATCH 15/45] Support filtering by severity for events tables (#6672) * hide tab if not editing and fix issue where configuration is null * show filters tab if editing * works with dropdown * add a none filter to remove 'filters applied' styling' * pass appropriate comparator * openmct side is ready * clear filter still not working * fix clearing of procedures * add filters * add some basic documentation * add some basic documentation * add some basic documentation * fix grammar issues and convert away from amd pattern * convert to permanent links * refactor: format with prettier * add aria labels for selects --- .../filters/FiltersInspectorViewProvider.js | 12 +- src/plugins/filters/README.md | 53 +++++++++ .../filters/components/FilterField.vue | 52 +++++++-- .../filters/components/FilterObject.vue | 15 ++- .../filters/components/GlobalFilters.vue | 11 ++ .../TableConfigurationViewProvider.js | 105 +++++++++--------- .../components/table-configuration.vue | 29 ++--- 7 files changed, 196 insertions(+), 81 deletions(-) create mode 100644 src/plugins/filters/README.md diff --git a/src/plugins/filters/FiltersInspectorViewProvider.js b/src/plugins/filters/FiltersInspectorViewProvider.js index d71fbd2fa1..16b5029fc1 100644 --- a/src/plugins/filters/FiltersInspectorViewProvider.js +++ b/src/plugins/filters/FiltersInspectorViewProvider.js @@ -49,10 +49,16 @@ define(['./components/FiltersView.vue', 'vue'], function (FiltersView, Vue) { }); }, showTab: function (isEditing) { - const hasPersistedFilters = Boolean(domainObject?.configuration?.filters); - const hasGlobalFilters = Boolean(domainObject?.configuration?.globalFilters); + if (isEditing) { + return true; + } - return hasPersistedFilters || hasGlobalFilters; + const metadata = openmct.telemetry.getMetadata(domainObject); + const metadataWithFilters = metadata + ? metadata.valueMetadatas.filter((value) => value.filters) + : []; + + return metadataWithFilters.length; }, priority: function () { return openmct.priority.DEFAULT; diff --git a/src/plugins/filters/README.md b/src/plugins/filters/README.md new file mode 100644 index 0000000000..321fa30432 --- /dev/null +++ b/src/plugins/filters/README.md @@ -0,0 +1,53 @@ + +# Server side filtering in Open MCT + +## Introduction + +In Open MCT, filters can be constructed to filter out telemetry data on the server side. This is useful for reducing the amount of data that needs to be sent to the client. For example, in [Open MCT for MCWS](https://github.com/NASA-AMMOS/openmct-mcws/blob/e8846d325cc3f659d8ad58d1d24efaafbe2b6bb7/src/constants.js#L115), they can be used to filter realtime data from recorded data. In the [Open MCT YAMCS plugin](https://github.com/akhenry/openmct-yamcs/blob/9c4ed03e23848db3215fdb6a988ba34b445a3989/src/providers/events.js#L44), we can use them to filter incoming event data by severity. + +## Installing the filter plugin + +You'll need to install the filter plugin first. For example: + +```js +openmct.install(openmct.plugins.Filters(['telemetry.plot.overlay', 'table'])); +``` + +will install the filters plugin and have it apply to overlay plots and tables. You can see an example of this in the [Open MCT YAMCS plugin](https://github.com/akhenry/openmct-yamcs/blob/9c4ed03e23848db3215fdb6a988ba34b445a3989/example/index.js#L58). + +## Defining a filter + +To define a filter, you'll need to add a new `filter` property to the domain object's `telemetry` metadata underneath the `values` array. For example, if you have a domain object with a `telemetry` metadata that looks like this: + +```js +{ + key: 'fruit', + name: 'Types of fruit', + filters: [{ + singleSelectionThreshold: true, + comparator: 'equals', + possibleValues: [ + { name: 'Apple', value: 'apple' }, + { name: 'Banana', value: 'banana' }, + { name: 'Orange', value: 'orange' } + ] + }] +} +``` + +This will define a filter that allows an operator to choose one (due to `singleSelectionThreshold` being `true`) of the three possible values. The `comparator` property defines how the filter will be applied to the telemetry data. +Setting `singleSelectionThreshold` to `false` will render the `possibleValues` as a series of checkboxes. Removing the `possibleValues` property will render the filter as a text box, allowing the operator to enter a value to filter on. + +Note that how the filter is interpreted is ultimately decided by the individual telemetry providers. + +## Implementing a filter in a telemetry provider + +Implementing a filter requires two parts: + +- First, one needs to add the filter implementation to the [subscribe](https://github.com/nasa/openmct/blob/5df7971438acb9e8b933edda2aed432b1b8bb27d/src/api/telemetry/TelemetryAPI.js#L366) method in your telemetry provider. The filter will be passed to you in the `options` argument. You can either add the filter to your telemetry subscription request, or filter manually as new messages appears. An example of the latter is [shown in the YAMCS plugin for Open MCT](https://github.com/akhenry/openmct-yamcs/blob/9c4ed03e23848db3215fdb6a988ba34b445a3989/src/providers/events.js#L95). + +- Second, one needs to add the filter implementation to the [request](https://github.com/nasa/openmct/blob/5df7971438acb9e8b933edda2aed432b1b8bb27d/src/api/telemetry/TelemetryAPI.js#L318) method in your telemetry provider. The filter again will be passed to you in the `options` argument. You can either add the filter to your telemetry request, or filter manually after the request is made. An example of the former is [shown in the YAMCS plugin for Open MCT](https://github.com/akhenry/openmct-yamcs/blob/9c4ed03e23848db3215fdb6a988ba34b445a3989/src/providers/historical-telemetry-provider.js#L171). + +## Using filters + +If you installed the plugin to have it apply to `table`, create a Telemetry Table in Open MCT and drag your telemetry object that contains the filter to it. Then click "Edit", and notice the "Filter" tab in the inspector. It allows operator to either select a "Global Filter", or a regular filter. The "Global Filter" will apply for all telemetry objects in the table, while the regular filter will only apply to the telemetry object that it is defined on. \ No newline at end of file diff --git a/src/plugins/filters/components/FilterField.vue b/src/plugins/filters/components/FilterField.vue index d8edc53fff..40daab0a71 100644 --- a/src/plugins/filters/components/FilterField.vue +++ b/src/plugins/filters/components/FilterField.vue @@ -37,14 +37,37 @@ :id="`${filter}filterControl`" class="c-input--flex" type="text" + :aria-label="label" :disabled="useGlobal" :value="persistedValue(filter)" - @change="updateFilterValue($event, filter)" + @change="updateFilterValueFromString($event, filter)" /> + + + -