From 10ff4e1781e483eee0b9dbbdb02b4fdf23083545 Mon Sep 17 00:00:00 2001 From: Vitor Henckel Date: Tue, 9 Aug 2022 17:06:43 -0300 Subject: [PATCH 001/274] fix: removing maelstrom theme from application (#5600) --- src/plugins/plugins.js | 3 --- src/plugins/themes/maelstrom-theme.scss | 22 ---------------------- src/plugins/themes/maelstrom.js | 7 ------- webpack.common.js | 1 - 4 files changed, 33 deletions(-) delete mode 100644 src/plugins/themes/maelstrom-theme.scss delete mode 100644 src/plugins/themes/maelstrom.js diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js index 10264993ec..48449f6915 100644 --- a/src/plugins/plugins.js +++ b/src/plugins/plugins.js @@ -58,7 +58,6 @@ define([ './condition/plugin', './conditionWidget/plugin', './themes/espresso', - './themes/maelstrom', './themes/snow', './URLTimeSettingsSynchronizer/plugin', './notificationIndicator/plugin', @@ -122,7 +121,6 @@ define([ ConditionPlugin, ConditionWidgetPlugin, Espresso, - Maelstrom, Snow, URLTimeSettingsSynchronizer, NotificationIndicator, @@ -207,7 +205,6 @@ define([ plugins.ClearData = ClearData; plugins.WebPage = WebPagePlugin.default; plugins.Espresso = Espresso.default; - plugins.Maelstrom = Maelstrom.default; plugins.Snow = Snow.default; plugins.Condition = ConditionPlugin.default; plugins.ConditionWidget = ConditionWidgetPlugin.default; diff --git a/src/plugins/themes/maelstrom-theme.scss b/src/plugins/themes/maelstrom-theme.scss deleted file mode 100644 index 69e327cf67..0000000000 --- a/src/plugins/themes/maelstrom-theme.scss +++ /dev/null @@ -1,22 +0,0 @@ -@import "../../styles/vendor/normalize-min"; -@import "../../styles/constants"; -@import "../../styles/constants-mobile.scss"; - -@import "../../styles/constants-maelstrom"; - -@import "../../styles/mixins"; -@import "../../styles/animations"; -@import "../../styles/about"; -@import "../../styles/glyphs"; -@import "../../styles/global"; -@import "../../styles/status"; -@import "../../styles/limits"; -@import "../../styles/controls"; -@import "../../styles/forms"; -@import "../../styles/table"; -@import "../../styles/legacy"; -@import "../../styles/legacy-plots"; -@import "../../styles/plotly"; -@import "../../styles/legacy-messages"; - -@import "../../styles/vue-styles.scss"; diff --git a/src/plugins/themes/maelstrom.js b/src/plugins/themes/maelstrom.js deleted file mode 100644 index c551b1f729..0000000000 --- a/src/plugins/themes/maelstrom.js +++ /dev/null @@ -1,7 +0,0 @@ -import { installTheme } from './installTheme'; - -export default function plugin() { - return function install(openmct) { - installTheme(openmct, 'maelstrom'); - }; -} diff --git a/webpack.common.js b/webpack.common.js index 95a76fd404..e48773f56f 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -30,7 +30,6 @@ const config = { inMemorySearchWorker: './src/api/objects/InMemorySearchWorker.js', espressoTheme: './src/plugins/themes/espresso-theme.scss', snowTheme: './src/plugins/themes/snow-theme.scss', - maelstromTheme: './src/plugins/themes/maelstrom-theme.scss' }, output: { globalObject: 'this', From a671be726bcd87fdd45ce02f1a067894628dfc55 Mon Sep 17 00:00:00 2001 From: John Hill Date: Fri, 12 Aug 2022 08:02:15 -0700 Subject: [PATCH 002/274] [CI] Update dependabot ignore list for dependencies which will always fail (#5652) --- .github/dependabot.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a5545f0cd7..a6f59cb089 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,12 +7,13 @@ updates: interval: "daily" open-pull-requests-limit: 10 labels: + - "pr:e2e" - "type:maintenance" - "dependencies" - - "pr:e2e" - "pr:daveit" - - "pr:visual" - "pr:platform" + ignore: + - dependency-name: "@playwright/test" #we source the container instead of the dependency in CI - package-ecosystem: "github-actions" directory: "/" From a584766618d7a1f9889460db889617aec6c1579d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Aug 2022 15:29:44 -0700 Subject: [PATCH 003/274] Bump imports-loader from 0.8.0 to 4.0.1 (#5658) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4ae90aafb6..81a3cd02e5 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "file-saver": "2.0.5", "git-rev-sync": "3.0.2", "html2canvas": "1.4.1", - "imports-loader": "0.8.0", + "imports-loader": "4.0.1", "jasmine-core": "4.3.0", "jsdoc": "3.6.11", "karma": "6.3.20", From 1d875cb8ca939a0fda0708fe3571b09d4b85733a Mon Sep 17 00:00:00 2001 From: Mariusz Rosinski Date: Mon, 15 Aug 2022 22:01:45 +0200 Subject: [PATCH 004/274] 4386 - In time conductor history, show them on hover if only milliseconds have changed (#4414) Co-authored-by: Jamie V Co-authored-by: John Hill Co-authored-by: Andrew Henry Co-authored-by: Shefali Joshi --- src/plugins/timeConductor/pluginSpec.js | 8 ++++---- src/utils/duration.js | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/plugins/timeConductor/pluginSpec.js b/src/plugins/timeConductor/pluginSpec.js index d394784135..3d12594fec 100644 --- a/src/plugins/timeConductor/pluginSpec.js +++ b/src/plugins/timeConductor/pluginSpec.js @@ -131,15 +131,15 @@ describe('time conductor', () => { describe('duration functions', () => { it('should transform milliseconds to DHMS', () => { const functionResults = [millisecondsToDHMS(0), millisecondsToDHMS(86400000), - millisecondsToDHMS(129600000), millisecondsToDHMS(661824000)]; - const validResults = [' ', '+ 1d', '+ 1d 12h', '+ 7d 15h 50m 24s']; + millisecondsToDHMS(129600000), millisecondsToDHMS(661824000), millisecondsToDHMS(213927028)]; + const validResults = [' ', '+ 1d', '+ 1d 12h', '+ 7d 15h 50m 24s', '+ 2d 11h 25m 27s 28ms']; expect(validResults).toEqual(functionResults); }); it('should get precise duration', () => { const functionResults = [getPreciseDuration(0), getPreciseDuration(643680000), - getPreciseDuration(1605312000)]; - const validResults = ['00:00:00:00', '07:10:48:00', '18:13:55:12']; + getPreciseDuration(1605312000), getPreciseDuration(213927028)]; + const validResults = ['00:00:00:00:000', '07:10:48:00:000', '18:13:55:12:000', '02:11:25:27:028']; expect(validResults).toEqual(functionResults); }); }); diff --git a/src/utils/duration.js b/src/utils/duration.js index 708d4b786d..70b98378b0 100644 --- a/src/utils/duration.js +++ b/src/utils/duration.js @@ -32,8 +32,16 @@ function normalizeAge(num) { return isWhole ? hundredtized / 100 : num; } +function padLeadingZeros(num, numOfLeadingZeros) { + return num.toString().padStart(numOfLeadingZeros, '0'); +} + function toDoubleDigits(num) { - return num >= 10 ? num : `0${num}`; + return padLeadingZeros(num, 2); +} + +function toTripleDigits(num) { + return padLeadingZeros(num, 3); } function addTimeSuffix(value, suffix) { @@ -46,7 +54,8 @@ export function millisecondsToDHMS(numericDuration) { addTimeSuffix(Math.floor(normalizeAge(ms / ONE_DAY)), 'd'), addTimeSuffix(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR)), 'h'), addTimeSuffix(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE)), 'm'), - addTimeSuffix(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)), 's') + addTimeSuffix(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)), 's'), + addTimeSuffix(Math.floor(normalizeAge(ms % ONE_SECOND)), "ms") ].filter(Boolean).join(' '); return `${ dhms ? '+' : ''} ${dhms}`; @@ -59,7 +68,8 @@ export function getPreciseDuration(value) { toDoubleDigits(Math.floor(normalizeAge(ms / ONE_DAY))), toDoubleDigits(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR))), toDoubleDigits(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE))), - toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND))) + toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND))), + toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND))) ].join(":"); } From f979e170ee4d708b5240e6fa5361ee58518730d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Aug 2022 20:55:28 +0000 Subject: [PATCH 005/274] Bump eslint from 8.18.0 to 8.22.0 (#5666) Bumps [eslint](https://github.com/eslint/eslint) from 8.18.0 to 8.22.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.18.0...v8.22.0) --- updated-dependencies: - dependency-name: eslint dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Shefali Joshi --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 81a3cd02e5..f0817351b4 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "d3-axis": "3.0.0", "d3-scale": "3.3.0", "d3-selection": "3.0.0", - "eslint": "8.18.0", + "eslint": "8.22.0", "eslint-plugin-compat": "4.0.2", "eslint-plugin-playwright": "0.10.0", "eslint-plugin-vue": "9.3.0", From bbb84c695d13e5d345421f5c5adddb5c54d7186a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Aug 2022 14:09:01 -0700 Subject: [PATCH 006/274] Bump plotly.js-gl2d-dist from 2.12.0 to 2.14.0 (#5667) Bumps [plotly.js-gl2d-dist](https://github.com/plotly/plotly.js) from 2.12.0 to 2.14.0. - [Release notes](https://github.com/plotly/plotly.js/releases) - [Changelog](https://github.com/plotly/plotly.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/plotly/plotly.js/compare/v2.12.0...v2.14.0) --- updated-dependencies: - dependency-name: plotly.js-gl2d-dist dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] 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 f0817351b4..bb197d653a 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "nyc":"15.1.0", "painterro": "1.2.78", "plotly.js-basic-dist": "2.12.0", - "plotly.js-gl2d-dist": "2.12.0", + "plotly.js-gl2d-dist": "2.14.0", "printj": "1.3.1", "request": "2.88.2", "resolve-url-loader": "5.0.0", From b8fa89af6eda50177571bf20eaf4e756fbcf19df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Aug 2022 21:14:12 +0000 Subject: [PATCH 007/274] Bump plotly.js-basic-dist from 2.12.0 to 2.14.0 (#5645) * Bump plotly.js-basic-dist from 2.12.0 to 2.14.0 Bumps [plotly.js-basic-dist](https://github.com/plotly/plotly.js) from 2.12.0 to 2.14.0. - [Release notes](https://github.com/plotly/plotly.js/releases) - [Changelog](https://github.com/plotly/plotly.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/plotly/plotly.js/compare/v2.12.0...v2.14.0) --- updated-dependencies: - dependency-name: plotly.js-basic-dist dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Also update plotly gs2d dist version Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Shefali --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bb197d653a..45270d15af 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "node-bourbon": "4.2.3", "nyc":"15.1.0", "painterro": "1.2.78", - "plotly.js-basic-dist": "2.12.0", + "plotly.js-basic-dist": "2.14.0", "plotly.js-gl2d-dist": "2.14.0", "printj": "1.3.1", "request": "2.88.2", From 6820e0d0442da0d370ea3335ddd1e3213d813cb8 Mon Sep 17 00:00:00 2001 From: John Hill Date: Thu, 18 Aug 2022 07:20:05 -0700 Subject: [PATCH 008/274] Remove lighthouse, jsdoc, and bourbon dependencies (#5081) Co-authored-by: Andrew Henry --- package.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/package.json b/package.json index 45270d15af..3b4908fe8e 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "html2canvas": "1.4.1", "imports-loader": "4.0.1", "jasmine-core": "4.3.0", - "jsdoc": "3.6.11", "karma": "6.3.20", "karma-chrome-launcher": "3.1.1", "karma-cli": "2.0.0", @@ -46,14 +45,12 @@ "karma-sourcemap-loader": "0.3.8", "karma-spec-reporter": "0.0.34", "karma-webpack": "5.0.0", - "lighthouse": "9.6.1", "location-bar": "3.0.1", "lodash": "4.17.21", "mini-css-extract-plugin": "2.6.1", "moment": "2.29.4", "moment-duration-format": "2.3.2", "moment-timezone": "0.5.34", - "node-bourbon": "4.2.3", "nyc":"15.1.0", "painterro": "1.2.78", "plotly.js-basic-dist": "2.14.0", @@ -98,11 +95,8 @@ "test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js", "test:perf": "npx playwright test --config=e2e/playwright-performance.config.js", "test:watch": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run", - "jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api", "update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2022/gm' ./src/ui/layout/AboutDialog.vue", "update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2022/gm'", - "otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'", - "docs": "npm run jsdoc ; npm run otherdoc", "cov:e2e:report":"nyc report --reporter=lcovonly --report-dir=./coverage/e2e", "cov:e2e:full:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full", "cov:e2e:stable:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-stable", From ca928370a41a063d4a4f15efe2feb98536dd3f59 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Aug 2022 08:41:23 -0700 Subject: [PATCH 009/274] Bump sass from 1.52.2 to 1.54.4 (#5644) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3b4908fe8e..c526c1e3dd 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "printj": "1.3.1", "request": "2.88.2", "resolve-url-loader": "5.0.0", - "sass": "1.52.2", + "sass": "1.54.4", "sass-loader": "13.0.2", "sinon": "14.0.0", "style-loader": "^1.0.1", From efadf9036fd96558430668740d080920a2db40fa Mon Sep 17 00:00:00 2001 From: Mariusz Rosinski Date: Thu, 18 Aug 2022 19:58:34 +0200 Subject: [PATCH 010/274] 5413 - [Notebook] Various visual issues with renaming sections/pages (#5475) * 5413 - [Notebook] Various visual issues with renaming sections/pages * 5413 - [Notebook] Various visual issues with renaming sections/pages - 3rd Expectation * 5413 - [Notebook] Various visual issues with renaming sections/pages - requested changes * 5413 - [Notebook] Various visual issues with renaming sections/pages - remove a magic number (ENTER) * 5413 - [Notebook] Various visual issues with renaming sections/pages - key codes * 5413 - [Notebook] Various visual issues with renaming sections/pages - Separate selectability and editability actions for `Section` and `Page` components * Fix layout of Notebook nav item to align affordance arrow * Add e2e test stubs Co-authored-by: Andrew Henry Co-authored-by: Charles Hacskaylo Co-authored-by: Jesse Mazzella Co-authored-by: Jesse Mazzella --- .../plugins/notebook/notebook.e2e.spec.js | 34 ++++++++++++ .../notebook/components/PageComponent.vue | 52 +++++++++++-------- .../notebook/components/SectionComponent.vue | 52 +++++++++++-------- src/plugins/notebook/components/sidebar.scss | 2 +- .../notebook/utils/notebook-key-code.js | 3 ++ src/styles/notebook.scss | 6 +++ 6 files changed, 102 insertions(+), 47 deletions(-) create mode 100644 src/plugins/notebook/utils/notebook-key-code.js diff --git a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js index 0c33421814..51dfd42995 100644 --- a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js @@ -86,6 +86,23 @@ test.describe('Notebook section tests', () => { //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', () => { @@ -107,6 +124,23 @@ test.describe('Notebook page tests', () => { //Delete 3rd page //First is now selected and there is no default notebook }); + test.fixme('Page rename operations', async ({ page }) => { + // Create a new notebook + // Add a page + // Rename the page but do not confirm + // Keyboard press 'Escape' + // Verify that the page name reverts to the default name + // Rename the page but do not confirm + // Keyboard press 'Enter' + // Verify that the page name is updated + // Rename the page to "" (empty string) + // Keyboard press 'Enter' to confirm + // Verify that the page name reverts to the default name + // Rename the page to something long that overflows the text box + // Verify that the page name is not truncated while input is active + // Confirm the page name edit + // Verify that the page name is truncated now that input is not active + }); }); test.describe('Notebook search tests', () => { diff --git a/src/plugins/notebook/components/PageComponent.vue b/src/plugins/notebook/components/PageComponent.vue index 950cc142b7..cf623112ac 100644 --- a/src/plugins/notebook/components/PageComponent.vue +++ b/src/plugins/notebook/components/PageComponent.vue @@ -12,8 +12,10 @@ + + diff --git a/src/ui/layout/RecentObjectsListItem.vue b/src/ui/layout/RecentObjectsListItem.vue new file mode 100644 index 0000000000..c7b651e9e2 --- /dev/null +++ b/src/ui/layout/RecentObjectsListItem.vue @@ -0,0 +1,134 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + + + + diff --git a/src/ui/layout/layout.scss b/src/ui/layout/layout.scss index f9dc4d8c15..c2c10d8cb2 100644 --- a/src/ui/layout/layout.scss +++ b/src/ui/layout/layout.scss @@ -289,7 +289,7 @@ } &__pane-tree { - width: 300px; + width: 100%; padding-left: nth($shellPanePad, 2); } diff --git a/src/ui/layout/mct-tree.scss b/src/ui/layout/mct-tree.scss index 3dadf18c83..f782898203 100644 --- a/src/ui/layout/mct-tree.scss +++ b/src/ui/layout/mct-tree.scss @@ -108,6 +108,10 @@ color: $colorItemTreeSelectedFg; } } + &.is-targeted-item { + $c: $colorBodyFg; + @include pulseProp($animName: flashTarget, $dur: 500ms, $iter: 8, $prop: background, $valStart: rgba($c, 0.4), $valEnd: rgba($c, 0)); + } &.is-new { animation-name: animTemporaryHighlight; diff --git a/src/ui/layout/mct-tree.vue b/src/ui/layout/mct-tree.vue index 9c2151d58a..a59497a56a 100644 --- a/src/ui/layout/mct-tree.vue +++ b/src/ui/layout/mct-tree.vue @@ -88,10 +88,12 @@ :item-height="itemHeight" :open-items="openTreeItems" :loading-items="treeItemLoading" + :targeted-path="targetedPath" @tree-item-mounted="scrollToCheck($event)" @tree-item-destroyed="removeCompositionListenerFor($event)" @tree-item-action="treeItemAction(treeItem, $event)" @tree-item-selection="treeItemSelection(treeItem)" + @targeted-path-animation-end="targetedPathAnimationEnd()" />
item.navigationPath === navigationPath); const scrollTopAmount = indexOfScroll * this.itemHeight; diff --git a/src/ui/layout/pane.scss b/src/ui/layout/pane.scss index b830f24aec..1a1c900e26 100644 --- a/src/ui/layout/pane.scss +++ b/src/ui/layout/pane.scss @@ -83,6 +83,8 @@ &[class*="--vertical"] { padding-top: $interiorMargin; padding-bottom: $interiorMargin; + min-height: 30px; // For Recents holder + &.l-pane--collapsed { padding-top: 0 !important; padding-bottom: 0 !important; diff --git a/src/ui/layout/pane.vue b/src/ui/layout/pane.vue index 75c4353ced..4126e89557 100644 --- a/src/ui/layout/pane.vue +++ b/src/ui/layout/pane.vue @@ -1,20 +1,12 @@ diff --git a/src/ui/components/tags/tags.scss b/src/ui/components/tags/tags.scss index ebd3e7a184..964b2361ab 100644 --- a/src/ui/components/tags/tags.scss +++ b/src/ui/components/tags/tags.scss @@ -54,6 +54,10 @@ } } +.c-tag-btn__label { + overflow: visible!important; +} + /******************************* HOVERS */ .has-tag-applier { // Apply this class to all components that should trigger tag removal btn on hover From 4d84b16d8be1ddb614bedddec99781b5f85b9b54 Mon Sep 17 00:00:00 2001 From: Jamie V Date: Fri, 20 Jan 2023 18:27:19 -0800 Subject: [PATCH 161/274] [Notebook] Convert full links in entries, into clickable links (#6090) * Automatically promote urls to hyperlinks if matches whitelist * Disable v-html lint warning for notebook entries * Check whether domain endswith given partial Co-authored-by: Jesse Mazzella Co-authored-by: Andrew Henry --- .../plugins/notebook/notebook.e2e.spec.js | 73 +++++++++++++++++++ .../notebook/notebookWithCouchDB.e2e.spec.js | 4 + package.json | 1 + src/plugins/notebook/NotebookViewProvider.js | 7 +- .../notebook/components/NotebookEntry.vue | 54 +++++++++++++- src/plugins/notebook/plugin.js | 12 +-- 6 files changed, 139 insertions(+), 12 deletions(-) diff --git a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js index 66c1b038c0..8d867e7517 100644 --- a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js @@ -263,4 +263,77 @@ test.describe('Notebook entry tests', () => { }); test.fixme('new entries persist through navigation events without save', async ({ page }) => {}); test.fixme('previous and new entries can be deleted', async ({ page }) => {}); + test.fixme('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 page.goto('./#/browse/mine', { waitUntil: 'networkidle' }); + + // Create Notebook + const notebook = await createDomainObjectWithDefaults(page, { + type: 'Notebook', + name: "Entry Link Test" + }); + + await expandTreePaneItemByName(page, 'My Items'); + + await page.goto(notebook.url); + + await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`); + + const validLink = page.locator(`a[href="${TEST_LINK}"]`); + + // Start waiting for popup before clicking. Note no await. + const popupPromise = page.waitForEvent('popup'); + + await validLink.click(); + const popup = await popupPromise; + + // Wait for the popup to load. + await popup.waitForLoadState(); + expect.soft(popup.url()).toContain('www.google.com'); + + expect(await validLink.count()).toBe(1); + }); + test.fixme('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 page.goto('./#/browse/mine', { waitUntil: 'networkidle' }); + + // Create Notebook + const notebook = await createDomainObjectWithDefaults(page, { + type: 'Notebook', + name: "Entry Link Test" + }); + + await expandTreePaneItemByName(page, 'My Items'); + + await page.goto(notebook.url); + + await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`); + + const invalidLink = page.locator(`a[href="${TEST_LINK}"]`); + + expect(await invalidLink.count()).toBe(0); + }); + test.fixme('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 page.goto('./#/browse/mine', { waitUntil: 'networkidle' }); + + // Create Notebook + const notebook = await createDomainObjectWithDefaults(page, { + type: 'Notebook', + name: "Entry Link Test" + }); + + await expandTreePaneItemByName(page, 'My Items'); + + await page.goto(notebook.url); + + 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/notebookWithCouchDB.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js index 87a352797d..9c01100472 100644 --- a/e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebookWithCouchDB.e2e.spec.js @@ -76,6 +76,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => { await page.locator('text=To start a new entry, click here or drag and drop any object').click(); await page.locator('[aria-label="Notebook Entry Input"]').click(); await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`); + await page.locator('[aria-label="Notebook Entry Input"]').press('Enter'); await page.waitForLoadState('networkidle'); expect(addingNotebookElementsRequests.length).toBeLessThanOrEqual(2); @@ -148,14 +149,17 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => { await page.locator('text=To start a new entry, click here or drag and drop any object').click(); await page.locator('[aria-label="Notebook Entry Input"]').click(); await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`); + await page.locator('[aria-label="Notebook Entry Input"]').press('Enter'); await page.locator('text=To start a new entry, click here or drag and drop any object').click(); await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').click(); await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').fill(`Second Entry`); + 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(); await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').click(); await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').fill(`Third Entry`); + await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').press('Enter'); // Add three tags await page.hover(`button:has-text("Add Tag") >> nth=2`); diff --git a/package.json b/package.json index 23731bb152..53d773c28d 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "plotly.js-gl2d-dist": "2.17.1", "printj": "1.3.1", "resolve-url-loader": "5.0.0", + "sanitize-html": "2.8.1", "sass": "1.57.1", "sass-loader": "13.2.0", "sinon": "15.0.1", diff --git a/src/plugins/notebook/NotebookViewProvider.js b/src/plugins/notebook/NotebookViewProvider.js index 66617789c7..8bcaf1ad82 100644 --- a/src/plugins/notebook/NotebookViewProvider.js +++ b/src/plugins/notebook/NotebookViewProvider.js @@ -25,13 +25,14 @@ import Notebook from './components/Notebook.vue'; import Agent from '@/utils/agent/Agent'; export default class NotebookViewProvider { - constructor(openmct, name, key, type, cssClass, snapshotContainer) { + 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) { @@ -43,6 +44,7 @@ export default class NotebookViewProvider { let openmct = this.openmct; let snapshotContainer = this.snapshotContainer; let agent = new Agent(window); + let entryUrlWhitelist = this.entryUrlWhitelist; return { show(container) { @@ -54,7 +56,8 @@ export default class NotebookViewProvider { provide: { openmct, snapshotContainer, - agent + agent, + entryUrlWhitelist }, data() { return { diff --git a/src/plugins/notebook/components/NotebookEntry.vue b/src/plugins/notebook/components/NotebookEntry.vue index 932b45a3c4..8d1c3a15b0 100644 --- a/src/plugins/notebook/components/NotebookEntry.vue +++ b/src/plugins/notebook/components/NotebookEntry.vue @@ -1,3 +1,4 @@ + /***************************************************************************** * Open MCT, Copyright (c) 2014-2022, United States Government * as represented by the Administrator of the National Aeronautics and Space @@ -75,12 +76,14 @@ class="c-ne__text c-ne__input" aria-label="Notebook Entry Input" tabindex="0" - contenteditable="true" + :contenteditable="canEdit" + @mouseover="checkEditability($event)" + @mouseleave="canEdit = true" @focus="editingEntry()" @blur="updateEntryValue($event)" @keydown.enter.exact.prevent @keyup.enter.exact.prevent="forceBlur($event)" - v-text="entry.text" + v-html="formattedText" >
@@ -91,7 +94,7 @@ class="c-ne__text" contenteditable="false" tabindex="0" - v-text="entry.text" + v-html="formattedText" > @@ -156,10 +159,16 @@ import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue'; import { createNewEmbed } from '../utils/notebook-entries'; import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image'; +import sanitizeHtml from 'sanitize-html'; import _ from 'lodash'; import Moment from 'moment'; +const SANITIZATION_SCHEMA = { + allowedTags: [], + allowedAttributes: {} +}; +const URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/g; const UNKNOWN_USER = 'Unknown'; export default { @@ -167,7 +176,7 @@ export default { NotebookEmbed, TextHighlight }, - inject: ['openmct', 'snapshotContainer'], + inject: ['openmct', 'snapshotContainer', 'entryUrlWhitelist'], props: { domainObject: { type: Object, @@ -224,6 +233,8 @@ export default { }, data() { return { + editMode: false, + canEdit: true, enableEmbedsWrapperScroll: false }; }, @@ -234,6 +245,31 @@ export default { createdOnTime() { return this.formatTime(this.entry.createdOn, 'HH:mm:ss'); }, + formattedText() { + // remove ANY tags + let text = sanitizeHtml(this.entry.text, SANITIZATION_SCHEMA); + + if (this.editMode || !this.urlWhitelist) { + return text; + } + + text = text.replace(URL_REGEX, (match) => { + const url = new URL(match); + const domain = url.hostname; + let result = match; + let isMatch = this.urlWhitelist.find((partialDomain) => { + return domain.endsWith(partialDomain); + }); + + if (isMatch) { + result = `${match}`; + } + + return result; + }); + + return text; + }, isSelectedEntry() { return this.selectedEntryId === this.entry.id; }, @@ -271,6 +307,9 @@ export default { this.manageEmbedLayout(); this.dropOnEntry = this.dropOnEntry.bind(this); + if (this.entryUrlWhitelist?.length > 0) { + this.urlWhitelist = this.entryUrlWhitelist; + } }, beforeDestroy() { if (this.embedsWrapperResizeObserver) { @@ -307,6 +346,11 @@ export default { event.dataTransfer.effectAllowed = 'none'; } }, + checkEditability($event) { + if ($event.target.nodeName === 'A') { + this.canEdit = false; + } + }, deleteEntry() { this.$emit('deleteEntry', this.entry.id); }, @@ -405,9 +449,11 @@ export default { this.$emit('updateEntry', this.entry); }, editingEntry() { + this.editMode = true; this.$emit('editingEntry'); }, updateEntryValue($event) { + this.editMode = false; const value = $event.target.innerText; if (value !== this.entry.text && value.match(/\S/)) { this.entry.text = value; diff --git a/src/plugins/notebook/plugin.js b/src/plugins/notebook/plugin.js index 7fcabbe747..f24742644b 100644 --- a/src/plugins/notebook/plugin.js +++ b/src/plugins/notebook/plugin.js @@ -103,7 +103,7 @@ function installBaseNotebookFunctionality(openmct) { monkeyPatchObjectAPIForNotebooks(openmct); } -function NotebookPlugin(name = 'Notebook') { +function NotebookPlugin(name = 'Notebook', entryUrlWhitelist = []) { return function install(openmct) { if (openmct[NOTEBOOK_INSTALLED_KEY]) { return; @@ -118,8 +118,8 @@ function NotebookPlugin(name = 'Notebook') { 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); - openmct.objectViews.addProvider(notebookView); + const notebookView = new NotebookViewProvider(openmct, name, NOTEBOOK_VIEW_TYPE, NOTEBOOK_TYPE, icon, snapshotContainer, entryUrlWhitelist); + openmct.objectViews.addProvider(notebookView, entryUrlWhitelist); installBaseNotebookFunctionality(openmct); @@ -127,7 +127,7 @@ function NotebookPlugin(name = 'Notebook') { }; } -function RestrictedNotebookPlugin(name = 'Notebook Shift Log') { +function RestrictedNotebookPlugin(name = 'Notebook Shift Log', entryUrlWhitelist = []) { return function install(openmct) { if (openmct[RESTRICTED_NOTEBOOK_INSTALLED_KEY]) { return; @@ -140,8 +140,8 @@ function RestrictedNotebookPlugin(name = 'Notebook Shift Log') { const notebookType = new NotebookType(name, description, icon); openmct.types.addType(RESTRICTED_NOTEBOOK_TYPE, notebookType); - const notebookView = new NotebookViewProvider(openmct, name, RESTRICTED_NOTEBOOK_VIEW_TYPE, RESTRICTED_NOTEBOOK_TYPE, icon, snapshotContainer); - openmct.objectViews.addProvider(notebookView); + const notebookView = new NotebookViewProvider(openmct, name, RESTRICTED_NOTEBOOK_VIEW_TYPE, RESTRICTED_NOTEBOOK_TYPE, icon, snapshotContainer, entryUrlWhitelist); + openmct.objectViews.addProvider(notebookView, entryUrlWhitelist); installBaseNotebookFunctionality(openmct); From 986c596d90da013172232ec5bccb7284d909259e Mon Sep 17 00:00:00 2001 From: Charles Hacskaylo Date: Fri, 20 Jan 2023 23:21:57 -0800 Subject: [PATCH 162/274] Imagery compass rose enhancements (#6140) * Fixes 6139 - Markup changes and improvements in CompassRose.vue. - Improved sun and edge gradients. - Related CSS styles updated. - Changed compass key color from cyan to white to avoid conflict with staleness color. * change var def to avoid collision * compass rose should size itself based on image * allow heading or camera pan for fixed cameras * suppress HUD if no camera pan * allow image to display compass rose for other cams * update example imagery to accept transformations * remove comments Co-authored-by: David Tsay Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com> --- example/imagery/plugin.js | 8 ++ .../imagery/components/Compass/Compass.vue | 31 +++-- .../components/Compass/CompassRose.vue | 119 +++++++++++++----- .../imagery/components/Compass/compass.scss | 23 +++- .../imagery/components/ImageryView.vue | 35 +++--- 5 files changed, 152 insertions(+), 64 deletions(-) diff --git a/example/imagery/plugin.js b/example/imagery/plugin.js index 2f323356dd..e193c99afd 100644 --- a/example/imagery/plugin.js +++ b/example/imagery/plugin.js @@ -242,6 +242,13 @@ 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 + }; return { name, @@ -251,6 +258,7 @@ function pointForTimestamp(timestamp, name, imageSamples, delay) { sunOrientation: getCompassValues(0, 360), cameraPan: getCompassValues(0, 360), heading: getCompassValues(0, 360), + transformations: navCamTransformations, imageDownloadName }; } diff --git a/src/plugins/imagery/components/Compass/Compass.vue b/src/plugins/imagery/components/Compass/Compass.vue index 850cd3c7d2..15d2f65090 100644 --- a/src/plugins/imagery/components/Compass/Compass.vue +++ b/src/plugins/imagery/components/Compass/Compass.vue @@ -26,19 +26,18 @@ :style="`width: 100%; height: 100%`" > @@ -47,18 +46,12 @@ import CompassHUD from './CompassHUD.vue'; import CompassRose from './CompassRose.vue'; -const CAMERA_ANGLE_OF_VIEW = 70; - export default { components: { CompassHUD, CompassRose }, props: { - compassRoseSizingClasses: { - type: String, - required: true - }, image: { type: Object, required: true @@ -69,13 +62,19 @@ export default { } }, computed: { - hasCameraFieldOfView() { - return this.cameraPan !== undefined && this.cameraAngleOfView > 0; + showCompassHUD() { + return this.hasCameraPan && this.cameraAngleOfView > 0; + }, + showCompassRose() { + return (this.hasCameraPan || this.hasHeading) && this.cameraAngleOfView > 0; }, // horizontal rotation from north in degrees heading() { return this.image.heading; }, + hasHeading() { + return this.heading !== undefined; + }, // horizontal rotation from north in degrees sunHeading() { return this.image.sunOrientation; @@ -84,8 +83,14 @@ export default { cameraPan() { return this.image.cameraPan; }, + hasCameraPan() { + return this.cameraPan !== undefined; + }, cameraAngleOfView() { - return CAMERA_ANGLE_OF_VIEW; + return this.transformations?.cameraAngleOfView; + }, + transformations() { + return this.image.transformations; } }, methods: { diff --git a/src/plugins/imagery/components/Compass/CompassRose.vue b/src/plugins/imagery/components/Compass/CompassRose.vue index d66e382a4a..958bd3779b 100644 --- a/src/plugins/imagery/components/Compass/CompassRose.vue +++ b/src/plugins/imagery/components/Compass/CompassRose.vue @@ -64,14 +64,14 @@ class="c-cr__edge" width="100" height="100" - fill="url(#paint0_radial)" + fill="url(#gradient_edge)" /> @@ -107,9 +107,26 @@ height="100" /> + + + + + + + + - - - + @@ -238,10 +254,6 @@ import { throttle } from 'lodash'; export default { props: { - compassRoseSizingClasses: { - type: String, - required: true - }, heading: { type: Number, required: true, @@ -253,16 +265,13 @@ export default { type: Number, default: undefined }, - cameraAngleOfView: { + cameraPan: { type: Number, default: undefined }, - cameraPan: { - type: Number, - required: true, - default() { - return 0; - } + transformations: { + type: Object, + default: undefined }, sizedImageDimensions: { type: Object, @@ -275,11 +284,38 @@ export default { }; }, computed: { + cameraHeading() { + return this.cameraPan ?? this.heading; + }, + cameraAngleOfView() { + const cameraAngleOfView = this.transformations?.cameraAngleOfView; + + if (!cameraAngleOfView) { + console.warn('No Camera Angle of View provided'); + } + + return cameraAngleOfView; + }, + camAngleAndPositionStyle() { + const translateX = this.transformations?.translateX; + const translateY = this.transformations?.translateY; + const rotation = this.transformations?.rotation; + const scale = this.transformations?.scale; + + return { transform: `translate(${translateX}%, ${translateY}%) rotate(${rotation}deg) scale(${scale})` }; + }, + camGimbalAngleStyle() { + const rotation = rotate(this.north, this.heading); + + return { + transform: `rotate(${ rotation }deg)` + }; + }, compassRoseStyle() { return { transform: `rotate(${ this.north }deg)` }; }, north() { - return this.lockCompass ? rotate(-this.cameraPan) : 0; + return this.lockCompass ? rotate(-this.cameraHeading) : 0; }, cardinalTextRotateN() { return { transform: `translateY(-27%) rotate(${ -this.north }deg)` }; @@ -297,6 +333,7 @@ export default { return this.heading !== undefined; }, headingStyle() { + /* Replaced with computed camGimbalStyle, but left here just in case. */ const rotation = rotate(this.north, this.heading); return { @@ -313,8 +350,8 @@ export default { transform: `rotate(${ rotation }deg)` }; }, - cameraPanStyle() { - const rotation = rotate(this.north, this.cameraPan); + cameraHeadingStyle() { + const rotation = rotate(this.north, this.cameraHeading); return { transform: `rotate(${ rotation }deg)` @@ -333,6 +370,24 @@ export default { return { transform: `rotate(${ -this.cameraAngleOfView / 2 }deg)` }; + }, + compassRoseSizingClasses() { + let compassRoseSizingClasses = ''; + if (this.sizedImageWidth < 300) { + compassRoseSizingClasses = '--rose-small --rose-min'; + } else if (this.sizedImageWidth < 500) { + compassRoseSizingClasses = '--rose-small'; + } else if (this.sizedImageWidth > 1000) { + compassRoseSizingClasses = '--rose-max'; + } + + return compassRoseSizingClasses; + }, + sizedImageWidth() { + return this.sizedImageDimensions.width; + }, + sizedImageHeight() { + return this.sizedImageDimensions.height; } }, watch: { diff --git a/src/plugins/imagery/components/Compass/compass.scss b/src/plugins/imagery/components/Compass/compass.scss index 5609bf9f60..a143706b2b 100644 --- a/src/plugins/imagery/components/Compass/compass.scss +++ b/src/plugins/imagery/components/Compass/compass.scss @@ -1,5 +1,5 @@ /***************************** THEME/UI CONSTANTS AND MIXINS */ -$interfaceKeyColor: #00B9C5; +$interfaceKeyColor: #fff; $elemBg: rgba(black, 0.7); @mixin sun($position: 'circle closest-side') { @@ -100,13 +100,19 @@ $elemBg: rgba(black, 0.7); } &__edge { - opacity: 0.1; + 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 @@ -115,7 +121,6 @@ $elemBg: rgba(black, 0.7); } &__nsew-text, - &__spacecraft-body, &__ticks-major, &__ticks-minor { fill: $color; @@ -166,3 +171,15 @@ $elemBg: rgba(black, 0.7); padding-top: $s; } } + +/************************** ROVER */ +.cr-vrover { + $scale: 0.4; + transform-origin: center; + + &__body { + fill: $interfaceKeyColor; + opacity: 0.3; + transform-origin: center 7% !important; // Places rotation center at mast position + } +} diff --git a/src/plugins/imagery/components/ImageryView.vue b/src/plugins/imagery/components/ImageryView.vue index db13f8f3f5..5b18cd29d7 100644 --- a/src/plugins/imagery/components/ImageryView.vue +++ b/src/plugins/imagery/components/ImageryView.vue @@ -93,7 +93,6 @@ > 1000) { - compassRoseSizingClasses = '--rose-max'; - } - - return compassRoseSizingClasses; - }, displayThumbnails() { return ( this.forceShowThumbnails @@ -432,7 +419,6 @@ export default { shouldDisplayCompass() { const imageHeightAndWidth = this.sizedImageHeight !== 0 && this.sizedImageWidth !== 0; - const display = this.focusedImage !== undefined && this.focusedImageNaturalAspectRatio !== undefined && this.imageContainerWidth !== undefined @@ -440,8 +426,9 @@ export default { && imageHeightAndWidth && this.zoomFactor === 1 && this.imagePanned !== true; + const hasCameraConfigurations = this.focusedImage?.transformations !== undefined; - return display; + return display && hasCameraConfigurations; }, isSpacecraftPositionFresh() { let isFresh = undefined; @@ -626,6 +613,7 @@ export default { this.spacecraftOrientationKeys = ['heading']; this.cameraKeys = ['cameraPan', 'cameraTilt']; this.sunKeys = ['sunOrientation']; + this.transformationsKeys = ['transformations']; // related telemetry await this.initializeRelatedTelemetry(); @@ -728,7 +716,13 @@ export default { this.relatedTelemetry = new RelatedTelemetry( this.openmct, this.domainObject, - [...this.spacecraftPositionKeys, ...this.spacecraftOrientationKeys, ...this.cameraKeys, ...this.sunKeys] + [ + ...this.spacecraftPositionKeys, + ...this.spacecraftOrientationKeys, + ...this.cameraKeys, + ...this.sunKeys, + ...this.transformationsKeys + ] ); if (this.relatedTelemetry.hasRelatedTelemetry) { @@ -837,6 +831,15 @@ export default { this.$set(this.focusedImageRelatedTelemetry, key, value); } } + + // set configuration for compass + this.transformationsKeys.forEach(key => { + const transformations = this.relatedTelemetry[key]; + + if (transformations !== undefined) { + this.$set(this.imageHistory[this.focusedImageIndex], key, transformations); + } + }); }, trackLatestRelatedTelemetry() { [...this.spacecraftPositionKeys, ...this.spacecraftOrientationKeys, ...this.cameraKeys, ...this.sunKeys].forEach(key => { From 5e530aa6254c985f253608c41f438725e9ca678a Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Sat, 21 Jan 2023 11:25:35 -0800 Subject: [PATCH 163/274] feat: Support thumbnails in ImageryView and ImageryTimeView (#6132) * fix: get image thumbnail formatter * refactor: ImageryView cleanup * docs: add comment * feat: Support thumbnails in ImageryView - Prefer an image's thumbnail URL if its available * feat: Support thumbnails in ImageryTimeView * refactor: rename variable * test(WIP): add thumbnail unit test, not working yet * test: temp disable test * feat: imagery thumbnail urls for example imagery * test: add unit test for imagery thumbnails * test(e2e): check for thumbnail urls - Update imagery view tests to check for use of thumbnail urls --- .../imagery/exampleImagery.e2e.spec.js | 35 ++++++--- example/imagery/plugin.js | 19 +++++ .../imagery/components/ImageThumbnail.vue | 2 +- .../imagery/components/ImageryTimeView.vue | 24 +++--- .../imagery/components/ImageryView.vue | 78 ++++++++++--------- .../imagery/components/imagery-view.scss | 2 +- src/plugins/imagery/mixins/imageryData.js | 35 +++++++-- src/plugins/imagery/pluginSpec.js | 44 ++++++++++- 8 files changed, 166 insertions(+), 73 deletions(-) diff --git a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js index 1311597f0a..41aea55c7c 100644 --- a/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js +++ b/e2e/tests/functional/plugins/imagery/exampleImagery.e2e.spec.js @@ -25,13 +25,13 @@ This test suite is dedicated to tests which verify the basic operations surround but only assume that example imagery is present. */ /* globals process */ -const { v4: uuid } = require('uuid'); const { waitForAnimations } = require('../../../../baseFixtures'); const { test, expect } = require('../../../../pluginFixtures'); const { createDomainObjectWithDefaults } = require('../../../../appActions'); const backgroundImageSelector = '.c-imagery__main-image__background-image'; const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt']; const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan'; +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', () => { @@ -397,13 +397,11 @@ test.describe('Example Imagery in Time Strip', () => { test.beforeEach(async ({ page }) => { await page.goto('./', { waitUntil: 'networkidle' }); timeStripObject = await createDomainObjectWithDefaults(page, { - type: 'Time Strip', - name: 'Time Strip'.concat(' ', uuid()) + type: 'Time Strip' }); await createDomainObjectWithDefaults(page, { type: 'Example Imagery', - name: 'Example Imagery'.concat(' ', uuid()), parent: timeStripObject.uuid }); // Navigate to timestrip @@ -414,17 +412,28 @@ test.describe('Example Imagery in Time Strip', () => { 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 url of the hovered image - const hoveredImg = page.locator('.c-imagery-tsv div.c-imagery-tsv__image-wrapper:hover img'); - const hoveredImgSrc = await hoveredImg.getAttribute('src'); - expect(hoveredImgSrc).toBeTruthy(); + + // 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 image of view large container + + // 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(); - expect(viewLargeImgSrc).toEqual(hoveredImgSrc); + + // Verify that the image in the large view is the same as the hovered thumbnail + expect(viewLargeImgSrc).toEqual(hoveredThumbnailImgSrc.split('?')[0]); }); }); @@ -441,6 +450,12 @@ 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); + // Click previous image button const previousImageButton = page.locator('.c-nav--prev'); await previousImageButton.click(); diff --git a/example/imagery/plugin.js b/example/imagery/plugin.js index e193c99afd..2eabb32361 100644 --- a/example/imagery/plugin.js +++ b/example/imagery/plugin.js @@ -107,6 +107,15 @@ export default function () { } ] }, + { + name: 'Image Thumbnail', + key: 'thumbnail-url', + format: 'thumbnail', + hints: { + thumbnail: 1 + }, + source: 'url' + }, { name: 'Image Download Name', key: 'imageDownloadName', @@ -143,6 +152,16 @@ export default function () { ] }); + 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()); diff --git a/src/plugins/imagery/components/ImageThumbnail.vue b/src/plugins/imagery/components/ImageThumbnail.vue index 99ef0febc7..1748c32bb4 100644 --- a/src/plugins/imagery/components/ImageThumbnail.vue +++ b/src/plugins/imagery/components/ImageThumbnail.vue @@ -39,7 +39,7 @@ diff --git a/src/plugins/imagery/components/ImageryTimeView.vue b/src/plugins/imagery/components/ImageryTimeView.vue index ccaf7b71ba..5adfd2d476 100644 --- a/src/plugins/imagery/components/ImageryTimeView.vue +++ b/src/plugins/imagery/components/ImageryTimeView.vue @@ -186,17 +186,17 @@ export default { item.remove(); }); let imagery = this.$el.querySelectorAll(`.${IMAGE_WRAPPER_CLASS}`); - imagery.forEach(item => { + imagery.forEach(imageElm => { if (clearAllImagery) { - item.remove(); + imageElm.remove(); } else { - const id = item.getAttributeNS(null, 'id'); + const id = imageElm.getAttributeNS(null, 'id'); if (id) { const timestamp = id.replace(ID_PREFIX, ''); if (!this.isImageryInBounds({ time: timestamp })) { - item.remove(); + imageElm.remove(); } } } @@ -343,25 +343,25 @@ export default { imageElement.style.display = 'block'; } }, - updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) { + updateExistingImageWrapper(existingImageWrapper, image, showImagePlaceholders) { //Update the x co-ordinates of the image wrapper and the url of image //this is to avoid tearing down all elements completely and re-drawing them this.setNSAttributesForElement(existingImageWrapper, { 'data-show-image-placeholders': showImagePlaceholders }); - existingImageWrapper.style.left = `${this.xScale(item.time)}px`; + existingImageWrapper.style.left = `${this.xScale(image.time)}px`; let imageElement = existingImageWrapper.querySelector('img'); this.setNSAttributesForElement(imageElement, { - src: item.url + src: image.thumbnailUrl || image.url }); this.setImageDisplay(imageElement, showImagePlaceholders); }, - createImageWrapper(index, item, showImagePlaceholders) { - const id = `${ID_PREFIX}${item.time}`; + createImageWrapper(index, image, showImagePlaceholders) { + const id = `${ID_PREFIX}${image.time}`; let imageWrapper = document.createElement('div'); imageWrapper.classList.add(IMAGE_WRAPPER_CLASS); - imageWrapper.style.left = `${this.xScale(item.time)}px`; + imageWrapper.style.left = `${this.xScale(image.time)}px`; this.setNSAttributesForElement(imageWrapper, { id, 'data-show-image-placeholders': showImagePlaceholders @@ -383,7 +383,7 @@ export default { //create image element let imageElement = document.createElement('img'); this.setNSAttributesForElement(imageElement, { - src: item.url + src: image.thumbnailUrl || image.url }); imageElement.style.width = `${IMAGE_SIZE}px`; imageElement.style.height = `${IMAGE_SIZE}px`; @@ -392,7 +392,7 @@ export default { //handle mousedown event to show the image in a large view imageWrapper.addEventListener('mousedown', (e) => { if (e.button === 0) { - this.expand(item.time); + this.expand(image.time); } }); diff --git a/src/plugins/imagery/components/ImageryView.vue b/src/plugins/imagery/components/ImageryView.vue index 5b18cd29d7..20446109aa 100644 --- a/src/plugins/imagery/components/ImageryView.vue +++ b/src/plugins/imagery/components/ImageryView.vue @@ -171,7 +171,7 @@ > { - const persistedLayer = persistedLayers.find(object => object.name === layer.name); - if (persistedLayer) { - layer.visible = persistedLayer.visible === true; - } - }); - this.visibleLayers = this.layers.filter(layer => layer.visible); - } else { - this.visibleLayers = []; - this.layers.forEach((layer) => { - layer.visible = false; - }); - } + const layersMetadata = this.imageMetadataValue.layers; + if (!layersMetadata) { + return; + } + + this.layers = layersMetadata; + if (this.domainObject.configuration) { + const persistedLayers = this.domainObject.configuration.layers; + layersMetadata.forEach((layer) => { + const persistedLayer = persistedLayers.find(object => object.name === layer.name); + if (persistedLayer) { + layer.visible = persistedLayer.visible === true; + } + }); + this.visibleLayers = this.layers.filter(layer => layer.visible); + } else { + this.visibleLayers = []; + this.layers.forEach((layer) => { + layer.visible = false; + }); } }, persistVisibleLayers() { diff --git a/src/plugins/imagery/components/imagery-view.scss b/src/plugins/imagery/components/imagery-view.scss index ee0713244b..22e38d9040 100644 --- a/src/plugins/imagery/components/imagery-view.scss +++ b/src/plugins/imagery/components/imagery-view.scss @@ -29,7 +29,7 @@ flex-direction: column; flex: 1 1 auto; - &.unnsynced{ + &.unsynced{ @include sUnsynced(); } diff --git a/src/plugins/imagery/mixins/imageryData.js b/src/plugins/imagery/mixins/imageryData.js index 94118e6813..0e51b6fd35 100644 --- a/src/plugins/imagery/mixins/imageryData.js +++ b/src/plugins/imagery/mixins/imageryData.js @@ -21,6 +21,9 @@ *****************************************************************************/ const DEFAULT_DURATION_FORMATTER = 'duration'; +const IMAGE_HINT_KEY = 'image'; +const IMAGE_THUMBNAIL_HINT_KEY = 'thumbnail'; +const IMAGE_DOWNLOAD_NAME_HINT_KEY = 'imageDownloadName'; export default { inject: ['openmct', 'domainObject', 'objectPath'], @@ -32,13 +35,20 @@ export default { this.setDataTimeContext(); this.openmct.objectViews.on('clearData', this.dataCleared); - // set + // Get metadata and formatters this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); - this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] }; + + 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.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); - this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints); - this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]}; + this.imageDownloadNameMetadataValue = { ...this.metadata.valuesForHints([IMAGE_DOWNLOAD_NAME_HINT_KEY])[0]}; // initialize this.timeKey = this.timeSystem.key; @@ -105,12 +115,19 @@ export default { return this.imageFormatter.format(datum); }, + formatImageThumbnailUrl(datum) { + if (!datum || !this.imageThumbnailFormatter) { + return; + } + + return this.imageThumbnailFormatter.format(datum); + }, formatTime(datum) { if (!datum) { return; } - let dateTimeStr = this.timeFormatter.format(datum); + const dateTimeStr = this.timeFormatter.format(datum); // Replace ISO "T" with a space to allow wrapping return dateTimeStr.replace("T", " "); @@ -118,7 +135,7 @@ export default { getImageDownloadName(datum) { let imageDownloadName = ''; if (datum) { - const key = this.imageDownloadNameHints.key; + const key = this.imageDownloadNameMetadataValue.key; imageDownloadName = datum[key]; } @@ -150,6 +167,7 @@ export default { 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); @@ -157,13 +175,14 @@ export default { ...datum, formattedTime, url, + thumbnailUrl, time, imageDownloadName }; }, getFormatter(key) { - let metadataValue = this.metadata.value(key) || { format: key }; - let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); + const metadataValue = this.metadata.value(key) || { format: key }; + const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); return valueFormatter; } diff --git a/src/plugins/imagery/pluginSpec.js b/src/plugins/imagery/pluginSpec.js index 15a81b09e3..226de27136 100644 --- a/src/plugins/imagery/pluginSpec.js +++ b/src/plugins/imagery/pluginSpec.js @@ -35,6 +35,10 @@ const MAIN_IMAGE_CLASS = '.js-imageryView-image'; 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'); +} + function getImageInfo(doc) { let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0]; let timestamp = imageElement.dataset.openmctImageTimestamp; @@ -124,6 +128,16 @@ describe("The Imagery View Layouts", () => { }, "source": "url" }, + { + "name": "Image Thumbnail", + "key": "thumbnail-url", + "format": "thumbnail", + "hints": { + "thumbnail": 1, + "priority": 3 + }, + "source": "url" + }, { "name": "Name", "key": "name", @@ -200,6 +214,11 @@ describe("The Imagery View Layouts", () => { originalRouterPath = openmct.router.path; + openmct.telemetry.addFormat({ + key: 'thumbnail', + format: formatThumbnail + }); + openmct.on('start', done); openmct.startHeadless(); }); @@ -384,15 +403,32 @@ describe("The Imagery View Layouts", () => { //Looks like we need Vue.nextTick here so that computed properties settle down await Vue.nextTick(); const layerEls = parent.querySelectorAll('.js-layer-image'); - console.log(layerEls); expect(layerEls.length).toEqual(1); }); + it("should 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 target = imageTelemetry[5].url; - parent.querySelectorAll(`img[src='${target}']`)[0].click(); + const thumbnailUrl = formatThumbnail(imageTelemetry[5].url); + parent.querySelectorAll(`img[src='${thumbnailUrl}']`)[0].click(); await Vue.nextTick(); const imageInfo = getImageInfo(parent); @@ -417,7 +453,7 @@ describe("The Imagery View Layouts", () => { it("should show that an image is not new", async () => { await Vue.nextTick(); - const target = imageTelemetry[4].url; + const target = formatThumbnail(imageTelemetry[4].url); parent.querySelectorAll(`img[src='${target}']`)[0].click(); await Vue.nextTick(); From 9980aab18f622ca9410e9c3aa61d3bb9b4a18e0f Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Sun, 22 Jan 2023 19:38:05 +0100 Subject: [PATCH 164/274] 5834 stacked plot removing objects from a stacked plot will not remove them from the legend (#6022) * Add listeners to remove stacked plot series and make keys unique Co-authored-by: Shefali Joshi --- src/plugins/plot/MctPlot.vue | 6 ++-- src/plugins/plot/inspector/PlotOptions.vue | 2 +- src/plugins/plot/legend/PlotLegend.vue | 4 +-- src/plugins/plot/stackedPlot/StackedPlot.vue | 31 ++++++++++++++----- .../plot/stackedPlot/StackedPlotItem.vue | 3 +- 5 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/plugins/plot/MctPlot.vue b/src/plugins/plot/MctPlot.vue index 8704069dd4..4bf5183453 100644 --- a/src/plugins/plot/MctPlot.vue +++ b/src/plugins/plot/MctPlot.vue @@ -353,10 +353,8 @@ export default { this.config = this.getConfig(); this.legend = this.config.legend; - if (this.isNestedWithinAStackedPlot) { - const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier); - this.$emit('configLoaded', configId); - } + const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier); + this.$emit('configLoaded', configId); this.listenTo(this.config.series, 'add', this.addSeries, this); this.listenTo(this.config.series, 'remove', this.removeSeries, this); diff --git a/src/plugins/plot/inspector/PlotOptions.vue b/src/plugins/plot/inspector/PlotOptions.vue index a72fcb8c9a..5bcbc5f09b 100644 --- a/src/plugins/plot/inspector/PlotOptions.vue +++ b/src/plugins/plot/inspector/PlotOptions.vue @@ -38,7 +38,7 @@ export default { PlotOptionsBrowse, PlotOptionsEdit }, - inject: ['openmct', 'domainObject'], + inject: ['openmct', 'domainObject', 'path'], data() { return { isEditing: this.openmct.editor.isEditing() diff --git a/src/plugins/plot/legend/PlotLegend.vue b/src/plugins/plot/legend/PlotLegend.vue index 1bdf4b3bb9..01054b1958 100644 --- a/src/plugins/plot/legend/PlotLegend.vue +++ b/src/plugins/plot/legend/PlotLegend.vue @@ -50,7 +50,7 @@ > { this.data = data; } @@ -183,6 +185,10 @@ export default { this.domainObject.configuration.series.splice(configIndex, 1); } + this.removeSeries({ + keyString: id + }); + const childObj = this.compositionObjects.filter((c) => { const identifier = this.openmct.objects.makeKeyString(c.identifier); @@ -244,18 +250,29 @@ export default { this.highlights = data; }, registerSeriesListeners(configId) { - this.seriesConfig[configId] = this.getConfig(configId); - this.listenTo(this.seriesConfig[configId].series, 'add', this.addSeries, this); - this.listenTo(this.seriesConfig[configId].series, 'remove', this.removeSeries, this); + const config = this.getConfig(configId); + this.seriesConfig[configId] = config; + const childObject = config.get('domainObject'); - this.seriesConfig[configId].series.models.forEach(this.addSeries, this); + //TODO differentiate between objects with composition and those without + if (childObject.type === 'telemetry.plot.overlay') { + this.listenTo(config.series, 'add', this.addSeries, this); + this.listenTo(config.series, 'remove', this.removeSeries, this); + } + + config.series.models.forEach(this.addSeries, this); }, addSeries(series) { - const index = this.seriesModels.length; - this.$set(this.seriesModels, index, series); + const childObject = series.domainObject; + //don't add the series if it can have child series this will happen in registerSeriesListeners + if (childObject.type !== 'telemetry.plot.overlay') { + const index = this.seriesModels.length; + this.$set(this.seriesModels, index, series); + } + }, removeSeries(plotSeries) { - const index = this.seriesModels.findIndex(seriesModel => this.openmct.objects.areIdsEqual(seriesModel.identifier, plotSeries.identifier)); + const index = this.seriesModels.findIndex(seriesModel => seriesModel.keyString === plotSeries.keyString); if (index > -1) { this.$delete(this.seriesModels, index); } diff --git a/src/plugins/plot/stackedPlot/StackedPlotItem.vue b/src/plugins/plot/stackedPlot/StackedPlotItem.vue index b0049a3613..e7ec5b8f46 100644 --- a/src/plugins/plot/stackedPlot/StackedPlotItem.vue +++ b/src/plugins/plot/stackedPlot/StackedPlotItem.vue @@ -133,6 +133,7 @@ export default { //If this object is not persistable, then package it with it's parent const object = this.getPlotObject(); + const getProps = this.getProps; const isMissing = openmct.objects.isMissing(object); let viewContainer = document.createElement('div'); @@ -160,7 +161,7 @@ export default { onGridLinesChange, setStatus, isMissing, - loading: true + loading: false }; }, methods: { From 1b71a3bf338a946eb743af1eadc9a8a60e5ec69b Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Mon, 23 Jan 2023 07:34:26 -0800 Subject: [PATCH 165/274] Multiple Y-Axes for Overlay Plots (#6153) Support multiple y-axes in overlay plots Co-authored-by: Khalid Adil Co-authored-by: Shefali Joshi Co-authored-by: Rukmini Bose --- .../plugins/plot/autoscale.e2e.spec.js | 2 +- .../plugins/plot/logPlot.e2e.spec.js | 5 +- .../plugins/plot/overlayPlot.e2e.spec.js | 124 +++++ src/plugins/plot/MctPlot.vue | 377 +++++++++---- src/plugins/plot/MctTicks.vue | 21 +- src/plugins/plot/axis/YAxis.vue | 186 +++++-- src/plugins/plot/chart/MctChart.vue | 350 ++++++++---- .../configuration/PlotConfigurationModel.js | 37 +- src/plugins/plot/configuration/PlotSeries.js | 7 +- .../plot/configuration/SeriesCollection.js | 3 + src/plugins/plot/configuration/YAxisModel.js | 111 ++-- .../plot/inspector/PlotOptionsBrowse.vue | 121 +++-- .../plot/inspector/PlotOptionsEdit.vue | 66 ++- .../plot/inspector/forms/YAxisForm.vue | 116 +++- src/plugins/plot/overlayPlot/pluginSpec.js | 504 ++++++++++++++++++ src/plugins/plot/pluginSpec.js | 16 + src/plugins/plot/stackedPlot/StackedPlot.vue | 3 +- src/plugins/plot/stackedPlot/pluginSpec.js | 18 +- src/styles/_glyphs.scss | 1 + src/styles/_legacy-plots.scss | 44 +- src/ui/inspector/ElementItem.vue | 7 +- src/ui/inspector/ElementItemGroup.vue | 101 ++++ src/ui/inspector/ElementsPool.vue | 4 +- src/ui/inspector/Inspector.vue | 23 +- src/ui/inspector/PlotElementsPool.vue | 330 ++++++++++++ src/ui/inspector/elements.scss | 29 +- 26 files changed, 2234 insertions(+), 372 deletions(-) create mode 100644 e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js create mode 100644 src/plugins/plot/overlayPlot/pluginSpec.js create mode 100644 src/ui/inspector/ElementItemGroup.vue create mode 100644 src/ui/inspector/PlotElementsPool.vue diff --git a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js index a615254194..fb9a2e2a73 100644 --- a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js @@ -156,7 +156,7 @@ async function turnOffAutoscale(page) { await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click(); // uncheck autoscale - await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"] >> nth=1').uncheck(); + await page.getByRole('listitem').filter({ hasText: 'Auto scale' }).getByRole('checkbox').uncheck(); // save await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); diff --git a/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js index fa6b43eec1..e923b15bb4 100644 --- a/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js @@ -205,7 +205,8 @@ async function enableEditMode(page) { */ async function enableLogMode(page) { // turn on log mode - await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check(); + await page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox').check(); + // await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check(); } /** @@ -213,7 +214,7 @@ async function enableLogMode(page) { */ async function disableLogMode(page) { // turn off log mode - await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().uncheck(); + await page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox').uncheck(); } /** diff --git a/e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js new file mode 100644 index 0000000000..3486b62a4d --- /dev/null +++ b/e2e/tests/functional/plugins/plot/overlayPlot.e2e.spec.js @@ -0,0 +1,124 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/* +Tests to verify log plot functionality. Note this test suite if very much under active development and should not +necessarily be used for reference when writing new tests in this area. +*/ + +const { test, expect } = require('../../../../pluginFixtures'); +const { createDomainObjectWithDefaults } = require('../../../../appActions'); + +test.describe('Overlay Plot', () => { + test('Plot legend color is in sync with plot series color', async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' }); + const overlayPlot = await createDomainObjectWithDefaults(page, { + type: "Overlay Plot" + }); + + await createDomainObjectWithDefaults(page, { + type: "Sine Wave Generator", + parent: overlayPlot.uuid + }); + + await page.goto(overlayPlot.url); + + // navigate to plot series color palette + await page.click('.l-browse-bar__actions__edit'); + await page.locator('li.c-tree__item.menus-to-left .c-disclosure-triangle').click(); + await page.locator('.c-click-swatch--menu').click(); + await page.locator('.c-palette__item[style="background: rgb(255, 166, 61);"]').click(); + + // gets color for swatch located in legend + const element = await page.waitForSelector('.plot-series-color-swatch'); + const color = await element.evaluate((el) => { + return window.getComputedStyle(el).getPropertyValue('background-color'); + }); + + expect(color).toBe('rgb(255, 166, 61)'); + }); + test('The elements pool supports dragging series into multiple y-axis buckets', async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' }); + const overlayPlot = await createDomainObjectWithDefaults(page, { + type: "Overlay Plot" + }); + + await createDomainObjectWithDefaults(page, { + type: "Sine Wave Generator", + name: 'swg a', + parent: overlayPlot.uuid + }); + await createDomainObjectWithDefaults(page, { + type: "Sine Wave Generator", + name: 'swg b', + parent: overlayPlot.uuid + }); + await createDomainObjectWithDefaults(page, { + type: "Sine Wave Generator", + name: 'swg c', + parent: overlayPlot.uuid + }); + await createDomainObjectWithDefaults(page, { + type: "Sine Wave Generator", + name: 'swg d', + parent: overlayPlot.uuid + }); + await createDomainObjectWithDefaults(page, { + type: "Sine Wave Generator", + name: 'swg e', + parent: overlayPlot.uuid + }); + + await page.goto(overlayPlot.url); + await page.click('button[title="Edit"]'); + + // Expand the elements pool vertically + await page.locator('.l-pane__handle').nth(2).hover({ trial: true }); + await page.mouse.down(); + await page.mouse.move(0, 100); + await page.mouse.up(); + + // Drag swg a, c, e into Y Axis 2 + await page.locator('#inspector-elements-tree >> text=swg a').dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); + await page.locator('#inspector-elements-tree >> text=swg c').dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); + await page.locator('#inspector-elements-tree >> text=swg e').dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); + + // Drag swg b into Y Axis 3 + await page.locator('#inspector-elements-tree >> text=swg b').dragTo(page.locator('[aria-label="Element Item Group Y Axis 3"]')); + + const yAxis1Group = page.getByLabel("Y Axis 1"); + const yAxis2Group = page.getByLabel("Y Axis 2"); + const yAxis3Group = page.getByLabel("Y Axis 3"); + + // Verify that the elements are in the correct buckets and in the correct order + expect(yAxis1Group.getByRole('listitem', { name: 'swg d' })).toBeTruthy(); + expect(yAxis1Group.getByRole('listitem').nth(0).getByText('swg d')).toBeTruthy(); + expect(yAxis2Group.getByRole('listitem', { name: 'swg e' })).toBeTruthy(); + expect(yAxis2Group.getByRole('listitem').nth(0).getByText('swg e')).toBeTruthy(); + expect(yAxis2Group.getByRole('listitem', { name: 'swg c' })).toBeTruthy(); + expect(yAxis2Group.getByRole('listitem').nth(1).getByText('swg c')).toBeTruthy(); + expect(yAxis2Group.getByRole('listitem', { name: 'swg a' })).toBeTruthy(); + expect(yAxis2Group.getByRole('listitem').nth(2).getByText('swg a')).toBeTruthy(); + expect(yAxis3Group.getByRole('listitem', { name: 'swg b' })).toBeTruthy(); + expect(yAxis3Group.getByRole('listitem').nth(0).getByText('swg b')).toBeTruthy(); + }); +}); diff --git a/src/plugins/plot/MctPlot.vue b/src/plugins/plot/MctPlot.vue index 4bf5183453..d970992e1c 100644 --- a/src/plugins/plot/MctPlot.vue +++ b/src/plugins/plot/MctPlot.vue @@ -34,23 +34,27 @@ @legendHoverChanged="legendHoverChanged" />
- +
+ +
@@ -69,9 +73,12 @@ /> @@ -88,6 +95,7 @@ :annotated-points="annotatedPoints" :annotation-selections="annotationSelections" :show-limit-line-labels="showLimitLineLabels" + :hidden-y-axis-ids="hiddenYAxisIds" :annotation-viewing-and-editing-allowed="annotationViewingAndEditingAllowed" @plotReinitializeCanvas="initCanvas" @chartLoaded="initialize" @@ -218,6 +226,7 @@ import KDBush from 'kdbush'; import _ from "lodash"; const OFFSET_THRESHOLD = 10; +const AXES_PADDING = 20; export default { components: { @@ -275,7 +284,6 @@ export default { annotatedPoints: [], annotationSelections: [], lockHighlightPoint: false, - tickWidth: 0, yKeyOptions: [], yAxisLabel: '', rectangles: [], @@ -290,12 +298,33 @@ export default { isTimeOutOfSync: false, showLimitLineLabels: this.limitLineLabels, isFrozenOnMouseDown: false, - hasSameRangeValue: true, cursorGuide: this.initCursorGuide, - gridLines: this.initGridLines + gridLines: this.initGridLines, + yAxes: [], + hiddenYAxisIds: [], + yAxisListWithRange: [] }; }, computed: { + xAxisStyle() { + const rightAxis = this.yAxesIds.find(yAxis => yAxis.id > 2); + const leftOffset = this.multipleLeftAxes ? 2 * AXES_PADDING : AXES_PADDING; + let style = { + left: `${this.plotLeftTickWidth + leftOffset}px` + }; + + if (rightAxis) { + style.right = `${rightAxis.tickWidth + AXES_PADDING}px`; + } + + return style; + }, + yAxesIds() { + return this.yAxes.filter(yAxis => yAxis.seriesCount > 0); + }, + multipleLeftAxes() { + return this.yAxes.filter(yAxis => yAxis.seriesCount > 0 && yAxis.id <= 2).length > 1; + }, isNestedWithinAStackedPlot() { const isNavigatedObject = this.openmct.router.isNavigatedObject([this.domainObject].concat(this.path)); @@ -322,8 +351,17 @@ export default { return 'plot-legend-collapsed'; } }, - plotWidth() { - return this.plotTickWidth || this.tickWidth; + plotLeftTickWidth() { + let leftTickWidth = 0; + this.yAxes.forEach((yAxis) => { + if (yAxis.id > 2) { + return; + } + + leftTickWidth = leftTickWidth + yAxis.tickWidth; + }); + + return this.plotTickWidth || leftTickWidth; } }, watch: { @@ -341,6 +379,7 @@ export default { } }, mounted() { + this.yAxisIdVisibility = {}; this.offsetWidth = 0; document.addEventListener('keydown', this.handleKeyDown); @@ -352,6 +391,20 @@ export default { this.config = this.getConfig(); this.legend = this.config.legend; + this.yAxes = [{ + id: this.config.yAxis.id, + seriesCount: 0, + tickWidth: 0 + }]; + if (this.config.additionalYAxes) { + this.yAxes = this.yAxes.concat(this.config.additionalYAxes.map(yAxis => { + return { + id: yAxis.id, + seriesCount: 0, + tickWidth: 0 + }; + })); + } const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier); this.$emit('configLoaded', configId); @@ -373,6 +426,8 @@ export default { this.openmct.selection.on('change', this.updateSelection); this.setTimeContext(); + this.yAxisListWithRange = [this.config.yAxis, ...this.config.additionalYAxes]; + this.loaded = true; }, beforeDestroy() { @@ -456,8 +511,10 @@ export default { }, setTimeContext() { this.stopFollowingTimeContext(); + this.timeContext = this.openmct.time.getContextForView(this.path); this.followTimeContext(); + }, followTimeContext() { this.updateDisplayBounds(this.timeContext.bounds()); @@ -490,33 +547,41 @@ export default { return config; }, addSeries(series, index) { + const yAxisId = series.get('yAxisId'); + this.updateAxisUsageCount(yAxisId, 1); this.$set(this.seriesModels, index, series); this.listenTo(series, 'change:xKey', (xKey) => { this.setDisplayRange(series, xKey); }, this); this.listenTo(series, 'change:yKey', () => { - this.checkSameRangeValue(); this.loadSeriesData(series); }, this); this.listenTo(series, 'change:interpolate', () => { this.loadSeriesData(series); }, this); + this.listenTo(series, 'change:yAxisId', this.updateTicksAndSeriesForYAxis, this); - this.checkSameRangeValue(); this.loadSeriesData(series); }, - checkSameRangeValue() { - this.hasSameRangeValue = this.seriesModels.every((model) => { - return model.get('yKey') === this.seriesModels[0].get('yKey'); - }); + removeSeries(plotSeries, index) { + const yAxisId = plotSeries.get('yAxisId'); + this.updateAxisUsageCount(yAxisId, -1); + this.seriesModels.splice(index, 1); + this.stopListening(plotSeries); }, - removeSeries(plotSeries, index) { - this.seriesModels.splice(index, 1); - this.checkSameRangeValue(); - this.stopListening(plotSeries); + updateTicksAndSeriesForYAxis(newAxisId, oldAxisId) { + this.updateAxisUsageCount(oldAxisId, -1); + this.updateAxisUsageCount(newAxisId, 1); + }, + + updateAxisUsageCount(yAxisId, updateCountBy) { + const foundYAxis = this.yAxes.find(yAxis => yAxis.id === yAxisId); + if (foundYAxis) { + foundYAxis.seriesCount = foundYAxis.seriesCount + updateCountBy; + } }, async loadAnnotations() { if (!this.openmct.annotation.getAvailableTags().length) { @@ -832,7 +897,13 @@ export default { // Setup canvas etc. this.xScale = new LinearScale(this.config.xAxis.get('displayRange')); - this.yScale = new LinearScale(this.config.yAxis.get('displayRange')); + this.yScale = []; + this.yAxisListWithRange.forEach((yAxis) => { + this.yScale.push({ + id: yAxis.id, + scale: new LinearScale(yAxis.get('displayRange')) + }); + }); this.pan = undefined; this.marquee = undefined; @@ -848,7 +919,9 @@ export default { this.cursorGuideHorizontal = this.$refs.cursorGuideHorizontal; this.listenTo(this.config.xAxis, 'change:displayRange', this.onXAxisChange, this); - this.listenTo(this.config.yAxis, 'change:displayRange', this.onYAxisChange, this); + this.yAxisListWithRange.forEach((yAxis) => { + this.listenTo(yAxis, 'change:displayRange', this.onYAxisChange.bind(this, yAxis.id), this); + }); }, onXAxisChange(displayBounds) { @@ -857,26 +930,45 @@ export default { } }, - onYAxisChange(displayBounds) { + onYAxisChange(yAxisId, displayBounds) { if (displayBounds) { - this.yScale.domain(displayBounds); + this.yScale.filter((yAxis) => yAxis.id === yAxisId).forEach((yAxis) => { + yAxis.scale.domain(displayBounds); + }); } }, - onTickWidthChange(width, fromDifferentObject) { - if (fromDifferentObject) { + onTickWidthChange(data, fromDifferentObject) { + const {width, yAxisId} = data; + if (yAxisId) { + const index = this.yAxes.findIndex(yAxis => yAxis.id === yAxisId); + if (fromDifferentObject) { // Always accept tick width if it comes from a different object. - this.tickWidth = width; - } else { + this.yAxes[index].tickWidth = width; + } else { // Otherwise, only accept tick with if it's larger. - const newWidth = Math.max(width, this.tickWidth); - if (newWidth !== this.tickWidth) { - this.tickWidth = newWidth; + const newWidth = Math.max(width, this.yAxes[index].tickWidth); + if (newWidth !== this.yAxes[index].tickWidth) { + this.yAxes[index].tickWidth = newWidth; + } } + + const id = this.openmct.objects.makeKeyString(this.domainObject.identifier); + this.$emit('plotTickWidth', this.yAxes[index].tickWidth, id); + } + }, + + toggleSeriesForYAxis({ id, visible}) { + //if toggling to visible, re-fetch the data for the series that are part of this y Axis + if (visible === true) { + this.config.series.models.filter(model => model.get('yAxisId') === id) + .forEach(this.loadSeriesData, this); } - const id = this.openmct.objects.makeKeyString(this.domainObject.identifier); - this.$emit('plotTickWidth', this.tickWidth, id); + this.yAxisIdVisibility[id] = visible; + this.hiddenYAxisIds = Object.keys(this.yAxisIdVisibility).map(Number).filter(key => { + return this.yAxisIdVisibility[key] === false; + }); }, trackMousePosition(event) { @@ -885,9 +977,11 @@ export default { min: 0, max: this.chartElementBounds.width }); - this.yScale.range({ - min: 0, - max: this.chartElementBounds.height + this.yScale.forEach((yAxis) => { + yAxis.scale.range({ + min: 0, + max: this.chartElementBounds.height + }); }); this.positionOverElement = { @@ -896,9 +990,13 @@ export default { - (event.clientY - this.chartElementBounds.top) }; + const yLocationForPositionOverPlot = this.yScale.map((yAxis) => yAxis.scale.invert(this.positionOverElement.y)); + const yAxisIds = this.yScale.map((yAxis) => yAxis.id); + // Also store the order of yAxisIds so that we can associate the y location to the yAxis this.positionOverPlot = { x: this.xScale.invert(this.positionOverElement.x), - y: this.yScale.invert(this.positionOverElement.y) + y: yLocationForPositionOverPlot, + yAxisIds }; if (this.cursorGuide) { @@ -911,6 +1009,12 @@ export default { event.preventDefault(); }, + getYPositionForYAxis(object, yAxis) { + const index = object.yAxisIds.findIndex(yAxisId => yAxisId === yAxis.get('id')); + + return object.y[index]; + }, + updateCrosshairs(event) { this.cursorGuideVertical.style.left = (event.clientX - this.chartElementBounds.x) + 'px'; this.cursorGuideHorizontal.style.top = (event.clientY - this.chartElementBounds.y) + 'px'; @@ -1017,8 +1121,9 @@ export default { } const { start, end } = this.marquee; + const someYPositionOverPlot = start.y.some(y => y); - return start.x === end.x && start.y === end.y; + return start.x === end.x && someYPositionOverPlot; }, updateMarquee() { @@ -1179,9 +1284,15 @@ export default { }, endAnnotationMarquee(event) { const minX = Math.min(this.marquee.start.x, this.marquee.end.x); - const minY = Math.min(this.marquee.start.y, this.marquee.end.y); + const startMinY = this.marquee.start.y.reduce((previousY, currentY) => { + return Math.min(previousY, currentY); + }, this.marquee.start.y[0]); + const endMinY = this.marquee.end.y.reduce((previousY, currentY) => { + return Math.min(previousY, currentY); + }, this.marquee.end.y[0]); + const minY = Math.min(startMinY, endMinY); const maxX = Math.max(this.marquee.start.x, this.marquee.end.x); - const maxY = Math.max(this.marquee.start.y, this.marquee.end.y); + const maxY = Math.max(startMinY, endMinY); const boundingBox = { minX, minY, @@ -1205,9 +1316,13 @@ export default { min: Math.min(this.marquee.start.x, this.marquee.end.x), max: Math.max(this.marquee.start.x, this.marquee.end.x) }); - this.config.yAxis.set('displayRange', { - min: Math.min(this.marquee.start.y, this.marquee.end.y), - max: Math.max(this.marquee.start.y, this.marquee.end.y) + this.yAxisListWithRange.forEach((yAxis) => { + const yStartPosition = this.getYPositionForYAxis(this.marquee.start, yAxis); + const yEndPosition = this.getYPositionForYAxis(this.marquee.end, yAxis); + yAxis.set('displayRange', { + min: Math.min(yStartPosition, yEndPosition), + max: Math.max(yStartPosition, yEndPosition) + }); }); this.userViewportChangeEnd(); } else { @@ -1238,11 +1353,17 @@ export default { zoom(zoomDirection, zoomFactor) { const currentXaxis = this.config.xAxis.get('displayRange'); - const currentYaxis = this.config.yAxis.get('displayRange'); + + let doesYAxisHaveRange = false; + this.yAxisListWithRange.forEach((yAxisModel) => { + if (yAxisModel.get('displayRange')) { + doesYAxisHaveRange = true; + } + }); // when there is no plot data, the ranges can be undefined // in which case we should not perform zoom - if (!currentXaxis || !currentYaxis) { + if (!currentXaxis || !doesYAxisHaveRange) { return; } @@ -1250,7 +1371,6 @@ export default { this.trackHistory(); const xAxisDist = (currentXaxis.max - currentXaxis.min) * zoomFactor; - const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor; if (zoomDirection === 'in') { this.config.xAxis.set('displayRange', { @@ -1258,9 +1378,17 @@ export default { max: currentXaxis.max - xAxisDist }); - this.config.yAxis.set('displayRange', { - min: currentYaxis.min + yAxisDist, - max: currentYaxis.max - yAxisDist + this.yAxisListWithRange.forEach((yAxisModel) => { + const currentYaxis = yAxisModel.get('displayRange'); + if (!currentYaxis) { + return; + } + + const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor; + yAxisModel.set('displayRange', { + min: currentYaxis.min + yAxisDist, + max: currentYaxis.max - yAxisDist + }); }); } else if (zoomDirection === 'out') { this.config.xAxis.set('displayRange', { @@ -1268,9 +1396,17 @@ export default { max: currentXaxis.max + xAxisDist }); - this.config.yAxis.set('displayRange', { - min: currentYaxis.min - yAxisDist, - max: currentYaxis.max + yAxisDist + this.yAxisListWithRange.forEach((yAxisModel) => { + const currentYaxis = yAxisModel.get('displayRange'); + if (!currentYaxis) { + return; + } + + const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor; + yAxisModel.set('displayRange', { + min: currentYaxis.min - yAxisDist, + max: currentYaxis.max + yAxisDist + }); }); } @@ -1287,11 +1423,17 @@ export default { } let xDisplayRange = this.config.xAxis.get('displayRange'); - let yDisplayRange = this.config.yAxis.get('displayRange'); + + let doesYAxisHaveRange = false; + this.yAxisListWithRange.forEach((yAxisModel) => { + if (yAxisModel.get('displayRange')) { + doesYAxisHaveRange = true; + } + }); // when there is no plot data, the ranges can be undefined // in which case we should not perform zoom - if (!xDisplayRange || !yDisplayRange) { + if (!xDisplayRange || !doesYAxisHaveRange) { return; } @@ -1299,22 +1441,19 @@ export default { window.clearTimeout(this.stillZooming); let xAxisDist = (xDisplayRange.max - xDisplayRange.min); - let yAxisDist = (yDisplayRange.max - yDisplayRange.min); let xDistMouseToMax = xDisplayRange.max - this.positionOverPlot.x; let xDistMouseToMin = this.positionOverPlot.x - xDisplayRange.min; - let yDistMouseToMax = yDisplayRange.max - this.positionOverPlot.y; - let yDistMouseToMin = this.positionOverPlot.y - yDisplayRange.min; let xAxisMaxDist = xDistMouseToMax / xAxisDist; let xAxisMinDist = xDistMouseToMin / xAxisDist; - let yAxisMaxDist = yDistMouseToMax / yAxisDist; - let yAxisMinDist = yDistMouseToMin / yAxisDist; let plotHistoryStep; if (!plotHistoryStep) { + const yRangeList = []; + this.yAxisListWithRange.map((yAxis) => yRangeList.push(yAxis.get('displayRange'))); plotHistoryStep = { - x: xDisplayRange, - y: yDisplayRange + x: this.config.xAxis.get('displayRange'), + y: yRangeList }; } @@ -1325,20 +1464,47 @@ export default { max: xDisplayRange.max - ((xAxisDist * ZOOM_AMT) * xAxisMaxDist) }); - this.config.yAxis.set('displayRange', { - min: yDisplayRange.min + ((yAxisDist * ZOOM_AMT) * yAxisMinDist), - max: yDisplayRange.max - ((yAxisDist * ZOOM_AMT) * yAxisMaxDist) + this.yAxisListWithRange.forEach((yAxisModel) => { + const yDisplayRange = yAxisModel.get('displayRange'); + if (!yDisplayRange) { + return; + } + + const yPosition = this.getYPositionForYAxis(this.positionOverPlot, yAxisModel); + let yAxisDist = (yDisplayRange.max - yDisplayRange.min); + let yDistMouseToMax = yDisplayRange.max - yPosition; + let yDistMouseToMin = yPosition - yDisplayRange.min; + let yAxisMaxDist = yDistMouseToMax / yAxisDist; + let yAxisMinDist = yDistMouseToMin / yAxisDist; + + yAxisModel.set('displayRange', { + min: yDisplayRange.min + ((yAxisDist * ZOOM_AMT) * yAxisMinDist), + max: yDisplayRange.max - ((yAxisDist * ZOOM_AMT) * yAxisMaxDist) + }); }); } else if (event.wheelDelta >= 0) { - this.config.xAxis.set('displayRange', { min: xDisplayRange.min - ((xAxisDist * ZOOM_AMT) * xAxisMinDist), max: xDisplayRange.max + ((xAxisDist * ZOOM_AMT) * xAxisMaxDist) }); - this.config.yAxis.set('displayRange', { - min: yDisplayRange.min - ((yAxisDist * ZOOM_AMT) * yAxisMinDist), - max: yDisplayRange.max + ((yAxisDist * ZOOM_AMT) * yAxisMaxDist) + this.yAxisListWithRange.forEach((yAxisModel) => { + const yDisplayRange = yAxisModel.get('displayRange'); + if (!yDisplayRange) { + return; + } + + const yPosition = this.getYPositionForYAxis(this.positionOverPlot, yAxisModel); + let yAxisDist = (yDisplayRange.max - yDisplayRange.min); + let yDistMouseToMax = yDisplayRange.max - yPosition; + let yDistMouseToMin = yPosition - yDisplayRange.min; + let yAxisMaxDist = yDistMouseToMax / yAxisDist; + let yAxisMinDist = yDistMouseToMin / yAxisDist; + + yAxisModel.set('displayRange', { + min: yDisplayRange.min - ((yAxisDist * ZOOM_AMT) * yAxisMinDist), + max: yDisplayRange.max + ((yAxisDist * ZOOM_AMT) * yAxisMaxDist) + }); }); } @@ -1371,24 +1537,48 @@ export default { } const dX = this.pan.start.x - this.positionOverPlot.x; - const dY = this.pan.start.y - this.positionOverPlot.y; const xRange = this.config.xAxis.get('displayRange'); - const yRange = this.config.yAxis.get('displayRange'); this.config.xAxis.set('displayRange', { min: xRange.min + dX, max: xRange.max + dX }); - this.config.yAxis.set('displayRange', { - min: yRange.min + dY, - max: yRange.max + dY + + const dY = []; + this.positionOverPlot.y.forEach((yAxisPosition, index) => { + const yAxisId = this.positionOverPlot.yAxisIds[index]; + dY.push({ + yAxisId: yAxisId, + y: this.pan.start.y[index] - yAxisPosition + }); + }); + + this.yAxisListWithRange.forEach((yAxis) => { + const yRange = yAxis.get('displayRange'); + if (!yRange) { + return; + } + + const yIndex = dY.findIndex(y => y.yAxisId === yAxis.get('id')); + + yAxis.set('displayRange', { + min: yRange.min + dY[yIndex].y, + max: yRange.max + dY[yIndex].y + }); }); }, trackHistory() { + const yRangeList = []; + const yAxisIds = []; + this.yAxisListWithRange.forEach((yAxis) => { + yRangeList.push(yAxis.get('displayRange')); + yAxisIds.push(yAxis.get('id')); + }); this.plotHistory.push({ x: this.config.xAxis.get('displayRange'), - y: this.config.yAxis.get('displayRange') + y: yRangeList, + yAxisIds }); }, @@ -1398,7 +1588,9 @@ export default { }, freeze() { - this.config.yAxis.set('frozen', true); + this.yAxisListWithRange.forEach((yAxis) => { + yAxis.set('frozen', true); + }); this.config.xAxis.set('frozen', true); this.setStatus(); }, @@ -1409,7 +1601,9 @@ export default { }, clearPanZoomHistory() { - this.config.yAxis.set('frozen', false); + this.yAxisListWithRange.forEach((yAxis) => { + yAxis.set('frozen', false); + }); this.config.xAxis.set('frozen', false); this.setStatus(); this.plotHistory = []; @@ -1424,12 +1618,17 @@ export default { } this.config.xAxis.set('displayRange', previousAxisRanges.x); - this.config.yAxis.set('displayRange', previousAxisRanges.y); + this.yAxisListWithRange.forEach((yAxis) => { + const yPosition = this.getYPositionForYAxis(previousAxisRanges, yAxis); + yAxis.set('displayRange', yPosition); + }); + this.userViewportChangeEnd(); }, - setYAxisKey(yKey) { - this.config.series.models[0].set('yKey', yKey); + setYAxisKey(yKey, yAxisId) { + const seriesForYAxis = this.config.series.models.filter((model => model.get('yAxisId') === yAxisId)); + seriesForYAxis.forEach(model => model.set('yKey', yKey)); }, pause() { diff --git a/src/plugins/plot/MctTicks.vue b/src/plugins/plot/MctTicks.vue index ab09cb6d1c..755678fc70 100644 --- a/src/plugins/plot/MctTicks.vue +++ b/src/plugins/plot/MctTicks.vue @@ -103,6 +103,12 @@ export default { return 6; } }, + axisId: { + type: Number, + default() { + return null; + } + }, position: { required: true, type: String, @@ -145,7 +151,15 @@ export default { throw new Error('config is missing'); } - return config[this.axisType]; + if (this.axisType === 'yAxis') { + if (this.axisId && this.axisId !== config.yAxis.id) { + return config.additionalYAxes.find(axis => axis.id === this.axisId); + } else { + return config.yAxis; + } + } else { + return config[this.axisType]; + } }, /** * Determine whether ticks should be regenerated for a given range. @@ -258,7 +272,10 @@ export default { }, 0)); this.tickWidth = tickWidth; - this.$emit('plotTickWidth', tickWidth); + this.$emit('plotTickWidth', { + width: tickWidth, + yAxisId: this.axisType === 'yAxis' ? this.axisId : '' + }); this.shouldCheckWidth = false; } } diff --git a/src/plugins/plot/axis/YAxis.vue b/src/plugins/plot/axis/YAxis.vue index 6e170fbd6a..0073a048b8 100644 --- a/src/plugins/plot/axis/YAxis.vue +++ b/src/plugins/plot/axis/YAxis.vue @@ -22,19 +22,28 @@ -
+
-
+
-
-
+
+ +
diff --git a/src/ui/components/tags/tags.scss b/src/ui/components/tags/tags.scss index 964b2361ab..23157937e3 100644 --- a/src/ui/components/tags/tags.scss +++ b/src/ui/components/tags/tags.scss @@ -1,19 +1,30 @@ +@mixin tagHolder() { + align-items: center; + display: flex; + flex-wrap: wrap; + + > * { + $m: $interiorMarginSm; + + margin: 0 $m $m 0; + } +} + + /******************************* TAGS */ .c-tag { - border-radius: 10px; //TODO: convert to theme constant + border-radius: $tagBorderRadius; display: inline-flex; - padding: 1px 10px; //TODO: convert to theme constant - - > * + * { - margin-left: $interiorMargin; - } + overflow: hidden; + padding: 1px 6px; //TODO: convert to theme constant + transition: $transIn; &__remove-btn { color: inherit !important; - display: none; opacity: 0; - overflow: hidden; - padding: 1px !important; + padding: 0; // Overrides default

    Legend

    @@ -97,20 +96,23 @@ export default { mounted() { eventHelpers.extend(this); this.config = this.getConfig(); - this.yAxes = [{ - id: this.config.yAxis.id, - seriesCount: 0 - }]; - if (this.config.additionalYAxes) { - this.yAxes = this.yAxes.concat(this.config.additionalYAxes.map(yAxis => { - return { - id: yAxis.id, - seriesCount: 0 - }; - })); + if (!this.isStackedPlotObject) { + this.yAxes = [{ + id: this.config.yAxis.id, + seriesCount: 0 + }]; + if (this.config.additionalYAxes) { + this.yAxes = this.yAxes.concat(this.config.additionalYAxes.map(yAxis => { + return { + id: yAxis.id, + seriesCount: 0 + }; + })); + } + + this.registerListeners(); } - this.registerListeners(); this.loaded = true; }, beforeDestroy() { diff --git a/src/plugins/plot/inspector/PlotsInspectorViewProvider.js b/src/plugins/plot/inspector/PlotsInspectorViewProvider.js index aebacfa7eb..36703c0263 100644 --- a/src/plugins/plot/inspector/PlotsInspectorViewProvider.js +++ b/src/plugins/plot/inspector/PlotsInspectorViewProvider.js @@ -12,11 +12,12 @@ export default function PlotsInspectorViewProvider(openmct) { } 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 isStackedPlotObject = object && object.type === 'telemetry.plot.stacked'; + const isParentStackedPlotObject = parent && parent.type === 'telemetry.plot.stacked'; - return isStackedPlotObject || isOverlayPlotObject; + return isOverlayPlotObject || isParentStackedPlotObject; }, view: function (selection) { let component; diff --git a/src/plugins/plot/inspector/StackedPlotsInspectorViewProvider.js b/src/plugins/plot/inspector/StackedPlotsInspectorViewProvider.js index 8cd6bc78d2..2e64675040 100644 --- a/src/plugins/plot/inspector/StackedPlotsInspectorViewProvider.js +++ b/src/plugins/plot/inspector/StackedPlotsInspectorViewProvider.js @@ -12,12 +12,10 @@ export default function StackedPlotsInspectorViewProvider(openmct) { } const object = selection[0][0].context.item; - const parent = selection[0].length > 1 && selection[0][1].context.item; - const isOverlayPlotObject = object && object.type === 'telemetry.plot.overlay'; - const isParentStackedPlotObject = parent && parent.type === 'telemetry.plot.stacked'; + const isStackedPlotObject = object && object.type === 'telemetry.plot.stacked'; - return !isOverlayPlotObject && isParentStackedPlotObject; + return isStackedPlotObject; }, view: function (selection) { let component; diff --git a/src/plugins/plot/legend/PlotLegend.vue b/src/plugins/plot/legend/PlotLegend.vue index 01054b1958..9b954e32bd 100644 --- a/src/plugins/plot/legend/PlotLegend.vue +++ b/src/plugins/plot/legend/PlotLegend.vue @@ -49,10 +49,10 @@ title="Cursor is point locked. Click anywhere in the plot to unlock." >
@@ -95,11 +95,10 @@ @@ -111,6 +110,9 @@ diff --git a/src/ui/inspector/annotations/AnnotationsInspectorView.vue b/src/ui/inspector/annotations/AnnotationsInspectorView.vue index a1b20b2970..db9dc17148 100644 --- a/src/ui/inspector/annotations/AnnotationsInspectorView.vue +++ b/src/ui/inspector/annotations/AnnotationsInspectorView.vue @@ -111,25 +111,31 @@ export default { return this?.selection?.[0]?.[0]?.context?.item; }, targetDetails() { - return this?.selection?.[0]?.[1]?.context?.targetDetails ?? {}; + return this?.selection?.[0]?.[0]?.context?.targetDetails ?? {}; }, shouldShowTagsEditor() { - return Object.keys(this.targetDetails).length > 0; + const showingTagsEditor = Object.keys(this.targetDetails).length > 0; + + if (showingTagsEditor) { + return true; + } + + return false; }, targetDomainObjects() { - return this?.selection?.[0]?.[1]?.context?.targetDomainObjects ?? {}; + return this?.selection?.[0]?.[0]?.context?.targetDomainObjects ?? {}; }, selectedAnnotations() { - return this?.selection?.[0]?.[1]?.context?.annotations; + return this?.selection?.[0]?.[0]?.context?.annotations; }, annotationType() { - return this?.selection?.[0]?.[1]?.context?.annotationType; + return this?.selection?.[0]?.[0]?.context?.annotationType; }, annotationFilter() { - return this?.selection?.[0]?.[1]?.context?.annotationFilter; + return this?.selection?.[0]?.[0]?.context?.annotationFilter; }, onAnnotationChange() { - return this?.selection?.[0]?.[1]?.context?.onAnnotationChange; + return this?.selection?.[0]?.[0]?.context?.onAnnotationChange; } }, async mounted() { @@ -195,6 +201,7 @@ export default { } }, async loadAnnotationForTargetObject(target) { + console.debug(`📝 Loading annotations for target`, target); const targetID = this.openmct.objects.makeKeyString(target.identifier); const allAnnotationsForTarget = await this.openmct.annotation.getAnnotations(target.identifier); const filteredAnnotationsForSelection = allAnnotationsForTarget.filter(annotation => { diff --git a/src/ui/inspector/annotations/annotation-inspector.scss b/src/ui/inspector/annotations/annotation-inspector.scss deleted file mode 100644 index af58979e4b..0000000000 --- a/src/ui/inspector/annotations/annotation-inspector.scss +++ /dev/null @@ -1,18 +0,0 @@ -.c-inspect-annotations { - > * + * { - margin-top: $interiorMargin; - } - - &__content{ - > * + * { - margin-top: $interiorMargin; - } - } - - &__content { - display: flex; - flex-direction: column; - } -} - - diff --git a/src/ui/layout/search/AnnotationSearchResult.vue b/src/ui/layout/search/AnnotationSearchResult.vue index f2eeff5075..d97f656339 100644 --- a/src/ui/layout/search/AnnotationSearchResult.vue +++ b/src/ui/layout/search/AnnotationSearchResult.vue @@ -150,16 +150,11 @@ export default { }); const selection = [ - { - element: this.openmct.layout.$refs.browseObject.$el, - context: { - item: this.result - } - }, { element: this.$el, context: { - type: 'plot-points-selection', + item: this.result.targetModels[0], + type: 'plot-annotation-search-result', targetDetails, targetDomainObjects, annotations: [this.result], From 0382d22f7f7d075c3250883b509dafb902682d1e Mon Sep 17 00:00:00 2001 From: Jamie V Date: Wed, 1 Feb 2023 11:55:08 -0800 Subject: [PATCH 184/274] [Notebook] Entry links tests (#6190) * removing dupe nb install, adding whitelist nb init script, testing whitelist urls * updating from copy * addressing PR comments for cleaner tests * removing .only * added a secure url test and a subdomain url test and simplified some code * not messin with protocols atm * update variable name --- e2e/helper/addInitNotebookWithUrls.js | 32 ++++ .../plugins/notebook/notebook.e2e.spec.js | 155 +++++++++++------- src/plugins/notebook/plugin.js | 6 - 3 files changed, 130 insertions(+), 63 deletions(-) create mode 100644 e2e/helper/addInitNotebookWithUrls.js diff --git a/e2e/helper/addInitNotebookWithUrls.js b/e2e/helper/addInitNotebookWithUrls.js new file mode 100644 index 0000000000..0af7d7b60b --- /dev/null +++ b/e2e/helper/addInitNotebookWithUrls.js @@ -0,0 +1,32 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +// This should be used to install the re-instal default Notebook plugin with a simple url whitelist. +// e.g. +// await page.addInitScript({ path: path.join(__dirname, 'addInitNotebookWithUrls.js') }); +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)); +}); diff --git a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js index 8d867e7517..cd73405757 100644 --- a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js @@ -25,8 +25,11 @@ This test suite is dedicated to tests which verify the basic operations surround */ const { test, expect } = require('../../../../pluginFixtures'); -const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../../../appActions'); +const { createDomainObjectWithDefaults } = require('../../../../appActions'); const nbUtils = require('../../../../helper/notebookUtils'); +const path = require('path'); + +const NOTEBOOK_NAME = 'Notebook'; test.describe('Notebook CRUD Operations', () => { test.fixme('Can create a Notebook Object', async ({ page }) => { @@ -73,8 +76,7 @@ test.describe('Notebook section tests', () => { // Create Notebook await createDomainObjectWithDefaults(page, { - type: 'Notebook', - name: "Test Notebook" + type: NOTEBOOK_NAME }); }); test('Default and new sections are automatically named Unnamed Section with Unnamed Page', async ({ page }) => { @@ -135,8 +137,7 @@ test.describe('Notebook page tests', () => { // Create Notebook await createDomainObjectWithDefaults(page, { - type: 'Notebook', - name: "Test Notebook" + type: NOTEBOOK_NAME }); }); //Test will need to be implemented after a refactor in #5713 @@ -207,24 +208,30 @@ test.describe('Notebook search tests', () => { }); 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: 'networkidle' }); + + notebookObject = await createDomainObjectWithDefaults(page, { + type: NOTEBOOK_NAME + }); + }); test.fixme('When a new entry is created, it should be focused', async ({ page }) => {}); test('When an object is dropped into a notebook, a new entry is created and it should be focused @unstable', async ({ page }) => { - await page.goto('./#/browse/mine', { waitUntil: 'networkidle' }); - - // Create Notebook - const notebook = await createDomainObjectWithDefaults(page, { - type: 'Notebook', - name: "Embed Test Notebook" - }); // Create Overlay Plot await createDomainObjectWithDefaults(page, { - type: 'Overlay Plot', - name: "Dropped Overlay Plot" + type: 'Overlay Plot' }); - await expandTreePaneItemByName(page, 'My Items'); + // 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.goto(notebook.url); await page.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', '.c-notebook__drag-area'); const embed = page.locator('.c-ne__embed__link'); @@ -234,22 +241,16 @@ test.describe('Notebook entry tests', () => { expect(embedName).toBe('Dropped Overlay Plot'); }); test('When an object is dropped into a notebooks existing entry, it should be focused @unstable', async ({ page }) => { - await page.goto('./#/browse/mine', { waitUntil: 'networkidle' }); - - // Create Notebook - const notebook = await createDomainObjectWithDefaults(page, { - type: 'Notebook', - name: "Embed Test Notebook" - }); // Create Overlay Plot await createDomainObjectWithDefaults(page, { - type: 'Overlay Plot', - name: "Dropped Overlay Plot" + type: 'Overlay Plot' }); - await expandTreePaneItemByName(page, 'My Items'); + // Navigate to the notebook object + await page.goto(notebookObject.url); - await page.goto(notebook.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.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', 'text=Entry to drop into'); @@ -263,19 +264,14 @@ test.describe('Notebook entry tests', () => { }); test.fixme('new entries persist through navigation events without save', async ({ page }) => {}); test.fixme('previous and new entries can be deleted', async ({ page }) => {}); - test.fixme('when a valid link is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => { + 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 page.goto('./#/browse/mine', { waitUntil: 'networkidle' }); - // Create Notebook - const notebook = await createDomainObjectWithDefaults(page, { - type: 'Notebook', - name: "Entry Link Test" - }); + // Navigate to the notebook object + await page.goto(notebookObject.url); - await expandTreePaneItemByName(page, 'My Items'); - - await page.goto(notebook.url); + // Reveal the notebook in the tree + await page.getByTitle('Show selected item in tree').click(); await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`); @@ -293,19 +289,14 @@ test.describe('Notebook entry tests', () => { expect(await validLink.count()).toBe(1); }); - test.fixme('when an invalid link is entered into a notebook entry, it does not become clickable when viewing', async ({ page }) => { + 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 page.goto('./#/browse/mine', { waitUntil: 'networkidle' }); - // Create Notebook - const notebook = await createDomainObjectWithDefaults(page, { - type: 'Notebook', - name: "Entry Link Test" - }); + // Navigate to the notebook object + await page.goto(notebookObject.url); - await expandTreePaneItemByName(page, 'My Items'); - - await page.goto(notebook.url); + // Reveal the notebook in the tree + await page.getByTitle('Show selected item in tree').click(); await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`); @@ -313,20 +304,70 @@ test.describe('Notebook entry tests', () => { expect(await invalidLink.count()).toBe(0); }); - test.fixme('when a nefarious link is entered into a notebook entry, it is sanitized when viewing', async ({ page }) => { + 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'; + + // 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, `This should NOT be a link: ${TEST_LINK} is it?`); + + const invalidLink = page.locator(`a[href="${TEST_LINK}"]`); + + 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'; + + // 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, `This should be a link: ${INVALID_TEST_LINK} is it?`); + + const validLink = page.locator(`a[href="${INVALID_TEST_LINK}"]`); + + 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'; + + // 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, `This should be a link: ${TEST_LINK} is it?`); + + const validLink = page.locator(`a[href="${TEST_LINK}"]`); + + // Start waiting for popup before clicking. Note no await. + const popupPromise = page.waitForEvent('popup'); + + await validLink.click(); + const popup = await popupPromise; + + // Wait for the popup to load. + await popup.waitForLoadState(); + expect.soft(popup.url()).toContain('www.google.com'); + + 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 page.goto('./#/browse/mine', { waitUntil: 'networkidle' }); - // Create Notebook - const notebook = await createDomainObjectWithDefaults(page, { - type: 'Notebook', - name: "Entry Link Test" - }); + // Navigate to the notebook object + await page.goto(notebookObject.url); - await expandTreePaneItemByName(page, 'My Items'); - - await page.goto(notebook.url); + // Reveal the notebook in the tree + await page.getByTitle('Show selected item in tree').click(); await nbUtils.enterTextEntry(page, `This should be a link, BUT not a bad link: ${TEST_LINK_BAD} is it?`); diff --git a/src/plugins/notebook/plugin.js b/src/plugins/notebook/plugin.js index f24742644b..41b379fc98 100644 --- a/src/plugins/notebook/plugin.js +++ b/src/plugins/notebook/plugin.js @@ -105,10 +105,6 @@ function installBaseNotebookFunctionality(openmct) { function NotebookPlugin(name = 'Notebook', entryUrlWhitelist = []) { return function install(openmct) { - if (openmct[NOTEBOOK_INSTALLED_KEY]) { - return; - } - const icon = 'icon-notebook'; const description = 'Create and save timestamped notes with embedded object snapshots.'; const snapshotContainer = getSnapshotContainer(openmct); @@ -122,8 +118,6 @@ function NotebookPlugin(name = 'Notebook', entryUrlWhitelist = []) { openmct.objectViews.addProvider(notebookView, entryUrlWhitelist); installBaseNotebookFunctionality(openmct); - - openmct[NOTEBOOK_INSTALLED_KEY] = true; }; } From c1c1d879536483e875366f6dd2841aa00b4f72a2 Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Wed, 1 Feb 2023 13:46:15 -0800 Subject: [PATCH 185/274] Fix multiple y axis issues (#6204) * Ensure enabling log mode does not reset series that don't belong to that yaxis. propagate both left and right y axes widths so that plots can adjust accordingly * Revert code Handle second axis resizing * Fixes issue where logMode was getting initialized incorrectly for multiple y axes * Get the yAxisId of the series from the model. * Address review comments - rename params for readability * Fix number of log ticks expected and the tick values since we reduced the number of secondary ticks * Fix log plot test * Add guard code during destroy * Add missing remove callback --- .../plugins/plot/logPlot.e2e.spec.js | 37 +++------- src/plugins/plot/MctPlot.vue | 72 ++++++++++++++----- src/plugins/plot/MctTicks.vue | 4 +- src/plugins/plot/axis/XAxis.vue | 2 +- src/plugins/plot/axis/YAxis.vue | 16 +++-- src/plugins/plot/configuration/PlotSeries.js | 13 +++- src/plugins/plot/configuration/YAxisModel.js | 3 +- src/plugins/plot/legend/PlotLegend.vue | 7 ++ src/plugins/plot/stackedPlot/StackedPlot.vue | 39 ++++++++-- .../plot/stackedPlot/StackedPlotItem.vue | 49 ++++++++++--- 10 files changed, 169 insertions(+), 73 deletions(-) diff --git a/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js index e923b15bb4..ca0689ce0e 100644 --- a/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/logPlot.e2e.spec.js @@ -160,35 +160,16 @@ async function testRegularTicks(page) { */ async function testLogTicks(page) { const yTicks = await page.locator('.gl-plot-y-tick-label'); - expect(await yTicks.count()).toBe(28); + expect(await yTicks.count()).toBe(9); await expect(yTicks.nth(0)).toHaveText('-2.98'); - await expect(yTicks.nth(1)).toHaveText('-2.50'); - await expect(yTicks.nth(2)).toHaveText('-2.00'); - await expect(yTicks.nth(3)).toHaveText('-1.51'); - await expect(yTicks.nth(4)).toHaveText('-1.20'); - await expect(yTicks.nth(5)).toHaveText('-1.00'); - await expect(yTicks.nth(6)).toHaveText('-0.80'); - await expect(yTicks.nth(7)).toHaveText('-0.58'); - await expect(yTicks.nth(8)).toHaveText('-0.40'); - await expect(yTicks.nth(9)).toHaveText('-0.20'); - await expect(yTicks.nth(10)).toHaveText('-0.00'); - await expect(yTicks.nth(11)).toHaveText('0.20'); - await expect(yTicks.nth(12)).toHaveText('0.40'); - await expect(yTicks.nth(13)).toHaveText('0.58'); - await expect(yTicks.nth(14)).toHaveText('0.80'); - await expect(yTicks.nth(15)).toHaveText('1.00'); - await expect(yTicks.nth(16)).toHaveText('1.20'); - await expect(yTicks.nth(17)).toHaveText('1.51'); - await expect(yTicks.nth(18)).toHaveText('2.00'); - await expect(yTicks.nth(19)).toHaveText('2.50'); - await expect(yTicks.nth(20)).toHaveText('2.98'); - await expect(yTicks.nth(21)).toHaveText('3.50'); - await expect(yTicks.nth(22)).toHaveText('4.00'); - await expect(yTicks.nth(23)).toHaveText('4.50'); - await expect(yTicks.nth(24)).toHaveText('5.31'); - await expect(yTicks.nth(25)).toHaveText('7.00'); - await expect(yTicks.nth(26)).toHaveText('8.00'); - await expect(yTicks.nth(27)).toHaveText('9.00'); + 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'); } /** diff --git a/src/plugins/plot/MctPlot.vue b/src/plugins/plot/MctPlot.vue index 2466e0798a..49fc537c88 100644 --- a/src/plugins/plot/MctPlot.vue +++ b/src/plugins/plot/MctPlot.vue @@ -34,13 +34,14 @@ v-for="(yAxis, index) in yAxesIds" :id="yAxis.id" :key="`yAxis-${yAxis.id}-${index}`" - :multiple-left-axes="multipleLeftAxes" + :has-multiple-left-axes="hasMultipleLeftAxes" :position="yAxis.id > 2 ? 'right' : 'left'" :class="{'plot-yaxis-right': yAxis.id > 2}" :tick-width="yAxis.tickWidth" + :used-tick-width="plotFirstLeftTickWidth" :plot-left-tick-width="yAxis.id > 2 ? yAxis.tickWidth: plotLeftTickWidth" @yKeyChanged="setYAxisKey" - @tickWidthChanged="onTickWidthChange" + @plotYTickWidth="onYTickWidthChange" @toggleAxisVisibility="toggleSeriesForYAxis" />
@@ -61,7 +62,6 @@ v-show="gridLines && !options.compact" :axis-type="'xAxis'" :position="'right'" - @plotTickWidth="onTickWidthChange" />
yAxis.id > 2); - const leftOffset = this.multipleLeftAxes ? 2 * AXES_PADDING : AXES_PADDING; + const leftOffset = this.hasMultipleLeftAxes ? 2 * AXES_PADDING : AXES_PADDING; let style = { left: `${this.plotLeftTickWidth + leftOffset}px` }; + const parentRightAxisWidth = this.parentYTickWidth.rightTickWidth; - if (rightAxis) { - style.right = `${rightAxis.tickWidth + AXES_PADDING}px`; + if (parentRightAxisWidth || rightAxis) { + style.right = `${(parentRightAxisWidth || rightAxis.tickWidth) + AXES_PADDING}px`; } return style; @@ -310,8 +315,8 @@ export default { yAxesIds() { return this.yAxes.filter(yAxis => yAxis.seriesCount > 0); }, - multipleLeftAxes() { - return this.yAxes.filter(yAxis => yAxis.seriesCount > 0 && yAxis.id <= 2).length > 1; + hasMultipleLeftAxes() { + return this.parentYTickWidth.hasMultipleLeftAxes || this.yAxes.filter(yAxis => yAxis.seriesCount > 0 && yAxis.id <= 2).length > 1; }, isNestedWithinAStackedPlot() { const isNavigatedObject = this.openmct.router.isNavigatedObject([this.domainObject].concat(this.path)); @@ -325,6 +330,11 @@ export default { // only allow annotations viewing/editing if plot is paused or in fixed time mode return this.isFrozen || !this.isRealTime; }, + plotFirstLeftTickWidth() { + const firstYAxis = this.yAxes.find(yAxis => yAxis.id === 1); + + return firstYAxis ? firstYAxis.tickWidth : 0; + }, plotLeftTickWidth() { let leftTickWidth = 0; this.yAxes.forEach((yAxis) => { @@ -334,8 +344,9 @@ export default { leftTickWidth = leftTickWidth + yAxis.tickWidth; }); + const parentLeftTickWidth = this.parentYTickWidth.leftTickWidth; - return this.plotTickWidth || leftTickWidth; + return parentLeftTickWidth || leftTickWidth; } }, watch: { @@ -557,6 +568,14 @@ export default { updateTicksAndSeriesForYAxis(newAxisId, oldAxisId) { this.updateAxisUsageCount(oldAxisId, -1); this.updateAxisUsageCount(newAxisId, 1); + + const foundYAxis = this.yAxes.find(yAxis => yAxis.id === oldAxisId); + if (foundYAxis.seriesCount === 0) { + this.onYTickWidthChange({ + width: foundYAxis.tickWidth, + yAxisId: foundYAxis.id + }); + } }, updateAxisUsageCount(yAxisId, updateCountBy) { @@ -934,8 +953,13 @@ export default { } }, - onTickWidthChange(data, fromDifferentObject) { - const {width, yAxisId} = data; + /** + * Aggregate widths of all left and right y axes and send them up to any parent plots + * @param {Object} tickWidthWithYAxisId - the width and yAxisId of the tick bar + * @param fromDifferentObject + */ + onYTickWidthChange(tickWidthWithYAxisId, fromDifferentObject) { + const {width, yAxisId} = tickWidthWithYAxisId; if (yAxisId) { const index = this.yAxes.findIndex(yAxis => yAxis.id === yAxisId); if (fromDifferentObject) { @@ -944,13 +968,23 @@ export default { } else { // Otherwise, only accept tick with if it's larger. const newWidth = Math.max(width, this.yAxes[index].tickWidth); - if (newWidth !== this.yAxes[index].tickWidth) { + if (width !== this.yAxes[index].tickWidth) { this.yAxes[index].tickWidth = newWidth; } } const id = this.openmct.objects.makeKeyString(this.domainObject.identifier); - this.$emit('plotTickWidth', this.yAxes[index].tickWidth, id); + const leftTickWidth = this.yAxes.filter(yAxis => yAxis.id < 3).reduce((previous, current) => { + return previous + current.tickWidth; + }, 0); + const rightTickWidth = this.yAxes.filter(yAxis => yAxis.id > 2).reduce((previous, current) => { + return previous + current.tickWidth; + }, 0); + this.$emit('plotYTickWidth', { + hasMultipleLeftAxes: this.hasMultipleLeftAxes, + leftTickWidth, + rightTickWidth + }, id); } }, @@ -1722,7 +1756,9 @@ export default { }, destroy() { - configStore.deleteStore(this.config.id); + if (this.config) { + configStore.deleteStore(this.config.id); + } this.stopListening(); diff --git a/src/plugins/plot/MctTicks.vue b/src/plugins/plot/MctTicks.vue index 755678fc70..3ceecdeb70 100644 --- a/src/plugins/plot/MctTicks.vue +++ b/src/plugins/plot/MctTicks.vue @@ -86,6 +86,8 @@ import eventHelpers from "./lib/eventHelpers"; import { ticks, getLogTicks, getFormattedTicks } from "./tickUtils"; import configStore from "./configuration/ConfigStore"; +const SECONDARY_TICK_NUMBER = 2; + export default { inject: ['openmct', 'domainObject'], props: { @@ -205,7 +207,7 @@ export default { } if (this.axisType === 'yAxis' && this.axis.get('logMode')) { - return getLogTicks(range.min, range.max, number, 4); + return getLogTicks(range.min, range.max, number, SECONDARY_TICK_NUMBER); } else { return ticks(range.min, range.max, number); } diff --git a/src/plugins/plot/axis/XAxis.vue b/src/plugins/plot/axis/XAxis.vue index a6bc176aa1..1b92c339ca 100644 --- a/src/plugins/plot/axis/XAxis.vue +++ b/src/plugins/plot/axis/XAxis.vue @@ -152,7 +152,7 @@ export default { this.selectedXKeyOptionKey = this.xKeyOptions.length > 0 ? this.getXKeyOption(xAxisKey).key : xAxisKey; }, onTickWidthChange(width) { - this.$emit('tickWidthChanged', width); + this.$emit('plotXTickWidth', width); } } }; diff --git a/src/plugins/plot/axis/YAxis.vue b/src/plugins/plot/axis/YAxis.vue index 215465fc73..2f55ef5a64 100644 --- a/src/plugins/plot/axis/YAxis.vue +++ b/src/plugins/plot/axis/YAxis.vue @@ -101,7 +101,13 @@ export default { return 0; } }, - multipleLeftAxes: { + usedTickWidth: { + type: Number, + default() { + return 0; + } + }, + hasMultipleLeftAxes: { type: Boolean, default() { return false; @@ -138,14 +144,14 @@ export default { let style = { width: `${this.tickWidth + AXIS_PADDING}px` }; - const multipleAxesPadding = this.multipleLeftAxes ? AXIS_PADDING : 0; + const multipleAxesPadding = this.hasMultipleLeftAxes ? AXIS_PADDING : 0; if (this.position === 'right') { style.left = `-${this.tickWidth + AXIS_PADDING}px`; } else { const thisIsTheSecondLeftAxis = (this.id - 1) > 0; - if (this.multipleLeftAxes && thisIsTheSecondLeftAxis) { - style.left = 0; + if (this.hasMultipleLeftAxes && thisIsTheSecondLeftAxis) { + style.left = `${this.plotLeftTickWidth - this.usedTickWidth - this.tickWidth}px`; style['border-right'] = `1px solid`; } else { style.left = `${ this.plotLeftTickWidth - this.tickWidth + multipleAxesPadding}px`; @@ -256,7 +262,7 @@ export default { } }, onTickWidthChange(data) { - this.$emit('tickWidthChanged', { + this.$emit('plotYTickWidth', { width: data.width, yAxisId: this.id }); diff --git a/src/plugins/plot/configuration/PlotSeries.js b/src/plugins/plot/configuration/PlotSeries.js index a0f32be54f..ac9a2ee724 100644 --- a/src/plugins/plot/configuration/PlotSeries.js +++ b/src/plugins/plot/configuration/PlotSeries.js @@ -73,7 +73,7 @@ export default class PlotSeries extends Model { super(options); - this.logMode = options.collection.plot.model.yAxis.logMode; + this.logMode = this.getLogMode(options); this.listenTo(this, 'change:xKey', this.onXKeyChange, this); this.listenTo(this, 'change:yKey', this.onYKeyChange, this); @@ -87,6 +87,17 @@ export default class PlotSeries extends Model { 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 diff --git a/src/plugins/plot/configuration/YAxisModel.js b/src/plugins/plot/configuration/YAxisModel.js index 80c0bf3f44..1107b8e803 100644 --- a/src/plugins/plot/configuration/YAxisModel.js +++ b/src/plugins/plot/configuration/YAxisModel.js @@ -287,7 +287,8 @@ export default class YAxisModel extends Model { this.resetSeries(); } resetSeries() { - this.plot.series.forEach((plotSeries) => { + const series = this.getSeriesForYAxis(this.seriesCollection); + series.forEach((plotSeries) => { plotSeries.logMode = this.get('logMode'); plotSeries.reset(plotSeries.getSeriesData()); }); diff --git a/src/plugins/plot/legend/PlotLegend.vue b/src/plugins/plot/legend/PlotLegend.vue index 9b954e32bd..2f5b003ad0 100644 --- a/src/plugins/plot/legend/PlotLegend.vue +++ b/src/plugins/plot/legend/PlotLegend.vue @@ -207,6 +207,13 @@ export default { this.registerListeners(config); } }, + removeTelemetryObject(identifier) { + const configId = this.openmct.objects.makeKeyString(identifier); + const config = configStore.get(configId); + if (config) { + config.series.forEach(this.removeSeries, this); + } + }, registerListeners(config) { //listen to any changes to the telemetry endpoints that are associated with the child this.listenTo(config.series, 'add', this.addSeries, this); diff --git a/src/plugins/plot/stackedPlot/StackedPlot.vue b/src/plugins/plot/stackedPlot/StackedPlot.vue index 5636ccd691..961cb32245 100644 --- a/src/plugins/plot/stackedPlot/StackedPlot.vue +++ b/src/plugins/plot/stackedPlot/StackedPlot.vue @@ -47,8 +47,8 @@ :color-palette="colorPalette" :cursor-guide="cursorGuide" :show-limit-line-labels="showLimitLineLabels" - :plot-tick-width="maxTickWidth" - @plotTickWidth="onTickWidthChange" + :parent-y-tick-width="maxTickWidth" + @plotYTickWidth="onYTickWidthChange" @loadingUpdated="loadingUpdated" @cursorGuide="onCursorGuideChange" @gridLines="onGridLinesChange" @@ -113,8 +113,21 @@ export default { return 'plot-legend-collapsed'; } }, + /** + * Returns the maximum width of the left and right y axes ticks of this stacked plots children + * @returns {{rightTickWidth: number, leftTickWidth: number, hasMultipleLeftAxes: boolean}} + */ maxTickWidth() { - return Math.max(...Object.values(this.tickWidthMap)); + const tickWidthValues = Object.values(this.tickWidthMap); + const maxLeftTickWidth = Math.max(...tickWidthValues.map(tickWidthItem => tickWidthItem.leftTickWidth)); + const maxRightTickWidth = Math.max(...tickWidthValues.map(tickWidthItem => tickWidthItem.rightTickWidth)); + const hasMultipleLeftAxes = tickWidthValues.some(tickWidthItem => tickWidthItem.hasMultipleLeftAxes === true); + + return { + leftTickWidth: maxLeftTickWidth, + rightTickWidth: maxRightTickWidth, + hasMultipleLeftAxes + }; } }, beforeDestroy() { @@ -175,7 +188,10 @@ export default { addChild(child) { const id = this.openmct.objects.makeKeyString(child.identifier); - this.$set(this.tickWidthMap, id, 0); + this.$set(this.tickWidthMap, id, { + leftTickWidth: 0, + rightTickWidth: 0 + }); this.compositionObjects.push({ object: child, @@ -231,7 +247,10 @@ export default { resetTelemetryAndTicks(domainObject) { this.compositionObjects = []; - this.tickWidthMap = {}; + this.tickWidthMap = { + leftTickWidth: 0, + rightTickWidth: 0 + }; }, exportJPG() { @@ -254,12 +273,18 @@ export default { this.hideExportButtons = false; }.bind(this)); }, - onTickWidthChange(width, plotId) { + /** + * @typedef {Object} PlotYTickData + * @property {Number} leftTickWidth the width of the ticks for all the y axes on the left of the plot. + * @property {Number} rightTickWidth the width of the ticks for all the y axes on the right of the plot. + * @property {Boolean} hasMultipleLeftAxes whether or not there is more than one left y axis. + */ + onYTickWidthChange(data, plotId) { if (!Object.prototype.hasOwnProperty.call(this.tickWidthMap, plotId)) { return; } - this.$set(this.tickWidthMap, plotId, width); + this.$set(this.tickWidthMap, plotId, data); }, legendHoverChanged(data) { this.showLimitLineLabels = data; diff --git a/src/plugins/plot/stackedPlot/StackedPlotItem.vue b/src/plugins/plot/stackedPlot/StackedPlotItem.vue index 64409db55a..0213a465af 100644 --- a/src/plugins/plot/stackedPlot/StackedPlotItem.vue +++ b/src/plugins/plot/stackedPlot/StackedPlotItem.vue @@ -72,10 +72,14 @@ export default { return undefined; } }, - plotTickWidth: { - type: Number, + parentYTickWidth: { + type: Object, default() { - return 0; + return { + leftTickWidth: 0, + rightTickWidth: 0, + hasMultipleLeftAxes: false + }; } } }, @@ -86,8 +90,8 @@ export default { cursorGuide(newCursorGuide) { this.updateComponentProp('cursorGuide', newCursorGuide); }, - plotTickWidth(width) { - this.updateComponentProp('plotTickWidth', width); + parentYTickWidth(width) { + this.updateComponentProp('parentYTickWidth', width); }, showLimitLineLabels: { handler(data) { @@ -121,7 +125,7 @@ export default { this.$el.innerHTML = ''; } - const onTickWidthChange = this.onTickWidthChange; + const onYTickWidthChange = this.onYTickWidthChange; const onLockHighlightPointUpdated = this.onLockHighlightPointUpdated; const onHighlightsUpdated = this.onHighlightsUpdated; const onConfigLoaded = this.onConfigLoaded; @@ -158,7 +162,7 @@ export default { data() { return { ...getProps(), - onTickWidthChange, + onYTickWidthChange, onLockHighlightPointUpdated, onHighlightsUpdated, onConfigLoaded, @@ -174,7 +178,30 @@ export default { this.loading = loaded; } }, - template: '
' + template: ` +
+ + +
` }); }, onLockHighlightPointUpdated() { @@ -186,8 +213,8 @@ export default { onConfigLoaded() { this.$emit('configLoaded', ...arguments); }, - onTickWidthChange() { - this.$emit('plotTickWidth', ...arguments); + onYTickWidthChange() { + this.$emit('plotYTickWidth', ...arguments); }, onCursorGuideChange() { this.$emit('cursorGuide', ...arguments); @@ -204,7 +231,7 @@ export default { limitLineLabels: this.showLimitLineLabels, gridLines: this.gridLines, cursorGuide: this.cursorGuide, - plotTickWidth: this.plotTickWidth, + parentYTickWidth: this.parentYTickWidth, options: this.options, status: this.status, colorPalette: this.colorPalette, From c1e8c7915c4daaa6946bd5dbfe3cca49df9d2ce1 Mon Sep 17 00:00:00 2001 From: Jamie V Date: Wed, 1 Feb 2023 14:06:54 -0800 Subject: [PATCH 186/274] [Staleness] Fix removed object error and clean up (#6241) * fixing error from plots when removing swg and making methods and props private for swg staleness provider * removing unsubscribes from destroy hooks if the item has been removed already and reverting an unneccesary change * checking for undefined staleness response * removed un-neccesary code --- .../generator/SinewaveStalenessProvider.js | 114 +++++++++--------- .../LADTable/components/LadTableSet.vue | 1 + .../components/ConditionCollection.vue | 5 +- .../criterion/AllTelemetryCriterion.js | 5 + src/plugins/plot/Plot.vue | 1 + src/plugins/telemetryTable/TelemetryTable.js | 1 + 6 files changed, 72 insertions(+), 55 deletions(-) diff --git a/example/generator/SinewaveStalenessProvider.js b/example/generator/SinewaveStalenessProvider.js index 3ebf4570e1..0fafcc14c2 100644 --- a/example/generator/SinewaveStalenessProvider.js +++ b/example/generator/SinewaveStalenessProvider.js @@ -23,14 +23,18 @@ import EventEmitter from 'EventEmitter'; export default class SinewaveLimitProvider extends EventEmitter { + #openmct; + #observingStaleness; + #watchingTheClock; + #isRealTime; 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) { @@ -38,114 +42,116 @@ export default class SinewaveLimitProvider extends EventEmitter { } isStale(domainObject, options) { - if (!this.providingStaleness(domainObject)) { + if (!this.#providingStaleness(domainObject)) { return Promise.resolve({ isStale: false, utc: 0 }); } - const id = this.getObjectKeyString(domainObject); + const id = this.#getObjectKeyString(domainObject); - if (!this.observerExists(id)) { - this.createObserver(id); + if (!this.#observerExists(id)) { + this.#createObserver(id); } - return Promise.resolve(this.observingStaleness[id].isStale); + return Promise.resolve(this.#observingStaleness[id].isStale); } subscribeToStaleness(domainObject, callback) { - const id = this.getObjectKeyString(domainObject); + const id = this.#getObjectKeyString(domainObject); - if (this.isRealTime === undefined) { - this.updateRealTime(this.openmct.time.clock()); + if (this.#isRealTime === undefined) { + this.#updateRealTime(this.#openmct.time.clock()); } - this.handleClockUpdate(); + this.#handleClockUpdate(); - if (this.observerExists(id)) { - this.addCallbackToObserver(id, callback); + if (this.#observerExists(id)) { + this.#addCallbackToObserver(id, callback); } else { - this.createObserver(id, callback); + this.#createObserver(id, callback); } const intervalId = setInterval(() => { - if (this.providingStaleness(domainObject)) { - this.updateStaleness(id, !this.observingStaleness[id].isStale); + if (this.#providingStaleness(domainObject)) { + this.#updateStaleness(id, !this.#observingStaleness[id].isStale); } }, 10000); return () => { clearInterval(intervalId); - this.updateStaleness(id, false); - this.handleClockUpdate(); - this.destroyObserver(id); + this.#updateStaleness(id, false); + this.#handleClockUpdate(); + this.#destroyObserver(id); }; } - handleClockUpdate() { - let observers = Object.values(this.observingStaleness).length > 0; + #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); + 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 + #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 + isStale: this.#observingStaleness[id].isStale }); } - createObserver(id, callback) { - this.observingStaleness[id] = { + #createObserver(id, callback) { + this.#observingStaleness[id] = { isStale: false, utc: Date.now() }; if (typeof callback === 'function') { - this.addCallbackToObserver(id, callback); + this.#addCallbackToObserver(id, callback); } } - destroyObserver(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/src/plugins/LADTable/components/LadTableSet.vue b/src/plugins/LADTable/components/LadTableSet.vue index 9bba9e8caa..3a3b000553 100644 --- a/src/plugins/LADTable/components/LadTableSet.vue +++ b/src/plugins/LADTable/components/LadTableSet.vue @@ -218,6 +218,7 @@ export default { this.stalenessSubscription[keystring].unsubscribe(); this.stalenessSubscription[keystring].stalenessUtils.destroy(); this.handleStaleness(keystring, { isStale: false }, SKIP_CHECK); + delete this.stalenessSubscription[keystring]; }; }, handleStaleness(id, stalenessResponse, skipCheck = false) { diff --git a/src/plugins/condition/components/ConditionCollection.vue b/src/plugins/condition/components/ConditionCollection.vue index c05bfc0939..f5336952c7 100644 --- a/src/plugins/condition/components/ConditionCollection.vue +++ b/src/plugins/condition/components/ConditionCollection.vue @@ -232,7 +232,9 @@ export default { this.stalenessSubscription[keyString].stalenessUtils = new StalenessUtils(this.openmct, domainObject); this.openmct.telemetry.isStale(domainObject).then((stalenessResponse) => { - this.hanldeStaleness(keyString, stalenessResponse); + if (stalenessResponse !== undefined) { + this.hanldeStaleness(keyString, stalenessResponse); + } }); const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(domainObject, (stalenessResponse) => { this.hanldeStaleness(keyString, stalenessResponse); @@ -259,6 +261,7 @@ export default { keyString, isStale: false }); + delete this.stalenessSubscription[keyString]; } }, hanldeStaleness(keyString, stalenessResponse) { diff --git a/src/plugins/condition/criterion/AllTelemetryCriterion.js b/src/plugins/condition/criterion/AllTelemetryCriterion.js index 316451dae3..c9ee3db8b9 100644 --- a/src/plugins/condition/criterion/AllTelemetryCriterion.js +++ b/src/plugins/condition/criterion/AllTelemetryCriterion.js @@ -83,6 +83,11 @@ export default class AllTelemetryCriterion extends TelemetryCriterion { 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) => { diff --git a/src/plugins/plot/Plot.vue b/src/plugins/plot/Plot.vue index 5140e7fa4c..6bada906e1 100644 --- a/src/plugins/plot/Plot.vue +++ b/src/plugins/plot/Plot.vue @@ -166,6 +166,7 @@ export default { this.stalenessSubscription[keystring].unsubscribe(); this.stalenessSubscription[keystring].stalenessUtils.destroy(); this.handleStaleness(keystring, { isStale: false }, SKIP_CHECK); + delete this.stalenessSubscription[keystring]; }, handleStaleness(id, stalenessResponse, skipCheck = false) { if (skipCheck || this.stalenessSubscription[id].stalenessUtils.shouldUpdateStaleness(stalenessResponse, id)) { diff --git a/src/plugins/telemetryTable/TelemetryTable.js b/src/plugins/telemetryTable/TelemetryTable.js index 2db58858ec..67178a6d11 100644 --- a/src/plugins/telemetryTable/TelemetryTable.js +++ b/src/plugins/telemetryTable/TelemetryTable.js @@ -293,6 +293,7 @@ define([ this.stalenessSubscription[keyString].unsubscribe(); this.stalenessSubscription[keyString].stalenessUtils.destroy(); this.handleStaleness(keyString, { isStale: false }, SKIP_CHECK); + delete this.stalenessSubscription[keyString]; } clearData() { From 800062d37e937a3f1e840e95750042bd48ad5238 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Thu, 2 Feb 2023 15:50:37 -0800 Subject: [PATCH 187/274] fix: remove 1px padding and re-enable autoscale snapshot test (#6271) * style: remove 1px padding from plot legend item * test: re-enable autoscale snapshot test --- e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js | 2 +- src/styles/_legacy-plots.scss | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js index 3a1e4e20d6..fb9a2e2a73 100644 --- a/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/autoscale.e2e.spec.js @@ -32,7 +32,7 @@ test.use({ } }); -test.fixme('ExportAsJSON', () => { +test.describe('ExportAsJSON', () => { test('User can set autoscale with a valid range @snapshot', async ({ page, openmctConfig }) => { const { myItemsFolderName } = openmctConfig; diff --git a/src/styles/_legacy-plots.scss b/src/styles/_legacy-plots.scss index 3a67c6e253..a13339036f 100644 --- a/src/styles/_legacy-plots.scss +++ b/src/styles/_legacy-plots.scss @@ -664,7 +664,6 @@ mct-plot { border-radius: $smallCr; display: flex; justify-content: stretch; - padding: 1px; .plot-series-swatch-and-name, .plot-series-value { From 422b7f3e0993f4324f39b0ed6479e476f0768840 Mon Sep 17 00:00:00 2001 From: Charles Hacskaylo Date: Thu, 2 Feb 2023 17:18:41 -0800 Subject: [PATCH 188/274] Compass rose rotation fixes (#6260) --- .../components/Compass/CompassRose.vue | 85 +++++++++---------- 1 file changed, 41 insertions(+), 44 deletions(-) diff --git a/src/plugins/imagery/components/Compass/CompassRose.vue b/src/plugins/imagery/components/Compass/CompassRose.vue index 958bd3779b..2b4a17f480 100644 --- a/src/plugins/imagery/components/Compass/CompassRose.vue +++ b/src/plugins/imagery/components/Compass/CompassRose.vue @@ -107,48 +107,53 @@ height="100" /> - - - - - - - - - + + + - - + + + + + + + - + @@ -305,7 +310,7 @@ export default { return { transform: `translate(${translateX}%, ${translateY}%) rotate(${rotation}deg) scale(${scale})` }; }, camGimbalAngleStyle() { - const rotation = rotate(this.north, this.heading); + const rotation = rotate(this.heading); return { transform: `rotate(${ rotation }deg)` @@ -332,14 +337,6 @@ export default { hasHeading() { return this.heading !== undefined; }, - headingStyle() { - /* Replaced with computed camGimbalStyle, but left here just in case. */ - const rotation = rotate(this.north, this.heading); - - return { - transform: `rotate(${ rotation }deg)` - }; - }, hasSunHeading() { return this.sunHeading !== undefined; }, From 0f312a88bb8ddfd839eb6286a20ceefe493350d2 Mon Sep 17 00:00:00 2001 From: Jamie V Date: Thu, 2 Feb 2023 18:16:45 -0800 Subject: [PATCH 189/274] [Notebook] Sanitize entries before save for extra protection (#6255) * Sanitizing before save as well to be be doubly safe --------- Co-authored-by: Andrew Henry --- src/plugins/notebook/components/NotebookEntry.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plugins/notebook/components/NotebookEntry.vue b/src/plugins/notebook/components/NotebookEntry.vue index 11015c0054..20e4f8784e 100644 --- a/src/plugins/notebook/components/NotebookEntry.vue +++ b/src/plugins/notebook/components/NotebookEntry.vue @@ -77,13 +77,13 @@ aria-label="Notebook Entry Input" tabindex="0" :contenteditable="canEdit" + v-bind.prop="formattedText" @mouseover="checkEditability($event)" @mouseleave="canEdit = true" @focus="editingEntry()" @blur="updateEntryValue($event)" @keydown.enter.exact.prevent @keyup.enter.exact.prevent="forceBlur($event)" - v-html="formattedText" >
@@ -250,7 +250,7 @@ export default { let text = sanitizeHtml(this.entry.text, SANITIZATION_SCHEMA); if (this.editMode || !this.urlWhitelist) { - return text; + return { innerText: text }; } text = text.replace(URL_REGEX, (match) => { @@ -268,7 +268,7 @@ export default { return result; }); - return text; + return { innerHTML: text }; }, isSelectedEntry() { return this.selectedEntryId === this.entry.id; @@ -456,7 +456,7 @@ export default { this.editMode = false; const value = $event.target.innerText; if (value !== this.entry.text && value.match(/\S/)) { - this.entry.text = value; + this.entry.text = sanitizeHtml(value, SANITIZATION_SCHEMA); this.timestampAndUpdate(); } else { this.$emit('cancelEdit'); From be38c3e6546e9d2217b1062cc12a419780e28a3c Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Fri, 3 Feb 2023 15:56:50 -0800 Subject: [PATCH 190/274] Fix stacked plot child selection (#6275) * Fix selections for different scenarios * Ensure plot selection in stacked plots works when there are no selected or found annotations * Adds e2e test for stacked plot selection and fixes the old e2e test which was testing overlay plots instead. * Fix selection of plots while in Edit mode * Improve tests for stacked plots * refactor: remove unnecessary `await`s * a11y: move aria-label to StackedPlotItem * refactor(e2e): combine like tests, unique object names - Use unique object names in `text=` selectors - Combine like tests to reduce execution time - Use `getByRole` selectors where able * docs(e2e): add comments to test * fix: add class back for unit test selector --------- Co-authored-by: Scott Bell Co-authored-by: Jesse Mazzella --- .../plugins/plot/stackedPlot.e2e.spec.js | 124 +++++++++++++----- src/plugins/plot/MctPlot.vue | 40 ++++-- .../plot/inspector/PlotOptionsBrowse.vue | 2 + .../plot/inspector/PlotOptionsEdit.vue | 1 + .../plot/inspector/forms/YAxisForm.vue | 5 +- .../plot/stackedPlot/StackedPlotItem.vue | 39 +++++- src/ui/inspector/InspectorViews.vue | 2 +- 7 files changed, 165 insertions(+), 48 deletions(-) diff --git a/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js index 5d6c47f65c..509fe267eb 100644 --- a/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js @@ -29,29 +29,39 @@ const { test, expect } = require('../../../../pluginFixtures'); const { createDomainObjectWithDefaults } = require('../../../../appActions'); test.describe('Stacked Plot', () => { + 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: 'networkidle' }); + 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 + }); + }); test('Using the remove action removes the correct plot', async ({ page }) => { - await page.goto('/', { waitUntil: 'networkidle' }); - const overlayPlot = await createDomainObjectWithDefaults(page, { - type: "Overlay Plot" - }); + 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 createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator", - name: 'swg a', - parent: overlayPlot.uuid - }); - await createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator", - name: 'swg b', - parent: overlayPlot.uuid - }); - await createDomainObjectWithDefaults(page, { - type: "Sine Wave Generator", - name: 'swg c', - parent: overlayPlot.uuid - }); - await page.goto(overlayPlot.url); await page.click('button[title="Edit"]'); // Expand the elements pool vertically @@ -60,20 +70,70 @@ test.describe('Stacked Plot', () => { await page.mouse.move(0, 100); await page.mouse.up(); - await page.locator('.js-elements-pool__tree >> text=swg b').click({ button: 'right' }); - await page.locator('li[role="menuitem"]:has-text("Remove")').click(); - await page.locator('.js-overlay .js-overlay__button >> text=OK').click(); + await swgBElementsPoolItem.click({ button: 'right' }); + await page.getByRole('menuitem').filter({ hasText: /Remove/ }).click(); + await page.getByRole('button').filter({ hasText: "OK" }).click(); - // Wait until the number of elements in the elements pool has changed, and then confirm that the correct children were retained - // await page.waitForFunction(() => { - // return Array.from(document.querySelectorAll('.js-elements-pool__tree .js-elements-pool__item')).length === 2; - // }); - // Wait until there are only two items in the elements pool (ie the remove action has completed) - await expect(page.locator('.js-elements-pool__tree .js-elements-pool__item')).toHaveCount(2); + 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(page.locator('.js-elements-pool__tree >> text=swg a')).toHaveCount(1); - await expect(page.locator('.js-elements-pool__tree >> text=swg b')).toHaveCount(0); - await expect(page.locator('.js-elements-pool__tree >> text=swg c')).toHaveCount(1); + await expect(swgAElementsPoolItem).toHaveCount(1); + await expect(swgBElementsPoolItem).toHaveCount(0); + await expect(swgCElementsPoolItem).toHaveCount(1); + }); + + test('Selecting a child plot while in browse and edit modes shows its properties in the inspector', async ({ page }) => { + await page.goto(stackedPlot.url); + + // 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('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis"); + 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('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis"); + 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('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis"); + 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"]'); + + // 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('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis"); + 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('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis"); + 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('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis"); + await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgC.name); }); }); diff --git a/src/plugins/plot/MctPlot.vue b/src/plugins/plot/MctPlot.vue index 49fc537c88..ecdee217ff 100644 --- a/src/plugins/plot/MctPlot.vue +++ b/src/plugins/plot/MctPlot.vue @@ -1190,28 +1190,42 @@ export default { selectNearbyAnnotations(event) { // need to stop propagation right away to prevent selecting the plot itself event.stopPropagation(); - if (!this.annotationViewingAndEditingAllowed || this.annotationSelections.length) { - return; - } const nearbyAnnotations = this.gatherNearbyAnnotations(); - if (!nearbyAnnotations.length) { - const emptySelection = this.createPathSelection(); - this.openmct.selection.select(emptySelection, true); - // should show plot itself if we didn't find any annotations + + if (this.annotationViewingAndEditingAllowed && this.annotationSelections.length) { + //no annotations were found, but we are adding some now + return; + } + + if (this.annotationViewingAndEditingAllowed && nearbyAnnotations.length) { + //show annotations if some were found + const { targetDomainObjects, targetDetails } = this.prepareExistingAnnotationSelection(nearbyAnnotations); + this.selectPlotAnnotations({ + targetDetails, + targetDomainObjects, + annotations: nearbyAnnotations + }); return; } - const { targetDomainObjects, targetDetails } = this.prepareExistingAnnotationSelection(nearbyAnnotations); - this.selectPlotAnnotations({ - targetDetails, - targetDomainObjects, - annotations: nearbyAnnotations - }); + //Fall through to here if either there is no new selection add tags or no existing annotations were retrieved + this.selectPlot(); + }, + selectPlot() { + // should show plot itself if we didn't find any annotations + const selection = this.createPathSelection(); + this.openmct.selection.select(selection, true); }, createPathSelection() { let selection = []; + selection.unshift({ + element: this.$el, + context: { + item: this.domainObject + } + }); this.path.forEach((pathObject, index) => { selection.push({ element: this.openmct.layout.$refs.browseObject.$el, diff --git a/src/plugins/plot/inspector/PlotOptionsBrowse.vue b/src/plugins/plot/inspector/PlotOptionsBrowse.vue index 9707f8a001..c99004d74a 100644 --- a/src/plugins/plot/inspector/PlotOptionsBrowse.vue +++ b/src/plugins/plot/inspector/PlotOptionsBrowse.vue @@ -27,6 +27,7 @@

    Plot Series

    Y Axis {{ yAxesWithSeries.length > 1 ? yAxis.id : '' }}

  • diff --git a/src/plugins/plot/inspector/PlotOptionsEdit.vue b/src/plugins/plot/inspector/PlotOptionsEdit.vue index ee83dd3081..a67baeec22 100644 --- a/src/plugins/plot/inspector/PlotOptionsEdit.vue +++ b/src/plugins/plot/inspector/PlotOptionsEdit.vue @@ -27,6 +27,7 @@

      Plot Series

    • -
        +

          Y Axis {{ id > 1 ? id : '' }}

        • diff --git a/src/plugins/plan/PlanViewConfiguration.js b/src/plugins/plan/PlanViewConfiguration.js new file mode 100644 index 0000000000..249d97be27 --- /dev/null +++ b/src/plugins/plan/PlanViewConfiguration.js @@ -0,0 +1,110 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2023, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +import EventEmitter from 'EventEmitter'; + +export const DEFAULT_CONFIGURATION = { + clipActivityNames: false, + swimlaneVisibility: {} +}; + +export default class PlanViewConfiguration extends EventEmitter { + constructor(domainObject, openmct) { + super(); + + this.domainObject = domainObject; + this.openmct = openmct; + + 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]; + } + + 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; + } + } + + 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(); + } +} diff --git a/src/plugins/plan/PlanViewProvider.js b/src/plugins/plan/PlanViewProvider.js index 7d8b237e65..dcf3ac1056 100644 --- a/src/plugins/plan/PlanViewProvider.js +++ b/src/plugins/plan/PlanViewProvider.js @@ -20,7 +20,7 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import Plan from './Plan.vue'; +import Plan from './components/Plan.vue'; import Vue from 'vue'; export default function PlanViewProvider(openmct) { @@ -35,11 +35,11 @@ export default function PlanViewProvider(openmct) { name: 'Plan', cssClass: 'icon-plan', canView(domainObject) { - return domainObject.type === 'plan'; + return domainObject.type === 'plan' || domainObject.type === 'gantt-chart'; }, canEdit(domainObject) { - return false; + return domainObject.type === 'gantt-chart'; }, view: function (domainObject, objectPath) { diff --git a/src/plugins/plan/components/ActivityTimeline.vue b/src/plugins/plan/components/ActivityTimeline.vue new file mode 100644 index 0000000000..ec0120d392 --- /dev/null +++ b/src/plugins/plan/components/ActivityTimeline.vue @@ -0,0 +1,187 @@ + + + + diff --git a/src/plugins/plan/components/Plan.vue b/src/plugins/plan/components/Plan.vue new file mode 100644 index 0000000000..fc7005b2e6 --- /dev/null +++ b/src/plugins/plan/components/Plan.vue @@ -0,0 +1,558 @@ +* 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. +*****************************************************************************/ + + + + diff --git a/src/plugins/plan/inspector/PlanInspectorViewProvider.js b/src/plugins/plan/inspector/ActivityInspectorViewProvider.js similarity index 90% rename from src/plugins/plan/inspector/PlanInspectorViewProvider.js rename to src/plugins/plan/inspector/ActivityInspectorViewProvider.js index 019125fb69..2dfb756911 100644 --- a/src/plugins/plan/inspector/PlanInspectorViewProvider.js +++ b/src/plugins/plan/inspector/ActivityInspectorViewProvider.js @@ -20,13 +20,13 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -import PlanActivitiesView from "./PlanActivitiesView.vue"; +import PlanActivitiesView from "./components/PlanActivitiesView.vue"; import Vue from 'vue'; -export default function PlanInspectorViewProvider(openmct) { +export default function ActivityInspectorViewProvider(openmct) { return { - key: 'plan-inspector', - name: 'Plan Inspector View', + key: 'activity-inspector', + name: 'Activity', canView: function (selection) { if (selection.length === 0 || selection[0].length === 0) { return false; @@ -44,6 +44,7 @@ export default function PlanInspectorViewProvider(openmct) { show: function (element) { component = new Vue({ el: element, + name: "PlanActivitiesView", components: { PlanActivitiesView: PlanActivitiesView }, diff --git a/src/plugins/plan/inspector/GanttChartInspectorViewProvider.js b/src/plugins/plan/inspector/GanttChartInspectorViewProvider.js new file mode 100644 index 0000000000..56f66b4a5b --- /dev/null +++ b/src/plugins/plan/inspector/GanttChartInspectorViewProvider.js @@ -0,0 +1,68 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2023, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +import 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; + } + + const domainObject = selection[0][0].context.item; + + 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: '' + }); + }, + priority: function () { + return openmct.priority.HIGH + 1; + }, + destroy: function () { + if (component) { + component.$destroy(); + component = undefined; + } + } + }; + } + }; +} diff --git a/src/plugins/plan/inspector/ActivityProperty.vue b/src/plugins/plan/inspector/components/ActivityProperty.vue similarity index 100% rename from src/plugins/plan/inspector/ActivityProperty.vue rename to src/plugins/plan/inspector/components/ActivityProperty.vue diff --git a/src/plugins/plan/inspector/PlanActivitiesView.vue b/src/plugins/plan/inspector/components/PlanActivitiesView.vue similarity index 96% rename from src/plugins/plan/inspector/PlanActivitiesView.vue rename to src/plugins/plan/inspector/components/PlanActivitiesView.vue index 29ae522c35..8e7b5c1641 100644 --- a/src/plugins/plan/inspector/PlanActivitiesView.vue +++ b/src/plugins/plan/inspector/components/PlanActivitiesView.vue @@ -36,16 +36,15 @@ import { getPreciseDuration } from "utils/duration"; import { v4 as uuid } from 'uuid'; const propertyLabels = { - 'start': 'Start DateTime', - 'end': 'End DateTime', - 'duration': 'Duration', - 'earliestStart': 'Earliest Start', - 'latestEnd': 'Latest End', - 'gap': 'Gap', - 'overlap': 'Overlap', - 'totalTime': 'Total Time' + start: 'Start DateTime', + end: 'End DateTime', + duration: 'Duration', + earliestStart: 'Earliest Start', + latestEnd: 'Latest End', + gap: 'Gap', + overlap: 'Overlap', + totalTime: 'Total Time' }; - export default { components: { PlanActivityView diff --git a/src/plugins/plan/inspector/PlanActivityView.vue b/src/plugins/plan/inspector/components/PlanActivityView.vue similarity index 100% rename from src/plugins/plan/inspector/PlanActivityView.vue rename to src/plugins/plan/inspector/components/PlanActivityView.vue diff --git a/src/plugins/plan/inspector/components/PlanViewConfiguration.vue b/src/plugins/plan/inspector/components/PlanViewConfiguration.vue new file mode 100644 index 0000000000..786e97ed4b --- /dev/null +++ b/src/plugins/plan/inspector/components/PlanViewConfiguration.vue @@ -0,0 +1,144 @@ + + + diff --git a/src/plugins/plan/plan.scss b/src/plugins/plan/plan.scss index 45bd9b2938..a582260fa9 100644 --- a/src/plugins/plan/plan.scss +++ b/src/plugins/plan/plan.scss @@ -21,21 +21,34 @@ *****************************************************************************/ .c-plan { - svg { - text-rendering: geometricPrecision; + svg { + text-rendering: geometricPrecision; - text { - stroke: none; + text { + stroke: none; + } } - .activity-label { - &--outside-rect { - fill: $colorBodyFg !important; - } - } - } + &__activity { + cursor: pointer; - canvas { - display: none; - } + &[s-selected] { + rect, use { + outline-style: dotted; + outline-width: 2px; + stroke: $colorGanttSelectedBorder; + stroke-width: 2px; + } + } + } + + &__activity-label { + &--outside-rect { + fill: $colorBodyFg !important; + } + } + + canvas { + display: none; + } } diff --git a/src/plugins/plan/plugin.js b/src/plugins/plan/plugin.js index d44a267da8..85022fd61b 100644 --- a/src/plugins/plan/plugin.js +++ b/src/plugins/plan/plugin.js @@ -21,15 +21,18 @@ *****************************************************************************/ import PlanViewProvider from './PlanViewProvider'; -import PlanInspectorViewProvider from "./inspector/PlanInspectorViewProvider"; +import ActivityInspectorViewProvider from "./inspector/ActivityInspectorViewProvider"; +import GanttChartInspectorViewProvider from "./inspector/GanttChartInspectorViewProvider"; +import ganttChartCompositionPolicy from './GanttChartCompositionPolicy'; +import { DEFAULT_CONFIGURATION } from './PlanViewConfiguration'; -export default function (configuration) { +export default function (options = {}) { return function install(openmct) { openmct.types.addType('plan', { name: 'Plan', key: 'plan', - description: 'A configurable timeline-like view for a compatible mission plan file.', - creatable: true, + description: 'A non-configurable timeline-like view for a compatible plan file.', + creatable: options.creatable ?? false, cssClass: 'icon-plan', form: [ { @@ -45,10 +48,30 @@ export default function (configuration) { } ], 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 PlanInspectorViewProvider(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 e7603ee24a..25db86289f 100644 --- a/src/plugins/plan/pluginSpec.js +++ b/src/plugins/plan/pluginSpec.js @@ -27,6 +27,7 @@ import Properties from "../inspectorViews/properties/Properties.vue"; describe('the plugin', function () { let planDefinition; + let ganttDefinition; let element; let child; let openmct; @@ -50,6 +51,7 @@ describe('the plugin', function () { 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'; @@ -74,15 +76,30 @@ describe('the plugin', function () { let mockPlanObject = { name: 'Plan', key: 'plan', + creatable: false + }; + + let mockGanttObject = { + name: 'Gantt', + key: 'gantt-chart', creatable: true }; - it('defines a plan object type with the correct key', () => { - expect(planDefinition.key).toEqual(mockPlanObject.key); + 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); + }); }); - - it('is 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', () => { @@ -107,7 +124,7 @@ describe('the plugin', function () { const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]); let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view'); - expect(planView.canEdit()).toBeFalse(); + expect(planView.canEdit(testViewObject)).toBeFalse(); }); }); @@ -179,10 +196,10 @@ describe('the plugin', function () { it('displays the group label', () => { const labelEl = element.querySelector('.c-plan__contents .c-object-label .c-object-label__name'); - expect(labelEl.innerHTML).toEqual('TEST-GROUP'); + expect(labelEl.innerHTML).toMatch(/TEST-GROUP/); }); - it('displays the activities and their labels', (done) => { + it('displays the activities and their labels', async () => { const bounds = { start: 1597160002854, end: 1597181232854 @@ -190,27 +207,22 @@ describe('the plugin', function () { openmct.time.bounds(bounds); - Vue.nextTick(() => { - const rectEls = element.querySelectorAll('.c-plan__contents rect'); - expect(rectEls.length).toEqual(2); - const textEls = element.querySelectorAll('.c-plan__contents text'); - expect(textEls.length).toEqual(3); - - done(); - }); + 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', (done) => { + it ('shows the status indicator when available', async () => { openmct.status.set({ key: "test-object", namespace: '' }, 'draft'); - Vue.nextTick(() => { - const statusEl = element.querySelector('.c-plan__contents .is-status--draft'); - expect(statusEl).toBeDefined(); - done(); - }); + await Vue.nextTick(); + const statusEl = element.querySelector('.c-plan__contents .is-status--draft'); + expect(statusEl).toBeDefined(); }); }); @@ -224,10 +236,12 @@ describe('the plugin', function () { key: 'test-plan', namespace: '' }, + created: 123456789, + modified: 123456790, version: 'v1' }; - beforeEach(() => { + beforeEach(async () => { openmct.selection.select([{ element: element, context: { @@ -241,19 +255,18 @@ describe('the plugin', function () { } }], false); - return Vue.nextTick().then(() => { - let viewContainer = document.createElement('div'); - child.append(viewContainer); - component = new Vue({ - el: viewContainer, - components: { - Properties - }, - provide: { - openmct: openmct - }, - template: '' - }); + await Vue.nextTick(); + let viewContainer = document.createElement('div'); + child.append(viewContainer); + component = new Vue({ + el: viewContainer, + components: { + Properties + }, + provide: { + openmct: openmct + }, + template: '' }); }); @@ -264,7 +277,6 @@ describe('the plugin', function () { it('provides an inspector view with the version information if available', () => { componentObject = component.$root.$children[0]; const propertiesEls = componentObject.$el.querySelectorAll('.c-inspect-properties__row'); - expect(propertiesEls.length).toEqual(7); const found = Array.from(propertiesEls).some((propertyEl) => { return (propertyEl.children[0].innerHTML.trim() === 'Version' && propertyEl.children[1].innerHTML.trim() === 'v1'); diff --git a/src/plugins/plan/util.js b/src/plugins/plan/util.js index 5b3934bd79..27cbcb5491 100644 --- a/src/plugins/plan/util.js +++ b/src/plugins/plan/util.js @@ -21,8 +21,8 @@ *****************************************************************************/ export function getValidatedData(domainObject) { - let sourceMap = domainObject.sourceMap; - let body = domainObject.selectFile?.body; + const sourceMap = domainObject.sourceMap; + const body = domainObject.selectFile?.body; let json = {}; if (typeof body === 'string') { try { @@ -64,3 +64,27 @@ export function getValidatedData(domainObject) { return json; } } + +export function getContrastingColor(hexColor) { + function cutHex(h, start, end) { + const hStr = (h.charAt(0) === '#') ? h.substring(1, 7) : h; + + return parseInt(hStr.substring(start, end), 16); + } + + // https://codepen.io/davidhalford/pen/ywEva/ + const cThreshold = 130; + + 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 cBrightness = ((hR * 299) + (hG * 587) + (hB * 114)) / 1000; + + return cBrightness > cThreshold ? "#000000" : "#ffffff"; +} diff --git a/src/plugins/timeline/TimelineCompositionPolicy.js b/src/plugins/timeline/TimelineCompositionPolicy.js index 49f7d5ed8a..4d2dc675e9 100644 --- a/src/plugins/timeline/TimelineCompositionPolicy.js +++ b/src/plugins/timeline/TimelineCompositionPolicy.js @@ -19,10 +19,12 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ + const ALLOWED_TYPES = [ 'telemetry.plot.overlay', 'telemetry.plot.stacked', - 'plan' + 'plan', + 'gantt-chart' ]; const DISALLOWED_TYPES = [ 'telemetry.plot.bar-graph', diff --git a/src/plugins/timeline/TimelineObjectView.vue b/src/plugins/timeline/TimelineObjectView.vue index 039ac3e4eb..6855ac22c1 100644 --- a/src/plugins/timeline/TimelineObjectView.vue +++ b/src/plugins/timeline/TimelineObjectView.vue @@ -19,12 +19,13 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +